images2gif.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122
  1. # Copyright (C) 2012, Almar Klein, Ant1, Marius van Voorden
  2. #
  3. # This code is subject to the (new) BSD license:
  4. #
  5. # Redistribution and use in source and binary forms, with or without
  6. # modification, are permitted provided that the following conditions are met:
  7. # * Redistributions of source code must retain the above copyright
  8. # notice, this list of conditions and the following disclaimer.
  9. # * Redistributions in binary form must reproduce the above copyright
  10. # notice, this list of conditions and the following disclaimer in the
  11. # documentation and/or other materials provided with the distribution.
  12. # * Neither the name of the <organization> nor the
  13. # names of its contributors may be used to endorse or promote products
  14. # derived from this software without specific prior written permission.
  15. #
  16. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  17. # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  18. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  19. # ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
  20. # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. """ Module images2gif
  27. Provides functionality for reading and writing animated GIF images.
  28. Use writeGif to write a series of numpy arrays or PIL images as an
  29. animated GIF. Use readGif to read an animated gif as a series of numpy
  30. arrays.
  31. Note that since July 2004, all patents on the LZW compression patent have
  32. expired. Therefore the GIF format may now be used freely.
  33. Acknowledgements:
  34. Many thanks to Ant1 for:
  35. * noting the use of "palette=PIL.Image.ADAPTIVE", which significantly
  36. improves the results.
  37. * the modifications to save each image with its own palette, or optionally
  38. the global palette (if its the same).
  39. Many thanks to Marius van Voorden for porting the NeuQuant quantization
  40. algorithm of Anthony Dekker to Python (See the NeuQuant class for its
  41. license).
  42. Many thanks to Alex Robinson for implementing the concept of subrectangles,
  43. which (depening on image content) can give a very significant reduction in
  44. file size.
  45. This code is based on gifmaker (in the scripts folder of the source
  46. distribution of PIL)
  47. Useful links:
  48. * http://tronche.com/computer-graphics/gif/
  49. * http://en.wikipedia.org/wiki/Graphics_Interchange_Format
  50. * http://www.w3.org/Graphics/GIF/spec-gif89a.txt
  51. """
  52. # todo: This module should be part of imageio (or at least based on)
  53. import os
  54. try:
  55. import PIL
  56. from PIL import Image
  57. pillow = True
  58. try:
  59. PIL.__version__ # test if user has Pillow or PIL
  60. except AttributeError:
  61. pillow = False
  62. from PIL.GifImagePlugin import getheader, getdata
  63. except ImportError:
  64. PIL = None
  65. try:
  66. import numpy as np
  67. except ImportError:
  68. np = None
  69. def get_cKDTree():
  70. try:
  71. from scipy.spatial import cKDTree
  72. except ImportError:
  73. cKDTree = None
  74. return cKDTree
  75. # getheader gives a 87a header and a color palette (two elements in a list)
  76. # getdata()[0] gives the Image Descriptor up to (including) "LZW min code size"
  77. # getdatas()[1:] is the image data itself in chuncks of 256 bytes (well
  78. # technically the first byte says how many bytes follow, after which that
  79. # amount (max 255) follows)
  80. def checkImages(images):
  81. """checkImages(images)
  82. Check numpy images and correct intensity range etc.
  83. The same for all movie formats.
  84. :param images:
  85. """
  86. # Init results
  87. images2 = []
  88. for im in images:
  89. if PIL and isinstance(im, PIL.Image.Image):
  90. # We assume PIL images are allright
  91. images2.append(im)
  92. elif np and isinstance(im, np.ndarray):
  93. # Check and convert dtype
  94. if im.dtype == np.uint8:
  95. images2.append(im) # Ok
  96. elif im.dtype in [np.float32, np.float64]:
  97. im = im.copy()
  98. im[im < 0] = 0
  99. im[im > 1] = 1
  100. im *= 255
  101. images2.append(im.astype(np.uint8))
  102. else:
  103. im = im.astype(np.uint8)
  104. images2.append(im)
  105. # Check size
  106. if im.ndim == 2:
  107. pass # ok
  108. elif im.ndim == 3:
  109. if im.shape[2] not in [3, 4]:
  110. raise ValueError("This array can not represent an image.")
  111. else:
  112. raise ValueError("This array can not represent an image.")
  113. else:
  114. raise ValueError("Invalid image type: " + str(type(im)))
  115. # Done
  116. return images2
  117. def intToBin(i):
  118. """Integer to two bytes"""
  119. # divide in two parts (bytes)
  120. i1 = i % 256
  121. i2 = int(i / 256)
  122. # make string (little endian)
  123. return chr(i1) + chr(i2)
  124. class GifWriter:
  125. """Class that contains methods for helping write the animated GIF file."""
  126. def getheaderAnim(self, im):
  127. """Get animation header. To replace PILs getheader()[0]
  128. :param im:
  129. """
  130. bb = "GIF89a"
  131. bb += intToBin(im.size[0])
  132. bb += intToBin(im.size[1])
  133. bb += "\x87\x00\x00"
  134. return bb
  135. def getImageDescriptor(self, im, xy=None):
  136. """Used for the local color table properties per image.
  137. Otherwise global color table applies to all frames irrespective of
  138. whether additional colors comes in play that require a redefined
  139. palette. Still a maximum of 256 color per frame, obviously.
  140. Written by Ant1 on 2010-08-22
  141. Modified by Alex Robinson in Janurari 2011 to implement subrectangles.
  142. :param im:
  143. :param xy:
  144. """
  145. # Defaule use full image and place at upper left
  146. if xy is None:
  147. xy = (0, 0)
  148. # Image separator,
  149. bb = "\x2C"
  150. # Image position and size
  151. bb += intToBin(xy[0]) # Left position
  152. bb += intToBin(xy[1]) # Top position
  153. bb += intToBin(im.size[0]) # image width
  154. bb += intToBin(im.size[1]) # image height
  155. # packed field: local color table flag1, interlace0, sorted table0,
  156. # reserved00, lct size111=7=2^(7 + 1)=256.
  157. bb += "\x87"
  158. # LZW min size code now comes later, beginning of [image data] blocks
  159. return bb
  160. def getAppExt(self, loops=float("inf")):
  161. """Application extension. This part specifies the amount of loops.
  162. If loops is 0 or inf, it goes on infinitely.
  163. :param float loops:
  164. """
  165. if loops == 0 or loops == float("inf"):
  166. loops = 2 ** 16 - 1
  167. # bb = ""
  168. # application extension should not be used
  169. # (the extension interprets zero loops
  170. # to mean an infinite number of loops)
  171. # Mmm, does not seem to work
  172. if True:
  173. bb = "\x21\xFF\x0B" # application extension
  174. bb += "NETSCAPE2.0"
  175. bb += "\x03\x01"
  176. bb += intToBin(loops)
  177. bb += "\x00" # end
  178. return bb
  179. def getGraphicsControlExt(self, duration=0.1, dispose=2):
  180. """Graphics Control Extension. A sort of header at the start of
  181. each image. Specifies duration and transparency.
  182. Dispose:
  183. * 0 - No disposal specified.
  184. * 1 - Do not dispose. The graphic is to be left in place.
  185. * 2 - Restore to background color. The area used by the graphic
  186. must be restored to the background color.
  187. * 3 - Restore to previous. The decoder is required to restore the
  188. area overwritten by the graphic with what was there prior to
  189. rendering the graphic.
  190. * 4-7 -To be defined.
  191. :param double duration:
  192. :param dispose:
  193. """
  194. bb = "\x21\xF9\x04"
  195. bb += chr((dispose & 3) << 2) # low bit 1 == transparency,
  196. # 2nd bit 1 == user input , next 3 bits, the low two of which are used,
  197. # are dispose.
  198. bb += intToBin(int(duration * 100)) # in 100th of seconds
  199. bb += "\x00" # no transparent color
  200. bb += "\x00" # end
  201. return bb
  202. def handleSubRectangles(self, images, subRectangles):
  203. """Handle the sub-rectangle stuff. If the rectangles are given by the
  204. user, the values are checked. Otherwise the subrectangles are
  205. calculated automatically.
  206. """
  207. if isinstance(subRectangles, (tuple, list)):
  208. # xy given directly
  209. # Check xy
  210. xy = subRectangles
  211. if xy is None:
  212. xy = (0, 0)
  213. if hasattr(xy, "__len__"):
  214. if len(xy) == len(images):
  215. xy = [xxyy for xxyy in xy]
  216. else:
  217. raise ValueError("len(xy) doesn't match amount of images.")
  218. else:
  219. xy = [xy for im in images]
  220. xy[0] = (0, 0)
  221. else:
  222. # Calculate xy using some basic image processing
  223. # Check Numpy
  224. if np is None:
  225. raise RuntimeError("Need Numpy to use auto-subRectangles.")
  226. # First make numpy arrays if required
  227. for i in range(len(images)):
  228. im = images[i]
  229. if isinstance(im, Image.Image):
  230. tmp = im.convert() # Make without palette
  231. a = np.asarray(tmp)
  232. if len(a.shape) == 0:
  233. raise MemoryError(
  234. "Too little memory to convert PIL image to array"
  235. )
  236. images[i] = a
  237. # Determine the sub rectangles
  238. images, xy = self.getSubRectangles(images)
  239. # Done
  240. return images, xy
  241. def getSubRectangles(self, ims):
  242. """getSubRectangles(ims)
  243. Calculate the minimal rectangles that need updating each frame.
  244. Returns a two-element tuple containing the cropped images and a
  245. list of x-y positions.
  246. Calculating the subrectangles takes extra time, obviously. However,
  247. if the image sizes were reduced, the actual writing of the GIF
  248. goes faster. In some cases applying this method produces a GIF faster.
  249. """
  250. # Check image count
  251. if len(ims) < 2:
  252. return ims, [(0, 0) for i in ims]
  253. # We need numpy
  254. if np is None:
  255. raise RuntimeError("Need Numpy to calculate sub-rectangles. ")
  256. # Prepare
  257. ims2 = [ims[0]]
  258. xy = [(0, 0)]
  259. # Iterate over images
  260. prev = ims[0]
  261. for im in ims[1:]:
  262. # Get difference, sum over colors
  263. diff = np.abs(im - prev)
  264. if diff.ndim == 3:
  265. diff = diff.sum(2)
  266. # Get begin and end for both dimensions
  267. X = np.argwhere(diff.sum(0))
  268. Y = np.argwhere(diff.sum(1))
  269. # Get rect coordinates
  270. if X.size and Y.size:
  271. x0, x1 = int(X[0]), int(X[-1] + 1)
  272. y0, y1 = int(Y[0]), int(Y[-1] + 1)
  273. else: # No change ... make it minimal
  274. x0, x1 = 0, 2
  275. y0, y1 = 0, 2
  276. # Cut out and store
  277. im2 = im[y0:y1, x0:x1]
  278. prev = im
  279. ims2.append(im2)
  280. xy.append((x0, y0))
  281. # Done
  282. # print('%1.2f seconds to determine subrectangles of %i images' %
  283. # (time.time()-t0, len(ims2)))
  284. return ims2, xy
  285. def convertImagesToPIL(self, images, dither, nq=0):
  286. """convertImagesToPIL(images, nq=0)
  287. Convert images to Paletted PIL images, which can then be
  288. written to a single animaged GIF.
  289. """
  290. # Convert to PIL images
  291. images2 = []
  292. for im in images:
  293. if isinstance(im, Image.Image):
  294. images2.append(im)
  295. elif np and isinstance(im, np.ndarray):
  296. if im.ndim == 3 and im.shape[2] == 3:
  297. im = Image.fromarray(im, "RGB")
  298. elif im.ndim == 3 and im.shape[2] == 4:
  299. im = Image.fromarray(im[:, :, :3], "RGB")
  300. elif im.ndim == 2:
  301. im = Image.fromarray(im, "L")
  302. images2.append(im)
  303. # Convert to paletted PIL images
  304. images, images2 = images2, []
  305. if nq >= 1:
  306. # NeuQuant algorithm
  307. for im in images:
  308. im = im.convert("RGBA") # NQ assumes RGBA
  309. nqInstance = NeuQuant(im, int(nq)) # Learn colors from image
  310. if dither:
  311. im = im.convert("RGB").quantize(palette=nqInstance.paletteImage())
  312. else:
  313. # Use to quantize the image itself
  314. im = nqInstance.quantize(im)
  315. images2.append(im)
  316. else:
  317. # Adaptive PIL algorithm
  318. AD = Image.ADAPTIVE
  319. for im in images:
  320. im = im.convert("P", palette=AD, dither=dither)
  321. images2.append(im)
  322. # Done
  323. return images2
  324. def writeGifToFile(self, fp, images, durations, loops, xys, disposes):
  325. """writeGifToFile(fp, images, durations, loops, xys, disposes)
  326. Given a set of images writes the bytes to the specified stream.
  327. Requires different handling of palette for PIL and Pillow:
  328. based on https://github.com/rec/echomesh/blob/master/
  329. code/python/external/images2gif.py
  330. """
  331. # Obtain palette for all images and count each occurrence
  332. palettes, occur = [], []
  333. for im in images:
  334. if not pillow:
  335. palette = getheader(im)[1]
  336. else:
  337. palette = getheader(im)[0][-1]
  338. if not palette:
  339. palette = im.palette.tobytes()
  340. palettes.append(palette)
  341. for palette in palettes:
  342. occur.append(palettes.count(palette))
  343. # Select most-used palette as the global one (or first in case no max)
  344. globalPalette = palettes[occur.index(max(occur))]
  345. # Init
  346. frames = 0
  347. firstFrame = True
  348. for im, palette in zip(images, palettes):
  349. if firstFrame:
  350. # Write header
  351. # Gather info
  352. header = self.getheaderAnim(im)
  353. appext = self.getAppExt(loops)
  354. # Write
  355. fp.write(header)
  356. fp.write(globalPalette)
  357. fp.write(appext)
  358. # Next frame is not the first
  359. firstFrame = False
  360. if True:
  361. # Write palette and image data
  362. # Gather info
  363. data = getdata(im)
  364. imdes, data = data[0], data[1:]
  365. graphext = self.getGraphicsControlExt(
  366. durations[frames], disposes[frames]
  367. )
  368. # Make image descriptor suitable for using 256 local color palette
  369. lid = self.getImageDescriptor(im, xys[frames])
  370. # Write local header
  371. if (palette != globalPalette) or (disposes[frames] != 2):
  372. # Use local color palette
  373. fp.write(graphext)
  374. fp.write(lid) # write suitable image descriptor
  375. fp.write(palette) # write local color table
  376. fp.write("\x08") # LZW minimum size code
  377. else:
  378. # Use global color palette
  379. fp.write(graphext)
  380. fp.write(imdes) # write suitable image descriptor
  381. # Write image data
  382. for d in data:
  383. fp.write(d)
  384. # Prepare for next round
  385. frames = frames + 1
  386. fp.write(";") # end gif
  387. return frames
  388. def writeGif(filename, images, duration=0.1, repeat=True, **kwargs):
  389. """Write an animated gif from the specified images.
  390. Depending on which PIL library is used, either writeGifVisvis or writeGifPillow
  391. is used here.
  392. :param str filename: the name of the file to write the image to.
  393. :param list images: should be a list consisting of PIL images or numpy
  394. arrays. The latter should be between 0 and 255 for
  395. integer types, and between 0 and 1 for float types.
  396. :param duration: scalar or list of scalars The duration for all frames, or
  397. (if a list) for each frame.
  398. :param repeat: bool or integer The amount of loops. If True, loops infinitetel
  399. :param kwargs: additional parameters for writeGifVisvis
  400. """
  401. if pillow:
  402. # Pillow >= 3.4.0 has animated GIF writing
  403. version = [int(i) for i in PIL.__version__.split(".")]
  404. if version[0] > 3 or (version[0] == 3 and version[1] >= 4):
  405. writeGifPillow(filename, images, duration, repeat)
  406. return
  407. # otherwise use the old one
  408. writeGifVisvis(filename, images, duration, repeat, **kwargs)
  409. def writeGifPillow(filename, images, duration=0.1, repeat=True):
  410. """Write an animated gif from the specified images.
  411. Uses native Pillow implementation, which is available since Pillow 3.4.0.
  412. :param str filename: the name of the file to write the image to.
  413. :param list images: should be a list consisting of PIL images or numpy
  414. arrays. The latter should be between 0 and 255 for
  415. integer types, and between 0 and 1 for float types.
  416. :param duration: scalar or list of scalars The duration for all frames, or
  417. (if a list) for each frame.
  418. :param repeat: bool or integer The amount of loops. If True, loops infinitetel
  419. """
  420. loop = 0 if repeat else 1
  421. quantized = []
  422. for im in images:
  423. quantized.append(im.quantize())
  424. quantized[0].save(
  425. filename,
  426. save_all=True,
  427. append_images=quantized[1:],
  428. loop=loop,
  429. duration=duration * 1000,
  430. )
  431. def writeGifVisvis(
  432. filename,
  433. images,
  434. duration=0.1,
  435. repeat=True,
  436. dither=False,
  437. nq=0,
  438. subRectangles=True,
  439. dispose=None,
  440. ):
  441. """Write an animated gif from the specified images.
  442. Uses VisVis implementation. Unfortunately it produces corrupted GIF
  443. with Pillow >= 3.4.0.
  444. :param str filename: the name of the file to write the image to.
  445. :param list images: should be a list consisting of PIL images or numpy
  446. arrays. The latter should be between 0 and 255 for
  447. integer types, and between 0 and 1 for float types.
  448. :param duration: scalar or list of scalars The duration for all frames, or
  449. (if a list) for each frame.
  450. :param repeat: bool or integer The amount of loops. If True, loops infinitetely.
  451. :param bool dither: whether to apply dithering
  452. :param int nq: If nonzero, applies the NeuQuant quantization algorithm to
  453. create the color palette. This algorithm is superior, but
  454. slower than the standard PIL algorithm. The value of nq is
  455. the quality parameter. 1 represents the best quality. 10 is
  456. in general a good tradeoff between quality and speed. When
  457. using this option, better results are usually obtained when
  458. subRectangles is False.
  459. :param subRectangles: False, True, or a list of 2-element tuples
  460. Whether to use sub-rectangles. If True, the minimal
  461. rectangle that is required to update each frame is
  462. automatically detected. This can give significant
  463. reductions in file size, particularly if only a part
  464. of the image changes. One can also give a list of x-y
  465. coordinates if you want to do the cropping yourself.
  466. The default is True.
  467. :param int dispose: how to dispose each frame. 1 means that each frame is
  468. to be left in place. 2 means the background color
  469. should be restored after each frame. 3 means the
  470. decoder should restore the previous frame. If
  471. subRectangles==False, the default is 2, otherwise it is 1.
  472. """
  473. # Check PIL
  474. if PIL is None:
  475. raise RuntimeError("Need PIL to write animated gif files.")
  476. # Check images
  477. images = checkImages(images)
  478. # Instantiate writer object
  479. gifWriter = GifWriter()
  480. # Check loops
  481. if repeat is False:
  482. loops = 1
  483. elif repeat is True:
  484. loops = 0 # zero means infinite
  485. else:
  486. loops = int(repeat)
  487. # Check duration
  488. if hasattr(duration, "__len__"):
  489. if len(duration) == len(images):
  490. duration = [d for d in duration]
  491. else:
  492. raise ValueError("len(duration) doesn't match amount of images.")
  493. else:
  494. duration = [duration for im in images]
  495. # Check subrectangles
  496. if subRectangles:
  497. images, xy = gifWriter.handleSubRectangles(images, subRectangles)
  498. defaultDispose = 1 # Leave image in place
  499. else:
  500. # Normal mode
  501. xy = [(0, 0) for im in images]
  502. defaultDispose = 2 # Restore to background color.
  503. # Check dispose
  504. if dispose is None:
  505. dispose = defaultDispose
  506. if hasattr(dispose, "__len__"):
  507. if len(dispose) != len(images):
  508. raise ValueError("len(xy) doesn't match amount of images.")
  509. else:
  510. dispose = [dispose for im in images]
  511. # Make images in a format that we can write easy
  512. images = gifWriter.convertImagesToPIL(images, dither, nq)
  513. # Write
  514. fp = open(filename, "wb")
  515. try:
  516. gifWriter.writeGifToFile(fp, images, duration, loops, xy, dispose)
  517. finally:
  518. fp.close()
  519. def readGif(filename, asNumpy=True):
  520. """Read images from an animated GIF file. Returns a list of numpy
  521. arrays, or, if asNumpy is false, a list if PIL images.
  522. """
  523. # Check PIL
  524. if PIL is None:
  525. raise RuntimeError("Need PIL to read animated gif files.")
  526. # Check Numpy
  527. if np is None:
  528. raise RuntimeError("Need Numpy to read animated gif files.")
  529. # Check whether it exists
  530. if not os.path.isfile(filename):
  531. raise IOError("File not found: " + str(filename))
  532. # Load file using PIL
  533. pilIm = PIL.Image.open(filename)
  534. pilIm.seek(0)
  535. # Read all images inside
  536. images = []
  537. try:
  538. while True:
  539. # Get image as numpy array
  540. tmp = pilIm.convert() # Make without palette
  541. a = np.asarray(tmp)
  542. if len(a.shape) == 0:
  543. raise MemoryError("Too little memory to convert PIL image to array")
  544. # Store, and next
  545. images.append(a)
  546. pilIm.seek(pilIm.tell() + 1)
  547. except EOFError:
  548. pass
  549. # Convert to normal PIL images if needed
  550. if not asNumpy:
  551. images2 = images
  552. images = []
  553. for im in images2:
  554. images.append(PIL.Image.fromarray(im))
  555. # Done
  556. return images
  557. class NeuQuant:
  558. """NeuQuant(image, samplefac=10, colors=256)
  559. samplefac should be an integer number of 1 or higher, 1
  560. being the highest quality, but the slowest performance.
  561. With avalue of 10, one tenth of all pixels are used during
  562. training. This value seems a nice tradeof between speed
  563. and quality.
  564. colors is the amount of colors to reduce the image to. This
  565. should best be a power of two.
  566. See also:
  567. http://members.ozemail.com.au/~dekker/NEUQUANT.HTML
  568. **License of the NeuQuant Neural-Net Quantization Algorithm**
  569. Copyright (c) 1994 Anthony Dekker
  570. Ported to python by Marius van Voorden in 2010
  571. NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994.
  572. See "Kohonen neural networks for optimal colour quantization"
  573. in "network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367.
  574. for a discussion of the algorithm.
  575. See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML
  576. Any party obtaining a copy of these files from the author, directly or
  577. indirectly, is granted, free of charge, a full and unrestricted
  578. irrevocable, world-wide, paid up, royalty-free, nonexclusive right and
  579. license to deal in this software and documentation files (the "Software"),
  580. including without limitation the rights to use, copy, modify, merge,
  581. publish, distribute, sublicense, and/or sell copies of the Software, and
  582. to permit persons who receive copies from any such party to do so, with
  583. the only requirement being that this copyright notice remain intact.
  584. """
  585. NCYCLES = None # Number of learning cycles
  586. NETSIZE = None # Number of colours used
  587. SPECIALS = None # Number of reserved colours used
  588. BGCOLOR = None # Reserved background colour
  589. CUTNETSIZE = None
  590. MAXNETPOS = None
  591. INITRAD = None # For 256 colours, radius starts at 32
  592. RADIUSBIASSHIFT = None
  593. RADIUSBIAS = None
  594. INITBIASRADIUS = None
  595. RADIUSDEC = None # Factor of 1/30 each cycle
  596. ALPHABIASSHIFT = None
  597. INITALPHA = None # biased by 10 bits
  598. GAMMA = None
  599. BETA = None
  600. BETAGAMMA = None
  601. network = None # The network itself
  602. colormap = None # The network itself
  603. netindex = None # For network lookup - really 256
  604. bias = None # Bias and freq arrays for learning
  605. freq = None
  606. pimage = None
  607. # Four primes near 500 - assume no image has a length so large
  608. # that it is divisible by all four primes
  609. PRIME1 = 499
  610. PRIME2 = 491
  611. PRIME3 = 487
  612. PRIME4 = 503
  613. MAXPRIME = PRIME4
  614. pixels = None
  615. samplefac = None
  616. a_s = None
  617. def setconstants(self, samplefac, colors):
  618. self.NCYCLES = 100 # Number of learning cycles
  619. self.NETSIZE = colors # Number of colours used
  620. self.SPECIALS = 3 # Number of reserved colours used
  621. self.BGCOLOR = self.SPECIALS - 1 # Reserved background colour
  622. self.CUTNETSIZE = self.NETSIZE - self.SPECIALS
  623. self.MAXNETPOS = self.NETSIZE - 1
  624. self.INITRAD = self.NETSIZE / 8 # For 256 colours, radius starts at 32
  625. self.RADIUSBIASSHIFT = 6
  626. self.RADIUSBIAS = 1 << self.RADIUSBIASSHIFT
  627. self.INITBIASRADIUS = self.INITRAD * self.RADIUSBIAS
  628. self.RADIUSDEC = 30 # Factor of 1/30 each cycle
  629. self.ALPHABIASSHIFT = 10 # Alpha starts at 1
  630. self.INITALPHA = 1 << self.ALPHABIASSHIFT # biased by 10 bits
  631. self.GAMMA = 1024.0
  632. self.BETA = 1.0 / 1024.0
  633. self.BETAGAMMA = self.BETA * self.GAMMA
  634. # The network itself
  635. self.network = np.empty((self.NETSIZE, 3), dtype="float64")
  636. # The network itself
  637. self.colormap = np.empty((self.NETSIZE, 4), dtype="int32")
  638. self.netindex = np.empty(256, dtype="int32") # For network lookup - really 256
  639. # Bias and freq arrays for learning
  640. self.bias = np.empty(self.NETSIZE, dtype="float64")
  641. self.freq = np.empty(self.NETSIZE, dtype="float64")
  642. self.pixels = None
  643. self.samplefac = samplefac
  644. self.a_s = {}
  645. def __init__(self, image, samplefac=10, colors=256):
  646. # Check Numpy
  647. if np is None:
  648. raise RuntimeError("Need Numpy for the NeuQuant algorithm.")
  649. # Check image
  650. if image.size[0] * image.size[1] < NeuQuant.MAXPRIME:
  651. raise IOError("Image is too small")
  652. if image.mode != "RGBA":
  653. raise IOError("Image mode should be RGBA.")
  654. # Initialize
  655. self.setconstants(samplefac, colors)
  656. self.pixels = np.fromstring(image.tobytes(), np.uint32)
  657. self.setUpArrays()
  658. self.learn()
  659. self.fix()
  660. self.inxbuild()
  661. def writeColourMap(self, rgb, outstream):
  662. for i in range(self.NETSIZE):
  663. bb = self.colormap[i, 0]
  664. gg = self.colormap[i, 1]
  665. rr = self.colormap[i, 2]
  666. outstream.write(rr if rgb else bb)
  667. outstream.write(gg)
  668. outstream.write(bb if rgb else rr)
  669. return self.NETSIZE
  670. def setUpArrays(self):
  671. self.network[0, 0] = 0.0 # Black
  672. self.network[0, 1] = 0.0
  673. self.network[0, 2] = 0.0
  674. self.network[1, 0] = 255.0 # White
  675. self.network[1, 1] = 255.0
  676. self.network[1, 2] = 255.0
  677. # RESERVED self.BGCOLOR # Background
  678. for i in range(self.SPECIALS):
  679. self.freq[i] = 1.0 / self.NETSIZE
  680. self.bias[i] = 0.0
  681. for i in range(self.SPECIALS, self.NETSIZE):
  682. p = self.network[i]
  683. p[:] = (255.0 * (i - self.SPECIALS)) / self.CUTNETSIZE
  684. self.freq[i] = 1.0 / self.NETSIZE
  685. self.bias[i] = 0.0
  686. # Omitted: setPixels
  687. def altersingle(self, alpha, i, b, g, r):
  688. """Move neuron i towards biased (b, g, r) by factor alpha"""
  689. n = self.network[i] # Alter hit neuron
  690. n[0] -= alpha * (n[0] - b)
  691. n[1] -= alpha * (n[1] - g)
  692. n[2] -= alpha * (n[2] - r)
  693. def geta(self, alpha, rad):
  694. try:
  695. return self.a_s[(alpha, rad)]
  696. except KeyError:
  697. length = rad * 2 - 1
  698. mid = length / 2
  699. q = np.array(list(range(mid - 1, -1, -1)) + list(range(-1, mid)))
  700. a = alpha * (rad * rad - q * q) / (rad * rad)
  701. a[mid] = 0
  702. self.a_s[(alpha, rad)] = a
  703. return a
  704. def alterneigh(self, alpha, rad, i, b, g, r):
  705. if i - rad >= self.SPECIALS - 1:
  706. lo = i - rad
  707. start = 0
  708. else:
  709. lo = self.SPECIALS - 1
  710. start = self.SPECIALS - 1 - (i - rad)
  711. if i + rad <= self.NETSIZE:
  712. hi = i + rad
  713. end = rad * 2 - 1
  714. else:
  715. hi = self.NETSIZE
  716. end = self.NETSIZE - (i + rad)
  717. a = self.geta(alpha, rad)[start:end]
  718. p = self.network[lo + 1 : hi]
  719. p -= np.transpose(np.transpose(p - np.array([b, g, r])) * a)
  720. # def contest(self, b, g, r):
  721. # """ Search for biased BGR values
  722. # Finds closest neuron (min dist) and updates self.freq
  723. # finds best neuron (min dist-self.bias) and returns position
  724. # for frequently chosen neurons, self.freq[i] is high and self.bias[i] is negative
  725. # self.bias[i] = self.GAMMA * ((1/self.NETSIZE)-self.freq[i])"""
  726. #
  727. # i, j = self.SPECIALS, self.NETSIZE
  728. # dists = abs(self.network[i:j] - np.array([b, g, r])).sum(1)
  729. # bestpos = i + np.argmin(dists)
  730. # biasdists = dists - self.bias[i:j]
  731. # bestbiaspos = i + np.argmin(biasdists)
  732. # self.freq[i:j] -= self.BETA * self.freq[i:j]
  733. # self.bias[i:j] += self.BETAGAMMA * self.freq[i:j]
  734. # self.freq[bestpos] += self.BETA
  735. # self.bias[bestpos] -= self.BETAGAMMA
  736. # return bestbiaspos
  737. def contest(self, b, g, r):
  738. """Search for biased BGR values
  739. Finds closest neuron (min dist) and updates self.freq
  740. finds best neuron (min dist-self.bias) and returns position
  741. for frequently chosen neurons, self.freq[i] is high and self.bias[i]
  742. is negative self.bias[i] = self.GAMMA * ((1/self.NETSIZE)-self.freq[i])
  743. """
  744. i, j = self.SPECIALS, self.NETSIZE
  745. dists = abs(self.network[i:j] - np.array([b, g, r])).sum(1)
  746. bestpos = i + np.argmin(dists)
  747. biasdists = dists - self.bias[i:j]
  748. bestbiaspos = i + np.argmin(biasdists)
  749. self.freq[i:j] *= 1 - self.BETA
  750. self.bias[i:j] += self.BETAGAMMA * self.freq[i:j]
  751. self.freq[bestpos] += self.BETA
  752. self.bias[bestpos] -= self.BETAGAMMA
  753. return bestbiaspos
  754. def specialFind(self, b, g, r):
  755. for i in range(self.SPECIALS):
  756. n = self.network[i]
  757. if n[0] == b and n[1] == g and n[2] == r:
  758. return i
  759. return -1
  760. def learn(self):
  761. biasRadius = self.INITBIASRADIUS
  762. alphadec = 30 + ((self.samplefac - 1) / 3)
  763. lengthcount = self.pixels.size
  764. samplepixels = lengthcount / self.samplefac
  765. delta = samplepixels / self.NCYCLES
  766. alpha = self.INITALPHA
  767. i = 0
  768. rad = biasRadius >> self.RADIUSBIASSHIFT
  769. if rad <= 1:
  770. rad = 0
  771. print(
  772. "Beginning 1D learning: samplepixels = %1.2f rad = %i"
  773. % (samplepixels, rad)
  774. )
  775. step = 0
  776. pos = 0
  777. if lengthcount % NeuQuant.PRIME1 != 0:
  778. step = NeuQuant.PRIME1
  779. elif lengthcount % NeuQuant.PRIME2 != 0:
  780. step = NeuQuant.PRIME2
  781. elif lengthcount % NeuQuant.PRIME3 != 0:
  782. step = NeuQuant.PRIME3
  783. else:
  784. step = NeuQuant.PRIME4
  785. i = 0
  786. printed_string = ""
  787. while i < samplepixels:
  788. if i % 100 == 99:
  789. tmp = "\b" * len(printed_string)
  790. printed_string = str((i + 1) * 100 / samplepixels) + "%\n"
  791. print(tmp + printed_string)
  792. p = self.pixels[pos]
  793. r = (p >> 16) & 0xFF
  794. g = (p >> 8) & 0xFF
  795. b = (p) & 0xFF
  796. if i == 0: # Remember background colour
  797. self.network[self.BGCOLOR] = [b, g, r]
  798. j = self.specialFind(b, g, r)
  799. if j < 0:
  800. j = self.contest(b, g, r)
  801. if j >= self.SPECIALS: # Don't learn for specials
  802. a = (1.0 * alpha) / self.INITALPHA
  803. self.altersingle(a, j, b, g, r)
  804. if rad > 0:
  805. self.alterneigh(a, rad, j, b, g, r)
  806. pos = (pos + step) % lengthcount
  807. i += 1
  808. if i % delta == 0:
  809. alpha -= alpha / alphadec
  810. biasRadius -= biasRadius / self.RADIUSDEC
  811. rad = biasRadius >> self.RADIUSBIASSHIFT
  812. if rad <= 1:
  813. rad = 0
  814. finalAlpha = (1.0 * alpha) / self.INITALPHA
  815. print("Finished 1D learning: final alpha = %1.2f!" % finalAlpha)
  816. def fix(self):
  817. for i in range(self.NETSIZE):
  818. for j in range(3):
  819. x = int(0.5 + self.network[i, j])
  820. x = max(0, x)
  821. x = min(255, x)
  822. self.colormap[i, j] = x
  823. self.colormap[i, 3] = i
  824. def inxbuild(self):
  825. previouscol = 0
  826. startpos = 0
  827. for i in range(self.NETSIZE):
  828. p = self.colormap[i]
  829. q = None
  830. smallpos = i
  831. smallval = p[1] # Index on g
  832. # Find smallest in i..self.NETSIZE-1
  833. for j in range(i + 1, self.NETSIZE):
  834. q = self.colormap[j]
  835. if q[1] < smallval: # Index on g
  836. smallpos = j
  837. smallval = q[1] # Index on g
  838. q = self.colormap[smallpos]
  839. # Swap p (i) and q (smallpos) entries
  840. if i != smallpos:
  841. p[:], q[:] = q, p.copy()
  842. # smallval entry is now in position i
  843. if smallval != previouscol:
  844. self.netindex[previouscol] = (startpos + i) >> 1
  845. for j in range(previouscol + 1, smallval):
  846. self.netindex[j] = i
  847. previouscol = smallval
  848. startpos = i
  849. self.netindex[previouscol] = (startpos + self.MAXNETPOS) >> 1
  850. for j in range(previouscol + 1, 256): # Really 256
  851. self.netindex[j] = self.MAXNETPOS
  852. def paletteImage(self):
  853. """PIL weird interface for making a paletted image: create an image
  854. which already has the palette, and use that in Image.quantize. This
  855. function returns this palette image."""
  856. if self.pimage is None:
  857. palette = []
  858. for i in range(self.NETSIZE):
  859. palette.extend(self.colormap[i][:3])
  860. palette.extend([0] * (256 - self.NETSIZE) * 3)
  861. # a palette image to use for quant
  862. self.pimage = Image.new("P", (1, 1), 0)
  863. self.pimage.putpalette(palette)
  864. return self.pimage
  865. def quantize(self, image):
  866. """Use a kdtree to quickly find the closest palette colors for the
  867. pixels
  868. :param image:
  869. """
  870. if get_cKDTree():
  871. return self.quantize_with_scipy(image)
  872. else:
  873. print("Scipy not available, falling back to slower version.")
  874. return self.quantize_without_scipy(image)
  875. def quantize_with_scipy(self, image):
  876. w, h = image.size
  877. px = np.asarray(image).copy()
  878. px2 = px[:, :, :3].reshape((w * h, 3))
  879. cKDTree = get_cKDTree()
  880. kdtree = cKDTree(self.colormap[:, :3], leafsize=10)
  881. result = kdtree.query(px2)
  882. colorindex = result[1]
  883. print("Distance: %1.2f" % (result[0].sum() / (w * h)))
  884. px2[:] = self.colormap[colorindex, :3]
  885. return Image.fromarray(px).convert("RGB").quantize(palette=self.paletteImage())
  886. def quantize_without_scipy(self, image):
  887. """ " This function can be used if no scipy is available.
  888. It's 7 times slower though.
  889. :param image:
  890. """
  891. w, h = image.size
  892. px = np.asarray(image).copy()
  893. memo = {}
  894. for j in range(w):
  895. for i in range(h):
  896. key = (px[i, j, 0], px[i, j, 1], px[i, j, 2])
  897. try:
  898. val = memo[key]
  899. except KeyError:
  900. val = self.convert(*key)
  901. memo[key] = val
  902. px[i, j, 0], px[i, j, 1], px[i, j, 2] = val
  903. return Image.fromarray(px).convert("RGB").quantize(palette=self.paletteImage())
  904. def convert(self, *color):
  905. i = self.inxsearch(*color)
  906. return self.colormap[i, :3]
  907. def inxsearch(self, r, g, b):
  908. """Search for BGR values 0..255 and return colour index"""
  909. dists = self.colormap[:, :3] - np.array([r, g, b])
  910. a = np.argmin((dists * dists).sum(1))
  911. return a
  912. if __name__ == "__main__":
  913. im = np.zeros((200, 200), dtype=np.uint8)
  914. im[10:30, :] = 100
  915. im[:, 80:120] = 255
  916. im[-50:-40, :] = 50
  917. images = [im * 1.0, im * 0.8, im * 0.6, im * 0.4, im * 0]
  918. writeGif("lala3.gif", images, duration=0.5, dither=0)