Batching Loss — Focal Head

This page describes the batched loss computation used by FocalHeadBatched.

You can find the implementation in the repository here:
  • FocalHeadBatched: demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py

  • FocalHead (reference): demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head.py

Overview and Goals

In the original implementation, the loss computation is partially batched: the loss functions are batched, while the bounding box matching/assignment is not. Here, we leverage the batching_helpers package to extend batching further by computing the cost matrix for matching in a batched fashion (though we still apply the Hungarian matching procedure itself on a per-sample basis).

Crucially, we preserve the batch dimension throughout almost the entire pipeline, only flattening data across samples and cameras at the final stage if the loss functions expect a flattened representation. Note that the flattening is only done to enable re-use of the already implemented loss functions, and is not necessary if a loss function is designed to process batched data.

Note that while the RaggedBatch class supports multiple batch dimensions, we only use a single batch dimension iterating over both samples and cameras as the distinction between the samples and cameras are not needed for the loss computation and the losses are accumulated over both samples and cameras to obtain the final loss value.

The late flattening allows us to keep per-entry (sample & camera) data more clearly separated, making operations like cost matrix computation, heatmap generation, and other per-sample tasks much more straightforward. As some of the needed operations cannot be easily implemented in a batched manner using PyTorch build-in functionality, functionality of the RaggedBatch class and the batching_helpers package in general is used where applicable.

Compared to the original approach, this batching strategy differs in two key ways:
  1. Data is no longer flattened early across samples and cameras; instead, the batch dimension is maintained, making it possible to apply operations on a per-entry basis (while still processing the batch as a whole). Only if and when the final loss computation implementation necessitates it, we flatten away the batch dimension.

  2. In the original implementation, the ground truth data was aligned with the predictions in the get_targets() function by creating prediction-sized tensors and filling them with the GT data according to the matching. Instead, we acquire matching indices from the assigner (as RaggedBatch objects) and perform alignment inside the loss implementation itself (see the loss_batched() function for more details). In this way, we can easily handle either matched-pairs-only alignment or align GT with all predictions for loss functions requiring non-matched items as well (classification loss, for example) in a clearer fashion. This approach yields smaller tensors for matched-only operations and keeps the code more modular and transparent.

For more details about the actual matching/assignment implementation including cost matrix computation, please also see the HungarianAssigner2DBatched implementation (see demos/streampetr/projects/mmdet3d_plugin/core/bbox/assigners/hungarian_assigner_2d_batched.py in the repository).

Note that the approach used here is an adaptation of the approach used in the batching_helpers Example, adjusted to the specifics of focal head implementation & the implementation of the used loss functions.

The implementation of the loss computation is done in the loss() function. Please see the notes there and in the related called functions for more details.

Note

We refer to the reference implementation (in demo/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head.py) in some of the notes/code comments. Please note that this reference implementation mostly corresponds to the original implementation. However, we added an option there to use the custom op implementation of the heatmap generation (for fairer comparison of the speedup achieved by our batching approach).

Below, we walk through the important parts of the implementation which were modified to support batching using the batching-helpers package.

Top-level Loss Computation Implementation

The top-level loss computation which is called from outside the class is called loss() and is described here.

Pre-processing

The pre-processing step performs some validation and extracts the necessary predictions from a dictionary. Compared to the original implementation, there is one important difference: the ground truth (GT) data of the individual samples and cameras is combined into RaggedBatch objects (see comments in the code).

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
343        # ===== Pre-processing (`loss()`) =====
344        nvtx_wrp.range_push("FocalHeadBatched.loss")
345        stopwatch.start_meas("FocalHeadBatched.loss")
346
347        assert gt_bboxes_ignore is None, \
348            f'{self.__class__.__name__} only supports ' \
349            f'for gt_bboxes_ignore setting to None.'
350
351        enc_cls_scores = preds_dicts['enc_cls_scores']
352        enc_bbox_preds = preds_dicts['enc_bbox_preds']
353        pred_centers2d = preds_dicts['pred_centers2d']
354        centerness = preds_dicts['centerness']
355        
356        #@NOTE 
357        # For each type of ground truth, we combine the data from all samples and cameras
358        # into single RaggedBatch objects using the batching-helpers (bhl) package. This differs 
359        # from the original implementation, where lists of tensors are used and the individual
360        # elements in the lists are processed individually.
361        #
362        # Note that the `flatten_batch_dims` flag is set to `True` to flatten the batch dimensions 
363        # (corresponding to samples and cameras). This is done as the distinction between the samples and 
364        # cameras is not needed for the loss computation, as the loss is accumulated over both.
365        gt_labels2d = bhl.combine_data(gt_labels2d_list, flatten_batch_dims=True)
366        gt_bboxes_2d = bhl.combine_data(gt_bboxes2d_list, gt_labels2d, flatten_batch_dims=True)
367        gt_centers2d = bhl.combine_data(centers2d, gt_labels2d, flatten_batch_dims=True)
368        gt_depths = bhl.combine_data(depths, gt_labels2d, flatten_batch_dims=True)
369

Compute Batched Loss (Delegation)

Loss computation is delegated to loss_batched with batched inputs.

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
370        # ===== Compute batched loss =====
371        #@NOTE 
372        # The actual loss computation is done in the `loss_batched()` function, using the combined data.
373        # Please see the notes there for more details on the approach. The main difference to the original 
374        # implementation is that the ground truth data is batched. Note that while there, the called function
375        # is called `loss_single()`, it also called once per batch and is provided with the data of all 
376        # samples and cameras (with the GT data as lists containing per-sample and per-camera tensors).
377        enc_loss_cls, enc_losses_bbox, enc_losses_iou, centers2d_losses, centerness_losses = \
378            self.loss_batched(enc_cls_scores, enc_bbox_preds, pred_centers2d, centerness,
379                              gt_bboxes_2d, gt_labels2d, gt_centers2d, gt_depths, img_metas)
380

Assemble Results

The results are collected into a dictionary and returned.

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
381        # ===== Assemble results =====
382        #@NOTE: This is similar to the original implementation.
383        loss_dict = dict()
384        loss_dict['enc_loss_cls'] = enc_loss_cls
385        loss_dict['enc_loss_bbox'] = enc_losses_bbox
386        loss_dict['enc_loss_iou'] = enc_losses_iou
387        loss_dict['centers2d_losses'] = centers2d_losses
388        loss_dict['centerness_losses'] = centerness_losses
389    
390        nvtx_wrp.range_pop("FocalHeadBatched.loss")
391        stopwatch.end_meas("FocalHeadBatched.loss")
392
393        return loss_dict

Detailed Loss Computation Implementation

The actual loss computation is done in the loss_batched() function. This function is called once per batch and is provided with the data of all samples and cameras (combined into RaggedBatch objects for the GT data inside loss()). The function is called internally from loss().

Pre-processing

Some simple steps are performed first. Note that splitting the predictions into individual samples and cameras (as done in the original implementation) is not needed here (see comment in the code).

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
447        # ===== Pre-processing (`loss_batched()`) =====
448        assert gt_bboxes_ignore_list is None, (
449            'Using gt_bboxes_ignore_list is not supported. This is consistent with the original '
450            'implementation, where setting ignore lists raises an error in the matcher.')
451        
452        num_imgs = cls_scores.size(0)
453        num_preds = bbox_preds.size(1)
454
455        #@NOTE 
456        # Also see notes at the beginning of the file for an overview of the batched loss implementation 
457        # approach.
458
459        #@NOTE 
460        # In the original implementation, the prediction tensors are split into individual samples and 
461        # cameras to match the per-entry ground truth and perform the matching on a per-entry basis. 
462        # Here, this step is omitted. The ground truth data is (already) batched instead, and we can directly 
463        # proceed to the matching step below.
464

Matching

Invoke the batched assigner to get matched indices as RaggedBatch pairs:

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
465        # ===== Perform the matching =====
466        #@NOTE 
467        # In the original implementation, the assigner is called within `get_targets()`, which also performs
468        # additional data processing. Here, we call the assigner directly since we need to return the matching 
469        # indices. The batched assigner returns `RaggedBatch` instances containing indices for matched 
470        # predictions and the corresponding GT bounding boxes. This is a modular approach, so that the 
471        # matching does not need to be concerned with the format that the loss computation expects (beyond the
472        # matching indices). This approach allows us to:
473        #   - Maintain a clear separation of concerns between matching and loss computation
474        #   - Use `batching-helpers` functionality for any additional processing of the data based on the 
475        #     matching results later if/as needed depending on the computed loss type (e.g., depending on 
476        #     whether the loss needs all predictions or only the matched pairs).
477        pred_match_indices, gt_match_indices = self.assigner2d.assign(
478            bbox_preds, cls_scores, pred_centers2d, gt_bboxes, gt_labels, gt_centers2d, img_metas
479        )
480
481        # DETR regress the relative position of boxes (cxcywh) in the image,
482        # thus the learning target is normalized by the image size. So here
483        # we need to re-scale them for calculating IoU loss
484        # construct factors used for rescale bboxes

Normalization Factors

Compute image-size-based factors used for scaling and conversions:

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
485        # ===== Normalization factors =====
486        img_h, img_w, _ = img_metas[0]['pad_shape'][0]
487
488        #@NOTE 
489        # The normalization factors are the same for all bboxes (both for GT and predictions). Therefore,
490        # the first 2 dimensions have the size 1 and are implicitly broadcasted to the correct size
491        # when applied to the bboxes. This is a difference from the original implementation mainly aimed at 
492        # improving clarity and conciseness.
493        factors = bbox_preds.new_tensor([img_w, img_h, img_w, img_h]).reshape(1,1,4)
494

Align Data by Matches

In this step, the alignment of the predictions and GT is performed using the matched indices, so that loss computation can be performed on tensors where the elements are aligned according to the matching results.

Note that in contrast to all other losses, the classification loss is computed for all predictions, not only for matched pairs. This leads to a different implementation of the alignment step (see comments in the code).

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
495        # ===== Align data by matches =====
496        nvtx_wrp.range_push("Indexing [FocalHeadBatched.loss_batched]")
497        #@NOTE 
498        # Get the aligned (i.e. indexed by indices established in the matching) versions of the 
499        # bounding boxes. Note that in constrast to the targets in the original implementation 
500        # (which are returned by the `get_targets()` function), the GT bboxes are still in the xyxy 
501        # format and are not normalized. The normalization is done later for the matched targets only.
502        #  
503        # Here, aligned means that only the elements which have matches are preserved, and the indices
504        # of the predictions and the mapped GT elements are the same, allowing for direct comparison of the 
505        # tensors containing predictions to tensors containing GT.
506        bboxes_preds_normed_matched = bhl.batched_indexing_access(bbox_preds, pred_match_indices)
507        bboxes_gt_nonnormed_matched = bhl.batched_indexing_access(gt_bboxes, gt_match_indices)
508        #@NOTE 
509        # Similar to the bounding boxes, get the aligned versions of the centers.
510        centers2d_gt_nonnormed_matched = bhl.batched_indexing_access(gt_centers2d, gt_match_indices)
511        centers2d_pred_normed_matched = bhl.batched_indexing_access(pred_centers2d, pred_match_indices)
512
513        #@NOTE 
514        # Align/map the GT labels to the predictions. This is done by creating a tensor corresponding to the 
515        # predictions in size and filling it with the GT labels according to the matching results (i.e. 
516        # matched indices of both predictions and GT). This is different from the alignment above, as the 
517        # classification loss is computed for all predictions, not only for matched predicitons. Therefore, 
518        # all predictions need to be preserved, and the GT labels need to be mapped to them.
519        #
520        # Note that this corresponds to the mapping done in the original implementation by the 
521        # `get_targets()` function.
522        # 
523        # As in the original implementation, the filler values corresponding to non-matched predictions are 
524        # set to the number of classes, which is a special label indicating non-existence and is understood as 
525        # such by the classification loss function implementation.
526        gt_labels_mapped_to_pred = torch.full(
527            (num_imgs, num_preds), fill_value=self.cls_out_channels, dtype=gt_labels.dtype, 
528            device=gt_labels.device
529        )
530        #@NOTE: Insert the GT labels into the positions corresponding to the matched predictions.
531        gt_labels_mapped_to_pred = bhl.batched_index_mapping(
532            gt_labels, gt_match_indices, pred_match_indices, gt_labels_mapped_to_pred
533        )
534        nvtx_wrp.range_pop()
535

Pre-process Tensors

Convert bounding boxes between formats, normalize/denormalize as required, and normalize centers. This step makes heavy use of the apply() method to modify the data in the RaggedBatch. Apart from this, it differs from the original implementation, as the format of the data is different at this point (some conversion is already performed there on the data before it is returned from the get_targets() function).

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
536        # ===== Pre-process tensors =====
537        nvtx_wrp.range_push("bboxes preproc. [FocalHeadBatched.loss_batched]")
538        #@NOTE 
539        # In constrast to the bboxes in the original implementation (which are returned by the 
540        # `get_targets()` function), the GT bboxes are still in the `xyxy` format and are not normalized. 
541        # The predictions are in `xywh` and normalized format (same as original implementation). We need both 
542        # GT and predictions in both formats. Here, we convert the normalized predictions to non-normalized 
543        # format and the non-normalized GT bboxes to normalized format.
544        # 
545        # Note the `apply()` method, which is a convenience method for applying a function to 
546        # the data in the RaggedBatch object. While here, only the data tensor (i.e. `tensor`) is input to the
547        # lambda function, the passed function may accept more inputs, which allows access to the 
548        # sample sizes and related mask (see `RaggedBatch.apply()` documentation for more details).
549        bboxes_preds_nonnormed_matched = bboxes_preds_normed_matched.apply(
550            lambda data: bbox_cxcywh_to_xyxy(data) * factors
551        )
552        bboxes_gt_normed_matched = bboxes_gt_nonnormed_matched.apply(
553            lambda data: bbox_xyxy_to_cxcywh(data) / factors
554        )
555        nvtx_wrp.range_pop()
556
557        nvtx_wrp.range_push("centers preproc [FocalHeadBatched.loss_batched]")
558        #centerness
559        #@NOTE 
560        # Compute the normalized versions of the centers. Note that in the original implementation,
561        # normalized centers are returned by the `get_targets()` function, so that no additional 
562        # normalization is needed there.
563        centers2d_gt_normed_matched = centers2d_gt_nonnormed_matched.apply(
564            lambda ctr2d: ctr2d / factors[..., 0:2]
565        )
566        nvtx_wrp.range_pop()
567        

Count Valid and Invalid Elements

Count the number of the valid and invalid elements, and derive existence weights. This is done using functionality of the RaggedBatch class. Note that all of these are also used in the original implementation, but there, they are provided by the get_targets() function.

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
568        # ===== Existence weights & count valid and invalid elements =====
569        nvtx_wrp.range_push("valid elements preproc [FocalHeadBatched.loss_batched]")
570        #@NOTE 
571        # Get weights differentiating between valid (weight: 1.0) and invalid (weight: 0.0) elements.
572        # In the original implementation, this is returned by the `get_targets()` function (in a different
573        # format/shape, corresponding to the batching approach used there).
574        bbox_weights_matched = bboxes_gt_nonnormed_matched.get_existence_weights(torch.float32)
575        #@NOTE 
576        # Get the total number of valid (i.e. existent, matched to GT) and invalid (i.e. non-existent, 
577        # non-matched) elements. In the original implementation, this information is provided by the 
578        # `get_targets()` function.
579        num_total_pos = bboxes_gt_nonnormed_matched.total_num_entries
580        num_total_neg = num_preds * num_imgs - num_total_pos
581        nvtx_wrp.range_pop()
582

Losses: IoU Loss and IoU Score

The IoU loss is a typical example of a loss computation, where only the matched pairs are considered. Apart from the loss, the IoU score is also computed and used as a weight for the classification loss later on.

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
583        # ===== Losses: IoU & IoU score =====
584        # regression IoU loss, defaultly GIoU loss
585        nvtx_wrp.range_push("loss_iou [FocalHeadBatched.loss_batched]")
586        #@NOTE 
587        # Compute the IoU loss between predicted and ground truth bounding boxes. Unlike the original 
588        # implementation, we compute this loss only for matched predictions. The original approach computes 
589        # loss for all predictions (with GT targets aligned to predictions) and uses zero weights for 
590        # unmatched predictions. Our implementation still contains filler values in the data tensors (with 
591        # corresponding zero weights), but the number of fillers is minimized, and only as many fillers as 
592        # needed to maintain uniform sample sizes (see `RaggedBatch` docs) are used.
593        #
594        # Note that in contrast to the original implementation, the inputs to this loss computation do 
595        # include a batch dimension. The loss function handles this automatically and now reduces over both
596        # the bounding boxes and over the entries in the batch dimension.
597        loss_iou = self.loss_iou2d(
598            bboxes_preds_nonnormed_matched.tensor, 
599            bboxes_gt_nonnormed_matched.tensor, 
600            bbox_weights_matched, 
601            avg_factor=num_total_pos,
602        )
603        
604        nvtx_wrp.range_pop()    
605
606        nvtx_wrp.range_push("iou_score [FocalHeadBatched.loss_batched]")
607        #@NOTE 
608        # Compute the IoU score between predicted and ground truth bounding boxes.
609        # The same function is used as in the original implementation. Note that the input tensors
610        # have a batch dimension in this implementation, while there is no batch dimension in the original 
611        # implementation (and a sequence of all bounding boxes from all samples and cameras is used instead).
612        # However, this function can handle both cases and does not need to be modified.
613        iou_score = bbox_overlaps(
614            bboxes_gt_nonnormed_matched.tensor, bboxes_preds_nonnormed_matched.tensor, is_aligned=True
615        )
616        nvtx_wrp.range_pop()
617

Losses: Classification (Setup & Weights)

Prepare class scores, average factor, and weights; compute IoU scores and map back to prediction shape for weighting:

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
618        # ===== Losses: Classification (setup & weights) =====
619        nvtx_wrp.range_push("cls pre-proc [FocalHeadBatched.loss_batched]")
620        # classification loss
621        cls_scores = cls_scores.reshape(-1, self.cls_out_channels)
622        # construct weighted avg_factor to match with the official DETR repo
623        cls_avg_factor = num_total_pos * 1.0 + \
624            num_total_neg * self.bg_cls_weight
625        if self.sync_cls_avg_factor:
626            cls_avg_factor = reduce_mean(
627                cls_scores.new_tensor([cls_avg_factor]))
628        cls_avg_factor = max(cls_avg_factor, 1)
629        nvtx_wrp.range_pop()
630
631        nvtx_wrp.range_push("labels pre-proc [FocalHeadBatched.loss_batched]")
632        #@NOTE: Flatten `gt_labels_mapped_to_pred` as expected by the loss function.
633        gt_labels_mapped_to_pred_flat = gt_labels_mapped_to_pred.view(-1)
634        #@NOTE: Create a tensor of ones as the label weights (consistent with the original implementation).
635        label_weights = torch.ones(
636            gt_labels_mapped_to_pred_flat.shape, dtype=torch.float32, device = gt_labels.device
637        )
638        nvtx_wrp.range_pop()
639
640        nvtx_wrp.range_push("iou score pre-proc [FocalHeadBatched.loss_batched]")
641        #@NOTE 
642        # For the classification loss, we need to weight the predictions with the IoU score (for existent 
643        # predictions). As the score is computed only for matched predictions, we need to map it back to match 
644        # the original prediction tensor. This is done by using `batched_inverse_indexing_access()`. This is 
645        # different from the original implementation, where all the data is mapped to the predictions inside 
646        # the `get_targets()` function, and the IoU scores for the individual predictions, which are computed 
647        # afterwards, are already located at the indices corresponding to the original prediction tensor. 
648        #
649        # Note that that for non-existent predictions, the IoU score is set to 0 in 
650        # `batched_inverse_indexing_access()`. Again, this is consistent with the original implementation, and 
651        # these values are ignored in any case.
652        #
653        # Also note that `batched_inverse_indexing_access()` creates a new tensor with constant values and
654        # then fills the values at the indexed locations with the values from the original tensor. 
655        # There is an alternative implementation (`batched_index_write()`) which fills the values
656        # into an already existing tensor (analogous to `__setitem__()`). Please refer to the 
657        # documentation of `batching-helpers` for more details and choose the appropriate method based 
658        # on the use case.
659        iou_score_mapped_to_pred = bhl.batched_inverse_indexing_access(
660            iou_score, pred_match_indices, num_preds, filler_value=0.0
661        )
662        #@NOTE flatten the mapped IoU scores as needed by the loss function implementation.
663        iou_score_mapped_to_pred_flat = iou_score_mapped_to_pred.view(-1)
664        nvtx_wrp.range_pop()
665        

Losses: Classification Loss

Compute the classification loss over all predictions, weighted by IoU for existent predictions:

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
666        # ===== Losses: Classification =====
667        nvtx_wrp.range_push("loss_cls [FocalHeadBatched.loss_batched]")
668        #@NOTE
669        # Compute the classification loss (for all predictions, and with the IoU scores as weights for 
670        # existent predictions). After the inputs were flattened, the inputs to the loss function correspond
671        # to the inputs in the original implementation, and the same loss functions can be used here.
672        # Whether this flattening is needed or not (as in the IoU loss above) depends on the loss function 
673        # implementation.
674        loss_cls = self.loss_cls2d(
675            cls_scores, 
676            (gt_labels_mapped_to_pred_flat, iou_score_mapped_to_pred_flat.detach()), 
677            label_weights, 
678            avg_factor=cls_avg_factor,
679        )
680        nvtx_wrp.range_pop()
681

Synchronize Positives

Synchronize positive counts across devices (same as reference):

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
682        # ===== Synchronize positives =====
683        nvtx_wrp.range_push("num pos preproc. [FocalHeadBatched.loss_batched]")
684        #@NOTE 
685        # The following lines are as in the original implementation and are done for cross-device 
686        # synchronization.
687        num_total_pos = loss_cls.new_tensor([num_total_pos])
688        num_total_pos = torch.clamp(reduce_mean(num_total_pos), min=1).item()
689        nvtx_wrp.range_pop()
690

Heatmap Generation

Generate target heatmaps in a batched way using draw_heatmap_batched. Note that by using the custom heatmap generation OP in combination with the RaggedBatch class, the implementation:

  • Is optimized by batching compared to the original implementation.

  • Is simplified compared to our batched implementation of the heatmap generation that was introduces in the reference implementation, as the RaggedBatch class makes it clear which bounding box is from which entry in the batch.

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
691        # ===== Heatmap generation =====
692        nvtx_wrp.range_push("Heatmap generation[FocalHeadBatched.loss_batched]")
693        img_shape = img_metas[0]['pad_shape'][0]
694        img_h, img_w, _ = img_shape
695        #@NOTE: Get the scaled centers and radii of the GT boxes using the `apply()` convenience method.
696        gt_centers_heatmap, gt_radii_heatmap = centers2d_gt_nonnormed_matched.apply(
697            lambda centers: self._get_scaled_centers_and_radii(centers, bboxes_gt_nonnormed_matched.tensor)
698        )
699        #@NOTE: Create the heatmap tensor (to be filled with the individual gaussian heatmaps).
700        heatmaps = torch.zeros(
701            (num_imgs, img_h // self.stride, img_w // self.stride), 
702            dtype=torch.float32, 
703            device=gt_centers_heatmap.device
704        )
705        nvtx_wrp.range_push("draw_heatmap call [FocalHeadBatched.loss_batched]")
706        #@NOTE
707        # Draw the heatmaps using `draw_heatmap_batched`. This function accepts
708        # the data in the `RaggedBatch` object. Compared to the reference, this simplifies the implementation 
709        # as we do not need to manually create additional tensors indicating to which image the individual 
710        # bboxes belong. 
711        #
712        # Please compare to the heatmap generation implementation in `FocalHead.loss_single()` (and see 
713        # the related noted there). There, we also add the option to use the custom op implementation of the 
714        # heatmap generation. However, we need to use `draw_heatmap()` and manually construct a tensor 
715        # indicating which bounding box is from which entry in the batch (i.e. which image in which sample), 
716        # as the batch dimensions is flattened away and so this information is not available from the data 
717        # tensors alone.
718        draw_heatmap_batched(heatmaps, gt_centers_heatmap, gt_radii_heatmap)
719        nvtx_wrp.range_pop()
720        nvtx_wrp.range_pop()
721

Losses: Centerness Loss

Centerness is computed for all predictions. Note that this loss is special in the sense that the fixed-sized heatmap is instead of the original, non-uniformly-sized GT.

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
722        # ===== Losses: Centerness =====
723        nvtx_wrp.range_push("loss_centerness [FocalHeadBatched.loss_batched]")
724        centerness = clip_sigmoid(centerness)
725        #@NOTE
726        # Compute the centerness loss. Note that similar to the classification loss, 
727        # the centerness loss is computed for all predictions.
728        # The GT targets are not used directly here, and instead, the previously generated heatmap is used. 
729        # The predictions correspond to individual pixels in the heatmap, so that they can be directly 
730        # compared to the heatmap.
731        #
732        # As the non-uniformly-sized GT is not directly used for this loss, the loss computation 
733        # is done as in the original implementation and no changes are needed to the format of the 
734        # input data.
735        loss_centerness = self.loss_centerness(
736                centerness,
737                heatmaps.view(num_imgs, -1, 1),
738                avg_factor=max(num_total_pos, 1))
739        nvtx_wrp.range_pop() 
740

Losses: BBox and Centers2D Losses

These losses are computed only for the matched predictions (similar to the IoU loss discussed before).

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
741        # ===== Losses: BBox and Centers2D =====
742        # regression L1 loss
743        nvtx_wrp.range_push("loss_bbox [FocalHeadBatched.loss_batched]")
744        #@NOTE 
745        # Compute the bounding box loss. Similar to `loss_iou2d`, this loss is computed only for the 
746        # predictions which have a match.
747        loss_bbox = self.loss_bbox2d(
748            bboxes_preds_normed_matched.tensor, 
749            bboxes_gt_normed_matched.tensor, 
750            bbox_weights_matched, 
751            avg_factor=num_total_pos,
752        )
753        nvtx_wrp.range_pop()     
754
755        pred_centers2d = pred_centers2d.view(-1, 2)
756        # centers2d L1 loss
757        nvtx_wrp.range_push("loss_centers2d [FocalHeadBatched.loss_batched]")
758        #@NOTE 
759        # Compute the centers2d loss. Note that similar to `loss_iou2d` and `loss_bbox`, this loss is computed
760        # only for the existent/matched predictions.
761        loss_centers2d = self.loss_centers2d(
762            centers2d_pred_normed_matched.tensor, 
763            centers2d_gt_normed_matched.tensor, 
764            bbox_weights_matched[..., 0:2], 
765            avg_factor=num_total_pos,
766        )
767        nvtx_wrp.range_pop()
768

Finalize Loss Computation

In the final step, the individual losses are returned.

demos/streampetr/projects/mmdet3d_plugin/models/dense_heads/focal_head_batched.py
769        # ===== Finalize =====
770        nvtx_wrp.range_pop("FocalHeadBatched.loss_batched")
771        return loss_cls, loss_bbox, loss_iou, loss_centers2d, loss_centerness