# Copyright (c) OpenMMLab. All rights reserved. import numpy as np from shapely.geometry import Polygon as plg import mmocr.utils as utils def ignore_pred(pred_boxes, gt_ignored_index, gt_polys, precision_thr): """Ignore the predicted box if it hits any ignored ground truth. Args: pred_boxes (list[ndarray or list]): The predicted boxes of one image. gt_ignored_index (list[int]): The ignored ground truth index list. gt_polys (list[Polygon]): The polygon list of one image. precision_thr (float): The precision threshold. Returns: pred_polys (list[Polygon]): The predicted polygon list. pred_points (list[list]): The predicted box list represented by point sequences. pred_ignored_index (list[int]): The ignored text index list. """ assert isinstance(pred_boxes, list) assert isinstance(gt_ignored_index, list) assert isinstance(gt_polys, list) assert 0 <= precision_thr <= 1 pred_polys = [] pred_points = [] pred_ignored_index = [] gt_ignored_num = len(gt_ignored_index) # get detection polygons for box_id, box in enumerate(pred_boxes): poly = points2polygon(box) pred_polys.append(poly) pred_points.append(box) if gt_ignored_num < 1: continue # ignore the current detection box # if its overlap with any ignored gt > precision_thr for ignored_box_id in gt_ignored_index: ignored_box = gt_polys[ignored_box_id] inter_area = poly_intersection(poly, ignored_box) area = poly.area precision = 0 if area == 0 else inter_area / area if precision > precision_thr: pred_ignored_index.append(box_id) break return pred_polys, pred_points, pred_ignored_index def compute_hmean(accum_hit_recall, accum_hit_prec, gt_num, pred_num): """Compute hmean given hit number, ground truth number and prediction number. Args: accum_hit_recall (int|float): Accumulated hits for computing recall. accum_hit_prec (int|float): Accumulated hits for computing precision. gt_num (int): Ground truth number. pred_num (int): Prediction number. Returns: recall (float): The recall value. precision (float): The precision value. hmean (float): The hmean value. """ assert isinstance(accum_hit_recall, (float, int)) assert isinstance(accum_hit_prec, (float, int)) assert isinstance(gt_num, int) assert isinstance(pred_num, int) assert accum_hit_recall >= 0.0 assert accum_hit_prec >= 0.0 assert gt_num >= 0.0 assert pred_num >= 0.0 if gt_num == 0: recall = 1.0 precision = 0.0 if pred_num > 0 else 1.0 else: recall = float(accum_hit_recall) / gt_num precision = 0.0 if pred_num == 0 else float(accum_hit_prec) / pred_num denom = recall + precision hmean = 0.0 if denom == 0 else (2.0 * precision * recall / denom) return recall, precision, hmean def box2polygon(box): """Convert box to polygon. Args: box (ndarray or list): A ndarray or a list of shape (4) that indicates 2 points. Returns: polygon (Polygon): A polygon object. """ if isinstance(box, list): box = np.array(box) assert isinstance(box, np.ndarray) assert box.size == 4 boundary = np.array( [box[0], box[1], box[2], box[1], box[2], box[3], box[0], box[3]]) point_mat = boundary.reshape([-1, 2]) return plg(point_mat) def points2polygon(points): """Convert k points to 1 polygon. Args: points (ndarray or list): A ndarray or a list of shape (2k) that indicates k points. Returns: polygon (Polygon): A polygon object. """ if isinstance(points, list): points = np.array(points) assert isinstance(points, np.ndarray) assert (points.size % 2 == 0) and (points.size >= 8) point_mat = points.reshape([-1, 2]) return plg(point_mat) def poly_make_valid(poly): """Convert a potentially invalid polygon to a valid one by eliminating self-crossing or self-touching parts. Args: poly (Polygon): A polygon needed to be converted. Returns: A valid polygon. """ return poly if poly.is_valid else poly.buffer(0) def poly_intersection(poly_det, poly_gt, invalid_ret=None, return_poly=False): """Calculate the intersection area between two polygon. Args: poly_det (Polygon): A polygon predicted by detector. poly_gt (Polygon): A gt polygon. invalid_ret (None|float|int): The return value when the invalid polygon exists. If it is not specified, the function allows the computation to proceed with invalid polygons by cleaning the their self-touching or self-crossing parts. return_poly (bool): Whether to return the polygon of the intersection area. Returns: intersection_area (float): The intersection area between two polygons. poly_obj (Polygon, optional): The Polygon object of the intersection area. Set as `None` if the input is invalid. """ assert isinstance(poly_det, plg) assert isinstance(poly_gt, plg) assert invalid_ret is None or isinstance(invalid_ret, float) or \ isinstance(invalid_ret, int) if invalid_ret is None: poly_det = poly_make_valid(poly_det) poly_gt = poly_make_valid(poly_gt) poly_obj = None area = invalid_ret if poly_det.is_valid and poly_gt.is_valid: poly_obj = poly_det.intersection(poly_gt) area = poly_obj.area return (area, poly_obj) if return_poly else area def poly_union(poly_det, poly_gt, invalid_ret=None, return_poly=False): """Calculate the union area between two polygon. Args: poly_det (Polygon): A polygon predicted by detector. poly_gt (Polygon): A gt polygon. invalid_ret (None|float|int): The return value when the invalid polygon exists. If it is not specified, the function allows the computation to proceed with invalid polygons by cleaning the their self-touching or self-crossing parts. return_poly (bool): Whether to return the polygon of the intersection area. Returns: union_area (float): The union area between two polygons. poly_obj (Polygon|MultiPolygon, optional): The Polygon or MultiPolygon object of the union of the inputs. The type of object depends on whether they intersect or not. Set as `None` if the input is invalid. """ assert isinstance(poly_det, plg) assert isinstance(poly_gt, plg) assert invalid_ret is None or isinstance(invalid_ret, float) or \ isinstance(invalid_ret, int) if invalid_ret is None: poly_det = poly_make_valid(poly_det) poly_gt = poly_make_valid(poly_gt) poly_obj = None area = invalid_ret if poly_det.is_valid and poly_gt.is_valid: poly_obj = poly_det.union(poly_gt) area = poly_obj.area return (area, poly_obj) if return_poly else area def boundary_iou(src, target, zero_division=0): """Calculate the IOU between two boundaries. Args: src (list): Source boundary. target (list): Target boundary. zero_division (int|float): The return value when invalid boundary exists. Returns: iou (float): The iou between two boundaries. """ assert utils.valid_boundary(src, False) assert utils.valid_boundary(target, False) src_poly = points2polygon(src) target_poly = points2polygon(target) return poly_iou(src_poly, target_poly, zero_division=zero_division) def poly_iou(poly_det, poly_gt, zero_division=0): """Calculate the IOU between two polygons. Args: poly_det (Polygon): A polygon predicted by detector. poly_gt (Polygon): A gt polygon. zero_division (int|float): The return value when invalid polygon exists. Returns: iou (float): The IOU between two polygons. """ assert isinstance(poly_det, plg) assert isinstance(poly_gt, plg) area_inters = poly_intersection(poly_det, poly_gt) area_union = poly_union(poly_det, poly_gt) return area_inters / area_union if area_union != 0 else zero_division def one2one_match_ic13(gt_id, det_id, recall_mat, precision_mat, recall_thr, precision_thr): """One-to-One match gt and det with icdar2013 standards. Args: gt_id (int): The ground truth id index. det_id (int): The detection result id index. recall_mat (ndarray): `gt_num x det_num` matrix with element (i,j) being the recall ratio of gt i to det j. precision_mat (ndarray): `gt_num x det_num` matrix with element (i,j) being the precision ratio of gt i to det j. recall_thr (float): The recall threshold. precision_thr (float): The precision threshold. Returns: True|False: Whether the gt and det are matched. """ assert isinstance(gt_id, int) assert isinstance(det_id, int) assert isinstance(recall_mat, np.ndarray) assert isinstance(precision_mat, np.ndarray) assert 0 <= recall_thr <= 1 assert 0 <= precision_thr <= 1 cont = 0 for i in range(recall_mat.shape[1]): if recall_mat[gt_id, i] > recall_thr and precision_mat[gt_id, i] > precision_thr: cont += 1 if cont != 1: return False cont = 0 for i in range(recall_mat.shape[0]): if recall_mat[i, det_id] > recall_thr and precision_mat[ i, det_id] > precision_thr: cont += 1 if cont != 1: return False if recall_mat[gt_id, det_id] > recall_thr and precision_mat[ gt_id, det_id] > precision_thr: return True return False def one2many_match_ic13(gt_id, recall_mat, precision_mat, recall_thr, precision_thr, gt_match_flag, det_match_flag, det_ignored_index): """One-to-Many match gt and detections with icdar2013 standards. Args: gt_id (int): gt index. recall_mat (ndarray): `gt_num x det_num` matrix with element (i,j) being the recall ratio of gt i to det j. precision_mat (ndarray): `gt_num x det_num` matrix with element (i,j) being the precision ratio of gt i to det j. recall_thr (float): The recall threshold. precision_thr (float): The precision threshold. gt_match_flag (ndarray): An array indicates each gt matched already. det_match_flag (ndarray): An array indicates each box has been matched already or not. det_ignored_index (list): A list indicates each detection box can be ignored or not. Returns: tuple (True|False, list): The first indicates the gt is matched or not; the second is the matched detection ids. """ assert isinstance(gt_id, int) assert isinstance(recall_mat, np.ndarray) assert isinstance(precision_mat, np.ndarray) assert 0 <= recall_thr <= 1 assert 0 <= precision_thr <= 1 assert isinstance(gt_match_flag, list) assert isinstance(det_match_flag, list) assert isinstance(det_ignored_index, list) many_sum = 0. det_ids = [] for det_id in range(recall_mat.shape[1]): if gt_match_flag[gt_id] == 0 and det_match_flag[ det_id] == 0 and det_id not in det_ignored_index: if precision_mat[gt_id, det_id] >= precision_thr: many_sum += recall_mat[gt_id, det_id] det_ids.append(det_id) if many_sum >= recall_thr: return True, det_ids return False, [] def many2one_match_ic13(det_id, recall_mat, precision_mat, recall_thr, precision_thr, gt_match_flag, det_match_flag, gt_ignored_index): """Many-to-One match gt and detections with icdar2013 standards. Args: det_id (int): Detection index. recall_mat (ndarray): `gt_num x det_num` matrix with element (i,j) being the recall ratio of gt i to det j. precision_mat (ndarray): `gt_num x det_num` matrix with element (i,j) being the precision ratio of gt i to det j. recall_thr (float): The recall threshold. precision_thr (float): The precision threshold. gt_match_flag (ndarray): An array indicates each gt has been matched already. det_match_flag (ndarray): An array indicates each detection box has been matched already or not. gt_ignored_index (list): A list indicates each gt box can be ignored or not. Returns: tuple (True|False, list): The first indicates the detection is matched or not; the second is the matched gt ids. """ assert isinstance(det_id, int) assert isinstance(recall_mat, np.ndarray) assert isinstance(precision_mat, np.ndarray) assert 0 <= recall_thr <= 1 assert 0 <= precision_thr <= 1 assert isinstance(gt_match_flag, list) assert isinstance(det_match_flag, list) assert isinstance(gt_ignored_index, list) many_sum = 0. gt_ids = [] for gt_id in range(recall_mat.shape[0]): if gt_match_flag[gt_id] == 0 and det_match_flag[ det_id] == 0 and gt_id not in gt_ignored_index: if recall_mat[gt_id, det_id] >= recall_thr: many_sum += precision_mat[gt_id, det_id] gt_ids.append(gt_id) if many_sum >= precision_thr: return True, gt_ids return False, [] def points_center(points): assert isinstance(points, np.ndarray) assert points.size % 2 == 0 points = points.reshape([-1, 2]) return np.mean(points, axis=0) def point_distance(p1, p2): assert isinstance(p1, np.ndarray) assert isinstance(p2, np.ndarray) assert p1.size == 2 assert p2.size == 2 dist = np.square(p2 - p1) dist = np.sum(dist) dist = np.sqrt(dist) return dist def box_center_distance(b1, b2): assert isinstance(b1, np.ndarray) assert isinstance(b2, np.ndarray) return point_distance(points_center(b1), points_center(b2)) def box_diag(box): assert isinstance(box, np.ndarray) assert box.size == 8 return point_distance(box[0:2], box[4:6]) def filter_2dlist_result(results, scores, score_thr): """Find out detected results whose score > score_thr. Args: results (list[list[float]]): The result list. score (list): The score list. score_thr (float): The score threshold. Returns: valid_results (list[list[float]]): The valid results. valid_score (list[float]): The scores which correspond to the valid results. """ assert isinstance(results, list) assert len(results) == len(scores) assert isinstance(score_thr, float) assert 0 <= score_thr <= 1 inds = np.array(scores) > score_thr valid_results = [results[idx] for idx in np.where(inds)[0].tolist()] valid_scores = [scores[idx] for idx in np.where(inds)[0].tolist()] return valid_results, valid_scores def filter_result(results, scores, score_thr): """Find out detected results whose score > score_thr. Args: results (ndarray): The results matrix of shape (n, k). score (ndarray): The score vector of shape (n,). score_thr (float): The score threshold. Returns: valid_results (ndarray): The valid results of shape (m,k) with m<=n. valid_score (ndarray): The scores which correspond to the valid results. """ assert results.ndim == 2 assert scores.shape[0] == results.shape[0] assert isinstance(score_thr, float) assert 0 <= score_thr <= 1 inds = scores > score_thr valid_results = results[inds, :] valid_scores = scores[inds] return valid_results, valid_scores def select_top_boundary(boundaries_list, scores_list, score_thr): """Select poly boundaries with scores >= score_thr. Args: boundaries_list (list[list[list[float]]]): List of boundaries. The 1st, 2nd, and 3rd indices are for image, text and vertice, respectively. scores_list (list(list[float])): List of lists of scores. score_thr (float): The score threshold to filter out bboxes. Returns: selected_bboxes (list[list[list[float]]]): List of boundaries. The 1st, 2nd, and 3rd indices are for image, text and vertice, respectively. """ assert isinstance(boundaries_list, list) assert isinstance(scores_list, list) assert isinstance(score_thr, float) assert len(boundaries_list) == len(scores_list) assert 0 <= score_thr <= 1 selected_boundaries = [] for boundary, scores in zip(boundaries_list, scores_list): if len(scores) > 0: assert len(scores) == len(boundary) inds = [ iter for iter in range(len(scores)) if scores[iter] >= score_thr ] selected_boundaries.append([boundary[i] for i in inds]) else: selected_boundaries.append(boundary) return selected_boundaries def select_bboxes_via_score(bboxes_list, scores_list, score_thr): """Select bboxes with scores >= score_thr. Args: bboxes_list (list[ndarray]): List of bboxes. Each element is ndarray of shape (n,8) scores_list (list(list[float])): List of lists of scores. score_thr (float): The score threshold to filter out bboxes. Returns: selected_bboxes (list[ndarray]): List of bboxes. Each element is ndarray of shape (m,8) with m<=n. """ assert isinstance(bboxes_list, list) assert isinstance(scores_list, list) assert isinstance(score_thr, float) assert len(bboxes_list) == len(scores_list) assert 0 <= score_thr <= 1 selected_bboxes = [] for bboxes, scores in zip(bboxes_list, scores_list): if len(scores) > 0: assert len(scores) == bboxes.shape[0] inds = [ iter for iter in range(len(scores)) if scores[iter] >= score_thr ] selected_bboxes.append(bboxes[inds, :]) else: selected_bboxes.append(bboxes) return selected_bboxes