根据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,实际上)实现(使用查找表):
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;
}
}
答案 0 :(得分:0)
请参阅上面@JimB的评论。显然,该规范并未涵盖此特定转换。实际上,它可能与一个实现不同。
答案 1 :(得分:0)
所以,问题不在于从 YCbCr 到 RGB 的转换,而在于 二次采样的 Cr/Cb 值的重建。引用 from GitHub:
<块引用>当 Cr/Cb 被二次采样时(以较低分辨率存储;这通常打开 除非在非常高质量的设置下),色度样本网格的位置 JPEG 未指定相对于亮度样本的值。 JFIF 选择一个 约定,Adobe JPEG 使用另一个源自 MPEG 的数字视频 标准使用另一种。 (我不记得 Adobe 和 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
函数,因为它们
有不同的签名。