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,502 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Geometry results
|
|
5
|
+
================
|
|
6
|
+
|
|
7
|
+
Geometry results are compute-friendly result containers for geometric outputs.
|
|
8
|
+
|
|
9
|
+
This module defines the `GeometryResult` class and related utilities:
|
|
10
|
+
|
|
11
|
+
- `GeometryResult`: geometric outputs (points, segments, circles, ...)
|
|
12
|
+
- `KindShape`: enumeration of geometric shape types
|
|
13
|
+
- Utility functions for geometry operations (concatenation, filtering, etc.)
|
|
14
|
+
|
|
15
|
+
Each result object is a simple data container with no behavior or methods:
|
|
16
|
+
|
|
17
|
+
- It contains the result of a 1-to-0 processing function
|
|
18
|
+
(e.g. `sigima.proc.image.contour_shape()`), i.e. a computation function that takes a
|
|
19
|
+
signal or image object (`SignalObj` or `ImageObj`) as input and produces a geometric
|
|
20
|
+
output (`GeometryResult`).
|
|
21
|
+
|
|
22
|
+
- The result may consist of multiple rows, each corresponding to a different ROI.
|
|
23
|
+
|
|
24
|
+
.. note::
|
|
25
|
+
|
|
26
|
+
No UI/HTML, no DataLab-specific metadata here. Adapters/formatters live in
|
|
27
|
+
DataLab. These classes are JSON-friendly via `to_dict()`/`from_dict()`.
|
|
28
|
+
|
|
29
|
+
Conventions
|
|
30
|
+
-----------
|
|
31
|
+
|
|
32
|
+
Conventions regarding ROI and geometry are as follows:
|
|
33
|
+
|
|
34
|
+
- ROI indexing:
|
|
35
|
+
|
|
36
|
+
- `NO_ROI = -1` sentinel is used for "full image / no ROI" rows.
|
|
37
|
+
- Per-ROI rows use non-negative indices (0-based).
|
|
38
|
+
|
|
39
|
+
- Geometry coordinates (physical units):
|
|
40
|
+
|
|
41
|
+
- `"point"` / `"marker"`: `[x, y]`
|
|
42
|
+
- `"segment"`: `[x0, y0, x1, y1]`
|
|
43
|
+
- `"rectangle"`: `[x0, y0, width, height]`
|
|
44
|
+
- `"circle"`: `[x0, y0, radius]`
|
|
45
|
+
- `"ellipse"`: `[x0, y0, a, b, theta]` # theta in radians
|
|
46
|
+
- `"polygon"`: `[x0, y0, x1, y1, ..., xn, yn]` (rows may be NaN-padded)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from __future__ import annotations
|
|
50
|
+
|
|
51
|
+
import dataclasses
|
|
52
|
+
import enum
|
|
53
|
+
from typing import Iterable
|
|
54
|
+
|
|
55
|
+
import numpy as np
|
|
56
|
+
import pandas as pd
|
|
57
|
+
|
|
58
|
+
from sigima.objects.scalar.common import (
|
|
59
|
+
NO_ROI,
|
|
60
|
+
DataFrameManager,
|
|
61
|
+
DisplayPreferencesManager,
|
|
62
|
+
ResultHtmlGenerator,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class KindShape(str, enum.Enum):
|
|
67
|
+
"""Geometric shape types."""
|
|
68
|
+
|
|
69
|
+
POINT = "point"
|
|
70
|
+
SEGMENT = "segment"
|
|
71
|
+
CIRCLE = "circle"
|
|
72
|
+
ELLIPSE = "ellipse"
|
|
73
|
+
RECTANGLE = "rectangle"
|
|
74
|
+
POLYGON = "polygon"
|
|
75
|
+
MARKER = "marker"
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def values(cls) -> list[str]:
|
|
79
|
+
"""Return all shape type values."""
|
|
80
|
+
return [e.value for e in cls]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclasses.dataclass(frozen=True)
|
|
84
|
+
class GeometryResult:
|
|
85
|
+
"""Geometric outputs, optionally per-ROI.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
title: Human-readable title for this geometric output set.
|
|
89
|
+
kind: Shape kind (`KindShape` member or its string value).
|
|
90
|
+
coords: 2-D array (N, K) with coordinates per row. K depends on `kind`
|
|
91
|
+
and may be NaN-padded (e.g., for polygons).
|
|
92
|
+
roi_indices: Optional 1-D array (N,) mapping rows to ROI indices.
|
|
93
|
+
Use NO_ROI (-1) for the "full signal/image / no ROI" row.
|
|
94
|
+
attrs: Optional algorithmic context (e.g. thresholds, method variant).
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ValueError: If dimensions are inconsistent or fields are invalid.
|
|
98
|
+
|
|
99
|
+
.. important::
|
|
100
|
+
**Coordinate System**: GeometryResult coordinates are stored in **physical
|
|
101
|
+
units** (e.g., mm, µm), not pixel coordinates. The conversion from pixel to
|
|
102
|
+
physical coordinates is performed automatically when creating GeometryResult
|
|
103
|
+
objects from image measurements using
|
|
104
|
+
:func:`~sigima.proc.image.base.compute_geometry_from_obj`.
|
|
105
|
+
|
|
106
|
+
This ensures that geometric measurements are:
|
|
107
|
+
|
|
108
|
+
* **Scale-independent**: Results remain valid when images are resized
|
|
109
|
+
* **Physically meaningful**: Measurements have real-world significance
|
|
110
|
+
* **Consistent**: Same geometric features yield same results across different
|
|
111
|
+
images
|
|
112
|
+
|
|
113
|
+
.. note::
|
|
114
|
+
|
|
115
|
+
Coordinate conventions are as follows:
|
|
116
|
+
|
|
117
|
+
- `KindShape.POINT`: `[x, y]`
|
|
118
|
+
- `KindShape.SEGMENT`: `[x0, y0, x1, y1]`
|
|
119
|
+
- `KindShape.RECTANGLE`: `[x0, y0, width, height]`
|
|
120
|
+
- `KindShape.CIRCLE`: `[x0, y0, radius]`
|
|
121
|
+
- `KindShape.ELLIPSE`: `[x0, y0, a, b, theta]` # theta in radians
|
|
122
|
+
- `KindShape.POLYGON`: `[x0, y0, x1, y1, ..., xn, yn]` (rows may be NaN-padded)
|
|
123
|
+
|
|
124
|
+
All coordinate values and dimensions (width, height, radius, semi-axes) are
|
|
125
|
+
expressed in the image's physical units as defined by the image calibration.
|
|
126
|
+
|
|
127
|
+
See Also:
|
|
128
|
+
:func:`~sigima.proc.image.base.compute_geometry_from_obj`: Function that
|
|
129
|
+
creates GeometryResult objects with automatic coordinate conversion from
|
|
130
|
+
pixel to physical units.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
title: str
|
|
134
|
+
kind: KindShape
|
|
135
|
+
coords: np.ndarray
|
|
136
|
+
roi_indices: np.ndarray | None = None
|
|
137
|
+
attrs: dict[str, object] = dataclasses.field(default_factory=dict)
|
|
138
|
+
|
|
139
|
+
def __post_init__(self) -> None:
|
|
140
|
+
"""Validate fields after initialization."""
|
|
141
|
+
# --- kind validation/coercion (smooth migration) ---
|
|
142
|
+
k = object.__getattribute__(self, "kind")
|
|
143
|
+
if isinstance(k, str):
|
|
144
|
+
try:
|
|
145
|
+
k = KindShape(k) # coerce "ellipse" -> KindShape.ELLIPSE
|
|
146
|
+
except ValueError as exc:
|
|
147
|
+
raise ValueError(f"Unsupported geometry kind: {k!r}") from exc
|
|
148
|
+
object.__setattr__(self, "kind", k)
|
|
149
|
+
elif not isinstance(k, KindShape):
|
|
150
|
+
raise ValueError("kind must be a KindShape or its string value")
|
|
151
|
+
if not isinstance(self.title, str) or not self.title:
|
|
152
|
+
raise ValueError("title must be a non-empty string")
|
|
153
|
+
if not isinstance(self.coords, np.ndarray) or self.coords.ndim != 2:
|
|
154
|
+
raise ValueError("coords must be a 2-D numpy array")
|
|
155
|
+
if k == KindShape.POINT and self.coords.shape[1] != 2:
|
|
156
|
+
raise ValueError("coords for 'point' must be (N,2)")
|
|
157
|
+
if k == KindShape.SEGMENT and self.coords.shape[1] != 4:
|
|
158
|
+
raise ValueError("coords for 'segment' must be (N,4)")
|
|
159
|
+
if k == KindShape.CIRCLE and self.coords.shape[1] != 3:
|
|
160
|
+
raise ValueError("coords for 'circle' must be (N,3)")
|
|
161
|
+
if k == KindShape.ELLIPSE and self.coords.shape[1] != 5:
|
|
162
|
+
raise ValueError("coords for 'ellipse' must be (N,5)")
|
|
163
|
+
if k == KindShape.RECTANGLE and self.coords.shape[1] != 4:
|
|
164
|
+
raise ValueError("coords for 'rectangle' must be (N,4)")
|
|
165
|
+
if k == KindShape.POLYGON and self.coords.shape[1] % 2 != 0:
|
|
166
|
+
raise ValueError("coords for 'polygon' must be (N,2M) for M vertices")
|
|
167
|
+
if self.roi_indices is not None:
|
|
168
|
+
if (
|
|
169
|
+
not isinstance(self.roi_indices, np.ndarray)
|
|
170
|
+
or self.roi_indices.ndim != 1
|
|
171
|
+
):
|
|
172
|
+
raise ValueError("roi_indices must be a 1-D numpy array if provided")
|
|
173
|
+
if len(self.roi_indices) != len(self.coords):
|
|
174
|
+
raise ValueError("roi_indices length must match number of coord rows")
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def name(self) -> str:
|
|
178
|
+
"""Get the unique identifier name for this geometry result.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
The string value of the kind attribute, which serves as a unique
|
|
182
|
+
name identifier for this geometry result type.
|
|
183
|
+
"""
|
|
184
|
+
return self.kind.value
|
|
185
|
+
|
|
186
|
+
# -------- Factory methods --------
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def from_coords(
|
|
190
|
+
cls,
|
|
191
|
+
title: str,
|
|
192
|
+
kind: KindShape,
|
|
193
|
+
coords: np.ndarray,
|
|
194
|
+
roi_indices: np.ndarray | None = None,
|
|
195
|
+
*,
|
|
196
|
+
attrs: dict[str, object] | None = None,
|
|
197
|
+
) -> GeometryResult:
|
|
198
|
+
"""Create a GeometryResult from raw data.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
title: Human-readable title for this geometric output.
|
|
202
|
+
kind: Shape kind (e.g. "point", "segment").
|
|
203
|
+
coords: 2-D array (N, K) with coordinates per row.
|
|
204
|
+
roi_indices: Optional 1-D array (N,) mapping rows to ROI indices.
|
|
205
|
+
attrs: Optional algorithmic context (e.g. thresholds, method variant).
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
A GeometryResult instance.
|
|
209
|
+
"""
|
|
210
|
+
return cls(
|
|
211
|
+
title,
|
|
212
|
+
kind,
|
|
213
|
+
np.asarray(coords, float),
|
|
214
|
+
None if roi_indices is None else np.asarray(roi_indices, int),
|
|
215
|
+
{} if attrs is None else dict(attrs),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# -------- JSON-friendly (de)serialization (no DataLab metadata coupling) -----
|
|
219
|
+
|
|
220
|
+
def to_dict(self) -> dict:
|
|
221
|
+
"""Convert the GeometryResult to a dictionary."""
|
|
222
|
+
return {
|
|
223
|
+
"schema": 1,
|
|
224
|
+
"title": self.title,
|
|
225
|
+
"kind": self.kind.value,
|
|
226
|
+
"coords": self.coords.tolist(),
|
|
227
|
+
"roi_indices": None
|
|
228
|
+
if self.roi_indices is None
|
|
229
|
+
else self.roi_indices.tolist(),
|
|
230
|
+
"attrs": dict(self.attrs) if self.attrs else {},
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def from_dict(d: dict) -> GeometryResult:
|
|
235
|
+
"""Convert a dictionary to a GeometryResult."""
|
|
236
|
+
return GeometryResult(
|
|
237
|
+
title=d["title"],
|
|
238
|
+
kind=KindShape(d["kind"]),
|
|
239
|
+
coords=np.asarray(d["coords"], dtype=float),
|
|
240
|
+
roi_indices=None
|
|
241
|
+
if d.get("roi_indices") is None
|
|
242
|
+
else np.asarray(d["roi_indices"], dtype=int),
|
|
243
|
+
attrs=dict(d.get("attrs", {})),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# -------- Pandas DataFrame interop --------
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def headers(self) -> list[str]:
|
|
250
|
+
"""Get column headers for the coordinates.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
List of column headers
|
|
254
|
+
"""
|
|
255
|
+
# Create headers based on the shape type
|
|
256
|
+
kind = self.kind.value
|
|
257
|
+
|
|
258
|
+
# Define headers based on shape type
|
|
259
|
+
headers_map = {
|
|
260
|
+
"point": ["x", "y"],
|
|
261
|
+
"marker": ["x", "y"],
|
|
262
|
+
"segment": ["x0", "y0", "x1", "y1"],
|
|
263
|
+
"rectangle": ["x", "y", "width", "height"],
|
|
264
|
+
"circle": ["x", "y", "r"],
|
|
265
|
+
"ellipse": ["x", "y", "a", "b", "θ"],
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if kind in headers_map:
|
|
269
|
+
return headers_map[kind]
|
|
270
|
+
|
|
271
|
+
num_coords = self.coords.shape[1]
|
|
272
|
+
|
|
273
|
+
if kind == "polygon":
|
|
274
|
+
headers = []
|
|
275
|
+
for i in range(0, num_coords, 2):
|
|
276
|
+
headers.extend([f"x{i // 2}", f"y{i // 2}"])
|
|
277
|
+
return headers[:num_coords]
|
|
278
|
+
|
|
279
|
+
# Generic headers for unknown shapes
|
|
280
|
+
return [f"coord_{i}" for i in range(num_coords)]
|
|
281
|
+
|
|
282
|
+
def to_dataframe(self, visible_only: bool = False):
|
|
283
|
+
"""Convert the result to a pandas DataFrame.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
visible_only: If True, include only visible headers based on display
|
|
287
|
+
preferences. Default is False.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
DataFrame with an optional 'roi_index' column.
|
|
291
|
+
If visible_only is True, only columns with visible headers are included.
|
|
292
|
+
"""
|
|
293
|
+
df = pd.DataFrame(self.coords, columns=self.headers)
|
|
294
|
+
visible_headers = self.get_visible_headers()
|
|
295
|
+
|
|
296
|
+
# For segments, add a length column
|
|
297
|
+
if self.kind == KindShape.SEGMENT:
|
|
298
|
+
lengths = self.segments_lengths()
|
|
299
|
+
# Name the length column "Δx" if y0 == y1 for all rows,
|
|
300
|
+
# "Δy" if x0 == x1 for all rows, else "length"
|
|
301
|
+
if np.allclose(self.coords[:, 1], self.coords[:, 3]):
|
|
302
|
+
length_name = "Δx"
|
|
303
|
+
elif np.allclose(self.coords[:, 0], self.coords[:, 2]):
|
|
304
|
+
length_name = "Δy"
|
|
305
|
+
else:
|
|
306
|
+
length_name = "length"
|
|
307
|
+
df[length_name] = lengths
|
|
308
|
+
visible_headers = [length_name] # always show length for segments
|
|
309
|
+
|
|
310
|
+
if self.roi_indices is not None:
|
|
311
|
+
df.insert(0, "roi_index", self.roi_indices)
|
|
312
|
+
|
|
313
|
+
# Filter to visible columns if requested
|
|
314
|
+
if visible_only:
|
|
315
|
+
df = DataFrameManager.apply_visible_only_filter(df, visible_headers)
|
|
316
|
+
|
|
317
|
+
return df
|
|
318
|
+
|
|
319
|
+
def get_display_preferences(self) -> dict[str, bool]:
|
|
320
|
+
"""Get display preferences for coordinate headers.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Dictionary mapping header names to visibility (True=visible, False=hidden).
|
|
324
|
+
By default, all coordinates are visible unless specified in attrs.
|
|
325
|
+
"""
|
|
326
|
+
return DisplayPreferencesManager.get_display_preferences(
|
|
327
|
+
self, self.headers, "hidden_coords"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def set_display_preferences(self, preferences: dict[str, bool]) -> None:
|
|
331
|
+
"""Set display preferences for coordinate headers.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
preferences: Dictionary mapping header names to visibility
|
|
335
|
+
(True=visible, False=hidden)
|
|
336
|
+
"""
|
|
337
|
+
DisplayPreferencesManager.set_display_preferences(
|
|
338
|
+
self, preferences, self.headers, "hidden_coords"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def get_visible_headers(self) -> list[str]:
|
|
342
|
+
"""Get list of currently visible headers based on display preferences.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
List of header names that should be displayed
|
|
346
|
+
"""
|
|
347
|
+
return DisplayPreferencesManager.get_visible_headers(
|
|
348
|
+
self, self.headers, "hidden_coords"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# -------- User-oriented methods --------
|
|
352
|
+
|
|
353
|
+
def __len__(self) -> int:
|
|
354
|
+
"""Return the number of coordinates (rows) in the result."""
|
|
355
|
+
return self.coords.shape[0]
|
|
356
|
+
|
|
357
|
+
def rows(self, roi: int | None = None) -> np.ndarray:
|
|
358
|
+
"""Return coords for all rows (this ROI or full-image row).
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
roi: Optional ROI index to filter rows.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
2-D array of shape (M, K) with coordinates for the selected rows.
|
|
365
|
+
"""
|
|
366
|
+
if self.roi_indices is None:
|
|
367
|
+
return self.coords
|
|
368
|
+
target = NO_ROI if roi is None else int(roi)
|
|
369
|
+
return self.coords[self.roi_indices == target]
|
|
370
|
+
|
|
371
|
+
# Optional convenience for common kinds:
|
|
372
|
+
def segments_lengths(self) -> np.ndarray:
|
|
373
|
+
"""For kind='segment': return vector of segment lengths."""
|
|
374
|
+
if self.kind != KindShape.SEGMENT:
|
|
375
|
+
raise ValueError("segments_lengths requires kind='segment'")
|
|
376
|
+
dx = self.coords[:, 2] - self.coords[:, 0]
|
|
377
|
+
dy = self.coords[:, 3] - self.coords[:, 1]
|
|
378
|
+
return np.sqrt(dx * dx + dy * dy)
|
|
379
|
+
|
|
380
|
+
def circles_radii(self) -> np.ndarray:
|
|
381
|
+
"""For kind='circle': return radii."""
|
|
382
|
+
if self.kind != KindShape.CIRCLE:
|
|
383
|
+
raise ValueError("circles_radii requires kind='circle'")
|
|
384
|
+
return self.coords[:, 2]
|
|
385
|
+
|
|
386
|
+
def ellipse_axes_angles(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
387
|
+
"""For kind='ellipse': return (a, b, theta)."""
|
|
388
|
+
if self.kind != KindShape.ELLIPSE:
|
|
389
|
+
raise ValueError("ellipse_axes_angles requires kind='ellipse'")
|
|
390
|
+
return self.coords[:, 2], self.coords[:, 3], self.coords[:, 4]
|
|
391
|
+
|
|
392
|
+
def to_html(
|
|
393
|
+
self,
|
|
394
|
+
obj=None,
|
|
395
|
+
visible_only: bool = True,
|
|
396
|
+
transpose_single_row: bool = True,
|
|
397
|
+
**kwargs,
|
|
398
|
+
) -> str:
|
|
399
|
+
"""Convert the result to HTML format.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
obj: Optional SignalObj or ImageObj for ROI title extraction
|
|
403
|
+
visible_only: If True, include only visible headers based on display
|
|
404
|
+
preferences. Default is False.
|
|
405
|
+
transpose_single_row: If True, transpose when there's only one row
|
|
406
|
+
**kwargs: Additional arguments passed to DataFrame.to_html()
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
HTML representation of the result
|
|
410
|
+
"""
|
|
411
|
+
return ResultHtmlGenerator.generate_html(
|
|
412
|
+
self, obj, visible_only, transpose_single_row, **kwargs
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ===========================
|
|
417
|
+
# Geometry utility functions
|
|
418
|
+
# ===========================
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def concat_geometries(
|
|
422
|
+
title: str,
|
|
423
|
+
items: Iterable[GeometryResult],
|
|
424
|
+
*,
|
|
425
|
+
kind: KindShape | None = None,
|
|
426
|
+
) -> GeometryResult:
|
|
427
|
+
"""Concatenate multiple GeometryResult objects of the same kind.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
title: Title for the concatenated result.
|
|
431
|
+
items: Iterable of GeometryResult objects to concatenate.
|
|
432
|
+
kind: Optional kind label for the concatenated result.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
GeometryResult with concatenated data and updated metadata.
|
|
436
|
+
"""
|
|
437
|
+
items = list(items)
|
|
438
|
+
if not items:
|
|
439
|
+
return GeometryResult(
|
|
440
|
+
title=title, kind=KindShape.POINT, coords=np.zeros((0, 2), float)
|
|
441
|
+
)
|
|
442
|
+
k = kind if kind is not None else items[0].kind
|
|
443
|
+
for it in items:
|
|
444
|
+
if it.kind != k:
|
|
445
|
+
raise ValueError(
|
|
446
|
+
"All GeometryResult objects must share the same kind to concatenate"
|
|
447
|
+
)
|
|
448
|
+
max_k = max(it.coords.shape[1] for it in items) if items else 0
|
|
449
|
+
# right-pad with NaNs to match width
|
|
450
|
+
padded = []
|
|
451
|
+
for it in items:
|
|
452
|
+
c = it.coords
|
|
453
|
+
if c.shape[1] < max_k:
|
|
454
|
+
pad = np.full((c.shape[0], max_k - c.shape[1]), np.nan, dtype=float)
|
|
455
|
+
c = np.hstack([c, pad])
|
|
456
|
+
padded.append(c)
|
|
457
|
+
coords = np.vstack(padded) if padded else np.zeros((0, max_k))
|
|
458
|
+
if any(it.roi_indices is not None for it in items):
|
|
459
|
+
parts = [
|
|
460
|
+
(
|
|
461
|
+
it.roi_indices
|
|
462
|
+
if it.roi_indices is not None
|
|
463
|
+
else np.full((len(it.coords),), NO_ROI, int)
|
|
464
|
+
)
|
|
465
|
+
for it in items
|
|
466
|
+
]
|
|
467
|
+
roi = np.concatenate(parts) if len(parts) else None
|
|
468
|
+
else:
|
|
469
|
+
roi = None
|
|
470
|
+
return GeometryResult(title=title, kind=k, coords=coords, roi_indices=roi)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def filter_geometry_by_roi(res: GeometryResult, roi: int | None) -> GeometryResult:
|
|
474
|
+
"""Filter shapes by ROI index. If roi is None, keeps NO_ROI rows.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
res: The GeometryResult to filter.
|
|
478
|
+
roi: The ROI index to filter by, or None to keep all.
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
A filtered GeometryResult.
|
|
482
|
+
"""
|
|
483
|
+
if res.roi_indices is None:
|
|
484
|
+
keep_all = roi in (None, NO_ROI)
|
|
485
|
+
coords = res.coords if keep_all else np.zeros((0, res.coords.shape[1]))
|
|
486
|
+
indices = None if keep_all else np.zeros((0,), int)
|
|
487
|
+
return GeometryResult(
|
|
488
|
+
title=res.title,
|
|
489
|
+
kind=res.kind,
|
|
490
|
+
coords=coords,
|
|
491
|
+
roi_indices=indices,
|
|
492
|
+
attrs=dict(res.attrs),
|
|
493
|
+
)
|
|
494
|
+
target = NO_ROI if roi is None else int(roi)
|
|
495
|
+
mask = res.roi_indices == target
|
|
496
|
+
return GeometryResult(
|
|
497
|
+
title=res.title,
|
|
498
|
+
kind=res.kind,
|
|
499
|
+
coords=res.coords[mask],
|
|
500
|
+
roi_indices=res.roi_indices[mask],
|
|
501
|
+
attrs=dict(res.attrs),
|
|
502
|
+
)
|