Offer the border fill on the merge page, experimental, with every knob on it

A Border choice beside the projection — crop to the picture, or fill it
— that redraws the preview filled so the invented pixels are seen before
they are confirmed (FR-MRG-1), greyed with the reason when the model is
not there. The job fills at half the composite's resolution in a
display-ish space (white balance, matrix, gamma; invertible) and samples
the result back into the linear DNG wherever no frame reached; the
sidecar's merge line says border filled and with which knobs.

Experimental because the fill is right in thin borders and wrong in deep
corners, where the model's Places2 prior puts clouds in sky and water
under grass; so its six knobs — working scale, edge erosion, coarse pass,
band width, mirror depth, seam feather — are sliders under the choice,
each committing a redraw, until the defaults are right.
This commit is contained in:
2026-09-19 20:41:22 +02:00
parent 104e3a106f
commit e43ae10439
7 changed files with 760 additions and 67 deletions
+26
View File
@@ -840,6 +840,17 @@ export component AppWindow inherits Window {
in property <[MergeFrameRow]> merge-frames;
in property <image> merge-preview;
in property <int> merge-projection-selected: 0;
in property <int> merge-border-selected: 0;
in property <bool> merge-fill-available: false;
in property <string> merge-fill-note: "";
in property <bool> merge-preview-filled: false;
// The fill's knobs (experimental; FR-MRG-4), in working pixels.
in property <float> merge-fill-scale: 2;
in property <float> merge-fill-erosion: 4;
in property <float> merge-fill-coarse: 4;
in property <float> merge-fill-band: 96;
in property <float> merge-fill-mirror: 48;
in property <float> merge-fill-feather: 48;
in property <bool> merge-done: false;
/// Merge the grid's selection: opens the page and starts the alignment.
callback library-merge-selection();
@@ -847,6 +858,9 @@ export component AppWindow inherits Window {
callback merge-confirm();
callback merge-abandon();
callback merge-projection-picked(int);
callback merge-border-picked(int);
/// A fill knob turned: its name and the new value.
callback merge-fill-knob(string, float);
callback merge-close();
callback import-open();
@@ -1312,10 +1326,22 @@ in property <bool> panel-visible: true;
frames: root.merge-frames;
preview: root.merge-preview;
projection-selected: root.merge-projection-selected;
border-selected: root.merge-border-selected;
fill-available: root.merge-fill-available;
fill-note: root.merge-fill-note;
preview-filled: root.merge-preview-filled;
fill-scale: root.merge-fill-scale;
fill-erosion: root.merge-fill-erosion;
fill-coarse: root.merge-fill-coarse;
fill-band: root.merge-fill-band;
fill-mirror: root.merge-fill-mirror;
fill-feather: root.merge-fill-feather;
done: root.merge-done;
merge() => { root.merge-confirm(); }
abandon() => { root.merge-abandon(); }
projection-picked(i) => { root.merge-projection-picked(i); }
border-picked(i) => { root.merge-border-picked(i); }
fill-knob(name, value) => { root.merge-fill-knob(name, value); }
close() => { root.merge-close(); }
}
+103 -1
View File
@@ -11,7 +11,7 @@
import { Theme } from "theme.slint";
import { Button, PanelHeading, Label, Value, Caption, Panel, ProgressBar } from "widgets.slint";
import { Segmented } from "controls.slint";
import { Segmented, SliderRow } from "controls.slint";
/// One source frame's line in the alignment table.
export struct MergeFrameRow {
@@ -43,6 +43,31 @@ export component MergePage inherits Rectangle {
in property <[string]> projection-labels: ["Auto", "Perspective", "Cylindrical", "Spherical"];
in property <int> projection-selected: 0;
// TRACES: FR-MRG-4
// The border: cropped to the picture, or filled with invented pixels.
// Crop is the default and the honest one; the fill is offered only
// where the device has the model, and what it invents is on the
// preview before it is confirmed.
in property <[string]> border-labels: ["Crop to the picture", "Fill the border"];
in property <int> border-selected: 0;
/// Whether the fill can be offered at all, and where it would run —
/// "TensorRT", "CPU" — or why not.
in property <bool> fill-available: false;
in property <string> fill-note;
/// Whether the preview on screen has its border filled.
in property <bool> preview-filled: false;
/// The fill's knobs — experimental, so every one is on the page: the
/// working scale (the composite's resolution over this), the coverage
/// fringe eroded, the coarse pass's reduction, the fine band width,
/// how deep the mirrored context reaches, and the feather across the
/// seam. Working pixels, at the merge's working scale.
in property <float> fill-scale: 2;
in property <float> fill-erosion: 4;
in property <float> fill-coarse: 4;
in property <float> fill-band: 96;
in property <float> fill-mirror: 48;
in property <float> fill-feather: 48;
/// The composite, once written: its name for the status line, and
/// whether Open can show it.
in property <bool> done: false;
@@ -50,6 +75,10 @@ export component MergePage inherits Rectangle {
callback merge();
callback abandon();
callback projection-picked(int);
callback border-picked(int);
/// A knob turned: its name ("scale", "erosion", "coarse", "band",
/// "mirror", "feather") and the new value.
callback fill-knob(string, float);
callback close();
background: Theme.ground;
@@ -168,6 +197,79 @@ export component MergePage inherits Rectangle {
enabled: !root.running && !root.done;
picked(i) => { root.projection-picked(i); }
}
Segmented {
label: "Border";
options: root.border-labels;
selected: root.border-selected;
enabled: !root.running && !root.done && root.fill-available;
picked(i) => { root.border-picked(i); }
}
// Said plainly, because these are pixels the camera never
// saw: what the fill is, where it runs, and that the
// composite will say it was filled.
Caption {
text: root.fill-available
? (root.preview-filled
? "The border above is filled with pixels the model invented from the picture beside them (" + root.fill-note + "). The composite records that it was filled."
: "Filling the border invents pixels from the picture beside them (" + root.fill-note + "). Choose it to see what it would do before merging.")
: root.fill-note;
wrap: word-wrap;
}
// The knobs, while the fill is experimental: turned
// under the fill, the preview redraws with them.
if root.fill-available && root.border-selected == 1 : VerticalLayout {
spacing: 4px;
Caption { text: "Experimental — the fill's knobs. Each redraws the preview."; wrap: word-wrap; }
SliderRow {
label: "Working scale";
hint: "the composite's resolution over this: 2 is half, 4 a quarter";
value: root.fill-scale; default-value: 2; minimum: 1; maximum: 8;
enabled: !root.running && !root.done;
changed(v) => { root.fill-knob("scale", v); }
reset => { root.fill-knob("scale", 2); }
}
SliderRow {
label: "Edge erosion";
hint: "working pixels of the coverage edge regenerated as fringe";
value: root.fill-erosion; default-value: 4; minimum: 0; maximum: 64;
enabled: !root.running && !root.done;
changed(v) => { root.fill-knob("erosion", v); }
reset => { root.fill-knob("erosion", 4); }
}
SliderRow {
label: "Coarse pass";
hint: "reduction of the structure pass; 1 skips it";
value: root.fill-coarse; default-value: 4; minimum: 1; maximum: 8;
enabled: !root.running && !root.done;
changed(v) => { root.fill-knob("coarse", v); }
reset => { root.fill-knob("coarse", 4); }
}
SliderRow {
label: "Band width";
hint: "the fine passes' bands from the edge outward, working pixels";
value: root.fill-band; default-value: 96; minimum: 16; maximum: 384;
enabled: !root.running && !root.done;
changed(v) => { root.fill-knob("band", v); }
reset => { root.fill-knob("band", 96); }
}
SliderRow {
label: "Mirror depth";
hint: "how far into the picture the mirrored context reaches";
value: root.fill-mirror; default-value: 48; minimum: 4; maximum: 512;
enabled: !root.running && !root.done;
changed(v) => { root.fill-knob("mirror", v); }
reset => { root.fill-knob("mirror", 48); }
}
SliderRow {
label: "Seam feather";
hint: "working pixels inside the real edge blended with the fill; 0 is a hard cut";
value: root.fill-feather; default-value: 48; minimum: 0; maximum: 256;
enabled: !root.running && !root.done;
changed(v) => { root.fill-knob("feather", v); }
reset => { root.fill-knob("feather", 48); }
}
}
}
// --- the frames ----------------------------------------