# 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)