[GRASS-SVN] r58181 - in grass/trunk/lib/python: . imaging
svn_grass at osgeo.org
svn_grass at osgeo.org
Sat Nov 9 19:58:14 PST 2013
Author: annakrat
Date: 2013-11-09 19:58:13 -0800 (Sat, 09 Nov 2013)
New Revision: 58181
Added:
grass/trunk/lib/python/imaging/
grass/trunk/lib/python/imaging/Makefile
grass/trunk/lib/python/imaging/README
grass/trunk/lib/python/imaging/__init__.py
grass/trunk/lib/python/imaging/images2avi.py
grass/trunk/lib/python/imaging/images2gif.py
grass/trunk/lib/python/imaging/images2ims.py
grass/trunk/lib/python/imaging/images2swf.py
grass/trunk/lib/python/imaging/imaginglib.dox
Log:
libpython: add library for animations (from visvis project)
Added: grass/trunk/lib/python/imaging/Makefile
===================================================================
--- grass/trunk/lib/python/imaging/Makefile (rev 0)
+++ grass/trunk/lib/python/imaging/Makefile 2013-11-10 03:58:13 UTC (rev 58181)
@@ -0,0 +1,32 @@
+MODULE_TOPDIR = ../../..
+
+include $(MODULE_TOPDIR)/include/Make/Other.make
+include $(MODULE_TOPDIR)/include/Make/Python.make
+include $(MODULE_TOPDIR)/include/Make/Doxygen.make
+
+PYDIR = $(ETC)/python
+GDIR = $(PYDIR)/grass
+DSTDIR = $(GDIR)/imaging
+
+MODULES = images2avi images2gif images2ims images2swf
+
+
+PYFILES := $(patsubst %,$(DSTDIR)/%.py,$(MODULES) __init__)
+PYCFILES := $(patsubst %,$(DSTDIR)/%.pyc,$(MODULES) __init__)
+
+default: $(PYFILES) $(PYCFILES) $(GDIR)/__init__.py $(GDIR)/__init__.pyc
+
+$(PYDIR):
+ $(MKDIR) $@
+
+$(GDIR): | $(PYDIR)
+ $(MKDIR) $@
+
+$(DSTDIR): | $(GDIR)
+ $(MKDIR) $@
+
+$(DSTDIR)/%: % | $(DSTDIR)
+ $(INSTALL_DATA) $< $@
+
+#doxygen:
+DOXNAME = imaginglib
Added: grass/trunk/lib/python/imaging/README
===================================================================
--- grass/trunk/lib/python/imaging/README (rev 0)
+++ grass/trunk/lib/python/imaging/README 2013-11-10 03:58:13 UTC (rev 58181)
@@ -0,0 +1,40 @@
+General
+=======
+This is a part of visvis library [1], specifically visvis.vvmovie.
+The files are from the visvis-1.8 version.
+
+[1] https://code.google.com/p/visvis/
+
+Changes
+=======
+In images2avi, the image format for temporary files is changed
+from JPG to PNG, to improve the resulting AVI. This makes the size larger,
+however the JPG format is unsuitable for maps in general.
+
+--- imaging/images2avi.py 2013-11-09 21:46:28.000000000 -0500
++++ visvis-1.8/vvmovie/images2avi.py 2012-04-25 17:15:40.000000000 -0400
+@@ -79,7 +79,7 @@
+
+ # Determine temp dir and create images
+ tempDir = os.path.join( os.path.expanduser('~'), '.tempIms')
+- images2ims.writeIms( os.path.join(tempDir, 'im*.png'), images)
++ images2ims.writeIms( os.path.join(tempDir, 'im*.jpg'), images)
+
+ # Determine formatter
+ N = len(images)
+@@ -93,7 +93,7 @@
+
+ # Compile command to create avi
+ command = "ffmpeg -r %i %s " % (int(fps), inputOptions)
+- command += "-i im%s.png " % (formatter,)
++ command += "-i im%s.jpg " % (formatter,)
+ command += "-g 1 -vcodec %s %s " % (encoding, outputOptions)
+ command += "output.avi"
+
+Questions
+=========
+Should we make these files PEP8 compliant? This would make
+merging of possible changes from visvis more difficult.
+
+
+
Added: grass/trunk/lib/python/imaging/__init__.py
===================================================================
--- grass/trunk/lib/python/imaging/__init__.py (rev 0)
+++ grass/trunk/lib/python/imaging/__init__.py 2013-11-10 03:58:13 UTC (rev 58181)
@@ -0,0 +1,4 @@
+from grass.imaging.images2gif import readGif, writeGif
+from grass.imaging.images2swf import readSwf, writeSwf
+from grass.imaging.images2avi import readAvi, writeAvi
+from grass.imaging.images2ims import readIms, writeIms
Added: grass/trunk/lib/python/imaging/images2avi.py
===================================================================
--- grass/trunk/lib/python/imaging/images2avi.py (rev 0)
+++ grass/trunk/lib/python/imaging/images2avi.py 2013-11-10 03:58:13 UTC (rev 58181)
@@ -0,0 +1,169 @@
+# Copyright (C) 2012, Almar Klein
+# All rights reserved.
+#
+# This code is subject to the (new) BSD license:
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of the <organization> nor the
+# names of its contributors may be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+#
+# changes of this file GRASS (PNG instead of JPG) by Anna Petrasova 2013
+
+""" Module images2avi
+
+Uses ffmpeg to read and write AVI files. Requires PIL
+
+I found these sites usefull:
+http://www.catswhocode.com/blog/19-ffmpeg-commands-for-all-needs
+http://linux.die.net/man/1/ffmpeg
+
+"""
+
+import os, time
+import subprocess, shutil
+from grass.imaging import images2ims
+
+
+def _cleanDir(tempDir):
+ for i in range(3):
+ try:
+ shutil.rmtree(tempDir)
+ except Exception:
+ time.sleep(0.2) # Give OS time to free sources
+ else:
+ break
+ else:
+ print("Oops, could not fully clean up temporary files.")
+
+
+def writeAvi(filename, images, duration=0.1, encoding='mpeg4',
+ inputOptions='', outputOptions='' ):
+ """ writeAvi(filename, duration=0.1, encoding='mpeg4',
+ inputOptions='', outputOptions='')
+
+ Export movie to a AVI file, which is encoded with the given
+ encoding. Hint for Windows users: the 'msmpeg4v2' codec is
+ natively supported on Windows.
+
+ Images should be a list consisting of PIL images or numpy arrays.
+ The latter should be between 0 and 255 for integer types, and
+ between 0 and 1 for float types.
+
+ Requires the "ffmpeg" application:
+ * Most linux users can install using their package manager
+ * There is a windows installer on the visvis website
+
+ """
+
+ # Get fps
+ try:
+ fps = float(1.0/duration)
+ except Exception:
+ raise ValueError("Invalid duration parameter for writeAvi.")
+
+ # Determine temp dir and create images
+ tempDir = os.path.join( os.path.expanduser('~'), '.tempIms')
+ images2ims.writeIms( os.path.join(tempDir, 'im*.png'), images)
+
+ # Determine formatter
+ N = len(images)
+ formatter = '%04d'
+ if N < 10:
+ formatter = '%d'
+ elif N < 100:
+ formatter = '%02d'
+ elif N < 1000:
+ formatter = '%03d'
+
+ # Compile command to create avi
+ command = "ffmpeg -r %i %s " % (int(fps), inputOptions)
+ command += "-i im%s.png " % (formatter,)
+ command += "-g 1 -vcodec %s %s " % (encoding, outputOptions)
+ command += "output.avi"
+
+ # Run ffmpeg
+ S = subprocess.Popen(command, shell=True, cwd=tempDir,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ # Show what ffmpeg has to say
+ outPut = S.stdout.read()
+
+ if S.wait():
+ # An error occured, show
+ print(outPut)
+ print(S.stderr.read())
+ # Clean up
+ _cleanDir(tempDir)
+ raise RuntimeError("Could not write avi.")
+ else:
+ # Copy avi
+ shutil.copy(os.path.join(tempDir, 'output.avi'), filename)
+ # Clean up
+ _cleanDir(tempDir)
+
+
+def readAvi(filename, asNumpy=True):
+ """ readAvi(filename, asNumpy=True)
+
+ Read images from an AVI (or MPG) movie.
+
+ Requires the "ffmpeg" application:
+ * Most linux users can install using their package manager
+ * There is a windows installer on the visvis website
+
+ """
+
+ # Check whether it exists
+ if not os.path.isfile(filename):
+ raise IOError('File not found: '+str(filename))
+
+ # Determine temp dir, make sure it exists
+ tempDir = os.path.join( os.path.expanduser('~'), '.tempIms')
+ if not os.path.isdir(tempDir):
+ os.makedirs(tempDir)
+
+ # Copy movie there
+ shutil.copy(filename, os.path.join(tempDir, 'input.avi'))
+
+ # Run ffmpeg
+ command = "ffmpeg -i input.avi im%d.jpg"
+ S = subprocess.Popen(command, shell=True, cwd=tempDir,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ # Show what mencodec has to say
+ outPut = S.stdout.read()
+
+ if S.wait():
+ # An error occured, show
+ print(outPut)
+ print(S.stderr.read())
+ # Clean up
+ _cleanDir(tempDir)
+ raise RuntimeError("Could not read avi.")
+ else:
+ # Read images
+ images = images2ims.readIms(os.path.join(tempDir, 'im*.jpg'), asNumpy)
+ # Clean up
+ _cleanDir(tempDir)
+
+ # Done
+ return images
Added: grass/trunk/lib/python/imaging/images2gif.py
===================================================================
--- grass/trunk/lib/python/imaging/images2gif.py (rev 0)
+++ grass/trunk/lib/python/imaging/images2gif.py 2013-11-10 03:58:13 UTC (rev 58181)
@@ -0,0 +1,1068 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2012, Almar Klein, Ant1, Marius van Voorden
+#
+# This code is subject to the (new) BSD license:
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of the <organization> nor the
+# names of its contributors may be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+""" Module images2gif
+
+Provides functionality for reading and writing animated GIF images.
+Use writeGif to write a series of numpy arrays or PIL images as an
+animated GIF. Use readGif to read an animated gif as a series of numpy
+arrays.
+
+Note that since July 2004, all patents on the LZW compression patent have
+expired. Therefore the GIF format may now be used freely.
+
+Acknowledgements
+----------------
+
+Many thanks to Ant1 for:
+* noting the use of "palette=PIL.Image.ADAPTIVE", which significantly
+ improves the results.
+* the modifications to save each image with its own palette, or optionally
+ the global palette (if its the same).
+
+Many thanks to Marius van Voorden for porting the NeuQuant quantization
+algorithm of Anthony Dekker to Python (See the NeuQuant class for its
+license).
+
+Many thanks to Alex Robinson for implementing the concept of subrectangles,
+which (depening on image content) can give a very significant reduction in
+file size.
+
+This code is based on gifmaker (in the scripts folder of the source
+distribution of PIL)
+
+
+Usefull links
+-------------
+ * http://tronche.com/computer-graphics/gif/
+ * http://en.wikipedia.org/wiki/Graphics_Interchange_Format
+ * http://www.w3.org/Graphics/GIF/spec-gif89a.txt
+
+"""
+# todo: This module should be part of imageio (or at least based on)
+
+import os, time
+
+try:
+ import PIL
+ from PIL import Image
+ from PIL.GifImagePlugin import getheader, getdata
+except ImportError:
+ PIL = None
+
+try:
+ import numpy as np
+except ImportError:
+ np = None
+
+def get_cKDTree():
+ try:
+ from scipy.spatial import cKDTree
+ except ImportError:
+ cKDTree = None
+ return cKDTree
+
+
+# getheader gives a 87a header and a color palette (two elements in a list).
+# getdata()[0] gives the Image Descriptor up to (including) "LZW min code size".
+# getdatas()[1:] is the image data itself in chuncks of 256 bytes (well
+# technically the first byte says how many bytes follow, after which that
+# amount (max 255) follows).
+
+def checkImages(images):
+ """ checkImages(images)
+ Check numpy images and correct intensity range etc.
+ The same for all movie formats.
+ """
+ # Init results
+ images2 = []
+
+ for im in images:
+ if PIL and isinstance(im, PIL.Image.Image):
+ # We assume PIL images are allright
+ images2.append(im)
+
+ elif np and isinstance(im, np.ndarray):
+ # Check and convert dtype
+ if im.dtype == np.uint8:
+ images2.append(im) # Ok
+ elif im.dtype in [np.float32, np.float64]:
+ im = im.copy()
+ im[im<0] = 0
+ im[im>1] = 1
+ im *= 255
+ images2.append( im.astype(np.uint8) )
+ else:
+ im = im.astype(np.uint8)
+ images2.append(im)
+ # Check size
+ if im.ndim == 2:
+ pass # ok
+ elif im.ndim == 3:
+ if im.shape[2] not in [3,4]:
+ raise ValueError('This array can not represent an image.')
+ else:
+ raise ValueError('This array can not represent an image.')
+ else:
+ raise ValueError('Invalid image type: ' + str(type(im)))
+
+ # Done
+ return images2
+
+
+def intToBin(i):
+ """ Integer to two bytes """
+ # devide in two parts (bytes)
+ i1 = i % 256
+ i2 = int( i/256)
+ # make string (little endian)
+ return chr(i1) + chr(i2)
+
+
+class GifWriter:
+ """ GifWriter()
+
+ Class that contains methods for helping write the animated GIF file.
+
+ """
+
+ def getheaderAnim(self, im):
+ """ getheaderAnim(im)
+
+ Get animation header. To replace PILs getheader()[0]
+
+ """
+ bb = "GIF89a"
+ bb += intToBin(im.size[0])
+ bb += intToBin(im.size[1])
+ bb += "\x87\x00\x00"
+ return bb
+
+
+ def getImageDescriptor(self, im, xy=None):
+ """ getImageDescriptor(im, xy=None)
+
+ Used for the local color table properties per image.
+ Otherwise global color table applies to all frames irrespective of
+ whether additional colors comes in play that require a redefined
+ palette. Still a maximum of 256 color per frame, obviously.
+
+ Written by Ant1 on 2010-08-22
+ Modified by Alex Robinson in Janurari 2011 to implement subrectangles.
+
+ """
+
+ # Defaule use full image and place at upper left
+ if xy is None:
+ xy = (0,0)
+
+ # Image separator,
+ bb = '\x2C'
+
+ # Image position and size
+ bb += intToBin( xy[0] ) # Left position
+ bb += intToBin( xy[1] ) # Top position
+ bb += intToBin( im.size[0] ) # image width
+ bb += intToBin( im.size[1] ) # image height
+
+ # packed field: local color table flag1, interlace0, sorted table0,
+ # reserved00, lct size111=7=2^(7+1)=256.
+ bb += '\x87'
+
+ # LZW minimum size code now comes later, begining of [image data] blocks
+ return bb
+
+
+ def getAppExt(self, loops=float('inf')):
+ """ getAppExt(loops=float('inf'))
+
+ Application extention. This part specifies the amount of loops.
+ If loops is 0 or inf, it goes on infinitely.
+
+ """
+
+ if loops==0 or loops==float('inf'):
+ loops = 2**16-1
+ #bb = "" # application extension should not be used
+ # (the extension interprets zero loops
+ # to mean an infinite number of loops)
+ # Mmm, does not seem to work
+ if True:
+ bb = "\x21\xFF\x0B" # application extension
+ bb += "NETSCAPE2.0"
+ bb += "\x03\x01"
+ bb += intToBin(loops)
+ bb += '\x00' # end
+ return bb
+
+
+ def getGraphicsControlExt(self, duration=0.1, dispose=2):
+ """ getGraphicsControlExt(duration=0.1, dispose=2)
+
+ Graphics Control Extension. A sort of header at the start of
+ each image. Specifies duration and transparancy.
+
+ Dispose
+ -------
+ * 0 - No disposal specified.
+ * 1 - Do not dispose. The graphic is to be left in place.
+ * 2 - Restore to background color. The area used by the graphic
+ must be restored to the background color.
+ * 3 - Restore to previous. The decoder is required to restore the
+ area overwritten by the graphic with what was there prior to
+ rendering the graphic.
+ * 4-7 -To be defined.
+
+ """
+
+ bb = '\x21\xF9\x04'
+ bb += chr((dispose & 3) << 2) # low bit 1 == transparency,
+ # 2nd bit 1 == user input , next 3 bits, the low two of which are used,
+ # are dispose.
+ bb += intToBin( int(duration*100) ) # in 100th of seconds
+ bb += '\x00' # no transparant color
+ bb += '\x00' # end
+ return bb
+
+
+ def handleSubRectangles(self, images, subRectangles):
+ """ handleSubRectangles(images)
+
+ Handle the sub-rectangle stuff. If the rectangles are given by the
+ user, the values are checked. Otherwise the subrectangles are
+ calculated automatically.
+
+ """
+
+ if isinstance(subRectangles, (tuple,list)):
+ # xy given directly
+
+ # Check xy
+ xy = subRectangles
+ if xy is None:
+ xy = (0,0)
+ if hasattr(xy, '__len__'):
+ if len(xy) == len(images):
+ xy = [xxyy for xxyy in xy]
+ else:
+ raise ValueError("len(xy) doesn't match amount of images.")
+ else:
+ xy = [xy for im in images]
+ xy[0] = (0,0)
+
+ else:
+ # Calculate xy using some basic image processing
+
+ # Check Numpy
+ if np is None:
+ raise RuntimeError("Need Numpy to use auto-subRectangles.")
+
+ # First make numpy arrays if required
+ for i in range(len(images)):
+ im = images[i]
+ if isinstance(im, Image.Image):
+ tmp = im.convert() # Make without palette
+ a = np.asarray(tmp)
+ if len(a.shape)==0:
+ raise MemoryError("Too little memory to convert PIL image to array")
+ images[i] = a
+
+ # Determine the sub rectangles
+ images, xy = self.getSubRectangles(images)
+
+ # Done
+ return images, xy
+
+
+ def getSubRectangles(self, ims):
+ """ getSubRectangles(ims)
+
+ 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.
+
+ Calculating the subrectangles takes extra time, obviously. However,
+ if the image sizes were reduced, the actual writing of the GIF
+ goes faster. In some cases applying this method produces a GIF faster.
+
+ """
+
+ # Check image count
+ if len(ims) < 2:
+ return ims, [(0,0) for i in ims]
+
+ # We need numpy
+ if np is None:
+ raise RuntimeError("Need Numpy to calculate sub-rectangles. ")
+
+ # Prepare
+ ims2 = [ims[0]]
+ xy = [(0,0)]
+ t0 = time.time()
+
+ # Iterate over images
+ prev = ims[0]
+ for im in ims[1:]:
+
+ # 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 = X[0], X[-1]+1
+ y0, y1 = Y[0], Y[-1]+1
+ else: # No change ... make it minimal
+ x0, x1 = 0, 2
+ y0, y1 = 0, 2
+
+ # Cut out and store
+ im2 = im[y0:y1,x0:x1]
+ prev = im
+ ims2.append(im2)
+ xy.append((x0,y0))
+
+ # Done
+ #print('%1.2f seconds to determine subrectangles of %i images' %
+ # (time.time()-t0, len(ims2)) )
+ return ims2, xy
+
+
+ def convertImagesToPIL(self, images, dither, nq=0):
+ """ convertImagesToPIL(images, nq=0)
+
+ Convert images to Paletted PIL images, which can then be
+ written to a single animaged GIF.
+
+ """
+
+ # Convert to PIL images
+ images2 = []
+ for im in images:
+ if isinstance(im, Image.Image):
+ images2.append(im)
+ elif np and isinstance(im, np.ndarray):
+ if im.ndim==3 and im.shape[2]==3:
+ im = Image.fromarray(im,'RGB')
+ elif im.ndim==3 and im.shape[2]==4:
+ im = Image.fromarray(im[:,:,:3],'RGB')
+ elif im.ndim==2:
+ im = Image.fromarray(im,'L')
+ images2.append(im)
+
+ # Convert to paletted PIL images
+ images, images2 = images2, []
+ if nq >= 1:
+ # NeuQuant algorithm
+ for im in images:
+ im = im.convert("RGBA") # NQ assumes RGBA
+ nqInstance = NeuQuant(im, int(nq)) # Learn colors from image
+ if dither:
+ im = im.convert("RGB").quantize(palette=nqInstance.paletteImage())
+ else:
+ im = nqInstance.quantize(im) # Use to quantize the image itself
+ images2.append(im)
+ else:
+ # Adaptive PIL algorithm
+ AD = Image.ADAPTIVE
+ for im in images:
+ im = im.convert('P', palette=AD, dither=dither)
+ images2.append(im)
+
+ # Done
+ return images2
+
+
+ def writeGifToFile(self, fp, images, durations, loops, xys, disposes):
+ """ writeGifToFile(fp, images, durations, loops, xys, disposes)
+
+ Given a set of images writes the bytes to the specified stream.
+
+ """
+
+ # Obtain palette for all images and count each occurance
+ palettes, occur = [], []
+ for im in images:
+ palettes.append( getheader(im)[1] )
+ for palette in palettes:
+ occur.append( palettes.count( palette ) )
+
+ # Select most-used palette as the global one (or first in case no max)
+ globalPalette = palettes[ occur.index(max(occur)) ]
+
+ # Init
+ frames = 0
+ firstFrame = True
+
+
+ for im, palette in zip(images, palettes):
+
+ if firstFrame:
+ # Write header
+
+ # Gather info
+ header = self.getheaderAnim(im)
+ appext = self.getAppExt(loops)
+
+ # Write
+ fp.write(header)
+ fp.write(globalPalette)
+ fp.write(appext)
+
+ # Next frame is not the first
+ firstFrame = False
+
+ if True:
+ # Write palette and image data
+
+ # Gather info
+ data = getdata(im)
+ imdes, data = data[0], data[1:]
+ graphext = self.getGraphicsControlExt(durations[frames],
+ disposes[frames])
+ # Make image descriptor suitable for using 256 local color palette
+ lid = self.getImageDescriptor(im, xys[frames])
+
+ # Write local header
+ if (palette != globalPalette) or (disposes[frames] != 2):
+ # Use local color palette
+ fp.write(graphext)
+ fp.write(lid) # write suitable image descriptor
+ fp.write(palette) # write local color table
+ fp.write('\x08') # LZW minimum size code
+ else:
+ # Use global color palette
+ fp.write(graphext)
+ fp.write(imdes) # write suitable image descriptor
+
+ # Write image data
+ for d in data:
+ fp.write(d)
+
+ # Prepare for next round
+ frames = frames + 1
+
+ fp.write(";") # end gif
+ return frames
+
+
+
+
+## Exposed functions
+
+def writeGif(filename, images, duration=0.1, repeat=True, dither=False,
+ nq=0, subRectangles=True, dispose=None):
+ """ writeGif(filename, images, duration=0.1, repeat=True, dither=False,
+ nq=0, subRectangles=True, dispose=None)
+
+ Write an animated gif from the specified images.
+
+ Parameters
+ ----------
+ filename : string
+ The name of the file to write the image to.
+ images : list
+ Should be a list consisting of PIL images or numpy arrays.
+ The latter should be between 0 and 255 for integer types, and
+ between 0 and 1 for float types.
+ duration : scalar or list of scalars
+ The duration for all frames, or (if a list) for each frame.
+ repeat : bool or integer
+ The amount of loops. If True, loops infinitetely.
+ dither : bool
+ Whether to apply dithering
+ nq : integer
+ If nonzero, applies the NeuQuant quantization algorithm to create
+ the color palette. This algorithm is superior, but slower than
+ the standard PIL algorithm. The value of nq is the quality
+ parameter. 1 represents the best quality. 10 is in general a
+ good tradeoff between quality and speed. When using this option,
+ better results are usually obtained when subRectangles is False.
+ subRectangles : False, True, or a list of 2-element tuples
+ Whether to use sub-rectangles. If True, the minimal rectangle that
+ is required to update each frame is automatically detected. This
+ can give significant reductions in file size, particularly if only
+ a part of the image changes. One can also give a list of x-y
+ coordinates if you want to do the cropping yourself. The default
+ is True.
+ dispose : int
+ How to dispose each frame. 1 means that each frame is to be left
+ in place. 2 means the background color should be restored after
+ each frame. 3 means the decoder should restore the previous frame.
+ If subRectangles==False, the default is 2, otherwise it is 1.
+
+ """
+
+ # Check PIL
+ if PIL is None:
+ raise RuntimeError("Need PIL to write animated gif files.")
+
+ # Check images
+ images = checkImages(images)
+
+ # Instantiate writer object
+ gifWriter = GifWriter()
+
+ # Check loops
+ if repeat is False:
+ loops = 1
+ elif repeat is True:
+ loops = 0 # zero means infinite
+ else:
+ loops = int(repeat)
+
+ # Check duration
+ if hasattr(duration, '__len__'):
+ if len(duration) == len(images):
+ duration = [d for d in duration]
+ else:
+ raise ValueError("len(duration) doesn't match amount of images.")
+ else:
+ duration = [duration for im in images]
+
+ # Check subrectangles
+ if subRectangles:
+ images, xy = gifWriter.handleSubRectangles(images, subRectangles)
+ defaultDispose = 1 # Leave image in place
+ else:
+ # Normal mode
+ xy = [(0,0) for im in images]
+ defaultDispose = 2 # Restore to background color.
+
+ # Check dispose
+ if dispose is None:
+ dispose = defaultDispose
+ if hasattr(dispose, '__len__'):
+ if len(dispose) != len(images):
+ raise ValueError("len(xy) doesn't match amount of images.")
+ else:
+ dispose = [dispose for im in images]
+
+
+ # Make images in a format that we can write easy
+ images = gifWriter.convertImagesToPIL(images, dither, nq)
+
+ # Write
+ fp = open(filename, 'wb')
+ try:
+ gifWriter.writeGifToFile(fp, images, duration, loops, xy, dispose)
+ finally:
+ fp.close()
+
+
+
+def readGif(filename, asNumpy=True):
+ """ readGif(filename, asNumpy=True)
+
+ Read images from an animated GIF file. Returns a list of numpy
+ arrays, or, if asNumpy is false, a list if PIL images.
+
+ """
+
+ # Check PIL
+ if PIL is None:
+ raise RuntimeError("Need PIL to read animated gif files.")
+
+ # Check Numpy
+ if np is None:
+ raise RuntimeError("Need Numpy to read animated gif files.")
+
+ # Check whether it exists
+ if not os.path.isfile(filename):
+ raise IOError('File not found: '+str(filename))
+
+ # Load file using PIL
+ pilIm = PIL.Image.open(filename)
+ pilIm.seek(0)
+
+ # Read all images inside
+ images = []
+ try:
+ while True:
+ # Get image as numpy array
+ tmp = pilIm.convert() # Make without palette
+ a = np.asarray(tmp)
+ if len(a.shape)==0:
+ raise MemoryError("Too little memory to convert PIL image to array")
+ # Store, and next
+ images.append(a)
+ pilIm.seek(pilIm.tell()+1)
+ except EOFError:
+ pass
+
+ # Convert to normal PIL images if needed
+ if not asNumpy:
+ images2 = images
+ images = []
+ for im in images2:
+ images.append( PIL.Image.fromarray(im) )
+
+ # Done
+ return images
+
+
+class NeuQuant:
+ """ NeuQuant(image, samplefac=10, colors=256)
+
+ samplefac should be an integer number of 1 or higher, 1
+ being the highest quality, but the slowest performance.
+ With avalue of 10, one tenth of all pixels are used during
+ training. This value seems a nice tradeof between speed
+ and quality.
+
+ colors is the amount of colors to reduce the image to. This
+ should best be a power of two.
+
+ See also:
+ http://members.ozemail.com.au/~dekker/NEUQUANT.HTML
+
+ License of the NeuQuant Neural-Net Quantization Algorithm
+ ---------------------------------------------------------
+
+ Copyright (c) 1994 Anthony Dekker
+ Ported to python by Marius van Voorden in 2010
+
+ NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994.
+ See "Kohonen neural networks for optimal colour quantization"
+ in "network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367.
+ for a discussion of the algorithm.
+ See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML
+
+ Any party obtaining a copy of these files from the author, directly or
+ indirectly, is granted, free of charge, a full and unrestricted irrevocable,
+ world-wide, paid up, royalty-free, nonexclusive right and license to deal
+ in this software and documentation files (the "Software"), including without
+ limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons who receive
+ copies from any such party to do so, with the only requirement being
+ that this copyright notice remain intact.
+
+ """
+
+ NCYCLES = None # Number of learning cycles
+ NETSIZE = None # Number of colours used
+ SPECIALS = None # Number of reserved colours used
+ BGCOLOR = None # Reserved background colour
+ CUTNETSIZE = None
+ MAXNETPOS = None
+
+ INITRAD = None # For 256 colours, radius starts at 32
+ RADIUSBIASSHIFT = None
+ RADIUSBIAS = None
+ INITBIASRADIUS = None
+ RADIUSDEC = None # Factor of 1/30 each cycle
+
+ ALPHABIASSHIFT = None
+ INITALPHA = None # biased by 10 bits
+
+ GAMMA = None
+ BETA = None
+ BETAGAMMA = None
+
+ network = None # The network itself
+ colormap = None # The network itself
+
+ netindex = None # For network lookup - really 256
+
+ bias = None # Bias and freq arrays for learning
+ freq = None
+
+ pimage = None
+
+ # Four primes near 500 - assume no image has a length so large
+ # that it is divisible by all four primes
+ PRIME1 = 499
+ PRIME2 = 491
+ PRIME3 = 487
+ PRIME4 = 503
+ MAXPRIME = PRIME4
+
+ pixels = None
+ samplefac = None
+
+ a_s = None
+
+
+ def setconstants(self, samplefac, colors):
+ self.NCYCLES = 100 # Number of learning cycles
+ self.NETSIZE = colors # Number of colours used
+ self.SPECIALS = 3 # Number of reserved colours used
+ self.BGCOLOR = self.SPECIALS-1 # Reserved background colour
+ self.CUTNETSIZE = self.NETSIZE - self.SPECIALS
+ self.MAXNETPOS = self.NETSIZE - 1
+
+ self.INITRAD = self.NETSIZE/8 # For 256 colours, radius starts at 32
+ self.RADIUSBIASSHIFT = 6
+ self.RADIUSBIAS = 1 << self.RADIUSBIASSHIFT
+ self.INITBIASRADIUS = self.INITRAD * self.RADIUSBIAS
+ self.RADIUSDEC = 30 # Factor of 1/30 each cycle
+
+ self.ALPHABIASSHIFT = 10 # Alpha starts at 1
+ self.INITALPHA = 1 << self.ALPHABIASSHIFT # biased by 10 bits
+
+ self.GAMMA = 1024.0
+ self.BETA = 1.0/1024.0
+ self.BETAGAMMA = self.BETA * self.GAMMA
+
+ self.network = np.empty((self.NETSIZE, 3), dtype='float64') # The network itself
+ self.colormap = np.empty((self.NETSIZE, 4), dtype='int32') # The network itself
+
+ self.netindex = np.empty(256, dtype='int32') # For network lookup - really 256
+
+ self.bias = np.empty(self.NETSIZE, dtype='float64') # Bias and freq arrays for learning
+ self.freq = np.empty(self.NETSIZE, dtype='float64')
+
+ self.pixels = None
+ self.samplefac = samplefac
+
+ self.a_s = {}
+
+ def __init__(self, image, samplefac=10, colors=256):
+
+ # Check Numpy
+ if np is None:
+ raise RuntimeError("Need Numpy for the NeuQuant algorithm.")
+
+ # Check image
+ if image.size[0] * image.size[1] < NeuQuant.MAXPRIME:
+ raise IOError("Image is too small")
+ if image.mode != "RGBA":
+ raise IOError("Image mode should be RGBA.")
+
+ # Initialize
+ self.setconstants(samplefac, colors)
+ self.pixels = np.fromstring(image.tostring(), np.uint32)
+ self.setUpArrays()
+
+ self.learn()
+ self.fix()
+ self.inxbuild()
+
+ def writeColourMap(self, rgb, outstream):
+ for i in range(self.NETSIZE):
+ bb = self.colormap[i,0];
+ gg = self.colormap[i,1];
+ rr = self.colormap[i,2];
+ outstream.write(rr if rgb else bb)
+ outstream.write(gg)
+ outstream.write(bb if rgb else rr)
+ return self.NETSIZE
+
+ def setUpArrays(self):
+ self.network[0,0] = 0.0 # Black
+ self.network[0,1] = 0.0
+ self.network[0,2] = 0.0
+
+ self.network[1,0] = 255.0 # White
+ self.network[1,1] = 255.0
+ self.network[1,2] = 255.0
+
+ # RESERVED self.BGCOLOR # Background
+
+ for i in range(self.SPECIALS):
+ self.freq[i] = 1.0 / self.NETSIZE
+ self.bias[i] = 0.0
+
+ for i in range(self.SPECIALS, self.NETSIZE):
+ p = self.network[i]
+ p[:] = (255.0 * (i-self.SPECIALS)) / self.CUTNETSIZE
+
+ self.freq[i] = 1.0 / self.NETSIZE
+ self.bias[i] = 0.0
+
+ # Omitted: setPixels
+
+ def altersingle(self, alpha, i, b, g, r):
+ """Move neuron i towards biased (b,g,r) by factor alpha"""
+ n = self.network[i] # Alter hit neuron
+ n[0] -= (alpha*(n[0] - b))
+ n[1] -= (alpha*(n[1] - g))
+ n[2] -= (alpha*(n[2] - r))
+
+ def geta(self, alpha, rad):
+ try:
+ return self.a_s[(alpha, rad)]
+ except KeyError:
+ length = rad*2-1
+ mid = length/2
+ q = np.array(list(range(mid-1,-1,-1))+list(range(-1,mid)))
+ a = alpha*(rad*rad - q*q)/(rad*rad)
+ a[mid] = 0
+ self.a_s[(alpha, rad)] = a
+ return a
+
+ def alterneigh(self, alpha, rad, i, b, g, r):
+ if i-rad >= self.SPECIALS-1:
+ lo = i-rad
+ start = 0
+ else:
+ lo = self.SPECIALS-1
+ start = (self.SPECIALS-1 - (i-rad))
+
+ if i+rad <= self.NETSIZE:
+ hi = i+rad
+ end = rad*2-1
+ else:
+ hi = self.NETSIZE
+ end = (self.NETSIZE - (i+rad))
+
+ a = self.geta(alpha, rad)[start:end]
+
+ p = self.network[lo+1:hi]
+ p -= np.transpose(np.transpose(p - np.array([b, g, r])) * a)
+
+ #def contest(self, b, g, r):
+ # """ Search for biased BGR values
+ # Finds closest neuron (min dist) and updates self.freq
+ # finds best neuron (min dist-self.bias) and returns position
+ # for frequently chosen neurons, self.freq[i] is high and self.bias[i] is negative
+ # self.bias[i] = self.GAMMA*((1/self.NETSIZE)-self.freq[i])"""
+ #
+ # i, j = self.SPECIALS, self.NETSIZE
+ # dists = abs(self.network[i:j] - np.array([b,g,r])).sum(1)
+ # bestpos = i + np.argmin(dists)
+ # biasdists = dists - self.bias[i:j]
+ # bestbiaspos = i + np.argmin(biasdists)
+ # self.freq[i:j] -= self.BETA * self.freq[i:j]
+ # self.bias[i:j] += self.BETAGAMMA * self.freq[i:j]
+ # self.freq[bestpos] += self.BETA
+ # self.bias[bestpos] -= self.BETAGAMMA
+ # return bestbiaspos
+ def contest(self, b, g, r):
+ """ Search for biased BGR values
+ Finds closest neuron (min dist) and updates self.freq
+ finds best neuron (min dist-self.bias) and returns position
+ for frequently chosen neurons, self.freq[i] is high and self.bias[i] is negative
+ self.bias[i] = self.GAMMA*((1/self.NETSIZE)-self.freq[i])"""
+ i, j = self.SPECIALS, self.NETSIZE
+ dists = abs(self.network[i:j] - np.array([b,g,r])).sum(1)
+ bestpos = i + np.argmin(dists)
+ biasdists = dists - self.bias[i:j]
+ bestbiaspos = i + np.argmin(biasdists)
+ self.freq[i:j] *= (1-self.BETA)
+ self.bias[i:j] += self.BETAGAMMA * self.freq[i:j]
+ self.freq[bestpos] += self.BETA
+ self.bias[bestpos] -= self.BETAGAMMA
+ return bestbiaspos
+
+
+
+
+ def specialFind(self, b, g, r):
+ for i in range(self.SPECIALS):
+ n = self.network[i]
+ if n[0] == b and n[1] == g and n[2] == r:
+ return i
+ return -1
+
+ def learn(self):
+ biasRadius = self.INITBIASRADIUS
+ alphadec = 30 + ((self.samplefac-1)/3)
+ lengthcount = self.pixels.size
+ samplepixels = lengthcount / self.samplefac
+ delta = samplepixels / self.NCYCLES
+ alpha = self.INITALPHA
+
+ i = 0;
+ rad = biasRadius >> self.RADIUSBIASSHIFT
+ if rad <= 1:
+ rad = 0
+
+ print("Beginning 1D learning: samplepixels = %1.2f rad = %i" %
+ (samplepixels, rad) )
+ step = 0
+ pos = 0
+ if lengthcount%NeuQuant.PRIME1 != 0:
+ step = NeuQuant.PRIME1
+ elif lengthcount%NeuQuant.PRIME2 != 0:
+ step = NeuQuant.PRIME2
+ elif lengthcount%NeuQuant.PRIME3 != 0:
+ step = NeuQuant.PRIME3
+ else:
+ step = NeuQuant.PRIME4
+
+ i = 0
+ printed_string = ''
+ while i < samplepixels:
+ if i%100 == 99:
+ tmp = '\b'*len(printed_string)
+ printed_string = str((i+1)*100/samplepixels)+"%\n"
+ print(tmp + printed_string)
+ p = self.pixels[pos]
+ r = (p >> 16) & 0xff
+ g = (p >> 8) & 0xff
+ b = (p ) & 0xff
+
+ if i == 0: # Remember background colour
+ self.network[self.BGCOLOR] = [b, g, r]
+
+ j = self.specialFind(b, g, r)
+ if j < 0:
+ j = self.contest(b, g, r)
+
+ if j >= self.SPECIALS: # Don't learn for specials
+ a = (1.0 * alpha) / self.INITALPHA
+ self.altersingle(a, j, b, g, r)
+ if rad > 0:
+ self.alterneigh(a, rad, j, b, g, r)
+
+ pos = (pos+step)%lengthcount
+
+ i += 1
+ if i%delta == 0:
+ alpha -= alpha / alphadec
+ biasRadius -= biasRadius / self.RADIUSDEC
+ rad = biasRadius >> self.RADIUSBIASSHIFT
+ if rad <= 1:
+ rad = 0
+
+ finalAlpha = (1.0*alpha)/self.INITALPHA
+ print("Finished 1D learning: final alpha = %1.2f!" % finalAlpha)
+
+ def fix(self):
+ for i in range(self.NETSIZE):
+ for j in range(3):
+ x = int(0.5 + self.network[i,j])
+ x = max(0, x)
+ x = min(255, x)
+ self.colormap[i,j] = x
+ self.colormap[i,3] = i
+
+ def inxbuild(self):
+ previouscol = 0
+ startpos = 0
+ for i in range(self.NETSIZE):
+ p = self.colormap[i]
+ q = None
+ smallpos = i
+ smallval = p[1] # Index on g
+ # Find smallest in i..self.NETSIZE-1
+ for j in range(i+1, self.NETSIZE):
+ q = self.colormap[j]
+ if q[1] < smallval: # Index on g
+ smallpos = j
+ smallval = q[1] # Index on g
+
+ q = self.colormap[smallpos]
+ # Swap p (i) and q (smallpos) entries
+ if i != smallpos:
+ p[:],q[:] = q, p.copy()
+
+ # smallval entry is now in position i
+ if smallval != previouscol:
+ self.netindex[previouscol] = (startpos+i) >> 1
+ for j in range(previouscol+1, smallval):
+ self.netindex[j] = i
+ previouscol = smallval
+ startpos = i
+ self.netindex[previouscol] = (startpos+self.MAXNETPOS) >> 1
+ for j in range(previouscol+1, 256): # Really 256
+ self.netindex[j] = self.MAXNETPOS
+
+
+ def paletteImage(self):
+ """ PIL weird interface for making a paletted image: create an image which
+ already has the palette, and use that in Image.quantize. This function
+ returns this palette image. """
+ if self.pimage is None:
+ palette = []
+ for i in range(self.NETSIZE):
+ palette.extend(self.colormap[i][:3])
+
+ palette.extend([0]*(256-self.NETSIZE)*3)
+
+ # a palette image to use for quant
+ self.pimage = Image.new("P", (1, 1), 0)
+ self.pimage.putpalette(palette)
+ return self.pimage
+
+
+ def quantize(self, image):
+ """ Use a kdtree to quickly find the closest palette colors for the pixels """
+ if get_cKDTree():
+ return self.quantize_with_scipy(image)
+ else:
+ print('Scipy not available, falling back to slower version.')
+ return self.quantize_without_scipy(image)
+
+
+ def quantize_with_scipy(self, image):
+ w,h = image.size
+ px = np.asarray(image).copy()
+ px2 = px[:,:,:3].reshape((w*h,3))
+
+ cKDTree = get_cKDTree()
+ kdtree = cKDTree(self.colormap[:,:3],leafsize=10)
+ result = kdtree.query(px2)
+ colorindex = result[1]
+ print("Distance: %1.2f" % (result[0].sum()/(w*h)) )
+ px2[:] = self.colormap[colorindex,:3]
+
+ return Image.fromarray(px).convert("RGB").quantize(palette=self.paletteImage())
+
+
+ def quantize_without_scipy(self, image):
+ """" This function can be used if no scipy is availabe.
+ It's 7 times slower though.
+ """
+ w,h = image.size
+ px = np.asarray(image).copy()
+ memo = {}
+ for j in range(w):
+ for i in range(h):
+ key = (px[i,j,0],px[i,j,1],px[i,j,2])
+ try:
+ val = memo[key]
+ except KeyError:
+ val = self.convert(*key)
+ memo[key] = val
+ px[i,j,0],px[i,j,1],px[i,j,2] = val
+ return Image.fromarray(px).convert("RGB").quantize(palette=self.paletteImage())
+
+ def convert(self, *color):
+ i = self.inxsearch(*color)
+ return self.colormap[i,:3]
+
+ def inxsearch(self, r, g, b):
+ """Search for BGR values 0..255 and return colour index"""
+ dists = (self.colormap[:,:3] - np.array([r,g,b]))
+ a= np.argmin((dists*dists).sum(1))
+ return a
+
+
+
+if __name__ == '__main__':
+ im = np.zeros((200,200), dtype=np.uint8)
+ im[10:30,:] = 100
+ im[:,80:120] = 255
+ im[-50:-40,:] = 50
+
+ images = [im*1.0, im*0.8, im*0.6, im*0.4, im*0]
+ writeGif('lala3.gif',images, duration=0.5, dither=0)
Added: grass/trunk/lib/python/imaging/images2ims.py
===================================================================
--- grass/trunk/lib/python/imaging/images2ims.py (rev 0)
+++ grass/trunk/lib/python/imaging/images2ims.py 2013-11-10 03:58:13 UTC (rev 58181)
@@ -0,0 +1,237 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2012, Almar Klein
+#
+# This code is subject to the (new) BSD license:
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of the <organization> nor the
+# names of its contributors may be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+""" Module images2ims
+
+Use PIL to create a series of images.
+
+"""
+
+import os
+
+try:
+ import numpy as np
+except ImportError:
+ np = None
+
+try:
+ import PIL
+ from PIL import Image
+except ImportError:
+ PIL = None
+
+
+def checkImages(images):
+ """ checkImages(images)
+ Check numpy images and correct intensity range etc.
+ The same for all movie formats.
+ """
+ # Init results
+ images2 = []
+
+ for im in images:
+ if PIL and isinstance(im, PIL.Image.Image):
+ # We assume PIL images are allright
+ images2.append(im)
+
+ elif np and isinstance(im, np.ndarray):
+ # Check and convert dtype
+ if im.dtype == np.uint8:
+ images2.append(im) # Ok
+ elif im.dtype in [np.float32, np.float64]:
+ theMax = im.max()
+ if theMax > 128 and theMax < 300:
+ pass # assume 0:255
+ else:
+ im = im.copy()
+ im[im<0] = 0
+ im[im>1] = 1
+ im *= 255
+ images2.append( im.astype(np.uint8) )
+ else:
+ im = im.astype(np.uint8)
+ images2.append(im)
+ # Check size
+ if im.ndim == 2:
+ pass # ok
+ elif im.ndim == 3:
+ if im.shape[2] not in [3,4]:
+ raise ValueError('This array can not represent an image.')
+ else:
+ raise ValueError('This array can not represent an image.')
+ else:
+ raise ValueError('Invalid image type: ' + str(type(im)))
+
+ # Done
+ return images2
+
+
+def _getFilenameParts(filename):
+ if '*' in filename:
+ return tuple( filename.split('*',1) )
+ else:
+ return os.path.splitext(filename)
+
+
+def _getFilenameWithFormatter(filename, N):
+
+ # Determine sequence number formatter
+ formatter = '%04i'
+ if N < 10:
+ formatter = '%i'
+ elif N < 100:
+ formatter = '%02i'
+ elif N < 1000:
+ formatter = '%03i'
+
+ # Insert sequence number formatter
+ part1, part2 = _getFilenameParts(filename)
+ return part1 + formatter + part2
+
+
+def _getSequenceNumber(filename, part1, part2):
+ # Get string bit
+ seq = filename[len(part1):-len(part2)]
+ # Get all numeric chars
+ seq2 = ''
+ for c in seq:
+ if c in '0123456789':
+ seq2 += c
+ else:
+ break
+ # Make int and return
+ return int(seq2)
+
+
+def writeIms(filename, images):
+ """ writeIms(filename, images)
+
+ Export movie to a series of image files. If the filenenumber
+ contains an asterix, a sequence number is introduced at its
+ location. Otherwise the sequence number is introduced right
+ before the final dot.
+
+ To enable easy creation of a new directory with image files,
+ it is made sure that the full path exists.
+
+ Images should be a list consisting of PIL images or numpy arrays.
+ The latter should be between 0 and 255 for integer types, and
+ between 0 and 1 for float types.
+
+ """
+
+ # Check PIL
+ if PIL is None:
+ raise RuntimeError("Need PIL to write series of image files.")
+
+ # Check images
+ images = checkImages(images)
+
+ # Get dirname and filename
+ filename = os.path.abspath(filename)
+ dirname, filename = os.path.split(filename)
+
+ # Create dir(s) if we need to
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+
+ # Insert formatter
+ filename = _getFilenameWithFormatter(filename, len(images))
+
+ # Write
+ seq = 0
+ for frame in images:
+ seq += 1
+ # Get filename
+ fname = os.path.join(dirname, filename%seq)
+ # Write image
+ if np and isinstance(frame, np.ndarray):
+ frame = PIL.Image.fromarray(frame)
+ frame.save(fname)
+
+
+
+def readIms(filename, asNumpy=True):
+ """ readIms(filename, asNumpy=True)
+
+ Read images from a series of images in a single directory. Returns a
+ list of numpy arrays, or, if asNumpy is false, a list if PIL images.
+
+ """
+
+ # Check PIL
+ if PIL is None:
+ raise RuntimeError("Need PIL to read a series of image files.")
+
+ # Check Numpy
+ if asNumpy and np is None:
+ raise RuntimeError("Need Numpy to return numpy arrays.")
+
+ # Get dirname and filename
+ filename = os.path.abspath(filename)
+ dirname, filename = os.path.split(filename)
+
+ # Check dir exists
+ if not os.path.isdir(dirname):
+ raise IOError('Directory not found: '+str(dirname))
+
+ # Get two parts of the filename
+ part1, part2 = _getFilenameParts(filename)
+
+ # Init images
+ images = []
+
+ # Get all files in directory
+ for fname in os.listdir(dirname):
+ if fname.startswith(part1) and fname.endswith(part2):
+ # Get sequence number
+ nr = _getSequenceNumber(fname, part1, part2)
+ # Get Pil image and store copy (to prevent keeping the file)
+ im = PIL.Image.open(os.path.join(dirname, fname))
+ images.append((im.copy(), nr))
+
+ # Sort images
+ images.sort(key=lambda x:x[1])
+ images = [im[0] for im in images]
+
+ # Convert to numpy if needed
+ if asNumpy:
+ images2 = images
+ images = []
+ for im in images2:
+ # Make without palette
+ if im.mode == 'P':
+ im = im.convert()
+ # Make numpy array
+ a = np.asarray(im)
+ if len(a.shape)==0:
+ raise MemoryError("Too little memory to convert PIL image to array")
+ # Add
+ images.append(a)
+
+ # Done
+ return images
Added: grass/trunk/lib/python/imaging/images2swf.py
===================================================================
--- grass/trunk/lib/python/imaging/images2swf.py (rev 0)
+++ grass/trunk/lib/python/imaging/images2swf.py 2013-11-10 03:58:13 UTC (rev 58181)
@@ -0,0 +1,1008 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2012, Almar Klein
+#
+# This code is subject to the (new) BSD license:
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of the <organization> nor the
+# names of its contributors may be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+""" Module images2swf
+
+Provides a function (writeSwf) to store a series of PIL images or numpy
+arrays in an SWF movie, that can be played on a wide range of OS's.
+
+This module came into being because I wanted to store a series of images
+in a movie that can be viewed by other people, and which I can embed in
+flash presentations. For writing AVI or MPEG you really need a c/c++
+library, and allthough the filesize is then very small, the quality is
+sometimes not adequate. Besides I'd like to be independant of yet another
+package. I tried writing animated gif using PIL (which is widely available),
+but the quality is so poor because it only allows for 256 different colors.
+[EDIT: thanks to Ant1, now the quality of animated gif isn't so bad!]
+I also looked into MNG and APNG, two standards similar to the PNG stanard.
+Both standards promise exactly what I need. However, hardly any application
+can read those formats, and I cannot import them in flash.
+
+Therefore I decided to check out the swf file format, which is very well
+documented. This is the result: a pure python module to create an SWF file
+that shows a series of images. The images are stored using the DEFLATE
+algorithm (same as PNG and ZIP and which is included in the standard Python
+distribution). As this compression algorithm is much more effective than
+that used in GIF images, we obtain better quality (24 bit colors + alpha
+channel) while still producesing smaller files (a test showed ~75%).
+Although SWF also allows for JPEG compression, doing so would probably
+require a third party library (because encoding JPEG is much harder).
+
+This module requires Python 2.x and numpy.
+
+sources and tools:
+- SWF on wikipedia
+- Adobes "SWF File Format Specification" version 10
+ (http://www.adobe.com/devnet/swf/pdf/swf_file_format_spec_v10.pdf)
+- swftools (swfdump in specific) for debugging
+- iwisoft swf2avi can be used to convert swf to avi/mpg/flv with really
+ good quality, while file size is reduced with factors 20-100.
+ A good program in my opinion. The free version has the limitation
+ of a watermark in the upper left corner.
+
+
+"""
+
+import os, sys, time
+import zlib
+
+try:
+ import numpy as np
+except ImportError:
+ np = None
+
+try:
+ import PIL.Image
+except ImportError:
+ PIL = None
+
+
+# True if we are running on Python 3.
+# Code taken from six.py by Benjamin Peterson (MIT licensed)
+import types
+PY3 = sys.version_info[0] == 3
+if PY3:
+ string_types = str,
+ integer_types = int,
+ class_types = type,
+ text_type = str
+ binary_type = bytes
+else:
+ string_types = basestring,
+ integer_types = (int, long)
+ class_types = (type, types.ClassType)
+ text_type = unicode
+ binary_type = str
+
+
+# todo: use imageio/FreeImage to support reading JPEG images from SWF?
+
+
+def checkImages(images):
+ """ checkImages(images)
+ Check numpy images and correct intensity range etc.
+ The same for all movie formats.
+ """
+ # Init results
+ images2 = []
+
+ for im in images:
+ if PIL and isinstance(im, PIL.Image.Image):
+ # We assume PIL images are allright
+ images2.append(im)
+
+ elif np and isinstance(im, np.ndarray):
+ # Check and convert dtype
+ if im.dtype == np.uint8:
+ images2.append(im) # Ok
+ elif im.dtype in [np.float32, np.float64]:
+ theMax = im.max()
+ if theMax > 128 and theMax < 300:
+ pass # assume 0:255
+ else:
+ im = im.copy()
+ im[im<0] = 0
+ im[im>1] = 1
+ im *= 255
+ images2.append( im.astype(np.uint8) )
+ else:
+ im = im.astype(np.uint8)
+ images2.append(im)
+ # Check size
+ if im.ndim == 2:
+ pass # ok
+ elif im.ndim == 3:
+ if im.shape[2] not in [3,4]:
+ raise ValueError('This array can not represent an image.')
+ else:
+ raise ValueError('This array can not represent an image.')
+ else:
+ raise ValueError('Invalid image type: ' + str(type(im)))
+
+ # Done
+ return images2
+
+
+## Base functions and classes
+
+
+class BitArray:
+ """ Dynamic array of bits that automatically resizes
+ with factors of two.
+ Append bits using .Append() or +=
+ You can reverse bits using .Reverse()
+ """
+
+ def __init__(self, initvalue=None):
+ self.data = np.zeros((16,), dtype=np.uint8)
+ self._len = 0
+ if initvalue is not None:
+ self.Append(initvalue)
+
+ def __len__(self):
+ return self._len #self.data.shape[0]
+
+ def __repr__(self):
+ return self.data[:self._len].tostring().decode('ascii')
+
+ def _checkSize(self):
+ # check length... grow if necessary
+ arraylen = self.data.shape[0]
+ if self._len >= arraylen:
+ tmp = np.zeros((arraylen*2,), dtype=np.uint8)
+ tmp[:self._len] = self.data[:self._len]
+ self.data = tmp
+
+ def __add__(self, value):
+ self.Append(value)
+ return self
+
+ def Append(self, bits):
+
+ # check input
+ if isinstance(bits, BitArray):
+ bits = str(bits)
+ if isinstance(bits, int):
+ bits = str(bits)
+ if not isinstance(bits, string_types):
+ raise ValueError("Append bits as strings or integers!")
+
+ # add bits
+ for bit in bits:
+ self.data[self._len] = ord(bit)
+ self._len += 1
+ self._checkSize()
+
+ def Reverse(self):
+ """ In-place reverse. """
+ tmp = self.data[:self._len].copy()
+ self.data[:self._len] = np.flipud(tmp)
+
+ def ToBytes(self):
+ """ Convert to bytes. If necessary,
+ zeros are padded to the end (right side).
+ """
+ bits = str(self)
+
+ # determine number of bytes
+ nbytes = 0
+ while nbytes*8 < len(bits):
+ nbytes +=1
+ # pad
+ bits = bits.ljust(nbytes*8, '0')
+
+ # go from bits to bytes
+ bb = binary_type()
+ for i in range(nbytes):
+ tmp = int( bits[i*8:(i+1)*8], 2)
+ bb += intToUint8(tmp)
+
+ # done
+ return bb
+
+
+if PY3:
+ def intToUint32(i):
+ return int(i).to_bytes(4,'little')
+ def intToUint16(i):
+ return int(i).to_bytes(2,'little')
+ def intToUint8(i):
+ return int(i).to_bytes(1,'little')
+else:
+ def intToUint32(i):
+ number = int(i)
+ n1, n2, n3, n4 = 1, 256, 256*256, 256*256*256
+ b4, number = number // n4, number % n4
+ b3, number = number // n3, number % n3
+ b2, number = number // n2, number % n2
+ b1 = number
+ return chr(b1) + chr(b2) + chr(b3) + chr(b4)
+ def intToUint16(i):
+ i = int(i)
+ # devide in two parts (bytes)
+ i1 = i % 256
+ i2 = int( i//256)
+ # make string (little endian)
+ return chr(i1) + chr(i2)
+ def intToUint8(i):
+ return chr(int(i))
+
+
+def intToBits(i,n=None):
+ """ convert int to a string of bits (0's and 1's in a string),
+ pad to n elements. Convert back using int(ss,2). """
+ ii = i
+
+ # make bits
+ bb = BitArray()
+ while ii > 0:
+ bb += str(ii % 2)
+ ii = ii >> 1
+ bb.Reverse()
+
+ # justify
+ if n is not None:
+ if len(bb) > n:
+ raise ValueError("intToBits fail: len larger than padlength.")
+ bb = str(bb).rjust(n,'0')
+
+ # done
+ return BitArray(bb)
+
+def bitsToInt(bb, n=8):
+ # Init
+ value = ''
+
+ # Get value in bits
+ for i in range(len(bb)):
+ b = bb[i:i+1]
+ tmp = bin(ord(b))[2:]
+ #value += tmp.rjust(8,'0')
+ value = tmp.rjust(8,'0') + value
+
+ # Make decimal
+ return( int(value[:n], 2) )
+
+def getTypeAndLen(bb):
+ """ bb should be 6 bytes at least
+ Return (type, length, length_of_full_tag)
+ """
+ # Init
+ value = ''
+
+ # Get first 16 bits
+ for i in range(2):
+ b = bb[i:i+1]
+ tmp = bin(ord(b))[2:]
+ #value += tmp.rjust(8,'0')
+ value = tmp.rjust(8,'0') + value
+
+ # Get type and length
+ type = int( value[:10], 2)
+ L = int( value[10:], 2)
+ L2 = L + 2
+
+ # Long tag header?
+ if L == 63: # '111111'
+ value = ''
+ for i in range(2,6):
+ b = bb[i:i+1] # becomes a single-byte bytes() on both PY3 and PY2
+ tmp = bin(ord(b))[2:]
+ #value += tmp.rjust(8,'0')
+ value = tmp.rjust(8,'0') + value
+ L = int( value, 2)
+ L2 = L + 6
+
+ # Done
+ return type, L, L2
+
+
+def signedIntToBits(i,n=None):
+ """ convert signed int to a string of bits (0's and 1's in a string),
+ pad to n elements. Negative numbers are stored in 2's complement bit
+ patterns, thus positive numbers always start with a 0.
+ """
+
+ # negative number?
+ ii = i
+ if i<0:
+ # A negative number, -n, is represented as the bitwise opposite of
+ ii = abs(ii) -1 # the positive-zero number n-1.
+
+ # make bits
+ bb = BitArray()
+ while ii > 0:
+ bb += str(ii % 2)
+ ii = ii >> 1
+ bb.Reverse()
+
+ # justify
+ bb = '0' + str(bb) # always need the sign bit in front
+ if n is not None:
+ if len(bb) > n:
+ raise ValueError("signedIntToBits fail: len larger than padlength.")
+ bb = bb.rjust(n,'0')
+
+ # was it negative? (then opposite bits)
+ if i<0:
+ bb = bb.replace('0','x').replace('1','0').replace('x','1')
+
+ # done
+ return BitArray(bb)
+
+
+def twitsToBits(arr):
+ """ Given a few (signed) numbers, store them
+ as compactly as possible in the wat specifief by the swf format.
+ The numbers are multiplied by 20, assuming they
+ are twits.
+ Can be used to make the RECT record.
+ """
+
+ # first determine length using non justified bit strings
+ maxlen = 1
+ for i in arr:
+ tmp = len(signedIntToBits(i*20))
+ if tmp > maxlen:
+ maxlen = tmp
+
+ # build array
+ bits = intToBits(maxlen,5)
+ for i in arr:
+ bits += signedIntToBits(i*20, maxlen)
+
+ return bits
+
+
+def floatsToBits(arr):
+ """ Given a few (signed) numbers, convert them to bits,
+ stored as FB (float bit values). We always use 16.16.
+ Negative numbers are not (yet) possible, because I don't
+ know how the're implemented (ambiguity).
+ """
+ bits = intToBits(31, 5) # 32 does not fit in 5 bits!
+ for i in arr:
+ if i<0:
+ raise ValueError("Dit not implement negative floats!")
+ i1 = int(i)
+ i2 = i - i1
+ bits += intToBits(i1, 15)
+ bits += intToBits(i2*2**16, 16)
+ return bits
+
+
+def _readFrom(fp, n):
+ bb = binary_type()
+ try:
+ while len(bb) < n:
+ tmp = fp.read(n-len(bb))
+ bb += tmp
+ if not tmp:
+ break
+ except EOFError:
+ pass
+ return bb
+
+
+## Base Tag
+
+class Tag:
+
+ def __init__(self):
+ self.bytes = binary_type()
+ self.tagtype = -1
+
+ def ProcessTag(self):
+ """ Implement this to create the tag. """
+ raise NotImplemented()
+
+ def GetTag(self):
+ """ Calls processTag and attaches the header. """
+ self.ProcessTag()
+
+ # tag to binary
+ bits = intToBits(self.tagtype,10)
+
+ # complete header uint16 thing
+ bits += '1'*6 # = 63 = 0x3f
+ # make uint16
+ bb = intToUint16( int(str(bits),2) )
+
+ # now add 32bit length descriptor
+ bb += intToUint32(len(self.bytes))
+
+ # done, attach and return
+ bb += self.bytes
+ return bb
+
+ def MakeRectRecord(self, xmin, xmax, ymin, ymax):
+ """ Simply uses makeCompactArray to produce
+ a RECT Record. """
+ return twitsToBits([xmin, xmax, ymin, ymax])
+
+ def MakeMatrixRecord(self, scale_xy=None, rot_xy=None, trans_xy=None):
+
+ # empty matrix?
+ if scale_xy is None and rot_xy is None and trans_xy is None:
+ return "0"*8
+
+ # init
+ bits = BitArray()
+
+ # scale
+ if scale_xy:
+ bits += '1'
+ bits += floatsToBits([scale_xy[0], scale_xy[1]])
+ else:
+ bits += '0'
+
+ # rotation
+ if rot_xy:
+ bits += '1'
+ bits += floatsToBits([rot_xy[0], rot_xy[1]])
+ else:
+ bits += '0'
+
+ # translation (no flag here)
+ if trans_xy:
+ bits += twitsToBits([trans_xy[0], trans_xy[1]])
+ else:
+ bits += twitsToBits([0,0])
+
+ # done
+ return bits
+
+
+## Control tags
+
+class ControlTag(Tag):
+ def __init__(self):
+ Tag.__init__(self)
+
+
+class FileAttributesTag(ControlTag):
+ def __init__(self):
+ ControlTag.__init__(self)
+ self.tagtype = 69
+
+ def ProcessTag(self):
+ self.bytes = '\x00'.encode('ascii') * (1+3)
+
+
+class ShowFrameTag(ControlTag):
+ def __init__(self):
+ ControlTag.__init__(self)
+ self.tagtype = 1
+ def ProcessTag(self):
+ self.bytes = binary_type()
+
+class SetBackgroundTag(ControlTag):
+ """ Set the color in 0-255, or 0-1 (if floats given). """
+ def __init__(self, *rgb):
+ self.tagtype = 9
+ if len(rgb)==1:
+ rgb = rgb[0]
+ self.rgb = rgb
+
+ def ProcessTag(self):
+ bb = binary_type()
+ for i in range(3):
+ clr = self.rgb[i]
+ if isinstance(clr, float):
+ clr = clr * 255
+ bb += intToUint8(clr)
+ self.bytes = bb
+
+
+class DoActionTag(Tag):
+ def __init__(self, action='stop'):
+ Tag.__init__(self)
+ self.tagtype = 12
+ self.actions = [action]
+
+ def Append(self, action):
+ self.actions.append( action )
+
+ def ProcessTag(self):
+ bb = binary_type()
+
+ for action in self.actions:
+ action = action.lower()
+ if action == 'stop':
+ bb += '\x07'.encode('ascii')
+ elif action == 'play':
+ bb += '\x06'.encode('ascii')
+ else:
+ print("warning, unkown action: %s" % action)
+
+ bb += intToUint8(0)
+ self.bytes = bb
+
+
+
+## Definition tags
+
+class DefinitionTag(Tag):
+ counter = 0 # to give automatically id's
+ def __init__(self):
+ Tag.__init__(self)
+ DefinitionTag.counter += 1
+ self.id = DefinitionTag.counter # id in dictionary
+
+
+class BitmapTag(DefinitionTag):
+
+ def __init__(self, im):
+ DefinitionTag.__init__(self)
+ self.tagtype = 36 # DefineBitsLossless2
+
+ # convert image (note that format is ARGB)
+ # even a grayscale image is stored in ARGB, nevertheless,
+ # the fabilous deflate compression will make it that not much
+ # more data is required for storing (25% or so, and less than 10%
+ # when storing RGB as ARGB).
+
+ if len(im.shape)==3:
+ if im.shape[2] in [3, 4]:
+ tmp = np.ones((im.shape[0], im.shape[1], 4), dtype=np.uint8)*255
+ for i in range(3):
+ tmp[:,:,i+1] = im[:,:,i]
+ if im.shape[2]==4:
+ tmp[:,:,0] = im[:,:,3] # swap channel where alpha is in
+ else:
+ raise ValueError("Invalid shape to be an image.")
+
+ elif len(im.shape)==2:
+ tmp = np.ones((im.shape[0], im.shape[1], 4), dtype=np.uint8)*255
+ for i in range(3):
+ tmp[:,:,i+1] = im[:,:]
+ else:
+ raise ValueError("Invalid shape to be an image.")
+
+ # we changed the image to uint8 4 channels.
+ # now compress!
+ self._data = zlib.compress(tmp.tostring(), zlib.DEFLATED)
+ self.imshape = im.shape
+
+
+ def ProcessTag(self):
+
+ # build tag
+ bb = binary_type()
+ bb += intToUint16(self.id) # CharacterID
+ bb += intToUint8(5) # BitmapFormat
+ bb += intToUint16(self.imshape[1]) # BitmapWidth
+ bb += intToUint16(self.imshape[0]) # BitmapHeight
+ bb += self._data # ZlibBitmapData
+
+ self.bytes = bb
+
+
+class PlaceObjectTag(ControlTag):
+ def __init__(self, depth, idToPlace=None, xy=(0,0), move=False):
+ ControlTag.__init__(self)
+ self.tagtype = 26
+ self.depth = depth
+ self.idToPlace = idToPlace
+ self.xy = xy
+ self.move = move
+
+ def ProcessTag(self):
+ # retrieve stuff
+ depth = self.depth
+ xy = self.xy
+ id = self.idToPlace
+
+ # build PlaceObject2
+ bb = binary_type()
+ if self.move:
+ bb += '\x07'.encode('ascii')
+ else:
+ bb += '\x06'.encode('ascii') # (8 bit flags): 4:matrix, 2:character, 1:move
+ bb += intToUint16(depth) # Depth
+ bb += intToUint16(id) # character id
+ bb += self.MakeMatrixRecord(trans_xy=xy).ToBytes() # MATRIX record
+ self.bytes = bb
+
+
+class ShapeTag(DefinitionTag):
+ def __init__(self, bitmapId, xy, wh):
+ DefinitionTag.__init__(self)
+ self.tagtype = 2
+ self.bitmapId = bitmapId
+ self.xy = xy
+ self.wh = wh
+
+ def ProcessTag(self):
+ """ Returns a defineshape tag. with a bitmap fill """
+
+ bb = binary_type()
+ bb += intToUint16(self.id)
+ xy, wh = self.xy, self.wh
+ tmp = self.MakeRectRecord(xy[0],wh[0],xy[1],wh[1]) # ShapeBounds
+ bb += tmp.ToBytes()
+
+ # make SHAPEWITHSTYLE structure
+
+ # first entry: FILLSTYLEARRAY with in it a single fill style
+ bb += intToUint8(1) # FillStyleCount
+ bb += '\x41'.encode('ascii') # FillStyleType (0x41 or 0x43, latter is non-smoothed)
+ bb += intToUint16(self.bitmapId) # BitmapId
+ #bb += '\x00' # BitmapMatrix (empty matrix with leftover bits filled)
+ bb += self.MakeMatrixRecord(scale_xy=(20,20)).ToBytes()
+
+# # first entry: FILLSTYLEARRAY with in it a single fill style
+# bb += intToUint8(1) # FillStyleCount
+# bb += '\x00' # solid fill
+# bb += '\x00\x00\xff' # color
+
+
+ # second entry: LINESTYLEARRAY with a single line style
+ bb += intToUint8(0) # LineStyleCount
+ #bb += intToUint16(0*20) # Width
+ #bb += '\x00\xff\x00' # Color
+
+ # third and fourth entry: NumFillBits and NumLineBits (4 bits each)
+ bb += '\x44'.encode('ascii') # I each give them four bits, so 16 styles possible.
+
+ self.bytes = bb
+
+ # last entries: SHAPERECORDs ... (individual shape records not aligned)
+ # STYLECHANGERECORD
+ bits = BitArray()
+ bits += self.MakeStyleChangeRecord(0,1,moveTo=(self.wh[0],self.wh[1]))
+ # STRAIGHTEDGERECORD 4x
+ bits += self.MakeStraightEdgeRecord(-self.wh[0], 0)
+ bits += self.MakeStraightEdgeRecord(0, -self.wh[1])
+ bits += self.MakeStraightEdgeRecord(self.wh[0], 0)
+ bits += self.MakeStraightEdgeRecord(0, self.wh[1])
+
+ # ENDSHAPRECORD
+ bits += self.MakeEndShapeRecord()
+
+ self.bytes += bits.ToBytes()
+
+ # done
+ #self.bytes = bb
+
+ def MakeStyleChangeRecord(self, lineStyle=None, fillStyle=None, moveTo=None):
+
+ # first 6 flags
+ # Note that we use FillStyle1. If we don't flash (at least 8) does not
+ # recognize the frames properly when importing to library.
+
+ bits = BitArray()
+ bits += '0' # TypeFlag (not an edge record)
+ bits += '0' # StateNewStyles (only for DefineShape2 and Defineshape3)
+ if lineStyle: bits += '1' # StateLineStyle
+ else: bits += '0'
+ if fillStyle: bits += '1' # StateFillStyle1
+ else: bits += '0'
+ bits += '0' # StateFillStyle0
+ if moveTo: bits += '1' # StateMoveTo
+ else: bits += '0'
+
+ # give information
+ # todo: nbits for fillStyle and lineStyle is hard coded.
+
+ if moveTo:
+ bits += twitsToBits([moveTo[0], moveTo[1]])
+ if fillStyle:
+ bits += intToBits(fillStyle,4)
+ if lineStyle:
+ bits += intToBits(lineStyle,4)
+
+ return bits
+ #return bitsToBytes(bits)
+
+
+ def MakeStraightEdgeRecord(self, *dxdy):
+ if len(dxdy)==1:
+ dxdy = dxdy[0]
+
+ # determine required number of bits
+ xbits, ybits = signedIntToBits(dxdy[0]*20), signedIntToBits(dxdy[1]*20)
+ nbits = max([len(xbits),len(ybits)])
+
+ bits = BitArray()
+ bits += '11' # TypeFlag and StraightFlag
+ bits += intToBits(nbits-2,4)
+ bits += '1' # GeneralLineFlag
+ bits += signedIntToBits(dxdy[0]*20,nbits)
+ bits += signedIntToBits(dxdy[1]*20,nbits)
+
+ # note: I do not make use of vertical/horizontal only lines...
+
+ return bits
+ #return bitsToBytes(bits)
+
+
+ def MakeEndShapeRecord(self):
+ bits = BitArray()
+ bits += "0" # TypeFlag: no edge
+ bits += "0"*5 # EndOfShape
+ return bits
+ #return bitsToBytes(bits)
+
+
+## Last few functions
+
+
+
+def buildFile(fp, taglist, nframes=1, framesize=(500,500), fps=10, version=8):
+ """ Give the given file (as bytes) a header. """
+
+ # compose header
+ bb = binary_type()
+ bb += 'F'.encode('ascii') # uncompressed
+ bb += 'WS'.encode('ascii') # signature bytes
+ bb += intToUint8(version) # version
+ bb += '0000'.encode('ascii') # FileLength (leave open for now)
+ bb += Tag().MakeRectRecord(0,framesize[0], 0, framesize[1]).ToBytes()
+ bb += intToUint8(0) + intToUint8(fps) # FrameRate
+ bb += intToUint16(nframes)
+ fp.write(bb)
+
+ # produce all tags
+ for tag in taglist:
+ fp.write( tag.GetTag() )
+
+ # finish with end tag
+ fp.write( '\x00\x00'.encode('ascii') )
+
+ # set size
+ sze = fp.tell()
+ fp.seek(4)
+ fp.write( intToUint32(sze) )
+
+
+def writeSwf(filename, images, duration=0.1, repeat=True):
+ """ writeSwf(filename, images, duration=0.1, repeat=True)
+
+ Write an swf-file from the specified images. If repeat is False,
+ the movie is finished with a stop action. Duration may also
+ be a list with durations for each frame (note that the duration
+ for each frame is always an integer amount of the minimum duration.)
+
+ Images should be a list consisting of PIL images or numpy arrays.
+ The latter should be between 0 and 255 for integer types, and
+ between 0 and 1 for float types.
+
+ """
+
+ # Check Numpy
+ if np is None:
+ raise RuntimeError("Need Numpy to write an SWF file.")
+
+ # Check images (make all Numpy)
+ images2 = []
+ images = checkImages(images)
+ if not images:
+ raise ValueError("Image list is empty!")
+ for im in images:
+ if PIL and isinstance(im, PIL.Image.Image):
+ if im.mode == 'P':
+ im = im.convert()
+ im = np.asarray(im)
+ if len(im.shape)==0:
+ raise MemoryError("Too little memory to convert PIL image to array")
+ images2.append(im)
+
+ # Init
+ taglist = [ FileAttributesTag(), SetBackgroundTag(0,0,0) ]
+
+ # Check duration
+ if hasattr(duration, '__len__'):
+ if len(duration) == len(images2):
+ duration = [d for d in duration]
+ else:
+ raise ValueError("len(duration) doesn't match amount of images.")
+ else:
+ duration = [duration for im in images2]
+
+ # Build delays list
+ minDuration = float(min(duration))
+ delays = [round(d/minDuration) for d in duration]
+ delays = [max(1,int(d)) for d in delays]
+
+ # Get FPS
+ fps = 1.0/minDuration
+
+ # Produce series of tags for each image
+ t0 = time.time()
+ nframes = 0
+ for im in images2:
+ bm = BitmapTag(im)
+ wh = (im.shape[1], im.shape[0])
+ sh = ShapeTag(bm.id, (0,0), wh)
+ po = PlaceObjectTag(1,sh.id, move=nframes>0)
+ taglist.extend( [bm, sh, po] )
+ for i in range(delays[nframes]):
+ taglist.append( ShowFrameTag() )
+ nframes += 1
+
+ if not repeat:
+ taglist.append(DoActionTag('stop'))
+
+ # Build file
+ t1 = time.time()
+ fp = open(filename,'wb')
+ try:
+ buildFile(fp, taglist, nframes=nframes, framesize=wh, fps=fps)
+ except Exception:
+ raise
+ finally:
+ fp.close()
+ t2 = time.time()
+
+ #print("Writing SWF took %1.2f and %1.2f seconds" % (t1-t0, t2-t1) )
+
+
+def _readPixels(bb, i, tagType, L1):
+ """ With pf's seed after the recordheader, reads the pixeldata.
+ """
+
+ # Check Numpy
+ if np is None:
+ raise RuntimeError("Need Numpy to read an SWF file.")
+
+ # Get info
+ charId = bb[i:i+2]; i+=2
+ format = ord(bb[i:i+1]); i+=1
+ width = bitsToInt( bb[i:i+2], 16 ); i+=2
+ height = bitsToInt( bb[i:i+2], 16 ); i+=2
+
+ # If we can, get pixeldata and make nunmpy array
+ if format != 5:
+ print("Can only read 24bit or 32bit RGB(A) lossless images.")
+ else:
+ # Read byte data
+ offset = 2+1+2+2 # all the info bits
+ bb2 = bb[i:i+(L1-offset)]
+
+ # Decompress and make numpy array
+ data = zlib.decompress(bb2)
+ a = np.frombuffer(data, dtype=np.uint8)
+
+ # Set shape
+ if tagType == 20:
+ # DefineBitsLossless - RGB data
+ try:
+ a.shape = height, width, 3
+ except Exception:
+ # Byte align stuff might cause troubles
+ print("Cannot read image due to byte alignment")
+ if tagType == 36:
+ # DefineBitsLossless2 - ARGB data
+ a.shape = height, width, 4
+ # Swap alpha channel to make RGBA
+ b = a
+ a = np.zeros_like(a)
+ a[:,:,0] = b[:,:,1]
+ a[:,:,1] = b[:,:,2]
+ a[:,:,2] = b[:,:,3]
+ a[:,:,3] = b[:,:,0]
+
+ return a
+
+
+def readSwf(filename, asNumpy=True):
+ """ readSwf(filename, asNumpy=True)
+
+ Read all images from an SWF (shockwave flash) file. Returns a list
+ of numpy arrays, or, if asNumpy is false, a list if PIL images.
+
+ Limitation: only read the PNG encoded images (not the JPG encoded ones).
+
+ """
+
+ # Check whether it exists
+ if not os.path.isfile(filename):
+ raise IOError('File not found: '+str(filename))
+
+ # Check PIL
+ if (not asNumpy) and (PIL is None):
+ raise RuntimeError("Need PIL to return as PIL images.")
+
+ # Check Numpy
+ if np is None:
+ raise RuntimeError("Need Numpy to read SWF files.")
+
+ # Init images
+ images = []
+
+ # Open file and read all
+ fp = open(filename, 'rb')
+ bb = fp.read()
+
+ try:
+ # Check opening tag
+ tmp = bb[0:3].decode('ascii', 'ignore')
+ if tmp.upper() == 'FWS':
+ pass # ok
+ elif tmp.upper() == 'CWS':
+ # Decompress movie
+ bb = bb[:8] + zlib.decompress(bb[8:])
+ else:
+ raise IOError('Not a valid SWF file: ' + str(filename))
+
+ # Set filepointer at first tag (skipping framesize RECT and two uin16's
+ i = 8
+ nbits = bitsToInt(bb[i:i+1], 5) # skip FrameSize
+ nbits = 5 + nbits * 4
+ Lrect = nbits / 8.0
+ if Lrect%1:
+ Lrect += 1
+ Lrect = int(Lrect)
+ i += Lrect+4
+
+ # Iterate over the tags
+ counter = 0
+ while True:
+ counter += 1
+
+ # Get tag header
+ head = bb[i:i+6]
+ if not head:
+ break # Done (we missed end tag)
+
+ # Determine type and length
+ T, L1, L2 = getTypeAndLen( head )
+ if not L2:
+ print('Invalid tag length, could not proceed')
+ break
+ #print(T, L2)
+
+ # Read image if we can
+ if T in [20, 36]:
+ im = _readPixels(bb, i+6, T, L1)
+ if im is not None:
+ images.append(im)
+ elif T in [6, 21, 35, 90]:
+ print('Ignoring JPEG image: cannot read JPEG.')
+ else:
+ pass # Not an image tag
+
+ # Detect end tag
+ if T==0:
+ break
+
+ # Next tag!
+ i += L2
+
+ finally:
+ fp.close()
+
+ # Convert to normal PIL images if needed
+ if not asNumpy:
+ images2 = images
+ images = []
+ for im in images2:
+ images.append( PIL.Image.fromarray(im) )
+
+ # Done
+ return images
Added: grass/trunk/lib/python/imaging/imaginglib.dox
===================================================================
--- grass/trunk/lib/python/imaging/imaginglib.dox (rev 0)
+++ grass/trunk/lib/python/imaging/imaginglib.dox 2013-11-10 03:58:13 UTC (rev 58181)
@@ -0,0 +1,33 @@
+/*! \page imaging Python library for animations
+
+
+\tableofcontents
+
+
+\section imagingIntro Introduction
+Library python.imaging is a third-party python library for animations.
+It comes from visvis project (https://code.google.com/p/visvis/), version 1.8.
+Library contains 4 main files: images2avi.py, images2gif.py, images2ims.py, images2swf.py for exporting
+AVI video file, animated GIF, series of images and SWF file format, respectively.
+There are functions for reading/writing those formats (writeAvi(), readAvi(), writeGif(), readGif(),
+writeIms(), readIms(), writeSwf(), readSwf()). Library requires PIL (Python Imaging Library) and numpy packages.
+The input of all write functions are PIL images.
+
+Please read README in library's directory.
+
+Example:
+
+\code{.py}
+from grass.imaging import writeIms
+...
+writeIms(filename=path/to/directory/anim.png, images=myPILImagesList)
+\endcode
+
+This creates files anim1.png, anim2.png, ... in path/to/directory.
+
+
+\section imagingAuthors Authors
+
+Almar Klein (see licence in header of library files)
+
+*/
More information about the grass-commit
mailing list