freeimagemulti.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. # -*- coding: utf-8 -*-
  2. # imageio is distributed under the terms of the (new) BSD License.
  3. """Plugin for multi-image freeimafe formats, like animated GIF and ico."""
  4. import logging
  5. import numpy as np
  6. from ..core import Format, image_as_uint
  7. from ._freeimage import fi, IO_FLAGS
  8. from .freeimage import FreeimageFormat
  9. logger = logging.getLogger(__name__)
  10. class FreeimageMulti(FreeimageFormat):
  11. """Base class for freeimage formats that support multiple images."""
  12. _modes = "iI"
  13. _fif = -1
  14. class Reader(Format.Reader):
  15. def _open(self, flags=0):
  16. flags = int(flags)
  17. # Create bitmap
  18. self._bm = fi.create_multipage_bitmap(
  19. self.request.filename, self.format.fif, flags
  20. )
  21. self._bm.load_from_filename(self.request.get_local_filename())
  22. def _close(self):
  23. self._bm.close()
  24. def _get_length(self):
  25. return len(self._bm)
  26. def _get_data(self, index):
  27. sub = self._bm.get_page(index)
  28. try:
  29. return sub.get_image_data(), sub.get_meta_data()
  30. finally:
  31. sub.close()
  32. def _get_meta_data(self, index):
  33. index = index or 0
  34. if index < 0 or index >= len(self._bm):
  35. raise IndexError()
  36. sub = self._bm.get_page(index)
  37. try:
  38. return sub.get_meta_data()
  39. finally:
  40. sub.close()
  41. # --
  42. class Writer(FreeimageFormat.Writer):
  43. def _open(self, flags=0):
  44. # Set flags
  45. self._flags = flags = int(flags)
  46. # Instantiate multi-page bitmap
  47. self._bm = fi.create_multipage_bitmap(
  48. self.request.filename, self.format.fif, flags
  49. )
  50. self._bm.save_to_filename(self.request.get_local_filename())
  51. def _close(self):
  52. # Close bitmap
  53. self._bm.close()
  54. def _append_data(self, im, meta):
  55. # Prepare data
  56. if im.ndim == 3 and im.shape[-1] == 1:
  57. im = im[:, :, 0]
  58. im = image_as_uint(im, bitdepth=8)
  59. # Create sub bitmap
  60. sub1 = fi.create_bitmap(self._bm._filename, self.format.fif)
  61. # Let subclass add data to bitmap, optionally return new
  62. sub2 = self._append_bitmap(im, meta, sub1)
  63. # Add
  64. self._bm.append_bitmap(sub2)
  65. sub2.close()
  66. if sub1 is not sub2:
  67. sub1.close()
  68. def _append_bitmap(self, im, meta, bitmap):
  69. # Set data
  70. bitmap.allocate(im)
  71. bitmap.set_image_data(im)
  72. bitmap.set_meta_data(meta)
  73. # Return that same bitmap
  74. return bitmap
  75. def _set_meta_data(self, meta):
  76. pass # ignore global meta data
  77. class MngFormat(FreeimageMulti):
  78. """An Mng format based on the Freeimage library.
  79. Read only. Seems broken.
  80. """
  81. _fif = 6
  82. def _can_write(self, request): # pragma: no cover
  83. return False
  84. class IcoFormat(FreeimageMulti):
  85. """An ICO format based on the Freeimage library.
  86. This format supports grayscale, RGB and RGBA images.
  87. The freeimage plugin requires a `freeimage` binary. If this binary
  88. is not available on the system, it can be downloaded by either
  89. - the command line script ``imageio_download_bin freeimage``
  90. - the Python method ``imageio.plugins.freeimage.download()``
  91. Parameters for reading
  92. ----------------------
  93. makealpha : bool
  94. Convert to 32-bit and create an alpha channel from the AND-
  95. mask when loading. Default False. Note that this returns wrong
  96. results if the image was already RGBA.
  97. """
  98. _fif = 1
  99. class Reader(FreeimageMulti.Reader):
  100. def _open(self, flags=0, makealpha=False):
  101. # Build flags from kwargs
  102. flags = int(flags)
  103. if makealpha:
  104. flags |= IO_FLAGS.ICO_MAKEALPHA
  105. return FreeimageMulti.Reader._open(self, flags)
  106. class GifFormat(FreeimageMulti):
  107. """A format for reading and writing static and animated GIF, based
  108. on the Freeimage library.
  109. Images read with this format are always RGBA. Currently,
  110. the alpha channel is ignored when saving RGB images with this
  111. format.
  112. The freeimage plugin requires a `freeimage` binary. If this binary
  113. is not available on the system, it can be downloaded by either
  114. - the command line script ``imageio_download_bin freeimage``
  115. - the Python method ``imageio.plugins.freeimage.download()``
  116. Parameters for reading
  117. ----------------------
  118. playback : bool
  119. 'Play' the GIF to generate each frame (as 32bpp) instead of
  120. returning raw frame data when loading. Default True.
  121. Parameters for saving
  122. ---------------------
  123. loop : int
  124. The number of iterations. Default 0 (meaning loop indefinitely)
  125. duration : {float, list}
  126. The duration (in seconds) of each frame. Either specify one value
  127. that is used for all frames, or one value for each frame.
  128. Note that in the GIF format the duration/delay is expressed in
  129. hundredths of a second, which limits the precision of the duration.
  130. fps : float
  131. The number of frames per second. If duration is not given, the
  132. duration for each frame is set to 1/fps. Default 10.
  133. palettesize : int
  134. The number of colors to quantize the image to. Is rounded to
  135. the nearest power of two. Default 256.
  136. quantizer : {'wu', 'nq'}
  137. The quantization algorithm:
  138. * wu - Wu, Xiaolin, Efficient Statistical Computations for
  139. Optimal Color Quantization
  140. * nq (neuqant) - Dekker A. H., Kohonen neural networks for
  141. optimal color quantization
  142. subrectangles : bool
  143. If True, will try and optimize the GIF by storing only the
  144. rectangular parts of each frame that change with respect to the
  145. previous. Unfortunately, this option seems currently broken
  146. because FreeImage does not handle DisposalMethod correctly.
  147. Default False.
  148. """
  149. _fif = 25
  150. class Reader(FreeimageMulti.Reader):
  151. def _open(self, flags=0, playback=True):
  152. # Build flags from kwargs
  153. flags = int(flags)
  154. if playback:
  155. flags |= IO_FLAGS.GIF_PLAYBACK
  156. FreeimageMulti.Reader._open(self, flags)
  157. def _get_data(self, index):
  158. im, meta = FreeimageMulti.Reader._get_data(self, index)
  159. # im = im[:, :, :3] # Drop alpha channel
  160. return im, meta
  161. # -- writer
  162. class Writer(FreeimageMulti.Writer):
  163. # todo: subrectangles
  164. # todo: global palette
  165. def _open(
  166. self,
  167. flags=0,
  168. loop=0,
  169. duration=None,
  170. fps=10,
  171. palettesize=256,
  172. quantizer="Wu",
  173. subrectangles=False,
  174. ):
  175. # Check palettesize
  176. if palettesize < 2 or palettesize > 256:
  177. raise ValueError("GIF quantize param must be 2..256")
  178. if palettesize not in [2, 4, 8, 16, 32, 64, 128, 256]:
  179. palettesize = 2 ** int(np.log2(128) + 0.999)
  180. logger.warning(
  181. "Warning: palettesize (%r) modified to a factor of "
  182. "two between 2-256." % palettesize
  183. )
  184. self._palettesize = palettesize
  185. # Check quantizer
  186. self._quantizer = {"wu": 0, "nq": 1}.get(quantizer.lower(), None)
  187. if self._quantizer is None:
  188. raise ValueError('Invalid quantizer, must be "wu" or "nq".')
  189. # Check frametime
  190. if duration is None:
  191. self._frametime = [int(1000 / float(fps) + 0.5)]
  192. elif isinstance(duration, list):
  193. self._frametime = [int(1000 * d) for d in duration]
  194. elif isinstance(duration, (float, int)):
  195. self._frametime = [int(1000 * duration)]
  196. else:
  197. raise ValueError("Invalid value for duration: %r" % duration)
  198. # Check subrectangles
  199. self._subrectangles = bool(subrectangles)
  200. self._prev_im = None
  201. # Init
  202. FreeimageMulti.Writer._open(self, flags)
  203. # Set global meta data
  204. self._meta = {}
  205. self._meta["ANIMATION"] = {
  206. # 'GlobalPalette': np.array([0]).astype(np.uint8),
  207. "Loop": np.array([loop]).astype(np.uint32),
  208. # 'LogicalWidth': np.array([x]).astype(np.uint16),
  209. # 'LogicalHeight': np.array([x]).astype(np.uint16),
  210. }
  211. def _append_bitmap(self, im, meta, bitmap):
  212. # Prepare meta data
  213. meta = meta.copy()
  214. meta_a = meta["ANIMATION"] = {}
  215. # If this is the first frame, assign it our "global" meta data
  216. if len(self._bm) == 0:
  217. meta.update(self._meta)
  218. meta_a = meta["ANIMATION"]
  219. # Set frame time
  220. index = len(self._bm)
  221. if index < len(self._frametime):
  222. ft = self._frametime[index]
  223. else:
  224. ft = self._frametime[-1]
  225. meta_a["FrameTime"] = np.array([ft]).astype(np.uint32)
  226. # Check array
  227. if im.ndim == 3 and im.shape[-1] == 4:
  228. im = im[:, :, :3]
  229. # Process subrectangles
  230. im_uncropped = im
  231. if self._subrectangles and self._prev_im is not None:
  232. im, xy = self._get_sub_rectangles(self._prev_im, im)
  233. meta_a["DisposalMethod"] = np.array([1]).astype(np.uint8)
  234. meta_a["FrameLeft"] = np.array([xy[0]]).astype(np.uint16)
  235. meta_a["FrameTop"] = np.array([xy[1]]).astype(np.uint16)
  236. self._prev_im = im_uncropped
  237. # Set image data
  238. sub2 = sub1 = bitmap
  239. sub1.allocate(im)
  240. sub1.set_image_data(im)
  241. # Quantize it if its RGB
  242. if im.ndim == 3 and im.shape[-1] == 3:
  243. sub2 = sub1.quantize(self._quantizer, self._palettesize)
  244. # Set meta data and return
  245. sub2.set_meta_data(meta)
  246. return sub2
  247. def _get_sub_rectangles(self, prev, im):
  248. """
  249. Calculate the minimal rectangles that need updating each frame.
  250. Returns a two-element tuple containing the cropped images and a
  251. list of x-y positions.
  252. """
  253. # Get difference, sum over colors
  254. diff = np.abs(im - prev)
  255. if diff.ndim == 3:
  256. diff = diff.sum(2)
  257. # Get begin and end for both dimensions
  258. X = np.argwhere(diff.sum(0))
  259. Y = np.argwhere(diff.sum(1))
  260. # Get rect coordinates
  261. if X.size and Y.size:
  262. x0, x1 = int(X[0]), int(X[-1]) + 1
  263. y0, y1 = int(Y[0]), int(Y[-1]) + 1
  264. else: # No change ... make it minimal
  265. x0, x1 = 0, 2
  266. y0, y1 = 0, 2
  267. # Cut out and return
  268. return im[y0:y1, x0:x1], (x0, y0)