Skip to main content

wordfeud_ocr/
layout.rs

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
9/// Represents the layout of a wordfeud board
10pub 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                    // println!("{}: Inrack: {} {}", i, sum, var);
135                    if close(sum, 24, tol) && (var == 0) {
136                        rack_height = i as u32 - rack_y;
137                        // println!("Done!");
138                        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        // the board area should be approximately square
163        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                        // println!("{}: InTile {}", i, n);
193                        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        // if state != Segment::Done {
211        //     return Err(Error::LayoutFailed(state));
212        // }
213        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    /// Create tile sub images for board
253    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        // find out what size our tiles should be
259        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    /// calculate mean pixel value in rect
298    #[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    /// calculate mean and variance pixel value in rect
312    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
333/// This is a modified copy of [imageproc::integral_image::variance]()
334pub 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    // TODO: same improvements as for sum_image_pixels, plus check that the given rect is valid.
343    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        // let b = img.bounds();
358        println!("{:?} {:?}", img.bounds(), sub.bounds());
359    }
360}