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,661 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Geometry computation module
|
|
5
|
+
---------------------------
|
|
6
|
+
|
|
7
|
+
This module implements geometric transformations and manipulations for images,
|
|
8
|
+
such as rotations, flips, resizing, axis swapping, binning, and padding.
|
|
9
|
+
|
|
10
|
+
Main features include:
|
|
11
|
+
|
|
12
|
+
- Rotation by arbitrary or fixed angles
|
|
13
|
+
- Horizontal and vertical flipping
|
|
14
|
+
- Resizing and binning of images
|
|
15
|
+
- Axis swapping and zero padding
|
|
16
|
+
|
|
17
|
+
These functions are useful for preparing and augmenting image data.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
21
|
+
|
|
22
|
+
# Note:
|
|
23
|
+
# ----
|
|
24
|
+
# - All `guidata.dataset.DataSet` parameter classes must also be imported
|
|
25
|
+
# in the `sigima.params` module.
|
|
26
|
+
# - All functions decorated by `computation_function` must be imported in the upper
|
|
27
|
+
# level `sigima.proc.image` module.
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import guidata.dataset as gds
|
|
32
|
+
import numpy as np
|
|
33
|
+
import scipy.ndimage as spi
|
|
34
|
+
|
|
35
|
+
from sigima.config import _
|
|
36
|
+
from sigima.enums import BorderMode, Interpolation2DMethod
|
|
37
|
+
from sigima.objects.image import ImageObj
|
|
38
|
+
from sigima.proc.base import dst_1_to_1
|
|
39
|
+
from sigima.proc.decorator import computation_function
|
|
40
|
+
from sigima.proc.image.base import restore_data_outside_roi
|
|
41
|
+
from sigima.proc.image.transformations import transformer
|
|
42
|
+
|
|
43
|
+
# NOTE: Only parameter classes DEFINED in this module should be included in __all__.
|
|
44
|
+
# Parameter classes imported from other modules (like sigima.proc.base) should NOT
|
|
45
|
+
# be re-exported to avoid Sphinx cross-reference conflicts. The sigima.params module
|
|
46
|
+
# serves as the central API point that imports and re-exports all parameter classes.
|
|
47
|
+
__all__ = [
|
|
48
|
+
"Resampling2DParam",
|
|
49
|
+
"ResizeParam",
|
|
50
|
+
"RotateParam",
|
|
51
|
+
"TranslateParam",
|
|
52
|
+
"UniformCoordsParam",
|
|
53
|
+
"XYZCalibrateParam",
|
|
54
|
+
"calibration",
|
|
55
|
+
"fliph",
|
|
56
|
+
"flipv",
|
|
57
|
+
"resampling",
|
|
58
|
+
"resize",
|
|
59
|
+
"rotate",
|
|
60
|
+
"rotate90",
|
|
61
|
+
"rotate270",
|
|
62
|
+
"set_uniform_coords",
|
|
63
|
+
"translate",
|
|
64
|
+
"transpose",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TranslateParam(gds.DataSet):
|
|
69
|
+
"""Translate parameters"""
|
|
70
|
+
|
|
71
|
+
dx = gds.FloatItem(_("X translation"), default=0.0)
|
|
72
|
+
dy = gds.FloatItem(_("Y translation"), default=0.0)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@computation_function()
|
|
76
|
+
def translate(src: ImageObj, p: TranslateParam) -> ImageObj:
|
|
77
|
+
"""Translate data with :py:func:`scipy.ndimage.shift`
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
src: input image object
|
|
81
|
+
p: parameters
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Output image object
|
|
85
|
+
"""
|
|
86
|
+
dst = dst_1_to_1(src, "translate", f"dx={p.dx}, dy={p.dy}")
|
|
87
|
+
if src.is_uniform_coords:
|
|
88
|
+
dst.set_uniform_coords(dst.dx, dst.dy, dst.x0 + p.dx, dst.y0 + p.dy)
|
|
89
|
+
else:
|
|
90
|
+
dst.set_coords(src.xcoords + p.dx, src.ycoords + p.dy)
|
|
91
|
+
transformer.transform_roi(dst, "translate", dx=p.dx, dy=p.dy)
|
|
92
|
+
return dst
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class RotateParam(gds.DataSet):
|
|
96
|
+
"""Rotate parameters"""
|
|
97
|
+
|
|
98
|
+
prop = gds.ValueProp(False)
|
|
99
|
+
|
|
100
|
+
angle = gds.FloatItem(f"{_('Angle')} (°)", default=0.0)
|
|
101
|
+
mode = gds.ChoiceItem(_("Mode"), BorderMode, default=BorderMode.CONSTANT)
|
|
102
|
+
cval = gds.FloatItem(
|
|
103
|
+
_("cval"),
|
|
104
|
+
default=0.0,
|
|
105
|
+
help=_(
|
|
106
|
+
"Value used for points outside the "
|
|
107
|
+
"boundaries of the input if mode is "
|
|
108
|
+
"'constant'"
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
reshape = gds.BoolItem(
|
|
112
|
+
_("Reshape the output array"),
|
|
113
|
+
default=False,
|
|
114
|
+
help=_(
|
|
115
|
+
"Reshape the output array "
|
|
116
|
+
"so that the input array is "
|
|
117
|
+
"contained completely in the output"
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
prefilter = gds.BoolItem(_("Prefilter the input image"), default=True).set_prop(
|
|
121
|
+
"display", store=prop
|
|
122
|
+
)
|
|
123
|
+
order = gds.IntItem(
|
|
124
|
+
_("Order"),
|
|
125
|
+
default=3,
|
|
126
|
+
min=0,
|
|
127
|
+
max=5,
|
|
128
|
+
help=_("Spline interpolation order"),
|
|
129
|
+
).set_prop("display", active=prop)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@computation_function()
|
|
133
|
+
def rotate(src: ImageObj, p: RotateParam) -> ImageObj:
|
|
134
|
+
"""Rotate data with :py:func:`scipy.ndimage.rotate`
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
src: input image object
|
|
138
|
+
p: parameters
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Output image object
|
|
142
|
+
"""
|
|
143
|
+
dst = dst_1_to_1(src, "rotate", f"α={p.angle:.3f}°, mode='{p.mode}'")
|
|
144
|
+
dst.data = spi.rotate(
|
|
145
|
+
src.data,
|
|
146
|
+
p.angle,
|
|
147
|
+
reshape=p.reshape,
|
|
148
|
+
order=p.order,
|
|
149
|
+
mode=p.mode,
|
|
150
|
+
cval=p.cval,
|
|
151
|
+
prefilter=p.prefilter,
|
|
152
|
+
)
|
|
153
|
+
dst.roi = None # Reset ROI as it may change after rotation
|
|
154
|
+
return dst
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@computation_function()
|
|
158
|
+
def rotate90(src: ImageObj) -> ImageObj:
|
|
159
|
+
"""Rotate data 90° with :py:func:`numpy.rot90`
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
src: input image object
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Output image object
|
|
166
|
+
"""
|
|
167
|
+
dst = dst_1_to_1(src, "rotate90")
|
|
168
|
+
dst.data = np.rot90(src.data)
|
|
169
|
+
transformer.transform_roi(dst, "rotate", angle=-np.pi / 2, center=(dst.xc, dst.yc))
|
|
170
|
+
return dst
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@computation_function()
|
|
174
|
+
def rotate270(src: ImageObj) -> ImageObj:
|
|
175
|
+
"""Rotate data 270° with :py:func:`numpy.rot90`
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
src: input image object
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Output image object
|
|
182
|
+
"""
|
|
183
|
+
dst = dst_1_to_1(src, "rotate270")
|
|
184
|
+
dst.data = np.rot90(src.data, 3)
|
|
185
|
+
transformer.transform_roi(dst, "rotate", angle=np.pi / 2, center=(dst.xc, dst.yc))
|
|
186
|
+
return dst
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@computation_function()
|
|
190
|
+
def fliph(src: ImageObj) -> ImageObj:
|
|
191
|
+
"""Flip data horizontally with :py:func:`numpy.fliplr`
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
src: input image object
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Output image object
|
|
198
|
+
"""
|
|
199
|
+
dst = dst_1_to_1(src, "fliph")
|
|
200
|
+
dst.data = np.fliplr(src.data)
|
|
201
|
+
transformer.transform_roi(dst, "fliph", cx=dst.xc)
|
|
202
|
+
return dst
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@computation_function()
|
|
206
|
+
def flipv(src: ImageObj) -> ImageObj:
|
|
207
|
+
"""Flip data vertically with :py:func:`numpy.flipud`
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
src: input image object
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Output image object
|
|
214
|
+
"""
|
|
215
|
+
dst = dst_1_to_1(src, "flipv")
|
|
216
|
+
dst.data = np.flipud(src.data)
|
|
217
|
+
transformer.transform_roi(dst, "flipv", cy=dst.yc)
|
|
218
|
+
return dst
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class ResizeParam(gds.DataSet):
|
|
222
|
+
"""Resize parameters"""
|
|
223
|
+
|
|
224
|
+
prop = gds.ValueProp(False)
|
|
225
|
+
|
|
226
|
+
zoom = gds.FloatItem(_("Zoom"), default=1.0)
|
|
227
|
+
mode = gds.ChoiceItem(_("Mode"), BorderMode, default=BorderMode.CONSTANT)
|
|
228
|
+
cval = gds.FloatItem(
|
|
229
|
+
_("cval"),
|
|
230
|
+
default=0.0,
|
|
231
|
+
help=_(
|
|
232
|
+
"Value used for points outside the "
|
|
233
|
+
"boundaries of the input if mode is "
|
|
234
|
+
"'constant'"
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
prefilter = gds.BoolItem(_("Prefilter the input image"), default=True).set_prop(
|
|
238
|
+
"display", store=prop
|
|
239
|
+
)
|
|
240
|
+
order = gds.IntItem(
|
|
241
|
+
_("Order"),
|
|
242
|
+
default=3,
|
|
243
|
+
min=0,
|
|
244
|
+
max=5,
|
|
245
|
+
help=_("Spline interpolation order"),
|
|
246
|
+
).set_prop("display", active=prop)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@computation_function()
|
|
250
|
+
def resize(src: ImageObj, p: ResizeParam) -> ImageObj:
|
|
251
|
+
"""Zooming function with :py:func:`scipy.ndimage.zoom`
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
src: input image object
|
|
255
|
+
p: parameters
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Output image object
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
ValueError: if source image has non-uniform coordinates
|
|
262
|
+
"""
|
|
263
|
+
if not src.is_uniform_coords:
|
|
264
|
+
raise ValueError("Source image must have uniform coordinates for resampling")
|
|
265
|
+
mode = p.mode
|
|
266
|
+
dst = dst_1_to_1(src, "resize", f"zoom={p.zoom:.3f}")
|
|
267
|
+
dst.data = spi.zoom(
|
|
268
|
+
src.data,
|
|
269
|
+
p.zoom,
|
|
270
|
+
order=p.order,
|
|
271
|
+
mode=mode,
|
|
272
|
+
cval=p.cval,
|
|
273
|
+
prefilter=p.prefilter,
|
|
274
|
+
)
|
|
275
|
+
if not np.isnan(dst.dx) and not np.isnan(dst.dy):
|
|
276
|
+
dst.set_uniform_coords(dst.dx / p.zoom, dst.dy / p.zoom, dst.x0, dst.y0)
|
|
277
|
+
return dst
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@computation_function()
|
|
281
|
+
def transpose(src: ImageObj) -> ImageObj:
|
|
282
|
+
"""Transpose image with :py:func:`numpy.transpose`.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
src: Input image object.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Output image object.
|
|
289
|
+
"""
|
|
290
|
+
dst = dst_1_to_1(src, "transpose")
|
|
291
|
+
dst.data = np.transpose(src.data)
|
|
292
|
+
dst.xlabel = src.ylabel
|
|
293
|
+
dst.ylabel = src.xlabel
|
|
294
|
+
dst.xunit = src.yunit
|
|
295
|
+
dst.yunit = src.xunit
|
|
296
|
+
if src.is_uniform_coords:
|
|
297
|
+
dst.set_uniform_coords(src.dy, src.dx, src.y0, src.x0)
|
|
298
|
+
else:
|
|
299
|
+
dst.set_coords(src.ycoords, src.xcoords)
|
|
300
|
+
transformer.transform_roi(dst, "transpose")
|
|
301
|
+
return dst
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class Resampling2DParam(gds.DataSet):
|
|
305
|
+
"""Resample parameters for 2D images"""
|
|
306
|
+
|
|
307
|
+
# Output coordinate system
|
|
308
|
+
xmin = gds.FloatItem(
|
|
309
|
+
"X<sub>min</sub>",
|
|
310
|
+
default=None,
|
|
311
|
+
allow_none=True,
|
|
312
|
+
help=_("Minimum X-coordinate of the output image"),
|
|
313
|
+
)
|
|
314
|
+
xmax = gds.FloatItem(
|
|
315
|
+
"X<sub>max</sub>",
|
|
316
|
+
default=None,
|
|
317
|
+
allow_none=True,
|
|
318
|
+
help=_("Maximum X-coordinate of the output image"),
|
|
319
|
+
)
|
|
320
|
+
ymin = gds.FloatItem(
|
|
321
|
+
"Y<sub>min</sub>",
|
|
322
|
+
default=None,
|
|
323
|
+
allow_none=True,
|
|
324
|
+
help=_("Minimum Y-coordinate of the output image"),
|
|
325
|
+
)
|
|
326
|
+
ymax = gds.FloatItem(
|
|
327
|
+
"Y<sub>max</sub>",
|
|
328
|
+
default=None,
|
|
329
|
+
allow_none=True,
|
|
330
|
+
help=_("Maximum Y-coordinate of the output image"),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Mode selection
|
|
334
|
+
_prop = gds.GetAttrProp("mode")
|
|
335
|
+
_modes = (("dxy", _("Pixel size")), ("shape", _("Output shape")))
|
|
336
|
+
mode = gds.ChoiceItem(_("Mode"), _modes, default="shape", radio=True).set_prop(
|
|
337
|
+
"display", store=_prop
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Pixel size mode parameters
|
|
341
|
+
dx = gds.FloatItem(
|
|
342
|
+
"ΔX", default=None, allow_none=True, help=_("Pixel size in X direction")
|
|
343
|
+
).set_prop("display", active=gds.FuncProp(_prop, lambda x: x == "dxy"))
|
|
344
|
+
dy = gds.FloatItem(
|
|
345
|
+
"ΔY", default=None, allow_none=True, help=_("Pixel size in Y direction")
|
|
346
|
+
).set_prop("display", active=gds.FuncProp(_prop, lambda x: x == "dxy"))
|
|
347
|
+
|
|
348
|
+
# Shape mode parameters
|
|
349
|
+
width = gds.IntItem(
|
|
350
|
+
_("Width"),
|
|
351
|
+
default=None,
|
|
352
|
+
allow_none=True,
|
|
353
|
+
help=_("Output image width in pixels"),
|
|
354
|
+
).set_prop("display", active=gds.FuncProp(_prop, lambda x: x == "shape"))
|
|
355
|
+
height = gds.IntItem(
|
|
356
|
+
_("Height"),
|
|
357
|
+
default=None,
|
|
358
|
+
allow_none=True,
|
|
359
|
+
help=_("Output image height in pixels"),
|
|
360
|
+
).set_prop("display", active=gds.FuncProp(_prop, lambda x: x == "shape"))
|
|
361
|
+
|
|
362
|
+
# Interpolation parameters
|
|
363
|
+
method = gds.ChoiceItem(
|
|
364
|
+
_("Interpolation method"),
|
|
365
|
+
Interpolation2DMethod,
|
|
366
|
+
default=Interpolation2DMethod.LINEAR,
|
|
367
|
+
)
|
|
368
|
+
fill_value = gds.FloatItem(
|
|
369
|
+
_("Fill value"),
|
|
370
|
+
default=None,
|
|
371
|
+
help=_(
|
|
372
|
+
"Value to use for points outside the input image domain. "
|
|
373
|
+
"If None, uses NaN for extrapolation."
|
|
374
|
+
),
|
|
375
|
+
check=False,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def update_from_obj(self, obj: ImageObj) -> None:
|
|
379
|
+
"""Update parameters from an image object."""
|
|
380
|
+
if self.xmin is None:
|
|
381
|
+
self.xmin = obj.x0
|
|
382
|
+
if self.xmax is None:
|
|
383
|
+
self.xmax = obj.x0 + obj.width
|
|
384
|
+
if self.ymin is None:
|
|
385
|
+
self.ymin = obj.y0
|
|
386
|
+
if self.ymax is None:
|
|
387
|
+
self.ymax = obj.y0 + obj.height
|
|
388
|
+
if self.dx is None:
|
|
389
|
+
self.dx = obj.dx
|
|
390
|
+
if self.dy is None:
|
|
391
|
+
self.dy = obj.dy
|
|
392
|
+
if self.width is None:
|
|
393
|
+
self.width = obj.data.shape[1]
|
|
394
|
+
if self.height is None:
|
|
395
|
+
self.height = obj.data.shape[0]
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@computation_function()
|
|
399
|
+
def resampling(src: ImageObj, p: Resampling2DParam) -> ImageObj:
|
|
400
|
+
"""Resample image to new coordinate grid using interpolation
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
src: source image
|
|
404
|
+
p: resampling parameters
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Resampled image object
|
|
408
|
+
|
|
409
|
+
Raises:
|
|
410
|
+
ValueError: if source image has non-uniform coordinates
|
|
411
|
+
"""
|
|
412
|
+
if not src.is_uniform_coords:
|
|
413
|
+
raise ValueError("Source image must have uniform coordinates for resampling")
|
|
414
|
+
|
|
415
|
+
# Set output range - use source image bounds if not specified
|
|
416
|
+
output_xmin = p.xmin if p.xmin is not None else src.x0
|
|
417
|
+
output_xmax = p.xmax if p.xmax is not None else src.x0 + src.width
|
|
418
|
+
output_ymin = p.ymin if p.ymin is not None else src.y0
|
|
419
|
+
output_ymax = p.ymax if p.ymax is not None else src.y0 + src.height
|
|
420
|
+
|
|
421
|
+
# Calculate output grid dimensions and spacing
|
|
422
|
+
output_width_phys = output_xmax - output_xmin
|
|
423
|
+
output_height_phys = output_ymax - output_ymin
|
|
424
|
+
|
|
425
|
+
# Determine output grid parameters
|
|
426
|
+
method: Interpolation2DMethod = p.method
|
|
427
|
+
if p.mode == "dxy":
|
|
428
|
+
# Calculate dimensions from pixel sizes
|
|
429
|
+
if p.dx is None or p.dy is None:
|
|
430
|
+
raise ValueError("dx and dy must be specified in pixel size mode")
|
|
431
|
+
output_width = int(np.ceil(output_width_phys / p.dx))
|
|
432
|
+
output_height = int(np.ceil(output_height_phys / p.dy))
|
|
433
|
+
output_dx = p.dx
|
|
434
|
+
output_dy = p.dy
|
|
435
|
+
fill_suffix = f", fill_value={p.fill_value}" if p.fill_value is not None else ""
|
|
436
|
+
suffix = f"method={method.value}, dx={p.dx:.3f}, dy={p.dy:.3f}{fill_suffix}"
|
|
437
|
+
else:
|
|
438
|
+
# Use specified shape
|
|
439
|
+
if p.width is None or p.height is None:
|
|
440
|
+
raise ValueError("width and height must be specified in shape mode")
|
|
441
|
+
output_width = p.width
|
|
442
|
+
output_height = p.height
|
|
443
|
+
output_dx = output_width_phys / p.width if p.width > 0 else src.dx
|
|
444
|
+
output_dy = output_height_phys / p.height if p.height > 0 else src.dy
|
|
445
|
+
fill_suffix = f", fill_value={p.fill_value}" if p.fill_value is not None else ""
|
|
446
|
+
suffix = f"method={method.value}, size=({p.width}x{p.height}){fill_suffix}"
|
|
447
|
+
|
|
448
|
+
# Create destination image
|
|
449
|
+
dst = dst_1_to_1(src, "resample", suffix)
|
|
450
|
+
|
|
451
|
+
# Output coordinates (physical) - ensure we sample pixel centers, not boundaries
|
|
452
|
+
# For an image spanning [xmin, xmax], we want to sample at pixel centers
|
|
453
|
+
# The pixel centers should be distributed within the range,
|
|
454
|
+
# not including the exact endpoints
|
|
455
|
+
if output_width > 1:
|
|
456
|
+
out_x = np.linspace(
|
|
457
|
+
output_xmin + output_dx / 2, output_xmax - output_dx / 2, output_width
|
|
458
|
+
)
|
|
459
|
+
else:
|
|
460
|
+
out_x = np.array([(output_xmin + output_xmax) / 2])
|
|
461
|
+
|
|
462
|
+
if output_height > 1:
|
|
463
|
+
out_y = np.linspace(
|
|
464
|
+
output_ymin + output_dy / 2, output_ymax - output_dy / 2, output_height
|
|
465
|
+
)
|
|
466
|
+
else:
|
|
467
|
+
out_y = np.array([(output_ymin + output_ymax) / 2])
|
|
468
|
+
|
|
469
|
+
# Create meshgrids
|
|
470
|
+
out_X, out_Y = np.meshgrid(out_x, out_y, indexing="xy")
|
|
471
|
+
|
|
472
|
+
# Convert interpolation method to scipy parameter
|
|
473
|
+
if method == Interpolation2DMethod.LINEAR:
|
|
474
|
+
order = 1
|
|
475
|
+
elif method == Interpolation2DMethod.CUBIC:
|
|
476
|
+
order = 3
|
|
477
|
+
elif method == Interpolation2DMethod.NEAREST:
|
|
478
|
+
order = 0
|
|
479
|
+
else:
|
|
480
|
+
order = 1 # fallback to linear
|
|
481
|
+
|
|
482
|
+
# Convert physical coordinates to source image indices
|
|
483
|
+
src_i = (out_X - src.x0) / src.dx
|
|
484
|
+
src_j = (out_Y - src.y0) / src.dy
|
|
485
|
+
|
|
486
|
+
# Perform interpolation using map_coordinates
|
|
487
|
+
# Note: map_coordinates expects (j, i) order (row, col)
|
|
488
|
+
coordinates = np.array([src_j.ravel(), src_i.ravel()])
|
|
489
|
+
|
|
490
|
+
# Determine fill value for interpolation
|
|
491
|
+
cval = p.fill_value if p.fill_value is not None else np.nan
|
|
492
|
+
|
|
493
|
+
# For NaN fill values, we need to work with float data to preserve NaN
|
|
494
|
+
# Convert to float if necessary to allow NaN representation
|
|
495
|
+
if np.isnan(cval) and not np.issubdtype(src.data.dtype, np.floating):
|
|
496
|
+
input_data = src.data.astype(np.float64)
|
|
497
|
+
else:
|
|
498
|
+
input_data = src.data
|
|
499
|
+
|
|
500
|
+
# Interpolate
|
|
501
|
+
resampled_data = spi.map_coordinates(
|
|
502
|
+
input_data, coordinates, order=order, mode="constant", cval=cval, prefilter=True
|
|
503
|
+
).reshape(output_height, output_width)
|
|
504
|
+
|
|
505
|
+
# Set output data and coordinate system
|
|
506
|
+
dst.data = resampled_data
|
|
507
|
+
dst.set_uniform_coords(output_dx, output_dy, output_xmin, output_ymin)
|
|
508
|
+
|
|
509
|
+
return dst
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class UniformCoordsParam(gds.DataSet):
|
|
513
|
+
"""Uniform coordinates parameters"""
|
|
514
|
+
|
|
515
|
+
x0 = gds.FloatItem("X<sub>0</sub>", default=0.0, help=_("Origin X-axis coordinate"))
|
|
516
|
+
y0 = gds.FloatItem("Y<sub>0</sub>", default=0.0, help=_("Origin Y-axis coordinate"))
|
|
517
|
+
dx = gds.FloatItem("Δx", default=1.0, help=_("Pixel size along X-axis"))
|
|
518
|
+
dy = gds.FloatItem("Δy", default=1.0, help=_("Pixel size along Y-axis"))
|
|
519
|
+
|
|
520
|
+
def update_from_obj(self, obj: ImageObj) -> None:
|
|
521
|
+
"""Update default values from image object's non-uniform coordinates.
|
|
522
|
+
|
|
523
|
+
This method extracts uniform coordinate approximations from non-uniform
|
|
524
|
+
coordinate arrays, handling numerical precision issues that may arise
|
|
525
|
+
from arrays created using linspace.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
obj: Image object with non-uniform coordinates
|
|
529
|
+
"""
|
|
530
|
+
if obj.is_uniform_coords:
|
|
531
|
+
# Already uniform, just copy the values
|
|
532
|
+
self.x0 = obj.x0
|
|
533
|
+
self.y0 = obj.y0
|
|
534
|
+
self.dx = obj.dx
|
|
535
|
+
self.dy = obj.dy
|
|
536
|
+
else:
|
|
537
|
+
# Extract from non-uniform coordinates
|
|
538
|
+
if obj.xcoords is not None and len(obj.xcoords) >= 2:
|
|
539
|
+
self.x0 = float(obj.xcoords[0])
|
|
540
|
+
# Calculate dx with rounding to handle numerical precision
|
|
541
|
+
dx_raw = (obj.xcoords[-1] - obj.xcoords[0]) / (len(obj.xcoords) - 1)
|
|
542
|
+
# Round to reasonable precision (12 decimal places)
|
|
543
|
+
self.dx = float(np.round(dx_raw, 12))
|
|
544
|
+
else:
|
|
545
|
+
self.x0 = 0.0
|
|
546
|
+
self.dx = 1.0
|
|
547
|
+
|
|
548
|
+
if obj.ycoords is not None and len(obj.ycoords) >= 2:
|
|
549
|
+
self.y0 = float(obj.ycoords[0])
|
|
550
|
+
# Calculate dy with rounding to handle numerical precision
|
|
551
|
+
dy_raw = (obj.ycoords[-1] - obj.ycoords[0]) / (len(obj.ycoords) - 1)
|
|
552
|
+
# Round to reasonable precision (12 decimal places)
|
|
553
|
+
self.dy = float(np.round(dy_raw, 12))
|
|
554
|
+
else:
|
|
555
|
+
self.y0 = 0.0
|
|
556
|
+
self.dy = 1.0
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
@computation_function()
|
|
560
|
+
def set_uniform_coords(src: ImageObj, p: UniformCoordsParam) -> ImageObj:
|
|
561
|
+
"""Convert image to uniform coordinate system
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
src: input image object
|
|
565
|
+
p: uniform coordinates parameters
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
Output image object with uniform coordinates
|
|
569
|
+
"""
|
|
570
|
+
dst = dst_1_to_1(src, "uniform_coords", f"dx={p.dx}, dy={p.dy}")
|
|
571
|
+
dst.set_uniform_coords(p.dx, p.dy, p.x0, p.y0)
|
|
572
|
+
return dst
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
class XYZCalibrateParam(gds.DataSet):
|
|
576
|
+
"""Image polynomial calibration parameters"""
|
|
577
|
+
|
|
578
|
+
axes = (("x", _("X-axis")), ("y", _("Y-axis")), ("z", _("Z-axis")))
|
|
579
|
+
axis = gds.ChoiceItem(_("Calibrate"), axes, default="z")
|
|
580
|
+
a0 = gds.FloatItem("a<sub>0</sub>", default=0.0, help=_("Constant term"))
|
|
581
|
+
a1 = gds.FloatItem("a<sub>1</sub>", default=1.0, help=_("Linear term"))
|
|
582
|
+
a2 = gds.FloatItem("a<sub>2</sub>", default=0.0, help=_("Quadratic term"))
|
|
583
|
+
a3 = gds.FloatItem("a<sub>3</sub>", default=0.0, help=_("Cubic term"))
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
@computation_function()
|
|
587
|
+
def calibration(src: ImageObj, p: XYZCalibrateParam) -> ImageObj:
|
|
588
|
+
"""Compute polynomial calibration
|
|
589
|
+
|
|
590
|
+
Applies polynomial transformation: dst = a0 + a1*src + a2*src² + a3*src³
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
src: input image object
|
|
594
|
+
p: calibration parameters
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
Output image object
|
|
598
|
+
"""
|
|
599
|
+
# Build polynomial description for metadata
|
|
600
|
+
terms = []
|
|
601
|
+
if p.a0 != 0.0:
|
|
602
|
+
terms.append(f"{p.a0}")
|
|
603
|
+
if p.a1 != 0.0:
|
|
604
|
+
terms.append(f"{p.a1}*{p.axis}" if p.a1 != 1.0 else p.axis)
|
|
605
|
+
if p.a2 != 0.0:
|
|
606
|
+
terms.append(f"{p.a2}*{p.axis}²")
|
|
607
|
+
if p.a3 != 0.0:
|
|
608
|
+
terms.append(f"{p.a3}*{p.axis}³")
|
|
609
|
+
poly_str = "+".join(terms) if terms else "0"
|
|
610
|
+
|
|
611
|
+
dst = dst_1_to_1(src, "calibration", f"{p.axis}={poly_str}")
|
|
612
|
+
|
|
613
|
+
shape = src.data.shape
|
|
614
|
+
|
|
615
|
+
if p.axis == "z":
|
|
616
|
+
# Apply polynomial to data values
|
|
617
|
+
data = src.data.astype(float)
|
|
618
|
+
dst.data = p.a0 + p.a1 * data + p.a2 * data**2 + p.a3 * data**3
|
|
619
|
+
restore_data_outside_roi(dst, src)
|
|
620
|
+
elif p.axis == "x":
|
|
621
|
+
# For X-axis, polynomial calibration requires non-uniform coordinates
|
|
622
|
+
# (unless it's linear but we don't special case that here)
|
|
623
|
+
if src.is_uniform_coords:
|
|
624
|
+
# Generate uniform coordinates array
|
|
625
|
+
x_uniform = src.x0 + np.arange(src.data.shape[1]) * src.dx
|
|
626
|
+
# Apply polynomial transformation
|
|
627
|
+
x_new = p.a0 + p.a1 * x_uniform + p.a2 * x_uniform**2 + p.a3 * x_uniform**3
|
|
628
|
+
# Set non-uniform coordinates
|
|
629
|
+
ycoords = np.linspace(src.y0, src.y0 + src.dy * (shape[0] - 1), shape[0])
|
|
630
|
+
dst.set_coords(x_new, ycoords)
|
|
631
|
+
else:
|
|
632
|
+
# Apply polynomial to existing non-uniform coordinates
|
|
633
|
+
x_new = (
|
|
634
|
+
p.a0
|
|
635
|
+
+ p.a1 * src.xcoords
|
|
636
|
+
+ p.a2 * src.xcoords**2
|
|
637
|
+
+ p.a3 * src.xcoords**3
|
|
638
|
+
)
|
|
639
|
+
dst.set_coords(x_new, dst.ycoords)
|
|
640
|
+
elif p.axis == "y":
|
|
641
|
+
# For Y-axis, polynomial calibration requires non-uniform coordinates
|
|
642
|
+
if src.is_uniform_coords:
|
|
643
|
+
# Generate uniform coordinates array
|
|
644
|
+
y_uniform = src.y0 + np.arange(src.data.shape[0]) * src.dy
|
|
645
|
+
# Apply polynomial transformation
|
|
646
|
+
y_new = p.a0 + p.a1 * y_uniform + p.a2 * y_uniform**2 + p.a3 * y_uniform**3
|
|
647
|
+
# Set non-uniform coordinates
|
|
648
|
+
xcoords = np.linspace(src.x0, src.x0 + src.dx * (shape[1] - 1), shape[1])
|
|
649
|
+
dst.set_coords(xcoords, y_new)
|
|
650
|
+
else:
|
|
651
|
+
# Apply polynomial to existing non-uniform coordinates
|
|
652
|
+
y_new = (
|
|
653
|
+
p.a0
|
|
654
|
+
+ p.a1 * src.ycoords
|
|
655
|
+
+ p.a2 * src.ycoords**2
|
|
656
|
+
+ p.a3 * src.ycoords**3
|
|
657
|
+
)
|
|
658
|
+
dst.set_coords(dst.xcoords, y_new)
|
|
659
|
+
else: # Should not happen
|
|
660
|
+
raise ValueError(f"Unknown axis: {p.axis}") # pragma: no cover
|
|
661
|
+
return dst
|