calibrate: find the chart beside a shadow, and hold the matrix to the camera's noise - #44
Conversation
…d luma The detector calls a pixel flat when its 3x3 range is under three times the frame's noise, and takes the noise from the quietest tenth of the frame. A sensor's noise is mostly shot noise, which grows with the light, so the quietest tenth is the darkest tenth -- and on a frame with real shadows the threshold landed under the noise of the chart's own bright patches. They shattered into specks and the chart was reported missing in plain view. Measured on a lab gk7605v100 + SC2239, chart lit by window light: threshold 10.9 counts, 9 of 24 patches survived as candidates (18 are needed), no chart. The same chart under a lamp, with brighter shadows, came out at 18.8 and was found -- so whether Calibrate worked depended on the room. The downscaled luma is now black-subtracted and square-rooted (Anscombe's transform less its constant scale, which a relative threshold does not need), so noise is one size at every brightness. On the three frames taken of that chart the threshold lands at 1.6 to 1.7 and detection goes: window light no chart -> 23 cells lamp, earlier 19 cells -> 22 cells lamp 22 cells -> 22 cells with the corners within 6 px of each other across all of them. The ramp test that picks the chart's orientation only asks which way the greys fall, and a root keeps that order. The smoke test draws a chart with shot noise beside a shadow covering a third of the frame. The engine before this finds nothing on it (only those two checks fail); after, all 24 cells, 0.0 px from the drawn corners. make-chart.mjs gains `shot` (variance per count) and `dark` for it.
A chart fit sees 24 averaged patches, so their noise is gone before the solver looks, and it is free to buy accuracy by amplifying the sensor's noise. On a lab gk7605v100 + SC2239 under a 2330 K lamp it did: mean ΔE2000 went from 8.9 to 5.7 with a green row amplifying noise 3.73 against the 1.69 of the matrix the camera ran, and after saving it the camera's video carried 2.2 to 2.6 times the chroma noise on flat surfaces (temporal std over 40 RTSP frames, back to back with the matrix it replaced). The picture's colour got better and the video got visibly worse. noiseGain() is that amplification per output channel: the length of each matrix row with the white balance gains folded in. solveFromPatches takes an optional noiseBudget and holds the fit under it with a penalty that is steepened until it holds (a fixed weight left rows a few per cent over). What the chart alone asked for comes back as noise.free, so the trade can be shown. A budget the free fit already meets changes nothing. The editor budgets against the camera's own matrix at the measured light, read from the profile it already runs, times NOISE_HEADROOM = 1.25. That figure is measured on the same camera, light and chart: held at 1.25 the video's chroma noise rose 0 to 30 per cent and the chart's colour error (a*b*, on the JPEG) was 10.4 against 10.8 unheld and 14.0 before; held at 1.0 it was 12.9. The result now says how much noisier than today the picture will be, and what an exact chart match would have cost -- or, with no profile to compare against, that the noise was not checked. The smoke test carries the SC2239's 24 measured patches and the 2525 K matrix that camera ran: free, 2.19 times noisier; held, within 0.13% of the budget at 7.18 ΔE2000; rows still sum to one. Both new UI checks fail without the editor change.
PR Summary by QodoDetect shadow-adjacent charts and limit calibration noise
AI Description
Diagram
High-Level Assessment
Files changed (9)
|
Code Review by Qodo
1.
|
| const own = readColour(parseIni(await calibrate.baseline())).ccm; | ||
| if (!own) why = 'no-baseline'; | ||
| else { | ||
| const at = vendorCcmAt(own, free.light.cct); | ||
| const budget = noiseGain(at, free.neutral).map((v) => v * NOISE_HEADROOM); |
There was a problem hiding this comment.
4. Noise allowance creeps up with each recalibration 🐞 Bug ≡ Correctness
measureChart builds the budget from calibrate.baseline() (the profile the camera runs today) times NOISE_HEADROOM, but persist(ini) via Build the camera profile writes this tool's held matrices back into that same profile. Every later calibration is therefore capped at 1.25× an already-raised matrix (≈1.95× stock after three rounds), while the note still reports staying within 1.25× of 'today'.
Agent Prompt
## Issue description
The noise budget is computed from the camera's current [static_ccm], which after persist contains this tool's own earlier output, so the 1.25× headroom compounds across calibrations.
## Fix Focus Areas
- src/editor.js[2863-2882]
- src/editor.js[3349-3370]
- dist/editor.js[2863-2882]
## Recommended Fix
Capture the vendor/stock CCM tables before the first persist (or store a marker/reference gain in the written fragment) and compute the budget from that fixed reference instead of the live baseline; rebuild dist.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
…t cannot hold Review findings on the noise budget: - The camera's matrix was scored through the chart's white balance, not the camera's. It is now scored through the frame's AsShotNeutral, the balance the camera chose for this picture -- which is what "the picture as it is now" means. On the lab SC2239 those differ by up to 36% in the blue gain under daylight, because the camera's AWB lands on the wrong temperature there. - A held fit that ended over its budget was returned as held. The final gains are now checked, and a budget no matrix can meet is an error that says how far over the closest one is. - The catch that fell back to an unheld fit also swallowed solver errors. It now covers only reading the camera's profile; a failed held solve reaches the operator as the error it is. Not changed: the reference is the camera's matrix as it is now, so a saved calibration becomes the reference for the next one. An absolute ceiling was measured instead and does not exist across sensors: IMX307's shipped tables reach 2.27x the noise of plain white balance, the SC2239 library's own tables 5.35x. The note now says what the reference is -- "a calibration saved earlier counts as now" -- so each step is visible.
|
Review addressed in bf25995:
|
… shadow and holds the matrix to the camera's noise (#621) Two Calibrate fixes, both found calibrating a gk7605v100 + SC2239 against a ColorChecker (OpenIPC/raw-editor#44): - The chart detector missed a chart in plain view whenever the frame had real shadows: its flatness threshold came from the darkest tenth of the frame and sat under the shot noise of the chart's bright patches. Under window light it reported no chart; it now finds 23 cells there. - A chart fit could buy accuracy by amplifying the sensor's noise: after saving one, the camera's video carried 2.2-2.6x the chroma noise. The matrix is now held to 1.25x the noise of the one the camera runs, read from the profile it already serves, and the panel says what that cost. v0.19.3 now asks baseline() during Measure as well as Build; the WebUI's MajesticCalibrate already provides it, so nothing else changes here. Every dist/ file jsDelivr serves at @v0.19.3 was checked byte-identical to the tag's dist/. npm test passes. Co-authored-by: AI Dev <ai@openipc.org>
Two Calibrate defects, both found calibrating a lab gk7605v100 + SC2239 against a ColorChecker.
The chart was not found in plain view
The detector calls a pixel flat when its local range is under three times the frame's noise, and it takes the noise from the frame's quietest tenth. Shot noise grows with the light, so the quietest tenth is the darkest tenth. On a frame with real shadows the threshold landed below the noise of the chart's own bright patches, and they broke into specks.
Under window light: threshold 10.9 counts, 9 of 24 patches survived (18 are needed), and the chart was reported missing. The same chart under a lamp was found (threshold 18.8), so whether Calibrate worked depended on the room.
The detector's downscaled luma is now black-subtracted and square-rooted (Anscombe's transform), so noise is one size at every brightness.
The corners agree within 6 px across all three frames. A smoke test draws a chart with shot noise beside a shadow covering a third of the frame. The engine before this finds nothing (only those two checks fail); after, it finds all 24 cells, 0.0 px from the drawn corners. The real chart-on-wood frame still passes.
The matrix made the video noisy
A chart fit sees 24 averaged patches, so it never sees noise, and it is free to buy accuracy by amplifying the sensor's noise. On the SC2239 it did: mean ΔE2000 went from 8.9 to 5.7 with a green row amplifying noise 3.73× against the 1.69× of the matrix the camera ran. After saving it, the camera's video carried 2.2–2.6× the chroma noise on flat surfaces (temporal std over 40 RTSP frames, back to back with the matrix it replaced).
noiseGain(): per output channel, the length of each matrix row with the white balance gains folded in.solveFromPatches(…, { noiseBudget }): holds the fit under the budget with a penalty that is steepened until it holds. What the chart alone asked for comes back asnoise.free. A budget the free fit already meets changes nothing.NOISE_HEADROOM = 1.25. The result says how much noisier than today the picture will be and what an exact match would have cost; with no profile to compare against, it says the noise was not checked.Why 1.25, measured on the same camera, light and chart:
The shipped path was also run against the camera under window light: chart detected (23 cells), held at 2.27 against a 2.26 budget, ΔE2000 5.36 against 4.82 free. Saved to the camera, the video's chroma noise stayed within stock's range on the dark floor, wardrobe and chair, and rose from 0.5 to 0.7 on the plain wall.
Checks
node --checkon every module,tools/build.shwithdist/in sync,tools/smoke.mjsandtools/ui-check.mjsall pass, run in node 20 + clang/lld + chromium. Each new check was watched failing without its fix.