PIL的颜色空间转换YCbCr -> RGB

19
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

2
它是否与http://en.wikipedia.org/wiki/YCbCr中的机制之一匹配? - Oliver Charlesworth
1个回答

10

如果你查看维基百科的定义,你会发现有两个相冲突的YCbCr定义。ITU-R BT.601定义将值压缩到16-235的范围内,以提供脚部和头部空间,而JPEG版本使用完整范围0-255。如果你使用JPEG的公式来解码BT.601空间中的值,结果肯定会看起来变得苍白无力。


1
谢谢,这让我找到了正确的方向,并且我在这里找到了各种标准的清晰解释(矛盾修辞?)-> http://www.equasys.de/colorconversion.html - wim
@wim 我知道你已经很久没有看过这个了(我也是!),但是你的评论让我想起了这个:https://xkcd.com/927/ 顺便说一句,你的链接已经失效了,但是Wayback机器有一个副本:https://web.archive.org/web/20100122105857/http://www.equasys.de/colorconversion.html - Mark Ransom

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接