[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