Skip to content

Commit 7d50907

Browse files
authored
[FIX] Improve bounding box computation from masks (#304)
* [FIX] Improve bounding box computation from masks Fixes #281 * [skip ci] add comments * fix tests * Update utils.py * [skip ci] Improve comments Co-Authored-By: oesteban <[email protected]> * take care of very small masks
1 parent 28fbb21 commit 7d50907

File tree

2 files changed

+39
-10
lines changed

2 files changed

+39
-10
lines changed

.circleci/config.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ jobs:
2121
- docker-v1-
2222
paths:
2323
- /tmp/cache/docker.tar.gz
24-
- /tmp/cache/ubuntu.tar.gz
2524
- checkout
2625
- setup_remote_docker
2726
- run:

niworkflows/viz/utils.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -199,20 +199,50 @@ def extract_svg(display_object, dpi=300, compress='auto'):
199199
def cuts_from_bbox(mask_nii, cuts=3):
200200
"""Finds equi-spaced cuts for presenting images"""
201201
from nibabel.affines import apply_affine
202-
mask_data = mask_nii.get_data()
203-
B = np.argwhere(mask_data > 0)
204-
start_coords = B.min(0)
205-
stop_coords = B.max(0) + 1
202+
203+
mask_data = mask_nii.get_data() > 0.0
204+
205+
# First, project the number of masked voxels on each axes
206+
ijk_counts = [
207+
mask_data.sum(2).sum(1), # project sagittal planes to transverse (i) axis
208+
mask_data.sum(2).sum(0), # project coronal planes to to longitudinal (j) axis
209+
mask_data.sum(1).sum(0), # project axial planes to vertical (k) axis
210+
]
211+
212+
# If all voxels are masked in a slice (say that happens at k=10),
213+
# then the value for ijk_counts for the projection to k (ie. ijk_counts[2])
214+
# at that element of the orthogonal axes (ijk_counts[2][10]) is
215+
# the total number of voxels in that slice (ie. Ni x Nj).
216+
# Here we define some thresholds to consider the plane as "masked"
217+
# The thresholds vary because of the shape of the brain
218+
# I have manually found that for the axial view requiring 30%
219+
# of the slice elements to be masked drops almost empty boxes
220+
# in the mosaic of axial planes (and also addresses #281)
221+
ijk_th = [
222+
int((mask_data.shape[1] * mask_data.shape[2]) * 0.2), # sagittal
223+
int((mask_data.shape[0] * mask_data.shape[2]) * 0.0), # coronal
224+
int((mask_data.shape[0] * mask_data.shape[1]) * 0.3), # axial
225+
]
206226

207227
vox_coords = []
208-
for start, stop in zip(start_coords, stop_coords):
209-
inc = abs(stop - start) / (cuts + 1)
210-
vox_coords.append([start + (i + 1) * inc for i in range(cuts)])
228+
for ax, (c, th) in enumerate(zip(ijk_counts, ijk_th)):
229+
B = np.argwhere(c > th)
230+
if B.size:
231+
smin, smax = B.min(), B.max()
232+
233+
# Avoid too narrow selections of cuts (very small masks)
234+
if not B.size or (th > 0 and (smin + cuts + 1) >= smax):
235+
B = np.argwhere(c > 0)
236+
237+
# Resort to full plane if mask is seemingly empty
238+
smin, smax = B.min(), B.max() if B.size else (0, mask_data.shape[ax])
239+
inc = (smax - smin) / (cuts + 1)
240+
vox_coords.append([smin + (i + 1) * inc for i in range(cuts)])
211241

212242
ras_coords = []
213243
for cross in np.array(vox_coords).T:
214-
ras_coords.append(apply_affine(mask_nii.affine, cross).tolist())
215-
244+
ras_coords.append(apply_affine(
245+
mask_nii.affine, cross).tolist())
216246
ras_cuts = [list(coords) for coords in np.transpose(ras_coords)]
217247
return {k: v for k, v in zip(['x', 'y', 'z'], ras_cuts)}
218248

0 commit comments

Comments
 (0)