Skip to content

focus: a grid you can read, and the zones that focus somewhere else - #37

Merged
widgetii merged 2 commits into
mainfrom
focus-zones
Sep 25, 2026
Merged

widgetii merged 2 commits into
mainfrom
focus-zones

Conversation

@widgetii

Copy link
Copy Markdown
Member

First feedback from someone actually calibrating autofocus with this:

пока не понятно, нужны цифры — меньше зон, зато в зоне рисовать цифру

Fair. 255 coloured cells show that there is a bright patch somewhere and say nothing about what any of it reads.

A readable grid

The readout is now 3×3 by default with the value drawn in each block, digits grouped (50 625) so five figures read at a glance, the sharpest block outlined and labelled. 3×3 / 4×4 / All zones switches it.

The fine grid is still what gets measured — this only changes what is drawn over the picture, so the readout cannot move where the peak actually is.

Three choices worth stating:

  • Mean, not sum. 17 columns do not divide by three; blocks come out 5, 6 and 6 wide, and a sum ranks the wider block higher for being wider.
  • Not the maximum, which is the reduction clamping already defeats (focus: a reading that cannot rise is not a measurement #34).
  • A block with nothing believable in it is null, not 0. Zero is a value, and it sorts below every real block as though it had been measured.

The zones that focus somewhere else

The same conversation named the real failure of cheap autofocus — after a year it focuses anywhere but the subject, and dirt on the glass the owner never notices is a large part of why:

через год у всех фокусируется куда угодно, кроме нужного … это связано с грязью на стекле, которую неподготовленный юзер может даже не заметить

That turns out to be measurable with what the sweep already collects. Dirt sits millimetres from the lens, so across a defocus sweep it peaks at a lens position nowhere near the rest of the frame. The sweep reads every zone at every position and was throwing all but the maximum away.

Each zone's own peak position is now kept, the consensus taken as the median — one smeared corner peaking at the far end drags a mean toward itself and then judges everything else against a consensus it invented — and zones far from it are ringed on the picture and named in the verdict.

Two deliberate restraints:

  • A zone that does not move across the sweep gets no opinion. A blank wall has an argmax and it is noise; asking it where it focuses returns an answer indistinguishable from a confident one.
  • It is reported as "focuses at a different distance", not as dirt. A genuinely near object gives the same signature and is not a fault. Naming the cause is the operator's job; knowing where to look is what they could not get from a live image, where a smear reads as nothing but a soft patch.

Also

sweepSettleMs becomes a host seam beside intervalMs and moveRepeatMs. A fixture has nothing to settle, and at 700 ms the sweep checks alone outran the page's whole 180 s budget.

Tests

Six new assertions in tools/smoke.mjs, thirteen in tests/ui-check.html. Every guard mutation-tested — breaking it turns exactly the checks written for it red:

break red
a flat zone gets an opinion a zone that never moved is not accused; not counted as having an opinion
consensus by mean, not median the scene agrees where focus is; the consensus is the median
block value is the sum blocks of different widths still compare
more blocks than zones allowed 4 checks
odd blocks not ringed and it is ringed on the picture
odd zones not reported called out; and the operator is told what it usually is

Also asserts the negative direction: a scene that all sits at one distance is not accused, because a warning that fires on every sweep is a warning nobody reads.

node tools/smoke.mjs and node tools/ui-check.mjs both pass. Running on an 85H50AI as a development build via MJ_RAW_BASE.

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

Copy link
Copy Markdown

PR Summary by Qodo

Add readable focus grids and sweep distance outlier detection

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Adds selectable numeric focus grids with mean values and sharpest-block highlighting.
• Detects and rings zones whose sweep peaks differ from the median scene distance.
• Makes sweep settling configurable and expands unit and browser regression coverage.
Diagram

graph TD
  CAM["AF Zone Grid"] --> SUM["Zone Summary"] --> FRAMES["Sweep Frames"] --> ANALYZE["Peak Analysis"] --> FINDINGS["Sweep Findings"] --> OVERLAY["Focus Overlay"]
  SUM --> COARSE["Block Means"] --> OVERLAY
  FINDINGS --> STATUS["Operator Verdict"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Cluster zone peak positions
  • ➕ Could model scenes containing several legitimate subject distances
  • ➕ Could identify multiple focus-depth groups instead of one consensus and outliers
  • ➕ Could attach confidence to each detected depth group
  • ➖ Adds tuning and interpretation complexity for small zone populations
  • ➖ May obscure the simple operator question of where to inspect the image
  • ➖ Requires substantially broader real-camera validation

Recommendation: Keep the PR's median-consensus approach for this iteration. It is robust against isolated extreme zones, suppresses flat or unmeasured regions, and produces an understandable advisory rather than claiming a specific fault. Peak clustering is worth considering later if real-world sweeps show that naturally multi-depth scenes create excessive warnings.

Files changed (8) +805 / -19

Enhancement (6) +596 / -14
aftune.jsAdd grid aggregation and sweep outlier analysis to the distribution +134/-0

Add grid aggregation and sweep outlier analysis to the distribution

• Exposes per-zone saturation state, reduces measured zones into mean-valued display blocks, and identifies meaningful per-zone sweep peaks that differ from the median consensus. Invalid block counts, changing grid shapes, short sweeps, and flat zones are guarded explicitly.

dist/aftune.js

editor.cssStyle numeric blocks and distance-outlier rings +11/-0

Style numeric blocks and distance-outlier rings

• Adds legible stroked text for focus values and sharpest-block labels. Defines a distinct dashed blue outline for blocks containing sweep distance outliers.

dist/editor.css

editor.jsIntegrate readable grids and sweep findings into the distributed editor +153/-7

Integrate readable grids and sweep findings into the distributed editor

• Adds 3×3, 4×4, and full-zone display controls while retaining the fine grid for measurement. Complete sweep frames are analyzed for distance outliers, rendered over the image, included in the verdict, and paced through a host-configurable settle delay.

dist/editor.js

aftune.jsImplement display coarsening and per-zone sweep analysis +134/-0

Implement display coarsening and per-zone sweep analysis

• Adds mean-based block aggregation with null handling for unmeasurable blocks and caps requested blocks to the measured grid. Adds median-based peak-position analysis that ignores insufficiently responsive zones and reports zones substantially separated from scene consensus.

src/aftune.js

editor.cssAdd focus value, peak label, and outlier presentation styles +11/-0

Add focus value, peak label, and outlier presentation styles

• Makes numeric SVG labels readable over varying image brightness and visually separates sharpest-block outlines from distance-outlier rings.

src/editor.css

editor.jsRender selectable numeric focus grids and flag sweep outliers +153/-7

Render selectable numeric focus grids and flag sweep outliers

• Connects block aggregation and sweep analysis to the focus workflow, defaulting the overlay to a readable 3×3 grid. It preserves complete sweep frames, rings suspect areas, explains findings to operators, clears stale findings, and supports configurable sweep settling.

src/editor.js

Tests (2) +209 / -5
ui-check.htmlCover readable grids, outlier warnings, and configurable sweep timing +135/-5

Cover readable grids, outlier warnings, and configurable sweep timing

• Adds browser checks for numeric grid modes, digit grouping, sharpest-block placement, distance-outlier rings and verdicts, and false-positive suppression. Test fixtures now shorten sweep settling while lifecycle tests retain enough delay to interrupt active sweeps reliably.

tests/ui-check.html

smoke.mjsTest focus block reduction and sweep consensus rules +74/-0

Test focus block reduction and sweep consensus rules

• Adds focused assertions for unequal block dimensions, null unmeasurable values, block-count validation, median consensus, flat-zone exclusion, outlier identification, and short-sweep rejection.

tools/smoke.mjs

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

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

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Exact-zone view hides anomaly rings ✓ Resolved 🐞 Bug ≡ Correctness
Description
drawFocusMarks returns through drawCoarse only when focusBlocks is nonzero, while the
full-grid branch creates ordinary cells and the peak rectangle without applying focusOdd.
Selecting “All zones” sets focusBlocks to zero and therefore removes every anomaly ring even when
the sweep verdict says the affected zones are ringed on the picture, preventing the exact flagged
zones from being identified.
Code

src/editor.js[R3994-3997]

+		if (focusBlocks) {
+			drawCoarse(svg, s, W, H, NS);
+			focusMarks.append(svg);
+			return;
Evidence
The selector explicitly sets focusBlocks to zero for “All zones,” causing drawFocusMarks to
bypass the coarse renderer. Only the coarse renderer checks focusOdd and appends re-fz-odd
rectangles; the full-grid loop draws normal cells and the peak outline without an equivalent anomaly
marker.

src/editor.js[3937-3949]
src/editor.js[3994-4038]
src/editor.js[3850-3864]
src/editor.js[3994-4025]

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 exact-zone focus view does not render the anomaly indices stored in `focusOdd`, so selecting “All zones” hides the sweep findings and prevents the detailed view from showing the zones named in the sweep warning.
## Fix Focus Areas
- src/editor.js[3937-3949]
- src/editor.js[3994-4038]
- dist/editor.js[3994-4038]
## Recommended Fix
Add anomaly-ring rendering to the full-grid path after each cell is drawn, using the same `focusOdd` membership check as the coarse-grid renderer. For every matching index, append an inset `re-fz-odd` rectangle aligned to the exact individual zone bounds. Keep the existing coarse-block rendering unchanged and synchronize the source and distribution files.

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


2. Saturated zones trigger false warnings ✓ Resolved 🐞 Bug ≡ Correctness
Description
sweepRun discards each frame’s satZone metadata, while sweepZones accepts every zone with
state === 'measured' and selects its first maximum without knowing whether the accumulator was
ceiling-clamped. Because summarise intentionally keeps saturation separate from state, a
saturated response plateau remains eligible for consensus and can make the first ceiling value look
like a distinct focus position, causing the zone to be ringed and reported as focusing elsewhere.
Code

src/editor.js[R3419-3422]

+					 * distance that part of the frame is at. Throwing all but
+					 * the maximum away discards the only measurement that can
+					 * see dirt on the glass. */
+					frames.push({ fv: s.fv, state: s.state });
Evidence
summarise records saturation separately because a ceiling-clamped accumulator has stopped
measuring, while state represents only luma/highlight validity and can therefore remain
measured. The sweep retains only fv and state, then selects the first strictly greatest value
among measured readings, so the anomaly algorithm has no saturation metadata with which to reject a
clipped, unreliable curve.

src/aftune.js[49-64]
src/aftune.js[139-157]
src/editor.js[3416-3422]
src/aftune.js[309-321]
src/aftune.js[70-76]
src/aftune.js[126-148]
src/editor.js[3413-3422]

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

## Issue description
Per-zone focus-distance detection loses saturation metadata and treats ceiling-clamped zone readings as measurable peak positions. A saturation plateau can therefore make the selected argmax depend on where the counter first reached its ceiling rather than where optical focus occurred.
## Fix Focus Areas
- src/aftune.js[141-148]
- src/aftune.js[309-321]
- src/editor.js[3416-3422]
- dist/aftune.js[309-321]
- dist/editor.js[3416-3422]
## Recommended Fix
Carry each frame’s per-zone saturation flags into the sweep input. Exclude a zone from peak-position consensus and suspicion when it is saturated at any relevant sampled position, or explicitly mark its peak position unavailable, while preserving the existing overall saturation warning separately. Add coverage for a saturated non-global zone that would otherwise appear as an outlier, then synchronize the source and distribution files.

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


3. Old focus warnings remain after changes ✓ Resolved 🐞 Bug ≡ Correctness
Description
focusOdd is populated after a sweep but survives the existing resetFocusState lifecycle, despite
representing anomalies from the prior lens, scene, and filter state. Opening a new frame, moving the
lens manually, or applying or reverting a focus filter can leave those indices active, and
subsequent polling applies them to current grids from incompatible measurements.
Code

src/editor.js[3911]

+	let focusOdd = null;
Evidence
The cited state handling populates focusOdd around sweep execution, while scene reset clears only
prior focus values and filter application resets only held peaks. Manual movement and filter changes
therefore alter the focus conditions without invalidating the anomaly indices, and the polling
renderer continues applying every surviving index directly to subsequently received grids.

src/editor.js[3403-3405]
src/editor.js[3473-3478]
src/editor.js[3177-3189]
src/editor.js[3709-3714]
src/editor.js[3937-3949]
src/editor.js[3029-3031]
src/editor.js[3054-3064]
src/editor.js[3698-3707]
src/editor.js[4224-4232]
src/editor.js[3937-3941]

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

## Issue description
Sweep anomaly indices remain active after the scene, lens position, or focus filter changes, so later grids can display rings derived from incompatible measurements.
## Fix Focus Areas
- src/editor.js[3029-3031]
- src/editor.js[3403-3405]
- src/editor.js[3677-3714]
- src/editor.js[3909-3911]
- dist/editor.js[3029-3031]
- dist/editor.js[3698-3707]
- dist/editor.js[3909-3911]
## Recommended Fix
Create a small invalidation helper that clears `focusOdd` and redraws the focus overlay when appropriate. Use it inside the common focus-state reset, before successful manual lens movement begins, and whenever filter application or reversion invalidates prior focus measurements, alongside the existing held-focus reset. Retain the sweep-start clearing as a defensive reset and keep the distribution build synchronized.

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



Remediation recommended

4. Changed grid shapes misplace warnings ✓ Resolved 🐞 Bug ☼ Reliability
Description
sweepZones calls a grid stable when every frame has the same fv.length, while sweepRun drops
each frame’s row and column dimensions. If the camera changes between two shapes with equal zone
counts, suspect indices pass validation and are later converted through the current column count
into different picture locations.
Code

src/aftune.js[R294-297]

+	const n = frames[0].fv.length;
+	for (const f of frames)
+		if (f.fv.length !== n)
+			throw new Error('the grid changed shape during the sweep');
Evidence
The new validation checks only array length, despite spatial identity depending on the row-major
shape. Production frames currently omit dimensions, and rendering derives each stored index’s row
from the current s.cols.

src/aftune.js[294-297]
src/editor.js[3416-3422]
src/editor.js[3937-3941]

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

## Issue description
Sweep validation compares only zone counts, allowing equal-sized grids with different row and column layouts to be combined and spatially misinterpreted.
## Fix Focus Areas
- src/aftune.js[294-297]
- src/editor.js[3416-3422]
- src/editor.js[3937-3941]
- dist/aftune.js[294-297]
- dist/editor.js[3416-3422]
## Recommended Fix
Store `rows` and `cols` with every sweep frame and reject the analysis unless both dimensions match the first frame. Ensure drawing only consumes findings whose dimensions match the current live summary, and synchronize source and distribution files.

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


5. Zero settle delay still waits 700 ms ✓ Resolved 🐞 Bug ➹ Performance
Description
settleMs uses a truthiness fallback, so a configured sweepSettleMs: 0 is indistinguishable from
an unset option and is replaced by SWEEP_SETTLE_MS, the 700 millisecond production delay.
Fixtures, hosts, or motors with no settling requirement therefore still wait after every successful
outward and return movement.
Code

src/editor.js[R3353-3357]

+	/* How long the lens is left alone after a step before the grid is read. A
+	 * host seam like intervalMs and moveRepeatMs: a motor that settles faster
+	 * than this is being waited on for nothing, and a test driving a fixture
+	 * has nothing to settle at all. */
+	const settleMs = () => (focus && focus.sweepSettleMs) || SWEEP_SETTLE_MS;
Evidence
The adjacent contract identifies fixtures with no settling requirement, but the helper's truthiness
fallback makes 0 || 700 evaluate to 700 rather than preserving zero. Both sweep directions invoke
the helper after each successful physical step, so the unintended default delay is multiplied across
the operation.

src/editor.js[3353-3357]
src/editor.js[3433-3438]
src/editor.js[3451-3454]
src/editor.js[3351-3357]
src/editor.js[3435-3438]

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

## Issue description
A configured `sweepSettleMs` value of zero is ignored because the truthiness fallback replaces it with the default 700 millisecond delay, preventing the settle-delay seam from representing no delay.
## Fix Focus Areas
- src/editor.js[3353-3357]
- src/editor.js[3435-3438]
- src/editor.js[3451-3454]
- dist/editor.js[3353-3357]
- tests/ui-check.html[2110-2114]
## Recommended Fix
Default `focus.sweepSettleMs` only when it is absent by using nullish handling or an explicit undefined/null check rather than truthiness, so zero is honored while retaining the existing 700 millisecond default. Validate that configured delays are finite nonnegative numbers, or clamp invalid negative values if necessary; add a test proving zero is honored on both outward and return steps, and synchronize the distribution file.

ⓘ 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 enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/editor.js
Comment thread src/editor.js Outdated
Comment thread src/aftune.js
Comment thread src/editor.js Outdated
Comment thread src/editor.js
@widgetii

Copy link
Copy Markdown
Member Author

All five were real and are fixed in 6702653.

1 — Exact-zone view hides anomaly rings. Correct, and the most embarrassing of the five: "All zones" is the view someone switches to precisely to see which zones were flagged, and it dropped every ring while the verdict went on saying they were on the picture. Both renderers now share one oddRing.

2 — Saturated zones trigger false warnings. Correct, and the subtle one. A clamped zone plateaus at the ceiling, so the first reading to reach it wins the argmax — and where the counter filled up is not where the lens was sharpest. The zone would be ringed as "focuses somewhere else" on the strength of an artefact. You are right that state cannot carry this: summarise keeps saturation separate deliberately, because a pinned zone is still perfectly well exposed. Frames now carry satZone, and a zone clamped anywhere along the sweep gets no peak position at all.

3 — Old warnings remain after changes. Correct. They were cleared when a sweep started and nowhere else, so opening a frame, driving the lens by hand, or applying/reverting a filter each left rings describing a camera that no longer existed. Cleared in all four paths.

4 — Changed grid shapes misplace warnings. Correct. Every index is read back as a row and column through the current width. Frames carry rows/cols, a change mid-sweep is refused, and the finding carries its own shape so drawing refuses to place it on a grid it does not match.

5 — Zero settle still waits 700 ms. Correct — 0 || 700. Now !== undefined.

On the tests

Two of these guards came back vacuous on the first mutation pass and needed real tests written before they meant anything:

  • nothing covered a new frame clearing the findings;
  • nothing covered sweepSettleMs: 0 — and the test fixture contained the same truthiness bug as the code under test (opts.settle || 30), so passing 0 silently became 30. Fixed in the fixture too.

Seven more checks. All five mutations now discriminate:

break red
full grid drops the rings and still ringed with every zone shown
a clamped zone still judged given no peak position; not accused of focusing elsewhere
findings survive a new frame and a new frame drops them
shape change allowed a grid that changed shape mid-sweep is refused
a zero settle falls back a settle of zero is a value, not an absence

Also asserts that the rest of the frame still reaches consensus when a zone is dropped for clamping — without that, the fix for #2 could quietly disable the whole feature and every other test would still pass.

node tools/smoke.mjs and node tools/ui-check.mjs both pass.

First feedback from someone calibrating autofocus with this: "пока не
понятно, нужны цифры -- меньше зон, зато в зоне рисовать цифру". Fair. 255
coloured cells show that there is a bright patch somewhere and say nothing
about what any of it reads.

The readout is now 3x3 by default with the value drawn in each block, digits
grouped so five figures can be read at a glance, the sharpest block outlined
and labelled. 3x3 / 4x4 / All zones switches it. The fine grid is still what
gets measured -- this only changes what is drawn over the picture, so the
readout cannot move where the peak actually is.

Block value is the MEAN over measured zones, not the sum: 17 columns do not
divide by three, and a sum ranks the wider block higher for being wider.
Not the maximum either, which is the reduction clamping already defeats. A
block with nothing believable in it is null, not zero -- zero is a value,
and it sorts below every real block as though it had been measured.

The same conversation named the real failure of cheap autofocus: after a
year it focuses anywhere but the subject, and dirt on the glass the owner
never notices is a large part of why. That turns out to be measurable with
what the sweep already collects. Dirt sits millimetres from the lens, so
across a defocus sweep it peaks at a lens position nowhere near the rest of
the frame. The sweep reads every zone at every position and was throwing all
but the maximum away.

Each zone's own peak position is now kept, the consensus taken as the median
-- one smeared corner peaking at the far end drags a mean toward itself and
then judges everything else against a consensus it invented -- and zones far
from it are ringed on the picture and named in the verdict.

Two restraints. A zone that does not move across the sweep gets no opinion:
a blank wall has an argmax and it is noise. And it is reported as focusing
at a different distance, not as dirt -- a genuinely near object gives the
same signature and is not a fault. Naming the cause is the operator's job;
knowing where to look is what they could not get from a live image, where a
smear reads as nothing but a soft patch.

Also makes the sweep settle a host seam (sweepSettleMs) beside intervalMs
and moveRepeatMs. A fixture has nothing to settle, and at 700ms the sweep
checks alone outran the page's whole time budget.

Every guard mutation-tested: breaking it turns exactly the checks written
for it red, and no others.
Five from review, all real.

"All zones" dropped every ring -- the one view someone switches to precisely
to see WHICH zones were flagged, while the verdict went on saying they were
on the picture. Both renderers now share the ring drawing.

A clamped zone was still being judged. Saturation plateaus at the ceiling,
so the FIRST reading to reach it wins the argmax, and where the counter
filled up is not where the lens was sharpest: the zone got accused of
focusing somewhere else on the strength of an artefact. `state` cannot carry
this -- summarise keeps saturation separate on purpose, because a pinned
zone is still perfectly well exposed -- so the frames now carry satZone and
a zone clamped anywhere along the sweep gets no opinion at all.

Findings outlived their conditions. They were cleared when a sweep started
and nowhere else, so opening a frame, driving the lens by hand, or applying
or reverting a filter each left rings describing a camera that no longer
existed.

Shape, not just zone count. Every index is read back as a row and a column
through the CURRENT width, so two grids of equal size and different shape
put the same index somewhere else in the picture. Frames carry rows and
cols, a change mid-sweep is refused, and the finding carries its shape so
drawing will not place it on a grid it does not match.

sweepSettleMs: 0 waited the full 700ms. `0 || 700` -- zero is a real answer,
a host with nothing to settle, and the fallback turned it into the
production wait twice per step in exactly the case that asked for none.

Seven more checks, including that the rest of the frame still reaches
consensus when a zone is dropped for clamping: without it the fix could
quietly disable the whole feature and every test would still pass. The
fixture had the same truthiness bug as the code under test, which is why
the settle guard first came back vacuous.
@widgetii
widgetii merged commit b82fa5c into main Sep 25, 2026
1 check passed
@widgetii
widgetii deleted the focus-zones branch September 25, 2026 05:24
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