"""
Visualization functions for CORE registration results
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool, ColorBar, Title
from bokeh.transform import linear_cmap
from bokeh.palettes import Viridis256, Inferno256
from bokeh.io import output_notebook
import sys
import importlib
from core.config import VisualizationParams
from skimage import color
[docs]
def setup_bokeh_notebook():
"""Setup Bokeh for notebook output"""
output_notebook()
[docs]
def visualize_cluster_alignment(
fixed_points,
moving_points,
moving_updated,
fixed_df=None,
moving_df=None,
figsize=(10, 10),
title='Cluster Centers: Fixed, Original Moving, and Transformed',
save_path=None
):
"""
Visualize and optionally save the alignment between fixed, moving, and transformed cluster centers.
Parameters
----------
fixed_points : np.ndarray
Array of fixed cluster centers with shape (N, 2).
moving_points : np.ndarray
Array of original moving cluster centers with shape (N, 2).
moving_updated : np.ndarray
Array of transformed (aligned) moving cluster centers with shape (N, 2).
fixed_df : pandas.DataFrame, optional
DataFrame to update with fixed point coordinates.
moving_df : pandas.DataFrame, optional
DataFrame to update with moving point coordinates.
figsize : tuple, default=(10, 10)
Figure size for the plot.
title : str, optional
Title for the plot.
save_path : str, optional
Path to save the plot (e.g., "results/alignment_plot.png"). If None, the plot is just shown.
Returns
-------
fixed_df, moving_df : pandas.DataFrame or None
Updated DataFrames if provided, else None.
"""
# Optionally update dataframes
if fixed_df is not None:
fixed_df[['global_x', 'global_y']] = fixed_points
if moving_df is not None:
moving_df[['global_x', 'global_y']] = moving_updated
# Create the plot
plt.figure(figsize=figsize)
# Fixed cluster centers (red)
plt.scatter(
fixed_points[:, 0], fixed_points[:, 1],
c='red', s=20, alpha=0.6, label='Fixed cluster centers'
)
# Original moving cluster centers (blue)
plt.scatter(
moving_points[:, 0], moving_points[:, 1],
c='blue', s=30, alpha=0.6, label='Original moving cluster centers'
)
# Transformed moving cluster centers (cyan)
plt.scatter(
moving_updated[:, 0], moving_updated[:, 1],
c='cyan', s=10, alpha=0.6, label='Transformed cluster centers'
)
# Plot formatting
plt.legend()
plt.title(title)
plt.xlabel('X coordinate (normalized)')
plt.ylabel('Y coordinate (normalized)')
plt.axis('equal')
plt.gca().invert_yaxis()
# Save or display
if save_path:
plt.savefig(save_path, bbox_inches='tight', dpi=300)
plt.close()
print(f"Plot saved to {save_path}")
else:
plt.show()
return fixed_df, moving_df
[docs]
def visualize_overlays(fixed_tile, moving_tile, transformed_tile):
overlay_before = np.dstack((
color.rgb2gray(moving_tile),
color.rgb2gray(fixed_tile),
color.rgb2gray(moving_tile)
))
overlay_after = np.dstack((
color.rgb2gray(transformed_tile),
color.rgb2gray(fixed_tile),
color.rgb2gray(transformed_tile)
))
_, axs = plt.subplots(1, 2, figsize=(15, 10))
axs[0].imshow(overlay_before, cmap="gray")
axs[0].set_title("Overlay Before")
axs[0].axis('off')
axs[1].imshow(overlay_after, cmap="gray")
axs[1].set_title("Overlay After")
axs[1].axis('off')
plt.show()
[docs]
def visualize_patches(fixed_tile, moving_tile, transformed_tile):
"""
Visualize fixed, moving, and transformed image patches
Args:
fixed_tile: Fixed image patch
moving_tile: Moving image patch
transformed_tile: Transformed image patch
"""
_, axs = plt.subplots(1, 3, figsize=(15, 10))
axs[0].imshow(fixed_tile, cmap="gray")
axs[0].set_title("Fixed Tile")
axs[0].axis('off')
axs[1].imshow(moving_tile, cmap="gray")
axs[1].set_title("Moving Tile")
axs[1].axis('off')
axs[2].imshow(transformed_tile, cmap="gray")
axs[2].set_title("Rigid Transformed Tile")
axs[2].axis('off')
plt.show()
[docs]
def create_nuclei_overlay_plot(moving_df, fixed_df, title="Fixed vs Moving Nuclei Coordinates"):
"""
Create interactive Bokeh plot for nuclei coordinates overlay
Args:
moving_df: DataFrame with moving nuclei coordinates
fixed_df: DataFrame with fixed nuclei coordinates
title: Plot title
Returns:
Bokeh figure object
"""
# Ensure 'area' column exists
if 'area' not in moving_df.columns:
moving_df['area'] = 1.0
if 'area' not in fixed_df.columns:
fixed_df['area'] = 1.0
# Create ColumnDataSources
source_moving = ColumnDataSource(moving_df)
source_fixed = ColumnDataSource(fixed_df)
# Create figure
p = figure(
title=title,
x_axis_label='Global X',
y_axis_label='Global Y',
width=VisualizationParams.FIGURE_WIDTH,
height=VisualizationParams.FIGURE_HEIGHT,
tools="pan,wheel_zoom,box_zoom,reset,save",
active_scroll="wheel_zoom"
)
# Plot fixed nuclei
p.triangle('global_x', 'global_y',
source=source_fixed,
size=VisualizationParams.POINT_SIZE_MEDIUM,
fill_color=VisualizationParams.FIXED_COLOR,
fill_alpha=VisualizationParams.ALPHA,
line_color=None,
legend_label='Fixed')
# Plot moving nuclei
p.circle('global_x', 'global_y',
source=source_moving,
size=VisualizationParams.POINT_SIZE_SMALL,
fill_color=VisualizationParams.MOVING_COLOR,
fill_alpha=VisualizationParams.ALPHA,
line_color=None,
legend_label='Moving')
# Add hover tool
hover = HoverTool(tooltips=[
("Global X", "@global_x{0.00}"),
("Global Y", "@global_y{0.00}"),
("Area", "@area"),
])
p.add_tools(hover)
# Flip Y-axis to match image coordinates
p.y_range.flipped = True
# Configure legend
p.legend.location = "top_left"
p.legend.click_policy = "hide"
return p
[docs]
def create_registration_comparison_plot(fixed_df, moving_df, moving_rigid_df,
moving_nonrigid_df=None):
"""
Create comparison plot showing original, rigid, and non-rigid registration
Args:
fixed_df: Fixed nuclei DataFrame
moving_df: Original moving nuclei DataFrame
moving_rigid_df: Rigid registered moving nuclei DataFrame
moving_nonrigid_df: Non-rigid registered moving nuclei DataFrame (optional)
Returns:
Bokeh figure object
"""
# Create ColumnDataSources
source_fixed = ColumnDataSource(fixed_df)
source_moving = ColumnDataSource(moving_df)
source_moving_rigid = ColumnDataSource(moving_rigid_df)
# Create figure
p = figure(
title="Registration Comparison: Original vs Rigid vs Non-Rigid",
x_axis_label='Global X',
y_axis_label='Global Y',
width=VisualizationParams.FIGURE_WIDTH,
height=VisualizationParams.FIGURE_HEIGHT,
tools="pan,wheel_zoom,box_zoom,reset,save",
active_scroll="wheel_zoom"
)
# Plot fixed nuclei
p.triangle('global_x', 'global_y',
source=source_fixed,
size=VisualizationParams.POINT_SIZE_MEDIUM,
fill_color=VisualizationParams.FIXED_COLOR,
fill_alpha=VisualizationParams.ALPHA,
line_color=None,
legend_label='Fixed')
# Plot original moving nuclei
p.circle('global_x', 'global_y',
source=source_moving,
size=VisualizationParams.POINT_SIZE_SMALL,
fill_color=VisualizationParams.MOVING_COLOR,
fill_alpha=VisualizationParams.ALPHA,
line_color=None,
legend_label='Moving (Original)')
# Plot rigid registered moving nuclei
p.square('global_x', 'global_y',
source=source_moving_rigid,
size=VisualizationParams.POINT_SIZE_MEDIUM,
fill_color=VisualizationParams.RIGID_COLOR,
fill_alpha=0.5,
line_color=None,
legend_label='Moving (Rigid Registered)')
# Plot non-rigid registered nuclei if provided
if moving_nonrigid_df is not None:
source_moving_nonrigid = ColumnDataSource(moving_nonrigid_df)
p.diamond('global_x', 'global_y',
source=source_moving_nonrigid,
size=VisualizationParams.POINT_SIZE_LARGE,
fill_color=VisualizationParams.NONRIGID_COLOR,
fill_alpha=0.5,
line_color=None,
legend_label='Moving (Non-Rigid Registered)')
# Add hover tool
hover = HoverTool(tooltips=[
("Global X", "@global_x{0.00}"),
("Global Y", "@global_y{0.00}"),
("Area", "@area"),
])
p.add_tools(hover)
# Flip Y-axis and configure appearance
p.y_range.flipped = True
p.xgrid.visible = False
p.ygrid.visible = False
# Configure legend
p.legend.location = "top_left"
p.legend.click_policy = "hide"
return p
[docs]
def visualize_shape_aware_registration(registrator_obj, title="Shape-Aware Point Set Registration"):
"""
Visualize shape-aware registration results using the built-in method
Args:
registrator_obj: ShapeAwarePointSetRegistration object after registration
title: Plot title
Returns:
Bokeh figure object
"""
if registrator_obj.registered_points is None:
raise ValueError("Registration must be performed before visualization")
# Create figure
p = figure(
title=title,
x_axis_label='Global X',
y_axis_label='Global Y',
width=VisualizationParams.FIGURE_WIDTH,
height=VisualizationParams.FIGURE_HEIGHT,
tools=["pan", "wheel_zoom", "box_zoom", "reset", "save"],
active_scroll="wheel_zoom"
)
# Prepare data sources
fixed_source = ColumnDataSource(registrator_obj.fixed_points)
moving_orig_source = ColumnDataSource(registrator_obj.moving_points)
moving_reg_source = ColumnDataSource(registrator_obj.registered_points)
# Plot fixed points
p.triangle(
'global_x', 'global_y',
source=fixed_source,
size=3.5,
fill_color='blue',
fill_alpha=0.7,
line_color=None,
legend_label='Fixed'
)
# Plot original moving points
p.circle(
'global_x', 'global_y',
source=moving_orig_source,
size=2.5,
fill_color='red',
fill_alpha=0.3,
line_color=None,
legend_label='Moving (Original)'
)
# Plot registered moving points
reg_circles = p.circle(
'registered_x', 'registered_y',
source=moving_reg_source,
size=3,
fill_color='green',
fill_alpha=0.7,
line_color='black',
line_alpha=0.5,
line_width=0.5,
legend_label='Moving (Registered)'
)
# Draw correspondence lines
if registrator_obj.correspondence_indices is not None:
step = max(1, len(registrator_obj.registered_points) // 100)
for i in range(0, len(registrator_obj.registered_points), step):
x0 = registrator_obj.registered_points.iloc[i]['registered_x']
y0 = registrator_obj.registered_points.iloc[i]['registered_y']
idx = registrator_obj.correspondence_indices[i]
x1 = registrator_obj.fixed_points.iloc[idx]['global_x']
y1 = registrator_obj.fixed_points.iloc[idx]['global_y']
p.line([x0, x1], [y0, y1], line_color='black', line_alpha=0.2, line_width=0.5)
# Add hover tool
hover = HoverTool(
renderers=[reg_circles],
tooltips=[
("Original X", "@global_x{0.00}"),
("Original Y", "@global_y{0.00}"),
("Registered X", "@registered_x{0.00}"),
("Registered Y", "@registered_y{0.00}"),
("Area", "@area"),
("Set", "Moving (Registered)")
]
)
p.add_tools(hover)
# Flip Y-axis and configure
p.y_range.flipped = True
p.legend.location = "top_left"
p.legend.click_policy = "hide"
return p
[docs]
def create_method_comparison_plot(fixed_df, moving_df, icp_registered_df, shape_aware_registered_df):
"""
Create comparison plot between ICP and Shape-Aware registration methods
Args:
fixed_df: Fixed nuclei DataFrame
moving_df: Original moving nuclei DataFrame
icp_registered_df: ICP registered moving nuclei DataFrame
shape_aware_registered_df: Shape-aware registered moving nuclei DataFrame
Returns:
Bokeh figure object
"""
# Create ColumnDataSources
source_fixed = ColumnDataSource(fixed_df)
source_moving = ColumnDataSource(moving_df)
source_icp = ColumnDataSource(icp_registered_df)
source_shape_aware = ColumnDataSource(shape_aware_registered_df)
# Create figure
p = figure(
title="Registration Method Comparison: ICP vs Shape-Aware",
x_axis_label='Global X',
y_axis_label='Global Y',
width=VisualizationParams.FIGURE_WIDTH,
height=VisualizationParams.FIGURE_HEIGHT,
tools="pan,wheel_zoom,box_zoom,reset,save",
active_scroll="wheel_zoom"
)
# Plot fixed nuclei
p.triangle('global_x', 'global_y',
source=source_fixed,
size=VisualizationParams.POINT_SIZE_MEDIUM,
fill_color='blue',
fill_alpha=0.7,
line_color=None,
legend_label='Fixed')
# Plot original moving nuclei
p.circle('global_x', 'global_y',
source=source_moving,
size=VisualizationParams.POINT_SIZE_SMALL,
fill_color='red',
fill_alpha=0.3,
line_color=None,
legend_label='Moving (Original)')
# Plot ICP registered nuclei
p.square('global_x', 'global_y',
source=source_icp,
size=VisualizationParams.POINT_SIZE_MEDIUM,
fill_color='green',
fill_alpha=0.6,
line_color=None,
legend_label='Moving (ICP)')
# Plot shape-aware registered nuclei
p.diamond('registered_x', 'registered_y',
source=source_shape_aware,
size=VisualizationParams.POINT_SIZE_MEDIUM,
fill_color='orange',
fill_alpha=0.6,
line_color=None,
legend_label='Moving (Shape-Aware)')
# Add hover tool
hover = HoverTool(tooltips=[
("Global X", "@global_x{0.00}"),
("Global Y", "@global_y{0.00}"),
("Area", "@area"),
])
p.add_tools(hover)
# Flip Y-axis and configure appearance
p.y_range.flipped = True
p.xgrid.visible = False
p.ygrid.visible = False
# Configure legend
p.legend.location = "top_left"
p.legend.click_policy = "hide"
return p
[docs]
def create_detailed_nuclei_plot_with_colormaps(moving_df, fixed_df):
"""
Create detailed nuclei plot with color mapping by area
Args:
moving_df: Moving nuclei DataFrame
fixed_df: Fixed nuclei DataFrame
Returns:
Bokeh figure object
"""
# Create ColumnDataSources
source_moving = ColumnDataSource(moving_df)
source_fixed = ColumnDataSource(fixed_df)
# Color mappers
color_mapper_moving = linear_cmap('area', Viridis256,
low=moving_df['area'].min(),
high=moving_df['area'].max())
color_mapper_fixed = linear_cmap('area', Inferno256,
low=fixed_df['area'].min(),
high=fixed_df['area'].max())
# Create figure
p = figure(
title="Fixed vs Moving Nuclei Coordinates (Color by Area)",
x_axis_label='Global X',
y_axis_label='Global Y',
width=VisualizationParams.FIGURE_WIDTH,
height=650,
tools="pan,wheel_zoom,box_zoom,reset,save",
active_scroll="wheel_zoom"
)
# Plot moving nuclei with color mapping
moving_renderer = p.circle('global_x', 'global_y',
source=source_moving,
size=VisualizationParams.POINT_SIZE_SMALL,
fill_color=color_mapper_moving,
fill_alpha=VisualizationParams.ALPHA,
line_color=None,
legend_label='Moving')
# Plot fixed nuclei with color mapping
fixed_renderer = p.triangle('global_x', 'global_y',
source=source_fixed,
size=2.5,
fill_color=color_mapper_fixed,
fill_alpha=VisualizationParams.ALPHA,
line_color=None,
legend_label='Fixed')
# Add hover tool
hover = HoverTool(tooltips=[
("Global X", "@global_x{0.00}"),
("Global Y", "@global_y{0.00}"),
("Area", "@area"),
], renderers=[moving_renderer, fixed_renderer])
p.add_tools(hover)
# Flip Y-axis
p.y_range.flipped = True
# Add color bars
color_bar_moving = ColorBar(color_mapper=color_mapper_moving['transform'],
title="Area (Moving)")
color_bar_fixed = ColorBar(color_mapper=color_mapper_fixed['transform'],
title="Area (Fixed)")
p.add_layout(color_bar_moving, 'right')
p.add_layout(color_bar_fixed, 'below')
# Configure legend
p.legend.location = "top_left"
p.legend.click_policy = "hide"
return p
[docs]
def show_plot(plot):
"""
Display a Bokeh plot
Args:
plot: Bokeh figure object
"""
show(plot)