Skip to content

Find the bad pixels, and rings that say where they really are - #33

Merged
openipc-ai merged 5 commits into
mainfrom
defect-marks-sample
Sep 24, 2026
Merged

openipc-ai merged 5 commits into
mainfrom
defect-marks-sample

Conversation

@openipc-ai

Copy link
Copy Markdown
Contributor

A camera frame carrying 4054 defects drew every one of its rings inside the top
sixth of the picture, and was read — reasonably — as the sensor's bad pixels
being clustered at the top. They are not. The same scan's own spread index, a
couple of inches to the right of that picture, said 1.01: scattered, as sensor
defects are. Two halves of one panel saying opposite things, and the picture is
the half a person believes.

Two independent prefix bugs, and behind them a feature with no way in.

The rings were a prefix, not a sample

drawMarks caps what it draws at 600, which is worth doing. But it took the
first 600 of a list the engine fills in raster order, so the cap and that
order between them made a spatial claim nothing had measured. 600 of 4054 is
14.8% of the rows; measured off the picture, the rings stopped at 15.0% of the
frame height. Every k-th now.

This also explains the rest of the report — "Darker half" and "Darkest" looked
random while "Everywhere" did not. Those gate candidates by local background,
so fewer than 600 survive, all of them are drawn, and nothing is truncated.

Clark-Evans divided by an area it had not observed

When the store overflows it keeps a census of the top of the frame, but the
index normalised by the whole frame — so R came out low by about
sqrt(h / y_reached). On a synthetic 2592×1944 frame carrying 5115 uniformly
scattered defects it read 0.91, which the panel renders as "clustered —
these are following the picture"
, about a set uniform by construction. Over
the window actually reached it reads 1.015.

Sampling the stored set was tried first and is wrong: the compare panel tallies
sites across captures by exact coordinate, so the stored list has to be stable
between scans. Raising the cap is no better — clark_evans is O(n²) and
already runs 16.8M distance evaluations at 4096 points.

Find the bad pixels

Everything needed to tell a sensor from a scene was already in this tab, behind
knowing to press Scan, to choose Darkest, to press Keep five times, and what
"4 of 5" buys. This drives those and owns no judgement of its own: five
captures, each scanned and kept, then the rule the compare panel would have
suggested.

It grades the first capture from the frame rather than from what it was told —
a covered lens and an operator who believes the lens is covered are different
things. Measured on a lab gk7205v300 + imx335, a capture inside a black box
sits 0.000–0.001 of the way from black to saturation across 0.5 s to 7 s, and
this repository's lit fixture sits at 0.105. A dark frame is then left alone; a
frame with a picture in it gets the gate and is told to move the camera.

Which turned up a second thing. The brightness gate ruins a dark frame. Its
cut is a percentile of the local background, so on a frame dark all over it
lands just under where nearly every pixel sits — and a hot pixel lifts its own
7×7 background above it and is gated out. It removes the defects first, on
exactly the frames where the defects are all there is. On that camera's 7 s
dark frame, 3921 sites at R = 1.00 become 104 at R = 0.68; on a live
capture, 571 become 6. The run never narrows a dark frame, and the gate says
why.

It ends in a list that leaves the page: the confirmed sites as plain text under
a header naming the sensor, exposure, gain and rule. That header is not
decoration — dark current builds with time, so a map taken at 7 s is not a map
of a camera running at 1/30. On this camera, in a black box: 882 sites at
0.5 s, 1329 at 1 s, 1782 at 2 s, 3921 at 7 s.

Verified

tools/build.sh, the dist/ diff, smoke.mjs and ui-check.mjs, all green,
plus a live capture from the lab camera: 571 defects, R = 1.02, rings spread
68/71/61/72/68/65/70/96 across eight bands where they had been 600/0/0/0/0/0/0/0.

Both guards were watched failing with their change reverted. The engine one
goes to R = 0.420 against 1.016. The first version of the ring guard was
worthless and passed with the bug put back — it measured the marks against
their own midpoint, and a prefix is evenly spread inside its own band. Measured
against the picture instead, it now reports the lowest ring at 44% and fails.

The defect scan reports a Clark-Evans index beside the count, and the panel
turns it into a sentence: below 0.8 it reads "clustered -- these are following
the picture", which is the scan telling an operator that what it found is the
furniture rather than the sensor. On any frame that overflowed the store that
sentence was wrong, and wrong in the direction that costs most.

R is a mean nearest-neighbour distance over what a Poisson process of the same
density would give, so the area underneath has to be the area that was
observed. Two things make that smaller than the frame. The walk needs two
pixels of margin to have four same-colour neighbours, so nothing is ever found
outside [2, w-3] x [2, h-3]. And when the store fills it fills in raster order,
so what is kept is every defect down to the row it stopped on and none below --
a complete census of the top of the frame rather than a sample of all of it.
Dividing that by the whole frame understates the density, which inflates the
expected distance and deflates R by roughly sqrt(h / y_reached).

Measured on a synthetic 2592x1944 frame carrying 5115 uniformly scattered
defects, of which 4096 were stored: R read 0.91 -- "following the picture",
about a set that is uniform by construction. Over the window it reached, the
same run reads 1.015.

Sampling the stored set instead of taking a prefix was tried first and is
wrong for a reason that is not obvious from here: the compare panel tallies
sites across captures by exact coordinate, so the stored list has to be stable
between scans. Any rank-based sample shifts phase the moment one extra defect
turns up early, and two scans of one sensor would come back nearly disjoint.
Raising the cap is no better -- clark_evans is O(n^2) and already runs 16.8
million distance evaluations at 4096 points. The prefix was never the problem.
The area was.

Reported now as well: the median of each plane, which is what says whether the
lens was covered. Nothing else in the reading does -- the black floor is the
0.1st percentile and sits near black in any frame with a shadow in it, and the
clipped fraction is about the other end. It comes off the value histogram the
scan already builds, so it is one more walk of 16384 bins rather than of the
frame: 86.1 ms without it against 85.3 ms with, both engines in one process
reading the same 2592x1944 capture twelve times each, which is to say the
difference is under the noise. Timed across two containers instead it appeared
to cost 4 ms, and that was the containers.

What it reads, measured on the lab gk7205v300 + imx335: a capture inside a
black box sits 0.000 to 0.001 of the way from black to saturation across
exposures from 0.5 s to 7 s, and this repository's own lit fixture sits at
0.105.

Watched failing with the change reverted, which also needed the last_y
assignment taken out -- left in, it is set and never read, and -Werror stops
the build before any test can run. With the area back to w*h the capped
reading goes to R = 0.420 against the same frame's 1.016 uncapped, and two of
the three new checks go red.
A camera frame carrying 4054 defects had every one of its rings inside the top
sixth of the picture, in a band with a ragged edge along the bottom of it, and
it was read -- reasonably -- as the sensor's bad pixels being clustered at the
top. They are not. The same scan's own spread index, a couple of inches to the
right of that picture, said 1.01: scattered, as sensor defects are. The two
halves of one panel said opposite things, and the picture is the half a person
believes.

The overlay caps what it draws at 600, which is worth doing -- a sensor with
ten thousand bad pixels would otherwise spend a second building circles nobody
can tell apart. But it took the first 600 of a list the engine fills in raster
order, so the cap and that order between them made a spatial claim that nothing
had measured. 600 of 4054 is 14.8% of the rows; measured off the picture, the
rings stopped at 15.0% of the frame height.

Every k-th now, k = ceil(n / 600). The same cost, deterministic so it survives
the redraws that a resize or a tab switch bring, and it samples the whole
frame. On the reproduction -- 2592x1944, 5115 scattered defects -- the drawn
set goes from 600/0/0/0/0/0/0/0 across eight bands to 88/98/90/95/85/94/36/0,
which tracks the full set's own 612/684/630/666/594/660/250/0. On a live
capture from the lab gk7205v300 + imx335: 571 defects, R = 1.02, rings
68/71/61/72/68/65/70/96.

The panel was quoting the wrong number too, and only sometimes. "of which the
first N are marked" named the stored list, up to 4096, while the overlay was
drawing 600 -- and it appeared only when the store had overflowed, so the
common case, a few thousand defects ringed six hundred at a time, said nothing
at all. The ring count now comes from the same helper the overlay draws with,
so the two cannot drift, and a scan that filled its store says so and says what
that costs the arrangement reported under it.

The first version of the guard was worthless and passed with the bug put back.
It took the midpoint of the drawn marks and counted how many fell below it --
but a prefix is evenly spread inside its own band, so about half of them always
do. It is measured against the picture now: with 1342 defects drawn 600 at a
time, a prefix reaches 44% of the way down and stops, and a sample reaches the
bottom. Watched going red with the stride reverted.
Everything needed to tell a sensor's own bad pixels from detail in a picture
was already in this tab -- the brightness gate, the tally across captures, the
arrangement index -- and every bit of it was behind knowing to press Scan,
knowing to choose Darkest, knowing to press Keep five times, and knowing what
"4 of 5" buys. This is a way in. It drives those and owns no judgement of its
own: five captures, each scanned and kept, then the rule the compare panel
would have suggested, applied through the same path the Mark button uses.

The first capture also decides what kind of run this is, and it decides it from
the frame rather than from what it was told. A covered lens and an operator who
believes the lens is covered are different things, and only one of them shows
up in the numbers. Measured on the lab gk7205v300 + imx335, a capture inside a
black box sits 0.000 to 0.001 of the way from black to saturation across
exposures from 0.5 s to 7 s, while this repository's lit fixture sits at 0.105;
the cut is at 0.01, an order of magnitude clear of both, so nothing turns on
where exactly it goes. A dark frame is then left alone -- the camera should not
be moved, because there is nothing for it to move relative to. A frame with a
picture in it switches the gate to Darkest and asks for the camera to be moved
between captures, which is what stops scene detail agreeing with itself.

Telling those two apart turned up a second thing worth having. The gate is for
separating a sensor from a scene, and on a frame that is dark all over it does
the opposite. Its cut is a percentile of the local background, so on such a
frame it lands just under the level nearly every pixel sits at -- and a hot
pixel lifts its own 7x7 background a couple of counts above that, which is
enough to be gated out. It therefore discards the defects in preference to
everything else, on exactly the frames where the defects are all there is. On
this camera's 7 s dark frame, 3921 sites arranged at R = 1.00 become 104 at
R = 0.68, which the panel reports as "these are following the picture" about a
frame with no picture in it; on a live capture, 571 become 6. A run never
narrows a dark frame, and the gate now says so when the frame on screen is one.

The camera's own burst is deliberately not used. /image.dng?frames=N averages
consecutive frames in the sensor, and an average is the one thing that cannot
answer this question: a pixel that was wrong in every frame and a pixel that
was wrong in one of sixteen come out of it looking alike. The same reasoning is
written out where the Plates tab chooses between its two paths.

Without a capture provider the run still works and asks for each frame to be
dropped in, the way Plates and Calibrate already degrade. It refuses to read
the same frame twice inside one run: a frame read twice agrees with itself
perfectly, which is the one answer the tally must never be allowed to give.

And it ends in something that leaves the page -- the confirmed sites as plain
text, one x,y to a line, under a header naming the sensor, the exposure, the
gain and the rule that produced them. Plain text because nothing on the camera
consumes a defect map yet, and a list of coordinates is the shape every other
tool can read. The header is not decoration: dark current builds up with time,
so a map taken at seven seconds describes a sensor that is not the one running
at a thirtieth. On this camera, in a black box, the same sensor reported 882
sites at 0.5 s, 1329 at 1 s, 1782 at 2 s and 3921 at 7 s.

The fixtures cannot exercise the gate honestly, and the check says so rather
than pretending. They are a flat field with spikes on it, so on the lit arm the
gate has no texture to discriminate against and takes the planted sites out
along with everything else -- 52 per capture become 0. That is the gate doing
what it does on a frame no camera produces, so what is pinned is that the run
still ends somewhere rather than offering an empty file.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Correct defect mapping and add guided bad-pixel discovery

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Samples defect rings across the frame and corrects truncated-scan spatial statistics.
• Guides five-capture defect confirmation using measured frame darkness and adaptive gating.
• Exports confirmed sensor coordinates with capture metadata and adds regression coverage.
Diagram

graph TD
  A["Operator"] --> B["Guided Run"] --> C["Five Captures"] --> D["Diagnosis Engine"] --> E{"Dark Frame?"}
  E -->|Yes| F["Full-frame Scan"] --> H["4-of-5 Results"]
  E -->|No| G["Darkest-quarter Scan"] --> H
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Reservoir-sample stored defects
  • ➕ Could estimate whole-frame spatial distribution without tracking the reached scan window.
  • ➕ Keeps the Clark-Evans input bounded.
  • ➖ Destabilizes stored coordinates between captures unless sampling is carefully deterministic.
  • ➖ Undermines exact-coordinate comparison used to confirm persistent sensor defects.
2. Raise the defect storage cap
  • ➕ Avoids truncation on more frames.
  • ➕ Requires less special handling of the observed area.
  • ➖ Clark-Evans remains quadratic and is already expensive at 4,096 points.
  • ➖ Only postpones overflow and increases memory and runtime costs.
3. Randomly select overlay rings
  • ➕ Can provide an unbiased visual sample across the frame.
  • ➕ Avoids raster-prefix clustering.
  • ➖ Can change across redraws and make the UI appear unstable.
  • ➖ Requires seeded randomness and offers little benefit over deterministic striding.

Recommendation: Keep the PR's approach: deterministic striding gives stable, frame-wide overlays; observed-window normalization preserves the stable coordinate census needed by cross-capture tallies; and the guided workflow reuses existing scan and comparison machinery. Sampling the engine results or increasing caps would compromise stability or performance.

Files changed (10) +1235 / -67

Enhancement (4) +926 / -62
editor.jsShip the guided defect workflow and corrected overlays +454/-29

Ship the guided defect workflow and corrected overlays

• Generated browser distribution now strides through large defect lists, guides five independent captures, adapts gating to measured darkness, tallies recurring coordinates, and exports confirmed sites. It also reports overlay and truncation behavior more accurately.

dist/editor.js

engine.jsExpose frame medians through the distributed engine binding +9/-2

Expose frame medians through the distributed engine binding

• Expands the diagnosis statistics buffer from 21 to 24 values and returns per-plane medians used to classify covered and illuminated frames.

dist/engine.js

editor.jsAdd guided bad-pixel discovery and representative marking +454/-29

Add guided bad-pixel discovery and representative marking

• Adds a five-capture guided workflow with frame freshness checks, measured darkness classification, adaptive gating, 4-of-5 coordinate confirmation, progress guidance, and text export. Defect overlays now deterministically sample the full list instead of drawing its raster-ordered prefix, while panel messaging distinguishes detected, retained, and ringed counts.

src/editor.js

engine.jsExtend the diagnosis API with per-plane medians +9/-2

Extend the diagnosis API with per-plane medians

• Updates the JavaScript/WASM statistics ABI to 24 floats and exposes the three new median values in diagnosis results.

src/engine.js

Bug fix (2) +65 / -5
engine.wasmRebuild WebAssembly diagnosis engine +0/-0

Rebuild WebAssembly diagnosis engine

• Recompiled engine artifact includes per-plane median reporting and Clark-Evans normalization over the scan window actually represented by stored defects.

dist/engine.wasm

engine.cCorrect spatial normalization and report frame brightness +65/-5

Correct spatial normalization and report frame brightness

• Computes per-plane medians from existing histograms for covered-lens classification. Clark-Evans statistics now use the valid scan width and, after truncation, the raster window reached by the stored coordinate census rather than the entire frame.

src/engine.c

Tests (3) +225 / -0
ui-check.htmlCover representative rings and guided defect discovery +161/-0

Cover representative rings and guided defect discovery

• Adds browser tests proving capped rings span the picture and that displayed counts match the overlay. Exercises covered and lit five-capture runs, frame-derived advice, progress, 4-of-5 completion, and export availability.

tests/ui-check.html

smoke.mjsRegress truncated spatial statistics and frame medians +60/-0

Regress truncated spatial statistics and frame medians

• Verifies capped and uncapped scans produce consistent Clark-Evans classifications. Also checks synthetic and fixture medians against known raw levels.

tools/smoke.mjs

ui-check.mjsParameterize synthetic frame brightness +4/-0

Parameterize synthetic frame brightness

• Accepts a level query parameter when generating defect fixtures so UI tests can exercise both covered-lens and illuminated-scene paths.

tools/ui-check.mjs

Documentation (1) +19 / -0
README.mdDocument the guided bad-pixel workflow +19/-0

Document the guided bad-pixel workflow

• Explains five-capture confirmation, automatic covered-lens detection, scene-dependent advice, and why brightness gating must remain disabled for uniformly dark frames.

README.md

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Guided-run comment names wrong tally ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The introductory comment for startHunt() says the existing workflow requires knowing what 3 of 5
provides, while finishHunt() and the comparison panel both calculate a four-of-five threshold with
Math.ceil(M * 0.8). A maintainer comparing the guided and manual paths is therefore told that they
use different rules even though both implementations select four captures.
Code

src/editor.js[R1215-1217]

+	 * knowing to press Scan, knowing to choose Darkest, knowing to press Keep
+	 * five times and knowing what "3 of 5" buys. This drives those, and owns
+	 * no judgement of its own.
Evidence
Compliance rule 2 requires changed comments to agree with the current tree. The cited comment names
3 of 5, while the immediately following text and the implementation use 4 of 5.

CLAUDE.md: Use Verified and Traceable Claims in Repository Content: CLAUDE.md: Use Verified and Traceable Claims in Repository Content: CLAUDE.md: Use Verified and Traceable Claims in Repository Content: CLAUDE.md: Use Verified and Traceable Claims in Repository Content
src/editor.js[1215-1221]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The guided-run comment names a three-of-five tally even though both relevant implementations calculate four of five.
## Fix Focus Areas
- src/editor.js[1215-1221]
## Recommended Fix
Change `3 of 5` to `4 of 5`, then regenerate the corresponding distribution artifact.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Ring timing lacks measurement context ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The drawMarks() comment claims that ten thousand rings would take one second to construct without
identifying the browser, device, test input, or measurement method. Readers cannot reproduce or
distinguish this timing from an estimate when evaluating whether the cap remains appropriate on
current platforms.
Code

src/editor.js[R944-947]

+		/*
+		 * A cap on what is drawn, not on what is counted: a sensor with ten
+		 * thousand bad pixels would otherwise spend a second building circles
+		 * nobody can tell apart.
Evidence
Compliance rule 2 requires timings and other numerical claims to identify their measurement context.
The changed comment gives a one-second duration and input count but no environment or measurement
source.

CLAUDE.md: Use Verified and Traceable Claims in Repository Content: CLAUDE.md: Use Verified and Traceable Claims in Repository Content: CLAUDE.md: Use Verified and Traceable Claims in Repository Content: CLAUDE.md: Use Verified and Traceable Claims in Repository Content
src/editor.js[944-947]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The ring-rendering comment presents a one-second timing without the measurement context required for quantitative repository claims.
## Fix Focus Areas
- src/editor.js[944-947]
## Recommended Fix
Either document the browser, hardware, input size, and measurement method behind the timing or replace it with a non-quantitative explanation, then regenerate the distribution artifact.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Temperature advice lacks attribution ✓ Resolved 📘 Rule violation ≡ Correctness
Description
The guidance in buildHunt() states that dark current roughly doubles every 6–8 °C without citing
a measurement from this repository or an external source. The statement appears whenever a dark
capture has exposure metadata, so operators receive quantitative temperature advice whose
applicability to their sensor cannot be traced.
Code

src/editor.js[R1450-1453]

+						'more of them — take these at the exposure the camera will really ' +
+						'run at, or the list will describe a sensor you are not using. ' +
+						(hunt.dark ? 'Dark current also roughly doubles every 6–8 °C, so a ' +
+							'cold camera understates a warm one.' : ''),
Evidence
Compliance rule 2 requires externally sourced figures to be attributed and measured figures to name
their context. The user-facing text provides a 6–8 °C quantitative relationship but neither cites
a source nor ties it to the documented camera measurements.

CLAUDE.md: Use Verified and Traceable Claims in Repository Content: CLAUDE.md: Use Verified and Traceable Claims in Repository Content: CLAUDE.md: Use Verified and Traceable Claims in Repository Content: CLAUDE.md: Use Verified and Traceable Claims in Repository Content
src/editor.js[1450-1453]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The user-facing dark-current guidance gives a numerical temperature relationship without a repository measurement context or external attribution.
## Fix Focus Areas
- src/editor.js[1450-1453]
## Recommended Fix
Add an attributable source and qualify the sensor-dependent relationship, or remove the numerical range and retain only advice supported by the repository measurements; then regenerate the distribution artifact.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View action required (3)
4. Crowded sensors export incomplete maps ✓ Resolved 🐞 Bug ≡ Correctness
Description
finishHunt() rebuilds diag from the capped coordinate arrays and unconditionally sets
truncated to false, discarding each scan's overflow status and total count. When any capture
contains more than the default 4096 stored defects, candidates outside its raster-order prefix can
never enter the tally or export, yet the result is presented as complete.
Code

src/editor.js[R1314-1317]

+		const pts = [...tally].filter(([, c]) => c >= need)
+			.map(([k]) => { const [x, y] = k.split(','); return { x: +x, y: +y }; });
+		diag = { ...diag, defects: pts, defectCount: pts.length, truncated: false,
+			fromTally: { need, of: M } };
Evidence
The engine defaults to 4096 retained coordinates and explicitly distinguishes the total count from
the capped defects array. The new retention helper stores only that array, while finishHunt()
clears truncation and exportDefects() writes only the surviving retained coordinates.

src/engine.js[191-234]
src/editor.js[1309-1317]
src/editor.js[1333-1352]
src/editor.js[1674-1682]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The guided workflow tallies only the coordinates retained under the engine's defect cap, then clears the truncation flag and exports the partial result as a complete map. Sensors producing more than 4096 candidates can therefore lose valid defect coordinates without warning.
## Fix Focus Areas
- src/editor.js[1277-1296]
- src/editor.js[1309-1320]
- src/editor.js[1674-1682]
- src/engine.js[191-234]
## Recommended Fix
Preserve each capture's `truncated` and `defectCount` metadata in `scanSet`. Refuse to finish or export a guided run containing a truncated capture, and display actionable guidance to reduce candidates or otherwise obtain a complete coordinate set rather than setting `truncated` to false.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Five captures can produce no result ✓ Resolved 🐞 Bug ≡ Correctness
Description
keepCurrentScan() removes prior hunt entries by state.name, although capture names are optional
display labels and the guided run uses opened to distinguish successful frames. When a provider
reuses one filename, five accepted captures increment hunt.taken but collapse to one scanSet
entry, which then reaches finishHunt() and fails its two-vote tally threshold.
Code

src/editor.js[R1679-1681]

+		scanSet = scanSet.filter((v) => v.name !== state.name);
+		scanSet.push({ name: state.name || 'a frame',
+			keys: diag.defects.map((p) => p.x + ',' + p.y) });
Evidence
The capture contract does not require unique names, as demonstrated by the existing provider test
that always returns captured.dng, while the hunt treats each newly opened capture as distinct and
increments hunt.taken. However, the shared retention helper deletes scans with matching names, so
the final tally receives only one retained entry despite five successful captures and cannot meet
its minimum two-vote rule.

src/editor.js[211-224]
src/editor.js[1259-1279]
src/editor.js[1310-1317]
src/editor.js[1677-1682]
src/editor.js[4453-4461]
src/editor.js[1253-1296]
src/editor.js[1309-1317]
src/editor.js[1674-1682]
tests/ui-check.html[112-121]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The guided defect hunt must retain one scan per successfully opened capture, even when a capture provider reuses the same display filename. Deduplicating `scanSet` by `state.name` causes distinct frames to overwrite each other, leaving the five-frame tally unable to confirm defects.
## Fix Focus Areas
- src/editor.js[1242-1242]
- src/editor.js[1253-1296]
- src/editor.js[1277-1296]
- src/editor.js[1674-1682]
- src/editor.js[1677-1682]
## Recommended Fix
Give each successfully opened hunt frame a stable unique identity derived from the `opened` sequence and retain that identity with its scan, or append hunt scans directly without name-based replacement. Keep the filename only as a display label; separate the manual comparison replacement workflow from guided-hunt retention, deduplicating manual rescans by frame identity while ensuring `finishHunt()` receives all five guided capture sets.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Large scans still draw rings at the top 🐞 Bug ≡ Correctness
Description
drawMarks() strides through diag.defects, but a truncated diagnosis exposes only the capped
raster-order prefix ending near the top of the frame. When the defect count exceeds the storage cap,
the rings remain spatially biased even though the new caption says they are spread through the
frame, and the loop cannot reach rows represented only by the separate total count.
Code

src/editor.js[R960-962]

+		const step = markStep(diag.defects.length);
+		for (let i = 0; i < diag.defects.length; i += step) {
+			const d = diag.defects[i];
Evidence
The native scan stores coordinates only while found < max_defects, while continuing to count later
detections and documenting overflow as a census of the top window. JavaScript exposes that capped
prefix as diag.defects, alongside the total count and truncation flag, so striding over the list
cannot select coordinates from later rows even though the adjacent text claims the sample is spread
through the frame and separately acknowledges that the scan stopped early.

src/engine.c[1815-1823]
src/engine.js[225-234]
src/editor.js[960-962]
src/editor.js[1851-1862]
src/engine.c[1815-1824]
src/engine.c[1839-1866]
src/engine.js[218-234]
src/editor.js[960-963]
src/editor.js[1851-1870]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The stride samples only the stored defect array. On truncated scans, every available coordinate belongs to the first raster-order prefix near the top of the frame, so the resulting rings are not a spatially representative frame-wide sample.
## Fix Focus Areas
- src/editor.js[938-963]
- src/editor.js[1851-1870]
- src/engine.c[1815-1824]
## Recommended Fix
Keep the stable raster-order defect list used for cross-capture comparison, but do not present its truncated prefix as a frame-wide sample. Either produce a separate bounded, spatially distributed overlay sample during diagnosis and have `drawMarks()` use it when `diag.truncated` is true, or suppress defect rings for truncated diagnoses and replace the frame-wide caption with an explicit message that no representative overlay is available because only the scan's top prefix was retained. Continue using the stride overlay for complete, untruncated lists.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

7. Capped scans can still look clustered ✓ Resolved 🐞 Bug ≡ Correctness
Description
The capped-path area uses the full interior width through last_y, although the final stored raster
row contains points only up to the column where the storage limit was reached. When a limit lands
partway across a row, clark_evans receives more area than the retained prefix covers and lowers
the calculated index through its expected-distance calculation.
Code

src/engine.c[R1863-1866]

+            const int win_w = w - 4;
+            const int win_h = found > max_defects ? last_y - 1 : h - 4;
+            if (win_w >= 1 && win_h >= 1) {
+                clark_evans(defects, n, win_w, win_h, &r, &z);
Evidence
The scan walks coordinates in raster order and records last_y only when it stores a point. The new
calculation converts that row into a complete-width rectangle, while the spatial index derives its
expectation from the supplied area, causing a downward bias whenever the final row is partial.

src/engine.c[1529-1550]
src/engine.c[1736-1739]
src/engine.c[1815-1823]
src/engine.c[1863-1869]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The capped defect list ends at a particular raster coordinate, not necessarily at a row boundary. Computing its observation area as the full width times all rows through `last_y` overstates the area when the cap is reached before the final interior column, biasing the Clark-Evans result downward.
## Fix Focus Areas
- src/engine.c[1736-1739]
- src/engine.c[1815-1822]
- src/engine.c[1863-1866]
## Recommended Fix
Track the final stored x coordinate alongside `last_y`, then calculate the raster prefix area including only completed interior rows and the scanned portion of the final row. Pass dimensions or an explicit area to the spatial-index calculation so the expected distance uses that partial-row area; add a capped test whose limit falls partway through a row.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can choose which labels appear on a finding, and whether they show icons or text

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/editor.js
Comment thread src/editor.js Outdated
Comment thread src/editor.js Outdated
Comment thread src/editor.js Outdated
Comment thread src/editor.js Outdated
Comment thread src/editor.js
Comment on lines +960 to +962
const step = markStep(diag.defects.length);
for (let i = 0; i < diag.defects.length; i += step) {
const d = diag.defects[i];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

6. Large scans still draw rings at the top 🐞 Bug ≡ Correctness

drawMarks() strides through diag.defects, but a truncated diagnosis exposes only the capped
raster-order prefix ending near the top of the frame. When the defect count exceeds the storage cap,
the rings remain spatially biased even though the new caption says they are spread through the
frame, and the loop cannot reach rows represented only by the separate total count.
Agent Prompt
## Issue description
The stride samples only the stored defect array. On truncated scans, every available coordinate belongs to the first raster-order prefix near the top of the frame, so the resulting rings are not a spatially representative frame-wide sample.

## Fix Focus Areas
- src/editor.js[938-963]
- src/editor.js[1851-1870]
- src/engine.c[1815-1824]

## Recommended Fix
Keep the stable raster-order defect list used for cross-capture comparison, but do not present its truncated prefix as a frame-wide sample. Either produce a separate bounded, spatially distributed overlay sample during diagnosis and have `drawMarks()` use it when `diag.truncated` is true, or suppress defect rings for truncated diagnoses and replace the frame-wide caption with an explicit message that no representative overlay is available because only the scan's top prefix was retained. Continue using the stride overlay for complete, untruncated lists.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/engine.c Outdated
…m whole

Review of the three changes below this one found two claims the code could not
support, two more it could not quite support, and three numbers that should not
have been written down.

A kept scan was identified by the frame's display name. Nothing in the mount
contract requires a host to vary that -- the camera's own WebUI happens to
timestamp each capture, but a host handing back one filename would have had
five captures collapse into a single kept scan, and a guided run that ended
having confirmed nothing at all. Identity is the frame that was opened now.
The case the old key existed for, reading one frame twice, is itself an open,
so the open is the thing to count.

Deduplicating on the READING instead was tried first, on the grounds that two
captures of a real sensor never give byte-identical defect lists. They do not.
But two other things do, and the suite caught both: a fixture built to repeat
one view exactly, which is how the "these are following the picture" verdict is
tested, and any pair of captures that find nothing at all, whose empty lists
are equal. Each collapsed to a single capture and took the tally with it, and
three checks went red -- one of them a check that predates this work.

A run whose captures overflowed the engine's store was presenting a partial
answer as a complete one. A capture that fills the store has looked at the top
of its frame and nothing below, so candidates under the row it stopped on were
never candidates for the tally either, and the list is short by an unknown
number in a known place. finishHunt cleared the truncated flag while doing it.
The flag is carried through the kept scans now, and the run says what it could
not see, in the panel and in the exported file. It still finishes rather than
refusing: the sites it confirmed are confirmed, and a list with a caveat is
worth more than no list. 1024x1024 with 20000 asked for yields about 4800 found
against 4096 stored, which is what the new check runs on; 512x512 cannot be
made to overflow at all, because the planted spikes start landing on each other
and the count saturates at 3538 however many are asked for.

The caption made the same overstatement in miniature. The rings sample the
stored list evenly, but that list ends at the row the scan filled up on, so on
a truncated frame the bottom of the picture carries none of them and "spread
through the frame" was the wrong phrase. It says which part of the frame it
reached instead.

And the area handed to the spatial index counted the last stored row whole,
although the store fills partway along a row and the rest of it was never
examined. Counting area nobody looked at reads as a set more clustered than it
is -- the same mistake the area fix below was written to undo. The column is
recorded alongside the row and the area is now exact: whole rows above, plus
the part of the last one actually walked. clark_evans takes that area rather
than two sides, because it was never really about a rectangle. The effect is
small, as expected: the capped fixture reads 1.087 either way.

The three numbers:

- The comment introducing the run said an operator had to know what "3 of 5"
  buys. Both implementations compute four of five.
- The overlay's cap was justified by ten thousand rings costing a second to
  build. Measured in headless Chromium on this desktop x86, median of seven:
  89.9 ms for ten thousand, 47.0 ms for 4096, 6.2 ms for 600. So the cap is
  right and the reason was not -- past a few hundred the rings merge into a
  wash that hides the picture, which is what it says now.
- The dark-frame advice told operators that dark current roughly doubles every
  6-8 degrees. Nothing here measured that. The advice keeps the part that is
  supportable -- take the map where the camera lives -- and drops the number.

Both new guards were watched failing with their fix reverted.
"Reading a plate here" asks three questions about one licence plate: how many
pixels across it is, and how much blur it survives before it stops reading.
Those are questions about the picture. Diagnose is about the sensor's own
health -- hot pixels, the black level the frame implies, what has clipped --
and the card has sat there since it was written, telling the reader to go to a
different tab and pick something before it could say anything at all.

The guided defect run went in above it and made the misfiling plain: the card
landed between the steps of a sequence somebody is meant to follow and the
result that sequence produces. Asked what it had to do with defect pixels, the
honest answer was nothing.

It moves to Plates, directly under the list the plate is chosen from, where
"pick one of the plates above" is an instruction the reader can act on without
leaving the panel. Choosing a different candidate now refreshes it in place --
it is refilled rather than rebuilt, so selecting a plate does not stack a
second copy of the card underneath the first -- and clearing the selection to
search again puts it back to its empty state.

Nothing about what it measures changed. The checks that placed it were pointed
at the tab it is on now, and one was added for the tab it is not on, because
"the card exists somewhere in the inspector" would have passed either way.
@openipc-ai
openipc-ai merged commit abc6c66 into main Sep 24, 2026
1 check passed
@openipc-ai
openipc-ai deleted the defect-marks-sample branch September 24, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant