Source code for iaa_od.visualisation.show_annotations

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