|
from typing import List |
|
|
|
import torch |
|
from mmdet.models.task_modules.assigners import AssignResult |
|
from mmdet.models.task_modules.assigners import BaseAssigner |
|
from mmengine.structures import InstanceData |
|
from torch import Tensor |
|
|
|
from mmdet3d.registry import TASK_UTILS |
|
from .util import normalize_bbox |
|
|
|
try: |
|
from scipy.optimize import linear_sum_assignment |
|
except ImportError: |
|
linear_sum_assignment = None |
|
|
|
|
|
@TASK_UTILS.register_module() |
|
class HungarianAssigner3D(BaseAssigner): |
|
"""Computes one-to-one matching between predictions and ground truth. |
|
|
|
This class computes an assignment between the targets and the predictions |
|
based on the costs. The costs are weighted sum of some components. |
|
For DETR3D the costs are weighted sum of classification cost, regression L1 |
|
cost and regression iou cost. The targets don't include the no_object, so |
|
generally there are more predictions than targets. After the one-to-one |
|
matching, the un-matched are treated as backgrounds. Thus each query |
|
prediction will be assigned with `0` or a positive integer indicating the |
|
ground truth index: |
|
|
|
- 0: negative sample, no assigned gt |
|
- positive integer: positive sample, index (1-based) of assigned gt |
|
|
|
Args: |
|
cls_cost (obj:`ConfigDict`) : Match cost configs. |
|
reg_cost. |
|
iou_cost. |
|
pc_range: perception range of the detector |
|
""" |
|
|
|
def __init__(self, |
|
cls_cost=dict(type='ClassificationCost', weight=1.), |
|
reg_cost=dict(type='BBoxL1Cost', weight=1.0), |
|
iou_cost=dict(type='IoUCost', weight=0.0), |
|
pc_range: List = None): |
|
self.cls_cost = TASK_UTILS.build(cls_cost) |
|
self.reg_cost = TASK_UTILS.build(reg_cost) |
|
self.iou_cost = TASK_UTILS.build(iou_cost) |
|
self.pc_range = pc_range |
|
|
|
def assign(self, |
|
bbox_pred: Tensor, |
|
cls_pred: Tensor, |
|
gt_bboxes: Tensor, |
|
gt_labels: Tensor, |
|
gt_bboxes_ignore=None, |
|
eps=1e-7) -> AssignResult: |
|
"""Computes one-to-one matching based on the weighted costs. |
|
This method assign each query prediction to a ground truth or |
|
background. The `assigned_gt_inds` with -1 means don't care, |
|
0 means negative sample, and positive number is the index (1-based) |
|
of assigned gt. |
|
The assignment is done in the following steps, the order matters. |
|
1. assign every prediction to -1 |
|
2. compute the weighted costs |
|
3. do Hungarian matching on CPU based on the costs |
|
4. assign all to 0 (background) first, then for each matched pair |
|
between predictions and gts, treat this prediction as foreground |
|
and assign the corresponding gt index (plus 1) to it. |
|
Args: |
|
bbox_pred (Tensor): Predicted boxes with normalized coordinates |
|
(cx,cy,l,w,cz,h,sin(φ),cos(φ),v_x,v_y) which are all in |
|
range [0, 1] and shape [num_query, 10]. |
|
cls_pred (Tensor): Predicted classification logits, shape |
|
[num_query, num_class]. |
|
gt_bboxes (Tensor): Ground truth boxes with unnormalized |
|
coordinates (cx,cy,cz,l,w,h,φ,v_x,v_y). Shape [num_gt, 9]. |
|
gt_labels (Tensor): Label of `gt_bboxes`, shape (num_gt,). |
|
gt_bboxes_ignore (Tensor, optional): Ground truth bboxes that are |
|
labelled as `ignored`. Default None. |
|
eps (int | float, optional): unused parameter |
|
Returns: |
|
:obj:`AssignResult`: The assigned result. |
|
""" |
|
assert gt_bboxes_ignore is None, \ |
|
'Only case when gt_bboxes_ignore is None is supported.' |
|
num_gts, num_bboxes = gt_bboxes.size(0), bbox_pred.size(0) |
|
|
|
|
|
assigned_gt_inds = bbox_pred.new_full((num_bboxes, ), |
|
-1, |
|
dtype=torch.long) |
|
assigned_labels = bbox_pred.new_full((num_bboxes, ), |
|
-1, |
|
dtype=torch.long) |
|
if num_gts == 0 or num_bboxes == 0: |
|
|
|
if num_gts == 0: |
|
|
|
assigned_gt_inds[:] = 0 |
|
return AssignResult( |
|
num_gts, assigned_gt_inds, None, labels=assigned_labels) |
|
|
|
|
|
|
|
|
|
pred_instances = InstanceData(scores=cls_pred) |
|
gt_instances = InstanceData(labels=gt_labels) |
|
cls_cost = self.cls_cost(pred_instances, gt_instances) |
|
|
|
normalized_gt_bboxes = normalize_bbox(gt_bboxes, self.pc_range) |
|
reg_cost = self.reg_cost(bbox_pred[:, :8], normalized_gt_bboxes[:, :8]) |
|
|
|
|
|
cost = cls_cost + reg_cost |
|
|
|
|
|
cost = cost.detach().cpu() |
|
if linear_sum_assignment is None: |
|
raise ImportError('Please run "pip install scipy" ' |
|
'to install scipy first.') |
|
matched_row_inds, matched_col_inds = linear_sum_assignment(cost) |
|
matched_row_inds = torch.from_numpy(matched_row_inds).to( |
|
bbox_pred.device) |
|
matched_col_inds = torch.from_numpy(matched_col_inds).to( |
|
bbox_pred.device) |
|
|
|
|
|
|
|
assigned_gt_inds[:] = 0 |
|
|
|
assigned_gt_inds[matched_row_inds] = matched_col_inds + 1 |
|
assigned_labels[matched_row_inds] = gt_labels[matched_col_inds] |
|
return AssignResult( |
|
num_gts, assigned_gt_inds, None, labels=assigned_labels) |
|
|