|
|
import torch |
|
|
import logging |
|
|
from typing import Any, List, Tuple, Dict |
|
|
from torchmetrics import MaxMetric, MeanMetric |
|
|
from torchmetrics.classification import BinaryAccuracy, BinaryF1Score |
|
|
from torch_geometric.nn.pool.consecutive import consecutive_cluster |
|
|
|
|
|
from src.utils import init_weights, PanopticSegmentationOutput, \ |
|
|
PartitionParameterSearchStorage |
|
|
from src.metrics import MeanAveragePrecision3D, PanopticQuality3D, \ |
|
|
ConfusionMatrix |
|
|
from src.models.semantic import SemanticSegmentationModule |
|
|
from src.loss import BCEWithLogitsLoss |
|
|
from src.data import NAG |
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
__all__ = ['PanopticSegmentationModule'] |
|
|
|
|
|
|
|
|
class PanopticSegmentationModule(SemanticSegmentationModule): |
|
|
"""A LightningModule for panoptic segmentation of point clouds. |
|
|
|
|
|
:param net: torch.nn.Module |
|
|
Backbone model. This can typically be an `SPT` object |
|
|
:param edge_affinity_head: torch.nn.Module |
|
|
Edge affinity prediction head for instance/panoptic graph |
|
|
clustering. This is typically an MLP |
|
|
:param partitioner: src.nn.instance.InstancePartitioner |
|
|
Instance partition head, expects a fully-fledged |
|
|
`InstancePartitioner` module as input. This module is only |
|
|
called when the actual instance/panoptic segmentation is |
|
|
required. At train time, it is not essential, since we do not |
|
|
propagate gradient to its parameters. However, we may still tune |
|
|
its parameters to maximize instance/panoptic metrics on the |
|
|
train set. This tuning involves a simple grid-search on a small |
|
|
range of parameters and needs to be called at least once at the |
|
|
very end of training |
|
|
:param criterion: torch.nn._Loss |
|
|
Loss |
|
|
:param optimizer: torch.optim.Optimizer |
|
|
Optimizer |
|
|
:param scheduler: torch.optim.lr_scheduler.LRScheduler |
|
|
Learning rate scheduler |
|
|
:param num_classes: int |
|
|
Number of classes in the dataset |
|
|
:param stuff_classes: List[int] |
|
|
Indices of the classes to be treated as 'stuff', as opposed to |
|
|
'thing' |
|
|
:param class_names: List[str] |
|
|
Name for each class |
|
|
:param sampling_loss: bool |
|
|
If True, the target labels will be obtained from labels of |
|
|
the points sampled in the batch at hand. This affects |
|
|
training supervision where sampling augmentations may be |
|
|
used for dropping some points or superpoints. If False, the |
|
|
target labels will be based on exact superpoint-wise |
|
|
histograms of labels computed at preprocessing time, |
|
|
disregarding potential level-0 point down-sampling |
|
|
:param loss_type: str |
|
|
Type of loss applied. |
|
|
'ce': cross-entropy (if `multi_stage_loss_lambdas` is used, |
|
|
all 1+ levels will be supervised with cross-entropy). |
|
|
'kl': Kullback-Leibler divergence (if `multi_stage_loss_lambdas` |
|
|
is used, all 1+ levels will be supervised with cross-entropy). |
|
|
'ce_kl': cross-entropy on level 1 and Kullback-Leibler for |
|
|
all levels above |
|
|
'wce': not documented for now |
|
|
'wce_kl': not documented for now |
|
|
:param weighted_loss: bool |
|
|
If True, the loss will be weighted based on the class |
|
|
frequencies computed on the train dataset. See |
|
|
`BaseDataset.get_class_weight()` for more |
|
|
:param init_linear: str |
|
|
Initialization method for all linear layers. Supports |
|
|
'xavier_uniform', 'xavier_normal', 'kaiming_uniform', |
|
|
'kaiming_normal', 'trunc_normal' |
|
|
:param init_rpe: str |
|
|
Initialization method for all linear layers producing |
|
|
relative positional encodings. Supports 'xavier_uniform', |
|
|
'xavier_normal', 'kaiming_uniform', 'kaiming_normal', |
|
|
'trunc_normal' |
|
|
:param transformer_lr_scale: float |
|
|
Scaling parameter applied to the learning rate for the |
|
|
`TransformerBlock` in each `Stage` and for the pooling block |
|
|
in `DownNFuseStage` modules. Setting this to a value lower |
|
|
than 1 mitigates exploding gradients in attentive blocks |
|
|
during training |
|
|
:param multi_stage_loss_lambdas: List[float] |
|
|
List of weights for combining losses computed on the output |
|
|
of each partition level. If not specified, the loss will |
|
|
be computed on the level 1 outputs only |
|
|
:param edge_affinity_criterion: torch.nn._Loss |
|
|
Loss on the edges of the superpoint level 1 for affinity |
|
|
prediction |
|
|
:param edge_affinity_loss_weights: List[float] |
|
|
Weights for insisting on certain cases in the edge affinity |
|
|
loss: |
|
|
- 0: same-class same-object edges |
|
|
- 1: same-class different-object edges |
|
|
- 2: different-class same-object edges |
|
|
- 3: different-class different-object edges |
|
|
:param edge_affinity_loss_lambda: float |
|
|
Weight for combining the semantic segmentation loss with the |
|
|
node offset and edge affinity losses. The final loss will be: |
|
|
`L_node_classif + edge_affinity_loss_lambda * L_edge_affinity |
|
|
+ node_offset_loss_lambda * L_node_offset` |
|
|
:param node_offset_criterion: torch.nn._Loss |
|
|
Loss on the nodes of the superpoint level 1 for node offset |
|
|
prediction |
|
|
:param node_offset_loss_lambda: float |
|
|
Weight for combining the semantic segmentation loss with the |
|
|
node offset and edge affinity losses. The final loss will be: |
|
|
`L_node_classif + edge_affinity_loss_lambda * L_edge_affinity |
|
|
+ node_offset_loss_lambda * L_node_offset` |
|
|
:param gc_every_n_steps: int |
|
|
Explicitly call the garbage collector after a certain number |
|
|
of steps. May involve a computation overhead. Mostly hear |
|
|
for debugging purposes when observing suspicious GPU memory |
|
|
increase during training |
|
|
:param track_val_every_n_epoch: int |
|
|
If specified, the output for a validation batch of interest |
|
|
specified with `track_val_idx` will be stored to disk every |
|
|
`track_val_every_n_epoch` epochs. Must be a multiple of |
|
|
`check_val_every_n_epoch`. See `track_batch()` for more |
|
|
:param track_val_idx: int |
|
|
If specified, the output for the `track_val_idx`th |
|
|
validation batch will be saved to disk periodically based on |
|
|
`track_val_every_n_epoch`. Importantly, this index is expected |
|
|
to match the `Dataloader`'s index wrt the current epoch |
|
|
and NOT an index wrt the `Dataset`. Said otherwise, if the |
|
|
`Dataloader(shuffle=True)` then, the stored batch will not be |
|
|
the same at each epoch. For this reason, if tracking the same |
|
|
object across training is needed, the `Dataloader` and the |
|
|
transforms should be free from any stochasticity |
|
|
:param track_test_idx: |
|
|
If specified, the output for the `track_test_idx`th |
|
|
test batch will be saved to disk. If `track_test_idx=-1`, |
|
|
predictions for the entire test set will be saved to disk |
|
|
:param min_instance_size: int |
|
|
Minimum target instance size to consider when computing the |
|
|
metrics. If a target is smaller, it will be ignored, as well |
|
|
as its matched prediction, if any. See `MeanAveragePrecision3D` |
|
|
:param partition_every_n_epoch: int |
|
|
Since we do not need to compute the actual panoptic/instance |
|
|
segmentation to train the model, we can simply do so once in a |
|
|
while to track the training and validation metrics. This |
|
|
parameter rules the frequency at which the panoptic/instance |
|
|
partition and metrics are computed during training |
|
|
:param no_instance_metrics: bool |
|
|
Whether instance segmentation metrics should be computed. These |
|
|
may incur an overhead. Besides, the SuperCluster formulation is |
|
|
mainly targeted for panoptic segmentation, as the model is not |
|
|
specifically trained to maximize instance metrics, which, among |
|
|
other things, involve predicting an instance confidence score |
|
|
:param no_instance_metrics_on_train_set: bool |
|
|
Same as `no_instance_metrics` but specifically for the train |
|
|
set. This is in case we still want the instance metrics every |
|
|
partition_every_n_epoch` on the validation set, but want to |
|
|
avoid the compute overhead of computing the instance partition |
|
|
and metrics at every single training epoch |
|
|
:param kwargs: Dict |
|
|
Kwargs will be passed to `_load_from_checkpoint()` |
|
|
""" |
|
|
|
|
|
_IGNORED_HYPERPARAMETERS = [ |
|
|
'net', |
|
|
'edge_affinity_head', |
|
|
'partitioner', |
|
|
'criterion', |
|
|
'edge_affinity_criterion', |
|
|
'node_offset_criterion'] |
|
|
|
|
|
def __init__( |
|
|
self, |
|
|
net: torch.nn.Module, |
|
|
edge_affinity_head: torch.nn.Module, |
|
|
partitioner: 'InstancePartitioner', |
|
|
criterion: 'torch.nn._Loss', |
|
|
optimizer: torch.optim.Optimizer, |
|
|
scheduler: torch.optim.lr_scheduler.LRScheduler, |
|
|
num_classes: int, |
|
|
stuff_classes: List[int], |
|
|
class_names: List[str] = None, |
|
|
sampling_loss: bool = False, |
|
|
loss_type: str = 'ce_kl', |
|
|
weighted_loss: bool = True, |
|
|
init_linear: str = None, |
|
|
init_rpe: str = None, |
|
|
transformer_lr_scale: float = 1, |
|
|
multi_stage_loss_lambdas: List[float] = None, |
|
|
edge_affinity_criterion: 'torch.nn._Loss' = None, |
|
|
edge_affinity_loss_weights: List[float] = None, |
|
|
edge_affinity_loss_lambda: float = 1, |
|
|
node_offset_criterion: 'torch.nn._Loss' = None, |
|
|
node_offset_loss_lambda: float = 1, |
|
|
gc_every_n_steps: int = 0, |
|
|
track_val_every_n_epoch: int = 1, |
|
|
track_val_idx: int = None, |
|
|
track_test_idx: int = None, |
|
|
min_instance_size: int = 100, |
|
|
partition_every_n_epoch: int = 50, |
|
|
no_instance_metrics: bool = True, |
|
|
no_instance_metrics_on_train_set: bool = True, |
|
|
**kwargs): |
|
|
super().__init__( |
|
|
net, |
|
|
criterion, |
|
|
optimizer, |
|
|
scheduler, |
|
|
num_classes, |
|
|
class_names=class_names, |
|
|
sampling_loss=sampling_loss, |
|
|
loss_type=loss_type, |
|
|
weighted_loss=weighted_loss, |
|
|
init_linear=init_linear, |
|
|
init_rpe=init_rpe, |
|
|
transformer_lr_scale=transformer_lr_scale, |
|
|
multi_stage_loss_lambdas=multi_stage_loss_lambdas, |
|
|
gc_every_n_steps=gc_every_n_steps, |
|
|
track_val_every_n_epoch=track_val_every_n_epoch, |
|
|
track_val_idx=track_val_idx, |
|
|
track_test_idx=track_test_idx, |
|
|
**kwargs) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.partition_every_n_epoch = partition_every_n_epoch |
|
|
self.no_instance_metrics = no_instance_metrics |
|
|
self.no_instance_metrics_on_train_set = no_instance_metrics_on_train_set |
|
|
self.partitioner = partitioner |
|
|
|
|
|
|
|
|
self.stuff_classes = stuff_classes |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.edge_affinity_criterion = BCEWithLogitsLoss() \ |
|
|
if edge_affinity_criterion is None else edge_affinity_criterion |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.edge_affinity_head = edge_affinity_head |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
init = lambda m: init_weights(m, linear=init_linear, rpe=init_rpe) |
|
|
self.edge_affinity_head.apply(init) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.train_panoptic = PanopticQuality3D( |
|
|
self.num_classes, |
|
|
ignore_unseen_classes=True, |
|
|
stuff_classes=self.stuff_classes, |
|
|
compute_on_cpu=True, |
|
|
**kwargs) |
|
|
self.val_panoptic = PanopticQuality3D( |
|
|
self.num_classes, |
|
|
ignore_unseen_classes=True, |
|
|
stuff_classes=self.stuff_classes, |
|
|
compute_on_cpu=True, |
|
|
**kwargs) |
|
|
self.test_panoptic = PanopticQuality3D( |
|
|
self.num_classes, |
|
|
ignore_unseen_classes=True, |
|
|
stuff_classes=self.stuff_classes, |
|
|
compute_on_cpu=True, |
|
|
**kwargs) |
|
|
|
|
|
|
|
|
|
|
|
self.train_semantic = ConfusionMatrix(self.num_classes) |
|
|
self.val_semantic = ConfusionMatrix(self.num_classes) |
|
|
self.test_semantic = ConfusionMatrix(self.num_classes) |
|
|
|
|
|
|
|
|
|
|
|
self.train_instance = MeanAveragePrecision3D( |
|
|
self.num_classes, |
|
|
stuff_classes=self.stuff_classes, |
|
|
min_size=min_instance_size, |
|
|
compute_on_cpu=True, |
|
|
remove_void=True, |
|
|
**kwargs) |
|
|
self.val_instance = MeanAveragePrecision3D( |
|
|
self.num_classes, |
|
|
stuff_classes=self.stuff_classes, |
|
|
min_size=min_instance_size, |
|
|
compute_on_cpu=True, |
|
|
remove_void=True, |
|
|
**kwargs) |
|
|
self.test_instance = MeanAveragePrecision3D( |
|
|
self.num_classes, |
|
|
stuff_classes=self.stuff_classes, |
|
|
min_size=min_instance_size, |
|
|
compute_on_cpu=True, |
|
|
remove_void=True, |
|
|
**kwargs) |
|
|
|
|
|
|
|
|
|
|
|
self.train_multi_partition_storage = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.train_affinity_oa = BinaryAccuracy() |
|
|
self.train_affinity_f1 = BinaryF1Score() |
|
|
self.val_affinity_oa = BinaryAccuracy() |
|
|
self.val_affinity_f1 = BinaryF1Score() |
|
|
self.test_affinity_oa = BinaryAccuracy() |
|
|
self.test_affinity_f1 = BinaryF1Score() |
|
|
|
|
|
|
|
|
self.train_semantic_loss = MeanMetric() |
|
|
self.train_edge_affinity_loss = MeanMetric() |
|
|
|
|
|
self.val_semantic_loss = MeanMetric() |
|
|
self.val_edge_affinity_loss = MeanMetric() |
|
|
|
|
|
self.test_semantic_loss = MeanMetric() |
|
|
self.test_edge_affinity_loss = MeanMetric() |
|
|
|
|
|
|
|
|
|
|
|
self.val_map_best = MaxMetric() |
|
|
self.val_pq_best = MaxMetric() |
|
|
self.val_pqmod_best = MaxMetric() |
|
|
self.val_mprec_best = MaxMetric() |
|
|
self.val_mrec_best = MaxMetric() |
|
|
self.val_instance_miou_best = MaxMetric() |
|
|
self.val_instance_oa_best = MaxMetric() |
|
|
self.val_instance_macc_best = MaxMetric() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.val_affinity_oa_best = MaxMetric() |
|
|
self.val_affinity_f1_best = MaxMetric() |
|
|
|
|
|
@property |
|
|
def needs_partition(self) -> bool: |
|
|
"""Whether the `self.partitioner` should be called to compute |
|
|
the actual panoptic segmentation. During training, the actual |
|
|
partition is not really needed, as we do not learn to partition, |
|
|
but learn to predict inputs for the partition step instead. For |
|
|
this reason, we save compute and time during training by only |
|
|
computing the partition once in a while with |
|
|
`self.partition_every_n_epoch`. |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
epoch = self.current_epoch + 1 |
|
|
|
|
|
|
|
|
if self._trainer is None: |
|
|
return True |
|
|
|
|
|
|
|
|
|
|
|
k = self.partition_every_n_epoch |
|
|
last_epoch = epoch == self.trainer.max_epochs |
|
|
first_epoch = epoch == 1 |
|
|
kth_epoch = epoch % k == 0 if k > 0 else False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.trainer.training: |
|
|
return (kth_epoch and not first_epoch) or last_epoch |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.trainer.validating: |
|
|
k_val = self.trainer.check_val_every_n_epoch |
|
|
nearest_multiple = epoch % k < k_val if k > 0 else False |
|
|
if 0 < k <= k_val: |
|
|
return not first_epoch or last_epoch |
|
|
else: |
|
|
return (nearest_multiple and not first_epoch) or last_epoch |
|
|
|
|
|
|
|
|
return True |
|
|
|
|
|
@property |
|
|
def needs_instance(self) -> bool: |
|
|
"""Returns True if the instance segmentation metrics need to be |
|
|
computed. In particular, since computing instance metrics can be |
|
|
computationally costly, we may want to skip it during training |
|
|
by setting `no_instance_metrics_on_train_set=True`, or all the |
|
|
time by setting `no_instance_metrics=True`. |
|
|
""" |
|
|
if self.no_instance_metrics: |
|
|
return False |
|
|
|
|
|
if self._trainer is None: |
|
|
return self.needs_partition |
|
|
|
|
|
if self.trainer.training and self.no_instance_metrics_on_train_set: |
|
|
return False |
|
|
|
|
|
return self.needs_partition |
|
|
|
|
|
def forward( |
|
|
self, |
|
|
nag: NAG, |
|
|
grid: Any = None |
|
|
) -> PanopticSegmentationOutput: |
|
|
|
|
|
x = self.net(nag) |
|
|
|
|
|
|
|
|
semantic_pred = [head(x_) for head, x_ in zip(self.head, x)] \ |
|
|
if self.multi_stage_loss else self.head(x) |
|
|
|
|
|
|
|
|
x = x[0] if self.multi_stage_loss else x |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
x_edge = x[nag[1].obj_edge_index] |
|
|
x_edge = torch.cat( |
|
|
((x_edge[0] - x_edge[1]).abs(), (x_edge[0] + x_edge[1]) / 2), dim=1) |
|
|
norm_index = torch.zeros( |
|
|
x_edge.shape[0], device=x_edge.device, dtype=torch.long) |
|
|
edge_affinity_logits = self.edge_affinity_head( |
|
|
x_edge, batch=norm_index).squeeze() |
|
|
|
|
|
|
|
|
output = PanopticSegmentationOutput( |
|
|
semantic_pred, |
|
|
self.stuff_classes, |
|
|
edge_affinity_logits, |
|
|
|
|
|
nag.get_sub_size(1)) |
|
|
|
|
|
|
|
|
output = self._forward_partition(nag, output, grid=grid) |
|
|
|
|
|
return output |
|
|
|
|
|
def _forward_partition( |
|
|
self, |
|
|
nag: NAG, |
|
|
output: PanopticSegmentationOutput, |
|
|
grid: Any = None, |
|
|
force: bool = False |
|
|
) -> PanopticSegmentationOutput: |
|
|
"""Compute the panoptic partition based on the predicted node |
|
|
offsets, node semantic logits, and edge affinity logits. |
|
|
|
|
|
The partition will only be computed if required. In general, |
|
|
during training, the actual partition is not needed for the |
|
|
model to be supervised. We only run it once in a while to |
|
|
evaluate the panoptic/instance segmentation metrics or tune |
|
|
the partition hyperparameters on the train set. |
|
|
|
|
|
:param nag: NAG object |
|
|
:param output: PanopticSegmentationOutput |
|
|
:param grid: Dict |
|
|
A dictionary containing settings for grid-searching optimal |
|
|
partition parameters |
|
|
:param force: bool |
|
|
Whether to forcefully compute the partition, regardless of |
|
|
`self.needs_partition`. This mechanism is typically needed |
|
|
during training when we want to store or log predictions for |
|
|
a batch of interest at an epoch when `self.needs_partition` |
|
|
is False |
|
|
|
|
|
:return: output |
|
|
""" |
|
|
if not self.needs_partition and not force: |
|
|
return output |
|
|
|
|
|
|
|
|
|
|
|
batch = nag[1].batch |
|
|
|
|
|
node_x = nag[1].pos |
|
|
node_size = nag.get_sub_size(1) |
|
|
node_logits = output.logits[0] if output.multi_stage else output.logits |
|
|
edge_index = nag[1].obj_edge_index |
|
|
edge_affinity_logits = output.edge_affinity_logits |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
obj_index = self.partitioner( |
|
|
batch, |
|
|
node_x.detach(), |
|
|
node_logits.detach(), |
|
|
self.stuff_classes, |
|
|
node_size, |
|
|
edge_index, |
|
|
edge_affinity_logits.detach(), |
|
|
grid=grid) |
|
|
|
|
|
|
|
|
output.obj_index_pred = obj_index |
|
|
|
|
|
return output |
|
|
|
|
|
def on_fit_start(self) -> None: |
|
|
super().on_fit_start() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
stuff_classes = self.trainer.datamodule.train_dataset.stuff_classes |
|
|
assert sorted(stuff_classes) == sorted(self.stuff_classes), \ |
|
|
f'LightningModule has the following stuff classes ' \ |
|
|
f'{self.stuff_classes} while the LightningDataModule has ' \ |
|
|
f'{stuff_classes}.' |
|
|
|
|
|
def on_train_start(self) -> None: |
|
|
|
|
|
|
|
|
|
|
|
super().on_train_start() |
|
|
self.val_panoptic.reset() |
|
|
self.val_semantic.reset() |
|
|
self.val_instance.reset() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.val_affinity_oa.reset() |
|
|
self.val_affinity_f1.reset() |
|
|
self.val_map_best.reset() |
|
|
self.val_pq_best.reset() |
|
|
self.val_pqmod_best.reset() |
|
|
self.val_mprec_best.reset() |
|
|
self.val_mrec_best.reset() |
|
|
self.val_instance_miou_best.reset() |
|
|
self.val_instance_oa_best.reset() |
|
|
self.val_instance_macc_best.reset() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.val_affinity_oa_best.reset() |
|
|
self.val_affinity_f1_best.reset() |
|
|
self.train_multi_partition_storage = [] |
|
|
|
|
|
def _create_empty_output(self, nag: NAG) -> PanopticSegmentationOutput: |
|
|
"""Local helper method to initialize an empty output for |
|
|
multi-run prediction. |
|
|
""" |
|
|
|
|
|
output_semseg = super()._create_empty_output(nag) |
|
|
|
|
|
|
|
|
num_edges = nag[1].obj_edge_index.shape[1] |
|
|
edge_affinity_logits = torch.zeros(num_edges, device=nag.device) |
|
|
|
|
|
node_size = nag.get_sub_size(1) |
|
|
|
|
|
return PanopticSegmentationOutput( |
|
|
output_semseg.logits, |
|
|
self.stuff_classes, |
|
|
edge_affinity_logits, |
|
|
|
|
|
node_size) |
|
|
|
|
|
@staticmethod |
|
|
def _update_output_multi( |
|
|
output_multi: PanopticSegmentationOutput, |
|
|
nag: NAG, output: PanopticSegmentationOutput, |
|
|
nag_transformed: NAG, |
|
|
key: str |
|
|
) -> PanopticSegmentationOutput: |
|
|
"""Local helper method to accumulate multiple predictions on |
|
|
the same--or part of the same--point cloud. |
|
|
""" |
|
|
raise NotImplementedError( |
|
|
"The current implementation does not properly support multi-run " |
|
|
"for instance/panoptic segmentation") |
|
|
|
|
|
|
|
|
output_multi = super()._update_output_multi( |
|
|
output_multi, nag, output, nag_transformed, key) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
node_id = nag_transformed[1][key] |
|
|
output_multi.node_offset_pred[node_id] = \ |
|
|
(output_multi.node_offset_pred[node_id] |
|
|
+ output.node_offset_pred) / 2 |
|
|
|
|
|
|
|
|
edge_index_1 = nag[1].obj_edge_index |
|
|
edge_index_2 = node_id[nag_transformed[1].obj_edge_index] |
|
|
base = nag[1].num_points + 1 |
|
|
edge_id_1 = edge_index_1[0] * base + edge_index_1[1] |
|
|
edge_id_2 = edge_index_2[0] * base + edge_index_2[1] |
|
|
edge_id_cat = consecutive_cluster(torch.cat((edge_id_1, edge_id_2)))[0] |
|
|
edge_id_1 = edge_id_cat[:edge_id_1.numel()] |
|
|
edge_id_2 = edge_id_cat[edge_id_1.numel():] |
|
|
pivot = torch.zeros(base ** 2, device=output.edge_affinity_logits) |
|
|
pivot[edge_id_1] = output_multi.edge_affinity_logits |
|
|
|
|
|
|
|
|
pivot[edge_id_2] = (pivot[edge_id_2] + output.edge_affinity_logits) / 2 |
|
|
output_multi.edge_affinity_logits = pivot[edge_id_1] |
|
|
|
|
|
return output_multi |
|
|
|
|
|
@staticmethod |
|
|
def _propagate_output_to_unseen_neighbors( |
|
|
output: PanopticSegmentationOutput, |
|
|
nag: NAG, seen: torch.Tensor, |
|
|
neighbors: torch.Tensor |
|
|
) -> PanopticSegmentationOutput: |
|
|
"""Local helper method to propagate predictions to unseen |
|
|
neighbors. |
|
|
""" |
|
|
|
|
|
output = super()._propagate_output_to_unseen_neighbors( |
|
|
output, nag, seen, neighbors) |
|
|
|
|
|
|
|
|
|
|
|
seen_idx = torch.where(seen)[0] |
|
|
unseen_idx = torch.where(~seen)[0] |
|
|
output.node_offset_pred[unseen_idx] = \ |
|
|
output.node_offset_pred[seen_idx][neighbors] |
|
|
|
|
|
|
|
|
|
|
|
seen_edge = nag[1].obj_edge_index[seen] |
|
|
unseen_edge_idx = torch.where(~seen_edge)[0] |
|
|
output.edge_affinity_logits[unseen_edge_idx] = 0.5 |
|
|
|
|
|
return output |
|
|
|
|
|
def get_target( |
|
|
self, |
|
|
nag: NAG, |
|
|
output: PanopticSegmentationOutput |
|
|
) -> PanopticSegmentationOutput: |
|
|
"""Recover the target data for semantic and panoptic |
|
|
segmentation and store it in the `output` object. |
|
|
|
|
|
More specifically: |
|
|
- label histogram(s) for semantic segmentation will be saved |
|
|
in `output.y_hist` |
|
|
- instance graph data `obj_edge_index` and `obj_edge_affinity` |
|
|
will be saved in `output.obj_edge_index` and |
|
|
`output.obj_edge_affinity`, respectively |
|
|
- node positions `pos` and `obj_pos` will be saved in |
|
|
`output.pos` and `output.obj_pos`, respectively. Besides, |
|
|
the `output.obj_offset` will carry the target offset, |
|
|
computed from those |
|
|
""" |
|
|
|
|
|
output = super().get_target(nag, output) |
|
|
|
|
|
|
|
|
output.obj_edge_index = getattr(nag[1], 'obj_edge_index', None) |
|
|
output.obj_edge_affinity = getattr(nag[1], 'obj_edge_affinity', None) |
|
|
output.pos = nag[1].pos |
|
|
output.obj_pos = getattr(nag[1], 'obj_pos', None) |
|
|
output.obj = nag[1].obj |
|
|
|
|
|
return output |
|
|
|
|
|
def _edge_affinity_weights( |
|
|
self, |
|
|
is_same_class: torch.Tensor, |
|
|
is_same_obj: torch.Tensor |
|
|
) -> torch.Tensor: |
|
|
"""Helper function to compute edge weights to be used by the |
|
|
edge affinity loss. Each edge may have a different weight, based |
|
|
on whether its source and target nodes have the same class or |
|
|
belong to the same object. The weight given to each case |
|
|
(same-class and same-object, same-class and different object, |
|
|
etc..) is specified in `edge_affinity_loss_weights`. |
|
|
|
|
|
:param is_same_class: BoolTensor |
|
|
Mask indicating edges between nodes of the same semantic |
|
|
class |
|
|
:param is_same_obj: BoolTensor |
|
|
Mask indicating edges between nodes of the same object |
|
|
""" |
|
|
|
|
|
w = self.hparams.edge_affinity_loss_weights |
|
|
|
|
|
|
|
|
|
|
|
if w is None or not len(w) == 4: |
|
|
return None |
|
|
|
|
|
|
|
|
edge_weight = torch.ones_like(is_same_class).float() |
|
|
edge_weight[is_same_class * is_same_obj] = w[0] |
|
|
edge_weight[is_same_class * ~is_same_obj] = w[1] |
|
|
edge_weight[~is_same_class * is_same_obj] = w[2] |
|
|
edge_weight[~is_same_class * ~is_same_obj] = w[3] |
|
|
return edge_weight |
|
|
|
|
|
def model_step( |
|
|
self, |
|
|
batch: NAG |
|
|
) -> Tuple[torch.Tensor, PanopticSegmentationOutput]: |
|
|
|
|
|
semantic_loss, output = super().model_step(batch) |
|
|
|
|
|
|
|
|
if not output.has_target: |
|
|
return None, output |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
edge_affinity_pred, edge_affinity_target, is_same_class, is_same_obj = \ |
|
|
output.sanitized_edge_affinities() |
|
|
edge_weight = self._edge_affinity_weights(is_same_class, is_same_obj) |
|
|
edge_affinity_loss = self.edge_affinity_criterion( |
|
|
edge_affinity_pred, edge_affinity_target, edge_weight) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
loss = semantic_loss \ |
|
|
+ self.hparams.edge_affinity_loss_lambda * edge_affinity_loss |
|
|
|
|
|
|
|
|
output.semantic_loss = semantic_loss |
|
|
|
|
|
|
|
|
output.edge_affinity_loss = edge_affinity_loss |
|
|
|
|
|
return loss, output |
|
|
|
|
|
def train_step_update_metrics( |
|
|
self, |
|
|
loss: torch.Tensor, |
|
|
output: PanopticSegmentationOutput |
|
|
) -> None: |
|
|
"""Update train metrics with the content of the output object. |
|
|
""" |
|
|
|
|
|
super().train_step_update_metrics(loss, output) |
|
|
|
|
|
|
|
|
if self.needs_partition and not output.has_multi_instance_pred: |
|
|
obj_score, obj_y, instance_data = output.panoptic_pred() |
|
|
obj_score = obj_score.detach().cpu() |
|
|
obj_y = obj_y.detach() |
|
|
obj_hist = instance_data.target_label_histogram(self.num_classes) |
|
|
self.train_panoptic.update(obj_y.cpu(), instance_data.cpu()) |
|
|
self.train_semantic(obj_y, obj_hist) |
|
|
if self.needs_instance: |
|
|
self.train_instance.update(obj_score, obj_y, instance_data.cpu()) |
|
|
elif self.needs_partition: |
|
|
logits = output.logits[0] if output.multi_stage else output.logits |
|
|
storage = PartitionParameterSearchStorage( |
|
|
logits.detach().cpu(), |
|
|
self.stuff_classes, |
|
|
output.node_size.detach().cpu(), |
|
|
output.edge_affinity_logits.detach().cpu(), |
|
|
output.obj.cpu(), |
|
|
[(v[0], v[1].detach().cpu()) for v in output.obj_index_pred]) |
|
|
self.train_multi_partition_storage.append(storage) |
|
|
|
|
|
|
|
|
self.train_semantic_loss(output.semantic_loss.detach()) |
|
|
|
|
|
self.train_edge_affinity_loss(output.edge_affinity_loss.detach()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ea_pred, ea_target, is_same_class, is_same_obj = \ |
|
|
output.sanitized_edge_affinities() |
|
|
ea_pred = ea_pred.detach() |
|
|
ea_target_binary = (ea_target.detach() > 0.5).long() |
|
|
self.train_affinity_oa(ea_pred, ea_target_binary) |
|
|
self.train_affinity_f1(ea_pred, ea_target_binary) |
|
|
|
|
|
def train_step_log_metrics(self) -> None: |
|
|
"""Log train metrics after a single step with the content of the |
|
|
output object. |
|
|
""" |
|
|
super().train_step_log_metrics() |
|
|
self.log( |
|
|
"train/semantic_loss", self.train_semantic_loss, on_step=False, |
|
|
on_epoch=True, prog_bar=True) |
|
|
|
|
|
|
|
|
|
|
|
self.log( |
|
|
"train/edge_affinity_loss", self.train_edge_affinity_loss, on_step=False, |
|
|
on_epoch=True, prog_bar=True) |
|
|
|
|
|
def on_train_epoch_end(self) -> None: |
|
|
|
|
|
super().on_train_epoch_end() |
|
|
|
|
|
|
|
|
if self.trainer.num_devices > 1: |
|
|
log.warning( |
|
|
"Panoptic and instance segmentation metrics are not guaranteed " |
|
|
"to be well-behaved on DDP yet.") |
|
|
|
|
|
if self.needs_partition: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setting = self._compute_best_partition_settings()[0] |
|
|
|
|
|
|
|
|
panoptic_results = self.train_panoptic.compute() |
|
|
if self.needs_instance: |
|
|
instance_results = self.train_instance.compute() |
|
|
|
|
|
|
|
|
pq = panoptic_results.pq |
|
|
sq = panoptic_results.sq |
|
|
rq = panoptic_results.rq |
|
|
pq_thing = panoptic_results.pq_thing |
|
|
pq_stuff = panoptic_results.pq_stuff |
|
|
pqmod = panoptic_results.pq_modified |
|
|
mprec = panoptic_results.mean_precision |
|
|
mrec = panoptic_results.mean_recall |
|
|
pq_per_class = panoptic_results.pq_per_class |
|
|
if self.needs_instance: |
|
|
map = instance_results.map |
|
|
map_50 = instance_results.map_50 |
|
|
map_75 = instance_results.map_75 |
|
|
map_per_class = instance_results.map_per_class |
|
|
|
|
|
|
|
|
self.log("train/pq", 100 * pq, prog_bar=True) |
|
|
self.log("train/sq", 100 * sq, prog_bar=True) |
|
|
self.log("train/rq", 100 * rq, prog_bar=True) |
|
|
self.log("train/pq_thing", 100 * pq_thing, prog_bar=True) |
|
|
self.log("train/pq_stuff", 100 * pq_stuff, prog_bar=True) |
|
|
self.log("train/pqmod", 100 * pqmod, prog_bar=True) |
|
|
self.log("train/mprec", 100 * mprec, prog_bar=True) |
|
|
self.log("train/mrec", 100 * mrec, prog_bar=True) |
|
|
self.log("train/instance_miou", self.train_semantic.miou(), prog_bar=True) |
|
|
self.log("train/instance_oa", self.train_semantic.oa(), prog_bar=True) |
|
|
self.log("train/instance_macc", self.train_semantic.macc(), prog_bar=True) |
|
|
for iou, seen, name in zip(*self.train_semantic.iou(), self.class_names): |
|
|
if seen: |
|
|
self.log(f"train/instance_iou_{name}", iou, prog_bar=True) |
|
|
if self.needs_instance: |
|
|
self.log("train/map", 100 * map, prog_bar=True) |
|
|
self.log("train/map_50", 100 * map_50, prog_bar=True) |
|
|
self.log("train/map_75", 100 * map_75, prog_bar=True) |
|
|
for pq_c, name in zip(pq_per_class, self.class_names): |
|
|
self.log(f"train/pq_{name}", 100 * pq_c, prog_bar=True) |
|
|
if self.needs_instance: |
|
|
for map_c, name in zip(map_per_class, self.class_names): |
|
|
self.log(f"train/map_{name}", 100 * map_c, prog_bar=True) |
|
|
if setting is not None: |
|
|
for k, v in setting.items(): |
|
|
self.log(f"partition_settings/{k}", v, prog_bar=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.log("train/affinity_oa", 100 * self.train_affinity_oa.compute(), prog_bar=True) |
|
|
self.log("train/affinity_f1", 100 * self.train_affinity_f1.compute(), prog_bar=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.train_affinity_oa.reset() |
|
|
self.train_affinity_f1.reset() |
|
|
self.train_panoptic.reset() |
|
|
self.train_semantic.reset() |
|
|
self.train_instance.reset() |
|
|
|
|
|
def _compute_best_partition_settings( |
|
|
self, |
|
|
monitor: str = 'pq', |
|
|
maximize: bool = True |
|
|
) -> Tuple[Dict, float]: |
|
|
"""Compute the best partition settings from |
|
|
`self.train_multi_partition_storage`. This will have the |
|
|
following internal effects: |
|
|
- `self.partitioner` will be updated with the settings which |
|
|
produced the best metrics on the epoch |
|
|
- `self.train_panoptic` will be updated with the batch |
|
|
predictions with the best settings |
|
|
- `self.train_instance` will be updated with the batch |
|
|
predictions with the best settings, if required |
|
|
|
|
|
:param monitor: str |
|
|
The metric based on which we will select the best settings |
|
|
:param maximize: bool |
|
|
Whether the monitored metric should be maximized or |
|
|
minimized |
|
|
:return: |
|
|
""" |
|
|
|
|
|
|
|
|
if len(self.train_multi_partition_storage) == 0: |
|
|
return None, None |
|
|
|
|
|
|
|
|
|
|
|
self.train_panoptic.reset() |
|
|
self.train_instance.reset() |
|
|
|
|
|
|
|
|
|
|
|
if monitor in self.train_panoptic.__slots__: |
|
|
task = 'panoptic' |
|
|
meter = self.train_panoptic |
|
|
elif monitor in self.train_instance.__slots__: |
|
|
task = 'instance' |
|
|
meter = self.train_instance |
|
|
else: |
|
|
raise ValueError(f"Unknown metric, cannot monitor '{monitor}'.") |
|
|
if task == 'instance' and not self.needs_instance: |
|
|
raise ValueError( |
|
|
'Cannot compute the best partition settings on the train set ' |
|
|
'based on instance metrics if `self.needs_instance` is False') |
|
|
|
|
|
|
|
|
|
|
|
settings = self.train_multi_partition_storage[0].settings |
|
|
|
|
|
|
|
|
|
|
|
best_metric = -torch.inf if maximize else torch.inf |
|
|
best_setting = None |
|
|
for s in settings: |
|
|
|
|
|
|
|
|
for storage in self.train_multi_partition_storage: |
|
|
obj_score, obj_y, instance_data = \ |
|
|
storage.panoptic_pred(s) |
|
|
if task == 'panoptic': |
|
|
meter.update(obj_y, instance_data) |
|
|
else: |
|
|
meter.update(obj_score, obj_y, instance_data) |
|
|
|
|
|
|
|
|
metric = getattr(meter.compute(), monitor) |
|
|
|
|
|
|
|
|
condition = (metric > best_metric) if maximize \ |
|
|
else (metric < best_setting) |
|
|
if condition: |
|
|
best_metric = metric |
|
|
best_setting = s |
|
|
|
|
|
|
|
|
|
|
|
meter.reset() |
|
|
|
|
|
|
|
|
for k, v in best_setting.items(): |
|
|
setattr(self.partitioner, k, v) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for storage in self.train_multi_partition_storage: |
|
|
obj_score, obj_y, instance_data = \ |
|
|
storage.panoptic_pred(best_setting) |
|
|
obj_hist = instance_data.target_label_histogram(self.num_classes) |
|
|
self.train_panoptic.update(obj_y, instance_data) |
|
|
self.train_semantic( |
|
|
obj_y.to(self.train_semantic.device), |
|
|
obj_hist.to(self.train_semantic.device)) |
|
|
if self.needs_instance: |
|
|
self.train_instance.update(obj_score, obj_y, instance_data) |
|
|
|
|
|
return best_setting, best_metric |
|
|
|
|
|
def validation_step_update_metrics( |
|
|
self, |
|
|
loss: torch.Tensor, |
|
|
output: PanopticSegmentationOutput |
|
|
) -> None: |
|
|
"""Update validation metrics with the content of the output |
|
|
object. |
|
|
""" |
|
|
|
|
|
super().validation_step_update_metrics(loss, output) |
|
|
|
|
|
|
|
|
if self.needs_partition: |
|
|
obj_score, obj_y, instance_data = output.panoptic_pred() |
|
|
obj_score = obj_score.detach().cpu() |
|
|
obj_y = obj_y.detach() |
|
|
obj_hist = instance_data.target_label_histogram(self.num_classes) |
|
|
self.val_panoptic.update(obj_y.cpu(), instance_data.cpu()) |
|
|
self.val_semantic(obj_y, obj_hist) |
|
|
if self.needs_instance: |
|
|
self.val_instance.update(obj_score, obj_y, instance_data.cpu()) |
|
|
|
|
|
|
|
|
self.val_semantic_loss(output.semantic_loss.detach()) |
|
|
|
|
|
self.val_edge_affinity_loss(output.edge_affinity_loss.detach()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ea_pred, ea_target, is_same_class, is_same_obj = \ |
|
|
output.sanitized_edge_affinities() |
|
|
ea_pred = ea_pred.detach() |
|
|
ea_target_binary = (ea_target.detach() > 0.5).long() |
|
|
self.val_affinity_oa(ea_pred, ea_target_binary) |
|
|
self.val_affinity_f1(ea_pred, ea_target_binary) |
|
|
|
|
|
def validation_step_log_metrics(self) -> None: |
|
|
"""Log validation metrics after a single step with the content |
|
|
of the output object. |
|
|
""" |
|
|
super().validation_step_log_metrics() |
|
|
self.log( |
|
|
"val/semantic_loss", self.val_semantic_loss, on_step=False, |
|
|
on_epoch=True, prog_bar=True) |
|
|
|
|
|
|
|
|
|
|
|
self.log( |
|
|
"val/edge_affinity_loss", self.val_edge_affinity_loss, on_step=False, |
|
|
on_epoch=True, prog_bar=True) |
|
|
|
|
|
def on_validation_epoch_end(self) -> None: |
|
|
|
|
|
super().on_validation_epoch_end() |
|
|
|
|
|
|
|
|
if self.trainer.num_devices > 1: |
|
|
log.warning( |
|
|
"Panoptic and instance segmentation metrics are not guaranteed " |
|
|
"to be well-behaved on DDP yet.") |
|
|
|
|
|
if self.needs_partition: |
|
|
|
|
|
panoptic_results = self.val_panoptic.compute() |
|
|
if self.needs_instance: |
|
|
instance_results = self.val_instance.compute() |
|
|
|
|
|
|
|
|
pq = panoptic_results.pq |
|
|
sq = panoptic_results.sq |
|
|
rq = panoptic_results.rq |
|
|
pq_thing = panoptic_results.pq_thing |
|
|
pq_stuff = panoptic_results.pq_stuff |
|
|
pqmod = panoptic_results.pq_modified |
|
|
mprec = panoptic_results.mean_precision |
|
|
mrec = panoptic_results.mean_recall |
|
|
pq_per_class = panoptic_results.pq_per_class |
|
|
if self.needs_instance: |
|
|
map = instance_results.map |
|
|
map_50 = instance_results.map_50 |
|
|
map_75 = instance_results.map_75 |
|
|
map_per_class = instance_results.map_per_class |
|
|
|
|
|
|
|
|
self.log("val/pq", 100 * pq, prog_bar=True) |
|
|
self.log("val/sq", 100 * sq, prog_bar=True) |
|
|
self.log("val/rq", 100 * rq, prog_bar=True) |
|
|
self.log("val/pq_thing", 100 * pq_thing, prog_bar=True) |
|
|
self.log("val/pq_stuff", 100 * pq_stuff, prog_bar=True) |
|
|
self.log("val/pqmod", 100 * pqmod, prog_bar=True) |
|
|
self.log("val/mprec", 100 * mprec, prog_bar=True) |
|
|
self.log("val/mrec", 100 * mrec, prog_bar=True) |
|
|
instance_miou = self.val_semantic.miou() |
|
|
instance_oa = self.val_semantic.oa() |
|
|
instance_macc = self.val_semantic.macc() |
|
|
self.log("val/instance_miou", instance_miou, prog_bar=True) |
|
|
self.log("val/instance_oa", instance_oa, prog_bar=True) |
|
|
self.log("val/instance_macc", instance_macc, prog_bar=True) |
|
|
for iou, seen, name in zip(*self.val_semantic.iou(), self.class_names): |
|
|
if seen: |
|
|
self.log(f"val/instance_iou_{name}", iou, prog_bar=True) |
|
|
if self.needs_instance: |
|
|
self.log("val/map", 100 * map, prog_bar=True) |
|
|
self.log("val/map_50", 100 * map_50, prog_bar=True) |
|
|
self.log("val/map_75", 100 * map_75, prog_bar=True) |
|
|
for pq_c, name in zip(pq_per_class, self.class_names): |
|
|
self.log(f"val/pq_{name}", 100 * pq_c, prog_bar=True) |
|
|
if self.needs_instance: |
|
|
for map_c, name in zip(map_per_class, self.class_names): |
|
|
self.log(f"val/map_{name}", 100 * map_c, prog_bar=True) |
|
|
|
|
|
|
|
|
self.val_pq_best(pq) |
|
|
self.val_pqmod_best(pqmod) |
|
|
self.val_mprec_best(mprec) |
|
|
self.val_mrec_best(mrec) |
|
|
if self.needs_instance: |
|
|
self.val_map_best(map) |
|
|
self.val_instance_miou_best(instance_miou) |
|
|
self.val_instance_oa_best(instance_oa) |
|
|
self.val_instance_macc_best(instance_macc) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.log("val/pq_best", 100 * self.val_pq_best.compute(), prog_bar=True) |
|
|
self.log("val/pqmod_best", 100 * self.val_pqmod_best.compute(), prog_bar=True) |
|
|
self.log("val/mprec_best", 100 * self.val_mprec_best.compute(), prog_bar=True) |
|
|
self.log("val/mrec_best", 100 * self.val_mrec_best.compute(), prog_bar=True) |
|
|
if self.needs_instance: |
|
|
self.log("val/map_best", 100 * self.val_map_best.compute(), prog_bar=True) |
|
|
self.log("val/instance_miou_best", self.val_instance_miou_best.compute(), prog_bar=True) |
|
|
self.log("val/instance_oa_best", self.val_instance_oa_best.compute(), prog_bar=True) |
|
|
self.log("val/instance_macc_best", self.val_instance_macc_best.compute(), prog_bar=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
affinity_oa = self.val_affinity_oa.compute() |
|
|
affinity_f1 = self.val_affinity_f1.compute() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.log("val/affinity_oa", 100 * affinity_oa, prog_bar=True) |
|
|
self.log("val/affinity_f1", 100 * affinity_f1, prog_bar=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.val_affinity_oa_best(affinity_oa) |
|
|
self.val_affinity_f1_best(affinity_f1) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.log("val/affinity_oa_best", 100 * self.val_affinity_oa_best.compute(), prog_bar=True) |
|
|
self.log("val/affinity_f1_best", 100 * self.val_affinity_f1_best.compute(), prog_bar=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.val_affinity_oa.reset() |
|
|
self.val_affinity_f1.reset() |
|
|
self.val_panoptic.reset() |
|
|
self.val_semantic.reset() |
|
|
self.val_instance.reset() |
|
|
|
|
|
def test_step_update_metrics( |
|
|
self, |
|
|
loss: torch.Tensor, |
|
|
output: PanopticSegmentationOutput |
|
|
) -> None: |
|
|
"""Update test metrics with the content of the output object. |
|
|
""" |
|
|
|
|
|
super().test_step_update_metrics(loss, output) |
|
|
|
|
|
|
|
|
|
|
|
if not self.test_has_target: |
|
|
return |
|
|
|
|
|
|
|
|
if self.needs_partition: |
|
|
obj_score, obj_y, instance_data = output.panoptic_pred() |
|
|
obj_score = obj_score.detach().cpu() |
|
|
obj_y = obj_y.detach() |
|
|
obj_hist = instance_data.target_label_histogram(self.num_classes) |
|
|
self.test_panoptic.update(obj_y.cpu(), instance_data.cpu()) |
|
|
self.test_semantic(obj_y, obj_hist) |
|
|
if self.needs_instance: |
|
|
self.test_instance.update(obj_score, obj_y, instance_data.cpu()) |
|
|
|
|
|
|
|
|
self.test_semantic_loss(output.semantic_loss.detach()) |
|
|
|
|
|
self.test_edge_affinity_loss(output.edge_affinity_loss.detach()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ea_pred, ea_target, is_same_class, is_same_obj = \ |
|
|
output.sanitized_edge_affinities() |
|
|
ea_pred = ea_pred.detach() |
|
|
ea_target_binary = (ea_target.detach() > 0.5).long() |
|
|
self.test_affinity_oa(ea_pred, ea_target_binary) |
|
|
self.test_affinity_f1(ea_pred, ea_target_binary) |
|
|
|
|
|
def test_step_log_metrics(self) -> None: |
|
|
"""Log test metrics after a single step with the content of the |
|
|
output object. |
|
|
""" |
|
|
super().test_step_log_metrics() |
|
|
|
|
|
|
|
|
|
|
|
if not self.test_has_target: |
|
|
return |
|
|
|
|
|
self.log( |
|
|
"test/semantic_loss", self.test_semantic_loss, on_step=False, |
|
|
on_epoch=True, prog_bar=True) |
|
|
|
|
|
|
|
|
|
|
|
self.log( |
|
|
"test/edge_affinity_loss", self.test_edge_affinity_loss, on_step=False, |
|
|
on_epoch=True, prog_bar=True) |
|
|
|
|
|
def on_test_epoch_end(self) -> None: |
|
|
|
|
|
super().on_test_epoch_end() |
|
|
|
|
|
|
|
|
if not self.test_has_target: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.test_affinity_oa.reset() |
|
|
self.test_affinity_f1.reset() |
|
|
self.test_panoptic.reset() |
|
|
self.test_semantic.reset() |
|
|
self.test_instance.reset() |
|
|
return |
|
|
|
|
|
|
|
|
if self.trainer.num_devices > 1: |
|
|
log.warning( |
|
|
"Panoptic and instance segmentation metrics are not guaranteed " |
|
|
"to be well-behaved on DDP yet.") |
|
|
|
|
|
if self.needs_partition: |
|
|
|
|
|
panoptic_results = self.test_panoptic.compute() |
|
|
if self.needs_instance: |
|
|
instance_results = self.test_instance.compute() |
|
|
|
|
|
|
|
|
pq = panoptic_results.pq |
|
|
sq = panoptic_results.sq |
|
|
rq = panoptic_results.rq |
|
|
pq_thing = panoptic_results.pq_thing |
|
|
pq_stuff = panoptic_results.pq_stuff |
|
|
pqmod = panoptic_results.pq_modified |
|
|
mprec = panoptic_results.mean_precision |
|
|
mrec = panoptic_results.mean_recall |
|
|
pq_per_class = panoptic_results.pq_per_class |
|
|
if self.needs_instance: |
|
|
map = instance_results.map |
|
|
map_50 = instance_results.map_50 |
|
|
map_75 = instance_results.map_75 |
|
|
map_per_class = instance_results.map_per_class |
|
|
|
|
|
|
|
|
self.log("test/pq", 100 * pq, prog_bar=True) |
|
|
self.log("test/sq", 100 * sq, prog_bar=True) |
|
|
self.log("test/rq", 100 * rq, prog_bar=True) |
|
|
self.log("test/pq_thing", 100 * pq_thing, prog_bar=True) |
|
|
self.log("test/pq_stuff", 100 * pq_stuff, prog_bar=True) |
|
|
self.log("test/pqmod", 100 * pqmod, prog_bar=True) |
|
|
self.log("test/mprec", 100 * mprec, prog_bar=True) |
|
|
self.log("test/mrec", 100 * mrec, prog_bar=True) |
|
|
self.log("test/instance_miou", self.test_semantic.miou(), prog_bar=True) |
|
|
self.log("test/instance_oa", self.test_semantic.oa(), prog_bar=True) |
|
|
self.log("test/instance_macc", self.test_semantic.macc(), prog_bar=True) |
|
|
for iou, seen, name in zip(*self.test_semantic.iou(), self.class_names): |
|
|
if seen: |
|
|
self.log(f"test/instance_iou_{name}", iou, prog_bar=True) |
|
|
if self.needs_instance: |
|
|
self.log("test/map", 100 * map, prog_bar=True) |
|
|
self.log("test/map_50", 100 * map_50, prog_bar=True) |
|
|
self.log("test/map_75", 100 * map_75, prog_bar=True) |
|
|
for pq_c, name in zip(pq_per_class, self.class_names): |
|
|
self.log(f"test/pq_{name}", 100 * pq_c, prog_bar=True) |
|
|
if self.needs_instance: |
|
|
for map_c, name in zip(map_per_class, self.class_names): |
|
|
self.log(f"test/map_{name}", 100 * map_c, prog_bar=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.log("test/affinity_oa", 100 * self.test_affinity_oa.compute(), prog_bar=True) |
|
|
self.log("test/affinity_f1", 100 * self.test_affinity_f1.compute(), prog_bar=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.test_affinity_oa.reset() |
|
|
self.test_affinity_f1.reset() |
|
|
self.test_panoptic.reset() |
|
|
self.test_semantic.reset() |
|
|
self.test_instance.reset() |
|
|
|
|
|
def track_batch( |
|
|
self, |
|
|
batch: NAG, |
|
|
batch_idx: int, |
|
|
output: PanopticSegmentationOutput, |
|
|
folder: str = None |
|
|
) -> None: |
|
|
"""Store a batch prediction to disk. The corresponding `NAG` |
|
|
object will be populated with panoptic segmentation predictions |
|
|
for: |
|
|
- levels 1+ if `multi_stage` output (i.e. loss supervision on |
|
|
levels 1 and above) |
|
|
- only level 1 otherwise |
|
|
|
|
|
Besides, we also pre-compute the level-0 predictions as this is |
|
|
frequently required for downstream tasks. However, we choose not |
|
|
to compute the full-resolution predictions for the sake of disk |
|
|
memory. |
|
|
|
|
|
If a `folder` is provided, the NAG will be saved there under: |
|
|
<folder>/predictions/<stage>/<epoch>/batch_<batch_idx>.h5 |
|
|
If not, the folder will be the logger's directory, if any. |
|
|
If not, the current working directory will be used. |
|
|
|
|
|
:param batch: NAG |
|
|
Object that will be stored to disk. Before that, the |
|
|
model predictions will be added to the attributes of each |
|
|
level, to facilitate downstream use of the stored `NAG` |
|
|
:param batch_idx: int |
|
|
Index of the batch to be stored |
|
|
:param output: PanopticSegmentationOutput |
|
|
Output of `self.model_step()` |
|
|
:param folder: str |
|
|
Path where to save the tracked batch. If not provided, the |
|
|
logger's saving directory will be used as fallback. If not |
|
|
logger is found, the current working directory will be used |
|
|
:return: |
|
|
""" |
|
|
|
|
|
if not isinstance(batch, NAG): |
|
|
raise NotImplementedError( |
|
|
f"Expected as NAG, but received a {type(batch)}. Are you " |
|
|
f"perhaps running multi-run inference ? If so, this is not " |
|
|
f"compatible with batch_saving, please deactivate either one.") |
|
|
|
|
|
|
|
|
if output.obj_index_pred is None: |
|
|
output = self._forward_partition(batch, output, force=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sp_y_pred, sp_obj_index_pred, sp_obj_pred = ( |
|
|
output.superpoint_panoptic_pred()) |
|
|
vox_y_pred, vox_obj_index_pred, vox_obj_pred = ( |
|
|
output.voxel_panoptic_pred(super_index=batch[0].super_index)) |
|
|
batch[1].obj_y_pred = sp_y_pred |
|
|
batch[1].obj_index_pred = sp_obj_index_pred |
|
|
batch[1].obj_pred = sp_obj_pred |
|
|
batch[0].obj_y_pred = vox_y_pred |
|
|
batch[0].obj_index_pred = vox_obj_index_pred |
|
|
batch[0].obj_pred = vox_obj_pred |
|
|
batch[1].edge_affinity_logits = output.edge_affinity_logits |
|
|
|
|
|
|
|
|
super().track_batch(batch, batch_idx, output, folder=folder) |
|
|
|
|
|
def load_state_dict(self, state_dict: Dict, strict: bool = True) -> None: |
|
|
"""Basic `load_state_dict` from `torch.nn.Module` with a bit of |
|
|
acrobatics due to `criterion.weight`. |
|
|
|
|
|
This attribute, when present in the `state_dict`, causes |
|
|
`load_state_dict` to crash. More precisely, `criterion.weight` |
|
|
is holding the per-class weights for classification losses. |
|
|
""" |
|
|
|
|
|
if self.edge_affinity_criterion.pos_weight is not None: |
|
|
pos_weight_bckp = self.edge_affinity_criterion.pos_weight |
|
|
self.edge_affinity_criterion.pos_weight = None |
|
|
|
|
|
if 'edge_affinity_criterion.pos_weight' in state_dict.keys(): |
|
|
pos_weight = state_dict.pop('edge_affinity_criterion.pos_weight') |
|
|
else: |
|
|
pos_weight = None |
|
|
|
|
|
|
|
|
super().load_state_dict(state_dict, strict=strict) |
|
|
|
|
|
|
|
|
if self.edge_affinity_criterion.pos_weight is not None: |
|
|
self.edge_affinity_criterion.pos_weight = pos_weight \ |
|
|
if pos_weight is not None else pos_weight_bckp |
|
|
|
|
|
def _load_from_checkpoint( |
|
|
self, |
|
|
checkpoint_path: str, |
|
|
**kwargs |
|
|
) -> 'PanopticSegmentationModule': |
|
|
"""Simpler version of `LightningModule.load_from_checkpoint()` |
|
|
for easier use: no need to explicitly pass `model.net`, |
|
|
`model.criterion`, etc. |
|
|
""" |
|
|
return self.__class__.load_from_checkpoint( |
|
|
checkpoint_path, |
|
|
net=self.net, |
|
|
edge_affinity_head=self.edge_affinity_head, |
|
|
partitioner=self.partitioner, |
|
|
criterion=self.criterion, |
|
|
**kwargs) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
import hydra |
|
|
import omegaconf |
|
|
import pyrootutils |
|
|
|
|
|
root = str(pyrootutils.setup_root(__file__, pythonpath=True)) |
|
|
cfg = omegaconf.OmegaConf.load(root + "/configs/model/panoptic/spt-2.yaml") |
|
|
_ = hydra.utils.instantiate(cfg) |
|
|
|