Merge: a histogram that reads the sensor, beside the one that reads the frame
FR-CULL-3's other two bullets. What existed was a display histogram tagged FR-DSP-7, counting AdjustPass's 8-bit output with r == 255 clipping counters -- it says a highlight is gone precisely where this requirement needs it to say the highlight is recoverable. The new reduction runs over the demosaiced scene-linear texture on a stops-below-saturation axis: camera-native, unbalanced, unmatrixed, uncurved, normalised by the sensor's own black and white levels, so 1.0 is saturation by construction. Four series, and the fourth is the brightest channel rather than luma, because a weighted sum of unbalanced values is a number about nothing. Cached per photograph, not per frame: nothing downstream of the demosaic can move a count. Both readings are legitimate and answer different questions, so the panel offers a choice rather than replacing one with the other. ARCH 5.5 is amended to match. It specified a pre-demosaic reduction; retaining the CFA samples costs 48 MB at 24 MP and 120 MB at 60 MP resident on every photograph opened, whether or not anyone looks at the histogram, on the platform ARCH 6.2 exists for. The spec now records two reductions, why the more complete one was not worth its cost, and what the cheaper one cannot answer: it counts pixels not photosites, it cannot see above white, and it is measured after the CFA pattern is gone. Verified: clippy -D warnings clean, 98 dr-gpu tests, 556 dr-ui tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,16 @@ export component AppWindow inherits Window {
|
||||
/// of a draft frame is a histogram of an image nobody is reading.
|
||||
in property <HistogramView> histogram;
|
||||
|
||||
/// The same photograph counted in the raw domain — the demosaiced
|
||||
/// scene-linear texture, before white balance, the camera matrix or any
|
||||
/// curve, on an axis of stops below sensor saturation.
|
||||
///
|
||||
/// Beside the display histogram rather than instead of it: the two answer
|
||||
/// different questions and the panel offers both. Unlike the one above it
|
||||
/// this is a property of the *file* and not of the render, so it survives
|
||||
/// a crop, a zoom and every slider — see `raw_histogram.rs`.
|
||||
in property <HistogramView> raw-histogram;
|
||||
|
||||
/// TRACES: FR-CULL-3
|
||||
/// Focus peaking: the marks, whether they describe *this* frame, and the
|
||||
/// three things the photographer chose. All of them are Rust's, because
|
||||
@@ -2264,6 +2274,7 @@ in property <bool> panel-visible: true;
|
||||
// answer than an honest one that is not yet scoped.
|
||||
HistogramPanel {
|
||||
data: root.histogram;
|
||||
raw-data: root.raw-histogram;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
||||
+102
-18
@@ -1,6 +1,22 @@
|
||||
// TRACES: FR-DSP-7
|
||||
// The live histogram, and what it says about clipping.
|
||||
//
|
||||
// **One plot, two readings, and a chip row to choose.** The panel draws either
|
||||
// the frame the display is about to show (FR-DSP-7) or the sensor data the
|
||||
// file holds (FR-CULL-3), from the same `HistogramView` and through the same
|
||||
// traces. They are not two presentations of one measurement: the first is
|
||||
// after white balance, the camera matrix and the whole tone chain, the second
|
||||
// is before any of it on an axis of stops below sensor saturation, and
|
||||
// FR-CULL-3 exists precisely because the first "systematically lies about what
|
||||
// is recoverable in the raw". A culling decision needs the second; an export
|
||||
// decision needs the first. Both are true.
|
||||
//
|
||||
// **So the words travel with the numbers.** The axis caption, the two clipping
|
||||
// titles and the empty-state line are fields of the view rather than literals
|
||||
// here, because they differ between the two readings — a raw saturation figure
|
||||
// drawn under a heading saying Highlights would be mislabelled exactly where
|
||||
// the difference matters.
|
||||
//
|
||||
// **Why this is hand-built when the rest of the column is generated.** The
|
||||
// develop panel is built from operation capabilities, and a histogram is not an
|
||||
// operation: it has no parameters, changes nothing about the photograph, and
|
||||
@@ -17,6 +33,7 @@
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { PanelHeading, Caption, Panel } from "widgets.slint";
|
||||
import { Segmented } from "controls.slint";
|
||||
|
||||
// One counted frame, ready to draw.
|
||||
//
|
||||
@@ -29,7 +46,11 @@ export struct HistogramView {
|
||||
// of one.
|
||||
available: bool,
|
||||
|
||||
luma: [float],
|
||||
// The aggregate trace, drawn filled behind the three channels: Rec.709
|
||||
// luma for the display reading, the brightest channel for the raw one. In
|
||||
// both cases the single curve exposure is read off — which is why it is
|
||||
// named for its role in the plot rather than for either quantity.
|
||||
overall: [float],
|
||||
red: [float],
|
||||
green: [float],
|
||||
blue: [float],
|
||||
@@ -43,6 +64,26 @@ export struct HistogramView {
|
||||
// being unable to tell the two apart.
|
||||
highlights-label: string,
|
||||
shadows-label: string,
|
||||
|
||||
// What the two clipping readouts are called. Different words for the two
|
||||
// readings — Shadows and Highlights describe a rendering, Black and
|
||||
// Saturated describe a sensor — and they belong to the reading rather than
|
||||
// to the panel for that reason.
|
||||
low-title: string,
|
||||
high-title: string,
|
||||
|
||||
// What the axis is, or what it currently says: the chip row's hint. For
|
||||
// the raw reading this is the headroom figure, which is the answer
|
||||
// FR-CULL-3 is written for.
|
||||
//
|
||||
// Kept short in Rust, and that is a layout constraint: `FieldRow` draws
|
||||
// the hint as an unwrapped Text, so its natural width becomes this panel's
|
||||
// preferred width and the develop column takes the largest of those.
|
||||
hint: string,
|
||||
|
||||
// What the plot says when there is nothing to draw. Never "0%" and never
|
||||
// an empty plot — both are claims about a photograph nobody has counted.
|
||||
unavailable: string,
|
||||
}
|
||||
|
||||
// One series, as a run of columns.
|
||||
@@ -121,7 +162,26 @@ component ClipReadout inherits HorizontalLayout {
|
||||
|
||||
// TRACES: FR-DSP-7
|
||||
export component HistogramPanel inherits Rectangle {
|
||||
/// The frame the display is about to show (FR-DSP-7).
|
||||
in property <HistogramView> data;
|
||||
/// The sensor data the file holds (FR-CULL-3).
|
||||
in property <HistogramView> raw-data;
|
||||
|
||||
/// Which of the two is on the plot. 0 is the display reading.
|
||||
///
|
||||
/// **Held here rather than pushed from Rust**, and for the reason
|
||||
/// `chosen-peaking` gives about the focus overlay: this is a way of
|
||||
/// *looking* rather than a property of a photograph. Someone culling three
|
||||
/// thousand frames switches to the raw reading once, and a mode that reset
|
||||
/// with each image would ask them to switch it three thousand times. Both
|
||||
/// views are pushed on every settled frame, so the choice needs no
|
||||
/// callback and no round trip — the raw one costs nothing to keep current,
|
||||
/// because it is computed once per photograph and cached.
|
||||
property <int> mode: 0;
|
||||
|
||||
/// The reading currently drawn. Everything below reads this and not the
|
||||
/// two above it, so there is exactly one place the choice is made.
|
||||
property <HistogramView> shown: root.mode == 1 ? root.raw-data : root.data;
|
||||
|
||||
|
||||
/// TRACES: FR-UI-2
|
||||
@@ -150,6 +210,21 @@ export component HistogramPanel inherits Rectangle {
|
||||
|
||||
PanelHeading { text: "HISTOGRAM"; }
|
||||
|
||||
// **Above the plot rather than below it**, because it says what the
|
||||
// plot *is*: a photographer glancing at a shape has to know which of
|
||||
// the two measurements they are looking at before they read it, not
|
||||
// after. Two chips wide, which fits the narrowest column the
|
||||
// application supports without the row setting the sidebar's width —
|
||||
// see `ChipGrid`.
|
||||
Segmented {
|
||||
label: "Measured on";
|
||||
hint: root.shown.hint;
|
||||
options: ["Display", "Raw"];
|
||||
selected: root.mode;
|
||||
columns: 2;
|
||||
picked(i) => { root.mode = i; }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
// Tall enough to read a shape off and no taller. The column is the
|
||||
// photographer's instrument panel, and every pixel this takes is a
|
||||
@@ -163,10 +238,17 @@ export component HistogramPanel inherits Rectangle {
|
||||
// row of pixels inside the box rather than over its edge.
|
||||
clip: true;
|
||||
|
||||
// Quarter gridlines, so a tone can be placed on the axis without
|
||||
// Quarter gridlines, so a value can be placed on the axis without
|
||||
// counting. Same construction and same weight as the tone curve's,
|
||||
// because the two plots sit in one column and any difference
|
||||
// between them would read as meaning something.
|
||||
//
|
||||
// They read as quarters of whichever axis is showing: 64 output
|
||||
// levels for the display reading, four stops for the raw one. That
|
||||
// is why the axis is named in the chip row's hint rather than
|
||||
// labelled here — one set of gridlines cannot carry two scales,
|
||||
// and drawing numbers against them would make one of the two
|
||||
// readings wrong.
|
||||
for i in [1, 2, 3]: Rectangle {
|
||||
x: parent.width * i / 4;
|
||||
width: 1px;
|
||||
@@ -174,12 +256,14 @@ export component HistogramPanel inherits Rectangle {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
// Luminance first, so it sits behind: it is the envelope the three
|
||||
// channels decompose, and a filled area drawn over a line hides it.
|
||||
// The aggregate first, so it sits behind: Rec.709 luma for the
|
||||
// display reading, the brightest channel for the raw one, and in
|
||||
// both cases the curve the three channels sit under. A filled area
|
||||
// drawn over a line hides it.
|
||||
Trace {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
heights: root.data.luma;
|
||||
heights: root.shown.overall;
|
||||
ink: Theme.plot-luma;
|
||||
filled: true;
|
||||
}
|
||||
@@ -190,21 +274,21 @@ export component HistogramPanel inherits Rectangle {
|
||||
Trace {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
heights: root.data.red;
|
||||
heights: root.shown.red;
|
||||
ink: Theme.plot-red;
|
||||
shade: 0.85;
|
||||
}
|
||||
Trace {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
heights: root.data.green;
|
||||
heights: root.shown.green;
|
||||
ink: Theme.plot-green;
|
||||
shade: 0.85;
|
||||
}
|
||||
Trace {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
heights: root.data.blue;
|
||||
heights: root.shown.blue;
|
||||
ink: Theme.plot-blue;
|
||||
shade: 0.85;
|
||||
}
|
||||
@@ -220,24 +304,24 @@ export component HistogramPanel inherits Rectangle {
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: Theme.warn-ink;
|
||||
visible: root.data.shadows-clipped;
|
||||
visible: root.shown.shadows-clipped;
|
||||
}
|
||||
Rectangle {
|
||||
x: parent.width - self.width;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: Theme.warn-ink;
|
||||
visible: root.data.highlights-clipped;
|
||||
visible: root.shown.highlights-clipped;
|
||||
}
|
||||
|
||||
// Nothing rendered yet. Said rather than shown as an empty plot,
|
||||
// which would be a claim that the photograph has no tones in it.
|
||||
Caption {
|
||||
text: "no frame yet";
|
||||
text: root.shown.unavailable;
|
||||
width: 100%;
|
||||
horizontal-alignment: center;
|
||||
y: (parent.height - self.height) / 2;
|
||||
visible: !root.data.available;
|
||||
visible: !root.shown.available;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,17 +331,17 @@ export component HistogramPanel inherits Rectangle {
|
||||
// controls below it up and back down as each image opens.
|
||||
HorizontalLayout {
|
||||
ClipReadout {
|
||||
title: "Shadows";
|
||||
figure: root.data.shadows-label;
|
||||
lit: root.data.shadows-clipped;
|
||||
title: root.shown.low-title;
|
||||
figure: root.shown.shadows-label;
|
||||
lit: root.shown.shadows-clipped;
|
||||
}
|
||||
|
||||
Rectangle { horizontal-stretch: 1; }
|
||||
|
||||
ClipReadout {
|
||||
title: "Highlights";
|
||||
figure: root.data.highlights-label;
|
||||
lit: root.data.highlights-clipped;
|
||||
title: root.shown.high-title;
|
||||
figure: root.shown.highlights-label;
|
||||
lit: root.shown.highlights-clipped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user