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

650 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Colony and Object Segmentation"
task: ""
lineage_type: import
upstream_source: https://github.com/mims-harvard/ToolUniverse/blob/e2520a96/skills/tooluniverse-image-analysis/references/segmentation.md
upstream_sha: e2520a96
imported_at: 2026-06-26
prompt_class: prompt
upstream_changes: accepted
author: upstream
validated: 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](#colony-morphometry-basics)
2. [Threshold Methods](#threshold-methods)
3. [Morphological Operations](#morphological-operations)
4. [Measurement and Analysis](#measurement-and-analysis)
5. [Swarming Assay Analysis](#swarming-assay-analysis)
6. [Time-Lapse Analysis](#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)
```python
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)
```python
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)
```python
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)
```python
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
```python
from skimage.morphology import remove_small_objects
# Remove objects smaller than threshold
binary_clean = remove_small_objects(binary, min_size=500)
```
### Opening (Remove Protrusions)
```python
from skimage.morphology import binary_opening, disk
# Remove small protrusions and thin connections
binary_opened = binary_opening(binary, disk(3))
```
### Closing (Fill Gaps)
```python
from skimage.morphology import binary_closing, disk
# Close small gaps between objects
binary_closed = binary_closing(binary, disk(5))
```
### Complete Cleanup Pipeline
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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
```python
# 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
```python
# Biofilms often have irregular edges
params = {
'threshold_method': 'li', # More sensitive
'min_area': 1000,
'opening_radius': 5, # Stronger cleanup
'closing_radius': 8,
}
```
### Fungal Colonies
```python
# Fungi can have very irregular shapes
params = {
'threshold_method': 'adaptive',
'block_size': 51,
'min_area': 2000,
'opening_radius': 0, # Preserve edges
'closing_radius': 10,
}
```