File: measurement.py

package info (click to toggle)
python-sigima 1.1.1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 25,608 kB
  • sloc: python: 35,251; makefile: 3
file content (198 lines) | stat: -rw-r--r-- 5,393 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
Measurement computation module
------------------------------

This module provides tools for extracting quantitative information from images,
such as object centroids, enclosing circles, and region-based statistics.

Main features include:

- Centroid and enclosing circle computation
- Region/property measurements
- Statistical analysis of image regions

These functions are useful for image quantification and morphometric analysis.
"""

# pylint: disable=invalid-name  # Allows short reference names like x, y, ...

# Note:
# ----
# - All `guidata.dataset.DataSet` parameter classes must also be imported
#   in the `sigima.params` module.
# - All functions decorated by `computation_function` must be imported in the upper
#   level `sigima.proc.image` module.

from __future__ import annotations

import numpy as np
from numpy import ma

import sigima.tools.image
from sigima.config import _
from sigima.objects import (
    GeometryResult,
    ImageObj,
    KindShape,
    SignalObj,
    TableKind,
    TableResult,
    TableResultBuilder,
)
from sigima.proc.base import new_signal_result
from sigima.proc.decorator import computation_function
from sigima.proc.image.base import compute_geometry_from_obj

# NOTE: Only parameter classes DEFINED in this module should be included in __all__.
# Parameter classes imported from other modules (like sigima.proc.base) should NOT
# be re-exported to avoid Sphinx cross-reference conflicts. The sigima.params module
# serves as the central API point that imports and re-exports all parameter classes.
__all__ = [
    "centroid",
    "enclosing_circle",
    "horizontal_projection",
    "stats",
    "vertical_projection",
]


def get_centroid_coords(data: np.ndarray) -> np.ndarray:
    """Return centroid coordinates
    with :py:func:`sigima.tools.image.get_centroid_auto`

    Args:
        data: input data

    Returns:
        Centroid coordinates
    """
    y, x = sigima.tools.image.get_centroid_auto(data)
    return np.array([(x, y)])


@computation_function()
def centroid(image: ImageObj) -> GeometryResult | None:
    """Compute centroid
    with :py:func:`sigima.tools.image.get_centroid_fourier`

    Args:
        image: input image

    Returns:
        Centroid coordinates
    """
    return compute_geometry_from_obj(
        "centroid", KindShape.MARKER, image, get_centroid_coords
    )


def get_enclosing_circle_coords(data: np.ndarray) -> np.ndarray:
    """Return diameter coords for the circle contour enclosing image
    values above threshold (FWHM)

    Args:
        data: input data

    Returns:
        Diameter coords
    """
    x, y, r = sigima.tools.image.get_enclosing_circle(data)
    return np.array([[x, y, r]])


@computation_function()
def enclosing_circle(image: ImageObj) -> GeometryResult | None:
    """Compute minimum enclosing circle
    with :py:func:`sigima.tools.image.get_enclosing_circle`

    Args:
        image: input image

    Returns:
        Diameter coords
    """
    return compute_geometry_from_obj(
        "enclosing_circle", KindShape.CIRCLE, image, get_enclosing_circle_coords
    )


def __calc_snr_without_warning(data: np.ndarray) -> float:
    """Calculate SNR based on <z>/σ(z), ignoring warnings

    Args:
        data: input data

    Returns:
        Signal-to-noise ratio
    """
    with np.errstate(divide="ignore", invalid="ignore"):
        snr = ma.mean(data) / ma.std(data)
    return snr


@computation_function()
def stats(obj: ImageObj) -> TableResult:
    """Compute statistics on an image

    Args:
        obj: input image object

    Returns:
        Result properties
    """
    builder = TableResultBuilder(_("Image statistics"), kind=TableKind.STATISTICS)
    builder.add(ma.min, "min")
    builder.add(ma.max, "max")
    builder.add(ma.mean, "mean")
    builder.add(ma.median, "median")
    builder.add(ma.std, "std")
    builder.add(__calc_snr_without_warning, "snr")
    builder.add(ma.ptp, "ptp")
    builder.add(ma.sum, "sum")
    return builder.compute(obj)


@computation_function()
def horizontal_projection(image: ImageObj) -> SignalObj:
    """Compute the sum of pixel intensities along each col. (projection on the x-axis).

    Args:
        image: Input image object.

    Returns:
        Signal object containing the profile.
    """
    dst_signal = new_signal_result(
        image,
        "horizontal_projection",
        units=(image.xunit, image.zunit),
        labels=(image.xlabel, image.zlabel),
    )
    x = np.linspace(image.x0, image.x0 + image.width - image.dx, image.data.shape[1])
    y = image.data.sum(axis=0, dtype=float)
    dst_signal.set_xydata(x, y)
    return dst_signal


@computation_function()
def vertical_projection(image: ImageObj) -> SignalObj:
    """Compute the sum of pixel intensities along each row (projection on the y-axis).

    Args:
        image: Input image object.

    Returns:
        Signal object containing the profile.
    """
    dst_signal = new_signal_result(
        image,
        "vertical_projection",
        units=(image.yunit, image.zunit),
        labels=(image.ylabel, image.zlabel),
    )
    x = np.linspace(image.y0, image.y0 + image.height - image.dy, image.data.shape[0])
    y = image.data.sum(axis=1, dtype=float)
    dst_signal.set_xydata(x, y)
    return dst_signal