# Authors:
# Sylvain Faure <sylvain.faure@universite-paris-saclay.fr>
# Bertrand Maury <bertrand.maury@universite-paris-saclay.fr>
# License: GPL
"""Geometry, navigation fields, and plotting utilities for Cromosim domains."""
import logging
import sys
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.lines import Line2D
from matplotlib.patches import Circle, Ellipse, Polygon, Rectangle
from PIL import Image
from PIL import ImageDraw
import skfmm
LOGGER = logging.getLogger(__name__)
def _rgb_mask(red, green, blue, colors):
"""Return a mask selecting pixels that match any RGB color in ``colors``."""
mask = np.zeros_like(red)
for color in colors:
mask += (
(red == color[0])
* (green == color[1])
* (blue == color[2])
)
return mask
def _grid_indices(xyr, xmin, ymin, pixel_size):
"""Return row and column indices for pedestrian coordinates."""
rows = np.floor(
(xyr[:, 1] - ymin - 0.5 * pixel_size) / pixel_size
).astype(int)
cols = np.floor(
(xyr[:, 0] - xmin - 0.5 * pixel_size) / pixel_size
).astype(int)
return rows, cols
[docs]
class Destination:
"""
Represent a target used to guide pedestrians through a domain.
A destination may correspond to a door, a staircase, another floor, or any
other target encoded in the domain image. Desired velocities are obtained
from a fast-marching travel-time field and may optionally be overridden on
pixels of specified colors.
Parameters and attributes
-------------------------
name : str
Destination name.
colors : list
RGB colors ``[[r, g, b], ...]`` used to identify destination pixels.
excluded_colors : list
RGB colors representing regions that cannot be crossed on the way to
this destination.
desired_velocity_from_color : list
Optional explicit desired velocities associated with colors, encoded as
``[r, g, b, vx, vy]``.
fmm_speed : numpy.ndarray or None
Speed field used by the fast-marching solver. If ``None``, a unit speed
field is created when the destination is added to a domain.
velocity_scale : float
Multiplicative factor applied to the pedestrian desired speed.
next_destination : object, optional
Description of the destination(s) reached after this one.
next_destination_choice : object, optional
Optional data used to select the next destination.
next_destination_weights : object, optional
Weights associated with the possible next destinations.
next_domain : object, optional
Domain or domains containing the next destination(s).
next_transit_box : object, optional
Transit-region description used when pedestrians must be duplicated or
transferred between domains.
next_transit_box_xshift, next_transit_box_yshift : object, optional
Coordinate shifts associated with transit regions.
excluded_id : numpy.ndarray or None
Mask identifying pixels excluded for this destination.
distance : numpy.ndarray or None
Fast-marching travel-time (or distance) field.
desired_velocity_X, desired_velocity_Y : numpy.ndarray or None
Components of the normalized desired-velocity field.
Examples
--------
See ``cromosim/examples/domain/domain_room.py``,
``cromosim/examples/domain/domain_stadium.py``,
``cromosim/examples/domain/domain_shibuya_crossing.py`` and
``cromosim/examples/domain/domain_from_json.py``.
"""
def __init__(
self,
name,
colors,
excluded_colors=[],
desired_velocity_from_color=[],
fmm_speed=None,
velocity_scale=1,
next_destination_choice=None,
next_destination=None,
next_domain=None,
next_destination_weights=None,
next_transit_box_xshift=None,
next_transit_box_yshift=None,
next_transit_box=None,
):
"""
Initialize a destination.
Parameters
----------
name : str
Destination name.
colors : list
RGB colors ``[[r, g, b], ...]`` identifying destination pixels.
excluded_colors : list, optional
RGB colors identifying regions that cannot be crossed.
desired_velocity_from_color : list, optional
Explicit velocity overrides encoded as ``[r, g, b, vx, vy]``.
fmm_speed : numpy.ndarray or None, optional
Fast-marching speed field.
velocity_scale : float, optional
Scale factor applied to desired pedestrian speeds.
next_destination_choice : object, optional
Optional next-destination selection data.
next_destination : object, optional
Next destination or destinations.
next_domain : object, optional
Domain or domains associated with the next destination(s).
next_destination_weights : object, optional
Selection weights associated with the next destination(s).
next_transit_box_xshift, next_transit_box_yshift : object, optional
Coordinate shifts associated with transit regions.
next_transit_box : object, optional
Transit-region description.
"""
self.name = name
self.colors = colors
self.excluded_colors = excluded_colors
self.desired_velocity_from_color = desired_velocity_from_color
self.fmm_speed = fmm_speed
self.velocity_scale = velocity_scale
self.next_destination = next_destination
self.next_destination_choice = next_destination_choice
self.next_destination_weights = next_destination_weights
self.next_transit_box = next_transit_box # [xmin,xmax,ymin,ymax]
self.next_transit_box_xshift = next_transit_box_xshift
self.next_transit_box_yshift = next_transit_box_yshift
self.next_domain = next_domain
self.excluded_id = None
self.distance = None
self.desired_velocity_X = None
self.desired_velocity_Y = None
def __str__(self):
"""Return a human-readable summary of the destination."""
return "--> Destination: " \
+ "\n name: "+str(self.name) \
+ "\n colors: "+str(self.colors) \
+ "\n excluded_colors: "+str(self.excluded_colors) \
+ "\n desired_velocity_from_color: "+str(self.desired_velocity_from_color) \
+ "\n next_destination: "+str(self.next_destination) \
+ "\n next_destination_choice: "+str(self.next_destination_choice) \
+ "\n next_destination_weights: "+str(self.next_destination_weights) \
+ "\n next_domain: "+str(self.next_domain) \
+ "\n velocity_scale: "+str(self.velocity_scale) \
+ "\n transit_box: "+str(self.next_transit_box) \
+ "\n transit_box shift: ("+str(self.next_transit_box_xshift)+", "+str(self.next_transit_box_yshift)+")"
[docs]
class Domain:
"""
Represent a two-dimensional computational domain.
A domain is defined by a raster image (or a white image created in memory),
its physical pixel size, wall colors, and a set of destinations. Matplotlib
shapes may be drawn onto the raster before the domain is built.
The class computes wall-distance fields and destination travel-time fields
with a fast-marching method. Their gradients are then used to obtain wall
normals and pedestrian desired-velocity directions.
Attributes
----------
name : str
Domain name.
pixel_size : float
Physical size of one image pixel.
width, height : int
Image dimensions in pixels.
xmin, ymin : float
Physical coordinates of the lower-left corner.
xmax, ymax : float
Physical coordinates of the upper-right corner.
X, Y : numpy.ndarray
Mesh-grid arrays containing pixel-center coordinates.
image : PIL.Image.Image or numpy.ndarray
Domain image. After :meth:`build_domain`, it is stored as a NumPy array
with the vertical axis flipped so that the origin is at the lower left.
image_red, image_green, image_blue : numpy.ndarray
RGB channels created by :meth:`build_domain`.
wall_colors : list
RGB colors used to identify wall pixels.
wall_mask : numpy.ndarray or None
Mask of wall pixels.
wall_id : tuple or None
Indices of wall pixels.
wall_distance : numpy.ndarray or None
Distance to the nearest wall.
wall_grad_X, wall_grad_Y : numpy.ndarray or None
Components of the normalized wall-distance gradient.
destinations : dict or None
Destinations registered in the domain.
Examples
--------
See ``cromosim/examples/domain/domain_room.py``,
``cromosim/examples/domain/domain_stadium.py``,
``cromosim/examples/domain/domain_shibuya_crossing.py`` and
``cromosim/examples/domain/domain_from_json.py``.
"""
def __init__(
self,
name="Domain",
background="White",
pixel_size=1.0,
xmin=0.0,
width=100,
ymin=0.0,
height=100,
wall_colors=[[0, 0, 0]],
npzfile=None,
):
"""
Initialize a computational domain.
Parameters
----------
name : str, optional
Domain name.
background : str, optional
Background image filename. ``"White"`` creates a blank white image.
pixel_size : float, optional
Physical size of one pixel.
xmin, ymin : float, optional
Coordinates of the lower-left corner.
width, height : int, optional
Image dimensions used when ``background="White"``.
wall_colors : list, optional
RGB colors used to identify walls.
npzfile : str or None, optional
Existing ``.npz`` domain file to load instead of creating a new domain.
"""
if (npzfile is None):
self.__shapes = []
self.__outline_color_shapes = []
self.__fill_color_shapes = []
self.__image_filename = ''
self.name = name
self.__background = background
self.destinations = None
self.pixel_size = pixel_size
self.xmin, self.ymin = [xmin, ymin]
if (self.__background != 'White'):
self.image = Image.open(self.__background)
self.width = self.image.size[0]
self.height = self.image.size[1]
else:
self.width, self.height = [width, height]
self.image = Image.new("RGB", (self.width, self.height), "white")
self.people_counter = 0
self.xmax = self.xmin + self.width*pixel_size
self.ymax = self.ymin + self.height*pixel_size
self.X, self.Y = np.meshgrid(np.arange(self.width), np.arange(self.height))
self.X = 0.5*self.pixel_size + self.xmin + self.X*self.pixel_size
self.Y = 0.5*self.pixel_size + self.ymin + self.Y*self.pixel_size
self.wall_colors = wall_colors # walls are black by default
self.wall_mask = None
self.wall_id = None
self.wall_distance = None
self.wall_grad_X = None
self.wall_grad_Y = None
self.draw = ImageDraw.Draw(self.image)
else:
data = np.load(npzfile, allow_pickle=True)
self.__shapes = data["__shapes"].tolist()
self.__outline_color_shapes = data["__outline_color_shapes"].tolist()
self.__fill_color_shapes = data["__fill_color_shapes"].tolist()
self.__image_filename = data["__image_filename"]
# print("read name: ",data["name"])
self.name = str(data["name"])
# print("read __background: ",data["__background"])
self.__background = str(data["__background"])
# print("read destinations: ",data["destinations"])
self.destinations = dict(data["destinations"].tolist())
# print("read pixel_size: ",data["pixel_size"])
self.pixel_size = data["pixel_size"]
# print("read xmin: ",data["xmin"])
self.xmin = data["xmin"]
# print("read ymin: ",data["ymin"])
self.ymin = data["ymin"]
# print("read xmax: ",data["xmax"])
self.xmax = data["xmax"]
# print("read ymax: ",data["ymax"])
self.ymax = data["ymax"]
# print("read width: ",data["width"])
self.width = data["width"]
# print("read height: ",data["height"])
self.height = data["height"]
# print("read X: ",data["X"])
self.X = data["X"]
# print("read Y: ",data["Y"])
self.Y = data["Y"]
# print("read wall_colors: ",data["wall_colors"])
self.wall_colors = data["wall_colors"]
# print("read wall_mask: ",data["wall_mask"])
self.wall_mask = data["wall_mask"]
# print("read wall_id: ",data["wall_id"])
self.wall_id = data["wall_id"]
# print("read wall_distance: ",data["wall_distance"])
self.wall_distance = data["wall_distance"]
# print("read wall_grad_X: ",data["wall_grad_X"])
self.wall_grad_X = data["wall_grad_X"]
# print("read wall_grad_Y: ",data["wall_grad_Y"])
self.wall_grad_Y = data["wall_grad_Y"]
self.image = data["image"]
[docs]
def save(self, outfile):
"""
Save the domain state to a NumPy ``.npz`` archive.
Parameters
----------
outfile : str or path-like
Output filename. NumPy appends ``.npz`` when appropriate.
"""
np.savez(
outfile,
__shapes=self.__shapes,
__outline_color_shapes=self.__outline_color_shapes,
__fill_color_shapes=self.__fill_color_shapes,
__image_filename=self.__image_filename,
name=self.name,
__background=self.__background,
pixel_size=self.pixel_size,
xmin=self.xmin,
ymin=self.ymin,
xmax=self.xmax,
ymax=self.ymax,
width=self.width,
height=self.height,
X=self.X,
Y=self.Y,
wall_colors=self.wall_colors,
wall_mask=self.wall_mask,
wall_id=self.wall_id,
wall_distance=self.wall_distance,
wall_grad_X=self.wall_grad_X,
wall_grad_Y=self.wall_grad_Y,
destinations=self.destinations,
image=self.image)
[docs]
def add_shape(
self,
shape,
outline_color=[0, 0, 0],
fill_color=[255, 255, 255],
):
"""
Draw a Matplotlib shape onto the domain image.
Supported shapes are :class:`~matplotlib.lines.Line2D`,
:class:`~matplotlib.patches.Circle`, :class:`~matplotlib.patches.Ellipse`,
:class:`~matplotlib.patches.Rectangle`, and
:class:`~matplotlib.patches.Polygon`.
Parameters
----------
shape : matplotlib artist
Shape to draw.
outline_color : sequence of int, optional
RGB outline color.
fill_color : sequence of int, optional
RGB fill color. It is ignored for ``Line2D`` objects.
"""
self.__shapes.append(shape)
self.__outline_color_shapes.append(outline_color)
self.__fill_color_shapes.append(fill_color)
if (isinstance(shape, Circle) or isinstance(shape, Ellipse) or
isinstance(shape, Rectangle) or isinstance(shape, Polygon)):
xy = shape.get_verts()/self.pixel_size
xy[:, 1] = self.height - xy[:, 1]
self.draw.polygon(
np.around(xy.flatten()).tolist(),
outline="rgb("+str(outline_color[0])+"," +
str(outline_color[1])+"," +
str(outline_color[2])+")",
fill="rgb("+str(fill_color[0])+"," +
str(fill_color[1])+"," +
str(fill_color[2])+")")
linewidth = shape.get_linewidth()
self.draw.line(
np.around(xy.flatten()).tolist(),
width=int(linewidth),
fill="rgb("+str(outline_color[0])+"," +
str(outline_color[1])+"," +
str(outline_color[2])+")")
elif isinstance(shape, Line2D):
linewidth = shape.get_linewidth()
xy = shape.get_xydata()/self.pixel_size
xy[:, 1] = self.height - xy[:, 1]
self.draw.line(
np.around(xy.flatten()).tolist(),
width=int(linewidth),
fill="rgb("+str(outline_color[0])+"," +
str(outline_color[1])+"," +
str(outline_color[2])+")")
[docs]
def build_domain(self):
"""
Finalize the raster representation and compute wall geometry.
The current image is saved to ``"<domain name>_domain.png"``, converted to a
NumPy array with a lower-left origin, and split into RGB channels. Wall
pixels are then identified and the wall-distance field is computed.
"""
self.__image_filename = self.name+'_domain.png'
self.image.save(self.__image_filename)
# Easy way to convert a Pillow image to numpy arrays...
# The origin of img is at the top (left) and flipud allows to put it down...
self.image = np.flipud(np.array(self.image))
self.image_red = self.image[:, :, 0]
self.image_green = self.image[:, :, 1]
self.image_blue = self.image[:, :, 2]
self.wall_mask = _rgb_mask(
self.image_red,
self.image_green,
self.image_blue,
self.wall_colors,
)
self.wall_id = np.where(self.wall_mask > 0)
# Compute wall distances: wall = "wall_colors" pixels
if self.wall_id[0].size > 0:
self.compute_wall_distance()
else:
LOGGER.error(
"Failed to compute wall distance: no pixels match wall colors %s.",
self.wall_colors,
)
LOGGER.error("Check the domain image and configured wall colors.")
sys.exit()
[docs]
def compute_wall_distance(self):
"""
Compute the distance to walls and its normalized gradient.
The distance field is evaluated with a fast-marching method using the wall
pixels as the zero level set.
"""
phi = np.ones(self.image_red.shape)
if len(self.wall_id[0]) > 0:
phi[self.wall_id] = 0
self.wall_distance = skfmm.distance(phi, dx=self.pixel_size)
grad = np.gradient(self.wall_distance, edge_order=2)
grad_X = grad[1]/self.pixel_size
grad_Y = grad[0]/self.pixel_size
norm = np.sqrt(grad_X**2+grad_Y**2)
norm = (norm > 0) * norm + (norm == 0) * 0.001
self.wall_grad_X = grad_X/norm
self.wall_grad_Y = grad_Y/norm
else:
self.wall_distance = 1.0e99*np.ones(self.image_red.shape)
[docs]
def add_destination(self, dest):
"""
Compute and register the fields associated with a destination.
A fast-marching travel-time field is computed outside the destination's
excluded colors. The negative normalized gradient defines the desired
velocity direction. Explicit color-based velocity overrides are applied
before normalization.
Parameters
----------
dest : Destination
Destination to add to the domain.
"""
# Compute mask for dest.excluded_colors (which can be for example the
# wall colors...)
excluded_color_mask = _rgb_mask(
self.image_red,
self.image_green,
self.image_blue,
dest.excluded_colors,
)
excluded_color_id = np.where(excluded_color_mask > 0)
dest.excluded_id = np.zeros_like(self.image_red)
dest.excluded_id[ excluded_color_id ] = 1
# Compute mask for dest.colors (which correspond which correspond to
# the destination of the people)
dest_mask = _rgb_mask(
self.image_red,
self.image_green,
self.image_blue,
dest.colors,
)
mask_id = np.where((dest_mask >= 1))
# Define rhs of the Eikonal equation (i.e. the speed)
if dest.fmm_speed is not None:
if dest.fmm_speed.shape != self.image_red.shape:
LOGGER.error(
"Cannot compute destination %r: fmm_speed has shape %s, expected %s.",
dest.name,
dest.fmm_speed.shape,
self.image_red.shape,
)
sys.exit()
else:
dest.fmm_speed = np.ones_like(self.image_red)
if mask_id[0].size > 0:
dest_mask[mask_id] = True
phi = np.ones(self.image_red.shape)
phi[mask_id] = 0
phi = np.ma.MaskedArray(phi, mask=excluded_color_mask)
dest.distance = skfmm.travel_time(phi, dest.fmm_speed, dx=self.pixel_size)
# dest.distance = skfmm.distance(phi, dx=self.pixel_size)
if excluded_color_id[0].size > 0:
tmp_dist = dest.distance.filled(9999)
else:
tmp_dist = dest.distance
grad = np.gradient(tmp_dist, edge_order=2)
else:
dest.distance = -np.ones_like(self.image_red)
grad = np.gradient(np.zeros_like(self.image_red), edge_order=2)
for rgbgrad in dest.desired_velocity_from_color:
test = (self.image_red == int(rgbgrad[0])) \
* (self.image_green == int(rgbgrad[1])) \
* (self.image_blue == int(rgbgrad[2]))
indices = np.where(test)
grad[1][indices] = -rgbgrad[3]
grad[0][indices] = -rgbgrad[4]
grad_X = -grad[1]/self.pixel_size
grad_Y = -grad[0]/self.pixel_size
norm = np.sqrt(grad_X**2+grad_Y**2)
norm = (norm > 0)*norm + (norm == 0)*0.001
dest.desired_velocity_X = grad_X/norm
dest.desired_velocity_Y = grad_Y/norm
if self.destinations is None:
self.destinations = {}
self.destinations[dest.name] = dest
[docs]
def people_desired_velocity(self, xyr, people_dest, II=None, JJ=None):
"""
Evaluate desired velocities for a collection of pedestrians.
Parameters
----------
xyr : numpy.ndarray
Array with at least four columns ``[x, y, radius, speed]``.
people_dest : array-like of str
Destination name associated with each pedestrian.
II, JJ : numpy.ndarray or None, optional
Precomputed row and column indices in the domain grid.
Returns
-------
II, JJ : numpy.ndarray
Row and column indices used for interpolation by pixel lookup.
Vd : numpy.ndarray
Desired velocities, with shape ``(n_people, 2)``.
"""
if II is None or JJ is None:
II, JJ = _grid_indices(
xyr, self.xmin, self.ymin, self.pixel_size
)
Vd = np.zeros((xyr.shape[0], 2))
for dest_name in np.unique(people_dest):
ind = np.where(np.array(people_dest) == dest_name)[0]
scale = self.destinations[dest_name].velocity_scale
Vd[ind, 0] = xyr[ind, 3]*scale*self.destinations[dest_name].desired_velocity_X[II[ind], JJ[ind]]
Vd[ind, 1] = xyr[ind, 3]*scale*self.destinations[dest_name].desired_velocity_Y[II[ind], JJ[ind]]
return II, JJ, Vd
[docs]
def people_target_distance(self, xyr, people_dest, II=None, JJ=None):
"""
Evaluate the distance to each pedestrian's current destination.
Parameters
----------
xyr : numpy.ndarray
Array containing at least ``[x, y, radius]`` for each pedestrian.
people_dest : array-like of str
Destination name associated with each pedestrian.
II, JJ : numpy.ndarray or None, optional
Precomputed row and column indices in the domain grid.
Returns
-------
II, JJ : numpy.ndarray
Row and column grid indices.
D : numpy.ndarray
Destination distance minus pedestrian radius.
"""
if II is None or JJ is None:
II, JJ = _grid_indices(
xyr, self.xmin, self.ymin, self.pixel_size
)
D = np.zeros(xyr.shape[0])
for dest_name in np.unique(people_dest):
ind = np.where(np.array(people_dest) == dest_name)[0]
D[ind] = self.destinations[dest_name].distance[II[ind], JJ[ind]]-xyr[ind, 2]
return II, JJ, D
[docs]
def people_wall_distance(self, xyr, II=None, JJ=None):
"""
Evaluate the distance from each pedestrian boundary to the nearest wall.
Parameters
----------
xyr : numpy.ndarray
Array containing at least ``[x, y, radius]`` for each pedestrian.
II, JJ : numpy.ndarray or None, optional
Precomputed row and column indices in the domain grid.
Returns
-------
II, JJ : numpy.ndarray
Row and column grid indices.
D : numpy.ndarray
Wall distance minus pedestrian radius.
"""
if II is None or JJ is None:
II, JJ = _grid_indices(
xyr, self.xmin, self.ymin, self.pixel_size
)
D = self.wall_distance[II, JJ]-xyr[:, 2]
return II, JJ, D
[docs]
def plot(self, id=1, title="", savefig=False, filename='fig.png', dpi=150):
"""
Plot the computational domain.
Parameters
----------
id : int, optional
Matplotlib figure number.
title : str, optional
Figure title.
savefig : bool, optional
Save the figure when ``True``.
filename : str, optional
Output image filename.
dpi : int, optional
Resolution of the saved figure.
"""
fig = plt.figure(id)
ax1 = fig.add_subplot(111)
ax1.imshow(self.image, interpolation='nearest', extent=[self.xmin, self.xmax,
self.ymin, self.ymax], origin='lower')
# plt.savefig('.png',dpi=dpi)
# ax1.axes.get_xaxis().set_visible(False)
# ax1.axes.get_xaxis().set_visible(False)
ax1.set_axis_off()
ax1.set_title(title)
# fig.add_axes(ax1)
plt.draw()
if savefig:
fig.savefig(filename, dpi=dpi, bbox_inches='tight', pad_inches=0)
[docs]
def plot_wall_dist(self,
step=10, scale=10, scale_units='inches', id=1, title="",
savefig=False, filename='fig.png', dpi=150):
"""
Plot the wall-distance field and its gradient.
Parameters
----------
step : int, optional
Plot one gradient arrow every ``step`` pixels.
scale : float, optional
Matplotlib quiver scale.
scale_units : str, optional
Matplotlib quiver scale units.
id : int, optional
Matplotlib figure number.
title : str, optional
Figure title.
savefig : bool, optional
Save the figure when ``True``.
filename : str, optional
Output image filename.
dpi : int, optional
Resolution of the saved figure.
"""
fig = plt.figure(id)
ax1 = fig.add_subplot(111)
ax1.imshow(self.image, interpolation='nearest',
extent=[self.xmin, self.xmax, self.ymin, self.ymax], origin='lower')
ax1.imshow(self.wall_distance, interpolation='nearest',
extent=[self.xmin, self.xmax, self.ymin, self.ymax], alpha=0.7,
origin='lower')
ax1.quiver(self.X[::step, ::step], self.Y[::step, ::step],
self.wall_grad_X[::step, ::step],
self.wall_grad_Y[::step, ::step],
scale=scale, scale_units=scale_units)
ax1.set_axis_off()
ax1.set_title(title)
# plt.savefig('.png',dpi=dpi)
plt.draw()
if savefig:
fig.savefig(filename, dpi=dpi, bbox_inches='tight', pad_inches=0)
[docs]
def plot_desired_velocity(self,
destination_name, step=10, scale=10, scale_units='inches', id=1,
title="", savefig=False, filename='fig.png', dpi=150):
"""
Plot a destination travel-time field and its desired-velocity directions.
Parameters
----------
destination_name : str
Name of the destination to display.
step : int, optional
Plot one velocity arrow every ``step`` pixels.
scale : float, optional
Matplotlib quiver scale.
scale_units : str, optional
Matplotlib quiver scale units.
id : int, optional
Matplotlib figure number.
title : str, optional
Figure title.
savefig : bool, optional
Save the figure when ``True``.
filename : str, optional
Output image filename.
dpi : int, optional
Resolution of the saved figure.
"""
fig = plt.figure(id)
ax1 = fig.add_subplot(111)
ax1.imshow(self.image, interpolation='nearest',
extent=[self.xmin, self.xmax, self.ymin, self.ymax], origin='lower')
ax1.imshow(self.destinations[destination_name].distance, interpolation='nearest',
extent=[self.xmin, self.xmax, self.ymin, self.ymax], alpha=0.7,
origin='lower')
ax1.quiver(self.X[::step, ::step], self.Y[::step, ::step],
self.destinations[destination_name].desired_velocity_X[::step, ::step],
self.destinations[destination_name].desired_velocity_Y[::step, ::step],
scale=scale, scale_units=scale_units)
ax1.set_title(title)
# plt.savefig('.png',dpi=dpi)
ax1.set_axis_off()
plt.draw()
if savefig:
fig.savefig(filename, dpi=dpi, bbox_inches='tight', pad_inches=0)
def __str__(self):
"""Return a human-readable summary of the domain."""
return "--> "+self.name \
+ ":\n dimensions: ["+str(self.xmin)+","+str(self.xmax)+"]x[" \
+ str(self.ymin)+","+str(self.ymax)+"]" \
+ "\n width: "+str(self.width)+" height: "+str(self.height) \
+ "\n background image: "+str(self.__background) \
+ "\n image of the domain: "+str(self.__image_filename) \
+ "\n wall_colors: "+str(self.wall_colors) \
+ "\n destinations: "+str(self.destinations)