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,392 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
ROI advanced unit tests
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
8
|
+
# guitest: show
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
import sigima.objects
|
|
16
|
+
import sigima.params
|
|
17
|
+
import sigima.proc.signal
|
|
18
|
+
from sigima.tests.data import create_paracetamol_signal
|
|
19
|
+
from sigima.tests.helpers import print_obj_data_dimensions
|
|
20
|
+
|
|
21
|
+
SIZE = 200
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def __create_test_signal() -> sigima.objects.SignalObj:
|
|
25
|
+
"""Create a test signal."""
|
|
26
|
+
return create_paracetamol_signal(size=SIZE)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_signal_roi_merge() -> None:
|
|
30
|
+
"""Test signal ROI merge"""
|
|
31
|
+
# Create a signal object with a single ROI, and another one with another ROI.
|
|
32
|
+
# Compute the average of the two objects, and check if the resulting object
|
|
33
|
+
# has the expected ROI (i.e. the union of the original object's ROI).
|
|
34
|
+
obj1 = __create_test_signal()
|
|
35
|
+
obj2 = __create_test_signal()
|
|
36
|
+
obj2.roi = sigima.objects.create_signal_roi([60, 120], indices=True)
|
|
37
|
+
obj1.roi = sigima.objects.create_signal_roi([50, 100], indices=True)
|
|
38
|
+
|
|
39
|
+
# Compute the average of the two objects
|
|
40
|
+
obj3 = sigima.proc.signal.average([obj1, obj2])
|
|
41
|
+
assert obj3.roi is not None, "Merged object should have a ROI"
|
|
42
|
+
assert len(obj3.roi) == 2, "Merged object should have two single ROIs"
|
|
43
|
+
for single_roi in obj3.roi:
|
|
44
|
+
assert single_roi.get_indices_coords(obj3) in ([50, 100], [60, 120]), (
|
|
45
|
+
"Merged object should have the union of the original object's ROIs"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_signal_roi_combine() -> None:
|
|
50
|
+
"""Test `SignalROI.combine_with` method"""
|
|
51
|
+
coords1, coords2 = [60, 120], [50, 100]
|
|
52
|
+
roi1 = sigima.objects.create_signal_roi(coords1, indices=True)
|
|
53
|
+
roi2 = sigima.objects.create_signal_roi(coords2, indices=True)
|
|
54
|
+
exp_combined = sigima.objects.create_signal_roi([coords1, coords2], indices=True)
|
|
55
|
+
# Check that combining two ROIs results in a new ROI with both coordinates:
|
|
56
|
+
roi3 = roi1.combine_with(roi2)
|
|
57
|
+
assert roi3 == exp_combined, "Combined ROI should match expected"
|
|
58
|
+
# Check that combining again with the same ROI does not change it:
|
|
59
|
+
roi3 = roi1.combine_with(roi2)
|
|
60
|
+
assert roi3 == exp_combined, "Combining with the same ROI should not change it"
|
|
61
|
+
# Check that combining with an image ROI raises an error:
|
|
62
|
+
with pytest.raises(
|
|
63
|
+
TypeError, match=r"Cannot combine([\S ]*)SignalROI([\S ]*)ImageROI"
|
|
64
|
+
):
|
|
65
|
+
roi1.combine_with(sigima.objects.create_image_roi("rectangle", [0, 0, 10, 10]))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Signal ROIs:
|
|
69
|
+
SROI1 = [26, 41]
|
|
70
|
+
SROI2 = [125, 146]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def __roi_str(obj: sigima.objects.SignalObj) -> str:
|
|
74
|
+
"""Return a string representation of a SignalROI object for context."""
|
|
75
|
+
if obj.roi is None:
|
|
76
|
+
return "None"
|
|
77
|
+
if obj.roi.is_empty():
|
|
78
|
+
return "Empty"
|
|
79
|
+
return ", ".join(
|
|
80
|
+
f"[{r.get_indices_coords(obj)[0]}, {r.get_indices_coords(obj)[1]}]"
|
|
81
|
+
for r in obj.roi.single_rois
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def __create_test_roi() -> sigima.objects.SignalROI:
|
|
86
|
+
"""Create a test ROI."""
|
|
87
|
+
return sigima.objects.create_signal_roi([SROI1, SROI2], indices=True)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def __test_processing_in_roi(src: sigima.objects.SignalObj) -> None:
|
|
91
|
+
"""Run signal processing in ROI.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
src: The source signal object (with or without ROI)
|
|
95
|
+
"""
|
|
96
|
+
print_obj_data_dimensions(src)
|
|
97
|
+
value = 1
|
|
98
|
+
p = sigima.params.ConstantParam.create(value=value)
|
|
99
|
+
dst = sigima.proc.signal.addition_constant(src, p)
|
|
100
|
+
orig = src.data
|
|
101
|
+
new = dst.data
|
|
102
|
+
context = f" [ROI: {__roi_str(src)}]"
|
|
103
|
+
if src.roi is not None and not src.roi.is_empty():
|
|
104
|
+
# Check if the processed data is correct: signal should be the same as the
|
|
105
|
+
# original data outside the ROI, and should be different inside the ROI.
|
|
106
|
+
assert not np.any(new[SROI1[0] : SROI1[1]] == orig[SROI1[0] : SROI1[1]]), (
|
|
107
|
+
f"Signal ROI 1 data mismatch{context}"
|
|
108
|
+
)
|
|
109
|
+
assert not np.any(new[SROI2[0] : SROI2[1]] == orig[SROI2[0] : SROI2[1]]), (
|
|
110
|
+
f"Signal ROI 2 data mismatch{context}"
|
|
111
|
+
)
|
|
112
|
+
assert np.all(new[: SROI1[0]] == orig[: SROI1[0]]), (
|
|
113
|
+
f"Signal before ROI 1 data mismatch{context}"
|
|
114
|
+
)
|
|
115
|
+
assert np.all(new[SROI1[1] : SROI2[0]] == orig[SROI1[1] : SROI2[0]]), (
|
|
116
|
+
f"Signal between ROIs data mismatch{context}"
|
|
117
|
+
)
|
|
118
|
+
assert np.all(new[SROI2[1] :] == orig[SROI2[1] :]), (
|
|
119
|
+
f"Signal after ROI 2 data mismatch{context}"
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
# No ROI: all data should be changed
|
|
123
|
+
assert np.all(new == orig + value), f"Signal data mismatch{context}"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_signal_roi_processing() -> None:
|
|
127
|
+
"""Test signal ROI processing"""
|
|
128
|
+
src = __create_test_signal()
|
|
129
|
+
base_roi = __create_test_roi()
|
|
130
|
+
empty_roi = sigima.objects.SignalROI()
|
|
131
|
+
for roi in (None, empty_roi, base_roi):
|
|
132
|
+
src.roi = roi
|
|
133
|
+
__test_processing_in_roi(src)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_empty_signal_roi() -> None:
|
|
137
|
+
"""Test empty signal ROI"""
|
|
138
|
+
src = __create_test_signal()
|
|
139
|
+
empty_roi = sigima.objects.SignalROI()
|
|
140
|
+
for roi in (None, empty_roi):
|
|
141
|
+
src.roi = roi
|
|
142
|
+
context = f" [ROI: {__roi_str(src)}]"
|
|
143
|
+
assert src.roi is None or src.roi.is_empty(), (
|
|
144
|
+
f"Source object ROI should be empty or None{context}"
|
|
145
|
+
)
|
|
146
|
+
if src.roi is not None:
|
|
147
|
+
# No ROI has been set in the source signal
|
|
148
|
+
sig1 = sigima.proc.signal.extract_roi(src, src.roi.to_params(src))
|
|
149
|
+
assert sig1.data.size == 0, f"Extracted signal should be empty{context}"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@pytest.mark.validation
|
|
153
|
+
def test_signal_extract_rois() -> None:
|
|
154
|
+
"""Validation test for signal ROI extraction into a single object"""
|
|
155
|
+
src = __create_test_signal()
|
|
156
|
+
src.roi = __create_test_roi()
|
|
157
|
+
context = f" [ROI: {__roi_str(src)}]"
|
|
158
|
+
size_roi1, size_roi2 = SROI1[1] - SROI1[0], SROI2[1] - SROI2[0]
|
|
159
|
+
assert len(src.roi) == 2, f"Source object should have two ROIs{context}"
|
|
160
|
+
# Single object mode: merge all ROIs into a single object
|
|
161
|
+
sig1 = sigima.proc.signal.extract_rois(src, src.roi.to_params(src))
|
|
162
|
+
assert sig1.data.size == size_roi1 + size_roi2, f"Signal size mismatch{context}"
|
|
163
|
+
assert np.all(sig1.data[:size_roi1] == src.data[SROI1[0] : SROI1[1]]), (
|
|
164
|
+
f"Signal 1 data mismatch{context}"
|
|
165
|
+
)
|
|
166
|
+
assert np.all(sig1.data[size_roi1:] == src.data[SROI2[0] : SROI2[1]]), (
|
|
167
|
+
f"Signal 2 data mismatch{context}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@pytest.mark.validation
|
|
172
|
+
def test_signal_extract_roi() -> None:
|
|
173
|
+
"""Validation test for signal ROI extraction into multiple objects"""
|
|
174
|
+
src = __create_test_signal()
|
|
175
|
+
src.roi = __create_test_roi()
|
|
176
|
+
context = f" [ROI: {__roi_str(src)}]"
|
|
177
|
+
size_roi1, size_roi2 = SROI1[1] - SROI1[0], SROI2[1] - SROI2[0]
|
|
178
|
+
assert len(src.roi) == 2, f"Source object should have two ROIs{context}"
|
|
179
|
+
# Multiple objects mode: extract each ROI as a separate object
|
|
180
|
+
signals: list[sigima.objects.SignalObj] = []
|
|
181
|
+
for index, single_roi in enumerate(src.roi):
|
|
182
|
+
roiparam = single_roi.to_param(src, index)
|
|
183
|
+
signal = sigima.proc.signal.extract_roi(src, roiparam)
|
|
184
|
+
signals.append(signal)
|
|
185
|
+
assert len(signals) == len(src.roi), (
|
|
186
|
+
f"Number of extracted signals mismatch{context}"
|
|
187
|
+
)
|
|
188
|
+
assert signals[0].data.size == size_roi1, f"Signal 1 size mismatch{context}"
|
|
189
|
+
assert signals[1].data.size == size_roi2, f"Signal 2 size mismatch{context}"
|
|
190
|
+
assert np.all(signals[0].data == src.data[SROI1[0] : SROI1[1]]), (
|
|
191
|
+
f"Signal 1 data mismatch{context}"
|
|
192
|
+
)
|
|
193
|
+
assert np.all(signals[1].data == src.data[SROI2[0] : SROI2[1]]), (
|
|
194
|
+
f"Signal 2 data mismatch{context}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_signal_roi_union() -> None:
|
|
199
|
+
"""Test signal ROI union operation"""
|
|
200
|
+
# Test union of overlapping ROIs
|
|
201
|
+
roi1 = sigima.objects.create_signal_roi([[10, 30], [20, 40]], indices=True)
|
|
202
|
+
roi_union = roi1.union()
|
|
203
|
+
assert len(roi_union) == 1, "Overlapping ROIs should merge into one"
|
|
204
|
+
assert np.array_equal(roi_union.single_rois[0].coords, [10, 40]), (
|
|
205
|
+
"Union should span full range"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Test union of non-overlapping ROIs
|
|
209
|
+
roi2 = sigima.objects.create_signal_roi([[10, 20], [30, 40]], indices=True)
|
|
210
|
+
roi_union2 = roi2.union()
|
|
211
|
+
assert len(roi_union2) == 2, "Non-overlapping ROIs should remain separate"
|
|
212
|
+
|
|
213
|
+
# Test union of adjacent ROIs
|
|
214
|
+
roi3 = sigima.objects.create_signal_roi([[10, 20], [20, 30]], indices=True)
|
|
215
|
+
roi_union3 = roi3.union()
|
|
216
|
+
assert len(roi_union3) == 1, "Adjacent ROIs should merge"
|
|
217
|
+
assert np.array_equal(roi_union3.single_rois[0].coords, [10, 30]), (
|
|
218
|
+
"Adjacent union should span full range"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Test empty ROI union
|
|
222
|
+
empty_roi = sigima.objects.SignalROI()
|
|
223
|
+
empty_union = empty_roi.union()
|
|
224
|
+
assert len(empty_union) == 0, "Empty ROI union should be empty"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_signal_roi_clipping() -> None:
|
|
228
|
+
"""Test signal ROI clipping operation"""
|
|
229
|
+
src = __create_test_signal()
|
|
230
|
+
x_min, x_max = src.x[0], src.x[-1]
|
|
231
|
+
|
|
232
|
+
# Test clipping ROI within signal range
|
|
233
|
+
roi = sigima.objects.create_signal_roi([[x_min + 10, x_max - 10]], indices=False)
|
|
234
|
+
original_coords = roi.single_rois[0].coords.copy()
|
|
235
|
+
clipped_roi = roi.clipped(x_min, x_max)
|
|
236
|
+
assert len(clipped_roi) == 1, "ROI within range should remain"
|
|
237
|
+
assert np.array_equal(clipped_roi.single_rois[0].coords, original_coords), (
|
|
238
|
+
"ROI within range should be unchanged"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Test clipping ROI partially outside signal range
|
|
242
|
+
roi2 = sigima.objects.create_signal_roi(
|
|
243
|
+
[[x_min - 5, x_min + 10], [x_max - 10, x_max + 5]], indices=False
|
|
244
|
+
)
|
|
245
|
+
clipped_roi2 = roi2.clipped(x_min, x_max)
|
|
246
|
+
assert len(clipped_roi2) == 2, "Partially outside ROIs should be clipped"
|
|
247
|
+
assert clipped_roi2.single_rois[0].coords[0] == x_min, (
|
|
248
|
+
"Left boundary should be clipped to x_min"
|
|
249
|
+
)
|
|
250
|
+
assert clipped_roi2.single_rois[1].coords[1] == x_max, (
|
|
251
|
+
"Right boundary should be clipped to x_max"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Test clipping ROI completely outside signal range
|
|
255
|
+
roi3 = sigima.objects.create_signal_roi([[x_max + 1, x_max + 10]], indices=False)
|
|
256
|
+
clipped_roi3 = roi3.clipped(x_min, x_max)
|
|
257
|
+
assert len(clipped_roi3) == 0, "ROI completely outside range should be removed"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_signal_roi_inversion() -> None:
|
|
261
|
+
"""Test signal ROI inversion operation"""
|
|
262
|
+
src = __create_test_signal()
|
|
263
|
+
x_min, x_max = src.x[0], src.x[-1]
|
|
264
|
+
|
|
265
|
+
# Test inversion of single ROI in middle
|
|
266
|
+
roi = sigima.objects.create_signal_roi([[20, 30]], indices=False)
|
|
267
|
+
inverted = roi.inverted(x_min, x_max)
|
|
268
|
+
assert len(inverted) == 2, "Single middle ROI should create two inverted segments"
|
|
269
|
+
assert inverted.single_rois[0].coords[0] == x_min, (
|
|
270
|
+
"First segment should start at x_min"
|
|
271
|
+
)
|
|
272
|
+
assert inverted.single_rois[0].coords[1] == 20, (
|
|
273
|
+
"First segment should end at ROI start"
|
|
274
|
+
)
|
|
275
|
+
assert inverted.single_rois[1].coords[0] == 30, (
|
|
276
|
+
"Second segment should start at ROI end"
|
|
277
|
+
)
|
|
278
|
+
assert inverted.single_rois[1].coords[1] == x_max, (
|
|
279
|
+
"Second segment should end at x_max"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Test inversion of ROI at signal start
|
|
283
|
+
roi_start = sigima.objects.create_signal_roi([[x_min, 20]], indices=False)
|
|
284
|
+
inverted_start = roi_start.inverted(x_min, x_max)
|
|
285
|
+
assert len(inverted_start) == 1, "ROI at start should create one inverted segment"
|
|
286
|
+
assert inverted_start.single_rois[0].coords[0] == 20, (
|
|
287
|
+
"Inverted segment should start after ROI"
|
|
288
|
+
)
|
|
289
|
+
assert inverted_start.single_rois[0].coords[1] == x_max, (
|
|
290
|
+
"Inverted segment should end at x_max"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Test inversion of ROI at signal end
|
|
294
|
+
roi_end = sigima.objects.create_signal_roi([[30, x_max]], indices=False)
|
|
295
|
+
inverted_end = roi_end.inverted(x_min, x_max)
|
|
296
|
+
assert len(inverted_end) == 1, "ROI at end should create one inverted segment"
|
|
297
|
+
assert inverted_end.single_rois[0].coords[0] == x_min, (
|
|
298
|
+
"Inverted segment should start at x_min"
|
|
299
|
+
)
|
|
300
|
+
assert inverted_end.single_rois[0].coords[1] == 30, (
|
|
301
|
+
"Inverted segment should end before ROI"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Test inversion of multiple ROIs
|
|
305
|
+
roi_multi = sigima.objects.create_signal_roi([[10, 15], [20, 30]], indices=False)
|
|
306
|
+
inverted_multi = roi_multi.inverted(x_min, x_max)
|
|
307
|
+
assert len(inverted_multi) == 3, "Two ROIs should create three inverted segments"
|
|
308
|
+
|
|
309
|
+
# Test error case: empty ROI inversion
|
|
310
|
+
empty_roi = sigima.objects.SignalROI()
|
|
311
|
+
with pytest.raises(ValueError, match="No ROIs defined, cannot invert"):
|
|
312
|
+
empty_roi.inverted(x_min, x_max)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def test_signal_roi_mask() -> None:
|
|
316
|
+
"""Test signal ROI mask creation"""
|
|
317
|
+
src = __create_test_signal()
|
|
318
|
+
|
|
319
|
+
# Test mask for single ROI
|
|
320
|
+
roi = sigima.objects.create_signal_roi([SROI1], indices=True)
|
|
321
|
+
mask = roi.to_mask(src)
|
|
322
|
+
assert mask.shape == src.xydata.shape, "Mask should have same shape as data"
|
|
323
|
+
assert mask.dtype == bool, "Mask should be boolean array"
|
|
324
|
+
# Check that ROI region is masked (False values)
|
|
325
|
+
assert not np.any(mask[:, SROI1[0] : SROI1[1]]), "ROI region should be masked"
|
|
326
|
+
# Check that non-ROI regions are not masked (True values)
|
|
327
|
+
assert np.all(mask[:, : SROI1[0]]), "Region before ROI should not be masked"
|
|
328
|
+
assert np.all(mask[:, SROI1[1] :]), "Region after ROI should not be masked"
|
|
329
|
+
|
|
330
|
+
# Test mask for multiple ROIs
|
|
331
|
+
roi_multi = __create_test_roi()
|
|
332
|
+
mask_multi = roi_multi.to_mask(src)
|
|
333
|
+
assert not np.any(mask_multi[:, SROI1[0] : SROI1[1]]), (
|
|
334
|
+
"First ROI region should be masked"
|
|
335
|
+
)
|
|
336
|
+
assert not np.any(mask_multi[:, SROI2[0] : SROI2[1]]), (
|
|
337
|
+
"Second ROI region should be masked"
|
|
338
|
+
)
|
|
339
|
+
assert np.all(mask_multi[:, SROI1[1] : SROI2[0]]), (
|
|
340
|
+
"Region between ROIs should not be masked"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Test mask for empty ROI
|
|
344
|
+
empty_roi = sigima.objects.SignalROI()
|
|
345
|
+
empty_mask = empty_roi.to_mask(src)
|
|
346
|
+
assert not np.any(empty_mask), "Empty ROI should mask everything"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_signal_roi_operations_edge_cases() -> None:
|
|
350
|
+
"""Test edge cases for signal ROI operations"""
|
|
351
|
+
src = __create_test_signal()
|
|
352
|
+
x_min, x_max = src.x[0], src.x[-1]
|
|
353
|
+
|
|
354
|
+
# Test union with identical ROIs
|
|
355
|
+
roi_identical = sigima.objects.create_signal_roi([[10, 20], [10, 20]], indices=True)
|
|
356
|
+
union_identical = roi_identical.union()
|
|
357
|
+
assert len(union_identical) == 1, "Identical ROIs should merge to one"
|
|
358
|
+
|
|
359
|
+
# Test clipping with ROI exactly at boundaries
|
|
360
|
+
roi_boundary = sigima.objects.create_signal_roi([[x_min, x_max]], indices=False)
|
|
361
|
+
roi_boundary.clipped(x_min, x_max)
|
|
362
|
+
assert len(roi_boundary) == 1, "ROI at exact boundaries should remain"
|
|
363
|
+
assert np.array_equal(roi_boundary.single_rois[0].coords, [x_min, x_max]), (
|
|
364
|
+
"Boundary ROI should be unchanged"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Test inversion with ROI covering entire signal
|
|
368
|
+
roi_full = sigima.objects.create_signal_roi([[x_min, x_max]], indices=False)
|
|
369
|
+
inverted_full = roi_full.inverted(x_min, x_max)
|
|
370
|
+
assert len(inverted_full) == 0, "Full signal ROI should invert to empty"
|
|
371
|
+
|
|
372
|
+
# Test operations with very small ROIs
|
|
373
|
+
small_roi = sigima.objects.create_signal_roi([[10, 10.1]], indices=False)
|
|
374
|
+
small_union = small_roi.union()
|
|
375
|
+
assert len(small_union) == 1, "Small ROI should remain in union"
|
|
376
|
+
|
|
377
|
+
small_roi.clipped(0, 100)
|
|
378
|
+
assert len(small_roi) == 1, "Small ROI within range should remain after clipping"
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
if __name__ == "__main__":
|
|
382
|
+
test_signal_roi_merge()
|
|
383
|
+
test_signal_roi_combine()
|
|
384
|
+
test_signal_roi_processing()
|
|
385
|
+
test_empty_signal_roi()
|
|
386
|
+
test_signal_extract_rois()
|
|
387
|
+
test_signal_extract_roi()
|
|
388
|
+
test_signal_roi_union()
|
|
389
|
+
test_signal_roi_clipping()
|
|
390
|
+
test_signal_roi_inversion()
|
|
391
|
+
test_signal_roi_mask()
|
|
392
|
+
test_signal_roi_operations_edge_cases()
|