Go中出现意外/不准确的图像颜色转换

时间:2017-11-29 10:43:24

标签: image go jpeg python-imaging-library

根据Python的PIL和ImageMagick的颜色值,似乎Go的从JPEG的YCbCr到RGBA的转换算法略有偏差,但我可能只是忽略了一些东西。

PIL和IM的结果完全相同。对于Go,我加载图像,转换为非alpha预乘模型,然后直接访问字段而不是使用RGBA getter(将alpha与颜色分量相乘)。不幸的是,许多单个组件值相等,但大多数颜色至少有一个组件在PIL / IM结果的相同位置偏离组件+ -1。

有人能为此提供一些智慧/解释吗?

使用ImageMagick(“convert image.jpg image.txt”;左右RGB匹配,此处为FYI):

# ImageMagick pixel enumeration: 100,67,255,srgb
0,0: (190,200,210)  #BEC8D2  srgb(190,200,210)
1,0: (193,203,213)  #C1CBD5  srgb(193,203,213)
2,0: (195,205,215)  #C3CDD7  srgb(195,205,215)
3,0: (195,205,215)  #C3CDD7  srgb(195,205,215)
4,0: (194,204,214)  #C2CCD6  srgb(194,204,214)
5,0: (195,205,215)  #C3CDD7  srgb(195,205,215)
6,0: (198,208,218)  #C6D0DA  srgb(198,208,218)
7,0: (200,210,220)  #C8D2DC  srgb(200,210,220)
8,0: (202,210,221)  #CAD2DD  srgb(202,210,221)
9,0: (203,211,222)  #CBD3DE  srgb(203,211,222)
10,0: (205,213,224)  #CDD5E0  srgb(205,213,224)
11,0: (208,217,226)  #D0D9E2  srgb(208,217,226)
12,0: (211,218,226)  #D3DAE2  srgb(211,218,226)
13,0: (213,220,228)  #D5DCE4  srgb(213,220,228)
14,0: (216,223,229)  #D8DFE5  srgb(216,223,229)
15,0: (217,224,230)  #D9E0E6  srgb(217,224,230)
16,0: (220,225,231)  #DCE1E7  srgb(220,225,231)
17,0: (221,226,232)  #DDE2E8  srgb(221,226,232)
18,0: (223,228,234)  #DFE4EA  srgb(223,228,234)
19,0: (224,229,235)  #E0E5EB  srgb(224,229,235)

使用PIL:

(代码)

import os

import PIL.Image as Image

def _main():
    image_filepath = 'image.jpg'
    output_filepath = image_filepath + '.python-dump'

    im = Image.open(image_filepath)
    width, height = im.size

    data = im.getdata()

    if os.path.exists(output_filepath):
        os.remove(output_filepath)

    with open(output_filepath, 'w') as f:
        for y in range(height):
            for x in range(width):
                r, g, b = data[y * im.size[0] + x]

                s = '({}, {}): [{} {} {}]\n'.format(y, x, r, g, b)
                f.write(s)

if __name__ == '__main__':
    _main()

(输出)

(0, 0): [190 200 210]
(0, 1): [193 203 213]
(0, 2): [195 205 215]
(0, 3): [195 205 215]
(0, 4): [194 204 214]
(0, 5): [195 205 215]
(0, 6): [198 208 218]
(0, 7): [200 210 220]
(0, 8): [202 210 221]
(0, 9): [203 211 222]
(0, 10): [205 213 224]
(0, 11): [208 217 226]
(0, 12): [211 218 226]
(0, 13): [213 220 228]
(0, 14): [216 223 229]
(0, 15): [217 224 230]
(0, 16): [220 225 231]
(0, 17): [221 226 232]
(0, 18): [223 228 234]
(0, 19): [224 229 235]

使用Go:

(代码)

package main

import (
    "os"
    "fmt"
    "image"
    "image/color"
    "reflect"

    _ "image/jpeg"
)

func main() {
    imageFilepath := "image.jpg"
    outputFilepath := imageFilepath + ".go-dump"

    f, err := os.Open(imageFilepath)
    if err != nil {
        panic(err)
    }

    defer f.Close()

    image, _, err := image.Decode(f)
    if err != nil {
        panic(err)
    }

    r := image.Bounds()
    width := r.Max.X
    height := r.Max.Y

    os.Remove(outputFilepath)

    g, err := os.OpenFile(outputFilepath, os.O_RDWR|os.O_CREATE, 0644)
    if err != nil {
        panic(err)
    }

    defer g.Close()

    for y := 0; y < height; y++ {
        for x := 0; x < width; x++ {
            p := image.At(x, y)
            c := color.NRGBAModel.Convert(p).(color.NRGBA)

            s := fmt.Sprintf("(%d, %d): [%d %d %d %d]\n", y, x, c.R, c.G, c.B, c.A)
            g.Write([]byte(s))
        }
    }
}

(输出)

(0, 0): [190 200 211 255]
(0, 1): [193 203 214 255]
(0, 2): [195 205 216 255]
(0, 3): [195 205 216 255]
(0, 4): [194 204 215 255]
(0, 5): [195 205 216 255]
(0, 6): [198 208 219 255]
(0, 7): [200 210 221 255]
(0, 8): [202 210 222 255]
(0, 9): [203 211 223 255]
(0, 10): [205 213 225 255]
(0, 11): [208 217 226 255]
(0, 12): [212 218 226 255]
(0, 13): [214 220 228 255]
(0, 14): [217 224 229 255]
(0, 15): [218 225 230 255]
(0, 16): [220 225 231 255]
(0, 17): [221 226 232 255]
(0, 18): [223 228 234 255]
(0, 19): [224 229 235 255]

编辑:哦,伙计。

Go代码似乎以完全不同的方式实现YCbCr-> RGB转换。它不仅表明它进行了一些小的舍入(偏离了JFIF规范),因此它可以实现更快的整数数学,而不是浮点数学,PIL / Pillow(和IM,暗示)使用查找表而不是实际的算术。这最终似乎意味着Go将永远不会产生与其他实现相同的颜色值。 如果Go与其他颜色值之间的颜色值相同至关重要,则可能需要使用替代实现。

执行:

https://golang.org/src/image/color/ycbcr.go

// YCbCrToRGB converts a Y'CbCr triple to an RGB triple.
func YCbCrToRGB(y, cb, cr uint8) (uint8, uint8, uint8) {
  // The JFIF specification says:
  //  R = Y' + 1.40200*(Cr-128)
  //  G = Y' - 0.34414*(Cb-128) - 0.71414*(Cr-128)
  //  B = Y' + 1.77200*(Cb-128)
  // http://www.w3.org/Graphics/JPEG/jfif3.pdf says Y but means Y'.
  //
  // Those formulae use non-integer multiplication factors. When computing,
  // integer math is generally faster than floating point math. We multiply
  // all of those factors by 1<<16 and round to the nearest integer:
  //   91881 = roundToNearestInteger(1.40200 * 65536).
  //   22554 = roundToNearestInteger(0.34414 * 65536).
  //   46802 = roundToNearestInteger(0.71414 * 65536).
  //  116130 = roundToNearestInteger(1.77200 * 65536).
  //
  // Adding a rounding adjustment in the range [0, 1<<16-1] and then shifting
  // right by 16 gives us an integer math version of the original formulae.
  //  R = (65536*Y' +  91881 *(Cr-128)                  + adjustment) >> 16
  //  G = (65536*Y' -  22554 *(Cb-128) - 46802*(Cr-128) + adjustment) >> 16
  //  B = (65536*Y' + 116130 *(Cb-128)                  + adjustment) >> 16
  // A constant rounding adjustment of 1<<15, one half of 1<<16, would mean
  // round-to-nearest when dividing by 65536 (shifting right by 16).
  // Similarly, a constant rounding adjustment of 0 would mean round-down.
  //
  // Defining YY1 = 65536*Y' + adjustment simplifies the formulae and
  // requires fewer CPU operations:
  //  R = (YY1 +  91881 *(Cr-128)                 ) >> 16
  //  G = (YY1 -  22554 *(Cb-128) - 46802*(Cr-128)) >> 16
  //  B = (YY1 + 116130 *(Cb-128)                 ) >> 16
  //
  // The inputs (y, cb, cr) are 8 bit color, ranging in [0x00, 0xff]. In this
  // function, the output is also 8 bit color, but in the related YCbCr.RGBA
  // method, below, the output is 16 bit color, ranging in [0x0000, 0xffff].
  // Outputting 16 bit color simply requires changing the 16 to 8 in the "R =
  // etc >> 16" equation, and likewise for G and B.
  //
  // As mentioned above, a constant rounding adjustment of 1<<15 is a natural
  // choice, but there is an additional constraint: if c0 := YCbCr{Y: y, Cb:
  // 0x80, Cr: 0x80} and c1 := Gray{Y: y} then c0.RGBA() should equal
  // c1.RGBA(). Specifically, if y == 0 then "R = etc >> 8" should yield
  // 0x0000 and if y == 0xff then "R = etc >> 8" should yield 0xffff. If we
  // used a constant rounding adjustment of 1<<15, then it would yield 0x0080
  // and 0xff80 respectively.
  //
  // Note that when cb == 0x80 and cr == 0x80 then the formulae collapse to:
  //  R = YY1 >> n
  //  G = YY1 >> n
  //  B = YY1 >> n
  // where n is 16 for this function (8 bit color output) and 8 for the
  // YCbCr.RGBA method (16 bit color output).
  //
  // The solution is to make the rounding adjustment non-constant, and equal
  // to 257*Y', which ranges over [0, 1<<16-1] as Y' ranges over [0, 255].
  // YY1 is then defined as:
  //  YY1 = 65536*Y' + 257*Y'
  // or equivalently:
  //  YY1 = Y' * 0x10101
  yy1 := int32(y) * 0x10101
  cb1 := int32(cb) - 128
  cr1 := int32(cr) - 128

  // The bit twiddling below is equivalent to
  //
  // r := (yy1 + 91881*cr1) >> 16
  // if r < 0 {
  //     r = 0
  // } else if r > 0xff {
  //     r = ^int32(0)
  // }
  //
  // but uses fewer branches and is faster.
  // Note that the uint8 type conversion in the return
  // statement will convert ^int32(0) to 0xff.
  // The code below to compute g and b uses a similar pattern.
  r := yy1 + 91881*cr1
  if uint32(r)&0xff000000 == 0 {
      r >>= 16
  } else {
      r = ^(r >> 31)
  }

  g := yy1 - 22554*cb1 - 46802*cr1
  if uint32(g)&0xff000000 == 0 {
      g >>= 16
  } else {
      g = ^(g >> 31)
  }

  b := yy1 + 116130*cb1
  if uint32(b)&0xff000000 == 0 {
      b >>= 16
  } else {
      b = ^(b >> 31)
  }

  return uint8(r), uint8(g), uint8(b)
}

PIL(Pillow,实际上)实现(使用查找表):

https://github.com/python-pillow/Pillow/blob/bb1b3a532ca3fef915f9cde17ba2227671ac691c/libImaging/ConvertYCbCr.c#L363

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;
    }
}

2 个答案:

答案 0 :(得分:0)

请参阅上面@JimB的评论。显然,该规范并未涵盖此特定转换。实际上,它可能与一个实现不同。

答案 1 :(得分:0)

所以,问题不在于从 YCbCr 到 RGB 的转换,而在于 二次采样的 Cr/Cb 值的重建。引用 from GitHub

<块引用>

当 Cr/Cb 被二次采样时(以较低分辨率存储;这通常打开 除非在非常高质量的设置下),色度样本网格的位置 JPEG 未指定相对于亮度样本的值。 JFIF 选择一个 约定,Adobe JPEG 使用另一个源自 MPEG 的数字视频 标准使用另一种。 (我不记得 Adob​​e 和 MPEG 约定同意)用于下采样和 重建也是完全未指定的。

Go 中的重构发生在 reconstructBlock function 内, 调用 Go 的 IDCT implementation。再次引用 GitHub:

<块引用>

文件中提到的是 MPEG-2 参考解码器的翻译。它 使用源自 Wang 因式分解(1984 年)的 IDCT 算法 具有 11 位小数精度的定点和一些的最终缩放 1/sqrt(2) 的结果,具有 8 位小数精度。

如前所述,stb_image 使用源自 IJG 的“slow”的 IDCT 算法,实际上并不慢;它基于 1989 年的算法 避免连续两次四舍五入的 Loeffler、Ligtenberg 和 Moschytz 在任何信号路径上相乘(Go 代码使用的 Wang 分解 确实,它在 x2/x4 中乘以低精度的 1/sqrt(2) 输出;降低精度是为了避免溢出)。 stb_image 代码 对其系数使用 12 位小数精度。

为了演示它的后果,我将使用它 sample image。对于此测试,我们将查看像素 X:15,Y:0。开始 使用 ImageMagick:

magick -colorspace YCbCr Lenna50.jpg Lenna50.txt

结果:

15,0: (156,105,175)  #9C69AF  ycbcr(156,105,175)

下一个 PIL,相同的结果:

from PIL import Image
im = Image.open('Lenna50.jpg').convert('YCbCr')
data = im.getdata()
x, y = 15, 0
print(data[x + y * im.size[0]]) # (155, 105, 175)

最后,你会发现三个值都相差一:

package main

import (
   "fmt"
   "image"
   "image/jpeg"
   "os"
)

func main() {
   f, err := os.Open("Lenna50.jpg")
   if err != nil {
      panic(err)
   }
   defer f.Close()
   pic, err := jpeg.Decode(f)
   if err != nil {
      panic(err)
   }
   p := pic.(*image.YCbCr).YCbCrAt(15, 0)
   fmt.Printf("%+v\n", p) // {Y:156 Cb:106 Cr:174}
}

事实上,这是从字面上的第一次提交记录 JPEG decoder

<块引用>

首先使用的逆DCT算法和Plan9的src/cmd/jpg一样, 与 libjpeg 的默认 IDCT 有不同的舍入误差 执行。注意libjpeg实际上有三种不同的IDCT 实现:一个浮点数和两个定点数。在这四个中, Plan9 似乎最容易理解,部分原因是它没有 #ifdef 或 C 宏。 [...] 与第一个原因的差异通常为零, 但有时在 YCbCr 空间中为 1(共 256 个),或在 RGB 空间中为两倍。

最终,解决此问题的方法是为 Go 实现更好的 JPEG IDCT。一世 没有真正找到任何第三方 Go 软件包,但我确实找到了一个 implementation in C。我能够在 Go 中reimplement it,但我 无法用新的函数替换当前的 idct 函数,因为它们 有不同的签名。