-
Notifications
You must be signed in to change notification settings - Fork 0
/
images2gif.py
217 lines (172 loc) · 6.24 KB
/
images2gif.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
""" MODULE images2gif
Provides a function (writeGif) to write animated gif from a series
of PIL images or numpy arrays.
This code is provided as is, and is free to use for all.
Almar Klein (June 2009)
- based on gifmaker (in the scripts folder of the source distribution of PIL)
- based on gif file structure as provided by wikipedia
"""
try:
import PIL
from PIL import Image, ImageChops
from PIL.GifImagePlugin import getheader, getdata
except ImportError:
PIL = None
try:
import numpy as np
except ImportError:
np = None
# getheader gives a 87a header and a color palette (two elements in a list).
# getdata()[0] gives the Image Descriptor up to (including) "LZW min code size".
# getdatas()[1:] is the image data itself in chuncks of 256 bytes (well
# technically the first byte says how many bytes follow, after which that
# amount (max 255) follows).
def intToBin(i):
""" Integer to two bytes """
# devide in two parts (bytes)
i1 = i % 256
i2 = int( i/256)
# make string (little endian)
return chr(i1) + chr(i2)
def getheaderAnim(im):
""" Animation header. To replace the getheader()[0] """
bb = "GIF89a"
bb += intToBin(im.size[0])
bb += intToBin(im.size[1])
bb += "\x87\x00\x00"
return bb
def getAppExt(loops=0):
""" Application extention. Part that secifies amount of loops.
if loops is 0, if goes on infinitely.
"""
bb = "\x21\xFF\x0B" # application extension
bb += "NETSCAPE2.0"
bb += "\x03\x01"
if loops == 0:
loops = 2**16-1
bb += intToBin(loops)
bb += '\x00' # end
return bb
def getGraphicsControlExt(duration=0.1):
""" Graphics Control Extension. A sort of header at the start of
each image. Specifies transparancy and duration. """
bb = '\x21\xF9\x04'
bb += '\x08' # no transparancy
bb += intToBin( int(duration*100) ) # in 100th of seconds
bb += '\x00' # no transparant color
bb += '\x00' # end
return bb
def _writeGifToFile(fp, images, durations, loops):
""" Given a set of images writes the bytes to the specified stream.
"""
# init
frames = 0
previous = None
for im in images:
if not previous:
# first image
# gather data
palette = getheader(im)[1]
data = getdata(im)
imdes, data = data[0], data[1:]
header = getheaderAnim(im)
appext = getAppExt(loops)
graphext = getGraphicsControlExt(durations[0])
# write global header
fp.write(header)
fp.write(palette)
fp.write(appext)
# write image
fp.write(graphext)
fp.write(imdes)
for d in data:
fp.write(d)
else:
# gather info (compress difference)
data = getdata(im)
imdes, data = data[0], data[1:]
graphext = getGraphicsControlExt(durations[frames])
# write image
fp.write(graphext)
fp.write(imdes)
for d in data:
fp.write(d)
# # delta frame - does not seem to work
# delta = ImageChops.subtract_modulo(im, previous)
# bbox = delta.getbbox()
#
# if bbox:
#
# # gather info (compress difference)
# data = getdata(im.crop(bbox), offset = bbox[:2])
# imdes, data = data[0], data[1:]
# graphext = getGraphicsControlExt(durations[frames])
#
# # write image
# fp.write(graphext)
# fp.write(imdes)
# for d in data:
# fp.write(d)
#
# else:
# # FIXME: what should we do in this case?
# pass
# prepare for next round
previous = im.copy()
frames = frames + 1
fp.write(";") # end gif
return frames
def writeGif(filename, images, duration=0.1, loops=0, dither=1):
""" writeGif(filename, images, duration=0.1, loops=0, dither=1)
Write an animated gif from the specified images.
images should be a list of numpy arrays of PIL images.
Numpy images of type float should have pixels between 0 and 1.
Numpy images of other types are expected to have values between 0 and 255.
"""
if PIL is None:
raise RuntimeError("Need PIL to write animated gif files.")
images2 = []
# convert to PIL
for im in images:
if isinstance(im,Image.Image):
images2.append( im.convert('P',dither=dither) )
elif np and isinstance(im, np.ndarray):
if im.dtype == np.uint8:
pass
elif im.dtype in [np.float32, np.float64]:
im = (im*255).astype(np.uint8)
else:
im = im.astype(np.uint8)
# convert
if len(im.shape)==3 and im.shape[2]==3:
im = Image.fromarray(im,'RGB').convert('P',dither=dither)
elif len(im.shape)==2:
im = Image.fromarray(im,'L').convert('P',dither=dither)
else:
raise ValueError("Array has invalid shape to be an image.")
images2.append(im)
else:
raise ValueError("Unknown image type.")
# check duration
if hasattr(duration, '__len__'):
if len(duration) == len(images2):
durations = [d for d in duration]
else:
raise ValueError("len(duration) doesn't match amount of images.")
else:
durations = [duration for im in images2]
# open file
fp = open(filename, 'wb')
# write
try:
n = _writeGifToFile(fp, images2, durations, loops)
print n, 'frames written'
finally:
fp.close()
if __name__ == '__main__':
im = np.zeros((200,200), dtype=np.uint8)
im[10:30,:] = 100
im[:,80:120] = 255
im[-50:-40,:] = 50
images = [im*1.0, im*0.8, im*0.6, im*0.4, im*0]
writeGif('lala3.gif',images, duration=0.5, dither=0)