Find hot photosites without repairing them, and measure a sensor's aging
The hot-pixel pass could only repair: it returned how many photosites it changed and threw away which. find_hot_pixels runs the same pass and returns them as sensor coordinates, leaving the frame alone, so a sensor's defects can be tracked across frames. sensor_scan prints each frame's candidates, and with --probe reads a list of coordinates back out of every frame. Run over 53 6D raws from 2015 to 2026, it found 32 persistent defects, 2 in 2015 and 32 by 2026, and showed what a defect map has to account for: a frame that does not flag a photosite proves nothing unless its neighbourhood is dark, and the 6D hides some of its defects itself above ISO 5000. docs/dev/sensor-health.md records the findings and the design they argue for.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# DarkRoom — Sensor health: a dated defect map per body
|
||||
|
||||
**Status:** Spike · 2026-10-04 · not built
|
||||
**Companion to:** [requirements.md](requirements.md) FR-RAW-3, [catalog.md](catalog.md)
|
||||
|
||||
A sensor gains defective photosites as it ages, and a photosite that has gone bad does not recover.
|
||||
This records, per camera body, which photosites are defective and since when, so that the library
|
||||
can show how a sensor has aged and the hot-pixel repair can fix the defects a body is known to have
|
||||
rather than only those that stand out in the frame at hand.
|
||||
|
||||
What exists is the measuring tool: `Demosaicer::find_hot_pixels` (the photosites the repair pass
|
||||
would replace, without replacing them) and `core/dr-gpu/examples/sensor_scan.rs`, which prints them
|
||||
per frame and, with `--probe`, reads a list of coordinates back out of each frame. The rest of this
|
||||
document is what a spike with them on the 6D found, and the design it argues for.
|
||||
|
||||
---
|
||||
|
||||
## 1. What is wanted
|
||||
|
||||
- **Settings → Bodies**, one entry per body, with a graph of the defective share over time in two
|
||||
series: photosites (the raw mosaic) and 2×2 cells holding at least one defective photosite (what
|
||||
reaches a pixel of the output).
|
||||
- **A dated defect map** per body, synced with the library like any other catalog data, and
|
||||
cumulative: an entry is never removed.
|
||||
- **The repair reads the map** whose date is nearest the frame's, and fixes every defect the body had
|
||||
by then, whether or not it stands out in that frame.
|
||||
- One body per model for now: the catalog stores `make model`, not a body serial.
|
||||
|
||||
## 2. What the spike found (6D, 2026-10-03)
|
||||
|
||||
53 CR2s, up to four per quarter at the highest ISO of a day, 2015 to 2026. The library holds almost
|
||||
no 6D raws from 2016–2022, so onsets in that span are dated to the span, not the year. `sensor_scan`
|
||||
ran at 0.8 s per frame, decode included.
|
||||
|
||||
**Persistence separates the sensor from the scene.** 4,179 photosites were flagged at least once;
|
||||
3,554 on one day only (stars, glints, noise). A defect is a photosite that keeps coming back.
|
||||
|
||||
**A frame that does not flag a photosite is not evidence it was clean.** The repair's test is
|
||||
relative to the neighbourhood, so a defect in a lit area does not stand out. Confirmed defects were
|
||||
flagged in a median 20 % of the frames after their first sighting. Only a frame whose neighbourhood
|
||||
at that photosite is dark counts, either way.
|
||||
|
||||
**The 6D hides some defects itself at high ISO.** (2517, 3172) reads 8,000–13,000 over neighbours
|
||||
near 200 at ISO 2000–5000, and does not stand out at all at ISO 6400–12800 (82 over 149 on
|
||||
2025-03-15). The camera appears to map out photosites it knows at those gains. So evidence for this
|
||||
body comes from ISO ≤ 5000; the cut-off must be learned per body, not fixed.
|
||||
|
||||
**Long exposures light everything.** A 9.8 s frame saturated every candidate; it confirms, it does
|
||||
not date.
|
||||
|
||||
**The curve.** Counting a photosite as defective from the first frame where it stands out, provided
|
||||
it stands out in at least 60 % of the observable frames after that (32 defects; 31 with a clean
|
||||
observable frame before onset to bracket it):
|
||||
|
||||
| Year | Defects | Share of photosites |
|
||||
|---|---|---|
|
||||
| 2015 | 2 | 0.1 ppm |
|
||||
| 2016–2021 | 2 | 0.1 ppm |
|
||||
| 2022 | 7 | 0.3 ppm |
|
||||
| 2023 | 26 | 1.3 ppm |
|
||||
| 2026 | 32 | 1.6 ppm |
|
||||
|
||||
(2517, 3172) is the shape every entry should have: clean at ISO 800–1000 in 2015 and at ISO 100–200
|
||||
in 2016, then 338 over 72 at ISO 100 on 2022-08-13 and in every comparable frame since.
|
||||
|
||||
Weak defects exist too, about twice their neighbours ((1814, 3039)); the blind repair misses them in
|
||||
most frames. A known map would catch them.
|
||||
|
||||
## 3. Design it argues for
|
||||
|
||||
- **Evidence per frame, per known photosite**: observable (dark neighbourhood, ISO inside the
|
||||
body's band) and, if so, lit or clean. Not just the frame's flagged list.
|
||||
- **A map entry is a bracket**: last clean observation, first lit observation, strength, kind. Onset
|
||||
lies between the two; the graph plots it at the first, and can show the bracket.
|
||||
- **The repair**: every entry whose first lit date is on or before the frame's capture date; for an
|
||||
entry whose bracket contains the date, probe the photosite in the frame itself.
|
||||
- **Storage and sync**: catalog tables created on first use (as `albums` does), so no schema bump
|
||||
breaks an older peer. They travel in the snapshot by default. Merge is a set union of photosites
|
||||
per body, the earlier first-lit and the later last-clean winning, which makes it commutative and
|
||||
keeps the map cumulative.
|
||||
- **Work**: a sample, not the library. Frames are picked for what they can reveal (dark, mid ISO,
|
||||
long exposures), a few per body per month, and after the first pass only new imports are read.
|
||||
@@ -9,7 +9,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
|
||||
|
||||
| Metric | Value |
|
||||
|---|---|
|
||||
| Source files scanned | 500 |
|
||||
| Source files scanned | 501 |
|
||||
| TRACES tags found | 2105 |
|
||||
| Requirements defined | 193 |
|
||||
| Requirements deferred (post-v1) | 24 |
|
||||
@@ -149,9 +149,9 @@ _None._
|
||||
| FR-PLAT-WIN-3 | [`apps/darkroom-desktop/src/main.rs:35`](../../apps/darkroom-desktop/src/main.rs#L35) |
|
||||
| FR-RAW-1 | [`core/dr-decode/src/lib.rs:266`](../../core/dr-decode/src/lib.rs#L266), [`core/dr-types/src/lib.rs:135`](../../core/dr-types/src/lib.rs#L135), [`core/dr-types/src/lib.rs:206`](../../core/dr-types/src/lib.rs#L206) |
|
||||
| FR-RAW-2 | [`core/dr-decode/src/decoder.rs:1`](../../core/dr-decode/src/decoder.rs#L1), [`core/dr-decode/src/decoder.rs:26`](../../core/dr-decode/src/decoder.rs#L26), [`core/dr-decode/src/decoder.rs:59`](../../core/dr-decode/src/decoder.rs#L59), [`core/dr-decode/src/decoder.rs:95`](../../core/dr-decode/src/decoder.rs#L95), [`ui/dr-ui/src/decoder_seam.rs:185`](../../ui/dr-ui/src/decoder_seam.rs#L185), [`ui/dr-ui/src/decoder_seam.rs:1`](../../ui/dr-ui/src/decoder_seam.rs#L1), [`ui/dr-ui/src/decoder_seam.rs:216`](../../ui/dr-ui/src/decoder_seam.rs#L216), [`ui/dr-ui/src/decoder_seam.rs:257`](../../ui/dr-ui/src/decoder_seam.rs#L257), [`ui/dr-ui/src/export.rs:1238`](../../ui/dr-ui/src/export.rs#L1238), [`ui/dr-ui/src/export.rs:973`](../../ui/dr-ui/src/export.rs#L973), [`ui/dr-ui/src/import.rs:96`](../../ui/dr-ui/src/import.rs#L96), [`ui/dr-ui/src/library/sweep.rs:322`](../../ui/dr-ui/src/library/sweep.rs#L322), [`ui/dr-ui/src/library/sweep.rs:654`](../../ui/dr-ui/src/library/sweep.rs#L654), [`ui/dr-ui/src/library/thumbnails_gen.rs:109`](../../ui/dr-ui/src/library/thumbnails_gen.rs#L109), [`ui/dr-ui/src/merge.rs:119`](../../ui/dr-ui/src/merge.rs#L119), [`ui/dr-ui/src/repairs.rs:243`](../../ui/dr-ui/src/repairs.rs#L243) |
|
||||
| FR-RAW-3 | [`core/dr-decode/src/lib.rs:162`](../../core/dr-decode/src/lib.rs#L162), [`core/dr-decode/src/lib.rs:564`](../../core/dr-decode/src/lib.rs#L564), [`core/dr-decode/src/locate.rs:1435`](../../core/dr-decode/src/locate.rs#L1435), [`core/dr-gpu/src/demosaic.rs:1063`](../../core/dr-gpu/src/demosaic.rs#L1063), [`core/dr-gpu/src/demosaic.rs:1409`](../../core/dr-gpu/src/demosaic.rs#L1409), [`core/dr-gpu/src/demosaic.rs:868`](../../core/dr-gpu/src/demosaic.rs#L868), [`core/dr-gpu/tests/hot_pixels.rs:1`](../../core/dr-gpu/tests/hot_pixels.rs#L1) |
|
||||
| FR-RAW-3 | [`core/dr-decode/src/lib.rs:162`](../../core/dr-decode/src/lib.rs#L162), [`core/dr-decode/src/lib.rs:564`](../../core/dr-decode/src/lib.rs#L564), [`core/dr-decode/src/locate.rs:1435`](../../core/dr-decode/src/locate.rs#L1435), [`core/dr-gpu/src/demosaic.rs:1063`](../../core/dr-gpu/src/demosaic.rs#L1063), [`core/dr-gpu/src/demosaic.rs:1461`](../../core/dr-gpu/src/demosaic.rs#L1461), [`core/dr-gpu/src/demosaic.rs:868`](../../core/dr-gpu/src/demosaic.rs#L868), [`core/dr-gpu/tests/hot_pixels.rs:1`](../../core/dr-gpu/tests/hot_pixels.rs#L1) |
|
||||
| FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../../core/dr-decode/src/error.rs#L1), [`core/dr-decode/src/error.rs:30`](../../core/dr-decode/src/error.rs#L30), [`ui/dr-ui/src/lib.rs:295`](../../ui/dr-ui/src/lib.rs#L295) |
|
||||
| FR-RAW-5 | [`core/dr-decode/src/lib.rs:190`](../../core/dr-decode/src/lib.rs#L190), [`core/dr-gpu/src/demosaic.rs:1204`](../../core/dr-gpu/src/demosaic.rs#L1204), [`core/dr-gpu/src/demosaic.rs:1283`](../../core/dr-gpu/src/demosaic.rs#L1283), [`core/dr-gpu/src/demosaic.rs:1407`](../../core/dr-gpu/src/demosaic.rs#L1407), [`core/dr-gpu/src/demosaic.rs:34`](../../core/dr-gpu/src/demosaic.rs#L34) |
|
||||
| FR-RAW-5 | [`core/dr-decode/src/lib.rs:190`](../../core/dr-decode/src/lib.rs#L190), [`core/dr-gpu/src/demosaic.rs:1256`](../../core/dr-gpu/src/demosaic.rs#L1256), [`core/dr-gpu/src/demosaic.rs:1335`](../../core/dr-gpu/src/demosaic.rs#L1335), [`core/dr-gpu/src/demosaic.rs:1459`](../../core/dr-gpu/src/demosaic.rs#L1459), [`core/dr-gpu/src/demosaic.rs:34`](../../core/dr-gpu/src/demosaic.rs#L34) |
|
||||
| FR-UI-1 | [`core/dr-types/src/settings.rs:1390`](../../core/dr-types/src/settings.rs#L1390), [`core/dr-types/src/settings.rs:492`](../../core/dr-types/src/settings.rs#L492), [`core/dr-types/src/settings.rs:512`](../../core/dr-types/src/settings.rs#L512), [`ui/dr-ui/src/develop_ui.rs:284`](../../ui/dr-ui/src/develop_ui.rs#L284), [`ui/dr-ui/src/lib.rs:126`](../../ui/dr-ui/src/lib.rs#L126), [`ui/dr-ui/src/lib.rs:141`](../../ui/dr-ui/src/lib.rs#L141), [`ui/dr-ui/src/lib.rs:163`](../../ui/dr-ui/src/lib.rs#L163), [`ui/dr-ui/src/lib.rs:3387`](../../ui/dr-ui/src/lib.rs#L3387), [`ui/dr-ui/src/lib.rs:3406`](../../ui/dr-ui/src/lib.rs#L3406), [`ui/dr-ui/src/lib.rs:3433`](../../ui/dr-ui/src/lib.rs#L3433), [`ui/dr-ui/src/lib.rs:3484`](../../ui/dr-ui/src/lib.rs#L3484), [`ui/dr-ui/src/lib.rs:3517`](../../ui/dr-ui/src/lib.rs#L3517), [`ui/dr-ui/src/library_ui/grid.rs:242`](../../ui/dr-ui/src/library_ui/grid.rs#L242), [`ui/dr-ui/src/masks_ui.rs:1608`](../../ui/dr-ui/src/masks_ui.rs#L1608), [`ui/dr-ui/src/settings_ui.rs:143`](../../ui/dr-ui/src/settings_ui.rs#L143), [`ui/dr-ui/ui/adjust.slint:1000`](../../ui/dr-ui/ui/adjust.slint#L1000), [`ui/dr-ui/ui/app.slint:3147`](../../ui/dr-ui/ui/app.slint#L3147), [`ui/dr-ui/ui/app.slint:670`](../../ui/dr-ui/ui/app.slint#L670), [`ui/dr-ui/ui/app.slint:815`](../../ui/dr-ui/ui/app.slint#L815), [`ui/dr-ui/ui/identity.slint:249`](../../ui/dr-ui/ui/identity.slint#L249), [`ui/dr-ui/ui/library.slint:1469`](../../ui/dr-ui/ui/library.slint#L1469), [`ui/dr-ui/ui/settings.slint:129`](../../ui/dr-ui/ui/settings.slint#L129), [`ui/dr-ui/ui/settings.slint:660`](../../ui/dr-ui/ui/settings.slint#L660), [`ui/dr-ui/ui/toolrail.slint:180`](../../ui/dr-ui/ui/toolrail.slint#L180), [`ui/dr-ui/ui/toolrail.slint:92`](../../ui/dr-ui/ui/toolrail.slint#L92), [`ui/dr-ui/ui/widgets.slint:1117`](../../ui/dr-ui/ui/widgets.slint#L1117) |
|
||||
| FR-UI-2 | [`ui/dr-ui/src/collections_ui/controller.rs:45`](../../ui/dr-ui/src/collections_ui/controller.rs#L45), [`ui/dr-ui/src/collections_ui/controller.rs:83`](../../ui/dr-ui/src/collections_ui/controller.rs#L83), [`ui/dr-ui/src/collections_ui/drag.rs:246`](../../ui/dr-ui/src/collections_ui/drag.rs#L246), [`ui/dr-ui/src/collections_ui/drag.rs:256`](../../ui/dr-ui/src/collections_ui/drag.rs#L256), [`ui/dr-ui/src/collections_ui/press.rs:453`](../../ui/dr-ui/src/collections_ui/press.rs#L453), [`ui/dr-ui/src/collections_ui/wiring_grid.rs:121`](../../ui/dr-ui/src/collections_ui/wiring_grid.rs#L121), [`ui/dr-ui/src/collections_ui/wiring_grid.rs:138`](../../ui/dr-ui/src/collections_ui/wiring_grid.rs#L138), [`ui/dr-ui/src/collections_ui/wiring_grid.rs:415`](../../ui/dr-ui/src/collections_ui/wiring_grid.rs#L415), [`ui/dr-ui/src/collections_ui/wiring_tree.rs:287`](../../ui/dr-ui/src/collections_ui/wiring_tree.rs#L287), [`ui/dr-ui/src/gestures.rs:1`](../../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/lib.rs:126`](../../ui/dr-ui/src/lib.rs#L126), [`ui/dr-ui/src/library_ui/filter_bar.rs:351`](../../ui/dr-ui/src/library_ui/filter_bar.rs#L351), [`ui/dr-ui/src/library_ui/grid.rs:242`](../../ui/dr-ui/src/library_ui/grid.rs#L242), [`ui/dr-ui/src/library_ui/window.rs:1164`](../../ui/dr-ui/src/library_ui/window.rs#L1164), [`ui/dr-ui/tests/scroll_cue.rs:1`](../../ui/dr-ui/tests/scroll_cue.rs#L1), [`ui/dr-ui/ui/app.slint:137`](../../ui/dr-ui/ui/app.slint#L137), [`ui/dr-ui/ui/app.slint:3545`](../../ui/dr-ui/ui/app.slint#L3545), [`ui/dr-ui/ui/app.slint:799`](../../ui/dr-ui/ui/app.slint#L799), [`ui/dr-ui/ui/collections.slint:1354`](../../ui/dr-ui/ui/collections.slint#L1354), [`ui/dr-ui/ui/controls.slint:626`](../../ui/dr-ui/ui/controls.slint#L626), [`ui/dr-ui/ui/gestures.slint:1`](../../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1356`](../../ui/dr-ui/ui/library.slint#L1356), [`ui/dr-ui/ui/library.slint:1389`](../../ui/dr-ui/ui/library.slint#L1389), [`ui/dr-ui/ui/library.slint:1717`](../../ui/dr-ui/ui/library.slint#L1717), [`ui/dr-ui/ui/library.slint:1724`](../../ui/dr-ui/ui/library.slint#L1724), [`ui/dr-ui/ui/library.slint:1825`](../../ui/dr-ui/ui/library.slint#L1825), [`ui/dr-ui/ui/library.slint:3168`](../../ui/dr-ui/ui/library.slint#L3168), [`ui/dr-ui/ui/library.slint:586`](../../ui/dr-ui/ui/library.slint#L586), [`ui/dr-ui/ui/presets.slint:114`](../../ui/dr-ui/ui/presets.slint#L114), [`ui/dr-ui/ui/settings.slint:143`](../../ui/dr-ui/ui/settings.slint#L143), [`ui/dr-ui/ui/widgets.slint:1117`](../../ui/dr-ui/ui/widgets.slint#L1117) |
|
||||
| FR-UI-3 | [`ui/dr-ui/src/collections_ui/controller.rs:143`](../../ui/dr-ui/src/collections_ui/controller.rs#L143), [`ui/dr-ui/src/collections_ui/controller.rs:348`](../../ui/dr-ui/src/collections_ui/controller.rs#L348), [`ui/dr-ui/src/collections_ui/controller.rs:361`](../../ui/dr-ui/src/collections_ui/controller.rs#L361), [`ui/dr-ui/src/collections_ui/wiring_tree.rs:287`](../../ui/dr-ui/src/collections_ui/wiring_tree.rs#L287), [`ui/dr-ui/src/develop/masks.rs:361`](../../ui/dr-ui/src/develop/masks.rs#L361), [`ui/dr-ui/src/develop/repairs.rs:49`](../../ui/dr-ui/src/develop/repairs.rs#L49), [`ui/dr-ui/src/library_ui/grid.rs:228`](../../ui/dr-ui/src/library_ui/grid.rs#L228), [`ui/dr-ui/src/masks_ui.rs:1700`](../../ui/dr-ui/src/masks_ui.rs#L1700), [`ui/dr-ui/src/masks_ui.rs:1722`](../../ui/dr-ui/src/masks_ui.rs#L1722), [`ui/dr-ui/src/masks_ui.rs:339`](../../ui/dr-ui/src/masks_ui.rs#L339), [`ui/dr-ui/src/spots_ui.rs:19`](../../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:2356`](../../ui/dr-ui/ui/app.slint#L2356), [`ui/dr-ui/ui/collections.slint:1228`](../../ui/dr-ui/ui/collections.slint#L1228), [`ui/dr-ui/ui/collections.slint:4`](../../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:78`](../../ui/dr-ui/ui/collections.slint#L78), [`ui/dr-ui/ui/collections.slint:978`](../../ui/dr-ui/ui/collections.slint#L978), [`ui/dr-ui/ui/masks.slint:1246`](../../ui/dr-ui/ui/masks.slint#L1246), [`ui/dr-ui/ui/widgets.slint:72`](../../ui/dr-ui/ui/widgets.slint#L72) |
|
||||
|
||||
Reference in New Issue
Block a user