【问题标题】:Unexpected/inaccurate image color conversions in GoGo 中意外/不准确的图像颜色转换
【发布时间】:2018-05-13 01:08:02
【问题描述】:

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

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

谁能为此提供一些智慧/解释?

使用 ImageMagick(“convert image.jpg image.txt”;左右 RGB 匹配,这里,仅供参考):

# 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 和其他颜色值必须相同,您可能需要使用替代实现。

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

【问题讨论】:

  • 我对颜色模型了解不多,但我会开始看这里:golang.org/src/image/jpeg/reader.go#L757 - 基于快速的一次检查,它似乎确实做了很多整数除法,所以与 IM/Python 的执行方式相比,可能存在一些舍入差异。不过,这纯粹是猜测。
  • @Adrian 好吧,解析像素所需的算术与将颜色从其基本表示形式转换为 RGB 所需的算术之间存在差异。在这种情况下,首先使用 JPEG 标准/流程规定的一种颜色编码(YCbCr,必须在您共享的 JPEG 特定逻辑中)表示图像,但我的问题是 how 它正在通过 Go 将其转换为 RGB,这里:golang.org/src/image/color/ycbcr.go(#58,虽然它后来也在同一个文件中被复制)
  • 从源代码看来,它在解码时转换为 RGB,但情况可能并非如此。无论如何,您链接的 ycbcr 转换与整数算术和舍入/截断具有相同的问题。
  • YCbCr 和 RGB 之间没有精确的转换,所以它总是依赖于实现。一些使用浮点值的实现以不同的精度开始,而其他实现则因用于计算的值的位大小而异(这会影响最终舍入)。如果 ImageMagick(或 GraphicsMagick)根据编译的量子返回略有不同的结果,我什至不会感到惊讶。

标签: image go jpeg python-imaging-library


【解决方案1】:

参见上面@JimB 的评论。显然,该规范没有涵盖这种特定的转换。因此,事实上,它可能因一种实现而异于下一种实现。

【讨论】:

    【解决方案2】:

    因此,问题不在于从 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 的“慢”的 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 函数,因为它们有不同的签名。

    【讨论】:

      猜你喜欢
      • 2020-07-20
      • 1970-01-01
      • 2011-06-23
      • 2018-04-08
      • 2019-12-29
      • 1970-01-01
      • 2021-02-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多