Skip to content

Commit a737901

Browse files
committed
Add support to compute and visualize PCA
1 parent e43adbb commit a737901

4 files changed

Lines changed: 86 additions & 12 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ dependencies = [
5151
csv = ["pandas>=1"]
5252
plot = ["distinctipy>=1.3.4", "matplotlib>=3.4"]
5353
video = ["ffmpeg-python>=0.2.0"]
54+
sklearn = [
55+
"scikit-learn>=1.6.1",
56+
]
5457

5558
[project.scripts]
5659
tiohd = "torchio.cli.print_info:app"

src/torchio/external/imports.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ def get_ffmpeg() -> ModuleType:
3636
return ffmpeg
3737

3838

39+
def get_sklearn() -> ModuleType:
40+
return _check_and_import(module='sklearn', extra='sklearn', package='scikit-learn')
41+
42+
3943
def _check_executable(executable: str) -> None:
4044
if which(executable) is None:
4145
message = (
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import numpy as np
2+
import torch
3+
from einops import rearrange
4+
5+
from ....external.imports import get_sklearn
6+
7+
8+
def _pca(
9+
data: torch.Tensor,
10+
num_components: int = 6,
11+
whiten: bool = True,
12+
vmin: float = -2.3,
13+
vmax: float = 2.3,
14+
) -> torch.Tensor:
15+
# Adapted from https://github.com/facebookresearch/capi/blob/main/eval_visualizations.py
16+
17+
sklearn = get_sklearn()
18+
PCA = sklearn.decomposition.PCA
19+
20+
_, size_x, size_y, size_z = data.shape
21+
X = rearrange(data, 'c x y z -> (x y z) c')
22+
pca = PCA(n_components=num_components, whiten=whiten)
23+
projected: np.ndarray = pca.fit_transform(X)
24+
projected /= projected[:, 0].std()
25+
for i in range(num_components):
26+
numerator = np.mean(np.power(projected[:, i], 3))
27+
denominator = np.power(np.mean(np.power(projected[:, i], 2)), 1.5)
28+
skew = numerator / denominator
29+
if skew < 0:
30+
projected[:, i] *= -1
31+
grid: np.ndarray = rearrange(
32+
projected,
33+
'(x y z) c -> c x y z',
34+
x=size_x,
35+
y=size_y,
36+
z=size_z,
37+
)
38+
grid = (grid - vmin) / (vmax - vmin)
39+
return torch.from_numpy(grid.clip(0, 1))

src/torchio/visualization.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import numpy as np
88
import torch
9+
from einops import rearrange
910

1011
from .data.image import Image
1112
from .data.image import LabelMap
@@ -35,7 +36,7 @@ def import_mpl_plt():
3536

3637
def rotate(image, radiological=True, n=-1):
3738
# Rotate for visualization purposes
38-
image = np.rot90(image, n)
39+
image = np.rot90(image, n, axes=(0, 1))
3940
if radiological:
4041
image = np.fliplr(image)
4142
return image
@@ -59,17 +60,18 @@ def _create_categorical_colormap(data: torch.Tensor) -> ListedColormap:
5960
def plot_volume(
6061
image: Image,
6162
radiological=True,
62-
channel=-1, # default to foreground for binary maps
63+
channel=None,
6364
axes=None,
6465
cmap=None,
6566
output_path=None,
6667
show=True,
6768
xlabels=True,
68-
percentiles: tuple[float, float] = (0.5, 99.5),
69+
percentiles: tuple[float, float] = (0, 100),
6970
figsize=None,
7071
title=None,
7172
reorient=True,
7273
indices=None,
74+
rgb=True,
7375
**imshow_kwargs,
7476
):
7577
_, plt = import_mpl_plt()
@@ -80,14 +82,25 @@ def plot_volume(
8082

8183
if reorient:
8284
image = ToCanonical()(image) # type: ignore[assignment]
83-
data = image.data[channel]
85+
86+
is_label = isinstance(image, LabelMap)
87+
if is_label: # probabilistic label map
88+
data = image.data[np.newaxis, -1]
89+
elif rgb and image.num_channels == 3:
90+
data = image.data # keep image as it is
91+
elif channel is None:
92+
data = image.data[0:1] # just use the first channel
93+
else:
94+
data = image.data[np.newaxis, channel]
95+
data = rearrange(data, 'c x y z -> x y z c')
96+
8497
if indices is None:
85-
indices = np.array(data.shape) // 2
98+
indices = np.array(data.shape[:3]) // 2
8699
i, j, k = indices
87100
slice_x = rotate(data[i, :, :], radiological=radiological)
88101
slice_y = rotate(data[:, j, :], radiological=radiological)
89102
slice_z = rotate(data[:, :, k], radiological=radiological)
90-
is_label = isinstance(image, LabelMap)
103+
91104
if isinstance(cmap, dict):
92105
slices = slice_x, slice_y, slice_z
93106
slice_x, slice_y, slice_z = color_labels(slices, cmap)
@@ -98,6 +111,9 @@ def plot_volume(
98111

99112
if is_label:
100113
imshow_kwargs['interpolation'] = 'none'
114+
else:
115+
if 'interpolation' not in imshow_kwargs:
116+
imshow_kwargs['interpolation'] = 'bicubic'
101117

102118
sr, sa, ss = image.spacing
103119
imshow_kwargs['origin'] = 'lower'
@@ -108,23 +124,35 @@ def plot_volume(
108124
imshow_kwargs['vmax'] = p2
109125

110126
sag_aspect = ss / sa
111-
sag_axis.imshow(slice_x, aspect=sag_aspect, **imshow_kwargs)
127+
sag_axis.imshow(
128+
slice_x,
129+
aspect=sag_aspect,
130+
**imshow_kwargs,
131+
)
112132
if xlabels:
113133
sag_axis.set_xlabel('A')
114134
sag_axis.set_ylabel('S')
115135
sag_axis.invert_xaxis()
116136
sag_axis.set_title('Sagittal')
117137

118138
cor_aspect = ss / sr
119-
cor_axis.imshow(slice_y, aspect=cor_aspect, **imshow_kwargs)
139+
cor_axis.imshow(
140+
slice_y,
141+
aspect=cor_aspect,
142+
**imshow_kwargs,
143+
)
120144
if xlabels:
121145
cor_axis.set_xlabel('R')
122146
cor_axis.set_ylabel('S')
123147
cor_axis.invert_xaxis()
124148
cor_axis.set_title('Coronal')
125149

126150
axi_aspect = sa / sr
127-
axi_axis.imshow(slice_z, aspect=axi_aspect, **imshow_kwargs)
151+
axi_axis.imshow(
152+
slice_z,
153+
aspect=axi_aspect,
154+
**imshow_kwargs,
155+
)
128156
if xlabels:
129157
axi_axis.set_xlabel('R')
130158
axi_axis.set_ylabel('A')
@@ -223,15 +251,15 @@ def plot_histogram(x: np.ndarray, show=True, **kwargs) -> None:
223251

224252
def color_labels(arrays, cmap_dict):
225253
results = []
226-
for array in arrays:
227-
si, sj = array.shape
254+
for slice_array in arrays:
255+
si, sj, _ = slice_array.shape
228256
rgb = np.zeros((si, sj, 3), dtype=np.uint8)
229257
for label, color in cmap_dict.items():
230258
if isinstance(color, str):
231259
mpl, _ = import_mpl_plt()
232260
color = mpl.colors.to_rgb(color)
233261
color = [255 * n for n in color]
234-
rgb[array == label] = color
262+
rgb[slice_array[..., 0] == label] = color
235263
results.append(rgb)
236264
return results
237265

0 commit comments

Comments
 (0)