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.
157 lines
4.8 KiB
Rust
157 lines
4.8 KiB
Rust
//! TRACES: FR-MRG-4
|
||
//! The largest rectangle inside a coverage mask, found a row at a time.
|
||
//!
|
||
//! A merged panorama has ragged edges: the frames' footprints under a
|
||
//! cylinder or a sphere are not rectangles, and the composite carries a
|
||
//! black border where none of them reached. FR-MRG-4 asks for an auto-crop
|
||
//! to the largest inscribed rectangle. This finds it as the bands are
|
||
//! produced, so the composite is never held to be measured (FR-MRG-11):
|
||
//! each row extends a running histogram of consecutive covered rows above
|
||
//! it, and the largest rectangle ending on that row is the largest
|
||
//! rectangle under the histogram — a stack pass, linear in the width.
|
||
//!
|
||
//! The crop is written as the DNG's `DefaultCropOrigin`/`DefaultCropSize`,
|
||
//! which every reader honours and which discards nothing: the pixels
|
||
//! outside it are still in the file for a photographer who wants them.
|
||
|
||
/// The rectangle so far, in pixels from the top left.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||
pub struct Rect {
|
||
pub x: u32,
|
||
pub y: u32,
|
||
pub width: u32,
|
||
pub height: u32,
|
||
}
|
||
|
||
impl Rect {
|
||
pub fn area(&self) -> u64 {
|
||
u64::from(self.width) * u64::from(self.height)
|
||
}
|
||
}
|
||
|
||
/// Feed rows top to bottom; ask for the best at any point.
|
||
#[derive(Debug, Clone)]
|
||
pub struct Inscribed {
|
||
width: usize,
|
||
/// How many consecutive covered rows end at the last row fed, per column.
|
||
heights: Vec<u32>,
|
||
rows: u32,
|
||
best: Rect,
|
||
}
|
||
|
||
impl Inscribed {
|
||
pub fn new(width: u32) -> Self {
|
||
Inscribed {
|
||
width: width as usize,
|
||
heights: vec![0; width as usize],
|
||
rows: 0,
|
||
best: Rect::default(),
|
||
}
|
||
}
|
||
|
||
/// One more row of coverage, `width` long.
|
||
pub fn push_row(&mut self, covered: &[bool]) {
|
||
debug_assert_eq!(covered.len(), self.width);
|
||
for (h, &c) in self.heights.iter_mut().zip(covered) {
|
||
*h = if c { *h + 1 } else { 0 };
|
||
}
|
||
self.rows += 1;
|
||
// Largest rectangle under the histogram, with a sentinel column of
|
||
// height 0 at the end so every bar is popped.
|
||
let mut stack: Vec<usize> = Vec::new();
|
||
for i in 0..=self.width {
|
||
let h = if i < self.width { self.heights[i] } else { 0 };
|
||
while let Some(&top) = stack.last() {
|
||
if self.heights[top] <= h {
|
||
break;
|
||
}
|
||
stack.pop();
|
||
let height = self.heights[top];
|
||
let left = stack.last().map_or(0, |&l| l + 1);
|
||
let width = (i - left) as u32;
|
||
let area = u64::from(width) * u64::from(height);
|
||
if area > self.best.area() {
|
||
self.best = Rect {
|
||
x: left as u32,
|
||
y: self.rows - height,
|
||
width,
|
||
height,
|
||
};
|
||
}
|
||
}
|
||
stack.push(i);
|
||
}
|
||
}
|
||
|
||
/// Several rows at once, as a band hands them over.
|
||
pub fn push_rows(&mut self, covered: &[bool], rows: u32) {
|
||
for r in 0..rows as usize {
|
||
self.push_row(&covered[r * self.width..(r + 1) * self.width]);
|
||
}
|
||
}
|
||
|
||
pub fn best(&self) -> Rect {
|
||
self.best
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn from_art(art: &[&str]) -> Rect {
|
||
let mut ins = Inscribed::new(art[0].len() as u32);
|
||
for row in art {
|
||
let covered: Vec<bool> = row.chars().map(|c| c == '#').collect();
|
||
ins.push_row(&covered);
|
||
}
|
||
ins.best()
|
||
}
|
||
|
||
#[test]
|
||
fn a_full_mask_is_its_own_rectangle() {
|
||
let r = from_art(&["####", "####", "####"]);
|
||
assert_eq!(
|
||
r,
|
||
Rect {
|
||
x: 0,
|
||
y: 0,
|
||
width: 4,
|
||
height: 3
|
||
}
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn ragged_edges_are_cut_off() {
|
||
// A cylinder's footprint: narrower at top and bottom.
|
||
let r = from_art(&[
|
||
"..####..", ".######.", "########", "########", ".######.", "..####..",
|
||
]);
|
||
// 6 wide × 4 tall = 24 beats 8 × 2 = 16 and 4 × 6 = 24 ties; the
|
||
// first found wins a tie, which is the wider one here.
|
||
assert_eq!(r.area(), 24);
|
||
assert!(r.width == 6 && r.height == 4 || r.width == 4 && r.height == 6);
|
||
}
|
||
|
||
#[test]
|
||
fn a_hole_is_avoided() {
|
||
let r = from_art(&["#####", "##.##", "#####", "#####"]);
|
||
// Left of the hole: 2 × 4 = 8; right: 2 × 4 = 8; below: 5 × 2 = 10.
|
||
assert_eq!(
|
||
r,
|
||
Rect {
|
||
x: 0,
|
||
y: 2,
|
||
width: 5,
|
||
height: 2
|
||
}
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn nothing_covered_is_nothing() {
|
||
assert_eq!(from_art(&["....", "...."]).area(), 0);
|
||
}
|
||
}
|