1use crate::layout::{Layout, THRESHOLD};
2use crate::Error;
3use image::imageops::{resize, FilterType};
4use image::math::Rect;
5use image::{GenericImageView, GrayImage};
6use imageproc::contrast::threshold;
7use imageproc::template_matching::{find_extremes, match_template, MatchTemplateMethod};
8use std::fmt;
9use std::ops::{Deref, DerefMut};
10
11#[derive(Debug, Clone, Default)]
13pub struct Ocr(pub Vec<Vec<String>>);
14
15impl Deref for Ocr {
16 type Target = Vec<Vec<String>>;
17 fn deref(&self) -> &Self::Target {
18 &self.0
19 }
20}
21
22impl DerefMut for Ocr {
23 fn deref_mut(&mut self) -> &mut Self::Target {
24 &mut self.0
25 }
26}
27
28pub type OcrStats = Vec<OcrStat>;
30
31#[derive(Debug, Clone, Default)]
33pub struct OcrStat {
34 index: usize,
36 tag: String,
38 min_value: f32,
40 min_value_location: (u32, u32),
42}
43#[derive(Debug, Clone)]
45pub struct OcrResults {
46 pub tiles_ocr: Ocr,
48 pub grid_ocr: Ocr,
50 pub rack_ocr: Ocr,
52 pub tiles_stats: OcrStats,
54 pub grid_stats: OcrStats,
56 pub rack_stats: OcrStats,
58 pub board_area: Rect,
60 pub rack_area: Rect,
62}
63
64impl fmt::Display for Ocr {
65 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
66 let ocr_string = self
67 .iter()
68 .map(|v| v.join(""))
69 .collect::<Vec<String>>()
70 .join("\n");
71 write!(f, "{}", ocr_string)
72 }
73}
74
75impl OcrResults {}
76
77const START_SQUARE: usize = 15 * 7 + 7;
78
79macro_rules! templates {
81 ( $( $x:expr ),* ) => {
82 [$(
83 ($x, include_bytes!(concat!("templates/", $x, ".png"))),
84 )*]
85 };
86}
87
88const LETTER_TEMPLATES: &[(&str, &[u8])] = &templates![
89 "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S",
90 "T", "U", "V", "W", "X", "Y", "Z", "Æ", "Å", "Ä", "Ñ", "Ö", "Ø", "CH", "LL", "RR"
91];
92
93const BONUS_TEMPLATES: &[(&str, &[u8])] = &templates!["2L", "3L", "2W", "3W"];
94
95fn template_from_buffer(name: &str, buf: &[u8]) -> (String, GrayImage) {
96 (
97 String::from(name),
98 image::load_from_memory(buf).unwrap().to_luma8(), )
100}
101
102pub struct Board {
104 pub templates: Vec<(String, GrayImage)>,
105 pub bonus_templates: Vec<(String, GrayImage)>,
106}
107
108impl Default for Board {
109 fn default() -> Self {
110 Board::new()
111 }
112}
113
114impl Board {
115 pub fn new() -> Board {
116 let templates = LETTER_TEMPLATES
117 .iter()
118 .map(|(name, buf)| template_from_buffer(name, buf))
119 .collect();
120 let bonus_templates = BONUS_TEMPLATES
121 .iter()
122 .map(|(name, buf)| template_from_buffer(name, buf))
123 .collect();
124 Board {
125 templates,
126 bonus_templates,
127 }
128 }
129
130 pub fn recognize_screenshot(&self, screenshot: &GrayImage) -> Result<OcrResults, Error> {
142 let layout = Layout::new(&screenshot).segment()?;
143
144 let cells = Layout::get_cells(&layout.rows, &layout.cols);
145 let tile_index = layout.get_tile_index(&cells);
147 let (tiles_ocr, tiles_stats) = self.recognize_tiles(
148 screenshot,
149 &layout,
150 &tile_index,
151 &cells,
152 &self.templates,
153 (15, 15),
154 );
155
156 let (grid_ocr, grid_stats) =
157 self.recognize_board(screenshot, &layout, &cells, &self.bonus_templates, (15, 15));
158
159 let cells = Layout::get_cells(&layout.rack_rows, &layout.rack_cols);
160 let index: Vec<usize> = (0..cells.len()).into_iter().collect();
161 let (rack_ocr, rack_stats) =
162 self.recognize_tiles(screenshot, &layout, &index, &cells, &self.templates, (1, 7));
163
164 let res = OcrResults {
165 tiles_ocr,
166 grid_ocr,
167 rack_ocr,
168 tiles_stats,
169 grid_stats,
170 rack_stats,
171 board_area: layout.board_area,
172 rack_area: layout.rack_area,
173 };
174
175 Ok(res)
176 }
177
178 pub fn recognize_screenshot_from_file(
179 &self,
180 screenshot_filename: &str,
181 ) -> Result<OcrResults, Error> {
182 let gray = image::open(&screenshot_filename)?.into_luma8();
183 self.recognize_screenshot(&gray)
184 }
185
186 pub fn recognize_screenshot_from_memory(&self, screenshot: &[u8]) -> Result<OcrResults, Error> {
187 let gray = image::load_from_memory(&screenshot)?.into_luma8();
188 self.recognize_screenshot(&gray)
189 }
190
191 fn topright(cell: Rect) -> Rect {
192 Rect {
193 x: cell.x + (0.73 * cell.width as f64).round() as u32,
194 y: cell.y + (0.06 * cell.height as f64).round() as u32,
195 width: (0.18 * cell.width as f64).round() as u32,
196 height: (0.27 * cell.height as f64).round() as u32,
197 }
198 }
199
200 fn recognize_tiles(
201 &self,
202 img: &GrayImage,
203 layout: &Layout,
204 tile_index: &[usize],
205 cells: &[Rect],
206 templates: &[(String, GrayImage)],
207 size: (usize, usize),
208 ) -> (Ocr, OcrStats) {
209 let (rows, cols) = size;
211 let row: Vec<String> = (0..cols).into_iter().map(|_| String::from(".")).collect();
212 let mut ocr = Ocr((0..rows)
213 .into_iter()
214 .map(|_| row.clone())
215 .collect::<Vec<_>>());
216 if tile_index.is_empty() {
217 println!("No tiles");
218 return (ocr, Vec::new());
219 }
220
221 let mut stats = Vec::new();
222 let thresh = (THRESHOLD * 256.) as u8;
223 for &index in tile_index.iter() {
224 let cell = cells[index];
225
226 let (mean, std) = layout.area_stats(&cell);
228 let is_blank = mean > 0.9 && std < 0.2;
229
230 let topright = Board::topright(cell);
232 let (mean, std) = layout.area_stats(&topright);
233 let is_wildcard = mean > 0.8 && std < 0.1;
234
235 let mut tile: GrayImage = img.view(cell.x, cell.y, cell.width, cell.height).to_image();
237 tile = threshold(&tile, thresh);
239
240 if tile.width() > 67 {
241 tile = resize(&tile, 67, 67, FilterType::Lanczos3);
242 }
243
244 let area = tile.view(6, 3, 40, 62).to_image();
247
248 let (letter, min_value, min_value_location) = if !is_blank {
250 Board::match_template(&area, templates)
251 } else {
252 (String::from("*"), 0.0_f32, (0_u32, 0_u32))
253 };
254 let (row, col) = (index / cols, index % cols);
255 ocr[row][col] = if !is_wildcard {
256 letter.to_lowercase()
257 } else {
258 letter.clone()
259 };
260 stats.push(OcrStat {
261 index,
262 tag: letter.clone(),
263 min_value,
264 min_value_location,
265 });
266 }
267 (ocr, stats)
268 }
269
270 fn recognize_board(
271 &self,
272 img: &GrayImage,
273 layout: &Layout,
274 cells: &[Rect],
275 templates: &[(String, GrayImage)],
276 size: (usize, usize),
277 ) -> (Ocr, OcrStats) {
278 let (rows, cols) = size;
280
281 let row: Vec<String> = (0..cols).into_iter().map(|_| String::from("--")).collect();
282 let mut ocr = Ocr((0..rows)
283 .into_iter()
284 .map(|_| row.clone())
285 .collect::<Vec<_>>());
286 ocr[7][7] = String::from("ss"); let mut stats = Vec::new();
288 for (index, cell) in cells.iter().enumerate() {
289 let mean = layout.mean(&cell);
290 if mean > THRESHOLD || mean < 0.25 || index == START_SQUARE {
291 continue;
292 }
293
294 let tile: GrayImage = img.view(cell.x, cell.y, cell.width, cell.height).to_image();
296
297 let area = tile.view(8, 21, 48, 28).to_image();
300
301 let (letter, min_value, min_value_location) = Board::match_template(&area, templates);
303 let (row, col) = (index / cols, index % cols);
304 ocr[row][col] = letter.to_lowercase();
305 stats.push(OcrStat {
306 index,
307 tag: letter.to_lowercase(),
308 min_value,
309 min_value_location,
310 });
311 }
312 (ocr, stats)
313 }
314
315 fn match_template(
316 tile: &GrayImage,
317 templates: &[(String, GrayImage)],
318 ) -> (String, f32, (u32, u32)) {
319 let method = MatchTemplateMethod::SumOfSquaredErrorsNormalized;
320 let mut matches = templates
321 .iter()
322 .map(|(letter, template)| {
323 (
324 letter.clone(),
325 find_extremes(&match_template(&tile, &template, method)),
326 )
327 })
328 .collect::<Vec<_>>();
329 matches.sort_by(|a, b| a.1.min_value.partial_cmp(&b.1.min_value).unwrap());
331 let (letter, extreme) = matches[0].clone();
332 (letter, extreme.min_value, extreme.min_value_location)
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn test_topright() {
342 let cell = Rect {
343 x: 0,
344 y: 0,
345 width: 67,
346 height: 67,
347 };
348 let topright = Board::topright(cell);
349 println!("{:?} {:?}", cell, topright);
350 assert_eq!(
351 topright,
352 Rect {
353 x: 49,
354 y: 4,
355 width: 12,
356 height: 18
357 }
358 );
359 }
360}