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
sigima/tests/helpers.py
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Module providing test utilities
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import atexit
|
|
10
|
+
import functools
|
|
11
|
+
import os
|
|
12
|
+
import os.path as osp
|
|
13
|
+
import pathlib
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
import warnings
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from typing import Any, Generator
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from guidata.configtools import get_module_data_path
|
|
23
|
+
|
|
24
|
+
from sigima.config import MOD_NAME
|
|
25
|
+
from sigima.io.image import ImageIORegistry
|
|
26
|
+
from sigima.io.signal import SignalIORegistry
|
|
27
|
+
from sigima.objects.image import ImageObj
|
|
28
|
+
from sigima.objects.signal import SignalObj
|
|
29
|
+
from sigima.tests.env import execenv
|
|
30
|
+
|
|
31
|
+
TST_PATH = []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_test_paths() -> list[str]:
|
|
35
|
+
"""Return the list of test data paths"""
|
|
36
|
+
return TST_PATH
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def add_test_path(path: str) -> None:
|
|
40
|
+
"""Appends test data path, after normalizing it and making it absolute.
|
|
41
|
+
Do nothing if the path is already in the list.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
Path to add to the list of test data paths
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
FileNotFoundError: if the path does not exist
|
|
48
|
+
"""
|
|
49
|
+
path = osp.abspath(osp.normpath(path))
|
|
50
|
+
if path not in TST_PATH:
|
|
51
|
+
if not osp.exists(path):
|
|
52
|
+
raise FileNotFoundError(f"Test data path does not exist: {path}")
|
|
53
|
+
TST_PATH.append(path)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def add_test_path_from_env(envvar: str) -> None:
|
|
57
|
+
"""Appends test data path from environment variable (fails silently)"""
|
|
58
|
+
# Note: this function is used in third-party plugins
|
|
59
|
+
path = os.environ.get(envvar)
|
|
60
|
+
if path:
|
|
61
|
+
add_test_path(path)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Add test data files and folders pointed by `SIGIMA_DATA` environment variable:
|
|
65
|
+
add_test_path_from_env("SIGIMA_DATA")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def add_test_module_path(modname: str, relpath: str) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Appends test data path relative to a module name.
|
|
71
|
+
Used to add module local data that resides in a module directory
|
|
72
|
+
but will be shipped under sys.prefix / share/ ...
|
|
73
|
+
|
|
74
|
+
modname must be the name of an already imported module as found in
|
|
75
|
+
sys.modules
|
|
76
|
+
"""
|
|
77
|
+
add_test_path(get_module_data_path(modname, relpath=relpath))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Add test data files and folders for the Sigima module:
|
|
81
|
+
add_test_module_path(MOD_NAME, osp.join("data", "tests"))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_test_fnames(pattern: str, in_folder: str | None = None) -> list[str]:
|
|
85
|
+
"""
|
|
86
|
+
Return the absolute path list to test files with specified pattern
|
|
87
|
+
|
|
88
|
+
Pattern may be a file name (basename), a wildcard (e.g. *.txt)...
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
pattern: pattern to match
|
|
92
|
+
in_folder: folder to search in, in test data path (default: None,
|
|
93
|
+
search in all test data paths)
|
|
94
|
+
"""
|
|
95
|
+
pathlist = []
|
|
96
|
+
for pth in [osp.join(TST_PATH[0], in_folder)] if in_folder else TST_PATH:
|
|
97
|
+
pathlist += sorted(pathlib.Path(pth).rglob(pattern))
|
|
98
|
+
if not pathlist:
|
|
99
|
+
raise FileNotFoundError(f"Test file(s) {pattern} not found")
|
|
100
|
+
return [str(path) for path in pathlist]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def read_test_objects(
|
|
104
|
+
registry: SignalIORegistry | ImageIORegistry,
|
|
105
|
+
pattern: str = "*.*",
|
|
106
|
+
in_folder: str | None = None,
|
|
107
|
+
) -> Generator[tuple[str, ImageObj | None] | tuple[str, SignalObj | None], None, None]:
|
|
108
|
+
"""Read test images and yield their file names and objects
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
registry: I/O registry to use
|
|
112
|
+
pattern: File name pattern to match
|
|
113
|
+
in_folder: Folder to search for test files
|
|
114
|
+
|
|
115
|
+
Yields:
|
|
116
|
+
Tuple of file name and object (or None if not implemented)
|
|
117
|
+
"""
|
|
118
|
+
if registry is ImageIORegistry:
|
|
119
|
+
in_folder = in_folder or "image_formats"
|
|
120
|
+
elif registry is SignalIORegistry:
|
|
121
|
+
in_folder = in_folder or "curve_formats"
|
|
122
|
+
else:
|
|
123
|
+
raise ValueError(f"Unsupported registry type: {registry}")
|
|
124
|
+
fnames = get_test_fnames(pattern, in_folder)
|
|
125
|
+
for fname in fnames:
|
|
126
|
+
try:
|
|
127
|
+
obj = registry.read(fname)[0]
|
|
128
|
+
yield fname, obj
|
|
129
|
+
except NotImplementedError:
|
|
130
|
+
yield fname, None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def try_open_test_data(title: str, pattern: str) -> Callable:
|
|
134
|
+
"""Decorator handling test data opening"""
|
|
135
|
+
|
|
136
|
+
def try_open_test_data_decorator(func: Callable) -> Callable:
|
|
137
|
+
"""Decorator handling test data opening"""
|
|
138
|
+
|
|
139
|
+
@functools.wraps(func)
|
|
140
|
+
def func_wrapper(*args, **kwargs) -> None:
|
|
141
|
+
"""Decorator wrapper function"""
|
|
142
|
+
execenv.print(title + ":")
|
|
143
|
+
execenv.print("-" * len(title))
|
|
144
|
+
try:
|
|
145
|
+
for fname in get_test_fnames(pattern):
|
|
146
|
+
execenv.print(f"=> Opening: {fname}")
|
|
147
|
+
func(fname, title, *args, **kwargs)
|
|
148
|
+
except FileNotFoundError:
|
|
149
|
+
execenv.print(f" No test data available for {pattern}")
|
|
150
|
+
finally:
|
|
151
|
+
execenv.print(os.linesep)
|
|
152
|
+
|
|
153
|
+
return func_wrapper
|
|
154
|
+
|
|
155
|
+
return try_open_test_data_decorator
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_default_test_name(suffix: str | None = None) -> str:
|
|
159
|
+
"""Return default test name based on script name"""
|
|
160
|
+
name = osp.splitext(osp.basename(sys.argv[0]))[0]
|
|
161
|
+
if suffix is not None:
|
|
162
|
+
name += "_" + suffix
|
|
163
|
+
return name
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_output_data_path(extension: str, suffix: str | None = None) -> str:
|
|
167
|
+
"""Return full path for data file with extension, generated by a test script"""
|
|
168
|
+
name = get_default_test_name(suffix)
|
|
169
|
+
return osp.join(TST_PATH[0], f"{name}.{extension}")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def reduce_path(filename: str) -> str:
|
|
173
|
+
"""Reduce a file path to a relative path
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
filename: path to reduce
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Relative path to the file, relative to its parent directory
|
|
180
|
+
"""
|
|
181
|
+
return osp.relpath(filename, osp.join(osp.dirname(filename), osp.pardir))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class WorkdirRestoringTempDir(tempfile.TemporaryDirectory):
|
|
185
|
+
"""Enhanced temporary directory with working directory preservation.
|
|
186
|
+
|
|
187
|
+
A subclass of :py:class:`tempfile.TemporaryDirectory` that:
|
|
188
|
+
|
|
189
|
+
* Preserves and automatically restores the working directory during cleanup
|
|
190
|
+
* Handles common cleanup errors silently (PermissionError, RecursionError)
|
|
191
|
+
|
|
192
|
+
Example::
|
|
193
|
+
|
|
194
|
+
with WorkdirRestoringTempDir() as tmpdir:
|
|
195
|
+
os.chdir(tmpdir) # Directory change is automatically reverted at exit
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(self) -> None:
|
|
199
|
+
super().__init__()
|
|
200
|
+
self.__cwd = os.getcwd()
|
|
201
|
+
|
|
202
|
+
def cleanup(self) -> None:
|
|
203
|
+
"""Clean up temporary directory, restore working directory, ignore errors."""
|
|
204
|
+
os.chdir(self.__cwd)
|
|
205
|
+
try:
|
|
206
|
+
super().cleanup()
|
|
207
|
+
except (PermissionError, RecursionError):
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def get_temporary_directory() -> str:
|
|
212
|
+
"""Return path to a temporary directory, and clean-up at exit"""
|
|
213
|
+
tmp = WorkdirRestoringTempDir()
|
|
214
|
+
atexit.register(tmp.cleanup)
|
|
215
|
+
return tmp.name
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def exec_script(
|
|
219
|
+
path: str,
|
|
220
|
+
wait: bool = True,
|
|
221
|
+
args: list[str] = None,
|
|
222
|
+
env: dict[str, str] | None = None,
|
|
223
|
+
verbose: bool = False,
|
|
224
|
+
) -> subprocess.Popen | None:
|
|
225
|
+
"""Run test script.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
path: path to script
|
|
229
|
+
wait: wait for script to finish
|
|
230
|
+
args: arguments to pass to script
|
|
231
|
+
env: environment variables to pass to script
|
|
232
|
+
verbose: if True, print command and output
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
subprocess.Popen object if wait is False, None otherwise
|
|
236
|
+
"""
|
|
237
|
+
stderr = subprocess.DEVNULL if execenv.unattended else None
|
|
238
|
+
# pylint: disable=consider-using-with
|
|
239
|
+
if verbose:
|
|
240
|
+
command = [sys.executable, path] + ([] if args is None else args)
|
|
241
|
+
proc = subprocess.Popen(
|
|
242
|
+
command,
|
|
243
|
+
stdout=subprocess.PIPE,
|
|
244
|
+
stderr=subprocess.PIPE,
|
|
245
|
+
env=env,
|
|
246
|
+
text=True,
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
command = [sys.executable, '"' + path + '"'] + ([] if args is None else args)
|
|
250
|
+
proc = subprocess.Popen(" ".join(command), shell=True, stderr=stderr, env=env)
|
|
251
|
+
if wait:
|
|
252
|
+
if verbose:
|
|
253
|
+
stdout, stderr = proc.communicate()
|
|
254
|
+
print("Command:", " ".join(command))
|
|
255
|
+
print("Return code:", proc.returncode)
|
|
256
|
+
print("---- STDOUT ----\n", stdout)
|
|
257
|
+
print("---- STDERR ----\n", stderr)
|
|
258
|
+
return None
|
|
259
|
+
proc.wait()
|
|
260
|
+
return proc
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def get_script_output(
|
|
264
|
+
path: str, args: list[str] = None, env: dict[str, str] | None = None
|
|
265
|
+
) -> str:
|
|
266
|
+
"""Run test script and return its output.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
path (str): path to script
|
|
270
|
+
args (list): arguments to pass to script
|
|
271
|
+
env (dict): environment variables to pass to script
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
str: script output
|
|
275
|
+
"""
|
|
276
|
+
command = [sys.executable, '"' + path + '"'] + ([] if args is None else args)
|
|
277
|
+
result = subprocess.run(
|
|
278
|
+
" ".join(command), capture_output=True, text=True, env=env, check=False
|
|
279
|
+
)
|
|
280
|
+
return result.stdout.strip()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def compare_lists(
|
|
284
|
+
list1: list, list2: list, level: int = 1, raise_on_diff: bool = False
|
|
285
|
+
) -> tuple[bool, list[str]]:
|
|
286
|
+
"""Compare two lists
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
list1: first list
|
|
290
|
+
list2: second list
|
|
291
|
+
level: recursion level
|
|
292
|
+
raise_on_diff: if True, raise an AssertionError on difference (default: False)
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
A tuple (same, diff) where `same` is True if lists are the same,
|
|
296
|
+
False otherwise, and `diff` is a list of differences found
|
|
297
|
+
|
|
298
|
+
Raises:
|
|
299
|
+
AssertionError: if raise_on_diff is True and lists are different
|
|
300
|
+
"""
|
|
301
|
+
same = True
|
|
302
|
+
prefix = " " * level
|
|
303
|
+
diff = []
|
|
304
|
+
# Check for length mismatch
|
|
305
|
+
if len(list1) != len(list2):
|
|
306
|
+
same = False
|
|
307
|
+
diff += [f"{prefix}Lists have different lengths: {len(list1)} != {len(list2)}"]
|
|
308
|
+
for idx, (elem1, elem2) in enumerate(zip(list1, list2)):
|
|
309
|
+
execenv.print(f"{prefix}Checking element {idx}...", end=" ")
|
|
310
|
+
if isinstance(elem1, (list, tuple)):
|
|
311
|
+
execenv.print("")
|
|
312
|
+
cl_same, cl_diff = compare_lists(elem1, elem2, level + 1)
|
|
313
|
+
diff += cl_diff
|
|
314
|
+
same = same and cl_same
|
|
315
|
+
elif isinstance(elem1, dict):
|
|
316
|
+
execenv.print("")
|
|
317
|
+
cm_same, cm_diff = compare_metadata(elem1, elem2, level + 1)
|
|
318
|
+
diff += cm_diff
|
|
319
|
+
same = same and cm_same
|
|
320
|
+
else:
|
|
321
|
+
same_value = str(elem1) == str(elem2)
|
|
322
|
+
if not same_value:
|
|
323
|
+
diff += [
|
|
324
|
+
f"{prefix}Different values for element {idx}: {elem1} != {elem2}"
|
|
325
|
+
]
|
|
326
|
+
same = same and same_value
|
|
327
|
+
execenv.print("OK" if same_value else "KO")
|
|
328
|
+
if diff:
|
|
329
|
+
all_diff = os.linesep.join(diff)
|
|
330
|
+
if raise_on_diff:
|
|
331
|
+
raise AssertionError(all_diff)
|
|
332
|
+
execenv.print("Lists are different:")
|
|
333
|
+
execenv.print(all_diff)
|
|
334
|
+
return same, diff
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def compare_metadata(
|
|
338
|
+
dict1: dict[str, Any],
|
|
339
|
+
dict2: dict[str, Any],
|
|
340
|
+
level: int = 1,
|
|
341
|
+
raise_on_diff: bool = False,
|
|
342
|
+
) -> tuple[bool, list[str]]:
|
|
343
|
+
"""Compare metadata dictionaries without private elements
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
dict1: first dictionary, exclusively with string keys
|
|
347
|
+
dict2: second dictionary, exclusively with string keys
|
|
348
|
+
level: recursion level
|
|
349
|
+
raise_on_diff: if True, raise an AssertionError on difference (default: False)
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
A tuple (same, diff) where `same` is True if dictionaries are the same,
|
|
353
|
+
False otherwise, and `diff` is a list of differences found
|
|
354
|
+
|
|
355
|
+
Raises:
|
|
356
|
+
AssertionError: if raise_on_diff is True and metadata is different
|
|
357
|
+
"""
|
|
358
|
+
dict_a, dict_b = dict1.copy(), dict2.copy()
|
|
359
|
+
for dict_ in (dict_a, dict_b):
|
|
360
|
+
for key in list(dict_.keys()):
|
|
361
|
+
if key.startswith("__"):
|
|
362
|
+
dict_.pop(key)
|
|
363
|
+
same = True
|
|
364
|
+
prefix = " " * level
|
|
365
|
+
diff = []
|
|
366
|
+
# Check for keys only in dict_a
|
|
367
|
+
for key in dict_a:
|
|
368
|
+
if key not in dict_b:
|
|
369
|
+
same = False
|
|
370
|
+
diff += [f"{prefix}Key {key} found in first dict but not in second"]
|
|
371
|
+
continue
|
|
372
|
+
val_a, val_b = dict_a[key], dict_b[key]
|
|
373
|
+
execenv.print(f"{prefix}Checking key {key}...", end=" ")
|
|
374
|
+
if isinstance(val_a, dict):
|
|
375
|
+
execenv.print("")
|
|
376
|
+
cm_same, cm_diff = compare_metadata(val_a, val_b, level + 1)
|
|
377
|
+
diff += cm_diff
|
|
378
|
+
same = same and cm_same
|
|
379
|
+
elif isinstance(val_a, (list, tuple)):
|
|
380
|
+
execenv.print("")
|
|
381
|
+
cl_same, cl_diff = compare_lists(val_a, val_b, level + 1)
|
|
382
|
+
diff += cl_diff
|
|
383
|
+
same = same and cl_same
|
|
384
|
+
else:
|
|
385
|
+
same_value = str(val_a) == str(val_b)
|
|
386
|
+
if not same_value:
|
|
387
|
+
diff += [f"{prefix}Different values for key {key}: {val_a} != {val_b}"]
|
|
388
|
+
same = same and same_value
|
|
389
|
+
execenv.print("OK" if same_value else "KO")
|
|
390
|
+
# Check for keys only in dict_b
|
|
391
|
+
for key in dict_b:
|
|
392
|
+
if key not in dict_a:
|
|
393
|
+
same = False
|
|
394
|
+
diff += [f"{prefix}Key {key} found in second dict but not in first"]
|
|
395
|
+
if diff:
|
|
396
|
+
all_diff = os.linesep.join(diff)
|
|
397
|
+
if raise_on_diff:
|
|
398
|
+
raise AssertionError(all_diff)
|
|
399
|
+
execenv.print("Dictionaries are different:")
|
|
400
|
+
execenv.print(all_diff)
|
|
401
|
+
return same, diff
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def __evaluate_func_safely(func: Callable, fallback: float | int = np.nan) -> Any:
|
|
405
|
+
"""Evaluate function, ignore warnings and exceptions.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
func: function to evaluate
|
|
409
|
+
fallback: value to return if function raises an exception (default: np.nan)
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Function result, or fallback value if function raises an exception
|
|
413
|
+
"""
|
|
414
|
+
with warnings.catch_warnings():
|
|
415
|
+
warnings.simplefilter("ignore")
|
|
416
|
+
try:
|
|
417
|
+
return func()
|
|
418
|
+
except Exception: # pylint: disable=broad-except
|
|
419
|
+
return fallback
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def __array_to_str(data: np.ndarray) -> str:
|
|
423
|
+
"""Return a compact description of the array properties.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
data: input array
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
String describing array dimensions, dtype, min/max, mean, std, sum
|
|
430
|
+
"""
|
|
431
|
+
dims = "×".join(str(dim) for dim in data.shape)
|
|
432
|
+
efs = __evaluate_func_safely
|
|
433
|
+
return (
|
|
434
|
+
f"{dims},{data.dtype},"
|
|
435
|
+
f"{efs(data.min):.2g}→{efs(data.max):.2g},"
|
|
436
|
+
f"µ={efs(data.mean):.2g},σ={efs(data.std):.2g},∑={efs(data.sum):.2g}"
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def check_array_result(
|
|
441
|
+
title: str,
|
|
442
|
+
res: np.ndarray,
|
|
443
|
+
exp: np.ndarray,
|
|
444
|
+
rtol: float = 1.0e-5,
|
|
445
|
+
atol: float = 1.0e-8,
|
|
446
|
+
similar: bool = False,
|
|
447
|
+
sort: bool = False,
|
|
448
|
+
verbose: bool = True,
|
|
449
|
+
) -> None:
|
|
450
|
+
"""Assert that two arrays are almost equal.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
title: title of the test
|
|
454
|
+
res: result array
|
|
455
|
+
exp: expected array
|
|
456
|
+
rtol: relative tolerance for comparison
|
|
457
|
+
atol: absolute tolerance for comparison
|
|
458
|
+
similar: if True, arrays are compared exclusively using their textual
|
|
459
|
+
global representation (e.g. '824,float64,-0.00012→0.036,µ=0.018')
|
|
460
|
+
sort: if True, sort arrays before comparison (default: False)
|
|
461
|
+
verbose: if True, print detailed result (default: True)
|
|
462
|
+
|
|
463
|
+
Raises:
|
|
464
|
+
AssertionError: if arrays are not almost equal or have different dtypes
|
|
465
|
+
"""
|
|
466
|
+
if sort:
|
|
467
|
+
res = np.sort(np.array(res, copy=True), axis=None)
|
|
468
|
+
exp = np.sort(np.array(exp, copy=True), axis=None)
|
|
469
|
+
restxt = f"{title}: {__array_to_str(res)} (expected: {__array_to_str(exp)})"
|
|
470
|
+
if verbose:
|
|
471
|
+
execenv.print(restxt)
|
|
472
|
+
assert res.shape == exp.shape, f"{restxt} - Different shapes"
|
|
473
|
+
try:
|
|
474
|
+
if similar:
|
|
475
|
+
assert __array_to_str(res) == __array_to_str(exp), restxt
|
|
476
|
+
else:
|
|
477
|
+
assert np.allclose(res, exp, rtol=rtol, atol=atol, equal_nan=True), restxt
|
|
478
|
+
except AssertionError as exc:
|
|
479
|
+
raise AssertionError(restxt) from exc
|
|
480
|
+
assert res.dtype == exp.dtype, f"{restxt} - Different dtypes"
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def check_scalar_result(
|
|
484
|
+
title: str,
|
|
485
|
+
res: float,
|
|
486
|
+
exp: float | tuple[float, ...],
|
|
487
|
+
rtol: float = 1.0e-5,
|
|
488
|
+
atol: float = 1.0e-8,
|
|
489
|
+
verbose: bool = True,
|
|
490
|
+
) -> None:
|
|
491
|
+
"""Assert that two scalars are almost equal.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
title: title of the test
|
|
495
|
+
res: result value
|
|
496
|
+
exp: expected value or tuple of expected values
|
|
497
|
+
rtol: relative tolerance for comparison
|
|
498
|
+
atol: absolute tolerance for comparison
|
|
499
|
+
verbose: if True, print detailed result (default: True)
|
|
500
|
+
|
|
501
|
+
Raises:
|
|
502
|
+
AssertionError: if values are not almost equal or if expected is not a scalar
|
|
503
|
+
or tuple
|
|
504
|
+
"""
|
|
505
|
+
restxt = f"{title}: {res} (expected: {exp}) ± {rtol * abs(exp) + atol:.2g}"
|
|
506
|
+
if verbose:
|
|
507
|
+
execenv.print(restxt)
|
|
508
|
+
if isinstance(exp, tuple):
|
|
509
|
+
assert any(np.isclose(res, exp_val, rtol=rtol, atol=atol) for exp_val in exp), (
|
|
510
|
+
restxt
|
|
511
|
+
)
|
|
512
|
+
else:
|
|
513
|
+
assert np.isclose(res, exp, rtol=rtol, atol=atol), restxt
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def print_obj_data_dimensions(obj: SignalObj | ImageObj, indent: int = 0) -> None:
|
|
517
|
+
"""Print data array shape for the given signal or image object,
|
|
518
|
+
including ROI data if available.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
obj: Signal or image object to print data dimensions for.
|
|
522
|
+
indent: Indentation level for printing (default: 0)
|
|
523
|
+
"""
|
|
524
|
+
indent_str = " " * indent
|
|
525
|
+
execenv.print(f"{indent_str}Accessing object '{obj.title}':")
|
|
526
|
+
execenv.print(f"{indent_str} data: {__array_to_str(obj.data)}")
|
|
527
|
+
if obj.roi is not None:
|
|
528
|
+
for idx in range(len(obj.roi)):
|
|
529
|
+
roi_data = obj.get_data(idx)
|
|
530
|
+
if isinstance(obj, SignalObj):
|
|
531
|
+
roi_data = roi_data[1] # y data
|
|
532
|
+
execenv.print(f"{indent_str} ROI[{idx}]: {__array_to_str(roi_data)}")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Unit tests for image features
|
|
5
|
+
-----------------------------
|
|
6
|
+
|
|
7
|
+
[1] Implementation note regarding scikit-image methods
|
|
8
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
9
|
+
|
|
10
|
+
The following note applies to:
|
|
11
|
+
- thresholding methods (isodata, li, mean, minimum, otsu, triangle, yen)
|
|
12
|
+
- exposure methods (adjust_gamma, adjust_log, adjust_sigmoid, rescale_intensity,
|
|
13
|
+
equalize_hist, equalize_adapthist)
|
|
14
|
+
- restoration methods (denoise_tv, denoise_bilateral, denoise_wavelet)
|
|
15
|
+
- morphology methods (white_tophat, black_tophat, erosion, dilation, opening, closing)
|
|
16
|
+
- edge detection methods (canny, roberts, prewitt, sobel, scharr, farid, laplace)
|
|
17
|
+
|
|
18
|
+
The thresholding, morphological, and edge detection methods are implemented
|
|
19
|
+
in the scikit-image library: those algorithms are considered to be validated,
|
|
20
|
+
so we can use them as reference.
|
|
21
|
+
As a consequence, the only purpose of the associated validation tests is to check
|
|
22
|
+
if the methods are correctly called and if the results are consistent with
|
|
23
|
+
the reference implementation.
|
|
24
|
+
|
|
25
|
+
In other words, we are not testing the correctness of the algorithms, but
|
|
26
|
+
the correctness of the interface between the Sigima and the scikit-image
|
|
27
|
+
libraries.
|
|
28
|
+
"""
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Image pixel binning computation test
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
8
|
+
# pylint: disable=duplicate-code
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pytest
|
|
14
|
+
from numpy import ma
|
|
15
|
+
|
|
16
|
+
import sigima.params
|
|
17
|
+
import sigima.proc.image
|
|
18
|
+
from sigima.enums import BinningOperation
|
|
19
|
+
from sigima.tests import guiutils
|
|
20
|
+
from sigima.tests.data import get_test_image
|
|
21
|
+
from sigima.tests.env import execenv
|
|
22
|
+
from sigima.tools.image import binning
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def compare_binning_images(data: ma.MaskedArray) -> None:
|
|
26
|
+
"""Compare binning images
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
data: Image data
|
|
30
|
+
"""
|
|
31
|
+
# pylint: disable=import-outside-toplevel
|
|
32
|
+
from plotpy.builder import make
|
|
33
|
+
|
|
34
|
+
from sigima.tests.vistools import view_image_items
|
|
35
|
+
|
|
36
|
+
items = []
|
|
37
|
+
items += [make.image(data, interpolation="nearest", eliminate_outliers=2.0)]
|
|
38
|
+
# Computing pixel binning
|
|
39
|
+
oa_t0 = time.time()
|
|
40
|
+
for ix in range(1, 5):
|
|
41
|
+
sx = 2**ix
|
|
42
|
+
for iy in range(1, 5):
|
|
43
|
+
sy = 2**iy
|
|
44
|
+
for operation in BinningOperation:
|
|
45
|
+
t0 = time.time()
|
|
46
|
+
bdata = binning(data, sx=sx, sy=sy, operation=operation)
|
|
47
|
+
title = f"[{sx}x{sy},{operation.value}]"
|
|
48
|
+
item = make.image(
|
|
49
|
+
bdata,
|
|
50
|
+
title=title,
|
|
51
|
+
interpolation="nearest",
|
|
52
|
+
eliminate_outliers=2.0,
|
|
53
|
+
xdata=[0, data.shape[1]],
|
|
54
|
+
ydata=[0, data.shape[0]],
|
|
55
|
+
)
|
|
56
|
+
item.hide()
|
|
57
|
+
items.append(item)
|
|
58
|
+
dt = time.time() - t0
|
|
59
|
+
execenv.print(f" {title}: {int(dt * 1e3):d} ms")
|
|
60
|
+
oa_dt = time.time() - oa_t0
|
|
61
|
+
execenv.print(f" Overall calculation time: {int(oa_dt * 1e3):d} ms")
|
|
62
|
+
view_image_items(items, title="Binning test", show_itemlist=True)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.gui
|
|
66
|
+
def test_binning_interactive() -> None:
|
|
67
|
+
"""Test binning computation and show results"""
|
|
68
|
+
with guiutils.lazy_qt_app_context(force=True):
|
|
69
|
+
data = get_test_image("*.scor-data").data[:500, :500]
|
|
70
|
+
execenv.print(f"Data[dtype={data.dtype},shape={data.shape}]")
|
|
71
|
+
compare_binning_images(data.view(ma.MaskedArray))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.validation
|
|
75
|
+
def test_binning() -> None:
|
|
76
|
+
"""Validation test for binning computation"""
|
|
77
|
+
# Implementation note:
|
|
78
|
+
# ---------------------
|
|
79
|
+
#
|
|
80
|
+
# Pixel binning algorithm is validated graphically by comparing the results of
|
|
81
|
+
# different binning operations and sizes: that is the purpose of the
|
|
82
|
+
# `test_binning_graphically`` function.
|
|
83
|
+
# Formal validation is not possible without reimplementation of the algorithm
|
|
84
|
+
# here, which would be redundant and proove nothing. Instead, as a complementary
|
|
85
|
+
# test, we only validate some basic properties of the binning algorithm:
|
|
86
|
+
# - The output shape is correct
|
|
87
|
+
# - The output data type is correct
|
|
88
|
+
# - Some basic properties of the output data are correct (e.g. min, max, mean)
|
|
89
|
+
|
|
90
|
+
src = get_test_image("*.scor-data")
|
|
91
|
+
src.data = data = np.array(src.data[:500, :500], dtype=float)
|
|
92
|
+
ny, nx = data.shape
|
|
93
|
+
|
|
94
|
+
p = sigima.params.BinningParam()
|
|
95
|
+
for operation in BinningOperation:
|
|
96
|
+
p.operation = operation
|
|
97
|
+
for sx in range(2, 3):
|
|
98
|
+
for sy in range(2, 5):
|
|
99
|
+
p.sx = sx
|
|
100
|
+
p.sy = sy
|
|
101
|
+
rdata = data[: ny - (ny % sy), : nx - (nx % sx)]
|
|
102
|
+
dst = sigima.proc.image.binning(src, p)
|
|
103
|
+
bdata = dst.data
|
|
104
|
+
assert bdata.shape == (data.shape[0] // sy, data.shape[1] // sx)
|
|
105
|
+
assert bdata.dtype == data.dtype
|
|
106
|
+
if operation == "min":
|
|
107
|
+
assert bdata.min() == rdata.min()
|
|
108
|
+
elif operation == "max":
|
|
109
|
+
assert bdata.max() == rdata.max()
|
|
110
|
+
elif operation == "sum":
|
|
111
|
+
assert bdata.sum() == rdata.sum()
|
|
112
|
+
elif operation == "average":
|
|
113
|
+
assert bdata.mean() == rdata.mean()
|
|
114
|
+
for src_dtype in (float, np.uint8, np.uint16, np.int16):
|
|
115
|
+
src.data = data = np.array(src.data[:500, :500], dtype=src_dtype)
|
|
116
|
+
for dtype_str in p.dtypes:
|
|
117
|
+
p.dtype_str = dtype_str
|
|
118
|
+
dst = sigima.proc.image.binning(src, p)
|
|
119
|
+
bdata = dst.data
|
|
120
|
+
if dtype_str == "dtype":
|
|
121
|
+
assert bdata.dtype is data.dtype
|
|
122
|
+
else:
|
|
123
|
+
assert bdata.dtype is np.dtype(dtype_str)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
test_binning_interactive()
|
|
128
|
+
test_binning()
|