Skip to content

Stitch multi-part scans into one composite frame#585

Merged
marcinz606 merged 1 commit into
mainfrom
feat/stitch
Jul 21, 2026
Merged

Stitch multi-part scans into one composite frame#585
marcinz606 merged 1 commit into
mainfrom
feat/stitch

Conversation

@marcinz606

@marcinz606 marcinz606 commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Implements in-app stitching for multi-shot camera scans (discussion #555): Lightroom-style photo merge, but on NegPy's scene-linear decode, so the composite converts like a raw instead of the oversaturated results external stitchers produce.

How it works

  • Multi-select 2+ frames → context menu Stitch selected frames. A background worker decodes the parts full-res and registers them (SIFT + ratio test + RANSAC similarity transform — ORB gets 5/1112 inliers on negatives and translation-only phase correlation can't handle the ~0.4° rotation between shots; SIFT hits 99% inliers, overlap NCC 0.98 on the sample pair).
  • Registration runs once; the transforms/canvas ride on a virtual composite asset (RGB-scan triplet pattern) and persist via session_stitches. No intermediate file is written.
  • Decode/preview replay the stored warp at any scale, with per-channel gain compensation in the overlap and distance-transform feather blending, in scene-linear before conversion. The composite then behaves like any frame: edits keyed to an order-sensitive stitch_hash, normal pipeline, normal export.
  • Flat-field is applied per part before warping (both decode paths); the pipeline skips its composite-level flat-field step.
  • IR planes are warped with the same transforms and max-blended when every part has one, so IR dust removal works on composites.
  • Unstitch dissolves the composite; part edits restore from the DB by content hash, composite edits survive a re-stitch of the same parts.

Verification

  • 18 unit tests: known-affine recovery (<0.05°, <2 px), seam RMS, gain cast removal, IR blend rules, preview/decode scale replay, config round-trip (tuple coercion), worker signals, session persist/restore. Real-sample integration test on samples/panorama/ (skips if absent).
  • Headless E2E: discover → stitch → 6141×6045 canvas → seamless rendered composite (screenshot-checked) → contact-sheet thumbnail present → unstitch restores both parts.
  • make all green (2379 passed).

closes #584

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

Preview load metrics (github-linux-x64)

format cold load throughput
CR2 0.758 s 29.57 MPx/s
NEF 0.601 s 40.88 MPx/s
ARW 0.228 s 88.51 MPx/s
RAF 0.627 s 25.94 MPx/s
DNG 0.630 s 37.97 MPx/s

fixtures: rawsamples.ch · sha: 297e670 · full JSON

Camera scans of medium/large format frames arrive as overlapping shots;
external stitchers output display-referred files that break negative
conversion (discussion #555). Multi-select frames -> "Stitch selected
frames" registers them once (SIFT + RANSAC similarity; ORB and phase
correlation fail on negatives) and stores the transforms on a virtual
composite asset. Every decode replays the warp + per-channel gain +
feather blend on scene-linear buffers, so the composite flows through
the normal pipeline and exports like any frame. IR planes are carried
when all parts have one; "Unstitch" restores the parts with their edits.

Bumps version to 0.41.0 with changelog entry.
@marcinz606
marcinz606 merged commit 5e141df into main Jul 21, 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.

implement stitching of multiple exposures

1 participant