doz95923
2017-11-29 10:43
浏览 175
已采纳

Go中意外的/不正确的图像颜色转换

It seems as if Go's conversion arithmetic from JPEG's YCbCr to RGBA is slightly off, based on Python's PIL and ImageMagick's color values, but I'm probably just overlooking something.

PIL's and IM's results are identical. For Go, I load the image, convert to a non-alpha-premultiplied model, and then access the fields directly instead of using the RGBA getter (which will multiply the alpha against the color components). Unfortunately, many of the individual component values are equal but most of the colors have at least one component that is +-1 off of the component in that same position in PIL/IM's results.

Can anyone lend some wisdom/interpretation to this?

With ImageMagick ("convert image.jpg image.txt"; the left and right RGBs match, here, 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)

With PIL:

(code)

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 = '({}, {}): [{} {} {}]
'.format(y, x, r, g, b)
                f.write(s)

if __name__ == '__main__':
    _main()

(output)

(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]

With Go:

(code)

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]
", y, x, c.R, c.G, c.B, c.A)
            g.Write([]byte(s))
        }
    }
}

(output)

(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]

EDIT: Oh, man.

The Go code seems to implement the YCbCr->RGB conversion in a totally different manner. Not only does it indicate that it does some minor rounding (which deviates from the JFIF specification) so that it can implement faster integer math, instead of floating point math, PIL/Pillow (and IM, by implication) uses lookup tables rather than actual arithmetic. This ultimately seems to imply that Go will never produce the same color values as the other implementations. If it's critical that the color values be identical between Go and the others, you might need to use an alternate implementation.

Go implementation:

(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, actually) implementation (uses lookup tables):

(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;
    }
}
  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 邀请回答

1条回答 默认 最新

  • doupi6737 2017-12-04 23:12
    已采纳

    See the comment by @JimB, above. Apparently the specification does not cover this particular conversion. So, it may, in fact, be different from one implementation to the next.

    点赞 打赏 评论

相关推荐 更多相似问题