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,312 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Blob detection tests
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
8
|
+
# pylint: disable=duplicate-code
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
import sigima.objects
|
|
18
|
+
import sigima.params
|
|
19
|
+
import sigima.proc.image
|
|
20
|
+
from sigima.tests import guiutils
|
|
21
|
+
from sigima.tests.env import execenv
|
|
22
|
+
|
|
23
|
+
CV2_AVAILABLE = importlib.util.find_spec("cv2") is not None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_simple_blob_test_image() -> np.ndarray:
|
|
27
|
+
"""Create a simple test image with one obvious blob for debugging.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Simple test image with a clear blob
|
|
31
|
+
"""
|
|
32
|
+
# Create a simple 100x100 image with a clear circular blob
|
|
33
|
+
size = 100
|
|
34
|
+
data = np.zeros((size, size), dtype=np.float64)
|
|
35
|
+
|
|
36
|
+
# Add a clear circular blob in the center
|
|
37
|
+
y, x = np.ogrid[:size, :size]
|
|
38
|
+
center_x, center_y = size // 2, size // 2
|
|
39
|
+
radius = 10
|
|
40
|
+
|
|
41
|
+
# Create a circular blob (step function, not Gaussian)
|
|
42
|
+
mask = (x - center_x) ** 2 + (y - center_y) ** 2 < radius**2
|
|
43
|
+
data[mask] = 1.0
|
|
44
|
+
|
|
45
|
+
# Keep as float64 for blob detection
|
|
46
|
+
return data
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def create_blob_test_image(
|
|
50
|
+
size: int = 200,
|
|
51
|
+
n_blobs: int = 5,
|
|
52
|
+
blob_radius: float = 10.0,
|
|
53
|
+
blob_intensity: float = 1000.0,
|
|
54
|
+
noise_level: float = 50.0,
|
|
55
|
+
seed: int | None = None,
|
|
56
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
57
|
+
"""Create a test image with synthetic blobs for blob detection testing.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
size: Image size (square image)
|
|
61
|
+
n_blobs: Number of blobs to create
|
|
62
|
+
blob_radius: Radius of the blobs in pixels
|
|
63
|
+
blob_intensity: Intensity of the blobs
|
|
64
|
+
noise_level: Standard deviation of Gaussian noise
|
|
65
|
+
seed: Random seed for reproducible results
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Tuple of (image_data, expected_coords) where expected_coords is an array
|
|
69
|
+
of shape (n_blobs, 3) with columns [x, y, radius]
|
|
70
|
+
"""
|
|
71
|
+
rng = np.random.default_rng(seed)
|
|
72
|
+
|
|
73
|
+
# Create base image with noise
|
|
74
|
+
data = rng.normal(0, noise_level, size=(size, size)).astype(np.float64)
|
|
75
|
+
|
|
76
|
+
# Generate blob centers avoiding edges
|
|
77
|
+
margin = int(blob_radius * 2)
|
|
78
|
+
valid_range = size - 2 * margin
|
|
79
|
+
|
|
80
|
+
blob_centers = []
|
|
81
|
+
for _ in range(n_blobs):
|
|
82
|
+
x = margin + rng.random() * valid_range
|
|
83
|
+
y = margin + rng.random() * valid_range
|
|
84
|
+
blob_centers.append((x, y))
|
|
85
|
+
|
|
86
|
+
# Add circular blobs to the image
|
|
87
|
+
expected_coords = []
|
|
88
|
+
y_grid, x_grid = np.ogrid[:size, :size]
|
|
89
|
+
|
|
90
|
+
for x_center, y_center in blob_centers:
|
|
91
|
+
# Create a circular blob using step function
|
|
92
|
+
mask = (x_grid - x_center) ** 2 + (y_grid - y_center) ** 2 < blob_radius**2
|
|
93
|
+
data[mask] += blob_intensity
|
|
94
|
+
|
|
95
|
+
# Store expected coordinates with radius
|
|
96
|
+
expected_coords.append([x_center, y_center, blob_radius])
|
|
97
|
+
|
|
98
|
+
# Ensure positive values and convert to float64 for blob detection
|
|
99
|
+
data = np.maximum(data, 0)
|
|
100
|
+
data = data.astype(np.float64)
|
|
101
|
+
# Normalize to [0, 1] range for blob detection algorithms
|
|
102
|
+
if data.max() > 0:
|
|
103
|
+
data = data / data.max()
|
|
104
|
+
|
|
105
|
+
return data, np.array(expected_coords)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pytest.mark.validation
|
|
109
|
+
def test_image_blob_dog():
|
|
110
|
+
"""Blob detection using Difference of Gaussian (DoG) method validation test"""
|
|
111
|
+
execenv.print("Testing blob_dog detection...")
|
|
112
|
+
|
|
113
|
+
# Test 1: Simple single blob
|
|
114
|
+
data = create_simple_blob_test_image()
|
|
115
|
+
obj = sigima.objects.create_image("blob_dog_simple", data=data)
|
|
116
|
+
param = sigima.params.BlobDOGParam.create(
|
|
117
|
+
min_sigma=1.0,
|
|
118
|
+
max_sigma=20.0,
|
|
119
|
+
threshold_rel=0.01,
|
|
120
|
+
overlap=0.5,
|
|
121
|
+
exclude_border=False,
|
|
122
|
+
)
|
|
123
|
+
result = sigima.proc.image.blob_dog(obj, param)
|
|
124
|
+
assert result is not None, "Simple blob detection should return results"
|
|
125
|
+
assert len(result.coords) > 0, "Should detect at least one blob in simple case"
|
|
126
|
+
execenv.print(f"✓ DoG simple: detected {len(result.coords)} blobs")
|
|
127
|
+
|
|
128
|
+
# Test 2: Multiple blobs (simplified)
|
|
129
|
+
data, expected_coords = create_blob_test_image(
|
|
130
|
+
size=150, n_blobs=2, blob_radius=12.0, noise_level=0.1, seed=42
|
|
131
|
+
)
|
|
132
|
+
obj = sigima.objects.create_image("blob_dog_multi", data=data)
|
|
133
|
+
param = sigima.params.BlobDOGParam.create(
|
|
134
|
+
min_sigma=5.0,
|
|
135
|
+
max_sigma=20.0,
|
|
136
|
+
threshold_rel=0.05,
|
|
137
|
+
overlap=0.3,
|
|
138
|
+
exclude_border=True,
|
|
139
|
+
)
|
|
140
|
+
result = sigima.proc.image.blob_dog(obj, param)
|
|
141
|
+
guiutils.view_images_if_gui(
|
|
142
|
+
obj,
|
|
143
|
+
title="DoG multi blob detection test image",
|
|
144
|
+
results=[result],
|
|
145
|
+
colormap="gray",
|
|
146
|
+
)
|
|
147
|
+
if result is not None and len(result.coords) > 0:
|
|
148
|
+
detected_count = len(result.coords)
|
|
149
|
+
expected_count = len(expected_coords)
|
|
150
|
+
execenv.print(
|
|
151
|
+
f"✓ DoG multi: detected {detected_count} blobs (expected ~{expected_count})"
|
|
152
|
+
)
|
|
153
|
+
# Validate coordinate format: should be [x, y, radius]
|
|
154
|
+
assert result.coords.shape[1] == 3, (
|
|
155
|
+
"Coordinates should have 3 columns [x, y, radius]"
|
|
156
|
+
)
|
|
157
|
+
# Check that all radii are positive
|
|
158
|
+
assert np.all(result.coords[:, 2] > 0), (
|
|
159
|
+
"All detected blob radii should be positive"
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
execenv.print("✓ DoG multi: no blobs detected (acceptable for noisy case)")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@pytest.mark.validation
|
|
166
|
+
def test_image_blob_doh():
|
|
167
|
+
"""Blob detection using Determinant of Hessian (DoH) method validation test"""
|
|
168
|
+
execenv.print("Testing blob_doh detection...")
|
|
169
|
+
|
|
170
|
+
# Start with a simple test case
|
|
171
|
+
data = create_simple_blob_test_image()
|
|
172
|
+
obj = sigima.objects.create_image("blob_doh_test", data=data)
|
|
173
|
+
param = sigima.params.BlobDOHParam.create(
|
|
174
|
+
min_sigma=1.0,
|
|
175
|
+
max_sigma=20.0,
|
|
176
|
+
threshold_rel=0.01,
|
|
177
|
+
overlap=0.5,
|
|
178
|
+
log_scale=False,
|
|
179
|
+
)
|
|
180
|
+
result = sigima.proc.image.blob_doh(obj, param)
|
|
181
|
+
assert result is not None, "Blob detection should return results"
|
|
182
|
+
assert len(result.coords) > 0, "Should detect at least one blob"
|
|
183
|
+
execenv.print(f"✓ DoH: detected {len(result.coords)} blobs")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@pytest.mark.validation
|
|
187
|
+
def test_image_blob_log():
|
|
188
|
+
"""Blob detection using Laplacian of Gaussian (LoG) method validation test"""
|
|
189
|
+
execenv.print("Testing blob_log detection...")
|
|
190
|
+
|
|
191
|
+
# Start with a simple test case
|
|
192
|
+
data = create_simple_blob_test_image()
|
|
193
|
+
obj = sigima.objects.create_image("blob_log_test", data=data)
|
|
194
|
+
param = sigima.params.BlobLOGParam.create(
|
|
195
|
+
min_sigma=1.0,
|
|
196
|
+
max_sigma=20.0,
|
|
197
|
+
threshold_rel=0.01,
|
|
198
|
+
overlap=0.5,
|
|
199
|
+
log_scale=False,
|
|
200
|
+
exclude_border=False,
|
|
201
|
+
)
|
|
202
|
+
result = sigima.proc.image.blob_log(obj, param)
|
|
203
|
+
assert result is not None, "Blob detection should return results"
|
|
204
|
+
assert len(result.coords) > 0, "Should detect at least one blob"
|
|
205
|
+
execenv.print(f"✓ LoG: detected {len(result.coords)} blobs")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@pytest.mark.validation
|
|
209
|
+
@pytest.mark.skipif(not CV2_AVAILABLE, reason="OpenCV (cv2) is not available")
|
|
210
|
+
def test_image_blob_opencv():
|
|
211
|
+
"""Blob detection using OpenCV method validation test"""
|
|
212
|
+
execenv.print("Testing blob_opencv detection...")
|
|
213
|
+
|
|
214
|
+
# Start with a simple test case
|
|
215
|
+
data = create_simple_blob_test_image()
|
|
216
|
+
obj = sigima.objects.create_image("blob_opencv_test", data=data)
|
|
217
|
+
param = sigima.params.BlobOpenCVParam.create(
|
|
218
|
+
min_threshold=10.0,
|
|
219
|
+
max_threshold=200.0,
|
|
220
|
+
min_repeatability=2,
|
|
221
|
+
min_dist_between_blobs=10.0,
|
|
222
|
+
filter_by_color=False,
|
|
223
|
+
blob_color=0,
|
|
224
|
+
filter_by_area=True,
|
|
225
|
+
min_area=10.0,
|
|
226
|
+
max_area=1000.0,
|
|
227
|
+
filter_by_circularity=False,
|
|
228
|
+
min_circularity=0.1,
|
|
229
|
+
max_circularity=1.0,
|
|
230
|
+
filter_by_inertia=False,
|
|
231
|
+
min_inertia_ratio=0.1,
|
|
232
|
+
max_inertia_ratio=1.0,
|
|
233
|
+
filter_by_convexity=False,
|
|
234
|
+
min_convexity=0.1,
|
|
235
|
+
max_convexity=1.0,
|
|
236
|
+
)
|
|
237
|
+
result = sigima.proc.image.blob_opencv(obj, param)
|
|
238
|
+
assert result is not None, "Blob detection should return results"
|
|
239
|
+
assert len(result.coords) > 0, "Should detect at least one blob"
|
|
240
|
+
execenv.print(f"✓ OpenCV: detected {len(result.coords)} blobs")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_blob_detection_consistency():
|
|
244
|
+
"""Test that different blob detection methods produce consistent results"""
|
|
245
|
+
execenv.print("Testing blob detection consistency across methods...")
|
|
246
|
+
|
|
247
|
+
# Create a simple test image with well-separated blobs
|
|
248
|
+
data = create_simple_blob_test_image()
|
|
249
|
+
|
|
250
|
+
# Test parameters for each method
|
|
251
|
+
methods_and_params = [
|
|
252
|
+
(
|
|
253
|
+
"dog",
|
|
254
|
+
sigima.params.BlobDOGParam.create(
|
|
255
|
+
min_sigma=1.0, max_sigma=20.0, threshold_rel=0.01, overlap=0.3
|
|
256
|
+
),
|
|
257
|
+
),
|
|
258
|
+
(
|
|
259
|
+
"log",
|
|
260
|
+
sigima.params.BlobLOGParam.create(
|
|
261
|
+
min_sigma=1.0, max_sigma=20.0, threshold_rel=0.01, overlap=0.3
|
|
262
|
+
),
|
|
263
|
+
),
|
|
264
|
+
(
|
|
265
|
+
"doh",
|
|
266
|
+
sigima.params.BlobDOHParam.create(
|
|
267
|
+
min_sigma=1.0, max_sigma=20.0, threshold_rel=0.01, overlap=0.3
|
|
268
|
+
),
|
|
269
|
+
),
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
results = {}
|
|
273
|
+
for method_name, param in methods_and_params:
|
|
274
|
+
obj = sigima.objects.create_image(f"blob_{method_name}_consistency", data=data)
|
|
275
|
+
|
|
276
|
+
if method_name == "dog":
|
|
277
|
+
result = sigima.proc.image.blob_dog(obj, param)
|
|
278
|
+
elif method_name == "log":
|
|
279
|
+
result = sigima.proc.image.blob_log(obj, param)
|
|
280
|
+
elif method_name == "doh":
|
|
281
|
+
result = sigima.proc.image.blob_doh(obj, param)
|
|
282
|
+
|
|
283
|
+
results[method_name] = result
|
|
284
|
+
if result is not None:
|
|
285
|
+
execenv.print(f"{method_name.upper()}: detected {len(result.coords)} blobs")
|
|
286
|
+
else:
|
|
287
|
+
execenv.print(f"{method_name.upper()}: no blobs detected")
|
|
288
|
+
|
|
289
|
+
# All methods should detect at least one blob (or we skip if none work)
|
|
290
|
+
working_methods = [
|
|
291
|
+
name
|
|
292
|
+
for name, result in results.items()
|
|
293
|
+
if result is not None and len(result.coords) > 0
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
if len(working_methods) == 0:
|
|
297
|
+
pytest.skip("No blob detection methods returned results")
|
|
298
|
+
|
|
299
|
+
for method_name, result in results.items():
|
|
300
|
+
if result is not None:
|
|
301
|
+
assert len(result.coords) > 0, (
|
|
302
|
+
f"{method_name} should detect at least one blob"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
if __name__ == "__main__":
|
|
307
|
+
guiutils.enable_gui()
|
|
308
|
+
test_image_blob_dog()
|
|
309
|
+
test_image_blob_doh()
|
|
310
|
+
test_image_blob_log()
|
|
311
|
+
test_image_blob_opencv()
|
|
312
|
+
test_blob_detection_consistency()
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Image centroid computation test
|
|
5
|
+
|
|
6
|
+
Comparing different algorithms for centroid calculation:
|
|
7
|
+
|
|
8
|
+
- SciPy (measurements.center_of_mass)
|
|
9
|
+
- OpenCV (moments)
|
|
10
|
+
- Method based on moments
|
|
11
|
+
- Method based on Fourier (Sigima's algorithm)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
15
|
+
# pylint: disable=duplicate-code
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import pytest
|
|
23
|
+
from numpy import ma
|
|
24
|
+
from skimage import measure
|
|
25
|
+
|
|
26
|
+
import sigima.objects
|
|
27
|
+
import sigima.proc.image
|
|
28
|
+
import sigima.tools.image
|
|
29
|
+
from sigima.config import _
|
|
30
|
+
from sigima.tests import guiutils
|
|
31
|
+
from sigima.tests.data import (
|
|
32
|
+
create_noisy_gaussian_image,
|
|
33
|
+
get_laser_spot_data,
|
|
34
|
+
get_test_image,
|
|
35
|
+
)
|
|
36
|
+
from sigima.tests.env import execenv
|
|
37
|
+
from sigima.tests.helpers import check_scalar_result
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_centroid_from_moments(data: np.ndarray) -> tuple[int, int]:
|
|
41
|
+
"""Computing centroid from image moments
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
data: 2D array of image data
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Tuple with centroid coordinates (y, x)
|
|
48
|
+
"""
|
|
49
|
+
y, x = np.ogrid[: data.shape[0], : data.shape[1]]
|
|
50
|
+
imx, imy = data.sum(axis=0)[None, :], data.sum(axis=1)[:, None]
|
|
51
|
+
m00 = np.array(data, dtype=float).sum() or 1.0
|
|
52
|
+
m10 = (np.array(imx, dtype=float) * x).sum() / m00
|
|
53
|
+
m01 = (np.array(imy, dtype=float) * y).sum() / m00
|
|
54
|
+
return int(m01), int(m10)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_centroid_with_cv2(data: np.ndarray) -> tuple[int, int]:
|
|
58
|
+
"""Compute centroid from moments with OpenCV
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
data: 2D array of image data
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Tuple with centroid coordinates (y, x)
|
|
65
|
+
"""
|
|
66
|
+
import cv2 # pylint: disable=import-outside-toplevel
|
|
67
|
+
|
|
68
|
+
m = cv2.moments(data)
|
|
69
|
+
col = int(m["m10"] / m["m00"])
|
|
70
|
+
row = int(m["m01"] / m["m00"])
|
|
71
|
+
return row, col
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def __compare_centroid_funcs(data: np.ndarray) -> None:
|
|
75
|
+
"""Compare different centroid computation methods
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
data: 2D array of image data
|
|
79
|
+
"""
|
|
80
|
+
# pylint: disable=import-outside-toplevel
|
|
81
|
+
from plotpy.builder import make
|
|
82
|
+
|
|
83
|
+
from sigima.tests import vistools
|
|
84
|
+
|
|
85
|
+
items = []
|
|
86
|
+
items += [make.image(data, interpolation="nearest", eliminate_outliers=2.0)]
|
|
87
|
+
# Computing centroid coordinates
|
|
88
|
+
for name, func in (
|
|
89
|
+
# ("SciPy", spi.center_of_mass),
|
|
90
|
+
# ("OpenCV", get_centroid_with_cv2),
|
|
91
|
+
("scikit-image", measure.centroid),
|
|
92
|
+
# ("Moments", get_centroid_from_moments),
|
|
93
|
+
("Fourier", sigima.tools.image.get_centroid_fourier),
|
|
94
|
+
("Auto", sigima.tools.image.get_centroid_auto),
|
|
95
|
+
("Projected Profile Median", sigima.tools.image.get_projected_profile_centroid),
|
|
96
|
+
(
|
|
97
|
+
"Projected Profile Barycenter",
|
|
98
|
+
lambda d: sigima.tools.image.get_projected_profile_centroid(
|
|
99
|
+
d, method="barycenter"
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
):
|
|
103
|
+
try:
|
|
104
|
+
t0 = time.time()
|
|
105
|
+
y, x = func(data)
|
|
106
|
+
dt = time.time() - t0
|
|
107
|
+
label = " " + f"{_('Centroid')}[{name}] (x=%s, y=%s)"
|
|
108
|
+
execenv.print(label % (x, y))
|
|
109
|
+
cursor = make.xcursor(x, y, label=label)
|
|
110
|
+
cursor.setTitle(name)
|
|
111
|
+
items.append(cursor)
|
|
112
|
+
execenv.print(f" Calculation time: {int(dt * 1e3):d} ms")
|
|
113
|
+
except ImportError:
|
|
114
|
+
execenv.print(f" Unable to compute {name}: missing module")
|
|
115
|
+
vistools.view_image_items(items)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.mark.gui
|
|
119
|
+
def test_image_centroid_interactive() -> None:
|
|
120
|
+
"""Interactive test for image centroid computation
|
|
121
|
+
|
|
122
|
+
This test will display the centroid of laser spot data using different methods.
|
|
123
|
+
It will also print the centroid coordinates and computation time for each method.
|
|
124
|
+
"""
|
|
125
|
+
with guiutils.lazy_qt_app_context(force=True):
|
|
126
|
+
centroid_test_data = get_test_image("centroid_test.npy").data
|
|
127
|
+
for data in get_laser_spot_data() + [centroid_test_data]:
|
|
128
|
+
execenv.print(f"Data[dtype={data.dtype},shape={data.shape}]")
|
|
129
|
+
# Testing with masked arrays
|
|
130
|
+
__compare_centroid_funcs(data.view(ma.MaskedArray))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def __check_centroid(
|
|
134
|
+
image: sigima.objects.ImageObj, expected_x: float, expected_y: float, debug_str: str
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Check centroid computation
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
image: Image object to compute centroid from
|
|
140
|
+
expected_x: Expected x coordinate of the centroid
|
|
141
|
+
expected_y: Expected y coordinate of the centroid
|
|
142
|
+
debug_str: Debug string for logging
|
|
143
|
+
"""
|
|
144
|
+
geometry = sigima.proc.image.centroid(image)
|
|
145
|
+
x, y = geometry.coords[0]
|
|
146
|
+
check_scalar_result(f"Centroid X [{debug_str}]", x, expected_x, atol=1.0)
|
|
147
|
+
check_scalar_result(f"Centroid Y [{debug_str}]", y, expected_y, atol=1.0)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.validation
|
|
151
|
+
def test_image_centroid() -> None:
|
|
152
|
+
"""Test centroid computation"""
|
|
153
|
+
param = sigima.objects.NewImageParam.create(height=500, width=500)
|
|
154
|
+
image = create_noisy_gaussian_image(param, center=(-2.0, 3.0), add_annotations=True)
|
|
155
|
+
circle_roi = sigima.objects.create_image_roi("circle", [200, 325, 10], indices=True)
|
|
156
|
+
for roi, x0, y0 in (
|
|
157
|
+
(None, 0.0, 0.0),
|
|
158
|
+
(None, 100.0, 100.0),
|
|
159
|
+
(circle_roi, 0.0, 0.0),
|
|
160
|
+
(circle_roi, 100.0, 100.0), # Test for regression like #106
|
|
161
|
+
):
|
|
162
|
+
image.roi = roi
|
|
163
|
+
image.set_uniform_coords(image.dx, image.dy, x0, y0)
|
|
164
|
+
debug_str = f"{roi}, x0: {x0}, y0: {y0}"
|
|
165
|
+
__check_centroid(image, 200.0 + x0, 325.0 + y0, debug_str)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
test_image_centroid_interactive()
|
|
170
|
+
test_image_centroid()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""Unit tests for 2D-array function checks decorators."""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from sigima.tools.checks import check_2d_array
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@check_2d_array(dtype=np.floating)
|
|
12
|
+
def identity(data: np.ndarray) -> np.ndarray:
|
|
13
|
+
"""Dummy image function returning input."""
|
|
14
|
+
return data
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_valid_2d_float_input() -> None:
|
|
18
|
+
"""Test with valid 2D float array."""
|
|
19
|
+
data = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float64)
|
|
20
|
+
result = identity(data)
|
|
21
|
+
np.testing.assert_array_equal(result, data)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_wrong_ndim() -> None:
|
|
25
|
+
"""Test with non-2D input."""
|
|
26
|
+
data = np.array([1.0, 2.0, 3.0], dtype=float) # 1D
|
|
27
|
+
with pytest.raises(ValueError, match="Input array must be 2D"):
|
|
28
|
+
identity(data)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_wrong_dtype() -> None:
|
|
32
|
+
"""Test with wrong dtype."""
|
|
33
|
+
data = np.array([[1, 2], [3, 4]], dtype=int)
|
|
34
|
+
with pytest.raises(
|
|
35
|
+
TypeError, match="Input array must be of type <class 'numpy.floating'>"
|
|
36
|
+
):
|
|
37
|
+
identity(data)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@check_2d_array(non_constant=True)
|
|
41
|
+
def normalize(data: np.ndarray) -> np.ndarray:
|
|
42
|
+
"""Normalize 2D array to range [0, 1]."""
|
|
43
|
+
return data / np.nanmax(data)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_non_constant_error() -> None:
|
|
47
|
+
"""Test with non-constant input."""
|
|
48
|
+
data = np.full((3, 3), 5.0)
|
|
49
|
+
with pytest.raises(ValueError, match="Input array has no dynamic range."):
|
|
50
|
+
normalize(data)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@check_2d_array(finite_only=True)
|
|
54
|
+
def sum_image(data: np.ndarray) -> float:
|
|
55
|
+
"""Sum all finite values in a 2D array."""
|
|
56
|
+
return np.sum(data)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_non_finite_values() -> None:
|
|
60
|
+
"""Test with non-finite values."""
|
|
61
|
+
data = np.array([[1.0, np.inf], [3.0, np.nan]])
|
|
62
|
+
with pytest.raises(ValueError, match="Input array contains non-finite values."):
|
|
63
|
+
sum_image(data)
|