1use crate::error::Error;
2use image::{math::Rect, GrayImage, ImageBuffer, Luma};
3use imageproc::integral_image::{integral_image, integral_squared_image, sum_image_pixels};
4
5pub const THRESHOLD: f64 = 0.65;
6
7type IntegralImage = ImageBuffer<Luma<u64>, Vec<u64>>;
8
9pub struct Layout {
11 integral: IntegralImage,
12 integral_squared: IntegralImage,
13 pub screen: Rect,
14 pub board_area: Rect,
15 pub rack_area: Rect,
16 pub rows: Vec<(usize, usize)>,
17 pub cols: Vec<(usize, usize)>,
18 pub rack_rows: Vec<(usize, usize)>,
19 pub rack_cols: Vec<(usize, usize)>,
20}
21
22#[derive(Debug, PartialEq)]
23pub enum Segment {
24 LookForTopBorder(usize),
25 InTopBorder,
26 LookForRisingEdge(usize),
27 InTile(usize),
28 LookForBottomBorder(usize),
29 InBottomBorder,
30 LookForRack,
31 InRack,
32 Done,
33}
34
35fn close(a: u32, b: u32, tol: u32) -> bool {
36 (a as i32 - b as i32).abs() < tol as i32
37}
38
39fn bounds(rect: Rect) -> (u32, u32, u32, u32) {
40 (rect.x, rect.y, rect.width, rect.height)
41}
42
43impl Layout {
44 pub fn new(img: &GrayImage) -> Layout {
45 let integral: IntegralImage = integral_image::<_, u64>(img);
46 let integral_squared: IntegralImage = integral_squared_image::<_, u64>(img);
47 let screen = Rect {
48 x: 0,
49 y: 0,
50 width: img.width(),
51 height: img.height(),
52 };
53 let board_area = Rect {
54 x: 0,
55 y: 0,
56 width: 0,
57 height: 0,
58 };
59 let rack_area = Rect {
60 x: 0,
61 y: 0,
62 width: 0,
63 height: 0,
64 };
65 Layout {
66 integral,
67 integral_squared,
68 screen,
69 board_area,
70 rack_area,
71 rows: Vec::new(),
72 cols: Vec::new(),
73 rack_rows: Vec::new(),
74 rack_cols: Vec::new(),
75 }
76 }
77
78 pub fn segment(mut self) -> Result<Self, Error> {
79 let mut state = Segment::LookForTopBorder(0);
80 let rowstats = self.stats(bounds(self.screen), true);
81 let (mut rack_y, mut rack_height) = (0, 0);
82 let tol = 2;
83 for (i, &(sum, var)) in rowstats.iter().enumerate() {
84 match state {
85 Segment::LookForTopBorder(n) => {
86 if close(sum, 51, tol) && (var < 25) {
87 state = Segment::LookForTopBorder(n + 1);
88 }
89 if n > 3 {
90 state = Segment::InTopBorder;
91 }
92 }
93 Segment::InTopBorder => {
94 if close(sum, 24, tol) {
95 state = Segment::LookForRisingEdge(0);
96 }
97 }
98 Segment::LookForRisingEdge(n) => {
99 if sum > 24 + tol {
100 self.rows.push((i, 0));
101 state = Segment::InTile(n);
102 }
103 }
104 Segment::InTile(n) => {
105 if close(sum, 24, tol) {
106 self.rows[n].1 = i - 1;
107 if n < 14 {
108 state = Segment::LookForRisingEdge(n + 1);
109 } else {
110 state = Segment::LookForBottomBorder(0);
111 }
112 }
113 }
114 Segment::LookForBottomBorder(n) => {
115 if close(sum, 51, tol) && (var < 25) {
116 state = Segment::LookForBottomBorder(n + 1);
117 }
118 if n > 5 {
119 state = Segment::InBottomBorder;
120 }
121 }
122 Segment::InBottomBorder => {
123 if close(sum, 24, tol) && (var < 10) {
124 state = Segment::LookForRack;
125 }
126 }
127 Segment::LookForRack => {
128 if var > 100 {
129 rack_y = i as u32;
130 state = Segment::InRack;
131 }
132 }
133 Segment::InRack => {
134 if close(sum, 24, tol) && (var == 0) {
136 rack_height = i as u32 - rack_y;
137 state = Segment::Done;
139 }
140 }
141 Segment::Done => {}
142 }
143 }
144 if state != Segment::Done {
145 return Err(Error::LayoutFailed(state));
146 }
147 let y0 = self.rows[0].0 as u32;
148 let y1 = self.rows[14].1 as u32;
149 self.board_area = Rect {
150 x: 0,
151 y: y0,
152 width: self.screen.width,
153 height: y1 - y0,
154 };
155 self.rack_area = Rect {
156 x: 0,
157 y: rack_y,
158 width: self.screen.width,
159 height: rack_height,
160 };
161
162 let w = self.board_area.width;
164 let h = self.board_area.height;
165 let aspect_ratio = h as f32 / w as f32;
166 if (aspect_ratio - 1.0).abs() > 0.02 {
167 return Err(Error::BoardNotSquare(aspect_ratio));
168 }
169 self.cols = self.segment_board_columns()?;
170 if state != Segment::Done {
171 return Err(Error::LayoutFailed(state));
172 }
173 self.rack_rows
174 .push((rack_y as usize, (rack_y + rack_height - 1) as usize));
175 self.rack_cols = self.segment_rack_columns()?;
176 Ok(self)
177 }
178
179 fn segment_columns(
180 threshold: u32,
181 maxcols: usize,
182 colstats: &[(u32, u32)],
183 ) -> Result<Vec<(usize, usize)>, Error> {
184 let mut cols = Vec::new();
185 let mut state = Segment::LookForRisingEdge(0);
186 let tol = 2;
187 for (i, &(sum, var)) in colstats.iter().enumerate() {
188 match state {
189 Segment::LookForRisingEdge(n) => {
190 if sum > threshold + tol {
191 cols.push((i, 0));
192 state = Segment::InTile(n);
194 }
195 }
196 Segment::InTile(n) => {
197 if close(sum, 24, tol) && (var == 0) {
198 cols[n].1 = i - 1;
199 if n + 1 < maxcols {
200 state = Segment::LookForRisingEdge(n + 1);
201 } else {
202 state = Segment::Done;
203 }
204 }
205 }
206 Segment::Done => {}
207 _ => panic!("Unexpected segment state"),
208 }
209 }
210 Ok(cols)
214 }
215 fn segment_board_columns(&self) -> Result<Vec<(usize, usize)>, Error> {
216 let colstats = self.stats(bounds(self.board_area), false);
217 Self::segment_columns(24, 15, &colstats)
218 }
219
220 fn segment_rack_columns(&self) -> Result<Vec<(usize, usize)>, Error> {
221 let colstats = self.stats(bounds(self.rack_area), false);
222 Self::segment_columns(48, 7, &colstats)
223 }
224
225 fn stats(&self, bounds: (u32, u32, u32, u32), horizontal: bool) -> Vec<(u32, u32)> {
226 let mut stats = Vec::new();
227 let (x, y, w, h) = bounds;
228 let (dim, count) = if horizontal { (h, w) } else { (w, h) };
229 let area = |i| {
230 if horizontal {
231 (x, i, x + w - 1, i)
232 } else {
233 (i, y, i, y + h - 1)
234 }
235 };
236 for i in 0..dim {
237 let (left, top, right, bottom) = area(i);
238 let sum = sum_image_pixels(&self.integral, left, top, right, bottom);
239 let var = variance(
240 &self.integral,
241 &self.integral_squared,
242 left,
243 top,
244 right,
245 bottom,
246 );
247 stats.push((sum[0] as u32 / count, var as u32));
248 }
249 stats
250 }
251
252 pub fn get_cells(rows: &[(usize, usize)], cols: &[(usize, usize)]) -> Vec<Rect> {
254 let mut cells = Vec::new();
255 if cols.is_empty() {
256 return cells;
257 }
258 let tiles_height: usize = rows.iter().map(|&(y0, y1)| y1 - y0).sum();
260 let tiles_width: usize = cols.iter().map(|&(x0, x1)| x1 - x0).sum();
261 let (tile_height, tile_width) = (
262 (tiles_height / rows.len()) as u32,
263 (tiles_width / cols.len()) as u32,
264 );
265 for &(y0, _y1) in rows.iter() {
266 for &(x0, _x1) in cols.iter() {
267 let cell = Rect {
268 x: x0 as u32,
269 y: y0 as u32,
270 width: tile_width,
271 height: tile_height,
272 };
273 cells.push(cell);
274 }
275 }
276 cells
277 }
278
279 pub fn get_tile_index(&self, cells: &[Rect]) -> Vec<usize> {
280 let mut index = Vec::new();
281 for (i, &cell) in cells.iter().enumerate() {
282 let (left, top, right, bottom) = (
283 cell.x,
284 cell.y,
285 cell.x + cell.width - 1,
286 cell.y + cell.height - 1,
287 );
288 let sum = sum_image_pixels(&self.integral, left, top, right, bottom);
289 let mean = sum[0] as f64 / (cell.width * cell.height) as f64 / 256.;
290 if mean > THRESHOLD {
291 index.push(i);
292 }
293 }
294 index
295 }
296
297 #[allow(dead_code)]
299 pub fn mean(&self, rect: &Rect) -> f64 {
300 let sum = sum_image_pixels(
301 &self.integral,
302 rect.x,
303 rect.y,
304 rect.x + rect.width - 1,
305 rect.y + rect.height - 1,
306 );
307 let count = rect.width * rect.height;
308 sum[0] as f64 / count as f64 / 256.
309 }
310
311 pub fn area_stats(&self, rect: &Rect) -> (f64, f64) {
313 let (left, top, right, bottom) = (
314 rect.x,
315 rect.y,
316 rect.x + rect.width - 1,
317 rect.y + rect.height - 1,
318 );
319 let sum = sum_image_pixels(&self.integral, left, top, right, bottom);
320 let var = variance(
321 &self.integral,
322 &self.integral_squared,
323 left,
324 top,
325 right,
326 bottom,
327 );
328 let count = rect.width * rect.height;
329 (sum[0] as f64 / count as f64 / 256., var.sqrt() / 256.)
330 }
331}
332
333pub fn variance(
335 integral_image: &IntegralImage,
336 integral_squared_image: &IntegralImage,
337 left: u32,
338 top: u32,
339 right: u32,
340 bottom: u32,
341) -> f64 {
342 let n = (right - left + 1) as f64 * (bottom - top + 1) as f64;
344 let sum_sq = sum_image_pixels(integral_squared_image, left, top, right, bottom)[0];
345 let sum = sum_image_pixels(integral_image, left, top, right, bottom)[0];
346 (sum_sq as f64 - (sum as f64).powi(2) / n) / n
347}
348
349#[cfg(test)]
350mod tests {
351 use image::{GenericImageView, GrayImage, ImageBuffer};
352
353 #[test]
354 fn test_subimg() {
355 let img: GrayImage = ImageBuffer::new(540, 1080);
356 let sub = img.view(10, 100, 500, 100);
357 println!("{:?} {:?}", img.bounds(), sub.bounds());
359 }
360}