images2gif.py 38 KB

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