mirror of
https://github.com/starovoid/rxing.git
synced 2026-07-26 04:12:34 +00:00
basic support for maxicode detection
This commit is contained in:
@@ -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] {
|
||||
|
||||
@@ -45,12 +45,12 @@ static RS_DECODER: Lazy<ReedSolomonDecoder> = Lazy::new(|| {
|
||||
))
|
||||
});
|
||||
|
||||
pub fn decode(bits: BitMatrix) -> Result<DecoderRXingResult, Exceptions> {
|
||||
pub fn decode(bits: &BitMatrix) -> Result<DecoderRXingResult, Exceptions> {
|
||||
decode_with_hints(bits, &HashMap::new())
|
||||
}
|
||||
|
||||
pub fn decode_with_hints(
|
||||
bits: BitMatrix,
|
||||
bits: &BitMatrix,
|
||||
_hints: &DecodingHintDictionary,
|
||||
) -> Result<DecoderRXingResult, Exceptions> {
|
||||
let parser = BitMatrixParser::new(bits);
|
||||
|
||||
984
src/maxicode/detector.rs
Normal file
984
src/maxicode/detector.rs
Normal file
@@ -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<RXingResultPoint>,
|
||||
}
|
||||
|
||||
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::<u32>() 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::<u32>() 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<MaxicodeDetectionResult, Exceptions> {
|
||||
// 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<Vec<Circle>> {
|
||||
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<bool> 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::<u32>() 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::<u32>() 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::<u32>() 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::<u32>() as f32) / 2.0).round() as u32; //buckets.iter().skip(6).sum::<u32>() + buckets[5] / 2;
|
||||
let center = column - radius; //buckets.iter().take(5).sum::<u32>() - (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<f64>,
|
||||
) -> (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::<u32>() 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<BitMatrix, Exceptions> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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<crate::RXingResult, crate::Exceptions> {
|
||||
// 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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod decoder;
|
||||
pub mod detector;
|
||||
mod maxi_code_reader;
|
||||
|
||||
pub use maxi_code_reader::*;
|
||||
|
||||
Reference in New Issue
Block a user