The frame-by-frame stitcher accumulated drift, so later passes landed rotated and offset, doubling text. myscan/pagegraph.py instead solves all frame poses together: ECC odometry between consecutive frames (bridging single corrupt frames), SIFT+RANSAC loop closures between overlapping passes, and a robust Gauss-Newton pose graph. Loop matches must agree over the whole overlap, which rejects matches on repeated text. Off-page frames and frames that disagree with the mosaic are left out of the render. calib.Undistort applies the EEPROM lens model (OpenCV k1..k3, p1, p2 plus the tilt homography, as decoded in EEPROM.md); up to 8.8 px at the corners. scan.py registers frames on a worker thread so capture no longer drops frames, then assembles the page after the stop press (~11 s for a full page). stitch.py uses the new assembly by default; --live keeps the old one. EEPROM.md documents the full calibration blob layout, checksum and signature. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
299 lines
21 KiB
Markdown
299 lines
21 KiB
Markdown
# EEPROM contents (Hama myScan / Dacuda LuckyCat)
|
|
|
|
This is a field-by-field map of the per-device calibration blob, from one unit's dump (`calib/eeprom_3000.bin`).
|
|
For the read protocol, see `FINDINGS.md` ("Per-device calibration in EEPROM"). For the app side, see
|
|
`RE_colour_stitch.md` section 1.4.
|
|
|
|
Status tags:
|
|
- **[P]**: parsed by `myscan/calib.py`.
|
|
- **[D]**: decoded by hand from the bytes; the value is clear.
|
|
- **[G]**: guessed field name or meaning.
|
|
- **[?]**: unknown.
|
|
- **[V]**: verified in disassembly or decompilation; the function address is given as evidence.
|
|
|
|
## EEPROM map
|
|
|
|
| Address | Size | Content |
|
|
|---|---|---|
|
|
| 0x3000 | 2 | u16 LE blob length N (19417 on this unit). Valid if 0 < N <= 20000 (0x4E20) [V 0x138c4e0, read of 0x3000 at 0x138c743, bound check 0x138c7a0]. The write path rejects N >= 0x4FB7 [V 0x138cc90] |
|
|
| 0x3002 | 4 | u32 LE standard CRC-32 (zlib polynomial) over the N blob bytes, signature included. Verified in Python against the dump: 0x3cd95f42 [D]. The app **does not recompute or compare it on read**: it is only a cache key (see below) [V 0x1335430] |
|
|
| 0x3006 | N | `boost::archive::binary_iarchive` blob (i386 sizes), then a 256-byte RSA signature at its tail |
|
|
| 0x7EB5 | 64 | mouse ID: raw 64-byte buffer, handled as a string. Read by `OVMouseManager::readMouseId` (0x138e1e0 [V], read at 0x138e4fa), written by `writeMouseId(const char*)` (0x138db20 [V], write at 0x138dd80). **Not yet read from the device.** The `HalLuckyCat` wrapper is 0x1334a60 |
|
|
| < 0x3000 | ? | **The app never touches anything below 0x3000.** A scan of every call to `OVIF_DaEEPROMRead/Write` (the stubs 0x17ff6fa / 0x17ff700) in the main binary gives only the callers 0x138c4e0 (0x3000, 0x3006), 0x138d410 (0x3002), 0x138e1e0 (0x7EB5), 0x138cc90 (`writeBinaryData`: a single write starting at 0x3000 for header + checksum + blob, call at 0x138cf3f; the caller builds the whole buffer, the CRC is not computed there) and 0x138db20 (0x7EB5). UVCControl.framework just wraps the raw read/write (`EEPROMRead:withStartAddr:withLength:`). So whatever is below 0x3000 is firmware-side only [V by absence] |
|
|
|
|
Everything the app reads from the EEPROM is therefore: length, checksum, blob and the 64-byte ID.
|
|
|
|
## Blob layout
|
|
|
|
All offsets below are relative to the blob start (EEPROM 0x3006). The whole blob parses with **no gaps**
|
|
(a stand-alone parser consumes exactly bytes 0 .. N-256, then the signature follows).
|
|
|
|
Stream = `boost::archive::binary_iarchive`, library version 7 (boost ~1.42), i386 sizes. Each class is written
|
|
as: `tracking u8`, `class version u8`, then its members, in the order of `serialize<binary_iarchive>`.
|
|
All five classes here have `00 0e` = tracking 0, **class version 14**. Fixed C arrays (`T[n]`) are written as
|
|
`u32 count` + elements. `std::vector<T>` is `u32 count`, `u32 item_version`, elements. A class that is
|
|
first met inside a container (`std::vector`, `boost::array`) writes its own `00 00` (tracking 0, version 0) once.
|
|
The app accepts class versions 7..15 only (else `WrongArchiveVersionException`, "Incompatible archive version!",
|
|
[V] 0x14cc1a0 and the other four `serialize` functions).
|
|
|
|
Serialize functions ([V], `serialize<binary_iarchive>`): Info 0x14cc1a0, Color 0x14cde20, Undistortion 0x14cefe0,
|
|
Sensor 0x14cfcd0, Roi 0x14d0990. Names come from the property-tree writers: Undistortion 0x14c0510, Sensor 0x14c1300,
|
|
Roi 0x14c23a0, Info 0x14c2bc0, Color 0x14bedb0 (all [V]); the umbrella `HardwareData` has members
|
|
color @0x00, undistortion @0x68, sensor @0xF4, roi @0x144, info @0x158 (0x14be690 [V]).
|
|
|
|
### Header
|
|
|
|
| Offset | Value | Meaning |
|
|
|---|---|---|
|
|
| 0x00 | u32 22, `serialization::archive` | boost signature [D] |
|
|
| 0x1A | `07 04 04 04 08` | library version 7; sizeof int/long/float/double = 4/4/4/8 [D] |
|
|
| 0x1F | u32 1 | endianness check [D] |
|
|
|
|
### RoiParams (0x23: `00 0e`) [V 0x14d0990, names 0x14c23a0]
|
|
|
|
Five plain i32 in this order, no version conditions.
|
|
|
|
| Offset | Member offset | Type | Name | Value |
|
|
|---|---|---|---|---|
|
|
| 0x25 | +0x00 | i32 | `yBaseOffset` | 105 |
|
|
| 0x29 | +0x04 | i32 | `roiX0` | 0 |
|
|
| 0x2D | +0x08 | i32 | `roiY0` | 0 |
|
|
| 0x31 | +0x0C | i32 | `roiX1` | 640 |
|
|
| 0x35 | +0x10 | i32 | `roiY1` | 295 |
|
|
|
|
`yBaseOffset` was previously unknown. It is the first Roi integer (105).
|
|
|
|
### SensorParams (0x39: `00 0e`) [V 0x14cfcd0, names 0x14c1300]
|
|
|
|
Read order below. "Version" is the stream class version. This unit is 14.
|
|
|
|
| Offset | Member offset | Type | Name | Needs ver | Value |
|
|
|---|---|---|---|---|---|
|
|
| 0x3B | +0x08 | f64 | `metersPerLeftMouseUnit` | any | 1.922e-5 |
|
|
| 0x43 | +0x10 | f64 | `metersPerRightMouseUnit` | any | 1.878e-5 |
|
|
| 0x4B | +0x00 | f64 | `laserReadoutDelay` | >= 9 | 0.0 |
|
|
| 0x53 | +0x18 | f32 | `camToLeftMouseAngle` | >= 14 | 0.0 |
|
|
| 0x57 | +0x1C | f64[2] (count 2) | `camToLeftMouseVec` (m) | any | (0.01125, 0.0538) |
|
|
| 0x6B | +0x2C | f32 | `camToRightMouseAngle` | >= 14 | 0.0 |
|
|
| 0x6F | +0x30 | f64[2] (count 2) | `camToRightMouseVec` (m) | any | (0.03355, -0.00067) |
|
|
| (absent) | +0x40 | f64 | `gyroDegPerStep` | >= 15 | **not in this blob** |
|
|
| (absent) | +0x48 | f64 | `gyroBias` | >= 15 | **not in this blob** |
|
|
|
|
So the gyro fields do not exist for this unit: it is a version-14 blob, and the gyro pair needs 15.
|
|
The two angles are `float` (setters `movss` 0x14c73e0 / 0x14c7450, getters `fld dword` 0x14c7400 / 0x14c7470) [V].
|
|
`imagerGain` and `exposureTimeUs` are ColorParams members, not Sensor members. `yBaseOffset` is in Roi.
|
|
|
|
### UndistortionParams (0x83: `00 0e`) [V 0x14cefe0, names 0x14c0510]
|
|
|
|
| Offset | Member offset | Type | Name | Needs ver | Value |
|
|
|---|---|---|---|---|---|
|
|
| 0x85 | +0x00 | f64 | `focalLength` (px) | any | 737.328 |
|
|
| 0x8D | +0x08 | f64 | `principalPointX` (px) | any | 317.546 |
|
|
| 0x95 | +0x10 | f64 | `principalPointY` (px) | any | 132.733 |
|
|
| 0x9D | +0x18 | i32 | `distModel` (`Utilities::Enums::EDistortionModel`) | >= 8 | 4 |
|
|
| 0xA1 | +0x1C | f64[5] (count 5) | `distCoefficients` | any | 0, -0.403169, 1.3805e-4, 2.6490e-3, 0 |
|
|
| 0xCD | +0x44 | f64[8] (count 8) | `tiltH` | any | see below |
|
|
| 0x111 | +0x84 | f64 | `pixelSize` (m) | any | 6.1305e-5 |
|
|
|
|
`tiltH` = 1.002744, 0.007833, -0.985882, -0.000164, 1.006565, -0.435809, -1.17e-6, 2.47e-5.
|
|
Eight numbers for a 3x3 homography whose ninth entry is implicit (=1). Usage: see "How it is used" below.
|
|
|
|
### ColorParams (0x119: `00 0e`) [V 0x14cde20, names 0x14bedb0]
|
|
|
|
The sample sets are `std::vector<std::vector<SamplingPointAbstract<short,unsigned char,3>>>` (member +0x00,
|
|
two inner vectors: black, white). Framing, in stream order:
|
|
|
|
| Offset | Content |
|
|
|---|---|
|
|
| 0x11B | `00 00` class info of outer vector |
|
|
| 0x11D | u32 2 (outer count), u32 0 (item version) |
|
|
| 0x125 | `00 00` class info of inner vector |
|
|
| 0x127 | u32 827 (count), u32 0 (item version), `00 00` class info of the sample type |
|
|
| 0x131 | 827 samples: i16 x, i16 y, `u8[3]` colour (u32 count 3 + b, g, r). The first sample also carries a `00 00` class info for `boost::array` between y and the count (13 bytes); the rest are 11 bytes |
|
|
| 0x24BD | u32 873 (count), u32 0 (item version), then 873 samples of 11 bytes (x, y, 3, b, g, r) |
|
|
|
|
Sample arrays are `SamplingPointAbstract<short, uchar, 3>`: the two coordinates are signed shorts. Scalar members follow:
|
|
|
|
| Offset | Member offset | Type | Name | Needs ver | Value |
|
|
|---|---|---|---|---|---|
|
|
| 0x4A47 | +0x0C | u32 | `exposureTimeUs` | >= 8 | 440 |
|
|
| 0x4A4B | +0x18 | i32 | `gainR` | >= 8 | 115 |
|
|
| 0x4A4F | +0x1C | i32 | `gainG` | >= 8 | 89 |
|
|
| 0x4A53 | +0x20 | i32 | `gainB` | >= 8 | 64 |
|
|
| 0x4A57 | +0x10 | f64 | `imagerGain` | >= 9 | 1.5 |
|
|
| 0x4A5F | +0x24 | f64[3] (count 3) | `lambda` (one per channel) | >= 10 | 1.921e12, 5.193e7, 5.098e7 |
|
|
| 0x4A7B | +0x3C | f64 | `lambdaNormFactor` | >= 11 | 6025357260.0 |
|
|
| 0x4A83 | +0x44 | i32 | `nurbsModelSize` | >= 12 | 5 |
|
|
| 0x4A87 | +0x48 | f32[3] (count 3) | `nurbsSmoothingFactors` | >= 12 | 50, 50, 50 |
|
|
| 0x4A97 | +0x54 | i32 | `nurbsNSmoothingIterations` | >= 12 | 100 |
|
|
| 0x4A9B | +0x58 | i32[4] (count 4) | `clipOffset` | >= 14 | 8, 12, 8, 12 |
|
|
|
|
The names are from the property-tree writer 0x14bedb0 (offsets confirm the mapping). Corrections to older notes:
|
|
`imagerGain` is the 1.5 (not "lambda"), `lambda` is the 3-vector (not "lambdaNormFactor"), the 6.0e9 double is
|
|
`lambdaNormFactor`, the "u32 5" is `nurbsModelSize` (a scalar, not a list), and `4, 8, 12, 8, 12` is the
|
|
`clipOffset` int[4] with its count (so clipOffset = 8, 12, 8, 12).
|
|
`clipOffset` is read as a `QList<int>` by 0x14c5600 and used in `QualityTrackManager` setup (0x115bc70 [V]): it is subtracted
|
|
from the ROI to size the overlap estimator, `height_px = (roiY1 - roiY0) - clip[1] - clip[3]`, `width_px = (roiX1 - roiX0) - clip[0] - clip[2]`
|
|
(then times `pixelSize`), so the order is `{left, top, right, bottom}` margins in pixels [G for the edge names, V for the pairing].
|
|
The app's built-in default (HWSetup ini, string `8 28 8 12`) has the same shape.
|
|
|
|
### InfoParams (0x4AAF: `00 0e`) [V 0x14cc1a0, names 0x14c2bc0]
|
|
|
|
The first 16 bytes of the Info member are a **GUID** (`Data1 u32, Data2 u16, Data3 u16, Data4 u8[8]`; the
|
|
array is written with `u32 count 8`). The previous "8 bytes before fingerprint" are Data1..Data3, and the old
|
|
"fingerprint" is Data4.
|
|
|
|
| Offset | Member offset | Type | Name | Needs ver | Value |
|
|
|---|---|---|---|---|---|
|
|
| 0x4AB1 | +0x00 | u32 | `fingerprint` Data1 | any | 0x84F3094A |
|
|
| 0x4AB5 | +0x04 | u16 | Data2 | any | 0x7B88 |
|
|
| 0x4AB7 | +0x06 | u16 | Data3 | any | 0x49C6 (UUID version 4) |
|
|
| 0x4AB9 | +0x08 | u8[8] (count 8) | Data4 | any | 86 57 3a 6a 38 5e 6f 68 |
|
|
| 0x4AC5 | +0x10 | u32 | `calibrationTime` (time_t) | any | 0x5269BEBF = 2013-10-24 23:35:27 UTC |
|
|
| 0x4AC9 | +0x14 | f32 | `avgReprojErrorIntrinsic` (px) | any | 0.158 |
|
|
| 0x4ACD | +0x18 | f32 | `avgReprojErrorHomography` (px) | any | 0.133 |
|
|
| 0x4AD1 | +0x1C | u32 | `product` (`Utilities::Enums::EProduct`; not in the ptree writer) | >= 13 | 4 |
|
|
| 0x4AD5 | +0x20 | u32 | `archiveVersion` | >= 11 (else it takes the class version) | 14 |
|
|
|
|
The old "enum 14" is `archiveVersion`, which equals the class version here. The old "archiveVersion 4" at +0x1C is the
|
|
`product` enum: its accessors are 0x14c7880 (set) and 0x14c7890 (get) [V]; `HalLuckyCat::open` (0x1331ae0) copies it into the
|
|
driver config (`config+8`) and validates it via 0x1300ff0, which maps it to a profile resource `:/mouse_driver_profiles/<EProduct_*>.xml`.
|
|
For archives older than 13 the product reads as 0, and `loadHardwareData` (0x1335430, "CONVERT_LSM200_HACK") rewrites it to 1 for old
|
|
units whose `calibrationTime` falls in a window around Jan-May 2012 (compared with `QDate(2012,1,1)` and `QDate(2012,5,1)`) and whose `pixelSize` is below a threshold,
|
|
and it also replaces camToLeft/Right vectors with fixed values (0.0141, 0.0525) and (0.036, 0) there [V 0x1335430, exact date logic not worked out]. This unit (2013) is unaffected. `open` picks `DriverFactoryLG` for product in {2, 13, 14}, otherwise
|
|
`DriverFactoryLL`; product 4 (this unit) therefore uses `DriverFactoryLL`. The profile resources are named
|
|
`EProduct_ADI_WIRED/WIRELESS, ASD_SLAMBASIC/SLAMBASIC2/SLAMGYRO/SLAMLITE/WIRELESS, FOX_WIRED/WIRED_GYRO/WIRELESS, LG_LSM100/150/200/300, Passport, WING_WIRED`; the
|
|
numeric order of the enum (moc table) is not readable in the binary, so which product is 4 is [?].
|
|
Full UUID: `84f3094a-7b88-49c6-8657-3a6a385e6f68` (the unit's unique id, matches the old "fingerprint").
|
|
None of the product/vendor/ODM text strings are in this archive; those live in the `HWSetup` ini struct
|
|
(0x1441300: `productString` `EProduct_LG_LSM100`, `vendorString` `EVendor_LG`, `odmString` `EOdm_Chicony`,
|
|
`exposureTimeUsMin/Max`, `skewedImageGain`).
|
|
|
|
### Signature
|
|
|
|
| Offset | Value | Meaning |
|
|
|---|---|---|
|
|
| 0x4AD9 | 256 bytes, to the end of the blob | RSA-2048 PKCS#1 v1.5 signature with SHA-1 (DigestInfo `3021300906052b0e03021a05000414`) over blob bytes `[0, N-256)`, i.e. everything before the signature, header excluded [D]. **Verifies** against `calib/calib_rsa.pub.pem` (public exponent 35, an unusual value) with `cryptography`. |
|
|
|
|
## Integrity
|
|
|
|
### Checksum (0x3002) [D, matched against the dump; not seen in disassembly]
|
|
|
|
`CRC-32` (the zlib `crc32()`, polynomial 0xEDB88320, init/xorout 0xFFFFFFFF) over **all N blob bytes including
|
|
the 256-byte signature**, stored little-endian. On this unit: `zlib.crc32(blob) == 0x3cd95f42` and the EEPROM bytes at
|
|
0x3002 are `42 5f d9 3c`. The sum, Adler-32, SHA-1 and MD5 variants were tried and do not match.
|
|
|
|
Use in the app (`HalLuckyCat::loadHardwareData`, 0x1335430 [V]): the checksum is read via `readBinaryDataCheckSum`
|
|
(0x138d410) and turned into a **cache file name** `<PathsPool::logDir()>/<crc as 8 hex digits>.bin`.
|
|
- Checksum 0 or 0xFFFFFFFF (unprogrammed EEPROM): skip the cache and read the blob from the mouse.
|
|
- Otherwise the app tries the cache file ("Read HardwareData from file.", parser 0x14ba4e0). On failure it logs "Error reading
|
|
HardwareData from file. Try reading from mouse" and reads the blob from the EEPROM (`ovtReadHardwareData`, 0x137e0e0).
|
|
After a read from the mouse it writes the cache (0x14bc150).
|
|
So the CRC is a cache key; a wrong value would only cause a cache miss or a stale cache hit, not a rejection.
|
|
No CRC computation happens on the read path (the CRC-32 table is at 0x1c0c730, used by a zlib routine at 0xeff640).
|
|
|
|
### Signature [V]
|
|
|
|
`hal::verifyRSASignature(data, dataLen, sig, sigLen)` is 0x12ff5d0: it loads the PEM public key from the Qt resource
|
|
`:/calib_rsa.pub.txt` (`PEM_read_bio_PUBKEY`) and runs `EVP_sha1` + `EVP_DigestUpdate` + `EVP_VerifyFinal`, i.e.
|
|
**RSASSA-PKCS1-v1_5 with SHA-1**.
|
|
It is called from `ovtReadHardwareData` (0x137e0e0) after `HardwareDataSerializer::fromRawData` (0x14c3720). `fromRawData`
|
|
returns the archive stream position after `InfoParams`; that is the length of the signed part. The acceptance rule is:
|
|
|
|
ok = (archiveVersion < 11) or (0 < consumed < N and verifyRSASignature(blob, consumed, blob + consumed, N - consumed))
|
|
|
|
So blobs with `archiveVersion` >= 11 must carry a valid signature over `blob[0 .. consumed)` (everything before the
|
|
signature; the 6-byte EEPROM header is not included). Older archives (< 11) are accepted unsigned.
|
|
This unit: consumed = N - 256 = 19161, the signature is the last 256 bytes, and **it verifies**:
|
|
`calib/calib_rsa.pub.pem` (RSA-2048, public exponent 35) recovers
|
|
`0001ff..ff00 3021300906052b0e03021a05000414 b5dab6a61406d37057c5381e242b7424f22a952e` and
|
|
`SHA1(blob[:19161]) = b5dab6a61406d37057c5381e242b7424f22a952e`. The key is copied to `calib/calib_rsa.pub.pem`.
|
|
(Verification only. The private key is not in the app; writes are produced by the factory tool.)
|
|
|
|
## How the calibration is used
|
|
|
|
### Undistortion map (`generateMapFromParams`, 0x120e160 [V]; per-pixel kernel 0x10f1750 [V])
|
|
|
|
The app builds the K matrix and the distortion vector from the Undistortion members and fills two float
|
|
maps `mapX`, `mapY` (one entry per *output* pixel). Accessors: focal 0x14c5d00, ppX 0x14c5d10, ppY 0x14c5d20,
|
|
`distCoefficients` 0x14c5d90 (raw pointer to member +0x1C), `tiltH` 0x14c5e00 (member +0x44), `pixelSize` 0x14c5e30.
|
|
|
|
K = [[f, 0, cx], [0, f, cy], [0, 0, 1]] (fx = fy = focalLength)
|
|
H = tiltH row-major, 3x3, H[2][2] = 1.0 (h00 h01 h02 h10 h11 h12 h20 h21)
|
|
M = inv(K) * inv(H) (two cvInvert, one cvGEMM; OpenCV 2.4.3)
|
|
for every output pixel (x, y):
|
|
(X, Y, W) = M * (x, y, 1); x' = X/W, y' = Y/W
|
|
r2 = x'^2 + y'^2
|
|
radial = 1 + c0*r2 + c1*r2^2 + c4*r2^3
|
|
xd = x'*radial + 2*c2*x'*y' + c3*(r2 + 2*x'^2)
|
|
yd = y'*radial + c2*(r2 + 2*y'^2) + 2*c3*x'*y'
|
|
mapX = K00*xd + K01*yd + K02 ; mapY = K10*xd + K11*yd + K12
|
|
|
|
- `distCoefficients` = OpenCV order `[k1, k2, p1, p2, k3]` ([V] from the kernel: indices 0, 1, 4 radial; 2, 3 tangential).
|
|
The unit's values: k1 = 0, **k2 = -0.40317**, p1 = 1.38e-4, p2 = 2.65e-3, k3 = 0. This is the standard OpenCV
|
|
5-parameter model (the same code path as OpenCV's `initUndistortRectifyMap` with `newCameraMatrix = H*K` in the sense
|
|
`dest = H * K * normalised`).
|
|
- `tiltH` is a homography in **pixel space, applied after K**: forward direction is `dest = H * K * (x_d-undistorted
|
|
normalised coords)`, which is what `M = inv(K)*inv(H)` encodes. It is *not* a rotation on normalised rays.
|
|
For this unit H is close to identity (shift about (-0.99, -0.44) px, scale 1.003 / 1.007, tiny perspective terms).
|
|
- `distModel` is **never read** in this build: its getter 0x14c5d40 has no callers (vtable slot only). The
|
|
`EDistortionModel` enum has a name map (0x14cadf0) but the map builder always uses the 5-coefficient model above. So
|
|
value 4 is cosmetic here [V by absence of callers]. What 4 means in the enum is [?].
|
|
- The cvUndistortPoints / cvInitUndistortMap error text "Distortion coefficients must be 1x4, 4x1, 1x5..." (0xb65990,
|
|
0xb6db50) is plain OpenCV 2.4.3 code; it takes the same 5-vector.
|
|
|
|
### Sensor set-up and misc (`HalLuckyCat::open`, 0x1331ae0 [V])
|
|
|
|
`open` copies the blob into the driver config (`config` = param_2) with the `HardwareData` accessors (`color` 0x14c4ca0 @+0x00,
|
|
`undistortion` 0x14c4cb0 @+0x68, `sensor` 0x14c4cd0 @+0xF4, `roi` 0x14c4cf0 @+0x144, `info` 0x14c4d10 @+0x158):
|
|
|
|
| Blob field | Goes to | Evidence |
|
|
|---|---|---|
|
|
| `exposureTimeUs` | `config+0x2F0` = float(exposureTimeUs) * 0.001f (so **ms**: 440 -> 0.44 ms) | getter 0x14c5470 called at 0x13328a8; mul at 0x13328ba (constant 0.001f at 0x1cf9a94). This confirms the u32 440 |
|
|
| `imagerGain` | `config+0x2F4` (double) | getter 0x14c54a0 at 0x13328d5 |
|
|
| `gainR/G/B` | `config+0x2FC/0x300/0x304` | getters 0x14c54d0 / 0x14c54e0 / 0x14c54f0 |
|
|
| roi x0, y0, x1, y1 | `config+0x2D4..0x2E0` (defaults 0, 0, 640, 296) | 0x14c7680 |
|
|
| `yBaseOffset` | `config+0x2E4` | getter 0x14c76d0 |
|
|
| `product` | `config+8` | 0x14c7890 |
|
|
|
|
The reverse direction (config -> blob, used when the app writes/derives a new calibration) uses the setters 0x14c5460 (exposure,
|
|
`int(ms / 0.001)`), 0x14c5480, 0x14c54b0, 0x14c7620, 0x14c76c0, 0x14c7880.
|
|
|
|
### Colour correction (NURBS surfaces)
|
|
|
|
The blob stores *samples*, not surfaces. `FUN_011e7950` ([V], 3 passes, one per colour channel `c` = 0, 1, 2) turns the black and the
|
|
white sample set into B-spline surfaces (library `UCBspl`: `SplineSurface`, `MGsystem` multigrid, `LSsystem` least squares):
|
|
|
|
for c in (0, 1, 2):
|
|
x[i], y[i] = sample.x, sample.y ; z[i] = sample.colour_byte[c] # sample = 8-byte struct: i16 x, i16 y, u8[3]
|
|
sys = MGsystem(1, 1, nurbsModelSize = 5, x, y, z) # 0x11fc2d0: multilevel B-spline, levels grow as 2^k
|
|
if nurbsSmoothingFactors[c] != 0:
|
|
sys.setSmoothing(nurbsSmoothingFactors[c]) # 0x11fdb70 (double)
|
|
sys.iterate(nurbsNSmoothingIterations, 3) # 0x11fda30
|
|
surfaces.append(SplineSurface(sys)) # 0x11fd660
|
|
|
|
So `nurbsModelSize` (5) is the number of refinement levels of the multilevel B-spline, `nurbsSmoothingFactors[c]` is the per-channel
|
|
smoothing weight (50 here) and `nurbsNSmoothingIterations` (100) is the iteration count of the smoothing solver; accessors
|
|
0x14c55a0 (modelSize), 0x14c55d0 (factors pointer), 0x14c55f0 (iterations) have the single caller 0x11e7950 [V]. The black surface and
|
|
the white surface are evaluated per pixel by the colour balancer (`NurbsColorBalancerPolicy`); the linear map
|
|
`out = slope*in + intercept` and the target 250 (`colorDestBrightImage`) are in `RE_colour_stitch.md`.
|
|
The sample sets themselves are the `nurbsModelBlack/White` members (the vector-of-vectors at ColorParams +0x00).
|
|
The three passes use colour-byte index 0, 1, 2 of each sample, which is the stored order b, g, r.
|
|
|
|
`lambda` / `lambdaNormFactor`: the library message "`LSsystem::calcLambda(): e_norm is zero when calculating lambda`" (string at 0x1b106c4)
|
|
suggests [G] that `lambda` is the **regularisation weight computed by the least-squares solver** for a fit
|
|
(`LSsystem::calcLambda`, balances data fit against the smoothness term), stored once per channel at calibration time
|
|
(1.92e12, 5.19e7, 5.10e7). That is an inference from the library name only; the strings have no direct code reference in the decompilation. `lambdaNormFactor` (6.025e9) normalises it. The only reader is accessor 0x14c5570, which returns
|
|
`lambda[0] / lambdaNormFactor` (= 318.9 on this unit) [V]. Its two callers are the tracker set-ups `FUN_011543e0` (0x115460b, `FastTrackLKM`
|
|
parameters) and `FUN_0115bc70` (0x115c341, `QualityTrackManager`), where it is passed as one double of the parameter struct next to
|
|
`pixelSize`. The factory-line settings key `colorExpectedNormalizedLambda1Min` (read at 0x1422b40, also 0x14800d0) looks like a lower bound
|
|
for this normalised value, i.e. a calibration quality gate [G]. What the tracker does with the value is [?]. `lambda[1]` and `lambda[2]`
|
|
have no direct accessor call: the pointer accessor 0x14c5530 has only a vtable data reference.
|
|
|
|
## Open questions
|
|
|
|
- Read the 64-byte ID at 0x7EB5 from the device (not done; static analysis only so far).
|
|
- Which product the `EProduct` value 4 is (the moc enumerator table is not in the binary in readable form).
|
|
- The `EDistortionModel` enum value 4 (its name map is built at 0x14cadf0), though the value is never read.
|
|
- What the tracker (`FastTrackLKM`, `QualityTrackManager`) does with `lambda[0]/lambdaNormFactor`; `lambda[1]`/`lambda[2]` are never read.
|