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,164 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""Common functions for file name handling."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import string
|
|
9
|
+
import sys
|
|
10
|
+
import unicodedata
|
|
11
|
+
from typing import Any, Iterable
|
|
12
|
+
|
|
13
|
+
from sigima.objects.image import ImageObj
|
|
14
|
+
from sigima.objects.signal import SignalObj
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CustomFormatter(string.Formatter):
|
|
18
|
+
"""Custom string formatter to handle uppercase and lowercase strings."""
|
|
19
|
+
|
|
20
|
+
def format_field(self, value, format_spec):
|
|
21
|
+
"""Format the given `value` according to the specified `format_spec`.
|
|
22
|
+
|
|
23
|
+
If the value is a string and the format_spec ends with 'upper' or 'lower',
|
|
24
|
+
convert the value to uppercase or lowercase, respectively, and remove the
|
|
25
|
+
suffix from `format_spec` before formatting.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
value: Value to format.
|
|
29
|
+
format_spec: Format specification, may end with 'upper' or 'lower'.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The formatted value.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: If `format_spec` is invalid.
|
|
36
|
+
"""
|
|
37
|
+
# Ignore dict objects silently (metadata should only be accessed via keys)
|
|
38
|
+
if isinstance(value, dict):
|
|
39
|
+
return ""
|
|
40
|
+
if isinstance(value, str):
|
|
41
|
+
if format_spec.endswith("upper"):
|
|
42
|
+
value = value.upper()
|
|
43
|
+
format_spec = format_spec[:-5]
|
|
44
|
+
elif format_spec.endswith("lower"):
|
|
45
|
+
value = value.lower()
|
|
46
|
+
format_spec = format_spec[:-5]
|
|
47
|
+
return super().format_field(value, format_spec)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def format_basenames(
|
|
51
|
+
objects: Iterable[SignalObj | ImageObj],
|
|
52
|
+
fmt: str,
|
|
53
|
+
replacement: str = "_",
|
|
54
|
+
) -> list[str]:
|
|
55
|
+
"""Generate sanitized filenames for SignalObj or ImageObj instances.
|
|
56
|
+
|
|
57
|
+
Format each object's name using the provided Python format string, then sanitize
|
|
58
|
+
the result for safe use as a filename. The format string may reference any of:
|
|
59
|
+
- {title}: object title
|
|
60
|
+
- {index}: 1-based index
|
|
61
|
+
- {count}: total number of objects
|
|
62
|
+
- {xlabel}, {xunit}, {ylabel}, {yunit}: axis labels/units (if present)
|
|
63
|
+
- {metadata[key]}: specific metadata value
|
|
64
|
+
(direct {metadata} use is silently ignored)
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
objects: Objects to name.
|
|
68
|
+
fmt: Python format string for naming.
|
|
69
|
+
replacement: Replacement for invalid filename characters.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Sanitized filenames for each object.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
KeyError: If the format string references an unknown placeholder.
|
|
76
|
+
"""
|
|
77
|
+
result: list[str] = []
|
|
78
|
+
formatter = CustomFormatter()
|
|
79
|
+
for i, obj in enumerate(objects):
|
|
80
|
+
# Note: We provide metadata dict only for {metadata[key]} access,
|
|
81
|
+
# not for direct {metadata} use (which would create overly long filenames)
|
|
82
|
+
metadata = getattr(obj, "metadata", {})
|
|
83
|
+
context: dict[str, Any] = {
|
|
84
|
+
"title": getattr(obj, "title", ""),
|
|
85
|
+
"index": i + 1,
|
|
86
|
+
"count": len(list(objects)),
|
|
87
|
+
# Attributes may not exist on all objects.
|
|
88
|
+
"xlabel": getattr(obj, "xlabel", ""),
|
|
89
|
+
"xunit": getattr(obj, "xunit", ""),
|
|
90
|
+
"ylabel": getattr(obj, "ylabel", ""),
|
|
91
|
+
"yunit": getattr(obj, "yunit", ""),
|
|
92
|
+
"metadata": metadata,
|
|
93
|
+
}
|
|
94
|
+
try:
|
|
95
|
+
formatted = formatter.format(fmt, **context)
|
|
96
|
+
except KeyError as exc:
|
|
97
|
+
missing = str(exc.args[0]) if exc.args else str(exc)
|
|
98
|
+
raise KeyError(f"Unknown format key in fmt: {missing!r}") from exc
|
|
99
|
+
except ValueError as exc:
|
|
100
|
+
# Re-raise with more context about which object failed
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"Invalid format string '{fmt}' for object '{context['title']}': {exc}"
|
|
103
|
+
) from exc
|
|
104
|
+
# Sanitize final result to ensure it's a safe basename.
|
|
105
|
+
result.append(sanitize_basename(formatted, replacement=replacement))
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def sanitize_basename(basename: str, replacement: str = "_") -> str:
|
|
110
|
+
"""Sanitize a string to create a valid basename for the current operating system.
|
|
111
|
+
|
|
112
|
+
This function removes or replaces characters that are invalid in basenames,
|
|
113
|
+
depending on the underlying OS (Windows, macOS, Linux). It also strips trailing dots
|
|
114
|
+
and spaces on Windows and normalizes unicode characters.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
basename: Input string.
|
|
118
|
+
replacement: Replacement string for invalid characters (default: "_").
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
A sanitized string that can safely be used as a basename.
|
|
122
|
+
"""
|
|
123
|
+
# Normalize unicode characters (NFKD form for decomposing accents and the like).
|
|
124
|
+
basename = unicodedata.normalize("NFKD", basename)
|
|
125
|
+
basename = basename.encode("ascii", "ignore").decode("ascii")
|
|
126
|
+
|
|
127
|
+
# Characters not allowed in filenames (platform-dependent).
|
|
128
|
+
if sys.platform.startswith("win"):
|
|
129
|
+
# Reserved characters on Windows.
|
|
130
|
+
invalid_chars = r'[<>:"/\\|?*\x00-\x1F]'
|
|
131
|
+
reserved_names = {
|
|
132
|
+
"CON",
|
|
133
|
+
"PRN",
|
|
134
|
+
"AUX",
|
|
135
|
+
"NUL",
|
|
136
|
+
*(f"COM{i}" for i in range(1, 10)),
|
|
137
|
+
*(f"LPT{i}" for i in range(1, 10)),
|
|
138
|
+
}
|
|
139
|
+
else:
|
|
140
|
+
# Only '/' is disallowed on Unix-based systems.
|
|
141
|
+
invalid_chars = r"/"
|
|
142
|
+
reserved_names = set()
|
|
143
|
+
|
|
144
|
+
# Replace invalid characters.
|
|
145
|
+
sanitized = re.sub(invalid_chars, replacement, basename)
|
|
146
|
+
|
|
147
|
+
# Strip leading/trailing whitespace.
|
|
148
|
+
sanitized = sanitized.strip()
|
|
149
|
+
# On Windows, also strip trailing dots and spaces.
|
|
150
|
+
if sys.platform.startswith("win"):
|
|
151
|
+
sanitized = sanitized.rstrip(" .")
|
|
152
|
+
|
|
153
|
+
# Truncate to a reasonable length to avoid OS path issues.
|
|
154
|
+
sanitized = sanitized[:255]
|
|
155
|
+
|
|
156
|
+
# Avoid reserved basenames.
|
|
157
|
+
if sanitized.upper() in reserved_names:
|
|
158
|
+
sanitized += "_"
|
|
159
|
+
|
|
160
|
+
# If result is empty, fallback to a default name.
|
|
161
|
+
if not sanitized:
|
|
162
|
+
sanitized = "unnamed"
|
|
163
|
+
|
|
164
|
+
return sanitized
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
I/O conversion functions
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Sequence
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import skimage
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def dtypes_to_sorted_short_codes(
|
|
18
|
+
dtypes: Sequence[Any], kind_filter: str | None = None
|
|
19
|
+
) -> list[str]:
|
|
20
|
+
"""Return sorted short dtype codes for numeric dtypes.
|
|
21
|
+
|
|
22
|
+
Convert each input to a numpy dtype and ignore non-numeric types.
|
|
23
|
+
Order:
|
|
24
|
+
- Integer types first, unsigned (and boolean) before signed,
|
|
25
|
+
sorted by itemsize ascending.
|
|
26
|
+
- floats numeric types, sorted by itemsize ascending.
|
|
27
|
+
- complex numeric types, sorted by itemsize ascending.
|
|
28
|
+
|
|
29
|
+
Short codes use numpy kind letter plus itemsize in bytes, e.g. "u1", "i2",
|
|
30
|
+
"f8".
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
dtypes: Sequence of objects acceptable by numpy.dtype (dtype, str, etc.)
|
|
34
|
+
kind_filter: String of dtype kind letters to keep, e.g. "iu" for
|
|
35
|
+
unsigned/signed integers. If empty or None, keep all numeric types
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of unique short dtype codes in the requested order.
|
|
39
|
+
"""
|
|
40
|
+
dtypes = [np.dtype(d).str[1:] for d in dtypes]
|
|
41
|
+
ordered: list[np.dtype] = []
|
|
42
|
+
|
|
43
|
+
if kind_filter is None:
|
|
44
|
+
kind_filter = "iubfc" # all numeric types
|
|
45
|
+
assert kind_filter != "", "kind_filter cannot be empty string"
|
|
46
|
+
|
|
47
|
+
# Standard dtype codes in desired order
|
|
48
|
+
bool_codes = ("b1",)
|
|
49
|
+
int_codes = ("u1", "i1", "u2", "i2", "u4", "i4", "u8", "i8")
|
|
50
|
+
float_codes = ("f2", "f4", "f8")
|
|
51
|
+
complex_codes = ("c8", "c16")
|
|
52
|
+
|
|
53
|
+
ordered = [
|
|
54
|
+
code
|
|
55
|
+
for code in bool_codes + int_codes + float_codes + complex_codes
|
|
56
|
+
if code in dtypes and code[0] in kind_filter
|
|
57
|
+
]
|
|
58
|
+
return ordered
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _convert_bool_array(array: np.ndarray) -> np.ndarray:
|
|
62
|
+
"""Convert boolean array to uint8."""
|
|
63
|
+
return skimage.util.img_as_ubyte(array)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _convert_int_array(
|
|
67
|
+
array: np.ndarray, supported_data_types: tuple[np.dtype]
|
|
68
|
+
) -> np.ndarray:
|
|
69
|
+
"""Convert an integer array to a standard type.
|
|
70
|
+
|
|
71
|
+
Select the smallest supported integer dtype that can represent all values in the
|
|
72
|
+
array. If no suitable integer dtype is found, convert the array to a supported
|
|
73
|
+
float type.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
array: Input numpy array of integer type.
|
|
77
|
+
supported_data_types: Tuple of supported numpy dtypes for destination object.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Converted numpy array with the selected dtype.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ValueError: If no supported dtype can represent the data.
|
|
84
|
+
"""
|
|
85
|
+
ordered_codes = dtypes_to_sorted_short_codes(supported_data_types, kind_filter="iu")
|
|
86
|
+
|
|
87
|
+
amin = np.min(array) if array.size > 0 else 0
|
|
88
|
+
amax = np.max(array) if array.size > 0 else 0
|
|
89
|
+
for code in ordered_codes:
|
|
90
|
+
info = np.iinfo(code)
|
|
91
|
+
if amin >= info.min and amax <= info.max:
|
|
92
|
+
new_type = np.dtype(code).newbyteorder("=")
|
|
93
|
+
break
|
|
94
|
+
else:
|
|
95
|
+
new_type = _convert_float_array(array, supported_data_types).dtype
|
|
96
|
+
|
|
97
|
+
return array.astype(new_type, copy=False)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _convert_float_array(
|
|
101
|
+
array: np.ndarray, supported_data_types: tuple[np.dtype]
|
|
102
|
+
) -> np.ndarray:
|
|
103
|
+
"""Convert float/complex array to smallest allowed type at least large as current.
|
|
104
|
+
|
|
105
|
+
Choose the smallest supported dtype of the same kind ("f" for floats,
|
|
106
|
+
"c" for complex) whose itemsize is greater than or equal to the array's
|
|
107
|
+
itemsize. If no such type exists, fall back to the largest supported
|
|
108
|
+
dtype for that kind.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
array: Array to convert.
|
|
112
|
+
supported_data_types: Sequence of allowed dtypes for the destination
|
|
113
|
+
object type.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Converted array with the selected dtype. If no supported dtype of the
|
|
117
|
+
same kind exists, return the original array.
|
|
118
|
+
"""
|
|
119
|
+
kind = array.dtype.kind
|
|
120
|
+
if kind in ["i", "u", "b"]:
|
|
121
|
+
kind = "f" # convert integers to floats
|
|
122
|
+
|
|
123
|
+
itemsize = array.dtype.itemsize
|
|
124
|
+
|
|
125
|
+
ordered_codes = dtypes_to_sorted_short_codes(supported_data_types, kind_filter=kind)
|
|
126
|
+
|
|
127
|
+
# Filter out any codes that don't match the requested kind (defensive).
|
|
128
|
+
valid_codes: list[str] = []
|
|
129
|
+
for code in ordered_codes:
|
|
130
|
+
try:
|
|
131
|
+
dt = np.dtype(code)
|
|
132
|
+
except TypeError:
|
|
133
|
+
continue
|
|
134
|
+
if dt.kind == kind:
|
|
135
|
+
valid_codes.append(code)
|
|
136
|
+
|
|
137
|
+
if not valid_codes:
|
|
138
|
+
# No supported dtype for this kind, return original array.
|
|
139
|
+
raise ValueError("Unsupported data type")
|
|
140
|
+
|
|
141
|
+
# Find smallest supported type with itemsize >= current itemsize.
|
|
142
|
+
selected_code: str | None = None
|
|
143
|
+
for code in valid_codes:
|
|
144
|
+
dt = np.dtype(code)
|
|
145
|
+
if dt.itemsize >= itemsize:
|
|
146
|
+
selected_code = code
|
|
147
|
+
break
|
|
148
|
+
else:
|
|
149
|
+
# Fallback to the largest supported type for this kind.
|
|
150
|
+
selected_code = valid_codes[-1]
|
|
151
|
+
|
|
152
|
+
new_type = np.dtype(selected_code).newbyteorder("=")
|
|
153
|
+
return array.astype(new_type, copy=False)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def convert_array_to_valid_dtype(
|
|
157
|
+
array: np.ndarray, valid_dtypes: tuple[np.dtype, ...]
|
|
158
|
+
) -> np.ndarray:
|
|
159
|
+
"""Convert array to the most appropriate valid dtype.
|
|
160
|
+
|
|
161
|
+
Converts arrays to one of the valid dtypes, choosing the most appropriate type
|
|
162
|
+
based on the input array's characteristics.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
array: array to convert
|
|
166
|
+
valid_dtypes: tuple of valid dtypes
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Converted array with the most appropriate valid dtype.
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
TypeError: if input is not a numpy ndarray
|
|
173
|
+
ValueError: if array dtype cannot be converted to any valid type
|
|
174
|
+
"""
|
|
175
|
+
if not isinstance(array, np.ndarray):
|
|
176
|
+
raise TypeError("Input must be a numpy ndarray.")
|
|
177
|
+
|
|
178
|
+
if array.dtype in valid_dtypes:
|
|
179
|
+
return array
|
|
180
|
+
|
|
181
|
+
kind: str = array.dtype.kind
|
|
182
|
+
if kind in ["f", "c"]:
|
|
183
|
+
return _convert_float_array(array, valid_dtypes)
|
|
184
|
+
if kind == "b":
|
|
185
|
+
return _convert_bool_array(array)
|
|
186
|
+
if kind in ["i", "u"]:
|
|
187
|
+
return _convert_int_array(array, valid_dtypes)
|
|
188
|
+
|
|
189
|
+
raise ValueError("Unsupported data type")
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""Sigima I/O module for handling object metadata and ROIs."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
8
|
+
|
|
9
|
+
from guidata.io import JSONHandler, JSONReader, JSONWriter
|
|
10
|
+
|
|
11
|
+
from sigima.objects import ImageROI, SignalROI
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from sigima.params import ROIGridParam
|
|
15
|
+
|
|
16
|
+
FORMAT_TAG = "sigima"
|
|
17
|
+
FORMAT_VERSION = "1.0"
|
|
18
|
+
ROI_TYPE_FIELD = "roi_type"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _check_tag(data: dict, expected_format: str) -> None:
|
|
22
|
+
"""Validate the presence and type of sigima tag.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
data: The data dictionary to check.
|
|
26
|
+
expected_format: The expected format string for the tag.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ValueError: If the tag is missing or does not match the expected format.
|
|
30
|
+
"""
|
|
31
|
+
tag: dict = data.get(FORMAT_TAG, {})
|
|
32
|
+
if tag.get("format") != expected_format:
|
|
33
|
+
raise ValueError(f"Unexpected or missing format: {tag}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def write_dict(filepath: str, data: dict) -> None:
|
|
37
|
+
"""Write a dictionary to a file in JSON format.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
filepath: The file path to write the data to.
|
|
41
|
+
data: The dictionary to serialize.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If the data is not a dictionary.
|
|
45
|
+
"""
|
|
46
|
+
if not isinstance(data, dict):
|
|
47
|
+
raise ValueError(f"Expected a dictionary, got {type(data)}")
|
|
48
|
+
handler = JSONHandler(filepath)
|
|
49
|
+
handler.set_json_dict(data)
|
|
50
|
+
handler.save()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def read_dict(filepath: str) -> dict:
|
|
54
|
+
"""Read a dictionary from a file and return it.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
filepath: The file path to read the data from.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The dictionary read from the file.
|
|
61
|
+
"""
|
|
62
|
+
handler = JSONHandler(filepath)
|
|
63
|
+
handler.load()
|
|
64
|
+
data = handler.get_json_dict()
|
|
65
|
+
return data
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def write_roi(filepath: str, roi: SignalROI | ImageROI) -> None:
|
|
69
|
+
"""Write a signal or image ROI to a file in JSON format.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
filepath: The file path to write the ROI data to.
|
|
73
|
+
roi: The signal or image ROI object to serialize.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: If the ROI object is not of type SignalROI or ImageROI.
|
|
77
|
+
"""
|
|
78
|
+
if isinstance(roi, SignalROI):
|
|
79
|
+
roi_type: Literal["signal", "image"] = "signal"
|
|
80
|
+
elif isinstance(roi, ImageROI):
|
|
81
|
+
roi_type = "image"
|
|
82
|
+
else:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"Unsupported ROI type: {type(roi)}. Expected SignalROI or ImageROI."
|
|
85
|
+
)
|
|
86
|
+
roi_dict = roi.to_dict()
|
|
87
|
+
roi_dict[ROI_TYPE_FIELD] = roi_type
|
|
88
|
+
data = {
|
|
89
|
+
FORMAT_TAG: {"format": "roi", "version": FORMAT_VERSION},
|
|
90
|
+
"roi": roi_dict,
|
|
91
|
+
}
|
|
92
|
+
write_dict(filepath, data)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def read_roi(filepath: str) -> SignalROI | ImageROI:
|
|
96
|
+
"""Read ROI data from a file and return the corresponding ROI object.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
filepath: The file path to read the ROI data from.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
The corresponding ROI object (SignalROI or ImageROI).
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
ValueError: If the file does not contain the expected format.
|
|
106
|
+
"""
|
|
107
|
+
json_dict = read_dict(filepath)
|
|
108
|
+
_check_tag(json_dict, expected_format="roi")
|
|
109
|
+
roi_dict = json_dict["roi"]
|
|
110
|
+
assert isinstance(roi_dict, dict), "ROI data must be a dictionary"
|
|
111
|
+
roi_type = roi_dict.pop(ROI_TYPE_FIELD, None)
|
|
112
|
+
if roi_type == "signal":
|
|
113
|
+
return SignalROI.from_dict(roi_dict)
|
|
114
|
+
if roi_type == "image":
|
|
115
|
+
return ImageROI.from_dict(roi_dict)
|
|
116
|
+
raise ValueError(f"Unsupported or missing ROI type: {roi_type}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def write_roi_grid(filepath: str, param: ROIGridParam) -> None:
|
|
120
|
+
"""Write ROI grid parameters to a file in JSON format.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
filepath: The file path to write the ROI grid parameters to.
|
|
124
|
+
param: The ROI grid parameters to serialize.
|
|
125
|
+
"""
|
|
126
|
+
writer = JSONWriter(filepath)
|
|
127
|
+
param.serialize(writer)
|
|
128
|
+
print(writer.jsondata)
|
|
129
|
+
writer.save()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def read_roi_grid(filepath: str) -> ROIGridParam:
|
|
133
|
+
"""Read ROI grid parameters from a file in JSON format.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
filepath: The file path to read the ROI grid parameters from.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The ROI grid parameters read from the file.
|
|
140
|
+
"""
|
|
141
|
+
from sigima.params import ROIGridParam # pylint: disable=import-outside-toplevel
|
|
142
|
+
|
|
143
|
+
handler = JSONReader(filepath)
|
|
144
|
+
handler.load()
|
|
145
|
+
param = ROIGridParam()
|
|
146
|
+
param.deserialize(handler)
|
|
147
|
+
return param
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def write_metadata(filepath: str, metadata: dict[str, Any]) -> None:
|
|
151
|
+
"""Write metadata to a file in JSON format.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
filepath: The file path to write the metadata to.
|
|
155
|
+
metadata: The metadata dictionary to serialize.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
ValueError: If the object does not have a metadata attribute.
|
|
159
|
+
"""
|
|
160
|
+
data = {
|
|
161
|
+
FORMAT_TAG: {"format": "metadata", "version": FORMAT_VERSION},
|
|
162
|
+
"metadata": metadata.copy(),
|
|
163
|
+
}
|
|
164
|
+
write_dict(filepath, data)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def read_metadata(filepath: str) -> dict[str, Any]:
|
|
168
|
+
"""Read metadata from a file and return the metadata dictionary.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
filepath: The file path to read the metadata from.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
The metadata dictionary.
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
ValueError: If the file does not contain the expected format.
|
|
178
|
+
"""
|
|
179
|
+
json_dict = read_dict(filepath)
|
|
180
|
+
_check_tag(json_dict, expected_format="metadata")
|
|
181
|
+
return json_dict["metadata"]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""I/O utility functions."""
|
|
4
|
+
|
|
5
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y...
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from itertools import islice
|
|
11
|
+
|
|
12
|
+
from sigima.io.enums import FileEncoding
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def count_lines(filename: str | os.PathLike[str]) -> int:
|
|
16
|
+
"""Count the number of lines in a file.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
filename: File name or path.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
The number of lines in the file.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
IOError: If the file cannot be read.
|
|
26
|
+
"""
|
|
27
|
+
for encoding in FileEncoding:
|
|
28
|
+
try:
|
|
29
|
+
with open(filename, "r", encoding=encoding) as file:
|
|
30
|
+
line_count = sum(1 for _ in file)
|
|
31
|
+
return line_count
|
|
32
|
+
except UnicodeDecodeError:
|
|
33
|
+
# Try next encoding.
|
|
34
|
+
pass
|
|
35
|
+
raise IOError(f"Cannot read file {filename}.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def read_first_n_lines(filename: str | os.PathLike[str], n: int = 100000) -> str:
|
|
39
|
+
"""Read the first `n` lines of a file.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
filename: File name or path.
|
|
43
|
+
n: Number of lines to read.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The first `n` lines of the file.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
IOError: If the file cannot be read.
|
|
50
|
+
"""
|
|
51
|
+
for encoding in FileEncoding:
|
|
52
|
+
try:
|
|
53
|
+
with open(filename, "r", encoding=encoding) as file:
|
|
54
|
+
return "".join(islice(file, n))
|
|
55
|
+
except UnicodeDecodeError:
|
|
56
|
+
# Try next encoding.
|
|
57
|
+
pass
|
|
58
|
+
raise IOError(f"Cannot read file {filename}.")
|