Name the two spaces a photograph lives in, so a turn cannot go the wrong way
Every orientation bug this codebase has had has been the same bug: a turn of the right size applied in the wrong direction. That failure is worth naming precisely, because it does not look like one — a quarter turn applied backwards lands 180 degrees from right, so the result is a plausible transform of the picture rather than anything obviously broken, and on landscape frames it is not wrong at all. It was the straighten shear, and it was the segmentation overlay, and each time it was found by eye rather than by a test. The reason it keeps happening is that "rotate 90 degrees clockwise" cannot be checked by reading it. The reader has to hold in their head which of the two images is being rotated and which way the y axis runs, and there were four hand-written copies of the permutation to hold it for: the shader prologue, its CPU twin, the thumbnail path, and the segmentation. So nothing added here says clockwise, anticlockwise, horizontal or vertical. The functions say *which space they take and which space they return* — `into_shown` and `into_stored`, `source_pixel` and `shown_pixel`, `into_shown_rect` and `into_stored_rect` — and each takes the dimensions of the space it reads from, so no caller has to work out which pair it is holding. `StoredRect` and `ShownRect` are separate types because they are the same four numbers meaning different things, which is exactly the case where a mistake is silent: a shown rect measured against stored dimensions produces a rectangle in the wrong place, not an error. Underneath there is one permutation. `source_pixel` was already shared by the prologue and the thumbnails; `source_point` is its normalised twin, written beside it so the two cannot drift, and everything else is those two read forwards or backwards. `Orientation::inverse` is the group inverse rather than `4 - turns`: mirrors apply after the turn, so undoing means undoing them first, and a mirror seen from the far side of an odd turn is about the other axis. That is the diagonal-mirror case, tags 5 and 7, and getting it wrong renders as — again — 180 degrees. Three call sites lose their own copy: the thumbnail path, `dr-ui`'s segmentation, and `dr-gpu`'s `local` example. "Upright" now means one thing across the application rather than one thing per caller. The gate that matters most is `the_render_and_the_orientation_map_agree`. The shader prologue and `Orientation` answer the same question by different routes, and until now nothing checked that they answered it the same way. It now checks every EXIF tag against every user rotation and mirror on top of it, because the composition is where the two could agree singly and disagree together. The rest earn their place by having caught something. Writing these found two real errors in this commit's own new code before it ran anywhere: `shown_pixel` was handed the dimensions of the wrong space and overflowed, and the rect map turned the wrong way for the diagonal mirrors. A round trip that returns what went in is the only check worth having here, since every wrong answer is still a picture. No behaviour changes. The permutations are the ones that were already being applied; they are simply applied from one place now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -342,53 +342,27 @@ fn render_stack(
|
||||
}
|
||||
|
||||
/// Turn the proxy the way the photographer is looking at it, so the model
|
||||
/// reads a photograph rather than a scanline order.
|
||||
/// reads a photograph rather than a scanline order — and turn the mask it
|
||||
/// answers with back again, because the composed shader samples masks in
|
||||
/// source space, after the framing map.
|
||||
///
|
||||
/// `Orientation::source_pixel` is the permutation, and it is the same function
|
||||
/// the grid's thumbnails go through — the point being that the detector and
|
||||
/// the thumbnailer agree about which way is up. A quarter turn is a bijection
|
||||
/// of the pixel grid, so nothing is resampled in either direction.
|
||||
/// Both are `dr_types::Orientation`, which is the one place the permutation
|
||||
/// is written: the grid's thumbnails, the develop session's segmentation and
|
||||
/// this example all go through it, so "upright" means one thing across the
|
||||
/// application. A local copy here would be a fourth opinion, and this example
|
||||
/// exists to be the shipping path rather than an imitation of it.
|
||||
fn stand_up(
|
||||
rgb: &[f32],
|
||||
width: usize,
|
||||
height: usize,
|
||||
o: dr_types::Orientation,
|
||||
) -> (Vec<f32>, usize, usize) {
|
||||
if o.is_normal() {
|
||||
return (rgb.to_vec(), width, height);
|
||||
}
|
||||
let (dw, dh) = o.oriented_size(width as u32, height as u32);
|
||||
let (dw, dh) = (dw as usize, dh as usize);
|
||||
|
||||
let mut out = vec![0.0f32; dw * dh * 3];
|
||||
for y in 0..dh {
|
||||
for x in 0..dw {
|
||||
let (sx, sy) = o.source_pixel(x as u32, y as u32, dw as u32, dh as u32);
|
||||
let s = (sy as usize * width + sx as usize) * 3;
|
||||
let d = (y * dw + x) * 3;
|
||||
out[d..d + 3].copy_from_slice(&rgb[s..s + 3]);
|
||||
}
|
||||
}
|
||||
(out, dw, dh)
|
||||
let (out, w, h) = o.into_shown(rgb, width as u32, height as u32, 3);
|
||||
(out, w as usize, h as usize)
|
||||
}
|
||||
|
||||
/// [`stand_up`] run backwards: the same map read as a scatter, which fills
|
||||
/// every source pixel exactly once because the map is a bijection.
|
||||
fn lay_down(mask: &[f32], dw: usize, dh: usize, o: dr_types::Orientation) -> Vec<f32> {
|
||||
if o.is_normal() {
|
||||
return mask.to_vec();
|
||||
}
|
||||
let (sw, sh) = o.oriented_size(dw as u32, dh as u32);
|
||||
let (sw, sh) = (sw as usize, sh as usize);
|
||||
|
||||
let mut out = vec![0.0f32; sw * sh];
|
||||
for y in 0..dh {
|
||||
for x in 0..dw {
|
||||
let (sx, sy) = o.source_pixel(x as u32, y as u32, dw as u32, dh as u32);
|
||||
out[sy as usize * sw + sx as usize] = mask[y * dw + x];
|
||||
}
|
||||
}
|
||||
out
|
||||
o.into_stored(mask, dw as u32, dh as u32, 1).0
|
||||
}
|
||||
|
||||
fn fit(w: u32, h: u32, longest: u32) -> (u32, u32) {
|
||||
|
||||
Reference in New Issue
Block a user