Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions doc/locale/fr/LC_MESSAGES/requirements.po
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ msgstr "Langage de programmation Python"
msgid "guidata"
msgstr "guidata"

msgid ">= 3.16.0"
msgstr ">= 3.16.0"
msgid ">= 3.17.0"
msgstr ">= 3.17.0"

msgid "Automatic GUI generation for easy dataset editing and display"
msgstr "Génération automatique d'interface graphique pour une édition et un affichage faciles des ensembles de données"
Expand Down
4 changes: 2 additions & 2 deletions doc/locale/fr/LC_MESSAGES/user_guide/installation.po
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ msgstr "Langage de programmation Python"
msgid "guidata"
msgstr "guidata"

msgid ">= 3.16.0"
msgstr ">= 3.16.0"
msgid ">= 3.17.0"
msgstr ">= 3.17.0"

msgid "Automatic GUI generation for easy dataset editing and display"
msgstr "Génération automatique d'interfaces graphiques pour l'édition et l'affichage de jeux de données"
Expand Down
7 changes: 5 additions & 2 deletions doc/release_notes/release_1.04.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Requirements ###

* Python 3.10 or later is now required. Python 3.9 users must keep an earlier compatible release.
* guidata 3.17.0 or later is now required: importing Sigima no longer fails when a translation catalog is corrupted (messages fall back to English).

### ✨ New features since version 1.3.0 ###

Expand All @@ -27,12 +28,12 @@ The [user guide](../user_guide/overview.rst) shows how readers and connectors at
* **Replay of processing in image ROIs**: The selection of image pixels by regions of interest is now recorded with each call on images carrying regions of interest, and with each call of the operations taking image regions of interest as parameters (`extract_roi`, `extract_rois`, `erase` and `offset_correction`), whatever their input (rule `sigima.image.roi.pixel_centers`, `sigima.proc.contracts.IMAGE_ROI_CONVENTION`, in the execution context described by `describe_call` and `prepare_execution`). Applications that replay recorded processing (e.g. DataLab capsules) refuse to replay such calls recorded with earlier versions, which selected other pixels, instead of reporting a different result. Other calls on images without regions of interest are not concerned.
* **Image center**: `ImageObj.xc` and `ImageObj.yc` are now the center of the pixel grid (halfway between the centers of the first and last pixels), for uniform and non-uniform coordinates: they were half a pixel off for uniform coordinates. `width` and `height` are unchanged.
* **Ellipse coordinates**: `EllipseCoordinates` (`sigima.objects.shape`) now holds the 5 values `(x, y, a, b, theta)` of ellipse geometry results, which can thus be transformed.
* **Operation contract versions**: The operation contracts keep version 1 in this release, although the result of some operations changed since 1.3.0 (for example `resampling`, `extract_roi` and the product of signals): the contracts had just been published with Sigima 1.3.0 and were not used yet. This one-off exception does not apply to later changes: from now on, the version of a contract is increased whenever a change of the computation changes its result, whatever the releases of the applications using Sigima, and calls recorded with an earlier version are refused instead of being replayed (see [Operation contracts and versions](../user_guide/contracts.rst)). The image profiles and the product of signals (`product`), changed afterwards (see below), are thus at version 2.
* **Operation contract versions**: The operation contracts keep version 1 in this release, although the result of some operations changed since 1.3.0 (for example `resampling`, `extract_roi` and the product of signals): the contracts had just been published with Sigima 1.3.0 and were not used yet. This one-off exception does not apply to later changes: from now on, the version of a contract is increased whenever a change of the computation changes its result, whatever the releases of the applications using Sigima, and calls recorded with an earlier version are refused instead of being replayed (see [Operation contracts and versions](../user_guide/contracts.rst)). The image profiles, the product of signals (`product`), the extraction of image ROIs (`extract_roi`, `extract_rois`) and the image analyses giving geometric results (centroid, enclosing circle, peaks, contours, blobs and Hough circles), changed afterwards (see below), are thus at version 2.
* **Image profiles in physical coordinates**: The line, segment, average and radial profiles of images are now defined and returned in the physical coordinates of the image, like regions of interest and analysis results, instead of pixel indices. Their parameters were renamed, so that scripts using the former names fail explicitly instead of silently using another meaning: `LineProfileParam.row`/`col` became `y`/`x` (the row or column whose pixel centers are the nearest), `SegmentProfileParam.row1`/`col1`/`row2`/`col2` and `AverageProfileParam.row1`/`col1`/`row2`/`col2` became `x1`/`y1`/`x2`/`y2` (for the average profile, `None` stands for the edge of the image instead of `-1`, and the pixels are those whose centers lie in the rectangle, like rectangular ROIs), and `RadialProfileParam.x0`/`y0` became `xc`/`yc`. The X axis of the profiles holds physical coordinates or distances, with the unit and label of the corresponding axis of the image. Segment profiles are sampled at regular steps from their start point to their end point included, with one sample per pixel along the axis where the segment spans the most pixels (as in Sigima 1.3), each sample taking the value of the nearest pixel. Radial profiles are binned in steps of the smallest distance between pixel centers, so that they are circular for anisotropic pixels and non-uniform coordinates. Segment and radial profiles are refused when the X and Y units of the image differ (and radial profiles when the centers of neighboring pixels coincide). Coordinates outside the image are refused instead of being clamped. On images without calibration (origin 0, pixel size 1), line, average and radial profiles are unchanged, and segment profiles take the same pixels for horizontal, vertical and diagonal segments: other oblique segments now take the nearest pixel of each sample instead of truncating its indices. The contracts of `line_profile`, `segment_profile`, `average_profile` and `radial_profile` are at version 2.

### 🛠️ Bug Fixes since version 1.3.0 ###

* **Image profiles**: The radial profile centered on the image center was computed around a wrong point on calibrated images (the physical center was used as pixel indices), and could even contain undefined values. Vertical profiles were labeled with the unit of the X axis instead of the Y axis, and all profiles with the unit of the image although their X axis was in pixels. Anti-diagonal segment profiles (e.g. from the top-right to the bottom-left corner) were extracted along the other diagonal.
* **Image profiles**: The radial profile centered on the image center was computed around a wrong point on calibrated images (the physical center was used as pixel indices), and could even contain undefined values. Vertical profiles were labeled with the unit of the X axis instead of the Y axis, and all profiles with the unit of the image although their X axis was in pixels. Anti-diagonal segment profiles (e.g. from the top-right to the bottom-left corner) were extracted along the other diagonal. The radial profile of an image holding no pixel is now refused with an explicit error, instead of an obscure NumPy error.
* **Exact integer import**: Integer data that does not fit any integer type supported by the destination object (for example `uint32` counts above 2 147 483 647 in an image, or large integers in a signal) is now converted to a floating-point type that represents every value exactly. Such data was previously converted to `float32` when the source used 4-byte integers, which rounded counts above 16 777 216. A warning is now emitted when no supported floating-point type is exact (integers beyond 2⁵³).
* **Image resampling**: Resampling an image onto its own grid now gives back the same image. The output pixels were sampled half a pixel away from the requested positions, so that every value was shifted and the last row and column were filled with NaN, while the output origin claimed the requested position. The `xmin`/`xmax` and `ymin`/`ymax` parameters of `resampling` are now the coordinates of the centers of the first and last output pixels, like the `xmin`/`xmax` of an image, and default to the source grid. In pixel size mode, the requested pixel size is now kept exactly (the last pixel is the last one not beyond `xmax`/`ymax`).
* **Real and complex signals**: The sum, average, product and standard deviation of several signals, and their conversion into an image, no longer depend on the order of the signals: when a real signal came first, the imaginary parts of the complex signals were discarded.
Expand All @@ -44,4 +45,6 @@ The [user guide](../user_guide/overview.rst) shows how readers and connectors at
* **Uncertainty of a product of signals**: The uncertainty of the product of signals is now propagated where a factor is zero: the result had no uncertainty at all there. Complex factors now give a real uncertainty, computed from their moduli. The uncertainty no longer becomes undefined (NaN) when the factors span a very wide range of magnitudes (e.g. 10³⁰⁸ × 1 × 10⁻³⁰⁸) or when the product itself is too small to be represented, and no longer vanishes when many signals are multiplied (it was wrong, then zero, from about 150 `float32` or 1 000 `float64` signals): it is defined wherever its value can be represented, whatever the number and the order of the signals. The contract of `product` is at version 2.
* **ROIs and annotations after flips and rotations**: Horizontal and vertical flips and 90° and 270° rotations now move the ROIs and annotations of an image with its pixels: they were mirrored about a point half a pixel away from the center of the pixel grid, hence shifted by one pixel, and non-uniform coordinates were ignored. The move is exact for uniform coordinates. For non-uniform coordinates, reversing the pixels along a non-uniform axis is not an affine mapping: points, bounds and unrotated rectangles are moved exactly, while circles, ellipses and polygons are moved approximately (a circle does not stay a circle). ROIs defined in pixel indices were moved as if they were physical coordinates, and inverse ROIs (which mask the inside of their shape) became normal ROIs after these operations, the transposition and the translation.
* **Coordinates of an extracted ROI**: `extract_roi` now gives the extracted image the coordinates of the extracted pixels. The origin of the source image was counted twice, and the coordinates of non-uniform images were shifted instead of being cut to the extracted pixels.
* **Extraction of ROIs holding no pixel**: Extracting regions of interest that hold no pixel of the image (for example the cells of a ROI grid narrower than a pixel, a rectangle outside the image or with no width, or a circle passing between pixel centers) is now refused with an explicit error: an empty image was created silently. The contracts of `extract_roi` and `extract_rois` are at version 2.
* **Analysis results in ROIs**: The coordinates of the analysis results computed in a ROI (centroid, peaks, contours, etc.) are now offset by the position of the first extracted pixel: they could be off by up to half a pixel when the ROI bounds did not lie on pixel centers.
* **Analysis results of non-uniform images**: The analysis results of images with non-uniform coordinates (centroid, enclosing circle, peaks, contours, blobs and Hough circles) are now given in physical coordinates, following the pixel centers: they were given in pixel indices, or according to a former uniform calibration left after switching to non-uniform coordinates. Radii and semi-axes are scaled by the local pixel size. The results of images with uniform coordinates are unchanged. The contracts of `centroid`, `enclosing_circle`, `peak_detection`, `contour_shape`, `blob_dog`, `blob_doh`, `blob_log`, `blob_opencv` and `hough_circle_peaks` are at version 2.
2 changes: 1 addition & 1 deletion doc/requirements.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The `sigima` package requires the following Python modules:
- >=3.10, <4
- Python programming language
* - guidata
- >= 3.16.0
- >= 3.17.0
- Automatic GUI generation for easy dataset editing and display
* - NumPy
- >= 1.22, < 2.6
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ PyWavelets >= 1.2, < 2.0
SciPy >= 1.10.1, < 1.19
babel
build
guidata >= 3.16.0
guidata >= 3.17.0
imagecodecs >= 2024.12.30; sys_platform != 'emscripten'
jsonschema >= 4
makefun >= 1.13.1
Expand Down
84 changes: 68 additions & 16 deletions sigima/proc/image/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,49 @@ def dst_1_to_1_signal(src: ImageObj, name: str, suffix: str | None = None) -> Si
)


def _indices_to_nonuniform_physical(
obj: ImageObj,
coords: np.ndarray,
colx: slice | int,
coly: slice | int,
ix0: int,
iy0: int,
) -> None:
"""Convert geometry coordinates from pixel indices to physical coordinates, in
place, for an image with non-uniform coordinates.

Points are interpolated linearly between the pixel centers (see
:meth:`~sigima.objects.ImageObj.axis_to_physical`). Distance-like values
(radius, semi-axes) are scaled by the local pixel size at the shape center,
i.e. the mean of the X and Y distances between the edges of the pixel there.

Args:
obj: image object with non-uniform coordinates
coords: coordinates in pixel indices, relative to the part of the image
starting at column ``ix0`` and row ``iy0`` (NaN padding is kept)
colx: X columns of *coords*
coly: Y columns of *coords*
ix0: first column of the part of the image
iy0: first row of the part of the image
"""
xi = coords[:, colx] + ix0
yi = coords[:, coly] + iy0
if coords.shape[1] % 2 != 0:
# Circle [x0, y0, r] or ellipse [x0, y0, a, b, theta] (theta is an angle)
size_x = np.abs(
obj.axis_to_physical(xi + 0.5, "x") - obj.axis_to_physical(xi - 0.5, "x")
)
size_y = np.abs(
obj.axis_to_physical(yi + 0.5, "y") - obj.axis_to_physical(yi - 0.5, "y")
)
pixel_scale = (size_x + size_y) / 2
coords[:, 2] *= pixel_scale
if coords.shape[1] >= 4:
coords[:, 3] *= pixel_scale
coords[:, colx] = obj.axis_to_physical(xi, "x")
coords[:, coly] = obj.axis_to_physical(yi, "y")


def compute_geometry_from_obj(
title: str,
shape: KindShape,
Expand Down Expand Up @@ -243,7 +286,11 @@ def compute_geometry_from_obj(

The conversion is performed using the image's calibration parameters:
``physical_x = obj.dx * pixel_x + obj.x0`` and
``physical_y = obj.dy * pixel_y + obj.y0``
``physical_y = obj.dy * pixel_y + obj.y0`` for uniform coordinates, and a
linear interpolation between the pixel centers (``obj.xcoords``,
``obj.ycoords``) for non-uniform coordinates. Distance-like values (radius,
semi-axes) are scaled by the mean pixel size: ``(obj.dx + obj.dy) / 2``, or
the local pixel size at the shape center for non-uniform coordinates.

.. warning::

Expand Down Expand Up @@ -306,25 +353,30 @@ def compute_geometry_from_obj(
else:
# Circle [x0, y0, r] or ellipse coordinates [x0, y0, a, b, theta]
colx, coly = 0, 1
coords[:, colx] = obj.dx * coords[:, colx] + obj.x0
coords[:, coly] = obj.dy * coords[:, coly] + obj.y0
if coords.shape[1] % 2 != 0:
# Scale distance-like values (radius, semi-axes) from pixel to
# physical units. Use average pixel size for isotropic scaling.
pixel_scale = (obj.dx + obj.dy) / 2
if coords.shape[1] >= 3:
# Column 2: radius (circle) or semi-axis a (ellipse)
coords[:, 2] *= pixel_scale
if coords.shape[1] >= 4:
# Column 3: semi-axis b (ellipse)
coords[:, 3] *= pixel_scale
# Column 4 (theta) is an angle: no scaling needed
ix0 = iy0 = 0
if obj.roi is not None:
# Coordinates are relative to the part of the image holding the
# ROI (see `ImageObj.get_data`): offset them by its first pixel
ix0, iy0, _ix1, _iy1 = obj.get_roi_crop_indices(i_roi)
coords[:, colx] += ix0 * obj.dx
coords[:, coly] += iy0 * obj.dy
if obj.is_uniform_coords:
coords[:, colx] = obj.dx * coords[:, colx] + obj.x0
coords[:, coly] = obj.dy * coords[:, coly] + obj.y0
if coords.shape[1] % 2 != 0:
# Scale distance-like values (radius, semi-axes) from pixel to
# physical units. Use average pixel size for isotropic scaling.
pixel_scale = (obj.dx + obj.dy) / 2
if coords.shape[1] >= 3:
# Column 2: radius (circle) or semi-axis a (ellipse)
coords[:, 2] *= pixel_scale
if coords.shape[1] >= 4:
# Column 3: semi-axis b (ellipse)
coords[:, 3] *= pixel_scale
# Column 4 (theta) is an angle: no scaling needed
if obj.roi is not None:
coords[:, colx] += ix0 * obj.dx
coords[:, coly] += iy0 * obj.dy
else:
_indices_to_nonuniform_physical(obj, coords, colx, coly, ix0, iy0)

rows.append(coords)
num_cols.append(coords.shape[1])
Expand Down
Loading
Loading