M5.2d: geometry resample on the GPU
The source is uploaded once per view (kept with the GPU stages) and resampled on the device: orientation as an integer pixel map, the Mitchell area prefilter with the CPU's exact taps, then one bilinear resample through the crop/straighten/flip affine or through the full warp (inverse perspective, manual distortion, embedded DNG WarpRectilinear per plane, lateral CA, manual and embedded vignetting gain, blank outside the source). `Frame::sample_plan` (new) decides prefilter size and mode for both renderers; sources larger than the device's buffer limit are still resampled on the CPU. Equivalence: all 8 orientations with and without crop/straighten/flip, embedded lens + perspective: max 1 LSB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2dfd65b39a
commit
07c3f0261c
|
|
@ -44,6 +44,11 @@ const MODULES: &[Module] = &[
|
|||
bindings: &[("img", false, "f32"), ("log_l", false, "f32"), ("aux", false, "f32"), ("c", true, "f32"), ("alpha", true, "f32")],
|
||||
entries: &["shape", "combine", "finalize"],
|
||||
},
|
||||
Module {
|
||||
src: include_str!("wgsl/geom.wgsl"),
|
||||
bindings: &[("src", false, "f32"), ("dst", true, "f32")],
|
||||
entries: &["orient", "sample_affine", "sample_warp"],
|
||||
},
|
||||
];
|
||||
|
||||
struct Kernel {
|
||||
|
|
@ -81,6 +86,8 @@ fn constants() -> String {
|
|||
}
|
||||
s += &format!("const TONE_MIN_EV: f32 = {LUT_MIN_EV:?};\nconst TONE_MAX_EV: f32 = {LUT_MAX_EV:?};\nconst TONE_N: u32 = {LUT_N}u;\n");
|
||||
s += &format!("const TONE_MIN_GAIN: f32 = {:?};\n", 2f32.powf(LUT_MIN_EV));
|
||||
let b = lightcraft_pipeline::geometry::BLANK;
|
||||
s += &format!("const BLANK_R: f32 = {:?};\nconst BLANK_G: f32 = {:?};\nconst BLANK_B: f32 = {:?};\n", b[0], b[1], b[2]);
|
||||
s += &format!("const SRGB_N: u32 = {SRGB_LUT_N}u;\nconst CURVE_N: u32 = {}u;\nconst MASK_TERMS: u32 = {MASK_TERMS}u;\n", crate::params::CURVE_N);
|
||||
for (i, h) in GRAIN_HASH.iter().enumerate() {
|
||||
s += &format!("const GRAIN_H{i}: u32 = {h}u;\n");
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@
|
|||
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::geometry::SampleMode;
|
||||
use lightcraft_pipeline::{Plan, RenderRequest, Rendered, SourceInfo, local};
|
||||
use lightcraft_raster::resample::Filter;
|
||||
use lightcraft_raster::{Histogram, Plane, Rgb32f, Rgba8};
|
||||
|
|
@ -18,6 +20,8 @@ use crate::params::{Present, finish_block};
|
|||
#[derive(Default)]
|
||||
pub struct GpuStages {
|
||||
entries: Mutex<Vec<Entry>>,
|
||||
/// The uploaded source (a view renders one photo at several sizes).
|
||||
source: Mutex<Option<(Arc<Rgb32f>, Arc<Buf>)>>,
|
||||
}
|
||||
|
||||
const CAPACITY: usize = 2;
|
||||
|
|
@ -56,6 +60,19 @@ impl GpuStages {
|
|||
}
|
||||
}
|
||||
|
||||
fn source(&self, src: &Arc<Rgb32f>, upload: impl FnOnce() -> Buf) -> Arc<Buf> {
|
||||
let mut g = self.source.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match &*g {
|
||||
Some((s, b)) if Arc::ptr_eq(s, src) => b.clone(),
|
||||
_ => {
|
||||
*g = None; // free the previous photo's buffer first
|
||||
let b = Arc::new(upload());
|
||||
*g = Some((src.clone(), b.clone()));
|
||||
b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lock(&self) -> std::sync::MutexGuard<'_, Vec<Entry>> {
|
||||
self.entries.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
|
@ -213,6 +230,127 @@ fn guided_fast(cx: &mut Cx<'_>, p: &Buf, w: usize, h: usize, sigma: f32, eps: f3
|
|||
q
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Geometry (`Frame::sample`)
|
||||
|
||||
/// Integer pixel map `(x, y) → (m0·x + m1·y + m2, m3·x + m4·y + m5)` from an oriented image back
|
||||
/// to the source of `w × h` (`Image::oriented`: optional horizontal flip, then quarter turns).
|
||||
pub(crate) fn orient_map(o: Orientation, w: usize, h: usize) -> [i32; 6] {
|
||||
let (flip, turns) = o.to_parts();
|
||||
let (w, h) = (w as i32, h as i32);
|
||||
let mut ops: Vec<fn(i32, i32) -> [i32; 6]> = Vec::new();
|
||||
if flip {
|
||||
ops.push(|w, _| [-1, 0, w - 1, 0, 1, 0]);
|
||||
}
|
||||
let cw: fn(i32, i32) -> [i32; 6] = |_, h| [0, 1, 0, -1, 0, h - 1];
|
||||
let half: fn(i32, i32) -> [i32; 6] = |w, h| [-1, 0, w - 1, 0, -1, h - 1];
|
||||
match turns {
|
||||
1 => ops.push(cw),
|
||||
2 => ops.push(half),
|
||||
3 => {
|
||||
ops.push(half);
|
||||
ops.push(cw);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// compose: T ← T ∘ op, tracking each op's input size
|
||||
let mut t = [1, 0, 0, 0, 1, 0];
|
||||
let (mut cw_, mut ch_) = (w, h);
|
||||
for op in ops {
|
||||
let o = op(cw_, ch_);
|
||||
t = [
|
||||
t[0] * o[0] + t[1] * o[3],
|
||||
t[0] * o[1] + t[1] * o[4],
|
||||
t[0] * o[2] + t[1] * o[5] + t[2],
|
||||
t[3] * o[0] + t[4] * o[3],
|
||||
t[3] * o[1] + t[4] * o[4],
|
||||
t[3] * o[2] + t[4] * o[5] + t[5],
|
||||
];
|
||||
if o[0] == 0 {
|
||||
(cw_, ch_) = (ch_, cw_);
|
||||
}
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn affine_bits(a: &lightcraft_geom::Affine) -> [u32; 6] {
|
||||
a.0.map(|v| (v as f32).to_bits())
|
||||
}
|
||||
|
||||
/// The `sample_warp` kernel's parameters (layout documented in `geom.wgsl`).
|
||||
fn warp_params(
|
||||
wp: &lightcraft_pipeline::optics::Warp,
|
||||
base: (usize, usize),
|
||||
out: (usize, usize),
|
||||
o2t: &lightcraft_geom::Affine,
|
||||
sx: f64,
|
||||
sy: f64,
|
||||
) -> Vec<u32> {
|
||||
let f = |v: f64| (v as f32).to_bits();
|
||||
let mut p = vec![base.0 as u32, base.1 as u32, out.0 as u32, out.1 as u32];
|
||||
p.extend(affine_bits(o2t));
|
||||
p.extend([f(sx), f(sy), f(wp.w), f(wp.h)]);
|
||||
let persp = wp.persp != lightcraft_geom::Homography::IDENTITY;
|
||||
p.push(persp as u32);
|
||||
p.extend(wp.persp_inv.0.map(f));
|
||||
p.push(f(wp.k1));
|
||||
p.extend(wp.ca.map(f));
|
||||
p.push(f(wp.lens_dist));
|
||||
let warp = wp.lens.and_then(|l| l.warp);
|
||||
p.push(warp.is_some() as u32);
|
||||
let w = warp.unwrap_or_default();
|
||||
p.extend(w.planes.iter().flatten().map(|v| f(*v)));
|
||||
p.extend([f(w.center.x), f(w.center.y), f(w.radius)]);
|
||||
p.extend([f(wp.vig_stops), f(wp.vig_power), f(wp.lens_vig)]);
|
||||
let vig = wp.lens.and_then(|l| l.vignette);
|
||||
p.push(vig.is_some() as u32);
|
||||
let v = vig.unwrap_or_default();
|
||||
p.extend(v.k.map(f));
|
||||
p.extend([f(v.center.x), f(v.center.y), f(v.radius)]);
|
||||
p.push(wp.per_channel() as u32);
|
||||
p.push(wp.has_gain() as u32);
|
||||
debug_assert_eq!(p.len(), 65);
|
||||
p
|
||||
}
|
||||
|
||||
/// Resample the source into the output frame (`Frame::sample`). May return the source buffer
|
||||
/// itself when the frame is the identity.
|
||||
fn sample(cx: &mut Cx<'_>, src: &Arc<Rgb32f>, src_buf: Arc<Buf>, plan: &Plan<'_>) -> Arc<Buf> {
|
||||
let (w, h) = (plan.w, plan.h);
|
||||
let fr = &plan.frame;
|
||||
let sp = fr.sample_plan(src.width, src.height, w, h);
|
||||
let oriented = if fr.orient == Orientation::Normal {
|
||||
src_buf
|
||||
} else {
|
||||
let out = cx.gpu.buffer(sp.ow * sp.oh * 3);
|
||||
let mut p = vec![src.width as u32, src.height as u32, sp.ow as u32, sp.oh as u32];
|
||||
p.extend(orient_map(fr.orient, src.width, src.height).map(|v| v as u32));
|
||||
cx.run("orient", &p, &[Some(&src_buf), Some(&out)], groups2(sp.ow, sp.oh, [16, 16]));
|
||||
Arc::new(out)
|
||||
};
|
||||
let (base, (bw, bh)) = match sp.prefilter {
|
||||
Some(d) => (Arc::new(resize(cx, &oriented, (sp.ow, sp.oh), d, 3, Filter::Mitchell)), d),
|
||||
None => (oriented, (sp.ow, sp.oh)),
|
||||
};
|
||||
match &sp.mode {
|
||||
SampleMode::Copy => base,
|
||||
SampleMode::Affine(xf) => {
|
||||
let out = cx.gpu.buffer(w * h * 3);
|
||||
let mut p = vec![bw as u32, bh as u32, w as u32, h as u32];
|
||||
p.extend(affine_bits(xf));
|
||||
cx.run("sample_affine", &p, &[Some(&base), Some(&out)], groups2(w, h, [16, 16]));
|
||||
Arc::new(out)
|
||||
}
|
||||
SampleMode::Warp(o2t) => {
|
||||
let wp = fr.warp.as_ref().expect("warp mode has a warp");
|
||||
let out = cx.gpu.buffer(w * h * 3);
|
||||
let p = warp_params(wp, (bw, bh), (w, h), o2t, sp.sx, sp.sy);
|
||||
cx.run("sample_warp", &p, &[Some(&base), Some(&out)], groups2(w, h, [16, 16]));
|
||||
Arc::new(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// The render
|
||||
|
||||
|
|
@ -248,7 +386,7 @@ pub fn render(
|
|||
let plan = lightcraft_pipeline::plan(src, info, s, req);
|
||||
let (w, h) = (plan.w, plan.h);
|
||||
let n = w * h;
|
||||
if !gpu.fits(n * 3) || !gpu.fits(src.data.len() * 3) {
|
||||
if !gpu.fits(n * 3) {
|
||||
return None;
|
||||
}
|
||||
let s = &*plan.settings;
|
||||
|
|
@ -256,10 +394,23 @@ pub fn render(
|
|||
let mut host = Host::default();
|
||||
let mut cx = Cx::new(gpu);
|
||||
|
||||
// 1. geometry
|
||||
// 1. geometry (on the CPU when the source exceeds the device's buffer limit)
|
||||
let sampled = match &cached {
|
||||
Some(e) => e.sampled.clone(),
|
||||
None => Arc::new(sample(&mut cx, src, &plan, &mut host)),
|
||||
None if gpu.fits(src.data.len() * 3) => {
|
||||
let upload = || gpu.upload(rgb_words(src));
|
||||
let src_buf = match stages {
|
||||
Some(c) => c.source(src, upload),
|
||||
None => Arc::new(upload()),
|
||||
};
|
||||
sample(&mut cx, src, src_buf, &plan)
|
||||
}
|
||||
None => {
|
||||
let img = plan.frame.sample(src, w, h);
|
||||
let b = Arc::new(gpu.upload(rgb_words(&img)));
|
||||
host.sampled = Some(img);
|
||||
b
|
||||
}
|
||||
};
|
||||
lap("sample", &mut t);
|
||||
|
||||
|
|
@ -314,14 +465,6 @@ pub fn render(
|
|||
Some(Rendered { image, histogram })
|
||||
}
|
||||
|
||||
/// Resample the source into the output frame.
|
||||
fn sample(cx: &mut Cx<'_>, src: &Arc<Rgb32f>, plan: &Plan<'_>, host: &mut Host) -> Buf {
|
||||
let img = plan.frame.sample(src, plan.w, plan.h);
|
||||
let b = cx.gpu.upload(rgb_words(&img));
|
||||
host.sampled = Some(img);
|
||||
b
|
||||
}
|
||||
|
||||
/// The white-balanced, retouched, denoised image.
|
||||
fn linear(cx: &mut Cx<'_>, sampled: &Buf, info: &SourceInfo, plan: &Plan<'_>, host: &mut Host) -> Buf {
|
||||
let (w, h) = (plan.w, plan.h);
|
||||
|
|
@ -553,3 +696,25 @@ fn masks(cx: &mut Cx<'_>, lin: &Buf, prep: &Prep, plan: &Plan<'_>, host: &mut Ho
|
|||
let terms = list.iter().map(|m| mask_terms(&m.adjust)).collect();
|
||||
(Some(alpha), terms)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn orient_map_matches_image_oriented() {
|
||||
use Orientation::*;
|
||||
let img = Rgb32f::from_fn(5, 3, |x, y| [x as f32, y as f32, 0.0]);
|
||||
for o in [Normal, Rotate90, Rotate180, Rotate270, FlipH, Transverse, FlipV, Transpose] {
|
||||
let r = img.oriented(o);
|
||||
let m = orient_map(o, 5, 3);
|
||||
for y in 0..r.height {
|
||||
for x in 0..r.width {
|
||||
let (x, y) = (x as i32, y as i32);
|
||||
let (sx, sy) = (m[0] * x + m[1] * y + m[2], m[3] * x + m[4] * y + m[5]);
|
||||
assert_eq!(r.get(x as usize, y as usize), img.get(sx as usize, sy as usize), "{o:?} at {x},{y}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
// Geometry (`lightcraft_pipeline::geometry::Frame::sample`): orientation, and the single bilinear
|
||||
// resample through the crop/straighten/flip affine or the full lens + perspective warp.
|
||||
// Bindings: src (rgb), dst (rgb).
|
||||
|
||||
fn src_px(w: u32, h: u32, x: i32, y: i32) -> vec3<f32> {
|
||||
let cx = u32(clamp(x, 0, i32(w) - 1));
|
||||
let cy = u32(clamp(y, 0, i32(h) - 1));
|
||||
let j = 3u * (cy * w + cx);
|
||||
return vec3<f32>(src[j], src[j + 1u], src[j + 2u]);
|
||||
}
|
||||
|
||||
// `Rgb32f::sample_bilinear` (pixel centres at +0.5, clamped edges).
|
||||
fn bilinear(w: u32, h: u32, x: f32, y: f32) -> vec3<f32> {
|
||||
let fx = x - 0.5;
|
||||
let fy = y - 0.5;
|
||||
let x0 = floor(fx);
|
||||
let y0 = floor(fy);
|
||||
let tx = fx - x0;
|
||||
let ty = fy - y0;
|
||||
let ix = i32(x0);
|
||||
let iy = i32(y0);
|
||||
let a = src_px(w, h, ix, iy);
|
||||
let b = src_px(w, h, ix + 1, iy);
|
||||
let c = src_px(w, h, ix, iy + 1);
|
||||
let d = src_px(w, h, ix + 1, iy + 1);
|
||||
let top = a + (b - a) * tx;
|
||||
let bot = c + (d - c) * tx;
|
||||
return top + (bot - top) * ty;
|
||||
}
|
||||
|
||||
fn put(i: u32, v: vec3<f32>) {
|
||||
dst[3u * i] = v.x;
|
||||
dst[3u * i + 1u] = v.y;
|
||||
dst[3u * i + 2u] = v.z;
|
||||
}
|
||||
|
||||
// Orientation: dst (ow × oh) pixel (x, y) = src pixel (P[4]·x + P[5]·y + P[6], P[7]·x + P[8]·y + P[9])
|
||||
// (integers). P: sw, sh, ow, oh, map[6].
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn orient(@builtin(global_invocation_id) g: vec3<u32>) {
|
||||
let sw = pu(0u);
|
||||
let ow = pu(2u);
|
||||
let oh = pu(3u);
|
||||
if (g.x >= ow || g.y >= oh) {
|
||||
return;
|
||||
}
|
||||
let x = i32(g.x);
|
||||
let y = i32(g.y);
|
||||
let m = array<i32, 6>(bitcast<i32>(pu(4u)), bitcast<i32>(pu(5u)), bitcast<i32>(pu(6u)), bitcast<i32>(pu(7u)), bitcast<i32>(pu(8u)), bitcast<i32>(pu(9u)));
|
||||
let sx = u32(m[0] * x + m[1] * y + m[2]);
|
||||
let sy = u32(m[3] * x + m[4] * y + m[5]);
|
||||
let j = 3u * (sy * sw + sx);
|
||||
put(g.y * ow + g.x, vec3<f32>(src[j], src[j + 1u], src[j + 2u]));
|
||||
}
|
||||
|
||||
// Bilinear resample through an affine (output px → base px). P: bw, bh, w, h, affine[6].
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn sample_affine(@builtin(global_invocation_id) g: vec3<u32>) {
|
||||
let w = pu(2u);
|
||||
let h = pu(3u);
|
||||
if (g.x >= w || g.y >= h) {
|
||||
return;
|
||||
}
|
||||
let px = f32(g.x) + 0.5;
|
||||
let py = f32(g.y) + 0.5;
|
||||
let x = pf(4u) * px + pf(6u) * py + pf(8u);
|
||||
let y = pf(5u) * px + pf(7u) * py + pf(9u);
|
||||
put(g.y * w + g.x, bilinear(pu(0u), pu(1u), x, y));
|
||||
}
|
||||
|
||||
// --- the lens / perspective warp (`lightcraft_pipeline::optics::Warp`), in oriented-source px.
|
||||
// P layout (see `geom_warp_params`): 0 bw, 1 bh, 2 w, 3 h, 4..10 output → transformed affine,
|
||||
// 10 sx, 11 sy, 12 W, 13 H, 14 persp?, 15..24 persp_inv, 24 k1, 25..28 ca, 28 lens_dist,
|
||||
// 29 warp?, 30..48 warp planes (3 × 6), 48 warp centre x, 49 y, 50 warp radius, 51 vig_stops,
|
||||
// 52 vig_power, 53 lens_vig, 54 vignette?, 55..60 vignette k, 60 vignette centre x, 61 y,
|
||||
// 62 vignette radius, 63 per-channel?, 64 gain?
|
||||
const WP_N: u32 = 65u;
|
||||
|
||||
fn centre() -> vec2<f32> {
|
||||
return vec2<f32>(pf(12u) / 2.0, pf(13u) / 2.0);
|
||||
}
|
||||
|
||||
fn to_corrected(p: vec2<f32>) -> vec2<f32> {
|
||||
if (pu(14u) == 0u) {
|
||||
return p;
|
||||
}
|
||||
let c = centre();
|
||||
let l = max(pf(12u), pf(13u)) / 2.0;
|
||||
let q = (p - c) / l;
|
||||
var wq = pf(21u) * q.x + pf(22u) * q.y + pf(23u);
|
||||
if (abs(wq) < 1e-30) {
|
||||
wq = 1e-30;
|
||||
}
|
||||
let x = (pf(15u) * q.x + pf(16u) * q.y + pf(17u)) / wq;
|
||||
let y = (pf(18u) * q.x + pf(19u) * q.y + pf(20u)) / wq;
|
||||
return c + vec2<f32>(x, y) * l;
|
||||
}
|
||||
|
||||
fn embedded_warp(p: vec2<f32>, ch: u32) -> vec2<f32> {
|
||||
let w = pf(12u);
|
||||
let h = pf(13u);
|
||||
let cc = vec2<f32>(pf(48u) * w, pf(49u) * h);
|
||||
let m = max(pf(50u) * max(w, h), 1e-9);
|
||||
let o = 30u + 6u * min(ch, 2u);
|
||||
let d = (p - cc) / m;
|
||||
let r2 = d.x * d.x + d.y * d.y;
|
||||
let f = pf(o) + r2 * (pf(o + 1u) + r2 * (pf(o + 2u) + r2 * pf(o + 3u)));
|
||||
let kt0 = pf(o + 4u);
|
||||
let kt1 = pf(o + 5u);
|
||||
let sx = d.x * f + kt0 * 2.0 * d.x * d.y + kt1 * (r2 + 2.0 * d.x * d.x);
|
||||
let sy = d.y * f + kt1 * 2.0 * d.x * d.y + kt0 * (r2 + 2.0 * d.y * d.y);
|
||||
return cc + m * vec2<f32>(sx, sy);
|
||||
}
|
||||
|
||||
fn corrected_to_source(p: vec2<f32>, ch: u32) -> vec2<f32> {
|
||||
let c = centre();
|
||||
let hd = max(sqrt(pf(12u) * pf(12u) + pf(13u) * pf(13u)) / 2.0, 1e-9);
|
||||
var q = p;
|
||||
let k1 = pf(24u);
|
||||
if (k1 != 0.0) {
|
||||
let d = q - c;
|
||||
let r2 = (d.x * d.x + d.y * d.y) / (hd * hd);
|
||||
q = c + d * (1.0 + k1 * r2);
|
||||
}
|
||||
let ld = pf(28u);
|
||||
if (ld != 0.0 && pu(29u) != 0u) {
|
||||
let s = embedded_warp(q, ch);
|
||||
q = q + (s - q) * ld;
|
||||
}
|
||||
let k = pf(25u + ch);
|
||||
if (k != 0.0) {
|
||||
q = c + (q - c) * (1.0 + k);
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
fn warp_gain(s: vec2<f32>) -> f32 {
|
||||
var g = 1.0;
|
||||
let stops = pf(51u);
|
||||
let w = pf(12u);
|
||||
let h = pf(13u);
|
||||
if (stops != 0.0) {
|
||||
let hd = max(sqrt(w * w + h * h) / 2.0, 1e-9);
|
||||
let r = min(length(s - centre()) / hd, 1.5);
|
||||
g *= exp2(stops * pow(r, pf(52u)));
|
||||
}
|
||||
let lv = pf(53u);
|
||||
if (lv != 0.0 && pu(54u) != 0u) {
|
||||
let cc = vec2<f32>(pf(60u) * w, pf(61u) * h);
|
||||
let m = max(pf(62u) * max(w, h), 1e-9);
|
||||
let d = s - cc;
|
||||
let r2 = (d.x * d.x + d.y * d.y) / (m * m);
|
||||
var vg = 1.0;
|
||||
var rp = r2;
|
||||
for (var k = 0u; k < 5u; k++) {
|
||||
vg += pf(55u + k) * rp;
|
||||
rp *= r2;
|
||||
}
|
||||
g *= 1.0 + (vg - 1.0) * lv;
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn sample_warp(@builtin(global_invocation_id) g: vec3<u32>) {
|
||||
let w = pu(2u);
|
||||
let h = pu(3u);
|
||||
if (g.x >= w || g.y >= h) {
|
||||
return;
|
||||
}
|
||||
let bw = pu(0u);
|
||||
let bh = pu(1u);
|
||||
let px = f32(g.x) + 0.5;
|
||||
let py = f32(g.y) + 0.5;
|
||||
let t = vec2<f32>(pf(4u) * px + pf(6u) * py + pf(8u), pf(5u) * px + pf(7u) * py + pf(9u));
|
||||
let c = to_corrected(t);
|
||||
let s = corrected_to_source(c, 1u);
|
||||
let sx = pf(10u);
|
||||
let sy = pf(11u);
|
||||
if (!(s.x >= -0.5 && s.y >= -0.5 && s.x <= pf(12u) + 0.5 && s.y <= pf(13u) + 0.5)) {
|
||||
put(g.y * w + g.x, vec3<f32>(BLANK_R, BLANK_G, BLANK_B));
|
||||
return;
|
||||
}
|
||||
var v = bilinear(bw, bh, s.x * sx, s.y * sy);
|
||||
if (pu(63u) != 0u) {
|
||||
let qr = corrected_to_source(c, 0u);
|
||||
v.x = bilinear(bw, bh, qr.x * sx, qr.y * sy).x;
|
||||
let qb = corrected_to_source(c, 2u);
|
||||
v.z = bilinear(bw, bh, qb.x * sx, qb.y * sy).z;
|
||||
}
|
||||
if (pu(64u) != 0u) {
|
||||
v = v * warp_gain(s);
|
||||
}
|
||||
put(g.y * w + g.x, v);
|
||||
}
|
||||
|
|
@ -297,6 +297,45 @@ fn rendered_sources_and_other_scenes() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_variants() {
|
||||
if !gpu() {
|
||||
return;
|
||||
}
|
||||
use lightcraft_develop::{EmbeddedLens, EmbeddedVignette, EmbeddedWarp};
|
||||
let src = scene(2, 900, 600);
|
||||
let raw = SourceInfo { raw: true, ..Default::default() };
|
||||
use Orientation::*;
|
||||
for o in [Normal, Rotate90, Rotate180, Rotate270, FlipH, Transverse, FlipV, Transpose] {
|
||||
for (crop, req) in [(false, RenderRequest::fit(900, 900)), (true, RenderRequest::fit(500, 500))] {
|
||||
let mut s = DevelopSettings { orientation: o, ..Default::default() };
|
||||
if crop {
|
||||
s.crop.geometry.rect = Rect::new(0.2, 0.1, 0.9, 0.8);
|
||||
s.crop.geometry.angle = -4.0;
|
||||
s.crop.flip_v = true;
|
||||
}
|
||||
check(&format!("{o:?} crop={crop}"), &src, &raw, &s, &req);
|
||||
}
|
||||
}
|
||||
// embedded DNG lens corrections (per-plane warp + vignette) with manual CA
|
||||
let lens = EmbeddedLens {
|
||||
warp: Some(EmbeddedWarp {
|
||||
planes: [[1.0, -0.03, 0.01, 0.0, 0.001, -0.002], [1.0, -0.028, 0.01, 0.0, 0.001, -0.002], [1.0, -0.026, 0.01, 0.0, 0.001, -0.002]],
|
||||
center: Point::new(0.52, 0.48),
|
||||
radius: 0.6,
|
||||
}),
|
||||
vignette: Some(EmbeddedVignette { k: [0.4, -0.1, 0.02, 0.0, 0.0], center: Point::new(0.5, 0.5), radius: 0.6 }),
|
||||
};
|
||||
let info = SourceInfo { lens: Some(lens), ..raw };
|
||||
let mut s = DevelopSettings::default();
|
||||
s.optics.lens_profile = true;
|
||||
s.optics.ca_red = 30.0;
|
||||
s.geometry.horizontal = -15.0;
|
||||
check("embedded lens + perspective", &src, &info, &s, &RenderRequest::fit(700, 700));
|
||||
s.orientation = Rotate270;
|
||||
check("embedded lens rotated", &src, &info, &s, &RenderRequest::fit(700, 700));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_renders_match_uncached() {
|
||||
if !gpu() {
|
||||
|
|
|
|||
|
|
@ -177,37 +177,48 @@ impl Frame {
|
|||
Point::new(p.x * self.ow / l, p.y * self.oh / l)
|
||||
}
|
||||
|
||||
/// How [`Frame::sample`] resamples a (EXIF-oriented) source of `src_w × src_h` into `w × h`.
|
||||
pub fn sample_plan(&self, src_w: usize, src_h: usize, w: usize, h: usize) -> SamplePlan {
|
||||
let (ow, oh) = if self.orient.swaps_axes() { (src_h, src_w) } else { (src_w, src_h) };
|
||||
let crop_px = self.crop.rect_px(self.ow, self.oh);
|
||||
let k = crop_px.width() / w as f64;
|
||||
let prefilter = (k > 1.25).then(|| (((ow as f64 / k).round() as usize).max(1), ((oh as f64 / k).round() as usize).max(1)));
|
||||
let (bw, bh) = prefilter.unwrap_or((ow, oh));
|
||||
let (sx, sy) = (bw as f64 / ow as f64, bh as f64 / oh as f64);
|
||||
let mode = if self.warp.is_some() {
|
||||
SampleMode::Warp(self.out_to_oriented(w, h))
|
||||
} else if self.crop.is_identity() && !self.flip_h && !self.flip_v && bw == w && bh == h {
|
||||
SampleMode::Copy
|
||||
} else {
|
||||
SampleMode::Affine(Affine::scale(sx, sy) * self.out_to_oriented(w, h))
|
||||
};
|
||||
SamplePlan { ow, oh, prefilter, sx, sy, mode }
|
||||
}
|
||||
|
||||
/// Sample the source into a `w × h` output buffer: orientation, crop, rotation, flips — one
|
||||
/// bilinear resample from a pre-filtered (area-downscaled) copy, so minification never aliases.
|
||||
pub fn sample(&self, src: &Rgb32f, w: usize, h: usize) -> Rgb32f {
|
||||
let oriented = if self.orient == Orientation::Normal { None } else { Some(src.oriented(self.orient)) };
|
||||
let o = oriented.as_ref().unwrap_or(src);
|
||||
let crop_px = self.crop.rect_px(self.ow, self.oh);
|
||||
let k = crop_px.width() / w as f64;
|
||||
let (base, s) = if k > 1.25 {
|
||||
let nw = ((o.width as f64 / k).round() as usize).max(1);
|
||||
let nh = ((o.height as f64 / k).round() as usize).max(1);
|
||||
(resize(o, nw, nh, Filter::Mitchell), nw as f64 / o.width as f64)
|
||||
} else {
|
||||
(o.clone(), 1.0)
|
||||
let plan = self.sample_plan(src.width, src.height, w, h);
|
||||
let base = match plan.prefilter {
|
||||
Some((nw, nh)) => std::borrow::Cow::Owned(resize(o, nw, nh, Filter::Mitchell)),
|
||||
None => std::borrow::Cow::Borrowed(o),
|
||||
};
|
||||
let (sx, sy) = (s, base.height as f64 / o.height as f64);
|
||||
if let Some(wp) = &self.warp {
|
||||
return sample_warped(&base, sx, sy, wp, self.out_to_oriented(w, h), w, h);
|
||||
}
|
||||
let xf = Affine::scale(sx, sy) * self.out_to_oriented(w, h);
|
||||
let identity_like = self.crop.is_identity() && !self.flip_h && !self.flip_v && base.width == w && base.height == h;
|
||||
if identity_like {
|
||||
return base;
|
||||
}
|
||||
let mut out = Rgb32f::new(w, h);
|
||||
par_rows(&mut out.data, w, |y, row| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let p = xf.apply(Point::new(x as f64 + 0.5, y as f64 + 0.5));
|
||||
*px = base.sample_bilinear(p.x as f32, p.y as f32);
|
||||
match plan.mode {
|
||||
SampleMode::Copy => base.into_owned(),
|
||||
SampleMode::Warp(o2t) => sample_warped(&base, plan.sx, plan.sy, self.warp.as_ref().expect("warp"), o2t, w, h),
|
||||
SampleMode::Affine(xf) => {
|
||||
let mut out = Rgb32f::new(w, h);
|
||||
par_rows(&mut out.data, w, |y, row| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let p = xf.apply(Point::new(x as f64 + 0.5, y as f64 + 0.5));
|
||||
*px = base.sample_bilinear(p.x as f32, p.y as f32);
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// The crop rectangle as a quad in normalized oriented coordinates (for overlays).
|
||||
|
|
@ -221,6 +232,30 @@ impl Frame {
|
|||
}
|
||||
}
|
||||
|
||||
/// How a render resamples its source (see [`Frame::sample_plan`]).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SamplePlan {
|
||||
/// Oriented source size.
|
||||
pub ow: usize,
|
||||
pub oh: usize,
|
||||
/// Size of the Mitchell prefilter (area downscale before the bilinear resample), if any.
|
||||
pub prefilter: Option<(usize, usize)>,
|
||||
/// Oriented-source px → prefiltered px.
|
||||
pub sx: f64,
|
||||
pub sy: f64,
|
||||
pub mode: SampleMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SampleMode {
|
||||
/// The (prefiltered) oriented source is the output.
|
||||
Copy,
|
||||
/// Bilinear through this affine (output px → prefiltered px).
|
||||
Affine(Affine),
|
||||
/// Through the warp; the affine maps output px → transformed px.
|
||||
Warp(Affine),
|
||||
}
|
||||
|
||||
/// The perspective homography of the settings (Upright, then the manual Transform sliders), lens-corrected →
|
||||
/// transformed, in centred coordinates.
|
||||
pub fn perspective(s: &DevelopSettings, ow: f64, oh: f64) -> Homography {
|
||||
|
|
|
|||
Loading…
Reference in New Issue