RGB 色彩矩阵变换:矩阵 × 向量 = 像素变换
🦀 Rust + WASM 实战系列 第 19 篇 阅读时间:约 5 分钟 | 实战可运行
📌 写在前面
Part 1 任务 5 讲了 7 种"单色滤镜"——sepia、灰度、暖色调……每个都是手写 RGB 系数:
r = 0.393*r_old + 0.769*g_old + 0.189*b_old
g = 0.349*r_old + 0.686*g_old + 0.168*b_old
b = 0.272*r_old + 0.534*g_old + 0.131*b_old
7 种滤镜 = 7 套系数 = 7 个不同公式——重复、难记。
这一篇用"矩阵 × 向量"统一——1 个公式搞定所有 7 种滤镜(和 Part 1 任务 5 完全相同的效果)。
🚀 TL;DR
RGB 像素 = 3 维向量 ,变换矩阵 = 3×3 系数表,新像素 = 矩阵 × 向量:
核心代码 5 行:
let m = Matrix3::new(a, b, c, d, e, f, g, h, i);
let rgb = Vector3::new(r, g, b);
let new_rgb = m * rgb;
(计算使用 nalgebra 库
📖 目录
一、前置基础:矩阵 × 向量(可跳过)
下面只讲一个运算——如果你记得,可以直接跳到 §二。
矩阵 = 数字表格
一张 3×3 的数表。没什么神秘的。
向量 = 一列数
RGB 三通道就是一个 3 维向量。
矩阵 × 向量(核心)
规则:矩阵的每一行和向量的每个元素对应相乘,再加起来:
就这么简单:一行算一个值,3 行 = 3 个新值。
二、RGB 像素 = 3 维向量
每个像素的 RGB 三通道 = 一个 3 维向量:
三、变换矩阵 = 一张 3×3 系数表
9 个数字按行优先(row-major)排列:
m00 m01 m02
m10 m11 m12
m20 m21 m22
矩阵的每一行 = 怎么算新通道:
- 第 1 行()= 怎么算新红色
- 第 2 行()= 怎么算新绿色
- 第 3 行()= 怎么算新蓝色
每一行有 3 个系数 ,这 3 个系数告诉"用多少原 R、多少原 G、多少原 B 混合"。
4、4 个例子
每张表 = 一个预设矩阵。
例 1:单位矩阵(不变)
新像素 = 旧像素。什么都不做。
例 2:对比度增强(对角线 ×1.5)
每个通道 ×1.5 → 颜色更鲜艳(注意:超过 255 会被截断)。
例 3:转灰度(BT.601 加权)
注意三行都一样——把 RGB 按 BT.601 系数混合成灰度。
| R 系数 | G 系数 | B 系数 | 物理含义 |
|---|---|---|---|
| 0.299 | 0.587 | 0.114 | 人眼对绿色最敏感(绿色权重最大) |
例 4:怀旧(Sepia)
经典的"老照片"色调。
5、关键代码
use nalgebra::{Matrix3, Vector3};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn color_matrix(
pixels: &[u8], width: u32, height: u32,
m00: f32, m01: f32, m02: f32,
m10: f32, m11: f32, m12: f32,
m20: f32, m21: f32, m22: f32,
) -> Vec<u8> {
let w = width as usize;
let h = height as usize;
let matrix = Matrix3::new(
m00, m01, m02,
m10, m11, m12,
m20, m21, m22,
);
let mut out = vec![0u8; pixels.len()];
for i in 0..(w * h) {
let idx = i * 4;
let rgb = Vector3::new(
pixels[idx] as f32,
pixels[idx + 1] as f32,
pixels[idx + 2] as f32,
);
let new_rgb = matrix * rgb;
out[idx] = new_rgb[0].clamp(0.0, 255.0) as u8;
out[idx + 1] = new_rgb[1].clamp(0.0, 255.0) as u8;
out[idx + 2] = new_rgb[2].clamp(0.0, 255.0) as u8;
out[idx + 3] = pixels[idx + 3]; // alpha 不变
}
out
}
核心循环 5 行——matrix * rgb 调库,其他都是装框。
对比:手写 7 种滤镜(任务 5)和 1 个矩阵
任务 5(7 个手写函数):
fn sepia(p: &[u8]) -> Vec<u8> {
// 9 行手写 RGB 系数
}
fn cool(p: &[u8]) -> Vec<u8> { /* 9 行 */ }
fn warm(p: &[u8]) -> Vec<u8> { /* 9 行 */ }
fn red_boost(p: &[u8]) -> Vec<u8> { /* 9 行 */ }
// ... 共 7 个函数 × 9 行 = 63 行
任务 19(1 个矩阵函数):
fn color_matrix(p: &[u8], ..., m00..m22) -> Vec<u8> {
// 5 行循环 + 调库
}
// 1 个函数 × 5 行 = 5 行
代码量减少 12 倍——这就是矩阵抽象的力量。
6、前端效果展示
打开页面后:
- 默认显示单位矩阵(图像不变)
- 点击 4 个预设按钮:原始 / 对比度增强 / 灰度 / 怀旧
- 9 个数字输入框可以自定义任意矩阵——拖滑块看实时效果
7、踩坑提醒
1. 矩阵超过 [0, 255] 会被截断
// 对比度 ×1.5:原本 R=200 → 300 → clamp 到 255(截断)
// 高亮区域会"爆白"——这是矩阵变换的固有问题
解决:如果不想截断,可以先做浮点运算再一起转 u8(代码里已经做了)。
2. 矩阵不一定是"颜色"变换
RGB 矩阵可以做任何线性变换:
- 转灰度
- 反色(对角线 = -1)
- 通道交换(红绿互换)
不限于"调色"——读者可以试试输入奇怪的矩阵看效果。
3. Alpha 通道不参与变换
out[idx + 3] = pixels[idx + 3]; // alpha 直接复制
透明度不在 RGB 颜色空间内,不能被 3×3 矩阵变换。
8、接下来
任务 20:PCA 主成分分析——用协方差矩阵找出图片"最关键的方向",实现图片压缩。
Mandelbrot:每个像素一个 c,z 从 0 开始 PCA:每张图片一个协方差矩阵,找最大方差方向
把 10000 维的图片压缩到 50 维的"主成分"——视觉上还能看清原图。
任务 21:最小二乘回归——用矩阵求逆解"拟合"问题,做图像去噪。
任务 22:协方差矩阵手写——深挖 PCA 内部原理,库 vs 手算对比。
一句话总结
RGB 像素 = 3 维向量,3×3 矩阵 × 向量 = 新颜色。
7 种滤镜的 7 个手写公式 → 1 个矩阵函数(5 行循环 + 调库)。
代码量减少 12 倍,这就是线性代数的"抽象力量"。
📦 项目地址:pixel-math-wasm 🦀 Rust + WebAssembly 实战系列
🏷️ 标签:#Rust #WebAssembly #图像处理 #矩阵 #线性代数 #RGB变换 #nalgebra