Skip to content

Commit 39a472f

Browse files
committed
Added cell stat + example notebook
1 parent 33d9e95 commit 39a472f

File tree

13 files changed

+4327
-15
lines changed

13 files changed

+4327
-15
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ __pycache__/
77
*.so
88

99
# unwanted results files
10-
*.csv
10+
*.tif
11+
napari_cellseg3d/_tests/res/*.csv
1112

1213
# Distribution / packaging
1314
.Python

docs/res/code/utils.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ dice_coeff
3333
**************************************
3434
.. autofunction:: napari_cellseg3d.utils::dice_coeff
3535

36+
sphericity_volume_area
37+
**************************************
38+
.. autofunction:: napari_cellseg3d.utils::sphericity_volume_area
39+
40+
sphericity_axis
41+
**************************************
42+
.. autofunction:: napari_cellseg3d.utils::sphericity_axis
43+
3644
normalize_x
3745
**************************************
3846
.. autofunction:: napari_cellseg3d.utils::normalize_x

docs/res/guides/inference_module_guide.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ Interface and functionalities
4141
| You can then choose one of the provided **models** above, which will be used for inference.
4242
| You may also choose to **load custom weights** rather than the pre-trained ones, simply ensure they are **compatible** (e.g. produced from the training module for the same model)
4343
44+
.. note::
45+
Currently the SegResNet model requires you to provide the size of the images the model was trained with due to the VAE module.
46+
Provided weights use a size of 128, please leave it as is if you're not using custom weights.
4447

4548
* **Inference parameters** :
4649

napari_cellseg3d/model_framework.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ def display_status_report(self):
230230
self.progress.setValue(0)
231231

232232
def toggle_weights_path(self):
233+
"""Toggle visibility of weight path"""
233234
self.toggle_visibility(
234235
self.custom_weights_choice, self.weights_path_container
235236
)

napari_cellseg3d/model_instance_seg.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@
33

44
import numpy as np
55
from skimage.measure import label
6+
from skimage.measure import marching_cubes
7+
from skimage.measure import mesh_surface_area
8+
from skimage.measure import regionprops
69
from skimage.morphology import remove_small_objects
710
from skimage.segmentation import watershed
811
from skimage.transform import resize
912
from tifffile import imread
1013

14+
from napari_cellseg3d.utils import fill_list_in_between
15+
from napari_cellseg3d.utils import sphericity_axis
16+
from napari_cellseg3d.utils import sphericity_volume_area
17+
1118

1219
def binary_connected(
1320
volume, thres=0.5, thres_small=3, scale_factors=(1.0, 1.0, 1.0)
@@ -162,3 +169,43 @@ def to_semantic(image, is_file_path=False):
162169
image[image >= 1] = 1
163170
result = image.astype(np.uint16)
164171
return result
172+
173+
174+
def volume_stats(volume_image):
175+
176+
properties = regionprops(volume_image)
177+
number_objects = np.amax(volume_image)
178+
179+
sphericity_va = []
180+
sphericity_ax = [
181+
sphericity_axis(
182+
region.axis_major_length * 0.5, region.axis_minor_length * 0.5
183+
)
184+
for region in properties
185+
]
186+
# for region in properties:
187+
# object = (volume_image == region.label).transpose(1, 2, 0)
188+
# verts, faces, _, values = marching_cubes(
189+
# object, level=0, spacing=(1.0, 1.0, 1.0)
190+
# )
191+
# surface_area_pixels = mesh_surface_area(verts, faces)
192+
# sphericity_va.append(
193+
# sphericity_volume_area(region.area, surface_area_pixels)
194+
# )
195+
196+
volume = [region.area for region in properties]
197+
198+
def fill(lst, n=len(properties) - 1):
199+
return fill_list_in_between(lst, n, "")
200+
201+
return {
202+
"Volume": volume,
203+
"Centroid": [region.centroid for region in properties],
204+
# "Sphericity (volume/area)": sphericity_va,
205+
"Sphericity (axes)": sphericity_ax,
206+
"Image size": fill([volume_image.shape]),
207+
"Total image volume": fill([len(volume_image.flatten())]),
208+
"Total object volume (pixels)": fill([np.sum(volume)]),
209+
"Filling ratio": fill([np.sum(volume) / len(volume_image.flatten())]),
210+
"Number objects": fill([len(properties)]),
211+
}

napari_cellseg3d/model_workers.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,7 @@ def log_parameters(self):
181181
self.log(f"Dataset loaded on {self.device}")
182182

183183
if self.transforms["zoom"][0]:
184-
self.log(
185-
f"Anisotropy parameters are : {self.transforms['zoom'][1]} microns in x,y,z"
186-
)
184+
self.log(f"Scaling factor : {self.transforms['zoom'][1]} (x,y,z)")
187185

188186
if self.instance_params["do_instance"]:
189187
self.log(
@@ -436,7 +434,7 @@ def method(image):
436434
instance_filepath = (
437435
self.results_path
438436
+ "/"
439-
+ f"Instance_seg_labels_{image_id}"
437+
+ f"Instance_seg_labels_{image_id}_"
440438
+ original_filename
441439
+ "_"
442440
+ self.model_dict["name"]

napari_cellseg3d/plugin_helper.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(self, viewer: "napari.viewer.Viewer"):
2828
self.btn2 = ui.make_button(
2929
"About...", lambda: ui.open_url(self.about_url)
3030
)
31+
3132
self.btnc = ui.make_button("Close", self.remove_from_viewer)
3233

3334
self.build()

napari_cellseg3d/plugin_model_inference.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import napari
55
import numpy as np
6+
import pandas as pd
67

78
# Qt
89
from qtpy.QtWidgets import QSizePolicy
@@ -11,6 +12,7 @@
1112
from napari_cellseg3d import interface as ui
1213
from napari_cellseg3d import utils
1314
from napari_cellseg3d.model_framework import ModelFramework
15+
from napari_cellseg3d.model_instance_seg import volume_stats
1416
from napari_cellseg3d.model_workers import InferenceWorker
1517

1618

@@ -70,6 +72,7 @@ def __init__(self, viewer: "napari.viewer.Viewer"):
7072
self.zoom = [1, 1, 1]
7173

7274
self.instance_params = None
75+
self.stats_to_csv = False
7376

7477
self.keep_on_cpu = False
7578
self.use_window_inference = False
@@ -211,6 +214,9 @@ def __init__(self, viewer: "napari.viewer.Viewer"):
211214
left_or_above=self.instance_small_object_thresh_lbl,
212215
horizontal=False,
213216
)
217+
self.save_stats_to_csv_box = ui.make_checkbox(
218+
"Save stats to csv", parent=self
219+
)
214220

215221
(
216222
self.instance_param_container,
@@ -315,6 +321,7 @@ def build(self):
315321
self.instance_method_choice,
316322
self.instance_prob_t_container,
317323
self.instance_small_object_t_container,
324+
self.save_stats_to_csv_box,
318325
],
319326
)
320327

@@ -557,6 +564,7 @@ def start(self): # TODO update
557564
"threshold": self.instance_prob_thresh.value(),
558565
"size_small": self.instance_small_object_thresh.value(),
559566
}
567+
self.stats_to_csv = self.save_stats_to_csv_box.isChecked()
560568
# print(f"METHOD : {self.instance_method_choice.currentText()}")
561569

562570
self.show_res_nbr = self.display_number_choice.value()
@@ -687,15 +695,35 @@ def on_yield(data, widget):
687695

688696
if data["instance_labels"] is not None:
689697

690-
number_cells = np.amax(data["instance_labels"])
691-
692-
widget.log.print_and_log(
693-
f"\nNUMBER OF CELLS : {number_cells}\n"
694-
)
695-
698+
labels = data["instance_labels"]
696699
method = widget.instance_params["method"]
700+
number_cells = np.amax(labels)
701+
697702
name = f"({number_cells})_{method}_instance_labels_{image_id}"
698703

699-
instance_layer = viewer.add_labels(
700-
data[f"instance_labels"], name=name
701-
)
704+
instance_layer = viewer.add_labels(labels, name=name)
705+
706+
if widget.stats_to_csv: # TODO move to worker
707+
708+
cell_data = volume_stats(
709+
labels
710+
) # TODO test with area mesh function
711+
# count = np.tile("", len(cell_data["Volume"])-1)
712+
# cell_data["Cell count"] = np.insert(count, 0, number_cells)
713+
# cell_data["Total cell volume"] = np.insert(
714+
# count, 0, tot_cell_volume
715+
# )
716+
# cell_data["Cell volume ratio"] = np.insert(
717+
# count, 0, tot_cell_volume / len(labels.flatten())
718+
# )
719+
720+
numeric_data = pd.DataFrame(cell_data)
721+
722+
csv_name = f"/{method}_seg_results_{image_id}_{utils.get_date_time()}.csv"
723+
numeric_data.to_csv(
724+
widget.results_path + csv_name, index=False
725+
)
726+
727+
# widget.log.print_and_log(
728+
# f"\nNUMBER OF CELLS : {number_cells}\n"
729+
# )

napari_cellseg3d/plugin_model_training.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ def make_csv(self):
985985
}
986986
)
987987
path = os.path.join(self.results_path_folder, "training.csv")
988-
self.df.to_csv(path)
988+
self.df.to_csv(path, index=False)
989989

990990
def plot_loss(self, loss, dice_metric):
991991
"""Creates two subplots to plot the training loss and validation metric"""

napari_cellseg3d/utils.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,35 @@ def normalize_y(image):
6060
return image
6161

6262

63+
def sphericity_volume_area(volume, surface_area):
64+
"""Computes the sphericity from volume and area
65+
66+
.. math::
67+
sphericity =\\frac {\\pi^\\frac{1}{3} (6 V_{p})^\\frac{2}{3}} {A_p}
68+
69+
"""
70+
return np.pi ** (1 / 3) * (6 * volume) ** (2 / 3) / surface_area
71+
72+
73+
def sphericity_axis(semi_major, semi_minor):
74+
"""Computes the sphericity from volume semi major (a) and semi minor (b) axes.
75+
76+
.. math::
77+
sphericity = \\frac {2 \\sqrt[3]{ab^2}} {a+ \\frac {b^2} {\\sqrt{a^2-b^2}}ln( \\frac {a+ \\sqrt{a^2-b^2}} {b} )}
78+
79+
"""
80+
a = semi_major
81+
b = semi_minor
82+
83+
root = (a**2 - b**2) ** (1 / 2)
84+
85+
return (
86+
2
87+
* (a * (b**2)) ** (1 / 3)
88+
/ (a + (b**2) / root * np.log((a + root) / b))
89+
)
90+
91+
6392
def dice_coeff(y_true, y_pred):
6493
"""Compute Dice-Sorensen coefficient between two numpy arrays
6594

0 commit comments

Comments
 (0)