diff --git a/Cargo.toml b/Cargo.toml index ac36434..ca6374c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cyberbot2077" -version = "0.1.0" +version = "0.2.0" edition = "2021" [dependencies] @@ -8,5 +8,3 @@ winapi = { version = "0.3", features = ["winuser"] } clipboard-win = "4.4" bmp = "0.5" winput = "0.2" - -#tesseract = "0.12" \ No newline at end of file diff --git a/README.md b/README.md index 628fa43..1130d1c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Bot, who solves breach protocol games from Cyberpunk 2077. -[![Preview](assets/preview.gif)](assets/preview.mp4) +![Preview](assets/preview.gif) ## Usage @@ -10,7 +10,7 @@ Bot, who solves breach protocol games from Cyberpunk 2077. - Launch `cyberbot2077.exe` - Launch a breach protocol game - Move mouse cursor away. It must not obstruct the game field -- Press `PrintScreen` keyboard button +- Press `PrintScreen` (or `Alt + PrintScreen` in window mode) keyboard button - Wait a second - ??? - PROFIT @@ -22,10 +22,8 @@ Bot, who solves breach protocol games from Cyberpunk 2077. ## How it works -This bot uses [Tesseract](https://github.com/tesseract-ocr/tesseract) library as OCR engine. - `PrintScreen` keyboard button triggers the bot, and it grabs screenshot -to recognize game field (matrix, conditions, max step count and screen coordinates). +to recognize game field (matrix, conditions, buffer size and screen coordinates). ![Recognize example](assets/recognize.jpg) @@ -83,33 +81,17 @@ Last solution `#6` will be applied. Download bot [here](https://github.com/ricorodriges/cyberbot2077/releases). -Bot uses `tesseract/tesseract.exe` binary. So please [download binaries](https://github.com/UB-Mannheim/tesseract/wiki) and extract to `tesseract` directory -``` -|- tesseract -| |- tesseract.exe -|- cyberbot2077.exe -``` - Then run `cyberbot2077.exe` and enjoy! ## Build -Since this project uses [Tesseract](https://github.com/tesseract-ocr/tesseract), -you need to build it yourself or [download binaries](https://github.com/UB-Mannheim/tesseract/wiki). - -Bot uses `tesseract/tesseract.exe` binary. So please extract binaries to `tesseract` directory -``` -|- tesseract -| |- tesseract.exe -|- src -|- test -|- Cargo.toml +You may run tests to make sure everything is ok +```sh +cargo test ``` -Then you may run tests to make sure everything is ok and build +And build as usual rust crate ```sh -cargo test cargo build --release - ./target/release/cyberbot2077.exe ``` \ No newline at end of file diff --git a/assets/preview.mp4 b/assets/preview.mp4 deleted file mode 100644 index 6e73aee..0000000 Binary files a/assets/preview.mp4 and /dev/null differ diff --git a/src/img.rs b/src/img.rs index 1804d43..16dca0f 100644 --- a/src/img.rs +++ b/src/img.rs @@ -34,84 +34,327 @@ pub fn load_img_from_file>(path: P) -> Image { return bmp::open(path).unwrap(); } -/// Clear image colors. Target `color` transforms to about black, other colors are white -pub fn filter_img(img: &mut Image, color: &Pixel, range: u8) { - for (x, y) in img.coordinates() { - let src = img.get_pixel(x, y); - let dest = Pixel::new(dif(src.r, color.r), dif(src.g, color.g), dif(src.b, color.b)); - - let result = if dest.r <= range && dest.g <= range && dest.b <= range { - dest // about BLACK - } else { - bmp::consts::WHITE - }; - img.set_pixel(x, y, result); - } +pub struct GrayImage { + w: u32, + h: u32, + data: Vec, } -#[inline] -fn dif(a: u8, b: u8) -> u8 { - return (a as i16 - b as i16).abs() as u8; +impl GrayImage { + /// Target `color` transforms to 255, other colors are 0 + pub fn filter(img: &Image, color: &Pixel, threshold: u8, left: u32, top: u32, right: u32, bottom: u32) -> Self { + debug_assert!(right > left); + debug_assert!(bottom > top); + + let r_min = i16::max(color.r as i16 - threshold as i16, 0) as u8; + let g_min = i16::max(color.g as i16 - threshold as i16, 0) as u8; + let b_min = i16::max(color.b as i16 - threshold as i16, 0) as u8; + let color_min = px!(r_min, g_min, b_min); + + let r_max = i16::min(color.r as i16 + threshold as i16, 255) as u8; + let g_max = i16::min(color.g as i16 + threshold as i16, 255) as u8; + let b_max = i16::min(color.b as i16 + threshold as i16, 255) as u8; + let color_max = px!(r_max, g_max, b_max); + + let w = right - left; + let h = bottom - top; + let mut data = Vec::with_capacity((w * h) as usize); + for y in 0..h { + for x in 0..w { + let p1 = img.get_pixel(x + left, y + top); + + let v = if p1.r >= color_min.r && p1.r <= color_max.r && + p1.g >= color_min.g && p1.g <= color_max.g && + p1.b >= color_min.b && p1.b <= color_max.b { + 255u8 + } else { + 0u8 + }; + data.push(v); + } + } + debug_assert_eq!(w * h, data.len() as u32); + return Self { w, h, data }; + } + + #[inline] + pub fn width(&self) -> u32 { + self.w + } + + #[inline] + pub fn height(&self) -> u32 { + self.h + } + + #[inline] + pub fn pixel(&self, x: u32, y: u32) -> u8 { + self.data[(y * self.w + x) as usize] + } + + /// Tries to find filled rectangle in reverse mode (the most bottom-right) + pub fn rfind_rect(&self, rect_width: u32, rect_height: u32) -> Option<(u32, u32)> { + if self.h < rect_height || self.w < rect_width || rect_width == 0 || rect_height == 0 { + return None; + } + (0..=(self.h - rect_height)).rev().find_map(|y_start| { + (0..=(self.w - rect_width)).rev().find(|x_start| { + let is_rect = (0..rect_height).all(|dy| { + (0..rect_width).all(|dx| + self.pixel(x_start + dx, y_start + dy) != 0 + ) + }); + is_rect + }).map(|x_start| (x_start, y_start)) + }) + } + + /// `result[x] == true` means column `x` has at least 1 non-zero pixel + pub fn columns_usage(&self) -> Vec { + (0..self.w).map(|x| { + (0..self.h).any(|y| + self.pixel(x, y) != 0 + ) + }).collect() + } + + /// `result[y] == true` means row `y` has at least 1 non-zero pixel + pub fn rows_usage(&self) -> Vec { + (0..self.h).map(|y| { + (0..self.w).any(|x| + self.pixel(x, y) != 0 + ) + }).collect() + } + + /// Returns the smallest rectangle, which contains all non-zero pixels in `self_*` area + pub fn rect_hull(&self, self_left: u32, self_top: u32, self_right: u32, self_bottom: u32) -> Option<(u32, u32, u32, u32)> { + debug_assert!(self_right >= self_left); + debug_assert!(self_bottom >= self_top); + debug_assert!(self_right < self.w); + debug_assert!(self_bottom < self.h); + + let mut result: Option<(u32, u32, u32, u32)> = None; + + for y in self_top..=self_bottom { + for x in self_left..=self_right { + if self.pixel(x, y) != 0 { + let prev = result.take() + .unwrap_or((u32::max_value(), u32::max_value(), u32::min_value(), u32::min_value())); + result = Some(( + u32::min(x, prev.0), + u32::min(y, prev.1), + u32::max(x, prev.2), + y, + )); + } + } + } + + return result; + } + + pub fn template_match_error_score(&self, self_left: u32, self_top: u32, self_right: u32, self_bottom: u32, template: &GrayImage) -> f64 { + debug_assert!(self_right >= self_left); + debug_assert!(self_bottom >= self_top); + debug_assert!(self_right < self.w); + debug_assert!(self_bottom < self.h); + + let self_width = self_right - self_left; + let self_height = self_bottom - self_top; + let self_pixel = |x: u32, y: u32| -> u8 { + self.pixel(self_left + x, self_top + y) + }; + + let x_norm = (template.w as f64) / (self_width as f64); + let y_norm = (template.h as f64) / (self_height as f64); + let x_max = template.w - 1; + let y_max = template.h - 1; + let template_pixel = |x: u32, y: u32| -> u8 { + template.pixel(u32::min((x as f64 * x_norm) as _, x_max), u32::min((y as f64 * y_norm) as _, y_max)) + }; + + let error: u32 = (0..self_height).map(|y| { + (0..self_width) + .filter(|x| self_pixel(*x, y) != template_pixel(*x, y)) + .count() as u32 + }).sum(); + return error as f64 / (self_width * self_height) as f64; + } } -pub fn crop_img(img: &Image, left: u32, top: u32, right: u32, bottom: u32) -> Image { - let mut dest = Image::new(right - left, bottom - top); +#[cfg(test)] +pub fn into_image(src: &GrayImage) -> Image { + let mut dest = Image::new(src.w, src.h); for (x, y) in dest.coordinates() { - dest.set_pixel(x, y, img.get_pixel(x + left, y + top)); + let v = src.data[(y * src.w + x) as usize]; + dest.set_pixel(x, y, px!(v, v, v)); } return dest; } - #[cfg(test)] mod tests { use bmp::{Pixel, px}; - use crate::img::{crop_img, dif, filter_img}; + use crate::img::{GrayImage, into_image}; #[test] - fn test_dif() { - assert_eq!(0, dif(0, 0)); - assert_eq!(1, dif(0, 1)); - assert_eq!(1, dif(1, 0)); - assert_eq!(0, dif(255, 255)); - assert_eq!(1, dif(255, 254)); - assert_eq!(1, dif(254, 255)); - assert_eq!(255, dif(0, 255)); - assert_eq!(255, dif(255, 0)); - } - - #[test] - fn test_filter_img() { + fn test_filter() { let mut img = bmp::Image::new(4, 4); img.set_pixel(0, 0, px!(0, 10, 20)); img.set_pixel(1, 0, px!(10, 20, 30)); img.set_pixel(0, 1, px!(15, 25, 35)); img.set_pixel(1, 1, px!(20, 30, 40)); - filter_img(&mut img, &px!(10, 20, 30), 5); + let filtered = GrayImage::filter(&img, &px!(10, 20, 30), 5, 0, 0, 2, 2); + assert_eq!(filtered.width(), 2); + assert_eq!(filtered.height(), 2); + assert_eq!(filtered.data, vec![0, 255, 255, 0]); + assert_eq!(filtered.pixel(0, 0), 0); + assert_eq!(filtered.pixel(1, 0), 255); + assert_eq!(filtered.pixel(0, 1), 255); + assert_eq!(filtered.pixel(1, 1), 0); + } - assert_eq!(px!(255, 255, 255), img.get_pixel(0, 0)); - assert_eq!(px!(0, 0, 0), img.get_pixel(1, 0)); - assert_eq!(px!(5, 5, 5), img.get_pixel(0, 1)); - assert_eq!(px!(255, 255, 255), img.get_pixel(1, 1)); + #[test] + fn test_rfind_rect() { + let pixels = vec![ + 255, 255, 000, + 255, 255, 000, + ]; - assert_eq!(4, img.get_width()); - assert_eq!(4, img.get_height()); + let filtered = into_img(pixels, 3, 2); + assert_eq!(None, filtered.rfind_rect(10, 10)); + assert_eq!(None, filtered.rfind_rect(3, 2)); + assert_eq!(None, filtered.rfind_rect(3, 1)); + assert_eq!(Some((0, 1)), filtered.rfind_rect(2, 1)); + assert_eq!(Some((0, 0)), filtered.rfind_rect(2, 2)); } #[test] - fn test_crop_img() { - let mut img = bmp::Image::new(4, 4); - img.set_pixel(0, 0, px!(0, 0, 0)); - img.set_pixel(1, 0, px!(10, 20, 30)); - img.set_pixel(0, 1, px!(0, 0, 0)); - img.set_pixel(1, 1, px!(0, 0, 0)); + fn test_columns_usage() { + let pixels = vec![ + 255, 000, 000, + 000, 000, 255, + ]; + + let filtered = into_img(pixels, 3, 2); + let usage = filtered.columns_usage(); + assert_eq!(vec![true, false, true], usage); + assert_eq!(3, usage.capacity()); + } + + #[test] + fn test_rows_usage() { + let pixels = vec![ + 255, 000, + 000, 000, + 000, 255, + ]; + + let filtered = into_img(pixels, 2, 3); + let usage = filtered.rows_usage(); + assert_eq!(vec![true, false, true], usage); + assert_eq!(3, usage.capacity()); + } + + #[test] + fn test_rect_hull() { + let pixels = vec![ + 000, 000, 000, 000, 000, + 000, 255, 255, 255, 255, + 000, 255, 000, 000, 255, + 000, 255, 255, 000, 255, + 000, 255, 255, 255, 255, + ]; + + let img = into_img(pixels, 5, 5); + assert_eq!(Some((1, 1, 4, 4)), img.rect_hull(0, 0, 4, 4)); + assert_eq!(Some((2, 3, 2, 3)), img.rect_hull(2, 2, 3, 3)); + assert_eq!(None, img.rect_hull(2, 2, 3, 2)); + } + + #[test] + fn test_template_match_error_score() { + let template_pixels = vec![ + 000, 000, 000, 000, + 000, 000, 000, 000, + 000, 000, 255, 255, + 000, 000, 255, 255, + ]; + let template = into_img(template_pixels, 4, 4); + + let exact_match = vec![ + 255, 255, 255, 255, 255, 255, + 255, 000, 000, 000, 000, 255, + 255, 000, 000, 000, 000, 255, + 255, 000, 000, 255, 255, 255, + 255, 000, 000, 255, 255, 255, + 255, 255, 255, 255, 255, 255, + ]; + assert_eq!(0f64, into_img(exact_match, 6, 6).template_match_error_score(1, 1, 4, 4, &template)); + + let exact_small_match = vec![ + 255, 255, 255, 255, + 255, 000, 000, 255, + 255, 000, 255, 255, + 255, 255, 255, 255, + ]; + assert_eq!(0f64, into_img(exact_small_match, 4, 4).template_match_error_score(1, 1, 2, 2, &template)); - let crop = crop_img(&img, 1, 0, 2, 1); + let exact_big_match = vec![ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 000, 000, 000, 000, 000, 000, 000, 000, 255, + 255, 000, 000, 000, 000, 000, 000, 000, 000, 255, + 255, 000, 000, 000, 000, 000, 000, 000, 000, 255, + 255, 000, 000, 000, 000, 000, 000, 000, 000, 255, + 255, 000, 000, 000, 000, 255, 255, 255, 255, 255, + 255, 000, 000, 000, 000, 255, 255, 255, 255, 255, + 255, 000, 000, 000, 000, 255, 255, 255, 255, 255, + 255, 000, 000, 000, 000, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + ]; + assert_eq!(0f64, into_img(exact_big_match, 10, 10).template_match_error_score(1, 1, 8, 8, &template)); + + let full_unmatch = vec![ + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 000, 000, + 255, 255, 000, 000, + ]; + assert_eq!(1f64, into_img(full_unmatch, 4, 4).template_match_error_score(0, 0, 3, 3, &template)); + + let half_match = vec![ + 255, 255, 000, 000, + 255, 255, 000, 000, + 000, 000, 000, 000, + 000, 000, 000, 000, + ]; + let half_error = into_img(half_match, 4, 4).template_match_error_score(0, 0, 3, 3, &template); + assert!(0.43 < half_error); + assert!(0.58 > half_error); + } + + #[test] + fn test_into_image() { + let pixels = vec![ + 255, 000, + 000, 255, + ]; + + let bmp: bmp::Image = into_image(&into_img(pixels, 2, 2)); + assert_eq!(bmp.get_width(), 2); + assert_eq!(bmp.get_height(), 2); + assert_eq!(px!(255, 255, 255), bmp.get_pixel(0, 0)); + assert_eq!(px!(000, 000, 000), bmp.get_pixel(1, 0)); + assert_eq!(px!(000, 000, 000), bmp.get_pixel(0, 1)); + assert_eq!(px!(255, 255, 255), bmp.get_pixel(1, 1)); + + } - assert_eq!(1, crop.get_width()); - assert_eq!(1, crop.get_height()); - assert_eq!(px!(10, 20, 30), crop.get_pixel(0, 0)); + fn into_img(data: Vec, w: u32, h: u32) -> GrayImage { + assert_eq!((w * h) as usize, data.len()); + return GrayImage { data, w, h }; } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d0ff0a5..9be92e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,9 +7,9 @@ use bmp::Image; use winapi::shared::minwindef::{LPARAM, LRESULT, WPARAM}; use winapi::um::winuser; -use crate::img::{crop_img, filter_img, load_img_from_clipboard, load_img_from_file}; +use crate::img::{GrayImage, load_img_from_clipboard, load_img_from_file}; use crate::input::click; -use crate::ocr::ocr_matrix; +use crate::ocr::{ocr_matrix, ocr_conditions, MatrixTemplates}; use crate::recognize::{CONDITION_COLOR, MATRIX_COLOR}; mod img; @@ -30,17 +30,20 @@ unsafe extern "system" fn keyboard_hook(code: i32, w_param: WPARAM, l_param: LPA if (*info).vkCode == winuser::VK_SNAPSHOT as _ { if LOCK.compare_exchange(false, true, Acquire, Acquire) == Ok(false) { thread::spawn(|| { + let templates = MatrixTemplates::load_templates(); // wait for clipboard buffer initialization - thread::sleep(Duration::from_secs(1)); - let img = load_img_from_clipboard(); - if img.is_none() { - eprintln!("Clipboard has no image data"); - } else { - let result = execute(img.unwrap(), false); - if result.is_err() { - eprintln!("{}", result.unwrap_err()); + thread::sleep(Duration::from_millis(600)); + match load_img_from_clipboard() { + None => { + eprintln!("Clipboard has no image data"); } - } + Some(img) => { + let result = execute(img, &templates, false); + if result.is_err() { + eprintln!("{}", result.unwrap_err()); + } + } + }; LOCK.store(false, Release); }); } @@ -49,11 +52,10 @@ unsafe extern "system" fn keyboard_hook(code: i32, w_param: WPARAM, l_param: LPA winuser::CallNextHookEx(std::ptr::null_mut(), code, w_param, l_param) } -fn execute(img: Image, solutions_only: bool) -> Result<(), String> { +fn execute(img: Image, templates: &MatrixTemplates, solutions_only: bool) -> Result<(), String> { let matrix_area = recognize::find_matrix_area(&img).ok_or_else(|| "Matrix was not found".to_owned())?; - let mut matrix_img = crop_img(&img, matrix_area.0, matrix_area.1, matrix_area.2, matrix_area.3); - filter_img(&mut matrix_img, &MATRIX_COLOR, 60); - let matrix = match ocr_matrix(&matrix_img) { + let matrix_img = GrayImage::filter(&img, &MATRIX_COLOR, 50, matrix_area.0, matrix_area.1, matrix_area.2, matrix_area.3); + let matrix = match ocr_matrix(&matrix_img, templates) { Ok(r) => r, Err(err) => Err(format!("Matrix was not recognized: {}", err))?, }; @@ -69,16 +71,15 @@ fn execute(img: Image, solutions_only: bool) -> Result<(), String> { println!(); let condition_area = recognize::find_condition_area(&img, &matrix_area).ok_or_else(|| "Conditions were not found".to_owned())?; - let mut condition_img = crop_img(&img, condition_area.0, condition_area.1, condition_area.2, condition_area.3); - filter_img(&mut condition_img, &CONDITION_COLOR, 60); - let conditions = match ocr_matrix(&condition_img) { + let condition_img = GrayImage::filter(&img, &CONDITION_COLOR, 50, condition_area.0, condition_area.1, condition_area.2, condition_area.3); + let conditions = match ocr_conditions(&condition_img, templates) { Ok(r) => r, Err(err) => Err(format!("Conditions were not recognized: {}", err))?, }; drop(condition_img); println!("Conditions:"); - for line in conditions.4.iter() { + for line in conditions.iter() { let hex = line.iter() .map(|v| format!("{:#04x} ", v)) .collect::(); @@ -86,17 +87,17 @@ fn execute(img: Image, solutions_only: bool) -> Result<(), String> { } println!(); - let blocks = recognize::find_blocks_count(&img, &condition_area).ok_or_else(|| "Blocks were not found".to_owned())?; - println!("Steps: {}", blocks); + let steps = recognize::find_buffer_size(&img, &condition_area).ok_or_else(|| "Buffer size was not recognized".to_owned())?; + println!("Steps: {}", steps); println!(); - let solutions = solver::solve(&matrix.4, &conditions.4, blocks); + let solutions = solver::solve(&matrix.4, &conditions, steps); println!("Found {} solutions", solutions.len()); let best = solver::filter_best(&solutions); println!("{} best solutions:", best.len()); for (i, s) in best.iter().enumerate() { let conditions = s.conditions.iter() - .map(|b| if *b { "✔ " } else { "✖ " }) + .map(|&b| if b { "✔ " } else { "✖ " }) .collect::(); let steps = s.steps.iter() .map(|step| matrix.4[step.y as usize][step.x as usize]) @@ -130,7 +131,7 @@ fn main() { let bmp_path = std::env::args().last().unwrap(); println!("Reading {} bmp file...", &bmp_path); let img = load_img_from_file(bmp_path); - execute(img, true).expect("Error"); + execute(img, &MatrixTemplates::load_templates(), true).expect("Error"); return; } diff --git a/src/ocr.rs b/src/ocr.rs index b01c162..e109a0d 100644 --- a/src/ocr.rs +++ b/src/ocr.rs @@ -1,314 +1,275 @@ -use std::process::{Command, Stdio}; +use bmp::{Pixel, px}; -use bmp::Image; +use crate::img::GrayImage; -// TODO: migrate to dll or `tesseract` crate -const TESSERACT_PATH: &str = "tesseract/tesseract.exe"; +// max space interval in px between 2 characters in same matrix item +const MAX_CHARACTER_SPACING: u32 = 15; -const ALL_MATRIX_ITEMS: [&str; 6] = ["1C", "55", "7A", "BD", "E9", "FF"]; -const WHITELIST: &str = "1579ABCDEF "; - -fn ocr(img: &Image, conf: &[(&str, &str)]) -> Result { - let m = conf.iter().flat_map(|v| ["-c".to_owned(), format!("{}={}", v.0, v.1)]); - let mut child = Command::new(TESSERACT_PATH) - .args(["stdin", "stdout", /*"-l", "eng", "--tessdata-dir", "/",*/ "--dpi", "72"]) - .args(m) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn().map_err(|e| e.to_string())?; - - let mut child_stdin = child.stdin.take().unwrap(); - img.to_writer(&mut child_stdin).map_err(|e| e.to_string())?; - // Close stdin - drop(child_stdin); +#[allow(non_snake_case)] +pub struct MatrixTemplates { + T_1C: GrayImage, + T_55: GrayImage, + T_7A: GrayImage, + T_BD: GrayImage, + T_E9: GrayImage, + T_FF: GrayImage, +} - let output = child.wait_with_output().map_err(|e| e.to_string())?; - if output.status.success() { - return Ok(std::str::from_utf8(&output.stdout).map_err(|e| e.to_string())?.trim().to_owned()); +impl MatrixTemplates { + pub fn load_templates() -> Self { + let bytes = include_bytes!("template.bmp"); + let bmp = bmp::from_reader(&mut bytes.as_ref()).unwrap(); + + let color = px!(255, 255, 255); + return Self { + T_1C: GrayImage::filter(&bmp, &color, 1, 000, 0, 023, 19), + T_55: GrayImage::filter(&bmp, &color, 1, 023, 0, 049, 20), + T_7A: GrayImage::filter(&bmp, &color, 1, 049, 0, 075, 19), + T_BD: GrayImage::filter(&bmp, &color, 1, 075, 0, 104, 20), + T_E9: GrayImage::filter(&bmp, &color, 1, 104, 0, 130, 20), + T_FF: GrayImage::filter(&bmp, &color, 1, 130, 0, 155, 20), + }; } - return Ok("".to_owned()); } -pub fn ocr_text(img: &Image) -> Result { - return ocr(&img, &[("tessedit_char_whitelist", WHITELIST), ("tessedit_create_txt", "1")]); +struct Location { + start: u32, + end: u32, } -pub fn ocr_tsv(img: &Image) -> Result, String> { - - let str = ocr(&img, &[("tessedit_char_whitelist", WHITELIST), ("tessedit_create_tsv", "1")])?; - - let mut result = Vec::new(); - for x in str.lines().skip(1) { - let mut line = x.split("\t").skip(6); - let x = u32::from_str_radix(line.next().unwrap(), 10).unwrap(); - let y = u32::from_str_radix(line.next().unwrap(), 10).unwrap(); - let w = u32::from_str_radix(line.next().unwrap(), 10).unwrap(); - let h = u32::from_str_radix(line.next().unwrap(), 10).unwrap(); - line.next(); - let str = line.next().unwrap().trim(); - if !str.is_empty() { - result.push((x, y, w, h, str.to_owned())); +/// Returns 2 vectors: +/// - columns (`x`) +/// - rows (`y`) +fn locate_matrix_regions(img: &GrayImage) -> Result<(Vec, Vec), String> { + // will think that usual matrix/conditions is not greater than 10x10. + // matrix_table_* stores x/y coordinates in format (start1, end1, start2, end2, ...) + let mut matrix_table_x: Vec = Vec::with_capacity(10 * 2); + let mut matrix_table_y: Vec = Vec::with_capacity(10 * 2); + + let mut was_space = true; + for (x, non_blank) in img.columns_usage().into_iter().enumerate() { + if non_blank { + if was_space { + // new character + + // each item consists of 2 characters + let same_item = if let Some(&prev) = matrix_table_x.last() { + x as u32 - prev <= MAX_CHARACTER_SPACING + } else { + false + }; + if same_item { + // item is continued. wait for new ending + matrix_table_x.pop(); + } else { + // new item + matrix_table_x.push(x as _); + } + } else { + // do nothing. same character + } + was_space = false; + } else { + if !was_space { + // end of character + matrix_table_x.push(x as _); + } + was_space = true; } } - return Ok(result); -} -pub fn ocr_matrix(img: &Image) -> Result<(u32, u32, u32, u32, Vec>), String> { - let ocr = ocr_tsv(&img)?; - - let mut left = u32::MAX; - let mut top = u32::MAX; - let mut right = u32::MIN; - let mut bottom = u32::MIN; - for (x, y, _, _, _) in ocr.iter() { - left = u32::min(left, *x); - right = u32::max(right, *x); - top = u32::min(top, *y); - bottom = u32::max(bottom, *y); + let mut was_space = true; + for (y, non_blank) in img.rows_usage().into_iter().enumerate() { + if non_blank { + if was_space { + // new character + matrix_table_y.push(y as _); + } else { + // do nothing. same character + } + was_space = false; + } else { + if !was_space { + // end of character + matrix_table_y.push(y as _); + } + was_space = true; + } } - let line_offset = 30; + if matrix_table_x.is_empty() || matrix_table_y.is_empty() { + return Err("Matrix items not found".to_owned()); + } + if matrix_table_x.len() % 2 != 0 || matrix_table_y.len() % 2 != 0 { + return Err("Matrix items were recognized incorrectly".to_owned()); + } - let mut result: Vec> = Vec::new(); - let mut current_line = top as i32; - loop { - let mut line: Vec<_> = ocr.iter() - .filter(|v| (v.1 as i32 - current_line).abs() <= line_offset) - .collect(); - if line.is_empty() { - break; - } + let columns = (0..(matrix_table_x.len() / 2)) + .map(|i| Location { start: matrix_table_x[i * 2], end: matrix_table_x[i * 2 + 1] }) + .collect(); + let rows = (0..(matrix_table_y.len() / 2)) + .map(|i| Location { start: matrix_table_y[i * 2], end: matrix_table_y[i * 2 + 1] }) + .collect(); + return Ok((columns, rows)); +} + +fn ocr_matrix_item(img: &GrayImage, templates: &MatrixTemplates, column: &Location, row: &Location) -> Option { + + let (x_start, y_start, x_end, y_end) = match img.rect_hull(column.start, row.start, column.end - 1, row.end - 1) { + Some(v) => v, + None => return None, + }; + + return Some([ + (0x1Cu8, img.template_match_error_score(x_start, y_start, x_end, y_end, &templates.T_1C)), + (0x55u8, img.template_match_error_score(x_start, y_start, x_end, y_end, &templates.T_55)), + (0x7Au8, img.template_match_error_score(x_start, y_start, x_end, y_end, &templates.T_7A)), + (0xBDu8, img.template_match_error_score(x_start, y_start, x_end, y_end, &templates.T_BD)), + (0xE9u8, img.template_match_error_score(x_start, y_start, x_end, y_end, &templates.T_E9)), + (0xFFu8, img.template_match_error_score(x_start, y_start, x_end, y_end, &templates.T_FF)), + ].into_iter() + .min_by(|(_, error1), (_, error2)| error1.partial_cmp(error2).unwrap()) + .unwrap().0); +} - line.sort_by_key(|v| v.0); - result.push(line.iter().map(|v| v.4.to_owned()).collect()); +pub fn ocr_matrix(img: &GrayImage, templates: &MatrixTemplates) -> Result<(u32, u32, u32, u32, Vec>), String> { - current_line = ocr.iter() - .map(|v| v.1 as i32) - .filter(|v| (v - current_line) > line_offset) - .min().unwrap_or(i32::MAX / 2); + let (columns, rows) = match locate_matrix_regions(&img) { + Ok(x) => x, + Err(e) => return Err(e), + }; + + if columns.len() < 3 || columns.len() != rows.len() { + return Err("Bad matrix dimension".to_owned()); } - // Corrections - // Convert "1" and "C" to "1C", ... - for v in result.iter_mut() { - for hex in v.iter_mut() { - if hex.len() != 2 { - let candidate = ALL_MATRIX_ITEMS.iter().find(|h| h.contains(&hex[..])); - if let Some(found) = candidate { - *hex = (*found).to_owned(); - } else { - return Err(format!("'{}' is not a hex value", hex)); - } - } else if ALL_MATRIX_ITEMS.iter().all(|h| *h != hex) { - return Err(format!("'{}' is not expected hex value", hex)); - } + let matrix_dimension = columns.len(); + let left = columns.first().unwrap().start; + let right = columns.last().unwrap().start; + let top = rows.first().unwrap().start; + let bottom = rows.last().unwrap().start; + let mut result: Vec> = Vec::with_capacity(matrix_dimension); + + for row in rows.iter() { + let mut matrix_row = Vec::with_capacity(matrix_dimension); + for column in columns.iter() { + let matrix_item = match ocr_matrix_item(&img, &templates, &column, &row) { + Some(v) => v, + None => return Err("One of matrix items was not recognized".to_owned()), + }; + matrix_row.push(matrix_item); } + result.push(matrix_row); } - let result = result.iter() - .map(|c| c.iter().map(|v| u8::from_str_radix(v, 16).unwrap()).collect()) - .collect(); + debug_assert_eq!(matrix_dimension, result.len()); + debug_assert_eq!(matrix_dimension, result[0].len()); + debug_assert_eq!(matrix_dimension, result[1].len()); return Ok((left, top, right, bottom, result)); } +pub fn ocr_conditions(img: &GrayImage, templates: &MatrixTemplates) -> Result>, String> { -#[cfg(test)] -mod tests { - use crate::img::{crop_img, filter_img, load_img_from_file}; - use crate::ocr::{ocr_matrix, ocr_text, ocr_tsv}; - use crate::recognize::{CONDITION_COLOR, MATRIX_COLOR}; - - #[test] - fn test_ocr_text() { - let img = load_img_from_file("test/ocr_simple_test.bmp"); + let (columns, rows) = match locate_matrix_regions(&img) { + Ok(x) => x, + Err(e) => return Err(e), + }; - let string = ocr_text(&img).unwrap(); - assert_eq!("BD 55\r\nE9", string); + if columns.is_empty() || rows.is_empty() { + return Err("Bad condition dimension".to_owned()); } - #[test] - fn test_ocr_tsv() { - let img = load_img_from_file("test/ocr_simple_test.bmp"); - - let result = ocr_tsv(&img).unwrap(); - assert_eq!(vec![ - (5, 4, 40, 20, "BD".to_owned()), - (69, 4, 34, 19, "55".to_owned()), - (64, 31, 34, 20, "E9".to_owned()), - ], result); + let mut result: Vec> = Vec::with_capacity(rows.len()); + + for row in rows.iter() { + let mut condition_row = Vec::with_capacity(columns.len()); + for column in columns.iter() { + let matrix_item = match ocr_matrix_item(&img, &templates, &column, &row) { + Some(v) => v, + None => break, // short condition. Goto next row + }; + condition_row.push(matrix_item); + } + result.push(condition_row); } - const MATRIX_AREA1: (u32, u32, u32, u32) = (142, 337, 837, 802); - const MATRIX_AREA2: (u32, u32, u32, u32) = (206, 337, 773, 673); + debug_assert_eq!(rows.len(), result.len()); + return Ok(result); +} + + +#[cfg(test)] +mod tests { + use crate::img::{GrayImage, load_img_from_file}; + use crate::ocr::{MatrixTemplates, ocr_conditions, ocr_matrix}; + use crate::recognize::{CONDITION_COLOR, MATRIX_COLOR}; + use crate::test_cases::{CONDITION_AREA1, CONDITION_AREA2, CONDITION_AREA3, CONDITION_AREA4, CONDITION_AREA5, conditions1, conditions2, conditions3, conditions4, conditions5, FILE1, FILE2, FILE3, FILE4, FILE5, matrix1, matrix2, matrix3, matrix4, matrix5, MATRIX_AREA1, MATRIX_AREA2, MATRIX_AREA3, MATRIX_AREA4, MATRIX_AREA5}; #[test] fn test_ocr_matrix1() { - let img = load_img_from_file("test/test1.bmp"); - let mut img = crop_img(&img, MATRIX_AREA1.0, MATRIX_AREA1.1, MATRIX_AREA1.2, MATRIX_AREA1.3); - filter_img(&mut img, &MATRIX_COLOR, 50); - - let result = ocr_matrix(&img); - let expected: Vec> = vec![ - vec!["BD", "55", "55", "7A", "E9", "7A", "55"], - vec!["55", "E9", "55", "BD", "55", "55", "E9"], - vec!["E9", "55", "BD", "7A", "1C", "55", "7A"], - vec!["55", "1C", "55", "55", "7A", "1C", "FF"], - vec!["1C", "7A", "7A", "1C", "BD", "1C", "BD"], - vec!["1C", "7A", "E9", "FF", "1C", "E9", "FF"], - vec!["1C", "7A", "7A", "BD", "7A", "55", "BD"], - ].iter().map(|c| c.iter().map(|v| u8::from_str_radix(v, 16).unwrap()).collect()).collect(); - assert_eq!(Ok((143, 29, 529, 415, expected)), result); + test_ocr_matrix(FILE1, MATRIX_AREA1, (143, 29, 526, 414, matrix1())); } #[test] fn test_ocr_matrix2() { - let img = load_img_from_file("test/test2.bmp"); - let mut img = crop_img(&img, MATRIX_AREA2.0, MATRIX_AREA2.1, MATRIX_AREA2.2, MATRIX_AREA2.3); - filter_img(&mut img, &MATRIX_COLOR, 50); - - let result = ocr_matrix(&img); - let expected: Vec> = vec![ - vec!["55", "BD", "BD", "BD", "55"], - vec!["BD", "1C", "55", "E9", "1C"], - vec!["BD", "BD", "1C", "1C", "55"], - vec!["55", "E9", "E9", "55", "E9"], - vec!["1C", "55", "55", "1C", "1C"], - ].iter().map(|c| c.iter().map(|v| u8::from_str_radix(v, 16).unwrap()).collect()).collect(); - assert_eq!(Ok((142, 30, 401, 286, expected)), result); + test_ocr_matrix(FILE2, MATRIX_AREA2, (142, 30, 400, 286, matrix2())); } #[test] fn test_ocr_matrix3() { - let img = load_img_from_file("test/test3.bmp"); - let mut img = crop_img(&img, MATRIX_AREA1.0, MATRIX_AREA1.1, MATRIX_AREA1.2, MATRIX_AREA1.3); - filter_img(&mut img, &MATRIX_COLOR, 50); - - let result = ocr_matrix(&img); - let expected: Vec> = vec![ - vec!["7A", "55", "E9", "BD", "1C", "55", "55"], - vec!["FF", "55", "55", "BD", "E9", "7A", "1C"], - vec!["55", "1C", "55", "55", "1C", "55", "E9"], - vec!["E9", "1C", "FF", "55", "1C", "1C", "55"], - vec!["BD", "1C", "1C", "1C", "E9", "55", "1C"], - vec!["1C", "FF", "55", "7A", "BD", "55", "1C"], - vec!["55", "55", "7A", "BD", "7A", "55", "55"], - ].iter().map(|c| c.iter().map(|v| u8::from_str_radix(v, 16).unwrap()).collect()).collect(); - assert_eq!(Ok((142, 29, 529, 415, expected)), result); + test_ocr_matrix(FILE3, MATRIX_AREA3, (142, 29, 528, 414, matrix3())); } #[test] fn test_ocr_matrix4() { - let img = load_img_from_file("test/test4.bmp"); - let mut img = crop_img(&img, MATRIX_AREA1.0, MATRIX_AREA1.1, MATRIX_AREA1.2, MATRIX_AREA1.3); - filter_img(&mut img, &MATRIX_COLOR, 50); - - let result = ocr_matrix(&img); - let expected: Vec> = vec![ - vec!["7A", "55", "FF", "BD", "BD", "BD", "E9"], - vec!["BD", "FF", "FF", "55", "E9", "E9", "7A"], - vec!["55", "7A", "7A", "BD", "E9", "FF", "BD"], - vec!["7A", "1C", "FF", "FF", "7A", "1C", "1C"], - vec!["7A", "BD", "55", "55", "E9", "55", "55"], - vec!["7A", "E9", "55", "BD", "BD", "1C", "1C"], - vec!["55", "7A", "7A", "7A", "7A", "7A", "1C"], - ].iter().map(|c| c.iter().map(|v| u8::from_str_radix(v, 16).unwrap()).collect()).collect(); - assert_eq!(Ok((142, 29, 529, 415, expected)), result); + test_ocr_matrix(FILE4, MATRIX_AREA4, (142, 29, 526, 414, matrix4())); } #[test] fn test_ocr_matrix5() { - let img = load_img_from_file("test/test5.bmp"); - let mut img = crop_img(&img, MATRIX_AREA1.0, MATRIX_AREA1.1, MATRIX_AREA1.2, MATRIX_AREA1.3); - filter_img(&mut img, &MATRIX_COLOR, 50); - - let result = ocr_matrix(&img); - let expected: Vec> = vec![ - vec!["7A", "FF", "E9", "55", "7A", "7A", "7A"], - vec!["7A", "E9", "1C", "55", "FF", "55", "1C"], - vec!["7A", "1C", "7A", "E9", "7A", "7A", "55"], - vec!["7A", "55", "55", "BD", "1C", "55", "1C"], - vec!["BD", "7A", "E9", "E9", "1C", "FF", "55"], - vec!["55", "FF", "BD", "BD", "1C", "7A", "1C"], - vec!["55", "1C", "BD", "BD", "7A", "FF", "BD"], - ].iter().map(|c| c.iter().map(|v| u8::from_str_radix(v, 16).unwrap()).collect()).collect(); - assert_eq!(Ok((142, 29, 529, 415, expected)), result); + test_ocr_matrix(FILE5, MATRIX_AREA5, (142, 29, 526, 414, matrix5())); } - const CONDITION_AREA1: (u32, u32, u32, u32) = (885, 337, 1346, 567); - const CONDITION_AREA2: (u32, u32, u32, u32) = (821, 337, 1282, 567); + fn test_ocr_matrix(filename: &str, matrix_area: (u32, u32, u32, u32), expected: (u32, u32, u32, u32, Vec>)) { + let templates = MatrixTemplates::load_templates(); + let img = load_img_from_file(filename); + let img = GrayImage::filter(&img, &MATRIX_COLOR, 50, matrix_area.0, matrix_area.1, matrix_area.2, matrix_area.3); + + let result = ocr_matrix(&img, &templates); + assert_eq!(Ok(expected), result); + } #[test] fn test_ocr_conditions1() { - let img = load_img_from_file("test/test1.bmp"); - let mut img = crop_img(&img, CONDITION_AREA1.0, CONDITION_AREA1.1, CONDITION_AREA1.2, CONDITION_AREA1.3); - filter_img(&mut img, &CONDITION_COLOR, 50); - - let result = ocr_matrix(&img); - let expected: Vec> = vec![ - vec!["1C", "7A"], - vec!["7A", "1C", "1C"], - vec!["7A", "7A", "BD", "7A"], - ].iter().map(|c| c.iter().map(|v| u8::from_str_radix(v, 16).unwrap()).collect()).collect(); - assert_eq!(Ok((25, 12, 151, 147, expected)), result); + test_ocr_conditions(FILE1, CONDITION_AREA1, conditions1()); } #[test] fn test_ocr_conditions2() { - let img = load_img_from_file("test/test2.bmp"); - let mut img = crop_img(&img, CONDITION_AREA2.0, CONDITION_AREA2.1, CONDITION_AREA2.2, CONDITION_AREA2.3); - filter_img(&mut img, &CONDITION_COLOR, 50); - - let result = ocr_matrix(&img); - let expected: Vec> = vec![ - vec!["1C", "55"], - vec!["BD", "55"], - vec!["55", "55", "1C"], - ].iter().map(|c| c.iter().map(|v| u8::from_str_radix(v, 16).unwrap()).collect()).collect(); - assert_eq!(Ok((24, 11, 110, 147, expected)), result); + test_ocr_conditions(FILE2, CONDITION_AREA2, conditions2()); } #[test] fn test_ocr_conditions3() { - let img = load_img_from_file("test/test3.bmp"); - let mut img = crop_img(&img, CONDITION_AREA1.0, CONDITION_AREA1.1, CONDITION_AREA1.2, CONDITION_AREA1.3); - filter_img(&mut img, &CONDITION_COLOR, 50); - - let result = ocr_matrix(&img); - let expected: Vec> = vec![ - vec!["1C", "E9", "BD"], - vec!["1C", "55", "1C"], - vec!["7A", "E9", "1C"], - ].iter().map(|c| c.iter().map(|v| u8::from_str_radix(v, 16).unwrap()).collect()).collect(); - assert_eq!(Ok((25, 12, 110, 147, expected)), result); + test_ocr_conditions(FILE3, CONDITION_AREA3, conditions3()); } #[test] fn test_ocr_conditions4() { - let img = load_img_from_file("test/test4.bmp"); - let mut img = crop_img(&img, CONDITION_AREA1.0, CONDITION_AREA1.1, CONDITION_AREA1.2, CONDITION_AREA1.3); - filter_img(&mut img, &CONDITION_COLOR, 50); - - let result = ocr_matrix(&img); - let expected: Vec> = vec![ - vec!["FF", "7A"], - vec!["1C", "7A", "BD"], - vec!["7A", "7A", "55", "1C"], - ].iter().map(|c| c.iter().map(|v| u8::from_str_radix(v, 16).unwrap()).collect()).collect(); - assert_eq!(Ok((25, 12, 152, 147, expected)), result); + test_ocr_conditions(FILE4, CONDITION_AREA4, conditions4()); } #[test] fn test_ocr_conditions5() { - let img = load_img_from_file("test/test5.bmp"); - let mut img = crop_img(&img, CONDITION_AREA1.0, CONDITION_AREA1.1, CONDITION_AREA1.2, CONDITION_AREA1.3); - filter_img(&mut img, &CONDITION_COLOR, 50); - - let result = ocr_matrix(&img); - let expected: Vec> = vec![ - vec!["7A", "E9", "FF"], - vec!["1C", "55", "E9"], - vec!["BD", "7A", "55", "E9"], - ].iter().map(|c| c.iter().map(|v| u8::from_str_radix(v, 16).unwrap()).collect()).collect(); - assert_eq!(Ok((24, 12, 151, 147, expected)), result); + test_ocr_conditions(FILE5, CONDITION_AREA5, conditions5()); + } + + fn test_ocr_conditions(filename: &str, condition_area: (u32, u32, u32, u32), expected: Vec>) { + let templates = MatrixTemplates::load_templates(); + let img = load_img_from_file(filename); + let img = GrayImage::filter(&img, &CONDITION_COLOR, 50, condition_area.0, condition_area.1, condition_area.2, condition_area.3); + + let result = ocr_conditions(&img, &templates); + assert_eq!(Ok(expected), result); } } \ No newline at end of file diff --git a/src/recognize.rs b/src/recognize.rs index a507fbd..8e7adb7 100644 --- a/src/recognize.rs +++ b/src/recognize.rs @@ -1,242 +1,277 @@ use bmp::{Image, Pixel, px}; -use crate::img::{crop_img, filter_img}; +use crate::img::GrayImage; pub const MATRIX_COLOR: Pixel = px!(0xD0, 0xED, 0x57); const CONDITION_BORDER_COLOR: Pixel = px!(0x81, 0x96, 0x38); pub const CONDITION_COLOR: Pixel = px!(0xF0, 0xF0, 0xF0); -const BLOCK_COLOR: Pixel = px!(0x4F, 0x5A, 0x25); +const BUFFER_COLOR: Pixel = px!(0x4F, 0x5A, 0x25); pub fn find_matrix_area(img: &Image) -> Option<(u32, u32, u32, u32)> { - let mut img = crop_img(&img, 0, 0, img.get_width() / 2, img.get_height()); - filter_img(&mut img, &MATRIX_COLOR, 50); - - let mut horizontal_area: Option<(u32, u32)> = None; - { - // tries to find second and third vertical lines in center from left - let line_height = 150; - - let mut l: Option = None; - let mut r: Option = None; - let mut lines = 0; - let center = img.get_height() / 2; - for x in 0..img.get_width() { - let vertical_line = (0..line_height).all(|v| img.get_pixel(x, center - v) != bmp::consts::WHITE); - if vertical_line { - lines += 1; - if lines == 2 { - l = Some(x); - } else if lines == 3 { - r = Some(x); - } - } - if lines >= 3 { - break; - } - } - if l.is_some() && r.is_some() { - horizontal_area = Some((l.unwrap(), r.unwrap())); - } - } - - if horizontal_area.is_none() { - return None; - } - // now we know matrix x coordinates between `l..r` - let (l, r) = horizontal_area.unwrap(); - - let mut vertical_area: Option<(u32, u32)> = None; - { - // tries to find second and third horizontal lines in center from bottom - let line_width = 150; - - let mut t: Option = None; - let mut b: Option = None; - let mut lines = 0; - let center = (r - l) / 2 + l; - let mut y = img.get_height() - 1; - while y > 0 { - let horizontal_line = (0..line_width).all(|v| img.get_pixel(center - v, y) != bmp::consts::WHITE || img.get_pixel(center - v, y - 1) != bmp::consts::WHITE); - if horizontal_line { - y -= 1; - - lines += 1; - if lines == 2 { - b = Some(y); - } else if lines == 3 { - t = Some(y); - } - } - if lines >= 3 { - break; - } - - y -= 1; - } - if b.is_some() && t.is_some() { - vertical_area = Some((t.unwrap(), b.unwrap())); - } - } - if let Some((t, b)) = vertical_area { - return Some((l + 1, t + 2, r - 1, b - 2)); - } - return None; + // matrix is on left part of image + let img = GrayImage::filter(img, &MATRIX_COLOR, 50, 0, 0, img.get_width() / 2, img.get_height()); + + // ██████████████████ + // █ matrix caption █ + // ██████████████████ + // │ matrix content │ + // └────────────────┘ + + // tries to find the most bottom-right rectangle + let rect_width = 300; + let rect_height = 5; + let (x_right, y_top) = match img.rfind_rect(rect_width, rect_height) { + Some((x_start, y_start)) => (x_start + rect_width - 1, y_start + rect_height - 1), + None => return None, + }; + + // right-bottom corner of matrix caption. Matrix content is below + // ███████████████│ + // ───────────────┤ <- (x_right, y_top) is here + // matrix content │ + debug_assert_eq!(img.pixel(x_right - 2, y_top + 0), 255); + debug_assert_eq!(img.pixel(x_right - 1, y_top + 0), 255); + debug_assert_eq!(img.pixel(x_right + 0, y_top + 0), 255); + debug_assert_eq!(img.pixel(x_right + 1, y_top + 0), 0); + debug_assert_eq!(img.pixel(x_right + 2, y_top + 0), 0); + debug_assert_eq!(img.pixel(x_right - 2, y_top + 1), 0); + debug_assert_eq!(img.pixel(x_right - 1, y_top + 1), 0); + debug_assert_eq!(img.pixel(x_right + 0, y_top + 1), 255); + debug_assert_eq!(img.pixel(x_right + 1, y_top + 1), 0); + debug_assert_eq!(img.pixel(x_right + 2, y_top + 1), 0); + + let y_bottom = match (100..(img.height() - y_top - 1)).find(|&dy| img.pixel(x_right, y_top + dy + 1) == 0) { + Some(height) => y_top + height, + None => return None, + }; + + // matrix content │ + // ───────────────┘ <- (x_right, y_bottom) is here + debug_assert_eq!(img.pixel(x_right - 2, y_bottom + 0), 255); + debug_assert_eq!(img.pixel(x_right - 1, y_bottom + 0), 255); + debug_assert_eq!(img.pixel(x_right + 0, y_bottom + 0), 255); + debug_assert_eq!(img.pixel(x_right + 1, y_bottom + 0), 0); + debug_assert_eq!(img.pixel(x_right + 2, y_bottom + 0), 0); + debug_assert_eq!(img.pixel(x_right - 2, y_bottom + 1), 0); + debug_assert_eq!(img.pixel(x_right - 1, y_bottom + 1), 0); + debug_assert_eq!(img.pixel(x_right + 0, y_bottom + 1), 0); + debug_assert_eq!(img.pixel(x_right + 1, y_bottom + 1), 0); + debug_assert_eq!(img.pixel(x_right + 2, y_bottom + 1), 0); + + let x_left = match (300..=x_right).find(|&dx| img.pixel(x_right - dx, y_bottom - 1) != 0) { + Some(width) => x_right - width, + None => return None, + }; + return Some((x_left + 1, y_top + 1, x_right - 1, y_bottom - 1)); } pub fn find_condition_area(img: &Image, matrix_area: &(u32, u32, u32, u32)) -> Option<(u32, u32, u32, u32)> { - let mut img = crop_img(&img, matrix_area.2, matrix_area.1, img.get_width(), matrix_area.3); - filter_img(&mut img, &CONDITION_BORDER_COLOR, 15); - - let mut bottom: Option = None; - { - // tries to find first horizontal line in center from bottom - let line_width = 50; - - let center = img.get_width() / 2; - for y in (0..img.get_height()).rev() { - let horizontal_line = (0..line_width).all(|v| img.get_pixel(center - v, y) != bmp::consts::WHITE); - if horizontal_line { - bottom = Some(y); - break; - } - } - } - if bottom.is_none() { - return None; - } - - let mut horizontal_area: Option<(u32, u32)> = None; - { - // tries to find first and second vertical lines in top from left - let line_height = 50; - - let mut l: Option = None; - let mut r: Option = None; - let mut lines = 0; - let top = 0; - for x in 0..img.get_width() { - let vertical_line = (0..line_height).all(|v| img.get_pixel(x, top + v) != bmp::consts::WHITE); - if vertical_line { - lines += 1; - if lines == 1 { - l = Some(x); - } else if lines == 2 { - r = Some(x); - } - } - if lines >= 2 { - break; - } - } - if l.is_some() && r.is_some() { - horizontal_area = Some((l.unwrap(), r.unwrap())); - } - } - - if let Some((l, r)) = horizontal_area { - return Some((matrix_area.2 + l + 1, matrix_area.1, matrix_area.2 + (l + r) / 2, matrix_area.1 + bottom.unwrap() - 1)); - } - - return None; + // conditions are near matrix + let (_, matrix_top, matrix_right, matrix_bottom) = *matrix_area; + + let img = GrayImage::filter(img, &CONDITION_BORDER_COLOR, 30, matrix_right, matrix_top, img.get_width(), matrix_bottom); + + // │ condition content descriptions │ + // └───────────────────────────────────┘ + + // tries to find the most bottom-right horizontal line + let rect_width = 300; + let rect_height = 1; + let (x_right, y_bottom) = match img.rfind_rect(rect_width, rect_height) { + Some((x_start, y_start)) => (x_start + rect_width - 1, y_start + rect_height - 1), + None => return None, + }; + + // │ condition content descriptions │ + // └───────────────────────────────────┘ <- (x_right, y_bottom) is here + debug_assert_eq!(img.pixel(x_right - 2, y_bottom + 0), 255); + debug_assert_eq!(img.pixel(x_right - 1, y_bottom + 0), 255); + debug_assert_eq!(img.pixel(x_right + 0, y_bottom + 0), 255); + debug_assert_eq!(img.pixel(x_right + 1, y_bottom + 0), 0); + debug_assert_eq!(img.pixel(x_right + 2, y_bottom + 0), 0); + debug_assert_eq!(img.pixel(x_right - 2, y_bottom + 1), 0); + debug_assert_eq!(img.pixel(x_right - 1, y_bottom + 1), 0); + debug_assert_eq!(img.pixel(x_right + 0, y_bottom + 1), 0); + debug_assert_eq!(img.pixel(x_right + 1, y_bottom + 1), 0); + debug_assert_eq!(img.pixel(x_right + 2, y_bottom + 1), 0); + + let x_left = match (300..x_right).find(|dx| img.pixel(x_right - dx - 1, y_bottom) == 0) { + Some(width) => x_right - width, + None => return None, + }; + + // │ condition content descriptions │ + // └───────────────────────────────────┘ <- (x_right, y_bottom) is here + // ^ + // (x_left, y_bottom) is here + // need to filter condition descriptions. Searching for description images + + let width = img.rect_hull(x_left + 1, 0, x_right - 1, y_bottom - 1) + .map(|(desc_start_x, _, _, _)| desc_start_x) + .unwrap_or(x_right) - x_left; + + return Some((matrix_right + x_left + 1, matrix_top, matrix_right + width - 1, matrix_top + y_bottom - 1)); } -pub fn find_blocks_count(img: &Image, condition_area: &(u32, u32, u32, u32)) -> Option { - let mut img = crop_img(&img, condition_area.0, condition_area.1 / 2, img.get_width(), condition_area.1); - filter_img(&mut img, &BLOCK_COLOR, 5); - - // tries to find first vertical line - let line_height = 8; - - let mut start: Option<(u32, u32)> = None; - for x in 0..img.get_width() { - for y in 0..(img.get_height() - line_height) { - let vertical_line = (0..line_height).all(|v| img.get_pixel(x, y + v) != bmp::consts::WHITE); - if vertical_line { - start = Some((x, y)); - break; - } - } - if start.is_some() { - break; - } - } - - if let Some((start_x, y)) = start { - let block_width = ((start_x + 1)..img.get_width()).take_while(|x| img.get_pixel(*x, y) == bmp::consts::WHITE).count() + 1; - let blocks = (start_x..img.get_width()).step_by(block_width).take_while(|x| img.get_pixel(*x, y) != bmp::consts::WHITE).count(); - return Some(blocks); - } - - return None; +pub fn find_buffer_size(img: &Image, condition_area: &(u32, u32, u32, u32)) -> Option { + let (condition_left, condition_top, condition_right, _) = *condition_area; + + let img = GrayImage::filter(img, &BUFFER_COLOR, 30, condition_left, condition_top / 2, condition_right, 3 * condition_top / 4); + + // ───────────────────┐ + // ┌ ─ ┐ ┌ ─ ┐ ┌ ─ ┐ │ + // │ + // │ │ │ │ │ │ │ + // │ + // └ ─ ┘ └ ─ ┘ └ ─ ┘ │ + // ───────────────────┘ + + // tries to find right border + let rect_width = 1; + let rect_height = 35; + let (x_right, y_bottom) = match img.rfind_rect(rect_width, rect_height) { + Some((x_start, y_start)) => (x_start + rect_width - 1, y_start + rect_height - 1), + None => return None, + }; + + let height = match (30..y_bottom).find(|dy| img.pixel(x_right, y_bottom - dy - 1) == 0) { + Some(height) => height, + None => return None, + }; + + let y = y_bottom - height / 2; + debug_assert_eq!(img.pixel(x_right + 2, y), 0); + debug_assert_eq!(img.pixel(x_right + 1, y), 0); + debug_assert_eq!(img.pixel(x_right + 0, y), 255); + debug_assert_eq!(img.pixel(x_right - 1, y), 0); + debug_assert_eq!(img.pixel(x_right - 2, y), 0); + + let count = (0..x_right).filter(|&x| img.pixel(x, y) != 0).count() / 2; + return Some(count); } #[cfg(test)] mod tests { - use crate::img::load_img_from_file; - use crate::recognize::{find_blocks_count, find_condition_area, find_matrix_area}; - - const MATRIX_AREA1: (u32, u32, u32, u32) = (142, 337, 837, 802); - const CONDITION_AREA1: (u32, u32, u32, u32) = (885, 337, 1346, 567); + use bmp::Image; - const MATRIX_AREA2: (u32, u32, u32, u32) = (206, 337, 773, 673); - const CONDITION_AREA2: (u32, u32, u32, u32) = (821, 337, 1282, 567); + use crate::img::load_img_from_file; + use crate::recognize::{find_buffer_size, find_condition_area, find_matrix_area}; + use crate::test_cases::{BUFFER_SIZE1, BUFFER_SIZE2, BUFFER_SIZE3, BUFFER_SIZE4, BUFFER_SIZE5, CONDITION_AREA1, CONDITION_AREA2, CONDITION_AREA3, CONDITION_AREA4, CONDITION_AREA5, FILE1, FILE2, FILE3, FILE4, FILE5, MATRIX_AREA1, MATRIX_AREA2, MATRIX_AREA3, MATRIX_AREA4, MATRIX_AREA5}; #[test] fn test_find_matrix_area1() { - let img = load_img_from_file("test/test1.bmp"); - let area = find_matrix_area(&img); - assert_eq!(Some(MATRIX_AREA1), area); + test_find_matrix_area(FILE1, MATRIX_AREA1); } #[test] fn test_find_matrix_area2() { - let img = load_img_from_file("test/test2.bmp"); - let area = find_matrix_area(&img); - assert_eq!(Some(MATRIX_AREA2), area); + test_find_matrix_area(FILE2, MATRIX_AREA2); + } + + #[test] + fn test_find_matrix_area3() { + test_find_matrix_area(FILE3, MATRIX_AREA3); + } + + #[test] + fn test_find_matrix_area4() { + test_find_matrix_area(FILE4, MATRIX_AREA4); + } + + #[test] + fn test_find_matrix_area5() { + test_find_matrix_area(FILE5, MATRIX_AREA5); + } + + fn test_find_matrix_area(filename: &str, expected: (u32, u32, u32, u32)) { + let img = load_img_from_file(filename); + let actual = find_matrix_area(&img); + assert_eq!(Some(expected), actual); } #[test] fn test_find_matrix_area_not_found() { - let img = load_img_from_file("test/ocr_simple_test.bmp"); + let img = Image::new(2, 2); let area = find_matrix_area(&img); assert_eq!(None, area); } #[test] fn test_find_condition_area1() { - let img = load_img_from_file("test/test1.bmp"); - let area = find_condition_area(&img, &MATRIX_AREA1); - assert_eq!(Some(CONDITION_AREA1), area); + test_find_condition_area(FILE1, &MATRIX_AREA1, CONDITION_AREA1); } #[test] fn test_find_condition_area2() { - let img = load_img_from_file("test/test2.bmp"); - let area = find_condition_area(&img, &MATRIX_AREA2); - assert_eq!(Some(CONDITION_AREA2), area); + test_find_condition_area(FILE2, &MATRIX_AREA2, CONDITION_AREA2); + } + + #[test] + fn test_find_condition_area3() { + test_find_condition_area(FILE3, &MATRIX_AREA3, CONDITION_AREA3); + } + + #[test] + fn test_find_condition_area4() { + test_find_condition_area(FILE4, &MATRIX_AREA4, CONDITION_AREA4); + } + + #[test] + fn test_find_condition_area5() { + test_find_condition_area(FILE5, &MATRIX_AREA5, CONDITION_AREA5); + } + + fn test_find_condition_area(filename: &str, matrix_area: &(u32, u32, u32, u32), expected: (u32, u32, u32, u32)) { + let img = load_img_from_file(filename); + let actual = find_condition_area(&img, matrix_area); + assert_eq!(Some(expected), actual); } #[test] fn test_find_condition_area_not_found() { - let img = load_img_from_file("test/ocr_simple_test.bmp"); - let area = find_condition_area(&img, &(0, 0, 0, 0)); + let img = Image::new(2, 2); + let area = find_condition_area(&img, &(0, 0, 1, 1)); assert_eq!(None, area); } #[test] - fn find_blocks_count1() { - let img = load_img_from_file("test/test1.bmp"); - let count = find_blocks_count(&img, &CONDITION_AREA1); - assert_eq!(Some(6), count); + fn test_find_buffer_size1() { + test_find_buffer_size(FILE1, &CONDITION_AREA1, BUFFER_SIZE1); + } + + #[test] + fn test_find_buffer_size2() { + test_find_buffer_size(FILE2, &CONDITION_AREA2, BUFFER_SIZE2); + } + + #[test] + fn test_find_buffer_size3() { + test_find_buffer_size(FILE3, &CONDITION_AREA3, BUFFER_SIZE3); + } + + #[test] + fn test_find_buffer_size4() { + test_find_buffer_size(FILE4, &CONDITION_AREA4, BUFFER_SIZE4); + } + + #[test] + fn test_find_buffer_size5() { + test_find_buffer_size(FILE5, &CONDITION_AREA5, BUFFER_SIZE5); + } + + fn test_find_buffer_size(filename: &str, condition_area: &(u32, u32, u32, u32), expected: usize) { + let img = load_img_from_file(filename); + let actual = find_buffer_size(&img, condition_area); + assert_eq!(Some(expected), actual); } #[test] - fn find_blocks_count2() { - let img = load_img_from_file("test/test2.bmp"); - let count = find_blocks_count(&img, &CONDITION_AREA2); - assert_eq!(Some(6), count); + fn test_find_buffer_size_not_found() { + let img = Image::new(6, 6); + let count = find_buffer_size(&img, &(4, 4, 6, 6)); + assert_eq!(None, count); } } \ No newline at end of file diff --git a/src/template.bmp b/src/template.bmp new file mode 100644 index 0000000..3fe9102 Binary files /dev/null and b/src/template.bmp differ diff --git a/src/test_cases.rs b/src/test_cases.rs index bafe922..4d5c7b9 100644 --- a/src/test_cases.rs +++ b/src/test_cases.rs @@ -1,7 +1,5 @@ -use std::collections::HashSet; -use crate::img::{crop_img, filter_img, load_img_from_file}; -use crate::ocr::ocr_matrix; -use crate::{recognize, solver}; +use crate::{ocr, recognize, solver}; +use crate::img::{GrayImage, load_img_from_file}; use crate::recognize::{CONDITION_COLOR, MATRIX_COLOR}; use crate::solver::{Solution, Step}; @@ -27,17 +25,20 @@ pub fn conditions1() -> Vec> { ] } -pub const BLOCKS1: usize = 6; +pub const BUFFER_SIZE1: usize = 6; + +pub const MATRIX_AREA1: (u32, u32, u32, u32) = (142, 337, 837, 802); +pub const CONDITION_AREA1: (u32, u32, u32, u32) = (885, 337, 1252, 567); pub fn solutions1() -> Vec { vec![ Solution { steps: vec![Step::new(5, 0), Step::new(5, 3), Step::new(1, 3), Step::new(1, 4)], - conditions: vec![true, true, false] + conditions: vec![true, true, false], }, Solution { steps: vec![Step::new(3, 0), Step::new(3, 2), Step::new(2, 2), Step::new(2, 4), Step::new(5, 4), Step::new(5, 0)], - conditions: vec![true, false, true] + conditions: vec![true, false, true], }, ] } @@ -45,7 +46,6 @@ pub fn solutions1() -> Vec { pub const HAS_FULL_SOLUTION1: bool = false; - pub const FILE2: &str = "test/test2.bmp"; pub fn matrix2() -> Vec> { @@ -66,25 +66,27 @@ pub fn conditions2() -> Vec> { ] } +pub const BUFFER_SIZE2: usize = 6; + +pub const MATRIX_AREA2: (u32, u32, u32, u32) = (206, 337, 773, 674); +pub const CONDITION_AREA2: (u32, u32, u32, u32) = (821, 337, 1188, 567); + pub fn solutions2() -> Vec { vec![ Solution { steps: vec![Step::new(2, 0), Step::new(2, 4), Step::new(1, 4), Step::new(1, 1), Step::new(2, 1)], - conditions: vec![true, true, true] + conditions: vec![true, true, true], }, Solution { steps: vec![Step::new(3, 0), Step::new(3, 3), Step::new(0, 3), Step::new(0, 4), Step::new(1, 4)], - conditions: vec![true, true, true] + conditions: vec![true, true, true], }, ] } -pub const BLOCKS2: usize = 6; - pub const HAS_FULL_SOLUTION2: bool = true; - pub const FILE3: &str = "test/test3.bmp"; pub fn matrix3() -> Vec> { @@ -107,29 +109,31 @@ pub fn conditions3() -> Vec> { ] } +pub const BUFFER_SIZE3: usize = 6; + +pub const MATRIX_AREA3: (u32, u32, u32, u32) = (142, 337, 837, 802); +pub const CONDITION_AREA3: (u32, u32, u32, u32) = (885, 337, 1252, 567); + pub fn solutions3() -> Vec { vec![ Solution { steps: vec![Step::new(0, 0), Step::new(0, 3), Step::new(1, 3), Step::new(1, 1), Step::new(6, 1)], - conditions: vec![false, true, true] + conditions: vec![false, true, true], }, Solution { steps: vec![Step::new(0, 0), Step::new(0, 3), Step::new(4, 3), Step::new(4, 1), Step::new(3, 1)], - conditions: vec![true, false, true] + conditions: vec![true, false, true], }, Solution { steps: vec![Step::new(4, 0), Step::new(4, 1), Step::new(3, 1), Step::new(3, 4), Step::new(5, 4), Step::new(5, 3)], - conditions: vec![true, true, false] + conditions: vec![true, true, false], }, ] } -pub const BLOCKS3: usize = 6; - pub const HAS_FULL_SOLUTION3: bool = false; - pub const FILE4: &str = "test/test4.bmp"; pub fn matrix4() -> Vec> { @@ -152,29 +156,31 @@ pub fn conditions4() -> Vec> { ] } +pub const BUFFER_SIZE4: usize = 6; + +pub const MATRIX_AREA4: (u32, u32, u32, u32) = (142, 337, 837, 802); +pub const CONDITION_AREA4: (u32, u32, u32, u32) = (885, 337, 1252, 567); + pub fn solutions4() -> Vec { vec![ Solution { steps: vec![Step::new(0, 0), Step::new(0, 4), Step::new(6, 4), Step::new(6, 3), Step::new(4, 3), Step::new(4, 0)], - conditions: vec![false, true, true] + conditions: vec![false, true, true], }, Solution { steps: vec![Step::new(0, 0), Step::new(0, 4), Step::new(6, 4), Step::new(6, 3), Step::new(2, 3), Step::new(2, 6)], - conditions: vec![true, false, true] + conditions: vec![true, false, true], }, Solution { steps: vec![Step::new(6, 0), Step::new(6, 3), Step::new(4, 3), Step::new(4, 0), Step::new(2, 0), Step::new(2, 2)], - conditions: vec![true, true, false] + conditions: vec![true, true, false], }, ] } -pub const BLOCKS4: usize = 6; - pub const HAS_FULL_SOLUTION4: bool = false; - pub const FILE5: &str = "test/test5.bmp"; pub fn matrix5() -> Vec> { @@ -197,25 +203,27 @@ pub fn conditions5() -> Vec> { ] } +pub const BUFFER_SIZE5: usize = 6; + +pub const MATRIX_AREA5: (u32, u32, u32, u32) = (142, 337, 837, 802); +pub const CONDITION_AREA5: (u32, u32, u32, u32) = (885, 337, 1252, 567); + pub fn solutions5() -> Vec { vec![ Solution { steps: vec![Step::new(0, 0), Step::new(0, 1), Step::new(1, 1), Step::new(1, 5)], - conditions: vec![true, false, false] + conditions: vec![true, false, false], }, Solution { steps: vec![Step::new(2, 0), Step::new(2, 1), Step::new(3, 1), Step::new(3, 2)], - conditions: vec![false, true, false] + conditions: vec![false, true, false], }, ] } -pub const BLOCKS5: usize = 6; - pub const HAS_FULL_SOLUTION5: bool = false; - pub const FILE6: &str = "test/test6.bmp"; pub fn matrix6() -> Vec> { @@ -235,25 +243,24 @@ pub fn conditions6() -> Vec> { ] } +pub const BUFFER_SIZE6: usize = 6; + pub fn solutions6() -> Vec { vec![ Solution { steps: vec![Step::new(0, 0), Step::new(0, 1), Step::new(2, 1), Step::new(2, 0)], - conditions: vec![true] + conditions: vec![true], }, Solution { steps: vec![Step::new(1, 0), Step::new(1, 1), Step::new(2, 1), Step::new(2, 0)], - conditions: vec![true] + conditions: vec![true], }, ] } -pub const BLOCKS6: usize = 6; - pub const HAS_FULL_SOLUTION6: bool = true; - pub const FILE7: &str = "test/test7.bmp"; pub fn matrix7() -> Vec> { @@ -276,25 +283,24 @@ pub fn conditions7() -> Vec> { ] } +pub const BUFFER_SIZE7: usize = 6; + pub fn solutions7() -> Vec { vec![ Solution { steps: vec![Step::new(5, 0), Step::new(5, 5), Step::new(3, 5), Step::new(3, 0), Step::new(4, 0), Step::new(4, 4)], - conditions: vec![true, true, false] + conditions: vec![true, true, false], }, Solution { steps: vec![Step::new(6, 0), Step::new(6, 2), Step::new(4, 2), Step::new(4, 4), Step::new(3, 4), Step::new(3, 0)], - conditions: vec![true, false, true] + conditions: vec![true, false, true], }, ] } -pub const BLOCKS7: usize = 6; - pub const HAS_FULL_SOLUTION7: bool = false; - pub const FILE8: &str = "test/test8.bmp"; pub fn matrix8() -> Vec> { @@ -315,21 +321,20 @@ pub fn conditions8() -> Vec> { ] } +pub const BUFFER_SIZE8: usize = 6; + pub fn solutions8() -> Vec { vec![ Solution { steps: vec![Step::new(4, 0), Step::new(4, 1), Step::new(5, 1), Step::new(5, 0), Step::new(0, 0), Step::new(0, 3)], - conditions: vec![true, true] + conditions: vec![true, true], }, ] } -pub const BLOCKS8: usize = 6; - pub const HAS_FULL_SOLUTION8: bool = true; - pub const FILE9: &str = "test/test9.bmp"; pub fn matrix9() -> Vec> { @@ -351,94 +356,81 @@ pub fn conditions9() -> Vec> { ] } +pub const BUFFER_SIZE9: usize = 6; + pub fn solutions9() -> Vec { vec![ Solution { steps: vec![Step::new(1, 0), Step::new(1, 2), Step::new(0, 2), Step::new(0, 0)], - conditions: vec![true, false, false] + conditions: vec![true, false, false], }, Solution { steps: vec![Step::new(3, 0), Step::new(3, 3), Step::new(4, 3), Step::new(4, 0)], - conditions: vec![false, true, false] + conditions: vec![false, true, false], }, Solution { steps: vec![Step::new(0, 0), Step::new(0, 3), Step::new(4, 3), Step::new(4, 2)], - conditions: vec![false, false, true] + conditions: vec![false, false, true], }, ] } -pub const BLOCKS9: usize = 6; - pub const HAS_FULL_SOLUTION9: bool = false; - -#[test] -fn all_hex() { - let mut hex = HashSet::new(); - for matrix in [matrix1(), matrix2(), matrix3(), matrix4(), matrix5(), matrix6(), matrix7(), matrix8(), matrix9()] { - for line in matrix { - for h in line { - hex.insert(h); - } - } - } - assert_eq!(HashSet::from([0x1C, 0x55, 0x7A, 0xBD, 0xE9, 0xFF]), hex); -} - #[test] fn test1() { - test(FILE1, &matrix1(), &conditions1(), BLOCKS1, &solutions1(), HAS_FULL_SOLUTION1); + test(FILE1, &matrix1(), &conditions1(), BUFFER_SIZE1, &solutions1(), HAS_FULL_SOLUTION1); } #[test] fn test2() { - test(FILE2, &matrix2(), &conditions2(), BLOCKS2, &solutions2(), HAS_FULL_SOLUTION2); + test(FILE2, &matrix2(), &conditions2(), BUFFER_SIZE2, &solutions2(), HAS_FULL_SOLUTION2); } #[test] fn test3() { - test(FILE3, &matrix3(), &conditions3(), BLOCKS3, &solutions3(), HAS_FULL_SOLUTION3); + test(FILE3, &matrix3(), &conditions3(), BUFFER_SIZE3, &solutions3(), HAS_FULL_SOLUTION3); } #[test] fn test4() { - test(FILE4, &matrix4(), &conditions4(), BLOCKS4, &solutions4(), HAS_FULL_SOLUTION4); + test(FILE4, &matrix4(), &conditions4(), BUFFER_SIZE4, &solutions4(), HAS_FULL_SOLUTION4); } #[test] fn test5() { - test(FILE5, &matrix5(), &conditions5(), BLOCKS5, &solutions5(), HAS_FULL_SOLUTION5); + test(FILE5, &matrix5(), &conditions5(), BUFFER_SIZE5, &solutions5(), HAS_FULL_SOLUTION5); } #[test] fn test6() { - test(FILE6, &matrix6(), &conditions6(), BLOCKS6, &solutions6(), HAS_FULL_SOLUTION6); + test(FILE6, &matrix6(), &conditions6(), BUFFER_SIZE6, &solutions6(), HAS_FULL_SOLUTION6); } #[test] fn test7() { - test(FILE7, &matrix7(), &conditions7(), BLOCKS7, &solutions7(), HAS_FULL_SOLUTION7); + test(FILE7, &matrix7(), &conditions7(), BUFFER_SIZE7, &solutions7(), HAS_FULL_SOLUTION7); } #[test] fn test8() { - test(FILE8, &matrix8(), &conditions8(), BLOCKS8, &solutions8(), HAS_FULL_SOLUTION8); + test(FILE8, &matrix8(), &conditions8(), BUFFER_SIZE8, &solutions8(), HAS_FULL_SOLUTION8); } #[test] fn test9() { - test(FILE9, &matrix9(), &conditions9(), BLOCKS9, &solutions9(), HAS_FULL_SOLUTION9); + test(FILE9, &matrix9(), &conditions9(), BUFFER_SIZE9, &solutions9(), HAS_FULL_SOLUTION9); } -fn test(path: &str, expected_matrix: &Vec>, expected_conditions: &Vec>, expected_blocks: usize, expected_solutions: &Vec, has_full_solution: bool) { +fn test(path: &str, expected_matrix: &Vec>, expected_conditions: &Vec>, expected_steps: usize, expected_solutions: &Vec, has_full_solution: bool) { + let templates = ocr::MatrixTemplates::load_templates(); + let img = load_img_from_file(path); let matrix_area = recognize::find_matrix_area(&img).expect("Matrix was not found"); - let mut matrix_img = crop_img(&img, matrix_area.0, matrix_area.1, matrix_area.2, matrix_area.3); - filter_img(&mut matrix_img, &MATRIX_COLOR, 60); - let matrix = match ocr_matrix(&matrix_img) { + let matrix_img = GrayImage::filter(&img, &MATRIX_COLOR, 50, matrix_area.0, matrix_area.1, matrix_area.2, matrix_area.3); + let matrix = match ocr::ocr_matrix(&matrix_img, &templates) { Ok(r) => r, Err(err) => panic!("Matrix was not recognized: {}", err), }; @@ -446,29 +438,28 @@ fn test(path: &str, expected_matrix: &Vec>, expected_conditions: &Vec r, Err(err) => panic!("Conditions were not recognized: {}", err), }; drop(condition_img); - assert_eq!(*expected_conditions, conditions.4); + assert_eq!(*expected_conditions, conditions); - let blocks = recognize::find_blocks_count(&img, &condition_area).expect("Blocks were not found"); - assert_eq!(expected_blocks, blocks); + let steps = recognize::find_buffer_size(&img, &condition_area).expect("Buffer size was not recognized"); + assert_eq!(expected_steps, steps); - let solutions = solver::solve(&matrix.4, &conditions.4, blocks); + let solutions = solver::solve(&matrix.4, &conditions, steps); for expected in expected_solutions.iter() { let found = solutions.iter().any(|actual| actual.conditions == expected.conditions && actual.steps == expected.steps); - assert_eq!(true, found); + assert!(found); } - assert_eq!(true, solutions.iter().all(|s| s.conditions.contains(&true)), "solution covers nothing"); + assert!(solutions.iter().all(|s| s.conditions.contains(&true)), "solution covers nothing"); for s in solutions.iter() { - assert_eq!(true, !s.steps.is_empty() && s.steps.len() <= blocks, "solution is too long or empty"); + assert_eq!(true, !s.steps.is_empty() && s.steps.len() <= steps, "solution is too long or empty"); for step in s.steps.iter() { - let found_steps = s.steps.iter().filter(|s| *s == step).count(); + let found_steps = s.steps.iter().filter(|&s| s == step).count(); assert_eq!(1, found_steps, "solution has same step 2 times"); } @@ -493,6 +484,6 @@ fn test(path: &str, expected_matrix: &Vec>, expected_conditions: &Vec