Skip to content

Feat/half frame rectangle editor - #761

Merged
marcinz606 merged 2 commits into
marcinz606:mainfrom
diegotonetti99:feat/half-frame-rectangle-editor
Aug 5, 2026
Merged

Feat/half frame rectangle editor#761
marcinz606 merged 2 commits into
marcinz606:mainfrom
diegotonetti99:feat/half-frame-rectangle-editor

Conversation

@diegotonetti99

Copy link
Copy Markdown
Contributor

Half-Frame: crop/gutter editor + export-matches-preview fix

Summary

Two commits on one branch:

  1. feat — half-frame scans get a visual crop-rectangle + split-line + gutter editor; the saved profile applies to every half-frame split regardless of acquisition method.
  2. fix — export "current frame" could export the wrong half, and the export's color/tonality diverged from the LQ preview (a color cast appeared on export that the preview didn't show). Both are prior bugs in the original half-frame feature, surfaced and fixed while dogfooding the editor.

Why

Half-frame cameras expose two frames on one scan, and scanners capture both in a single pass. NegPy already split each scan at an auto-detected x, but the split position couldn't be corrected when the gutter fell outside the default, the physical black separator bled into both halves, and there was no way to crop the scan to the usable film area.

While testing that, two export bugs turned up:

  • Wrong-half export. request_export looked up the export asset by path. Both halves share a path, so next(...) returned the first match (half 1) regardless of which half was current. Exporting frame 2 shipped frame 1's file_info (half=0) — the whole scan, or the wrong half.
  • Preview/export color drift. The LQ preview downsampled the whole scan to preview_render_size, then sliced the half in the controller. Export (and HQ preview) sliced the half at full resolution, then the analysis stage downsampled the half. INTER_AREA averages different pixel neighborhoods in the two orders — the whole-scan downsample bleeds the gutter/other half into the analysis buffer, shifting the measured log bounds. Different bounds → different normalization → a color cast (e.g. red) on export that the LQ preview didn't show. HQ preview matched export because full_resolution=True skips the whole-scan downsample, so both did slice-then-downsample.

What changed

Crop/gutter editor (commit 48bece6)

  • Editor dialog (negpy/desktop/view/widgets/half_frame_dialog.py, new): modal pop-up showing a positive (inverted + auto-levelled) preview of one scan. A draggable/resizable green rectangle defines the crop (everything outside is discarded); a draggable orange centerline sets the split; a Cut thickness slider discards a band centered on the split. Returns crop_rect, split_x, gutter_thickness on Apply.
  • Slicing (negpy/services/assets/half_frame.py): slice_half() and slice_for_asset() accept crop_rect (normalized x1,y1,x2,y2) and gutter_thickness (normalized fraction of cropped width). The gutter discards a band centered on the split so the physical separator doesn't bleed into either half.
  • Controller (negpy/desktop/controller.py): new open_half_frame_dialog() loads the scan, seeds the editor from the saved profile (or auto-detect via detect_split_x), and persists the result as a global setting (half_frame_profile). _active_half() and _slice_half_source() now carry crop_rect and gutter_thickness through the render path. The profile is forwarded on asset discovery requests so re-discovery applies it.
  • Files sidebar (negpy/desktop/view/sidebar/files.py): new Adjust Half Frame toolbutton (tune icon) re-opens the editor on the current scan and triggers re-discovery so the new profile takes effect immediately. Enabling Half Frame mode also offers the editor on the current frame.
  • Render & export paths (negpy/services/rendering/image_processor.py, negpy/desktop/workers/export.py, negpy/services/assets/thumbnails.py): crop_rect and gutter_thickness flow through export, tile rendering, and thumbnail generation so previews, full-res exports, and filmstrip thumbs all honour the profile.
  • Tests (tests/test_half_frame.py): TestSliceHalfCropGutter covers crop-rect-only slicing, gutter band discard, and slice_for_asset reading both fields. tests/test_file_browser_toolbar.py updated for the new button.
  • Docs (docs/USER_GUIDE.md): Half Frame entry documents the rectangle editor, the Cut thickness slider, the saved-profile behaviour across acquisition methods, and the Adjust Half Frame toolbutton.

Export-matches-preview fix (commit 59b2237)

  • Export asset lookup (negpy/desktop/controller.py): request_export now matches the asset by hash (unique per half) instead of path (shared), so the active half's file_info reaches the exporter.
  • Slice before downsample (negpy/services/rendering/preview_manager.py): the active half is now sliced from the full-res decode before the preview downsample (both the splash and linear paths), so the analysis stage sees the same pixels export analyzes. Threaded a half_slice tuple (half, split_x, crop_rect, gutter_thickness) through:
    • PreviewLoadTask (negpy/desktop/workers/render.py) — the worker passes it to load_linear_preview, load_splash_and_linear, and the cache-warm path.
    • load_linear_preview / load_splash_and_linear / _load_from_open_raw / _try_splash_from_open_raw — slice full_linear and ir_full (and the splash thumbnail) before the resize.
  • Cache key (negpy/services/rendering/preview_cache.py): PreviewCacheKey now includes half, split_x, crop_rect, gutter_thickness, so the two halves get separate cache entries (they're different buffers now).
  • Controller (negpy/desktop/controller.py): _split_active_half is a passthrough (preview buffers arrive already sliced); _active_half refactored to share a new _half_slice_for_asset helper; prefetch now warms the half-suffixed hash with the neighbor's half_slice so cache-warm keys match real loads.

How to test

  1. Load a folder of half-frame scans, enable Half Frame — the editor should open on the current scan.
  2. Drag the green rectangle to crop; drag the orange line to reposition the split; adjust Cut thickness to discard the separator band. Apply.
  3. Both halves should render, export, and thumbnail with the crop + gutter applied; re-discover and confirm the saved profile persists.
  4. Re-open via Adjust Half Frame and confirm the saved profile seeds the editor.
  5. Select half 2, Export → current frame. Confirm the exported file is half 2 (not half 1).
  6. On a frame where the LQ preview and export previously differed (color cast on export), toggle HQ preview off and export — the export should now match the LQ preview. Toggle HQ on and the preview should stay consistent.
  7. Switch between halves rapidly and export; the export should match the displayed half.
  8. Confirm a whole-frame (non-half) scan is unaffected.
  9. Confirm navigation/prefetch still hits the warm cache (no re-decode on navigate-back).

Notes

  • The export-matches-preview fix predates the crop/gutter editor. The original half-frame feature (present at b80eb4f) already sliced after the whole-scan downsample in the controller via _split_active_half. The crop/gutter work inherited that ordering unchanged; the fix moves the slice ahead of the downsample so preview and export analyze identical pixels.
  • No GPU/pipeline math changes; slicing happens before the render pipeline. The only change is where the slice happens relative to the preview downsample.

Disclosure

Portions of this change were written with the assistance of an AI coding tool (Claude / opencode). All code was reviewed and is the author's responsibility.⏎

@marcinz606
marcinz606 merged commit 8823afc into marcinz606:main Aug 5, 2026
2 checks passed
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.

2 participants