From d318f040067b2b38700e0ca7684eb94cc61fc5d0 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Tue, 8 Jul 2025 13:52:37 +0200 Subject: [PATCH 1/2] migrate pre-commit to ruff --- .pre-commit-config.yaml | 66 ++++++++++++----------------- src/PartSeg_smfish/copy_labels.py | 4 +- src/PartSeg_smfish/segmentation.py | 61 ++++++-------------------- src/PartSeg_smfish/verify_points.py | 13 ++---- 4 files changed, 45 insertions(+), 99 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e925735..402dbcb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,41 +1,27 @@ repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 - hooks: - - id: check-docstring-first - - id: end-of-file-fixer - - id: trailing-whitespace - exclude: ^.napari-hub/* - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 - hooks: - - id: pyupgrade - args: [--py38-plus, --keep-runtime-typing] - - repo: https://github.com/myint/autoflake - rev: v2.3.1 - hooks: - - id: autoflake - args: ["--in-place", "--remove-all-unused-imports"] - - repo: https://github.com/psf/black - rev: 24.8.0 - hooks: - - id: black - - repo: https://github.com/PyCQA/flake8 - rev: 7.1.1 - hooks: - - id: flake8 - additional_dependencies: [flake8-typing-imports>=1.9.0] - - repo: https://github.com/tlambert03/napari-plugin-checks - rev: v0.3.0 - hooks: - - id: napari-plugin-checks - # https://mypy.readthedocs.io/en/stable/introduction.html - # you may wish to add this as well! - # - repo: https://github.com/pre-commit/mirrors-mypy - # rev: v0.910-1 - # hooks: - # - id: mypy +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.1 + hooks: + - id: ruff-format + exclude: examples + - id: ruff + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + # .py files are skipped cause already checked by other hooks + hooks: + - id: check-yaml + - id: check-toml + - id: check-merge-conflict + exclude: .*\.py + - id: end-of-file-fixer + exclude: .*\.py + - id: trailing-whitespace + # trailing whitespace has meaning in markdown https://www.markdownguide.org/hacks/#indent-tab + exclude: .*\.py|.*\.md + - id: mixed-line-ending + exclude: .*\.py +- repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.33.1 + hooks: + - id: check-github-workflows diff --git a/src/PartSeg_smfish/copy_labels.py b/src/PartSeg_smfish/copy_labels.py index 6508ba8..51e95e5 100644 --- a/src/PartSeg_smfish/copy_labels.py +++ b/src/PartSeg_smfish/copy_labels.py @@ -43,9 +43,7 @@ def __init__(self, napari_viewer: Viewer): self.setLayout(layout) - self.viewer.layers.selection.events.active.connect( - self.activate_widget - ) + self.viewer.layers.selection.events.active.connect(self.activate_widget) self.copy_btn.clicked.connect(self.copy_action) self.check_all_btn.clicked.connect(self._check_all) self.uncheck_all_btn.clicked.connect(self._uncheck_all) diff --git a/src/PartSeg_smfish/segmentation.py b/src/PartSeg_smfish/segmentation.py index 3e26fcd..ee8fe08 100644 --- a/src/PartSeg_smfish/segmentation.py +++ b/src/PartSeg_smfish/segmentation.py @@ -57,9 +57,7 @@ class GaussBackgroundEstimateParameters(BaseModel): foreground_estimate_radius: float = Field( 2.5, description="Radius of foreground gauss filter", ge=0, le=20 ) - estimate_mask: bool = Field( - True, description="Estimate background outside mask" - ) + estimate_mask: bool = Field(True, description="Estimate background outside mask") class GaussBackgroundEstimate(SpotDetect): @@ -104,13 +102,10 @@ class LaplacianBackgroundEstimateParameters(BaseModel): ge=0, le=20, ) - estimate_mask: bool = Field( - True, description="Estimate background outside mask" - ) + estimate_mask: bool = Field(True, description="Estimate background outside mask") class LaplacianBackgroundEstimate(SpotDetect): - __argument_class__ = LaplacianBackgroundEstimateParameters @classmethod @@ -125,9 +120,7 @@ def spot_estimate( if not parameters.estimate_mask: return _laplacian_estimate(array, parameters.laplacian_radius) mask = mask if mask is not None else array > 0 - return _laplacian_estimate_mask( - array, mask, parameters.laplacian_radius - ) + return _laplacian_estimate_mask(array, mask, parameters.laplacian_radius) @classmethod def get_name(cls) -> str: @@ -198,9 +191,7 @@ def calculation_run( self, report_fun: Callable[[str, int], None] ) -> SegmentationResult: channel_nuc = self.get_channel(self.new_parameters.channel_nuc) - noise_filtering_parameters = ( - self.new_parameters.noise_filtering_nucleus - ) + noise_filtering_parameters = self.new_parameters.noise_filtering_nucleus cleaned_image = NoiseFilterSelection[ noise_filtering_parameters.name ].noise_filter( @@ -229,9 +220,7 @@ def calculation_run( if self.new_parameters.leave_the_biggest: nucleus_segmentation[nucleus_segmentation > 1] = 0 - channel_molecule = self.get_channel( - self.new_parameters.channel_molecule - ) + channel_molecule = self.get_channel(self.new_parameters.channel_molecule) background_estimate: SpotDetect = SpotExtractionSelection[ self.new_parameters.spot_method.name ] @@ -359,11 +348,7 @@ def gauss_background_estimate( clip_bellow_0: bool = True, ) -> LayerDataTuple: # process the image - mask = ( - mask.data - if mask is not None - else np.ones(image.data.shape, dtype=np.uint8) - ) + mask = mask.data if mask is not None else np.ones(image.data.shape, dtype=np.uint8) resp = _gauss_background_estimate_mask( image.data[0], mask[0], @@ -435,11 +420,7 @@ def _gauss_background_estimate( def laplacian_estimate( image: Image, mask: Labels, radius=1.30, clip_bellow_0=True ) -> LayerDataTuple: - mask = ( - mask.data - if mask is not None - else np.ones(image.data.shape, dtype=np.uint8) - ) + mask = mask.data if mask is not None else np.ones(image.data.shape, dtype=np.uint8) res = _laplacian_estimate_mask(image.data[0], mask[0], radius=radius) if clip_bellow_0: res[res < 0] = 0 @@ -480,9 +461,7 @@ def laplacian_check( ) -> LayerDataTuple: data = image.data[0] laplaced = -SimpleITK.GetArrayFromImage( - SimpleITK.LaplacianRecursiveGaussian( - SimpleITK.GetImageFromArray(data), radius - ) + SimpleITK.LaplacianRecursiveGaussian(SimpleITK.GetImageFromArray(data), radius) ) labeling = SimpleITK.GetArrayFromImage( @@ -499,20 +478,12 @@ def laplacian_check( ) ) labeling = labeling.reshape((1,) + data.shape) - return LayerDataTuple( - (labeling, {"scale": image.scale, "name": "Signal estimate"}) - ) + return LayerDataTuple((labeling, {"scale": image.scale, "name": "Signal estimate"})) -class LayerRangeThresholdFlowParameters( - CellFromNucleusFlow.__argument_class__ -): - lower_layer: int = Field( - 0, title="Lower layer", ge=-1, le=1000, position=0 - ) - upper_layer: int = Field( - -1, title="Lower layer", ge=-1, le=1000, position=1 - ) +class LayerRangeThresholdFlowParameters(CellFromNucleusFlow.__argument_class__): + lower_layer: int = Field(0, title="Lower layer", ge=-1, le=1000, position=0) + upper_layer: int = Field(-1, title="Lower layer", ge=-1, le=1000, position=1) class LayerRangeThresholdFlow(StackAlgorithm): @@ -581,9 +552,7 @@ def report_fun_wrap(text, num): partial_res = segment_method.calculation_run(report_fun_wrap) report_fun("Copy layers", count[0] + 1) - res_roi = np.zeros( - self.image.get_channel(0).shape, dtype=partial_res.roi.dtype - ) + res_roi = np.zeros(self.image.get_channel(0).shape, dtype=partial_res.roi.dtype) base_index = (slice(None),) * (self.image.stack_pos) for i in range(lower_layer, upper_layer): res_roi[base_index + (i,)] = partial_res.roi @@ -630,9 +599,7 @@ def maximum_projection( ) -class ThresholdFlowAlgorithmParametersWithDilation( - ThresholdFlowAlgorithmParameters -): +class ThresholdFlowAlgorithmParametersWithDilation(ThresholdFlowAlgorithmParameters): dilation_radius: int = Field( 0, title="Dilation radius", diff --git a/src/PartSeg_smfish/verify_points.py b/src/PartSeg_smfish/verify_points.py index c6bf100..738df68 100644 --- a/src/PartSeg_smfish/verify_points.py +++ b/src/PartSeg_smfish/verify_points.py @@ -52,9 +52,7 @@ def _shift_array(points_to_roi: int, ndim: int) -> np.ndarray: return np.array( [ base + (x, y) - for x, y in product( - range(-points_to_roi, points_to_roi + 1), repeat=2 - ) + for x, y in product(range(-points_to_roi, points_to_roi + 1), repeat=2) if x**2 + y**2 <= points_to_roi**2 ], dtype=np.uint16, @@ -67,9 +65,7 @@ def __init__(self, points_grouped, labels): self.matched_points: List[bool] = [False for _ in self.points_grouped] if 0 in labels: labels.remove(0) - self.labels_preserve: np.ndarray = np.arange( - (max(labels) if labels else 0) + 1 - ) + self.labels_preserve: np.ndarray = np.arange((max(labels) if labels else 0) + 1) self.labels = labels self.ignored = 0 @@ -140,8 +136,7 @@ def verify_segmentation( f"matched {np.sum(match_result.matched_points)} of" f" {len(match_result.matched_points)}" f"\nconsumed {all_labels - len(match_result.labels)} of" - f" {all_labels} segmentation components" - + f"\nignored {match_result.ignored}" + f" {all_labels} segmentation components" + f"\nignored {match_result.ignored}" if ignore_single_points else "" ) @@ -181,7 +176,7 @@ def find_single_points( points_res = [x[0] for x in points_grouped if len(x) == 1] find_single_points.info.value = ( f"Single points count: {len(points_res)} of {len(points_grouped)}," - f" ratio {len(points_res)/len(points_grouped)}" + f" ratio {len(points_res) / len(points_grouped)}" ) points_res = np.array(points_res) if points_res else None return LayerDataTuple( From bc3519cdcf8351c3118083675ac21eb441b60a97 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Tue, 8 Jul 2025 14:05:28 +0200 Subject: [PATCH 2/2] add ruff configuration --- .pre-commit-config.yaml | 4 +- pyproject.toml | 81 ++++++- src/PartSeg_smfish/__init__.py | 3 +- .../_tests/test_segmentation.py | 4 +- src/PartSeg_smfish/copy_labels.py | 21 +- src/PartSeg_smfish/segmentation.py | 220 +++++++++++++----- src/PartSeg_smfish/verify_points.py | 28 ++- 7 files changed, 275 insertions(+), 86 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 402dbcb..d7595ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.1 + rev: v0.12.2 hooks: - id: ruff-format exclude: examples @@ -22,6 +22,6 @@ repos: - id: mixed-line-ending exclude: .*\.py - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.1 + rev: 0.33.2 hooks: - id: check-github-workflows diff --git a/pyproject.toml b/pyproject.toml index 3c16eeb..f89f293 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,9 +6,82 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "src/PartSeg_smfish/_version.py" -[tool.black] +[tool.ruff] line-length = 79 +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".mypy_cache", + ".pants.d", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "*vendored*", + "*_vendor*", +] -[tool.isort] -profile = "black" -line_length = 79 +fix = true + +[tool.ruff.format] +quote-style = "double" + +[tool.ruff.lint] +select = [ + "E", "F", "W", #flake8 + "UP", # pyupgrade + "I", # isort + "YTT", #flake8-2020 + "TC", # flake8-type-checing + "BLE", # flake8-blind-exception + "B", # flake8-bugbear + "A", # flake8-builtins + "C4", # flake8-comprehensions + "ISC", # flake8-implicit-str-concat + "G", # flake8-logging-format + "PIE", # flake8-pie + "COM", # flake8-commas + "SIM", # flake8-simplify + "INP", # flake8-no-pep420 + "PYI", # flake8-pyi + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "TID", # flake8-tidy-imports # replace absolutify import + "TRY", # tryceratops + "ICN", # flake8-import-conventions + "RUF", # ruff specyfic rules + "NPY201", # checks compatibility with numpy version 2.0 + "ASYNC", # flake8-async + "EXE", # flake8-executable + "FA", # flake8-future-annotations + "LOG", # flake8-logging + "SLOT", # flake8-slots + "PT", # flake8-pytest-style + "T20", # flake8-print +] +ignore = [ + "COM812", # conflict with formatter +] + +[tool.ruff.lint.pyupgrade] +keep-runtime-typing = true + + +[tool.ruff.lint.flake8-quotes] +docstring-quotes = "double" +inline-quotes = "double" +multiline-quotes = "double" + +[tool.ruff.lint.isort] +known-first-party=['PartSeg_smfish'] +combine-as-imports = true diff --git a/src/PartSeg_smfish/__init__.py b/src/PartSeg_smfish/__init__.py index 74e43ad..9057a97 100644 --- a/src/PartSeg_smfish/__init__.py +++ b/src/PartSeg_smfish/__init__.py @@ -9,8 +9,7 @@ def register(): - from PartSegCore.register import RegisterEnum - from PartSegCore.register import register as register_fun + from PartSegCore.register import RegisterEnum, register as register_fun from . import measurement, segmentation diff --git a/src/PartSeg_smfish/_tests/test_segmentation.py b/src/PartSeg_smfish/_tests/test_segmentation.py index 480700c..1d7ab3c 100644 --- a/src/PartSeg_smfish/_tests/test_segmentation.py +++ b/src/PartSeg_smfish/_tests/test_segmentation.py @@ -9,7 +9,7 @@ ) -@pytest.mark.parametrize("mask", (True, False)) +@pytest.mark.parametrize("mask", [True, False]) def test_gauss_estimate(mask): param = GaussBackgroundEstimateParameters( background_estimate_radius=5, @@ -22,7 +22,7 @@ def test_gauss_estimate(mask): GaussBackgroundEstimate.spot_estimate(data, mask, (10, 10), param) -@pytest.mark.parametrize("mask", (True, False)) +@pytest.mark.parametrize("mask", [True, False]) def test_laplacian_estimate(mask): param = LaplacianBackgroundEstimateParameters( laplacian_radius=1.3, diff --git a/src/PartSeg_smfish/copy_labels.py b/src/PartSeg_smfish/copy_labels.py index 51e95e5..6796963 100644 --- a/src/PartSeg_smfish/copy_labels.py +++ b/src/PartSeg_smfish/copy_labels.py @@ -43,7 +43,9 @@ def __init__(self, napari_viewer: Viewer): self.setLayout(layout) - self.viewer.layers.selection.events.active.connect(self.activate_widget) + self.viewer.layers.selection.events.active.connect( + self.activate_widget, + ) self.copy_btn.clicked.connect(self.copy_action) self.check_all_btn.clicked.connect(self._check_all) self.uncheck_all_btn.clicked.connect(self._uncheck_all) @@ -114,8 +116,21 @@ def copy_action(self): checked = {layer.selected_label} z_position = self.viewer.dims.current_step[layer.dtype.ndim - 3] for component_num in checked: - mask = layer.data[leading_zeros + (z_position,)] == component_num + mask = ( + layer.data[ + ( + *leading_zeros, + z_position, + ) + ] + == component_num + ) start = max(0, self.lower.value()) end = min(layer.data.shape[1] - 1, self.upper.value()) + 1 for i in range(start, end): - layer.data[leading_zeros + (i,)][mask] = component_num + layer.data[ + ( + *leading_zeros, + i, + ) + ][mask] = component_num diff --git a/src/PartSeg_smfish/segmentation.py b/src/PartSeg_smfish/segmentation.py index ee8fe08..cbb767a 100644 --- a/src/PartSeg_smfish/segmentation.py +++ b/src/PartSeg_smfish/segmentation.py @@ -1,7 +1,7 @@ import operator from abc import ABC, abstractmethod from copy import deepcopy -from typing import Callable, List, Tuple, Union +from typing import Callable, Union import numpy as np import SimpleITK @@ -38,8 +38,7 @@ ThresholdSelection, ) from PartSegCore.utils import BaseModel -from PartSegImage import Channel -from PartSegImage import Image as PSImage +from PartSegImage import Channel, Image as PSImage from pydantic import Field @@ -52,12 +51,21 @@ def spot_estimate(cls, array, mask, spacing, parameters): class GaussBackgroundEstimateParameters(BaseModel): background_estimate_radius: float = Field( - 5, description="Radius of background gauss filter", ge=0, le=20 + 5, + description="Radius of background gauss filter", + ge=0, + le=20, ) foreground_estimate_radius: float = Field( - 2.5, description="Radius of foreground gauss filter", ge=0, le=20 + 2.5, + description="Radius of foreground gauss filter", + ge=0, + le=20, + ) + estimate_mask: bool = Field( + True, + description="Estimate background outside mask", ) - estimate_mask: bool = Field(True, description="Estimate background outside mask") class GaussBackgroundEstimate(SpotDetect): @@ -102,7 +110,10 @@ class LaplacianBackgroundEstimateParameters(BaseModel): ge=0, le=20, ) - estimate_mask: bool = Field(True, description="Estimate background outside mask") + estimate_mask: bool = Field( + True, + description="Estimate background outside mask", + ) class LaplacianBackgroundEstimate(SpotDetect): @@ -120,7 +131,11 @@ def spot_estimate( if not parameters.estimate_mask: return _laplacian_estimate(array, parameters.laplacian_radius) mask = mask if mask is not None else array > 0 - return _laplacian_estimate_mask(array, mask, parameters.laplacian_radius) + return _laplacian_estimate_mask( + array, + mask, + parameters.laplacian_radius, + ) @classmethod def get_name(cls) -> str: @@ -142,13 +157,18 @@ class SpotExtractionSelection( class SMSegmentationBaseParameters(BaseModel): channel_nuc: Channel = Field(0, title="Nucleus channel") noise_filtering_nucleus: NoiseFilterSelection = Field( - NoiseFilterSelection.get_default(), title="Filter nucleus" + NoiseFilterSelection.get_default(), + title="Filter nucleus", ) nucleus_threshold: ThresholdSelection = Field( - ThresholdSelection.get_default(), title="Nucleus threshold" + ThresholdSelection.get_default(), + title="Nucleus threshold", ) minimum_nucleus_size: int = Field( - 500, title="Minimum nucleus size (px)", ge=0, le=10**6 + 500, + title="Minimum nucleus size (px)", + ge=0, + le=10**6, ) leave_the_biggest: bool = Field( True, @@ -157,13 +177,18 @@ class SMSegmentationBaseParameters(BaseModel): ) channel_molecule: Channel = Field(1, title="Channel molecule") molecule_threshold: ThresholdSelection = Field( - ThresholdSelection.get_default(), title="Molecule threshold" + ThresholdSelection.get_default(), + title="Molecule threshold", ) minimum_molecule_size: int = Field( - 5, title="Minimum molecule size (px)", ge=0, le=10**6 + 5, + title="Minimum molecule size (px)", + ge=0, + le=10**6, ) spot_method: SpotExtractionSelection = Field( - SpotExtractionSelection.get_default(), title="Spot method" + SpotExtractionSelection.get_default(), + title="Spot method", ) @@ -188,10 +213,13 @@ def get_name(cls) -> str: return "sm-fish spot segmentation" def calculation_run( - self, report_fun: Callable[[str, int], None] + self, + report_fun: Callable[[str, int], None], ) -> SegmentationResult: channel_nuc = self.get_channel(self.new_parameters.channel_nuc) - noise_filtering_parameters = self.new_parameters.noise_filtering_nucleus + noise_filtering_parameters = ( + self.new_parameters.noise_filtering_nucleus + ) cleaned_image = NoiseFilterSelection[ noise_filtering_parameters.name ].noise_filter( @@ -209,18 +237,22 @@ def calculation_run( operator.ge, ) nucleus_connect = SimpleITK.ConnectedComponent( - SimpleITK.GetImageFromArray(nucleus_mask), True + SimpleITK.GetImageFromArray(nucleus_mask), + True, ) nucleus_segmentation = SimpleITK.GetArrayFromImage( SimpleITK.RelabelComponent( - nucleus_connect, self.new_parameters.minimum_nucleus_size - ) + nucleus_connect, + self.new_parameters.minimum_nucleus_size, + ), ) nucleus_segmentation = convex_fill(nucleus_segmentation) if self.new_parameters.leave_the_biggest: nucleus_segmentation[nucleus_segmentation > 1] = 0 - channel_molecule = self.get_channel(self.new_parameters.channel_molecule) + channel_molecule = self.get_channel( + self.new_parameters.channel_molecule, + ) background_estimate: SpotDetect = SpotExtractionSelection[ self.new_parameters.spot_method.name ] @@ -248,25 +280,27 @@ def calculation_run( operator.ge, ) nucleus_connect = SimpleITK.ConnectedComponent( - SimpleITK.GetImageFromArray(molecule_mask), True + SimpleITK.GetImageFromArray(molecule_mask), + True, ) molecule_segmentation = SimpleITK.GetArrayFromImage( SimpleITK.RelabelComponent( - nucleus_connect, self.new_parameters.minimum_molecule_size - ) + nucleus_connect, + self.new_parameters.minimum_molecule_size, + ), ) sizes = np.bincount(molecule_segmentation.flat) elements = np.unique(molecule_segmentation[molecule_segmentation > 0]) cellular_components = set( - np.unique(molecule_segmentation[nucleus_segmentation == 0]) + np.unique(molecule_segmentation[nucleus_segmentation == 0]), ) if 0 in cellular_components: cellular_components.remove(0) nucleus_components = set( - np.unique(molecule_segmentation[nucleus_segmentation > 0]) + np.unique(molecule_segmentation[nucleus_segmentation > 0]), ) if 0 in nucleus_components: nucleus_components.remove(0) @@ -274,9 +308,9 @@ def calculation_run( cellular_components = cellular_components - mixed_components nucleus_components = nucleus_components - mixed_components label_types = ( - {i: "Nucleus" for i in nucleus_components} - | {i: "Cytoplasm" for i in cellular_components} - | {i: "Mixed" for i in mixed_components} + dict.fromkeys(nucleus_components, "Nucleus") + | dict.fromkeys(cellular_components, "Cytoplasm") + | dict.fromkeys(mixed_components, "Mixed") ) self._spots_count = { "Nucleus": len(nucleus_components), @@ -305,19 +339,24 @@ def calculation_run( parameters=self.get_segmentation_profile(), additional_layers={ "nucleus segmentation": AdditionalLayerDescription( - data=nucleus_segmentation, layer_type="labels" + data=nucleus_segmentation, + layer_type="labels", ), "roi segmentation": AdditionalLayerDescription( - data=molecule_segmentation, layer_type="labels" + data=molecule_segmentation, + layer_type="labels", ), "estimated signal": AdditionalLayerDescription( - data=estimated, layer_type="image" + data=estimated, + layer_type="image", ), "channel molecule": AdditionalLayerDescription( - data=channel_molecule, layer_type="image" + data=channel_molecule, + layer_type="image", ), "position": AdditionalLayerDescription( - data=position_array, layer_type="labels" + data=position_array, + layer_type="labels", ), }, roi_annotation=annotation, @@ -348,7 +387,11 @@ def gauss_background_estimate( clip_bellow_0: bool = True, ) -> LayerDataTuple: # process the image - mask = mask.data if mask is not None else np.ones(image.data.shape, dtype=np.uint8) + mask = ( + mask.data + if mask is not None + else np.ones(image.data.shape, dtype=np.uint8) + ) resp = _gauss_background_estimate_mask( image.data[0], mask[0], @@ -358,7 +401,7 @@ def gauss_background_estimate( ) if clip_bellow_0: resp[resp < 0] = 0 - resp = resp.reshape((1,) + resp.shape) + resp = resp.reshape((1, *resp.shape)) # return it + some layer properties return LayerDataTuple( ( @@ -368,14 +411,14 @@ def gauss_background_estimate( "scale": image.scale, "name": "Signal estimate", }, - ) + ), ) def _gauss_background_estimate_mask( channel: np.ndarray, mask: np.ndarray, - scale: Union[List[float], Tuple[Union[float, int]]], + scale: Union[list[float], tuple[Union[float, int]]], background_gauss_radius: float, foreground_gauss_radius: float, ) -> np.ndarray: @@ -385,7 +428,10 @@ def _gauss_background_estimate_mask( data = gaussian(data, 15, False) data[mask > 0] = channel[mask > 0] resp = _gauss_background_estimate( - data, scale, background_gauss_radius, foreground_gauss_radius + data, + scale, + background_gauss_radius, + foreground_gauss_radius, ) resp[mask == 0] = 0 return resp @@ -393,7 +439,7 @@ def _gauss_background_estimate_mask( def _gauss_background_estimate( channel: np.ndarray, - scale: Union[List[float], Tuple[Union[float, int]]], + scale: Union[list[float], tuple[Union[float, int]]], background_gauss_radius: float, foreground_gauss_radius: float, ) -> np.ndarray: @@ -418,9 +464,16 @@ def _gauss_background_estimate( def laplacian_estimate( - image: Image, mask: Labels, radius=1.30, clip_bellow_0=True + image: Image, + mask: Labels, + radius=1.30, + clip_bellow_0=True, ) -> LayerDataTuple: - mask = mask.data if mask is not None else np.ones(image.data.shape, dtype=np.uint8) + mask = ( + mask.data + if mask is not None + else np.ones(image.data.shape, dtype=np.uint8) + ) res = _laplacian_estimate_mask(image.data[0], mask[0], radius=radius) if clip_bellow_0: res[res < 0] = 0 @@ -433,12 +486,14 @@ def laplacian_estimate( "scale": image.scale, "name": "Laplacian estimate", }, - ) + ), ) def _laplacian_estimate_mask( - channel: np.ndarray, mask: np.ndarray, radius=1.30 + channel: np.ndarray, + mask: np.ndarray, + radius=1.30, ) -> np.ndarray: data = channel.astype(np.float64) mean_background = np.mean(data[mask > 0]) @@ -451,17 +506,25 @@ def _laplacian_estimate_mask( def _laplacian_estimate(channel: np.ndarray, radius=1.30) -> np.ndarray: return -SimpleITK.GetArrayFromImage( SimpleITK.LaplacianRecursiveGaussian( - SimpleITK.GetImageFromArray(channel), radius - ) + SimpleITK.GetImageFromArray(channel), + radius, + ), ) def laplacian_check( - image: Image, mask: Labels, radius=1.0, threshold=10.0, min_size=50 + image: Image, + mask: Labels, + radius=1.0, + threshold=10.0, + min_size=50, ) -> LayerDataTuple: data = image.data[0] laplaced = -SimpleITK.GetArrayFromImage( - SimpleITK.LaplacianRecursiveGaussian(SimpleITK.GetImageFromArray(data), radius) + SimpleITK.LaplacianRecursiveGaussian( + SimpleITK.GetImageFromArray(data), + radius, + ), ) labeling = SimpleITK.GetArrayFromImage( @@ -475,15 +538,31 @@ def laplacian_check( SimpleITK.GetImageFromArray(mask.data[0]), ), min_size, - ) + ), + ) + labeling = labeling.reshape((1, *data.shape)) + return LayerDataTuple( + (labeling, {"scale": image.scale, "name": "Signal estimate"}), ) - labeling = labeling.reshape((1,) + data.shape) - return LayerDataTuple((labeling, {"scale": image.scale, "name": "Signal estimate"})) -class LayerRangeThresholdFlowParameters(CellFromNucleusFlow.__argument_class__): - lower_layer: int = Field(0, title="Lower layer", ge=-1, le=1000, position=0) - upper_layer: int = Field(-1, title="Lower layer", ge=-1, le=1000, position=1) +class LayerRangeThresholdFlowParameters( + CellFromNucleusFlow.__argument_class__, +): + lower_layer: int = Field( + 0, + title="Lower layer", + ge=-1, + le=1000, + position=0, + ) + upper_layer: int = Field( + -1, + title="Lower layer", + ge=-1, + le=1000, + position=1, + ) class LayerRangeThresholdFlow(StackAlgorithm): @@ -508,7 +587,8 @@ def get_steps_num(): return CellFromNucleusFlow.get_steps_num() + 2 def calculation_run( - self, report_fun: Callable[[str, int], None] + self, + report_fun: Callable[[str, int], None], ) -> ROIExtractionResult: count = [0] @@ -526,7 +606,8 @@ def report_fun_wrap(text, num): slice_arr[self.image.stack_pos] = slice(lower_layer, upper_layer) slice_arr.pop(self.image.channel_pos) image: PSImage = self.image.substitute(mask=self.mask).cut_image( - slice_arr, frame=0 + slice_arr, + frame=0, ) new_data = np.max(image.get_data(), axis=image.stack_pos) @@ -552,15 +633,25 @@ def report_fun_wrap(text, num): partial_res = segment_method.calculation_run(report_fun_wrap) report_fun("Copy layers", count[0] + 1) - res_roi = np.zeros(self.image.get_channel(0).shape, dtype=partial_res.roi.dtype) + res_roi = np.zeros( + self.image.get_channel(0).shape, + dtype=partial_res.roi.dtype, + ) base_index = (slice(None),) * (self.image.stack_pos) for i in range(lower_layer, upper_layer): - res_roi[base_index + (i,)] = partial_res.roi + res_roi[ + ( + *base_index, + i, + ) + ] = partial_res.roi report_fun("Prepare result", count[0] + 2) additional_layer = { "maximum_projection": AdditionalLayerDescription( - new_data, "image", "maximum projection" + new_data, + "image", + "maximum projection", ), **partial_res.additional_layers, } @@ -595,11 +686,13 @@ def maximum_projection( "scale": image.scale, "name": "Maximum projection", }, - ) + ), ) -class ThresholdFlowAlgorithmParametersWithDilation(ThresholdFlowAlgorithmParameters): +class ThresholdFlowAlgorithmParametersWithDilation( + ThresholdFlowAlgorithmParameters, +): dilation_radius: int = Field( 0, title="Dilation radius", @@ -622,7 +715,8 @@ def get_steps_num(cls) -> int: return ThresholdFlowAlgorithm.get_steps_num() + 1 def calculation_run( - self, report_fun: Callable[[str, int], None] + self, + report_fun: Callable[[str, int], None], ) -> ROIExtractionResult: res = super().calculation_run(report_fun) if self.new_parameters.dilation_radius == 0: @@ -644,7 +738,9 @@ def calculation_run( ) res2.additional_layers["dilated roi"] = AdditionalLayerDescription( - res.roi, "labels", "Dilated ROI" + res.roi, + "labels", + "Dilated ROI", ) report_fun("Calculation done", self.get_steps_num()) return res2 diff --git a/src/PartSeg_smfish/verify_points.py b/src/PartSeg_smfish/verify_points.py index 738df68..295d8d2 100644 --- a/src/PartSeg_smfish/verify_points.py +++ b/src/PartSeg_smfish/verify_points.py @@ -1,5 +1,4 @@ from itertools import product -from typing import List import numpy as np from magicgui import magic_factory @@ -8,7 +7,7 @@ from scipy.spatial.distance import cdist -def group_points(points: np.ndarray, max_dist=1) -> List[List[np.ndarray]]: +def group_points(points: np.ndarray, max_dist=1) -> list[list[np.ndarray]]: points = np.copy(points) points[:, -3] = np.round(points[:, -3]) sort = np.argsort(points[:, -3]) @@ -51,8 +50,11 @@ def _shift_array(points_to_roi: int, ndim: int) -> np.ndarray: base = tuple([0] * (ndim - 2)) return np.array( [ - base + (x, y) - for x, y in product(range(-points_to_roi, points_to_roi + 1), repeat=2) + (*base, x, y) + for x, y in product( + range(-points_to_roi, points_to_roi + 1), + repeat=2, + ) if x**2 + y**2 <= points_to_roi**2 ], dtype=np.uint16, @@ -61,11 +63,13 @@ def _shift_array(points_to_roi: int, ndim: int) -> np.ndarray: class MatchResults: def __init__(self, points_grouped, labels): - self.points_grouped: List[List[np.ndarray]] = points_grouped - self.matched_points: List[bool] = [False for _ in self.points_grouped] + self.points_grouped: list[list[np.ndarray]] = points_grouped + self.matched_points: list[bool] = [False for _ in self.points_grouped] if 0 in labels: labels.remove(0) - self.labels_preserve: np.ndarray = np.arange((max(labels) if labels else 0) + 1) + self.labels_preserve: np.ndarray = np.arange( + (max(labels) if labels else 0) + 1, + ) self.labels = labels self.ignored = 0 @@ -122,7 +126,7 @@ def verify_segmentation( points_to_roi: int = 1, ignore_single_points: bool = True, info: str = "", -) -> List[LayerDataTuple]: +) -> list[LayerDataTuple]: match_result = verify_sm_segmentation( segmentation.data, points.data, @@ -136,13 +140,15 @@ def verify_segmentation( f"matched {np.sum(match_result.matched_points)} of" f" {len(match_result.matched_points)}" f"\nconsumed {all_labels - len(match_result.labels)} of" - f" {all_labels} segmentation components" + f"\nignored {match_result.ignored}" + f" {all_labels} segmentation components" + f"\nignored {match_result.ignored}" if ignore_single_points else "" ) res = [] for ok, points_group in zip( - match_result.matched_points, match_result.points_grouped + match_result.matched_points, + match_result.points_grouped, ): if not ok: res.extend(points_group) @@ -188,5 +194,5 @@ def find_single_points( "face_color": "green", }, "points", - ) + ), )