Rust图像处理第14节-图片缩放与斜切畸变:齐次坐标 + 共享仿射变换

🦀 Rust + WASM 实战系列 第 14 篇 阅读时间:约 8 分钟 | 实战可运行

📌 写在前面

上一篇用 Matrix2 做了任意角度旋转。Matrix2 够用,但有个限制:没法表示平移。

这一篇引入 齐次坐标 Matrix3(3×3) ------一个能统一表示缩放 / 斜切 / 旋转 / 平移的"超级矩阵"。后面 16 篇(PCA、回归、3D 投影......)都会用到它。

这一篇 = 学一个工具,干两件事:缩放 + 斜切都靠 Matrix3。


🚀 TL;DR

操作 矩阵 新尺寸
缩放 sx 0 tx 0 sy ty 0 0 1 \begin{bmatrix} s_x & 0 & t_x \\ 0 & s_y & t_y \\ 0 & 0 & 1 \end{bmatrix} sx000sy0txty1 w′=wsx, h′=hsy w' = w s_x,\ h' = h s_y w′=wsx, h′=hsy
斜切 1 kx tx ky 1 ty 0 0 1 \begin{bmatrix} 1 & k_x & t_x \\ k_y & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} 1ky0kx10txty1 w′=w+∥kx∥hw' = w + \|k_x\| h w′=w+∥kx∥h

关键认知 :两件事用的是同一个 affine_apply() 函数 ------只是构造的矩阵不同。这就是 Matrix3 的威力


📖 目录

  1. [齐次坐标:为什么需要 Matrix3?](#齐次坐标:为什么需要 Matrix3? "#%E4%B8%80%E9%BD%90%E6%AC%A1%E5%9D%90%E6%A0%87%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-matrix3")
  2. 缩放:最简单的非平凡变换
  3. 斜切:把矩形扭成平行四边形
  4. [关键代码:共享 affine_apply](#关键代码:共享 affine_apply "#%E5%9B%9B%E5%85%B3%E9%94%AE%E4%BB%A3%E7%A0%81%E5%85%B1%E4%BA%AB-affine_apply")
  5. 前端效果展示
  6. [为什么 "1 个矩阵" 比 "2 个 if" 更强?](#为什么 "1 个矩阵" 比 "2 个 if" 更强? "#%E5%85%AD%E4%B8%BA%E4%BB%80%E4%B9%88-1-%E4%B8%AA%E7%9F%A9%E9%98%B5-%E6%AF%94-2-%E4%B8%AA-if-%E6%9B%B4%E5%BC%BA")
  7. 踩坑提醒
  8. 下篇预告

一、齐次坐标:为什么需要 Matrix3?

2D 变换的三种"形状"

变换 数学形式 需要 2×2 还是 3×3?
旋转 x′=xcos⁡−ysin⁡x' = x\cos - y\sin x′=xcos−ysin 2×2 够(纯线性)
缩放 x′=sx⋅x x' = s_x \cdot x x′=sx⋅x 2×2 够(纯线性)
斜切 x′=x+kx⋅y x' = x + k_x \cdot y x′=x+kx⋅y 2×2 够(纯线性)
平移 x′=x+tx x' = x + t_x x′=x+tx 2×2 不够

平移为什么搞不定 2×2?

2×2 矩阵乘法永远是"线性变换"------原点 (0,0)(0, 0) (0,0) 永远映射到原点。无法表达"加常数"

解决方案:齐次坐标(Homogeneous Coordinates)

把 2D 点升维成 3D: (x,y)→(x,y,1)(x, y) \rightarrow (x, y, 1) (x,y)→(x,y,1)。

然后用 3×3 矩阵:
a b tx c d ty 0 0 1 x y 1 = ax+by+tx cx+dy+ty 1 = x′ y′ 1 \begin{bmatrix} a & b & t_x \\ c & d & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} a x + b y + t_x \\ c x + d y + t_y \\ 1 \end{bmatrix} = \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} ac0bd0txty1 xy1 = ax+by+txcx+dy+ty1 = x′y′1

第三行的 [0 0 1] 是"占位符",保证 (x,y,1)(x, y, 1) (x,y,1) 永远映射回 (x′,y′,1)(x', y', 1) (x′,y′,1)。

一张表背齐所有变换

S = sx 0 0 0 sy 0 0 0 1 R = cos⁡θ −sin⁡θ 0 sin⁡θ cos⁡θ 0 0 0 1 H = 1 kx 0 ky 1 0 0 0 1 T = 1 0 tx 0 1 ty 0 0 1 \begin{aligned} S &= \begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix} & R &= \begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix} \\ H &= \begin{bmatrix} 1 & k_x & 0 \\ k_y & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} & T &= \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} \end{aligned} SH= sx000sy0001 = 1ky0kx10001 RT= cosθsinθ0−sinθcosθ0001 = 100010txty1

任何"线性 + 平移"的组合都可以写成一个 Matrix3


二、缩放:最简单的非平凡变换

缩放矩阵

围绕图片中心缩放,结果画布大小 = sxw×syh s_x w \times s_y h sxw×syh:
Mscale= sx 0 w′−sxw 2 0 sy h′−syh 2 0 0 1 M_{\text{scale}} = \begin{bmatrix} s_x & 0 & \frac{w' - s_x w}{2} \\ 0 & s_y & \frac{h' - s_y h}{2} \\ 0 & 0 & 1 \end{bmatrix} Mscale= sx000sy02w′−sxw2h′−syh1

平移项 w′−sxw 2 \frac{w' - s_x w}{2} 2w′−sxw 是为了让"中心"映射到"中心"。

💡 如果不写平移项(设为 0),缩放会围绕左上角进行------也是合法的,但视觉上"图像会跑到画布外"。

反向映射

对每个 dst 像素 (x′,y′)(x', y') (x′,y′),用 M−1M^{-1} M−1 求出 src:
M−1= 1/sx 0 − w′−sxw 2sx 0 1/sy − h′−syh 2sy 0 0 1 M^{-1} = \begin{bmatrix} 1/s_x & 0 & -\frac{w' - s_x w}{2 s_x} \\ 0 & 1/s_y & -\frac{h' - s_y h}{2 s_y} \\ 0 & 0 & 1 \end{bmatrix} M−1= 1/sx0001/sy0−2sxw′−sxw−2syh′−syh1

代码

rust 复制代码
#[wasm_bindgen]
pub fn scale(pixels: &[u8], width: u32, height: u32, sx: f64, sy: f64) -> Vec<u8> {
    let w = width as usize;
    let h = height as usize;
    let new_w = ((w as f64) * sx.abs()).round() as usize;
    let new_h = ((h as f64) * sy.abs()).round() as usize;

    let tx = ((new_w as f64) - sx * (w as f64)) / 2.0;
    let ty = ((new_h as f64) - sy * (h as f64)) / 2.0;
    let m = Matrix3::new(
        sx,  0.0, tx,
        0.0, sy,  ty,
        0.0, 0.0, 1.0,
    );

    affine_apply(pixels, w, h, new_w, new_h, m)
}

affine_apply()scale 和 shear 共用的实现------下面 §四会讲。

几个常见的缩放场景

scaleX scaleY 效果
2.0 2.0 整体放大 2 倍
0.5 0.5 整体缩小一半
2.0 1.0 横向拉长(变成宽屏)
1.0 0.5 纵向压扁
0.5 2.0 横向压缩 + 纵向拉长(类似哈哈镜)

三、斜切:把矩形扭成平行四边形

斜切的直觉

ini 复制代码
原图(矩形):        斜切 kx=0.5 后(平行四边形):
┌──┬──┬──┐           ┌──┬──┬──┐
│  │  │  │           │ ╲│ ╲│ ╲│
├──┼──┼──┤           ├──┼──┼──┤
│  │  │  │     →     │  ╲│  ╲│  ╲│
├──┼──┼──┤           ├──┼──┼──┤
│  │  │  │           │  ╲│   ╲│   ╲│
└──┴──┴──┘           └──┴──┴──┴──┘
                     ↑
                  每向下 1 像素,x 往右偏 0.5

斜切矩阵

Mshear= 1 kx tx ky 1 ty 0 0 1 M_{\text{shear}} = \begin{bmatrix} 1 & k_x & t_x \\ k_y & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} Mshear= 1ky0kx10txty1

其中:

  • kx k_x kx:水平斜切(每向下 1 像素,x 偏移 kx k_x kx)
  • ky k_y ky:垂直斜切(每向右 1 像素,y 偏移 ky k_y ky)
  • tx,ty t_x, t_y tx,ty:当斜切系数为负时,需要平移回画布

为什么需要平移?

scss 复制代码
kx = 0.3 时:                  kx = -0.3 时:
src(0, 0) → dst(0, 0)        src(0, 0) → dst(0, 0)
src(0, h-1) → dst(3*(h-1), h-1)  src(0, h-1) → dst(-3*(h-1), h-1)  ← 出画布!
                                 需要 tx = 0.3 * (h-1) 平移回来

新画布尺寸

w′ =w+∣kx∣⋅h h′ =h+∣ky∣⋅w \begin{aligned} w' &= w + |k_x| \cdot h \\ h' &= h + |k_y| \cdot w \end{aligned} w′h′=w+∣kx∣⋅h=h+∣ky∣⋅w

直觉:水平斜切 kx k_x kx 会让图像"向右下伸长 ∣kx∣⋅h |k_x| \cdot h ∣kx∣⋅h 像素",所以宽度变 w+∣kx∣⋅h w + |k_x| \cdot h w+∣kx∣⋅h。

代码

rust 复制代码
#[wasm_bindgen]
pub fn shear(pixels: &[u8], width: u32, height: u32, kx: f64, ky: f64) -> Vec<u8> {
    let w = width as usize;
    let h = height as usize;

    let new_w = ((w as f64) + kx.abs() * ((h as f64) - 1.0)).round() as usize;
    let new_h = ((h as f64) + ky.abs() * ((w as f64) - 1.0)).round() as usize;

    // 负斜切时,平移回画布
    let offset_x = if kx < 0.0 { -kx * ((h as f64) - 1.0) } else { 0.0 };
    let offset_y = if ky < 0.0 { -ky * ((w as f64) - 1.0) } else { 0.0 };

    let m = Matrix3::new(
        1.0, kx,  offset_x,
        ky,  1.0, offset_y,
        0.0, 0.0, 1.0,
    );

    affine_apply(pixels, w, h, new_w, new_h, m)
}

四、关键代码:共享 affine_apply

最大亮点 :scale 和 shear 都调用同一个函数 affine_apply()Matrix3 让"代码复用"达到了极致

rust 复制代码
/// 通用 3×3 仿射变换:反向映射 + 双线性插值 + 自动补白边
pub(super) fn affine_apply(
    pixels: &[u8],
    w: usize,
    h: usize,
    new_w: usize,
    new_h: usize,
    m: Matrix3<f64>,
) -> Vec<u8> {
    let m_inv = m.try_inverse()
        .expect("affine matrix must be invertible");
    let pixel_len = new_w * new_h * 4;
    let mut result = vec![0u8; pixel_len + 8];

    for y in 0..new_h {
        for x in 0..new_w {
            // 反向映射:dst → src
            let v = m_inv * Vector3::new(x as f64, y as f64, 1.0);
            let src_x = v.x;
            let src_y = v.y;

            let dst_idx = (y * new_w + x) * 4;

            // 越界 → 填白色
            if src_x < 0.0 || src_x >= (w as f64) - 1.0
                || src_y < 0.0 || src_y >= (h as f64) - 1.0
            {
                result[dst_idx]     = 255;
                result[dst_idx + 1] = 255;
                result[dst_idx + 2] = 255;
                result[dst_idx + 3] = 255;
                continue;
            }

            sample_bilinear(pixels, w, h, src_x, src_y, &mut result, dst_idx);
        }
    }

    // 末尾 8 字节写新尺寸
    result[pixel_len..pixel_len + 4]
        .copy_from_slice(&(new_w as u32).to_le_bytes());
    result[pixel_len + 4..pixel_len + 8]
        .copy_from_slice(&(new_h as u32).to_le_bytes());

    result
}

scale 和 shear 都只是"喂矩阵"

rust 复制代码
// scale 喂一个缩放矩阵
scale(pixels, w, h, 2.0, 2.0)
  → 构造 M = [2 0 ?; 0 2 ?; 0 0 1]
  → affine_apply(pixels, w, h, 2w, 2h, M)

// shear 喂一个斜切矩阵
shear(pixels, w, h, 0.5, 0.0)
  → 构造 M = [1 0.5 ?; 0 1 ?; 0 0 1]
  → affine_apply(pixels, w, h, w+0.5h, h, M)

// 如果以后想加"先缩放再斜切"?
// → 喂 M = S × H(矩阵乘法),scale 函数都不用动

这就是"组件化"的胜利 :变换是数据(矩阵),不是控制流(if/else)。要加新变换,只需要构造新矩阵,核心循环一行都不用改


五、前端效果展示


六、为什么"齐次坐标"

来对比一下从第12节到14的写法:

第十二节:每个变换单独写(90° 旋转就是这样)

rust 复制代码
// 翻
if direction == "horizontal" { src_x = w - 1 - x }
// 转
let (src_x, src_y) = if direction == "ccw" { (y, w - 1 - x) } else { (new_h - 1 - y, x) }

第十三节:用 Matrix2 但要单独写平移(任意角度旋转)

rust 复制代码
let r = Matrix2::new(c, -s, s, c);
// 中心化偏移...
let src_x = r_inv[(0, 0)] * (x - nw2) + r_inv[(0, 1)] * (y - nh2) + w2;
let src_y = r_inv[(1, 0)] * (x - nw2) + r_inv[(1, 1)] * (y - nh2) + h2;

问题:Matrix2 没法内置平移,中心化要手算坐标。

本节:Matrix3 一统天下(这一篇)✅

rust 复制代码
let m = Matrix3::new(sx, 0.0, tx, 0.0, sy, ty, 0.0, 0.0, 1.0);
// 平移已经在矩阵里了!
let src = m.try_inverse().unwrap() * Vector3::new(x as f64, y as f64, 1.0);

优势

  • 平移、缩放、斜切统一用同一个矩阵类型
  • try_inverse() 自动求逆,不用手算
  • 加新变换只需要构造矩阵,核心循环 0 改动

性能对比

维度 if/else Matrix2 Matrix3
速度 最快 快(SIMD 优化)
可扩展 ❌ 差 一般 ✅ 极好
支持平移 手动 手动 ✅ 内置
代码复用 ❌ 差 一般 ✅ 极好

实际上 nalgebra 的 Matrix 运算会被 LLVM 优化到和手写循环一样的机器码,速度上几乎无差


七、注意问题

1. try_inverse() 可能失败

rust 复制代码
// 不可逆的矩阵(比如缩放系数 = 0)会让 try_inverse 返回 None
let m_inv = m.try_inverse().expect("matrix not invertible");

何时不可逆 ?当矩阵的行列式 = 0 时(例如 sx=sy=0 s_x = s_y = 0 sx=sy=0,或所有元素都为 0)。一般正常使用不会触发。

2. 缩放系数为 0 时 new_w = 0

rust 复制代码
let new_w = ((w as f64) * sx.abs()).round() as usize;

scaleX = 0 会让 new_w = 0,导致后续 vec![0u8; 0 + 8] 出错。前端 UI 限制滑块范围到 [0.1, 3] 就规避了。

3. 边界判断要用 >= w - 1.0 而不是 >= w

双线性插值要取 (x0, x0 + 1),所以 src_x = w - 1 仍合法(x0 = w-1, x1 = w-1 clamp 一下)。和上一篇 rotate 一样的坑

4. 斜切系数很大时图像严重变形

rust 复制代码
let kx: f64 = 2.0;  // 每向下 1 像素,x 偏 2 像素

kx = 2 + h = 100 → new_w = w + 200,画布宽度翻倍多。斜切系数最好限制在 [-1, 1]


八、下篇预告

任务 15:鱼眼广角畸变------模拟鱼眼镜头的球面扭曲。

旋转、缩放、斜切都是线性变换(Matrix3 表达):
x′=ax+by+tx,y′=cx+dy+tyx' = ax + by + t_x, \quad y' = cx + dy + t_y x′=ax+by+tx,y′=cx+dy+ty

鱼眼是非线性变换
r = x2+y2 θ =arctan⁡(r) x′ =θ⋅xr,y′=θ⋅yr \begin{aligned} r &= \sqrt{x^2 + y^2} \\ \theta &= \arctan(r) \\ x' &= \theta \cdot \frac{x}{r}, \quad y' = \theta \cdot \frac{y}{r} \end{aligned} rθx′=x2+y2 =arctan(r)=θ⋅rx,y′=θ⋅ry

关键差异:鱼眼没有矩阵形式 ------但仍然用反向映射 + 插值这套打法。

这一篇会把前面的"几何变换三件套"扩展到非线性领域,为下一部分的"分形画板"做铺垫。


🎁 写在最后

这一篇最大的收获不是缩放或斜切,而是:

认知
✅ 齐次坐标 Matrix3 = 线性变换 + 平移的"瑞士军刀"
✅ 一个affine_apply 函数 + 不同矩阵 = 不同变换
✅ "变换是数据(矩阵),不是控制流(if/else)"
任务 矩阵 中心 越界处理
12 翻转 不用 N/A 不会发生
13 旋转 Matrix2 图片中心 填白边
14 缩放/斜切 Matrix3 图片中心 填白边
15 鱼眼 非线性 图片中心 填白边

Matrix3 的优势会在下下篇(任务 18:分形缩放 + 拖拽)爆发------那时你做的不是"画一次",而是"反复用同一个 Matrix3 渲染",性能优势会真正显现。


📦 项目地址pixel-math-wasm 🦀 Rust + WebAssembly 实战系列


🏷️ 标签#Rust #WebAssembly #图像处理 #几何变换 #缩放 #斜切 #齐次坐标 #Matrix3 #nalgebra

相关推荐
梦帮科技3 小时前
GRAVIS v4.0:基于Web的极速套利架构设计与实时数据流实现
前端·人工智能·rust·自动化·区块链·智能合约·数字货币
花褪残红青杏小11 小时前
Rust图像处理第13节-任意角度旋转:旋转矩阵 + 双线性插值 + 自动补白边
rust·webassembly·图形学
SmalBox13 小时前
【节点】[IrisUVLocation节点]原理解析与实际应用
unity3d·游戏开发·图形学
小杍随笔15 小时前
【Rust + Argon2id 生产级密码管理模块设计与实现】
网络·后端·算法·rust
梦帮科技15 小时前
GRAVIS v5.5:向硬核桌面端进化与云端多节点容灾部署实践
windows·架构·rust·自动化·区块链·智能合约·数字货币
梦醒沉醉16 小时前
2、Rust程序设计语言——常见编程概念
rust
程序员爱钓鱼17 小时前
Rust 开发环境安装(Windows|macOS|Linux)
后端·rust
花褪残红青杏小1 天前
Rust图像处理第12节-图片翻转与旋转:坐标重映射几何变换
rust·webassembly·图形学
CodeStats1 天前
【编程语言】深度梳理C/C++、Java、Python、Go、Rust的区别
java·linux·c语言·c++·python·rust·go