Promote Quadrilateral to default

The library now uses the `Quadrilateral` struct when handling sets of four points in `GridSampler` and `PerspectiveTransform`
This commit is contained in:
Henry Schimke
2023-03-08 11:17:43 -06:00
parent d5e6a5d0a7
commit 2b7d053646
13 changed files with 348 additions and 422 deletions

View File

@@ -19,17 +19,20 @@
// import org.junit.Assert;
// import org.junit.Test;
use crate::point;
/**
* @author Sean Owen
*/
// public final class PerspectiveTransformTestCase extends Assert {
use super::PerspectiveTransform;
use super::{PerspectiveTransform, Quadrilateral};
static EPSILON: f32 = 1.0E-4f32;
#[test]
fn test_square_to_quadrilateral() {
let pt = PerspectiveTransform::squareToQuadrilateral(2.0, 3.0, 10.0, 4.0, 16.0, 15.0, 4.0, 9.0);
let square = Quadrilateral::new(point(2.0, 3.0), point(10.0, 3.0), point(16.0, 15.0), point(4.0,9.0));
let pt = PerspectiveTransform::squareToQuadrilateral(square);
assert_point_equals(2.0, 3.0, 0.0, 0.0, &pt);
assert_point_equals(10.0, 4.0, 1.0, 0.0, &pt);
assert_point_equals(4.0, 9.0, 0.0, 1.0, &pt);
@@ -40,10 +43,10 @@ fn test_square_to_quadrilateral() {
#[test]
fn test_quadrilateral_to_quadrilateral() {
let pt = PerspectiveTransform::quadrilateralToQuadrilateral(
2.0, 3.0, 10.0, 4.0, 16.0, 15.0, 4.0, 9.0, 103.0, 110.0, 300.0, 120.0, 290.0, 270.0, 150.0,
280.0,
);
let quad1 = Quadrilateral::new(point(2.0, 3.0), point(10.0, 4.0), point(16.0, 15.0), point(4.0, 9.0));
let quad2 = Quadrilateral::new(point(103.0, 110.0), point(300.0, 120.0), point(290.0, 270.0), point(150.0,280.0));
let pt = PerspectiveTransform::quadrilateralToQuadrilateral(quad1, quad2);
assert_point_equals(103.0, 110.0, 2.0, 3.0, &pt);
assert_point_equals(300.0, 120.0, 10.0, 4.0, &pt);
assert_point_equals(290.0, 270.0, 16.0, 15.0, &pt);

View File

@@ -18,10 +18,10 @@
// import com.google.zxing.NotFoundException;
use crate::common::Result;
use crate::{common::Result};
use crate::Exceptions;
use super::{BitMatrix, GridSampler, PerspectiveTransform};
use super::{BitMatrix, GridSampler, PerspectiveTransform, Quadrilateral, SamplerControl};
/**
* @author Sean Owen
@@ -35,29 +35,14 @@ impl GridSampler for DefaultGridSampler {
image: &BitMatrix,
dimensionX: u32,
dimensionY: u32,
p1ToX: f32,
p1ToY: f32,
p2ToX: f32,
p2ToY: f32,
p3ToX: f32,
p3ToY: f32,
p4ToX: f32,
p4ToY: f32,
p1FromX: f32,
p1FromY: f32,
p2FromX: f32,
p2FromY: f32,
p3FromX: f32,
p3FromY: f32,
p4FromX: f32,
p4FromY: f32,
dst: Quadrilateral,
src:Quadrilateral
) -> Result<BitMatrix> {
let transform = PerspectiveTransform::quadrilateralToQuadrilateral(
p1ToX, p1ToY, p2ToX, p2ToY, p3ToX, p3ToY, p4ToX, p4ToY, p1FromX, p1FromY, p2FromX,
p2FromY, p3FromX, p3FromY, p4FromX, p4FromY,
dst, src,
);
self.sample_grid(image, dimensionX, dimensionY, &transform)
self.sample_grid(image, dimensionX, dimensionY, &[SamplerControl::new(dimensionX, dimensionY, transform)])
}
fn sample_grid(
@@ -65,7 +50,7 @@ impl GridSampler for DefaultGridSampler {
image: &BitMatrix,
dimensionX: u32,
dimensionY: u32,
transform: &PerspectiveTransform,
controls: &[SamplerControl]
) -> Result<BitMatrix> {
if dimensionX == 0 || dimensionY == 0 {
return Err(Exceptions::NOT_FOUND);
@@ -83,7 +68,7 @@ impl GridSampler for DefaultGridSampler {
points[x + 1] = i_value;
x += 2;
}
transform.transform_points_single(&mut points);
controls.first().unwrap().transform.transform_points_single(&mut points);
// Quick check to see if points transformed to something inside the image;
// sufficient to check the endpoints
self.checkAndNudgePoints(image, &mut points)?;

View File

@@ -18,10 +18,10 @@
// import com.google.zxing.NotFoundException;
use crate::common::Result;
use crate::Exceptions;
use crate::{common::Result, Point};
use crate::{Exceptions, point};
use super::{BitMatrix, PerspectiveTransform};
use super::{BitMatrix, PerspectiveTransform, Quadrilateral};
/**
* Implementations of this class can, given locations of finder patterns for a QR code in an
@@ -93,22 +93,8 @@ pub trait GridSampler {
image: &BitMatrix,
dimensionX: u32,
dimensionY: u32,
p1ToX: f32,
p1ToY: f32,
p2ToX: f32,
p2ToY: f32,
p3ToX: f32,
p3ToY: f32,
p4ToX: f32,
p4ToY: f32,
p1FromX: f32,
p1FromY: f32,
p2FromX: f32,
p2FromY: f32,
p3FromX: f32,
p3FromY: f32,
p4FromX: f32,
p4FromY: f32,
dst: Quadrilateral,
src:Quadrilateral
) -> Result<BitMatrix>;
fn sample_grid(
@@ -116,7 +102,7 @@ pub trait GridSampler {
image: &BitMatrix,
dimensionX: u32,
dimensionY: u32,
transform: &PerspectiveTransform,
controls: &[SamplerControl]
) -> Result<BitMatrix>;
/**
@@ -195,3 +181,15 @@ pub trait GridSampler {
Ok(())
}
}
pub struct SamplerControl {
pub p0: Point,
pub p1: Point,
pub transform: PerspectiveTransform
}
impl SamplerControl {
pub fn new( width: u32, height: u32, transform: PerspectiveTransform ) -> Self {
Self{ p0: point(0.0, 0.0), p1: point(width as f32, height as f32), transform }
}
}

View File

@@ -109,6 +109,9 @@ pub use hybrid_binarizer::*;
mod eci;
pub use eci::*;
mod quad;
pub use quad::*;
#[cfg(feature = "otsu_level")]
mod otsu_level_binarizer;
#[cfg(feature = "otsu_level")]

View File

@@ -16,6 +16,12 @@
// package com.google.zxing.common;
use std::ops::Mul;
use crate::point;
use super::Quadrilateral;
/**
* <p>This class implements a perspective transform in two dimensions. Given four source and four
* destination points, it will compute the transformation implied between them. The code is based
@@ -63,27 +69,18 @@ impl PerspectiveTransform {
#[allow(clippy::too_many_arguments)]
pub fn quadrilateralToQuadrilateral(
x0: f32,
y0: f32,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
x3: f32,
y3: f32,
x0p: f32,
y0p: f32,
x1p: f32,
y1p: f32,
x2p: f32,
y2p: f32,
x3p: f32,
y3p: f32,
dst: Quadrilateral,
src: Quadrilateral
) -> Self {
let q_to_s = PerspectiveTransform::quadrilateralToSquare(x0, y0, x1, y1, x2, y2, x3, y3);
// let q_to_s = PerspectiveTransform::quadrilateralToSquare(x0, y0, x1, y1, x2, y2, x3, y3);
// let s_to_q =
// PerspectiveTransform::squareToQuadrilateral(x0p, y0p, x1p, y1p, x2p, y2p, x3p, y3p);
let q_to_s = PerspectiveTransform::quadrilateralToSquare(dst);
let s_to_q =
PerspectiveTransform::squareToQuadrilateral(x0p, y0p, x1p, y1p, x2p, y2p, x3p, y3p);
s_to_q.times(&q_to_s)
PerspectiveTransform::squareToQuadrilateral(src);
s_to_q * q_to_s
}
pub fn transform_points_single(&self, points: &mut [f32]) {
@@ -122,35 +119,29 @@ impl PerspectiveTransform {
#[allow(clippy::too_many_arguments)]
pub fn squareToQuadrilateral(
x0: f32,
y0: f32,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
x3: f32,
y3: f32,
square: Quadrilateral
) -> Self {
let dx3 = x0 - x1 + x2 - x3;
let dy3 = y0 - y1 + y2 - y3;
if dx3 == 0.0 && dy3 == 0.0 {
let [p0, p1, p2, p3 ] = square.0;
let d3 = p0 - p1 + p2 - p3;
if d3 == point(0.0,0.0) {
// Affine
PerspectiveTransform::new(x1 - x0, x2 - x1, x0, y1 - y0, y2 - y1, y0, 0.0, 0.0, 1.0)
PerspectiveTransform::new(p1.x - p0.x, p2.x - p1.x, p0.x, p1.y - p0.y, p2.y - p1.y, p0.y, 0.0, 0.0, 1.0)
} else {
let dx1 = x1 - x2;
let dx2 = x3 - x2;
let dy1 = y1 - y2;
let dy2 = y3 - y2;
let denominator = dx1 * dy2 - dx2 * dy1;
let a13 = (dx3 * dy2 - dx2 * dy3) / denominator;
let a23 = (dx1 * dy3 - dx3 * dy1) / denominator;
let d1 = p1 - p2;
let d2 = p3 - p2;
let denominator = d1.cross(d2);
let a13 = (d3.x * d2.y - d2.x * d3.y) / denominator;
let a23 = (d1.x * d3.y - d3.x * d1.y) / denominator;
PerspectiveTransform::new(
x1 - x0 + a13 * x1,
x3 - x0 + a23 * x3,
x0,
y1 - y0 + a13 * y1,
y3 - y0 + a23 * y3,
y0,
p1.x - p0.x + a13 * p1.x,
p3.x - p0.x + a23 * p3.x,
p0.x,
p1.y - p0.y + a13 * p1.y,
p3.y - p0.y + a23 * p3.y,
p0.y,
a13,
a23,
1.0,
@@ -160,17 +151,10 @@ impl PerspectiveTransform {
#[allow(clippy::too_many_arguments)]
pub fn quadrilateralToSquare(
x0: f32,
y0: f32,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
x3: f32,
y3: f32,
quad: Quadrilateral
) -> Self {
// Here, the adjoint serves as the inverse
PerspectiveTransform::squareToQuadrilateral(x0, y0, x1, y1, x2, y2, x3, y3).buildAdjoint()
PerspectiveTransform::squareToQuadrilateral(quad).buildAdjoint()
}
fn buildAdjoint(&self) -> Self {
@@ -187,18 +171,22 @@ impl PerspectiveTransform {
self.a11 * self.a22 - self.a12 * self.a21,
)
}
}
fn times(&self, other: &Self) -> Self {
impl Mul for PerspectiveTransform {
type Output = PerspectiveTransform;
fn mul(self, rhs: Self) -> Self::Output {
PerspectiveTransform::new(
self.a11 * other.a11 + self.a21 * other.a12 + self.a31 * other.a13,
self.a11 * other.a21 + self.a21 * other.a22 + self.a31 * other.a23,
self.a11 * other.a31 + self.a21 * other.a32 + self.a31 * other.a33,
self.a12 * other.a11 + self.a22 * other.a12 + self.a32 * other.a13,
self.a12 * other.a21 + self.a22 * other.a22 + self.a32 * other.a23,
self.a12 * other.a31 + self.a22 * other.a32 + self.a32 * other.a33,
self.a13 * other.a11 + self.a23 * other.a12 + self.a33 * other.a13,
self.a13 * other.a21 + self.a23 * other.a22 + self.a33 * other.a23,
self.a13 * other.a31 + self.a23 * other.a32 + self.a33 * other.a33,
self.a11 * rhs.a11 + self.a21 * rhs.a12 + self.a31 * rhs.a13,
self.a11 * rhs.a21 + self.a21 * rhs.a22 + self.a31 * rhs.a23,
self.a11 * rhs.a31 + self.a21 * rhs.a32 + self.a31 * rhs.a33,
self.a12 * rhs.a11 + self.a22 * rhs.a12 + self.a32 * rhs.a13,
self.a12 * rhs.a21 + self.a22 * rhs.a22 + self.a32 * rhs.a23,
self.a12 * rhs.a31 + self.a22 * rhs.a32 + self.a32 * rhs.a33,
self.a13 * rhs.a11 + self.a23 * rhs.a12 + self.a33 * rhs.a13,
self.a13 * rhs.a21 + self.a23 * rhs.a22 + self.a33 * rhs.a23,
self.a13 * rhs.a31 + self.a23 * rhs.a32 + self.a33 * rhs.a33,
)
}
}
}

217
src/common/quad.rs Normal file
View File

@@ -0,0 +1,217 @@
use crate::Point;
#[derive(Clone, Copy, Debug)]
pub struct Quadrilateral(pub [Point; 4]);
impl Quadrilateral {
// using Base = std::array<T, 4>;
// using Base::at;
// public:
// using Point = T;
#[allow(dead_code)]
pub fn new(tl:Point, tr: Point, br: Point, bl: Point) -> Self {
Self([tl,tr,br,bl])
}
// pub fn with_f32( tl:f32, tr:f32, br:f32, bl:f32) -> Self {
// Self([tl, tr,br, bl ])
// }
pub fn with_points(tl: Point, tr: Point, br: Point, bl: Point) -> Self {
Self([tl, tr, br, bl])
}
pub fn top_left(&self) -> &Point {
&self.0[0]
} //const noexcept { return at(0); }
pub fn top_right(&self) -> &Point {
&self.0[1]
} //const noexcept { return at(1); }
pub fn bottom_right(&self) -> &Point {
&self.0[2]
} //const noexcept { return at(2); }
pub fn bottom_left(&self) -> &Point {
&self.0[3]
} //const noexcept { return at(3); }
#[allow(dead_code)]
pub fn orientation(&self) -> f64 {
let centerLine =
(*self.top_right() + *self.bottom_right()) - (*self.top_left() + *self.bottom_left());
if (centerLine == Point { x: 0.0, y: 0.0 }) {
return 0.0;
}
let centerLineF = Point::normalized(centerLine);
f32::atan2(centerLineF.y, centerLineF.x).into()
}
pub fn points(&self) -> &[Point] {
&self.0
}
}
impl Quadrilateral {
#[allow(dead_code)]
pub fn rectangle(width: i32, height: i32, margin: Option<i32>) -> Quadrilateral {
let margin = margin.unwrap_or(0);
Quadrilateral([
Point {
x: margin as f32,
y: margin as f32,
},
Point {
x: width as f32 - margin as f32,
y: margin as f32,
},
Point {
x: width as f32 - margin as f32,
y: height as f32 - margin as f32,
},
Point {
x: margin as f32,
y: height as f32 - margin as f32,
},
])
}
#[allow(dead_code)]
pub fn centered_square(size: i32) -> Quadrilateral {
Self::scale(
&Quadrilateral([
Point { x: -1.0, y: -1.0 },
Point { x: 1.0, y: -1.0 },
Point { x: 1.0, y: 1.0 },
Point { x: -1.0, y: 1.0 },
]),
size / 2,
)
}
#[allow(dead_code)]
pub fn line(y: i32, xStart: i32, xStop: i32) -> Quadrilateral {
Quadrilateral([
Point {
x: xStart as f32,
y: y as f32,
},
Point {
x: xStop as f32,
y: y as f32,
},
Point {
x: xStop as f32,
y: y as f32,
},
Point {
x: xStart as f32,
y: y as f32,
},
])
}
#[allow(dead_code)]
pub fn is_convex(&self) -> bool {
let N = self.0.len();
let mut sign = false;
let mut m = f32::INFINITY;
let mut M = 0.0_f32;
for i in 0..N
// for(int i = 0; i < N; i++)
{
let d1 = self.0[(i + 2) % N] - self.0[(i + 1) % N];
let d2 = self.0[i] - self.0[(i + 1) % N];
let cp = d1.cross(d2);
m = if m.abs() > cp { cp } else { m.abs() };
M = if M.abs() > cp { M.abs() } else { cp };
// m = std::cmp::min((m).abs(), cp);
// M = std::cmp::max((M).abs(), cp);
if i == 0 {
sign = cp > 0.0;
} else if sign != (cp > 0.0) {
return false;
}
}
// It turns out being convex is not enough to prevent a "numerical instability"
// that can cause the corners being projected inside the image boundaries but
// some points near the corners being projected outside. This has been observed
// where one corner is almost in line with two others. The M/m ratio is below 2
// for the complete existing sample set. For very "skewed" QRCodes a value of
// around 3 is realistic. A value of 14 has been observed to trigger the
// instability.
M / m < 4.0
}
#[allow(dead_code)]
pub fn scale(&self, factor: i32) -> Quadrilateral {
Quadrilateral([
self.0[0] * factor as f32,
self.0[1] * factor as f32,
self.0[2] * factor as f32,
self.0[3] * factor as f32,
])
}
#[allow(dead_code)]
pub fn center(&self) -> Point {
let reduced: Point = self.0.iter().sum();
let size = self.0.len() as f32;
reduced / size
// return Reduce(q) / Size(q);
}
#[allow(dead_code)]
pub fn rotated_corners(&self, n: Option<i32>, mirror: Option<bool>) -> Quadrilateral {
let n = if let Some(n) = n { n } else { 1 };
let mirror = if let Some(m) = mirror { m } else { false };
let mut res = self.clone();
res.0.rotate_left(((n + 4) % 4) as usize);
// std::rotate_copy(q.begin(), q.begin() + ((n + 4) % 4), q.end(), res.begin());
if mirror {
res.0.swap(1, 3);
}
// {std::swap(res[1], res[3]);}
res
}
#[allow(dead_code)]
pub fn is_inside(&self, p: Point) -> bool {
// Test if p is on the same side (right or left) of all polygon segments
let mut pos = 0;
let mut neg = 0;
for i in 0..self.0.len()
// for (int i = 0; i < Size(q); ++i)
{
if Point::cross(p - self.0[i], self.0[(i + 1) % self.0.len()] - self.0[i]) < 0.0 {
neg += 1;
} else {
pos += 1;
}
// (cross(p - q[i], q[(i + 1) % Size(q)] - q[i]) < 0 ? neg : pos)++;
}
pos == 0 || neg == 0
}
#[allow(dead_code)]
pub fn have_intersecting_bounding_boxes(&self, b: &Quadrilateral) -> bool {
// TODO: this is only a quick and dirty approximation that works for the trivial standard cases
let x = b.top_right().x < self.top_left().x || b.top_left().x > self.top_right().x;
let y = b.bottom_left().y < self.top_left().y || b.top_left().y > self.bottom_left().y;
!(x || y)
}
}
impl Default for Quadrilateral {
fn default() -> Self {
Self([Point { x: 0.0, y: 0.0 }; 4])
}
}