The dr-face comparison is master's: a negated partial-order test on the eye box's width, rewritten as the two conditions it meant.
263 lines
9.1 KiB
Rust
263 lines
9.1 KiB
Rust
//! The grayscale proxy a detector reads.
|
||
//!
|
||
//! Alignment runs on proxies (FR-MRG-7) — a detector at 1024 px sees
|
||
//! everything it needs, and the full-resolution frames never leave the GPU.
|
||
//! This is that proxy: one channel, `f32` in `0.0..=1.0`, upright, and no
|
||
//! larger than the detector's fixed input.
|
||
|
||
/// A single-channel image, row-major, values in `0.0..=1.0`.
|
||
#[derive(Debug, Clone, PartialEq)]
|
||
pub struct Gray {
|
||
pub width: usize,
|
||
pub height: usize,
|
||
pub data: Vec<f32>,
|
||
}
|
||
|
||
impl Gray {
|
||
/// From tightly packed 8-bit RGBA, by the Rec. 709 luma weights.
|
||
///
|
||
/// The proxy is what a detector looks at, not what the photographer
|
||
/// sees, so which luma is used matters less than that it is the same one
|
||
/// for every frame — a keypoint's descriptor must not change between two
|
||
/// frames because they were converted differently.
|
||
pub fn from_rgba8(rgba: &[u8], width: usize, height: usize) -> Gray {
|
||
let n = width * height;
|
||
assert!(
|
||
rgba.len() >= n * 4,
|
||
"rgba buffer is short for {width}×{height}"
|
||
);
|
||
let data = rgba[..n * 4]
|
||
.chunks_exact(4)
|
||
.map(|p| {
|
||
(0.2126 * f32::from(p[0]) + 0.7152 * f32::from(p[1]) + 0.0722 * f32::from(p[2]))
|
||
/ 255.0
|
||
})
|
||
.collect();
|
||
Gray {
|
||
width,
|
||
height,
|
||
data,
|
||
}
|
||
}
|
||
|
||
/// Apply an EXIF orientation so the image is upright.
|
||
///
|
||
/// Learned detectors are not rotation-invariant — a descriptor of a
|
||
/// feature seen sideways is a different descriptor — and a portrait set
|
||
/// (the 6D fixture is one) would match poorly or not at all fed as
|
||
/// stored. The camera says which way is up; the proxy is turned before
|
||
/// anything looks at it, and the composite is written upright.
|
||
///
|
||
/// The value is the EXIF `Orientation` tag. Mirrored values (2, 4, 5, 7)
|
||
/// are not produced by any camera and are treated as their unmirrored
|
||
/// counterparts.
|
||
pub fn oriented(&self, orientation: u16) -> Gray {
|
||
match orientation {
|
||
3 | 4 => self.rotated_180(),
|
||
6 | 5 => self.rotated_90_cw(),
|
||
8 | 7 => self.rotated_90_ccw(),
|
||
_ => self.clone(),
|
||
}
|
||
}
|
||
|
||
fn rotated_90_cw(&self) -> Gray {
|
||
let (w, h) = (self.width, self.height);
|
||
let mut data = vec![0.0; w * h];
|
||
for y in 0..h {
|
||
for x in 0..w {
|
||
// Source (x, y) lands at (h - 1 - y, x) in an h-wide image.
|
||
data[x * h + (h - 1 - y)] = self.data[y * w + x];
|
||
}
|
||
}
|
||
Gray {
|
||
width: h,
|
||
height: w,
|
||
data,
|
||
}
|
||
}
|
||
|
||
fn rotated_90_ccw(&self) -> Gray {
|
||
let (w, h) = (self.width, self.height);
|
||
let mut data = vec![0.0; w * h];
|
||
for y in 0..h {
|
||
for x in 0..w {
|
||
// Source (x, y) lands at (y, w - 1 - x) in an h-wide image.
|
||
data[(w - 1 - x) * h + y] = self.data[y * w + x];
|
||
}
|
||
}
|
||
Gray {
|
||
width: h,
|
||
height: w,
|
||
data,
|
||
}
|
||
}
|
||
|
||
fn rotated_180(&self) -> Gray {
|
||
let mut data = self.data.clone();
|
||
data.reverse();
|
||
Gray {
|
||
width: self.width,
|
||
height: self.height,
|
||
data,
|
||
}
|
||
}
|
||
|
||
/// Resample to exactly `width × height` by area averaging on the way
|
||
/// down and bilinear on the way up.
|
||
///
|
||
/// Area averaging, not point sampling, for a reduction: a 5472 px frame
|
||
/// to 1024 is a factor of five, and picking one source pixel in
|
||
/// twenty-five aliases every edge the detector is looking for.
|
||
pub fn resampled(&self, width: usize, height: usize) -> Gray {
|
||
if width == self.width && height == self.height {
|
||
return self.clone();
|
||
}
|
||
let mut data = vec![0.0f32; width * height];
|
||
let sx = self.width as f64 / width as f64;
|
||
let sy = self.height as f64 / height as f64;
|
||
if sx >= 1.0 && sy >= 1.0 {
|
||
for oy in 0..height {
|
||
let y0 = (oy as f64 * sy) as usize;
|
||
let y1 = (((oy + 1) as f64 * sy) as usize).clamp(y0 + 1, self.height);
|
||
for ox in 0..width {
|
||
let x0 = (ox as f64 * sx) as usize;
|
||
let x1 = (((ox + 1) as f64 * sx) as usize).clamp(x0 + 1, self.width);
|
||
let mut sum = 0.0f32;
|
||
for y in y0..y1 {
|
||
let row = &self.data[y * self.width..(y + 1) * self.width];
|
||
sum += row[x0..x1].iter().sum::<f32>();
|
||
}
|
||
data[oy * width + ox] = sum / ((y1 - y0) * (x1 - x0)) as f32;
|
||
}
|
||
}
|
||
} else {
|
||
for oy in 0..height {
|
||
let fy = ((oy as f64 + 0.5) * sy - 0.5).max(0.0);
|
||
let y0 = (fy as usize).min(self.height - 1);
|
||
let y1 = (y0 + 1).min(self.height - 1);
|
||
let ty = (fy - y0 as f64) as f32;
|
||
for ox in 0..width {
|
||
let fx = ((ox as f64 + 0.5) * sx - 0.5).max(0.0);
|
||
let x0 = (fx as usize).min(self.width - 1);
|
||
let x1 = (x0 + 1).min(self.width - 1);
|
||
let tx = (fx - x0 as f64) as f32;
|
||
let p = |x: usize, y: usize| self.data[y * self.width + x];
|
||
let top = p(x0, y0) * (1.0 - tx) + p(x1, y0) * tx;
|
||
let bot = p(x0, y1) * (1.0 - tx) + p(x1, y1) * tx;
|
||
data[oy * width + ox] = top * (1.0 - ty) + bot * ty;
|
||
}
|
||
}
|
||
}
|
||
Gray {
|
||
width,
|
||
height,
|
||
data,
|
||
}
|
||
}
|
||
|
||
/// Scale so the image fits inside `max_width × max_height`, preserving
|
||
/// aspect, never enlarging. Returns the image and the scale applied,
|
||
/// which is what maps a proxy keypoint back to the source.
|
||
pub fn fitted(&self, max_width: usize, max_height: usize) -> (Gray, f64) {
|
||
let scale = (max_width as f64 / self.width as f64)
|
||
.min(max_height as f64 / self.height as f64)
|
||
.min(1.0);
|
||
let w = ((self.width as f64 * scale).round() as usize).max(1);
|
||
let h = ((self.height as f64 * scale).round() as usize).max(1);
|
||
(self.resampled(w, h), w as f64 / self.width as f64)
|
||
}
|
||
|
||
/// Copy into the top-left of a `width × height` canvas, zero elsewhere.
|
||
///
|
||
/// The detector's input is a fixed shape (S15.2), and a frame that fits
|
||
/// inside it is padded rather than stretched: stretching changes the
|
||
/// aspect and with it every descriptor.
|
||
pub fn padded(&self, width: usize, height: usize) -> Gray {
|
||
assert!(self.width <= width && self.height <= height);
|
||
let mut data = vec![0.0; width * height];
|
||
for y in 0..self.height {
|
||
data[y * width..y * width + self.width]
|
||
.copy_from_slice(&self.data[y * self.width..(y + 1) * self.width]);
|
||
}
|
||
Gray {
|
||
width,
|
||
height,
|
||
data,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn ramp(w: usize, h: usize) -> Gray {
|
||
Gray {
|
||
width: w,
|
||
height: h,
|
||
data: (0..w * h).map(|i| i as f32).collect(),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn rotating_four_quarter_turns_is_the_identity() {
|
||
let g = ramp(5, 3);
|
||
let mut r = g.clone();
|
||
for _ in 0..4 {
|
||
r = r.rotated_90_cw();
|
||
}
|
||
assert_eq!(r, g);
|
||
assert_eq!(g.rotated_90_cw().rotated_90_ccw(), g);
|
||
assert_eq!(g.rotated_180().rotated_180(), g);
|
||
}
|
||
|
||
#[test]
|
||
fn a_clockwise_turn_moves_the_top_left_to_the_top_right() {
|
||
// 2×3 image, pixel values by position.
|
||
let g = ramp(2, 3);
|
||
let r = g.rotated_90_cw();
|
||
assert_eq!((r.width, r.height), (3, 2));
|
||
// Top-left of source (value 0) is at top-right of result.
|
||
assert_eq!(r.data[2], 0.0);
|
||
// Bottom-left of source (value 4) is at top-left of result.
|
||
assert_eq!(r.data[0], 4.0);
|
||
}
|
||
|
||
#[test]
|
||
fn orientation_8_is_a_counter_clockwise_turn() {
|
||
let g = ramp(4, 2);
|
||
assert_eq!(g.oriented(8), g.rotated_90_ccw());
|
||
assert_eq!(g.oriented(6), g.rotated_90_cw());
|
||
assert_eq!(g.oriented(1), g);
|
||
}
|
||
|
||
#[test]
|
||
fn downsampling_by_two_averages_blocks() {
|
||
let g = Gray {
|
||
width: 4,
|
||
height: 2,
|
||
data: vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
|
||
};
|
||
let r = g.resampled(2, 1);
|
||
assert_eq!(r.data, vec![2.5, 4.5]);
|
||
}
|
||
|
||
#[test]
|
||
fn fitting_never_enlarges_and_reports_the_scale() {
|
||
let g = ramp(100, 50);
|
||
let (f, s) = g.fitted(1024, 768);
|
||
assert_eq!((f.width, f.height), (100, 50));
|
||
assert_eq!(s, 1.0);
|
||
let (f, s) = g.fitted(50, 50);
|
||
assert_eq!((f.width, f.height), (50, 25));
|
||
assert_eq!(s, 0.5);
|
||
}
|
||
|
||
#[test]
|
||
fn padding_places_the_image_at_the_origin() {
|
||
let g = ramp(2, 2);
|
||
let p = g.padded(3, 3);
|
||
assert_eq!(p.data, vec![0.0, 1.0, 0.0, 2.0, 3.0, 0.0, 0.0, 0.0, 0.0]);
|
||
}
|
||
}
|