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 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329
|
# -*- coding: utf-8 -*-
# imageio is distributed under the terms of the (new) BSD License.
""" Plugin for multi-image freeimafe formats, like animated GIF and ico.
"""
from __future__ import absolute_import, print_function, division
import numpy as np
from .. import formats
from ..core import Format, image_as_uint
from ._freeimage import fi, IO_FLAGS
from .freeimage import FreeimageFormat
class FreeimageMulti(FreeimageFormat):
""" Base class for freeimage formats that support multiple images.
"""
_modes = "iI"
_fif = -1
class Reader(Format.Reader):
def _open(self, flags=0):
flags = int(flags)
# Create bitmap
self._bm = fi.create_multipage_bitmap(
self.request.filename, self.format.fif, flags
)
self._bm.load_from_filename(self.request.get_local_filename())
def _close(self):
self._bm.close()
def _get_length(self):
return len(self._bm)
def _get_data(self, index):
sub = self._bm.get_page(index)
try:
return sub.get_image_data(), sub.get_meta_data()
finally:
sub.close()
def _get_meta_data(self, index):
index = index or 0
if index < 0 or index >= len(self._bm):
raise IndexError()
sub = self._bm.get_page(index)
try:
return sub.get_meta_data()
finally:
sub.close()
# --
class Writer(FreeimageFormat.Writer):
def _open(self, flags=0):
# Set flags
self._flags = flags = int(flags)
# Instantiate multi-page bitmap
self._bm = fi.create_multipage_bitmap(
self.request.filename, self.format.fif, flags
)
self._bm.save_to_filename(self.request.get_local_filename())
def _close(self):
# Close bitmap
self._bm.close()
def _append_data(self, im, meta):
# Prepare data
if im.ndim == 3 and im.shape[-1] == 1:
im = im[:, :, 0]
im = image_as_uint(im, bitdepth=8)
# Create sub bitmap
sub1 = fi.create_bitmap(self._bm._filename, self.format.fif)
# Let subclass add data to bitmap, optionally return new
sub2 = self._append_bitmap(im, meta, sub1)
# Add
self._bm.append_bitmap(sub2)
sub2.close()
if sub1 is not sub2:
sub1.close()
def _append_bitmap(self, im, meta, bitmap):
# Set data
bitmap.allocate(im)
bitmap.set_image_data(im)
bitmap.set_meta_data(meta)
# Return that same bitmap
return bitmap
def _set_meta_data(self, meta):
pass # ignore global meta data
class MngFormat(FreeimageMulti):
""" An Mng format based on the Freeimage library.
Read only. Seems broken.
"""
_fif = 6
def _can_write(self, request): # pragma: no cover
return False
class IcoFormat(FreeimageMulti):
""" An ICO format based on the Freeimage library.
This format supports grayscale, RGB and RGBA images.
The freeimage plugin requires a `freeimage` binary. If this binary
is not available on the system, it can be downloaded by either
- the command line script ``imageio_download_bin freeimage``
- the Python method ``imageio.plugins.freeimage.download()``
Parameters for reading
----------------------
makealpha : bool
Convert to 32-bit and create an alpha channel from the AND-
mask when loading. Default False. Note that this returns wrong
results if the image was already RGBA.
"""
_fif = 1
class Reader(FreeimageMulti.Reader):
def _open(self, flags=0, makealpha=False):
# Build flags from kwargs
flags = int(flags)
if makealpha:
flags |= IO_FLAGS.ICO_MAKEALPHA
return FreeimageMulti.Reader._open(self, flags)
class GifFormat(FreeimageMulti):
""" A format for reading and writing static and animated GIF, based
on the Freeimage library.
Images read with this format are always RGBA. Currently,
the alpha channel is ignored when saving RGB images with this
format.
The freeimage plugin requires a `freeimage` binary. If this binary
is not available on the system, it can be downloaded by either
- the command line script ``imageio_download_bin freeimage``
- the Python method ``imageio.plugins.freeimage.download()``
Parameters for reading
----------------------
playback : bool
'Play' the GIF to generate each frame (as 32bpp) instead of
returning raw frame data when loading. Default True.
Parameters for saving
---------------------
loop : int
The number of iterations. Default 0 (meaning loop indefinitely)
duration : {float, list}
The duration (in seconds) of each frame. Either specify one value
that is used for all frames, or one value for each frame.
Note that in the GIF format the duration/delay is expressed in
hundredths of a second, which limits the precision of the duration.
fps : float
The number of frames per second. If duration is not given, the
duration for each frame is set to 1/fps. Default 10.
palettesize : int
The number of colors to quantize the image to. Is rounded to
the nearest power of two. Default 256.
quantizer : {'wu', 'nq'}
The quantization algorithm:
* wu - Wu, Xiaolin, Efficient Statistical Computations for
Optimal Color Quantization
* nq (neuqant) - Dekker A. H., Kohonen neural networks for
optimal color quantization
subrectangles : bool
If True, will try and optimize the GIF by storing only the
rectangular parts of each frame that change with respect to the
previous. Unfortunately, this option seems currently broken
because FreeImage does not handle DisposalMethod correctly.
Default False.
"""
_fif = 25
class Reader(FreeimageMulti.Reader):
def _open(self, flags=0, playback=True):
# Build flags from kwargs
flags = int(flags)
if playback:
flags |= IO_FLAGS.GIF_PLAYBACK
FreeimageMulti.Reader._open(self, flags)
def _get_data(self, index):
im, meta = FreeimageMulti.Reader._get_data(self, index)
# im = im[:, :, :3] # Drop alpha channel
return im, meta
# -- writer
class Writer(FreeimageMulti.Writer):
# todo: subrectangles
# todo: global palette
def _open(
self,
flags=0,
loop=0,
duration=None,
fps=10,
palettesize=256,
quantizer="Wu",
subrectangles=False,
):
# Check palettesize
if palettesize < 2 or palettesize > 256:
raise ValueError("GIF quantize param must be 2..256")
if palettesize not in [2, 4, 8, 16, 32, 64, 128, 256]:
palettesize = 2 ** int(np.log2(128) + 0.999)
print(
"Warning: palettesize (%r) modified to a factor of "
"two between 2-256." % palettesize
)
self._palettesize = palettesize
# Check quantizer
self._quantizer = {"wu": 0, "nq": 1}.get(quantizer.lower(), None)
if self._quantizer is None:
raise ValueError('Invalid quantizer, must be "wu" or "nq".')
# Check frametime
if duration is None:
self._frametime = [int(1000 / float(fps) + 0.5)]
elif isinstance(duration, list):
self._frametime = [int(1000 * d) for d in duration]
elif isinstance(duration, (float, int)):
self._frametime = [int(1000 * duration)]
else:
raise ValueError("Invalid value for duration: %r" % duration)
# Check subrectangles
self._subrectangles = bool(subrectangles)
self._prev_im = None
# Init
FreeimageMulti.Writer._open(self, flags)
# Set global meta data
self._meta = {}
self._meta["ANIMATION"] = {
# 'GlobalPalette': np.array([0]).astype(np.uint8),
"Loop": np.array([loop]).astype(np.uint32),
# 'LogicalWidth': np.array([x]).astype(np.uint16),
# 'LogicalHeight': np.array([x]).astype(np.uint16),
}
def _append_bitmap(self, im, meta, bitmap):
# Prepare meta data
meta = meta.copy()
meta_a = meta["ANIMATION"] = {}
# If this is the first frame, assign it our "global" meta data
if len(self._bm) == 0:
meta.update(self._meta)
meta_a = meta["ANIMATION"]
# Set frame time
index = len(self._bm)
if index < len(self._frametime):
ft = self._frametime[index]
else:
ft = self._frametime[-1]
meta_a["FrameTime"] = np.array([ft]).astype(np.uint32)
# Check array
if im.ndim == 3 and im.shape[-1] == 4:
im = im[:, :, :3]
# Process subrectangles
im_uncropped = im
if self._subrectangles and self._prev_im is not None:
im, xy = self._get_sub_rectangles(self._prev_im, im)
meta_a["DisposalMethod"] = np.array([1]).astype(np.uint8)
meta_a["FrameLeft"] = np.array([xy[0]]).astype(np.uint16)
meta_a["FrameTop"] = np.array([xy[1]]).astype(np.uint16)
self._prev_im = im_uncropped
# Set image data
sub2 = sub1 = bitmap
sub1.allocate(im)
sub1.set_image_data(im)
# Quantize it if its RGB
if im.ndim == 3 and im.shape[-1] == 3:
sub2 = sub1.quantize(self._quantizer, self._palettesize)
# If single image, omit animation data
if self.request.mode[1] == "i":
del meta["ANIMATION"]
# Set meta data and return
sub2.set_meta_data(meta)
return sub2
def _get_sub_rectangles(self, prev, im):
"""
Calculate the minimal rectangles that need updating each frame.
Returns a two-element tuple containing the cropped images and a
list of x-y positions.
"""
# Get difference, sum over colors
diff = np.abs(im - prev)
if diff.ndim == 3:
diff = diff.sum(2)
# Get begin and end for both dimensions
X = np.argwhere(diff.sum(0))
Y = np.argwhere(diff.sum(1))
# Get rect coordinates
if X.size and Y.size:
x0, x1 = int(X[0]), int(X[-1]) + 1
y0, y1 = int(Y[0]), int(Y[-1]) + 1
else: # No change ... make it minimal
x0, x1 = 0, 2
y0, y1 = 0, 2
# Cut out and return
return im[y0:y1, x0:x1], (x0, y0)
# formats.add_format(MngFormat('MNG', 'Multiple network graphics',
# '.mng', 'iI'))
formats.add_format(IcoFormat("ICO-FI", "Windows icon", ".ico", "iI"))
formats.add_format(
GifFormat("GIF-FI", "Static and animated gif (FreeImage)", ".gif", "iI")
)
|