Quantitative Analysis¶
Description¶
This notebook extracts quantitative measures from the final label maps:
- Volume (voxel count) per labeled region
- Center of Mass (CoM) in voxel coordinates
- Equivalent diameter assuming a perfect sphere
Outputs¶
- Tabulated metrics per label (volume, CoM, equivalent diameter)
- Optional CSV export for downstream analysis
Part of Pipeline¶
- Previous Step: Watershed Segmentation
- Next Step: N/A
In [1]:
Copied!
# Import required libraries
import numpy as np
# Integrate figures into notebook
%matplotlib notebook
%matplotlib inline
# Import required libraries
import numpy as np
# Integrate figures into notebook
%matplotlib notebook
%matplotlib inline
Load the labeled, i.e. segmented, image from the previous step¶
In [2]:
Copied!
# Load binary image
labeled_image = np.load(file='../src/data/labeled_image.npy')
# Load binary image
labeled_image = np.load(file='../src/data/labeled_image.npy')
Step 1 — Collect unique labels¶
We collect unique label IDs from the 3D label maps and exclude background (0).
In [3]:
Copied!
# Custom
labels = np.unique(labeled_image)
print("Labels:", labels.tolist())
# Custom
labels = np.unique(labeled_image)
print("Labels:", labels.tolist())
Labels: [0, 1, 2]
Step 2 — Index grids¶
We create index arrays (z_idx, y_idx, x_idx) matching the label map shape so we can compute CoM by averaging index positions where each label occurs
In [4]:
Copied!
# Index grids (z, y, x) for the custom map
z_idx_c, y_idx_c, x_idx_c = np.indices(labeled_image.shape)
# Index grids for the library map
z_idx_l, y_idx_l, x_idx_l = np.indices(labeled_image.shape)
print("Shape check (custom):", labeled_image.shape)
print("Shape check (library):", labeled_image.shape)
# Index grids (z, y, x) for the custom map
z_idx_c, y_idx_c, x_idx_c = np.indices(labeled_image.shape)
# Index grids for the library map
z_idx_l, y_idx_l, x_idx_l = np.indices(labeled_image.shape)
print("Shape check (custom):", labeled_image.shape)
print("Shape check (library):", labeled_image.shape)
Shape check (custom): (16, 16, 16) Shape check (library): (16, 16, 16)
Step 3 — Volume and CoM for custom segmentation¶
For each label:
mask = (labels_3d == lab)voxel_count = mask.sum()com_z = z_idx[mask].sum() / voxel_count, likewise forcom_y,com_x.
In [5]:
Copied!
# Prepare result arrays
volume = np.zeros(labels.shape, dtype=int)
CoM = np.zeros((labels.size, 3), dtype=float) # (z, y, x)
# Loop over labels (custom)
for i, lab in enumerate(labels):
mask = (labeled_image == lab)
count = int(mask.sum())
volume[i] = count
if count > 0:
com_z = z_idx_c[mask].sum() / count
com_y = y_idx_c[mask].sum() / count
com_x = x_idx_c[mask].sum() / count
CoM[i] = (com_z, com_y, com_x)
else:
CoM[i] = (np.nan, np.nan, np.nan)
print("Custom — volumes:", volume.tolist())
print("Custom — CoM (z,y,x) sample:", CoM[:min(3, len(CoM))])
# Prepare result arrays
volume = np.zeros(labels.shape, dtype=int)
CoM = np.zeros((labels.size, 3), dtype=float) # (z, y, x)
# Loop over labels (custom)
for i, lab in enumerate(labels):
mask = (labeled_image == lab)
count = int(mask.sum())
volume[i] = count
if count > 0:
com_z = z_idx_c[mask].sum() / count
com_y = y_idx_c[mask].sum() / count
com_x = x_idx_c[mask].sum() / count
CoM[i] = (com_z, com_y, com_x)
else:
CoM[i] = (np.nan, np.nan, np.nan)
print("Custom — volumes:", volume.tolist())
print("Custom — CoM (z,y,x) sample:", CoM[:min(3, len(CoM))])
Custom — volumes: [3725, 253, 118] Custom — CoM (z,y,x) sample: [[7.51758389 7.62255034 7.72214765] [6.99209486 4.96047431 3.96047431] [8.03389831 9.07627119 8.07627119]]
Step 5 — Equivalent diameter¶
We derive the equivalent diameter assuming a sphere with the same volume: $d_\text{eq} = \left(\frac{6\,V}{\pi}\right)^{1/3}$ (Units: voxels)
In [6]:
Copied!
equiv_diameter = ((6.0 * volume / np.pi) ** (1.0 / 3.0))
equiv_diameter = ((6.0 * volume / np.pi) ** (1.0 / 3.0))
Step 6 — Tables¶
We print label, volume, CoM (z, y, x) (rounded for display), and eq. diameter.
In [7]:
Copied!
print("Label | Volume (voxels) | COM (z, y, x) | Eq. Diameter (voxels)")
print("----------------------------------------------------------------------")
for i, lab in enumerate(labels):
z, y, x = CoM[i]
com_str = f"({z:.0f}, {y:.0f}, {x:.0f})"
print(f"{lab:5d} | {volume[i]:15d} | {com_str:25s} | {equiv_diameter[i]:.2f}")
print("Label | Volume (voxels) | COM (z, y, x) | Eq. Diameter (voxels)")
print("----------------------------------------------------------------------")
for i, lab in enumerate(labels):
z, y, x = CoM[i]
com_str = f"({z:.0f}, {y:.0f}, {x:.0f})"
print(f"{lab:5d} | {volume[i]:15d} | {com_str:25s} | {equiv_diameter[i]:.2f}")
Label | Volume (voxels) | COM (z, y, x) | Eq. Diameter (voxels)
----------------------------------------------------------------------
0 | 3725 | (8, 8, 8) | 19.23
1 | 253 | (7, 5, 4) | 7.85
2 | 118 | (8, 9, 8) | 6.09
Exercise 1: Label Count Consistency
- Task: Compare the number of labels (excluding background) to the number of spheres you generated in Step 01.
- Questions: If counts differ, why? (e.g., overlapping spheres, merged watershed basins, missing seeds)
Exercise 2: Sphere Sanity: Diameter vs Known Radius
- Task: For spherical segments, check whether
Eq. Diameter ≈ 2 × known radius(voxel units). - Questions: Where do you see deviations? Consider discretization, partial overlap, boundary leakage.