sigima 0.0.1.dev0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sigima/__init__.py +142 -2
- sigima/client/__init__.py +105 -0
- sigima/client/base.py +780 -0
- sigima/client/remote.py +469 -0
- sigima/client/stub.py +814 -0
- sigima/client/utils.py +90 -0
- sigima/config.py +444 -0
- sigima/data/logo/Sigima.svg +135 -0
- sigima/data/tests/annotations.json +798 -0
- sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
- sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
- sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
- sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
- sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
- sigima/data/tests/curve_formats/bandwidth.txt +201 -0
- sigima/data/tests/curve_formats/boxcar.npy +0 -0
- sigima/data/tests/curve_formats/datetime.txt +1001 -0
- sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
- sigima/data/tests/curve_formats/fw1e2.txt +301 -0
- sigima/data/tests/curve_formats/fwhm.txt +319 -0
- sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
- sigima/data/tests/curve_formats/noised_saw.mat +0 -0
- sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
- sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
- sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
- sigima/data/tests/curve_formats/paracetamol.npy +0 -0
- sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
- sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
- sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
- sigima/data/tests/curve_formats/pulse1.npy +0 -0
- sigima/data/tests/curve_formats/pulse2.npy +0 -0
- sigima/data/tests/curve_formats/simple.txt +5 -0
- sigima/data/tests/curve_formats/spectrum.mca +2139 -0
- sigima/data/tests/curve_formats/square2.npy +0 -0
- sigima/data/tests/curve_formats/step.npy +0 -0
- sigima/data/tests/fabry-perot1.jpg +0 -0
- sigima/data/tests/fabry-perot2.jpg +0 -0
- sigima/data/tests/flower.npy +0 -0
- sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
- sigima/data/tests/image_formats/binary_image.npy +0 -0
- sigima/data/tests/image_formats/binary_image.png +0 -0
- sigima/data/tests/image_formats/centroid_test.npy +0 -0
- sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
- sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
- sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
- sigima/data/tests/image_formats/fiber.csv +480 -0
- sigima/data/tests/image_formats/fiber.jpg +0 -0
- sigima/data/tests/image_formats/fiber.png +0 -0
- sigima/data/tests/image_formats/fiber.txt +480 -0
- sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
- sigima/data/tests/image_formats/mr-brain.dcm +0 -0
- sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
- sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
- sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
- sigima/data/tests/image_formats/tiling.txt +10 -0
- sigima/data/tests/image_formats/uint16.tiff +0 -0
- sigima/data/tests/image_formats/uint8.tiff +0 -0
- sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
- sigima/enums.py +195 -0
- sigima/io/__init__.py +123 -0
- sigima/io/base.py +311 -0
- sigima/io/common/__init__.py +5 -0
- sigima/io/common/basename.py +164 -0
- sigima/io/common/converters.py +189 -0
- sigima/io/common/objmeta.py +181 -0
- sigima/io/common/textreader.py +58 -0
- sigima/io/convenience.py +157 -0
- sigima/io/enums.py +17 -0
- sigima/io/ftlab.py +395 -0
- sigima/io/image/__init__.py +9 -0
- sigima/io/image/base.py +177 -0
- sigima/io/image/formats.py +1016 -0
- sigima/io/image/funcs.py +414 -0
- sigima/io/signal/__init__.py +9 -0
- sigima/io/signal/base.py +129 -0
- sigima/io/signal/formats.py +290 -0
- sigima/io/signal/funcs.py +723 -0
- sigima/objects/__init__.py +260 -0
- sigima/objects/base.py +937 -0
- sigima/objects/image/__init__.py +88 -0
- sigima/objects/image/creation.py +556 -0
- sigima/objects/image/object.py +524 -0
- sigima/objects/image/roi.py +904 -0
- sigima/objects/scalar/__init__.py +57 -0
- sigima/objects/scalar/common.py +215 -0
- sigima/objects/scalar/geometry.py +502 -0
- sigima/objects/scalar/table.py +784 -0
- sigima/objects/shape.py +290 -0
- sigima/objects/signal/__init__.py +133 -0
- sigima/objects/signal/constants.py +27 -0
- sigima/objects/signal/creation.py +1428 -0
- sigima/objects/signal/object.py +444 -0
- sigima/objects/signal/roi.py +274 -0
- sigima/params.py +405 -0
- sigima/proc/__init__.py +96 -0
- sigima/proc/base.py +381 -0
- sigima/proc/decorator.py +330 -0
- sigima/proc/image/__init__.py +513 -0
- sigima/proc/image/arithmetic.py +335 -0
- sigima/proc/image/base.py +260 -0
- sigima/proc/image/detection.py +519 -0
- sigima/proc/image/edges.py +329 -0
- sigima/proc/image/exposure.py +406 -0
- sigima/proc/image/extraction.py +458 -0
- sigima/proc/image/filtering.py +219 -0
- sigima/proc/image/fourier.py +147 -0
- sigima/proc/image/geometry.py +661 -0
- sigima/proc/image/mathops.py +340 -0
- sigima/proc/image/measurement.py +195 -0
- sigima/proc/image/morphology.py +155 -0
- sigima/proc/image/noise.py +107 -0
- sigima/proc/image/preprocessing.py +182 -0
- sigima/proc/image/restoration.py +235 -0
- sigima/proc/image/threshold.py +217 -0
- sigima/proc/image/transformations.py +393 -0
- sigima/proc/signal/__init__.py +376 -0
- sigima/proc/signal/analysis.py +206 -0
- sigima/proc/signal/arithmetic.py +551 -0
- sigima/proc/signal/base.py +262 -0
- sigima/proc/signal/extraction.py +60 -0
- sigima/proc/signal/features.py +310 -0
- sigima/proc/signal/filtering.py +484 -0
- sigima/proc/signal/fitting.py +276 -0
- sigima/proc/signal/fourier.py +259 -0
- sigima/proc/signal/mathops.py +420 -0
- sigima/proc/signal/processing.py +580 -0
- sigima/proc/signal/stability.py +175 -0
- sigima/proc/title_formatting.py +227 -0
- sigima/proc/validation.py +272 -0
- sigima/tests/__init__.py +7 -0
- sigima/tests/common/__init__.py +0 -0
- sigima/tests/common/arithmeticparam_unit_test.py +26 -0
- sigima/tests/common/basename_unit_test.py +126 -0
- sigima/tests/common/client_unit_test.py +412 -0
- sigima/tests/common/converters_unit_test.py +77 -0
- sigima/tests/common/decorator_unit_test.py +176 -0
- sigima/tests/common/examples_unit_test.py +104 -0
- sigima/tests/common/kernel_normalization_unit_test.py +242 -0
- sigima/tests/common/roi_basic_unit_test.py +73 -0
- sigima/tests/common/roi_geometry_unit_test.py +171 -0
- sigima/tests/common/scalar_builder_unit_test.py +142 -0
- sigima/tests/common/scalar_unit_test.py +991 -0
- sigima/tests/common/shape_unit_test.py +183 -0
- sigima/tests/common/stat_unit_test.py +138 -0
- sigima/tests/common/title_formatting_unit_test.py +338 -0
- sigima/tests/common/tools_coordinates_unit_test.py +60 -0
- sigima/tests/common/transformations_unit_test.py +178 -0
- sigima/tests/common/validation_unit_test.py +205 -0
- sigima/tests/conftest.py +129 -0
- sigima/tests/data.py +998 -0
- sigima/tests/env.py +280 -0
- sigima/tests/guiutils.py +163 -0
- sigima/tests/helpers.py +532 -0
- sigima/tests/image/__init__.py +28 -0
- sigima/tests/image/binning_unit_test.py +128 -0
- sigima/tests/image/blob_detection_unit_test.py +312 -0
- sigima/tests/image/centroid_unit_test.py +170 -0
- sigima/tests/image/check_2d_array_unit_test.py +63 -0
- sigima/tests/image/contour_unit_test.py +172 -0
- sigima/tests/image/convolution_unit_test.py +178 -0
- sigima/tests/image/datatype_unit_test.py +67 -0
- sigima/tests/image/edges_unit_test.py +155 -0
- sigima/tests/image/enclosingcircle_unit_test.py +88 -0
- sigima/tests/image/exposure_unit_test.py +223 -0
- sigima/tests/image/fft2d_unit_test.py +189 -0
- sigima/tests/image/filtering_unit_test.py +166 -0
- sigima/tests/image/geometry_unit_test.py +654 -0
- sigima/tests/image/hough_circle_unit_test.py +147 -0
- sigima/tests/image/imageobj_unit_test.py +737 -0
- sigima/tests/image/morphology_unit_test.py +71 -0
- sigima/tests/image/noise_unit_test.py +57 -0
- sigima/tests/image/offset_correction_unit_test.py +72 -0
- sigima/tests/image/operation_unit_test.py +518 -0
- sigima/tests/image/peak2d_limits_unit_test.py +41 -0
- sigima/tests/image/peak2d_unit_test.py +133 -0
- sigima/tests/image/profile_unit_test.py +159 -0
- sigima/tests/image/projections_unit_test.py +121 -0
- sigima/tests/image/restoration_unit_test.py +141 -0
- sigima/tests/image/roi2dparam_unit_test.py +53 -0
- sigima/tests/image/roi_advanced_unit_test.py +588 -0
- sigima/tests/image/roi_grid_unit_test.py +279 -0
- sigima/tests/image/spectrum2d_unit_test.py +40 -0
- sigima/tests/image/threshold_unit_test.py +91 -0
- sigima/tests/io/__init__.py +0 -0
- sigima/tests/io/addnewformat_unit_test.py +125 -0
- sigima/tests/io/convenience_funcs_unit_test.py +470 -0
- sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
- sigima/tests/io/datetime_csv_unit_test.py +198 -0
- sigima/tests/io/imageio_formats_test.py +41 -0
- sigima/tests/io/ioregistry_unit_test.py +69 -0
- sigima/tests/io/objmeta_unit_test.py +87 -0
- sigima/tests/io/readobj_unit_test.py +130 -0
- sigima/tests/io/readwriteobj_unit_test.py +67 -0
- sigima/tests/signal/__init__.py +0 -0
- sigima/tests/signal/analysis_unit_test.py +135 -0
- sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
- sigima/tests/signal/convolution_unit_test.py +404 -0
- sigima/tests/signal/datetime_unit_test.py +176 -0
- sigima/tests/signal/fft1d_unit_test.py +303 -0
- sigima/tests/signal/filters_unit_test.py +403 -0
- sigima/tests/signal/fitting_unit_test.py +929 -0
- sigima/tests/signal/fwhm_unit_test.py +111 -0
- sigima/tests/signal/noise_unit_test.py +128 -0
- sigima/tests/signal/offset_correction_unit_test.py +34 -0
- sigima/tests/signal/operation_unit_test.py +489 -0
- sigima/tests/signal/peakdetection_unit_test.py +145 -0
- sigima/tests/signal/processing_unit_test.py +657 -0
- sigima/tests/signal/pulse/__init__.py +112 -0
- sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
- sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
- sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
- sigima/tests/signal/roi_advanced_unit_test.py +392 -0
- sigima/tests/signal/signalobj_unit_test.py +603 -0
- sigima/tests/signal/stability_unit_test.py +431 -0
- sigima/tests/signal/uncertainty_unit_test.py +611 -0
- sigima/tests/vistools.py +1030 -0
- sigima/tools/__init__.py +59 -0
- sigima/tools/checks.py +290 -0
- sigima/tools/coordinates.py +308 -0
- sigima/tools/datatypes.py +26 -0
- sigima/tools/image/__init__.py +97 -0
- sigima/tools/image/detection.py +451 -0
- sigima/tools/image/exposure.py +77 -0
- sigima/tools/image/extraction.py +48 -0
- sigima/tools/image/fourier.py +260 -0
- sigima/tools/image/geometry.py +190 -0
- sigima/tools/image/preprocessing.py +165 -0
- sigima/tools/signal/__init__.py +86 -0
- sigima/tools/signal/dynamic.py +254 -0
- sigima/tools/signal/features.py +135 -0
- sigima/tools/signal/filtering.py +171 -0
- sigima/tools/signal/fitting.py +1171 -0
- sigima/tools/signal/fourier.py +466 -0
- sigima/tools/signal/interpolation.py +70 -0
- sigima/tools/signal/peakdetection.py +126 -0
- sigima/tools/signal/pulse.py +1626 -0
- sigima/tools/signal/scaling.py +50 -0
- sigima/tools/signal/stability.py +258 -0
- sigima/tools/signal/windowing.py +90 -0
- sigima/worker.py +79 -0
- sigima-1.0.0.dist-info/METADATA +233 -0
- sigima-1.0.0.dist-info/RECORD +262 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
- sigima-0.0.1.dev0.dist-info/METADATA +0 -60
- sigima-0.0.1.dev0.dist-info/RECORD +0 -6
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Threshold computation module
|
|
5
|
+
----------------------------
|
|
6
|
+
|
|
7
|
+
This module provides various thresholding techniques for image segmentation.
|
|
8
|
+
Thresholding is a simple yet effective method to separate objects from the background
|
|
9
|
+
in an image by converting it into a binary image based on a specified threshold value.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
13
|
+
|
|
14
|
+
# Note:
|
|
15
|
+
# ----
|
|
16
|
+
# - All `guidata.dataset.DataSet` parameter classes must also be imported
|
|
17
|
+
# in the `sigima.params` module.
|
|
18
|
+
# - All functions decorated by `computation_function` must be imported in the upper
|
|
19
|
+
# level `sigima.proc.image` module.
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import guidata.dataset as gds
|
|
24
|
+
import skimage.util
|
|
25
|
+
from skimage import filters
|
|
26
|
+
|
|
27
|
+
from sigima.config import _
|
|
28
|
+
from sigima.objects.image import ImageObj
|
|
29
|
+
from sigima.proc.base import dst_1_to_1
|
|
30
|
+
from sigima.proc.decorator import computation_function
|
|
31
|
+
from sigima.proc.image.base import restore_data_outside_roi
|
|
32
|
+
|
|
33
|
+
# NOTE: Only parameter classes DEFINED in this module should be included in __all__.
|
|
34
|
+
# Parameter classes imported from other modules (like sigima.proc.base) should NOT
|
|
35
|
+
# be re-exported to avoid Sphinx cross-reference conflicts. The sigima.params module
|
|
36
|
+
# serves as the central API point that imports and re-exports all parameter classes.
|
|
37
|
+
__all__ = [
|
|
38
|
+
"ThresholdParam",
|
|
39
|
+
"threshold",
|
|
40
|
+
"threshold_isodata",
|
|
41
|
+
"threshold_li",
|
|
42
|
+
"threshold_mean",
|
|
43
|
+
"threshold_minimum",
|
|
44
|
+
"threshold_otsu",
|
|
45
|
+
"threshold_triangle",
|
|
46
|
+
"threshold_yen",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ThresholdParam(gds.DataSet):
|
|
51
|
+
"""Histogram threshold parameters"""
|
|
52
|
+
|
|
53
|
+
methods = (
|
|
54
|
+
("manual", _("Manual")),
|
|
55
|
+
("isodata", "ISODATA"),
|
|
56
|
+
("li", "Li"),
|
|
57
|
+
("mean", _("Mean")),
|
|
58
|
+
("minimum", _("Minimum")),
|
|
59
|
+
("otsu", "Otsu"),
|
|
60
|
+
("triangle", _("Triangle")),
|
|
61
|
+
("yen", "Yen"),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
_method_prop = gds.GetAttrProp("method")
|
|
65
|
+
method = gds.ChoiceItem(_("Threshold method"), methods, default="manual").set_prop(
|
|
66
|
+
"display", store=_method_prop
|
|
67
|
+
)
|
|
68
|
+
bins = gds.IntItem(_("Number of bins"), default=256, min=1).set_prop(
|
|
69
|
+
"display",
|
|
70
|
+
active=gds.FuncProp(_method_prop, lambda x: x not in ("li", "mean", "manual")),
|
|
71
|
+
)
|
|
72
|
+
value = gds.FloatItem(_("Threshold value"), default=0.0).set_prop(
|
|
73
|
+
"display", active=gds.FuncProp(_method_prop, lambda x: x == "manual")
|
|
74
|
+
)
|
|
75
|
+
operation = gds.ChoiceItem(
|
|
76
|
+
_("Operation"),
|
|
77
|
+
((">", _("Greater than")), ("<", _("Less than"))),
|
|
78
|
+
default=">",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@computation_function()
|
|
83
|
+
def threshold(src: ImageObj, p: ThresholdParam) -> ImageObj:
|
|
84
|
+
"""Compute the threshold, using one of the available algorithms:
|
|
85
|
+
|
|
86
|
+
- Manual: a fixed threshold value
|
|
87
|
+
- ISODATA: :py:func:`skimage.filters.threshold_isodata`
|
|
88
|
+
- Li: :py:func:`skimage.filters.threshold_li`
|
|
89
|
+
- Mean: :py:func:`skimage.filters.threshold_mean`
|
|
90
|
+
- Minimum: :py:func:`skimage.filters.threshold_minimum`
|
|
91
|
+
- Otsu: :py:func:`skimage.filters.threshold_otsu`
|
|
92
|
+
- Triangle: :py:func:`skimage.filters.threshold_triangle`
|
|
93
|
+
- Yen: :py:func:`skimage.filters.threshold_yen`
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
src: input image object
|
|
97
|
+
p: parameters
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Output image object
|
|
101
|
+
"""
|
|
102
|
+
if p.method == "manual":
|
|
103
|
+
suffix = f"value={p.value}"
|
|
104
|
+
value = p.value
|
|
105
|
+
else:
|
|
106
|
+
suffix = f"method={p.method}"
|
|
107
|
+
if p.method not in ("li", "mean"):
|
|
108
|
+
suffix += f", nbins={p.bins}"
|
|
109
|
+
func = getattr(filters, f"threshold_{p.method}")
|
|
110
|
+
args = [] if p.method in ("li", "mean") else [p.bins]
|
|
111
|
+
value = func(src.data, *args)
|
|
112
|
+
suffix += f", op='{p.operation}'"
|
|
113
|
+
dst = dst_1_to_1(src, "threshold", suffix)
|
|
114
|
+
data = src.data > value if p.operation == ">" else src.data < value
|
|
115
|
+
dst.data = skimage.util.img_as_ubyte(data)
|
|
116
|
+
dst.zscalemin, dst.zscalemax = 0, 255 # LUT range
|
|
117
|
+
dst.set_metadata_option("colormap", "gray")
|
|
118
|
+
restore_data_outside_roi(dst, src)
|
|
119
|
+
return dst
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@computation_function()
|
|
123
|
+
def threshold_isodata(src: ImageObj) -> ImageObj:
|
|
124
|
+
"""Compute the threshold using the Isodata algorithm with default parameters,
|
|
125
|
+
see :py:func:`skimage.filters.threshold_isodata`
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
src: input image object
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Output image object
|
|
132
|
+
"""
|
|
133
|
+
return threshold(src, ThresholdParam.create(method="isodata"))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@computation_function()
|
|
137
|
+
def threshold_li(src: ImageObj) -> ImageObj:
|
|
138
|
+
"""Compute the threshold using the Li algorithm with default parameters,
|
|
139
|
+
see :py:func:`skimage.filters.threshold_li`
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
src: input image object
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Output image object
|
|
146
|
+
"""
|
|
147
|
+
return threshold(src, ThresholdParam.create(method="li"))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@computation_function()
|
|
151
|
+
def threshold_mean(src: ImageObj) -> ImageObj:
|
|
152
|
+
"""Compute the threshold using the Mean algorithm,
|
|
153
|
+
see :py:func:`skimage.filters.threshold_mean`
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
src: input image object
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Output image object
|
|
160
|
+
"""
|
|
161
|
+
return threshold(src, ThresholdParam.create(method="mean"))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@computation_function()
|
|
165
|
+
def threshold_minimum(src: ImageObj) -> ImageObj:
|
|
166
|
+
"""Compute the threshold using the Minimum algorithm with default parameters,
|
|
167
|
+
see :py:func:`skimage.filters.threshold_minimum`
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
src: input image object
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Output image object
|
|
174
|
+
"""
|
|
175
|
+
return threshold(src, ThresholdParam.create(method="minimum"))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@computation_function()
|
|
179
|
+
def threshold_otsu(src: ImageObj) -> ImageObj:
|
|
180
|
+
"""Compute the threshold using the Otsu algorithm with default parameters,
|
|
181
|
+
see :py:func:`skimage.filters.threshold_otsu`
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
src: input image object
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Output image object
|
|
188
|
+
"""
|
|
189
|
+
return threshold(src, ThresholdParam.create(method="otsu"))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@computation_function()
|
|
193
|
+
def threshold_triangle(src: ImageObj) -> ImageObj:
|
|
194
|
+
"""Compute the threshold using the Triangle algorithm with default parameters,
|
|
195
|
+
see :py:func:`skimage.filters.threshold_triangle`
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
src: input image object
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Output image object
|
|
202
|
+
"""
|
|
203
|
+
return threshold(src, ThresholdParam.create(method="triangle"))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@computation_function()
|
|
207
|
+
def threshold_yen(src: ImageObj) -> ImageObj:
|
|
208
|
+
"""Compute the threshold using the Yen algorithm with default parameters,
|
|
209
|
+
see :py:func:`skimage.filters.threshold_yen`
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
src: input image object
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Output image object
|
|
216
|
+
"""
|
|
217
|
+
return threshold(src, ThresholdParam.create(method="yen"))
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Geometry transformations module
|
|
5
|
+
===============================
|
|
6
|
+
|
|
7
|
+
This module provides a unified interface for applying geometric transformations
|
|
8
|
+
to both geometry results (:class:`sigima.objects.GeometryResult`) and ROI objects
|
|
9
|
+
using the shape coordinate system (:mod:`sigima.objects.shape`).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from sigima.objects.scalar import GeometryResult, KindShape
|
|
19
|
+
from sigima.objects.shape import (
|
|
20
|
+
CircleCoordinates,
|
|
21
|
+
EllipseCoordinates,
|
|
22
|
+
PointCoordinates,
|
|
23
|
+
PolygonCoordinates,
|
|
24
|
+
RectangleCoordinates,
|
|
25
|
+
SegmentCoordinates,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from sigima.objects import CircularROI, ImageObj, PolygonalROI, RectangularROI
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"GeometryTransformer",
|
|
34
|
+
"transformer",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GeometryTransformer:
|
|
39
|
+
"""
|
|
40
|
+
Singleton class for applying transformations to geometry objects.
|
|
41
|
+
|
|
42
|
+
Provides a unified interface for transforming both GeometryResult and ROI
|
|
43
|
+
objects using the shape coordinate system.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
_instance: GeometryTransformer | None = None
|
|
47
|
+
|
|
48
|
+
def __new__(cls) -> GeometryTransformer:
|
|
49
|
+
"""Ensure singleton pattern."""
|
|
50
|
+
if cls._instance is None:
|
|
51
|
+
cls._instance = super().__new__(cls)
|
|
52
|
+
cls._instance._initialized = False
|
|
53
|
+
return cls._instance
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
"""Initialize the transformer (only once due to singleton)."""
|
|
57
|
+
if self._initialized: # pylint: disable=access-member-before-definition
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# Mapping from GeometryResult kinds to shape coordinate classes
|
|
61
|
+
self._geometry_shape_map: dict[
|
|
62
|
+
KindShape,
|
|
63
|
+
type[
|
|
64
|
+
RectangleCoordinates
|
|
65
|
+
| CircleCoordinates
|
|
66
|
+
| PolygonCoordinates
|
|
67
|
+
| SegmentCoordinates
|
|
68
|
+
],
|
|
69
|
+
] = {
|
|
70
|
+
KindShape.POINT: PointCoordinates,
|
|
71
|
+
KindShape.RECTANGLE: RectangleCoordinates,
|
|
72
|
+
KindShape.CIRCLE: CircleCoordinates,
|
|
73
|
+
KindShape.ELLIPSE: EllipseCoordinates,
|
|
74
|
+
KindShape.POLYGON: PolygonCoordinates,
|
|
75
|
+
KindShape.SEGMENT: SegmentCoordinates,
|
|
76
|
+
KindShape.MARKER: PointCoordinates,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Mapping from ROI types to shape coordinate classes (lazy loaded)
|
|
80
|
+
self._roi_shape_map: dict[type, type] = {}
|
|
81
|
+
|
|
82
|
+
self._initialized = True
|
|
83
|
+
|
|
84
|
+
def _get_roi_shape_map(
|
|
85
|
+
self,
|
|
86
|
+
) -> dict[
|
|
87
|
+
type[CircularROI | RectangularROI | PolygonalROI],
|
|
88
|
+
type[RectangleCoordinates | CircleCoordinates | PolygonCoordinates],
|
|
89
|
+
]:
|
|
90
|
+
"""Lazy load ROI shape mapping to avoid circular imports."""
|
|
91
|
+
if not self._roi_shape_map:
|
|
92
|
+
# pylint: disable=import-outside-toplevel
|
|
93
|
+
from sigima.objects.image import CircularROI, PolygonalROI, RectangularROI
|
|
94
|
+
|
|
95
|
+
self._roi_shape_map = {
|
|
96
|
+
RectangularROI: RectangleCoordinates,
|
|
97
|
+
CircularROI: CircleCoordinates,
|
|
98
|
+
PolygonalROI: PolygonCoordinates,
|
|
99
|
+
}
|
|
100
|
+
return self._roi_shape_map
|
|
101
|
+
|
|
102
|
+
def transform_geometry(
|
|
103
|
+
self, geometry: GeometryResult, operation: str, **kwargs: Any
|
|
104
|
+
) -> GeometryResult:
|
|
105
|
+
"""
|
|
106
|
+
Transform a GeometryResult and return a new one.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
geometry: The GeometryResult to transform.
|
|
110
|
+
operation: Operation name ('rotate', 'translate', 'fliph', 'flipv',
|
|
111
|
+
'transpose', 'scale').
|
|
112
|
+
**kwargs: Operation-specific parameters.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
New GeometryResult with transformed coordinates.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ValueError: If operation is unknown or geometry kind is unsupported.
|
|
119
|
+
"""
|
|
120
|
+
coord_class = self._geometry_shape_map.get(geometry.kind)
|
|
121
|
+
if coord_class is None:
|
|
122
|
+
raise ValueError(f"Unsupported geometry kind: {geometry.kind}")
|
|
123
|
+
|
|
124
|
+
# Transform each row of coordinates
|
|
125
|
+
transformed_coords = []
|
|
126
|
+
for row in geometry.coords:
|
|
127
|
+
# Create coordinate object for this row
|
|
128
|
+
shape_coords = coord_class(row.copy())
|
|
129
|
+
|
|
130
|
+
# Apply transformation
|
|
131
|
+
self._apply_operation(shape_coords, operation, **kwargs)
|
|
132
|
+
|
|
133
|
+
transformed_coords.append(shape_coords.data)
|
|
134
|
+
|
|
135
|
+
# Create new GeometryResult with transformed coordinates
|
|
136
|
+
return GeometryResult(
|
|
137
|
+
title=geometry.title,
|
|
138
|
+
kind=geometry.kind,
|
|
139
|
+
coords=np.array(transformed_coords),
|
|
140
|
+
roi_indices=(
|
|
141
|
+
geometry.roi_indices.copy()
|
|
142
|
+
if geometry.roi_indices is not None
|
|
143
|
+
else None
|
|
144
|
+
),
|
|
145
|
+
attrs=geometry.attrs.copy(),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def transform_single_roi(
|
|
149
|
+
self,
|
|
150
|
+
single_roi: RectangularROI | CircularROI | PolygonalROI,
|
|
151
|
+
operation: str,
|
|
152
|
+
**kwargs: Any,
|
|
153
|
+
) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Transform ROI coordinates inplace.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
single_roi: ROI object with .coords attribute.
|
|
159
|
+
operation: Operation name.
|
|
160
|
+
**kwargs: Operation-specific parameters.
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
ValueError: If ROI type is unsupported or operation is unknown.
|
|
164
|
+
"""
|
|
165
|
+
roi_shape_map = self._get_roi_shape_map()
|
|
166
|
+
coord_class = roi_shape_map.get(type(single_roi))
|
|
167
|
+
if coord_class is None:
|
|
168
|
+
raise ValueError(f"Unsupported ROI type: {type(single_roi)}")
|
|
169
|
+
|
|
170
|
+
# Create shape coordinates and transform
|
|
171
|
+
shape_coords = coord_class(single_roi.coords.copy())
|
|
172
|
+
self._apply_operation(shape_coords, operation, **kwargs)
|
|
173
|
+
|
|
174
|
+
# Update ROI coordinates inplace
|
|
175
|
+
single_roi.coords[:] = shape_coords.data
|
|
176
|
+
|
|
177
|
+
def transform_roi(self, image: ImageObj, operation: str, **kwargs: Any) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Transform all ROI coordinates in an ImageObj inplace.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
image: Image object whose ROI coordinates will be transformed.
|
|
183
|
+
operation: Operation name.
|
|
184
|
+
**kwargs: Operation-specific parameters.
|
|
185
|
+
"""
|
|
186
|
+
if image.roi is None or image.roi.is_empty():
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
# Import here to avoid circular imports
|
|
190
|
+
# pylint: disable=import-outside-toplevel
|
|
191
|
+
from sigima.objects.image import ImageROI
|
|
192
|
+
|
|
193
|
+
# Determine ROI type and set up appropriate classes
|
|
194
|
+
new_roi = ImageROI()
|
|
195
|
+
|
|
196
|
+
# Transform each single ROI
|
|
197
|
+
for single_roi in image.roi.single_rois:
|
|
198
|
+
coords = single_roi.coords.copy()
|
|
199
|
+
roi_class = single_roi.__class__
|
|
200
|
+
|
|
201
|
+
# Create shape coordinates and transform
|
|
202
|
+
roi_shape_map = self._get_roi_shape_map()
|
|
203
|
+
coord_class = roi_shape_map.get(roi_class)
|
|
204
|
+
if coord_class is None:
|
|
205
|
+
raise ValueError(f"Unsupported ROI type: {roi_class}")
|
|
206
|
+
|
|
207
|
+
shape_coords = coord_class(coords)
|
|
208
|
+
self._apply_operation(shape_coords, operation, **kwargs)
|
|
209
|
+
|
|
210
|
+
new_coords = shape_coords.data
|
|
211
|
+
new_single_roi = roi_class(new_coords, single_roi.indices, single_roi.title)
|
|
212
|
+
new_roi.add_roi(new_single_roi)
|
|
213
|
+
|
|
214
|
+
image.roi = new_roi
|
|
215
|
+
|
|
216
|
+
def _apply_operation(
|
|
217
|
+
self,
|
|
218
|
+
shape_coords: (
|
|
219
|
+
PointCoordinates
|
|
220
|
+
| RectangleCoordinates
|
|
221
|
+
| CircleCoordinates
|
|
222
|
+
| EllipseCoordinates
|
|
223
|
+
| PolygonCoordinates
|
|
224
|
+
| SegmentCoordinates
|
|
225
|
+
),
|
|
226
|
+
operation: str,
|
|
227
|
+
**kwargs: Any,
|
|
228
|
+
) -> None:
|
|
229
|
+
"""
|
|
230
|
+
Apply the specified operation to shape coordinates.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
shape_coords: Shape coordinate object to transform.
|
|
234
|
+
operation: Operation name.
|
|
235
|
+
**kwargs: Operation-specific parameters.
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
ValueError: If operation is unknown.
|
|
239
|
+
"""
|
|
240
|
+
if operation == "rotate":
|
|
241
|
+
angle = kwargs.get("angle", 0)
|
|
242
|
+
center = kwargs.get("center", (0, 0))
|
|
243
|
+
shape_coords.rotate(angle, center)
|
|
244
|
+
elif operation == "translate":
|
|
245
|
+
dx = kwargs.get("dx", 0)
|
|
246
|
+
dy = kwargs.get("dy", 0)
|
|
247
|
+
shape_coords.translate(dx, dy)
|
|
248
|
+
elif operation == "fliph":
|
|
249
|
+
cx = kwargs.get("cx", 0.0)
|
|
250
|
+
shape_coords.fliph(cx)
|
|
251
|
+
elif operation == "flipv":
|
|
252
|
+
cy = kwargs.get("cy", 0.0)
|
|
253
|
+
shape_coords.flipv(cy)
|
|
254
|
+
elif operation == "transpose":
|
|
255
|
+
shape_coords.transpose()
|
|
256
|
+
elif operation == "scale":
|
|
257
|
+
sx = kwargs.get("sx", 1.0)
|
|
258
|
+
sy = kwargs.get("sy", 1.0)
|
|
259
|
+
center = kwargs.get("center", (0, 0))
|
|
260
|
+
shape_coords.scale(sx, sy, center)
|
|
261
|
+
else:
|
|
262
|
+
raise ValueError(f"Unknown operation: {operation}")
|
|
263
|
+
|
|
264
|
+
# Convenience methods for common operations
|
|
265
|
+
def rotate(
|
|
266
|
+
self,
|
|
267
|
+
obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
|
|
268
|
+
angle: float,
|
|
269
|
+
center: tuple[float, float],
|
|
270
|
+
) -> GeometryResult | None:
|
|
271
|
+
"""
|
|
272
|
+
Rotate geometry or ROI by given angle around center.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
obj: GeometryResult or single ROI object.
|
|
276
|
+
angle: Rotation angle in radians.
|
|
277
|
+
center: Center of rotation (x, y).
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
New GeometryResult if input was GeometryResult, None if ROI (inplace).
|
|
281
|
+
"""
|
|
282
|
+
if isinstance(obj, GeometryResult):
|
|
283
|
+
return self.transform_geometry(obj, "rotate", angle=angle, center=center)
|
|
284
|
+
self.transform_single_roi(obj, "rotate", angle=angle, center=center)
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
def translate(
|
|
288
|
+
self,
|
|
289
|
+
obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
|
|
290
|
+
dx: float,
|
|
291
|
+
dy: float,
|
|
292
|
+
) -> GeometryResult | None:
|
|
293
|
+
"""
|
|
294
|
+
Translate geometry or ROI by given offset.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
obj: GeometryResult or single ROI object.
|
|
298
|
+
dx: Translation in x direction.
|
|
299
|
+
dy: Translation in y direction.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
New GeometryResult if input was GeometryResult, None if ROI (inplace).
|
|
303
|
+
"""
|
|
304
|
+
if isinstance(obj, GeometryResult):
|
|
305
|
+
return self.transform_geometry(obj, "translate", dx=dx, dy=dy)
|
|
306
|
+
self.transform_single_roi(obj, "translate", dx=dx, dy=dy)
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
def fliph(
|
|
310
|
+
self,
|
|
311
|
+
obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
|
|
312
|
+
cx: float,
|
|
313
|
+
) -> GeometryResult | None:
|
|
314
|
+
"""
|
|
315
|
+
Flip geometry or ROI horizontally around given x-coordinate.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
obj: GeometryResult or single ROI object.
|
|
319
|
+
cx: X-coordinate of flip axis.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
New GeometryResult if input was GeometryResult, None if ROI (inplace).
|
|
323
|
+
"""
|
|
324
|
+
if isinstance(obj, GeometryResult):
|
|
325
|
+
return self.transform_geometry(obj, "fliph", cx=cx)
|
|
326
|
+
self.transform_single_roi(obj, "fliph", cx=cx)
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
def flipv(
|
|
330
|
+
self,
|
|
331
|
+
obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
|
|
332
|
+
cy: float,
|
|
333
|
+
) -> GeometryResult | None:
|
|
334
|
+
"""
|
|
335
|
+
Flip geometry or ROI vertically around given y-coordinate.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
obj: GeometryResult or single ROI object.
|
|
339
|
+
cy: Y-coordinate of flip axis.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
New GeometryResult if input was GeometryResult, None if ROI (inplace).
|
|
343
|
+
"""
|
|
344
|
+
if isinstance(obj, GeometryResult):
|
|
345
|
+
return self.transform_geometry(obj, "flipv", cy=cy)
|
|
346
|
+
self.transform_single_roi(obj, "flipv", cy=cy)
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
def transpose(
|
|
350
|
+
self, obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI
|
|
351
|
+
) -> GeometryResult | None:
|
|
352
|
+
"""
|
|
353
|
+
Transpose geometry or ROI (swap x and y coordinates).
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
obj: GeometryResult or single ROI object.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
New GeometryResult if input was GeometryResult, None if ROI (inplace).
|
|
360
|
+
"""
|
|
361
|
+
if isinstance(obj, GeometryResult):
|
|
362
|
+
return self.transform_geometry(obj, "transpose")
|
|
363
|
+
self.transform_single_roi(obj, "transpose")
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
def scale(
|
|
367
|
+
self,
|
|
368
|
+
obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
|
|
369
|
+
sx: float,
|
|
370
|
+
sy: float,
|
|
371
|
+
center: tuple[float, float],
|
|
372
|
+
) -> GeometryResult | None:
|
|
373
|
+
"""
|
|
374
|
+
Scale geometry or ROI by given factors around center.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
obj: GeometryResult or single ROI object.
|
|
378
|
+
sx: Scale factor in x direction.
|
|
379
|
+
sy: Scale factor in y direction.
|
|
380
|
+
center: Center of scaling (x, y).
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
New GeometryResult if input was GeometryResult, None if ROI (inplace).
|
|
384
|
+
"""
|
|
385
|
+
if isinstance(obj, GeometryResult):
|
|
386
|
+
return self.transform_geometry(obj, "scale", sx=sx, sy=sy, center=center)
|
|
387
|
+
self.transform_single_roi(obj, "scale", sx=sx, sy=sy, center=center)
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
#: Global singleton instance of GeometryTransformer for applying geometric
|
|
392
|
+
#: transformations to geometry results and ROI objects.
|
|
393
|
+
transformer = GeometryTransformer()
|