图像处理基础:YUV与RGB转换、Bitmap操作、矩阵变换、高斯模糊

说实话,图像处理这块内容,我当年刚接触时也觉得挺枯燥的。一堆数学公式,各种色彩空间,看着就头大。但后来在实际项目中摔过几次跟头,才明白这些基础有多重要。今天咱们就聊聊这几个核心概念,我会尽量用大白话讲清楚。

一、YUV与RGB:两种色彩世界的对话

先问个问题:为什么视频编码不用RGB,非要用YUV?

我刚开始做音视频时也纳闷。RGB多直观啊,红绿蓝三原色,每个像素三个值,简单粗暴。但后来发现,人眼对亮度变化比对颜色变化敏感得多。YUV就是利用这个特点——把亮度(Y)和色度(U、V)分开存储。

说白了,YUV可以"偷懒"。你可以降低色度分量的采样率,人眼基本察觉不到。比如最常见的NV12格式,每4个像素共享一组UV值,数据量直接减半。这在视频传输中太香了。

核心公式(ITU-R BT.601标准):

RGB → YUV:

Y  = 0.299*R + 0.587*G + 0.114*B
U  = -0.147*R - 0.289*G + 0.436*B + 128
V  = 0.615*R - 0.515*G - 0.100*B + 128

YUV → RGB:

R = Y + 1.140*(V - 128)
G = Y - 0.394*(U - 128) - 0.581*(V - 128)
B = Y + 2.032*(U - 128)

嗯,这里要注意:公式里的系数不是随便写的,不同标准(BT.601、BT.709、BT.2020)系数不一样。我曾在项目中吃过这个亏——用BT.601的系数去解码BT.709的视频,颜色整个偏掉,排查了半天才发现是标准没对齐。

二、Bitmap操作:Android上的图像管家

在Android里,Bitmap就是图像的"本体"。你从相机拿到的每一帧,从网络下载的每一张图,最终都会变成Bitmap对象。

我个人习惯把Bitmap操作分成三类:

  • 创建与加载:BitmapFactory.decodeResource()、Bitmap.createBitmap()
  • 像素级操作:getPixels()、setPixels()、copyPixelsToBuffer()
  • 变换与裁剪:createScaledBitmap()、Bitmap.createBitmap()带参数

举个例子,把一张图片转成YUV数据,最直接的方式就是先拿到像素数组:

int[] pixels = new int[width * height];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);

// 然后遍历每个像素,提取RGB分量
for (int i = 0; i < pixels.length; i++) {
    int pixel = pixels[i];
    int r = (pixel >> 16) & 0xFF;
    int g = (pixel >> 8) & 0xFF;
    int b = pixel & 0xFF;
    // 再用前面的公式转成YUV...
}

避坑指南:我曾经在循环里直接操作Bitmap的像素,结果卡成PPT。后来发现getPixels()一次性读取到数组再操作,比反复调用getPixel()快了一个数量级。另外,别忘了Bitmap的内存回收——recycle()该调就调,尤其是大图。

三、矩阵变换:图像的"变形金刚"

说到矩阵变换,很多人第一反应是数学课上的矩阵乘法。其实在图像处理里,矩阵就是用来描述"怎么变"的规则。

Android提供了Matrix类,支持平移、旋转、缩放、错切四种基本变换。你可以把它们组合起来,实现任意效果。

我画了一张图,帮你理清这些变换的关系:

图像矩阵变换核心逻辑 原始图像 (像素坐标) Matrix 变换矩阵 [a b c] [d e f] [0 0 1] 变换后图像 (新坐标) 平移 (Translate) 旋转 (Rotate) 缩放 (Scale) 错切 (Skew) 组合使用:matrix.preTranslate().preRotate().preScale() 最终效果 = 多个变换矩阵的乘积

实际开发中,我常用Matrix来做图片的裁剪和旋转。比如相机预览方向不对,用Matrix.postRotate(90)转一下就好。但要注意:矩阵乘法不满足交换律,先旋转再平移和先平移再旋转,结果完全不同。

Matrix matrix = new Matrix();
matrix.postRotate(90);          // 先旋转
matrix.postTranslate(100, 0);   // 再平移
// 顺序很重要!postXxx是右乘,preXxx是左乘

四、高斯模糊:让图像"朦胧"的艺术

高斯模糊,说白了就是用周围像素的加权平均值来代替当前像素。权重由高斯函数决定——离得越近的像素权重越大,越远越小。

为什么叫"高斯"?因为权重分布符合高斯分布(正态分布)的曲线形状。中间高两边低,像个钟形。

Android里实现高斯模糊有几种方式:

方法 优点 缺点 适用场景
RenderScript 速度快,硬件加速 API 17+,已废弃 旧项目维护
Canvas.drawBitmap + Paint.setMaskFilter 简单易用 性能一般 简单模糊效果
OpenGL ES 着色器 性能极佳,可定制 实现复杂 实时视频处理
Java/Kotlin 纯算法 无依赖,可移植 速度慢 学习研究

我的经验:如果只是做静态图片的模糊效果,用RenderScript最省事。但如果是视频流实时模糊,我建议用OpenGL ES实现分离式高斯模糊——先横向模糊,再纵向模糊,效率比二维卷积高得多。

分离式高斯模糊的核心思路很简单:一个N×N的高斯核,拆成两个1×N的核。比如5×5的核,拆成横向5个权重和纵向5个权重。这样计算量从N²降到2N,对于大核来说效果显著。

// 伪代码:分离式高斯模糊
// 第一步:横向模糊
for each row:
    for each pixel in row:
        newPixel = sum(neighbor * weight_x)
        
// 第二步:纵向模糊
for each col:
    for each pixel in col:
        finalPixel = sum(neighbor * weight_y)

嗯,这里要注意边界处理。我刚开始写的时候没考虑边缘像素,结果图片四周出现黑边。后来加了边界像素的镜像处理,问题就解决了。

五、把这些串起来:一个实际场景

假设你要做一个美颜相机,流程大概是这样的:

  1. 相机输出YUV帧(NV21格式)
  2. 转成RGB/Bitmap方便处理
  3. 用Matrix做镜像翻转(前置摄像头需要)
  4. 对皮肤区域做高斯模糊(磨皮效果)
  5. 再转回YUV编码或显示

每一步都有坑。比如YUV转RGB时,不同平台的YUV排列方式可能不同——NV21是VU交替,NV12是UV交替。我曾在某款手机上踩过这个坑,画面颜色整个是反的。

一句话总结:图像处理没有捷径,理解YUV/RGB的转换原理、Bitmap的内存管理、矩阵变换的数学逻辑、高斯模糊的优化技巧,才能在遇到问题时快速定位并解决。

这些基础打牢了,后面学滤镜、特效、视频编辑才会顺手。别急,慢慢来。


公众号:蓝海资料掘金营,微信deep3321