Files
drug-discovery-prompts/upstream/mims-harvard-ToolUniverse/skills/tooluniverse-image-analysis/references/segmentation.md

18 KiB
Raw Permalink Blame History

title, task, lineage_type, upstream_source, upstream_sha, imported_at, prompt_class, upstream_changes, author, validated
title task lineage_type upstream_source upstream_sha imported_at prompt_class upstream_changes author validated
Colony and Object Segmentation import https://github.com/mims-harvard/ToolUniverse/blob/e2520a96/skills/tooluniverse-image-analysis/references/segmentation.md e2520a96 2026-06-26 prompt accepted upstream false

Colony and Object Segmentation

Complete guide for segmenting and measuring bacterial colonies, biofilms, and other large objects in microscopy images.


Table of Contents

  1. Colony Morphometry Basics
  2. Threshold Methods
  3. Morphological Operations
  4. Measurement and Analysis
  5. Swarming Assay Analysis
  6. Time-Lapse Analysis

Colony Morphometry Basics

Why Colony Morphometry?

Colony morphometry measures:

  • Area: Colony size (pixels or calibrated units)
  • Circularity: Shape roundness (4π × area / perimeter²)
  • Roundness: Alternative shape metric (4 × area / π × major_axis²)
  • Solidity: Convexity (area / convex_hull_area)
  • Eccentricity: Elongation (0 = circle, 1 = line)

Used in:

  • Bacterial swarming assays
  • Biofilm formation studies
  • Fungal colony growth
  • Cell aggregate analysis

Threshold Methods

Otsu Threshold (Most Common)

from skimage import filters, measure, morphology
import numpy as np
import pandas as pd

def segment_colonies_otsu(image, min_area=100):
    """Segment colonies using Otsu's method.

    Best for: Well-defined colonies with clear contrast to background.

    Args:
        image: numpy array (grayscale or RGB)
        min_area: minimum colony area in pixels

    Returns: (binary_mask, labeled_image, properties_df)
    """
    # Convert to grayscale if needed
    if image.ndim == 3:
        from skimage.color import rgb2gray
        img = rgb2gray(image)
    else:
        img = image.astype(float)

    # Otsu threshold
    thresh = filters.threshold_otsu(img)
    binary = img > thresh  # or img < thresh if colonies are dark

    # Clean up small objects
    binary = morphology.remove_small_objects(binary, min_size=min_area)
    binary = morphology.binary_fill_holes(binary)

    # Label connected components
    labels = measure.label(binary)

    # Measure properties
    props = measure.regionprops_table(labels, properties=[
        'label', 'area', 'perimeter', 'eccentricity',
        'major_axis_length', 'minor_axis_length', 'solidity'
    ])
    props_df = pd.DataFrame(props)

    # Calculate circularity
    props_df['circularity'] = 4 * np.pi * props_df['area'] / (props_df['perimeter']**2)

    # Calculate roundness
    props_df['roundness'] = 4 * props_df['area'] / (np.pi * props_df['major_axis_length']**2)

    return binary, labels, props_df


# Example usage
import tifffile
image = tifffile.imread("swarming_plate.tif")
binary, labels, props = segment_colonies_otsu(image, min_area=500)
print(f"Found {labels.max()} colonies")
print(props.head())

Li Threshold (More Sensitive)

def segment_colonies_li(image, min_area=100):
    """Segment colonies using Li's method.

    Best for: Low-contrast images, faint colonies.

    Args:
        image: numpy array
        min_area: minimum colony area

    Returns: (binary_mask, labeled_image, properties_df)
    """
    if image.ndim == 3:
        from skimage.color import rgb2gray
        img = rgb2gray(image)
    else:
        img = image.astype(float)

    # Li threshold (more sensitive than Otsu)
    thresh = filters.threshold_li(img)
    binary = img > thresh

    # Cleanup
    binary = morphology.remove_small_objects(binary, min_size=min_area)
    binary = morphology.binary_fill_holes(binary)

    # Label and measure
    labels = measure.label(binary)
    props = measure.regionprops_table(labels, properties=[
        'label', 'area', 'perimeter', 'eccentricity',
        'major_axis_length', 'minor_axis_length', 'solidity'
    ])
    props_df = pd.DataFrame(props)

    # Add shape metrics
    props_df['circularity'] = 4 * np.pi * props_df['area'] / (props_df['perimeter']**2)
    props_df['roundness'] = 4 * props_df['area'] / (np.pi * props_df['major_axis_length']**2)

    return binary, labels, props_df

Adaptive Threshold (Uneven Illumination)

def segment_colonies_adaptive(image, block_size=51, offset=0.01, min_area=100):
    """Segment colonies with adaptive thresholding.

    Best for: Uneven illumination, gradient backgrounds.

    Args:
        image: numpy array
        block_size: size of local region (odd number)
        offset: threshold offset
        min_area: minimum colony area

    Returns: (binary_mask, labeled_image, properties_df)
    """
    if image.ndim == 3:
        from skimage.color import rgb2gray
        img = rgb2gray(image)
    else:
        img = image.astype(float)

    # Adaptive threshold
    local_thresh = filters.threshold_local(img, block_size=block_size, offset=offset)
    binary = img > local_thresh

    # Cleanup
    binary = morphology.remove_small_objects(binary, min_size=min_area)
    binary = morphology.binary_fill_holes(binary)

    # Label and measure
    labels = measure.label(binary)
    props = measure.regionprops_table(labels, properties=[
        'label', 'area', 'perimeter', 'eccentricity',
        'major_axis_length', 'minor_axis_length', 'solidity'
    ])
    props_df = pd.DataFrame(props)

    # Shape metrics
    props_df['circularity'] = 4 * np.pi * props_df['area'] / (props_df['perimeter']**2)
    props_df['roundness'] = 4 * props_df['area'] / (np.pi * props_df['major_axis_length']**2)

    return binary, labels, props_df

Morphological Operations

Fill Holes (Interior Gaps)

from skimage.morphology import binary_fill_holes

# Fill all holes
binary_filled = binary_fill_holes(binary)

# Fill only small holes
from skimage.morphology import remove_small_holes
binary_filled = remove_small_holes(binary, area_threshold=200)

Remove Small Objects

from skimage.morphology import remove_small_objects

# Remove objects smaller than threshold
binary_clean = remove_small_objects(binary, min_size=500)

Opening (Remove Protrusions)

from skimage.morphology import binary_opening, disk

# Remove small protrusions and thin connections
binary_opened = binary_opening(binary, disk(3))

Closing (Fill Gaps)

from skimage.morphology import binary_closing, disk

# Close small gaps between objects
binary_closed = binary_closing(binary, disk(5))

Complete Cleanup Pipeline

def morphological_cleanup(binary, min_area=500, opening_radius=3, closing_radius=5):
    """Apply morphological operations to clean up binary mask.

    Args:
        binary: binary mask
        min_area: minimum object area to keep
        opening_radius: radius for opening operation
        closing_radius: radius for closing operation

    Returns: Cleaned binary mask
    """
    # Remove small objects
    cleaned = morphology.remove_small_objects(binary, min_size=min_area)

    # Opening to remove protrusions
    if opening_radius > 0:
        cleaned = morphology.binary_opening(cleaned, morphology.disk(opening_radius))

    # Closing to fill gaps
    if closing_radius > 0:
        cleaned = morphology.binary_closing(cleaned, morphology.disk(closing_radius))

    # Fill holes
    cleaned = morphology.binary_fill_holes(cleaned)

    return cleaned

Measurement and Analysis

Complete Colony Measurement

def measure_colonies(image, threshold_method='otsu', min_area=500):
    """Segment and measure colonies from image.

    Returns: DataFrame with Area, Circularity, Roundness, Perimeter per colony
    """
    # Convert to grayscale
    if image.ndim == 3:
        from skimage.color import rgb2gray
        gray = rgb2gray(image)
    else:
        gray = image.astype(float)

    # Threshold
    if threshold_method == 'otsu':
        thresh = filters.threshold_otsu(gray)
    elif threshold_method == 'li':
        thresh = filters.threshold_li(gray)
    elif threshold_method == 'triangle':
        thresh = filters.threshold_triangle(gray)
    else:
        thresh = threshold_method  # numeric value

    binary = gray > thresh

    # Clean up
    binary = morphology.remove_small_objects(binary, min_size=min_area)
    binary = morphology.binary_fill_holes(binary)

    # Label and measure
    labels = measure.label(binary)
    props = measure.regionprops_table(labels, properties=[
        'area', 'perimeter', 'eccentricity', 'solidity',
        'major_axis_length', 'minor_axis_length'
    ])

    results = pd.DataFrame(props)

    # Calculate shape metrics
    results['circularity'] = 4 * np.pi * results['area'] / (results['perimeter']**2)
    results['roundness'] = 4 * results['area'] / (np.pi * results['major_axis_length']**2)

    # Rename to match ImageJ/CellProfiler conventions
    results = results.rename(columns={
        'area': 'Area',
        'perimeter': 'Perimeter',
        'circularity': 'Circularity',
        'roundness': 'Round',
        'eccentricity': 'Eccentricity',
        'solidity': 'Solidity'
    })

    return results

Calibrated Measurements

def measure_colonies_calibrated(image, pixel_size_um, threshold_method='otsu', min_area_um2=0.1):
    """Measure colonies with calibrated units.

    Args:
        image: numpy array
        pixel_size_um: size of one pixel in micrometers (e.g., 0.65 for 0.65 µm/pixel)
        threshold_method: 'otsu', 'li', or numeric
        min_area_um2: minimum area in mm² (1 mm² = 1,000,000 µm²)

    Returns: DataFrame with calibrated measurements
    """
    # Convert min_area to pixels
    min_area_pixels = int(min_area_um2 * 1e6 / (pixel_size_um**2))

    # Segment and measure in pixels
    results = measure_colonies(image, threshold_method, min_area_pixels)

    # Convert to calibrated units
    results['Area_um2'] = results['Area'] * (pixel_size_um**2)
    results['Area_mm2'] = results['Area_um2'] / 1e6
    results['Perimeter_um'] = results['Perimeter'] * pixel_size_um

    # Circularity and roundness are dimensionless (no conversion needed)

    return results

Swarming Assay Analysis

Complete Swarming Assay Workflow

def analyze_swarming_plate(image_path, genotype, replicate,
                          pixel_size_um=1.0, min_area_mm2=0.5):
    """Complete analysis of bacterial swarming plate.

    Args:
        image_path: path to image file
        genotype: genotype label
        replicate: replicate number
        pixel_size_um: pixel size in micrometers
        min_area_mm2: minimum colony area in mm²

    Returns: DataFrame with measurements for this plate
    """
    import tifffile
    import os

    # Load image
    image = tifffile.imread(image_path)

    # Measure colonies
    results = measure_colonies_calibrated(
        image,
        pixel_size_um=pixel_size_um,
        threshold_method='otsu',
        min_area_um2=min_area_mm2 * 1e6  # Convert mm² to µm²
    )

    # Add metadata
    results['Genotype'] = genotype
    results['Replicate'] = replicate
    results['Image'] = os.path.basename(image_path)

    return results


# Example: Batch process multiple plates
def batch_analyze_swarming(image_info_df, pixel_size_um=1.0):
    """Analyze multiple swarming plates.

    Args:
        image_info_df: DataFrame with columns: 'image_path', 'genotype', 'replicate'
        pixel_size_um: pixel size

    Returns: Combined DataFrame with all measurements
    """
    all_results = []

    for idx, row in image_info_df.iterrows():
        results = analyze_swarming_plate(
            row['image_path'],
            row['genotype'],
            row['replicate'],
            pixel_size_um=pixel_size_um
        )
        all_results.append(results)

    combined = pd.concat(all_results, ignore_index=True)
    return combined

Colony Morphometry Statistics

def colony_morphometry_analysis(df, genotype_col='Genotype',
                               area_col='Area', circ_col='Circularity'):
    """Full colony morphometry analysis for swarming assays.

    Args:
        df: DataFrame with colony measurements
        genotype_col: column with genotype labels
        area_col: column with area measurements
        circ_col: column with circularity measurements

    Returns: dict with per-genotype summaries and max-area genotype info
    """
    # Group statistics for area
    area_summary = df.groupby(genotype_col)[area_col].agg(
        Mean='mean',
        SD='std',
        Median='median',
        Min='min',
        Max='max',
        N='count'
    ).reset_index()
    area_summary['SEM'] = area_summary['SD'] / np.sqrt(area_summary['N'])

    # Group statistics for circularity
    circ_summary = df.groupby(genotype_col)[circ_col].agg(
        Mean='mean',
        SD='std',
        Median='median',
        N='count'
    ).reset_index()
    circ_summary['SEM'] = circ_summary['SD'] / np.sqrt(circ_summary['N'])

    # Merge area and circularity summaries
    merged = area_summary.merge(
        circ_summary, on=genotype_col, suffixes=('_Area', '_Circ')
    )

    # Find genotype with largest mean area
    max_area_idx = merged['Mean_Area'].idxmax()
    max_area_genotype = merged.loc[max_area_idx, genotype_col]
    max_area_circularity = merged.loc[max_area_idx, 'Mean_Circ']

    return {
        'area_summary': area_summary,
        'circ_summary': circ_summary,
        'merged_summary': merged,
        'max_area_genotype': max_area_genotype,
        'max_area_circularity': max_area_circularity,
    }


# Example usage (colony morphometry)
df = pd.read_csv("Swarm_1.csv")
result = colony_morphometry_analysis(df, 'Genotype', 'Area', 'Circularity')
print(f"Genotype with largest area: {result['max_area_genotype']}")
print(f"Mean circularity: {result['max_area_circularity']:.4f}")

Time-Lapse Analysis

Track Colony Growth Over Time

def analyze_timelapse(image_paths, timepoints_hours):
    """Analyze colony growth from time-lapse images.

    Args:
        image_paths: list of image file paths (sorted by time)
        timepoints_hours: list of timepoint values in hours

    Returns: DataFrame with area vs time for each colony
    """
    import tifffile

    growth_data = []

    for i, (img_path, time_hr) in enumerate(zip(image_paths, timepoints_hours)):
        # Load and measure
        img = tifffile.imread(img_path)
        results = measure_colonies(img, threshold_method='otsu', min_area=100)

        # Add time information
        results['timepoint'] = i
        results['time_hours'] = time_hr

        growth_data.append(results)

    # Combine all timepoints
    combined = pd.concat(growth_data, ignore_index=True)

    return combined


# Example: Plot growth curves
import matplotlib.pyplot as plt

def plot_growth_curves(growth_df, colony_labels=None):
    """Plot colony area vs time.

    Args:
        growth_df: DataFrame from analyze_timelapse
        colony_labels: Optional list of colony IDs to plot
    """
    if colony_labels is None:
        # Plot mean area over time
        summary = growth_df.groupby('time_hours')['Area'].agg(['mean', 'std'])
        plt.errorbar(summary.index, summary['mean'], yerr=summary['std'],
                    marker='o', capsize=5)
        plt.xlabel('Time (hours)')
        plt.ylabel('Mean Colony Area (pixels)')
        plt.title('Colony Growth Over Time')
    else:
        # Plot individual colonies
        for colony_id in colony_labels:
            colony_data = growth_df[growth_df['label'] == colony_id]
            plt.plot(colony_data['time_hours'], colony_data['Area'],
                    marker='o', label=f'Colony {colony_id}')
        plt.xlabel('Time (hours)')
        plt.ylabel('Colony Area (pixels)')
        plt.title('Individual Colony Growth')
        plt.legend()

    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.show()

Troubleshooting

Common Issues

Multiple colonies merged into one:

  • Use watershed segmentation (see cell_counting.md)
  • Increase erosion before labeling
  • Try more conservative threshold (Li instead of Otsu)

Background detected as colony:

  • Increase min_area threshold
  • Apply morphological opening
  • Use adaptive threshold for uneven backgrounds

Colony edges jagged:

  • Apply Gaussian smoothing before thresholding
  • Use morphological closing
  • Increase binary dilation

Small colonies missed:

  • Use Li or Triangle threshold (more sensitive)
  • Reduce min_area threshold
  • Enhance contrast before segmentation

Uneven illumination affects segmentation:

  • Use adaptive thresholding
  • Apply background subtraction first
  • Use rolling ball background correction

Threshold Method Selection Guide

Image Type Best Method Notes
High contrast, even illumination Otsu Fast, reliable
Low contrast Li More sensitive
Gradient background Adaptive Block size = ~1/10 image width
Dark background Otsu or Li Colony brighter than background
Bright background Otsu or Li Invert binary result
Multiple colonies, well-separated Otsu + cleanup Simple and effective
Touching colonies Watershed See cell_counting.md

Parameter Recommendations

Bacterial Swarming Plates

# Typical parameters for Petri dish images (100mm plates)
params = {
    'threshold_method': 'otsu',
    'min_area': 500,  # pixels (adjust based on magnification)
    'opening_radius': 3,
    'closing_radius': 5,
}

Biofilm Analysis

# Biofilms often have irregular edges
params = {
    'threshold_method': 'li',  # More sensitive
    'min_area': 1000,
    'opening_radius': 5,  # Stronger cleanup
    'closing_radius': 8,
}

Fungal Colonies

# Fungi can have very irregular shapes
params = {
    'threshold_method': 'adaptive',
    'block_size': 51,
    'min_area': 2000,
    'opening_radius': 0,  # Preserve edges
    'closing_radius': 10,
}