Skip to content

Commit 32cd5b6

Browse files
Implement intensity and morphology measurement function
1 parent efd164a commit 32cd5b6

File tree

4 files changed

+188
-0
lines changed

4 files changed

+188
-0
lines changed

flamingo_tools/measurements.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import multiprocessing as mp
2+
from concurrent import futures
3+
from typing import Optional
4+
5+
import numpy as np
6+
import pandas as pd
7+
import trimesh
8+
from skimage.measure import marching_cubes
9+
from tqdm import tqdm
10+
11+
from .file_utils import read_image_data
12+
13+
14+
def _measure_volume_and_surface(mask, resolution):
15+
# Use marching_cubes for 3D data
16+
verts, faces, normals, _ = marching_cubes(mask, spacing=(resolution,) * 3)
17+
18+
mesh = trimesh.Trimesh(vertices=verts, faces=faces, vertex_normals=normals)
19+
surface = mesh.area
20+
if mesh.is_watertight:
21+
volume = np.abs(mesh.volume)
22+
else:
23+
volume = np.nan
24+
25+
return volume, surface
26+
27+
28+
# Could also support s3 directly?
29+
def compute_object_measures(
30+
image_path: str,
31+
segmentation_path: str,
32+
segmentation_table_path: str,
33+
output_table_path: str,
34+
image_key: Optional[str] = None,
35+
segmentation_key: Optional[str] = None,
36+
n_threads: Optional[int] = None,
37+
resolution: float = 0.38,
38+
):
39+
"""
40+
41+
Args:
42+
image_path:
43+
segmentation_path:
44+
segmentation_table_path:
45+
output_table_path:
46+
image_key:
47+
segmentation_key:
48+
n_threads:
49+
resolution:
50+
"""
51+
# First, we load the pre-computed segmentation table from MoBIE.
52+
table = pd.read_csv(segmentation_table_path, sep="\t")
53+
54+
# Then, open the volumes.
55+
image = read_image_data(image_path, image_key)
56+
segmentation = read_image_data(segmentation_path, segmentation_key)
57+
58+
def intensity_measures(seg_id):
59+
# Get the bounding box.
60+
row = table[table.label_id == seg_id]
61+
62+
bb_min = np.array([
63+
row.bb_min_z.item(), row.bb_min_y.item(), row.bb_min_x.item()
64+
]) / resolution
65+
bb_min = np.round(bb_min, 0).astype("uint32")
66+
67+
bb_max = np.array([
68+
row.bb_max_z.item(), row.bb_max_y.item(), row.bb_max_x.item()
69+
]) / resolution
70+
bb_max = np.round(bb_max, 0).astype("uint32")
71+
72+
bb = tuple(
73+
slice(max(bmin - 1, 0), min(bmax + 1, sh))
74+
for bmin, bmax, sh in zip(bb_min, bb_max, image.shape)
75+
)
76+
77+
local_image = image[bb]
78+
mask = segmentation[bb] == seg_id
79+
masked_intensity = local_image[mask]
80+
81+
# Do the base intensity measurements.
82+
measures = {
83+
"label_id": seg_id,
84+
"mean": np.mean(masked_intensity),
85+
"stdev": np.std(masked_intensity),
86+
"min": np.min(masked_intensity),
87+
"max": np.max(masked_intensity),
88+
"median": np.median(masked_intensity),
89+
}
90+
for percentile in (5, 10, 25, 75, 90, 95):
91+
measures[f"percentile-{percentile}"] = np.percentile(masked_intensity, percentile)
92+
93+
# Do the volume and surface measurement.
94+
volume, surface = _measure_volume_and_surface(mask, resolution)
95+
measures["volume"] = volume
96+
measures["surface"] = surface
97+
return measures
98+
99+
seg_ids = table.label_id.values
100+
n_threads = mp.cpu_count() if n_threads is None else n_threads
101+
with futures.ThreadPoolExecutor(n_threads) as pool:
102+
measures = list(tqdm(
103+
pool.map(intensity_measures, seg_ids),
104+
total=len(seg_ids), desc="Compute intensity measures"
105+
))
106+
107+
# Create the result table and save it.
108+
keys = measures[0].keys()
109+
measures = pd.DataFrame({k: [measure[k] for measure in measures] for k in keys})
110+
measures.to_csv(output_table_path, sep="\t", index=False)

flamingo_tools/segmentation/postprocessing.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,21 @@ def _compute_table(segmentation, resolution):
119119
coordinates = np.array([prop.centroid for prop in props])
120120
# transform pixel distance to physical units
121121
coordinates = coordinates * resolution
122+
bb_min = np.array([prop.bbox[:3] for prop in props]) * resolution
123+
bb_max = np.array([prop.bbox[3:] for prop in props]) * resolution
122124
sizes = np.array([prop.area for prop in props])
123125
table = pd.DataFrame({
124126
"label_id": label_ids,
125127
"n_pixels": sizes,
126128
"anchor_x": coordinates[:, 2],
127129
"anchor_y": coordinates[:, 1],
128130
"anchor_z": coordinates[:, 0],
131+
"bb_min_x": bb_min[:, 2],
132+
"bb_min_y": bb_min[:, 1],
133+
"bb_min_z": bb_min[:, 0],
134+
"bb_max_x": bb_max[:, 2],
135+
"bb_max_y": bb_max[:, 1],
136+
"bb_max_z": bb_max[:, 0],
129137
})
130138
return table
131139

flamingo_tools/test_data.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,35 @@
11
import os
2+
from typing import Tuple
23

34
import imageio.v3 as imageio
45
from skimage.data import binary_blobs
6+
from skimage.measure import label
7+
8+
from .segmentation.postprocessing import _compute_table
9+
10+
11+
def create_image_data_and_segmentation(
12+
folder: str, size: int = 256
13+
) -> Tuple[str, str, str]:
14+
"""Create test data containing an image, a corresponding segmentation and segmentation table.
15+
16+
Args:
17+
folder: The test data folder.
18+
"""
19+
os.makedirs(folder, exist_ok=True)
20+
data = binary_blobs(size, n_dim=3).astype("uint8") * 255
21+
seg = label(data)
22+
23+
image_path = os.path.join(folder, "image.tif")
24+
segmentation_path = os.path.join(folder, "segmentation.tif")
25+
imageio.imwrite(image_path, data)
26+
imageio.imwrite(segmentation_path, seg)
27+
28+
table_path = os.path.join(folder, "default.tsv")
29+
table = _compute_table(seg, resolution=0.38)
30+
table.to_csv(table_path, sep="\t", index=False)
31+
32+
return image_path, segmentation_path, table_path
533

634

735
# TODO add metadata

test/test_measurements.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import os
2+
import unittest
3+
from shutil import rmtree
4+
5+
import pandas as pd
6+
7+
8+
class TestDataConversion(unittest.TestCase):
9+
folder = "./tmp"
10+
11+
def setUp(self):
12+
from flamingo_tools.test_data import create_image_data_and_segmentation
13+
14+
self.image_path, self.seg_path, self.table_path =\
15+
create_image_data_and_segmentation(self.folder)
16+
17+
def tearDown(self):
18+
try:
19+
rmtree(self.folder)
20+
except Exception:
21+
pass
22+
23+
def test_compute_object_measures(self):
24+
from flamingo_tools.measurements import compute_object_measures
25+
26+
output_path = os.path.join(self.folder, "measurements.tsv")
27+
compute_object_measures(
28+
self.image_path, self.seg_path, self.table_path, output_path, n_threads=1
29+
)
30+
self.assertTrue(os.path.exists(output_path))
31+
32+
table = pd.read_csv(output_path, sep="\t")
33+
self.assertTrue(len(table) >= 1)
34+
expected_columns = ["label_id", "mean", "stdev", "min", "max", "median"]
35+
expected_columns.extend([f"percentile-{p}" for p in (5, 10, 25, 75, 90, 95)])
36+
expected_columns.extend(["volume", "surface"])
37+
for col in expected_columns:
38+
self.assertIn(col, table.columns)
39+
40+
41+
if __name__ == "__main__":
42+
unittest.main()

0 commit comments

Comments
 (0)