From f5abe869220f197d51ad80ec8e22d48ee294dc6e Mon Sep 17 00:00:00 2001 From: Henry Schimke Date: Mon, 23 Jan 2023 17:14:01 -0600 Subject: [PATCH] basic support for maxicode detection --- .gitignore | 2 + Cargo.toml | 5 +- src/maxicode/decoder/bit_matrix_parser.rs | 8 +- src/maxicode/decoder/maxicode_decoder.rs | 4 +- src/maxicode/detector.rs | 984 ++++++++++++++++++++++ src/maxicode/maxi_code_reader.rs | 28 +- src/maxicode/mod.rs | 1 + 7 files changed, 1018 insertions(+), 14 deletions(-) create mode 100644 src/maxicode/detector.rs diff --git a/.gitignore b/.gitignore index 5085ff3..f451242 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ Cargo.lock /Cargo.lock .DS_Store + +dbgfle* \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 001df47..80faf1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rxing" -version = "0.2.19" +version = "0.2.20" description="A rust port of the zxing barcode library." license="Apache-2.0" repository="https://github.com/hschimke/rxing" @@ -42,4 +42,5 @@ image = ["dep:image", "dep:imageproc"] allow_forced_iso_ied_18004_compliance = [] svg_write = ["dep:svg"] svg_read = ["dep:resvg", "image"] -wasm_support = ["chrono/wasmbind"] \ No newline at end of file +wasm_support = ["chrono/wasmbind"] +experimental_features = [] \ No newline at end of file diff --git a/src/maxicode/decoder/bit_matrix_parser.rs b/src/maxicode/decoder/bit_matrix_parser.rs index ee5d4ea..a595fea 100644 --- a/src/maxicode/decoder/bit_matrix_parser.rs +++ b/src/maxicode/decoder/bit_matrix_parser.rs @@ -155,14 +155,14 @@ const BITNR: [[i16; 30]; 33] = [ * @author mike32767 * @author Manuel Kasten */ -pub struct BitMatrixParser(BitMatrix); +pub struct BitMatrixParser<'a>(&'a BitMatrix); -impl BitMatrixParser { +impl<'a> BitMatrixParser<'_> { /** * @param bitMatrix {@link BitMatrix} to parse */ - pub fn new(bitMatrix: BitMatrix) -> Self { - Self(bitMatrix) + pub fn new(bitMatrix: &'a BitMatrix) -> BitMatrixParser<'a> { + BitMatrixParser(bitMatrix) } pub fn readCodewords(&self) -> [u8; 144] { diff --git a/src/maxicode/decoder/maxicode_decoder.rs b/src/maxicode/decoder/maxicode_decoder.rs index ed210f6..b7ac5d5 100644 --- a/src/maxicode/decoder/maxicode_decoder.rs +++ b/src/maxicode/decoder/maxicode_decoder.rs @@ -45,12 +45,12 @@ static RS_DECODER: Lazy = Lazy::new(|| { )) }); -pub fn decode(bits: BitMatrix) -> Result { +pub fn decode(bits: &BitMatrix) -> Result { decode_with_hints(bits, &HashMap::new()) } pub fn decode_with_hints( - bits: BitMatrix, + bits: &BitMatrix, _hints: &DecodingHintDictionary, ) -> Result { let parser = BitMatrixParser::new(bits); diff --git a/src/maxicode/detector.rs b/src/maxicode/detector.rs new file mode 100644 index 0000000..7c443de --- /dev/null +++ b/src/maxicode/detector.rs @@ -0,0 +1,984 @@ +use crate::{ + common::{BitMatrix, DefaultGridSampler, DetectorRXingResult, GridSampler}, + Exceptions, RXingResultPoint, +}; + +use super::MaxiCodeReader; + +const ROW_SCAN_SKIP: u32 = 5; + +#[derive(Debug)] +pub struct MaxicodeDetectionResult { + bits: BitMatrix, + points: Vec, +} + +impl DetectorRXingResult for MaxicodeDetectionResult { + fn getBits(&self) -> &BitMatrix { + &self.bits + } + + fn getPoints(&self) -> &[RXingResultPoint] { + &self.points + } +} + +struct Circle { + center: (u32, u32), + radius: u32, + horizontal_buckets: [u32; 11], + vertical_buckets: [u32; 11], +} + +impl Circle { + pub fn calculate_circle_variance(&self) -> f32 { + let total_width_even = self + .horizontal_buckets + .iter() + .zip(self.vertical_buckets.iter()) + .enumerate() + .filter_map(|e| { + if e.0 != 5 && (e.0 == 0 || e.0 % 2 == 0) { + Some(*e.1 .0 + *e.1 .1) + } else { + None + } + }) + .sum::() as f32; + let total_width_odd = self + .horizontal_buckets + .iter() + .zip(self.vertical_buckets.iter()) + .enumerate() + .filter_map(|e| { + if e.0 != 5 && (e.0 != 0 && e.0 % 2 != 0) { + Some(*e.1 .0 + *e.1 .1) + } else { + None + } + }) + .sum::() as f32; + + let estimated_module_size_even = total_width_even / 10.0; + let estimated_module_size_odd = total_width_odd / 10.0; + + // let expected_module_size = total_circle_pixels / (self.horizontal_buckets.len() - 1) as f32; + let total_variance_even = self + .horizontal_buckets + .iter() + .enumerate() + .filter(|p| p.0 != 5 && (p.0 == 0 || p.0 % 2 == 0)) + .fold(0.0, |acc, (_, module_size)| { + acc + (estimated_module_size_even - *module_size as f32).abs() + }); + + let total_variance_odd = self + .horizontal_buckets + .iter() + .enumerate() + .filter(|p| p.0 != 5 && (p.0 != 0 || p.0 % 2 != 0)) + .fold(0.0, |acc, (_, module_size)| { + acc + (estimated_module_size_odd - *module_size as f32).abs() + }); + + let expected_area_vertical = + (self.horizontal_buckets[5] / 2).pow(2) as f32 * std::f32::consts::PI; + let expected_area_horizontal = + (self.vertical_buckets[5] / 2).pow(2) as f32 * std::f32::consts::PI; + let circle_area_average = (expected_area_horizontal + expected_area_vertical) / 2.0; + + let circle_area_variance = (expected_area_horizontal - circle_area_average).abs() + + (expected_area_vertical - circle_area_average).abs(); + + total_variance_even + total_variance_odd + circle_area_variance + } + + pub fn calculate_center_point_std_dev(circles: &[Self]) -> ((u32, u32), (u32, u32)) { + let (x_total, y_total) = circles.iter().fold((0, 0), |(x_acc, y_acc), c| { + (x_acc + c.center.0, y_acc + c.center.1) + }); + let x_mean = x_total as f64 / circles.len() as f64; + let y_mean = y_total as f64 / circles.len() as f64; + let (x_squared_variances, y_squared_variances) = + circles.iter().fold((0.0, 0.0), |(x_acc, y_acc), c| { + ( + x_acc + (c.center.0 as f64 - x_mean).powf(2.0), + y_acc + (c.center.1 as f64 - y_mean).powf(2.0), + ) + }); + let x_squared_variance_mean = x_squared_variances / circles.len() as f64; + let y_squared_variance_mean = y_squared_variances / circles.len() as f64; + let x_standard_deviation = x_squared_variance_mean.sqrt(); + let y_standard_deviation = y_squared_variance_mean.sqrt(); + + ( + (x_standard_deviation as u32, y_standard_deviation as u32), + (x_mean as u32, y_mean as u32), + ) + } +} + +pub fn detect(image: &BitMatrix, try_harder: bool) -> Result { + // find concentric circles + let Some( mut circles) = find_concentric_circles(image) else { + return Err(Exceptions::NotFoundException(None)); + }; + + // we should have an idea where the center is at this point, + // so we should be able to remove points that are widly far + // from what we have otherwise found. + let center_point_std_dev = Circle::calculate_center_point_std_dev(&circles); + circles.retain(|c| { + (c.center.0 as i32 - center_point_std_dev.1 .0 as i32).unsigned_abs() + <= center_point_std_dev.0 .0 + && (c.center.1 as i32 - center_point_std_dev.1 .1 as i32).unsigned_abs() + <= center_point_std_dev.0 .1 + }); + + // Sort the points based on variance + circles.sort_by(compare_circle); + + for circle in &circles { + // build a box around this circle, trying to find the barcode + let Ok(symbol_box) = box_symbol(image, circle) else { + if try_harder { + continue; + }else { + return Err(Exceptions::NotFoundException(None)) + } + }; + let grid_sampler = DefaultGridSampler::default(); + + let [tl, bl, tr, br] = &symbol_box; + + let target_width = (tr.0 - tl.0).round() as u32; + let target_height = (br.1 - tr.1).round() as u32; + + let Ok(bits) = grid_sampler.sample_grid_detailed( + image, + target_width, + target_height, + 0.0, + 0.0, + target_width as f32 , + 0.0, + target_width as f32, + target_height as f32, + 0.0, + target_height as f32, + tl.0, + tl.1, + tr.0, + tr.1, + br.0, + br.1, + bl.0, + bl.1, + ) else { + if try_harder { + continue; + }else { + return Err(Exceptions::NotFoundException(None)) + } + }; + return Ok(MaxicodeDetectionResult { + bits, + points: symbol_box + .iter() + .map(|p| RXingResultPoint { x: p.0, y: p.1 }) + .collect(), + }); + } + + Err(Exceptions::NotFoundException(None)) +} + +/// Locate concentric circles. +/// A bullseye looks like: +/// + +/// - +/// + +/// - +/// + +/// +-+-+-+-+-+ +/// + +/// - +/// + +/// - +/// + +fn find_concentric_circles(image: &BitMatrix) -> Option> { + let mut bullseyes = Vec::new(); + + // find things that might be bullseye patterns, we start 6 in because a bullseye is at least six pixels in diameter + let mut row = 6; + while row < image.getHeight() - 6 { + let mut current_column = 6; + while current_column < image.getWidth() - 6 { + // check if we can find something that looks like a bullseye + if let Some((center, radius, horizontal_buckets)) = + find_next_bullseye_horizontal(image, row, current_column) + { + // check that the bullseye is not just a figment of our one-dimensional imagination + let (target_good, vertical_buckets) = verify_bullseye_vertical(image, row, center); + if target_good { + // found a bullseye! + + // add it + bullseyes.push(Circle { + center: (center, row), + radius, + horizontal_buckets, + vertical_buckets, + }); + + // update the search to the next possible location + current_column = center + radius; + continue; + } else { + // false alarm, go on with the row + current_column = center - radius + (radius / 4); + continue; + } + } else { + row += ROW_SCAN_SKIP; + break; + } + } + } + + if bullseyes.is_empty() { + None + } else { + Some(bullseyes) + } +} + +#[derive(PartialEq, Eq, Clone, Copy)] +enum Color { + Black, + White, +} + +impl std::ops::Not for Color { + type Output = Self; + + fn not(self) -> Self::Output { + match self { + Color::Black => Color::White, + Color::White => Color::Black, + } + } +} + +impl From for Color { + fn from(value: bool) -> Self { + match value { + true => Color::Black, + false => Color::White, + } + } +} + +/// If a bullseye is found, returns (center, radius) +fn find_next_bullseye_horizontal( + image: &BitMatrix, + row: u32, + start_column: u32, +) -> Option<(u32, u32, [u32; 11])> { + let mut buckets = [0_u32; 11]; + + let mut column = start_column; + let mut last_color = Color::Black; + let mut pointer = 0; + + // remove leading white space + while !image.get(column, row) && column < image.getWidth() - 6 { + column += 1; + } + + while column < image.getWidth() - 6 { + let local_bit = image.get(column, row); + if Color::from(local_bit) != last_color { + last_color = !last_color; + pointer += 1; + + // if we reached the end of our buckets, validate the segment + if pointer == 11 { + if validate_bullseye_widths(&buckets) && last_color == Color::White { + // bullseye widths look good, this is a bullseye + return Some(get_bullseye_metadata(&buckets, column)); + } else { + // false alarm, this pattern doesn't look enough like it's evenly distributed + // move on to the next set. + pointer -= 1; + buckets.copy_within(1.., 0); + buckets[10] = 0; + } + } + } + + buckets[pointer] += 1; + + column += 1; + } + + None +} + +/// look up and down from the provided column to verify that a possible bullseye exists +fn verify_bullseye_vertical(image: &BitMatrix, row: u32, column: u32) -> (bool, [u32; 11]) { + // look up + let up_vector = get_column_vector(image, column, row, true); + + // look down + let down_vector = get_column_vector(image, column, row, false); + + let potential_bullseye = [ + down_vector[5], + down_vector[4], + down_vector[3], + down_vector[2], + down_vector[1], + down_vector[0] + up_vector[0], + up_vector[1], + up_vector[2], + up_vector[3], + up_vector[4], + up_vector[5], + ]; + + if validate_bullseye_widths(&potential_bullseye) { + ( + validate_bullseye_widths(&potential_bullseye), + potential_bullseye, + ) + } else { + // try to nudge one in either direction and try again + // look up + let up_vector = get_column_vector(image, column + 1, row, true); + + // look down + let down_vector = get_column_vector(image, column + 1, row, false); + + let potential_bullseye = [ + down_vector[5], + down_vector[4], + down_vector[3], + down_vector[2], + down_vector[1], + down_vector[0] + up_vector[0], + up_vector[1], + up_vector[2], + up_vector[3], + up_vector[4], + up_vector[5], + ]; + + if validate_bullseye_widths(&potential_bullseye) { + ( + validate_bullseye_widths(&potential_bullseye), + potential_bullseye, + ) + } else { + // look up + let up_vector = get_column_vector(image, column - 1, row, true); + + // look down + let down_vector = get_column_vector(image, column - 1, row, false); + + let potential_bullseye = [ + down_vector[5], + down_vector[4], + down_vector[3], + down_vector[2], + down_vector[1], + down_vector[0] + up_vector[0], + up_vector[1], + up_vector[2], + up_vector[3], + up_vector[4], + up_vector[5], + ]; + + if validate_bullseye_widths(&potential_bullseye) { + ( + validate_bullseye_widths(&potential_bullseye), + potential_bullseye, + ) + } else { + (false, potential_bullseye) + } + } + } +} + +fn get_column_vector(image: &BitMatrix, column: u32, start_row: u32, looking_up: bool) -> [u32; 6] { + let mut buckets = [0_u32; 6]; + + let mut row = start_row; + let mut last_color = Color::White; + let mut pointer = 0; + while row > 0 && row < image.getHeight() { + let local_bit = image.get(column, row); + if Color::from(local_bit) != last_color { + last_color = !last_color; + pointer += 1; + } + if pointer > 5 { + break; + } + buckets[pointer] += 1; + + row = if looking_up { row + 1 } else { row - 1 }; + } + + buckets +} + +fn validate_bullseye_widths(buckets: &[u32; 11]) -> bool { + let total_width_even = buckets + .iter() + .enumerate() + .filter_map(|e| { + if e.0 != 5 && (e.0 == 0 || e.0 % 2 == 0) { + Some(*e.1) + } else { + None + } + }) + .sum::() as f32; + let total_width_odd = buckets + .iter() + .enumerate() + .filter_map(|e| { + if e.0 != 5 && (e.0 != 0 && e.0 % 2 != 0) { + Some(*e.1) + } else { + None + } + }) + .sum::() as f32; + + let estimated_module_size_even = total_width_even / 5.0; + let estimated_module_size_odd = total_width_odd / 5.0; + + let max_variance_even = estimated_module_size_even / 2.0; + let max_variance_odd = estimated_module_size_odd / 2.0; + + // let total_width = buckets.iter().sum::() as f32; + // let estimated_module_size = total_width / (buckets.len() -1 ) as f32; + + // let max_variance = estimated_module_size / 2.5; + + let b1 = (estimated_module_size_even - buckets[0] as f32).abs(); + let b2 = (estimated_module_size_odd - buckets[1] as f32).abs(); + let b3 = (estimated_module_size_even - buckets[2] as f32).abs(); + let b4 = (estimated_module_size_odd - buckets[3] as f32).abs(); + let b5 = (estimated_module_size_even - buckets[4] as f32).abs(); + // let b6 = (estimated_module_size - buckets[5] as f32).abs(); + let b7 = (estimated_module_size_even - buckets[6] as f32).abs(); + let b8 = (estimated_module_size_odd - buckets[7] as f32).abs(); + let b9 = (estimated_module_size_even - buckets[8] as f32).abs(); + let b10 = (estimated_module_size_odd - buckets[9] as f32).abs(); + let b11 = (estimated_module_size_even - buckets[10] as f32).abs(); + + b1 < max_variance_even + && b2 < max_variance_odd + && b3 < max_variance_even + && b4 < max_variance_odd + && b5 < max_variance_even + // && b6 < max_variance * 2.0 + && b7 < max_variance_even + && b8 < max_variance_odd + && b9 < max_variance_even + && b10 < max_variance_odd + && b11 < max_variance_even +} + +/// returns the (center , radius) of the possible bullseye +fn get_bullseye_metadata(buckets: &[u32; 11], column: u32) -> (u32, u32, [u32; 11]) { + let radius = ((buckets.iter().sum::() as f32) / 2.0).round() as u32; //buckets.iter().skip(6).sum::() + buckets[5] / 2; + let center = column - radius; //buckets.iter().take(5).sum::() - (buckets[5] / 2); + (center, radius, *buckets) +} + +const LEFT_SHIFT_PERCENT_ADJUST: f32 = 0.0; +const RIGHT_SHIFT_PERCENT_ADJUST: f32 = 0.03; +const ACCEPTED_SCALES: [f64; 5] = [0.065, 0.069, 0.07, 0.075, 0.08]; + +fn box_symbol(image: &BitMatrix, circle: &Circle) -> Result<[(f32, f32); 4], Exceptions> { + let (left_boundary, right_boundary, top_boundary, bottom_boundary) = + calculate_simple_boundary(circle, Some(image), None); + + let naive_box = [ + RXingResultPoint::new(left_boundary as f32, bottom_boundary as f32), + RXingResultPoint::new(left_boundary as f32, top_boundary as f32), + RXingResultPoint::new(right_boundary as f32, bottom_boundary as f32), + RXingResultPoint::new(right_boundary as f32, top_boundary as f32), + ]; + + let mut result_box = naive_box; + + #[cfg(feature="experimental_features")] + for scale in ACCEPTED_SCALES { + if let Some(found_rotation) = attempt_rotation_box(image, circle, &naive_box, scale) { + result_box = found_rotation; + break; + } + } + // let result_box = if let Some(found_rotation) = attempt_rotation_box(image, circle, &naive_box) { + // found_rotation + // } else { + // naive_box + // }; + + Ok([ + (result_box[0].x, result_box[0].y), + (result_box[1].x, result_box[1].y), + (result_box[2].x, result_box[2].y), + (result_box[3].x, result_box[3].y), + ]) +} + +fn calculate_simple_boundary( + circle: &Circle, + image: Option<&BitMatrix>, + center_scale: Option, +) -> (u32, u32, u32, u32) { + let (symbol_width, symbol_height) = if image.is_some() { + guess_barcode_size(circle) + } else if let Some(s) = center_scale { + guess_barcode_size_general(circle, 0.03, s, 0.97) + } else { + guess_barcode_size_tighter(circle) + }; + + let (image_width, image_height) = if let Some(i) = image { + (i.getWidth(), i.getHeight()) + } else { + (symbol_width, symbol_height) + }; + + let up_down_shift = symbol_height as i32 / 2; + + let left_shift = + ((symbol_width as f32 / 2.0) - (symbol_width as f32 * LEFT_SHIFT_PERCENT_ADJUST)) as i32; + let right_shift = + ((symbol_width as f32 / 2.0) + (symbol_width as f32 * RIGHT_SHIFT_PERCENT_ADJUST)) as i32; + + let left_boundary = + (circle.center.0 as i32 - left_shift).clamp(0, image_width as i32 - 33) as u32; + let right_boundary = + (circle.center.0 as i32 + right_shift).clamp(33, image_width as i32) as u32; + //symbol_width.clamp(30, image.getWidth()); + let top_boundary = + (circle.center.1 as i32 + up_down_shift).clamp(33, image_height as i32) as u32; + //symbol_height.clamp(33, image.getHeight()); + let bottom_boundary = + (circle.center.1 as i32 - up_down_shift).clamp(0, image_height as i32 - 30) as u32; + (left_boundary, right_boundary, top_boundary, bottom_boundary) +} + +const TOP_LEFT_ORIENTATION_POS: ((u32, u32), (u32, u32), (u32, u32)) = ((10, 9), (11, 9), (11, 10)); +const TOP_RIGHT_ORIENTATION_POS: ((u32, u32), (u32, u32), (u32, u32)) = + ((17, 9), (17, 10), (18, 10)); +const LEFT_ORIENTATION_POS: ((u32, u32), (u32, u32), (u32, u32)) = ((7, 15), (7, 16), (8, 16)); +const RIGHT_ORIENTATION_POS: ((u32, u32), (u32, u32), (u32, u32)) = ((20, 16), (21, 16), (20, 17)); +const BOTTOM_LEFT_ORIENTATION_POS: ((u32, u32), (u32, u32), (u32, u32)) = + ((10, 22), (11, 22), (10, 23)); +const BOTTOM_RIGHT_ORIENTATION_POS: ((u32, u32), (u32, u32), (u32, u32)) = + ((17, 22), (16, 23), (17, 23)); + +fn attempt_rotation_box( + image: &BitMatrix, + circle: &Circle, + naive_box: &[RXingResultPoint; 4], + center_scale: f64, +) -> Option<[RXingResultPoint; 4]> { + // use the expected symbol syze to calculate the x,y module size (best guess) + // let (symbol_width, symbol_height) = + // guess_barcode_size_general(circle, 0.03, center_scale, 0.97); //guess_barcode_size_tighter(circle); + // let x_module_size = (symbol_width as f32 / MaxiCodeReader::MATRIX_WIDTH as f32).round() as u32; + // let y_module_size = + // (symbol_height as f32 / MaxiCodeReader::MATRIX_HEIGHT as f32).round() as u32; + + // we know that the locator symbols should appear at 60 degree increments around the circle + + // top left + let (topl_p1, topl_p2, topl_p3) = + get_adjusted_points(TOP_LEFT_ORIENTATION_POS, circle, center_scale); + + // top right + let (topr_p1, topr_p2, topr_p3) = + get_adjusted_points(TOP_RIGHT_ORIENTATION_POS, circle, center_scale); + + // left + let (l_p1, l_p2, l_p3) = get_adjusted_points(LEFT_ORIENTATION_POS, circle, center_scale); + + // right + let (r_p1, r_p2, r_p3) = get_adjusted_points(RIGHT_ORIENTATION_POS, circle, center_scale); + + // bottom left + let (bottoml_p1, bottoml_p2, bottoml_p3) = + get_adjusted_points(BOTTOM_LEFT_ORIENTATION_POS, circle, center_scale); + + // bottom right + let (bottomr_p1, bottomr_p2, bottomr_p3) = + get_adjusted_points(BOTTOM_RIGHT_ORIENTATION_POS, circle, center_scale); + + let mut found = false; + let mut final_rotation = 0.0; + + for int_rotation in 0..355 { + let rotation = int_rotation as f32; + // look for top left + // * * + // * + let p1_rot = get_point(circle.center, topl_p1, rotation); + let p2_rot = get_point(circle.center, topl_p2, rotation); + let p3_rot = get_point(circle.center, topl_p3, rotation); + let found_tl = image.get(p1_rot.0 as u32, p1_rot.1 as u32) + && image.get(p2_rot.0 as u32, p2_rot.1 as u32) + && image.get(p3_rot.0 as u32, p3_rot.1 as u32); + if !found_tl { + continue; + } + + // look for top right + // /\ + // __ + let p1_rot = get_point(circle.center, topr_p1, rotation); + let p2_rot = get_point(circle.center, topr_p2, rotation); + let p3_rot = get_point(circle.center, topr_p3, rotation); + let found_tr = !image.get(p1_rot.0 as u32, p1_rot.1 as u32) + && !image.get(p2_rot.0 as u32, p2_rot.1 as u32) + && !image.get(p3_rot.0 as u32, p3_rot.1 as u32); + if !found_tr { + continue; + } + + // look for left + // * + // * + let p1_rot = get_point(circle.center, l_p1, rotation); + let p2_rot = get_point(circle.center, l_p2, rotation); + let p3_rot = get_point(circle.center, l_p3, rotation); + let found_l = image.get(p1_rot.0 as u32, p1_rot.1 as u32) + && !image.get(p2_rot.0 as u32, p2_rot.1 as u32) + && image.get(p3_rot.0 as u32, p3_rot.1 as u32); + if !found_l { + continue; + } + + // look for right + // * + // * + let p1_rot = get_point(circle.center, r_p1, rotation); + let p2_rot = get_point(circle.center, r_p2, rotation); + let p3_rot = get_point(circle.center, r_p3, rotation); + let found_r = image.get(p1_rot.0 as u32, p1_rot.1 as u32) + && !image.get(p2_rot.0 as u32, p2_rot.1 as u32) + && image.get(p3_rot.0 as u32, p3_rot.1 as u32); + if !found_r { + continue; + } + + // look for bottom left + // * + // * + let p1_rot = get_point(circle.center, bottoml_p1, rotation); + let p2_rot = get_point(circle.center, bottoml_p2, rotation); + let p3_rot = get_point(circle.center, bottoml_p3, rotation); + let found_bl = image.get(p1_rot.0 as u32, p1_rot.1 as u32) + && !image.get(p2_rot.0 as u32, p2_rot.1 as u32) + && image.get(p3_rot.0 as u32, p3_rot.1 as u32); + if !found_bl { + continue; + } + + // look for bottom right + // * + // * + let p1_rot = get_point(circle.center, bottomr_p1, rotation); + let p2_rot = get_point(circle.center, bottomr_p2, rotation); + let p3_rot = get_point(circle.center, bottomr_p3, rotation); + let found_br = image.get(p1_rot.0 as u32, p1_rot.1 as u32) + && !image.get(p2_rot.0 as u32, p2_rot.1 as u32) + && image.get(p3_rot.0 as u32, p3_rot.1 as u32); + if !found_br { + continue; + } + + // did we find it? + found = found_tl && found_tr && found_l && found_r && found_bl && found_br; + + if found { + final_rotation = rotation; + break; + } + } + + if found { + let new_1 = get_point( + circle.center, + (naive_box[0].x as u32, naive_box[0].y as u32), + final_rotation, + ); + let new_2 = get_point( + circle.center, + (naive_box[1].x as u32, naive_box[1].y as u32), + final_rotation, + ); + let new_3 = get_point( + circle.center, + (naive_box[2].x as u32, naive_box[2].y as u32), + final_rotation, + ); + let new_4 = get_point( + circle.center, + (naive_box[3].x as u32, naive_box[3].y as u32), + final_rotation, + ); + Some([ + RXingResultPoint::new(new_1.0, new_1.1), + RXingResultPoint::new(new_2.0, new_2.1), + RXingResultPoint::new(new_3.0, new_3.1), + RXingResultPoint::new(new_4.0, new_4.1), + ]) + } else { + // panic!("couldn't find"); + None + } +} + +fn get_adjusted_points( + origin: ((u32, u32), (u32, u32), (u32, u32)), + circle: &Circle, + center_scale: f64, +) -> ((u32, u32), (u32, u32), (u32, u32)) { + ( + adjust_point_alternate(origin.0, circle, center_scale), + adjust_point_alternate(origin.1, circle, center_scale), + adjust_point_alternate(origin.2, circle, center_scale), + ) +} + +fn get_point(center: (u32, u32), original: (u32, u32), angle: f32) -> (f32, f32) { + let radians = angle.to_radians(); + let x = radians.cos() * (original.0 as f32 - center.0 as f32) + - radians.sin() * (original.1 as f32 - center.1 as f32) + + center.0 as f32; + let y = radians.sin() * (original.0 as f32 - center.0 as f32) + + radians.cos() * (original.1 as f32 - center.1 as f32) + + center.1 as f32; + + (x, y) +} + +fn adjust_point_alternate(point: (u32, u32), circle: &Circle, center_scale: f64) -> (u32, u32) { + let (left_boundary, right_boundary, top_boundary, bottom_boundary) = + calculate_simple_boundary(circle, None, Some(center_scale)); + + let top = bottom_boundary; + let height = top_boundary - bottom_boundary; + let width = right_boundary - left_boundary; + let left = left_boundary; + + let y = point.1; + let x = point.0; + + let iy = (top + (y * height + height / 2) / MaxiCodeReader::MATRIX_HEIGHT).min(height - 1); + let ix = left + + ((x * width + width / 2 + (y & 0x01) * width / 2) / MaxiCodeReader::MATRIX_WIDTH) + .min(width - 1); + + (ix, iy) +} + +/// calculate a likely size for the barcode. +/// returns (width, height) +fn guess_barcode_size(circle: &Circle) -> (u32, u32) { + // let circle_area = std::f64::consts::PI * circle.radius.pow(2) as f64; + // let ideal_symbol_area = (circle_area / 0.065) / 0.97; + // let ideal_symbol_side = ideal_symbol_area.sqrt(); + // ( + // ideal_symbol_side.round() as u32, + // (ideal_symbol_side).round() as u32, + // ) + guess_barcode_size_general(circle, 0.03, 0.066, 1.0) +} + +fn guess_barcode_size_tighter(circle: &Circle) -> (u32, u32) { + guess_barcode_size_general(circle, 0.03, 0.0695, 0.97) +} + +// fn guess_barcode_size(circle: &Circle) -> (u32, u32) { +// let diameter = circle.horizontal_buckets.iter().sum::() as f32; +// ((diameter / 0.29) as u32, ((diameter / 0.29) * 0.97) as u32) +// } + +fn guess_barcode_size_general( + circle: &Circle, + height_adjust_percent: f64, + circle_area_percent: f64, + height_final_adjust_percent: f64, +) -> (u32, u32) { + let circle_area = std::f64::consts::PI * circle.radius.pow(2) as f64; + let ideal_symbol_area = (circle_area / circle_area_percent) / (1.0 - height_adjust_percent); + let ideal_symbol_side = ideal_symbol_area.sqrt(); + + // let estimated_module_x = ideal_symbol_side / MaxiCodeReader::MATRIX_WIDTH as f64; + // let estimated_module_y = + // (ideal_symbol_side * height_final_adjust_percent) / MaxiCodeReader::MATRIX_HEIGHT as f64; + + ( + ideal_symbol_side.round() as u32, + (ideal_symbol_side * height_final_adjust_percent).round() as u32, + ) +} + +/// compare two circles to determine which has a better variance. +fn compare_circle(a: &Circle, b: &Circle) -> std::cmp::Ordering { + let a_var = a.calculate_circle_variance(); + let b_var = b.calculate_circle_variance(); + + // a_var.partial_cmp(&b_var).unwrap() + + if a_var < b_var { + std::cmp::Ordering::Greater + } else if a_var > b_var { + std::cmp::Ordering::Less + } else { + std::cmp::Ordering::Equal + } +} + +/// Read appropriate bits from a bitmatrix for the maxicode decoder +pub fn read_bits(image: &BitMatrix) -> Result { + let enclosingRectangle = image.getEnclosingRectangle().unwrap(); + + let left = enclosingRectangle[0]; + let top = enclosingRectangle[1]; + let width = enclosingRectangle[2]; + let height = enclosingRectangle[3]; + + // Now just read off the bits + let mut bits = BitMatrix::new(MaxiCodeReader::MATRIX_WIDTH, MaxiCodeReader::MATRIX_HEIGHT)?; + for y in 0..MaxiCodeReader::MATRIX_HEIGHT { + // for (int y = 0; y < MATRIX_HEIGHT; y++) { + let iy = (top + (y * height + height / 2) / MaxiCodeReader::MATRIX_HEIGHT).min(height - 1); + for x in 0..MaxiCodeReader::MATRIX_WIDTH { + // for (int x = 0; x < MATRIX_WIDTH; x++) { + // srowen: I don't quite understand why the formula below is necessary, but it + // can walk off the image if left + width = the right boundary. So cap it. + let ix = left + + ((x * width + width / 2 + (y & 0x01) * width / 2) / MaxiCodeReader::MATRIX_WIDTH) + .min(width - 1); + if image.get(ix, iy) { + bits.set(x, y); + } + } + } + + Ok(bits) +} + +#[cfg(feature = "image")] +#[cfg(test)] +mod detector_test { + use std::io::Read; + + use crate::{ + common::{DetectorRXingResult, HybridBinarizer}, + maxicode::detector::read_bits, + Binarizer, BufferedImageLuminanceSource, + }; + + #[test] + fn mode_1() { + finder_test( + "test_resources/blackbox/maxicode-1/1.png", + "test_resources/blackbox/maxicode-1/1.txt", + ) + } + + #[test] + fn mode_2() { + finder_test( + "test_resources/blackbox/maxicode-1/MODE2.png", + "test_resources/blackbox/maxicode-1/MODE2.txt", + ) + } + + #[test] + fn mode3() { + finder_test( + "test_resources/blackbox/maxicode-1/MODE3.png", + "test_resources/blackbox/maxicode-1/MODE3.txt", + ) + } + + #[test] + fn mixed_sets() { + finder_test( + "test_resources/blackbox/maxicode-1/mode4-mixed-sets.png", + "test_resources/blackbox/maxicode-1/mode4-mixed-sets.txt", + ) + } + + #[test] + fn mode4() { + finder_test( + "test_resources/blackbox/maxicode-1/MODE4.png", + "test_resources/blackbox/maxicode-1/MODE4.txt", + ) + } + + #[test] + fn mode5() { + finder_test( + "test_resources/blackbox/maxicode-1/MODE5.png", + "test_resources/blackbox/maxicode-1/MODE5.txt", + ) + } + + #[test] + fn mode6() { + finder_test( + "test_resources/blackbox/maxicode-1/MODE6.png", + "test_resources/blackbox/maxicode-1/MODE6.txt", + ) + } + + fn finder_test(image: &str, data: &str) { + let filename = image; + let img = image::open(filename).unwrap(); + let lum_src = BufferedImageLuminanceSource::new(img); + let binarizer = HybridBinarizer::new(Box::new(lum_src)); + let bitmatrix = binarizer.getBlackMatrix().unwrap(); + + // let i: image::DynamicImage = bitmatrix.into(); + // i.save("dbgfle.png").expect("should write image"); + + let mut expected_result = String::new(); + + std::fs::File::open(data) + .unwrap() + .read_to_string(&mut expected_result) + .unwrap(); + + let detection = super::detect(bitmatrix, true).unwrap(); + + // let i: image::DynamicImage = detection.getBits().into(); + // i.save("dbgfle-transformed.png") + // .expect("should write image"); + + let bits = read_bits(detection.getBits()).expect("read bits"); + + // std::fs::File::create("dbgfle-read").unwrap().write_all(bits.to_string().as_bytes()).expect("write"); + + let result = crate::maxicode::decoder::decode(&bits).expect("must decode"); + + assert_eq!(expected_result, result.getText()); + } +} diff --git a/src/maxicode/maxi_code_reader.rs b/src/maxicode/maxi_code_reader.rs index 32dcb2b..c422d59 100644 --- a/src/maxicode/maxi_code_reader.rs +++ b/src/maxicode/maxi_code_reader.rs @@ -17,10 +17,12 @@ use std::collections::HashMap; use crate::{ - common::BitMatrix, BarcodeFormat, Exceptions, RXingResult, RXingResultMetadataType, Reader, + common::{BitMatrix, DetectorRXingResult}, + BarcodeFormat, DecodeHintType, DecodeHintValue, Exceptions, RXingResult, + RXingResultMetadataType, Reader, }; -use super::decoder::maxicode_decoder; +use super::{decoder::maxicode_decoder, detector}; /** * This implementation can detect and decode a MaxiCode in an image. @@ -61,8 +63,22 @@ impl Reader for MaxiCodeReader { ) -> Result { // Note that MaxiCode reader effectively always assumes PURE_BARCODE mode // and can't detect it in an image - let bits = Self::extractPureBits(image.getBlackMatrix())?; - let decoderRXingResult = maxicode_decoder::decode_with_hints(bits, hints)?; + let try_harder = matches!( + hints.get(&DecodeHintType::TRY_HARDER), + Some(DecodeHintValue::TryHarder(true)) + ); + + let decoderRXingResult = if try_harder { + let result = detector::detect(image.getBlackMatrixMut(), try_harder)?; + let parsed_result = detector::read_bits(result.getBits())?; + maxicode_decoder::decode_with_hints(&parsed_result, hints)? + } else { + let bits = Self::extractPureBits(image.getBlackMatrix())?; + maxicode_decoder::decode_with_hints(&bits, hints)? + }; + + // let bits = Self::extractPureBits(image.getBlackMatrix())?; + // let decoderRXingResult = maxicode_decoder::decode_with_hints(bits, hints)?; let mut result = RXingResult::new( decoderRXingResult.getText(), decoderRXingResult.getRawBytes().clone(), @@ -85,8 +101,8 @@ impl Reader for MaxiCodeReader { } } impl MaxiCodeReader { - const MATRIX_WIDTH: u32 = 30; - const MATRIX_HEIGHT: u32 = 33; + pub const MATRIX_WIDTH: u32 = 30; + pub const MATRIX_HEIGHT: u32 = 33; /** * This method detects a code in a "pure" image -- that is, pure monochrome image diff --git a/src/maxicode/mod.rs b/src/maxicode/mod.rs index 0b16aca..0da7e20 100644 --- a/src/maxicode/mod.rs +++ b/src/maxicode/mod.rs @@ -1,4 +1,5 @@ pub mod decoder; +pub mod detector; mod maxi_code_reader; pub use maxi_code_reader::*;