A queued export could not see the server, so its name check always
answered "free" and the upload PUT over whatever was there: Increment
and Skip behaved as Overwrite on Nextcloud, and two exports of the same
name queued before either uploaded landed on one file.
The batch now names around what the album records of earlier exports
and what the outbox already holds for that folder. The outbox record
carries the policy, and the drain lists each destination folder once
and applies it against what the server holds: Increment steps past a
taken name and re-points the album's row, Skip drops the entry. A
record without a policy (older builds, a merge's composite) is sent as
named, as before. The album is recorded before the drain starts so a
rename has a row to move.
The presets menu at the foot of the tool rail is a PopupWindow, and
showing a popup takes focus off the develop view until it closes. The
menu held nothing focusable, so Android's Back gesture, pressed to
dismiss it, found no focus item, went unanswered, and the platform
closed the application. Slint closes a popup on Escape by itself but
not on Back.
The menu now holds a key scope, as the film list does, that closes it
on Back or Escape. The next Back leaves develop for the grid.
The merge weighted every overlap pixel by its distance from each frame's
edge, a 200 px linear cross-fade. Anything the frames disagreed on —
parallax in the near foreground, grass in the wind, a walker — came out
twice at half strength: a soft double edge at 1:1.
dr_pano::seam picks, per output texel at proxy resolution, which frame a
pixel comes from. Where a new frame overlaps the composite the cost is the
gain-corrected difference plus local detail plus nearness to either
frame's edge, taken as the worst over a small window, and the cut is a
dynamic-programming path across the overlap. merge.wgsl weights each frame
by its tent-filtered share of that map, a 64 px blend that follows the
seam, with the edge feather kept as the fallback. The page's preview uses
the same map, and examples/merge.rs takes --feather-only for comparison.
WhatsApp strips every EXIF tag and names the file "WhatsApp Image
2023-06-15 at 07.00.42.jpeg"; Windows Phone, Android cameras and
darktable's import put the date in the name too. Those images sorted
after everything else and were absent from the timeline.
name_dates reads a date (and a time, when one follows) from the file
name, then from the innermost folder that states one. A sequence number
after a date is not read as a time, and a bare year folder is not a date.
EXIF always wins: only examined rows still undated are filled.
The sweep and the metadata repair fill as they mark an image examined,
and the open backfill fills catalogs examined by earlier builds. On the
reference library that takes 274 undated images to 10; the no-op case is
a seek on images_captured, 0.6 ms an open.
Import was switched off on Android: `imports_supported` was true only for
`target_os = "linux"`, and its comment said Android has no path to read a
card by and nowhere to write the copies. Neither holds. With "all files
access" (MANAGE_EXTERNAL_STORAGE, API 30) an app reads the root of an SD
card or a USB card reader by path, `/storage/9C33-6BBD`, and the importer
only ever writes into its own staging directory, which is a plain
directory on Android too. So the engine runs unchanged; what was missing
was finding the card and the permission.
- The manifest declares MANAGE_EXTERNAL_STORAGE, and
READ_EXTERNAL_STORAGE up to API 29 with requestLegacyExternalStorage,
which is the same access on 28 and 29.
- Cards.java lists the mounted non-primary volumes through
StorageManager and opens the system "All files access" page for this
app. dr_ui::cards is the JNI bridge, through saf's helpers.
- The import page on Android asks for the permission with an "Allow
access" button until it has it, rather than showing an empty list that
reads as "no card", and watches for the grant so the list fills in when
the user comes back from settings.
Google Play restricts this permission to file managers and the like;
DarkRoom is sideloaded, so that does not apply.
Four bar handles at the midpoints of the sides, each moving only its
own side along the axis across it. The overlay reports an edge as 0.5
on the axis it does not move, so the anchor Rust takes is the middle
of the far side.
Under a ratio lock the dragged axis leads. with_aspect grew the short
axis onto the ratio, which for an edge pulled inward made the untouched
axis the leader and pushed the edge straight back out.
A crop handle straddles the picture's edge. With the photograph fitted
to the whole canvas, the bottom handles sat in the photo roll's band,
whose swipe handler takes every press inside it, and the bottom-left
one under the "Done Composing" row, which is declared after the overlay
and whose buttons press 44px tall. Both were drawn and could not be
grabbed: on a laptop-shaped window, one or the other for nearly any
photograph.
While composing, the canvas box is inset 16px from its clip and stops
above the band, the row's margin and the row's hit height. The band is
measured once, on the canvas, and the row reads it from there.
The README gave cargo run and nothing else. A binary run from target/
finds no models and, in a release build, no manual, because both are
looked up only where an install puts them. Spell out the Arch package,
a /usr/local install mirroring the PKGBUILD, the Windows installer
cross-build and where it lands, and the Android APK.
The panorama scene now ends in the grid, on the composite in its wide
cell beside the twelve frames it was made from, with the thumbnail the
merge made: the picture is taken straight after the merge, before any
rescan could have found the file. The manual's library section says how
a panorama's cell is sized and packed, and its panorama section that the
composite is in the grid the moment it is written. panorama.md §9 records
the race that kept it out, the catalogue-then-register path that
replaced it, how the thumbnail is developed, and the layout's classes.
A panorama the merge did not make, a stitch from another program or a
phone's sweep, never had a width or height in the catalog, so it could
never be given a wide cell. The header read that dates a photograph now
records its size as it is seen, orientation applied, alongside the date.
One lookup, natural_span, maps a photograph's aspect to the columns its
cell spans, and the same number names its thumbnail class, Wide2, Wide3
or Wide4, 512 pixels of long edge per column, so a 4:1 panorama is as
sharp across four columns as a frame is in one. The boundaries are
where the two neighbouring cells would leave the same share of
themselves undrawn, sqrt(s(s+1)): 2.45 and 3.46, with the first at 1.9
so a 3:2 frame stays a frame. A grid too narrow for the class falls
back to the widest that fits, the tablet gives the whole row, and a
cell asks for the class it is actually drawn at: its span, but never
more than its own class. The merge renders each wide class up to the
composite's own, which covers every fallback.
A 4:1 composite drawn in one square cell is a strip a few pixels high.
A photograph about twice as wide as it is tall (1.9 and up) now spans
two columns, three from 2.9, with a thumbnail class of its own whose
long edge is sized for that width; on the tablet, or where the columns
are too few to put it beside anything, it takes the whole row.
Rows are computed in one place, library_ui::layout. The grid is a
lattice of slots: each cell is drawn at the slot Rust gives it, and a
wide cell that would not fit in what is left of a row starts the next
one, leaving the gap empty so the grid still reads in capture order.
The scrollbar spans the slots, a scroll reports a slot that the layout
turns back into an ordinal, and scrubs, restores and the cursor go
through the same conversion. Up and down step by rows through the
layout rather than by a row's worth of ordinals; left and right, a
shift-click's run, burst folding and the timeline are ordinal-based
and unchanged.
The window's own read carries each photograph's w and h, so the cells
know their shape with no query per cell. Where the wide ones sit in
the whole list is one query, run when what the grid lists changes or
when a window finds the layout out of date, and a library with no
panorama answers it from a partial index created on first use
(images_wide), not a schema bump. The merge makes the wide thumbnail
for a wide composite along with the others.
The DNG a merge writes has no embedded preview, and an embedded preview
is all the grid's thumbnail path reads, so a composite stood in the grid
as a blank cell until it was opened. Reading an 800 MB file back to make
one would cost what the merge already has in hand.
The bands are box-reduced as they are written, after the border fill,
into a copy 4096 pixels long. That copy is written as a linear DNG in
memory with the composite's own profile, header and crop, and opened
through open_session, the function develop opens every file with: the
same decode, the default graph and view transform, the as-shot white
balance and the conversion to the display's space. The grid and large
thumbnails are rendered from that session, staged beside the payload
before the rename releases it to a drain, and put in the store under the
file id once the upload has learned it. Until then the grid draws them
from memory, so the cell is not blank while the file is uploading.
A test develops a synthetic composite both ways, the whole file as
develop opens it and the merge's reduced copy, and holds the thumbnail's
mean, 95th and 99.5th luma percentiles to within 3-4 levels of develop's
render; the naive balanced-and-gamma picture the merge's preview draws
misses by 13.
A finished merge drained the outbox and started a rescan beside it. The
scan raced the upload: on a folder library the 800 MB copy was still
running when the folder was listed, on Nextcloud the upload takes
minutes, and either way the listing lacked the composite, recorded the
folder's validator, and nothing looked again until the next sync pass.
The job now reports what the catalog needs (the name, the size of the
picture it opens on, the capture time it wrote into the DNG) and the
library writes the row at once, in one transaction, keyed where the scan
will list the file; a composite with no time of its own takes its
sources' earliest. The grid reloads and shows it beside its sources.
The upload then gives the row what only the server knows: after sending
a file the catalog already has a row for, it lists the folder once, takes
the file id the server assigned, and records it (and, once the merge
makes them, the thumbnails waiting beside the payload) under that id.
Every drain now rescans when something landed in the library, after the
upload rather than beside it. A second merge of the same frames is no
longer named over the first: the name is checked against the catalog's
names in that folder, which is all that knows it once the outbox is empty.
A finished merge drained the outbox and the sync pass that followed
drained it again beside it: each read the 800 MB composite into memory
and sent it, and the later one found its record cleared underneath it
and logged the file as missing. Drains now take a lock and the one that
waited finds the queue empty.
The merge writes its outbox record before the DNG, which takes minutes,
and a drain that ran in the meantime took whatever lay at the record's
name: a sync pass that fired mid-merge uploaded the first part of the
composite and cleared the record. The file is now written as x.dng.part
and renamed into place once the last strip is in; the drain skips a
record whose payload does not exist yet.
A design draft for the learned stage outstanding.md §3 lists as missing:
a joint demosaic-and-denoise network that runs on the mosaic, in the
slot architecture.md §5.2 reserves for it, with its result kept as a
cache rather than a new file in the library. It covers the model and
tiling, synthetic training pairs from the library and a per-body noise
calibration, evaluation on real pairs, the Amount control, speed on the
tablet, X-Trans, and the decisions still open. Nothing in it is built;
figures marked "estimate" wait for the measurements that replace them.
presets-film.gif came from the tree before preset-tree landed, so it
opened a flat list; the scene now walks Film › Black and white under
the folders, on the scene-referred pipeline. presets.png and
presets-menu.png re-recorded identical: preset-tree recorded them on
D19 already.
A 22 927 × 8966 Lightroom panorama of a glacier, opened in develop:
the wheel in from fit to the peaks, a slow pan along the ridge at that
zoom, back out, then 1:1 with a double-click and fit with another. The
scene borrows the file from outside the demo library, copies it into a
panorama folder, restarts so the scan finds it, and deletes it and the
sidecar the app writes beside it before restarting again; it is
registered last so no other scene sees it.
The film is fine ice and rock in every frame and was 22 MB at the
usual 960 px and ten frames a second, so a scene can now name its own
GIF size (GIF_SIZE, read by record.sh through `scenes.py gif`); this
one is 640 px at eight, 8.7 MB.
D19 changed how every raw photograph renders, so every picture was
stale; all forty are recorded again from this tree. Beside the ones
already in the page there is now one for Tone Mapping: an alpine frame
whose clouds sit near white, its White Point raised to bring them back
and lowered for a brighter picture, its Contrast raised, then Before.
The panorama film unticks the first frame and ticks it back, so the
Frames list and the re-solve show; it no longer films minutes of
"Reading the frames", cutting from the first seconds of the job to the
alignment. The manual's presets menu opens beside the rail, not beside
the photograph.
The develop paragraph counted eighteen operations fused into one
dispatch, before the view transform made nineteen and a detail stage
made the fusion two passes, and said nothing of editing on the scene
with Tone Mapping last. Presets are now a menu on the tool rail, a
panorama frame can be left out, and a linear DNG larger than one
texture develops. The requirement count and coverage are
traceability.md's (193, 85.5%), and tiled rendering is no longer
wholly unbuilt. outstanding.md gains its sweep note for 0.19.0 and
says in §11 what the frame choice changed.
ARCH §5.3 still described only a tile cache nobody built; it now says
what 0.19.0 tiles and what it does not: a linear DNG past PROXY_EDGE
opens on a reduced copy, a finer render samples a full-resolution
window, and the export is cut into halo-grown tiles. display-and-
extension.md's FR-DSP-2 row said absent, outstanding.md said the halo
had nothing to read it and that such a file fell to the embedded
preview.
rawler now builds from third_party, which ARCH's stack table and §3.2,
the root Cargo.toml's comment ("two upstream crates ... for Android")
and third_party/README.md's bump procedure did not know; the README
also names each vendored crate's licence. panorama.md and the manual
say a composite this wide develops and exports.
panorama.md still had a frame that did not fit end the job, and FR-MRG-5
still said the merge stops; since 621a6b83 the frame is named on its
row and unticked, and the rest are solved again from the pairs already
matched. §15 records the split of align into match_pairs and solve and
why re-aligning a subset from scratch moved every RANSAC seed, and the
rule e98b1def gave a blown sample, which §8's "nothing about being a
RAW is lost" now mentions. The manual says where the boxes are and
what an unplaceable frame does to Merge.
D19 retired the per-body base curve, moved the matrix ahead of the
edits and the film into the view transform's place, but a dozen doc
comments still listed the curve among what a pixel passes through, or
said the film skipped it. The detail stage's module doc still drew the
matrix after the edits and the last detail pass encoding, which the
view pass took over. The film crate's README gave the base curves as
its reason for being data, and the ops README's list of hand-written
nodes had neither the view transform nor three of the five kernels.
FR-MRG-2 gave the base curve as why the merge cuts below the profile;
the view transform is why now. The decision table still said colour
defaults were a per-body curve, and FR-DEV-3j said only the default
view transform skips a JPEG, where the node skips one whatever its
sliders say. frame-budget.md records the view pass as unmeasured.
The presets menu and sheet listed every preset under flat section
headings, seventy rows to scroll past. They now list folders, closed
until opened, with how many presets each holds; opening one shows what
is inside it, folders and presets indented beneath.
A category is spelled in the name: "Portraits/Warm skin" is Warm skin
in a Portraits folder under Yours. The file format does not change, so
an older build lists the whole path as the name; renaming a preset is
how it moves, and saving or renaming into a folder opens the way to it.
A Lightroom import names what it reads after the folders below the one
chosen, and a "/" in a displayed name becomes "∕" so it files nothing.
The shipped film sections become Film › Colour, Cinema and Black and
white.
The tree is built and flattened in Rust (PresetTree), each row carrying
its depth, and which folders are open is remembered for the session.
A PopupWindow keeps the size it was shown at, so a folder opened in the
menu pushed its contents under "Save or manage…"; the menu is shown
again after each toggle to take its new height. That is a function on
the rail because Slint 1.17 generates Rust that does not compile for a
popup's close() reached from inside the popup. The sheet's list takes a
preferred height of up to 400px, since a Flickable reports next to
nothing and an opened folder showed three rows.
The manual describes the folders and naming. Its pictures show the menu
with Film › Colour open, and a black-and-white stock applied from Film
› Black and white; the scenes aim popup rows from the rail's entry, and
pick the menu's "Film" over the develop column's film chooser.
The test demanded a full halo on every side except the frame's own
edge, but a tile one step in from it is grown to the edge and no
further, exactly where the untiled render stops too.
FR-DSP-2 and NFR-RES-2 said tiling was unbuilt; the export half of it
now exists for a linear DNG past one texture. The interactive path and
spike S6 are unchanged.
A 22927×8966 Lightroom panorama opened as its embedded preview with
develop withheld, because no texture could hold it. DevelopSession now
opens a linear DNG past PROXY_EDGE (8192) on a box-reduced copy, and
keeps the full resolution on the CPU. The canvas at fit, the thumbnail,
the histograms and the masks work from the copy; a render finer than
it — the canvas zoomed in, a tile of the export — samples a window cut
from the full resolution, kept while the view stays inside it. The
export renders in halo-grown tiles of 4096 and assembles them.
On the panorama: decode 1.6 s, open 210 ms, canvas 37 ms, a zoomed
window 130 ms, the full-size export 5.9 s.
DemosaicedImage::linear_rgb16_window uploads part of a linear DNG, or a
box-reduced copy of it, and says where it sits in the frame; size() now
reports the frame and texture_size() the texels, and the fused pass
writes the window into the shader's uniforms. EditGraph::source_region
finds the part of the source a view reads, and tiles::plan cuts a render
too large for one texture into halo-grown, grid-aligned tiles.
The GPU test renders frames a tile at a time from their own windows and
compares them with the whole: identical for point operations, within one
code value when straightened with clarity on.
A frame fraction was taken of the render's short edge, and render_scale
folds the zoom into the region the render stands for. Zoomed to 1:1 on a
corner, clarity, texture and dehaze drew a quarter of the halo the export
gets, though the docs promise they preview honestly at any zoom.
RenderScale now carries the whole framed frame (within), and a frame
fraction is measured against it; at fit nothing changes.
A photograph larger than one texture has to be developed from a part of
it or from a reduced copy of it. The generated prologue measured the frame
by the bound texture, so either would have moved every crop, warp and
grain seed. Two uniform vec4s now say which part of the frame the texture
holds and how large the frame is; the sampler maps into the texture
through to_window, and a texture holding the whole frame samples exactly
as before ((uv - 0) / 1 is uv to the bit).
rawler's allocation guard is sized in samples but worded in pixels, and
a linear DNG passes width × 3. A 22927 × 8966 Lightroom panorama was
refused as ">50000 px wide", and develop fell back silently to the
embedded preview. Route rawler through third_party with the guard at
1.5 G samples and 200 000 per axis.
The crate as crates.io publishes it, minus .cargo-ok, its Cargo.lock and
data/testdata (13 MB of sample files only its own tests read). Not yet
routed through [patch.crates-io]; the next commit is the patch.
The Light group gains `Tone Mapping`, and a film stock now takes its
place at the end of the chain rather than sitting after exposure (D19).
Say what the two sliders do and why every adjustment above them works on
the scene, highlights beyond white included. The pictures still show the
old rendering and are re-recorded separately.
The tone curve clamped its input to [0, 1] and its output back to 1.0
around a 2.2 gamma, mid-chain — master and per-channel both. So any
edit with a curve made every scene value above 1.0 the same number
before the view transform ever saw it: D19's fourth finding. Beyond its
last point the curve now continues along its last span, whose secant
is the tangent the spline already gives that point, so the join is
smooth and an identity curve stays the identity to any height. The only
clamp left is the floor at zero, where there is no light to curve.
FR-DEV-2's acceptance test is how the rule stays true.
`scene_referred_until_the_view` wraps every point operation, at
non-neutral settings, between a gain of sixteen and one of a
sixty-fourth, with an identity in the view transform's place, and
renders a ramp from 0.4 to 15.2 times sensor saturation through it. The
output must still increase with the input and still separate the top of
the ramp. Run against the previous commit's tone curve it fails with
[21, 29, 33, 33, 33, 33, 33, 33]; every other operation already passed.
It renders on a device because dr-pipeline has none, and the spec's
pointer to it says so.
A film stock ran at order 25, after white balance and exposure, and
everything after it — contrast, the curves, the colour mixer, the
grading, every mask layer's tone — acted on the film's display-referred
output, as though the frame had been scanned and then worked on. That
was a display-referred rendering in the middle of the chain, which D19
removes (FR-DEV-3f, FR-DEV-3j).
`film_sim` is now in `Stage::View` beside `view_transform`. While a
stock is loaded the composer emits it in the view transform's place and
not the sigmoid; otherwise the sigmoid. So every edit is a decision
about the exposure the negative receives, and the film is the last
thing that happens to the picture — after the detail stage too, in the
view pass, which already binds the film's tables, the mask array and
the grain's source position. Its per-layer settings blend there as they
did in the fused pass.
`op_renders` goes: a rendering is chosen, not suppressed, and the only
render with no view transform is the camera-space tap. The YAML order
moves to 190 so the panel reads in pipeline order; the stage, not the
number, is what places it.
Existing edits that combine a stock with tone or colour operations now
render differently: those operations used to act on the print, and now
act on the scene.
The fused pass stops at "linear working values" when a sharpener, a
blur or a repair follows, and the detail passes convolve what it hands
on. Until now it handed on the rendering: the base curve, and since the
last commit the view transform, ran before the store. So every kernel
worked on display-referred values while its comments promised the
opposite — D19's second finding.
A fused pass composed for a detail stage now stops before the view
transform, and carries a second shader, `ComposedShader::view`, composed
from the same inputs. It runs the same prologue, for the positions a
fragment reads (a film's grain seeds from `source_px`) and the corners
it blacks out, takes its colour from the detail stage's result bound
where the sample cache would be, and runs the view transform, the
output transform and the mask reveal. `render_detailed` dispatches it
after the last detail pass, in the same encoder.
So no detail pass encodes any more. Every pass writes an intermediate,
the last one included, which retires three things that existed only to
make the last pass encode: `writes_output` and the runner's second
layout, the body-less resolve pass for an active kernel with nothing to
draw at this scale, and capture sharpening's pass-through, which now
emits no pass at all. An empty chain is a whole render: the view pass
reads the fused result directly. The detail stage no longer takes an
output space either, so `compose_detail_for` folds into
`compose_detail` and the space is named once, on the fused half.
The cost is one full-render read and write per frame when a detail
stage exists, and a third intermediate for a one-pass chain.
FR-DEV-3j gives the view transform two controls, contrast and the white
point in stops above middle grey, persisted and held per mask layer like
any other setting. A scene-referred pipeline whose white point cannot be
moved hands the photographer a shoulder they cannot place.
`view_transform` is a hand-written node in `Stage::View`, a new stage
the composer emits last and emits whatever the node's state: a neutral
operation is otherwise left out of the shader, but a photograph with no
view transform is a scan. "Active" keeps meaning "moved from the
defaults", so an untouched photograph writes nothing for it and
`every_node_starts_neutral` still holds. A caller whose chain holds no
view operation gets a default one.
The composer's loop becomes an ordered list of steps — camera nodes, the
matrix, scene nodes, layer-only nodes, the view — so each is emitted in
exactly one place. The base curve could not be a node because it
belonged to the camera; the ops README records why that argument went
with it (D19). The panel shows it in the Light group as "Tone Mapping".
Tests that counted the blocks of a neutral graph now count one, the
view transform, and the two chain-wide tests that load a film expect the
view transform to be absent, since a stock replaces it.
The base curve was a five-point spline on the unit square, flat past its
last point: every value above 1.0 left it as the same number, per
channel. Exposure and highlight recovery put values up there, and the
curve threw them away, then handed the result on as though it were
still scene-linear. The six per-body curves were also, by their own
file's account, hand-tuned shapes rather than measurements, and not
enough is known about where they came from to keep them (D19).
In their place, one view transform for every body (FR-DEV-3j): a
log-logistic sigmoid per channel, with the middle channel put back
between the other two so a hue survives the shoulder. Its two free
constants are solved from two conditions rather than set: scene grey
0.13, where the retired default curve put it, lands on display 0.18,
and the scene white four stops above grey lands on 1.0. So a highlight
a stop past sensor saturation still rolls into white, and the midtones
stay within 0.26 EV of the retired default between scene 0.03 and 1.0.
`dr_pipeline::view` holds the CPU reference and the WGSL, and the tests
there are FR-DEV-3j's acceptance criteria.
It is still fixed and still in the fused pass's tail, so a detail stage
still sees rendered values; the next commits make it an operation and
move it after the detail stage. It is skipped for a JPEG, as the base
curve was, and absent from the camera-space tap.
The base curve's database, its lookup and its twelve uniform slots go.
`RawImage` and `DemosaicedImage` lose the field, and the GPU test that
proved a curve reached the shader is replaced by one that renders the
view transform against the CPU reference and shows two highlights above
1.0 still render apart. The JPEG-and-sensor test now asserts the two
differ by exactly the view transform, where before an identity fixture
curve had made them match.
Every point operation ran in camera RGB and the camera matrix came after
them all, against the order ARCH §5.2 draws. So `luminance()` applied
Rec.709 weights to a body's own primaries, a band in the colour mixer
was a different hue on every make of sensor, and the vibrance skin guard
tested channel order in a space where skin does not have one.
Operations now declare a `Stage`. White balance is the only camera-stage
node — its multipliers scale the sensor's channels, and after a matrix
that mixes them the same numbers are a different correction — and says
so with `stage: camera` in its YAML, a key both the build-time generator
and the load-time declared op read. The composer emits the camera nodes,
then the matrix, then the rest, each group in graph order; an empty
chain still gets the matrix.
Film simulation stops converting out of camera space itself, since it is
now handed working-space colour like every other scene node. The base
curve stays where it was, after the operations, and so now acts on
working-space colour; the next commit replaces it (D19).
The spec missed Ansel, and with it Aurélien Pierre's argument that a
display-referred curve early in the pipeline throws away what every
later stage needs. Read against that argument, the code broke it four
ways: the base curve was flat past its last point and so clipped every
recovered highlight; the detail stage was handed curved, clipped values
while its comments promised linear ones; the edits ran in camera RGB
because the matrix had drifted to after them, against ARCH §5.2; and the
tone curve clamped to [0, 1] around a 2.2 gamma mid-chain.
D19 records the decision: unbounded scene-linear colour from the matrix
to one view transform, last, after the detail stage. FR-DEV-3j specifies
that view transform as an adjustable operation (contrast, white) with a
default fitted to where the retired default curve put middle grey.
FR-DEV-3e retires the per-body base curves, whose own file called them
hand-tuned shapes of unknown provenance. FR-DEV-3f makes a film stock
the view transform when one is chosen, instead of a rendering at order
25 that the later edits acted on. FR-DEV-2 gains the acceptance test
that holds the rule, and ARCH gains §6.14 and the corrected §5.2 order.
A frame that did not fit ended the job with its name, and the only way
on was Back, a smaller selection and every frame read, demosaiced and
searched for keypoints again. Each row on the page now has a box. An
unticked frame is left out and the rest are solved again from what the
first pass measured.
dr_pano::align is split for it: match_pairs does the matching and the
pairwise RANSAC once over every frame (about 5 s for twelve), and solve
takes a subset of the frames and uses only the links among them (about
0.1 s). Solving a subset by re-aligning it also moved every RANSAC seed,
which are keyed on frame position, and on the fixture set that was
enough to lose a marginal link and strand a neighbour of the frame left
out.
A frame that cannot be placed no longer stops the job either. Its row
names it and Merge stays off until it is unticked; the headless example
leaves such frames out the same way, and takes --leave-out N to try it.