Coherence Factor for Ultrasound Beamforming#

This example demonstrates different coherence factor formulations and their applications in ultrasound imaging. Coherence factors measure the degree to which signals from different array elements are in phase, providing quality metrics for adaptive beamforming and aberration correction.

We’ll explore:

  • Different coherence scenarios (perfect to severely aberrated)

  • Multiple coherence factor types (Standard, Generalized, Sign, Phase)

  • Parameter effects and practical considerations

This example demonstrates coherence factor calculations for beamforming quality assessment [1], with applications to aberration correction [2]. We compare different formulations including the generalized coherence factor [3] and phase coherence methods [4].

References#

Example#

# Note: it would probably be interesting to use a PICMUS dataset with a beamformer
# at some point.

1. Import required modules and functions#

import matplotlib

matplotlib.use("Agg")  # Use non-interactive backend
import matplotlib.pyplot as plt
import numpy as np
from scipy import ndimage

from ultrasound_metrics.metrics.coherence import (
    coherence_factor,
    generalized_coherence_factor,
    phase_coherence_factor,
    sign_coherence_factor,
)

2. Setup: Simulate realistic ultrasound beamforming scenarios#

# Define image dimensions and number of receive elements
height, width = 128, 96
n_elements = 32

# Create a base complex-valued signal pattern with point targets and speckle
np.random.seed(42)  # For reproducible results
base_signal = np.random.randn(height, width) + 1j * np.random.randn(height, width)
base_signal = base_signal.astype(np.complex64)

# Add some structured features (simulating point targets and speckle)
# Point target in upper region (high coherence expected)
base_signal[30:35, 45:50] += 5.0 + 5.0j

# Speckle pattern in middle region
y_speckle, x_speckle = np.mgrid[50:80, 20:75]
speckle_pattern = np.exp(1j * (x_speckle * 0.3 + y_speckle * 0.2))
base_signal[50:80, 20:75] += 2.0 * speckle_pattern

3. Create scenarios with different coherence levels#

# These scenarios simulate different imaging conditions from ideal focusing
# to severe aberrations, showing how coherence factors respond to signal quality.

# Scenario 1: Perfect coherence (all elements receive identical signal)
beamsum_perfect = np.tile(base_signal[None, :, :], (n_elements, 1, 1))
coherence_perfect = coherence_factor(beamsum_perfect)

# Scenario 2: High coherence with small phase variations (good focusing)
beamsum_high = np.zeros((n_elements, height, width), dtype=np.complex64)
for i in range(n_elements):
    # Small random phase shifts (simulating good focusing)
    phase_noise = np.random.normal(0, 0.1, (height, width))  # Small phase variations
    beamsum_high[i] = base_signal * np.exp(1j * phase_noise)

coherence_high = coherence_factor(beamsum_high)

# Scenario 3: Medium coherence with moderate aberrations
beamsum_medium = np.zeros((n_elements, height, width), dtype=np.complex64)
for i in range(n_elements):
    # Moderate phase aberrations
    phase_aberration = np.random.normal(0, 0.5, (height, width))
    amplitude_variation = 1.0 + np.random.normal(0, 0.2, (height, width))
    beamsum_medium[i] = base_signal * amplitude_variation * np.exp(1j * phase_aberration)

coherence_medium = coherence_factor(beamsum_medium)

# Scenario 4: Low coherence (strong aberrations/noise)
beamsum_low = np.zeros((n_elements, height, width), dtype=np.complex64)
for i in range(n_elements):
    # Strong phase aberrations and amplitude variations
    phase_aberration = np.random.uniform(-np.pi, np.pi, (height, width))
    amplitude_variation = np.random.uniform(0.2, 1.8, (height, width))
    noise = 0.5 * (np.random.randn(height, width) + 1j * np.random.randn(height, width))
    beamsum_low[i] = base_signal * amplitude_variation * np.exp(1j * phase_aberration) + noise

coherence_low = coherence_factor(beamsum_low)

4. Visualize the coherence maps#

fig, axes = plt.subplots(2, 4, figsize=(16, 8))

# Define extent for proper axis scaling (simulate realistic ultrasound coordinates)
lateral_extent = [-width / 2 * 0.1, width / 2 * 0.1]  # mm
axial_extent = [0, height * 0.1]  # mm
extent = [lateral_extent[0], lateral_extent[1], axial_extent[1], axial_extent[0]]

scenarios = [
    (beamsum_perfect, coherence_perfect, "Perfect Coherence"),
    (beamsum_high, coherence_high, "High Coherence\n(Good Focusing)"),
    (beamsum_medium, coherence_medium, "Medium Coherence\n(Moderate Aberrations)"),
    (beamsum_low, coherence_low, "Low Coherence\n(Strong Aberrations)"),
]

for idx, (beamsum, coherence, title) in enumerate(scenarios):
    # Top row: Magnitude of summed beamforming result
    summed_magnitude = np.abs(np.sum(beamsum, axis=0))

    im1 = axes[0, idx].imshow(summed_magnitude, cmap="gray", extent=extent)
    axes[0, idx].set_title(f"{title}\nMagnitude Image")
    axes[0, idx].set_xlabel("Lateral (mm)")
    if idx == 0:
        axes[0, idx].set_ylabel("Axial (mm)")
    plt.colorbar(im1, ax=axes[0, idx], shrink=0.8)

    # Bottom row: Coherence factor
    im2 = axes[1, idx].imshow(coherence, cmap="viridis", vmin=0, vmax=1, extent=extent)
    axes[1, idx].set_title(f"Coherence Factor\nMean: {coherence.mean():.3f}")
    axes[1, idx].set_xlabel("Lateral (mm)")
    if idx == 0:
        axes[1, idx].set_ylabel("Axial (mm)")
    plt.colorbar(im2, ax=axes[1, idx], shrink=0.8, label="Coherence")

plt.tight_layout()
plt.show()
Perfect Coherence Magnitude Image, High Coherence (Good Focusing) Magnitude Image, Medium Coherence (Moderate Aberrations) Magnitude Image, Low Coherence (Strong Aberrations) Magnitude Image, Coherence Factor Mean: 1.000, Coherence Factor Mean: 0.990, Coherence Factor Mean: 0.756, Coherence Factor Mean: 0.032

5. Analyze coherence distribution across scenarios#

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Histogram of coherence values
coherence_data = [
    (coherence_perfect.flatten(), "Perfect", "red"),
    (coherence_high.flatten(), "High", "orange"),
    (coherence_medium.flatten(), "Medium", "green"),
    (coherence_low.flatten(), "Low", "blue"),
]

for coherence_vals, label, color in coherence_data:
    # Use consistent binning across all datasets for fair comparison
    axes[0].hist(coherence_vals, bins=50, alpha=0.7, label=label, color=color, range=(0, 1), density=False)

axes[0].set_xlabel("Coherence Factor")
axes[0].set_ylabel("Count")
axes[0].set_title("Distribution of Coherence Values")
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Box plot comparison
coherence_values = [
    coherence_perfect.flatten(),
    coherence_high.flatten(),
    coherence_medium.flatten(),
    coherence_low.flatten(),
]
labels = ["Perfect", "High", "Medium", "Low"]

axes[1].boxplot(coherence_values, tick_labels=labels)
axes[1].set_ylabel("Coherence Factor")
axes[1].set_title("Coherence Factor Distribution by Scenario")
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
Distribution of Coherence Values, Coherence Factor Distribution by Scenario

6. Compare magnitude-based vs power-based coherence formulations#

Two mathematical formulations exist in the literature:

  • Magnitude-based: CF = \|Σ s_i\| / Σ \|s_i\| (original formulation)

  • Power-based: CF = \|Σ s_i\|² / (M * Σ \|s_i\|²) (vbeam library)

# Test with perfect coherence scenario
coherence_perfect_mag = coherence_factor(beamsum_perfect, power_based=False)
coherence_perfect_pow = coherence_factor(beamsum_perfect, power_based=True)

# Test with low coherence scenario
coherence_low_mag = coherence_factor(beamsum_low, power_based=False)
coherence_low_pow = coherence_factor(beamsum_low, power_based=True)

# Create comparison visualization
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# Perfect coherence comparison
im00 = axes[0, 0].imshow(coherence_perfect_mag, cmap="viridis", vmin=0, vmax=1, extent=extent)
axes[0, 0].set_title(f"Perfect Coherence (Magnitude)\nMean: {coherence_perfect_mag.mean():.6f}")
axes[0, 0].set_xlabel("Lateral (mm)")
axes[0, 0].set_ylabel("Axial (mm)")
plt.colorbar(im00, ax=axes[0, 0], label="coherence")

im01 = axes[0, 1].imshow(coherence_perfect_pow, cmap="viridis", vmin=0, vmax=1, extent=extent)
axes[0, 1].set_title(f"Perfect Coherence (Power)\nMean: {coherence_perfect_pow.mean():.6f}")
axes[0, 1].set_xlabel("Lateral (mm)")
axes[0, 1].set_ylabel("Axial (mm)")
plt.colorbar(im01, ax=axes[0, 1], label="coherence")

# Low coherence comparison
im10 = axes[1, 0].imshow(coherence_low_mag, cmap="viridis", vmin=0, vmax=1, extent=extent)
axes[1, 0].set_title(f"Low Coherence (Magnitude)\nMean: {coherence_low_mag.mean():.6f}")
axes[1, 0].set_xlabel("Lateral (mm)")
axes[1, 0].set_ylabel("Axial (mm)")
plt.colorbar(im10, ax=axes[1, 0], label="coherence")

im11 = axes[1, 1].imshow(coherence_low_pow, cmap="viridis", vmin=0, vmax=1, extent=extent)
axes[1, 1].set_title(f"Low Coherence (Power)\nMean: {coherence_low_pow.mean():.6f}")
axes[1, 1].set_xlabel("Lateral (mm)")
axes[1, 1].set_ylabel("Axial (mm)")
plt.colorbar(im11, ax=axes[1, 1], label="coherence")

plt.tight_layout()
plt.show()
Perfect Coherence (Magnitude) Mean: 1.000000, Perfect Coherence (Power) Mean: 1.000000, Low Coherence (Magnitude) Mean: 0.175807, Low Coherence (Power) Mean: 0.031737

7. Individual Channel Visualization (Before Beamforming)#

Individual channel visualization shows magnitude, phase, and real components before beamforming to understand signal relationships

# Select a few channels to visualize (channels 5, 15, 25)
channels_to_show = [5, 15, 25]
channel_labels = [f"Channel {ch}" for ch in channels_to_show]

# Use medium coherence data for demonstration
selected_beamsum = beamsum_medium

fig, axes = plt.subplots(3, len(channels_to_show), figsize=(15, 12))

for i, ch in enumerate(channels_to_show):
    channel_data = selected_beamsum[ch]  # Shape: (height, width)

    # Top row: Magnitude images
    magnitude = np.abs(channel_data)
    im1 = axes[0, i].imshow(magnitude, cmap="gray", extent=extent)
    axes[0, i].set_title(f"{channel_labels[i]}\nMagnitude")
    axes[0, i].set_xlabel("Lateral (mm)")
    if i == 0:
        axes[0, i].set_ylabel("Axial (mm)")
    plt.colorbar(im1, ax=axes[0, i], shrink=0.8)

    # Middle row: Phase images (using circular colormap)
    phase = np.angle(channel_data)
    im2 = axes[1, i].imshow(phase, cmap="hsv", vmin=-np.pi, vmax=np.pi, extent=extent)
    axes[1, i].set_title("Phase (radians)")
    axes[1, i].set_xlabel("Lateral (mm)")
    if i == 0:
        axes[1, i].set_ylabel("Axial (mm)")
    plt.colorbar(im2, ax=axes[1, i], shrink=0.8)

    # Bottom row: Real part
    real_part = np.real(channel_data)
    im3 = axes[2, i].imshow(real_part, cmap="RdBu_r", extent=extent)
    axes[2, i].set_title("Real Part")
    axes[2, i].set_xlabel("Lateral (mm)")
    if i == 0:
        axes[2, i].set_ylabel("Axial (mm)")
    plt.colorbar(im3, ax=axes[2, i], shrink=0.8)

plt.suptitle("Individual Channel Data (Before Beamforming)", fontsize=16)
plt.tight_layout()
plt.show()
Individual Channel Data (Before Beamforming), Channel 5 Magnitude, Channel 15 Magnitude, Channel 25 Magnitude, Phase (radians), Phase (radians), Phase (radians), Real Part, Real Part, Real Part

8. Delay-and-Sum Beamforming vs Coherence Factors#

Compare DAS beamformed image with coherence quality map

# Compute delay-and-sum (DAS) beamformed image
das_image = np.sum(selected_beamsum, axis=0)
das_magnitude = np.abs(das_image)

# Compute standard coherence factor
cf_standard = coherence_factor(selected_beamsum, power_based=False)

fig, axes = plt.subplots(1, 2, figsize=(12, 6))

# DAS magnitude image
im1 = axes[0].imshow(das_magnitude, cmap="gray", extent=extent)
axes[0].set_title("Delay-and-Sum Beamformed Image\n(Sum of all channels)")
axes[0].set_xlabel("Lateral (mm)")
axes[0].set_ylabel("Axial (mm)")
plt.colorbar(im1, ax=axes[0], shrink=0.8)

# Coherence factor
im2 = axes[1].imshow(cf_standard, cmap="viridis", vmin=0, vmax=1, extent=extent)
axes[1].set_title("Standard Coherence Factor\n(Quality Map)")
axes[1].set_xlabel("Lateral (mm)")
axes[1].set_ylabel("Axial (mm)")
plt.colorbar(im2, ax=axes[1], shrink=0.8, label="Coherence")

plt.tight_layout()
plt.show()
Delay-and-Sum Beamformed Image (Sum of all channels), Standard Coherence Factor (Quality Map)

9. Comparison of Different Coherence Factor Types#

Compare all coherence factor types: Standard, Generalized, Sign, and Phase

# Compute all coherence factor types
cf_magnitude = coherence_factor(selected_beamsum, power_based=False)
cf_power = coherence_factor(selected_beamsum, power_based=True)
gcf = generalized_coherence_factor(selected_beamsum)  # Optimal for diffuse scatterers
scf = sign_coherence_factor(selected_beamsum, power=1.0)
pcf = phase_coherence_factor(selected_beamsum, power=1.0)

# Create comparison visualization
coherence_types = [
    (cf_magnitude, "Standard CF\n(Magnitude-based)", "Standard coherence factor using magnitude ratios"),
    (cf_power, "Standard CF\n(Power-based)", "Power-based coherence factor (vbeam-compatible)"),
    (gcf, "Generalized CF\n(GCF)", "Uses spatial DFT for robustness in speckle regions"),
    (scf, "Sign CF\n(SCF)", "Uses only signal polarity - hardware efficient"),
    (pcf, "Phase CF\n(PCF)", "Based on phase coherence - amplitude independent"),
]

fig, axes = plt.subplots(2, 3, figsize=(18, 12))
axes = axes.flatten()

for i, (cf_data, title, _description) in enumerate(coherence_types):
    im = axes[i].imshow(cf_data, cmap="viridis", vmin=0, vmax=1, extent=extent)
    axes[i].set_title(f"{title}\nMean: {cf_data.mean():.3f}")
    axes[i].set_xlabel("Lateral (mm)")
    axes[i].set_ylabel("Axial (mm)")
    plt.colorbar(im, ax=axes[i], shrink=0.8, label="Coherence")

# Remove the last subplot (we only have 5 coherence types)
axes[5].remove()

plt.suptitle("Comparison of Different Coherence Factor Types", fontsize=16)
plt.tight_layout()
plt.show()
Comparison of Different Coherence Factor Types, Standard CF (Magnitude-based) Mean: 0.886, Standard CF (Power-based) Mean: 0.756, Generalized CF (GCF) Mean: 0.772, Sign CF (SCF) Mean: 0.543, Phase CF (PCF) Mean: 0.886

10. Quantitative Comparison of Coherence Factors#

Quantitative comparison of coherence factor statistics

# Compute statistics for each coherence factor type
cf_stats = []
for cf_data, name, _ in coherence_types:
    stats = {
        "name": name,
        "mean": float(cf_data.mean()),
        "std": float(cf_data.std()),
        "min": float(cf_data.min()),
        "max": float(cf_data.max()),
        "median": float(np.median(cf_data)),
    }
    cf_stats.append(stats)


# Create histograms of coherence values
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
axes = axes.flatten()

colors = ["red", "orange", "green", "blue", "purple"]
for i, ((cf_data, name, _), color) in enumerate(zip(coherence_types, colors)):
    axes[i].hist(cf_data.flatten(), bins=50, alpha=0.7, color=color, density=True)
    axes[i].set_xlabel("Coherence Value")
    axes[i].set_ylabel("Density")
    axes[i].set_title(name)
    axes[i].grid(True, alpha=0.3)
    axes[i].set_xlim(0, 1)

# Remove the last subplot
axes[5].remove()

plt.suptitle("Distribution of Coherence Values by Type", fontsize=16)
plt.tight_layout()
plt.show()
Distribution of Coherence Values by Type, Standard CF (Magnitude-based), Standard CF (Power-based), Generalized CF (GCF), Sign CF (SCF), Phase CF (PCF)

11. GCF Parameter M0 Analysis#

Demonstrate the effect of the M0 parameter in Generalized Coherence Factor This is the spatial-frequency (i.e. field-of-view) cutoff for the GCF.

# Demonstrate the effect of different M0 values as discussed in Li & Li (2003)
# M0 controls the width of the low-frequency region used for GCF calculation
# Reduce to 3 values for cleaner 1x3 layout
m0_values = [0, 1, 2]
gcf_m0_results = []

for m0 in m0_values:
    try:
        gcf_m0 = generalized_coherence_factor(selected_beamsum, m0=m0)
        gcf_m0_results.append((gcf_m0, m0))
        mean_val = gcf_m0.mean()
        std_val = gcf_m0.std()
        print(f"   M0={m0}: Mean={mean_val:.3f}, Std={std_val:.3f}")
    except ValueError as e:
        print(f"   M0={m0}: {e}")
        gcf_m0_results.append((None, m0))

# Visualize different M0 values
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

for i, (gcf_data, m0) in enumerate(gcf_m0_results):
    if gcf_data is not None:
        im = axes[i].imshow(gcf_data, cmap="viridis", vmin=0, vmax=1, extent=extent)
        axes[i].set_title(f"GCF with M0={m0}\nMean: {gcf_data.mean():.3f} ± {gcf_data.std():.3f}")
        axes[i].set_xlabel("Azimuth (mm)")
        axes[i].set_ylabel("Range (mm)")
        plt.colorbar(im, ax=axes[i], fraction=0.046, pad=0.04)
    else:
        axes[i].text(0.5, 0.5, f"M0={m0}\nInvalid", ha="center", va="center", transform=axes[i].transAxes, fontsize=14)
        axes[i].set_title(f"GCF with M0={m0} (Invalid)")
        axes[i].axis("off")

plt.suptitle(
    "Effect of GCF Cutoff Frequency M0\n"
    + "M0=0: Point targets only | M0=1: Optimal for diffuse scatterers | M0=2: More stable",
    fontsize=14,
)
plt.tight_layout()
plt.show()
Effect of GCF Cutoff Frequency M0 M0=0: Point targets only | M0=1: Optimal for diffuse scatterers | M0=2: More stable, GCF with M0=0 Mean: 0.756 ± 0.048, GCF with M0=1 Mean: 0.772 ± 0.046, GCF with M0=2 Mean: 0.788 ± 0.045
M0=0: Mean=0.756, Std=0.048
M0=1: Mean=0.772, Std=0.046
M0=2: Mean=0.788, Std=0.045

12. Spatial Smoothing Effect#

Many methods, such as [2], smooth the coherence map to reduce noise before using it for aberration correction.

# Effect of spatial smoothing on coherence maps

# Apply different levels of spatial smoothing to standard coherence factor
cf_original = coherence_factor(selected_beamsum, power_based=False)

# Different smoothing kernel sizes - reduced to 3 for cleaner layout
smoothing_kernels = [1, 3, 5]  # in pixels
smoothed_cfs = []

for kernel_size in smoothing_kernels:
    if kernel_size == 1:
        # No smoothing
        smoothed_cf = cf_original
    else:
        # Gaussian smoothing
        smoothed_cf = ndimage.gaussian_filter(cf_original, sigma=kernel_size / 2)
    smoothed_cfs.append(smoothed_cf)

fig, axes = plt.subplots(1, 3, figsize=(15, 5))

for i, (smoothed_cf, kernel_size) in enumerate(zip(smoothed_cfs, smoothing_kernels)):
    im = axes[i].imshow(smoothed_cf, cmap="viridis", vmin=0, vmax=1, extent=extent)
    if kernel_size == 1:
        title = f"Original (No Smoothing)\nMean: {smoothed_cf.mean():.3f}"
    else:
        title = f"Gaussian sigma={kernel_size / 2:.1f}\nMean: {smoothed_cf.mean():.3f}"
    axes[i].set_title(title)
    axes[i].set_xlabel("Lateral (mm)")
    axes[i].set_ylabel("Axial (mm)")
    plt.colorbar(im, ax=axes[i], shrink=0.8)

plt.suptitle("Effect of Spatial Smoothing on Coherence Factor", fontsize=16)
plt.tight_layout()
plt.show()
Effect of Spatial Smoothing on Coherence Factor, Original (No Smoothing) Mean: 0.886, Gaussian sigma=1.5 Mean: 0.886, Gaussian sigma=2.5 Mean: 0.886

Total running time of the script: (0 minutes 4.344 seconds)

Gallery generated by Sphinx-Gallery