from __future__ import annotations
from typing import Dict, List
import torch
import torch.nn as nn
import torch.nn.functional as F
from pimm.models.losses.builder import LOSSES
from pimm.models.losses.instance import SingleLayerInstanceLoss
from pimm.models.panda_detector.matcher_fast import (
FastHungarianMatcher,
build_target_cache,
get_target_masks,
split_by_counts,
)
[docs]
@LOSSES.register_module()
class FastInstanceSegmentationLoss(nn.Module):
"""Mask2Former-style instance segmentation loss (cached, vectorized).
Computes a set-prediction loss between per-query predicted masks and
ground-truth instances. Targets are cached once per call (built from the
per-point ``truth_label`` ids), a fast Hungarian matcher assigns queries to
instances, and matched pairs incur a focal mask loss, a Dice loss, and a
classification loss (matched -> instance class, unmatched -> a no-object
class). Optional momentum and IoU terms are added when their weights are
positive and the predictions/targets are present. Deep supervision over
``aux_outputs`` is summed and scaled by ``aux_loss_weight``.
``forward(pred, input_dict)`` expects ``pred`` to be a dict with
``pred_masks`` (list of ``(Q, P)`` tensors per batch element) and optionally
``pred_logits``/``pred_momentum``/``pred_iou``/``aux_outputs``, and
``input_dict`` to provide ``truth_label`` (and ``segment`` for the class
term). Returns ``(loss, components)`` where ``components`` is a dict of scalar
diagnostics (``focal``, ``dice``, ``cls_matched``, ``num_pairs``, ...).
Registered as ``FastInstanceSegmentationLoss``.
Args:
cost_mask (float): Matcher weight on the focal mask cost. Defaults to
``1.0``.
cost_dice (float): Matcher weight on the Dice cost. Defaults to ``1.0``.
cost_class (float): Matcher weight on the classification cost. Defaults to
``0.0``.
num_points (int): Number of sampled points for the mask cost (``0`` uses
all points). Defaults to ``0``.
ignore_index (int): Instance/segment id treated as void. Defaults to
``-1``.
loss_weight_focal (float): Weight on the focal mask loss. Defaults to
``1.0``.
loss_weight_dice (float): Weight on the Dice loss. Defaults to ``1.0``.
cls_weight_matched (float): Weight on the matched-query CE term. Defaults
to ``2.0``.
cls_weight_noobj (float): Weight on the no-object CE term. Defaults to
``0.1``.
focal_alpha (float): Focal-loss positive-class weight. Defaults to
``0.25``.
focal_gamma (float): Focal-loss focusing exponent. Defaults to ``2.0``.
truth_label (str): ``input_dict`` key holding per-point instance ids.
Defaults to ``"segment"``.
aux_loss_weight (float): Scale on the summed auxiliary (deep-supervision)
loss. Defaults to ``1.0``.
momentum_loss_weight (float): Weight on the per-instance momentum
regression term (``0`` disables). Defaults to ``0.0``.
iou_loss_weight (float): Weight on the mask-IoU prediction term (``0``
disables). Defaults to ``0.0``.
Example:
.. code-block:: python
>>> import torch
>>> from pimm.models.losses.instance_fast import FastInstanceSegmentationLoss
>>> crit = FastInstanceSegmentationLoss(truth_label="instance")
>>> # pred: dict with per-batch mask logits; here 1 event, Q=8 queries, P=50 points
>>> pred = {"pred_masks": [torch.randn(8, 50)]}
>>> inst = torch.zeros(50, dtype=torch.long) # per-point instance ids
>>> inst[10:30] = 1; inst[30:50] = 2
>>> input_dict = {"instance": inst.view(-1, 1)}
>>> loss, comp = crit(pred, input_dict) # scalar loss + diagnostics dict
>>> float(loss) # e.g. 0.71 (focal + dice)
0.7063...
>>> sorted(comp)[:4]
['cls_matched', 'cls_noobj', 'dice', 'focal']
>>> # add pred["pred_logits"]/["pred_momentum"] + input_dict["segment"]/["momentum"]
>>> # to activate the classification / momentum terms.
"""
def __init__(
self,
cost_mask: float = 1.0,
cost_dice: float = 1.0,
cost_class: float = 0.0,
num_points: int = 0,
ignore_index: int = -1,
loss_weight_focal: float = 1.0,
loss_weight_dice: float = 1.0,
cls_weight_matched: float = 2.0,
cls_weight_noobj: float = 0.1,
focal_alpha: float = 0.25,
focal_gamma: float = 2.0,
truth_label: str = "segment",
aux_loss_weight: float = 1.0,
momentum_loss_weight: float = 0.0,
iou_loss_weight: float = 0.0,
):
super().__init__()
self.aux_loss_weight = aux_loss_weight
self.criterion = FastSingleLayerInstanceLoss(
cost_mask=cost_mask,
cost_dice=cost_dice,
cost_class=cost_class,
num_points=num_points,
ignore_index=ignore_index,
loss_weight_focal=loss_weight_focal,
loss_weight_dice=loss_weight_dice,
cls_weight_matched=cls_weight_matched,
cls_weight_noobj=cls_weight_noobj,
focal_alpha=focal_alpha,
focal_gamma=focal_gamma,
truth_label=truth_label,
momentum_loss_weight=momentum_loss_weight,
iou_loss_weight=iou_loss_weight,
)
def _target_cache(self, pred: Dict, input_dict: Dict) -> List[Dict]:
pred_masks_list = pred["pred_masks"]
counts = [pm.shape[1] for pm in pred_masks_list]
return build_target_cache(
input_dict[self.criterion.truth_label],
counts,
segment=input_dict.get("segment", None),
ignore_index=self.criterion.ignore_index,
device=pred_masks_list[0].device if pred_masks_list else None,
)
[docs]
def forward(self, pred: Dict, input_dict: Dict) -> torch.Tensor:
if hasattr(pred, "outputs"):
pred = pred.outputs
target_cache = self._target_cache(pred, input_dict)
final_loss, components = self.criterion(pred, input_dict, target_cache)
if "aux_outputs" in pred and pred["aux_outputs"]:
aux_loss = pred["pred_masks"][0].new_tensor(0.0)
for layer_idx, aux_pred in enumerate(pred["aux_outputs"]):
aux_loss_val, aux_comp = self.criterion(
aux_pred, input_dict, target_cache
)
aux_loss = aux_loss + aux_loss_val
for key in ("focal", "dice", "cls_matched", "cls_noobj", "momentum", "iou"):
if key in aux_comp:
components[f"aux_{key}_L{layer_idx}"] = aux_comp[key]
final_loss = final_loss + self.aux_loss_weight * aux_loss
return final_loss, components
class FastSingleLayerInstanceLoss(SingleLayerInstanceLoss):
def __init__(
self,
cost_mask: float = 1.0,
cost_dice: float = 1.0,
cost_class: float = 0.0,
num_points: int = 0,
ignore_index: int = -1,
loss_weight_focal: float = 1.0,
loss_weight_dice: float = 1.0,
cls_weight_matched: float = 2.0,
cls_weight_noobj: float = 0.1,
focal_alpha: float = 0.25,
focal_gamma: float = 2.0,
truth_label: str = "segment",
segment_key: str = "segment",
momentum_loss_weight: float = 0.0,
iou_loss_weight: float = 0.0,
):
super().__init__(
cost_mask=cost_mask,
cost_dice=cost_dice,
cost_class=cost_class,
num_points=num_points,
ignore_index=ignore_index,
loss_weight_focal=loss_weight_focal,
loss_weight_dice=loss_weight_dice,
cls_weight_matched=cls_weight_matched,
cls_weight_noobj=cls_weight_noobj,
focal_alpha=focal_alpha,
focal_gamma=focal_gamma,
truth_label=truth_label,
momentum_loss_weight=momentum_loss_weight,
iou_loss_weight=iou_loss_weight,
)
self.segment_key = segment_key
self.matcher = FastHungarianMatcher(
cost_class=cost_class,
cost_mask=cost_mask,
cost_dice=cost_dice,
num_points=num_points,
ignore_index=ignore_index,
focal_alpha=focal_alpha,
focal_gamma=focal_gamma,
)
def _build_target_cache(self, pred_masks_list, input_dict):
counts = [pm.shape[1] for pm in pred_masks_list]
return build_target_cache(
input_dict[self.truth_label],
counts,
segment=input_dict.get(self.segment_key),
ignore_index=self.ignore_index,
device=pred_masks_list[0].device if pred_masks_list else None,
)
def _batch_tensor(self, tensor_or_list, batch_idx, counts, device, valid_mask):
tensor_b = split_by_counts(tensor_or_list, counts)[batch_idx].to(device)
if valid_mask is not None:
tensor_b = tensor_b[valid_mask]
return tensor_b
def _mean_per_instance(self, values, inverse, num_instances):
out = values.new_zeros((num_instances,) + values.shape[1:])
counts = values.new_zeros((num_instances,) + (1,) * (values.dim() - 1))
out.index_add_(0, inverse, values)
ones = torch.ones_like(inverse, dtype=values.dtype)
counts.index_add_(0, inverse, ones.view(-1, *([1] * (values.dim() - 1))))
return out / counts.clamp_min(1)
def _classification_terms(self, logits_b, inst_class, idx_q, idx_gt):
C = logits_b.shape[-1] - 1
cls_loss_b = logits_b.new_tensor(0.0)
cls_count_b = 0
ce_matched = None
ce_noobj = None
if idx_q.numel() > 0 and inst_class is not None:
logits_matched = logits_b[idx_q.long()]
target_matched = inst_class[idx_gt.long()]
ce_matched = F.cross_entropy(
logits_matched, target_matched, reduction="mean"
)
cls_loss_b = cls_loss_b + self.cls_weight_matched * ce_matched
cls_count_b += 1
mask_unmatched = torch.ones(logits_b.shape[0], dtype=torch.bool, device=logits_b.device)
if idx_q.numel() > 0:
mask_unmatched[idx_q.long()] = False
if mask_unmatched.any():
logits_unmatched = logits_b[mask_unmatched]
target_noobj = logits_unmatched.new_full(
(logits_unmatched.shape[0],), C, dtype=torch.long
)
ce_noobj = F.cross_entropy(
logits_unmatched, target_noobj, reduction="mean"
)
cls_loss_b = cls_loss_b + self.cls_weight_noobj * ce_noobj
cls_count_b += 1
return cls_loss_b, cls_count_b, ce_matched, ce_noobj
def _matched_mask_losses(self, pred_sel, target_masks, target_sizes, idx_gt):
gt_sel = target_masks[idx_gt]
prob = pred_sel.sigmoid()
ce_loss = F.binary_cross_entropy_with_logits(
pred_sel, gt_sel, reduction="none"
)
p_t = prob * gt_sel + (1 - prob) * (1 - gt_sel)
focal_weight = (1 - p_t) ** self.focal_gamma
alpha_t = self.focal_alpha * gt_sel + (1 - self.focal_alpha) * (1 - gt_sel)
focal_per_pair = (alpha_t * focal_weight * ce_loss).mean(dim=1)
numerator = 2 * (prob * gt_sel).sum(dim=1)
denominator = prob.sum(dim=1) + target_sizes[idx_gt].to(prob.dtype)
dice = 1 - (numerator + 1) / (denominator + 1)
return focal_per_pair, dice
def forward(self, pred: Dict[str, List[torch.Tensor]], input_dict: Dict, target_cache=None):
assert isinstance(pred, dict) and "pred_masks" in pred, (
"pred must be a dict with key 'pred_masks'"
)
assert self.truth_label in input_dict, (
f"input_dict must contain key '{self.truth_label}'"
)
pred_masks_list = pred["pred_masks"]
counts = [pm.shape[1] for pm in pred_masks_list]
if target_cache is None:
target_cache = self._build_target_cache(pred_masks_list, input_dict)
indices = self.matcher(
{
"pred_masks": pred_masks_list,
"pred_logits": pred.get("pred_logits", None),
},
target_cache,
)
total_loss_focal = pred_masks_list[0].new_tensor(0.0)
total_loss_dice = pred_masks_list[0].new_tensor(0.0)
total_loss_cls = pred_masks_list[0].new_tensor(0.0)
total_loss_momentum = pred_masks_list[0].new_tensor(0.0)
total_loss_iou = pred_masks_list[0].new_tensor(0.0)
num_batches_with_loss = 0
num_batches_with_momentum = 0
num_batches_with_iou = 0
total_focal = pred_masks_list[0].new_tensor(0.0)
total_dice = pred_masks_list[0].new_tensor(0.0)
total_pairs = 0
total_ce_matched = pred_masks_list[0].new_tensor(0.0)
count_ce_matched = pred_masks_list[0].new_tensor(0.0)
total_ce_noobj = pred_masks_list[0].new_tensor(0.0)
count_ce_noobj = pred_masks_list[0].new_tensor(0.0)
total_momentum = pred_masks_list[0].new_tensor(0.0)
count_momentum_batches = 0
total_iou = pred_masks_list[0].new_tensor(0.0)
count_iou_batches = 0
queries_total = pred_masks_list[0].new_tensor(0.0)
gt_instances_total = pred_masks_list[0].new_tensor(0.0)
for batch_idx, (pm_b_full, meta, (idx_q, idx_gt)) in enumerate(
zip(pred_masks_list, target_cache, indices)
):
queries_total = queries_total + pm_b_full.shape[0]
if idx_q.numel() == 0 or meta["num_instances"] == 0:
continue
pm_b = pm_b_full[:, meta["valid_mask"]]
gt_instances_total = gt_instances_total + meta["num_instances"]
target_masks = get_target_masks(meta, pm_b.dtype)
idx_q = idx_q.to(pm_b.device).long()
idx_gt = idx_gt.to(pm_b.device).long()
pred_sel = pm_b[idx_q]
num_pairs_b = pred_sel.shape[0]
if num_pairs_b == 0:
continue
focal_per_pair, dice = self._matched_mask_losses(
pred_sel,
target_masks,
meta["target_sizes"].to(pm_b.device),
idx_gt,
)
focal_loss_b = focal_per_pair.sum() / num_pairs_b
dice_loss_b = dice.sum() / num_pairs_b
total_loss_focal = total_loss_focal + focal_loss_b
total_loss_dice = total_loss_dice + dice_loss_b
total_focal = total_focal + focal_per_pair.sum()
total_dice = total_dice + dice.sum()
total_pairs += num_pairs_b
num_batches_with_loss += 1
if "pred_logits" in pred and self.segment_key in input_dict:
logits_b = pred["pred_logits"][batch_idx]
C = logits_b.shape[-1] - 1
inst_class = meta["inst_class"].to(pm_b.device)
cls_loss_b, cls_count_b, ce_matched, ce_noobj = self._classification_terms(
logits_b, inst_class, idx_q, idx_gt
)
if cls_count_b > 0:
total_loss_cls = total_loss_cls + (cls_loss_b / cls_count_b)
if ce_matched is not None:
total_ce_matched = total_ce_matched + ce_matched
count_ce_matched = count_ce_matched + 1.0
if ce_noobj is not None:
total_ce_noobj = total_ce_noobj + ce_noobj
count_ce_noobj = count_ce_noobj + 1.0
if (
self.momentum_loss_weight > 0
and "pred_momentum" in pred
and "momentum" in input_dict
):
momentum_gt = self._batch_tensor(
input_dict["momentum"],
batch_idx,
counts,
pm_b.device,
meta["valid_mask"],
)
mom_pred_b = pred["pred_momentum"][batch_idx].to(pm_b.device)
mom_gt_per_inst = self._mean_per_instance(
momentum_gt, meta["inverse"], meta["num_instances"]
)
loss_momentum_b = self._compute_momentum_loss(
mom_pred_b, mom_gt_per_inst, idx_q, idx_gt
)
total_loss_momentum = total_loss_momentum + loss_momentum_b
total_momentum = total_momentum + loss_momentum_b
num_batches_with_momentum += 1
count_momentum_batches += 1
if self.iou_loss_weight > 0 and "pred_iou" in pred:
gt_sel = target_masks[idx_gt]
iou_pred_b = pred["pred_iou"][batch_idx].to(pm_b.device)
loss_iou_b = self._compute_iou_loss(
iou_pred_b, pred_sel, gt_sel, idx_q
)
total_loss_iou = total_loss_iou + loss_iou_b
total_iou = total_iou + loss_iou_b
num_batches_with_iou += 1
count_iou_batches += 1
if num_batches_with_momentum > 0:
total_loss_momentum = total_loss_momentum / num_batches_with_momentum
if num_batches_with_iou > 0:
total_loss_iou = total_loss_iou / num_batches_with_iou
denom = max(num_batches_with_loss, 1)
loss_masks = self.loss_weight_focal * (
total_loss_focal / denom
) + self.loss_weight_dice * (total_loss_dice / denom)
loss_cls = total_loss_cls / denom
loss = (
loss_masks
+ loss_cls
+ self.momentum_loss_weight * total_loss_momentum
+ self.iou_loss_weight * total_loss_iou
)
unmatched_queries = queries_total - total_pairs
unmatched_gt = gt_instances_total - total_pairs
components = {
"focal": total_focal / max(total_pairs, 1),
"dice": total_dice / max(total_pairs, 1),
"cls_matched": total_ce_matched / count_ce_matched.clamp_min(1),
"cls_noobj": total_ce_noobj / count_ce_noobj.clamp_min(1),
"momentum": total_momentum / max(count_momentum_batches, 1),
"iou": total_iou / max(count_iou_batches, 1),
"num_pairs": total_pairs,
"queries_total": queries_total,
"gt_instances_total": gt_instances_total,
"unmatched_queries": unmatched_queries,
"unmatched_gt": unmatched_gt,
"num_cls_matched": count_ce_matched,
"num_cls_noobj": count_ce_noobj,
}
return loss, components
[docs]
@LOSSES.register_module()
class InstanceSegmentationLoss(FastInstanceSegmentationLoss):
"""DEPRECATED alias for :class:`FastInstanceSegmentationLoss`.
The original O(QxJxP) non-fast instance loss + ``HungarianMatcher`` were removed;
they were verified bitwise-equal to the fast path. This name is kept registered
only so historical dumped configs (`type="InstanceSegmentationLoss"`) still build.
New configs should use ``FastInstanceSegmentationLoss``.
"""