from __future__ import annotations from collections.abc import Sequence import numpy as np import torch from scipy.optimize import linear_sum_assignment from torch import Tensor from .boxes import box_cxcywh_to_xyxy, generalized_box_iou_batched def hungarian_match( outputs: dict[str, Tensor], targets: Sequence[dict[str, Tensor]], class_cost: float = 2.0, bbox_cost: float = 5.0, giou_cost: float = 2.0, ) -> list[tuple[Tensor, Tensor]]: """Match predictions to targets with a detached CPU Hungarian solve.""" return hungarian_match_layers([outputs], targets, class_cost, bbox_cost, giou_cost)[0] def hungarian_match_layers( outputs: Sequence[dict[str, Tensor]], targets: Sequence[dict[str, Tensor]], class_cost: float = 2.0, bbox_cost: float = 5.0, giou_cost: float = 2.0, ) -> list[list[tuple[Tensor, Tensor]]]: """Match all decoder layers with one GPU-to-CPU synchronization.""" if not outputs: return [] device = outputs[0]["pred_boxes"].device target_sizes = [len(target["labels"]) for target in targets] max_targets = max(target_sizes, default=0) if max_targets == 0: empty = torch.empty(0, dtype=torch.int64, device=device) return [[(empty, empty) for _ in targets] for _ in outputs] batch_size = len(targets) padded_boxes = torch.zeros(batch_size, max_targets, 4, dtype=torch.float32, device=device) padded_labels = torch.zeros(batch_size, max_targets, dtype=torch.int64, device=device) for batch_index, target in enumerate(targets): count = target_sizes[batch_index] if count > 0: padded_boxes[batch_index, :count] = target["boxes"] padded_labels[batch_index, :count] = target["labels"] padded_costs = [] for layer_output in outputs: probabilities = layer_output["pred_logits"].sigmoid().detach() boxes = layer_output["pred_boxes"].detach() num_queries = boxes.shape[1] gather_index = padded_labels[:, None, :].expand(-1, num_queries, -1) class_term = -probabilities.gather(2, gather_index) bbox_term = torch.cdist(boxes, padded_boxes, p=1) giou_term = -generalized_box_iou_batched( box_cxcywh_to_xyxy(boxes), box_cxcywh_to_xyxy(padded_boxes) ) cost = (class_cost * class_term + bbox_cost * bbox_term + giou_cost * giou_term).float() for batch_index in range(batch_size): padded_costs.append(cost[batch_index]) costs = torch.stack(padded_costs).cpu().numpy() cpu_matches: list[tuple[np.ndarray, np.ndarray]] = [] for cost, target_size in zip(costs, target_sizes * len(outputs), strict=True): if target_size == 0: empty = np.empty(0, dtype=np.int64) cpu_matches.append((empty, empty)) else: cpu_matches.append(linear_sum_assignment(cost[:, :target_size])) lengths = [len(rows) for rows, _ in cpu_matches] all_rows = torch.as_tensor( np.concatenate([rows for rows, _ in cpu_matches]), dtype=torch.int64, device=device, ) all_cols = torch.as_tensor( np.concatenate([cols for _, cols in cpu_matches]), dtype=torch.int64, device=device, ) flat_matches = [] offset = 0 for length in lengths: flat_matches.append( (all_rows[offset : offset + length], all_cols[offset : offset + length]) ) offset += length batch_size = len(targets) return [ flat_matches[start : start + batch_size] for start in range(0, len(flat_matches), batch_size) ]