mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-29 04:27:30 -06:00
refactor(pipeline): group pixel finish parameters
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
use super::quick::QuickSource;
|
||||
use super::source::{DecodedSource, SourceLevel, SourceRef};
|
||||
use super::*;
|
||||
use super::{
|
||||
Arc, DevelopSettings, Hash128, Histogram, PhotoId, PreviewCache, Quality, RenderRequest, Rendered, Rgb32f, Rgba8, Source, SourceInfo, StageCache,
|
||||
};
|
||||
|
||||
/// Everything needed to render one photo, detached from the session.
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use lightcraft_develop::DevelopSettings;
|
||||
use lightcraft_geom::Orientation;
|
||||
use lightcraft_pipeline::finish::{FinishParams, MASK_TERMS, mask_terms};
|
||||
use lightcraft_pipeline::finish::{FinishParams, FinishParamsInput, MASK_TERMS, mask_terms};
|
||||
use lightcraft_pipeline::geometry::SampleMode;
|
||||
use lightcraft_pipeline::local::plane_at;
|
||||
use lightcraft_pipeline::{Plan, RenderRequest, Rendered, SourceInfo, local};
|
||||
@@ -668,7 +668,15 @@ pub fn render(
|
||||
lap("masks", &mut t, &mut cx);
|
||||
|
||||
// 5. per-pixel stage
|
||||
let mut fp = FinishParams::new(s, &plan.frame, info, w, h, plan.px_per_long, prep.air, req.space);
|
||||
let mut fp = FinishParams::new(FinishParamsInput {
|
||||
settings: s,
|
||||
frame: &plan.frame,
|
||||
info,
|
||||
image_size: (w, h),
|
||||
px_per_long: plan.px_per_long,
|
||||
air_pre: prep.air,
|
||||
space: req.space,
|
||||
});
|
||||
// soft proofing as the CPU's `finish` sets it up (an output-device table never gets here: see `render`)
|
||||
fp.proof = req.proof.map(|pr| pr.params(req.space));
|
||||
if let Some(d) = &req.display {
|
||||
|
||||
+156
-159
@@ -186,19 +186,20 @@ pub struct FinishParams {
|
||||
pub peak: f32,
|
||||
}
|
||||
|
||||
pub struct FinishParamsInput<'a> {
|
||||
pub settings: &'a DevelopSettings,
|
||||
pub frame: &'a Frame,
|
||||
pub info: &'a SourceInfo,
|
||||
pub image_size: (usize, usize),
|
||||
pub px_per_long: f64,
|
||||
pub air_pre: f32,
|
||||
pub space: OutputSpace,
|
||||
}
|
||||
|
||||
impl FinishParams {
|
||||
/// Parameters for a `w × h` render into `space`; `ev`/`air_pre` as in [`crate::Prepared`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
s: &DevelopSettings,
|
||||
frame: &Frame,
|
||||
info: &SourceInfo,
|
||||
w: usize,
|
||||
h: usize,
|
||||
px_per_long: f64,
|
||||
air_pre: f32,
|
||||
space: OutputSpace,
|
||||
) -> FinishParams {
|
||||
pub fn new(input: FinishParamsInput<'_>) -> FinishParams {
|
||||
let FinishParamsInput { settings: s, frame, info, image_size: (w, h), px_per_long, air_pre, space } = input;
|
||||
let effects = s.section_enabled("effects");
|
||||
let (clar, tex, dehaze) = if effects {
|
||||
((s.effects.clarity / 100.0) as f32, (s.effects.texture / 100.0) as f32, (s.effects.dehaze / 100.0) as f32)
|
||||
@@ -288,7 +289,8 @@ pub(crate) fn finish(
|
||||
proof: Option<crate::Proof>,
|
||||
) -> Rgba8 {
|
||||
let (w, h) = (p.img.width, p.img.height);
|
||||
let mut fp = FinishParams::new(s, frame, info, w, h, p.px_per_long, p.air, space);
|
||||
let mut fp =
|
||||
FinishParams::new(FinishParamsInput { settings: s, frame, info, image_size: (w, h), px_per_long: p.px_per_long, air_pre: p.air, space });
|
||||
fp.proof = proof.map(|pr| pr.params(space));
|
||||
if let Some(d) = display {
|
||||
fp.for_display(d, proof);
|
||||
@@ -305,7 +307,6 @@ pub(crate) fn finish(
|
||||
}
|
||||
|
||||
/// [`finish`] into 16-bit display-encoded or 32-bit float linear samples (output primaries).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn finish_deep(
|
||||
p: &Prepared,
|
||||
s: &DevelopSettings,
|
||||
@@ -317,7 +318,8 @@ pub(crate) fn finish_deep(
|
||||
) -> DeepImage {
|
||||
use lightcraft_color::transfer::srgb_to_linear;
|
||||
let (w, h) = (p.img.width, p.img.height);
|
||||
let mut fp = FinishParams::new(s, frame, info, w, h, p.px_per_long, p.air, space);
|
||||
let mut fp =
|
||||
FinishParams::new(FinishParamsInput { settings: s, frame, info, image_size: (w, h), px_per_long: p.px_per_long, air_pre: p.air, space });
|
||||
fp.proof = proof.map(|pr| pr.params(space));
|
||||
let trc = fp.out_trc;
|
||||
let samples = match depth {
|
||||
@@ -354,29 +356,7 @@ pub(crate) fn finish_with<T: Copy + Default + Send>(
|
||||
) -> Vec<T> {
|
||||
let (w, h) = (p.img.width, p.img.height);
|
||||
let p_lut = fp.lut.clone();
|
||||
let FinishParams {
|
||||
tone,
|
||||
ops,
|
||||
curves,
|
||||
vig,
|
||||
to_out,
|
||||
out_luma,
|
||||
hl,
|
||||
sh,
|
||||
clar,
|
||||
tex,
|
||||
dehaze,
|
||||
sharpen,
|
||||
sharpen_mask,
|
||||
air,
|
||||
air_pre,
|
||||
gain,
|
||||
ev,
|
||||
grain,
|
||||
..
|
||||
} = fp;
|
||||
let (hl, sh, clar, tex, dehaze, sharpen, sharpen_mask, air, air_pre, gain, ev) =
|
||||
(*hl, *sh, *clar, *tex, *dehaze, *sharpen, *sharpen_mask, *air, *air_pre, *gain, *ev);
|
||||
let FinishParams { curves, vig, to_out, out_luma, grain, .. } = fp;
|
||||
let terms: Vec<[f32; MASK_TERMS]> = p.masks.iter().map(|m| mask_terms(&m.adjust)).collect();
|
||||
let out_to_norm = fp.out_to_norm;
|
||||
let long = fp.ow.max(fp.oh);
|
||||
@@ -392,128 +372,17 @@ pub(crate) fn finish_with<T: Copy + Default + Send>(
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let i = y * w + x;
|
||||
let raw = p.img.data[i];
|
||||
let mut c = if gain == 1.0 { raw } else { raw.map(|v| v * gain) };
|
||||
let local = pixel_local_terms(p, &terms, i);
|
||||
let l_sat = local.values[11];
|
||||
let l_hue = local.values[12];
|
||||
let l_pre = p.log_l.data[i];
|
||||
let l0 = l_pre + ev;
|
||||
|
||||
// --- local (mask) contributions: alpha-weighted sums of the masks' terms
|
||||
let mut l = [0.0f32; MASK_SUMS];
|
||||
let mut tint_col: Option<([f32; 3], f32)> = None;
|
||||
for (m, t) in p.masks.iter().zip(&terms) {
|
||||
let a = m.alpha.data[i];
|
||||
if a <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
for k in 0..MASK_SUMS {
|
||||
l[k] += a * t[k];
|
||||
}
|
||||
if t[MASK_SUMS] > 0.0 {
|
||||
tint_col = Some(([t[MASK_SUMS + 1], t[MASK_SUMS + 2], 0.0], a * t[MASK_SUMS + 3]));
|
||||
}
|
||||
}
|
||||
let [l_exp, l_temp, l_tint, l_con, l_hl, l_sh, l_wh, l_bl, l_tex, l_clar, l_dehaze, l_sat, l_hue, l_sharp, l_noise, l_moire, l_defringe] =
|
||||
l;
|
||||
|
||||
// --- local Moiré (and the colour part of Noise): chromaticity towards its blur
|
||||
let mt = (l_moire + 0.5 * l_noise.max(0.0)).clamp(-1.0, 1.0);
|
||||
if mt != 0.0
|
||||
&& let Some(cb) = &p.chroma_blur
|
||||
{
|
||||
let (y, ch0, cb) = (luminance_2020(c), crate::masks::chromaticity(raw), cb.data[i]);
|
||||
c = std::array::from_fn(|k| ((ch0[k] + (cb[k] - ch0[k]) * mt) * y).max(0.0));
|
||||
}
|
||||
// --- local Defringe: desaturate purple / green fringes along edges
|
||||
let df = l_defringe.clamp(0.0, 1.0);
|
||||
if df > 0.0
|
||||
&& let Some(b) = &p.texture_blur
|
||||
{
|
||||
let k = df * defringe_weight(c, p.log_l.data[i] - b.data[i]);
|
||||
if k > 0.0 {
|
||||
let y = luminance_2020(c);
|
||||
c = c.map(|v| v + (y - v) * k);
|
||||
}
|
||||
}
|
||||
|
||||
// --- dehaze (scene linear)
|
||||
let dz = dehaze + l_dehaze;
|
||||
if dz != 0.0
|
||||
&& let Some(dark) = &p.dark
|
||||
{
|
||||
// The dark channel is a blur, not a patch minimum: near bright neighbours it credits
|
||||
// dark pixels with veil they don't hold (and a windowed render borrows the whole
|
||||
// frame's airlight), so the subtraction went past zero and crushed them to black
|
||||
// (#2504). Never attribute more veil than the pixel's own minimum channel.
|
||||
let c_min = c[0].min(c[1]).min(c[2]);
|
||||
let d = (if air_pre > 0.0 { dark.data[i] / air_pre } else { 0.0 }).min(if air > 0.0 { c_min / air } else { 0.0 }).clamp(0.0, 1.0);
|
||||
if dz > 0.0 {
|
||||
let t = (1.0 - 0.95 * dz.min(1.0) * d).max(0.12);
|
||||
c = c.map(|v| ((v - air * (1.0 - t)) / t).max(0.0));
|
||||
} else {
|
||||
let k = (-dz).min(1.0) * 0.7 * (0.35 + 0.65 * d);
|
||||
c = c.map(|v| v + (air * 0.9 - v) * k);
|
||||
}
|
||||
}
|
||||
|
||||
// --- local exposure / temp / tint
|
||||
if l_exp != 0.0 {
|
||||
let g = l_exp.exp2();
|
||||
c = c.map(|v| v * g);
|
||||
}
|
||||
if l_temp != 0.0 || l_tint != 0.0 {
|
||||
let y0 = luminance_2020(c);
|
||||
c = [c[0] * (1.0 + 0.3 * l_temp), c[1] * (1.0 - 0.22 * l_tint), c[2] * (1.0 - 0.3 * l_temp).max(0.0)];
|
||||
let y1 = luminance_2020(c).max(1e-9);
|
||||
c = c.map(|v| v * y0 / y1);
|
||||
}
|
||||
let l0 = l_pre + fp.ev;
|
||||
let (mut c, l1) = adjust_scene_pixel(p, fp, i, raw, &local);
|
||||
|
||||
// --- local tone in log luminance
|
||||
let l1 = if dz != 0.0 || l_exp != 0.0 { log_lum(c) } else { l0 };
|
||||
let shift = l1 - l0;
|
||||
let base = p.base.data[i] + ev + shift;
|
||||
let mut delta = 0.0f32;
|
||||
let (hh, ss) = (hl + l_hl, sh + l_sh);
|
||||
if hh != 0.0 || ss != 0.0 {
|
||||
let ws = 1.0 - smooth(-4.8, 0.3, base);
|
||||
let wh = smooth(-1.0, 2.8, base);
|
||||
delta += ss * 1.7 * ws * ws.sqrt() + hh * 1.7 * wh;
|
||||
}
|
||||
if l_wh != 0.0 {
|
||||
delta += l_wh * 0.8 * smooth(0.5, 3.0, l1);
|
||||
}
|
||||
if l_bl != 0.0 {
|
||||
delta += l_bl * 0.8 * (1.0 - smooth(-6.0, -1.5, l1));
|
||||
}
|
||||
if l_con != 0.0 {
|
||||
delta += l_con * 0.14 * l1.clamp(-6.0, 4.0);
|
||||
}
|
||||
let cl = clar + l_clar;
|
||||
if cl != 0.0
|
||||
&& let Some(b) = &p.clarity_blur
|
||||
{
|
||||
let det = (l_pre - b.data[i]).clamp(-2.5, 2.5);
|
||||
let mid = (-(base / 3.2).powi(2)).exp();
|
||||
delta += cl * 0.85 * det * (0.35 + 0.65 * mid);
|
||||
}
|
||||
let tx = tex + l_tex;
|
||||
let sp = l_sharp * 0.6 + sharpen;
|
||||
if (tx != 0.0 || sp != 0.0)
|
||||
&& let Some(b) = &p.texture_blur
|
||||
{
|
||||
let det = l_pre - b.data[i];
|
||||
let tame = 1.0 - 0.6 * smooth(0.4, 1.6, det.abs());
|
||||
delta += tx * 1.1 * det.clamp(-1.0, 1.0) * tame;
|
||||
if sp != 0.0 {
|
||||
let m = if sharpen_mask > 0.0 { smooth(sharpen_mask * 0.25, sharpen_mask * 0.25 + 0.15, det.abs()) } else { 1.0 };
|
||||
delta += sp * 1.3 * det.clamp(-0.8, 0.8) * m;
|
||||
}
|
||||
}
|
||||
// local Noise: smooth (or, negative, boost) small-amplitude detail, keep edges
|
||||
if l_noise != 0.0
|
||||
&& let Some(b) = &p.texture_blur
|
||||
{
|
||||
let det = l_pre - b.data[i];
|
||||
delta -= l_noise.clamp(-1.0, 1.0) * 0.9 * det * (1.0 - smooth(0.1, 0.5, det.abs()));
|
||||
}
|
||||
let base = p.base.data[i] + fp.ev + shift;
|
||||
let delta = local_tone_delta(p, fp, i, base, l1, &local);
|
||||
if delta != 0.0 {
|
||||
let g = delta.exp2();
|
||||
c = c.map(|v| v * g);
|
||||
@@ -526,9 +395,9 @@ pub(crate) fn finish_with<T: Copy + Default + Send>(
|
||||
|
||||
// --- tone map on luminance, highlight desaturation
|
||||
let yl = luminance_2020(c);
|
||||
let o = tone.apply(yl);
|
||||
let o = fp.tone.apply(yl);
|
||||
let mut d = if yl > 1e-9 { c.map(|v| v * o / yl) } else { [0.0; 3] };
|
||||
let k = tone.chroma_scale(o);
|
||||
let k = fp.tone.chroma_scale(o);
|
||||
if k != 1.0 {
|
||||
d = d.map(|v| o + (v - o) * k);
|
||||
}
|
||||
@@ -539,8 +408,8 @@ pub(crate) fn finish_with<T: Copy + Default + Send>(
|
||||
}
|
||||
|
||||
// --- colour
|
||||
d = ops.apply(d, l_sat, l_hue);
|
||||
if let Some((dir, amt)) = tint_col {
|
||||
d = fp.ops.apply(d, l_sat, l_hue);
|
||||
if let Some((dir, amt)) = local.tint {
|
||||
let lab = lightcraft_color::perceptual::oklab_from_2020(d);
|
||||
d = lightcraft_color::perceptual::oklab_to_2020([lab[0], lab[1] + dir[0] * 0.08 * amt, lab[2] + dir[1] * 0.08 * amt]);
|
||||
}
|
||||
@@ -663,6 +532,134 @@ pub(crate) fn finish_with<T: Copy + Default + Send>(
|
||||
out
|
||||
}
|
||||
|
||||
struct PixelLocalTerms {
|
||||
values: [f32; MASK_SUMS],
|
||||
tint: Option<([f32; 3], f32)>,
|
||||
}
|
||||
|
||||
fn pixel_local_terms(p: &Prepared, terms: &[[f32; MASK_TERMS]], index: usize) -> PixelLocalTerms {
|
||||
let mut values = [0.0; MASK_SUMS];
|
||||
let mut tint = None;
|
||||
for (mask, terms) in p.masks.iter().zip(terms) {
|
||||
let alpha = mask.alpha.data[index];
|
||||
if alpha <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
for (sum, term) in values.iter_mut().zip(terms.iter().take(MASK_SUMS)) {
|
||||
*sum += alpha * term;
|
||||
}
|
||||
if terms[MASK_SUMS] > 0.0 {
|
||||
tint = Some(([terms[MASK_SUMS + 1], terms[MASK_SUMS + 2], 0.0], alpha * terms[MASK_SUMS + 3]));
|
||||
}
|
||||
}
|
||||
PixelLocalTerms { values, tint }
|
||||
}
|
||||
|
||||
fn adjust_scene_pixel(p: &Prepared, fp: &FinishParams, index: usize, raw: [f32; 3], local: &PixelLocalTerms) -> ([f32; 3], f32) {
|
||||
let [l_exp, l_temp, l_tint, _, _, _, _, _, _, _, l_dehaze, _, _, _, l_noise, l_moire, l_defringe] = local.values;
|
||||
let mut color = if fp.gain == 1.0 { raw } else { raw.map(|value| value * fp.gain) };
|
||||
|
||||
let moire = (l_moire + 0.5 * l_noise.max(0.0)).clamp(-1.0, 1.0);
|
||||
if moire != 0.0
|
||||
&& let Some(blur) = &p.chroma_blur
|
||||
{
|
||||
let (luminance, original, blurred) = (luminance_2020(color), crate::masks::chromaticity(raw), blur.data[index]);
|
||||
color = std::array::from_fn(|channel| ((original[channel] + (blurred[channel] - original[channel]) * moire) * luminance).max(0.0));
|
||||
}
|
||||
let defringe = l_defringe.clamp(0.0, 1.0);
|
||||
if defringe > 0.0
|
||||
&& let Some(blur) = &p.texture_blur
|
||||
{
|
||||
let amount = defringe * defringe_weight(color, p.log_l.data[index] - blur.data[index]);
|
||||
if amount > 0.0 {
|
||||
let luminance = luminance_2020(color);
|
||||
color = color.map(|value| value + (luminance - value) * amount);
|
||||
}
|
||||
}
|
||||
|
||||
let dehaze = fp.dehaze + l_dehaze;
|
||||
if dehaze != 0.0
|
||||
&& let Some(dark) = &p.dark
|
||||
{
|
||||
// Limit the estimated veil to the pixel's own minimum channel. This avoids crushing bright
|
||||
// neighbours when the dark-channel blur spans the edge of a local window.
|
||||
let min_channel = color[0].min(color[1]).min(color[2]);
|
||||
let veil = (if fp.air_pre > 0.0 { dark.data[index] / fp.air_pre } else { 0.0 })
|
||||
.min(if fp.air > 0.0 { min_channel / fp.air } else { 0.0 })
|
||||
.clamp(0.0, 1.0);
|
||||
if dehaze > 0.0 {
|
||||
let transmission = (1.0 - 0.95 * dehaze.min(1.0) * veil).max(0.12);
|
||||
color = color.map(|value| ((value - fp.air * (1.0 - transmission)) / transmission).max(0.0));
|
||||
} else {
|
||||
let amount = (-dehaze).min(1.0) * 0.7 * (0.35 + 0.65 * veil);
|
||||
color = color.map(|value| value + (fp.air * 0.9 - value) * amount);
|
||||
}
|
||||
}
|
||||
|
||||
if l_exp != 0.0 {
|
||||
let gain = l_exp.exp2();
|
||||
color = color.map(|value| value * gain);
|
||||
}
|
||||
if l_temp != 0.0 || l_tint != 0.0 {
|
||||
let luminance_before = luminance_2020(color);
|
||||
color = [color[0] * (1.0 + 0.3 * l_temp), color[1] * (1.0 - 0.22 * l_tint), color[2] * (1.0 - 0.3 * l_temp).max(0.0)];
|
||||
let luminance_after = luminance_2020(color).max(1e-9);
|
||||
color = color.map(|value| value * luminance_before / luminance_after);
|
||||
}
|
||||
|
||||
let l0 = p.log_l.data[index] + fp.ev;
|
||||
let log_luminance = if dehaze != 0.0 || l_exp != 0.0 { log_lum(color) } else { l0 };
|
||||
(color, log_luminance)
|
||||
}
|
||||
|
||||
fn local_tone_delta(p: &Prepared, fp: &FinishParams, index: usize, base: f32, l1: f32, local: &PixelLocalTerms) -> f32 {
|
||||
let [_, _, _, l_con, l_hl, l_sh, l_wh, l_bl, l_tex, l_clar, _, _, _, l_sharp, l_noise, _, _] = local.values;
|
||||
let mut delta = 0.0;
|
||||
let (highlights, shadows) = (fp.hl + l_hl, fp.sh + l_sh);
|
||||
if highlights != 0.0 || shadows != 0.0 {
|
||||
let shadow_weight = 1.0 - smooth(-4.8, 0.3, base);
|
||||
let highlight_weight = smooth(-1.0, 2.8, base);
|
||||
delta += shadows * 1.7 * shadow_weight * shadow_weight.sqrt() + highlights * 1.7 * highlight_weight;
|
||||
}
|
||||
if l_wh != 0.0 {
|
||||
delta += l_wh * 0.8 * smooth(0.5, 3.0, l1);
|
||||
}
|
||||
if l_bl != 0.0 {
|
||||
delta += l_bl * 0.8 * (1.0 - smooth(-6.0, -1.5, l1));
|
||||
}
|
||||
if l_con != 0.0 {
|
||||
delta += l_con * 0.14 * l1.clamp(-6.0, 4.0);
|
||||
}
|
||||
let clarity = fp.clar + l_clar;
|
||||
if clarity != 0.0
|
||||
&& let Some(blur) = &p.clarity_blur
|
||||
{
|
||||
let detail = (p.log_l.data[index] - blur.data[index]).clamp(-2.5, 2.5);
|
||||
let midtone = (-(base / 3.2).powi(2)).exp();
|
||||
delta += clarity * 0.85 * detail * (0.35 + 0.65 * midtone);
|
||||
}
|
||||
let texture = fp.tex + l_tex;
|
||||
let sharpen = l_sharp * 0.6 + fp.sharpen;
|
||||
if (texture != 0.0 || sharpen != 0.0)
|
||||
&& let Some(blur) = &p.texture_blur
|
||||
{
|
||||
let detail = p.log_l.data[index] - blur.data[index];
|
||||
let tame = 1.0 - 0.6 * smooth(0.4, 1.6, detail.abs());
|
||||
delta += texture * 1.1 * detail.clamp(-1.0, 1.0) * tame;
|
||||
if sharpen != 0.0 {
|
||||
let mask = if fp.sharpen_mask > 0.0 { smooth(fp.sharpen_mask * 0.25, fp.sharpen_mask * 0.25 + 0.15, detail.abs()) } else { 1.0 };
|
||||
delta += sharpen * 1.3 * detail.clamp(-0.8, 0.8) * mask;
|
||||
}
|
||||
}
|
||||
if l_noise != 0.0
|
||||
&& let Some(blur) = &p.texture_blur
|
||||
{
|
||||
let detail = p.log_l.data[index] - blur.data[index];
|
||||
delta -= l_noise.clamp(-1.0, 1.0) * 0.9 * detail * (1.0 - smooth(0.1, 0.5, detail.abs()));
|
||||
}
|
||||
delta
|
||||
}
|
||||
|
||||
/// A colour needing more desaturation than this (scale < `GAMUT_WARN`) to fit is out of gamut for
|
||||
/// the gamut warnings (tolerates rounding at the gamut boundary).
|
||||
const GAMUT_WARN: f32 = 0.995;
|
||||
|
||||
Reference in New Issue
Block a user