图像处理一记录

分享记录

最近公司做了与图片处理有关的业务,下面是在组内分享了一些图像相关的基础知识。

第3张

图像中的单个点称为像素(pixel),每个像素都有一个值,称为像素值,它表示特定颜色的强度。

第4张
颜色模型是用简单方法描述所有颜色的一套规则和定义。

  • RGB: 任何一种颜色都可用三种基本颜色按不同的比例混合得到。在 RGB 模型中,一个像素值往往用 RGB 三个分量表示。当 RGB 的每个分量数值不同,混合得到的颜色就不同。
  • CMY: 任何一种颜色都可以用三种基本颜料(青色(Cyan)、品红(Magenta)和黄色(Yellow)按一定比例混合得到
  • YUV: 多用于描述模拟信号。每一个颜色有一个亮度信号 Y,和两个色度信号 U 和 V。YUV 使用 RGB 的信息,但它从全彩色图像中产生一个黑白图像,然后提取出三个主要的颜色变成两个额外的信号来描述颜色。产生的目的是当年黑白电视和彩色电视的过渡,可以减少图像传输的数据量,因为 RGB 的颜色太多,每个像素占带宽太多,YUV 中就可以通过减少对色度信号的抽样,减少带宽占用,而且YUV可以单独传输,容易兼容黑白电视,黑白电视只用亮度信号就可以。根据信号抽样的频率不同,YUV 又可以细分。
  • YCrCb: 描述数字信号的 YUV 模型。
  • HSL: RGB 模型的另外一种描述,在HSL模型中,H定义颜色的波长,称为色调; S 定义颜色的强度(intensity),表示颜色的深浅程度, 称为饱和度; L定义掺入的白光量,称为亮度。

第5张

如果每个像素的每个颜色分量用二进制的1位来表示,那每个颜色的分量只有“1”和“0”这两个值,也就是说,每种颜色的强度是100%,或者是0%。

第6张

如果每个分量用8位表示,因此可产生 ${2^{24}} = 16 777 216 ,1600万种颜色。
其他几种颜色模型也有类似 RGB 这种的机制,每个分量可以用不同的位数表示。在合适的地方用合适的分量位数。

第8张

YUV 使用 RGB 的信息,但它从全彩色图像中产生一个黑白图像,然后提取出三个主要的颜色变成两个额外的信号来描述颜色。
YUV 根据不同的采样比和YUV分量的排列方式有很多细分的格式。

  • 4:4:4采样率中:YUY2 从每个 RGB 图像像素里提取 YUV 分量,然后按照 VUY 排列。
  • 4:2:2 中:YUY2 从 RGB 图像像素里每个像素提取 Y 分量,每隔一个像素提取一对 UV 分量。
  • 4:2:0 中: NV12 从 RGB 图像像素里每个像素提取 Y 分量,每隔2个像素提取一对 UV 分量,排列方式是,先排所有的 Y,然后所有的 V,然后所有的 U

第9张

Android 中的一些 YUV 的格式

实例

颜色编码

可参考色彩基础: 20180925-Shaoxing-Basics-of-Colors, YUV 编码: https://zh.wikipedia.org/wiki/YUVhttps://docs.microsoft.com/en-us/previous-versions/aa904813(v=vs.80)

涉及的编码格式有 YUV RGB,细分有 NV21、RGBA、ARGB. YUV 主要用于视频处理中,我们这里为了实时性,是对每帧做处理,Android Camera 每帧返回的是 YUV 格式。RGB 主要用在图像显示中,算法需要的数据都是 RGB 的,需要做 YUV 到 RGB 的转换。

  • YUV 分别表示明度、色度和浓度,不同的 YUV 编码的区别主要在于 YUV 3 个分量的排列顺序和采样率的区别。
  • NV21(YUV420P)就代表YUV的采样比是 4:2:0,代表在水平方向和竖直方向上 Y 的采样率和 UV 采样率之比都是 2:1。
  • RGB 分别代表三原色红绿蓝,Android 上的图像编码主要有 ARGB_8888, ARGB_4444,RGB_565,ALPHA_8,RGB_F16等,分别表示图像的储存方式
    • 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。
    • RGB_565 则是只有 RGB 通道,R 和 B 用 5位表示,G 用 6位表示,一个像素 (5+6+5) = 16 bits 占 2 byte。

Android Camera

Android 的相机接口 Camera 。这次使用的是 Camera 接口,不是 Camera2 接口,Camera2 虽然提供了全高清支持、更全面的摄像头硬件支持,但是使用相对较复杂,且用不到那些功能。
Android 的 Camera 默认的预览格式是 NV21(YUV的一种,又称 YUV420P),也支持通过 setPreviewFormat 设置其他编码,但是其他编码不一定在每台设备上支持。所以为了安全和方便,这次直接使用了最通用的 NV21。
拿取 Camera 的实时数据也有几种方式,这里使用的是给 Camera 设置 previewCallBack,在 onPreviewCallBack 中拿取,onPreviewCallBack 这个方法需要注意:它是相机每帧都会回调的,基本是 33 帧每秒,每 33ms 左右系统就会回调该方法,如果在 onPreviewCallBack 内发生了超过 33ms 的阻塞,就会出现丢帧现象。

NV21 转 RGBA

到目前为止,这里我尝试了3种方式:

  • 使用 Android 提供的 YuvImage (简单方便、但耗时太多;如果图片不进行压缩,耗时陡增)
  • 使用 libyuv(耗时相对较少,但是需要自己写 JNI;如果图片不进行压缩,耗时也会增大)
  • 使用 RenderScript(耗时最少,需要理解 RenderScript 的机制;如果图片不进行压缩,耗时也较稳定)

压缩旋转

针对上面的3种方式,尝试了3种方法。相比之下,也是 RenderScript 耗时最少。

  • YuvImage 写入到 Bitmap,通过对 Bitmap 做 Matrix 和边界压缩,构造新的 Bitmap,然后从 Bitmap 中获取像素数据
  • 从格式转换到压缩、旋转、镜像都由 libyuv 做
  • 从格式转换到压缩、旋转、镜像都由 RenderScript 做

获取单通道

灰度图就是只有一个颜色通道的图片,只要有 RGB 任意一个通道就行,所以我的做法是遍历所有像素,取出一个通道的。3 种方式做法都类似。

从 Bitmap 中拿到的像素数据,每一个值都是 RGBA 的一个复合值,需要通过 Color.xxx 拿到对应的实际值。
直接从连续的 RGBA 数组中拿,就通过遍历做了。

  • 从 Bitmap 的像素中拿
1
2
3
4
5
6
7
8
val pixels = IntArray(bitmap.width * bitmap.height)
bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
val grayBytes = ByteArray(pixels.size)
for (i in 0.until(grayBytes.size)) {
// 获取单个通道存入
val red = Color.red(pixels[i])
grayBytes[i] = red.toByte()
}
  • 从通过 RenderScript 处理的 RGBA 的像素中拿
1
2
3
4
5
6
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:

1
2
3
4
5
6
7
8
9
10
11
12
val bgrPixel = ByteArray(rgbaPixels.size / 4 * 3)
val bgrRange = IntProgression.fromClosedRange(0, bgrPixel.size - 1, 3)
var i = 0
for (j in bgrRange) {
val r = rgbaPixels[i]
val g = rgbaPixels[i+1]
val b = rgbaPixels[i+2]
bgrPixel[j] = b
bgrPixel[j + 1] = g
bgrPixel[j + 2] = r
i += 4
}