Rust图像处理第12节-图片翻转与旋转:坐标重映射几何变换

图片翻转与旋转:第一组"坐标重映射"几何变换

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

📌 写在前面

前两部分做了 11 个图像处理算法,但都不改变像素的位置------只是改颜色。

从这一篇开始,进入"几何变换" ------像素要搬家

翻转和旋转是最简单的几何变换:只改坐标,不改颜色值。原理一句话就能讲完:

找到"目标像素"应该从"原图哪个坐标"取颜色。

没有卷积核,没有梯度,纯坐标重映射 ------从这节之后,就要使用线代做变换了。


🚀 TL;DR

操作 坐标映射 尺寸变化
左右翻转 src_x = w - 1 - x 不变
上下翻转 src_y = h - 1 - y 不变
顺时针 90° dst(x,y) ← src(h-1-y, x) 宽高互换
逆时针 90° dst(x,y) ← src(y, w-1-x) 宽高互换

全部代码加起来 < 60 行。这一篇就是给后面的"任意角度旋转 / 缩放 / 畸变"热身的。


📖 目录

  1. 什么是几何变换?
  2. 翻转:最简单的坐标镜像
  3. [旋转 90°:宽高互换](#旋转 90°:宽高互换 "#%E4%B8%89%E6%97%8B%E8%BD%AC-90%E5%AE%BD%E9%AB%98%E4%BA%92%E6%8D%A2")
  4. 前端效果展示
  5. 坐标映射的几种思路
  6. [进阶:180° 旋转的 3 种写法](#进阶:180° 旋转的 3 种写法 "#%E5%85%AD%E8%BF%9B%E9%98%B6180-%E6%97%8B%E8%BD%AC%E7%9A%84-3-%E7%A7%8D%E5%86%99%E6%B3%95")
  7. 踩坑提醒
  8. 下篇预告

一、什么是几何变换?

几何变换 = 改变像素的"位置",不改变像素的"颜色"

之前我们做的:

scss 复制代码
模糊 / Sobel / 浮雕 / RGB 偏移
└─ 输出 (x, y) 的颜色,可能来自原图多个邻居的混合
   → 颜色变了,但像素总数和位置感不变

现在要做的:

arduino 复制代码
翻转 / 旋转 / 缩放 / 畸变
└─ 输出 (x, y) 的颜色,从原图某个特定坐标取
   → 颜色值不变,但像素的"住址"变了

核心公式(所有几何变换都长这样):
result(x,y)=original(fx(x,y),fy(x,y))\text{result}(x, y) = \text{original}(f_x(x, y), f_y(x, y)) result(x,y)=original(fx(x,y),fy(x,y))

其中 fx,fy f_x, f_y fx,fy 就是变换函数。


二、翻转:最简单的坐标镜像

左右翻转(horizontal flip)

沿垂直中轴镜像:

css 复制代码
原图:         左右翻转后:
┌──┬──┬──┬──┐  ┌──┬──┬──┬──┐
│ A│ B│ C│ D│  │ D│ C│ B│ A│
├──┼──┼──┼──┤  ├──┼──┼──┼──┤
│ E│ F│ G│ H│  │ H│ G│ F│ E│
└──┴──┴──┴──┘  └──┴──┴──┴──┘

坐标映射
srcx=w−1−x,srcy=y \text{src}_x = w - 1 - x, \quad \text{src}_y = y srcx=w−1−x,srcy=y

直觉:第 0 列的内容跑到第 w-1 列,第 w-1 列的内容跑到第 0 列。

上下翻转(vertical flip)

沿水平中轴镜像:

css 复制代码
原图:         上下翻转后:
┌──┬──┐       ┌──┬──┐
│ A│ B│       │ E│ F│
├──┼──┤  →    ├──┼──┤
│ E│ F│       │ A│ B│
└──┴──┘       └──┴──┘

坐标映射
srcx=x,srcy=h−1−y \text{src}_x = x, \quad \text{src}_y = h - 1 - y srcx=x,srcy=h−1−y

⚠️ 命名约定horizontal flip = 左右翻转(沿水平轴方向翻转的视觉结果),vertical flip = 上下翻转(沿垂直轴方向翻转的视觉结果)。这是图像处理领域的通用约定。

代码(Rust)

rust 复制代码
let flip_x = direction == "horizontal";
let flip_y = direction == "vertical";

for y in 0..h {
    for x in 0..w {
        let src_x = if flip_x { w - 1 - x } else { x };
        let src_y = if flip_y { h - 1 - y } else { y };
        let src = (src_y * w + src_x) * 4;
        let dst = (y * w + x) * 4;

        // 拷贝 RGBA 4 字节
        result[dst]     = pixels[src];
        result[dst + 1] = pixels[src + 1];
        result[dst + 2] = pixels[src + 2];
        result[dst + 3] = pixels[src + 3];
    }
}

总共不到 20 行


三、旋转 90°:宽高互换

旋转 90° 比翻转复杂一点,因为输出尺寸变了 ------w × h 变成 h × w

顺时针 90°

css 复制代码
原图 (3×2):       顺时针 90° 后 (2×3):
┌──┬──┬──┐         ┌──┬──┐
│ A│ B│ C│         │ D│ A│
├──┼──┼──┤   →     ├──┼──┤
│ D│ E│ F│         │ E│ B│
└──┴──┴──┘         ├──┼──┤
                   │ F│ C│
                   └──┴──┘

坐标映射(关键!):
src(x,y)=dst(h−1−y,x)\text{src}(x, y) = \text{dst}(h - 1 - y, x) src(x,y)=dst(h−1−y,x)

翻译:原图第 (x, y) 个像素,旋转后出现在新图的第 (h-1-y, x) 个位置。

逆时针 90°

坐标映射
src(x,y)=dst(y,w−1−x)\text{src}(x, y) = \text{dst}(y, w - 1 - x) src(x,y)=dst(y,w−1−x)

怎么记?画一张图

把原图左上角 A 标出来,跟着它走:

操作 A 原坐标 A 旋转后坐标
顺时针 90° (0, 0) (0,w-1) → (0, h-1) 在新图
逆时针 90° (0, 0) (h-1, 0)

小技巧:A 一定跑到新图的对角位置(顺时针到左上角 / 逆时针到左下角,对应不同的对角线)。

代码(Rust)

rust 复制代码
#[wasm_bindgen]
pub fn rotate90(
    pixels: &[u8],
    width: u32,
    height: u32,
    direction: &str,
) -> Vec<u8> {
    let w = width as usize;
    let h = height as usize;
    let new_w = h; // 旋转后:宽 = 原来的高
    let new_h = w; // 旋转后:高 = 原来的宽
    let pixel_len = new_w * new_h * 4;
    let mut result = vec![0u8; pixel_len + 8]; // 8 字节留给新宽高

    for y in 0..new_h {
        for x in 0..new_w {
            let (src_x, src_y) = match direction {
                "ccw" => (y, w - 1 - x),       // 逆时针
                _     => (new_h - 1 - y, x),   // 顺时针(默认)
            };
            let src = (src_y * w + src_x) * 4;
            let dst = (y * new_w + x) * 4;

            result[dst]     = pixels[src];
            result[dst + 1] = pixels[src + 1];
            result[dst + 2] = pixels[src + 2];
            result[dst + 3] = pixels[src + 3];
        }
    }

    // 把新宽高打包到末尾 8 字节(小端 u32)
    let nw = new_w as u32;
    let nh = new_h as u32;
    result[pixel_len..pixel_len + 4].copy_from_slice(&nw.to_le_bytes());
    result[pixel_len + 4..pixel_len + 8].copy_from_slice(&nh.to_le_bytes());

    result
}

关键点 :旋转后宽高互换,所以末尾要打包 8 字节新尺寸给前端解码。


四、前端效果展示

五、坐标映射的几种思路

几何变换有两种思考方向,对应不同的代码结构:

思路 A:前向映射(source-driven)

对每个原图像素,算它"应该去目标图的哪里"。

rust 复制代码
for src_y in 0..h {
    for src_x in 0..w {
        let dst_x = ...;  // 算出 src 映射到哪个 dst
        let dst_y = ...;
        result[dst] = pixels[src];
    }
}

优点 :每个原像素只处理一次,无重复。 缺点 :目标图可能有空洞 (多个原像素映射到同一个 dst)或漏点(某些 dst 没有原像素映射过来)。

思路 B:反向映射(destination-driven)✅ 本篇采用

对每个目标像素,反推"应该从原图哪个位置取"。

rust 复制代码
for dst_y in 0..new_h {
    for dst_x in 0..new_w {
        let src_x = ...;  // 算出 dst 应该从哪个 src 取
        let src_y = ...;
        result[dst] = pixels[src];
    }
}

优点 :每个目标像素保证有值 ,无空洞。 缺点:原图的某些像素可能被多次采样(但 90° 旋转这种整点变换不会重复采样)。

💡 几乎所有实际图像变换都用反向映射------简单、不会空洞。后面学任意角度旋转、双线性插值时你会反复看到它。


六、进阶:180° 旋转的 3 种写法

虽然这一篇没实现 180° 旋转,但作为思考题,给出 3 种等价写法:

写法 1:左右 + 上下翻转组合(最直观)

rust 复制代码
// 180° = 水平翻转 + 垂直翻转
let temp = flip(pixels, w, h, "horizontal");
let result = flip(temp, w, h, "vertical");

写法 2:单次循环,src_x 和 src_y 都翻转

rust 复制代码
for y in 0..h {
    for x in 0..w {
        let src = ((h - 1 - y) * w + (w - 1 - x)) * 4;
        let dst = (y * w + x) * 4;
        // copy RGBA
    }
}

写法 3:旋转 90° 两次(最浪费)

rust 复制代码
let once = rotate90(pixels, w, h, "cw");  // w×h → h×w
let twice = rotate90(once, h, w, "cw");   // h×w → w×h

三种结果完全一样。写法 1 最直观(两次函数调用),写法 2 最高效(一次循环),写法 3 是绕远路(但能加深对旋转的理解)。


七、踩坑提醒

1. 旋转 90° 后宽高会互换

90° 旋转后 w × h 变成 h × w很多人会忘记这点导致 buffer 算错。我们的解决方案是把新宽高编码进返回数组末尾。

2. 别忘了乘以 4(RGBA 4 字节)

rust 复制代码
let idx = (y * w + x) * 4;  // ← *4 千万别忘

每个像素是 4 字节(R/G/B/A),坐标索引要乘以 4 才到字节位置。前面 11 篇都讲过这一点,但实际写代码时仍会忘。

3. Alpha 通道也要拷贝

rust 复制代码
result[dst + 3] = pixels[src + 3];  // 别漏了 alpha

漏了 alpha 通道,半透明 PNG 会突然变成不透明。

4. Rust 中 usizeu32 的转换

rust 复制代码
let w = width as usize;  // u32 → usize:要做减法 / 索引必须 usize
let new_w = w as u32;    // usize → u32:写回 WASM 时要转回来

WASM 边界统一用 u32(wasm-bindgen 不支持 usize 的跨边界传递),Rust 内部循环和索引用 usize。转换别忘了。

5. 连续操作的"当前图"概念

前端实现里我们维护了一个 currentPixels 状态------每次操作基于"当前图",所以可以连续点击 4 次顺时针回到原图。这是 UX 设计的关键。


八、下篇预告

任务 13:任意角度旋转(比如 30°、45°)。

90° 旋转只需要整点映射,任意角度就要面对"小数坐标"和"插值"问题了:

scss 复制代码
旋转 30° 时,原图 (0, 0) 旋转后落在 (0.866, 0.5)
反向映射时,dst (0, 0) 要从 src (-0.866, -0.5) 取颜色
→ 坐标是负数、是小数
→ 越界怎么办?
→ 小数坐标的颜色怎么算?(最近邻插值 / 双线性插值)

这些就是下篇要解决的问题 。你会发现:90° 旋转只是个特例,通用变换需要"矩阵 × 插值"两件武器


🎁 写在最后

第三部分开篇 。从这篇开始,你做的每个算法都会和坐标 / 矩阵 / 插值打交道。

任务 难度 核心概念
12 翻转 / 旋转 90° 坐标重映射
13 任意角度旋转 ⭐⭐ 矩阵 + 插值
14 缩放 / 斜切 ⭐⭐ 矩阵缩放
15 鱼眼畸变 ⭐⭐⭐ 非线性坐标变换

坐标系转换的直觉 从这里开始建立。掌握了"原图 → 目标图"的反向映射思路,后面所有几何变换都长得差不多


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


🏷️ 标签#Rust #WebAssembly #图像处理 #几何变换 #坐标映射 #翻转 #旋转 #算法

相关推荐
CodeStats9 小时前
【编程语言】深度梳理C/C++、Java、Python、Go、Rust的区别
java·linux·c语言·c++·python·rust·go
程序员爱钓鱼11 小时前
为什么学习 Rust?Rust 能做什么?
后端·rust
SmalBox12 小时前
【节点】[IrisOutOfBoundColorClamp节点]原理解析与实际应用
unity3d·游戏开发·图形学
米沙AI13 小时前
Rust001--08字符串高级操作
rust
独孤留白13 小时前
从C到Rust:容器,容器,容器,Index和Iterator这两口糖甜不甜?
rust
小杍随笔15 小时前
【Rust全栈项目接入UUID主键实战:从选型到PostgreSQL适配(含v7优化)】
开发语言·postgresql·rust
doiito(Do It Together)1 天前
media_agent 进化之路:把 Gliding Horse 的 Agent 超能力注入 ComfyUI,让图片生成自己“学会”优化
人工智能·架构·rust·knowledge graph
techdashen1 天前
Arborium:把 tree-sitter 语法高亮打包成 Rust 文档生态的基础设施
开发语言·后端·rust
SmalBox1 天前
【节点】[IrisOffset节点]原理解析与实际应用
unity3d·游戏开发·图形学