Segmentation inspection


Segmentation quality should be inspected both visually and quantitatively. In this module we compare segmentation outputs against a reference segmentation using a minimal and practical set of checks that are easy to apply in teaching and in daily analysis workflows.


Prerequisites

Before starting this lesson, you should be familiar with:

Learning Objectives

After completing this lesson, learners should be able to:
  • Visually inspect segmentation quality by overlaying segmentations on the raw image

  • Compute IoU for semantic segmentation by converting label masks to binary masks

  • Compare instance segmentations using object counts and mean object area

Concept map

graph TD R("Raw image") --> V("Visual inspection") L("Label masks (incl. ground truth)") --> V L --> S("Semantic comparison") S --> M("IoU") L --> I("Instance comparison") I --> C("Object count and features")

Figure


Intensity image and label mask images obtained at different intensity thresholds.



Activities

Visual inspection by overlaying segmentations on the raw image


Show activity for:  

ImageJ GUI

Visual inspection by overlaying segmentations on the raw image

skimage napari

# %%
# Visual inspection of segmentation overlays

# %%
# Create a napari viewer
import napari

viewer = napari.Viewer()

# %%
# Load raw and segmentation images
from OpenIJTIFF import open_ij_tiff

raw_url = "https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit__mitocheck_incenp_t1.tif"
gt_url = "https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit_labels__mitocheck_incenp_t1.tif"
low_url = "https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit_labels__mitocheck_incenp_t1_low_threshold.tif"
high_url = "https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit_labels__mitocheck_incenp_t1_high_threshold.tif"

raw, *_ = open_ij_tiff(raw_url)
gt_labels, *_ = open_ij_tiff(gt_url)
low_labels, *_ = open_ij_tiff(low_url)
high_labels, *_ = open_ij_tiff(high_url)

# %%
# Overlay all segmentations on top of the raw image
viewer.add_image(raw, name="raw")
viewer.add_labels(gt_labels, name="ground_truth", opacity=0.45)
viewer.add_labels(low_labels, name="low_threshold", opacity=0.45)
viewer.add_labels(high_labels, name="high_threshold", opacity=0.45)

# %%
# Close viewer at the end so the script can run in automated tests
viewer.close()



Semantic comparison using IoU


Show activity for:  

ImageJ GUI

Semantic comparison with IoU in ImageJ GUI

Prepare binary masks (foreground > 0)

For each image (reference and candidate):

  • Convert to 8-bit using [Image > Type > 8-bit] if needed
  • Set threshold using [Image > Adjust > Threshold…]
    • Lower threshold: 1
    • Upper threshold: maximal value
  • Press Apply to create a binary image

Compute IoU for one candidate mask

Let Ref_bin be the reference binary image and Cand_bin be one candidate binary image.

  • Compute intersection with [Process > Image Calculator…]
    • Operation: AND
    • Image1: Ref_bin
    • Image2: Cand_bin
  • Compute union with [Process > Image Calculator…]
    • Operation: OR
    • Image1: Ref_bin
    • Image2: Cand_bin
  • Count foreground pixels (value 255) in both images using [Analyze > Histogram]
    • N_intersection: count at value 255 in the AND image
    • N_union: count at value 255 in the OR image
  • Compute IoU:
    • IoU = N_intersection / N_union

Repeat this for both low-threshold and high-threshold candidates and compare their IoU values.

skimage napari

# %%
# Semantic segmentation comparison using IoU

# %%
# Load segmentation label masks
import numpy as np
from OpenIJTIFF import open_ij_tiff

gt_url = "https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit_labels__mitocheck_incenp_t1.tif"
low_url = "https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit_labels__mitocheck_incenp_t1_low_threshold.tif"
high_url = "https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit_labels__mitocheck_incenp_t1_high_threshold.tif"

gt_labels, *_ = open_ij_tiff(gt_url)
low_labels, *_ = open_ij_tiff(low_url)
high_labels, *_ = open_ij_tiff(high_url)

# %%
# Convert label masks to binary masks for semantic comparison
# Foreground is any pixel with value > 0.
gt_binary = np.asarray(gt_labels) > 0
low_binary = np.asarray(low_labels) > 0
high_binary = np.asarray(high_labels) > 0

# %%
# Compute intersection-over-union (IoU)
def compute_iou(reference_mask, candidate_mask):
    intersection = np.logical_and(reference_mask, candidate_mask).sum()
    union = np.logical_or(reference_mask, candidate_mask).sum()
    return float(intersection / union) if union > 0 else 0.0


low_iou = compute_iou(gt_binary, low_binary)
high_iou = compute_iou(gt_binary, high_binary)

print(f"IoU (low threshold vs ground truth):  {low_iou:.4f}")
print(f"IoU (high threshold vs ground truth): {high_iou:.4f}")



Instance comparison using object count and mean object area


Show activity for:  

ImageJ GUI

Instance comparison using object count and mean object area

For each image:

  • If needed, convert to binary mask using [Image > Adjust > Threshold…] and Apply
  • Set measurements using [Analyze > Set Measurements…]
    • Select Area
    • Select Display Label
  • Run connected components and measurements using [Analyze > Analyze Particles…]
    • Size: 0-Infinity
    • Circularity: 0.00-1.00
    • Select Display results and Summarize
  • Record from the Summary table:
    • Count
    • Average Size

Compare Count and Average Size for low-threshold and high-threshold segmentations against the reference.

skimage napari

# %%
# Instance segmentation comparison: object counts and mean object area

# %%
# Load segmentation label masks
import numpy as np
from skimage.measure import label, regionprops_table
from OpenIJTIFF import open_ij_tiff

gt_url = "https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit_labels__mitocheck_incenp_t1.tif"
low_url = "https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit_labels__mitocheck_incenp_t1_low_threshold.tif"
high_url = "https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit_labels__mitocheck_incenp_t1_high_threshold.tif"

gt_labels, *_ = open_ij_tiff(gt_url)
low_labels, *_ = open_ij_tiff(low_url)
high_labels, *_ = open_ij_tiff(high_url)

# %%
# Convert to connected-component instances and compute summary statistics
def instance_stats(label_like_image):
    binary = np.asarray(label_like_image) > 0
    instance_labels = label(binary)
    object_count = int(instance_labels.max())

    if object_count == 0:
        return object_count, 0.0

    props = regionprops_table(instance_labels, properties=("area",))
    mean_area = float(np.mean(props["area"]))
    return object_count, mean_area


gt_count, gt_mean_area = instance_stats(gt_labels)
low_count, low_mean_area = instance_stats(low_labels)
high_count, high_mean_area = instance_stats(high_labels)

# %%
# Print direct comparison against the reference segmentation
print("Reference (ground truth):")
print(f"  object count   = {gt_count}")
print(f"  mean area      = {gt_mean_area:.2f}")
print()
print("Low-threshold segmentation:")
print(f"  object count   = {low_count} (difference: {low_count - gt_count:+d})")
print(f"  mean area      = {low_mean_area:.2f} (difference: {low_mean_area - gt_mean_area:+.2f})")
print()
print("High-threshold segmentation:")
print(f"  object count   = {high_count} (difference: {high_count - gt_count:+d})")
print(f"  mean area      = {high_mean_area:.2f} (difference: {high_mean_area - gt_mean_area:+.2f})")






Assessment

Fill in the blanks

Solution

  • union
  • binary masks




Follow-up material

Recommended follow-up modules:

Learn more: