The merge job and its page: a selection to a panorama DNG, confirmed first
dr_ui::merge is the orchestration with no interface in it: decode each frame to sensor data and build its graph as a session would (orientation, lens profile); render each through the camera-space tap at proxy size and detect keypoints there, so the alignment is measured in the undistorted frame the tiles are rendered in; align; solve one gain per frame from the proxies' overlaps; draw the aligned set in colour for the page; then wait. Nothing is written until a Decision arrives (FR-MRG-1). The merge writes a linear DNG through the outbox with a destination record, so the drain puts it beside its sources on a folder library and a server alike, and the library rescans (FR-MRG-3). merge.slint is the page, on the import page's model: the alignment table with a failed frame named on its row and the button held off (FR-MRG-5), the preview, the projection choice, Stop and Back. A "Merge to panorama" button joins the grid's selection bar at two frames. Headless, the example produces the fixture's 22 993 x 5 980 DNG in 45 s on the reference desktop, exposures balanced across the stop of drift.
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
// TRACES: FR-MRG-1 | FR-MRG-4 | FR-MRG-5 | FR-MRG-7
|
||||
// The merge page: a panorama proposed, then confirmed.
|
||||
//
|
||||
// Same shape as the import page — a header with the one verb, a progress
|
||||
// bar under it while work runs, a scrolling body — because that is the
|
||||
// shape a page that runs a job has in this application, and the merge is
|
||||
// one. The difference is the pause in the middle: the job aligns, this
|
||||
// page shows what it found, and nothing is written until the photographer
|
||||
// presses Merge (FR-MRG-1). A frame that could not be placed is named on
|
||||
// its row and the button stays off (FR-MRG-5).
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, ProgressBar } from "widgets.slint";
|
||||
import { Segmented } from "controls.slint";
|
||||
|
||||
/// One source frame's line in the alignment table.
|
||||
export struct MergeFrameRow {
|
||||
name: string,
|
||||
/// "yaw −55.9° · pitch −1.6°", or why it is not aligned.
|
||||
detail: string,
|
||||
aligned: bool,
|
||||
}
|
||||
|
||||
export component MergePage inherits Rectangle {
|
||||
/// The stage the job is in, as a word for the header: "Reading",
|
||||
/// "Aligning", "Merging"; empty when it is waiting or done.
|
||||
in property <string> stage;
|
||||
in property <bool> running: false;
|
||||
in property <float> fraction: 0;
|
||||
/// One line of what happened last, or what is happening.
|
||||
in property <string> status;
|
||||
in property <string> error;
|
||||
|
||||
/// The alignment, once there is one.
|
||||
in property <bool> aligned: false;
|
||||
/// Whether every frame was placed — the merge can only go ahead then.
|
||||
in property <bool> complete: false;
|
||||
in property <string> summary;
|
||||
in property <[MergeFrameRow]> frames;
|
||||
/// The preview of the aligned set, drawn by Rust from the proxies.
|
||||
in property <image> preview;
|
||||
|
||||
in property <[string]> projection-labels: ["Auto", "Perspective", "Cylindrical", "Spherical"];
|
||||
in property <int> projection-selected: 0;
|
||||
|
||||
/// The composite, once written: its name for the status line, and
|
||||
/// whether Open can show it.
|
||||
in property <bool> done: false;
|
||||
|
||||
callback merge();
|
||||
callback abandon();
|
||||
callback projection-picked(int);
|
||||
callback close();
|
||||
|
||||
background: Theme.ground;
|
||||
|
||||
FocusScope {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
init => { self.focus(); }
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
Rectangle {
|
||||
height: 44px;
|
||||
background: Theme.surface;
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: Theme.gap;
|
||||
padding-right: Theme.gap;
|
||||
spacing: Theme.gap;
|
||||
|
||||
Button {
|
||||
text: "‹ Back";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.close(); }
|
||||
}
|
||||
|
||||
Value { text: "Panorama"; }
|
||||
|
||||
Rectangle { horizontal-stretch: 1; }
|
||||
|
||||
if root.running: Button {
|
||||
text: "Stop";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.abandon(); }
|
||||
}
|
||||
|
||||
// The one verb, and it is off until there is a complete
|
||||
// alignment to confirm: pressing it is the confirmation.
|
||||
if !root.running && !root.done: Button {
|
||||
text: "Merge";
|
||||
primary: true;
|
||||
y: (parent.height - self.height) / 2;
|
||||
enabled: root.aligned && root.complete;
|
||||
clicked => { root.merge(); }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
y: parent.height - 1px;
|
||||
height: 1px;
|
||||
background: Theme.rule;
|
||||
}
|
||||
}
|
||||
|
||||
if root.running: ProgressBar {
|
||||
fraction: root.fraction;
|
||||
indeterminate: root.stage == "Aligning";
|
||||
label: root.stage;
|
||||
}
|
||||
|
||||
if root.error != "": Rectangle {
|
||||
height: 32px;
|
||||
background: Theme.surface;
|
||||
HorizontalLayout {
|
||||
padding-left: Theme.gap;
|
||||
padding-right: Theme.gap;
|
||||
Caption { text: root.error; warn: true; overflow: elide; }
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
vertical-stretch: 1;
|
||||
viewport-height: content.preferred-height;
|
||||
|
||||
content := VerticalLayout {
|
||||
width: 100%;
|
||||
padding: Theme.gap;
|
||||
spacing: Theme.gap;
|
||||
alignment: start;
|
||||
|
||||
// --- the proposal --------------------------------------
|
||||
if root.aligned: Panel {
|
||||
PanelHeading { text: "Alignment"; }
|
||||
|
||||
// The set as it will lie on the chosen surface. Drawn at
|
||||
// proxy resolution, which is the resolution the alignment
|
||||
// was found at: ghosting here is the alignment's error,
|
||||
// and what it looks like is what the photographer is
|
||||
// confirming.
|
||||
if root.preview.width > 0: Rectangle {
|
||||
height: min(320px, parent.width * root.preview.height / max(1, root.preview.width));
|
||||
background: Theme.rule;
|
||||
Image {
|
||||
source: root.preview;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
image-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
Caption { text: root.summary; wrap: word-wrap; }
|
||||
|
||||
Segmented {
|
||||
label: "Projection";
|
||||
options: root.projection-labels;
|
||||
selected: root.projection-selected;
|
||||
enabled: !root.running && !root.done;
|
||||
picked(i) => { root.projection-picked(i); }
|
||||
}
|
||||
}
|
||||
|
||||
// --- the frames ----------------------------------------
|
||||
Panel {
|
||||
PanelHeading { text: "Frames"; }
|
||||
|
||||
for row in root.frames: HorizontalLayout {
|
||||
spacing: Theme.gap;
|
||||
Label {
|
||||
text: row.name;
|
||||
width: 160px;
|
||||
overflow: elide;
|
||||
}
|
||||
Caption {
|
||||
text: row.detail;
|
||||
warn: !row.aligned;
|
||||
overflow: elide;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
}
|
||||
|
||||
if root.frames.length == 0: Caption {
|
||||
text: "Reading the frames…";
|
||||
}
|
||||
}
|
||||
|
||||
if root.status != "": Caption {
|
||||
text: root.status;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user