当前位置: 华文问答 > 数码

如何看待相机 RAW 格式大部分时候没用的说法?

2019-11-08数码

开门见山,直接说jpeg的缺点:

8位色彩深度 (理论上也可以支持更高深度,但我从未见过高于8位的jpeg),也就是像素值范围属于[0,255];而单反/微单的raw一般都为12位,像素值范围[0,4095];甚至有14位的,像素值范围[0,16383]。显然raw对颜色的分辨力更高,一般为jpeg的16倍甚至64倍。

有趣的是,RGB三通道均为8位、共24位色深的图像,称之为「真彩色」;那12位的raw岂不是「很真彩色」,14位的就成了「超真彩色」...

YUV420采样 (jpeg支持RGB,但是也从未见RGB的jpeg,毕竟jpeg本来就是做压缩的,用RGB与压缩的本意相违背),数据量Y:U:V=4:1:1,与原始的R:G:B=1:1:1相比,亮度信息不变,但颜色信息直接损失了75%。但是有些相机的raw也是使用YUV420。

不过,相机cmos的实际输出中,每个像素点似乎只有一个值(R或G或B)。

cmos实际的输出,每个像素点只有RGB三者之一的值

而从raw解析出的图像,每个像素点的RGB值是从邻域插值得到的,例如某个像素点本来为R,则它的G和B值被当作是八邻域内的R和B值的均值。人眼对绿色更为敏感,因此cmos的R:G:B数量一般为1:2:1。在这种情况下,使用yuv420采样也会有颜色损失,但损失小于75%。

高频信息有大量损失。 也就是图像中变化丰富的细节部分很难保留。这是dct+量化导致的。

块效应。 由于jpeg的基本处理单元是一个个小块,小块之间的过渡不自然就会出现色块效应。这在压缩比较高或图像中颜色变化剧烈时,尤为明显。块效应其实也是有损压缩最难解决的问题之一,在低质量的视频中更常见。

不适合多次编辑。 这一点 @Timothy Wang提到了,我给补充上去并详细说明。因为每一次对图像进行ps然后保存成jpeg,都会有一次损失。所以修一次,保存,再修一次,保存...图像质量会越来越差...

不过这个问题也好解决,你只需要把jpeg先转换成bmp再编辑即可。(那你为什么不直接使用同为无损的raw呢?)

例如使用质量因子99,将同一张图片不断读取-编码-保存jpeg:

第一次
第二次
第三次
第四次

现在画质已经惨不忍睹了....

类似的例子可见于

顺便说一句,后缀是jpeg与jpg没有任何区别,文件头都是0xFFD8。由于Windows上习惯用三个字母的文件后缀,所以jpeg简写为jpg。既然老大哥Windows都这样用了,大家也就都混着用了。

下面是原理解释,有兴趣的同学可以

---------------------------------------------------------------------

我手写过jpeg编码器,因此对jpeg编码算是比较了解。源代码(欢迎点star):

raw是一种无损的图像存储格式,类似的还有bmp、png等,均为无损存储,而bmp不会做任何压缩,png会做压缩,但也是无损压缩。无损的存储,会忠实记录每一个像素点的像素值。将图片保存为文件,然后再从文件中读取图片,读的的图片与原先的图片没有任何区别。

jpeg是一种 有损 的压缩标准,有损意味着,将原图保存成jpeg,再从jpeg读取出图片,得到的图片与原来的图片并非完全相同。

例如使用我自己编写的jpeg编码器,将bmp图像先编码成jpeg再解码,得到的图像与原图像作差,并可视化,得到误差图像:

原图像
先编码再解码,然后与原图像作差并可视化,得到的误差图像

误差图像中,越白的像素,表示此处的误差越大。可以发现,jpeg图像的误差注意集中在图像中的轮廓(边缘)区域。

下面介绍一下jpeg编码的基本原理,以及误差的来源。

jpeg的编码过程为:分块-dct变换-量化-扫描-编码

分块: 将图片分成8*8或16*16的小区域,边长不为8的整数倍则补齐。小块的边长为2的次幂,这是为了加速dct变换。

imageWidth = srcImageWidth imageHeight = srcImageHeight # add width and height to %8==0 if (srcImageWidth % 8 != 0): imageWidth = srcImageWidth // 8 * 8 + 8 if (srcImageHeight % 8 != 0): imageHeight = srcImageHeight // 8 * 8 + 8 print('added to: ', imageWidth, imageHeight) # copy data from srcImageMatrix to addedImageMatrix addedImageMatrix = numpy.zeros((imageHeight, imageWidth, 3), dtype=numpy.uint8) for y in range(srcImageHeight): for x in range(srcImageWidth): addedImageMatrix[y][x] = srcImageMatrix[y][x]

注意,jpeg很少对RGB空间进行操作,一般都是先将RGB转化到YUV(亮度色度浓度)空间,而且这一过程往往会进行下采样:

RAW中的RGB存储数据量是R:G:B=1:1:1(但是也有不少raw中使用YUV420而非RGB)

如果使用YUV420(绝大多数jpeg都是YUV420,不过我的代码中使用的是YUV444),亮度不会被下采样,但色度和浓度都是四取一,则Y:U:V=4:1:1

可以发现,仅仅在这一步,从RGB到YUV420,颜色信息就损失了75%。

好在人眼对亮度更敏感,对颜色其实并不敏感,普通人很难看出经过YUV420采样后图片与原图片有何区别。

dct变换: 遍历每个小块,进行dct变换。dct变换实际上是将原矩阵中的数据,按照频率的大小进行重新排列。什么是图像的频率?可以理解成图像中灰度变化剧烈程度。显然图像中轮廓处的灰度变化剧烈,所以轮廓是高频区域。

8*8的矩阵经过dct变换后,得到的仍然是一个8*8的矩阵。dct变换后,原矩阵的直流分量(也就是均值)会被存储到新矩阵的左上角,称为DC量;而新矩阵中其他的63个值,称为AC量。变换后的矩阵,越靠近右下方,所存储的频率越高。也就是,图像中的高频细节,都存储在新矩阵的右下部分,且细节越丰富、变化越剧烈,在新图像中的位置越靠近右下角。

for y in range(0, imageHeight, 8): for x in range(0, imageWidth, 8): print('block (y,x): ',y, x, ' -> ', y + 8, x + 8) yDctMatrix = fftpack.dct(fftpack.dct(yImageMatrix[y:y + 8, x:x + 8], norm='ortho').T, norm='ortho').T uDctMatrix = fftpack.dct(fftpack.dct(uImageMatrix[y:y + 8, x:x + 8], norm='ortho').T, norm='ortho').T vDctMatrix = fftpack.dct(fftpack.dct(vImageMatrix[y:y + 8, x:x + 8], norm='ortho').T, norm='ortho').T

# 这里用4*4举例 [[127 127 127 127] [127 127 127 127] [127 127 127 127] [127 127 127 127]] dct-> [[508. 0. 0. 0.] [ 0. 0. 0. 0.] [ 0. 0. 0. 0.] [ 0. 0. 0. 0.]] [[ 127 127 127 127] [ 127 127 127 127] [ 127 127 -100 127] # 将127换成-100,导致这个位置成为「高频区域」 [ 127 127 127 127]] dct-> # 可以发现dct变换后得到的矩阵,与原来dct变换得到的矩阵大为不同 [[451.2 30.7 56.8 -74.1] [ 30.7 -16.6 -30.7 40.1] [ 56.8 -30.7 -56.8 74.1] [-74.1 40.1 74.1 -96.9]]

dct变换是完全可逆的,但是计算机的精度有限,因此存在一定的截断误差。例如1/3只能表示为0.333......3,小数位数是有限的。但这种截断误差非常微小,可以忽略。

解码器在进行idct变换(dct变换的逆操作)后,通常得到的也不会恰好是整数,而像素值必须为整数,因此此处又会出现舍入误差。

下面的gif图,是对原图片进行dct变换,然后分别取前0到1023个频率的分量求和,再反变换得到的视频,取的频率越多,图片越接近原图片:

分别取前0到1023个频率 https://www.zhihu.com/video/1146758691666763776

量化: 其实就是取整。选取一个基准矩阵(称之为量化表),然后用dct变换后的矩阵去点除量化表,得到量化矩阵。

例如量化表和dct变换后的矩阵为

luminanceQuantTbl: [[ 2 1 1 2 2 4 5 6] [ 1 1 1 2 3 6 6 6] [ 1 1 2 2 4 6 7 6] [ 1 2 2 3 5 9 8 6] [ 2 2 4 6 7 11 10 8] [ 2 4 6 6 8 10 11 9] [ 5 6 8 9 10 12 12 10] [ 7 9 10 10 11 10 10 10]] yDctMatrix: [[ 2.36750000e+02 5.53303392e+00 2.01307085e+00 -7.19686853e-02 -1.57009246e-16 3.96662146e-01 -6.24580225e+00 7.60578828e+00] [ 6.66855886e+00 -7.52969799e-01 1.48262936e-01 -3.92175621e+00 2.30297862e+00 3.29949388e+00 -6.18734755e+00 5.08374735e+00] [-4.84356843e+00 -5.43104954e-01 -1.19822330e+00 8.12655697e-01 -9.47093175e-01 -5.06521493e-01 3.05698052e+00 -2.63930259e+00] [ 2.93827790e+00 1.53779026e+00 1.40202735e+00 1.54364563e+00 -6.90691317e-01 -1.43266006e+00 2.09031500e-01 4.41897106e-01] [-1.75000000e+00 -1.52408871e+00 -7.24572153e-02 -1.48975772e+00 2.00000000e+00 9.95424283e-01 -2.13477164e+00 3.03160093e-01] [ 1.36194964e+00 6.56452465e-01 -2.05142958e+00 -4.80757100e-01 -2.56621229e+00 1.19379124e+00 2.35377408e+00 3.00353396e-01] [-1.24090487e+00 2.47316174e-01 3.30698052e+00 2.26126343e+00 2.28648519e+00 -2.94759335e+00 -1.55177670e+00 -1.09970969e+00] [ 8.16663251e-01 -4.64349688e-01 -2.50381139e+00 -2.06205701e+00 -1.32619360e+00 2.47989422e+00 6.46286535e-01 1.01553293e+00]]

进行量化,也就是用dct变换后的矩阵去点除量化矩阵:

yQuantMatrix = numpy.rint(yDctMatrix / luminanceQuantTbl)

则量化得到:

yQuantMatrix: [[118. 6. 2. -0. -0. 0. -1. 1.] [ 7. -1. 0. -2. 1. 1. -1. 1.] [ -5. -1. -1. 0. -0. -0. 0. -0.] [ 3. 1. 1. 1. -0. -0. 0. 0.] [ -1. -1. -0. -0. 0. 0. -0. 0.] [ 1. 0. -0. -0. -0. 0. 0. 0.] [ -0. 0. 0. 0. 0. -0. -0. -0.] [ 0. -0. -0. -0. -0. 0. 0. 0.]]

注意,dct变换后的矩阵中,越靠近右下角,频率越高,但值往往很小(因为高频的数据在原图像中只占少数部分)。这些很小的值,在除以量化表后,很容易得到0。 也就是这些高频数据,在量化的这一过程中被舍入了,jpeg的主要误差就来源于此。

此外,虽然YUV有3通道,但jpeg总是使用2个量化表:一个用于亮度,一个用于色度和浓度。

std_luminance_quant_tbl = numpy.array( [ 16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55, 14, 13, 16, 24, 40, 57, 69, 56, 14, 17, 22, 29, 51, 87, 80, 62, 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 104, 113, 92, 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99],dtype=int) std_luminance_quant_tbl = std_luminance_quant_tbl.reshape([8,8]) std_chrominance_quant_tbl = numpy.array( [ 17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99, 24, 26, 56, 99, 99, 99, 99, 99, 47, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99],dtype=int) std_chrominance_quant_tbl = std_chrominance_quant_tbl.reshape([8,8])

3个通道却用两张量化表,而不是专门为色度和浓度各设计一个量化表,显然误差会增加(但并不明显)。

blockNum = 0 for y in range(0, imageHeight, 8): for x in range(0, imageWidth, 8): print('block (y,x): ',y, x, ' -> ', y + 8, x + 8) yDctMatrix = fftpack.dct(fftpack.dct(yImageMatrix[y:y + 8, x:x + 8], norm='ortho').T, norm='ortho').T uDctMatrix = fftpack.dct(fftpack.dct(uImageMatrix[y:y + 8, x:x + 8], norm='ortho').T, norm='ortho').T vDctMatrix = fftpack.dct(fftpack.dct(vImageMatrix[y:y + 8, x:x + 8], norm='ortho').T, norm='ortho').T if(blockSum<=8): print('yDctMatrix:\n',yDctMatrix) print('uDctMatrix:\n',uDctMatrix) print('vDctMatrix:\n',vDctMatrix) yQuantMatrix = numpy.rint(yDctMatrix / luminanceQuantTbl) uQuantMatrix = numpy.rint(uDctMatrix / chrominanceQuantTbl) vQuantMatrix = numpy.rint(vDctMatrix / chrominanceQuantTbl)

由于jpeg的主要误差就来自于量化,因此改变压缩率,或者说图像质量,往往是通过改变量化表实现的。例如相机设置中,jpeg 的标准/精细,其实就是对应不同的量化表。

量化表中的数值越小,点除后小数部分会越小,取整后舍入误差也越小,则jpeg质量越高;同理,量化表中越靠近右下的部分值越大,则图像中高频信息的损失越严重。

在代码中,我们通过「质量因数」这一数值,对量化表进行简单的线性变换来改变图像质量:

if(quality <= 0): quality = 1 if(quality > 100): quality = 100 if(quality < 50): qualityScale = 5000 / quality else: qualityScale = 200 - quality * 2 luminanceQuantTbl = numpy.array(numpy.floor((std_luminance_quant_tbl * qualityScale + 50) / 100)) luminanceQuantTbl[luminanceQuantTbl == 0] = 1 luminanceQuantTbl[luminanceQuantTbl > 255] = 255 luminanceQuantTbl = luminanceQuantTbl.reshape([8, 8]).astype(int) print('luminanceQuantTbl:\n', luminanceQuantTbl) chrominanceQuantTbl = numpy.array(numpy.floor((std_chrominance_quant_tbl * qualityScale + 50) / 100)) chrominanceQuantTbl[chrominanceQuantTbl == 0] = 1 chrominanceQuantTbl[chrominanceQuantTbl > 255] = 255 chrominanceQuantTbl = chrominanceQuantTbl.reshape([8, 8]).astype(int) print('chrominanceQuantTbl:\n', chrominanceQuantTbl)

bmp体积为786.5KB,改变质量因数,得到的jpeg图片和体积如下:

原图,786.5KB
q=100,472.6KB

量化表均为1,几乎没有量化误差:

luminanceQuantTbl: [[1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1]] chrominanceQuantTbl: [[1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1]]

q=80,62.0KB

luminanceQuantTbl: [[ 6 4 4 6 10 16 20 24] [ 5 5 6 8 10 23 24 22] [ 6 5 6 10 16 23 28 22] [ 6 7 9 12 20 35 32 25] [ 7 9 15 22 27 44 41 31] [10 14 22 26 32 42 45 37] [20 26 31 35 41 48 48 40] [29 37 38 39 45 40 41 40]] chrominanceQuantTbl: [[ 7 7 10 19 40 40 40 40] [ 7 8 10 26 40 40 40 40] [10 10 22 40 40 40 40 40] [19 26 40 40 40 40 40 40] [40 40 40 40 40 40 40 40] [40 40 40 40 40 40 40 40] [40 40 40 40 40 40 40 40] [40 40 40 40 40 40 40 40]]

q=10,13.7KB

luminanceQuantTbl: [[ 80 55 50 80 120 200 255 255] [ 60 60 70 95 130 255 255 255] [ 70 65 80 120 200 255 255 255] [ 70 85 110 145 255 255 255 255] [ 90 110 185 255 255 255 255 255] [120 175 255 255 255 255 255 255] [245 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255]] chrominanceQuantTbl: [[ 85 90 120 235 255 255 255 255] [ 90 105 130 255 255 255 255 255] [120 130 255 255 255 255 255 255] [235 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255]]

q=1,9.3KB

这个量化表....均为255,高频信息几乎完全丢失(所以每个色块内都几乎一个色了)

luminanceQuantTbl: [[255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255]] chrominanceQuantTbl: [[255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255] [255 255 255 255 255 255 255 255]]

扫描: 将二维矩阵扫描,得到一维数组。希望更多的0元素存储到一起,以便存储时节省空间。因此按z字形扫描:

z字形扫描久负盛名,著名的FFMPEG(绝大多数播放器都是给FFMPEG套个图形界面的壳,当然也有少部分是用gstreamer套壳..),采用的就是这个图标:

注意,实际上每个小块中的z字形扫描只针对AC值;所有小块的DC值会被单独存储,且只存储第一个DC值的真正值,剩下的都是存储与前一个的差值。

编码: 霍夫曼编码或者算术编码。是无损编码。DC值采用(Size,Value)格式,AC值采用(Run/Size,Value)格式。很复杂,不做详细介绍。详情请见于./python-jpeg-encoder/huffmanEncode.py