Skip to content

Half Frame: plain auto-detecting toggle, more accurate detection - #1112

Draft
PabloRuizCuevas wants to merge 7 commits into
marcinz606:mainfrom
PabloRuizCuevas:feat/pr-half-frame-detection
Draft

PabloRuizCuevas wants to merge 7 commits into
marcinz606:mainfrom
PabloRuizCuevas:feat/pr-half-frame-detection

Conversation

@PabloRuizCuevas

@PabloRuizCuevas PabloRuizCuevas commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Half Frame becomes a plain, auto-detecting toggle instead of an editor you step through first, and its detection gets more accurate.

  • Plain auto-detecting toggle, per-roll state: turning it on auto-detects the gutter on every loaded scan directly. Each roll remembers its own Half Frame state; an ad hoc session keeps the one sticky flag it always had. The tune-icon menu still offers the per-scan editor for the odd frame the batch auto-detect gets wrong (Adjust Split…, Auto-detect All Splits, Unsplit Diptych).
  • Sub-pixel gutter edges, plus auto-detected thickness: detect_gutter now returns both the split position and the gutter's own thickness, refined to sub-pixel accuracy, rejecting a smooth in-scene gradient (an overexposed sky) that could otherwise pull the detected center off target.
  • Auto-detect the outer film crop, not just the gutter: a new detect_film_crop finds the rebate/bed margin around a diptych so the batch auto-detect can also trim the outer edges, not only find the split.
  • Detect the outer film crop from each edge inward (accuracy fix): detect_film_crop no longer delegates to the scanner-bed autocrop detector, which finds nothing on a tightly-framed camera scan and can crop a whole exposure off a frame it misreads in low light. Each of the four sides is now searched independently, inward from the scan's own boundary, for the same uniform-and-extremal band detect_gutter finds in the middle.
  • The per-scan editor's own Auto-detect also crops: brings the per-scan editor's Auto-detect button in line with the batch version.

Note: this PR is based on main but its branch (feat/pr-half-frame-detection) is built on top of the current #1109 (feat/pr-rolls-and-roll-settings), since the per-roll Half Frame state needs the Roll concept #1109 introduces -- please review/merge #1109 first. Once it lands, this diff will automatically shrink to just the half-frame changes.

Testing

make all (ruff check/format, full pytest) passes with only one known unrelated failure (test_load_tooltip_follows_a_rebinding, a macOS key-symbol rendering difference, pre-existing on main).

🤖 Generated with Claude Code

PabloRuizCuevas and others added 7 commits September 17, 2026 17:13
… workflow

Rolls (negpy/services/assets/rolls.py) replace folders as the library's
primary listing: importing a folder recognizes it as a roll; Save as
Roll… turns any loaded batch into a reopenable virtual roll, not
necessarily from one folder. The Library tree lists every roll flat,
color-coded (folder amber, virtual coral, no left-side twisty gutter),
with Rename…, bulk Delete…, and an opt-in "rename the folder on disk"
checkbox that rehomes every loaded path when the renamed roll is the
active one.

Roll Settings dialog tags gear, capture, place, process and scanning
metadata for the current frame, a selection, or the whole roll in one
dialog, with a Load-preset flow to fill and tick fields.

New Roll moves off the toolbar onto the Film Strip section's own
header menu (CollapsibleSection.set_actions_menu), next to Reset Roll
to Defaults — a roll-wide reset (reused _asset_defaults, one frame's
worth of logic applied to every visible one) with its own confirm
dialog. The top-level toolbar is gone: actions live in the row of the
section they act on, Library's own corner row or the Film Strip row
next to the loaded frames. restore_session now recognizes the active
roll on restart too, the same "only when unambiguous" rule an
already-open multi-folder session uses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Turning Half Frame on used to pop the rectangle editor before splitting
anything -- now it just runs the same batch auto-detect Auto-detect All
Splits does, directly, no dialog. The editor stays reachable from the
tune-icon menu (Adjust Split...) for the odd frame the batch gets wrong.

The toggle was one global flag shared by every roll. Now each roll
remembers its own (half_frame_mode_by_roll, keyed by the real roll id from
rolls.py); an ad hoc session with no recognized roll keeps the single
sticky flag it always had. AppController.half_frame_mode_for_roll() is the
one place that resolves which applies, read both by request_asset_discovery
(so a roll always splits into what it was left as) and by the toggle's own
displayed state. A new roll saved from an ad hoc session
(create_roll_from_session) seeds its own entry from that session's current
value, so saving a roll never silently resets the toggle for it.

half_frame_mode_changed (mirroring the existing rgb_scan_mode_changed) fires
wherever the active roll changes -- open_roll, open_library_folders, a
library search landing on ad hoc results -- so the button follows without
re-toggling itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Auto-detect All Splits (and the toggle, which runs it on turning on) only
ever found split_x -- the crop rect it saved was whatever was already
there, defaulting to the full uncropped scan. The scanner bed / holder
around the film was never trimmed by auto-detect at all, only the gutter
between the two frames, which is why the edges stayed loose after
detecting.

detect_split_and_crop_for_file (half_frame.py) decodes the file once and
runs two independent detectors on the same buffer: the existing 1-D
gutter heuristic for split_x, and detect_film_crop -- a thin wrapper
around get_autocrop_coords(mode=FILM, ratio=Free), the same single-frame
film-bounds detector the Geometry tab's own Auto Crop already uses.
FILM mode keeps the rebate/sprockets a two-up diptych's outer crop must
not cut into; a forced aspect ratio would be wrong for a two-up frame.

process_auto_detect_all_splits (render.py) and _on_splits_detected
(controller.py) now carry (split_x, crop_rect | None) pairs instead of a
bare float; a file whose crop detection fails keeps whatever crop it
already had, only the split moves for it. The per-frame rectangle
editor's own Auto-detect is unchanged (split-only) -- it already has a
crop the user drags by hand.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
detect_split_x took the midpoint of the whole band that deviated from a
local background -- correct for a clean gutter, but a smooth in-scene
gradient blending into one edge (an overexposed sky) widens that band on
one side only, dragging the midpoint toward it. The gutter's own edges are
now found directly, as the steepest slope on each side of the rough peak,
refined to sub-pixel precision by a parabola through each edge's neighbors,
and the true center is the midpoint between those two edges instead.

Renamed to detect_gutter and returns (split_x, gutter_thickness): the edges
it now finds also measure the band's own width, previously never detected
at all -- Auto-detect only ever moved the split line, leaving Cut thickness
wherever it last was. detect_split_x stays as a thin wrapper for callers
that only want the position.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
feat/half-frame-auto-detect-accuracy gave the per-frame rectangle editor's
own Auto-detect button sub-pixel split_x and gutter thickness
(detect_gutter), but left the batch path (Auto-detect All Splits, and the
toggle that runs it) on the old split-only detect_split_x_for_file --
merged in here, so this branch had it, without the batch benefiting.

detect_split_and_crop_for_file now calls detect_gutter instead of
detect_split_x, returning (split_x, gutter_thickness, crop_rect) instead
of just (split_x, crop_rect); _on_splits_detected applies all three to
each file's override. Thickness comes free out of the same computation
that already finds split_x, so this costs nothing extra to detect --
only the plumbing to save it was missing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Its own Auto-detect button (HalfFrameDialog) only ever ran detect_gutter --
the rectangle stayed whatever it already was, same gap the batch path had
before the previous commit. A user pressing it expected the same result
the toggle gives, since both call themselves "Auto-detect".

_on_auto now also runs detect_film_crop and sets the rectangle from it.
Fixed alongside: split_x is relative to the cropped width (slice_half's own
convention), so the gutter search has to run inside the newly detected
crop, not the full uncropped scan -- detect_split_and_crop_for_file had
the same bug (detecting the gutter and the crop independently, on the same
uncropped buffer), fixed there too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
detect_film_crop no longer delegates to the scanner-bed autocrop detector:
it finds nothing on a tightly-framed camera scan (no bed to separate from
film) and can crop a whole exposure off a frame it misreads, especially in
low light. Each of the four sides is now searched independently, inward from
the scan's own boundary, for the same uniform-and-extremal band detect_gutter
finds in the middle -- unexposed rebate reads as a narrow run near the
frame's own darkest or brightest tone. A side with no rebate, or a margin too
wide or not extremal enough against the frame's own tonal range to trust, is
left uncropped rather than guessed at.

Verified by eye against real half-frame camera-scan rolls: trims genuine
narrow rebate margins, leaves tightly-framed sides alone, and does not crop
into a blank or dark exposure or into locally-uniform scene content (calm
water, sky) that only coincidentally looks like film base.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@PabloRuizCuevas
PabloRuizCuevas force-pushed the feat/pr-half-frame-detection branch from 50ffebb to b388c93 Compare September 17, 2026 10:27
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