Summary
Add a new image processing feature that converts detected contours into polygon ROIs,
enabling automatic mask generation from threshold-based contour detection. This bridges
the existing contour detection capability (contour_shape) with the ROI system
(PolygonalROI), allowing users to automatically define regions of interest from image
intensity contours.
Motivation
Currently, DataLab can:
- Detect contours via
contour_shape() → produces GeometryResult for visualization
- Define polygon ROIs manually via
PolygonalROI with arbitrary vertices
- Create ROIs from detection for blobs/peaks → uniform-sized rectangles/circles
around detected points
However, there is no way to automatically create ROIs from contour outlines. Users who
want to isolate regions based on intensity thresholds must manually draw polygon ROIs,
which is tedious and imprecise.
Use cases:
- Isolating bright/dark regions in microscopy images for further analysis
- Creating masks from thresholded features (e.g., particles, defects)
- Automatic segmentation-based ROI definition for batch processing
- Extracting regions of interest based on intensity level curves
Current State Analysis
What exists
| Component |
Status |
Location |
skimage.measure.find_contours() |
Available |
Used in get_contour_shapes() |
get_contour_shapes(data, shape=POLYGON, level) |
Exists |
sigima/tools/image/detection.py |
PolygonalROI class |
Exists |
sigima/objects/image/roi.py |
create_image_roi("polygon", coords) |
Exists |
sigima/objects/image/roi.py |
contour_shape() proc function |
Exists |
sigima/proc/image/detection.py |
| Contour → ROI bridge |
Missing |
— |
| Mask → ROI conversion |
Missing |
— |
contour_shape with ROI output |
Missing |
No DetectionROIParam support |
Key insight
The POLYGON branch in get_contour_shapes() already extracts raw contour coordinates
from find_contours() and returns them as flattened [x0, y0, x1, y1, ...] arrays
(NaN-padded). The PolygonalROI class accepts exactly this format. The gap is purely a
bridge function connecting the two.
Implementation Plan
Phase 1: Sigima — Low-level contour-to-polygon extraction
File: sigima/tools/image/detection.py
Add a new function that extracts contours as polygon coordinate arrays suitable for
direct PolygonalROI construction:
def find_contour_polygons(
data: np.ndarray | ma.MaskedArray,
level: float = 0.5,
min_area: float = 0.0,
min_vertices: int = 3,
) -> list[np.ndarray]:
"""Find contours at a given level and return polygon coordinate arrays.
Args:
data: 2D image array
level: Relative threshold level (0.0 to 1.0, mapped to data range)
min_area: Minimum contour area in pixels (filters small contours)
min_vertices: Minimum number of vertices per contour
Returns:
List of 1D arrays, each containing flattened [x0, y0, x1, y1, ...] pairs
in physical coordinates (compatible with PolygonalROI)
"""
Key decisions:
- Use
skimage.measure.find_contours() directly (like existing code)
- Convert relative level (0–1) to absolute level using data min/max
- Filter by minimum area (using
skimage.measure.regionprops or shoelace formula)
and minimum vertex count to discard noise
- Return coordinates in pixel indices (to be converted to physical coordinates at the
proc layer when attaching to an object)
Phase 2: Sigima — High-level computation function
File: sigima/proc/image/detection.py
Add a new @computation_function that wraps the low-level tool and creates ROIs on
the image object:
class ContourToROIParam(gds.DataSet):
"""Parameters for contour-to-ROI conversion."""
level = gds.FloatItem(
_("Threshold level"),
default=0.5, min=0.0, max=1.0,
help=_("Relative threshold (0=min, 1=max)")
)
min_area = gds.FloatItem(
_("Min. contour area (pixels)"),
default=100.0, min=0.0,
help=_("Minimum enclosed area to keep a contour")
)
min_vertices = gds.IntItem(
_("Min. vertices"),
default=5, min=3,
help=_("Minimum vertex count per contour polygon")
)
max_contours = gds.IntItem(
_("Max. contours"),
default=10, min=1, max=100,
help=_("Maximum number of contours to convert to ROIs")
)
inverse = gds.BoolItem(
_("Inverse mask"),
default=False,
help=_("If True, ROI masks the inside of contours (focus outside)")
)
@computation_function()
def contour_to_roi(image: ImageObj, p: ContourToROIParam) -> ImageObj:
"""Create polygon ROIs from image contours at a threshold level.
Detects contours at the specified threshold and converts each contour
into a PolygonalROI, setting them as the image's ROI.
Args:
image: Input image object
p: Contour-to-ROI parameters
Returns:
Image object with polygon ROIs set from detected contours
"""
Processing type: This is a 1-to-1 operation (input image → same image with ROIs
attached). The image data is not modified; only the ROI metadata changes.
Coordinate conversion: Contour vertices from find_contours() are in pixel
indices (row, col). They must be converted to physical coordinates using the image's
x0, y0, dx, dy attributes before storing in PolygonalROI (which uses physical
coordinates).
Sorting: Contours should be sorted by area (largest first) and truncated to
max_contours.
Phase 3: Sigima — Exports and parameters
-
Export function in sigima/proc/image/__init__.py:
from sigima.proc.image.detection import contour_to_roi, ContourToROIParam
-
Export parameter in sigima/params.py:
from sigima.proc.image import ContourToROIParam
-
Export tool function in sigima/tools/image/__init__.py (if not already
re-exported):
from sigima.tools.image.detection import find_contour_polygons
Phase 4: DataLab — Processor registration
File: datalab/gui/processor/image.py
Register in ImageProcessor:
def register_detection(self) -> None:
# ... existing registrations ...
self.register_1_to_1(
sipi.contour_to_roi,
_("Contour to ROI"),
paramclass=sipi.ContourToROIParam,
# No icon initially, can be added later
)
Processing type choice: register_1_to_1 is appropriate because:
- Input: 1 image → Output: 1 image (same data, ROIs added)
- Multi-selection: each selected image gets its own contour-based ROIs
- The result is visible immediately (ROI overlay on the image plot)
Phase 5: DataLab — Menu integration
File: datalab/gui/actionhandler.py
Add the action to the Analysis menu, near the existing contour detection:
# In ImageActionHandler, within the Analysis menu setup
act = self.action_for("contour_to_roi")
# Add near existing contour_shape action
Refer to scripts/datalab_menus.txt for exact menu placement.
Phase 6: Tests
Sigima unit tests
File: sigima/tests/image/detection_unit_test.py (or new file)
def test_find_contour_polygons():
"""Test low-level contour polygon extraction."""
# Create synthetic image with known shapes (circle, rectangle)
data = np.zeros((100, 100))
rr, cc = skimage.draw.disk((50, 50), 20)
data[rr, cc] = 1.0
polygons = find_contour_polygons(data, level=0.5)
assert len(polygons) >= 1
assert all(len(p) >= 6 for p in polygons) # At least 3 vertices
def test_contour_to_roi():
"""Test high-level contour-to-ROI function."""
image = create_test_image_with_blobs()
p = ContourToROIParam.create(level=0.5, min_area=50)
result = contour_to_roi(image, p)
assert result.roi is not None
assert len(result.roi.single_rois) > 0
assert all(isinstance(r, PolygonalROI) for r in result.roi.single_rois)
def test_contour_to_roi_empty():
"""Test with uniform image (no contours)."""
image = create_uniform_image()
p = ContourToROIParam.create(level=0.5)
result = contour_to_roi(image, p)
assert result.roi is None or len(result.roi.single_rois) == 0
DataLab integration tests
File: datalab/tests/features/image/contour_to_roi_test.py
Test the full GUI pipeline: create image → run contour_to_roi → verify ROIs on result.
Phase 7: Documentation
- Sphinx docs (
doc/features/image/menu_analysis.rst): Add section describing
the "Contour to ROI" feature with screenshots and parameter descriptions
- Release notes (
doc/release_notes/): Add user-focused entry
- Translations: Run
guidata.utils.translations scan for both Sigima and DataLab
Design Decisions & Alternatives
Why PolygonalROI and not a new MaskROI?
PolygonalROI already supports arbitrary shapes and produces masks via
skimage.draw.polygon
- All existing ROI infrastructure (serialization, visualization, mask computation,
inverse logic) works with PolygonalROI
- A bitmap-based
MaskROI would require new serialization, new visualization code,
and could not leverage the existing ROI combination logic
- Polygon ROIs are editable by the user after creation; bitmap masks would not be
Trade-off: Very complex contours with many vertices may be slow to render or edit.
The min_vertices and max_contours parameters mitigate this. A vertex decimation
step (e.g., Douglas-Peucker simplification via skimage.measure.approximate_polygon)
could be added as an optional parameter.
Why a new function instead of extending contour_shape()?
contour_shape() returns GeometryResult (scalar analysis) — fundamentally
different from ROI creation
- Adding ROI creation to
contour_shape would mix two concerns (analysis vs. ROI
definition)
contour_shape fits shapes (circles, ellipses) to contours; this feature preserves
raw contour geometry
- A separate function is cleaner and follows the single-responsibility principle
Why register_1_to_1 instead of register_1_to_0?
- The output is the same image with ROIs attached, not a scalar result
1_to_0 is for analysis functions producing GeometryResult/TableResult
1_to_1 allows the ROI to be immediately visible on the image plot
- Alternative: a dedicated ROI-setting mechanism, but
1_to_1 returning the same
image with ROIs is the simplest approach
Vertex simplification
Consider adding an optional Douglas-Peucker tolerance parameter:
tolerance = gds.FloatItem(
_("Simplification tolerance"),
default=0.0, min=0.0,
help=_("Douglas-Peucker tolerance for vertex reduction (0=no simplification)")
)
This would use skimage.measure.approximate_polygon(contour, tolerance) to reduce
vertex count on complex contours, improving performance and editability.
Open Questions
-
Should existing ROIs be replaced or appended? When contour_to_roi is applied
to an image that already has ROIs, should the new contour-based ROIs replace or be
added to the existing ones? (Suggested default: replace, with an option to append.)
-
Should the function support ROI from a separate threshold image? For example,
applying a threshold on channel A of a multi-channel image and using the resulting
contours as ROI on the original image. This could be a future extension.
-
What about closed vs. open contours? find_contours may return open contours
at image boundaries. Should these be closed automatically or filtered out?
(Suggested: close them by connecting first and last points.)
-
Maximum vertex count per polygon? Very detailed contours can have thousands of
vertices. Should there be a maximum, with automatic decimation?
(Suggested: add optional simplification via Douglas-Peucker.)
Acceptance Criteria
Summary
Add a new image processing feature that converts detected contours into polygon ROIs,
enabling automatic mask generation from threshold-based contour detection. This bridges
the existing contour detection capability (
contour_shape) with the ROI system(
PolygonalROI), allowing users to automatically define regions of interest from imageintensity contours.
Motivation
Currently, DataLab can:
contour_shape()→ producesGeometryResultfor visualizationPolygonalROIwith arbitrary verticesaround detected points
However, there is no way to automatically create ROIs from contour outlines. Users who
want to isolate regions based on intensity thresholds must manually draw polygon ROIs,
which is tedious and imprecise.
Use cases:
Current State Analysis
What exists
skimage.measure.find_contours()get_contour_shapes()get_contour_shapes(data, shape=POLYGON, level)sigima/tools/image/detection.pyPolygonalROIclasssigima/objects/image/roi.pycreate_image_roi("polygon", coords)sigima/objects/image/roi.pycontour_shape()proc functionsigima/proc/image/detection.pycontour_shapewith ROI outputDetectionROIParamsupportKey insight
The
POLYGONbranch inget_contour_shapes()already extracts raw contour coordinatesfrom
find_contours()and returns them as flattened[x0, y0, x1, y1, ...]arrays(NaN-padded). The
PolygonalROIclass accepts exactly this format. The gap is purely abridge function connecting the two.
Implementation Plan
Phase 1: Sigima — Low-level contour-to-polygon extraction
File:
sigima/tools/image/detection.pyAdd a new function that extracts contours as polygon coordinate arrays suitable for
direct
PolygonalROIconstruction:Key decisions:
skimage.measure.find_contours()directly (like existing code)skimage.measure.regionpropsor shoelace formula)and minimum vertex count to discard noise
proclayer when attaching to an object)Phase 2: Sigima — High-level computation function
File:
sigima/proc/image/detection.pyAdd a new
@computation_functionthat wraps the low-level tool and creates ROIs onthe image object:
Processing type: This is a 1-to-1 operation (input image → same image with ROIs
attached). The image data is not modified; only the ROI metadata changes.
Coordinate conversion: Contour vertices from
find_contours()are in pixelindices
(row, col). They must be converted to physical coordinates using the image'sx0, y0, dx, dyattributes before storing inPolygonalROI(which uses physicalcoordinates).
Sorting: Contours should be sorted by area (largest first) and truncated to
max_contours.Phase 3: Sigima — Exports and parameters
Export function in
sigima/proc/image/__init__.py:Export parameter in
sigima/params.py:Export tool function in
sigima/tools/image/__init__.py(if not alreadyre-exported):
Phase 4: DataLab — Processor registration
File:
datalab/gui/processor/image.pyRegister in
ImageProcessor:Processing type choice:
register_1_to_1is appropriate because:Phase 5: DataLab — Menu integration
File:
datalab/gui/actionhandler.pyAdd the action to the Analysis menu, near the existing contour detection:
Refer to
scripts/datalab_menus.txtfor exact menu placement.Phase 6: Tests
Sigima unit tests
File:
sigima/tests/image/detection_unit_test.py(or new file)DataLab integration tests
File:
datalab/tests/features/image/contour_to_roi_test.pyTest the full GUI pipeline: create image → run contour_to_roi → verify ROIs on result.
Phase 7: Documentation
doc/features/image/menu_analysis.rst): Add section describingthe "Contour to ROI" feature with screenshots and parameter descriptions
doc/release_notes/): Add user-focused entryguidata.utils.translations scanfor both Sigima and DataLabDesign Decisions & Alternatives
Why
PolygonalROIand not a newMaskROI?PolygonalROIalready supports arbitrary shapes and produces masks viaskimage.draw.polygoninverse logic) works with
PolygonalROIMaskROIwould require new serialization, new visualization code,and could not leverage the existing ROI combination logic
Trade-off: Very complex contours with many vertices may be slow to render or edit.
The
min_verticesandmax_contoursparameters mitigate this. A vertex decimationstep (e.g., Douglas-Peucker simplification via
skimage.measure.approximate_polygon)could be added as an optional parameter.
Why a new function instead of extending
contour_shape()?contour_shape()returnsGeometryResult(scalar analysis) — fundamentallydifferent from ROI creation
contour_shapewould mix two concerns (analysis vs. ROIdefinition)
contour_shapefits shapes (circles, ellipses) to contours; this feature preservesraw contour geometry
Why
register_1_to_1instead ofregister_1_to_0?1_to_0is for analysis functions producingGeometryResult/TableResult1_to_1allows the ROI to be immediately visible on the image plot1_to_1returning the sameimage with ROIs is the simplest approach
Vertex simplification
Consider adding an optional Douglas-Peucker tolerance parameter:
This would use
skimage.measure.approximate_polygon(contour, tolerance)to reducevertex count on complex contours, improving performance and editability.
Open Questions
Should existing ROIs be replaced or appended? When
contour_to_roiis appliedto an image that already has ROIs, should the new contour-based ROIs replace or be
added to the existing ones? (Suggested default: replace, with an option to append.)
Should the function support ROI from a separate threshold image? For example,
applying a threshold on channel A of a multi-channel image and using the resulting
contours as ROI on the original image. This could be a future extension.
What about closed vs. open contours?
find_contoursmay return open contoursat image boundaries. Should these be closed automatically or filtered out?
(Suggested: close them by connecting first and last points.)
Maximum vertex count per polygon? Very detailed contours can have thousands of
vertices. Should there be a maximum, with automatic decimation?
(Suggested: add optional simplification via Douglas-Peucker.)
Acceptance Criteria
PolygonalROIobjects