Pyclesperanto Simple Implementation

Module: openhcs.processing.backends.analysis.cell_counting_pyclesperanto_simple Status: STABLE

Overview

The simplified pyclesperanto cell counting implementation provides a direct, 334-line implementation of the Voronoi-Otsu labeling workflow with full OpenHCS materialization compatibility. This represents a 5x reduction in complexity compared to the original 1,578-line implementation.

Quick Reference

from openhcs.processing.backends.analysis.cell_counting_pyclesperanto_simple import (
    count_cells_single_channel, count_cells_simple, DetectionMethod
)

# Full function (compatible with existing system)
output_stack, results, masks = count_cells_single_channel(
    image_stack,
    detection_method=DetectionMethod.VORONOI_OTSU,
    gaussian_sigma=1.0,
    min_cell_area=50,
    max_cell_area=5000,
    return_segmentation_mask=True
)

# Quick function (minimal parameters)
cell_count, positions = count_cells_simple(
    image,
    gaussian_sigma=1.0,
    min_cell_area=50,
    max_cell_area=5000
)

Voronoi-Otsu Workflow

The implementation follows the exact pyclesperanto reference workflow:

import pyclesperanto_prototype as cle

def voronoi_otsu_labeling(gpu_image):
    """Direct implementation of pyclesperanto reference workflow."""

    # 1. Gaussian blur for noise reduction
    blurred = cle.gaussian_blur(gpu_image, sigma_x=1.0, sigma_y=1.0)

    # 2. Detect spots (cell centers)
    spots = cle.detect_spots(blurred, radius_x=1, radius_y=1)

    # 3. Otsu threshold for cell boundaries
    binary = cle.threshold_otsu(blurred)

    # 4. Masked Voronoi labeling (cell segmentation)
    voronoi_labels = cle.masked_voronoi_labeling(spots, binary)

    return voronoi_labels

Key insight: This workflow combines spot detection (cell centers) with Voronoi tessellation (cell boundaries) for robust cell segmentation.

Detection Methods

THRESHOLD (Fallback)

output_stack, results, masks = count_cells_single_channel(
    image_stack,
    detection_method=DetectionMethod.THRESHOLD,
    gaussian_sigma=1.0,
    min_cell_area=50,
    max_cell_area=5000
)

Best for:

  • High-contrast images

  • Well-separated cells

  • Quick testing and debugging

How it works:

  1. Gaussian blur reduces noise

  2. Otsu threshold creates binary mask

  3. Connected components labels cells

  4. Area filtering removes noise

Materialization Compatibility

The simplified implementation maintains full compatibility with OpenHCS materialization:

# Results are CellCountResult objects
result = results[0]

# Access cell count
print(f"Cell count: {result.cell_count}")

# Access cell positions (N x 2 array)
print(f"Positions: {result.cell_positions}")

# Access cell areas (N-element array)
print(f"Areas: {result.cell_areas}")

# Segmentation masks (labeled image)
mask = masks[0]  # Each cell has unique label

Materialization outputs:

  • JSON summary: Cell counts per image

  • CSV details: Cell positions and areas

  • ROI extraction: Segmentation masks for all backends

  • Backend support: Disk, OMERO, Napari, Fiji streaming

Pipeline Integration

The simplified implementation integrates seamlessly with OpenHCS pipelines:

from openhcs.core.pipeline import Pipeline
from openhcs.core.steps import FunctionStep

# Create pipeline with cell counting step
pipeline = Pipeline(
    steps=[
        FunctionStep(
            function='count_cells_single_channel',
            parameters={
                'detection_method': DetectionMethod.VORONOI_OTSU,
                'gaussian_sigma': 1.0,
                'min_cell_area': 50,
                'max_cell_area': 5000,
                'return_segmentation_mask': True
            }
        )
    ]
)

# Execute pipeline
pipeline.execute(plate_path)

Key insight: The simplified implementation uses the same materialization system as the original, ensuring backward compatibility.

Testing the Implementation

Run the test script to verify the implementation:

cd openhcs/processing/backends/analysis/
python test_simple_implementation.py

Test Coverage

# Test script covers:
# 1. Synthetic cell image generation
# 2. Full function testing
# 3. Simple function testing
# 4. Method comparison (VORONOI_OTSU vs THRESHOLD)
# 5. Memory efficiency testing
# 6. Visualization generation

def test_simple_implementation():
    """Test simplified cell counting implementation."""

    # Create synthetic cell image
    image = create_synthetic_cells(
        num_cells=50,
        image_size=(512, 512),
        cell_radius=10
    )

    # Test full function
    output, results, masks = count_cells_single_channel(
        image[np.newaxis, ...],  # Add Z dimension
        detection_method=DetectionMethod.VORONOI_OTSU
    )

    # Verify results
    assert results[0].cell_count > 0
    assert len(results[0].cell_positions) == results[0].cell_count

    # Test simple function
    count, positions = count_cells_simple(image)
    assert count > 0
    assert len(positions) == count

Test outputs:

  • Console summary of detected cells

  • Comparison plots (VORONOI_OTSU vs THRESHOLD)

  • Memory usage statistics

  • Visualization of segmentation masks

Common Patterns

Basic Cell Counting

from openhcs.processing.backends.analysis.cell_counting_pyclesperanto_simple import (
    count_cells_single_channel, DetectionMethod
)

# Load image stack (Z, Y, X format)
image_stack = load_image_stack('path/to/images')

# Count cells
output_stack, results, masks = count_cells_single_channel(
    image_stack,
    detection_method=DetectionMethod.VORONOI_OTSU,
    gaussian_sigma=1.0,
    min_cell_area=50,
    max_cell_area=5000,
    return_segmentation_mask=True
)

# Process results
for i, result in enumerate(results):
    print(f"Image {i}: {result.cell_count} cells")

Quick Analysis

from openhcs.processing.backends.analysis.cell_counting_pyclesperanto_simple import count_cells_simple

# For 2D images - quick one-liner
cell_count, positions = count_cells_simple(
    your_2d_image,
    gaussian_sigma=1.0,
    min_cell_area=50,
    max_cell_area=5000
)

print(f"Detected {cell_count} cells")
print(f"Positions:\n{positions}")

Parameter Tuning

# Adjust gaussian_sigma for noise level
# - Higher sigma: More smoothing, fewer false positives
# - Lower sigma: Less smoothing, more sensitivity

# Low noise images
output, results, masks = count_cells_single_channel(
    image_stack,
    gaussian_sigma=0.5  # Less smoothing
)

# High noise images
output, results, masks = count_cells_single_channel(
    image_stack,
    gaussian_sigma=2.0  # More smoothing
)

# Adjust area filters for cell size
# - min_cell_area: Remove small noise
# - max_cell_area: Remove large artifacts

# Small cells (e.g., bacteria)
output, results, masks = count_cells_single_channel(
    image_stack,
    min_cell_area=10,
    max_cell_area=500
)

# Large cells (e.g., neurons)
output, results, masks = count_cells_single_channel(
    image_stack,
    min_cell_area=200,
    max_cell_area=10000
)

Migration from Original Implementation

The simplified implementation provides a straightforward migration path:

# Old approach (complex, 50+ parameters)
from openhcs.processing.backends.analysis.cell_counting_pyclesperanto import (
    count_cells_single_channel
)

output_stack, results, masks = count_cells_single_channel(
    image_stack,
    detection_method=DetectionMethod.BLOB_LOG,
    min_sigma=1.0,
    max_sigma=10.0,
    num_sigma=10,
    threshold=0.1,
    overlap=0.5,
    # ... 45+ more parameters
)

# New approach (simple, 5 core parameters)
from openhcs.processing.backends.analysis.cell_counting_pyclesperanto_simple import (
    count_cells_single_channel, DetectionMethod
)

output_stack, results, masks = count_cells_single_channel(
    image_stack,
    detection_method=DetectionMethod.VORONOI_OTSU,
    gaussian_sigma=1.0,
    min_cell_area=50,
    max_cell_area=5000,
    return_segmentation_mask=True
)

Migration benefits:

  • 5x reduction in code complexity

  • Fewer parameters to tune

  • Better performance (less overhead)

  • Same materialization outputs

  • Full backward compatibility

Implementation Notes

🔬 Source Code:

  • Implementation: openhcs/processing/backends/analysis/cell_counting_pyclesperanto_simple.py (line 1)

  • Tests: openhcs/processing/backends/analysis/test_simple_implementation.py (line 1)

  • README: openhcs/processing/backends/analysis/README_SIMPLE_IMPLEMENTATION.md (line 1)

🏗️ Architecture:

📊 Performance:

  • Code size: 334 lines (vs 1,578 lines original)

  • Memory usage: Automatic GPU cleanup (no manual management)

  • Processing speed: Faster (simplified workflow)

  • Parameter count: 5 core parameters (vs 50+ original)

Key Design Decisions

Why Voronoi-Otsu instead of blob detection?

Voronoi-Otsu is the pyclesperanto reference workflow, providing robust cell segmentation with minimal parameters. Blob detection requires extensive parameter tuning.

Why two functions (full vs simple)?

The full function maintains compatibility with existing pipelines. The simple function provides a quick API for interactive analysis.

Why automatic GPU memory management?

Pyclesperanto handles GPU memory automatically. Manual cleanup adds complexity without benefit.

Common Gotchas

  • Image format must be (Z, Y, X): Single 2D images need image[np.newaxis, ...] to add Z dimension

  • GPU memory required: Pyclesperanto requires CUDA-capable GPU

  • Area filters are in pixels: Adjust based on image resolution and cell size

  • Gaussian sigma affects sensitivity: Higher sigma reduces false positives but may miss small cells

Debugging Cell Counting Issues

Symptom: Too Many False Positives

Cause: Noise or low gaussian_sigma

Diagnosis: Visualize segmentation masks

Fix: Increase gaussian_sigma or min_cell_area

# Increase smoothing
output, results, masks = count_cells_single_channel(
    image_stack,
    gaussian_sigma=2.0  # Increased from 1.0
)

# Or increase minimum area
output, results, masks = count_cells_single_channel(
    image_stack,
    min_cell_area=100  # Increased from 50
)

Symptom: Missing Cells

Cause: Over-smoothing or high min_cell_area

Diagnosis: Check cell sizes in original image

Fix: Decrease gaussian_sigma or min_cell_area

# Reduce smoothing
output, results, masks = count_cells_single_channel(
    image_stack,
    gaussian_sigma=0.5  # Decreased from 1.0
)

# Or decrease minimum area
output, results, masks = count_cells_single_channel(
    image_stack,
    min_cell_area=20  # Decreased from 50
)

Symptom: Touching Cells Not Separated

Cause: THRESHOLD method doesn’t separate touching cells

Diagnosis: Switch to VORONOI_OTSU

Fix: Use Voronoi-Otsu method

# Use Voronoi-Otsu for cell separation
output, results, masks = count_cells_single_channel(
    image_stack,
    detection_method=DetectionMethod.VORONOI_OTSU  # Changed from THRESHOLD
)