如何将gif分割成图片

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

How to split gif into images

问题

你可以使用Go语言将GIF分割成图像。image/gif包中的DecodeAll函数返回一个GIF对象,其中包含一个调色板数组。但是,我不知道如何将每个调色板转换为图像。

英文:

How can I split gif into images in go?
image/gif's DecodeAll return GIF, which contains an array of palette. But don't know how to convert each palette into an image?

答案1

得分: 8

考虑以下内容:
帧可以包含透明像素或区域,一个很好的例子是维基百科上的这个图片(https://de.wikipedia.org/wiki/Graphics_Interchange_Format#/media/File:SmallFullColourGIF.gif),每帧(我猜)都有一个这样的全彩块,其余部分是透明的。

这给你带来了一个问题:特别是对于不使用多个帧创建真彩静态图像的动画GIF,DecodeAll返回的帧与你在浏览器中打开图像时实际看到的内容是不同的。

你需要以与浏览器相同的方式处理图像,即在一种画布上保留旧帧并用新帧进行覆盖。但是这并不总是正确的。据我所知,GIF帧可以包含一个处理方法,指定如何(或是否)应该处理该帧。

无论如何,为了达到你的目的,在大多数情况下最简单的方法是像下面这样:

import (
	"errors"
	"fmt"
	"image"
	"image/draw"
	"image/gif"
	"image/png"
	"io"
	"os"
)

// Decode reads and analyzes the given reader as a GIF image
func SplitAnimatedGIF(reader io.Reader) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("解码时出错:%s", r)
		}
	}()

	gif, err := gif.DecodeAll(reader)

	if err != nil {
		return err
	}

	imgWidth, imgHeight := getGifDimensions(gif)

	overpaintImage := image.NewRGBA(image.Rect(0, 0, imgWidth, imgHeight))
	draw.Draw(overpaintImage, overpaintImage.Bounds(), gif.Image[0], image.ZP, draw.Src)

	for i, srcImg := range gif.Image {
		draw.Draw(overpaintImage, overpaintImage.Bounds(), srcImg, image.ZP, draw.Over)

		// 保存当前帧“堆栈”。这将覆盖同名的现有文件
		file, err := os.Create(fmt.Sprintf("<某个路径>%d.png", i))
		if err != nil {
			return err
		}

		err = png.Encode(file, overpaintImage)
		if err != nil {
			return err
		}

		file.Close()
	}

	return nil
}

func getGifDimensions(gif *gif.GIF) (x, y int) {
	var lowestX int
	var lowestY int
	var highestX int
	var highestY int

	for _, img := range gif.Image {
		if img.Rect.Min.X < lowestX {
			lowestX = img.Rect.Min.X
		}
		if img.Rect.Min.Y < lowestY {
			lowestY = img.Rect.Min.Y
		}
		if img.Rect.Max.X > highestX {
			highestX = img.Rect.Max.X
		}
		if img.Rect.Max.Y > highestY {
			highestY = img.Rect.Max.Y
		}
	}

	return highestX - lowestX, highestY - lowestY
}

请注意,gif.DecodeAll经常会引发恐慌,因为互联网上的许多GIF图像都有些问题。你的浏览器会尝试解码它们,并且会用黑色替换丢失的颜色。image/gif不会这样做,而是引发恐慌。这就是为什么我们使用deferrecover的原因。

另外,我使用getGifDimensions是出于与上述相似的原因:单个帧可能不是你在浏览器中看到的内容。在这种情况下,帧只是完整图像的一部分小得多,这就是为什么我们必须遍历所有帧并获取图像的“真实”尺寸的原因。

如果你真的非常想做得正确,你应该阅读GIF规范GIF87aGIF89a以及类似这篇文章的内容,后者更容易理解。从中,你应该决定如何处理帧并在覆盖时处理透明度。

编辑:你可以很容易地观察到前面提到的一些效果,如果你拆分一些在线的GIF,例如这个这个 - 尝试调整“忽略优化”和“使用前一帧的细节重新绘制每一帧”来看看我是什么意思。

英文:

Consider the following:
Frames can contain transparent pixels or areas, a good example is this image on wikipedia which (I guess) has one of these full-color blocks per frame and the rest of the frame transparent.

This introduces a problem for you: Especially with animated GIFs, that do not use multiple frames to create a true-colored static image, the frames that DecodeAll returns are not what you actually see if you, for example, open the image in your browser.

You'll have to process the image in the same way your browser would, i.e. leave the old frames on a kind of canvas and overpaint with the new frame. BUT this is not always true. GIF frames can, AFAIK, contain a disposal method, specifying how (or if?) you should dispose of the frame.

Anyways, to get to your point, the most simple approach that will also work in most cases is something like

import (
&quot;errors&quot;
&quot;fmt&quot;
&quot;image&quot;
&quot;image/draw&quot;
&quot;image/gif&quot;
&quot;image/png&quot;
&quot;io&quot;
&quot;os&quot;
)
// Decode reads and analyzes the given reader as a GIF image
func SplitAnimatedGIF(reader io.Reader) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf(&quot;Error while decoding: %s&quot;, r)
}
}()
gif, err := gif.DecodeAll(reader)
if err != nil {
return err
}
imgWidth, imgHeight := getGifDimensions(gif)
overpaintImage := image.NewRGBA(image.Rect(0, 0, imgWidth, imgHeight))
draw.Draw(overpaintImage, overpaintImage.Bounds(), gif.Image[0], image.ZP, draw.Src)
for i, srcImg := range gif.Image {
draw.Draw(overpaintImage, overpaintImage.Bounds(), srcImg, image.ZP, draw.Over)
// save current frame &quot;stack&quot;. This will overwrite an existing file with that name
file, err := os.Create(fmt.Sprintf(&quot;%s%d%s&quot;, &quot;&lt;some path&gt;&quot;, i, &quot;.png&quot;))
if err != nil {
return err
}
err = png.Encode(file, overpaintImage)
if err != nil {
return err
}
file.Close()
}
return nil
}
func getGifDimensions(gif *gif.GIF) (x, y int) {
var lowestX int
var lowestY int
var highestX int
var highestY int
for _, img := range gif.Image {
if img.Rect.Min.X &lt; lowestX {
lowestX = img.Rect.Min.X
}
if img.Rect.Min.Y &lt; lowestY {
lowestY = img.Rect.Min.Y
}
if img.Rect.Max.X &gt; highestX {
highestX = img.Rect.Max.X
}
if img.Rect.Max.Y &gt; highestY {
highestY = img.Rect.Max.Y
}
}
return highestX - lowestX, highestY - lowestY
}

(untested, but should work)

Note that gif.DecodeAll can and will panic frequently, because a lot of the GIF images on the internet are somewhat broken. Your browser tries to decode them and will, for example, replace missing colors with black. image/gif will not do that, but panic instead. That's why we defer the recover.

Also, I used the getGifDimensions for a similar reason as stated above: single frames need not be what you see in your browser. In this case, the frames are just smaller than the complete image, that's why we have to iterate over all frames and get the "true" dimensions of the image.

If you really really want to do it right, you should probably read the GIF spec GIF87a, GIF89a and something like this article which is a lot easier to understand. From that, you should decide how to dispose of the frames and what to do with transparency while overpainting.

EDIT: Some of the effects mentioned earlier can be observed easily if you split some GIFs online, for example this or this - play around with "Ignore optimizations" and "Redraw every frame with details from previous frames" to see what I mean.

答案2

得分: 1

image.Image 是一个接口,*image.Paletted 实现了这个接口,所以例如如果你想将 GIF 的每一帧保存为 PNG 文件,你可以对每个图像进行编码:

for i, frame := range img.Image {
    frameFile, err := os.OpenFile(fmt.Sprintf("%d.png", i+1), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
    if err != nil {
        log.Fatal(err)
    }
    err = png.Encode(frameFile, frame)
    if err != nil {
        log.Fatal(err)
    }
    // 在这里不使用 defer,因为我们在一个循环中,而不是一个函数中。
    frameFile.Close()
}
英文:

image.Image is an interface, and *image.Paletted implements the interface, so for example if you want to save every frame of a GIF into a PNG file, you can just encode every image:

for i, frame := range img.Image {
frameFile, err := os.OpenFile(fmt.Sprintf(&quot;%d.png&quot;, i+1), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
err = png.Encode(frameFile, frame)
if err != nil {
log.Fatal(err)
}
// Not using defer here because we&#39;re in a loop, not a function.
frameFile.Close()
}

huangapple
  • 本文由 发表于 2015年10月23日 12:43:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/33295023.html
匿名

发表评论

匿名网友

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

确定