Skip to content

visualizer 🧠

Init visualizer submodule of xai4mri.

Author: Simon M. Hofmann
Years: 2023

plot_heatmap 🧠

plot_heatmap(
    ipt: ndarray,
    analyser_obj: ndarray,
    cmap_name: str = "black-fire-red",
    mode: str = "triplet",
    fig_name: str = "Heatmap",
    **kwargs
) -> Figure

Plot an XAI-based analyzer object over the model input in the form of a heatmap.

How to use

from xai4mri.model.interpreter import analyze_model
from xai4mri.visualizer import plot_heatmap

# Analyze model
analyzer_obj = analyze_model(model=model, ipt=mri_image, ...)

# Visualize heatmap / relevance map
analyzer_fig = plot_heatmap(ipt=mri_image, analyser_obj=analyzer_obj, ...)

Parameters:

Name Type Description Default
ipt ndarray

Model input image.

required
analyser_obj ndarray

Analyzer object (relevance map) that is computed by the model interpreter (e.g., LRP). Both the input image and the analyzer object must have the same shape.

required
cmap_name str

Name of color-map (cmap) to be applied.

'black-fire-red'
mode str

"triplet": Plot three slices of different axes. "all": Plot all slices (w/ brain OR w/o brain → set: plot_empty=True in kwargs)

'triplet'
fig_name str

name of figure

'Heatmap'
kwargs

Additional kwargs: "c_intensifier", "clip_q", "min_sym_clip", "true_scale", "plot_empty", "axis", "every", "crosshair", "gamma". And, kwargs for plot_mid_slice() and slice_through() from xai4mri.visualizer.visualize_mri.

{}

Returns:

Type Description
Figure

plt.Figure object of the heatmap plot.

Source code in src/xai4mri/visualizer/visualize_heatmap.py
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
def plot_heatmap(
    ipt: np.ndarray,
    analyser_obj: np.ndarray,
    cmap_name: str = "black-fire-red",
    mode: str = "triplet",
    fig_name: str = "Heatmap",
    **kwargs,
) -> plt.Figure:
    """
    Plot an XAI-based analyzer object over the model input in the form of a heatmap.

    !!! example "How to use"
        ```python
        from xai4mri.model.interpreter import analyze_model
        from xai4mri.visualizer import plot_heatmap

        # Analyze model
        analyzer_obj = analyze_model(model=model, ipt=mri_image, ...)

        # Visualize heatmap / relevance map
        analyzer_fig = plot_heatmap(ipt=mri_image, analyser_obj=analyzer_obj, ...)
        ```

    :param ipt: Model input image.
    :param analyser_obj: Analyzer object (relevance map) that is computed by the model interpreter (e.g., `LRP`).
                         Both the input image and the analyzer object must have the same shape.
    :param cmap_name: Name of color-map (`cmap`) to be applied.
    :param mode: "triplet": Plot three slices of different axes.
                 "all": Plot all slices (w/ brain OR w/o brain → set: `plot_empty=True` in `kwargs`)
    :param fig_name: name of figure
    :param kwargs: Additional kwargs:
                   "c_intensifier", "clip_q", "min_sym_clip", "true_scale", "plot_empty", "axis", "every", "crosshair",
                    "gamma".
                    And, `kwargs` for `plot_mid_slice()` and `slice_through()` from `xai4mri.visualizer.visualize_mri`.
    :return: `plt.Figure` object of the heatmap plot.
    """
    a = analyser_obj.copy().squeeze().astype(np.float32)
    mode = mode.lower()
    if mode not in {"triplet", "all"}:
        msg = "mode must be 'triplet', or 'all'!"
        raise ValueError(msg)

    # Extract kwargs
    cintensifier = kwargs.pop("c_intensifier", 1.0)
    clipq = kwargs.pop("clip_q", 1e-2)
    min_sym_clip = kwargs.pop("min_sym_clip", True)
    true_scale = kwargs.pop("true_scale", False)
    plot_empty = kwargs.pop("plot_empty", False)
    axis = kwargs.pop("axis", 0)
    every = kwargs.pop("every", 2)
    crosshairs = kwargs.pop("crosshair", False)
    gamma = kwargs.pop("gamma", 0.2)

    # Render image
    colored_a = _apply_colormap(
        analyser_obj=a,
        input_image=ipt.squeeze().astype(np.float32),
        cmap_name=cmap_name,
        c_intensifier=cintensifier,
        clip_q=clipq,
        min_sym_clip=min_sym_clip,
        gamma=gamma,
        true_scale=true_scale,
    )
    heatmap = colored_a[0]

    cbar_range = (-1, 1) if not true_scale else (-colored_a[2], colored_a[2])
    if mode == "triplet":
        fig = plot_mid_slice(
            volume=heatmap,
            fig_name=fig_name,
            cmap=_create_cmap(gregoire_black_fire_red),
            c_range="full",
            cbar=True,
            cbar_range=cbar_range,
            edges=False,
            crosshairs=crosshairs,
            **kwargs,
        )

    else:  # mode == "all"
        if not plot_empty:
            # Remove planes with no information
            heatmap = heatmap.compress(
                ~np.all(
                    heatmap == 0,
                    axis=tuple(ax for ax in range(heatmap.ndim) if ax != axis),
                ),
                axis=axis,
            )

        fig = slice_through(
            volume=heatmap,
            every=every,
            axis=axis,
            fig_name=fig_name,
            edges=False,
            cmap=_create_cmap(gregoire_black_fire_red),
            c_range="full",
            crosshairs=crosshairs,
            **kwargs,
        )
    return fig

plot_mid_slice 🧠

plot_mid_slice(
    volume: ndarray,
    axis: int | _Axis | None = None,
    fig_name: str | None = None,
    edges: bool = True,
    c_range: str | None = None,
    **kwargs
) -> Figure

Plot 2D-slices of a given 3D-volume.

If no axis is given, plot for each axis its middle slice.

Parameters:

Name Type Description Default
volume ndarray

3D volume (MRI).

required
axis int | _Axis | None

None: Slices from all three axes are plotted. To plot a specific axis only, provide int or _Axis.

None
fig_name str | None

Name of the figure.

None
edges bool

Draw edges around the brain if set to True.

True
c_range str | None

Color range: "full": Full possible spectrum (0-255 or 0-1), "single": Take min/max of given volume. None: Default: do not change color range.

None
kwargs

Additional keyword arguments for plt.imshow(). And, crosshairs: bool = True, ticks: bool = False. Also, cbar: bool = False and cbar_range: tuple[int, int] = None for a color bar, suptitle: str = None for a title, and slice_idx: int | tuple[int, int, int] = None for specific slice indices to plot, if not provided, the middle slice(s) are plotted.

{}

Returns:

Type Description
Figure

The figure object of the plot.

Source code in src/xai4mri/visualizer/visualize_mri.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
def plot_mid_slice(
    volume: np.ndarray,
    axis: int | _Axis | None = None,
    fig_name: str | None = None,
    edges: bool = True,
    c_range: str | None = None,
    **kwargs,
) -> plt.Figure:
    """
    Plot 2D-slices of a given 3D-volume.

    If no axis is given, plot for each axis its middle slice.

    :param volume: 3D volume (MRI).
    :param axis: `None`: Slices from all three axes are plotted.
                 To plot a specific axis only, provide `int` or `_Axis`.
    :param fig_name: Name of the figure.
    :param edges:  Draw edges around the brain if set to `True`.
    :param c_range: Color range:
                    "full": Full possible spectrum (0-255 or 0-1),
                    "single": Take min/max of given volume.
                    `None`: Default: do not change color range.
    :param kwargs: Additional keyword arguments for `plt.imshow()`.
                   And, `crosshairs: bool = True`, `ticks: bool = False`.
                   Also, `cbar: bool = False` and `cbar_range: tuple[int, int] = None` for a color bar,
                   `suptitle: str = None` for a title,
                   and `slice_idx: int | tuple[int, int, int] = None` for specific slice indices to plot,
                   if not provided, the middle slice(s) are plotted.
    :return: The figure object of the plot.
    """
    # Get color bar kwargs (if any)
    cbar = kwargs.pop("cbar", False)
    cbar_range = kwargs.pop("cbar_range") if ("cbar_range" in kwargs and cbar) else None
    # only if cbar is active
    suptitle = kwargs.pop("suptitle", None)
    slice_idx = kwargs.pop("slice_idx", None)  # in case mid or other slice is given

    # Set (mid-)slice index
    sl = int(np.round(volume.shape[0] / 2)) if slice_idx is None else slice_idx
    max_n_axes = 3
    if slice_idx is None:
        sl_str = "mid"
    elif isinstance(sl, (list, tuple)):
        if len(sl) != max_n_axes:
            msg = "slice_idx must be tuple or list of length 3, is None or single int."
            raise ValueError(msg)
        sl_str = str(sl)
    else:
        sl_str = str(int(sl))

    # Begin plotting
    if axis is None:
        _fs = {"size": 10}  # define font size

        fig = plt.figure(num=f"{fig_name if fig_name else ''} volume {sl_str}-slice", figsize=(12, 4))
        if isinstance(suptitle, str) and suptitle:
            fig.suptitle(suptitle, fontsize=14)

        # Planes
        ims = []
        axs = []
        sls = [sl] * 3 if isinstance(sl, int) else sl  # is tuple pf length 3 or int
        for ip, _plane in enumerate(PLANES):
            axs.append(fig.add_subplot(1, 3, ip + 1))
            ims.append(
                plot_slice(
                    volume,
                    axis=ip,
                    idx_slice=sls[ip],
                    edges=edges,
                    c_range=c_range,
                    **kwargs,
                )
            )
            plt.title(_plane, fontdict=_fs)

            divider = make_axes_locatable(axs[ip])
            cax = divider.append_axes("right", size="5%", pad=0.05)
            cax.axis("off")
            if cbar and ip == len(PLANES) - 1:
                cax_bar = fig.colorbar(ims[-1], ax=cax, fraction=0.048, pad=0.04)  # shrink=.8, aspect=50)
                if cbar_range:
                    cax_bar.set_ticks(ticks=np.linspace(start=0, stop=1, num=7), minor=True)
                    cax_bar.ax.set_yticklabels(
                        labels=[
                            f"{tick:.2g}"
                            for tick in np.linspace(cbar_range[0], cbar_range[1], len(cax_bar.get_ticks()))
                        ]
                    )

    else:  # If specific axis to plot
        axis = _Axis(axis)
        axis_name = axis.name.lower()

        fig = plt.figure(f"{fig_name if fig_name else ''} {axis_name} {sl_str}-slice")
        im = plot_slice(volume, axis.value, idx_slice=sl, edges=edges, c_range=c_range, **kwargs)
        if cbar:
            cax_bar = fig.colorbar(im, fraction=0.048, pad=0.04)  # shrink=0.8, aspect=50)
            if cbar_range:
                cax_bar.set_ticks(ticks=np.linspace(start=0, stop=1, num=7), minor=True)
                cax_bar.ax.set_yticklabels(
                    labels=[
                        f"{tick:.2g}" for tick in np.linspace(cbar_range[0], cbar_range[1], len(cax_bar.get_ticks()))
                    ]
                )

        plt.tight_layout()

    return fig

plot_slice 🧠

plot_slice(
    volume: ndarray,
    axis: int | _Axis,
    idx_slice: int,
    edges: bool = False,
    c_range: str | None = None,
    **kwargs
) -> AxesImage

Plot one slice of a 3D volume.

Parameters:

Name Type Description Default
volume ndarray

3D volume (MRI).

required
axis int | _Axis

The volume axis to plot from.

required
idx_slice int

The slice index to plot.

required
edges bool

Draw edges around the brain if set to True.

False
c_range str | None

Color range: "full": Full possible spectrum (0-255 or 0-1), "single": Take min/max of given volume. None: Default: do not change color range.

None
kwargs

Additional keyword arguments for plt.imshow(). And, crosshairs: bool = True, ticks: bool = False.

{}
Source code in src/xai4mri/visualizer/visualize_mri.py
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def plot_slice(
    volume: np.ndarray,
    axis: int | _Axis,
    idx_slice: int,
    edges: bool = False,
    c_range: str | None = None,
    **kwargs,
) -> plt.AxesImage:
    """
    Plot one slice of a 3D volume.

    :param volume: 3D volume (MRI).
    :param axis: The volume axis to plot from.
    :param idx_slice: The slice index to plot.
    :param edges: Draw edges around the brain if set to `True`.
    :param c_range: Color range:
                    "full": Full possible spectrum (0-255 or 0-1),
                    "single": Take min/max of given volume.
                    `None`: Default: do not change color range.
    :param kwargs: Additional keyword arguments for `plt.imshow()`.
                   And, `crosshairs: bool = True`, `ticks: bool = False`.
    """
    im, _edges = None, None  # init
    if edges:
        n_dims = 4
        _edges = find_brain_edges(volume if volume.shape[-1] > n_dims else volume[..., -1])
        # works for transparent RGB images (x,y,z,4) and volumes (x,y,z)

    # Set color range
    if c_range == "full":  # takes full possible spectrum
        i_max = 255 if np.max(volume) > 1 else 1.0
        i_min = 0 if np.min(volume) >= 0 else -1.0
    elif c_range == "single":  # takes min/max of given brain
        i_max = np.max(volume)
        i_min = np.min(volume)
    else:  # c_range=None
        if c_range is not None:
            msg = "c_range must be 'full', 'single' or None."
            raise ValueError(msg)
        i_max, i_min = None, None

    # Get kwargs (which are not for imshow)
    crosshairs = kwargs.pop("crosshairs", False)
    ticks = kwargs.pop("ticks", False)

    axis = _Axis(axis)  # this also checks if the axis is valid
    if axis is _Axis.SAGITTAL:  # sagittal
        im = plt.imshow(volume[idx_slice, :, :], vmin=i_min, vmax=i_max, **kwargs)
        if edges:
            plt.hlines(_edges[2] - 1, 2, volume.shape[1] - 2, colors="darkgrey", alpha=0.3)  # == max edges
            plt.hlines(_edges[3] + 1, 2, volume.shape[1] - 2, colors="darkgrey", alpha=0.3)
            plt.vlines(_edges[4] - 1, 2, volume.shape[0] - 2, colors="darkgrey", alpha=0.3)
            plt.vlines(_edges[5] + 1, 2, volume.shape[0] - 2, colors="darkgrey", alpha=0.3)

    elif axis == _Axis.TRANSVERSE:  # transverse / superior
        im = plt.imshow(
            np.rot90(volume[:, idx_slice, :], axes=(0, 1)),
            vmin=i_min,
            vmax=i_max,
            **kwargs,
        )
        if edges:
            plt.hlines(
                volume.shape[0] - _edges[5] - 1,
                2,
                volume.shape[0] - 2,
                colors="darkgrey",
                alpha=0.3,
            )
            plt.hlines(
                volume.shape[0] - _edges[4] + 1,
                2,
                volume.shape[0] - 2,
                colors="darkgrey",
                alpha=0.3,
            )
            plt.vlines(_edges[0] - 1, 2, volume.shape[1] - 2, colors="darkgrey", alpha=0.3)
            plt.vlines(_edges[1] + 1, 2, volume.shape[1] - 2, colors="darkgrey", alpha=0.3)

    elif axis == _Axis.CORONAL:  # coronal / posterior
        im = plt.imshow(
            np.rot90(volume[:, :, idx_slice], axes=(1, 0)),
            vmin=i_min,
            vmax=i_max,
            **kwargs,
        )
        if edges:
            plt.hlines(_edges[2] - 1, 2, volume.shape[0] - 2, colors="darkgrey", alpha=0.3)
            plt.hlines(_edges[3] + 1, 2, volume.shape[0] - 2, colors="darkgrey", alpha=0.3)
            plt.vlines(
                volume.shape[1] - _edges[1] - 1,
                2,
                volume.shape[1] - 2,
                colors="darkgrey",
                alpha=0.3,
            )
            plt.vlines(
                volume.shape[1] - _edges[0] + 1,
                2,
                volume.shape[1] - 2,
                colors="darkgrey",
                alpha=0.3,
            )

    # Add mid-cross ('crosshairs')
    if crosshairs:
        plt.hlines(int(volume.shape[axis.value] / 2), 2, len(volume) - 2, colors="red", alpha=0.3)
        plt.vlines(int(volume.shape[axis.value] / 2), 2, len(volume) - 2, colors="red", alpha=0.3)

    if not ticks:
        plt.axis("off")

    return im

slice_through 🧠

slice_through(
    volume: ndarray,
    every: int = 2,
    axis: int | _Axis = 0,
    fig_name: str | None = None,
    edges: bool = True,
    c_range: str | None = None,
    **kwargs
) -> Figure

Plot several slices of a given 3D-volume in the form of a grid.

For fancy slicing, check: https://docs.pyvista.org/examples/01-filter/slicing.html.

Parameters:

Name Type Description Default
volume ndarray

3D volume (MRI).

required
every int

Plot every n-th slice. That is, for every=1, all slices are plotted. And, for every=2, every second slice is plotted, and so on.

2
axis int | _Axis

The volume axis to plot from.

0
fig_name str | None

Name of the figure.

None
edges bool

Draw edges around the brain if set to True.

True
c_range str | None

Color range: "full": Full possible spectrum (0-255 or 0-1), "single": Take min/max of given volume. None: Default: do not change color range.

None
kwargs

Additional keyword arguments for plt.imshow(). And, crosshairs: bool = True, ticks: bool = False.

{}

Returns:

Type Description
Figure

The figure object of the plot.

Source code in src/xai4mri/visualizer/visualize_mri.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
def slice_through(
    volume: np.ndarray,
    every: int = 2,
    axis: int | _Axis = 0,
    fig_name: str | None = None,
    edges: bool = True,
    c_range: str | None = None,
    **kwargs,
) -> plt.Figure:
    """
    Plot several slices of a given 3D-volume in the form of a grid.

    For fancy slicing, check: https://docs.pyvista.org/examples/01-filter/slicing.html.

    :param volume: 3D volume (MRI).
    :param every: Plot every n-th slice.
                  That is, for `every=1`, all slices are plotted.
                  And, for `every=2`, every second slice is plotted, and so on.
    :param axis: The volume axis to plot from.
    :param fig_name: Name of the figure.
    :param edges: Draw edges around the brain if set to `True`.
    :param c_range: Color range:
                    "full": Full possible spectrum (0-255 or 0-1),
                    "single": Take min/max of given volume.
                    `None`: Default: do not change color range.
    :param kwargs: Additional keyword arguments for `plt.imshow()`.
                   And, `crosshairs: bool = True`, `ticks: bool = False`.
    :return: The figure object of the plot.
    """
    axis = _Axis(axis)

    n_row_sq_grid = 5
    n_slices = volume.shape[axis.value] // every
    n_figs = np.round(n_slices // n_row_sq_grid**2)

    axis_name = axis.name.lower()

    fig_n = 1
    sub_n = 1
    fig = None  # init
    for scl in range(n_slices):
        if scl % (n_row_sq_grid**2) == 0:
            fig = plt.figure(
                num=f"{fig_name if fig_name else ''} {axis_name} slice-through {fig_n}|{n_figs}",
                figsize=(10, 10),
            )

        plt.subplot(n_row_sq_grid, n_row_sq_grid, sub_n)

        plot_slice(
            volume=volume,
            axis=axis,
            idx_slice=scl + (every - 1),
            edges=edges,
            c_range=c_range,
            **kwargs,
        )

        plt.tight_layout()

        sub_n += 1

        if ((sub_n - 1) % (n_row_sq_grid**2) == 0) or (scl + 1 == n_slices):
            fig_n += 1
            sub_n = 1

    return fig