use crate::error::{CvError, CvResult};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AffineTransform {
pub a: f64,
pub b: f64,
pub tx: f64,
pub c: f64,
pub d: f64,
pub ty: f64,
}
impl AffineTransform {
#[must_use]
pub const fn new(a: f64, b: f64, tx: f64, c: f64, d: f64, ty: f64) -> Self {
Self { a, b, tx, c, d, ty }
}
#[must_use]
pub const fn identity() -> Self {
Self::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0)
}
#[must_use]
pub const fn translation(tx: f64, ty: f64) -> Self {
Self::new(1.0, 0.0, tx, 0.0, 1.0, ty)
}
#[must_use]
pub const fn scaling(sx: f64, sy: f64) -> Self {
Self::new(sx, 0.0, 0.0, 0.0, sy, 0.0)
}
#[must_use]
pub fn rotation(angle: f64) -> Self {
let cos = angle.cos();
let sin = angle.sin();
Self::new(cos, -sin, 0.0, sin, cos, 0.0)
}
#[must_use]
pub fn rotation_around(angle: f64, cx: f64, cy: f64) -> Self {
Self::translation(-cx, -cy)
.then(&Self::rotation(angle))
.then(&Self::translation(cx, cy))
}
#[must_use]
pub const fn shear(shx: f64, shy: f64) -> Self {
Self::new(1.0, shx, 0.0, shy, 1.0, 0.0)
}
#[must_use]
pub fn translate(self, tx: f64, ty: f64) -> Self {
self.then(&Self::translation(tx, ty))
}
#[must_use]
pub fn scale(self, sx: f64, sy: f64) -> Self {
self.then(&Self::scaling(sx, sy))
}
#[must_use]
pub fn rotate(self, angle: f64) -> Self {
self.then(&Self::rotation(angle))
}
#[must_use]
pub fn rotate_around(self, angle: f64, cx: f64, cy: f64) -> Self {
self.then(&Self::rotation_around(angle, cx, cy))
}
#[must_use]
pub fn then(&self, other: &Self) -> Self {
Self {
a: other.a * self.a + other.b * self.c,
b: other.a * self.b + other.b * self.d,
tx: other.a * self.tx + other.b * self.ty + other.tx,
c: other.c * self.a + other.d * self.c,
d: other.c * self.b + other.d * self.d,
ty: other.c * self.tx + other.d * self.ty + other.ty,
}
}
#[must_use]
pub fn transform_point(&self, x: f64, y: f64) -> (f64, f64) {
(
self.a * x + self.b * y + self.tx,
self.c * x + self.d * y + self.ty,
)
}
#[must_use]
pub fn transform_points(&self, points: &[(f64, f64)]) -> Vec<(f64, f64)> {
points
.iter()
.map(|&(x, y)| self.transform_point(x, y))
.collect()
}
#[must_use]
pub fn determinant(&self) -> f64 {
self.a * self.d - self.b * self.c
}
#[must_use]
pub fn is_invertible(&self) -> bool {
self.determinant().abs() > f64::EPSILON
}
#[must_use]
pub fn inverse(&self) -> Option<Self> {
let det = self.determinant();
if det.abs() <= f64::EPSILON {
return None;
}
let inv_det = 1.0 / det;
Some(Self {
a: self.d * inv_det,
b: -self.b * inv_det,
tx: (self.b * self.ty - self.d * self.tx) * inv_det,
c: -self.c * inv_det,
d: self.a * inv_det,
ty: (self.c * self.tx - self.a * self.ty) * inv_det,
})
}
#[must_use]
pub const fn as_matrix(&self) -> [[f64; 3]; 3] {
[
[self.a, self.b, self.tx],
[self.c, self.d, self.ty],
[0.0, 0.0, 1.0],
]
}
#[must_use]
pub fn from_matrix(m: [[f64; 3]; 3]) -> Self {
Self::new(m[0][0], m[0][1], m[0][2], m[1][0], m[1][1], m[1][2])
}
}
impl Default for AffineTransform {
fn default() -> Self {
Self::identity()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Interpolation {
Nearest,
#[default]
Bilinear,
Bicubic,
}
pub fn transform_image(
src: &[u8],
src_width: u32,
src_height: u32,
transform: &AffineTransform,
dst_width: u32,
dst_height: u32,
interpolation: Interpolation,
) -> CvResult<Vec<u8>> {
if src_width == 0 || src_height == 0 {
return Err(CvError::invalid_dimensions(src_width, src_height));
}
if dst_width == 0 || dst_height == 0 {
return Err(CvError::invalid_dimensions(dst_width, dst_height));
}
let expected_size = src_width as usize * src_height as usize;
if src.len() < expected_size {
return Err(CvError::insufficient_data(expected_size, src.len()));
}
let inv_transform = transform
.inverse()
.ok_or_else(|| CvError::transform_error("Transform is not invertible"))?;
let dst_size = dst_width as usize * dst_height as usize;
let mut dst = vec![0u8; dst_size];
for dy in 0..dst_height {
for dx in 0..dst_width {
let (sx, sy) = inv_transform.transform_point(dx as f64, dy as f64);
let pixel = match interpolation {
Interpolation::Nearest => sample_nearest(src, src_width, src_height, sx, sy),
Interpolation::Bilinear => sample_bilinear(src, src_width, src_height, sx, sy),
Interpolation::Bicubic => sample_bicubic(src, src_width, src_height, sx, sy),
};
dst[dy as usize * dst_width as usize + dx as usize] = pixel;
}
}
Ok(dst)
}
fn sample_nearest(src: &[u8], width: u32, height: u32, x: f64, y: f64) -> u8 {
let sx = x.round() as i32;
let sy = y.round() as i32;
if sx >= 0 && sx < width as i32 && sy >= 0 && sy < height as i32 {
src[sy as usize * width as usize + sx as usize]
} else {
0
}
}
fn sample_bilinear(src: &[u8], width: u32, height: u32, x: f64, y: f64) -> u8 {
if x < 0.0 || y < 0.0 || x >= width as f64 - 1.0 || y >= height as f64 - 1.0 {
return sample_nearest(src, width, height, x, y);
}
let x0 = x.floor() as usize;
let y0 = y.floor() as usize;
let x1 = x0 + 1;
let y1 = y0 + 1;
let fx = x - x0 as f64;
let fy = y - y0 as f64;
let w = width as usize;
let p00 = src[y0 * w + x0] as f64;
let p10 = src[y0 * w + x1] as f64;
let p01 = src[y1 * w + x0] as f64;
let p11 = src[y1 * w + x1] as f64;
let top = p00 * (1.0 - fx) + p10 * fx;
let bottom = p01 * (1.0 - fx) + p11 * fx;
let value = top * (1.0 - fy) + bottom * fy;
value.round().clamp(0.0, 255.0) as u8
}
fn sample_bicubic(src: &[u8], width: u32, height: u32, x: f64, y: f64) -> u8 {
if x < 1.0 || y < 1.0 || x >= width as f64 - 2.0 || y >= height as f64 - 2.0 {
return sample_bilinear(src, width, height, x, y);
}
let x_int = x.floor() as i32;
let y_int = y.floor() as i32;
let fx = x - x_int as f64;
let fy = y - y_int as f64;
let w = width as usize;
let mut value = 0.0;
for ky in -1..=2 {
let wy = cubic_weight(ky as f64 - fy);
let py = (y_int + ky) as usize;
for kx in -1..=2 {
let wx = cubic_weight(kx as f64 - fx);
let px = (x_int + kx) as usize;
value += src[py * w + px] as f64 * wx * wy;
}
}
value.round().clamp(0.0, 255.0) as u8
}
#[inline]
fn cubic_weight(x: f64) -> f64 {
let x = x.abs();
if x < 1.0 {
(1.5 * x - 2.5) * x * x + 1.0
} else if x < 2.0 {
((-0.5 * x + 2.5) * x - 4.0) * x + 2.0
} else {
0.0
}
}
pub fn estimate_affine(
src_points: &[(f64, f64)],
dst_points: &[(f64, f64)],
) -> CvResult<AffineTransform> {
if src_points.len() < 3 || dst_points.len() < 3 {
return Err(CvError::invalid_parameter(
"points",
"need at least 3 point pairs",
));
}
if src_points.len() != dst_points.len() {
return Err(CvError::invalid_parameter(
"points",
"source and destination must have same length",
));
}
let n = src_points.len();
if n == 3 {
return solve_affine_3points(src_points, dst_points);
}
let mut ata = [[0.0; 6]; 6];
let mut atb = [0.0; 6];
for i in 0..n {
let (sx, sy) = src_points[i];
let (dx, dy) = dst_points[i];
let row_x = [sx, sy, 1.0, 0.0, 0.0, 0.0];
let row_y = [0.0, 0.0, 0.0, sx, sy, 1.0];
for j in 0..6 {
for k in 0..6 {
ata[j][k] += row_x[j] * row_x[k] + row_y[j] * row_y[k];
}
atb[j] += row_x[j] * dx + row_y[j] * dy;
}
}
let x = solve_6x6(&ata, &atb)?;
Ok(AffineTransform::new(x[0], x[1], x[2], x[3], x[4], x[5]))
}
fn solve_affine_3points(src: &[(f64, f64)], dst: &[(f64, f64)]) -> CvResult<AffineTransform> {
let (s0x, s0y) = src[0];
let (s1x, s1y) = src[1];
let (s2x, s2y) = src[2];
let (d0x, d0y) = dst[0];
let (d1x, d1y) = dst[1];
let (d2x, d2y) = dst[2];
let det = (s0x - s2x) * (s1y - s2y) - (s1x - s2x) * (s0y - s2y);
if det.abs() < f64::EPSILON {
return Err(CvError::transform_error("Points are collinear"));
}
let inv_det = 1.0 / det;
let a = ((d0x - d2x) * (s1y - s2y) - (d1x - d2x) * (s0y - s2y)) * inv_det;
let b = ((s0x - s2x) * (d1x - d2x) - (s1x - s2x) * (d0x - d2x)) * inv_det;
let tx = d0x - a * s0x - b * s0y;
let c = ((d0y - d2y) * (s1y - s2y) - (d1y - d2y) * (s0y - s2y)) * inv_det;
let d = ((s0x - s2x) * (d1y - d2y) - (s1x - s2x) * (d0y - d2y)) * inv_det;
let ty = d0y - c * s0x - d * s0y;
Ok(AffineTransform::new(a, b, tx, c, d, ty))
}
fn solve_6x6(a: &[[f64; 6]; 6], b: &[f64; 6]) -> CvResult<[f64; 6]> {
let mut aug = [[0.0; 7]; 6];
for i in 0..6 {
for j in 0..6 {
aug[i][j] = a[i][j];
}
aug[i][6] = b[i];
}
for i in 0..6 {
let mut max_row = i;
for k in (i + 1)..6 {
if aug[k][i].abs() > aug[max_row][i].abs() {
max_row = k;
}
}
aug.swap(i, max_row);
if aug[i][i].abs() < f64::EPSILON {
return Err(CvError::transform_error("Matrix is singular"));
}
for k in (i + 1)..6 {
let factor = aug[k][i] / aug[i][i];
for j in i..7 {
aug[k][j] -= factor * aug[i][j];
}
}
}
let mut x = [0.0; 6];
for i in (0..6).rev() {
x[i] = aug[i][6];
for j in (i + 1)..6 {
x[i] -= aug[i][j] * x[j];
}
x[i] /= aug[i][i];
}
Ok(x)
}
pub fn warp_affine_image(
src: &[u8],
src_w: usize,
src_h: usize,
channels: usize,
m: [[f64; 3]; 2],
dst_w: usize,
dst_h: usize,
) -> CvResult<Vec<u8>> {
if src_w == 0 || src_h == 0 {
return Err(CvError::invalid_dimensions(src_w as u32, src_h as u32));
}
if dst_w == 0 || dst_h == 0 {
return Err(CvError::invalid_dimensions(dst_w as u32, dst_h as u32));
}
if channels == 0 {
return Err(CvError::invalid_parameter("channels", "must be > 0"));
}
let expected = src_w * src_h * channels;
if src.len() < expected {
return Err(CvError::insufficient_data(expected, src.len()));
}
let mut out = vec![0u8; dst_w * dst_h * channels];
let sample = |row: i32, col: i32, ch: usize| -> f32 {
if row < 0 || row >= src_h as i32 || col < 0 || col >= src_w as i32 {
return 0.0;
}
src[(row as usize * src_w + col as usize) * channels + ch] as f32
};
for dy in 0..dst_h {
for dx in 0..dst_w {
let sx = m[0][0] * dx as f64 + m[0][1] * dy as f64 + m[0][2];
let sy = m[1][0] * dx as f64 + m[1][1] * dy as f64 + m[1][2];
let x0 = sx.floor() as i32;
let y0 = sy.floor() as i32;
let fx = (sx - sx.floor()) as f32;
let fy = (sy - sy.floor()) as f32;
let dst_off = (dy * dst_w + dx) * channels;
for ch in 0..channels {
let v00 = sample(y0, x0, ch);
let v10 = sample(y0, x0 + 1, ch);
let v01 = sample(y0 + 1, x0, ch);
let v11 = sample(y0 + 1, x0 + 1, ch);
let val = v00 * (1.0 - fx) * (1.0 - fy)
+ v10 * fx * (1.0 - fy)
+ v01 * (1.0 - fx) * fy
+ v11 * fx * fy;
out[dst_off + ch] = val.clamp(0.0, 255.0) as u8;
}
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_identity() {
let transform = AffineTransform::identity();
let (x, y) = transform.transform_point(10.0, 20.0);
assert!((x - 10.0).abs() < 0.001);
assert!((y - 20.0).abs() < 0.001);
}
#[test]
fn test_translation() {
let transform = AffineTransform::translation(10.0, 20.0);
let (x, y) = transform.transform_point(5.0, 5.0);
assert!((x - 15.0).abs() < 0.001);
assert!((y - 25.0).abs() < 0.001);
}
#[test]
fn test_scaling() {
let transform = AffineTransform::scaling(2.0, 3.0);
let (x, y) = transform.transform_point(10.0, 10.0);
assert!((x - 20.0).abs() < 0.001);
assert!((y - 30.0).abs() < 0.001);
}
#[test]
fn test_rotation() {
let transform = AffineTransform::rotation(PI / 2.0);
let (x, y) = transform.transform_point(1.0, 0.0);
assert!(x.abs() < 0.001);
assert!((y - 1.0).abs() < 0.001);
}
#[test]
fn test_rotation_around() {
let transform = AffineTransform::rotation_around(PI, 50.0, 50.0);
let (x, y) = transform.transform_point(100.0, 50.0);
assert!((x - 0.0).abs() < 0.001);
assert!((y - 50.0).abs() < 0.001);
}
#[test]
fn test_compose() {
let t1 = AffineTransform::translation(10.0, 0.0);
let t2 = AffineTransform::scaling(2.0, 2.0);
let composed = t1.then(&t2);
let (x, y) = composed.transform_point(5.0, 5.0);
assert!((x - 30.0).abs() < 0.001);
assert!((y - 10.0).abs() < 0.001);
}
#[test]
fn test_chain() {
let transform = AffineTransform::identity()
.scale(2.0, 2.0)
.translate(10.0, 10.0);
let (x, y) = transform.transform_point(5.0, 5.0);
assert!((x - 20.0).abs() < 0.001);
assert!((y - 20.0).abs() < 0.001);
}
#[test]
fn test_determinant() {
let transform = AffineTransform::scaling(2.0, 3.0);
assert!((transform.determinant() - 6.0).abs() < 0.001);
}
#[test]
fn test_inverse() {
let transform = AffineTransform::translation(10.0, 20.0).scale(2.0, 2.0);
let inverse = transform.inverse().expect("inverse should succeed");
let (x, y) = transform.transform_point(5.0, 5.0);
let (rx, ry) = inverse.transform_point(x, y);
assert!((rx - 5.0).abs() < 0.001);
assert!((ry - 5.0).abs() < 0.001);
}
#[test]
fn test_singular_not_invertible() {
let transform = AffineTransform::new(1.0, 2.0, 0.0, 2.0, 4.0, 0.0);
assert!(!transform.is_invertible());
assert!(transform.inverse().is_none());
}
#[test]
fn test_transform_image() {
let src = vec![100u8; 100];
let transform = AffineTransform::identity();
let result = transform_image(&src, 10, 10, &transform, 10, 10, Interpolation::Bilinear)
.expect("transform_image should succeed");
assert_eq!(result.len(), 100);
}
#[test]
fn test_estimate_affine() {
let src_points = vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)];
let dst_points = vec![(0.0, 0.0), (2.0, 0.0), (0.0, 2.0)];
let transform =
estimate_affine(&src_points, &dst_points).expect("estimate_affine should succeed");
let (x, y) = transform.transform_point(1.0, 1.0);
assert!((x - 2.0).abs() < 0.001);
assert!((y - 2.0).abs() < 0.001);
}
}