图像的数字化过程
图像的数字化主要分两个过程,采样和量化。
采样
用多少点来描述一张图像
例如一张 600x400 尺寸的图, 会采样 240000 个点, 又叫 24 万像素
量化
把采样点上对应的亮度连续变化区间转化为单个特定数值的过程
- 每个像素点都有一个色值, 代表一种颜色
- 色值在不同的颜色模型下会用不同的分量表示
图像的颜色模型
- RGB: 任何一种颜色都可用三种基本颜色(红、绿、蓝)按不同的比例混合得到。在 RGB 模型中,一个像素值往往用 RGB 三个分量表示。当 RGB 的每个分量数值不同,混合得到的颜色就不同。
- CMY: 任何一种颜色都可以用三种基本颜料(青色(Cyan)、品红(Magenta)和黄色(Yellow)按一定比例混合得到。CMY 模型常用于打印。
- YUV: 多用于描述模拟信号。每一个颜色有一个亮度信号 Y,和两个色度信号 U 和 V。YUV 使用 RGB 的信息,但它从全彩色图像中产生一个黑白图像,然后提取出三个主要的颜色变成两个额外的信号来描述颜色。产生的目的是当年黑白电视和彩色电视的过渡,可以减少图像传输的数据量,因为 RGB 的颜色太多,每个像素占带宽太多,YUV 中就可以通过减少对色度信号的抽样,减少带宽占用,而且YUV可以单独传输,容易兼容黑白电视,黑白电视只用亮度信号就可以。根据信号抽样的频率不同,YUV 又可以细分。
- YCrCb: 描述数字信号的 YUV 模型。
- HSL: RGB 模型的另外一种描述,在HSL模型中,H 定义颜色的波长,称为色调; S 定义颜色的强度(intensity),表示颜色的深浅程度,称为饱和度; L 定义掺入的白光量,称为亮度。
RGB 模型
在 RGB 模型下,每个像素色值用 RGB 3 个分量表示, 每个分量可以通过占用不同的 bit 数来达到不同的色彩效果。
RGB 每个分量用 1 bit 表示
如果每个像素的每个颜色分量只用二进制的 1 位来表示,那每个颜色的分量只有 “1” 和 “0” 这两个值,也就是说,每种颜色的强度要么是 100%,要么就是 0%. RGB 每个分量有2种可能(0和1), 3 个分量就有 8($2^3$) 种组合, 能表示 8 种颜色。 图像大小就为 width * height * 3(1个像素3个分量) * 1(每个分量占1位) * / 8(bits 转 bytes)
=> W x H x 3 / 8
个字节
RGB888 每个分量用 8 bit 表示
如果每个像素的每个颜色分量用 8 位二进制来表示, 则 RGB 每个分量有 256($2^8$) 种值, 3 个分量就有 16777216 ($2^24$) 种组合, 能表示 1600 万种颜色(也就是我们常说的1600万色)。 图像大小就为 width * height * 3(1个像素3个分量) * 8(每个分量占8位) * / 8(bits 转 bytes)
=> W x H x 24 / 8
个字节
RGB 的排列
抽象排列是指我们在思维上认为图片应该是以行列分布的, 实际在内存中, 图像其实更常见是以一维数组做存储, 以 RGB RGB RGB 的顺序存储. 待需要做一些矩阵运算时再转换为二维数组形式.
Android 上的 RGB 模型
在 Android 上的 Bitmap
有几种图像模型, 其区别就在于: 用不同的位数来存储各个分量和是否带 Alpha 通道, 我们可以在解析 Bitmap 时通过 Bitmap.Config 指定期望的模型,但是最终系统使用什么模型还要根据解析的图片的颜色模型来决定. 比如: 即使我们设置使用 RGB_565
去解析一张带 Alpha 通道的图片, 但最终系统还是会使用 ARGB_8888
模型来解析. 如果我们想减少内存占用则可以使用 ARGB_4444
来解析, 但是质量会降低许多; 或者可以去掉原图的 Alpha 通道, 然后使用 RGB_565
解析.
格式 | 描述 |
---|---|
RGB_565 | 只有 RGB 通道,R 和 B 用 5位表示,G 用 6位表示,一个像素 (5+6+5) = 16 bits 占 2 byte |
ARGB_8888 | 包含 Alpha 通道的 RGB 图像。每个通道用8位表示,每个通道可取值 0~255,一个像素 4*8 = 32 bits 占 4 byte。每个通道色值丰富,图像质量高 |
ARGB_4444 | 包含 Alpha 通道的 RGB 图像。每个通道用4位表示,每个通道可取值 0~15,一个像素 4*4 = 16 bits 占 2 byte |
ALPHA_8 | 只有 Alpha 通道, Alpha 通道用 8 位表示, 一个像素占 1 byte. 该格式可用来加载只有 Alpha 通道的蒙版类图像 |
RGBA_F16 | 按 RGBA 排列的图像, 每个通道 8 位表示, 一个像素占 8 bytes. 该格式应该是解析质量最高, 内存占用最大的模式 |
其他几种颜色模型也有类似 RGB 这种的机制,每个分量可以用不同的位数表示。在合适的地方用合适的分量位数。
YUV 模型
YUV 是 Android 相机开发中输出图像的默认模型
现代, YUV 一般是从 RGB 图像中进行采样, 先产生一个使用 Y 通道表示的黑白图像, 然后提取出图像中三个主要的颜色变成两个额外的信号(U 和 V)来描述颜色. YUV 根据不同的采样比和 YUV 分量的排列方式有很多细分的格式.
格式 | 采样过程 | YUV分量采样比 | 垂直采样比 | 水平采样比 | 每像素位数 | 排列方式 | 格式别名 |
---|---|---|---|---|---|---|---|
YUV444 | 从 RGB 图像里每个像素里提取 YUV 分量,然后按照 VUY 排列 | 4:4:4 | 1:1 | 1:1 | 32 bits | $V_{1}$$U_{1}$$Y_{1}$$A_{1}$ $V_{2}$$U_{2}$$Y_{2}$$A_{2}$ 每个像素的4个分量(多个一个 Alpha 分量)按顺序排列 | YUV444 |
YUV422 | 从 RGB 图像里每个像素提取 Y 分量, 每隔一个像素提取一对 UV 分量 | 4:2:2 | 2:1 | 1:1 | 16 bits | $Y_{1}$$U_{1}$$Y_{2}$$V_{1}$ $Y_{2}$$U_{2}$$Y_{3}$$V_{2}$ 两个Y 一个U或V | YUY2 UYVY |
YUV420 | 从 RGB 图像里每个像素提取 Y 分量, 每隔两个像素提取一对 UV 分量 | 4:2:0 | 2:1 | 2:1 | 16 or 12 bits | 先排所有的 Y, 然后所有的 V, 然后所有的 U | IMC1 IMC2 IMC3 IMC4 YV12 NV12 NV21 YUV420P |
Android 相机图像处理
Android Camera 接口
Android 的 Camera 默认的预览格式是 NV21(YUV的一种,又称 YUV420P), 也支持通过 setPreviewFormat
设置其他编码,但是其他编码不一定在每台设备上支持。所以为了安全和方便,使用最通用的 NV21。
拿取 Camera 的实时数据也有几种方式, 这里使用的是给 Camera 设置 previewCallBack
, 在 onPreviewCallBack
中拿取.
onPreviewCallBack 这个方法需要注意: 它是相机每帧都会回调的, 基本是 33 帧每秒, 每 33ms 左右系统就会回调该方法, 如果在 onPreviewCallBack 内发生了超过 33ms 的阻塞, 就会出现丢帧现象。
NV21 转 RGBA
由于算法需要的图像数据是 RGB 模型下的, 所以需要进行 NV21 到 RGB 的转换.
这里我尝试了3种方式:
- 使用 Android 提供的
YuvImage
(简单方便、但耗时太多; 如果图片不进行压缩, 耗时陡增) - 使用 libyuv (耗时相对较少,但是需要自己写 JNI; 如果图片不进行压缩, 耗时也会增大)
- 使用 RenderScript(耗时最少, 需要理解 RenderScript 的机制; 如果图片不进行压缩, 耗时也较稳定)
压缩旋转
针对上面的3种方式, 尝试了 3 种方法:
- YuvImage 写入到 Bitmap, 通过对 Bitmap 做 Matrix 和边界压缩, 构造新的 Bitmap, 然后从 Bitmap 中获取像素数据
- 从格式转换到压缩、旋转、镜像都由 libyuv 做
- 从格式转换到压缩、旋转、镜像都由 RenderScript 做
相比之下, 也是 RenderScript 耗时最少.
获取单通道
灰度图就是只有一个颜色通道的图片,算法要求是只要有 RGB 任意一个通道就行, 所以我的做法是遍历所有像素, 取出一个通道的. 3 种方式做法都类似。
- 从 Bitmap 的像素中拿
从 Bitmap 中拿到的像素数据,每一个值都是 RGBA 的一个复合值, 需要通过 Color.xxx 拿到对应的实际值。然后从连续的 RGBA 数组中拿一个通道, 就通过遍历做.
|
|
- 从通过 RenderScript 处理的 RGBA 的像素中拿
val singleChannel = ByteArray(pixels.size/4)
val range = IntProgression.fromClosedRange(0, pixels.size - 1, 4)
for (i in range) {
// 每4个像素的第一个像素是红色 R
singleChannel[i / 4] = triple.first[i]
}
RGBA -> BGR
遍历 RGBA 数据, 进行变换:
|
|