"""Unified, fully configurable Panda detector (detector-v4).
This query-based instance and panoptic decoder supports the following config
options:
It exposes four axes as pure config:
1. **Pre-cross-attention point removal** (``point_filter``): an optional,
pluggable module that drops points (e.g. "stuff" / low-energy) *before* the
decoder cross-attention; mask logits are scattered back to the full cloud.
2. **Arbitrary per-query heads** (``query_heads``): any number of categorical
*and* continuous heads per label, on top of the primary categorical (PID)
head that drives matching.
3. **Configurable query count** per label.
4. **Overlapping queries** (``overlap`` per label): switches ground-truth
construction + fusion between mutually-exclusive (one-hot, greedy NMS) and
overlap-aware (multi-hot membership, independent sigmoid).
The decoder (``GenericMultiTaskDecoder``) lives in this file and composes the
low-level transformer layers from ``layers.py`` (SDPA masked cross-attention with
a bounded ``log(sigmoid+eps)`` mask). It is **not** forced to fp32; precision is
controlled by ``upcast_softmax`` (cheap, softmax-only) and an optional
``decoder_fp32`` escape hatch.
"""
from __future__ import annotations
from copy import deepcopy
from typing import Dict, Literal, Optional, Tuple
import torch
import torch.distributed as dist
import torch.nn.functional as F
from torch import nn
from timm.layers import trunc_normal_
from pimm.models.builder import MODELS, build_model
from pimm.models.losses import build_criteria
from pimm.models.modules import PointModel
from pimm.models.utils.misc import offset2bincount
from pimm.models.utils.structure import Point
from pimm.utils.comm import get_world_size, reduce_scalar_outputs_for_logging
from .layers import MLP, Block # low-level transformer layers; the decoder lives here
from .postprocess import postprocess_batch, postprocess_overlap_batch
# Default per-label specs (instance/segment keys, class counts, stuff classes).
_DEFAULT_LABEL_SPECS = {
"particle": dict(
instance_key="instance_particle",
segment_key="segment_particle",
segment_fallback_key="segment_pid",
num_classes=6,
stuff_classes=[5],
),
"interaction": dict(
instance_key="instance_interaction",
segment_key="segment_interaction",
num_classes=2,
stuff_classes=[0],
),
}
# Output keys the decoder/forward populate itself -- user head pred_keys may not
# collide with these.
_RESERVED_OUTPUT_KEYS = {
"pred_masks",
"pred_logits",
"seg_logits",
"aux_outputs",
"stuff_logits",
"stuff_probs",
"pred_regression",
"filter_logits",
}
def _as_list(value):
"""Accept a list, a single dict (with ``name``), or a ``{name: cfg}`` dict."""
if value is None:
return []
if isinstance(value, dict):
if "name" in value:
return [value]
return [dict(cfg, name=name) for name, cfg in value.items()]
return list(value)
# --------------------------------------------------------------------------- #
# Head-spec normalization #
# --------------------------------------------------------------------------- #
_DEFAULT_CONTINUOUS_CRITERION = dict(
type="SmoothL1RegressionLoss", beta=1.0, reduction="mean"
)
_DEFAULT_CATEGORICAL_CRITERION = dict(type="CrossEntropyHeadLoss", reduction="mean")
def normalize_query_heads(heads):
"""Normalize a ``query_heads`` config into a list of fully-specified dicts.
Each head is one of two kinds:
* ``kind="continuous"`` -- a regression head. Keys: ``dim`` (default 1),
``aggregation`` ('mean'|'first', default 'mean'), ``squeeze``
(default ``dim == 1``), ``criterion`` (default SmoothL1).
* ``kind="categorical"`` -- an extra classification head (in addition to the
primary PID head). Keys: ``num_classes`` (required), ``aggregation``
('mode'|'first', default 'mode'), ``criterion`` (default CE).
Common keys: ``name`` (required, unique), ``pred_key`` (default
``pred_<name>``), ``target_key`` (default ``name``), ``loss_weight``
(default 1.0), ``use_class_logits`` / ``detach_class_logits`` (default
False -- condition the head input on the primary class logits), ``required``
(default True), ``head_mlp`` (default True -- MLP vs Linear).
"""
targets = []
seen = set()
for cfg in _as_list(heads):
cfg = deepcopy(cfg)
name = cfg.get("name")
if not name:
raise ValueError("Each query head needs a non-empty 'name'")
if name in seen:
raise ValueError(f"Duplicate query head name: {name!r}")
seen.add(name)
kind = cfg.setdefault("kind", "continuous")
if kind not in ("continuous", "categorical"):
raise ValueError(
f"query head {name!r}: kind must be 'continuous' or 'categorical', "
f"got {kind!r}"
)
cfg.setdefault("pred_key", f"pred_{name}")
if cfg["pred_key"] in _RESERVED_OUTPUT_KEYS:
raise ValueError(
f"query head {name!r} uses reserved pred_key {cfg['pred_key']!r}"
)
cfg.setdefault("target_key", name)
cfg.setdefault("loss_weight", 1.0)
cfg.setdefault("use_class_logits", False)
cfg.setdefault("detach_class_logits", False)
cfg.setdefault("required", True)
cfg.setdefault("head_mlp", True)
if kind == "continuous":
cfg["dim"] = int(cfg.get("dim", cfg.get("out_dim", 1)))
cfg.setdefault("aggregation", "mean")
cfg.setdefault("squeeze", cfg["dim"] == 1)
cfg.setdefault("criterion", deepcopy(_DEFAULT_CONTINUOUS_CRITERION))
if cfg["aggregation"] not in ("mean", "first"):
raise ValueError(
f"continuous head {name!r}: aggregation must be 'mean'|'first'"
)
else: # categorical
if "num_classes" not in cfg:
raise ValueError(
f"categorical head {name!r} requires 'num_classes'"
)
cfg["num_classes"] = int(cfg["num_classes"])
cfg["dim"] = cfg["num_classes"]
cfg.setdefault("aggregation", "mode")
cfg.setdefault("squeeze", False)
cfg.setdefault("criterion", deepcopy(_DEFAULT_CATEGORICAL_CRITERION))
if cfg["aggregation"] not in ("mode", "first"):
raise ValueError(
f"categorical head {name!r}: aggregation must be 'mode'|'first'"
)
targets.append(cfg)
return targets
def _with_query_heads(label_configs, labels, query_heads):
"""Fold a top-level ``query_heads`` (dict-by-label or list for single label)
into per-label ``label_configs``."""
label_configs = deepcopy(label_configs or {})
if query_heads is None:
return label_configs
if isinstance(query_heads, dict) and not (
"name" in query_heads or "kind" in query_heads
):
for label, heads in query_heads.items():
label_configs.setdefault(label, {})["query_heads"] = heads
return label_configs
if len(labels) != 1:
raise ValueError(
"Top-level query_heads must be a dict keyed by label when training "
"multiple labels"
)
label_configs.setdefault(labels[0], {})["query_heads"] = query_heads
return label_configs
# --------------------------------------------------------------------------- #
# Pre-cross-attention point filter #
# --------------------------------------------------------------------------- #
class PointFilter(nn.Module):
"""Optional, pluggable removal of points before the decoder cross-attention.
Types:
* ``"stuff"`` -- a learned per-point binary classifier; keeps points with
``sigmoid(logit) < threshold``. During training, if ``train_filter_use_gt``
and a GT segment is available, keeps points whose class is not in
``drop_classes``. Emits ``filter_logits`` for an auxiliary BCE.
* ``"energy_threshold"`` -- fixed rule: keep points whose feature channel
``energy_channel`` exceeds ``energy_threshold`` (operates on the *raw*
backbone feature; no learnable params).
Returns ``(keep_mask, filter_logits_or_None)``. Always keeps at least one
point per non-empty cloud so the decoder still runs.
"""
def __init__(
self,
type: str = "stuff",
full_in_channels: int = 0,
hidden_channels: int = 256,
threshold: float = 0.5,
drop_classes=None,
train_filter_use_gt: bool = False,
energy_channel: int = 0,
energy_threshold: float = 0.0,
):
super().__init__()
self.type = type
self.threshold = float(threshold)
self.drop_classes = list(drop_classes or [])
self.train_filter_use_gt = bool(train_filter_use_gt)
self.energy_channel = int(energy_channel)
self.energy_threshold = float(energy_threshold)
if type == "stuff":
self.head = nn.Sequential(
nn.Linear(full_in_channels, hidden_channels),
nn.ReLU(),
nn.Linear(hidden_channels, 1),
)
elif type == "energy_threshold":
self.head = None
else:
raise ValueError(f"Unknown PointFilter type: {type!r}")
@staticmethod
def _ensure_nonempty(keep_mask):
if not keep_mask.any():
# Keep a single point so the decoder doesn't crash on an empty cloud.
keep_mask = keep_mask.clone()
keep_mask[0] = True
return keep_mask
def forward(self, feat, gt_segment=None, training=False):
filter_logits = None
if self.type == "stuff":
filter_logits = self.head(feat).squeeze(-1) # (N,)
if (
training
and self.train_filter_use_gt
and gt_segment is not None
and len(self.drop_classes) > 0
):
segment = gt_segment
if segment.dim() == 2 and segment.shape[1] == 1:
segment = segment.squeeze(1)
is_drop = torch.zeros_like(segment, dtype=torch.bool)
for cls_id in self.drop_classes:
is_drop |= segment == int(cls_id)
keep_mask = ~is_drop
else:
keep_mask = filter_logits.sigmoid() < self.threshold
else: # energy_threshold
keep_mask = feat[:, self.energy_channel] > self.energy_threshold
return self._ensure_nonempty(keep_mask), filter_logits
# --------------------------------------------------------------------------- #
# Decoder #
# --------------------------------------------------------------------------- #
class GenericMultiTaskDecoder(nn.Module):
"""Masked-cross-attention query decoder (loosely Mask2Former / OneFormer3D),
generalized for multiple labels and arbitrary per-query heads + a point filter.
"""
def __init__(
self,
label_specs,
full_in_channels,
hidden_channels,
num_heads,
depth=3,
mlp_ratio=4.0,
qkv_bias=False,
qk_scale=None,
attn_drop=0.0,
proj_drop=0.0,
drop_path=0.0,
layer_scale=None,
norm_layer=nn.LayerNorm,
act_layer=nn.GELU,
pre_norm=True,
enable_flash=True,
upcast_attention=False,
upcast_softmax=False,
pos_emb=True,
enc_mode=False,
query_type: Literal["learned"] = "learned",
mlp_point_proj=False,
supervise_attn_mask=True,
point_filter_cfg=None,
):
nn.Module.__init__(self)
self.labels = tuple(label_specs.keys())
self.label_specs = label_specs
query_slices = {}
query_start = 0
max_classes = 1
for label, spec in label_specs.items():
query_end = query_start + int(spec["num_queries"])
query_slices[label] = (query_start, query_end)
query_start = query_end
max_classes = max(max_classes, int(spec["num_classes"]))
self.query_slices = query_slices
self.total_queries = query_start
# ---- shared decoder modules ------------------------------------- #
self.full_in_channels = full_in_channels
self.mask_channels = hidden_channels
self.num_classes = max_classes
self.enc_mode = enc_mode
self.num_queries = self.total_queries
self.query_type = query_type
if query_type == "learned":
self.query_feat = nn.Embedding(self.total_queries, hidden_channels)
self.query_embed = nn.Embedding(self.total_queries, hidden_channels)
self.pos_emb = nn.Sequential(
nn.Linear(3, hidden_channels),
nn.GELU(),
nn.Linear(hidden_channels, hidden_channels),
) if pos_emb else None
# attention-mask annealing (configured via set_attn_mask_anneal)
self.attn_mask_anneal = False
self.attn_mask_anneal_steps = 10000
self.attn_mask_warmup_steps = 0
self.attn_mask_progressive = False
self.attn_mask_progressive_delay = 0
self.supervise_attn_mask = supervise_attn_mask
self.blocks = nn.ModuleList(
[
Block(
channels=hidden_channels,
num_heads=num_heads,
mlp_ratio=mlp_ratio,
qkv_bias=qkv_bias,
qk_scale=qk_scale,
attn_drop=attn_drop,
proj_drop=proj_drop,
drop_path=drop_path,
layer_scale=layer_scale,
norm_layer=norm_layer,
act_layer=act_layer,
pre_norm=pre_norm,
enable_flash=enable_flash,
upcast_attention=upcast_attention,
upcast_softmax=upcast_softmax,
use_attn_mask=True,
attn_mask_eps=1e-6,
attn_mask_anneal=self.attn_mask_anneal,
attn_mask_anneal_steps=self.attn_mask_anneal_steps,
attn_mask_warmup_steps=self.attn_mask_warmup_steps,
supervise_attn_mask=self.supervise_attn_mask,
is_last_block=(i == depth - 1),
)
for i in range(depth)
]
)
self.final_norm = norm_layer(hidden_channels)
self.full_point_proj = (
MLP(full_in_channels, hidden_channels, hidden_channels)
if mlp_point_proj
else nn.Linear(full_in_channels, hidden_channels)
)
# ---- per-label / per-query heads -------------------------------- #
# No single-task class head; we use per-label heads instead.
self.cls_pred = nn.Identity()
self.cls_pred_by_label = nn.ModuleDict() # primary PID head (matching)
self.stuff_head_by_label = nn.ModuleDict() # auxiliary per-label stuff loss
self.head_by_key = nn.ModuleDict() # extra categorical/continuous
self.query_heads_by_label = {}
for label, spec in label_specs.items():
out_classes = int(spec["num_classes"]) + 1
self.cls_pred_by_label[label] = (
MLP(hidden_channels, hidden_channels, out_classes)
if mlp_point_proj
else nn.Linear(hidden_channels, out_classes)
)
if spec["use_stuff_head"]:
self.stuff_head_by_label[label] = nn.Sequential(
nn.Linear(full_in_channels, hidden_channels),
nn.ReLU(),
nn.Linear(hidden_channels, 1),
)
heads = normalize_query_heads(spec.get("query_heads"))
self.query_heads_by_label[label] = heads
for head in heads:
head_key = self._head_key(label, head["name"])
in_dim = hidden_channels
if head["use_class_logits"]:
in_dim += int(spec["num_classes"])
out_dim = int(head["dim"])
self.head_by_key[head_key] = (
MLP(in_dim, hidden_channels, out_dim)
if head.get("head_mlp", True)
else nn.Linear(in_dim, out_dim)
)
# Optional pre-cross-attention point filter (global, shared across labels).
self.point_filter = None
if point_filter_cfg:
cfg = deepcopy(point_filter_cfg)
cfg.setdefault("full_in_channels", full_in_channels)
cfg.setdefault("hidden_channels", hidden_channels)
self.point_filter = PointFilter(**cfg)
self.apply(self._init_weights)
# ----- decoder core ---------------------------------------------------- #
@staticmethod
def _init_weights(module):
if isinstance(module, nn.Linear):
trunc_normal_(module.weight, std=0.02)
if module.bias is not None:
nn.init.zeros_(module.bias)
def set_attn_mask_anneal(
self,
enable: bool,
anneal_steps: int = 10000,
warmup_steps: int = 0,
progressive: bool = False,
progressive_delay: int = 0,
):
"""Configure attention-mask annealing for all blocks."""
self.attn_mask_anneal = enable
self.attn_mask_anneal_steps = anneal_steps
self.attn_mask_warmup_steps = warmup_steps
self.attn_mask_progressive = progressive
self.attn_mask_progressive_delay = progressive_delay
for i, block in enumerate(self.blocks):
block.attn_mask_anneal = enable
block.attn_mask_anneal_steps = anneal_steps
if progressive and progressive_delay > 0:
block.attn_mask_warmup_steps = warmup_steps + (i * progressive_delay)
else:
block.attn_mask_warmup_steps = warmup_steps
def update_anneal_step(self, step: int):
for block in self.blocks:
block.set_anneal_step(step)
def _get_queries(
self, point: Point
) -> Tuple[torch.Tensor, Optional[torch.Tensor], torch.Tensor, torch.Tensor]:
batch_size = point.offset.shape[0]
device = point.feat.device
max_queries = self.num_queries
if self.query_type == "learned":
base_q = self.query_feat.weight # [Q, C]
base_pos = self.query_embed.weight if hasattr(self, "query_embed") else None
q = base_q.unsqueeze(0).repeat(batch_size, 1, 1) # [B, Q, C]
pos_q = base_pos.unsqueeze(0).repeat(batch_size, 1, 1) if base_pos is not None else None
counts = torch.full((batch_size,), max_queries, dtype=torch.int32, device=device)
valid_mask = torch.ones(batch_size, max_queries, dtype=torch.bool, device=device)
return q, pos_q, counts, valid_mask
raise NotImplementedError(f"Invalid query type: {self.query_type}")
def _forward_decoder(self, point: Point, return_aux: bool = False):
"""Run the transformer decoder blocks. Returns out_q, final_mask_logits,
query_counts, query_valid (+ aux lists if return_aux)."""
point_proj = point.feat # projection done in caller
pos_k = self.pos_emb(point.coord) if self.pos_emb else None
cu_seqlens_kv = torch.cat([point.offset.new_zeros(1), point.offset]).int() # [B + 1]
max_seqlen_kv = cu_seqlens_kv.diff().max()
q, pos_q, query_counts, query_valid = self._get_queries(point)
cu_seqlens_q = torch.cat([query_counts.new_zeros(1), query_counts.cumsum(dim=0)]).int()
max_seqlen_q = int(query_counts.max().item()) if query_counts.numel() > 0 else 0
q = q.reshape(-1, self.mask_channels) # [B * Q, C]
pos_q = pos_q.reshape(-1, self.mask_channels) if pos_q is not None else None
query_valid = query_valid.reshape(-1, 1)
query_valid_f = query_valid.to(q.dtype)
aux_outputs = []
aux_p_hat_list = []
final_mask_logits = None
for blk in self.blocks:
q, mask_logits, mask_embed, mask_point_proj = blk(
q, point_proj, cu_seqlens_q, cu_seqlens_kv,
max_seqlen_q, max_seqlen_kv, pos_q, pos_k,
)
if mask_logits is not None:
mask_logits = mask_logits * query_valid_f
final_mask_logits = mask_logits
q = q * query_valid_f
if return_aux:
aux_outputs.append(self.final_norm(q))
aux_p_hat_list.append(mask_logits if self.supervise_attn_mask else None)
outputs = {
"out_q": self.final_norm(q),
"final_mask_logits": final_mask_logits,
"query_counts": query_counts.to(torch.long),
"query_valid": query_valid.squeeze(-1).bool(),
}
if return_aux:
outputs["aux_q_list"] = aux_outputs[:-1]
outputs["aux_mask_logits_list"] = aux_p_hat_list[:-1]
return outputs
def up_cast(self, point):
"""enc_mode=False: no-op. enc_mode=True: walk the pooling hierarchy,
concatenating multi-scale features up to point resolution."""
if not self.enc_mode:
return point
while "pooling_parent" in point.keys():
assert "pooling_inverse" in point.keys()
parent = point.pop("pooling_parent")
inverse = point.pop("pooling_inverse")
parent.feat = torch.cat([parent.feat, point.feat[inverse]], dim=-1)
point = parent
return point
@staticmethod
def _head_key(label, name):
return f"{label}__{name}".replace(".", "_").replace("/", "_")
# ----- per-query head predictions ------------------------------------- #
def _compute_head_embeddings(self, label, q_features, class_embed):
spec = self.label_specs[label]
num_classes = int(spec["num_classes"])
embeddings = {}
for head in self.query_heads_by_label.get(label, []):
head_input = q_features
if head["use_class_logits"]:
cls_input = class_embed[:, :num_classes]
if head["detach_class_logits"]:
cls_input = cls_input.detach()
head_input = torch.cat([q_features, cls_input], dim=-1)
pred = self.head_by_key[self._head_key(label, head["name"])](head_input)
if head.get("squeeze") and pred.dim() == 2 and pred.shape[-1] == 1:
pred = pred.squeeze(-1)
embeddings[head["name"]] = (head, pred)
return embeddings
def _compute_label_predictions(
self, label, q_features, mask_logits, point, query_valid
):
class_embed = self.cls_pred_by_label[label](q_features)
head_embeddings = self._compute_head_embeddings(label, q_features, class_embed)
pred_masks = []
pred_cls = []
pred_logits = []
pred_heads = {name: [] for name in head_embeddings}
C = int(self.label_specs[label]["num_classes"])
label_q_start, label_q_end = self.query_slices[label]
total_q = self.total_queries
B = point.offset.shape[0]
counts = offset2bincount(point.offset).to(torch.long)
point_offsets = torch.cat([counts.new_zeros(1), point.offset])
for b in range(B):
P_b = counts[b].item()
q_start = b * total_q + label_q_start
q_end = b * total_q + label_q_end
p_start, p_end = point_offsets[b], point_offsets[b + 1]
mask_logits_b = mask_logits[q_start:q_end, p_start:p_end]
cls_b = class_embed[q_start:q_end]
valid_b = query_valid[q_start:q_end]
mask_logits_b = mask_logits_b[valid_b]
cls_b = cls_b[valid_b]
pred_masks.append(mask_logits_b)
pred_cls.append(cls_b)
for name, (_, values) in head_embeddings.items():
pred_heads[name].append(values[q_start:q_end][valid_b])
if mask_logits_b.shape[0] > 0:
s = mask_logits_b.transpose(0, 1).unsqueeze(-1)
c = cls_b[:, :C].unsqueeze(0)
logits_b = torch.logsumexp(s + c, dim=1)
else:
logits_b = mask_logits.new_zeros((P_b, C))
pred_logits.append(logits_b)
pred_logits = (
torch.cat(pred_logits, dim=0)
if pred_logits
else mask_logits.new_zeros((0, C))
)
output = {
"pred_masks": pred_masks,
"pred_logits": pred_cls,
"seg_logits": pred_logits,
}
if pred_heads:
output["pred_regression"] = pred_heads # back-compat alias for postprocess
for name, values in pred_heads.items():
head = head_embeddings[name][0]
output[head["pred_key"]] = values
return output
# ----- point-filter plumbing ------------------------------------------ #
def _scatter_mask_logits(self, mask_logits, keep_mask, num_points, dropped_mask_logit=-1e4):
"""Expand (Q_total, N_kept) mask logits back to (Q_total, N_full),
filling dropped columns with a large-negative sentinel."""
if mask_logits is None:
return None
full = mask_logits.new_full(
(mask_logits.shape[0], num_points), dropped_mask_logit
)
full[:, keep_mask] = mask_logits
return full
def forward(self, point: Point):
point_full = self.up_cast(point)
num_points = point_full.feat.shape[0]
# ---- optional pre-cross-attention point removal ------------------- #
keep_mask = None
filter_logits = None
if self.point_filter is not None:
gt_segment = getattr(point_full, "_filter_gt_segment", None)
keep_mask, filter_logits = self.point_filter(
point_full.feat, gt_segment=gt_segment, training=self.training
)
decoder_point = point_full[keep_mask]
else:
decoder_point = point_full.copy()
decoder_point.feat = self.full_point_proj(decoder_point.feat)
return_aux = self.training
decoder_outputs = self._forward_decoder(decoder_point, return_aux=return_aux)
query_valid = decoder_outputs["query_valid"]
out_q = decoder_outputs["out_q"]
final_mask_logits = decoder_outputs["final_mask_logits"]
aux_q_list = decoder_outputs.get("aux_q_list", [])
aux_mask_logits_list = decoder_outputs.get("aux_mask_logits_list", [])
if keep_mask is not None:
final_mask_logits = self._scatter_mask_logits(
final_mask_logits, keep_mask, num_points
)
aux_mask_logits_list = [
self._scatter_mask_logits(m, keep_mask, num_points)
for m in aux_mask_logits_list
]
outputs_by_label = {}
for label in self.labels:
predictions = self._compute_label_predictions(
label, out_q, final_mask_logits, point_full, query_valid
)
if return_aux:
aux_outputs = []
for aux_q, aux_mask_logits in zip(aux_q_list, aux_mask_logits_list):
if aux_mask_logits is None:
continue
aux_outputs.append(
self._compute_label_predictions(
label, aux_q, aux_mask_logits, point_full, query_valid
)
)
if aux_outputs:
predictions["aux_outputs"] = aux_outputs
if label in self.stuff_head_by_label:
stuff_logits = self.stuff_head_by_label[label](
point_full.feat
).squeeze(-1)
predictions["stuff_logits"] = stuff_logits
predictions["stuff_probs"] = stuff_logits.sigmoid()
outputs_by_label[label] = predictions
if filter_logits is not None:
outputs_by_label["__filter_logits__"] = filter_logits
return outputs_by_label
# --------------------------------------------------------------------------- #
# Detector #
# --------------------------------------------------------------------------- #
[docs]
@MODELS.register_module("detector-v4")
class UnifiedDetector(PointModel):
"""Unified Panda detector: arbitrary per-query heads, optional pre-attention
point removal, configurable queries, and per-label overlap.
Self-contained: owns its decoder stack and multi-label plumbing; imports only
helpers (postprocess, registries, point utils)."""
# ----- per-label config selection helpers ----------------------------- #
@staticmethod
def _select_value(value, labels, label, idx, default=None):
if value is None:
return default
if isinstance(value, dict):
return value.get(label, default)
if isinstance(value, (list, tuple)) and len(value) == len(labels):
return value[idx]
return value
@classmethod
def _select_stuff_classes(cls, value, labels, label, idx, default=None):
if value is None:
return default
if isinstance(value, dict):
return value.get(label, default)
if isinstance(value, (list, tuple)) and len(value) == len(labels):
if all(isinstance(v, (list, tuple, set)) for v in value):
return value[idx]
return value
# ----- shared plumbing ------------------------------------------------ #
[docs]
def up_cast(self, point: Point) -> Point:
enc_mode = getattr(self.backbone, "enc_mode", True)
if not enc_mode:
return point
while "pooling_parent" in point.keys():
assert "pooling_inverse" in point.keys()
parent = point.pop("pooling_parent")
inverse = point.pop("pooling_inverse")
parent.feat = torch.cat([parent.feat, point.feat[inverse]], dim=-1)
point = parent
return point
[docs]
def update_anneal_step(self, step: int):
self.decoder.update_anneal_step(step)
def _segment_key(self, input_dict: Dict, spec: Dict) -> str:
segment_key = spec["segment_key"]
if segment_key in input_dict:
return segment_key
fallback = spec.get("segment_fallback_key")
if fallback is not None and fallback in input_dict:
return fallback
return segment_key
def _task_input(self, input_dict: Dict, label: str) -> Dict:
spec = self.label_specs[label]
instance_key = spec["instance_key"]
segment_key = self._segment_key(input_dict, spec)
if instance_key not in input_dict:
raise KeyError(f"Missing instance labels for {label!r}: {instance_key}")
if segment_key not in input_dict:
raise KeyError(f"Missing semantic labels for {label!r}: {segment_key}")
task_input = dict(input_dict)
task_input["instance"] = input_dict[instance_key]
task_input["segment"] = input_dict[segment_key]
return task_input
@staticmethod
def _compute_stuff_loss(decoder, label, stuff_logits, task_input):
segment = task_input["segment"]
if isinstance(segment, torch.Tensor) and segment.dim() == 2 and segment.shape[1] == 1:
segment = segment.squeeze(1)
stuff_target = torch.zeros_like(stuff_logits)
for stuff_class in decoder.label_specs[label]["stuff_classes"]:
stuff_target[segment == int(stuff_class)] = 1.0
return F.binary_cross_entropy_with_logits(
stuff_logits, stuff_target, reduction="mean"
)
def _sync_scalar_outputs(self, return_dict: Dict) -> Dict:
return reduce_scalar_outputs_for_logging(return_dict)
def __init__(
self,
full_in_channels,
hidden_channels,
num_heads,
labels=("particle",),
num_queries=None,
num_classes=None,
label_configs=None,
query_heads=None,
overlap=None, # None -> per-label `label_configs[...]["overlap"]` (default False);
# a bool here forces it for ALL labels (see _select_value)
point_filter=None,
filter_loss_weight=1.0,
loss_weights=None,
eval_label=None,
backbone=None,
criteria=None,
criteria_by_label=None,
depth=3,
mlp_ratio=4.0,
qkv_bias=False,
qk_scale=None,
attn_drop=0.0,
proj_drop=0.0,
drop_path=0.0,
layer_scale=None,
norm_layer=nn.LayerNorm,
act_layer=nn.GELU,
pre_norm=True,
enable_flash=True,
upcast_attention=False,
upcast_softmax=False,
pos_emb=True,
attn_mask_anneal=False,
attn_mask_anneal_steps=10000,
attn_mask_warmup_steps=0,
attn_mask_progressive=False,
attn_mask_progressive_delay=0,
query_type: Literal["learned"] = "learned",
use_stuff_head=False,
stuff_classes=None,
supervise_attn_mask=True,
mlp_point_proj=False,
stuff_threshold=0.5,
mask_threshold=0.5,
conf_threshold=0.5,
nms_kernel="gaussian",
nms_sigma=2.0,
nms_pre=-1,
nms_max=-1,
min_points=2,
fill_uncovered=False,
):
PointModel.__init__(self)
if isinstance(labels, str):
labels = (labels,)
self.labels = tuple(labels)
if len(self.labels) == 0:
raise ValueError("labels must contain at least one label name")
label_configs = _with_query_heads(label_configs, self.labels, query_heads)
self.label_specs = self._build_label_specs(
self.labels,
label_configs=label_configs,
num_queries=num_queries,
num_classes=num_classes,
stuff_classes=stuff_classes,
use_stuff_head=use_stuff_head,
loss_weights=loss_weights,
overlap=overlap,
)
self.eval_label = eval_label or self.labels[-1]
if self.eval_label not in self.label_specs:
raise ValueError(
f"eval_label={self.eval_label!r} must be one of {self.labels}"
)
self.point_filter_cfg = deepcopy(point_filter) if point_filter else None
self.filter_loss_weight = float(filter_loss_weight)
self.backbone = build_model(backbone)
self.criteria_by_label = {
label: build_criteria(self._criteria_cfg(label, criteria, criteria_by_label))
for label in self.labels
}
self.decoder = GenericMultiTaskDecoder(
label_specs=self.label_specs,
full_in_channels=full_in_channels,
hidden_channels=hidden_channels,
num_heads=num_heads,
depth=depth,
mlp_ratio=mlp_ratio,
qkv_bias=qkv_bias,
qk_scale=qk_scale,
attn_drop=attn_drop,
proj_drop=proj_drop,
drop_path=drop_path,
layer_scale=layer_scale,
norm_layer=norm_layer,
act_layer=act_layer,
pre_norm=pre_norm,
enable_flash=enable_flash,
upcast_attention=upcast_attention,
upcast_softmax=upcast_softmax,
pos_emb=pos_emb,
enc_mode=False,
query_type=query_type,
mlp_point_proj=mlp_point_proj,
supervise_attn_mask=supervise_attn_mask,
point_filter_cfg=self.point_filter_cfg,
)
if attn_mask_anneal:
self.decoder.set_attn_mask_anneal(
enable=True,
anneal_steps=attn_mask_anneal_steps,
warmup_steps=attn_mask_warmup_steps,
progressive=attn_mask_progressive,
progressive_delay=attn_mask_progressive_delay,
)
self.postprocess_cfg = dict(
stuff_threshold=stuff_threshold,
mask_threshold=mask_threshold,
conf_threshold=conf_threshold,
nms_kernel=nms_kernel,
nms_sigma=nms_sigma,
nms_pre=nms_pre,
nms_max=nms_max,
min_points=min_points,
fill_uncovered=fill_uncovered,
)
# ----- config / spec building ----------------------------------------- #
def _criteria_cfg(self, label, criteria, criteria_by_label):
"""Auto-inject the per-label head specs + overlap flag into the loss cfg
so the user does not have to duplicate them."""
base = (
criteria_by_label[label]
if criteria_by_label is not None and label in criteria_by_label
else criteria
)
spec = self.label_specs[label]
# Instance-loss variants are all upgraded to the unified loss, which is a
# superset (handles the primary mask/dice/cls plus arbitrary per-query
# heads and the overlap path). Per-label head specs + overlap flag are
# injected so configs don't have to duplicate them.
_instance_types = {
"InstanceSegmentationLoss",
"FastInstanceSegmentationLoss",
"FastInstanceSegmentationRegressionLoss",
"FastUnifiedInstanceLoss",
}
def _inject(cfg):
cfg = deepcopy(cfg)
if isinstance(cfg, dict):
if cfg.get("type") in _instance_types:
cfg["type"] = "FastUnifiedInstanceLoss"
# carry over regression_targets written the v3m3 way
legacy = cfg.pop("regression_targets", None)
heads = cfg.get("query_heads", spec.get("query_heads"))
if heads is None and legacy is not None:
heads = legacy
cfg["query_heads"] = heads
cfg.setdefault("overlap", spec.get("overlap", False))
return cfg
if isinstance(cfg, (list, tuple)):
return type(cfg)(_inject(c) for c in cfg)
return cfg
return _inject(base)
@classmethod
def _build_label_specs(
cls,
labels,
label_configs=None,
num_queries=None,
num_classes=None,
stuff_classes=None,
use_stuff_head=True,
loss_weights=None,
overlap=None,
):
label_configs = label_configs or {}
specs = {}
for idx, label in enumerate(labels):
spec = deepcopy(_DEFAULT_LABEL_SPECS.get(label, {}))
spec.update(deepcopy(label_configs.get(label, {})))
# precedence: explicit top-level num_queries > label_configs > 32
queries = cls._select_value(
num_queries, labels, label, idx, spec.get("num_queries", 32)
)
classes = cls._select_value(
num_classes, labels, label, idx, spec.get("num_classes")
)
stuff = cls._select_stuff_classes(
stuff_classes, labels, label, idx, spec.get("stuff_classes", [])
)
task_use_stuff = cls._select_value(use_stuff_head, labels, label, idx, True)
loss_weight = cls._select_value(loss_weights, labels, label, idx, 1.0)
label_overlap = cls._select_value(
overlap, labels, label, idx, spec.get("overlap", False)
)
if queries is None:
raise ValueError(f"num_queries is required for label {label!r}")
if classes is None:
raise ValueError(
f"num_classes is required for label {label!r}; provide "
"num_classes or label_configs"
)
if "instance_key" not in spec:
spec["instance_key"] = f"instance_{label}"
if "segment_key" not in spec:
spec["segment_key"] = f"segment_{label}"
spec["num_queries"] = int(queries)
spec["num_classes"] = int(classes)
spec["stuff_classes"] = list(stuff or [])
spec["use_stuff_head"] = bool(task_use_stuff)
spec["loss_weight"] = float(loss_weight)
spec["overlap"] = bool(label_overlap)
spec["query_heads"] = normalize_query_heads(spec.get("query_heads"))
specs[label] = spec
return specs
def _prediction_keys_for_label(self, label):
return [
head["pred_key"]
for head in self.label_specs[label].get("query_heads", [])
]
# ----- forward -------------------------------------------------------- #
[docs]
def forward(self, input_dict, return_point=False):
point = Point(input_dict)
point = self.backbone(point)
point_full = self.up_cast(point)
# Stash GT segment for an optional GT-driven point filter.
if self.point_filter_cfg and self.training:
eval_spec = self.label_specs[self.eval_label]
seg_key = self._segment_key(input_dict, eval_spec)
if seg_key in input_dict:
point_full._filter_gt_segment = input_dict[seg_key]
decoder_point = Point(point_full.copy())
if hasattr(point_full, "_filter_gt_segment"):
decoder_point._filter_gt_segment = point_full._filter_gt_segment
outputs_by_label = self.decoder(decoder_point)
filter_logits = outputs_by_label.pop("__filter_logits__", None)
loss = None
return_dict = {}
emit_predictions = not self.training
for label in self.labels:
task_input = None
spec = self.label_specs[label]
segment_key = self._segment_key(input_dict, spec)
has_targets = (
spec["instance_key"] in input_dict and segment_key in input_dict
)
if has_targets or self.training:
task_input = self._task_input(input_dict, label)
outputs = outputs_by_label[label]
if emit_predictions:
return_dict[f"{label}_seg_logits"] = outputs["seg_logits"]
return_dict[f"{label}_pred_logits"] = outputs.get("pred_logits")
return_dict[f"{label}_pred_masks"] = outputs.get("pred_masks")
for pred_key in self._prediction_keys_for_label(label):
if pred_key in outputs:
return_dict[f"{label}_{pred_key}"] = outputs[pred_key]
if "stuff_probs" in outputs:
return_dict[f"{label}_stuff_probs"] = outputs["stuff_probs"]
if task_input is not None:
loss_result = self.criteria_by_label[label](outputs, task_input)
if isinstance(loss_result, tuple):
task_loss, components = loss_result
else:
task_loss, components = loss_result, {}
task_loss = task_loss * spec["loss_weight"]
loss = task_loss if loss is None else loss + task_loss
return_dict[f"{label}_loss"] = task_loss
for key, value in components.items():
return_dict[f"{label}_{key}"] = value
if spec["use_stuff_head"] and "stuff_logits" in outputs:
stuff_loss = self._compute_stuff_loss(
self.decoder, label, outputs["stuff_logits"], task_input
)
loss = loss + stuff_loss
return_dict[f"{label}_stuff_loss"] = stuff_loss
# Optional point-filter supervision (global, label-agnostic).
if filter_logits is not None and self.training:
filter_loss = self._compute_filter_loss(filter_logits, input_dict)
if filter_loss is not None:
loss = (
filter_loss if loss is None else loss + filter_loss
)
return_dict["filter_loss"] = filter_loss
if emit_predictions or return_point:
primary_outputs = outputs_by_label[self.eval_label]
primary_spec = self.label_specs[self.eval_label]
point_full.outputs = primary_outputs
point_full.outputs_by_label = outputs_by_label
point_full.pred_cls = primary_outputs["pred_logits"]
point_full.pred_masks = primary_outputs["pred_masks"]
point_full.pred_logits = primary_outputs["seg_logits"]
point_full.pred_regression = primary_outputs.get("pred_regression")
if primary_spec["instance_key"] in input_dict:
point_full.instance = input_dict[primary_spec["instance_key"]]
primary_segment_key = self._segment_key(input_dict, primary_spec)
if primary_segment_key in input_dict:
point_full.segment = input_dict[primary_segment_key]
if emit_predictions:
return_dict["outputs_by_label"] = outputs_by_label
return_dict["seg_logits_by_label"] = {
label: o["seg_logits"] for label, o in outputs_by_label.items()
}
return_dict["pred_logits_by_label"] = {
label: o["pred_logits"] for label, o in outputs_by_label.items()
}
return_dict["pred_masks_by_label"] = {
label: o["pred_masks"] for label, o in outputs_by_label.items()
}
return_dict["pred_regression_by_label"] = {
label: o.get("pred_regression", {})
for label, o in outputs_by_label.items()
}
return_dict["seg_logits"] = primary_outputs["seg_logits"]
return_dict["pred_logits"] = primary_outputs.get("pred_logits")
return_dict["pred_masks"] = primary_outputs.get("pred_masks")
return_dict["pred_regression"] = primary_outputs.get("pred_regression")
for pred_key in self._prediction_keys_for_label(self.eval_label):
if pred_key in primary_outputs:
return_dict[pred_key] = primary_outputs[pred_key]
return_dict["stuff_probs"] = primary_outputs.get("stuff_probs")
return_dict["point_counts"] = offset2bincount(point_full.offset)
if loss is not None:
return_dict["loss"] = loss
if return_point:
return_dict["point"] = point_full
return self._sync_scalar_outputs(return_dict)
def _compute_filter_loss(self, filter_logits, input_dict):
"""Binary BCE supervising the point filter to drop `drop_classes`."""
drop_classes = self.point_filter_cfg.get("drop_classes") or []
if not drop_classes:
return None
eval_spec = self.label_specs[self.eval_label]
seg_key = self._segment_key(input_dict, eval_spec)
if seg_key not in input_dict:
return None
segment = input_dict[seg_key]
if segment.dim() == 2 and segment.shape[1] == 1:
segment = segment.squeeze(1)
target = torch.zeros_like(filter_logits)
for cls_id in drop_classes:
target[segment == int(cls_id)] = 1.0
loss = F.binary_cross_entropy_with_logits(filter_logits, target)
return self.filter_loss_weight * loss
# ----- postprocess ---------------------------------------------------- #
[docs]
def postprocess(
self,
forward_output: dict,
label: str = None,
stuff_threshold: float = None,
mask_threshold: float = None,
conf_threshold: float = None,
nms_kernel: str = None,
nms_sigma: float = None,
nms_pre: int = None,
nms_max: int = None,
min_points: int = None,
background_class_label: int = None,
fill_uncovered: bool = None,
):
label = label or self.eval_label
cfg = self.postprocess_cfg.copy()
overrides = {
"stuff_threshold": stuff_threshold,
"mask_threshold": mask_threshold,
"conf_threshold": conf_threshold,
"nms_kernel": nms_kernel,
"nms_sigma": nms_sigma,
"nms_pre": nms_pre,
"nms_max": nms_max,
"min_points": min_points,
"background_class_label": background_class_label,
"fill_uncovered": fill_uncovered,
}
for key, value in overrides.items():
if value is not None:
cfg[key] = value
if "outputs_by_label" in forward_output:
task_output = forward_output["outputs_by_label"][label]
pred_masks = task_output["pred_masks"]
pred_logits = task_output["pred_logits"]
stuff_probs = task_output.get("stuff_probs")
pred_regression = task_output.get("pred_regression")
point_counts = forward_output.get("point_counts")
elif (
"pred_masks_by_label" in forward_output
and label in forward_output["pred_masks_by_label"]
):
pred_masks = forward_output["pred_masks_by_label"][label]
pred_logits = forward_output["pred_logits_by_label"][label]
stuff_probs = None
if "stuff_probs_by_label" in forward_output:
stuff_probs = forward_output["stuff_probs_by_label"].get(label)
pred_regression = None
if "pred_regression_by_label" in forward_output:
pred_regression = forward_output["pred_regression_by_label"].get(label)
point_counts = forward_output.get("point_counts")
else:
pred_masks = forward_output["pred_masks"]
pred_logits = forward_output["pred_logits"]
stuff_probs = forward_output.get("stuff_probs")
pred_regression = forward_output.get("pred_regression")
point_counts = forward_output.get("point_counts")
if point_counts is None:
point_counts = forward_output.get(f"{label}_point_counts")
# Single entry point, dispatched by the label's `overlap` config:
# overlap=False -> mutually-exclusive greedy panoptic fusion (per-point
# instance/class labels). UNCHANGED.
# overlap=True -> overlap-preserving per-query rings (independent sigmoid
# masks), selected by mask/class confidence only (no physics dedup).
if self.label_specs[label].get("overlap", False):
return postprocess_overlap_batch(
pred_masks=pred_masks,
pred_logits=pred_logits,
num_classes=int(self.label_specs[label]["num_classes"]),
pred_regression=pred_regression,
**cfg,
)
return postprocess_batch(
pred_masks=pred_masks,
pred_logits=pred_logits,
stuff_probs=stuff_probs,
pred_regression=pred_regression,
point_counts=point_counts,
stuff_classes=self.label_specs[label]["stuff_classes"],
**cfg,
)