mimick/library/lightbox/ocr/
geometry.rs1use crate::api_client::OcrBox;
4
5const ROW_THRESHOLD: f64 = 0.02;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub(super) struct ImageRect {
11 pub x: f64,
12 pub y: f64,
13 pub w: f64,
14 pub h: f64,
15}
16
17pub(super) fn contain_rect(area: (f64, f64), image: (f64, f64)) -> Option<ImageRect> {
19 let (area_w, area_h) = area;
20 let (image_w, image_h) = image;
21 if ![area_w, area_h, image_w, image_h].iter().all(|v| *v > 0.0) {
22 return None;
23 }
24 let scale = (area_w / image_w).min(area_h / image_h);
25 let (w, h) = (image_w * scale, image_h * scale);
26 Some(ImageRect {
27 x: (area_w - w) / 2.0,
28 y: (area_h - h) / 2.0,
29 w,
30 h,
31 })
32}
33
34pub(super) fn quad_in_area(rect: ImageRect, corners: [(f64, f64); 4]) -> [(f64, f64); 4] {
36 corners.map(|(nx, ny)| (rect.x + nx * rect.w, rect.y + ny * rect.h))
37}
38
39pub(super) fn quad_contains(quad: &[(f64, f64); 4], p: (f64, f64)) -> bool {
41 let (mut positive, mut negative) = (false, false);
42 for i in 0..4 {
43 let (ax, ay) = quad[i];
44 let (bx, by) = quad[(i + 1) % 4];
45 let cross = (bx - ax) * (p.1 - ay) - (by - ay) * (p.0 - ax);
46 positive |= cross > 0.0;
47 negative |= cross < 0.0;
48 }
49 !(positive && negative)
50}
51
52pub(super) fn reading_order(mut boxes: Vec<OcrBox>) -> Vec<OcrBox> {
54 boxes.sort_by(|a, b| a.y1.total_cmp(&b.y1));
55 let mut rows: Vec<Vec<OcrBox>> = Vec::new();
56 for ocr_box in boxes {
57 match rows.last_mut() {
58 Some(row) if ocr_box.y1 - row[0].y1 < ROW_THRESHOLD => row.push(ocr_box),
59 _ => rows.push(vec![ocr_box]),
60 }
61 }
62 rows.into_iter()
63 .flat_map(|mut row| {
64 row.sort_by(|a, b| a.x1.total_cmp(&b.x1));
65 row
66 })
67 .collect()
68}
69
70#[cfg(test)]
72pub(super) fn ob(text: &str, x: f64, y: f64) -> OcrBox {
73 OcrBox {
74 text: text.to_string(),
75 x1: x,
76 y1: y,
77 x2: x + 0.25,
78 y2: y,
79 x3: x + 0.25,
80 y3: y + 0.0625,
81 x4: x,
82 y4: y + 0.0625,
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn contain_rect_letterboxes_wide_image() {
92 assert_eq!(
93 contain_rect((1000.0, 1000.0), (2000.0, 1000.0)),
94 Some(ImageRect {
95 x: 0.0,
96 y: 250.0,
97 w: 1000.0,
98 h: 500.0
99 })
100 );
101 }
102
103 #[test]
104 fn contain_rect_pillarboxes_tall_image() {
105 assert_eq!(
106 contain_rect((1000.0, 500.0), (500.0, 1000.0)),
107 Some(ImageRect {
108 x: 375.0,
109 y: 0.0,
110 w: 250.0,
111 h: 500.0
112 })
113 );
114 }
115
116 #[test]
117 fn contain_rect_rejects_empty_sizes() {
118 assert_eq!(contain_rect((0.0, 500.0), (100.0, 100.0)), None);
119 assert_eq!(contain_rect((500.0, 500.0), (100.0, 0.0)), None);
120 }
121
122 #[test]
123 fn quad_maps_into_image_rect() {
124 let rect = ImageRect {
125 x: 0.0,
126 y: 250.0,
127 w: 1000.0,
128 h: 500.0,
129 };
130 assert_eq!(
131 quad_in_area(rect, [(0.125, 0.25), (0.5, 0.25), (0.5, 0.5), (0.125, 0.5)]),
132 [
133 (125.0, 375.0),
134 (500.0, 375.0),
135 (500.0, 500.0),
136 (125.0, 500.0)
137 ]
138 );
139 }
140
141 #[test]
142 fn quad_contains_handles_rotated_box() {
143 let diamond = [(50.0, 0.0), (100.0, 50.0), (50.0, 100.0), (0.0, 50.0)];
144 assert!(quad_contains(&diamond, (50.0, 50.0)));
145 assert!(quad_contains(&diamond, (50.0, 0.0)));
146 assert!(!quad_contains(&diamond, (10.0, 10.0)));
147 }
148
149 #[test]
150 fn reading_order_groups_rows_then_columns() {
151 let order = reading_order(vec![
152 ob("right", 0.625, 0.5),
153 ob("left", 0.125, 0.51),
154 ob("top", 0.5, 0.25),
155 ]);
156 let texts: Vec<_> = order.iter().map(|b| b.text.as_str()).collect();
157 assert_eq!(texts, ["top", "left", "right"]);
158 }
159
160 #[test]
161 fn reading_order_survives_staircase_rows() {
162 let boxes: Vec<_> = (0..30)
163 .map(|i| ob(&i.to_string(), 1.0 - i as f64 * 0.03, i as f64 * 0.015))
164 .collect();
165 let mut texts: Vec<_> = reading_order(boxes).into_iter().map(|b| b.text).collect();
166 texts.sort();
167 let mut expected: Vec<_> = (0..30).map(|i| i.to_string()).collect();
168 expected.sort();
169 assert_eq!(texts, expected);
170 }
171}