Skip to content

Commit 9691364

Browse files
committed
Improve visualization and PCA
1 parent a737901 commit 9691364

2 files changed

Lines changed: 36 additions & 20 deletions

File tree

src/torchio/transforms/preprocessing/intensity/pca.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,39 @@ def _pca(
99
data: torch.Tensor,
1010
num_components: int = 6,
1111
whiten: bool = True,
12-
vmin: float = -2.3,
13-
vmax: float = 2.3,
12+
clip_range: tuple[float, float] | None = (-2.3, 2.3),
13+
normalize: bool = True,
14+
make_skewness_positive: bool = True,
15+
**pca_kwargs,
1416
) -> torch.Tensor:
1517
# Adapted from https://github.com/facebookresearch/capi/blob/main/eval_visualizations.py
16-
18+
# 2.3 is roughly 2σ for a standard-normal variable, 99% of values map inside [0,1].
1719
sklearn = get_sklearn()
1820
PCA = sklearn.decomposition.PCA
1921

2022
_, size_x, size_y, size_z = data.shape
2123
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
24+
pca = PCA(n_components=num_components, whiten=whiten, **pca_kwargs)
25+
projected: np.ndarray = pca.fit_transform(X).T
26+
if normalize:
27+
projected /= projected[0].std()
28+
if make_skewness_positive:
29+
for component in projected:
30+
third_cumulant = np.mean(component**3)
31+
second_cumulant = np.mean(component**2)
32+
skewness = third_cumulant / second_cumulant ** (3 / 2)
33+
if skewness < 0:
34+
component *= -1
3135
grid: np.ndarray = rearrange(
32-
projected,
36+
projected.T,
3337
'(x y z) c -> c x y z',
3438
x=size_x,
3539
y=size_y,
3640
z=size_z,
3741
)
42+
if clip_range is not None:
43+
vmin, vmax = clip_range
44+
else:
45+
vmin, vmax = grid.min(), grid.max()
3846
grid = (grid - vmin) / (vmax - vmin)
3947
return torch.from_numpy(grid.clip(0, 1))

src/torchio/visualization.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def import_mpl_plt():
3434
return mpl, plt
3535

3636

37-
def rotate(image, radiological=True, n=-1):
37+
def rotate(image: np.ndarray, *, radiological: bool = True, n: int = -1) -> np.ndarray:
3838
# Rotate for visualization purposes
3939
image = np.rot90(image, n, axes=(0, 1))
4040
if radiological:
@@ -93,13 +93,14 @@ def plot_volume(
9393
else:
9494
data = image.data[np.newaxis, channel]
9595
data = rearrange(data, 'c x y z -> x y z c')
96+
data_numpy: np.ndarray = data.cpu().numpy()
9697

9798
if indices is None:
98-
indices = np.array(data.shape[:3]) // 2
99+
indices = np.array(data_numpy.shape[:3]) // 2
99100
i, j, k = indices
100-
slice_x = rotate(data[i, :, :], radiological=radiological)
101-
slice_y = rotate(data[:, j, :], radiological=radiological)
102-
slice_z = rotate(data[:, :, k], radiological=radiological)
101+
slice_x = rotate(data_numpy[i, :, :], radiological=radiological)
102+
slice_y = rotate(data_numpy[:, j, :], radiological=radiological)
103+
slice_z = rotate(data_numpy[:, :, k], radiological=radiological)
103104

104105
if isinstance(cmap, dict):
105106
slices = slice_x, slice_y, slice_z
@@ -118,8 +119,15 @@ def plot_volume(
118119
sr, sa, ss = image.spacing
119120
imshow_kwargs['origin'] = 'lower'
120121

121-
if percentiles is not None and not is_label:
122-
p1, p2 = np.percentile(data, percentiles)
122+
if not is_label:
123+
displayed_data = np.concatenate(
124+
[
125+
slice_x.flatten(),
126+
slice_y.flatten(),
127+
slice_z.flatten(),
128+
]
129+
)
130+
p1, p2 = np.percentile(displayed_data, percentiles)
123131
imshow_kwargs['vmin'] = p1
124132
imshow_kwargs['vmax'] = p2
125133

0 commit comments

Comments
 (0)