650 lines
18 KiB
Markdown
650 lines
18 KiB
Markdown
---
|
||
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,
|
||
}
|
||
```
|