| """ |
| Utiltites for analyizing and visualizing model segmentations on dataset. |
| Yelena Bagdasarova, Scott Song |
| """ |
|
|
| import json |
| import os |
| import pickle |
| import sys |
| import warnings |
|
|
| import cv2 |
| import detectron2 |
| import detectron2.utils.comm as comm |
| import matplotlib.pyplot as plt |
| import numpy as np |
| import pandas as pd |
| import seaborn as sns |
| import torch |
| from detectron2.data import DatasetCatalog, MetadataCatalog |
| from detectron2.engine import DefaultPredictor |
| from detectron2.evaluation import COCOEvaluator |
| from detectron2.utils.visualizer import Visualizer |
| from matplotlib.backends.backend_pdf import PdfPages |
| from PIL import Image |
| from pycocotools.coco import COCO |
| from pycocotools.cocoeval import COCOeval |
| from pycocotools.mask import decode |
| from sklearn.metrics import average_precision_score, precision_recall_curve |
| from tqdm import tqdm |
|
|
| |
| |
| plt.style.use("./scripts/ybpres.mplstyle") |
|
|
|
|
| def grab_dataset(name): |
| """Creates a function to load a pickled dataset by name. |
| |
| This function returns another function that, when called, loads a dataset |
| from a pickle file located in the "datasets/" directory. |
| |
| Args: |
| name (str): The base name of the dataset file (without extension). |
| |
| Returns: |
| function: A zero-argument function that loads and returns the dataset. |
| """ |
|
|
| def f(): |
| return pickle.load(open("datasets/" + name + ".pk", "rb")) |
|
|
| return f |
|
|
|
|
| class OutputVis: |
| """A class to visualize model outputs and ground truth annotations.""" |
|
|
| def __init__( |
| self, |
| dataset_name, |
| cfg=None, |
| prob_thresh=0.5, |
| pred_mode="model", |
| pred_file=None, |
| has_annotations=True, |
| draw_mode="default", |
| ): |
| """Initializes the OutputVis class. |
| |
| Args: |
| dataset_name (str): The name of the registered Detectron2 dataset. |
| cfg (CfgNode, optional): The Detectron2 configuration object. |
| Required if `pred_mode` is "model". Defaults to None. |
| prob_thresh (float, optional): The probability threshold to apply |
| to model predictions for visualization. Defaults to 0.5. |
| pred_mode (str, optional): The mode for getting predictions. Must be |
| either "model" (to use a live predictor) or "file" (to load |
| from a COCO results file). Defaults to "model". |
| pred_file (str, optional): The path to the COCO JSON results file. |
| Required if `pred_mode` is "file". Defaults to None. |
| has_annotations (bool, optional): Whether the dataset has ground |
| truth annotations to visualize. Defaults to True. |
| draw_mode (str, optional): The drawing style for visualizations. |
| Can be "default" (color) or "bw" (monochrome). Defaults to "default". |
| """ |
| self.dataset_name = dataset_name |
| self.cfg = cfg |
| self.prob_thresh = prob_thresh |
| self.data = DatasetCatalog.get(dataset_name) |
| if pred_mode == "model": |
| self.predictor = DefaultPredictor(cfg) |
| self._mode = "model" |
| elif pred_mode == "file": |
| with open(pred_file, "r") as f: |
| self.pred_instances = json.load(f) |
| self.instance_img_list = [p["image_id"] for p in self.pred_instances] |
| self._mode = "file" |
| else: |
| sys.exit('Invalid mode. Only "model" or "file" permitted.') |
| self.has_annotations = has_annotations |
| self.permitted_draw_modes = ["default", "bw"] |
| self.set_draw_mode(draw_mode) |
| self.font_size = 16 |
| self.annotation_color = "r" |
| self.scale = 3.0 |
|
|
| def set_draw_mode(self, draw_mode): |
| """Sets the drawing mode for visualizations. |
| |
| Args: |
| draw_mode (str): The drawing style. Must be one of the permitted |
| modes (e.g., "default", "bw"). |
| """ |
| if draw_mode not in self.permitted_draw_modes: |
| sys.exit("draw_mode must be one of the following: {}".format(self.permitted_draw_modes)) |
| self.draw_mode = draw_mode |
|
|
| def get_ori_image(self, imgid): |
| """Retrieves the original image for a given image ID. |
| |
| The image is scaled up by a factor of 3 for better visualization. |
| |
| Args: |
| imgid (str): The 'image_id' from the dataset dictionary. |
| |
| Returns: |
| PIL.Image: The original image. |
| """ |
| dat = self.get_gt_image_data(imgid) |
| im = cv2.imread(dat["file_name"]) |
| v_gt = Visualizer(im, MetadataCatalog.get(self.dataset_name), scale=self.scale) |
| result_image = v_gt.output.get_image() |
| img = Image.fromarray(result_image) |
| return img |
|
|
| def get_gt_image_data(self, imgid): |
| """Returns the ground truth data dictionary for a given image ID. |
| |
| Args: |
| imgid (str): The 'image_id' from the dataset dictionary. |
| |
| Returns: |
| dict: The dataset dictionary for the specified image. |
| """ |
| gt_data = next(item for item in self.data if (item["image_id"] == imgid)) |
| return gt_data |
|
|
| def produce_gt_image(self, dat, im): |
| """Creates an image with ground truth annotations overlaid. |
| |
| The visualization can be in color or monochrome depending on the draw mode. |
| |
| Args: |
| dat (dict): The dataset dictionary containing ground truth annotations. |
| im (np.ndarray): The input image in RGB format (H, W, C) as a NumPy array. |
| |
| Returns: |
| PIL.Image: The image with ground truth instances overlaid. |
| """ |
| v_gt = Visualizer(im, MetadataCatalog.get(self.dataset_name), scale=self.scale) |
| if self.has_annotations: |
| segs = [ddict["segmentation"] for ddict in dat["annotations"]] |
| if self.draw_mode == "bw": |
| _bboxes = None |
| assigned_colors = [self.annotation_color] * len(segs) |
| else: |
| bboxes = [ddict["bbox"] for ddict in dat["annotations"]] |
| _bboxes = detectron2.structures.Boxes(bboxes) |
| _bboxes = detectron2.structures.BoxMode.convert( |
| _bboxes.tensor, from_mode=1, to_mode=0 |
| ) |
| assigned_colors = None |
|
|
| result_image = v_gt.overlay_instances( |
| boxes=_bboxes, masks=segs, assigned_colors=assigned_colors, alpha=1.0 |
| ).get_image() |
| else: |
| result_image = v_gt.output.get_image() |
| img = Image.fromarray(result_image) |
| return img |
|
|
| def produce_model_image(self, imgid, dat, im): |
| """Creates an image with model-predicted instances overlaid. |
| |
| Predictions are either generated by the model or loaded from a file, |
| based on the configured `pred_mode`. |
| |
| Args: |
| imgid (str): The 'image_id' from the dataset dictionary. |
| dat (dict): The dataset dictionary for the image (used for height/width). |
| im (np.ndarray): The input image in RGB format (H, W, C) as a NumPy array. |
| |
| Returns: |
| PIL.Image: The image with model-predicted instances overlaid. |
| """ |
| v_dt = Visualizer(im, MetadataCatalog.get(self.dataset_name), scale=self.scale) |
| v_dt._default_font_size = self.font_size |
|
|
| |
| if self._mode == "model": |
| outputs = self.predictor(im)["instances"].to("cpu") |
| elif self._mode == "file": |
| outputs = self.get_outputs_from_file(imgid, (dat["height"], dat["width"])) |
| outputs = outputs[outputs.scores > self.prob_thresh] |
| if self.draw_mode == "bw": |
| result_model = v_dt.overlay_instances( |
| masks=outputs.pred_masks, assigned_colors=[self.annotation_color] * len(outputs), alpha=1.0 |
| ).get_image() |
| else: |
| result_model = v_dt.draw_instance_predictions(outputs).get_image() |
| img_model = Image.fromarray(result_model) |
| return img_model |
|
|
| def get_image(self, imgid): |
| """Generates both ground truth and model prediction overlay images. |
| |
| Args: |
| imgid (str): The 'image_id' from the dataset dictionary. |
| |
| Returns: |
| tuple[PIL.Image, PIL.Image]: A tuple containing the ground truth |
| image and the model prediction image. |
| """ |
| dat = self.get_gt_image_data(imgid) |
| im = cv2.imread(dat["file_name"]) |
| img = self.produce_gt_image(dat, im) |
| img_model = self.produce_model_image(imgid, dat, im) |
| return img, img_model |
|
|
| def get_outputs_from_file(self, imgid, imgsize): |
| """Loads and formats model predictions from a COCO results file. |
| |
| Converts COCO-formatted instances into a Detectron2 `Instances` object |
| suitable for the visualizer. |
| |
| Args: |
| imgid (str): The 'image_id' of the desired image. |
| imgsize (tuple[int, int]): The (height, width) of the image. |
| |
| Returns: |
| detectron2.structures.Instances: An `Instances` object containing |
| the predictions. |
| """ |
|
|
| pred_boxes = [] |
| scores = [] |
| pred_classes = [] |
| pred_masks = [] |
| for i, img in enumerate(self.instance_img_list): |
| if img == imgid: |
| pred_boxes.append(self.pred_instances[i]["bbox"]) |
| scores.append(self.pred_instances[i]["score"]) |
| pred_classes.append(int(self.pred_instances[i]["category_id"])) |
| |
| pred_masks.append(decode(self.pred_instances[i]["segmentation"])) |
| _bboxes = detectron2.structures.Boxes(pred_boxes) |
| pred_boxes = detectron2.structures.BoxMode.convert(_bboxes.tensor, from_mode=1, to_mode=0) |
| inst_dict = dict( |
| pred_boxes=pred_boxes, |
| scores=torch.tensor(np.array(scores)), |
| pred_classes=torch.tensor(np.array(pred_classes)), |
| pred_masks=torch.tensor(np.array(pred_masks)).to(torch.bool), |
| ) |
| outputs = detectron2.structures.Instances(imgsize, **inst_dict) |
| return outputs |
|
|
| @staticmethod |
| def height_crop_range(im, height_target=256): |
| """Calculates a vertical crop range centered on the brightest part of an image. |
| |
| Args: |
| im (np.ndarray): The input image as a NumPy array (H, W, C). |
| height_target (int, optional): The desired height of the crop. |
| Defaults to 256. |
| |
| Returns: |
| range: A range object representing the start and end pixel rows for the crop. |
| """ |
| yhist = im.sum(axis=1) |
| mu = np.average(np.arange(yhist.shape[0]), weights=yhist) |
| h1 = int(np.floor(mu - height_target / 2)) |
| h2 = int(np.ceil(mu + height_target / 2)) |
| if h1 < 0: |
| h1 = 0 |
| h2 = height_target |
| if h2 > yhist.shape[0]: |
| h2 = yhist.shape[0] |
| h1 = h2 - height_target |
| return range(h1, h2) |
|
|
| def output_to_pdf(self, imgids, outname, dfimg=None): |
| """Exports visualizations of ground truth and model predictions to a PDF file. |
| |
| Each page of the PDF contains the ground truth and model prediction for one image. |
| |
| Args: |
| imgids (list[str]): A list of 'image_id' values to include in the PDF. |
| outname (str): The path and filename for the output PDF. |
| dfimg (pd.DataFrame, optional): A DataFrame with image statistics |
| to display on each page. Index should be `imgid`. Defaults to None. |
| """ |
|
|
| gtstr = "" |
| dtstr = "" |
|
|
| if dfimg is not None: |
| gtcols = dfimg.columns[["gt_" in col for col in dfimg.columns]] |
| dtcols = dfimg.columns[["dt_" in col for col in dfimg.columns]] |
|
|
| with PdfPages(outname) as pdf: |
| for imgid in tqdm(imgids): |
| img, img_model = self.get_image(imgid) |
| |
| crop_range = self.height_crop_range(np.array(img.convert("L")), height_target=256 * self.scale) |
| img = np.array(img)[crop_range] |
| img_model = np.array(img_model)[crop_range] |
|
|
| fig, ax = plt.subplots(2, 1, figsize=[22, 10], dpi=200) |
| ax[0].imshow(img) |
| ax[0].set_title(imgid + " Ground Truth") |
| ax[0].set_axis_off() |
| ax[1].imshow(img_model) |
| ax[1].set_title(imgid + " Model Prediction") |
| ax[1].set_axis_off() |
| if dfimg is not None: |
| gtstr = ["{:s}={:.2f}".format(col, dfimg.loc[imgid, col]) for col in gtcols] |
| ax[0].text(0, 0.05 * (ax[0].get_ylim()[0]), gtstr, color="white", fontsize=14) |
| dtstr = ["{:s}={:.2f}".format(col, dfimg.loc[imgid, col]) for col in dtcols] |
| ax[1].text(0, 0.05 * (ax[1].get_ylim()[0]), dtstr, color="white", fontsize=14) |
| pdf.savefig(fig) |
| plt.close(fig) |
|
|
| def save_imgarr_to_tiff(self, imgs, outname): |
| """Saves a list of PIL images to a multi-page TIFF file. |
| |
| Args: |
| imgs (list[PIL.Image]): A list of images to save. |
| outname (str): The path and filename for the output TIFF. |
| """ |
| if len(imgs) > 1: |
| imgs[0].save(outname, dpi=(400, 400), tags="", compression=None, save_all=True, append_images=imgs[1:]) |
| else: |
| imgs[0].save(outname) |
|
|
| def output_ori_to_tiff(self, imgids, outname): |
| """Saves the original images for a list of IDs to a multi-page TIFF. |
| |
| Args: |
| imgids (list[str]): A list of 'image_id' values. |
| outname (str): The path and filename for the output TIFF. |
| """ |
| imgs = [] |
| for imgid in tqdm(imgids): |
| img_ori = self.get_ori_image(imgid) |
| imgs.append(img_ori) |
| self.save_imgarr_to_tiff(imgs, outname) |
|
|
| def output_pred_to_tiff(self, imgids, outname, pred_only=False): |
| """Saves model prediction overlays for a list of IDs to a multi-page TIFF. |
| |
| Args: |
| imgids (list[str]): A list of 'image_id' values. |
| outname (str): The path and filename for the output TIFF. |
| pred_only (bool, optional): If True, overlays predictions on a |
| black background instead of the original image. Defaults to False. |
| """ |
| imgs = self.output_pred_to_list(imgids, pred_only) |
| self.save_imgarr_to_tiff(imgs, outname) |
|
|
| def output_pred_to_list(self, imgids, pred_only=False): |
| """Generates a list of images with model predictions overlaid. |
| |
| Args: |
| imgids (list[str]): A list of 'image_id' values. |
| pred_only (bool, optional): If True, overlays predictions on a |
| black background. Defaults to False. |
| |
| Returns: |
| list[PIL.Image]: A list of the generated visualization images. |
| """ |
| imgs = [] |
| for imgid in tqdm(imgids): |
| dat = self.get_gt_image_data(imgid) |
| if pred_only: |
| im = np.zeros((dat["height"], dat["width"], 3)) |
| assert ( |
| self._mode == "file" |
| ), 'pred_mode must be "file" when pred_only flage is set to True.' |
| else: |
| im = cv2.imread(dat["file_name"]) |
| img_dt = self.produce_model_image(imgid, dat, im) |
| imgs.append(img_dt) |
| return imgs |
|
|
| def output_all_to_tiff(self, imgids, outname): |
| """Saves a combined visualization (original, GT, prediction) to a TIFF. |
| |
| For each image ID, it creates a single composite image by concatenating |
| the original, ground truth overlay, and model prediction overlay, then |
| saves them to a multi-page TIFF. |
| |
| Args: |
| imgids (list[str]): A list of 'image_id' values. |
| outname (str): The path and filename for the output TIFF. |
| """ |
| imgs = [] |
| for imgid in tqdm(imgids): |
| img_gt, img_dt = self.get_image(imgid) |
| img_ori = self.get_ori_image(imgid) |
| hcrange = list(self.height_crop_range(np.array(img_ori.convert("L")), height_target=256 * self.scale)) |
| img_result = Image.fromarray( |
| np.concatenate( |
| ( |
| np.array(img_ori.convert("RGB"))[hcrange, :], |
| np.array(img_gt)[hcrange, :], |
| np.array(img_dt)[hcrange], |
| ) |
| ) |
| ) |
| imgs.append(img_result) |
| self.save_imgarr_to_tiff(imgs, outname) |
|
|
| def get_enface_dt(self, grp, scan_height, scan_width, scan_spacing): |
| """Generates an en-face view of model predictions for a scan volume. |
| |
| Args: |
| grp (pd.DataFrame): DataFrame for a single scan volume, indexed by imgid. |
| scan_height (int): The height of a single scan image in pixels. |
| scan_width (int): The width of a single scan image in pixels. |
| scan_spacing (float): The spacing between scan centers in pixels. |
| |
| Returns: |
| np.ndarray: An en-face image of the model predictions. |
| """ |
| grp = grp.sort_index() |
| nscans = len(grp) |
| enface_height = int(np.ceil((nscans - 1) * scan_spacing)) |
| enface = np.zeros((enface_height, scan_width, 3), dtype=int) |
| for i, imgid in enumerate(grp.index): |
| pos = int(np.clip(np.floor(scan_spacing * i), 0, scan_width - 1)) |
|
|
| outputs = self.get_outputs_from_file(imgid, (scan_height, scan_width)) |
| outputs = outputs[outputs.scores > self.prob_thresh] |
| instances = outputs.pred_boxes[:, (0, 2)].round().clip(0, scan_width - 1).to(np.int) |
|
|
| for inst in instances: |
| try: |
| enface[max(pos - 4, 0) : min(pos + 4, scan_width - 1), inst[0] : inst[1]] = np.array( |
| [255, 255, 255] |
| ) |
| except IndexError: |
| print(pos, inst[0], inst[1]) |
| return enface |
|
|
| def get_enface_gt(self, grp, scan_height, scan_width, scan_spacing): |
| """Generates an en-face view of ground truth annotations for a scan volume. |
| |
| Args: |
| grp (pd.DataFrame): DataFrame for a single scan volume, indexed by imgid. |
| scan_height (int): The height of a single scan image in pixels. |
| scan_width (int): The width of a single scan image in pixels. |
| scan_spacing (float): The spacing between scan centers in pixels. |
| |
| Returns: |
| np.ndarray: An en-face image of the ground truth annotations. |
| """ |
| grp = grp.sort_index() |
| nscans = len(grp) |
| enface_height = int(np.ceil((nscans - 1) * scan_spacing)) |
| enface = np.zeros((enface_height, scan_width, 3), dtype=int) |
| if not self.has_annotations: |
| enface[:, :] = np.array([100, 100, 100]) |
|
|
| else: |
| |
| for i, imgid in enumerate(grp.index): |
| pos = int(np.clip(np.floor(scan_spacing * i), 0, scan_width - 1)) |
| instances = self.get_gt_image_data(imgid)["annotations"] |
| for inst in instances: |
| x1 = inst["bbox"][0] |
| |
| x2 = x1 + inst["bbox"][2] |
| try: |
| enface[max(pos - 4, 0) : min(pos + 4, scan_width - 1), x1:x2] = np.array( |
| [255, 255, 255] |
| ) |
| except IndexError: |
| print(pos, x1, x2) |
| return enface |
|
|
| def compare_enface(self, grp, name, scan_height, scan_width, scan_spacing): |
| """Creates a figure comparing the en-face views of predictions and ground truth. |
| |
| Args: |
| grp (pd.DataFrame): DataFrame for a single scan volume, indexed by imgid. |
| name (str): The name/ID of the scan volume for the plot title. |
| scan_height (int): The height of a single scan image in pixels. |
| scan_width (int): The width of a single scan image in pixels. |
| scan_spacing (float): The spacing between scan centers in pixels. |
| |
| Returns: |
| tuple[plt.Figure, np.ndarray]: A tuple containing the figure and axes objects. |
| """ |
| fig, ax = plt.subplots(1, 2, figsize=[18, 9], dpi=120) |
|
|
| enface = self.get_enface_dt(grp, scan_height, scan_width, scan_spacing) |
| ax[0].imshow(enface) |
| ax[0].set_title(str(name) + " DT") |
| ax[0].set_aspect("equal") |
|
|
| enface = self.get_enface_gt(grp, scan_height, scan_width, scan_spacing) |
| ax[1].imshow(enface) |
| ax[1].set_title(str(name) + " GT") |
| ax[1].set_aspect("equal") |
| return fig, ax |
|
|
|
|
| def wilson_ci(p, n, z): |
| """Calculates the Wilson score interval for a binomial proportion. |
| |
| Args: |
| p (float): The observed proportion of successes. |
| n (int): The total number of trials. |
| z (float): The z-score for the desired confidence level (e.g., 1.96 for 95%). |
| |
| Returns: |
| tuple[float, float]: A tuple containing the lower and upper bounds of the confidence interval. |
| """ |
| if p < 0 or p > 1 or n == 0: |
| if p < 0 or p > 1: |
| warnings.warn(f"The value of proportion {p} must be in the range [0,1]. Returning identity for CIs.") |
| else: |
| warnings.warn(f"The number of counts {n} must be above zero. Returning identity for CIs.") |
| return (p, p) |
| sym = z * (p * (1 - p) / n + z * z / 4 / n / n) ** 0.5 |
| asym = p + z * z / 2 / n |
| fact = 1 / (1 + z * z / n) |
| upper = fact * (asym + sym) |
| lower = fact * (asym - sym) |
| return (lower, upper) |
|
|
|
|
| class EvaluateClass(COCOEvaluator): |
| """A custom evaluation class extending COCOEvaluator for detailed analysis.""" |
|
|
| def __init__(self, dataset_name, output_dir, prob_thresh=0.5, iou_thresh=0.1, evalsuper=True): |
| """Initializes the custom evaluator. |
| |
| Args: |
| dataset_name (str): The name of the registered Detectron2 dataset. |
| output_dir (str): Directory to store temporary evaluation files. |
| prob_thresh (float, optional): Probability threshold for calculating |
| precision, recall, and FPR. Defaults to 0.5. |
| iou_thresh (float, optional): IoU threshold for defining a true positive. |
| Defaults to 0.1. |
| evalsuper (bool, optional): If True, run the parent COCOEvaluator's |
| evaluate method to generate standard COCO metrics. Defaults to True. |
| """ |
| super().__init__(dataset_name, tasks={"bbox", "segm"}, output_dir=output_dir) |
| self.dataset_name = dataset_name |
| self.mycoco = None |
| self.cocoDt = None |
| self.cocoGt = None |
| self.evalsuper = evalsuper |
| self.prob_thresh = prob_thresh |
| self.iou_thresh = iou_thresh |
| self.pr = None |
| self.rc = None |
| self.fpr = None |
|
|
| def reset(self): |
| """Resets the evaluator's state for a new evaluation run.""" |
| super().reset() |
| self.mycoco = None |
|
|
| def process(self, inputs, outputs): |
| """Processes a batch of inputs and outputs from the model. |
| |
| This method is called by the evaluation loop for each batch. |
| |
| Args: |
| inputs (list[dict]): A list of dataset dictionaries. |
| outputs (list[dict]): A list of model output dictionaries. |
| """ |
| super().process(inputs, outputs) |
|
|
| def evaluate(self): |
| """Runs the evaluation and calculates detailed performance metrics. |
| |
| This method orchestrates the COCO evaluation, calculates precision-recall |
| curves, and other custom metrics. |
| |
| Returns: |
| tuple[float, float]: The precision and recall at the specified |
| `prob_thresh` and `iou_thresh`. |
| """ |
| if self.evalsuper: |
| _ = super().evaluate() |
| comm.synchronize() |
| if not comm.is_main_process(): |
| return () |
| self.cocoGt = COCO( |
| os.path.join(self._output_dir, self.dataset_name + "_coco_format.json") |
| ) |
| self.cocoDt = self.cocoGt.loadRes( |
| os.path.join(self._output_dir, "coco_instances_results.json") |
| ) |
| self.mycoco = COCOeval(self.cocoGt, self.cocoDt, iouType="segm") |
| self.num_images = len(self.mycoco.params.imgIds) |
| print("Calculated metrics for {} images".format(self.num_images)) |
| self.mycoco.params.iouThrs = np.arange(0.10, 0.6, 0.1) |
| self.mycoco.params.maxDets = [100] |
| self.mycoco.params.areaRng = [[0, 10000000000.0]] |
|
|
| self.mycoco.evaluate() |
| self.mycoco.accumulate() |
|
|
| self.pr = self.mycoco.eval["precision"][ |
| :, :, 0, 0, 0 |
| ] |
| self.rc = self.mycoco.params.recThrs |
| self.iou = self.mycoco.params.iouThrs |
| self.scores = self.mycoco.eval["scores"][:, :, 0, 0, 0] |
| p, r = self.get_precision_recall() |
| return p, r |
|
|
| def plot_pr_curve(self, ax=None): |
| """Plots precision-recall curves for various IoU thresholds. |
| |
| Args: |
| ax (plt.Axes, optional): A matplotlib axes object to plot on. If None, |
| a new figure and axes are created. |
| """ |
| if ax is None: |
| fig, ax = plt.subplots(1, 1) |
| for i in range(len(self.iou)): |
| ax.plot(self.rc, self.pr[i], label="{:.2}".format(self.iou[i])) |
| ax.set_xlabel("Recall") |
| ax.set_ylabel("Precision") |
| ax.set_title("") |
| ax.legend(title="IoU") |
|
|
| def plot_recall_vs_prob(self): |
| """Plots model score thresholds versus recall for various IoU thresholds.""" |
| plt.figure() |
| for i in range(len(self.iou)): |
| plt.plot(self.rc, self.scores[i], label="{:.2}".format(self.iou[i])) |
| plt.ylabel("Model probability") |
| plt.xlabel("Recall") |
| plt.legend(title="IoU") |
|
|
| def get_precision_recall(self): |
| """Gets the precision and recall for the configured IoU and probability thresholds. |
| |
| Returns: |
| tuple[float, float]: The calculated precision and recall. |
| """ |
| iou_idx, rc_idx = self._find_iou_rc_inds() |
| precision = self.pr[iou_idx, rc_idx] |
| recall = self.rc[rc_idx] |
| return precision, recall |
|
|
| def _calculate_fpr_matrix(self): |
| """(Private) Calculates the false positive rate matrix across all IoU and recall thresholds.""" |
|
|
| |
| if (self.scores.min() == -1) and (self.scores.max() == -1): |
| print( |
| "WARNING: Scores for all iou thresholds and all recall levels are not defined. " |
| "This can arise if ground truth annotations contain no instances. Leaving fpr matrix as None" |
| ) |
| self.fpr = None |
| return |
|
|
| fpr = np.zeros((len(self.iou), len(self.rc))) |
| for i in range(len(self.iou)): |
| for j, s in enumerate(self.scores[i]): |
| ng = 0 |
| fp = 0 |
| for el in self.mycoco.evalImgs: |
| if el is None: |
| ng = ng + 1 |
| elif len(el["gtIds"]) == 0: |
| ng = ng + 1 |
| if ( |
| np.array(el["dtScores"]) > s |
| ).sum() > 0: |
| fp = fp + 1 |
| else: |
| continue |
| fpr[i, j] = fp / ng |
| self.fpr = fpr |
|
|
| def _calculate_fpr(self): |
| """(Private) Calculates FPR for a single probability threshold. |
| |
| This is an alternate calculation used when the main FPR matrix cannot |
| be computed (e.g., no positive ground truth instances). |
| |
| Returns: |
| float: The calculated false positive rate. |
| """ |
| print("Using alternate calculation for fpr at instance score threshold of {}".format(self.prob_thresh)) |
| ng = 0 |
| fp = 0 |
| for el in self.mycoco.evalImgs: |
| if el is None: |
| ng = ng + 1 |
| elif len(el["gtIds"]) == 0: |
| ng = ng + 1 |
| if ( |
| np.array(el["dtScores"]) > self.prob_thresh |
| ).sum() > 0: |
| fp = fp + 1 |
| else: |
| continue |
| return fp / (ng + 1e-5) |
|
|
| def _find_iou_rc_inds(self): |
| """(Private) Finds the indices corresponding to the configured IoU and probability thresholds. |
| |
| Returns: |
| tuple[int, int]: The index for the IoU threshold and the index for the recall level. |
| """ |
| try: |
| iou_idx = np.argwhere(self.iou == self.iou_thresh)[0][0] |
| except IndexError: |
| print( |
| "iou threshold {} not found in mycoco.params.iouThrs {}".format( |
| self.iou_thresh, self.mycoco.params.iouThrs |
| ) |
| ) |
| exit(1) |
| |
| inds = np.argwhere(self.scores[iou_idx] >= self.prob_thresh) |
| if len(inds) > 0: |
| rc_idx = inds[-1][0] |
| else: |
| rc_idx = 0 |
| return iou_idx, rc_idx |
|
|
| def get_fpr(self): |
| """Gets the false positive rate for the configured thresholds. |
| |
| Returns: |
| float: The calculated false positive rate. Returns -1 if it cannot be computed. |
| """ |
| if self.fpr is None: |
| self._calculate_fpr_matrix() |
|
|
| if self.fpr is not None: |
| iou_idx, rc_idx = self._find_iou_rc_inds() |
| fpr = self.fpr[iou_idx, rc_idx] |
| elif len(self.mycoco.cocoGt.anns) == 0: |
| fpr = self._calculate_fpr() |
| else: |
| fpr = -1 |
| return fpr |
|
|
| def summarize_scalars(self): |
| """Generates a dictionary summarizing key performance metrics with confidence intervals. |
| |
| Returns: |
| dict: A dictionary containing precision, recall, F1-score, FPR, |
| and their confidence intervals. |
| """ |
| p, r = self.get_precision_recall() |
| f1 = 2 * (p * r) / (p + r) |
| fpr = self.get_fpr() |
|
|
| |
| z = 1.96 |
| |
| inst_cnt = self.count_instances() |
| n_r = inst_cnt["gt_instances"] |
| n_p = inst_cnt["dt_instances"] |
| n_fpr = inst_cnt["gt_neg_scans"] |
|
|
| def stat_ci(p, n, z): |
| return z * np.sqrt(p * (1 - p) / n) |
|
|
| r_ci = wilson_ci(r, n_r, z) |
| p_ci = wilson_ci(p, n_p, z) |
| fpr_ci = wilson_ci(fpr, n_fpr, z) |
|
|
| |
| int_r = stat_ci(r, n_r, z) |
| int_p = stat_ci(p, n_p, z) |
| int_f1 = (f1) * np.sqrt(int_r**2 * (1 / r - 1 / (p + r)) ** 2 + int_p**2 * (1 / p - 1 / (p + r)) ** 2) |
| f1_ci = (f1 - int_f1, f1 + int_f1) |
|
|
| dd = dict( |
| dataset=self.dataset_name, |
| precision=float(p), |
| precision_ci=p_ci, |
| recall=float(r), |
| recall_ci=r_ci, |
| f1=float(f1), |
| f1_ci=f1_ci, |
| fpr=float(fpr), |
| fpr_ci=fpr_ci, |
| iou=self.iou_thresh, |
| probability=self.prob_thresh, |
| ) |
| return dd |
|
|
| def count_instances(self): |
| """Counts ground truth and detected instances across the dataset. |
| |
| Returns: |
| dict: A dictionary with counts for 'gt_instances', 'dt_instances', |
| and 'gt_neg_scans' (images with no GT instances). |
| """ |
| gt_inst = 0 |
| dt_inst = 0 |
| gt_neg_scans = 0 |
| for _, val in self.cocoGt.imgs.items(): |
| imgid = val["id"] |
| |
| annids_gt = self.cocoGt.getAnnIds([imgid]) |
| anns_gt = self.cocoGt.loadAnns(annids_gt) |
| gt_inst += len(anns_gt) |
| if len(anns_gt) == 0: |
| gt_neg_scans += 1 |
|
|
| |
| annids_dt = self.cocoDt.getAnnIds([imgid]) |
| anns_dt = self.cocoDt.loadAnns(annids_dt) |
| anns_dt = [ann for ann in anns_dt if ann["score"] > self.prob_thresh] |
| dt_inst += len(anns_dt) |
|
|
| return dict(gt_instances=gt_inst, dt_instances=dt_inst, gt_neg_scans=gt_neg_scans) |
|
|
|
|
| class CreatePlotsRPD: |
| """A class to create various plots for analyzing RPD (Reticular Pseudodrusen) data.""" |
|
|
| def __init__(self, dfimg): |
| """Initializes the plotting class with image-level data. |
| |
| Args: |
| dfimg (pd.DataFrame): A DataFrame where each row corresponds to an |
| image, containing counts for ground truth and detected instances |
| and pixels. Must include a 'volID' column. |
| """ |
| self.dfimg = dfimg |
| self.dfvol = self.dfimg.groupby(["volID"])[ |
| ["gt_instances", "gt_pxs", "gt_xpxs", "dt_instances", "dt_pxs", "dt_xpxs"] |
| ].sum() |
|
|
| @classmethod |
| def initfromcoco(cls, mycoco, prob_thresh): |
| """Initializes the class from a COCOeval object. |
| |
| Args: |
| mycoco (COCOeval): An evaluated COCOeval object. |
| prob_thresh (float): The probability threshold to apply to detections. |
| |
| Returns: |
| CreatePlotsRPD: An instance of the class. |
| """ |
| df = pd.DataFrame( |
| index=mycoco.cocoGt.imgs.keys(), |
| columns=["gt_instances", "gt_pxs", "gt_xpxs", "dt_instances", "dt_pxs", "dt_xpxs"], |
| dtype=np.uint64, |
| ) |
|
|
| for key, val in mycoco.cocoGt.imgs.items(): |
| imgid = val["id"] |
| |
| annids_gt = mycoco.cocoGt.getAnnIds([imgid]) |
| anns_gt = mycoco.cocoGt.loadAnns(annids_gt) |
| inst_gt = [mycoco.cocoGt.annToMask(ann).sum() for ann in anns_gt] |
| xproj_gt = [(mycoco.cocoGt.annToMask(ann).sum(axis=0) > 0).astype("uint8").sum() for ann in anns_gt] |
| |
| annids_dt = mycoco.cocoDt.getAnnIds([imgid]) |
| anns_dt = mycoco.cocoDt.loadAnns(annids_dt) |
| anns_dt = [ann for ann in anns_dt if ann["score"] > prob_thresh] |
| inst_dt = [mycoco.cocoDt.annToMask(ann).sum() for ann in anns_dt] |
| xproj_dt = [(mycoco.cocoDt.annToMask(ann).sum(axis=0) > 0).astype("uint8").sum() for ann in anns_dt] |
|
|
| dat = [ |
| len(inst_gt), |
| np.array(inst_gt).sum(), |
| np.array(xproj_gt).sum(), |
| len(inst_dt), |
| np.array(inst_dt).sum(), |
| np.array(xproj_dt).sum(), |
| ] |
| df.loc[key] = dat |
|
|
| newdf = pd.DataFrame( |
| [idx.rsplit(".", 1)[0].rsplit("_", 1) for idx in df.index], columns=["volID", "scan"], index=df.index |
| ) |
| df = df.merge(newdf, how="inner", left_index=True, right_index=True) |
| return cls(df) |
|
|
| @classmethod |
| def initfromcsv(cls, fname): |
| """Initializes the class from a CSV file. |
| |
| Args: |
| fname (str): The path to the CSV file. |
| |
| Returns: |
| CreatePlotsRPD: An instance of the class. |
| """ |
| df = pd.read_csv(fname) |
| return cls(df) |
|
|
| def get_max_limits(self, df): |
| """Calculates the maximum values for plotting limits. |
| |
| Args: |
| df (pd.DataFrame): The DataFrame to analyze. |
| |
| Returns: |
| tuple[int, int, int]: Max values for instances, x-pixels, and total pixels. |
| """ |
| max_inst = np.max([df.gt_instances.max(), df.dt_instances.max()]) |
| max_xpxs = np.max([df.gt_xpxs.max(), df.dt_xpxs.max()]) |
| max_pxs = np.max([df.gt_pxs.max(), df.dt_pxs.max()]) |
| |
| |
| |
| return max_inst, max_xpxs, max_pxs |
|
|
| def vol_level_prc(self, df, gt_thresh=5, ax=None): |
| """Plots a volume-level precision-recall curve. |
| |
| Args: |
| df (pd.DataFrame): DataFrame with volume-level statistics. |
| gt_thresh (int, optional): The minimum number of ground truth |
| instances for a volume to be considered positive. Defaults to 5. |
| ax (plt.Axes, optional): Axes to plot on. Defaults to None. |
| |
| Returns: |
| tuple[float, tuple]: The average precision and the PR curve data. |
| """ |
| prc = precision_recall_curve(df.gt_instances >= gt_thresh, df.dt_instances) |
| if ax is None: |
| fig, ax = plt.subplots(1, 1) |
| ax.plot(prc[1], prc[0]) |
| ax.set_xlabel("RPD Volume Recall") |
| ax.set_ylabel("RPD Volume Precision") |
|
|
| ap = average_precision_score(df.gt_instances >= gt_thresh, df.dt_instances) |
| return ap, prc |
|
|
| def plot_img_level_instance_thresholding(self, df, inst): |
| """Plots P/R/FPR as a function of the instance count threshold. |
| |
| Args: |
| df (pd.DataFrame): DataFrame with image-level statistics. |
| inst (list[int]): A list of instance count thresholds to evaluate. |
| |
| Returns: |
| tuple[np.ndarray, np.ndarray, np.ndarray]: Arrays for precision, |
| recall, and FPR at each threshold. |
| """ |
| rc = np.zeros((len(inst),)) |
| pr = np.zeros((len(inst),)) |
| fpr = np.zeros((len(inst),)) |
|
|
| fig, ax = plt.subplots(1, 3, figsize=[15, 5]) |
| for i, dt_thresh in enumerate(inst): |
| gt = df.gt_instances > dt_thresh |
| dt = df.dt_instances > dt_thresh |
| rc[i] = (gt & dt).sum() / gt.sum() |
| pr[i] = (gt & dt).sum() / dt.sum() |
| fpr[i] = ((~gt) & (dt)).sum() / ((~gt).sum()) |
|
|
| ax[1].plot(inst, pr) |
| ax[1].set_ylim(0.45, 1.01) |
| ax[1].set_xlabel("instance threshold") |
| ax[1].set_ylabel("Precision") |
|
|
| ax[0].plot(inst, rc) |
| ax[0].set_ylim(0.45, 1.01) |
| ax[0].set_ylabel("Recall") |
| ax[0].set_xlabel("instance threshold") |
|
|
| ax[2].plot(inst, fpr) |
| ax[2].set_ylim(0, 0.80) |
| ax[2].set_xlabel("instance threshold") |
| ax[2].set_ylabel("FPR") |
|
|
| plt.tight_layout() |
| return pr, rc, fpr |
|
|
| def plot_img_level_instance_thresholding2(self, df, inst, gt_thresh, plot=True): |
| """Plots P/R/FPR vs. instance threshold with confidence intervals. |
| |
| Args: |
| df (pd.DataFrame): DataFrame with image-level statistics. |
| inst (list[int]): A list of instance count thresholds to evaluate. |
| gt_thresh (int): The ground truth instance threshold. |
| plot (bool, optional): Whether to generate a plot. Defaults to True. |
| |
| Returns: |
| dict: A dictionary containing arrays for P/R/FPR and their CIs. |
| """ |
|
|
| rc = np.zeros((len(inst),)) |
| pr = np.zeros((len(inst),)) |
| fpr = np.zeros((len(inst),)) |
| rc_ci = np.zeros((len(inst), 2)) |
| pr_ci = np.zeros((len(inst), 2)) |
| fpr_ci = np.zeros((len(inst), 2)) |
|
|
| for i, dt_thresh in enumerate(inst): |
| gt = df.gt_instances >= gt_thresh |
| dt = df.dt_instances >= dt_thresh |
| rc[i] = (gt & dt).sum() / gt.sum() |
| pr[i] = (gt & dt).sum() / dt.sum() |
| fpr[i] = ((~gt) & (dt)).sum() / ((~gt).sum()) |
| rc_ci[i, :] = wilson_ci(rc[i], gt.sum(), 1.96) |
| pr_ci[i, :] = wilson_ci(pr[i], dt.sum(), 1.96) |
| fpr_ci[i, :] = wilson_ci(fpr[i], ((~gt).sum()), 1.96) |
|
|
| if plot: |
| fig, ax = plt.subplots(1, 3, figsize=[15, 5]) |
| |
| |
| |
|
|
| ax[1].plot(inst, pr) |
| ax[1].fill_between(inst, pr_ci[:, 0], pr_ci[:, 1], alpha=0.25) |
| |
| ax[1].set_xlabel("instance threshold") |
| ax[1].set_ylabel("Precision") |
|
|
| ax[0].plot(inst, rc) |
| ax[0].fill_between(inst, rc_ci[:, 0], rc_ci[:, 1], alpha=0.25) |
| |
| ax[0].set_ylabel("Recall") |
| ax[0].set_xlabel("instance threshold") |
|
|
| ax[2].plot(inst, fpr) |
| ax[2].fill_between(inst, fpr_ci[:, 0], fpr_ci[:, 1], alpha=0.25) |
| |
| ax[2].set_xlabel("instance threshold") |
| ax[2].set_ylabel("FPR") |
|
|
| plt.tight_layout() |
| return dict(precision=pr, precision_ci=pr_ci, recall=rc, recall_ci=rc_ci, fpr=fpr, fpr_ci=fpr_ci) |
|
|
| def gt_vs_dt_instances(self, ax=None): |
| """Plots mean detected instances vs. ground truth instances with error bars. |
| |
| Args: |
| ax (plt.Axes, optional): Axes to plot on. Defaults to None. |
| |
| Returns: |
| plt.Axes: The axes object with the plot. |
| """ |
| df = self.dfimg |
| max_inst, max_xpxs, max_pxs = self.get_max_limits(df) |
| idx = (df.gt_instances > 0) & (df.dt_instances > 0) |
|
|
| if ax is None: |
| fig = plt.figure(dpi=100) |
| ax = fig.add_subplot(111) |
|
|
| y = df[idx].groupby("gt_instances")["dt_instances"].mean() |
| yerr = df[idx].groupby("gt_instances")["dt_instances"].std() |
| ax.errorbar(y.index, y.values, yerr.values, fmt="*") |
| plt.plot([0, max_inst], [0, max_inst], alpha=0.5) |
| plt.xlim(0, max_inst + 1) |
| plt.ylim(0, max_inst + 1) |
| ax.set_aspect(1) |
| plt.xlabel("gt_instances") |
| plt.ylabel("dt_instances") |
| plt.tight_layout() |
| return ax |
|
|
| def gt_vs_dt_instances_boxplot(self, ax=None): |
| """Creates a boxplot of detected instances for each ground truth instance count. |
| |
| Args: |
| ax (plt.Axes, optional): Axes to plot on. Defaults to None. |
| |
| Returns: |
| plt.Axes: The axes object with the plot. |
| """ |
| df = self.dfimg |
| max_inst, max_xpxs, max_pxs = self.get_max_limits(df) |
| max_inst = int(max_inst) |
| if ax is None: |
| fig = plt.figure(dpi=100) |
| ax = fig.add_subplot(111) |
|
|
| ax.plot([0, max_inst + 1], [0, max_inst + 1], alpha=0.5) |
| x = df["gt_instances"].values.astype(int) |
| y = df["dt_instances"].values.astype(int) |
| sns.boxplot(x, y, ax=ax, width=0.5) |
| ax.set_xbound(0, max_inst + 1) |
| ax.set_ybound(0, max_inst + 1) |
| ax.set_aspect("equal") |
|
|
| ax.set_title("") |
| ax.set_xlabel("gt_instances") |
| ax.set_ylabel("dt_instances") |
|
|
| import matplotlib.ticker as pltticker |
|
|
| loc = pltticker.MultipleLocator(base=2.0) |
| ax.xaxis.set_major_locator(loc) |
| ax.yaxis.set_major_locator(loc) |
|
|
| return ax |
|
|
| def gt_vs_dt_xpxs(self): |
| """Creates scatter plots comparing ground truth and detected x-pixels. |
| |
| Returns: |
| tuple[plt.Figure, plt.Figure, plt.Figure]: Figure handles for the three generated plots. |
| """ |
| df = self.dfimg |
| max_inst, max_xpxs, max_pxs = self.get_max_limits(df) |
| idx = (df.gt_instances > 0) & (df.dt_instances > 0) |
| dfsub = df[idx] |
|
|
| fig1 = plt.figure(figsize=[10, 10], dpi=100) |
| ax = fig1.add_subplot(111) |
| sc = ax.scatter(dfsub["gt_xpxs"], dfsub["dt_xpxs"], c=dfsub["gt_instances"], cmap="viridis") |
| ax.set_aspect(1) |
| |
| plt.plot([0, max_xpxs], [0, max_xpxs], alpha=0.5) |
| plt.xlim(0, max_xpxs) |
| plt.ylim(0, max_xpxs) |
| plt.xlabel("gt_xpxs") |
| plt.ylabel("dt_xpxs") |
| cbar = plt.colorbar(sc) |
| cbar.ax.set_ylabel("gt_instances") |
| plt.tight_layout() |
|
|
| fig2 = plt.figure(figsize=[10, 10], dpi=100) |
| ax = fig2.add_subplot(111) |
| sc = ax.scatter(dfsub["gt_xpxs"], dfsub["gt_xpxs"] - dfsub["dt_xpxs"], c=dfsub["gt_instances"], cmap="viridis") |
| |
| plt.plot([0, max_xpxs], [0, 0], alpha=0.5) |
| plt.xlabel("gt_xpxs") |
| plt.ylabel("gt_xpxs-dt_xpxs") |
| cbar = plt.colorbar(sc) |
| cbar.ax.set_ylabel("gt_instances") |
| plt.tight_layout() |
|
|
| fig3 = plt.figure(dpi=100) |
| plt.hist(dfsub["gt_xpxs"] - dfsub["dt_xpxs"]) |
| plt.xlabel("gt_xpxs - dt_xpxs") |
| plt.ylabel("B-scans") |
|
|
| return fig1, fig2, fig3 |
|
|
| def gt_vs_dt_xpxs_mu(self): |
| """Plots binned means of detected vs. ground truth x-pixels. |
| |
| Returns: |
| plt.Figure: The figure handle for the plot. |
| """ |
| df = self.dfimg |
| max_inst, max_xpxs, max_pxs = self.get_max_limits(df) |
| idx = (df.gt_instances > 0) & (df.dt_instances > 0) |
| dfsub = df[idx] |
|
|
| from scipy import stats |
|
|
| mu_dt, bins, bnum = stats.binned_statistic(dfsub["gt_xpxs"], dfsub["dt_xpxs"], statistic="mean", bins=10) |
| std_dt, _, _ = stats.binned_statistic(dfsub["gt_xpxs"], dfsub["dt_xpxs"], statistic="std", bins=bins) |
| mu_gt, _, _ = stats.binned_statistic(dfsub["gt_xpxs"], dfsub["gt_xpxs"], statistic="mean", bins=bins) |
| std_gt, _, _ = stats.binned_statistic(dfsub["gt_xpxs"], dfsub["gt_xpxs"], statistic="std", bins=bins) |
| fig = plt.figure(dpi=100) |
| plt.errorbar(mu_gt, mu_dt, yerr=std_dt, xerr=std_gt, fmt="*") |
| plt.xlabel("gt_xpxs") |
| plt.ylabel("dt_xpxs") |
| plt.plot([0, max_xpxs], [0, max_xpxs], alpha=0.5) |
| plt.xlim(0, max_xpxs) |
| plt.ylim(0, max_xpxs) |
| plt.gca().set_aspect(1) |
| plt.tight_layout() |
| return fig |
|
|
| def gt_dt_fp_fn_count(self): |
| """Plots histograms of false positive and false negative instance counts. |
| |
| Returns: |
| plt.Figure: The figure handle for the plot. |
| """ |
| df = self.dfimg |
| fig, ax = plt.subplots(1, 2, figsize=[10, 5]) |
|
|
| idx = (df.gt_instances == 0) & (df.dt_instances > 0) |
| ax[0].hist(df[idx]["dt_instances"], bins=range(1, 10)) |
| ax[0].set_xlabel("dt instances") |
| ax[0].set_ylabel("B-scans") |
| ax[0].set_title("FP dt instance count per B-scan") |
|
|
| idx = (df.gt_instances > 0) & (df.dt_instances == 0) |
| ax[1].hist(df[idx]["gt_instances"], bins=range(1, 10)) |
| ax[1].set_xlabel("gt instances") |
| ax[1].set_ylabel("B-scans") |
| ax[1].set_title("FN gt instance count per B-scan") |
|
|
| plt.tight_layout() |
| return fig |
|
|
| def avg_inst_size(self): |
| """Plots histograms of the average instance size in pixels. |
| |
| Compares the average size (in both total pixels and x-axis projection) |
| between ground truth and detected instances. |
| |
| Returns: |
| plt.Figure: The figure handle for the plot. |
| """ |
| df = self.dfimg |
| max_inst, max_xpxs, max_pxs = self.get_max_limits(df) |
| idx = (df.gt_instances > 0) & (df.dt_instances > 0) |
| dfsub = df[idx] |
|
|
| fig = plt.figure(figsize=[10, 5]) |
| plt.subplot(121) |
| bins = np.arange(0, 120, 10) |
| ax = (dfsub.gt_xpxs / dfsub.gt_instances).hist(bins=bins, alpha=0.5, label="gt") |
| ax = (dfsub.dt_xpxs / dfsub.dt_instances).hist(bins=bins, alpha=0.5, label="dt") |
| ax.set_xlabel("xpxs") |
| ax.set_ylabel("B-scans") |
| ax.set_title("Average size of instance") |
| ax.legend() |
|
|
| plt.subplot(122) |
| bins = np.arange(0, 600, 40) |
| ax = (dfsub.gt_pxs / dfsub.gt_instances).hist(bins=bins, alpha=0.5, label="gt") |
| ax = (dfsub.dt_pxs / dfsub.dt_instances).hist(bins=bins, alpha=0.5, label="dt") |
| ax.set_xlabel("pxs") |
| ax.set_ylabel("B-scans") |
| ax.set_title("Average size of instance") |
| ax.legend() |
|
|
| plt.tight_layout() |
| return fig |
|
|