import argparse import ast import re from typing import List, Optional, Tuple, Union import cv2 import numpy as np import torch import torchvision.transforms.functional as F from scipy.optimize import linear_sum_assignment from timm.data import IMAGENET_DEFAULT_MEAN, IMAGENET_DEFAULT_STD CROP_ROUND_RATE = 0.1 MIN_PERSON_CROP_NONZERO = 0.5 def aggregate_votes_winsorized(ages, max_age_dist=6): # Replace any annotation that is more than a max_age_dist away from the median # with the median + max_age_dist if higher or max_age_dist - max_age_dist if below median = np.median(ages) ages = np.clip(ages, median - max_age_dist, median + max_age_dist) return np.mean(ages) def cropout_black_parts(img, tol=0.3): # Create a binary mask of zero pixels zero_pixels_mask = np.all(img == 0, axis=2) # Calculate the threshold for zero pixels in rows and columns threshold = img.shape[0] - img.shape[0] * tol # Calculate row sums and column sums of zero pixels mask row_sums = np.sum(zero_pixels_mask, axis=1) col_sums = np.sum(zero_pixels_mask, axis=0) # Find the first and last rows with zero pixel sums above the threshold start_row = np.argmin(row_sums > threshold) end_row = img.shape[0] - np.argmin(row_sums[::-1] > threshold) # Find the first and last columns with zero pixel sums above the threshold start_col = np.argmin(col_sums > threshold) end_col = img.shape[1] - np.argmin(col_sums[::-1] > threshold) # Crop the image cropped_img = img[start_row:end_row, start_col:end_col, :] area = cropped_img.shape[0] * cropped_img.shape[1] area_orig = img.shape[0] * img.shape[1] return cropped_img, area / area_orig def natural_key(string_): """See http://www.codinghorror.com/blog/archives/001018.html""" return [int(s) if s.isdigit() else s for s in re.split(r"(\d+)", string_.lower())] def add_bool_arg(parser, name, default=False, help=""): dest_name = name.replace("-", "_") group = parser.add_mutually_exclusive_group(required=False) group.add_argument("--" + name, dest=dest_name, action="store_true", help=help) group.add_argument("--no-" + name, dest=dest_name, action="store_false", help=help) parser.set_defaults(**{dest_name: default}) def cumulative_score(pred_ages, gt_ages, L, tol=1e-6): n = pred_ages.shape[0] num_correct = torch.sum(torch.abs(pred_ages - gt_ages) <= L + tol) cs_score = num_correct / n return cs_score def cumulative_error(pred_ages, gt_ages, L, tol=1e-6): n = pred_ages.shape[0] num_correct = torch.sum(torch.abs(pred_ages - gt_ages) >= L + tol) cs_score = num_correct / n return cs_score class ParseKwargs(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): kw = {} for value in values: key, value = value.split("=") try: kw[key] = ast.literal_eval(value) except ValueError: kw[key] = str(value) # fallback to string (avoid need to escape on command line) setattr(namespace, self.dest, kw) def box_iou(box1, box2, over_second=False): """ Return intersection-over-union (Jaccard index) of boxes. If over_second == True, return mean(intersection-over-union, (inter / area2)) Both sets of boxes are expected to be in (x1, y1, x2, y2) format. Arguments: box1 (Tensor[N, 4]) box2 (Tensor[M, 4]) Returns: iou (Tensor[N, M]): the NxM matrix containing the pairwise IoU values for every element in boxes1 and boxes2 """ def box_area(box): # box = 4xn return (box[2] - box[0]) * (box[3] - box[1]) area1 = box_area(box1.T) area2 = box_area(box2.T) # inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2) inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2) iou = inter / (area1[:, None] + area2 - inter) # iou = inter / (area1 + area2 - inter) if over_second: return (inter / area2 + iou) / 2 # mean(inter / area2, iou) else: return iou def split_batch(bs: int, dev: int) -> Tuple[int, int]: full_bs = (bs // dev) * dev part_bs = bs - full_bs return full_bs, part_bs def assign_faces( persons_bboxes: List[torch.tensor], faces_bboxes: List[torch.tensor], iou_thresh: float = 0.0001 ) -> Tuple[List[Optional[int]], List[int]]: """ Assign person to each face if it is possible. Return: - assigned_faces List[Optional[int]]: mapping of face_ind to person_ind ( assigned_faces[face_ind] = person_ind ). person_ind can be None - unassigned_persons_inds List[int]: persons indexes without any assigned face """ assigned_faces: List[Optional[int]] = [None for _ in range(len(faces_bboxes))] unassigned_persons_inds: List[int] = [p_ind for p_ind in range(len(persons_bboxes))] if len(persons_bboxes) == 0 or len(faces_bboxes) == 0: return assigned_faces, unassigned_persons_inds cost_matrix = box_iou(torch.stack(persons_bboxes), torch.stack(faces_bboxes), over_second=True).cpu().numpy() persons_indexes, face_indexes = [], [] if len(cost_matrix) > 0: persons_indexes, face_indexes = linear_sum_assignment(cost_matrix, maximize=True) matched_persons = set() for person_idx, face_idx in zip(persons_indexes, face_indexes): ciou = cost_matrix[person_idx][face_idx] if ciou > iou_thresh: if person_idx in matched_persons: # Person can not be assigned twice, in reality this should not happen continue assigned_faces[face_idx] = person_idx matched_persons.add(person_idx) unassigned_persons_inds = [p_ind for p_ind in range(len(persons_bboxes)) if p_ind not in matched_persons] return assigned_faces, unassigned_persons_inds def class_letterbox(im, new_shape=(640, 640), color=(0, 0, 0), scaleup=True): # Resize and pad image while meeting stride-multiple constraints shape = im.shape[:2] # current shape [height, width] if isinstance(new_shape, int): new_shape = (new_shape, new_shape) if im.shape[0] == new_shape[0] and im.shape[1] == new_shape[1]: return im # Scale ratio (new / old) r = min(new_shape[0] / shape[0], new_shape[1] / shape[1]) if not scaleup: # only scale down, do not scale up (for better val mAP) r = min(r, 1.0) # Compute padding # ratio = r, r # width, height ratios new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r)) dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding dw /= 2 # divide padding into 2 sides dh /= 2 if shape[::-1] != new_unpad: # resize im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR) top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) # add border return im def prepare_classification_images( img_list: List[Optional[np.ndarray]], target_size: int = 224, mean=IMAGENET_DEFAULT_MEAN, std=IMAGENET_DEFAULT_STD, device=None, ) -> torch.tensor: prepared_images: List[torch.tensor] = [] for img in img_list: if img is None: img = torch.zeros((3, target_size, target_size), dtype=torch.float32) img = F.normalize(img, mean=mean, std=std) img = img.unsqueeze(0) prepared_images.append(img) continue img = class_letterbox(img, new_shape=(target_size, target_size)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = img / 255.0 img = (img - mean) / std img = img.astype(dtype=np.float32) img = img.transpose((2, 0, 1)) img = np.ascontiguousarray(img) img = torch.from_numpy(img) img = img.unsqueeze(0) prepared_images.append(img) prepared_input = torch.concat(prepared_images) if device: prepared_input = prepared_input.to(device) return prepared_input def IOU(bb1: Union[tuple, list], bb2: Union[tuple, list], norm_second_bbox: bool = False) -> float: # expects [ymin, xmin, ymax, xmax], doesnt matter absolute or relative assert bb1[1] < bb1[3] assert bb1[0] < bb1[2] assert bb2[1] < bb2[3] assert bb2[0] < bb2[2] # determine the coordinates of the intersection rectangle x_left = max(bb1[1], bb2[1]) y_top = max(bb1[0], bb2[0]) x_right = min(bb1[3], bb2[3]) y_bottom = min(bb1[2], bb2[2]) if x_right < x_left or y_bottom < y_top: return 0.0 # The intersection of two axis-aligned bounding boxes is always an # axis-aligned bounding box intersection_area = (x_right - x_left) * (y_bottom - y_top) # compute the area of both AABBs bb1_area = (bb1[3] - bb1[1]) * (bb1[2] - bb1[0]) bb2_area = (bb2[3] - bb2[1]) * (bb2[2] - bb2[0]) if not norm_second_bbox: # compute the intersection over union by taking the intersection # area and dividing it by the sum of prediction + ground-truth # areas - the interesection area iou = intersection_area / float(bb1_area + bb2_area - intersection_area) else: # for cases when we search if second bbox is inside first one iou = intersection_area / float(bb2_area) assert iou >= 0.0 assert iou <= 1.01 return iou