Files
DarkRoom/core/dr-export/src/inscribed.rs
T
dtourolle 42d11d919b cargo fmt and clippy across the panorama work, and one lint master carried
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.
2026-09-19 15:53:06 +02:00

157 lines
4.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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);
}
}