Source code for imtools

# imtools.py - Simple but often used load/save/show routines for images
# 
# Author: Stefan Fuertinger [stefan.fuertinger@gmx.at]
# Created: June 13 2012
# Last modified: <2017-09-21 11:15:41>

from __future__ import division

import numpy as np
import matplotlib.pyplot as plt
import os
import datetime
import shutil
from glob import glob
from string import join

##########################################################################################
[docs]def imwrite(figobj,fstr,dpi=None): """ Save a Matplotlib figure camera-ready using a "tight" bounding box Parameters ---------- figobj : Matplotlib figure Matplotlib figure object to be saved fstr : string String holding the filename to be used to save the figure. If a specific file format is wanted, provide it with `fstr`, e.g., `fstr = 'output.tiff'`. If `fstr` does not contain a filename extension the Matplotlib default (png) will be used. dpi : integer The wanted resolution of the output in dots per inch. If `None` the Matplotlib default will be used. Returns ------- Nothing : None Notes ----- This is a humble attempt to get rid of the huge white areas around plots that are generated by Matplotlib's `savefig` when saving a figure as an image using default values. It tries to mimic `export_fig for MATLAB <http://www.mathworks.com/matlabcentral/fileexchange/23629-export-fig>`_. The result, however, is not perfect yet... See also -------- savefig : in the `Matplotlib documentation <http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.savefig>`_ """ # Check if `figobj` is really a figure if type(figobj).__name__ != "Figure": raise TypeError("figobj has to be a valid matplotlib Figure object!") # Make sure `fstr` doesn't contain weirdness and points to an existing place if not isinstance(fstr,(str,unicode)): raise TypeError("Output filename has to be a string!") fstr = str(fstr) if fstr.find("~") == 0: fstr = os.path.expanduser('~') + fstr[1:] slash = fstr.rfind(os.sep) if slash >= 0 and not os.path.isdir(fstr[:slash]): raise ValueError('Invalid path for output file: '+fstr+'!') # Check if filename extension has been provided dt = fstr.rfind('.') if dt == -1: fname = fstr+'.png' ext = 'png' elif len(fstr[dt+1:]) < 2: print "Invalid filename extension: "+fstr[dt:]+" Defaulting to png..." fname = fstr[:dt]+'.png' ext = 'png' else: fname = fstr ext = fstr[dt+1:] # Save the figure using "tight" options for the bounding box figobj.savefig(fname,bbox_inches="tight",ppad_inches=0,dpi=dpi,format=ext) return
##########################################################################################
[docs]def normalize(I,a=0,b=1): """ Re-scale a NumPy ndarray Parameters ---------- I: NumPy ndarray Array to be normalized a : float The lower normalization bound. By default `a = 0` (`a` has to satisfy `a < b`) b : float The upper normalization bound. By default `b = 1` (`b` has to satisfy `b < a`) Returns ------- In : NumPy ndarray Scaled version of the input array `I`, such that `a = In.min()` and `b = In.max()` Examples -------- >>> I = array([[-1,.2],[100,0]]) >>> In = normalize(I,a=-10,b=12) >>> In array([[-10. , -9.73861386], [ 12. , -10. ]]) """ # Ensure that `I` is a NumPy-ndarray try: tmp = I.size == 1 except TypeError: raise TypeError('I has to be a NumPy ndarray!') if (tmp): raise ValueError('I has to be a NumPy ndarray of size > 1!') # If normalization bounds are user specified, check them try: tmp = b <= a except TypeError: raise TypeError('a and b have to be scalars satisfying a < b!') if (tmp): raise ValueError('a has to be strictly smaller than b!') if np.absolute(a - b) < np.finfo(float).eps: raise ValueError('|a-b|<eps, no normalization possible') # Get min and max of I Imin = I.min() Imax = I.max() # If min and max values of I are identical do nothing, if they differ close to machine precision abort if Imin == Imax: return I elif np.absolute(Imin - Imax) < np.finfo(float).eps: raise ValueError('|Imin-Imax|<eps, no normalization possible') # Make a local copy of I I = I.copy() # Here the normalization is done I = (I - Imin)*(b - a)/(Imax - Imin) + a # Return normalized array return I
##########################################################################################
[docs]def blendedges(Im,chim): """ Superimpose a (binary) edge set on an gray-scale image using Matplotlib's `imshow` Parameters ---------- Im: NumPy 2darray Grayscale image (has to be a 2D array) chim: NumPy 2darray Binary edge map (has to be a 2D array). Note that the edge map must be binary, i.e., it must only contain the values 0 and 1 Returns ------- Nothing : None See also -------- imshow : in the `Matplotlib documentation <http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.imshow>`_ Stackoverflow : `This submission <http://stackoverflow.com/questions/2495656/variable-alpha-blending-in-pylab>`_ illustrates how to use variable alpha blending in Matplotlib """ # Sanity checks if type(Im).__name__ != "ndarray": raise TypeError("Im has to be a NumPy ndarray!") else: if len(Im.shape) > 2: raise ValueError("Im has to be 2-dimensional!") try: Im.shape[1] except: raise ValueError("Im has to be an image!") if np.isnan(Im).max() == True or np.isinf(Im).max() == True: raise ValueError("Im must not contain NaNs or Infs!") if type(chim).__name__ != "ndarray": raise TypeError("chim has to be a NumPy ndarray!") else: if len(chim.shape) > 2: raise ValueError("chim has to be 2-dimensional!") try: chim.shape[1] except: raise ValueError("chim has to be an edge map!") if np.isnan(chim).max() == True or np.isinf(chim).max() == True: raise ValueError("chim must not contain NaNs or Infs!") chim = chim.astype(float) chiu = np.unique(chim) if chiu.size != 2: raise ValueError("chim has to be binary!") if chiu.min() != 0 or chiu.max() != 1: raise ValueError("chim has to be a binary edge map!") # Now do something plt.imshow(Im,cmap="gray",interpolation="nearest") plt.hold(True) plt.imshow(mycmap(chim)) plt.axis("off") plt.draw() return
########################################################################################## def mycmap(x): """ Generate a custom color map, setting alpha values to one on edge points, and to zero otherwise Notes ----- This code is based on the suggestion found at this `Stackoverflow thread <http://stackoverflow.com/questions/2495656/variable-alpha-blending-in-pylab >` """ # Convert edge map to Matplotlib colormap (shape (N,N,4)) tmp = plt.cm.hsv(x) # Set alpha values to one on edge points tmp[:,:,3] = x return tmp ##########################################################################################
[docs]def recmovie(figobj=None, movie=None, savedir=None, fps=None): """ Save Matplotlib figures and generate a movie sequences Parameters ---------- figobj : Matplotlib figure Figure to base the movie on movie : str Filename of the generated movie savedir : str Output directory for video file or name of directory of/for source image files fps : int Target frames per second Returns ------- Nothing : None Examples -------- The command >>> recmovie(figobj) saves the Matplotlib figure-object `figobj` in the default directory `_tmp` as png-image. If the default directory is empty the image will be named `_tmp0000.png`, otherwise the highest number in the png-file-names incremented by one will be used as filename. Use >>> recmovie(figobj,savedir="somedir") to save the Matplotlib figure-object `figobj` in the directory defined by the string `savedir`. If the directory does not exist, it will be created. If the directory `savedir` is empty the image will be named `_tmp0000.png`, otherwise the highest number in the png-file-names incremented by one will be used as filename. The command `recmovie()` will attempt to use `mencoder <http://en.wikipedia.org/wiki/MEncoder>`_ to generate an avi-movie composed of the png-images found in the default directory `_tmp`. The movie's default name will be composed of the default prefix `_tmp` and the current date and time. After the movie has been generated the default-directory `_tmp` and its contents will be deleted. Use >>> recmovie(movie="somename") to launch mencoder and generate an avi-video composed of the png-images found in the default directory `_tmp`. The string `movie` will be used as filename for the generated movie (in the above example a file `somename.avi` will be created). After the movie has been generated the default-directory `_tmp` and its contents will be deleted. Similarly >>> recmovie(savedir="somedir") will generate an avi-movie composed of the png-images found in the directory specified by the string `savedir`. The movie's name is given by the prefix `_tmp` and the current date and time. After the movie has been generated it will be moved to the directory `savedir`. If a movie-file of the same name exists in `savedir` a *WARNING* is printed and the movie will not be moved. Analogously, >>> recmovie(movie="somename",savedir="somedir") will generate an avi-movie named "somename" composed of the png-images found in the directory specified by the string `savedir`. After the movie has been generated it will be moved to the directory `savedir`. If a movie-file of the same name exists in `savedir` a *WARNING* is printed and the movie will not be moved. **Note:** the command >>> recmovie(figobj,movie="somename",savedir="somedir") will ONLY save the Matplotlib-figure-object `figobj` in the directory defined by the string `savdir`. The optional argument `movie` will be ignored. **Note:** the default-directory, image-format and movie-type can be changed in the source code by editing the variables `prefix`, `imgtype` and `movtype`. See also -------- Matplotlib : a `collection of codes <http://matplotlib.org/examples/animation/index.html>`_ illustrating how to use animation in Matplotlib """ # Set default values prefix = "_tmp" numdigits = "%04d" imgtype = "png" movtype = "avi" fpsno = 25 now = datetime.datetime.now() savedirname = prefix moviename = "movie"+"_"+repr(now.hour)+repr(now.minute)+repr(now.second) # Assign defaults if movie is None: movie = moviename if savedir is None: savedir = savedirname if fps is None: fps = fpsno # Make sure `figobj` is actually a figure if figobj != None: if type(figobj).__name__ != "Figure": raise TypeError("figobj has to be a valid Matplotlib Figure object!") # Check if movie filename makes sense and points to an existing location if not isinstance(movie,(str,unicode)): raise TypeError("Output filename for movie has to be a string!") movie = str(movie) if movie.find("~") == 0: movie = os.path.expanduser('~') + movie[1:] if not os.path.isdir(movie[:movie.rfind(os.sep)]): raise ValueError('Invalid path for output filename for movie: '+movie+'!') # Make sure `savedir` exists if not isinstance(savedir,(str,unicode)): raise TypeError('Output filename has to be a string!') savedir = str(savedir) if savedir.find("~") == 0: savedir = os.path.expanduser('~') + savedir[1:] if not os.path.isdir(savedir): raise ValueError('Invalid path for output file: '+savedir+'!') # Convert possible float argument to integer, if it does not work raise a TypeError try: fps = int(fps) except: raise TypeError("fps has to be an integer (see man mencoder for details)!") # Check if mencoder is available if os.system("which mencoder > /dev/null") != 0: print "\n\nWARNING: mencoder was not found on your system - movie generation won't work!!!\n\n" # Check if movie already exists, if yes abort if len(glob(movie)) != 0: errormsg = "Movie %s already exists! Aborting..."%savedir raise ValueError(errormsg) # If not already existent, create directory savedir if len(glob(savedir)) == 0: os.mkdir(savedir) # If a none-default savedir was chosen, automatically keep images # and move movie to this non-standard savedir if savedir != savedirname: keepimgs = 1 else: keepimgs = 0 # List all imgtype-files in directory savedir filelist = glob(savedir+os.sep+"*."+imgtype) # If we have been called with a figobj save it in savedir if figobj != None: # If there are already imgtype-files in the directory then filelist!=0 if len(filelist) != 0: # Sort filelist, s.t. the file having the highest no. is the last element filelist.sort() scounter = filelist[-1] # Remove the path and prefix from the last elements filename scounter = scounter.replace(savedir+os.sep+prefix,"") # Split the name further s.t. it is only number+imgtype scounter = scounter.split(".")[0] # Convert the string holding the file's no. to an integer counter = int(scounter) + 1 # No files are present in savedir, start with 0 else: counter = 0 # Generate the name the file is stored under (prefix+numdigits(counter).imgtype, e.g. _tmp0102.png) fname = savedir+os.sep+prefix+numdigits+"."+imgtype fname = fname%counter # Save the figure using the just generated filename figobj.savefig(fname) # User wants to generate a movie consisting of imgtyp-files in a directory savedir else: # Check if there are any files to process in savedir, if not raise an error if len(filelist) == 0: errormsg = "No %s-images found in directory %s! Aborting..."%(imgtype,savedir) raise ValueError(errormsg) # This is the standard command used to generate the movie command = ["mencoder", "mf://*.png", "-mf", "type=png:w=800:h=600:fps=25", "-ovc", "lavc", "-lavcopts", "vcodec=mpeg4", "-oac", "copy", "-o", "output.avi"] # Make necessary changes here (like pointing to the right savedir, imgtype, movie,...) command[1] = "mf://"+savedir+os.sep+"*."+imgtype command[3] = "type="+imgtype+":w=800:h=600:fps="+str(fps) command[-1] = movie+"."+movtype # Call mencoder to generate movie os.system(join(command)) # If we have been called using the default savedir, erase it (and its contents) if keepimgs == 0: for f in glob(savedir+os.sep+"*"+imgtype): os.unlink(f) os.rmdir(savedir) # If not don't erase it but (try to) move the generated movie into this savedir else: try: shutil.move(movie+"."+movtype,savedir+os.sep) except: print "\n\n\nWARNING: Movie %s already exists in directory %s. I won't move it there but keep it here. "%(movie,savedir)