//! 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, 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 = 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 = 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); } }