改变单个像素的颜色- Golang 图像

19

我希望打开JPEG图像文件,对其进行编码,更改一些像素颜色,然后将其保存为原样。

我想做类似于这样的事情。

imgfile, err := os.Open("unchanged.jpeg")
defer imgfile.Close()
if err != nil {
    fmt.Println(err.Error())
}

img,err := jpeg.Decode(imgfile)
if err != nil {
    fmt.Println(err.Error())
}
img.Set(0, 0, color.RGBA{85, 165, 34, 1})
img.Set(1,0,....)

outFile, _ := os.Create("changed.jpeg")
defer outFile.Close()
jpeg.Encode(outFile, img, nil)

由于编码图像文件后得到的默认图像类型没有Set方法,我无法提出可行的解决方案。

有人能解释一下如何做吗?非常感谢。


相关 / 可能是 在 Golang 中绘制一个矩形? 的副本。 - icza
公正地说,我不理解那个答案。即使我读了两遍,我也不知道如何使用那个Set方法。 - Matúš Čongrády
好的,下面是我的答案。 - icza
3个回答

23

在成功解码image.Decode()(以及特定的解码函数,如jpeg.Decode())后,会返回一个image.Image值。 image.Image是一个接口,它定义了图像的只读视图:不提供更改/绘制图像的方法。

image包提供了几个image.Image实现,允许您更改/绘制图像,通常使用Set(x, y int, c color.Color)方法。

然而,image.Decode()不能保证返回的图像将是image包中定义的任何图像类型之一,甚至可能动态类型的图像没有Set()方法(也可能有,但不保证)。已注册的自定义图像解码器可能会返回一个image.Image值,它是一种自定义实现(意味着不是image包中定义的图像类型)。

如果(动态类型的)图像确实具有Set()方法,则可以使用类型断言并使用其Set()方法进行绘制。以下是实现的方式:

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

imgfile, err := os.Open("unchanged.jpg")
if err != nil {
    panic(err.Error())
}
defer imgfile.Close()

img, err := jpeg.Decode(imgfile)
if err != nil {
    panic(err.Error())
}

if cimg, ok := img.(Changeable); ok {
    // cimg is of type Changeable, you can call its Set() method (draw on it)
    cimg.Set(0, 0, color.RGBA{85, 165, 34, 255})
    cimg.Set(0, 1, color.RGBA{255, 0, 0, 255})
    // when done, save img as usual
} else {
    // No luck... see your options below
}
如果图像没有Set()方法,你可以选择通过实现一个自定义类型来"覆盖其视图",该类型实现了image.Image接口,但在它的At(x,y int) color.Color方法中(返回/提供像素颜色),你要返回如果图像是可更改的所需设置的新颜色,并返回你不想更改图像的原始图像的像素。
最简单的实现image.Image接口的方法是利用嵌入,这样你只需要实现你想要的更改。以下是如何完成这个过程:
type MyImg struct {
    // Embed image.Image so MyImg will implement image.Image
    // because fields and methods of Image will be promoted:
    image.Image
}

func (m *MyImg) At(x, y int) color.Color {
    // "Changed" part: custom colors for specific coordinates:
    switch {
    case x == 0 && y == 0:
        return color.RGBA{85, 165, 34, 255}
    case x == 0 && y == 1:
        return color.RGBA{255, 0, 0, 255}
    }
    // "Unchanged" part: the colors of the original image:
    return m.Image.At(x, y)
}

使用它非常简单。像往常一样加载图像,但在保存时,提供我们的MyImg类型的值,它将负责在编码器请求时提供更改后的图像内容(颜色):

jpeg.Encode(outFile, &MyImg{img}, nil)

如果你需要改变很多像素,将所有的像素都包含在At()方法里是不切实际的。为此,我们可以扩展MyImg以拥有我们的Set()实现,它存储我们想要更改的像素。例如实现:

type MyImg struct {
    image.Image
    custom map[image.Point]color.Color
}

func NewMyImg(img image.Image) *MyImg {
    return &MyImg{img, map[image.Point]color.Color{}}
}

func (m *MyImg) Set(x, y int, c color.Color) {
    m.custom[image.Point{x, y}] = c
}

func (m *MyImg) At(x, y int) color.Color {
    // Explicitly changed part: custom colors of the changed pixels:
    if c := m.custom[image.Point{x, y}]; c != nil {
        return c
    }
    // Unchanged part: colors of the original image:
    return m.Image.At(x, y)
}

使用它:

// Load image as usual, then

my := NewMyImg(img)
my.Set(0, 0, color.RGBA{85, 165, 34, 1})
my.Set(0, 1, color.RGBA{255, 0, 0, 255})

// And when saving, save 'my' instead of the original:
jpeg.Encode(outFile, my, nil)

如果你需要改变许多像素,那么创建一个支持更改像素的新图像可能更具收益性,例如image.RGBA,将原始图像绘制在其上,然后继续更改所需的像素。

要将一幅图像绘制到另一幅图像上,可以使用image/draw包。

cimg := image.NewRGBA(img.Bounds())
draw.Draw(cimg, img.Bounds(), img, image.Point{}, draw.Over)

// Now you have cimg which contains the original image and is changeable
// (it has a Set() method)
cimg.Set(0, 0, color.RGBA{85, 165, 34, 255})
cimg.Set(0, 1, color.RGBA{255, 0, 0, 255})

// And when saving, save 'cimg' of course:
jpeg.Encode(outFile, cimg, nil)

上面的代码只是为了演示。在“现实生活”图像中,Image.Bounds() 可能返回一个不以 (0;0) 点开始的矩形,这种情况下可能需要进行一些调整才能使其正常工作。


即使查看了这些包的Go源代码,我也不知道您是如何确定可以在其上创建具有Set方法的接口的。您是如何得出这个结论的? - Justin Self
1
@JustinSelf image包中不同的具体image.Image实现均具有Set(x, y int, c color.Color)方法。 因此,标准库的图像解码函数很可能返回一个拥有此Set()方法的图像实现。这就是为什么我们应该首先进行检查的原因。如果图像没有Set()方法,那么我们可以随意命名自定义的Set()方法(只要我们自己调用即可),我只是出于清晰起见使用了相同的方法签名。 - icza
@icza 有些地方我不理解。发帖者说他是从文件中读取图像,但您的解决方案似乎使用了已经初始化的图像结构体,而不是从文件中解码得到的图像。例如,代码jpeg.Encode(outFile,&MyImg {img},nil)中的MyImg {img}似乎并不是从文件中解码得到的。请问您对应于发帖者的 img,err := jpeg.Decode(imgfile) 这一句在哪里? - bit
@icza 我好像错过了这个“使用它:非常简单。像往常一样加载图像,但在保存时提供我们的MyImg类型的值,它将负责在编码器要求时提供更改后的图像内容(颜色):”。此外,“draw.Draw(cimg,img.Bounds(),img,image.Point{},draw.Over)”似乎使用文件中的图像,但这仅适用于您的最后一种方法。 - bit
或者my := NewMyImg(img)中的img是在img,err := jpeg.Decode(imgfile)中创建的图像对象吗? - bit
@bit 是的,传递给 NewMyImg(img)img 和在复合字面量 &MyImg{img} 中使用的是从文件加载和解码的那个(原始图像)。 - icza

2
图像解码返回一个图像接口,该接口有一个Bounds方法,可获取图像像素的宽度和高度。
img, _, err := image.Decode(imgfile)
if err != nil {
    fmt.Println(err.Error())
}
size := img.Bounds().Size()

一旦你有了宽度和高度,你可以使用两个嵌套的for循环(一个用于x坐标,另一个用于y坐标)遍历像素。

for x := 0; x < size.X; x++ {
    for y := 0; y < size.Y; y++ {
        color := color.RGBA{
            uint8(255 * x / size.X),
            uint8(255 * y / size.Y),
            55,
            255}
        m.Set(x, y, color)
    }
}

当你完成图像处理后,可以对文件进行编码。但是由于image.Image没有Set方法,因此你可以创建一个新的RGBA图像,返回一个RGBA结构,在其上使用Set方法。

m := image.NewRGBA(image.Rect(0, 0, width, height))
outFile, err := os.Create("changed.jpg")
if err != nil {
    log.Fatal(err)
}
defer outFile.Close()
png.Encode(outFile, m)

我不能使用这个。我仍然无法使用Set方法,因为我的编码图像是image.Image类型,它没有Set方法。循环整个图像也不是我想要做的事情,我只需要改变一些像素。无论如何,感谢您的答案。 - Matúš Čongrády
要创建一个新的RGBA图像,可以使用Set方法返回RGBA接口,如下所示:m := image.NewRGBA(image.Rect(0, 0, width, height)) - Endre Simo
好的,我刚才想到我可以直接在编码图像上绘制像素。创建新图像并复制旧编码图像的内容,只是为了更改一些像素,对我来说似乎有点奇怪。 - Matúš Čongrády

0

image.Image 默认是不可变的,但 draw.Image 是可变的。

如果将其类型转换为 draw.Image,那么应该可以使用 Set 方法。

img.(draw.Image).Set(0,0, color.RGBA{85, 165, 34, 1})

4
我之前尝试过这个。我出现了这个错误:"*image.YCbCr不是draw.Image:缺少方法Set"。 - Matúš Čongrády

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接