image - Go 中意外/不准确的图像颜色转换

标签 image go jpeg python-imaging-library

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

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

任何人都可以对此提供一些智慧/解释吗?

使用 ImageMagick(“转换 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 规范)以便它可以实现更快的整数数学,而不是 float 学,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;
    }
}

最佳答案

请参阅上面@JimB 的评论。显然,规范不包括这种特殊的转换。因此,实际上,一种实现方式与另一种实现方式可能不同。

关于image - Go 中意外/不准确的图像颜色转换,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47550838/

相关文章:

jquery - 我希望我的 body 背景图像每 5 秒淡入淡出一次,如果不是至少每 5 秒改变一次

python - 如何在图像中找到类似结构的表格

go - 如何扩展标准 Golang 未导出的结构类型?

Java 从 URL 读取 JPG 文件

javascript - 在客户端的 JavaScript 中访问 JPEG EXIF 旋转数据

android - 故意使用 setType() 只允许 jpeg 和 png 图像;不是 gif 图片

android - 动嘴动画

javascript - 进度条问题(使用onloadstart、onloadend、onloadprogress)

go - 如何比较公共(public)键的 map 并打印输出?

templates - Helm 模板在 map 上循环