feat(classify): attenuate binary by panel_density so form-scan wins (MK-24) #41

Merged
David merged 1 commit from feat/classify-binary-panel-attenuation-MK-24 into main 2026-05-25 01:17:03 +02:00
Owner

What

Adds a (1 - panel_density_high) attenuation factor to the binary score in src/image/classify.rs::score, symmetric to the existing (1 - halftone) factor. Resolves MK-24 (parent MK-5, follow-up to MK-20).

Why

MK-20 lifted form-scan from 1/11 to 5/11 fixtures, but the remaining misses could not be reached by threshold tuning. The blocker is a scoring-formula asymmetry:

  • binary = bimodal * low_sat * (1 - halftone) is a 3-factor product.
  • form_scan = soft_ge(panel_density) * soft_ge(non_panel_bimodality) * edges_text * low_sat * (1 - color) is a 5-factor product where every factor lives in [0, 1].

On a feature-similar form (high bimodality because the text outside the panels reads as a clean binary scan, plus low saturation), all of binary's factors fire near 1.0 while form_scan multiplies five sub-1.0 gates and loses the argmax. Halftone-scan already enjoys attenuation via binary's (1 - halftone) factor; there was no symmetric panel-density attenuation. This PR adds it.

Change

// Computed once, reused in both binary (attenuation) and form_scan (first factor).
let panel_signal = soft_ge(f.panel_density, t.panel_density_min, t.soft_scale_panel);

let binary = bimodal * low_sat * (1.0 - halftone) * (1.0 - panel_signal);

form_scan's first factor now references the shared panel_signal binding instead of recomputing the same soft_ge. No behavioural change to form_scan. No new threshold knobs: panel_density_min and soft_scale_panel already ship in classify.toml from MK-16 / MK-20. classify.toml is NOT modified. No new dependencies.

Verification done in-sandbox

  • just check passes: cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo build --all-targets, the test suite, and the docker builder-stage compile.
  • All 15 in-tree classify unit tests pass unmodified, including the two Phase-3 guards classifies_form_scan and classifies_binary_scan (the latter has panel_density ~0, so (1 - panel_signal) ~ 1.0 and binary is unaffected).

Verification REQUIRED from a reviewer with fixture access

The fixture tree under tests/fixtures/classify/ is gitignored (MK-14) and never reaches the CI / agent sandbox, so the empirical probe (issue Phase 1/2) could not run here. Per the issue's "ship a draft and ask the human to validate" path, please run locally and confirm before merge:

  1. just classify-fixtures against every populated class directory. Expected: form-scan 9/11 (5 from MK-20 + 4 new), binary-scan 52/52, receipt 5/5, color-photo 1/1, no regression on mono-photo / graphic / halftone-scan.
  2. Fill in the per-fixture before/after binary and form_scan scores (via monkey image classify --probe or --features) for the 4 target form-scan fixtures and the 2 high-panel-density binary-scan fixtures, to make the flip visible:
fixture binary before form_scan before binary after form_scan after result
Scan-20251006T055028.png (form-scan target) ~0.48 ~0.34 TBD TBD TBD
Scan-20251006T055236.png (form-scan target) ~0.48 ~0.33 TBD TBD TBD
Scan-20251006T060008.png (form-scan target) ~0.53 ~0.42 TBD TBD TBD
Scan-20260524T132524.png (form-scan target) ~0.52 ~0.45 TBD TBD TBD
Scan-20260513T064857.png (binary, pd~0.05) TBD TBD TBD TBD must stay binary
Scan-20260513T071104.png (binary, pd~0.07) TBD TBD TBD TBD must stay binary

If form-scan does not reach 9/11, the issue authorizes iterating on panel_density_min / soft_scale_panel; if binary-scan regresses, widen soft_scale_panel to soften the curve. Per scope discipline, the two known MK-20 misses (135334 -> unknown, 140051 -> mono-photo) are explicitly out of scope here and need their own follow-up.

Acceptance criteria status

  • score adds (1 - panel_density_high) from the same thresholds form_scan uses.
  • In-tree synthetic tests pass without modification (incl. classifies_form_scan, classifies_binary_scan).
  • classify.toml NOT modified.
  • just check passes.
  • No new dependencies.
  • PR description includes per-fixture before/after scores (table above; reviewer to fill _TBD_ from local corpus).
  • All 4 target form-scan fixtures classify as form-scan with confidence >= 0.20 (reviewer to confirm locally).
  • All 52 binary-scan fixtures stay binary-scan (reviewer to confirm locally).
  • All 5 receipt fixtures stay receipt (reviewer to confirm locally).

MK-24

## What Adds a `(1 - panel_density_high)` attenuation factor to the `binary` score in `src/image/classify.rs::score`, symmetric to the existing `(1 - halftone)` factor. Resolves MK-24 (parent MK-5, follow-up to MK-20). ## Why MK-20 lifted form-scan from 1/11 to 5/11 fixtures, but the remaining misses could not be reached by threshold tuning. The blocker is a scoring-formula asymmetry: - `binary = bimodal * low_sat * (1 - halftone)` is a 3-factor product. - `form_scan = soft_ge(panel_density) * soft_ge(non_panel_bimodality) * edges_text * low_sat * (1 - color)` is a 5-factor product where every factor lives in `[0, 1]`. On a feature-similar form (high bimodality because the text outside the panels reads as a clean binary scan, plus low saturation), all of binary's factors fire near 1.0 while form_scan multiplies five sub-1.0 gates and loses the argmax. Halftone-scan already enjoys attenuation via binary's `(1 - halftone)` factor; there was no symmetric panel-density attenuation. This PR adds it. ## Change ```rust // Computed once, reused in both binary (attenuation) and form_scan (first factor). let panel_signal = soft_ge(f.panel_density, t.panel_density_min, t.soft_scale_panel); let binary = bimodal * low_sat * (1.0 - halftone) * (1.0 - panel_signal); ``` `form_scan`'s first factor now references the shared `panel_signal` binding instead of recomputing the same `soft_ge`. No behavioural change to `form_scan`. No new threshold knobs: `panel_density_min` and `soft_scale_panel` already ship in `classify.toml` from MK-16 / MK-20. `classify.toml` is NOT modified. No new dependencies. ## Verification done in-sandbox - `just check` passes: `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, `cargo build --all-targets`, the test suite, and the docker builder-stage compile. - All 15 in-tree classify unit tests pass unmodified, including the two Phase-3 guards `classifies_form_scan` and `classifies_binary_scan` (the latter has panel_density ~0, so `(1 - panel_signal)` ~ 1.0 and binary is unaffected). ## Verification REQUIRED from a reviewer with fixture access The fixture tree under `tests/fixtures/classify/` is gitignored (MK-14) and never reaches the CI / agent sandbox, so the empirical probe (issue Phase 1/2) could not run here. Per the issue's "ship a draft and ask the human to validate" path, please run locally and confirm before merge: 1. `just classify-fixtures` against every populated class directory. Expected: form-scan 9/11 (5 from MK-20 + 4 new), binary-scan 52/52, receipt 5/5, color-photo 1/1, no regression on mono-photo / graphic / halftone-scan. 2. Fill in the per-fixture before/after `binary` and `form_scan` scores (via `monkey image classify --probe` or `--features`) for the 4 target form-scan fixtures and the 2 high-panel-density binary-scan fixtures, to make the flip visible: | fixture | binary before | form_scan before | binary after | form_scan after | result | |---|---|---|---|---|---| | Scan-20251006T055028.png (form-scan target) | ~0.48 | ~0.34 | _TBD_ | _TBD_ | _TBD_ | | Scan-20251006T055236.png (form-scan target) | ~0.48 | ~0.33 | _TBD_ | _TBD_ | _TBD_ | | Scan-20251006T060008.png (form-scan target) | ~0.53 | ~0.42 | _TBD_ | _TBD_ | _TBD_ | | Scan-20260524T132524.png (form-scan target) | ~0.52 | ~0.45 | _TBD_ | _TBD_ | _TBD_ | | Scan-20260513T064857.png (binary, pd~0.05) | _TBD_ | _TBD_ | _TBD_ | _TBD_ | must stay binary | | Scan-20260513T071104.png (binary, pd~0.07) | _TBD_ | _TBD_ | _TBD_ | _TBD_ | must stay binary | If form-scan does not reach 9/11, the issue authorizes iterating on `panel_density_min` / `soft_scale_panel`; if binary-scan regresses, widen `soft_scale_panel` to soften the curve. Per scope discipline, the two known MK-20 misses (135334 -> unknown, 140051 -> mono-photo) are explicitly out of scope here and need their own follow-up. ## Acceptance criteria status - [x] `score` adds `(1 - panel_density_high)` from the same thresholds form_scan uses. - [x] In-tree synthetic tests pass without modification (incl. `classifies_form_scan`, `classifies_binary_scan`). - [x] `classify.toml` NOT modified. - [x] `just check` passes. - [x] No new dependencies. - [ ] PR description includes per-fixture before/after scores (table above; reviewer to fill `_TBD_` from local corpus). - [ ] All 4 target form-scan fixtures classify as form-scan with confidence >= 0.20 (reviewer to confirm locally). - [ ] All 52 binary-scan fixtures stay binary-scan (reviewer to confirm locally). - [ ] All 5 receipt fixtures stay receipt (reviewer to confirm locally). MK-24
feat(classify): attenuate binary by panel_density so form-scan wins
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 17s
Create release / Create release from merged PR (pull_request) Has been skipped
d408bf0e02
MK-20 lifted form-scan from 1/11 to 5/11 but the remaining misses could not be reached by threshold tuning. The blocker was a scoring-formula asymmetry: `binary = bimodal * low_sat * (1 - halftone)` is a 3-factor product while `form_scan` is a 5-factor product, so on a feature-similar form (high bimodality, low saturation, real panel signal) binary's shorter product wins the argmax because nothing attenuated it in the presence of panels. Halftone-scan already gets this protection via binary's `(1 - halftone)` factor; there was no symmetric `(1 - panel_density_high)` factor.

This adds `panel_signal = soft_ge(panel_density, panel_density_min, soft_scale_panel)` (the exact gate form_scan already uses as its first factor, now computed once and reused in both places) and multiplies `(1 - panel_signal)` into the binary score. When panel_density is high, binary collapses and form-scan wins; binary-scan fixtures (panel_density 0.00-0.09 at the MK-20-tuned thresholds) lose essentially nothing. No new threshold knobs, classify.toml unchanged, no new dependencies.

In-tree synthetic guards `classifies_form_scan` and `classifies_binary_scan` both still pass unmodified, along with the rest of the classify suite. `just check` passes (fmt, clippy, build, tests, docker builder-stage).

Empirical validation against the gitignored fixture corpus (MK-14) cannot run in the agent sandbox: the imagery never reaches CI. Per the issue's "ship a draft and ask the human to validate" path, the per-fixture before/after score table and the 4-form-scan-win / 52-binary-no-regression / 5-receipt acceptance checks must be confirmed locally by a reviewer with fixture access via `just classify-fixtures`.

#MK-24 State Done

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
David merged commit 619188ff07 into main 2026-05-25 01:17:03 +02:00
David deleted branch feat/classify-binary-panel-attenuation-MK-24 2026-05-25 01:17:03 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pandoras-box/monkey!41
No description provided.