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