|
| 1 | +# The package atldld is a tool to download atlas data. |
| 2 | +# |
| 3 | +# Copyright (C) 2021 EPFL/Blue Brain Project |
| 4 | +# |
| 5 | +# This program is free software: you can redistribute it and/or modify |
| 6 | +# it under the terms of the GNU Lesser General Public License as published by |
| 7 | +# the Free Software Foundation, either version 3 of the License, or |
| 8 | +# (at your option) any later version. |
| 9 | +# |
| 10 | +# This program is distributed in the hope that it will be useful, |
| 11 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 | +# GNU Lesser General Public License for more details. |
| 14 | +# |
| 15 | +# You should have received a copy of the GNU Lesser General Public License |
| 16 | +# along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 17 | +"""Different plotting routines.""" |
| 18 | +from typing import Iterable |
| 19 | + |
| 20 | +import numpy as np |
| 21 | +from matplotlib.figure import Figure |
| 22 | +from matplotlib.lines import Line2D |
| 23 | + |
| 24 | +from atldld.constants import REF_DIM_1UM |
| 25 | +from atldld.dataset import PlaneOfSection |
| 26 | + |
| 27 | + |
| 28 | +def dataset_preview( |
| 29 | + all_corners: Iterable[np.ndarray], |
| 30 | + plane_of_section: PlaneOfSection, |
| 31 | +) -> Figure: |
| 32 | + """Plot a preview of how section images fit into the reference space. |
| 33 | +
|
| 34 | + Parameters |
| 35 | + ---------- |
| 36 | + all_corners |
| 37 | + The corners of all section images. Each element in this iterable should |
| 38 | + be a NumPy array of shape (4, 3). The format of this array corresponds |
| 39 | + to that returned by the `atldld.utils.get_corners_in_ref_space` |
| 40 | + function. |
| 41 | +
|
| 42 | + The first axis refers to the four corners of a section image in the |
| 43 | + following order: |
| 44 | +
|
| 45 | + 1. Lower left (0, 0) |
| 46 | + 2. Lower right (0, 1) |
| 47 | + 3. Upper right (1, 1) |
| 48 | + 4. Upper left (1, 0) |
| 49 | +
|
| 50 | + This corresponds to following the corners counterclockwise starting with |
| 51 | + the corner in the axes origin. The second array axis contains the 3D |
| 52 | + coordinates of the corners in the standard PIR references space. |
| 53 | +
|
| 54 | + plane_of_section |
| 55 | + The plane of section of the dataset. Can be either coronal or sagittal. |
| 56 | +
|
| 57 | + Returns |
| 58 | + ------- |
| 59 | + fig |
| 60 | + The figure with the plot. |
| 61 | + """ |
| 62 | + # A semi-arbitrary choice of the reference space scale. This choice only |
| 63 | + # changes the ticks on the axes, but not the overall plot. The 25µm scale |
| 64 | + # is one of the common scales used for volumes. |
| 65 | + ref_space_scale = 25 |
| 66 | + ref_space_size = np.array(REF_DIM_1UM) / ref_space_scale |
| 67 | + p, i, r = 0, 1, 2 |
| 68 | + labels = { |
| 69 | + p: "p (coronal)", |
| 70 | + i: "i (transverse)", |
| 71 | + r: "r (sagittal)", |
| 72 | + } |
| 73 | + # We'll plot the views of all four edges in counterclockwise order starting |
| 74 | + # with the bottom edge. The last two x-axes are inverted so that the edge |
| 75 | + # vertices on the right of a plot appear on the left of the following plot. |
| 76 | + edges = [[0, 1], [1, 2], [2, 3], [3, 0]] |
| 77 | + titles = ["Bottom Edges", "Right Edges", "Top Edges", "Left Edges"] |
| 78 | + inverts = [False, False, True, True] |
| 79 | + |
| 80 | + # Depending on the plane of section the views are different |
| 81 | + if plane_of_section == PlaneOfSection.CORONAL: |
| 82 | + y_axis = p |
| 83 | + x_axes = [r, i, r, i] |
| 84 | + elif plane_of_section == PlaneOfSection.SAGITTAL: |
| 85 | + y_axis = r |
| 86 | + x_axes = [p, i, p, i] |
| 87 | + else: # pragma: no cover |
| 88 | + raise NotImplementedError(f"Unknown plane of section: {plane_of_section}") |
| 89 | + |
| 90 | + # The figure width is fixed and arbitrary. (Is there a more clever choice?) |
| 91 | + # The figure height is computed to match the ratio between the total width |
| 92 | + # and height of all subplots. This way the ratios of the x and y axes are |
| 93 | + # roughly the same (but not quite since the in-between spaces, titles, etc. |
| 94 | + # are not taken into account...) |
| 95 | + plot_width = sum(ref_space_size[x_axis] for x_axis in x_axes) |
| 96 | + plot_height = ref_space_size[y_axis] |
| 97 | + fig_width_inches = 14 |
| 98 | + fig_height_inches = fig_width_inches * plot_height / plot_width |
| 99 | + |
| 100 | + fig = Figure(figsize=(fig_width_inches, fig_height_inches)) |
| 101 | + fig.set_tight_layout(True) |
| 102 | + # The width ratios of subplots are based on the reference volume dimensions, |
| 103 | + # this way the scales of the x-axes roughly match. |
| 104 | + axs = fig.subplots( |
| 105 | + ncols=4, |
| 106 | + sharey=True, |
| 107 | + gridspec_kw={"width_ratios": [ref_space_size[x_axis] for x_axis in x_axes]}, |
| 108 | + ) |
| 109 | + # Y-label only on the left-most plot because it's the same for all plots |
| 110 | + axs[0].set_ylabel(labels[y_axis], fontsize=16) |
| 111 | + |
| 112 | + # Add the legend for the reference space lines |
| 113 | + ref_space_line_style = {"color": "blue", "linestyle": ":"} |
| 114 | + line = Line2D([], [], **ref_space_line_style) |
| 115 | + axs[0].legend( |
| 116 | + [line], |
| 117 | + ["Reference space boundary"], |
| 118 | + loc="upper left", |
| 119 | + bbox_to_anchor=(0, -0.2), |
| 120 | + borderaxespad=0, |
| 121 | + frameon=False, |
| 122 | + ) |
| 123 | + |
| 124 | + # The actual plotting |
| 125 | + for ax, edge, x_axis, invert, title in zip(axs, edges, x_axes, inverts, titles): |
| 126 | + # Axes setup |
| 127 | + ax.grid(True, linestyle=":", color="gray") |
| 128 | + ax.set_title(title) |
| 129 | + ax.set_xlabel(labels[x_axis], fontsize=16) |
| 130 | + |
| 131 | + # Reference space boundary lines |
| 132 | + ax.axvline(0, **ref_space_line_style) |
| 133 | + ax.axvline(ref_space_size[x_axis], **ref_space_line_style) |
| 134 | + ax.axhline(0, **ref_space_line_style) |
| 135 | + ax.axhline(ref_space_size[y_axis], **ref_space_line_style) |
| 136 | + |
| 137 | + # Plot the section image edges |
| 138 | + for corners in all_corners: |
| 139 | + points = corners[np.ix_(edge, [x_axis, y_axis])] |
| 140 | + coords = points.T / ref_space_scale |
| 141 | + ax.plot(*coords, color="green") |
| 142 | + ax.scatter(*coords, color="red") |
| 143 | + if invert: |
| 144 | + ax.invert_xaxis() |
| 145 | + |
| 146 | + return fig |
0 commit comments