Skip to content

focus: a low number can mean nothing to focus on, and says which - #38

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

widgetii merged 2 commits into
mainfrom
focus-nodetail

Conversation

@widgetii

Copy link
Copy Markdown
Member

Second round of feedback from the same calibration session:

теперь классическая проблема — на 9 квадрате не нашлось резких объектов и ему маленькую цифру дали

He is right, and it is the classic one. A blank wall, a patch of sky, a smooth painted door all report a small focus value wherever the lens is, because these statistics measure detail and there is none there to measure. Shown as a bare small number it reads as "this part of the picture is soft", and the operator goes chasing focus that was never the problem.

The test

A zone with something in it responds when the lens moves; a zone without one does not. That is the whole test — and peakHold already had half of it. It tracked each zone's best, so it now tracks the low as well, and swing becomes a live statistic accumulated as the barrel turns rather than something needing a sweep.

Nothing is claimed until the frame as a whole has moved. "We have not looked yet" and "there is nothing there" are different answers and only one of them is the operator's problem, so before that every block is unknown.

What it shows

An empty block keeps its number — it is what the camera reported — but dimmed and captioned "nothing to focus on", so it stops carrying the weight of a verdict.

It can never be named the sharpest, either: that points the operator at the one part of the frame which can never answer. Unless nothing has detail at all, in which case the plain maximum stands rather than naming no block on a frame that may simply not have been swept yet.

One textured corner is enough, so a block is only called empty when every zone in it that could be measured agrees.

And from the sweep

sweepZones now reports why a zone had no opinion — responded / flat / pinned / unmeasured — rather than dropping all four into the same null. flat is this same finding taken from the one measurement where the lens definitely moved.

Two of my own mistakes, both caught by the tests

  • zoneDetail was handed the holder rather than the push result, so it saw no record at all.
  • coarsen read a missing entry as known.

Between them those captioned all nine blocks "nothing to focus on" on a frame nobody had swept. Only the two real answers count as knowing now, and there are tests for a short array and for no array at all.

Tests

Nine assertions in tools/smoke.mjs, five in tests/ui-check.html. Every guard mutation-tested:

break red
claim before the lens moved before the lens moves, nothing is claimed (×2)
nothing ever called empty 4 checks
an empty block can be sharpest an empty block is never the sharpest
a stray entry counts as known short array / no array leave blocks unknown
no caption on an empty block says so rather than scoring low; number stays dimmed

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

Second round of feedback from the same calibration: "теперь классическая
проблема - на 9 квадрате не нашлось резких объектов и ему маленькую цифру
дали".

He is right, and it is the classic one. A blank wall, a patch of sky, a
smooth painted door all report a small focus value wherever the lens is,
because these statistics measure detail and there is none there to measure.
Shown as a bare small number it reads as "this part of the picture is soft",
and the operator goes chasing focus that was never the problem.

A zone with something in it RESPONDS when the lens moves; a zone without one
does not. That is the whole test, and peakHold already had half of it -- it
tracked each zone's best, so it now tracks the low as well and swing becomes
a live statistic, accumulated as the barrel turns rather than needing a
sweep.

Nothing is claimed until the frame AS A WHOLE has moved. "We have not looked
yet" and "there is nothing there" are different answers and only one of them
is the operator's problem, so before that every block is unknown.

An empty block keeps its number -- it is what the camera reported -- but
dimmed and captioned, so it stops carrying the weight of a verdict. It can
never be named the sharpest either: that points the operator at the one part
of the frame which can never answer. Unless nothing has detail at all, when
the plain maximum stands rather than naming no block on a frame that may
simply not have been swept yet.

sweepZones now reports WHY a zone had no opinion -- responded, flat, pinned,
unmeasured -- rather than dropping all four into the same null. `flat` is
this same finding taken from the one measurement where the lens definitely
moved.

Two of my own mistakes, both caught by the tests: zoneDetail was handed the
holder rather than the push result, so it saw no record at all; and coarsen
read a missing entry as KNOWN, which between them captioned all nine blocks
"nothing to focus on" on a frame nobody had swept. Only the two real answers
count as knowing now.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Distinguish detail-free zones from soft focus

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Classifies zones by focus response only after meaningful lens movement.
• Dims and captions detail-free blocks while excluding them from sharpest selection.
• Reports sweep outcomes explicitly and adds unit and UI regression coverage.
Diagram

graph TD
  A["Camera zones"] --> B["Frame summary"] --> C["Peak history"] --> D["Detail classifier"] --> E["Coarse blocks"] --> F["Focus overlay"]
  B --> G["Sweep analysis"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Classify only after an explicit sweep
  • ➕ Uses a known lens traversal for stronger confidence.
  • ➕ Could reuse sweep results directly without live-history thresholds.
  • ➖ Delays feedback until a complete sweep finishes.
  • ➖ Requires explicit sweep lifecycle integration for the live focus display.
2. Measure image texture directly
  • ➕ Can identify edge-free regions without waiting for lens movement.
  • ➕ Separates scene texture from focus response more directly.
  • ➖ Requires pixel data or additional camera statistics not currently available.
  • ➖ Introduces a separate metric that may disagree with hardware focus zones.

Recommendation: Retain the PR's response-swing approach. It reuses existing focus statistics and peak history, provides live feedback during manual focusing, and conservatively preserves unknown until overall movement makes classification meaningful. Explicit sweep reasons remain a useful complementary diagnostic rather than the sole classifier.

Files changed (8) +354 / -38

Enhancement (2) +8 / -0
editor.cssDim non-actionable focus readings +4/-0

Dim non-actionable focus readings

• Adds the distributable opacity style used to visually de-emphasize readings from blocks with nothing to focus on.

dist/editor.css

editor.cssStyle detail-free focus labels as muted +4/-0

Style detail-free focus labels as muted

• Adds a reusable opacity class so low readings in detail-free blocks remain visible without appearing authoritative.

src/editor.css

Bug fix (4) +246 / -38
aftune.jsAdd distributed detail-response classification +101/-15

Add distributed detail-response classification

• Mirrors the source focus-analysis changes in the distributable module. Peak history now retains lows, coarse blocks carry detail states, sweeps report outcome reasons, and 'zoneDetail' classifies response only after sufficient movement.

dist/aftune.js

editor.jsRender detail-free focus blocks explicitly +22/-4

Render detail-free focus blocks explicitly

• Integrates detail classification into the distributed editor. Empty blocks retain a dimmed numeric reading, receive a “nothing to focus on” caption, and cannot normally receive the sharpest marker.

dist/editor.js

aftune.jsClassify focus-zone detail from response swing +101/-15

Classify focus-zone detail from response swing

• Extends peak hold with per-zone and overall low records, then introduces 'zoneDetail' to distinguish responsive, flat, and unknown zones. Coarsening propagates detail states and avoids selecting empty blocks, while sweep analysis now explains missing opinions as responded, flat, pinned, or unmeasured.

src/aftune.js

editor.jsAnnotate focus blocks without measurable detail +22/-4

Annotate focus blocks without measurable detail

• Passes accumulated focus records through 'zoneDetail' before coarse aggregation. The SVG overlay dims and captions empty blocks while preserving their reported values and eligible sharpest-block behavior.

src/editor.js

Tests (2) +100 / -0
ui-check.htmlVerify detail-free focus overlay behavior +50/-0

Verify detail-free focus overlay behavior

• Adds browser-level coverage confirming no premature captions, correct empty-block annotation, exclusion from sharpest selection, and retention of dimmed numeric values after lens movement.

tests/ui-check.html

smoke.mjsCover focus-detail classification edge cases +50/-0

Cover focus-detail classification edge cases

• Adds smoke assertions for unknown pre-movement states, responsive and flat zones, block aggregation, missing or short detail arrays, and sharpest-block fallback behavior.

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. Focus labels move to the wrong blocks ✓ Resolved 🐞 Bug ≡ Correctness
Description
peakHold.push() checks only sum.fv.length and clears only best and low on a mismatch, so it
neither identifies equal-count row/column reshapes nor clears the retained bestOverall and
lowOverall movement history. When polling supplies a differently shaped grid, zoneDetail() can
classify current zones from records tied to the previous spatial layout, and drawCoarse() maps
those details through the new dimensions when captioning regions and selecting the sharpest blocks.
Code

src/aftune.js[R174-177]

+			if (!best || best.length !== sum.fv.length) {
+				best = sum.fv.map(() => null);
+				low = sum.fv.map(() => null);
+			}
Evidence
The detail feature consumes history with no grid-shape metadata, and the polling path pushes each
new summary without checking whether its rows and columns match the held layout. The mismatch branch
clears only the per-zone best and low arrays while retaining the overall extrema used by the
movement gate, and the existing sweep path confirms that both dimensions are required to identify
spatial records because equal-length grids with different shapes map the same indices to different
frame regions.

src/aftune.js[174-177]
src/aftune.js[195-197]
src/aftune.js[430-437]
src/aftune.js[257-270]
src/editor.js[4017-4021]
src/editor.js[4906-4907]
src/editor.js[4884-4891]
src/aftune.js[170-200]
src/aftune.js[332-345]
src/editor.js[3994-4020]

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

## Issue description
`peakHold()` retains focus and movement history when a camera changes from one `(rows, cols)` shape to another with the same total zone count. The retained indices then refer to different spatial regions, but `zoneDetail()` returns classifications based on the previous layout and `coarsen()` interprets them using the new shape.
## Fix Focus Areas
- src/aftune.js[170-200]
- dist/aftune.js[170-200]
- src/editor.js[3994-4021]
- src/editor.js[4906-4907]
## Recommended Fix
Track the held grid's `rows` and `cols` inside `peakHold`. Before accepting a new summary, reset `best`, `low`, `bestOverall`, and `lowOverall` whenever either dimension changes, rather than relying only on the flattened zone count. Rebuild the distribution artifact so `dist/aftune.js` has the same behavior, and add regression tests for both changed zone counts and equal-count grids with different shapes.

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



Remediation recommended

2. Zero-value blocks stay unexplained ✓ Resolved 🐞 Bug ≡ Correctness
Description
zoneDetail() leaves a zone unknown whenever its held maximum is zero, even though peak holding
explicitly treats zero as a valid measured reading. After another zone proves that the lens moved, a
lit measured zone that consistently reports zero therefore receives neither the dimming nor the
“nothing to focus on” caption.
Code

src/aftune.js[R435-437]

+		const hi = hold.best[i], lo = hold.low[i];
+		if (hi === null || lo === null || hi <= 0) continue;
+		out[i] = (hi - lo) / hi < detail ? 'none' : 'some';
Evidence
Summary validation permits zero accumulator values, zone state is determined from luma and clipping
rather than focus response, and the holder's own comment distinguishes a zero reading from no
reading. Nevertheless, the new classifier skips all hi <= 0 records, preventing the editor from
receiving none for a valid stable-zero zone.

src/aftune.js[107-125]
src/aftune.js[127-145]
src/aftune.js[178-192]
src/aftune.js[430-437]
src/editor.js[4902-4907]

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 measured zone whose focus response remains exactly zero is treated as unknown forever, so the live grid omits the empty-area explanation.
## Fix Focus Areas
- src/aftune.js[423-438]
- dist/aftune.js[423-438]
- tools/smoke.mjs[1287-1329]
## Recommended Fix
After the frame-level movement gate succeeds, classify a zone with `hi === 0` and `lo === 0` as `none`; reserve `unknown` for zones without measured records. Add a test with one responding zone and one measured zero-valued zone.

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


3. Sweeps mislabel measured zero zones ✓ Resolved 🐞 Bug ≡ Correctness
Description
sweepZones() assigns unmeasured when hi <= 0 even if ever proves that valid measured samples
were collected. During a definite lens sweep, a lit zone whose focus response remains zero is
consequently reported with the wrong reason instead of flat.
Code

src/aftune.js[R376-378]

+		if (pinned) { why[i] = 'pinned'; continue; }
+		if (!ever || hi <= 0) { why[i] = 'unmeasured'; continue; }
+		/* Never moved: there is nothing in this zone to focus on. Reported as
Evidence
The sweep loop sets ever for every sample whose state is measured and accepts its zero value into
the extrema. The added condition then overrides that evidence solely because the maximum is zero,
despite summaries accepting zero focus counts for otherwise measured zones.

src/aftune.js[107-145]
src/aftune.js[358-369]
src/aftune.js[376-383]

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 results label valid measured zero-response zones as unmeasured rather than flat.
## Fix Focus Areas
- src/aftune.js[357-383]
- dist/aftune.js[357-383]
- tools/smoke.mjs[1339-1387]
## Recommended Fix
Use `ever` alone to distinguish unmeasured zones, then handle `hi === 0` as a flat response without dividing by zero. Add a sweep test containing a measured zone whose values are zero at every lens position.

ⓘ 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/aftune.js Outdated
Comment thread src/aftune.js
Comment thread src/aftune.js Outdated
Three from review, all real, and two of them one mistake: I conflated a zero
reading with no reading, in code whose own comment already says they are
different.

A lit zone reading zero at every lens position is the emptiest zone there
is. zoneDetail skipped it for hi <= 0 and left it unknown, so the block most
in need of "nothing to focus on" was the one block that never got it; and
sweepZones called it unmeasured, telling the operator the zone could not be
read when it had been read and found empty. null stays unknown -- that one
really is never measured. Zero is 'none' and 'flat'. The accumulators cannot
go negative, so hi === 0 means lo === 0 and there is no swing to divide for.

peakHold kept its record across a reshape. Same class as the sweep bug fixed
last round, in the live path this time: two grids of the same size and
different shape put the same index somewhere else in the picture, so the
held per-zone record describes the wrong zones. Worse, the mismatch branch
cleared only those arrays and never bestOverall/lowOverall -- which are the
gate deciding whether the lens has moved at all, so the gate went on
answering from a range measured on a camera that no longer existed. The
record is now keyed on the shape and all four are cleared together.
@widgetii

Copy link
Copy Markdown
Member Author

All three were real and are fixed in 6ec3423. Two of them are one mistake on my part: I conflated a zero reading with no reading, in code whose own comment already says they are different.

2 — Zero-value blocks stay unexplained. Correct. A lit zone reading zero at every lens position is the emptiest zone there is, and hi <= 0 skipped it — so the block most in need of "nothing to focus on" was the one block that never got it. null still stays unknown, because that one really is never measured. Zero is now none. The accumulators cannot go negative, so hi === 0 implies lo === 0 and there is no swing to divide for.

3 — Sweeps mislabel measured zero zones. Same mistake in sweepZones: ever is the whole question of whether anything was measured, and a zero maximum is an answer to it. Calling it unmeasured told the operator the zone could not be read when it had been read and found empty. Now flat.

1 — Focus labels move to the wrong blocks. Correct, and it is the same class as the sweep shape bug fixed last round, in the live path this time. Two grids of the same size and different shape put the same index somewhere else in the picture, so the held per-zone record describes the wrong zones.

Your evidence caught something worse than the headline, and thank you for it: the mismatch branch cleared only best and low and never bestOverall/lowOverall — and those are the gate deciding whether the lens has moved at all. So even on a plain zone-count change, the gate went on answering from a range measured on a camera that no longer existed. The record is now keyed on rows×cols and all four are cleared together, reset() included.

Tests

Five assertions added. All four mutations discriminate:

break red
shape change kept a reshaped grid starts the record again
overall range kept on reshape a reshaped grid starts the record again
a zero zone stays unknown a zone that reads zero throughout is empty, not unknown
a zero zone called unmeasured measured zero zone is flat, not unmeasured; and it is counted as such

The reshape test first asserts the lens has visibly moved on the old shape, so it cannot pass by everything being unknown for some unrelated reason.

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

@widgetii
widgetii merged commit b83f834 into main Sep 25, 2026
1 check passed
@widgetii
widgetii deleted the focus-nodetail branch September 25, 2026 06:03
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