Commit Graph
30 Commits
Author SHA1 Message Date
Brandon Thomas 0fb809c671 Merge PR #1614 (rebased)
# Conflicts:
#	crates/merge/src/hdr.rs
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-15 01:44:24 -04:00
Brandon ThomasandClaude Opus 5.5 3c0300fcbb Merge PR #1614
Resolved against rb-base-36: docs combine the 1 px clip neighbourhood and frame order with the base's moving-highlight rule and 2048 px deghost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:42:11 -04:00
Brandon Thomas 51a784cd95 Merge PR #1475 (rebased)
# Conflicts:
#	crates/raw/examples/rawinfo.rs
#	crates/raw/src/lib.rs
#	crates/raw/src/vendor/arw.rs
#	crates/raw/src/vendor/erf.rs
#	crates/raw/src/vendor/mef.rs
#	crates/raw/src/vendor/nef.rs
#	crates/raw/tests/lossy_dng.rs
#	crates/raw/tests/robust.rs
2026-10-15 01:13:36 -04:00
Brandon ThomasandClaude Opus 5.5 52a3b0ed84 Batch 37: #1471's test mosaic sets #1176's lens_metadata
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:12:03 -04:00
Brandon Thomas cf0002b484 Merge PR #1471 (rebased)
# Conflicts:
#	docs/parity-checklist.md
2026-10-15 01:10:17 -04:00
Brandon Thomas 66a0cd826c Merge PR #1475
# Conflicts:
#	crates/merge/src/output.rs
#	crates/raw/src/vendor/nef.rs
2026-10-15 01:05:31 -04:00
Brandon Thomas 7f7401713a Merge PR #1471
# Conflicts:
#	crates/engine/src/camera_preview.rs
#	crates/engine/src/merge.rs
#	crates/merge/src/frame.rs
#	crates/merge/src/hdr.rs
#	docs/parity-checklist.md
2026-10-15 00:56:52 -04:00
Brandon Thomas f613aeba59 Merge PR #1176
# Conflicts:
#	crates/raw/src/vendor/arw.rs
#	docs/parity-checklist.md
#	docs/sony-lens-corrections.md
2026-10-14 23:54:06 -04:00
Brandon Thomas 2fd769963e Merge PR #1549
# Conflicts:
#	crates/engine/src/tests_merge.rs
#	crates/ui-egui/locales/de-formats.json
#	crates/ui-egui/locales/es-formats.json
#	crates/ui-egui/locales/fr-formats.json
#	crates/ui-egui/locales/ja-formats.json
#	crates/ui-egui/locales/pt-br-formats.json
#	crates/ui-egui/locales/ru-formats.json
#	crates/ui-egui/locales/uk-formats.json
#	crates/ui-egui/locales/zh-hans-formats.json
#	crates/ui-egui/locales/zh-hant-formats.json
#	docs/merge.md
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-14 22:33:16 -04:00
Claude ac17c580f5 Merge storytold/lightcraft main into LR-MERGE-HDR (frame order, clipped pixels)
Brings in #1423 (HDR merges keep the lens corrections), which landed in batch 35 / v0.8.0. hdr.rs merged cleanly:
the result's lens corrections come from the reference frame after the brightness ordering. Conflicts only in docs:
the LR-MERGE-HDR row keeps main's lens-correction text plus this branch's two items, and this branch's What's New
entry moves to the 2026-10-14 section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015gRtdWF1E9358FfzhzYqCS
2026-10-14 16:22:37 +00:00
Michael Anderson 02d5d6bc03 LR-MERGE-PANO: Keep panoramas at full resolution when memory allows (#1455)
Drop the 40 MP default and free each photo's pixels once it's projected.
2026-10-14 07:32:45 -05:00
dev-null-undefinedandClaude Opus 5.5 a142179f20 LR-MERGE-HDR: keep the reference's clipped highlights where other frames moved; deghost catches ghosts in dark skies
A star moving across a night bracket became a black hole: where the reference was clipped, frames in which the star
had moved read the dark sky and replaced it. Such frames are now left out there (a clipped reference pixel means at
least that bright). Deghost compared ≤ 1024 px versions and, in dark areas, let the brighter frames predict
themselves, so their ghosts looked consistent; it now uses ≤ 2048 px maps (same feather share) and trusts the
reference down to 0.1 % of its clip. Aligned frames are warped one at a time, each replacing its original, instead of
all copies at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:56:59 +02:00
Alex ChambersandClaude Opus 5.5 ac677a2624 LR-EDIT-OPTICS-PROFILE: decode Sony shading, CA and distortion tables with settings and provenance; apply nothing new
Sony ARWs record shading, lateral chromatic aberration and distortion as a camera setting plus a radial
table in the raw image IFD (0x7031-0x7037), with copies in the encrypted SR2SubIFD (0x797c-0x7982).
They are now decoded for every body that records them and carried through RawInfo and RawImage as
`lens_metadata.sony` (types in crates/raw/src/lens_metadata.rs, decoder in
crates/raw/src/vendor/arw_lens.rs):

- Raw-IFD fields are the ones the TIFF reader parsed, which is what the image itself uses. A scan of
  the directory adds diagnostics only: repeated tags, and why an entry the reader skipped was dropped
  (values outside the file, an unknown type, past the reader's limits). SR2SubIFD fields are read
  from their first entry. Only the exact layouts are accepted: SHORT[1] or non-negative SSHORT[1]
  settings, SSHORT[17] headed by 16 for shading and distortion, SSHORT[33] headed by 32 for CA.
  Nothing is allocated from a count in the file.
- The raw IFD's copy wins. The SR2SubIFD copy fills in only for a tag the raw IFD lacks, never for a
  malformed one, and the two copies' agreement is recorded.
- Settings keep their number and their documented meaning, Unknown(value) otherwise (never Off).
- Framing evidence (stored frame, Sony and default crops, reference rectangle, coding, 3:2) and a
  per-component status. Only Applied components become opcodes.

Applied corrections are unchanged: the ILCE-7RM4A distortion conversion receives exactly the table it
received before, and no other body or component gains a correction. The ILCE-7CR distortion misses its
camera JPEG at 24 mm, and shading and CA semantics are not validated on any body; see
docs/sony-lens-corrections.md.

Locating the SR2SubIFD, now done for every ARW that has one, selects the SR2Private offset, length and
key with the TIFF reader's own rules, which lightcraft-tiff now exposes and builds its reader on
(directory_entries, entry_span, ParseOptions::budget_for, decode_value). Only each field's first value
is decoded, so the directory's other entries cost no memory sized by the file: a 16 MiB file whose
SR2Private holds eight fields aliasing it peaks at 37 kB. DirectoryEntries keeps its layout private,
and its positions() iterator uses checked arithmetic.

Tests: synthetic decoder tests; a differential test of the SR2Private selection against the TIFF
reader's in both byte orders (0 to 8,193 filler entries, unreadable and unusual first occurrences of
each field, a used-up value budget); a dhat peak-heap test; public-corpus checks against the values
ExifTool prints, with pinned and anchored SHA-256s (LIGHTCRAFT_CORPUS_REQUIRED=1 makes absent files
fail); and an opt-in local inventory check (crates/raw/tests/sony_lens_inventory.rs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:19:21 +02:00
radialmonster 2dad18d2ec LR-RAW-NEF: stretch non-square pixels (D1X/D1H) to square
The D1X/D1H raw SubIFD is 4028x1324 with XResolution 600 / YResolution 300:
the pixel is twice as tall as it is wide. Nothing read that, so the
picture came out about 3:1. Add a pixel aspect to RawImage/RawInfo, read
it from the raw IFD resolution ratio (NEF) and from DefaultScale (DNG),
accept only 1.5..4, and resample the rows after demosaic. developed_size()
reports the stretched size, binning is off for non-square pixels, and the
catalog and lens-profile sizing use developed_size().
2026-10-13 06:01:40 -04:00
Claude 951d52788a LR-MERGE-HDR: pixels next to clipped ones don't tint HDR merges
Measured against the darkest frame on real ARW brackets, each frame is linear to 1 % up to the clip far from
clipped pixels, but within 2 px of them the +2 EV frame's green reads 3–5 % low: demosaicing interpolates it from
clipped photosites. That frame carries the most weight (k = 4) and those pixels were unclipped themselves, so they
kept it, and bright windows took a magenta tinge.

A frame's weight now goes by its brightest channel within 1 px (3 × 3; wider margins changed nothing more). On two
real interior brackets (ILCE-7M3, 0 / −2 / +2 EV), bright pixels whose green against red and blue is over 10 % below
the darkest frame's, in camera RGB: dining room 0.32 % → 0.08 %, kitchen 2.86 % → 0.28 %. Rendered at −3 EV with
main's colour tags (#1123), window pixels with G < 0.85·min(R, B) go 50 → 3 and 16 → 10 (the darkest frame alone:
2 and 205); most of the visible magenta those brackets showed before came from the missing colour tags. Test: a
bracket whose +2 EV frame has 5 % less green next to its clipped pixels merges without the deficit (fails before:
−1.96 %).

Docs: docs/merge.md, the LR-MERGE-HDR row, docs/whats-new.md (also for the frame-order fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015gRtdWF1E9358FfzhzYqCS
2026-10-13 09:51:11 +00:00
eliets2 0cd7513a8d LR-MERGE-HDR: an HDR merge of raws keeps the as-shot white balance of its inputs (#1144)
The HDR merge has the same cause as the panorama: its result took the
reference frame's colour tags, and a raw whose white balance is only in the
vendor's multipliers (CR2, NEF, ARW...) has no AsShotNeutral in them.

merge_hdr now takes the median multipliers of all its inputs (the rule of
consensus_multipliers: the mean of the middle two for an even count; missing,
non-finite and non-positive values do not vote) and sets the result's
AsShotNeutral to their reciprocal, as stitch does. With no multipliers (DNG
sources) the reference frame's tags are kept, as before. HdrResult carries the
multipliers so that the brackets of an HDR panorama vote as frames of their own
when the merged brackets are stitched (the engine passes them on).

In the app and the CLI the neutral from starting_colour_tags (#965) is already
there for frames balanced alike, so those renders do not change.

Tests: an HDR merge of raw-style frames gets the median AsShotNeutral (odd and
even counts, missing and unusable multipliers ignored) and the written DNG says
it; without multipliers the reference frame's colour is unchanged.
2026-10-13 12:23:36 +03:00
eliets2 25314aaacf LR-MERGE-PANO: a panorama of raws keeps the as-shot white balance of its frames (#1144)
A raw whose white balance is only in the vendor's multipliers (CR2, NEF, ARW...)
has no AsShotNeutral of its own. Loading a frame dropped the multipliers and
stitch copied the first frame's colour tags, so a panorama of frames that reach
the stitcher without a neutral wrote a DNG with neither AsShotNeutral nor
AsShotWhiteXY, and a viewer had no white to start from (a strong colour cast
until White Balance > Auto).

Frames now carry the multipliers (Frame::wb_multipliers, filled from the decoded
raw; load_frame_with is split so a decoded raw can be tested without a file).
stitch takes the median R/G and B/G of the multipliers of the frames that are in
the panorama (the mean of the middle two for an even count; frames without
multipliers, or with non-finite or non-positive ones, do not vote) and sets the
result's AsShotNeutral to their reciprocal, green = 1. The source's
AsShotWhiteXY is dropped with it: a reader takes it before the neutral and the
DNG spec makes the two exclusive. With no multipliers anywhere (DNG sources) the
first frame's tags are kept, as before.

In the app and the CLI a raw without colour matrices already gets a neutral from
starting_colour_tags (#965). For frames balanced alike the new neutral renders
the same (checked in the engine crate); it differs when the camera balanced the
frames differently: the panorama follows the median instead of the first frame.

Tests: the frame carries the multipliers; the consensus for odd and even counts,
with missing and unusable values; stitching raw-style frames gives the median
AsShotNeutral, ignores frames outside the panorama and leaves the colour as it
was without multipliers; the written DNG, read back with the raw reader, has the
reciprocal AsShotNeutral and the white balance the source's multipliers give.
2026-10-13 12:23:36 +03:00
Claude 146cb7b975 LR-MERGE-HDR: the merge no longer depends on the order the frames come in
The same bracket merged in another order gave a different result: on real ARW brackets, the app's selection
order and the CLI's file order differed on 0.6 % of the pixels (up to 102 levels), while the same order gave
byte-identical files. Alignment seeds were the frames' input positions, and sums, deghost masks and brightness
ties followed them too.

merge_hdr now puts the frames in brightness order first (ties by median, then a content fingerprint, never by
position), runs everything in that order, and returns the per-frame results (EV, alignment, reference index)
in the caller's order. Test: every permutation of a bracket gives the same radiance and deghost overlay bit for
bit, and the same per-frame results (fails before: "the radiance differs").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015gRtdWF1E9358FfzhzYqCS
2026-10-13 05:57:13 +00:00
Claude 8bb084074c Merge storytold/lightcraft main into LR-MERGE-HDR (lens corrections)
Conflicts: crates/engine/src/tests_merge.rs (#1123's test landed where this branch's is; both kept),
docs/parity-checklist.md (the LR-MERGE-HDR row: both notes) and docs/whats-new.md (both entries).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015gRtdWF1E9358FfzhzYqCS
2026-10-13 05:57:08 +00:00
bflatastic 14b111f6f5 Merge PR #1344
# Conflicts:
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-13 11:01:49 +07:00
Claude 4c1dfc5b0f LR-MERGE-HDR: keep the reference frame's lens corrections in HDR merges
Merged HDR DNGs lost the camera's embedded lens correction: load_frame strips the lens opcodes from each
source (frames are merged uncorrected) and write_linear_dng wrote no OpcodeList3, so Optics ▸ Enable Lens
Corrections did nothing on the result (Sony ILCE-7M3/7RM4A, Panasonic/Leica, DNGs with their own opcodes).

Each raw frame now keeps the lens corrections it applies (the DNG 1.6 skip rule already applied),
re-expressed for the default crop develop keeps by the new Opcode::cropped: the centre moves into the crop
and coefficients are rescaled for the crop's farthest corner. HdrResult carries the reference frame's and the
DNG writes them as its OpcodeList3. Frames turned before merging (mixed orientations) and panoramas carry none.

Tests: cropped corrections give the crop of the corrected whole (WarpRectilinear, WarpRectilinear2 with and
without reciprocal, FixVignetteRadial); an HDR merge of cropped, corrected brackets, upright and vertical, gives
the same profile correction as its reference photo (fails without the fix). Real brackets not measured yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015gRtdWF1E9358FfzhzYqCS
2026-10-13 01:05:26 +00:00
PiMinusMaxandClaude Opus 5.5 d9006c96f6 LR-EDIT-COLOR-TEMP: Kelvin white balance from Sony's white-balance presets
Sony ARWs of cameras without measured colour matrices were edited on the
relative scale (6500 K / 0 = as shot). Their SR2SubIFD stores the
multipliers of the camera's presets at 2500, 3200, 4500, 6000 and 8500 K;
lightcraft_raw::kelvin_presets reads the as-shot multipliers on the curve
through them and they take the spectral matrices' place in the #730
design (as-shot Kelvin and tint only; look, Bradford adaptation and the
pixel-identical legacy upgrade unchanged).

Covered: ILCA-99M2, ILCA-77M2, SLT-A33, DSC-RX10, ILCE-3500, ILCE-7S.
Kelvin-dial shots read back 3400 -> 3396, 5200 -> 5193, 4700 -> 4708,
5600 -> 5594 K; Shade 7493 K. Off-curve constants (isotherm tilt 0.74,
tint spacing 1) measured from the 14 Sony bodies with both spectral
matrices and presets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:37:22 +02:00
lloydandClaude Opus 5.5 090553d14d LR-MERGE-HDR: merges of raws without colour matrices keep their colour (#965)
Photo Merge decoded each raw itself and wrote the reference frame's own colour
tags into the merged DNG. A CR2 (and NEF, ARW and the other raws whose colour is
fitted from the camera JPEG) has none, so an HDR merge of CR2s rendered with no
camera colour: green and about 16 L* dark.

`lightcraft_merge::load_frame_with` takes the raw's colour tags from the caller,
and the engine passes `starting_colour_tags`: the colour the raw opens with
(`starting_colour`) as DNG tags. That is one forward matrix with a colour matrix
and AsShotNeutral that agree with it at the as-shot white, the look's hue/sat
table as ProfileHueSatMap and its tone curve as ProfileToneCurve. Raws with
their own matrices (DNG) keep their tags. The look's chroma curve has no DNG
tag and is left out.

On two CC0 CR2s (EOS 550D, 40D) an HDR merge of three copies against the photo
alone: mean CIE76 dE 26.15 -> 1.07 and 22.89 -> 0.47.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:42:18 +02:00
Exalted5373andClaude Opus 5.5 ce7b8f511e LR-IMP-FORMATS: read DNG BaselineSharpness and keep it in DNG exports
ColorData::baseline_sharpness holds the file's BaselineSharpness (raw IFD
or IFD 0; zero, negative, non-finite or above 16 is ignored) and the DNG
writer writes it back, so a conversion or smart preview keeps it. Merge
outputs leave it unset. Rendering doesn't use it yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:18:54 -07:00
Exalted5373andClaude Opus 5.5 a28dc4dbdf LR-PROF-CAMERACOLOR: read and keep DNG ProfileGainTableMap (not rendered)
Parse ProfileGainTableMap (DNG 1.6) and ProfileGainTableMap2 (DNG 1.7):
both versions, DNG 1.7 precedence (version 2 in IFD 0, then the raw IFD;
version 1 in the raw IFD, then IFD 0), u8/u16/f16/f32 gains, gamma.
Malformed tags (wrong size, zero or huge grids, non-finite or negative
gains, bad spacing or gamma) are ignored, never allocated for.

Byte order: the specification gives this tag no byte order of its own,
unlike opcode lists, so the payload is read in the file's byte order with
a big-endian fallback; a wrong-order read can't pass the size checks.
The module docs quote the DNG 1.6 "Byte Order" rule.

The map is kept in ColorData, written back by the DNG writer (version 1,
or version 2 when its gamma isn't 1) and dropped from merge outputs, whose
geometry no longer matches the capture's active area. It is not applied
to the render: Lightroom Classic renders Apple ProRAW without it.
Supersedes the parser of #273.

The CC0 iPhone 12 Pro ProRAW from raw.pixls.us joins the corpus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:18:16 -07:00
radialmonster 03bc1469f9 LR-EDIT-OPTICS-PROFILE: apply the distortion correction Panasonic/Leica record in RW2/RWL (#256)
The RW2 reader reads PanasonicRaw tag 0x0119 (16 LE words) and expresses the
camera's own correction as the equivalent DNG WarpRectilinear in OpcodeList3,
so embedded_lens() turns it into the existing EmbeddedLens/EmbeddedWarp and
"Enable Profile Corrections" applies it like a DNG's warp (no new warp path;
Convert to DNG keeps it). Rule, from our own measurements against the
embedded camera JPEGs (clean-room; ExifTool tag names only):
corrected = rho*(1 + (w8 rho^2 + w4 rho^4 + w11 rho^6)/32768), rho = r/w12
(w12 = active-area half diagonal), output = corrected / (1 + w5/32768),
on when (w7 & 0xf) == 1. Chosen on 38 GX85 photos, held out on 35 photos
from 12 other bodies / 9 other lenses; corrected renders line up with the
camera JPEG within 0.75 px at 1440 px at the corners (up to 13 % of the
half diagonal before). Implausible values leave the photo uncorrected.

The loader now removes lens opcodes before the RW2 camera-look fit (its
binned proxy needs an empty OpcodeList3); Opcode::is_lens_correction()
replaces the duplicated matches. RENDER_CACHE_VERSION 11 -> 12.
2026-10-08 00:13:58 -04:00
Brandon ThomasandClaude Opus 5.5 71eac70696 Issue #138: DNG: apply the file's own camera-profile look (HueSatMap, LookTable, ToneCurve)
Lightroom-converted DNGs carry their camera profile's ProfileHueSatMapData1/2
and ProfileLookTableData (and sometimes ProfileToneCurve). We used only the
colour matrices, so such DNGs rendered flat and muted.

- raw: new `profile` module reads the tags (DNG spec ch. 6: value-major,
  hue-middle, saturation-minor tables, wrap-around hue, tri-linear lookups,
  sRGB value encoding) from the raw IFD or IFD 0; malformed tags are ignored.
- engine: the raw loader applies the hue/sat map (interpolated by the same
  illuminant weight as the matrices) before BaselineExposure and the look
  table after it, in linear ProPhoto; a ProfileToneCurve becomes the source's
  camera tone. Without one the standard base tone is kept.
- dngwrite keeps the look with the data (conversions, smart previews, merges).
- Render cache version 7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:36:30 -04:00
Brandon ThomasandClaude Opus 5.5 bd2e756b24 Never crash: gpu and merge free of panicking calls
gpu (its render already runs under catch_unwind and falls back to the CPU):
- a failed readback (device lost, out of memory) logs, marks the GPU
  broken and returns zeros, so crate::render discards the result and
  renders on the CPU, instead of panicking;
- an unknown kernel name does the same instead of panic!;
- finish-parameter fields are looked up as Option (debug_assert on an
  unknown name) instead of panic!;
- the warp sampler matches (mode, warp) like the CPU pipeline;
- Buf::raw keeps one documented expect (Some from construction to Drop).

merge:
- pyramid building (push_pull, Blender::finish), camera graph growth,
  bundle adjustment, focal median and alignment use let-else / filter_map
  instead of expect;
- synth::bracket_dngs / pano_views return Result (a missing demo scene
  or a DNG write error is an error); lightcraft-cli synth-merge and the
  engine merge tests handle it.

lightcraft-gpu, lightcraft-merge and lightcraft-cli now #![deny]
unwrap/expect/panic/unimplemented/todo/unreachable; clippy.toml as in the
steering PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:34:53 -04:00
Brandon ThomasandClaude Opus 5.5 bc2cda97cf M13.2: Photo Merge commands, dialog with live preview, CLI
- engine: `merge.hdr`, `merge.panorama`, `merge.hdrPanorama` (bracket size from
  EXIF); MergeJob (Send) with progress/cancel, preview renders through the develop
  pipeline (deghost overlay, auto-crop frame); finish writes <name>-HDR/-Pano.dng
  next to the first source, imports, selects, applies Auto Settings / auto crop
- ui: Photo Merge dialog (HDR / Panorama / HDR Panorama) with live preview on a
  worker thread, background final merge with progress toast; context menu and
  Ctrl+H / Ctrl+M (Ctrl now means the physical Control key on macOS)
- cli: `merge hdr|panorama|hdr-panorama`, `synth-merge` for synthetic inputs
- merge::synth: procedural brackets (DNG) and panorama views; docs/merge.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:30:03 -04:00
Brandon ThomasandClaude Opus 5.5 543fdde41e M13.1: lightcraft-merge crate — HDR merge, panorama stitching core
New L3 crate `crates/merge` (no UI deps), clean-room from published papers:
- features: Harris/MOPS-style oriented patch descriptors, ANMS, ratio-test matching
- ransac: normalised DLT homographies, similarity/translation, adaptive RANSAC
- hdr: brightness ordering, auto-align (model selection), robust exposure-ratio
  estimation chained to the reference, saturation-aware weighted radiance merge,
  deghost None/Low/Medium/High with feathered masks + overlay
- pano: pairwise matching, Brown–Lowe verification, focal from homographies,
  rotation+focal bundle adjustment (LM), straightening, spherical/cylindrical/
  perspective + auto, gain compensation, max-weight seams, multi-band blending
  in log space, boundary warp, auto crop, fill edges (push–pull)
- output: LinearRaw DNG carrying source colour tags; raw crate gains Deflate
  tiles (16/32-bit float with FP predictor, 16-bit int with predictor 2)

Tests: synthetic brackets (alignment < 0.3 px, EV < 0.02, radiance error
noise-limited, ghost removal) and synthetic panoramas (relative rotations
< 0.1°, focal < 1 %, error vs ground truth, seam test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:06:00 -04:00