将RGBA图像转换为灰度图 Golang

huangapple go评论73阅读模式
英文:

Converting RGBA image to Grayscale Golang

问题

我目前正在开发一个程序,用于将RGBA图像转换为灰度图像。

我之前提出了一个问题,并得到了以下答案的指引-https://stackoverflow.com/questions/36573413/change-color-of-a-single-pixel-go-lang-image

这是我的原始问题-https://stackoverflow.com/questions/42513797/program-to-convert-rgba-to-grayscale-golang?noredirect=1#comment72166960_42513797

我已经编辑了我的代码,所以现在它成功运行了-然而输出的图像不是我想要的。它被转换为灰度图像,但像素都乱了,看起来像是旧电视上的噪音。

package main

import (
    "image"
    "image/color"
    "image/jpeg"
    "log"
    "os"
)

type ImageSet interface {
    Set(x, y int, c color.Color)
}


func main() {
    file, err := os.Open("flower.jpg")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    img, err := jpeg.Decode(file)
    if err != nil {
        log.Fatal(os.Stderr, "%s: %v\n", "flower.jpg", err)
    }

    b := img.Bounds()

    imgSet := image.NewRGBA(b)
    for y := 0; y < b.Max.Y; y++ {
        for x := 0; x < b.Max.X; x++ {
            oldPixel := img.At(x, y)
            r, g, b, a := oldPixel.RGBA()
            r = (r+g+b)/3
            pixel := color.RGBA{uint8(r), uint8(r), uint8(r), uint8(a)}
            imgSet.Set(x, y, pixel)
        }
    }

    outFile, err := os.Create("changed.jpg")
    if err != nil {
        log.Fatal(err)
    }
    defer outFile.Close()
    jpeg.Encode(outFile, imgSet, nil)

}

我知道我还没有添加用于检查图像是否可以接受Set()方法的if else语句,但是简单地创建一个新图像的建议似乎解决了这个问题。

非常感谢任何帮助。

编辑:

我已经添加了下面答案中建议的一些代码:

package main

import (
    "image"
    "image/color"
    "image/jpeg"
    "log"
    "os"
)

type ImageSet interface {
    Set(x, y int, c color.Color)
}


func main() {
    file, err := os.Open("flower.jpg")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    img, err := jpeg.Decode(file)
    if err != nil {
        log.Fatal(os.Stderr, "%s: %v\n", "flower.jpg", err)
    }

    b := img.Bounds()
    imgSet := image.NewRGBA(b)
    for y := 0; y < b.Max.Y; y++ {
        for x := 0; x < b.Max.X; x++ {
            oldPixel := img.At(x, y)
            r, g, b, _ := oldPixel.RGBA()
            y := 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)
            pixel := color.Gray{uint8(y / 256)}
            imgSet.Set(x, y, pixel)
        }
    }

    outFile, err := os.Create("changed.jpg")
    if err != nil {
        log.Fatal(err)
    }
    defer outFile.Close()
    jpeg.Encode(outFile, imgSet, nil)

}

我目前得到以下错误。

.\rgbtogray.go:36: cannot use y (type uint32) as type int in argument to imgSet.Set

我是否漏掉了答案中的某些内容?任何提示将不胜感激。

英文:

I'm currently working on a program to convert and RGBA image to grayscale.

I asked a question earlier and was directed to the following answer -
https://stackoverflow.com/questions/36573413/change-color-of-a-single-pixel-go-lang-image

Here is my original question - https://stackoverflow.com/questions/42513797/program-to-convert-rgba-to-grayscale-golang?noredirect=1#comment72166960_42513797

I have edited my code so it now successfully runs - however the image outputted is not what I want. It is converted to grayscale however the pixels are all messed up making it look like noise on an old TV.

package main
import (
&quot;image&quot;
&quot;image/color&quot;
&quot;image/jpeg&quot;
&quot;log&quot;
&quot;os&quot;
)
type ImageSet interface {
Set(x, y int, c color.Color)
}
func main() {
file, err := os.Open(&quot;flower.jpg&quot;)
if err != nil {
log.Fatal(err)
}
defer file.Close()
img, err := jpeg.Decode(file)
if err != nil {
log.Fatal(os.Stderr, &quot;%s: %v\n&quot;, &quot;flower.jpg&quot;, err)
}
b := img.Bounds()
imgSet := image.NewRGBA(b)
for y := 0; y &lt; b.Max.Y; y++ {
for x := 0; x &lt; b.Max.X; x++ {
oldPixel := img.At(x, y)
r, g, b, a:= oldPixel.RGBA()
r = (r+g+b)/3
pixel := color.RGBA{uint8(r), uint8(r), uint8(r), uint8(a)}
imgSet.Set(x, y, pixel)
}
}
outFile, err := os.Create(&quot;changed.jpg&quot;)
if err != nil {
log.Fatal(err)
}
defer outFile.Close()
jpeg.Encode(outFile, imgSet, nil)
}

I know I haven't added in the if else statement for checking if the image can accept the Set() method, however the suggestion for simply making a new image seems to have solved this.

Any help much appreciated.

Edit:

I've added in some suggested code from the answer below:

package main
import (
&quot;image&quot;
&quot;image/color&quot;
&quot;image/jpeg&quot;
&quot;log&quot;
&quot;os&quot;
)
type ImageSet interface {
Set(x, y int, c color.Color)
}
func main() {
file, err := os.Open(&quot;flower.jpg&quot;)
if err != nil {
log.Fatal(err)
}
defer file.Close()
img, err := jpeg.Decode(file)
if err != nil {
log.Fatal(os.Stderr, &quot;%s: %v\n&quot;, &quot;flower.jpg&quot;, err)
}
b := img.Bounds()
imgSet := image.NewRGBA(b)
for y := 0; y &lt; b.Max.Y; y++ {
for x := 0; x &lt; b.Max.X; x++ {
oldPixel := img.At(x, y)
r, g, b, _ := oldPixel.RGBA()
y := 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)
pixel := color.Gray{uint8(y / 256)}
imgSet.Set(x, y, pixel)
}
}
outFile, err := os.Create(&quot;changed.jpg&quot;)
if err != nil {
log.Fatal(err)
}
defer outFile.Close()
jpeg.Encode(outFile, imgSet, nil)
}

I currently get the following error.

.\rgbtogray.go:36: cannot use y (type uint32) as type int in argument to imgSet.Set

Am I missing something from the answer? Any tips appreciated.

答案1

得分: 35

Color.RGBA() 是一个方法,返回预乘 alpha 的红色、绿色、蓝色和 alpha 值,它们都是 uint32 类型,但只在范围 [0, 0xffff] 内(只使用 32 位中的 16 位)。这意味着你可以将这些分量相加,它们不会溢出(每个分量的最大值适合于 16 位,因此它们的和将适合于 32 位)。

这里需要注意的一点是,结果也将是预乘 alpha 的,并且在除以 3 后仍将在范围 [0..0xffff] 内。因此,通过进行 uint8(r) 类型转换,你只保留了最低的 8 位,与整数相比,它们似乎只是一个随机值。你应该选择最高的 8 位。

但是不要这么快。我们在这里要做的是将彩色图像转换为灰度图像,这将丢失“颜色”信息,我们想要的基本上是每个像素的亮度。你提出的解决方案称为“平均”方法,它给出的结果相当差,因为它以相等的权重取所有的 R、G 和 B 分量,尽管这些颜色具有不同的波长,因此对整体像素的亮度有不同的贡献。在这里阅读更多相关信息:灰度到 RGB 转换

对于实际的 RGB -> 灰度转换,必须使用以下权重:

Y = 0.299 * R +  0.587 * G + 0.114 * B

你可以在维基百科上阅读更多关于这些权重(和变体)的信息:灰度。这被称为“亮度”方法,这将给出最佳的灰度图像。

到目前为止,我们有了亮度,如何从这里转换为 color.Color 值?一种选择是使用 color.RGBA 颜色值,其中你为所有分量指定相同的亮度(可以保留 alpha)。如果你打算使用 image.NewRGBA() 返回的 image.RGBA,可能这是最好的方法,因为在设置颜色时不需要进行颜色转换(因为它与图像的颜色模型匹配)。

另一个诱人的选择是使用 color.Gray,它是一种颜色(实现了 color.Color 接口),并以我们现在拥有的方式模拟颜色:使用 Y,以 uint8 存储。另一种选择可能是 color.Gray16,它基本上是“相同的”,但使用 16 位来存储 Y,作为 uint16。对于这些,最好也使用具有匹配颜色模型的图像,例如 image.Grayimage.Gray16(尽管这不是必需的)。

因此,转换应该是:

oldPixel := img.At(x, y)
r, g, b, _ := oldPixel.RGBA()
lum := 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)
pixel := color.Gray{uint8(lum / 256)}
imgSet.Set(x, y, pixel)

请注意,我们需要将 R、G、B 分量转换为 float64,以便能够乘以权重。由于 rgb 已经是 uint32 类型,我们可以用整数操作替换它们(不会溢出)。

不详细介绍(因为标准库已经有了解决方案),这里是代码:

oldPixel := img.At(x, y)
r, g, b, _ := oldPixel.RGBA()
lum := (19595*r + 38470*g + 7471*b + 1<<15) >> 24
imgSet.Set(x, y, color.Gray{uint8(lum)})

现在,不需要编写这样“丑陋”的代码,推荐的方法是简单地使用 image/color 包的颜色转换器,称为 Model。准备好的 color.GrayModel 模型能够将任何颜色转换为 color.Gray 模型。

非常简单:

oldPixel := img.At(x, y)
pixel := color.GrayModel.Convert(oldPixel)
imgSet.Set(x, y, pixel)

它与我们最后一个加权模型的效果相同,使用整数运算。或者一行代码实现:

imgSet.Set(x, y, color.GrayModel.Convert(img.At(x, y)))

要获得更高的 16 位灰度分辨率:

imgSet.Set(x, y, color.Gray16Model.Convert(img.At(x, y)))

最后一点注意:由于你正在绘制由 image.NewRGBA() 返回的图像,它的类型是 *image.RGBA。你不需要检查它是否有 Set() 方法,因为 image.RGBA 是一个静态类型(不是接口),它确实有一个 Set() 方法,它在编译时进行检查。需要检查的情况是如果你有一个通用的 image.Image 类型的图像,它是一个接口,但该接口不包含/“规定”Set() 方法;但是实现该接口的动态类型可能仍然提供该方法。

英文:

Color.RGBA() is a method that returns the alpha-premultiplied red, green, blue and alpha values, all being of type uint32, but only being in the range of [0, 0xffff] (using only 16 bits out of 32). This means you can add these components, they will not overflow (max value of each component fits into 16 bits, so their sum will fit into 32 bits).

One thing to note here: the result will also be alpha-premultiplied, and after dividing by 3, it will still be in the range of [0..0xffff]. So by doing a uint8(r) type conversion, you're just keeping the lowest 8 bits, which will be seemingly just a random value compared to the whole number. You should rather take the highest 8 bits.

But not so fast. What we're trying to do here is convert a color image to a grayscale image, which will lose the "color" information, and what we want is basically the luminosity of each pixel. Your proposed solution is called the average method, and it gives rather poor result, because it takes all the R, G and B components with equal weight, even though these colors have different wavelength and thus contribute in different measures to the luminosity of the overall pixel. Read more about it here: Grayscale to RGB Conversion.

For a realistic RGB -> grayscale conversion, the following weights have to be used:

Y = 0.299 * R +  0.587 * G + 0.114 * B

You can read more behind these weights (and variants) on wikipedia: Grayscale. This is called the luminosity method, and this will give the best grayscale images.

So far so good, we have the luminosity, how do we go to a color.Color value from here? One option is to use a color.RGBA color value, where you specify the same luminosity to all components (alpha may be kept). And if you intend to use an image.RGBA returned by image.NewRGBA(), probably this is the best way as no color conversion will be needed when setting the color (as it matches the image's color model).

Another tempting choice is to use the color.Gray which is a color (implements the color.Color interface), and models a color just the way we have it now: with Y, stored using an uint8. An alternative could be color.Gray16 which is basically the "same", but uses 16 bits to store Y as an uint16. For these, best would be to also use an image with the matching color model, such as image.Gray or image.Gray16 (although this is not a requirement).

So the conversion should be:

oldPixel := img.At(x, y)
r, g, b, _ := oldPixel.RGBA()
lum := 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)
pixel := color.Gray{uint8(lum / 256)}
imgSet.Set(x, y, pixel)

Note that we needed to convert the R, G, B components to float64 to be able to multiply by the weights. Since r, g, b are already of type uint32, we could substitute this with integer operations (without overflow).

Without going into details –and because the standard lib already has a solution for this–, here it is:

oldPixel := img.At(x, y)
r, g, b, _ := oldPixel.RGBA()
lum := (19595*r + 38470*g + 7471*b + 1&lt;&lt;15) &gt;&gt; 24
imgSet.Set(x, y, color.Gray{uint8(lum)})

Now without writing such "ugly" things, the recommended way is to simply use the color converters of the image/color package, called Models. The prepared color.GrayModel model is able to convert any colors to the model of color.Gray.

It's this simple:

oldPixel := img.At(x, y)
pixel := color.GrayModel.Convert(oldPixel)
imgSet.Set(x, y, pixel)

It does the same as our last luminosity weighted model, using integer-arithmetic. Or in one line:

imgSet.Set(x, y, color.GrayModel.Convert(img.At(x, y)))

To have a higher, 16-bit grayscale resolution:

imgSet.Set(x, y, color.Gray16Model.Convert(img.At(x, y)))

One last note: since you're drawing on an image returned by image.NewRGBA(), it is of type *image.RGBA. You don't need to check if it has a Set() method, because image.RGBA is a static type (not interface), and it does have a Set() method, it is checked at compile time. The case when you do need to check is if you have an image of the general image.Image type which is an interface, but this interface does not contain / "prescribe" the Set() method; but the dynamic type implementing this interface may provide that nonetheless.

huangapple
  • 本文由 发表于 2017年3月1日 02:28:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/42516203.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定