from iaa_od.models import AnnotationProtocol, Image, GroundTruthProtocol, Result, ScaleComplexity
from iaa_od.models.constants import FIGURE_BASE_SIZE, FIGURE_COLUMN_SPACING, FIGURE_FONT_SIZE, FIGURE_FRAME_ON, FIGURE_HANDLE_HEIGHT, FIGURE_HANDLE_LENGTH, FIGURE_LEFT_MARGIN, FIGURE_LEGEND_BAND_SIZE, FIGURE_LEGEND_LOCATION, FIGURE_RIGHT_MARGIN, FIGURE_TITLE_HEADROOM, FIGURE_TOP_MARGIN, FIGURE_WSPACE, LABEL_OFFSET, FIGURE_SAVE_BBOX_INCHES, FIGURE_SAVE_DPI, FIGURE_SAVE_PAD_INCHES
from iaa_od.palette import colormap_to_color, colormap_to_macrocat_color
from .show_utils import upsert_handle, get_image_path, build_save_path
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from matplotlib.patches import Patch, Rectangle
import numpy as np
from typing import Optional
from copy import deepcopy
[docs]
def show_gts_with_annotations(result: Result, filepath: str, /, *, enable_labels: bool = False, full_legend: bool = False, show_image_filename: bool = False, save_path: Optional[str] = None, save_extension: Optional[str] = None) -> None:
"""
A wrapper for the function "show_image_with_annotations" that runs it for all images in a given ground truth set.
Parameters:
result (Result): The Result object containing the ground truths and annotations.
filepath (str): The filepath where the images are located.
enable_labels (bool, optional): Whether to display category labels above bounding boxes. Defaults to False.
full_legend (bool, optional): Whether to include all categories in the legend, even if not present in the image. Defaults to False.
show_image_filename (bool, optional): Whether to include the image filename in the title above the image. Defaults to False.
save_path (str | None, optional): If provided, the figures will be saved to this path instead of being displayed. Defaults to None.
save_extension (str | None, optional): The file extension to use when saving the figures (e.g., ".png", ".jpg"). If not provided, defaults to ".png". Defaults to None.
"""
if filepath is None or filepath == "":
raise ValueError("No filepath provided.")
# Get GTs from result object
gts: list[GroundTruthProtocol] = result.gts
# Get all image filenames from the first GT (all GTs have the same images, this is checked earlier)
filenames: list[str] = list(gts[0].images.keys())
if not filenames:
raise ValueError("No images found in the provided Ground Truths.")
for filename in filenames:
show_image_with_annotations(result, filename, filepath, enable_labels=enable_labels, full_legend=full_legend, show_image_filename=show_image_filename, save_path=save_path, save_extension=save_extension)
return
[docs]
def show_image_with_annotations(result: Result, filename: str, filepath: str, /, *, enable_labels: bool = False, full_legend: bool = False, show_image_filename: bool = False, save_path: Optional[str] = None, save_extension: Optional[str] = None) -> None:
"""
Displays the specified image with bounding box annotations from all ground truths in the provided Result object.
Parameters:
result (Result): The Result object containing the ground truths and annotations.
filename (str): The filename of the image to display.
filepath (str): The filepath where the image is located.
enable_labels (bool, optional): Whether to display category labels above bounding boxes. Defaults to False.
full_legend (bool, optional): Whether to include all categories in the legend, even if not present in the image. Defaults to False.
show_image_filename (bool, optional): Whether to include the image filename in the title above the image. Defaults to False.
save_path (str | None, optional): If provided, the figure will be saved to this path instead of being displayed. Defaults to None.
save_extension (str | None, optional): The file extension to use when saving the figure. If not provided, defaults to ".png". Defaults to None.
"""
if filename is None or filename == "":
raise ValueError("No filename provided.")
if filepath is None or filepath == "":
raise ValueError("No filepath provided.")
# Get GTs from result object
gts: list[GroundTruthProtocol] = result.gts
# Get the selected image from the first GT (all GTs have the same images, this is checked earlier)
full_path: str = get_image_path(filename, filepath)
image: Image | None = gts[0].images.get(filename)
if not image:
raise ValueError(f"Image '{filename}' not found in the provided Ground Truths.")
# Get all annotations for this image from all GTs
annotations: dict[str, list[AnnotationProtocol]] = {}
for gt in gts:
anns = deepcopy(gt.annotations[filename])
annotations[gt.name] = anns
# Order annotations by GT name to ensure consistent ordering of GTs across images
annotations = dict(sorted(annotations.items(), key=lambda item: item[0]))
# Initialise colour dictionary for categories
colour_map = plt.get_cmap('hsv', len(gts[0].categories_dict) + 1)
colour_dict = colormap_to_color(colour_map, gts[0].categories_dict)
# Initialise the image plot
fig_width = FIGURE_BASE_SIZE * len(gts)
fig_height = FIGURE_BASE_SIZE + FIGURE_LEGEND_BAND_SIZE
fig, axes = plt.subplots(1, len(gts), figsize=(fig_width, fig_height), sharex=True, sharey=True)
img: np.ndarray = mpimg.imread(full_path)
# NOTE: This is needed as plt.Rectangle requires the anchor point of the image to be in the lower-left corner
for ax in axes:
ax.imshow(img, origin='upper')
ax.set_xticks([])
ax.set_yticks([])
for spine in ax.spines.values():
spine.set_visible(False)
handles = []
# Fills the legend with all categories, regardless whether they are present in the image or not
if full_legend:
for cat in gts[0].categories_dict.keys():
colours = colour_dict[cat]
line_colour = colours.line_color
fill_colour = colours.fill_color
handles.append(Patch(color=fill_colour, label=gts[0].categories_dict[cat]))
# Plot all annotations from all GTs
for idx, (gt_name, anns) in enumerate(annotations.items()):
# Write the GT name above the image
title_str: str
if idx == 0 and show_image_filename:
title_str = f"{filename} - {gt_name}"
else:
title_str = gt_name
axes[idx].set_title(title_str, fontsize=16, fontweight='bold')
for ann in anns:
# Get the colour for this category
colours = colour_dict[ann.category_id]
line_colour = colours.line_color
fill_colour = colours.fill_color
# Add the legend handle for this category if it was not already added
new_handle = Patch(color=line_colour, label=gts[0].categories_dict[ann.category_id])
upsert_handle(handles, new_handle)
bbox = ann.bbox_coords.coords
rect = Rectangle((bbox.x, bbox.y), bbox.w, bbox.h, linewidth=1, edgecolor=line_colour, facecolor=fill_colour, label=gt_name)
axes[idx].add_patch(rect)
# Add the label above the bounding box with the category name
cat_name = gts[0].categories_dict.get(ann.category_id)
if enable_labels:
axes[idx].text(bbox.x, bbox.y - LABEL_OFFSET, cat_name, color='white', fontsize=8, backgroundcolor='black')
# Add the legend to the right of the last subplot
if handles:
fig.legend(handles=handles, loc=FIGURE_LEGEND_LOCATION, ncols=len(handles), fontsize=FIGURE_FONT_SIZE, handlelength=FIGURE_HANDLE_LENGTH, handleheight=FIGURE_HANDLE_HEIGHT, columnspacing=FIGURE_COLUMN_SPACING, frameon=FIGURE_FRAME_ON)
# If save_path is provided, save the figure to the specified path with the given extension
if save_path:
save_extension = save_extension if save_extension else ".png"
save_path = build_save_path(save_path, save_extension)
fig.savefig(save_path, bbox_inches=FIGURE_SAVE_BBOX_INCHES, dpi=FIGURE_SAVE_DPI, pad_inches=FIGURE_SAVE_PAD_INCHES)
plt.close(fig)
return
# Otherwise, display the figure
fig.subplots_adjust(left=FIGURE_LEFT_MARGIN, right=FIGURE_RIGHT_MARGIN, top=FIGURE_TOP_MARGIN, bottom=FIGURE_LEGEND_BAND_SIZE / fig_height, wspace=FIGURE_WSPACE)
plt.show()
return
[docs]
def show_image_with_annotations_macrocategories(result: Result, filename: str, filepath: str, collapsed_categories: dict[str, list[int]], /, *, enable_labels: bool = False, full_legend: bool = False, show_image_filename: bool = False, save_path: Optional[str] = None, save_extension: Optional[str] = None) -> None:
"""
Displays the specified image with bounding box annotations from all ground truths in the provided Result object.
Parameters:
result (Result): The Result object containing the ground truths and annotations.
filename (str): The filename of the image to display.
filepath (str): The filepath where the image is located.
collapsed_categories (dict[str, list[int]]): A dictionary mapping macro-category names to lists of category IDs that belong to that macro-category.
enable_labels (bool, optional): Whether to display category labels above bounding boxes. Defaults to False.
full_legend (bool, optional): Whether to include all macro-categories in the legend, even if not present in the image. Defaults to False.
show_image_filename (bool, optional): Whether to include the image filename in the title above the image. Defaults to False.
save_path (str | None, optional): If provided, the figure will be saved to this path instead of being displayed. Defaults to None.
save_extension (str | None, optional): The file extension to use when saving the figure. If not provided, defaults to ".png". Defaults to None.
"""
if filename is None or filename == "":
raise ValueError("No filename provided.")
if filepath is None or filepath == "":
raise ValueError("No filepath provided.")
# Get GTs from result object
gts: list[GroundTruthProtocol] = result.gts
# Get the selected image from the first GT (all GTs have the same images, this is checked earlier)
full_path: str = get_image_path(filename, filepath)
image: Image | None = gts[0].images.get(filename)
if not image:
raise ValueError(f"Image '{filename}' not found in the provided Ground Truths.")
# Get all annotations for this image from all GTs
annotations: dict[str, list[AnnotationProtocol]] = {}
for gt in gts:
anns = gt.annotations[filename]
annotations[gt.name] = anns
# Initialise colour dictionary for categories
colour_map = plt.get_cmap('hsv', len(collapsed_categories) + 1)
colour_dict = colormap_to_macrocat_color(colour_map, collapsed_categories)
# Initialise lookup table for collapsed categories
macro_lut: dict[int, str] = {}
cat_ids: list[int] = list(gts[0].categories_dict.keys())
for cat_id in cat_ids:
for macro_cat, subcats in collapsed_categories.items():
if cat_id in subcats:
macro_lut[cat_id] = macro_cat
break
# Initialise the image plot
fig_width = FIGURE_BASE_SIZE * len(gts)
fig_height = FIGURE_BASE_SIZE + FIGURE_LEGEND_BAND_SIZE
fig, axes = plt.subplots(1, len(gts), figsize=(fig_width, fig_height), sharex=True, sharey=True)
img: np.ndarray = mpimg.imread(full_path)
# NOTE: This is needed as plt.Rectangle requires the anchor point of the image to be in the lower-left corner
for ax in axes:
ax.imshow(img, origin='upper')
ax.set_xticks([])
ax.set_yticks([])
for spine in ax.spines.values():
spine.set_visible(False)
handles = []
# Fills the legend with all categories, regardless whether they are present in the image or not
if full_legend:
for macro_cat in collapsed_categories.keys():
colours = colour_dict[macro_cat]
line_colour = colours.line_color
fill_colour = colours.fill_color
handles.append(Patch(color=fill_colour, label=macro_cat))
# Plot all annotations from all GTs
for idx, (gt_name, anns) in enumerate(annotations.items()):
# Write the GT name above the image
title_str: str
if idx == 0 and show_image_filename:
title_str = f"{filename} - {gt_name}"
else:
title_str = gt_name
axes[idx].set_title(title_str, fontsize=16, fontweight='bold')
for ann in anns:
# Get the colour for this category
macrocat = macro_lut[ann.category_id]
colours = colour_dict[macrocat]
line_colour = colours.line_color
fill_colour = colours.fill_color
# Add the legend handle for this category if it was not already added
new_handle = Patch(color=line_colour, label=macrocat)
upsert_handle(handles, new_handle)
bbox = ann.bbox_coords.coords
rect = Rectangle((bbox.x, bbox.y), bbox.w, bbox.h, linewidth=1, edgecolor=line_colour, facecolor=fill_colour, label=gt_name)
axes[idx].add_patch(rect)
# Add the label above the bounding box with the category name
cat_name = gts[0].categories_dict.get(ann.category_id)
if enable_labels:
axes[idx].text(bbox.x, bbox.y - LABEL_OFFSET, cat_name, color='white', fontsize=8, backgroundcolor='black')
# Add the legend to the right of the last subplot
if handles:
fig.legend(handles=handles, loc=FIGURE_LEGEND_LOCATION, ncols=2, fontsize=FIGURE_FONT_SIZE, handlelength=FIGURE_HANDLE_LENGTH, handleheight=FIGURE_HANDLE_HEIGHT, columnspacing=FIGURE_COLUMN_SPACING, frameon=FIGURE_FRAME_ON)
# If save_path is provided, save the figure to the specified path with the given extension
if save_path:
save_extension = save_extension if save_extension else ".png"
save_path = build_save_path(save_path, save_extension)
fig.savefig(save_path, bbox_inches=FIGURE_SAVE_BBOX_INCHES, dpi=FIGURE_SAVE_DPI, pad_inches=FIGURE_SAVE_PAD_INCHES)
plt.close(fig)
return
# Otherwise, display the figure
fig.subplots_adjust(left=FIGURE_LEFT_MARGIN, right=FIGURE_RIGHT_MARGIN, top=FIGURE_TOP_MARGIN, bottom=FIGURE_LEGEND_BAND_SIZE / fig_height, wspace=FIGURE_WSPACE)
plt.show()
return
[docs]
def compare_images_with_macrocategories(result: Result, filename: str, filepath: str, collapsed_categories: dict[str, list[int]], /, *, enable_labels: bool = False, show_image_filename: bool = False, sc_data: Optional[ScaleComplexity] = None, save_path: Optional[str] = None, save_extension: Optional[str] = None) -> None:
"""
Displays the specified image with bounding box annotations from all ground truths in the provided Result object, comparing both the original categories (top row) and the collapsed macro-categories (bottom row).
Parameters:
result (Result): The Result object containing the ground truths and annotations.
filename (str): The filename of the image to display.
filepath (str): The filepath where the image is located.
collapsed_categories (dict[str, list[int]]): A dictionary mapping macro-category names to lists of category IDs that belong to that macro-category.
enable_labels (bool, optional): Whether to display category labels above bounding boxes. Defaults to False.
show_image_filename (bool, optional): Whether to include the image filename in the title above the image. Defaults to False.
sc_data (ScaleComplexity | None): Optional ScaleComplexity object containing Scale Complexity data for the images, which will be displayed in the title of the bottom row if provided. Defaults to None.
save_path (str | None, optional): If provided, the figure will be saved to this path instead of being displayed. Defaults to None.
save_extension (str | None, optional): The file extension to use when saving the figure. If not provided, defaults to ".png". Defaults to None.
"""
if filename is None or filename == "":
raise ValueError("No filename provided.")
if filepath is None or filepath == "":
raise ValueError("No filepath provided.")
# Get GTs from result object
gts: list[GroundTruthProtocol] = result.gts
# Get the selected image from the first GT (all GTs have the same images, this is checked earlier)
full_path: str = get_image_path(filename, filepath)
image: Image | None = gts[0].images.get(filename)
if not image:
raise ValueError(f"Image '{filename}' not found in the provided Ground Truths.")
# Get all annotations for this image from all GTs
annotations: dict[str, list[AnnotationProtocol]] = {}
for gt in gts:
anns = gt.annotations[filename]
annotations[gt.name] = anns
# Sort this dictionary by GT name to ensure consistent ordering of GTs across images
annotations = dict(sorted(annotations.items(), key=lambda item: item[0]))
# Initialise colour dictionary for categories and macro-categories
colour_map_cat = plt.get_cmap('hsv', len(gts[0].categories_dict) + 1)
colour_dict_cat = colormap_to_color(colour_map_cat, gts[0].categories_dict)
colour_map_macro = plt.get_cmap('hsv', len(collapsed_categories) + 1)
colour_dict_macro = colormap_to_macrocat_color(colour_map_macro, collapsed_categories)
# Initialise lookup table for collapsed categories
macro_lut: dict[int, str] = {}
cat_ids: list[int] = list(gts[0].categories_dict.keys())
for cat_id in cat_ids:
for macro_cat, subcats in collapsed_categories.items():
if cat_id in subcats:
macro_lut[cat_id] = macro_cat
break
# Initialise the image plot
fig_width = FIGURE_BASE_SIZE * len(gts)
fig_height = 2 * FIGURE_BASE_SIZE + 2 * FIGURE_LEGEND_BAND_SIZE + 2 * FIGURE_TITLE_HEADROOM
fig, axes = plt.subplots(2, len(gts), figsize=(fig_width, fig_height), sharex=True, sharey=True)
# Maximise window size
fig = plt.gcf()
manager = plt.get_current_fig_manager()
# Try different methods to maximise the window, as this is platform-dependent
try:
manager.window.showMaximized()
except:
try:
manager.window.state('zoomed')
except:
pass
img: np.ndarray = mpimg.imread(full_path)
# NOTE: This is needed as plt.Rectangle requires the anchor point of the image to be in the lower-left corner
for c in range(len(gts)):
for r in range(2):
ax = axes[r, c]
ax.imshow(img, origin='upper')
ax.set_xticks([])
ax.set_yticks([])
for spine in ax.spines.values():
spine.set_visible(False)
handles_cat = []
handles_macro = []
# Plot all annotations from all GTs
for idx, (gt_name, anns) in enumerate(annotations.items()):
# Write the GT name above the image
title_str: str
if idx == 0 and show_image_filename:
title_str = f"{filename} - {gt_name}"
else:
title_str = gt_name
axes[0, idx].set_title(title_str, fontsize=16, fontweight='bold')
# If Scale Complexity data is provided, show the agreement and SC values for this image in the titles of the bottom row
if sc_data is not None:
sc_value: float = sc_data.sc_for_image(filename)
agreement_value: tuple[float, float] = sc_data.agreement_for_image(filename)
title_str_sc: str = f"Agreement: {agreement_value[0]:.2f} -> {agreement_value[1]:.2f}\nSC: {sc_value:.2f}"
axes[1, len(gts) // 2].set_title(title_str_sc, fontsize=16, fontweight='bold')
for ann in anns:
# Top row: categories
# Get the colour for this category
colours = colour_dict_cat[ann.category_id]
line_colour = colours.line_color
fill_colour = colours.fill_color
# Add the legend handle for this category if it was not already added
new_handle = Patch(color=line_colour, label=gts[0].categories_dict[ann.category_id])
upsert_handle(handles_cat, new_handle)
bbox = ann.bbox_coords.coords
rect = Rectangle((bbox.x, bbox.y), bbox.w, bbox.h, linewidth=1, edgecolor=line_colour, facecolor=fill_colour, label=gt_name)
axes[0, idx].add_patch(rect)
# Add the label above the bounding box with the category name
cat_name = gts[0].categories_dict.get(ann.category_id)
if enable_labels:
axes[0, idx].text(bbox.x, bbox.y - LABEL_OFFSET, cat_name, color='white', fontsize=8, backgroundcolor='black')
# Bottom row: macro-categories
# Get the colour for this category
macrocat = macro_lut[ann.category_id]
colours = colour_dict_macro[macrocat]
line_colour = colours.line_color
fill_colour = colours.fill_color
# Add the legend handle for this category if it was not already added
new_handle = Patch(color=line_colour, label=macrocat)
upsert_handle(handles_macro, new_handle)
bbox = ann.bbox_coords.coords
rect = Rectangle((bbox.x, bbox.y), bbox.w, bbox.h, linewidth=1, edgecolor=line_colour, facecolor=fill_colour, label=gt_name)
axes[1, idx].add_patch(rect)
# Add the label above the bounding box with the category name
cat_name = gts[0].categories_dict.get(ann.category_id)
if enable_labels:
axes[1, idx].text(bbox.x, bbox.y - LABEL_OFFSET, cat_name, color='white', fontsize=8, backgroundcolor='black')
# Add the legend to the right of the last subplot
band_frac = FIGURE_LEGEND_BAND_SIZE / fig_height
title_frac = FIGURE_TITLE_HEADROOM / fig_height
base_frac = FIGURE_BASE_SIZE / fig_height
fig.subplots_adjust(left=FIGURE_LEFT_MARGIN, right=FIGURE_RIGHT_MARGIN, top=FIGURE_TOP_MARGIN - title_frac, bottom=band_frac, wspace=FIGURE_WSPACE, hspace=(FIGURE_LEGEND_BAND_SIZE + FIGURE_TITLE_HEADROOM) / FIGURE_BASE_SIZE)
if handles_cat:
fig.legend(handles=handles_cat, loc='upper center', bbox_to_anchor=(0.5, 1 - title_frac - base_frac), ncols=len(handles_cat), fontsize=FIGURE_FONT_SIZE, handlelength=FIGURE_HANDLE_LENGTH, handleheight=FIGURE_HANDLE_HEIGHT, frameon=FIGURE_FRAME_ON)
if handles_macro:
fig.legend(handles=handles_macro, loc='upper center', bbox_to_anchor=(0.5, band_frac), ncols=len(handles_macro), fontsize=FIGURE_FONT_SIZE, handlelength=FIGURE_HANDLE_LENGTH, handleheight=FIGURE_HANDLE_HEIGHT, frameon=FIGURE_FRAME_ON)
# If save_path is provided, save the figure to the specified path with the given extension
if save_path:
save_extension = save_extension if save_extension else ".png"
save_path = build_save_path(save_path, save_extension)
fig.savefig(save_path, bbox_inches=FIGURE_SAVE_BBOX_INCHES, dpi=FIGURE_SAVE_DPI, pad_inches=FIGURE_SAVE_PAD_INCHES)
plt.close(fig)
return
# Otherwise, display the figure
plt.show()
return