"""Module of image visualization functions
This module contains an assortment of functions relevant to the plotting and
visualization of various image-relevant data
"""
# This library was developed for the Georgia Tech graduate course ECE 6258:
# Digital Image Processing with Professor Ghassan AlRegib.
# For comments and feedback, please email dippykit[at]gmail.com
# Functional imports
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.axes import Axes
from multiprocessing import Process, Queue
# General imports
from typing import Callable, Any, Tuple, List
__author__ = 'Brighton Ancelin, Motaz Alfarraj, Ghassan AlRegib'
__all__ = ['imshow', 'quiver', 'surf', 'setup_continuous_rendering', 'zlabel']
[docs]def imshow(
im: np.ndarray,
*args,
**kwargs
) -> None:
"""Displays an image
Displays the argument image with optional parameters. If the image has a
dtype of uint8, then by default the vmin and vmax parameters will be set
to 0 and 255 respectively. This is to provided accurate depictions of
otherwise dark images.
This function is essentially a wrapper for `matplotlib.pyplot.imshow`_,
so more detailed documentation may be found there.
:type im: ``numpy.ndarray``
:param im: The image to be displayed.
:return: None
.. note::
This function wraps around functions from other packages. Reading
these functions' documentations may be useful. See the **See also**
section for more information.
.. seealso::
`matplotlib.pyplot.imshow`_
Documentation of the random_noise function from Scikit Image
.. _matplotlib.pyplot.imshow: https://matplotlib.org/api/_as_gen
/matplotlib.pyplot.imshow.html
"""
if im.dtype == np.uint8:
info = np.iinfo(im.dtype)
if 'vmin' not in kwargs:
kwargs['vmin'] = info.min
if 'vmax' not in kwargs:
kwargs['vmax'] = info.max
plt.imshow(im, *args, **kwargs)
else:
plt.imshow(im, *args, **kwargs)
plt.axis('off')
[docs]def surf(
x: np.ndarray,
y: np.ndarray,
z: np.ndarray,
**kwargs
) -> None:
"""Plots a surface
Plots the x, y, and z values as a surface in 3D.
This function is essentially a wrapper for
`mpl_toolkits.mplot3d.axes3d.Axes3D.plot_surface`_, so more detailed
documentation may be found there.
:type x: ``numpy.ndarray``
:param x: The array of x coordinates for the surface plot.
:type y: ``numpy.ndarray``
:param y: The array of y coordinates for the surface plot.
:type z: ``numpy.ndarray``
:param z: The array of z coordinates for the surface plot.
.. note::
This function wraps around functions from other packages. Reading
these functions' documentations may be useful. See the **See also**
section for more information.
.. seealso::
`mpl_toolkits.mplot3d.axes3d.Axes3D.plot_surface`_
Documentation of the plot_surface function from Matplotlib
.. _mpl_toolkits.mplot3d.axes3d.Axes3D.plot_surface: https://matplotlib
.org/mpl_toolkits/mplot3d/tutorial.html#surface-plots
"""
x = np.array(x)
y = np.array(y)
z = np.array(z)
if (1 == x.ndim) and (1 == y.ndim):
x = x.reshape(-1, 1)
y = y.reshape(1, -1)
ax = plt.gca(projection='3d')
ax.plot_surface(x, y, z, **kwargs)
[docs]def zlabel(
s: str,
*args,
**kwargs
) -> None:
"""Writes a string to the z axis label
Provided that the current axes has a z axis, this function will write
the given string to the axis label.
This function is essentially a wrapper for
`mpl_toolkits.mplot3d.axes3d.Axes3D.set_zlabel`_, so more detailed
documentation may be found there.
:type s: ``str``
:param s: The string to write to the z axis label.
:return: None
.. note::
This function wraps around functions from other packages. Reading
these functions' documentations may be useful. See the **See also**
section for more information.
.. seealso::
`mpl_toolkits.mplot3d.axes3d.Axes3D.set_zlabel`_
Documentation of the set_ylabel function (set_zlabel has no
formal documentation) from Matplotlib
.. _mpl_toolkits.mplot3d.axes3d.Axes3D.set_zlabel: https://matplotlib.org
/api/_as_gen/matplotlib.axes.Axes.set_ylabel.html
"""
ax = plt.gca()
assert 'set_zlabel' in dir(ax), \
"Can't set z label if the current axes don't have a z axis."
ax.set_zlabel(s, *args, **kwargs)
[docs]def quiver(
*args,
**kwargs
) -> None:
"""Plots a field of arrows
Plots a field of arrow on the current axes. If the following keyword
arguments are not set, then they will take on the following default values:
* 'units': 'xy'
* 'angles': 'xy'
* 'scale_units': 'xy'
* 'scale': The mean of the magnitudes of the U and V vectors.
This function is essentially a wrapper for
`matplotlib.axes.Axes.quiver`_, so more detailed documentation may be
found there.
:return: None
.. note::
This function wraps around functions from other packages. Reading
these functions' documentations may be useful. See the **See also**
section for more information.
.. seealso::
`matplotlib.axes.Axes.quiver`_
Documentation of the quiver function from Matplotlib
.. _matplotlib.axes.Axes.quiver: https://matplotlib.org/api/_as_gen
/matplotlib.axes.Axes.quiver.html
"""
if 'units' not in kwargs:
kwargs['units'] = 'xy'
if 'angles' not in kwargs:
kwargs['angles'] = 'xy'
if 'scale_units' not in kwargs:
kwargs['scale_units'] = 'xy'
if 'scale' not in kwargs:
if 2 == len(args) or 3 == len(args):
scale = np.mean((args[0] ** 2 + args[1] ** 2) ** 0.5)
elif 4 == len(args) or 5 == len(args):
scale = np.mean((args[2] ** 2 + args[3] ** 2) ** 0.5)
kwargs['scale'] = scale
plt.quiver(*args, **kwargs)
[docs]def setup_continuous_rendering(
render: Callable[[Axes, Any], None],
update: Callable[[Queue], None],
delay: int=100,
auto_play: bool=True,
precompute: bool=False,
clear_axes: bool=True
) -> None:
"""Sets up a continuous renderer
This function sets up a window to display data that can continuously
change. To best understand this function, try copying the example code
below into a python file, running it, and then observing the results.
:type render: ``Callable[[Axes, Any], None]``
:param render: A function that takes a matplotlib Axes object and any
data as arguments. This function returns nothing. This function should
use the Axes object to update the rendering each time a new datum is
received. These data are supplied through the *update* function.
:type update: ``Callable[[Queue], None]``
:param update: A function that takes a Queue as an argument and returns
nothing. This function should update the rendering by putting each new
datum into its queue via the ``Queue.put()`` function. These data are
then subsequently rendered by the *render* function. Once this
function places ``None`` into the queue, the rendering will cease to
update.
:type delay: ``int``
:param delay: (default=100) The time (in milliseconds) between calling
the *render* function to update the rendering. Also known as the
refresh rate.
:type auto_play: ``bool``
:param auto_play: (default=True) If set to false, the rendering will
prompt the user before each update.
:type precompute: ``bool``
:param precompute: (default=False) If set to true, the update function will
be called once before any rendering takes place. In this single call,
the queue is to be populated with all data needed for the entire
rendering process. Setting this parameter to true also removes the
multiprocessing aspect of the visualization, which can be useful for
those with restrictions to multiprocessing in their code.
:type clear_axes: ``bool``
:param clear_axes: (default=True) If set to true, the axes will be cleared
before each rendering. This is generally an individuals desired
performance, but can be disabled if one wants to hand axes manually.
:return: None
Examples:
.. code-block:: python
# This file will generate a rendering of a square moving in an image
import numpy as np
import dippykit as dip
def render_square(ax, data):
# Show the image without any axis
ax.imshow(data, 'gray')
ax.axis('off')
def update_square(queue):
# Create an arbitrary animation of a square progressively moving
# through the image
for i in range(9):
square = np.zeros(9)
square[i] = 1
square = square.reshape((3, 3))
queue.put(square)
# Putting None into the queue tells the renderer to cease updating
queue.put(None)
if __name__ == '__main__':
# Sets up a continuous rendering using the functions above.
# The rendering will update every 1000 milliseconds (1 second).
dip.setup_continuous_rendering(render_square, update_square, 1000)
.. code-block:: python
# This file will generate renderings of happy and sad faces
import numpy as np
import dippykit as dip
def render_face(ax, data):
# Break the data into more manageable variable names
mouth, left_eye, right_eye, is_happy = data
mouth_x, mouth_y = mouth
left_eye_x, left_eye_y = left_eye
right_eye_x, right_eye_y = right_eye
# First, clear the axes
ax.clear()
# Draw the face
ax.plot(mouth_x, mouth_y)
ax.plot(left_eye_x, left_eye_y)
ax.plot(right_eye_x, right_eye_y)
# Set the appropriate title to the axes
if is_happy:
ax.set_title('Happy Face - Press ENTER to toggle')
else:
ax.set_title('Sad Face - Press ENTER to toggle')
def update_face(queue):
# Defining all the arrays
mouth_x = np.array(range(31)) - 15
happy_mouth_y = 20 * (mouth_x/15) ** 2
sad_mouth_y = 20 - happy_mouth_y
u = np.linspace(0, 1, 21)
left_eye_x = np.cos(2 * np.pi * u) - 9
right_eye_x = np.cos(2 * np.pi * u) + 9
eye_y = np.sin(2 * np.pi * u) + 29
# Aggregating the data into single tuples
happy_mouth = (mouth_x, happy_mouth_y)
sad_mouth = (mouth_x, sad_mouth_y)
left_eye = (left_eye_x, eye_y)
right_eye = (right_eye_x, eye_y)
happy_face = (happy_mouth, left_eye, right_eye, True)
sad_face = (sad_mouth, left_eye, right_eye, False)
is_happy = True
while True:
# If the queue is empty
if queue.empty():
# Alternate with happy and sad faces
if is_happy:
queue.put(happy_face)
is_happy = False
else:
queue.put(sad_face)
is_happy = True
if __name__ == '__main__':
# Sets up a continuous rendering using the functions above.
# This rendering will await user input before updating the display.
dip.setup_continuous_rendering(render_face, update_face,
auto_play=False)
"""
queue = Queue()
if not precompute:
update_process = Process(target=update, args=(queue,))
update_process.start()
else:
update(queue)
try:
ax = _setup_window()
_update_window(render, queue, ax, delay, auto_play, clear_axes)
except (KeyboardInterrupt, SystemExit):
# Allow the other process to be terminated
pass
if not precompute:
update_process.terminate()
def _setup_window(
) -> Axes:
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.figure.canvas.draw()
plt.ion()
plt.show()
return ax
def _update_window(
render: Callable[[Axes, Any], None],
queue: Queue,
ax: Axes,
delay: int,
auto_play: bool,
clear_axes: bool,
) -> None:
while queue.empty():
plt.pause(0.005)
val = queue.get_nowait()
while val is not None:
if clear_axes:
ax.clear()
render(ax, val)
# Inefficient, but convenient for users
ax.figure.canvas.draw()
if not auto_play:
input('Press ENTER to continue...')
if delay > 0:
plt.pause(delay/1000)
else:
plt.pause(0.001)
while queue.empty():
plt.pause(0.005)
val = queue.get_nowait()