#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# pept is a Python library that unifies Positron Emission Particle
# Tracking (PEPT) research, including tracking, simulation, data analysis
# and visualisation tools.
#
# If you used this codebase or any software making use of it in a scientific
# publication, you must cite the following paper:
# Nicuşan AL, Windows-Yule CR. Positron emission particle tracking
# using machine learning. Review of Scientific Instruments.
# 2020 Jan 1;91(1):013329.
# https://doi.org/10.1063/1.5129251
#
# Copyright (C) 2019-2021 the pept developers
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# File : pixel_data.py
# License: GNU v3.0
# Author : Andrei Leonard Nicusan <a.l.nicusan@bham.ac.uk>
# Date : 07.01.2020
import pickle
import time
import textwrap
import numpy as np
import plotly.graph_objects as go
import matplotlib.pyplot as plt
from pept.utilities.traverse import traverse2d
from .iterable_samples import PEPTObject
[docs]class Pixels(np.ndarray, PEPTObject):
'''A class that manages a 2D pixel space, including tools for pixel
traversal of lines, manipulation and visualisation.
This class can be instantiated in a couple of ways:
1. The constructor receives a pre-defined pixel space (i.e. a 2D numpy
array), along with the space boundaries `xlim` and `ylim`.
2. The `from_lines` method receives a sample of 2D lines (i.e. a 2D numpy
array), each defined by two points, creating a pixel space and
traversing / pixellising the lines.
3. The `empty` method creates a pixel space filled with zeros.
This subclasses the `numpy.ndarray` class, so any `Pixels` object acts
exactly like a 2D numpy array. All numpy methods and operations are valid
on `Pixels` (e.g. add 1 to all pixels with `pixels += 1`).
It is possible to add multiple samples of lines to the same pixel space
using the `add_lines` method.
Attributes
----------
pixels: (M, N) numpy.ndarray
The 2D numpy array containing the number of lines that pass through
each pixel. They are stored as `float`s. This class assumes a uniform
grid of pixels - that is, the pixel size in each dimension is constant,
but can vary from one dimension to another. The number of pixels in
each dimension is defined by `number_of_pixels`.
number_of_pixels: 2-tuple
A 2-tuple corresponding to the shape of `pixels`.
pixel_size: (2,) numpy.ndarray
The lengths of a pixel in the x- and y-dimensions, respectively.
xlim: (2,) numpy.ndarray
The lower and upper boundaries of the pixellised volume in the
x-dimension, formatted as [x_min, x_max].
ylim: (2,) numpy.ndarray
The lower and upper boundaries of the pixellised volume in the
y-dimension, formatted as [y_min, y_max].
pixel_grids: list[numpy.ndarray]
A list containing the pixel gridlines in the x- and y-dimensions.
Each dimension's gridlines are stored as a numpy of the pixel
delimitations, such that it has length (M + 1), where M is the number
of pixels in a given dimension.
Methods
-------
save(filepath)
Save a `Pixels` instance as a binary `pickle` object.
load(filepath)
Load a saved / pickled `Pixels` object from `filepath`.
from_lines(lines, number_of_pixels, xlim = None, ylim = None, \
verbose = True)
Create a pixel space and traverse / pixellise a given sample of
`lines`.
empty(number_of_pixels, xlim, ylim, verbose = False)
Create an empty pixel space for the 2D rectangle bounded by `xlim` and
`ylim`.
get_cutoff(p1, p2)
Return a numpy array containing the minimum and maximum value found
across the two input arrays.
add_lines(lines, verbose = False)
Pixellise a sample of lines, adding 1 to each pixel traversed, for
each line in the sample.
cube_trace(index, color = None, opacity = 0.4, colorbar = True,\
colorscale = "magma")
Get the Plotly `Mesh3d` trace for a single pixel at `index`.
cubes_traces(condition = lambda pixels: pixels > 0, color = None,\
opacity = 0.4, colorbar = True, colorscale = "magma")
Get a list of Plotly `Mesh3d` traces for all pixel selected by the
`condition` filtering function.
pixels_trace(condition = lambda pixels: pixels > 0, size = 4,\
color = None, opacity = 0.4, colorbar = True,\
colorscale = "Magma", colorbar_title = None)
Create and return a trace for all the pixels in this class, with
possible filtering.
heatmap_trace(ix = None, iy = None, iz = None, width = 0,\
colorscale = "Magma", transpose = True)
Create and return a Plotly `Heatmap` trace of a 2D slice through the
voxels.
Notes
-----
The traversed lines do not need to be fully bounded by the pixel space.
Their intersection is automatically computed.
The class saves `pixels` as a **contiguous** numpy array for efficient
access in C / Cython functions. The inner data can be mutated, but do not
change the shape of the array after instantiating the class.
Examples
--------
This class is most often instantiated from a sample of lines to pixellise:
>>> import pept
>>> import numpy as np
>>> lines = np.arange(70).reshape(10, 7)
>>> number_of_pixels = [3, 4]
>>> pixels = pept.Pixels.from_lines(lines, number_of_pixels)
>>> Initialised Pixels class in 0.0006861686706542969 s.
>>> print(pixels)
>>> pixels:
>>> [[[2. 1. 0. 0. 0.]
>>> [0. 2. 0. 0. 0.]
>>> [0. 0. 0. 0. 0.]
>>> [0. 0. 0. 0. 0.]]
>>> [[0. 0. 0. 0. 0.]
>>> [0. 1. 1. 0. 0.]
>>> [0. 0. 1. 1. 0.]
>>> [0. 0. 0. 0. 0.]]
>>> [[0. 0. 0. 0. 0.]
>>> [0. 0. 0. 0. 0.]
>>> [0. 0. 0. 2. 0.]
>>> [0. 0. 0. 1. 2.]]]
>>> number_of_pixels = (3, 4, 5)
>>> pixel_size = [22. 16.5 13.2]
>>> xlim = [ 1. 67.]
>>> ylim = [ 2. 68.]
>>> zlim = [ 3. 69.]
>>> pixel_grids:
>>> [array([ 1., 23., 45., 67.]),
>>> array([ 2. , 18.5, 35. , 51.5, 68. ]),
>>> array([ 3. , 16.2, 29.4, 42.6, 55.8, 69. ])]
Note that it is important to define the `number_of_pixels`.
See Also
--------
pept.LineData : Encapsulate lines for ease of iteration and plotting.
pept.PointData : Encapsulate points for ease of iteration and plotting.
pept.utilities.read_csv : Fast CSV file reading into numpy arrays.
PlotlyGrapher : Easy, publication-ready plotting of PEPT-oriented data.
'''
def __new__(
cls,
pixels_array,
xlim,
ylim,
):
'''`Pixels` class constructor.
Parameters
----------
pixels_array: 3D numpy.ndarray
A 3D numpy array, corresponding to a pre-defined pixel space.
xlim: (2,) numpy.ndarray
The lower and upper boundaries of the pixellised volume in the
x-dimension, formatted as [x_min, x_max].
ylim: (2,) numpy.ndarray
The lower and upper boundaries of the pixellised volume in the
y-dimension, formatted as [y_min, y_max].
Raises
------
ValueError
If `pixels_array` does not have exactly 3 dimensions or if
`xlim` or `ylim` do not have exactly 2 values each.
'''
# Type-checking inputs
pixels_array = np.asarray(
pixels_array,
order = "C",
dtype = float
)
if pixels_array.ndim != 2:
raise ValueError(textwrap.fill((
"The input `pixels_array` must contain an array-like with "
"exactly three dimensions (i.e. pre-made pixels array). "
f"Received an array with {pixels_array.ndim} dimensions. "
"Note: if you would like to create pixels from a sample of"
"lines, use the `Pixels.from_lines` method. "
)))
xlim = np.asarray(xlim, dtype = float)
if xlim.ndim != 1 or len(xlim) != 2:
raise ValueError(textwrap.fill((
"The input `xlim` parameter must be a list with exactly "
"two values, corresponding to the minimum and maximum "
"coordinates of the pixel space in the x-dimension. "
f"Received parameter with shape {xlim.shape}."
)))
ylim = np.asarray(ylim, dtype = float)
if ylim.ndim != 1 or len(ylim) != 2:
raise ValueError(textwrap.fill((
"The input `ylim` parameter must be a list with exactly "
"two values, corresponding to the minimum and maximum "
"coordinates of the pixel space in the y-dimension. "
f"Received parameter with shape {ylim.shape}."
)))
# Setting class attributes
pixels = pixels_array.view(cls)
pixels._number_of_pixels = pixels.shape
pixels._xlim = xlim
pixels._ylim = ylim
pixels._pixel_size = np.array([
(pixels._xlim[1] - pixels._xlim[0]) / pixels._number_of_pixels[0],
(pixels._ylim[1] - pixels._ylim[0]) / pixels._number_of_pixels[1],
])
pixels._pixel_grids = tuple([
np.linspace(lim[0], lim[1], pixels._number_of_pixels[i] + 1)
for i, lim in enumerate((pixels._xlim, pixels._ylim))
])
return pixels
def __array_finalize__(self, pixels):
# Required method for numpy subclassing
if pixels is None:
return
self._number_of_pixels = getattr(pixels, "_number_of_pixels", None)
self._pixel_size = getattr(pixels, "_pixel_size", None)
self._xlim = getattr(pixels, "_xlim", None)
self._ylim = getattr(pixels, "_ylim", None)
self._zlim = getattr(pixels, "_zlim", None)
self._pixel_grids = getattr(pixels, "_pixel_grids", None)
def __reduce__(self):
# __reduce__ and __setstate__ ensure correct pickling behaviour. See
# https://stackoverflow.com/questions/26598109/preserve-custom-
# attributes-when-pickling-subclass-of-numpy-array
# Get the parent's __reduce__ tuple
pickled_state = super(Pixels, self).__reduce__()
# Create our own tuple to pass to __setstate__
new_state = pickled_state[2] + (
self._number_of_pixels,
self._xlim,
self._ylim,
self._pixel_size,
self._pixel_grids,
)
# Return a tuple that replaces the parent's __setstate__ tuple with
# our own
return (pickled_state[0], pickled_state[1], new_state)
def __setstate__(self, state):
# __reduce__ and __setstate__ ensure correct pickling behaviour
# https://stackoverflow.com/questions/26598109/preserve-custom-
# attributes-when-pickling-subclass-of-numpy-array
# Set the class attributes
self._pixel_grids = state[-1]
self._pixel_size = state[-2]
self._ylim = state[-3]
self._xlim = state[-4]
self._number_of_pixels = state[-5]
# Call the parent's __setstate__ with the other tuple elements.
super(Pixels, self).__setstate__(state[0:-5])
@property
def pixels(self):
return self.__array__()
@property
def number_of_pixels(self):
return self._number_of_pixels
@property
def xlim(self):
return self._xlim
@property
def ylim(self):
return self._ylim
@property
def pixel_size(self):
return self._pixel_size
@property
def pixel_grids(self):
return self._pixel_grids
[docs] @staticmethod
def from_lines(
lines,
number_of_pixels,
xlim = None,
ylim = None,
verbose = True,
):
'''Create a pixel space and traverse / pixellise a given sample of
`lines`.
The `number_of_pixels` in each dimension must be defined. If the
pixel space boundaries `xlim` or `ylim` are not defined, they
are inferred as the boundaries of the `lines`.
Parameters
----------
lines : (M, N>=5) numpy.ndarray
The lines that will be pixellised, each defined by a timestamp and
two 2D points, so that the data columns are [time, x1, y1, x2, y2].
Note that extra columns are ignored.
number_of_pixels : (2,) list[int]
The number of pixels in the x- and y-dimensions, respectively.
xlim : (2,) list[float], optional
The lower and upper boundaries of the pixellised volume in the
x-dimension, formatted as [x_min, x_max]. If undefined, it is
inferred from the boundaries of `lines`.
ylim : (2,) list[float], optional
The lower and upper boundaries of the pixellised volume in the
y-dimension, formatted as [y_min, y_max]. If undefined, it is
inferred from the boundaries of `lines`.
Returns
-------
pept.Pixels
A new `Pixels` object with the pixels through which the lines were
traversed.
Raises
------
ValueError
If the input `lines` does not have the shape (M, N>=5). If the
`number_of_pixels` is not a 1D list with exactly 2 elements, or
any dimension has fewer than 2 pixels.
'''
if verbose:
start = time.time()
# Type-checking inputs
lines = np.asarray(lines, order = "C", dtype = float)
if lines.ndim != 2 or lines.shape[1] < 5:
raise ValueError(textwrap.fill((
"The input `lines` must be a 2D numpy array containing lines "
"defined by a timestamp and two 2D points, with every row "
"formatted as [t, x1, y1, x2, y2]. The `lines` must then have "
f"shape (M, 5). Received array with shape {lines.shape}."
)))
number_of_pixels = np.asarray(
number_of_pixels,
order = "C",
dtype = int
)
if number_of_pixels.ndim != 1 or len(number_of_pixels) != 2:
raise ValueError(textwrap.fill((
"The input `number_of_pixels` must be a list-like "
"with exactly two values, corresponding to the "
"number of pixels in the x- and y-dimension. "
f"Received parameter with shape {number_of_pixels.shape}."
)))
if (number_of_pixels < 2).any():
raise ValueError(textwrap.fill((
"The input `number_of_pixels` must set at least two "
"pixels in each dimension (i.e. all elements in "
"`number_of_elements` must be larger or equal to two). "
f"Received `{number_of_pixels}`."
)))
if xlim is None:
xlim = Pixels.get_cutoff(lines[:, 1], lines[:, 3])
else:
xlim = np.asarray(xlim, dtype = float)
if xlim.ndim != 1 or len(xlim) != 2:
raise ValueError(textwrap.fill((
"The input `xlim` parameter must be a list with exactly "
"two values, corresponding to the minimum and maximum "
"coordinates of the pixel space in the x-dimension. "
f"Received parameter with shape {xlim.shape}."
)))
if ylim is None:
ylim = Pixels.get_cutoff(lines[:, 1], lines[:, 3])
else:
ylim = np.asarray(ylim, dtype = float)
if ylim.ndim != 1 or len(ylim) != 2:
raise ValueError(textwrap.fill((
"The input `ylim` parameter must be a list with exactly "
"two values, corresponding to the minimum and maximum "
"coordinates of the pixel space in the y-dimension. "
f"Received parameter with shape {ylim.shape}."
)))
pixels_array = np.zeros(tuple(number_of_pixels))
pixels = Pixels(
pixels_array,
xlim = xlim,
ylim = ylim,
)
pixels.add_lines(lines, verbose = False)
if verbose:
end = time.time()
print((
f"Initialised Pixels class in {end - start} s."
))
return pixels
[docs] @staticmethod
def empty(number_of_pixels, xlim, ylim):
'''Create an empty pixel space for the 3D cube bounded by `xlim` and
`ylim`.
Parameters
----------
number_of_pixels: (2,) numpy.ndarray
A list-like containing the number of pixels to be created in the
x- and y-dimension, respectively.
xlim: (2,) numpy.ndarray
The lower and upper boundaries of the pixellised volume in the
x-dimension, formatted as [x_min, x_max].
ylim: (2,) numpy.ndarray
The lower and upper boundaries of the pixellised volume in the
y-dimension, formatted as [y_min, y_max].
Time the pixellisation step and print it to the terminal.
Raises
------
ValueError
If `number_of_pixels` does not have exactly 2 values, or it has
values smaller than 2. If `xlim` or `ylim` do not have exactly 2
values each.
'''
number_of_pixels = np.asarray(
number_of_pixels,
order = "C",
dtype = int
)
if number_of_pixels.ndim != 1 or len(number_of_pixels) != 2:
raise ValueError(textwrap.fill((
"The input `number_of_pixels` must be a list-like "
"with exactly three values, corresponding to the "
"number of pixels in the x- and y-dimension. "
f"Received parameter with shape {number_of_pixels.shape}."
)))
if (number_of_pixels < 2).any():
raise ValueError(textwrap.fill((
"The input `number_of_pixels` must set at least two "
"pixels in each dimension (i.e. all elements in "
"`number_of_elements` must be larger or equal to two). "
f"Received `{number_of_pixels}`."
)))
number_of_pixels = tuple(number_of_pixels)
empty_pixels = np.zeros(number_of_pixels)
return Pixels(
empty_pixels,
xlim = xlim,
ylim = ylim,
)
[docs] @staticmethod
def get_cutoff(p1, p2):
'''Return a numpy array containing the minimum and maximum value found
across the two input arrays.
Parameters
----------
p1 : (N,) numpy.ndarray
The first 1D numpy array.
p2 : (N,) numpy.ndarray
The second 1D numpy array.
Returns
-------
(2,) numpy.ndarray
The minimum and maximum value found across `p1` and `p2`.
Notes
-----
The input parameters *must* be numpy arrays, otherwise an error will
be raised.
'''
return np.array([
min(p1.min(), p2.min()),
max(p1.max(), p2.max()),
])
[docs] def save(self, filepath):
'''Save a `Pixels` instance as a binary `pickle` object.
Saves the full object state, including the inner `.pixels` NumPy array,
`xlim`, etc. in a fast, portable binary format. Load back the object
using the `load` method.
Parameters
----------
filepath : filename or file handle
If filepath is a path (rather than file handle), it is relative
to where python is called.
Examples
--------
Save a `Pixels` instance, then load it back:
>>> pixels = pept.Pixels.empty((640, 480), [0, 20], [0, 10])
>>> pixels.save("pixels.pickle")
>>> pixels_reloaded = pept.Pixels.load("pixels.pickle")
'''
with open(filepath, "wb") as f:
pickle.dump(self, f)
[docs] @staticmethod
def load(filepath):
'''Load a saved / pickled `Pixels` object from `filepath`.
Most often the full object state was saved using the `.save` method.
Parameters
----------
filepath : filename or file handle
If filepath is a path (rather than file handle), it is relative
to where python is called.
Returns
-------
pept.Pixels
The loaded `pept.Pixels` instance.
Examples
--------
Save a `Pixels` instance, then load it back:
>>> pixels = pept.Pixels.empty((640, 480), [0, 20], [0, 10])
>>> pixels.save("pixels.pickle")
>>> pixels_reloaded = pept.Pixels.load("pixels.pickle")
'''
with open(filepath, "rb") as f:
obj = pickle.load(f)
return obj
[docs] def add_lines(self, lines, verbose = False):
'''Pixellise a sample of lines, adding 1 to each pixel traversed, for
each line in the sample.
Parameters
----------
lines : (M, N >= 5) numpy.ndarray
The sample of 2D lines to pixellise. Each line is defined as a
timestamp followed by two 2D points, such that the data columns are
`[time, x1, y1, x2, y2, ...]`. Note that there can be extra data
columns which will be ignored.
verbose : bool, default False
Time the pixel traversal and print it to the terminal.
Raises
------
ValueError
If `lines` has fewer than 5 columns.
'''
lines = np.asarray(lines, order = "C", dtype = float)
if lines.ndim != 2 or lines.shape[1] < 5:
raise ValueError(textwrap.fill((
"The input `lines` must be a 2D array of lines, where each "
"line (i.e. row) is defined by a timestamp and two 2D points, "
"so the data columns are [time, x1, y1, x2, y2]. "
f"Received array of shape {lines.shape}."
)))
if verbose:
start = time.time()
traverse2d(
self.pixels,
lines,
self._pixel_grids[0],
self._pixel_grids[1],
)
if verbose:
end = time.time()
print(f"Traversing {len(lines)} lines took {end - start} s.")
[docs] def pixels_trace(
self,
condition = lambda pixels: pixels > 0,
opacity = 0.9,
colorscale = "Magma",
):
'''Create and return a trace with all the pixels in this class, with
possible filtering.
Creates a `plotly.graph_objects.Surface` object for the centres of
all pixels encapsulated in a `pept.Pixels` instance, colour-coding the
pixel value.
The `condition` parameter is a filtering function that should return
a boolean mask (i.e. it is the result of a condition evaluation). For
example `lambda x: x > 0` selects all pixels that have a value larger
than 0.
Parameters
----------
condition : function, default `lambda pixels: pixels > 0`
The filtering function applied to the pixel data before plotting
it. It should return a boolean mask (a numpy array of the same
shape, filled with True and False), selecting all pixels that
should be plotted. The default, `lambda x: x > 0` selects all
pixels which have a value larger than 0.
opacity : float, default 0.4
The opacity of the surface, where 0 is transparent and 1 is fully
opaque.
colorscale : str, default "Magma"
The Plotly scheme for color-coding the voxel values in the input
data. Typical ones include "Cividis", "Viridis" and "Magma".
A full list is given at `plotly.com/python/builtin-colorscales/`.
Only has an effect if `colorbar = True` and `color` is not set.
Examples
--------
Pixellise an array of lines and add them to a `PlotlyGrapher` instance:
>>> grapher = PlotlyGrapher()
>>> lines = np.array(...) # shape (N, M >= 7)
>>> lines2d = lines[:, [0, 1, 2, 4, 5]] # select x, y of lines
>>> number_of_pixels = [10, 10]
>>> pixels = pept.Pixels.from_lines(lines2d, number_of_pixels)
>>> grapher.add_lines(lines)
>>> grapher.add_trace(pixels.pixels_trace())
>>> grapher.show()
'''
filtered = self.copy()
filtered[~condition(self)] = 0.
# Compute the pixel centres
x = self.pixel_grids[0]
x = (x[1:] + x[:-1]) / 2
y = self.pixel_grids[1]
y = (y[1:] + y[:-1]) / 2
trace = go.Surface(
x = x,
y = y,
z = filtered,
opacity = opacity,
colorscale = colorscale,
)
return trace
[docs] def heatmap_trace(
self,
colorscale = "Magma",
transpose = True,
xgap = 0.,
ygap = 0.,
):
'''Create and return a Plotly `Heatmap` trace of the pixels.
Parameters
----------
colorscale : str, default "Magma"
The Plotly scheme for color-coding the pixel values in the input
data. Typical ones include "Cividis", "Viridis" and "Magma".
A full list is given at `plotly.com/python/builtin-colorscales/`.
Only has an effect if `colorbar = True` and `color` is not set.
transpose : bool, default True
Transpose the heatmap (i.e. flip it across its diagonal).
Examples
--------
Pixellise an array of lines and add them to a `PlotlyGrapher2D`
instance:
>>> lines = np.array(...) # shape (N, M >= 7)
>>> lines2d = lines[:, [0, 1, 2, 4, 5]] # select x, y of lines
>>> number_of_pixels = [10, 10]
>>> pixels = pept.Pixels.from_lines(lines2d, number_of_pixels)
>>> grapher = pept.visualisation.PlotlyGrapher2D()
>>> grapher.add_pixels(pixels)
>>> grapher.show()
Or add them directly to a raw `plotly.graph_objs` figure:
>>> import plotly.graph_objs as go
>>> fig = go.Figure()
>>> fig.add_trace(pixels.heatmap_trace())
>>> fig.show()
'''
# Compute the pixel centres
x = self.pixel_grids[0]
x = (x[1:] + x[:-1]) / 2
y = self.pixel_grids[1]
y = (y[1:] + y[:-1]) / 2
heatmap = dict(
x = x,
y = y,
z = self,
colorscale = colorscale,
transpose = transpose,
xgap = xgap,
ygap = ygap,
)
return go.Heatmap(heatmap)
[docs] def plot(self, ax = None):
'''Plot pixels as a heatmap using Matplotlib.
Returns matplotlib figure and axes objects containing the pixel values
colour-coded in a Matplotlib image (i.e. heatmap).
Parameters
----------
ax : mpl_toolkits.mplot3D.Axes3D object, optional
The 3D matplotlib-based axis for plotting. If undefined, new
Matplotlib figure and axis objects are created.
Returns
-------
fig, ax : matplotlib figure and axes objects
Examples
--------
Pixellise an array of lines and plot them with Matplotlib:
>>> lines = np.array(...) # shape (N, M >= 7)
>>> lines2d = lines[:, [0, 1, 2, 4, 5]] # select x, y of lines
>>> number_of_pixels = [10, 10]
>>> pixels = pept.Pixels.from_lines(lines2d, number_of_pixels)
>>> fig, ax = pixels.plot()
>>> fig.show()
'''
if ax is None:
fig = plt.figure()
ax = fig.add_subplot(111)
else:
fig = plt.gcf()
# Plot the values in pixels (this class is a numpy array subclass)
ax.imshow(self)
# Compute the pixel centres and set them in the Matplotlib image
x = self.pixel_grids[0]
x = (x[1:] + x[:-1]) / 2
y = self.pixel_grids[1]
y = (y[1:] + y[:-1]) / 2
# Matplotlib shows numbers in a long format ("102.000032411"), so round
# them to two decimals before plotting
ax.set_xticklabels(np.round(x, 2))
ax.set_yticklabels(np.round(y, 2))
ax.set_xlabel("x (mm)")
ax.set_ylabel("y (mm)")
return fig, ax
def __str__(self):
# Shown when calling print(class)
docstr = (
f"{self.__array__()}\n\n"
f"number_of_pixels = {self._number_of_pixels}\n"
f"pixel_size = {self._pixel_size}\n\n"
f"xlim = {self._xlim}\n"
f"ylim = {self._ylim}\n"
f"pixel_grids:\n"
f"([{self._pixel_grids[0][0]} ... {self._pixel_grids[0][-1]}],\n"
f" [{self._pixel_grids[1][0]} ... {self._pixel_grids[1][-1]}])"
)
return docstr
def __repr__(self):
# Shown when writing the class on a REPR
docstr = (
"Class instance that inherits from `pept.Pixels`.\n"
f"Type:\n{type(self)}\n\n"
"Attributes\n----------\n"
f"{self.__str__()}"
)
return docstr