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
15 changes: 15 additions & 0 deletions doc/release_notes/release_1.01.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Version 1.1 #

## Sigima Version 1.1.3 (unreleased) ##

### 🛠️ Bug Fixes since version 1.1.2 ###

* **Ellipse/circle contour detection**: Fixed swapped X/Y coordinates when using scikit-image ≥ 0.26.0
* The new `EllipseModel`/`CircleModel` API returns center coordinates as `(row, col)` but the code was treating them as `(x, y)`, causing detected ellipses and circles to appear at wrong positions on the image
* Semi-axis lengths were also swapped for ellipses, resulting in incorrect aspect ratios
* This bug only affected scikit-image ≥ 0.26.0; older versions were handled correctly
* **Ellipse visualization**: Fixed incorrect minor axis direction in `ellipse_to_diameters` coordinate conversion
* The minor axis endpoints were computed with a wrong sign, making the minor axis non-perpendicular to the major axis for rotated ellipses (e.g., at θ=45° both axes pointed in the same direction). This caused distorted ellipse overlays in DataLab when displaying detected or annotated ellipses at non-trivial rotation angles
* **Circle/ellipse detection with calibrated images**: Fixed radius and semi-axes not being converted from pixel units to physical units
* When an image had non-default pixel calibration (e.g., dx=2 mm/pixel), center coordinates were correctly converted to physical units but the radius and semi-axes remained in pixels, causing values to be wrong by a factor equal to the pixel size



## Sigima Version 1.1.2 (2026-04-20) ##

### 💥 Breaking changes since version 1.1.1 ###
Expand Down
11 changes: 11 additions & 0 deletions sigima/proc/image/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,17 @@ def compute_geometry_from_obj(
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
if obj.roi is not None:
x0, y0, _x1, _y1 = obj.roi.get_single_roi(i_roi).get_bounding_box(obj)
coords[:, colx] += x0 - obj.x0
Expand Down
4 changes: 2 additions & 2 deletions sigima/tools/coordinates.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def ellipse_to_diameters(
Ellipse X/Y diameters (major/minor axes) coordinates
"""
dxa, dya = a * np.cos(theta), a * np.sin(theta)
dxb, dyb = b * np.sin(theta), b * np.cos(theta)
dxb, dyb = -b * np.sin(theta), b * np.cos(theta)
x0, y0, x1, y1 = xc - dxa, yc - dya, xc + dxa, yc + dya
x2, y2, x3, y3 = xc - dxb, yc - dyb, xc + dxb, yc + dyb
return x0, y0, x1, y1, x2, y2, x3, y3
Expand All @@ -117,7 +117,7 @@ def array_ellipse_to_diameters(data: np.ndarray) -> np.ndarray:
"""
xc, yc, a, b, theta = data[:, 0], data[:, 1], data[:, 2], data[:, 3], data[:, 4]
dxa, dya = a * np.cos(theta), a * np.sin(theta)
dxb, dyb = b * np.sin(theta), b * np.cos(theta)
dxb, dyb = -b * np.sin(theta), b * np.cos(theta)
x0, y0, x1, y1 = xc - dxa, yc - dya, xc + dxa, yc + dya
x2, y2, x3, y3 = xc - dxb, yc - dyb, xc + dxb, yc + dyb
result = np.column_stack((x0, y0, x1, y1, x2, y2, x3, y3)).astype(float)
Expand Down
9 changes: 6 additions & 3 deletions sigima/tools/image/preprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ def fit_circle_model(contour: np.ndarray) -> tuple[float, float, float] | None:
if _USE_NEW_SHAPE_API:
model = measure.CircleModel.from_estimate(contour)
if model:
return model.center[0], model.center[1], model.radius
# model.center is (row, col) = (y, x), swap to (x, y)
return model.center[1], model.center[0], model.radius
else:
model = measure.CircleModel()
if model.estimate(contour):
Expand All @@ -73,8 +74,10 @@ def fit_ellipse_model(
if _USE_NEW_SHAPE_API:
model = measure.EllipseModel.from_estimate(contour)
if model:
xc, yc = model.center[0], model.center[1]
a, b = model.axis_lengths[0], model.axis_lengths[1]
# model.center is (row, col) = (y, x), swap to (x, y)
# model.axis_lengths is (semi_row, semi_col), swap to (semi_x, semi_y)
xc, yc = model.center[1], model.center[0]
a, b = model.axis_lengths[1], model.axis_lengths[0]
return xc, yc, a, b, model.theta
else:
model = measure.EllipseModel()
Expand Down
Loading