Skip to content

Scan: exposure lock for nkscan roll scanning - #1146

Merged
marcinz606 merged 2 commits into
mainfrom
feat/nkscan-exposure-lock
Sep 25, 2026
Merged

marcinz606 merged 2 commits into
mainfrom
feat/nkscan-exposure-lock

Conversation

@marcinz606

@marcinz606 marcinz606 commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

Problem

nkscan meters every frame on its own. For color negative it scales each channel so the 99.9th percentile of the frame rectangle lands at 97% of full scale. In the middle of a strip, the brightest area is the orange film base, so the mask comes off before the ADC. At a strip end it is the bare light past the cut, so the mask stays in. Those frames come out 0.4–0.6 D denser in G/B, which is what broke Roll Analysis on the marbella rolls (#1145).

Measured on the scanner, one 6-frame strip:

Frame Red Green Blue
1 (strip end) 82 788 88 160 67 205
2–6 141 635–146 001 351 944–358 393 378 857–389 728

A roll is six strips, so six batches, and the lock has to last across them.

Change

  • Scan panel: an Exposure row (Meter Frame… / Unlock) with a hint line, for backends with ScannerCapabilities.exposure_lock (nkscan only). The lock is saved in ScannerSettings, so it survives batches and restarts. A lock metered on another device is ignored.
  • Scanner layer: ScanParams.exposures reaches nkscan's scan_frame, which then skips metering. ScanResult.exposures reports the exposures a scan ran at.
  • NkscanBackend.meter: meters the whole detected frame with its offset, never the scan window, because the film base in the gap is what the meter should land on. Infrared is included, so the lock also holds for IR and ICE scans.
  • Worker: MeterRequest / ScanWorker.run_meter, modelled on prescan. The batch's per-frame offset formula is one function, shared with metering.
  • Shortcut registry entry scan_meter_frame (no default key), and a USER_GUIDE bullet.

Test plan

  • make all: lint, ty, full pytest (6892 passed)
  • New tests: nkscan backend (lock, meter_frame path, fallback, LS-50 superfine), service, worker, settings round-trip, scan sidebar
  • nkscan fork hardware test a_frame_scans_at_the_exposures_metered_for_it on the scanner
  • On the scanner: Meter Frame… on frame 2, scan 1–6, and check that frame 1's film base matches frames 2–6

@marcinz606
marcinz606 force-pushed the feat/nkscan-exposure-lock branch from 69fb8fc to 648df0a Compare September 23, 2026 09:33
nkscan meters every frame by itself. For colour negative it scales each
channel so the frame rectangle's 99.9th percentile reaches 97% of full
scale. Mid-strip that is the orange film base, so the mask comes off
before the ADC; at a strip end it is bare light past the cut, so the mask
stays in. Metering every frame of a loaded LS strip on the scanner gave
frames 2-6 within 3% of each other and frame 1 at -0.24/-0.61/-0.76 log10
(R/G/B), the same G/B offset the strip-end frames showed in Roll Analysis.

- Scan panel: Exposure row (Meter Frame... / Unlock) for backends with
  ScannerCapabilities.exposure_lock. The lock lives in ScannerSettings, so
  it holds across the six batches of a roll and across restarts; a lock
  metered on another device is ignored.
- ScanParams.exposures reaches nkscan's scan_frame, which then skips
  metering; ScanResult.exposures reports what a scan ran at.
- NkscanBackend.meter meters the whole detected frame with its offset,
  never the scan window, infrared included. It uses Session.meter_frame
  where nkscan has it and falls back to the smallest scan_frame.
- MeterRequest / ScanWorker.run_meter, modelled on prescan. The batch's
  per-frame offset formula is one function shared with metering.
nkscan 0.12.0 ships Session.meter_frame, so metering no longer falls
back to the smallest scan_frame on older releases.

0.12 also reports whether a pass arrived whole. A short frame pass
raises TransientScanError, so ScannerService retries it rather than
writing planes that hold data only for the blocks that arrived. A short
thumbnail pass raises before its frames are cached, since its fit reads
the missing blocks as film ending early.

The user guide says release builds include nkscan and sends source
installs to CONTRIBUTING.md, which gains a Coolscan entry.
@marcinz606
marcinz606 force-pushed the feat/nkscan-exposure-lock branch from 648df0a to c925779 Compare September 25, 2026 07:26
@marcinz606
marcinz606 marked this pull request as ready for review September 25, 2026 07:26
@marcinz606
marcinz606 merged commit 7cce94a into main Sep 25, 2026
1 check passed
@marcinz606
marcinz606 deleted the feat/nkscan-exposure-lock branch September 25, 2026 20:22
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