From a901a5a6de775d9cafe259cc1047e45930c2ab0b Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Wed, 1 Oct 2025 16:57:49 +0200 Subject: [PATCH 1/9] chore: Scripts for segmentation dilation and max projection --- examples/dilate_segmentation_mask.py | 74 ++++++++++++++++++++++++++++ examples/max_projection.py | 32 ++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 examples/dilate_segmentation_mask.py create mode 100644 examples/max_projection.py diff --git a/examples/dilate_segmentation_mask.py b/examples/dilate_segmentation_mask.py new file mode 100644 index 000000000..af87c7a8c --- /dev/null +++ b/examples/dilate_segmentation_mask.py @@ -0,0 +1,74 @@ +"""Example script to dilate segmentation masks in image files.""" + +from argparse import ArgumentParser +from glob import glob +from itertools import chain +from pathlib import Path + +import numpy as np + +from PartSegCore.image_operations import dilate, to_binary_image +from PartSegCore.mask.io_functions import LoadROIImage, MaskProjectTuple, SaveROI, SaveROIOptions +from PartSegCore.roi_info import ROIInfo +from PartSegCore.segmentation.watershed import calculate_distances_array, get_neigh +from PartSegCore_compiled_backend.sprawl_utils.find_split import euclidean_sprawl + + +def convert_mask(file_path: Path, radius: float, suffix: str): + print(f"Converting {file_path} to {suffix} with radius {radius}") + if radius <= 0: + return + project = LoadROIImage.load([str(file_path)]) + + roi_ = project.roi_info.roi.squeeze() + + bin_roi = to_binary_image(roi_) + sprawl_area = dilate(bin_roi, [radius, radius], True) + components_num = np.max(project.roi_info.roi) + neigh, dist = calculate_distances_array(project.image.spacing, get_neigh(True)) + roi = project.image.fit_array_to_image( + euclidean_sprawl( + sprawl_area, + roi_, + components_num, + neigh, + dist, + ) + ) + new_file_path = file_path.with_name(file_path.stem + suffix + file_path.suffix) + print("Saving to ", new_file_path) + SaveROI.save( + str(new_file_path), + MaskProjectTuple( + file_path=str(new_file_path), + image=project.image, + roi_info=ROIInfo(roi), + spacing=project.spacing, + frame_thickness=project.frame_thickness, + ), + SaveROIOptions( + relative_path=True, + mask_data=True, + frame_thickness=project.frame_thickness, + spacing=project.spacing, + ), + ) + + +def main(): + parser = ArgumentParser() + parser.add_argument("project_files", nargs="+", type=str) + parser.add_argument("--dilate", type=int, default=1) + parser.add_argument("--suffix", type=str, default="_dilated") + + args = parser.parse_args() + + files = list(chain.from_iterable(glob(x) for x in args.project_files)) + print(f"{files=} {args.project_files=}") + + for file_path in files: + convert_mask(Path(file_path).absolute(), args.dilate, args.suffix) + + +if __name__ == "__main__": + main() diff --git a/examples/max_projection.py b/examples/max_projection.py new file mode 100644 index 000000000..8d257ea45 --- /dev/null +++ b/examples/max_projection.py @@ -0,0 +1,32 @@ +""" +For collections of tiff files save a max projection of each file. +""" + +from argparse import ArgumentParser +from glob import glob +from itertools import chain +from pathlib import Path + +from PartSegImage import Image, ImageWriter, TiffImageReader + + +def max_projection(file_path: Path, suffix: str = "_max"): + image = TiffImageReader.read_image(str(file_path)) + max_proj = image.get_data().max(axis=image.axis_order.index("Z")) + image2 = Image(max_proj, image.spacing[1:], axes_order=image.axis_order.replace("Z", "")) + ImageWriter.save(image2, str(file_path.with_name(file_path.stem + suffix + file_path.suffix))) + + +def main(): + parser = ArgumentParser() + parser.add_argument("image_files", nargs="+", type=str) + parser.add_argument("--suffix", type=str, default="_max") + args = parser.parse_args() + files = list(chain.from_iterable(glob(f) for f in args.image_files)) + for file_path in files: + print(f"Processing {file_path}") + max_projection(Path(file_path), args.suffix) + + +if __name__ == "__main__": + main() From a38c793a1c5b52ffd34467d073093c4c875d2821 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Wed, 1 Oct 2025 17:43:00 +0200 Subject: [PATCH 2/9] improve error messaging --- examples/dilate_segmentation_mask.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/dilate_segmentation_mask.py b/examples/dilate_segmentation_mask.py index af87c7a8c..cc8d09de0 100644 --- a/examples/dilate_segmentation_mask.py +++ b/examples/dilate_segmentation_mask.py @@ -1,5 +1,6 @@ """Example script to dilate segmentation masks in image files.""" +import sys from argparse import ArgumentParser from glob import glob from itertools import chain @@ -15,9 +16,10 @@ def convert_mask(file_path: Path, radius: float, suffix: str): - print(f"Converting {file_path} to {suffix} with radius {radius}") if radius <= 0: - return + raise ValueError("Radius must be positive") + print(f"Converting {file_path} to {suffix} with radius {radius}") + project = LoadROIImage.load([str(file_path)]) roi_ = project.roi_info.roi.squeeze() @@ -58,17 +60,20 @@ def convert_mask(file_path: Path, radius: float, suffix: str): def main(): parser = ArgumentParser() parser.add_argument("project_files", nargs="+", type=str) - parser.add_argument("--dilate", type=int, default=1) + parser.add_argument("--dilate", type=float, default=1.0) parser.add_argument("--suffix", type=str, default="_dilated") args = parser.parse_args() files = list(chain.from_iterable(glob(x) for x in args.project_files)) - print(f"{files=} {args.project_files=}") + if not files: + print("No files found") + return -1 for file_path in files: convert_mask(Path(file_path).absolute(), args.dilate, args.suffix) + return 0 if __name__ == "__main__": - main() + sys.exit(main()) From 1a4c733dcdc6e1243d2c983305112a16ed10ff90 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Wed, 1 Oct 2025 17:44:42 +0200 Subject: [PATCH 3/9] check if "Z" is in image axis --- examples/max_projection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/max_projection.py b/examples/max_projection.py index 8d257ea45..356b56324 100644 --- a/examples/max_projection.py +++ b/examples/max_projection.py @@ -12,6 +12,8 @@ def max_projection(file_path: Path, suffix: str = "_max"): image = TiffImageReader.read_image(str(file_path)) + if "Z" not in image.axis_order: + raise ValueError(f"Image {file_path} does not have Z axis") max_proj = image.get_data().max(axis=image.axis_order.index("Z")) image2 = Image(max_proj, image.spacing[1:], axes_order=image.axis_order.replace("Z", "")) ImageWriter.save(image2, str(file_path.with_name(file_path.stem + suffix + file_path.suffix))) From 5c0eef25df4c68f9b6fd8498c408851ffb7fc696 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Wed, 15 Oct 2025 12:56:01 +0200 Subject: [PATCH 4/9] Add only selected option --- examples/dilate_segmentation_mask.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/examples/dilate_segmentation_mask.py b/examples/dilate_segmentation_mask.py index cc8d09de0..e784811c5 100644 --- a/examples/dilate_segmentation_mask.py +++ b/examples/dilate_segmentation_mask.py @@ -15,7 +15,7 @@ from PartSegCore_compiled_backend.sprawl_utils.find_split import euclidean_sprawl -def convert_mask(file_path: Path, radius: float, suffix: str): +def convert_mask(file_path: Path, radius: float, suffix: str, only_selected: bool): if radius <= 0: raise ValueError("Radius must be positive") print(f"Converting {file_path} to {suffix} with radius {radius}") @@ -23,6 +23,18 @@ def convert_mask(file_path: Path, radius: float, suffix: str): project = LoadROIImage.load([str(file_path)]) roi_ = project.roi_info.roi.squeeze() + selected_components = project.selected_components + if only_selected and selected_components is not None: + mask = np.isin(roi_, selected_components) + roi_ = roi_ * mask + + unique_values = np.unique(roi_, sorted=True) + mapping = np.zeros(unique_values[-1] + 1, dtype=roi_.dtype) + for new_val, old_val in enumerate(unique_values): + mapping[old_val] = new_val + roi_ = mapping[roi_] + + selected_components = list(range(1, len(unique_values))) bin_roi = to_binary_image(roi_) sprawl_area = dilate(bin_roi, [radius, radius], True) @@ -47,6 +59,7 @@ def convert_mask(file_path: Path, radius: float, suffix: str): roi_info=ROIInfo(roi), spacing=project.spacing, frame_thickness=project.frame_thickness, + selected_components=selected_components, ), SaveROIOptions( relative_path=True, @@ -60,8 +73,9 @@ def convert_mask(file_path: Path, radius: float, suffix: str): def main(): parser = ArgumentParser() parser.add_argument("project_files", nargs="+", type=str) - parser.add_argument("--dilate", type=float, default=1.0) + parser.add_argument("--dilate", type=int, default=1) parser.add_argument("--suffix", type=str, default="_dilated") + parser.add_argument("--only-selected", action="store_true") args = parser.parse_args() @@ -71,7 +85,7 @@ def main(): return -1 for file_path in files: - convert_mask(Path(file_path).absolute(), args.dilate, args.suffix) + convert_mask(Path(file_path).absolute(), args.dilate, args.suffix, args.only_selected) return 0 From 213331ba3fe70d945831f488bf7e966d21f9d925 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Wed, 15 Oct 2025 13:23:56 +0200 Subject: [PATCH 5/9] do not use `unique(..., sorted=True)` as it require numpy 2.3 --- examples/dilate_segmentation_mask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dilate_segmentation_mask.py b/examples/dilate_segmentation_mask.py index e784811c5..1e243aa6a 100644 --- a/examples/dilate_segmentation_mask.py +++ b/examples/dilate_segmentation_mask.py @@ -28,7 +28,7 @@ def convert_mask(file_path: Path, radius: float, suffix: str, only_selected: boo mask = np.isin(roi_, selected_components) roi_ = roi_ * mask - unique_values = np.unique(roi_, sorted=True) + unique_values = np.unique(roi_) mapping = np.zeros(unique_values[-1] + 1, dtype=roi_.dtype) for new_val, old_val in enumerate(unique_values): mapping[old_val] = new_val From 50be8f46035f078608f8fe26c85b03323e4cbdeb Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Wed, 15 Oct 2025 13:28:44 +0200 Subject: [PATCH 6/9] fix components num calculation --- examples/dilate_segmentation_mask.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/dilate_segmentation_mask.py b/examples/dilate_segmentation_mask.py index 1e243aa6a..c2e940d7c 100644 --- a/examples/dilate_segmentation_mask.py +++ b/examples/dilate_segmentation_mask.py @@ -29,7 +29,7 @@ def convert_mask(file_path: Path, radius: float, suffix: str, only_selected: boo roi_ = roi_ * mask unique_values = np.unique(roi_) - mapping = np.zeros(unique_values[-1] + 1, dtype=roi_.dtype) + mapping = np.zeros(np.max(unique_values) + 1, dtype=roi_.dtype) for new_val, old_val in enumerate(unique_values): mapping[old_val] = new_val roi_ = mapping[roi_] @@ -38,7 +38,7 @@ def convert_mask(file_path: Path, radius: float, suffix: str, only_selected: boo bin_roi = to_binary_image(roi_) sprawl_area = dilate(bin_roi, [radius, radius], True) - components_num = np.max(project.roi_info.roi) + components_num = np.max(roi_) neigh, dist = calculate_distances_array(project.image.spacing, get_neigh(True)) roi = project.image.fit_array_to_image( euclidean_sprawl( From b6fb457cec8137b52eacf037514bfa3fb093410a Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Wed, 19 Nov 2025 15:04:05 +0100 Subject: [PATCH 7/9] add ability to do max projection with mask --- examples/max_projection.py | 23 +++++++++++++++++++---- package/PartSegImage/image_reader.py | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/examples/max_projection.py b/examples/max_projection.py index 356b56324..505b52481 100644 --- a/examples/max_projection.py +++ b/examples/max_projection.py @@ -10,24 +10,39 @@ from PartSegImage import Image, ImageWriter, TiffImageReader -def max_projection(file_path: Path, suffix: str = "_max"): - image = TiffImageReader.read_image(str(file_path)) +def max_projection(file_path: Path, suffix: str = "_max", with_mask: bool = False): + if with_mask: + mask_path = str(file_path.parent / (file_path.stem + "_mask" + file_path.suffix)) + else: + mask_path = None + image = TiffImageReader.read_image(str(file_path), mask_path) if "Z" not in image.axis_order: raise ValueError(f"Image {file_path} does not have Z axis") max_proj = image.get_data().max(axis=image.axis_order.index("Z")) - image2 = Image(max_proj, image.spacing[1:], axes_order=image.axis_order.replace("Z", "")) + if with_mask: + mask_projection = image.mask.max(axis=image.array_axis_order.index("Z")) + else: + mask_projection = None + image2 = Image( + max_proj, spacing=image.spacing[1:], axes_order=image.axis_order.replace("Z", ""), mask=mask_projection + ) ImageWriter.save(image2, str(file_path.with_name(file_path.stem + suffix + file_path.suffix))) + if with_mask: + ImageWriter.save_mask(image2, str(file_path.with_name(file_path.stem + suffix + "_mask" + file_path.suffix))) def main(): parser = ArgumentParser() parser.add_argument("image_files", nargs="+", type=str) parser.add_argument("--suffix", type=str, default="_max") + parser.add_argument("--with-mask", action="store_true") args = parser.parse_args() files = list(chain.from_iterable(glob(f) for f in args.image_files)) for file_path in files: + if args.with_mask and Path(file_path).stem.endswith("_mask"): + continue print(f"Processing {file_path}") - max_projection(Path(file_path), args.suffix) + max_projection(Path(file_path), args.suffix, args.with_mask) if __name__ == "__main__": diff --git a/package/PartSegImage/image_reader.py b/package/PartSegImage/image_reader.py index 3aeaaf128..e1c234c0b 100644 --- a/package/PartSegImage/image_reader.py +++ b/package/PartSegImage/image_reader.py @@ -275,7 +275,7 @@ def read_image( read image file with optional mask file :param image_path: path or opened file contains image - :param mask_path: + :param mask_path: path or opened file contains mask :param callback_function: function for provide information about progress in reading file (for progressbar) :param default_spacing: used if file do not contains information about spacing (or metadata format is not supported) From 7c53af3f80d63b65fb38d35f632bada9ae9c8b43 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Wed, 19 Nov 2025 15:16:10 +0100 Subject: [PATCH 8/9] add script for fast save components --- examples/extract_components_from_project.py | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 examples/extract_components_from_project.py diff --git a/examples/extract_components_from_project.py b/examples/extract_components_from_project.py new file mode 100644 index 000000000..5ac79d6ea --- /dev/null +++ b/examples/extract_components_from_project.py @@ -0,0 +1,32 @@ +from argparse import ArgumentParser +from glob import glob +from itertools import chain +from pathlib import Path + +from PartSegCore.mask.io_functions import LoadROIImage, SaveComponents, SaveComponentsOptions + + +def cut_components(project_file: Path): + project = LoadROIImage.load([str(project_file)]) + SaveComponents.save( + str(project_file.parent / (project_file.stem + "_components")), + project, + SaveComponentsOptions( + frame=0, + mask_data=True, + ), + ) + + +def main(): + parser = ArgumentParser() + parser.add_argument("project_files", nargs="+", type=str) + args = parser.parse_args() + files = list(chain.from_iterable(glob(f) for f in args.project_files)) + for file_path in files: + print(f"Processing {file_path}") + cut_components(Path(file_path), args.no_mask) + + +if __name__ == "__main__": + main() From 8182ac51a67b70c9f4e07b00ea86481e8e47bc2f Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Wed, 19 Nov 2025 23:25:16 +0100 Subject: [PATCH 9/9] fix --- examples/extract_components_from_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/extract_components_from_project.py b/examples/extract_components_from_project.py index 5ac79d6ea..bb177289a 100644 --- a/examples/extract_components_from_project.py +++ b/examples/extract_components_from_project.py @@ -25,7 +25,7 @@ def main(): files = list(chain.from_iterable(glob(f) for f in args.project_files)) for file_path in files: print(f"Processing {file_path}") - cut_components(Path(file_path), args.no_mask) + cut_components(Path(file_path)) if __name__ == "__main__":