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,524 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Image object definition
|
|
5
|
+
=======================
|
|
6
|
+
|
|
7
|
+
This module defines the main `ImageObj` class for representing 2D image data.
|
|
8
|
+
|
|
9
|
+
The `ImageObj` class provides:
|
|
10
|
+
|
|
11
|
+
- Data storage for 2D arrays with associated metadata
|
|
12
|
+
- Physical coordinate system with origin and pixel spacing
|
|
13
|
+
- Axis labeling and units
|
|
14
|
+
- Scale management (linear/logarithmic)
|
|
15
|
+
- DICOM template support
|
|
16
|
+
- ROI (Region of Interest) integration
|
|
17
|
+
- Coordinate conversion utilities (physical ↔ pixel)
|
|
18
|
+
|
|
19
|
+
This is the core class for image processing operations in Sigima.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
23
|
+
# pylint: disable=duplicate-code
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
from collections.abc import Mapping
|
|
29
|
+
from typing import Any, Literal, Type
|
|
30
|
+
|
|
31
|
+
import guidata.dataset as gds
|
|
32
|
+
import numpy as np
|
|
33
|
+
from numpy import ma
|
|
34
|
+
|
|
35
|
+
from sigima.config import _
|
|
36
|
+
from sigima.objects import base
|
|
37
|
+
from sigima.objects.image.roi import ImageROI
|
|
38
|
+
from sigima.tools.datatypes import clip_astype
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def to_builtin(obj) -> str | int | float | list | dict | np.ndarray | None:
|
|
42
|
+
"""Convert an object implementing a numeric value or collection
|
|
43
|
+
into the corresponding builtin/NumPy type.
|
|
44
|
+
|
|
45
|
+
Return None if conversion fails."""
|
|
46
|
+
try:
|
|
47
|
+
return int(obj) if int(obj) == float(obj) else float(obj)
|
|
48
|
+
except (TypeError, ValueError):
|
|
49
|
+
pass
|
|
50
|
+
if isinstance(obj, str):
|
|
51
|
+
return obj
|
|
52
|
+
if hasattr(obj, "__iter__"):
|
|
53
|
+
try:
|
|
54
|
+
return list(obj)
|
|
55
|
+
except (TypeError, ValueError):
|
|
56
|
+
pass
|
|
57
|
+
if hasattr(obj, "__dict__"):
|
|
58
|
+
try:
|
|
59
|
+
return dict(obj.__dict__)
|
|
60
|
+
except (TypeError, ValueError):
|
|
61
|
+
pass
|
|
62
|
+
if isinstance(obj, np.ndarray):
|
|
63
|
+
return obj
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ImageObj(gds.DataSet, base.BaseObj[ImageROI]):
|
|
68
|
+
"""Image object"""
|
|
69
|
+
|
|
70
|
+
PREFIX = "i"
|
|
71
|
+
VALID_DTYPES = (
|
|
72
|
+
np.uint8,
|
|
73
|
+
np.uint16,
|
|
74
|
+
np.int16,
|
|
75
|
+
np.int32,
|
|
76
|
+
np.float32,
|
|
77
|
+
np.float64,
|
|
78
|
+
np.complex128,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def __init__(self, title=None, comment=None, icon=""):
|
|
82
|
+
"""Constructor
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
title: title
|
|
86
|
+
comment: comment
|
|
87
|
+
icon: icon
|
|
88
|
+
"""
|
|
89
|
+
gds.DataSet.__init__(self, title, comment, icon)
|
|
90
|
+
base.BaseObj.__init__(self)
|
|
91
|
+
self._dicom_template = None
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def get_roi_class() -> Type[ImageROI]:
|
|
95
|
+
"""Return ROI class"""
|
|
96
|
+
# Import here to avoid circular imports
|
|
97
|
+
|
|
98
|
+
return ImageROI
|
|
99
|
+
|
|
100
|
+
def __add_metadata(self, key: str, value: Any) -> None:
|
|
101
|
+
"""Add value to metadata if value can be converted into builtin/NumPy type
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
key: key
|
|
105
|
+
value: value
|
|
106
|
+
"""
|
|
107
|
+
stored_val = to_builtin(value)
|
|
108
|
+
if stored_val is not None:
|
|
109
|
+
self.metadata[key] = stored_val
|
|
110
|
+
|
|
111
|
+
def __set_metadata_from(self, obj: Mapping | dict) -> None:
|
|
112
|
+
"""Set metadata from object: dict-like (only string keys are considered)
|
|
113
|
+
or any other object (iterating over supported attributes)
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
obj: object
|
|
117
|
+
"""
|
|
118
|
+
self.reset_metadata_to_defaults()
|
|
119
|
+
ptn = r"__[\S_]*__$"
|
|
120
|
+
if isinstance(obj, Mapping):
|
|
121
|
+
for key, value in obj.items():
|
|
122
|
+
if isinstance(key, str) and not re.match(ptn, key):
|
|
123
|
+
self.__add_metadata(key, value)
|
|
124
|
+
else:
|
|
125
|
+
for attrname in dir(obj):
|
|
126
|
+
if attrname != "GroupLength" and not re.match(ptn, attrname):
|
|
127
|
+
try:
|
|
128
|
+
attr = getattr(obj, attrname)
|
|
129
|
+
if not callable(attr) and attr:
|
|
130
|
+
self.__add_metadata(attrname, attr)
|
|
131
|
+
except AttributeError:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def dicom_template(self):
|
|
136
|
+
"""Get DICOM template"""
|
|
137
|
+
return self._dicom_template
|
|
138
|
+
|
|
139
|
+
@dicom_template.setter
|
|
140
|
+
def dicom_template(self, template):
|
|
141
|
+
"""Set DICOM template"""
|
|
142
|
+
if template is not None:
|
|
143
|
+
ipp = getattr(template, "ImagePositionPatient", None)
|
|
144
|
+
x0, y0 = 0.0, 0.0 if ipp is None else (float(ipp[0]), float(ipp[1]))
|
|
145
|
+
pxs = getattr(template, "PixelSpacing", None)
|
|
146
|
+
dx, dy = 1.0, 1.0 if pxs is None else (float(pxs[0]), float(pxs[1]))
|
|
147
|
+
self.set_uniform_coords(dx, dy, x0, y0)
|
|
148
|
+
self.__set_metadata_from(template)
|
|
149
|
+
self._dicom_template = template
|
|
150
|
+
|
|
151
|
+
_tabs = gds.BeginTabGroup("all")
|
|
152
|
+
|
|
153
|
+
_datag = gds.BeginGroup(_("Data"))
|
|
154
|
+
data = gds.FloatArrayItem(_("Data")) # type: ignore[assignment]
|
|
155
|
+
metadata = gds.DictItem(_("Metadata"), default={}) # type: ignore[assignment]
|
|
156
|
+
annotations = gds.StringItem(_("Annotations"), default="").set_prop(
|
|
157
|
+
"display",
|
|
158
|
+
hide=True,
|
|
159
|
+
) # Annotations as a serialized JSON string # type: ignore[assignment]
|
|
160
|
+
_e_datag = gds.EndGroup(_("Data"))
|
|
161
|
+
|
|
162
|
+
def _compute_xmin(self) -> float:
|
|
163
|
+
"""Compute Xmin"""
|
|
164
|
+
if self.data is None or self.data.size == 0:
|
|
165
|
+
return 0.0
|
|
166
|
+
if self.is_uniform_coords:
|
|
167
|
+
return self.x0
|
|
168
|
+
if self.xcoords is None or self.xcoords.size == 0:
|
|
169
|
+
return np.nan
|
|
170
|
+
return self.xcoords[0]
|
|
171
|
+
|
|
172
|
+
def _compute_xmax(self) -> float:
|
|
173
|
+
"""Compute Xmax"""
|
|
174
|
+
if self.data is None or self.data.size == 0:
|
|
175
|
+
return 0.0
|
|
176
|
+
if self.is_uniform_coords:
|
|
177
|
+
return self.x0 + self.width - self.dx
|
|
178
|
+
if self.xcoords is None or self.xcoords.size == 0:
|
|
179
|
+
return np.nan
|
|
180
|
+
return self.xcoords[-1]
|
|
181
|
+
|
|
182
|
+
def _compute_ymin(self) -> float:
|
|
183
|
+
"""Compute Ymin"""
|
|
184
|
+
if self.data is None or self.data.size == 0:
|
|
185
|
+
return 0.0
|
|
186
|
+
if self.is_uniform_coords:
|
|
187
|
+
return self.y0
|
|
188
|
+
if self.ycoords is None or self.ycoords.size == 0:
|
|
189
|
+
return np.nan
|
|
190
|
+
return self.ycoords[0]
|
|
191
|
+
|
|
192
|
+
def _compute_ymax(self) -> float:
|
|
193
|
+
"""Compute Ymax"""
|
|
194
|
+
if self.data is None or self.data.size == 0:
|
|
195
|
+
return 0.0
|
|
196
|
+
if self.is_uniform_coords:
|
|
197
|
+
return self.y0 + self.height - self.dy
|
|
198
|
+
if self.ycoords is None or self.ycoords.size == 0:
|
|
199
|
+
return np.nan
|
|
200
|
+
return self.ycoords[-1]
|
|
201
|
+
|
|
202
|
+
_dxdyg = gds.BeginGroup(f"{_('Origin')} / {_('Pixel spacing')}")
|
|
203
|
+
_prop_uniform = gds.GetAttrProp("is_uniform_coords")
|
|
204
|
+
is_uniform_coords = gds.BoolItem(_("Uniform coordinates"), default=True).set_prop(
|
|
205
|
+
"display", store=_prop_uniform, active=False
|
|
206
|
+
)
|
|
207
|
+
_origin = gds.BeginGroup(_("Origin"))
|
|
208
|
+
x0 = gds.FloatItem("X<sub>0</sub>", default=0.0).set_prop(
|
|
209
|
+
"display", active=_prop_uniform
|
|
210
|
+
)
|
|
211
|
+
y0 = (
|
|
212
|
+
gds.FloatItem("Y<sub>0</sub>", default=0.0)
|
|
213
|
+
.set_prop("display", active=_prop_uniform)
|
|
214
|
+
.set_pos(col=1)
|
|
215
|
+
)
|
|
216
|
+
_e_origin = gds.EndGroup(_("Origin"))
|
|
217
|
+
_pixel_spacing = gds.BeginGroup(_("Pixel spacing"))
|
|
218
|
+
dx = gds.FloatItem("Δx", default=1.0).set_prop("display", active=_prop_uniform)
|
|
219
|
+
dy = (
|
|
220
|
+
gds.FloatItem("Δy", default=1.0)
|
|
221
|
+
.set_prop("display", active=_prop_uniform)
|
|
222
|
+
.set_pos(col=1)
|
|
223
|
+
)
|
|
224
|
+
_e_pixel_spacing = gds.EndGroup(_("Pixel spacing"))
|
|
225
|
+
_boundaries = gds.BeginGroup(_("Extent"))
|
|
226
|
+
xmin = gds.FloatItem("X<sub>MIN</sub>").set_computed(_compute_xmin)
|
|
227
|
+
xmax = gds.FloatItem("X<sub>MAX</sub>").set_pos(col=1).set_computed(_compute_xmax)
|
|
228
|
+
ymin = gds.FloatItem("Y<sub>MIN</sub>").set_computed(_compute_ymin)
|
|
229
|
+
ymax = gds.FloatItem("Y<sub>MAX</sub>").set_pos(col=1).set_computed(_compute_ymax)
|
|
230
|
+
_e_boundaries = gds.EndGroup(_("Extent"))
|
|
231
|
+
_e_dxdyg = gds.EndGroup(f"{_('Origin')} / {_('Pixel spacing')}")
|
|
232
|
+
|
|
233
|
+
_coordsg = gds.BeginGroup(_("Coordinates"))
|
|
234
|
+
xcoords = gds.FloatArrayItem(
|
|
235
|
+
_("X coordinates"),
|
|
236
|
+
default=np.array([], dtype=float),
|
|
237
|
+
).set_prop("display", active=gds.NotProp(_prop_uniform)) # type: ignore[assignment]
|
|
238
|
+
ycoords = (
|
|
239
|
+
gds.FloatArrayItem(_("Y coordinates"), default=np.array([], dtype=float))
|
|
240
|
+
.set_prop("display", active=gds.NotProp(_prop_uniform))
|
|
241
|
+
.set_pos(col=1)
|
|
242
|
+
) # type: ignore[assignment]
|
|
243
|
+
_e_coordsg = gds.EndGroup(_("Coordinates"))
|
|
244
|
+
|
|
245
|
+
def set_uniform_coords(
|
|
246
|
+
self, dx: float, dy: float, x0: float = 0.0, y0: float = 0.0
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Set uniform coordinates and clear non-uniform arrays.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
dx: pixel size along X-axis
|
|
252
|
+
dy: pixel size along Y-axis
|
|
253
|
+
x0: origin X-axis coordinate
|
|
254
|
+
y0: origin Y-axis coordinate
|
|
255
|
+
"""
|
|
256
|
+
self.is_uniform_coords = True
|
|
257
|
+
self.xcoords = np.array([], dtype=float)
|
|
258
|
+
self.ycoords = np.array([], dtype=float)
|
|
259
|
+
self.dx, self.dy, self.x0, self.y0 = dx, dy, x0, y0
|
|
260
|
+
|
|
261
|
+
def set_coords(self, xcoords: np.ndarray, ycoords: np.ndarray) -> None:
|
|
262
|
+
"""Set non-uniform coordinates.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
xcoords: X coordinates
|
|
266
|
+
ycoords: Y coordinates
|
|
267
|
+
"""
|
|
268
|
+
self.is_uniform_coords = False
|
|
269
|
+
self.xcoords = xcoords
|
|
270
|
+
self.ycoords = ycoords
|
|
271
|
+
|
|
272
|
+
def switch_coords_to(self, coords_type: Literal["uniform", "non-uniform"]) -> None:
|
|
273
|
+
"""Switch coordinates to uniform or non-uniform representation.
|
|
274
|
+
|
|
275
|
+
If switching to uniform, the image pixel size and origin are computed from
|
|
276
|
+
the current non-uniform coordinates. If switching to non-uniform, the
|
|
277
|
+
corresponding coordinate arrays are generated from the current pixel size
|
|
278
|
+
and origin. If the current coordinates are already of the requested type,
|
|
279
|
+
no action is performed.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
coords_type: 'uniform' or 'non-uniform'
|
|
283
|
+
|
|
284
|
+
Raises:
|
|
285
|
+
ValueError: If switching to uniform coordinates fails due to insufficient
|
|
286
|
+
non-uniform coordinates defined
|
|
287
|
+
"""
|
|
288
|
+
if coords_type == "uniform" and not self.is_uniform_coords:
|
|
289
|
+
if self.xcoords.size >= 2 and self.ycoords.size >= 2:
|
|
290
|
+
x0, y0 = float(self.xcoords[0]), float(self.ycoords[0])
|
|
291
|
+
dx = float(self.xcoords[-1] - self.xcoords[0]) / (self.xcoords.size - 1)
|
|
292
|
+
dy = float(self.ycoords[-1] - self.ycoords[0]) / (self.ycoords.size - 1)
|
|
293
|
+
self.set_uniform_coords(dx, dy, x0, y0)
|
|
294
|
+
else:
|
|
295
|
+
raise ValueError(
|
|
296
|
+
"Cannot switch to uniform coordinates: "
|
|
297
|
+
"not enough non-uniform coordinates defined"
|
|
298
|
+
)
|
|
299
|
+
elif coords_type == "non-uniform" and self.is_uniform_coords:
|
|
300
|
+
shape = self.data.shape
|
|
301
|
+
xcoords = np.linspace(self.x0, self.x0 + self.dx * (shape[1] - 1), shape[1])
|
|
302
|
+
ycoords = np.linspace(self.y0, self.y0 + self.dy * (shape[0] - 1), shape[0])
|
|
303
|
+
self.set_coords(xcoords, ycoords)
|
|
304
|
+
|
|
305
|
+
_unitsg = gds.BeginGroup(_("Titles / Units"))
|
|
306
|
+
title = gds.StringItem(_("Image title"), default=_("Untitled"))
|
|
307
|
+
_tabs_u = gds.BeginTabGroup("units")
|
|
308
|
+
_unitsx = gds.BeginGroup(_("X-axis"))
|
|
309
|
+
xlabel = gds.StringItem(_("Title"), default="")
|
|
310
|
+
xunit = gds.StringItem(_("Unit"), default="")
|
|
311
|
+
_e_unitsx = gds.EndGroup(_("X-axis"))
|
|
312
|
+
_unitsy = gds.BeginGroup(_("Y-axis"))
|
|
313
|
+
ylabel = gds.StringItem(_("Title"), default="")
|
|
314
|
+
yunit = gds.StringItem(_("Unit"), default="")
|
|
315
|
+
_e_unitsy = gds.EndGroup(_("Y-axis"))
|
|
316
|
+
_unitsz = gds.BeginGroup(_("Z-axis"))
|
|
317
|
+
zlabel = gds.StringItem(_("Title"), default="")
|
|
318
|
+
zunit = gds.StringItem(_("Unit"), default="")
|
|
319
|
+
_e_unitsz = gds.EndGroup(_("Z-axis"))
|
|
320
|
+
_e_tabs_u = gds.EndTabGroup("units")
|
|
321
|
+
_e_unitsg = gds.EndGroup(_("Titles / Units"))
|
|
322
|
+
|
|
323
|
+
_scalesg = gds.BeginGroup(_("Scales"))
|
|
324
|
+
_prop_autoscale = gds.GetAttrProp("autoscale")
|
|
325
|
+
autoscale = gds.BoolItem(_("Auto scale"), default=True).set_prop(
|
|
326
|
+
"display", store=_prop_autoscale
|
|
327
|
+
)
|
|
328
|
+
_tabs_b = gds.BeginTabGroup("bounds")
|
|
329
|
+
_boundsx = gds.BeginGroup(_("X-axis"))
|
|
330
|
+
xscalelog = gds.BoolItem(_("Logarithmic scale"), default=False)
|
|
331
|
+
xscalemin = gds.FloatItem(_("Lower bound"), check=False).set_prop(
|
|
332
|
+
"display", active=gds.NotProp(_prop_autoscale)
|
|
333
|
+
)
|
|
334
|
+
xscalemax = gds.FloatItem(_("Upper bound"), check=False).set_prop(
|
|
335
|
+
"display", active=gds.NotProp(_prop_autoscale)
|
|
336
|
+
)
|
|
337
|
+
_e_boundsx = gds.EndGroup(_("X-axis"))
|
|
338
|
+
_boundsy = gds.BeginGroup(_("Y-axis"))
|
|
339
|
+
yscalelog = gds.BoolItem(_("Logarithmic scale"), default=False)
|
|
340
|
+
yscalemin = gds.FloatItem(_("Lower bound"), check=False).set_prop(
|
|
341
|
+
"display", active=gds.NotProp(_prop_autoscale)
|
|
342
|
+
)
|
|
343
|
+
yscalemax = gds.FloatItem(_("Upper bound"), check=False).set_prop(
|
|
344
|
+
"display", active=gds.NotProp(_prop_autoscale)
|
|
345
|
+
)
|
|
346
|
+
_e_boundsy = gds.EndGroup(_("Y-axis"))
|
|
347
|
+
_boundsz = gds.BeginGroup(_("LUT range"))
|
|
348
|
+
zscalemin = gds.FloatItem(_("Lower bound"), check=False)
|
|
349
|
+
zscalemax = gds.FloatItem(_("Upper bound"), check=False)
|
|
350
|
+
_e_boundsz = gds.EndGroup(_("LUT range"))
|
|
351
|
+
_e_tabs_b = gds.EndTabGroup("bounds")
|
|
352
|
+
_e_scalesg = gds.EndGroup(_("Scales"))
|
|
353
|
+
|
|
354
|
+
_e_tabs = gds.EndTabGroup("all")
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def width(self) -> float:
|
|
358
|
+
"""Return image width, i.e. number of columns multiplied by pixel size"""
|
|
359
|
+
return self.data.shape[1] * self.dx
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def height(self) -> float:
|
|
363
|
+
"""Return image height, i.e. number of rows multiplied by pixel size"""
|
|
364
|
+
return self.data.shape[0] * self.dy
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def xc(self) -> float:
|
|
368
|
+
"""Return image center X-axis coordinate"""
|
|
369
|
+
return self.x0 + 0.5 * self.width
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def yc(self) -> float:
|
|
373
|
+
"""Return image center Y-axis coordinate"""
|
|
374
|
+
return self.y0 + 0.5 * self.height
|
|
375
|
+
|
|
376
|
+
def get_data(self, roi_index: int | None = None) -> np.ndarray:
|
|
377
|
+
"""
|
|
378
|
+
Return original data (if ROI is not defined or `roi_index` is None),
|
|
379
|
+
or ROI data (if both ROI and `roi_index` are defined).
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
roi_index: ROI index
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Masked data
|
|
386
|
+
"""
|
|
387
|
+
if self.roi is None or roi_index is None:
|
|
388
|
+
view = self.data.view(ma.MaskedArray)
|
|
389
|
+
view.mask = np.isnan(self.data)
|
|
390
|
+
return view
|
|
391
|
+
single_roi = self.roi.get_single_roi(roi_index)
|
|
392
|
+
# pylint: disable=unbalanced-tuple-unpacking
|
|
393
|
+
x0, y0, x1, y1 = self.physical_to_indices(single_roi.get_bounding_box(self))
|
|
394
|
+
return self.get_masked_view()[y0:y1, x0:x1]
|
|
395
|
+
|
|
396
|
+
def copy(
|
|
397
|
+
self,
|
|
398
|
+
title: str | None = None,
|
|
399
|
+
dtype: np.dtype | None = None,
|
|
400
|
+
all_metadata: bool = False,
|
|
401
|
+
) -> ImageObj:
|
|
402
|
+
"""Copy object.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
title: title
|
|
406
|
+
dtype: data type
|
|
407
|
+
all_metadata: if True, copy all metadata, otherwise only basic metadata
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Copied object
|
|
411
|
+
"""
|
|
412
|
+
title = self.title if title is None else title
|
|
413
|
+
obj = ImageObj(title=title)
|
|
414
|
+
obj.title = title
|
|
415
|
+
obj.xlabel = self.xlabel
|
|
416
|
+
obj.ylabel = self.ylabel
|
|
417
|
+
obj.zlabel = self.zlabel
|
|
418
|
+
obj.xunit = self.xunit
|
|
419
|
+
obj.yunit = self.yunit
|
|
420
|
+
obj.zunit = self.zunit
|
|
421
|
+
obj.metadata = base.deepcopy_metadata(self.metadata, all_metadata=all_metadata)
|
|
422
|
+
obj.annotations = self.annotations
|
|
423
|
+
if self.data is not None:
|
|
424
|
+
obj.data = np.array(self.data, copy=True, dtype=dtype)
|
|
425
|
+
obj.is_uniform_coords = self.is_uniform_coords
|
|
426
|
+
if self.is_uniform_coords:
|
|
427
|
+
obj.dx = self.dx
|
|
428
|
+
obj.dy = self.dy
|
|
429
|
+
obj.x0 = self.x0
|
|
430
|
+
obj.y0 = self.y0
|
|
431
|
+
else:
|
|
432
|
+
obj.xcoords = np.array(self.xcoords, copy=True)
|
|
433
|
+
obj.ycoords = np.array(self.ycoords, copy=True)
|
|
434
|
+
obj.autoscale = self.autoscale
|
|
435
|
+
obj.xscalelog = self.xscalelog
|
|
436
|
+
obj.xscalemin = self.xscalemin
|
|
437
|
+
obj.xscalemax = self.xscalemax
|
|
438
|
+
obj.yscalelog = self.yscalelog
|
|
439
|
+
obj.yscalemin = self.yscalemin
|
|
440
|
+
obj.yscalemax = self.yscalemax
|
|
441
|
+
obj.zscalemin = self.zscalemin
|
|
442
|
+
obj.zscalemax = self.zscalemax
|
|
443
|
+
obj.dicom_template = self.dicom_template
|
|
444
|
+
return obj
|
|
445
|
+
|
|
446
|
+
def set_data_type(self, dtype: np.dtype) -> None:
|
|
447
|
+
"""Change data type.
|
|
448
|
+
If data type is integer, clip values to the new data type's range, thus avoiding
|
|
449
|
+
overflow or underflow.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
Data type
|
|
453
|
+
"""
|
|
454
|
+
self.data = clip_astype(self.data, dtype)
|
|
455
|
+
|
|
456
|
+
def physical_to_indices(
|
|
457
|
+
self, coords: list[float], clip: bool = False, as_float: bool = False
|
|
458
|
+
) -> list[int] | list[float]:
|
|
459
|
+
"""Convert coordinates from physical (real world) to indices (pixel)
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
coords: flat list of physical coordinates [x0, y0, x1, y1, ...]
|
|
463
|
+
clip: if True, clip values to image boundaries
|
|
464
|
+
as_float: if True, return float indices (i.e. without rounding)
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
Indices
|
|
468
|
+
|
|
469
|
+
Raises:
|
|
470
|
+
ValueError: if coords does not contain an even number of elements
|
|
471
|
+
"""
|
|
472
|
+
if len(coords) % 2 != 0:
|
|
473
|
+
raise ValueError(
|
|
474
|
+
"coords must contain an even number of elements (x, y pairs)."
|
|
475
|
+
)
|
|
476
|
+
indices = np.array(coords, float)
|
|
477
|
+
if indices.size > 0:
|
|
478
|
+
if self.is_uniform_coords:
|
|
479
|
+
# Use existing uniform conversion
|
|
480
|
+
indices[::2] = (indices[::2] - self.x0) / self.dx
|
|
481
|
+
indices[1::2] = (indices[1::2] - self.y0) / self.dy
|
|
482
|
+
else:
|
|
483
|
+
# Use interpolation for non-uniform coordinates
|
|
484
|
+
x_indices = np.arange(len(self.xcoords))
|
|
485
|
+
y_indices = np.arange(len(self.ycoords))
|
|
486
|
+
indices[::2] = np.interp(indices[::2], self.xcoords, x_indices)
|
|
487
|
+
indices[1::2] = np.interp(indices[1::2], self.ycoords, y_indices)
|
|
488
|
+
|
|
489
|
+
if clip:
|
|
490
|
+
indices[::2] = np.clip(indices[::2], 0, self.data.shape[1] - 1)
|
|
491
|
+
indices[1::2] = np.clip(indices[1::2], 0, self.data.shape[0] - 1)
|
|
492
|
+
if as_float:
|
|
493
|
+
return indices.tolist()
|
|
494
|
+
return np.floor(indices + 0.5).astype(int).tolist()
|
|
495
|
+
|
|
496
|
+
def indices_to_physical(self, indices: list[float]) -> list[float]:
|
|
497
|
+
"""Convert coordinates from indices to physical (real world)
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
indices: flat list of indices [x0, y0, x1, y1, ...]
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
Coordinates
|
|
504
|
+
|
|
505
|
+
Raises:
|
|
506
|
+
ValueError: if indices does not contain an even number of elements
|
|
507
|
+
"""
|
|
508
|
+
if len(indices) % 2 != 0:
|
|
509
|
+
raise ValueError(
|
|
510
|
+
"indices must contain an even number of elements (x, y pairs)."
|
|
511
|
+
)
|
|
512
|
+
coords = np.array(indices, float)
|
|
513
|
+
if coords.size > 0:
|
|
514
|
+
if self.is_uniform_coords:
|
|
515
|
+
# Use existing uniform conversion
|
|
516
|
+
coords[::2] = coords[::2] * self.dx + self.x0
|
|
517
|
+
coords[1::2] = coords[1::2] * self.dy + self.y0
|
|
518
|
+
else:
|
|
519
|
+
# Use interpolation for non-uniform coordinates
|
|
520
|
+
x_indices = np.arange(len(self.xcoords))
|
|
521
|
+
y_indices = np.arange(len(self.ycoords))
|
|
522
|
+
coords[::2] = np.interp(coords[::2], x_indices, self.xcoords)
|
|
523
|
+
coords[1::2] = np.interp(coords[1::2], y_indices, self.ycoords)
|
|
524
|
+
return coords.tolist()
|