PIL的色彩空间转换 YCbCr -> RGB
PIL v1.1.7使用的算法会导致图像看起来“褪色”。而用ffmpeg
转换同样的数据时,效果就正常。使用mplayer
的结果和ffmpeg
是一样的(可能它们底层用的是同一个库)。这让我觉得PIL在颜色空间转换上可能出问题了。这个转换的源头似乎在libImaging/ConvertYCbCr.c
文件里:
/* JPEG/JFIF YCbCr conversions
Y = R * 0.29900 + G * 0.58700 + B * 0.11400
Cb = R * -0.16874 + G * -0.33126 + B * 0.50000 + 128
Cr = R * 0.50000 + G * -0.41869 + B * -0.08131 + 128
R = Y + + (Cr - 128) * 1.40200
G = Y + (Cb - 128) * -0.34414 + (Cr - 128) * -0.71414
B = Y + (Cb - 128) * 1.77200
*/
这只是源代码中的一段注释,当然这是C语言代码,实际的功能是通过查找表来实现,而不是用矩阵乘法(为了简洁,static INT16 R_Cr
等内容省略了):
void
ImagingConvertYCbCr2RGB(UINT8* out, const UINT8* in, int pixels)
{
int x;
UINT8 a;
int r, g, b;
int y, cr, cb;
for (x = 0; x < pixels; x++, in += 4, out += 4) {
y = in[0];
cb = in[1];
cr = in[2];
a = in[3];
r = y + (( R_Cr[cr]) >> SCALE);
g = y + ((G_Cb[cb] + G_Cr[cr]) >> SCALE);
b = y + ((B_Cb[cb] ) >> SCALE);
out[0] = (r <= 0) ? 0 : (r >= 255) ? 255 : r;
out[1] = (g <= 0) ? 0 : (g >= 255) ? 255 : g;
out[2] = (b <= 0) ? 0 : (b >= 255) ? 255 : b;
out[3] = a;
}
}
我在网上查了一下,但关于“正确”的颜色空间转换方法似乎有很多混淆。所以我的问题是,上面的做法对吗?如果不对,有没有更好的方法?
补充: 在阅读了Mark Ransom提供的链接后,我发现根据你是使用完整的YCbCr范围还是限制在有效范围内,定义是有冲突的。更多信息请见以下链接:
看来PIL版本使用了错误的算法,所以我自己写了一个转换函数,效果看起来正常(“SDTV”版本)。下面是代码,供未来的读者使用:
from numpy import dot, ndarray, array
A_SDTV = array([[1., 0., 0.701 ],
[1., -0.886*0.114/0.587, -0.701*0.299/0.587],
[1., 0.886, 0.]])
A_SDTV[:,0] *= 255./219.
A_SDTV[:,1:] *= 255./112.
A_HDTV = array([[1.164, 0., 1.793],
[1.164, -0.213, -0.533],
[1.164, 2.112, 0.]])
def yuv2rgb(im, version='SDTV'):
"""
Convert array-like YUV image to RGB colourspace
version:
- 'SDTV': ITU-R BT.601 version (default)
- 'HDTV': ITU-R BT.709 version
"""
if not im.dtype == 'uint8':
raise TypeError('yuv2rgb only implemented for uint8 arrays')
# clip input to the valid range
yuv = ndarray(im.shape) # float64
yuv[:,:, 0] = im[:,:, 0].clip(16, 235).astype(yuv.dtype) - 16
yuv[:,:,1:] = im[:,:,1:].clip(16, 240).astype(yuv.dtype) - 128
if version.upper() == 'SDTV':
A = A_SDTV
elif version.upper() == 'HDTV':
A = A_HDTV
else:
raise Exception("Unrecognised version (choose 'SDTV' or 'HDTV')")
rgb = dot(yuv, A.T)
result = rgb.clip(0, 255).astype('uint8')
return result
1 个回答
10
如果你去维基百科查一下YCbCr的定义,会发现有两个相互矛盾的说法。一个是ITU-R BT.601的定义,它把数值压缩到16到235之间,这样可以留出一些空间,避免数值过低或过高。而另一个是JPEG的版本,它使用的是0到255的完整范围。如果你用JPEG的公式去解码BT.601范围内的数值,结果看起来肯定会很淡,像是失去了颜色一样。