|
|
|
|
|
|
|
|
|
|
|
import contextlib
|
|
import copy
|
|
import io
|
|
import itertools
|
|
import logging
|
|
import numpy as np
|
|
import os
|
|
from collections import OrderedDict
|
|
from typing import Dict, Iterable, List, Optional
|
|
import pycocotools.mask as mask_utils
|
|
import torch
|
|
from pycocotools.coco import COCO
|
|
from tabulate import tabulate
|
|
|
|
from detectron2.config import CfgNode
|
|
from detectron2.data import MetadataCatalog
|
|
from detectron2.evaluation import DatasetEvaluator
|
|
from detectron2.structures import BoxMode
|
|
from detectron2.utils.comm import gather, get_rank, is_main_process, synchronize
|
|
from detectron2.utils.file_io import PathManager
|
|
from detectron2.utils.logger import create_small_table
|
|
|
|
from densepose.converters import ToChartResultConverter, ToMaskConverter
|
|
from densepose.data.datasets.coco import maybe_filter_and_map_categories_cocoapi
|
|
from densepose.structures import (
|
|
DensePoseChartPredictorOutput,
|
|
DensePoseEmbeddingPredictorOutput,
|
|
quantize_densepose_chart_result,
|
|
)
|
|
|
|
from .densepose_coco_evaluation import DensePoseCocoEval, DensePoseEvalMode
|
|
from .mesh_alignment_evaluator import MeshAlignmentEvaluator
|
|
from .tensor_storage import (
|
|
SingleProcessFileTensorStorage,
|
|
SingleProcessRamTensorStorage,
|
|
SingleProcessTensorStorage,
|
|
SizeData,
|
|
storage_gather,
|
|
)
|
|
|
|
|
|
class DensePoseCOCOEvaluator(DatasetEvaluator):
|
|
def __init__(
|
|
self,
|
|
dataset_name,
|
|
distributed,
|
|
output_dir=None,
|
|
evaluator_type: str = "iuv",
|
|
min_iou_threshold: float = 0.5,
|
|
storage: Optional[SingleProcessTensorStorage] = None,
|
|
embedder=None,
|
|
should_evaluate_mesh_alignment: bool = False,
|
|
mesh_alignment_mesh_names: Optional[List[str]] = None,
|
|
):
|
|
self._embedder = embedder
|
|
self._distributed = distributed
|
|
self._output_dir = output_dir
|
|
self._evaluator_type = evaluator_type
|
|
self._storage = storage
|
|
self._should_evaluate_mesh_alignment = should_evaluate_mesh_alignment
|
|
|
|
assert not (
|
|
should_evaluate_mesh_alignment and embedder is None
|
|
), "Mesh alignment evaluation is activated, but no vertex embedder provided!"
|
|
if should_evaluate_mesh_alignment:
|
|
self._mesh_alignment_evaluator = MeshAlignmentEvaluator(
|
|
embedder,
|
|
mesh_alignment_mesh_names,
|
|
)
|
|
|
|
self._cpu_device = torch.device("cpu")
|
|
self._logger = logging.getLogger(__name__)
|
|
|
|
self._metadata = MetadataCatalog.get(dataset_name)
|
|
self._min_threshold = min_iou_threshold
|
|
json_file = PathManager.get_local_path(self._metadata.json_file)
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
self._coco_api = COCO(json_file)
|
|
maybe_filter_and_map_categories_cocoapi(dataset_name, self._coco_api)
|
|
|
|
def reset(self):
|
|
self._predictions = []
|
|
|
|
def process(self, inputs, outputs):
|
|
"""
|
|
Args:
|
|
inputs: the inputs to a COCO model (e.g., GeneralizedRCNN).
|
|
It is a list of dict. Each dict corresponds to an image and
|
|
contains keys like "height", "width", "file_name", "image_id".
|
|
outputs: the outputs of a COCO model. It is a list of dicts with key
|
|
"instances" that contains :class:`Instances`.
|
|
The :class:`Instances` object needs to have `densepose` field.
|
|
"""
|
|
for input, output in zip(inputs, outputs):
|
|
instances = output["instances"].to(self._cpu_device)
|
|
if not instances.has("pred_densepose"):
|
|
continue
|
|
prediction_list = prediction_to_dict(
|
|
instances,
|
|
input["image_id"],
|
|
self._embedder,
|
|
self._metadata.class_to_mesh_name,
|
|
self._storage is not None,
|
|
)
|
|
if self._storage is not None:
|
|
for prediction_dict in prediction_list:
|
|
dict_to_store = {}
|
|
for field_name in self._storage.data_schema:
|
|
dict_to_store[field_name] = prediction_dict[field_name]
|
|
record_id = self._storage.put(dict_to_store)
|
|
prediction_dict["record_id"] = record_id
|
|
prediction_dict["rank"] = get_rank()
|
|
for field_name in self._storage.data_schema:
|
|
del prediction_dict[field_name]
|
|
self._predictions.extend(prediction_list)
|
|
|
|
def evaluate(self, img_ids=None):
|
|
if self._distributed:
|
|
synchronize()
|
|
predictions = gather(self._predictions)
|
|
predictions = list(itertools.chain(*predictions))
|
|
else:
|
|
predictions = self._predictions
|
|
|
|
multi_storage = storage_gather(self._storage) if self._storage is not None else None
|
|
|
|
if not is_main_process():
|
|
return
|
|
return copy.deepcopy(self._eval_predictions(predictions, multi_storage, img_ids))
|
|
|
|
def _eval_predictions(self, predictions, multi_storage=None, img_ids=None):
|
|
"""
|
|
Evaluate predictions on densepose.
|
|
Return results with the metrics of the tasks.
|
|
"""
|
|
self._logger.info("Preparing results for COCO format ...")
|
|
|
|
if self._output_dir:
|
|
PathManager.mkdirs(self._output_dir)
|
|
file_path = os.path.join(self._output_dir, "coco_densepose_predictions.pth")
|
|
with PathManager.open(file_path, "wb") as f:
|
|
torch.save(predictions, f)
|
|
|
|
self._logger.info("Evaluating predictions ...")
|
|
res = OrderedDict()
|
|
results_gps, results_gpsm, results_segm = _evaluate_predictions_on_coco(
|
|
self._coco_api,
|
|
predictions,
|
|
multi_storage,
|
|
self._embedder,
|
|
class_names=self._metadata.get("thing_classes"),
|
|
min_threshold=self._min_threshold,
|
|
img_ids=img_ids,
|
|
)
|
|
res["densepose_gps"] = results_gps
|
|
res["densepose_gpsm"] = results_gpsm
|
|
res["densepose_segm"] = results_segm
|
|
if self._should_evaluate_mesh_alignment:
|
|
res["densepose_mesh_alignment"] = self._evaluate_mesh_alignment()
|
|
return res
|
|
|
|
def _evaluate_mesh_alignment(self):
|
|
self._logger.info("Mesh alignment evaluation ...")
|
|
mean_ge, mean_gps, per_mesh_metrics = self._mesh_alignment_evaluator.evaluate()
|
|
results = {
|
|
"GE": mean_ge * 100,
|
|
"GPS": mean_gps * 100,
|
|
}
|
|
mesh_names = set()
|
|
for metric_name in per_mesh_metrics:
|
|
for mesh_name, value in per_mesh_metrics[metric_name].items():
|
|
results[f"{metric_name}-{mesh_name}"] = value * 100
|
|
mesh_names.add(mesh_name)
|
|
self._print_mesh_alignment_results(results, mesh_names)
|
|
return results
|
|
|
|
def _print_mesh_alignment_results(self, results: Dict[str, float], mesh_names: Iterable[str]):
|
|
self._logger.info("Evaluation results for densepose, mesh alignment:")
|
|
self._logger.info(f'| {"Mesh":13s} | {"GErr":7s} | {"GPS":7s} |')
|
|
self._logger.info("| :-----------: | :-----: | :-----: |")
|
|
for mesh_name in mesh_names:
|
|
ge_key = f"GE-{mesh_name}"
|
|
ge_str = f"{results[ge_key]:.4f}" if ge_key in results else " "
|
|
gps_key = f"GPS-{mesh_name}"
|
|
gps_str = f"{results[gps_key]:.4f}" if gps_key in results else " "
|
|
self._logger.info(f"| {mesh_name:13s} | {ge_str:7s} | {gps_str:7s} |")
|
|
self._logger.info("| :-------------------------------: |")
|
|
ge_key = "GE"
|
|
ge_str = f"{results[ge_key]:.4f}" if ge_key in results else " "
|
|
gps_key = "GPS"
|
|
gps_str = f"{results[gps_key]:.4f}" if gps_key in results else " "
|
|
self._logger.info(f'| {"MEAN":13s} | {ge_str:7s} | {gps_str:7s} |')
|
|
|
|
|
|
def prediction_to_dict(instances, img_id, embedder, class_to_mesh_name, use_storage):
|
|
"""
|
|
Args:
|
|
instances (Instances): the output of the model
|
|
img_id (str): the image id in COCO
|
|
|
|
Returns:
|
|
list[dict]: the results in densepose evaluation format
|
|
"""
|
|
scores = instances.scores.tolist()
|
|
classes = instances.pred_classes.tolist()
|
|
raw_boxes_xywh = BoxMode.convert(
|
|
instances.pred_boxes.tensor.clone(), BoxMode.XYXY_ABS, BoxMode.XYWH_ABS
|
|
)
|
|
|
|
if isinstance(instances.pred_densepose, DensePoseEmbeddingPredictorOutput):
|
|
results_densepose = densepose_cse_predictions_to_dict(
|
|
instances, embedder, class_to_mesh_name, use_storage
|
|
)
|
|
elif isinstance(instances.pred_densepose, DensePoseChartPredictorOutput):
|
|
if not use_storage:
|
|
results_densepose = densepose_chart_predictions_to_dict(instances)
|
|
else:
|
|
results_densepose = densepose_chart_predictions_to_storage_dict(instances)
|
|
|
|
results = []
|
|
for k in range(len(instances)):
|
|
result = {
|
|
"image_id": img_id,
|
|
"category_id": classes[k],
|
|
"bbox": raw_boxes_xywh[k].tolist(),
|
|
"score": scores[k],
|
|
}
|
|
results.append({**result, **results_densepose[k]})
|
|
return results
|
|
|
|
|
|
def densepose_chart_predictions_to_dict(instances):
|
|
segmentations = ToMaskConverter.convert(
|
|
instances.pred_densepose, instances.pred_boxes, instances.image_size
|
|
)
|
|
|
|
results = []
|
|
for k in range(len(instances)):
|
|
densepose_results_quantized = quantize_densepose_chart_result(
|
|
ToChartResultConverter.convert(instances.pred_densepose[k], instances.pred_boxes[k])
|
|
)
|
|
densepose_results_quantized.labels_uv_uint8 = (
|
|
densepose_results_quantized.labels_uv_uint8.cpu()
|
|
)
|
|
segmentation = segmentations.tensor[k]
|
|
segmentation_encoded = mask_utils.encode(
|
|
np.require(segmentation.numpy(), dtype=np.uint8, requirements=["F"])
|
|
)
|
|
segmentation_encoded["counts"] = segmentation_encoded["counts"].decode("utf-8")
|
|
result = {
|
|
"densepose": densepose_results_quantized,
|
|
"segmentation": segmentation_encoded,
|
|
}
|
|
results.append(result)
|
|
return results
|
|
|
|
|
|
def densepose_chart_predictions_to_storage_dict(instances):
|
|
results = []
|
|
for k in range(len(instances)):
|
|
densepose_predictor_output = instances.pred_densepose[k]
|
|
result = {
|
|
"coarse_segm": densepose_predictor_output.coarse_segm.squeeze(0).cpu(),
|
|
"fine_segm": densepose_predictor_output.fine_segm.squeeze(0).cpu(),
|
|
"u": densepose_predictor_output.u.squeeze(0).cpu(),
|
|
"v": densepose_predictor_output.v.squeeze(0).cpu(),
|
|
}
|
|
results.append(result)
|
|
return results
|
|
|
|
|
|
def densepose_cse_predictions_to_dict(instances, embedder, class_to_mesh_name, use_storage):
|
|
results = []
|
|
for k in range(len(instances)):
|
|
cse = instances.pred_densepose[k]
|
|
results.append(
|
|
{
|
|
"coarse_segm": cse.coarse_segm[0].cpu(),
|
|
"embedding": cse.embedding[0].cpu(),
|
|
}
|
|
)
|
|
return results
|
|
|
|
|
|
def _evaluate_predictions_on_coco(
|
|
coco_gt,
|
|
coco_results,
|
|
multi_storage=None,
|
|
embedder=None,
|
|
class_names=None,
|
|
min_threshold: float = 0.5,
|
|
img_ids=None,
|
|
):
|
|
logger = logging.getLogger(__name__)
|
|
|
|
densepose_metrics = _get_densepose_metrics(min_threshold)
|
|
if len(coco_results) == 0:
|
|
logger.warn("No predictions from the model! Set scores to -1")
|
|
results_gps = {metric: -1 for metric in densepose_metrics}
|
|
results_gpsm = {metric: -1 for metric in densepose_metrics}
|
|
results_segm = {metric: -1 for metric in densepose_metrics}
|
|
return results_gps, results_gpsm, results_segm
|
|
|
|
coco_dt = coco_gt.loadRes(coco_results)
|
|
|
|
results = []
|
|
for eval_mode_name in ["GPS", "GPSM", "IOU"]:
|
|
eval_mode = getattr(DensePoseEvalMode, eval_mode_name)
|
|
coco_eval = DensePoseCocoEval(
|
|
coco_gt, coco_dt, "densepose", multi_storage, embedder, dpEvalMode=eval_mode
|
|
)
|
|
result = _derive_results_from_coco_eval(
|
|
coco_eval, eval_mode_name, densepose_metrics, class_names, min_threshold, img_ids
|
|
)
|
|
results.append(result)
|
|
return results
|
|
|
|
|
|
def _get_densepose_metrics(min_threshold: float = 0.5):
|
|
metrics = ["AP"]
|
|
if min_threshold <= 0.201:
|
|
metrics += ["AP20"]
|
|
if min_threshold <= 0.301:
|
|
metrics += ["AP30"]
|
|
if min_threshold <= 0.401:
|
|
metrics += ["AP40"]
|
|
metrics.extend(["AP50", "AP75", "APm", "APl", "AR", "AR50", "AR75", "ARm", "ARl"])
|
|
return metrics
|
|
|
|
|
|
def _derive_results_from_coco_eval(
|
|
coco_eval, eval_mode_name, metrics, class_names, min_threshold: float, img_ids
|
|
):
|
|
if img_ids is not None:
|
|
coco_eval.params.imgIds = img_ids
|
|
coco_eval.params.iouThrs = np.linspace(
|
|
min_threshold, 0.95, int(np.round((0.95 - min_threshold) / 0.05)) + 1, endpoint=True
|
|
)
|
|
coco_eval.evaluate()
|
|
coco_eval.accumulate()
|
|
coco_eval.summarize()
|
|
results = {metric: float(coco_eval.stats[idx] * 100) for idx, metric in enumerate(metrics)}
|
|
logger = logging.getLogger(__name__)
|
|
logger.info(
|
|
f"Evaluation results for densepose, {eval_mode_name} metric: \n"
|
|
+ create_small_table(results)
|
|
)
|
|
if class_names is None or len(class_names) <= 1:
|
|
return results
|
|
|
|
|
|
|
|
precisions = coco_eval.eval["precision"]
|
|
|
|
assert len(class_names) == precisions.shape[2]
|
|
|
|
results_per_category = []
|
|
for idx, name in enumerate(class_names):
|
|
|
|
|
|
precision = precisions[:, :, idx, 0, -1]
|
|
precision = precision[precision > -1]
|
|
ap = np.mean(precision) if precision.size else float("nan")
|
|
results_per_category.append((f"{name}", float(ap * 100)))
|
|
|
|
|
|
n_cols = min(6, len(results_per_category) * 2)
|
|
results_flatten = list(itertools.chain(*results_per_category))
|
|
results_2d = itertools.zip_longest(*[results_flatten[i::n_cols] for i in range(n_cols)])
|
|
table = tabulate(
|
|
results_2d,
|
|
tablefmt="pipe",
|
|
floatfmt=".3f",
|
|
headers=["category", "AP"] * (n_cols // 2),
|
|
numalign="left",
|
|
)
|
|
logger.info(f"Per-category {eval_mode_name} AP: \n" + table)
|
|
|
|
results.update({"AP-" + name: ap for name, ap in results_per_category})
|
|
return results
|
|
|
|
|
|
def build_densepose_evaluator_storage(cfg: CfgNode, output_folder: str):
|
|
storage_spec = cfg.DENSEPOSE_EVALUATION.STORAGE
|
|
if storage_spec == "none":
|
|
return None
|
|
evaluator_type = cfg.DENSEPOSE_EVALUATION.TYPE
|
|
|
|
hout = cfg.MODEL.ROI_DENSEPOSE_HEAD.HEATMAP_SIZE
|
|
wout = cfg.MODEL.ROI_DENSEPOSE_HEAD.HEATMAP_SIZE
|
|
n_csc = cfg.MODEL.ROI_DENSEPOSE_HEAD.NUM_COARSE_SEGM_CHANNELS
|
|
|
|
if evaluator_type == "iuv":
|
|
n_fsc = cfg.MODEL.ROI_DENSEPOSE_HEAD.NUM_PATCHES + 1
|
|
schema = {
|
|
"coarse_segm": SizeData(dtype="float32", shape=(n_csc, hout, wout)),
|
|
"fine_segm": SizeData(dtype="float32", shape=(n_fsc, hout, wout)),
|
|
"u": SizeData(dtype="float32", shape=(n_fsc, hout, wout)),
|
|
"v": SizeData(dtype="float32", shape=(n_fsc, hout, wout)),
|
|
}
|
|
elif evaluator_type == "cse":
|
|
embed_size = cfg.MODEL.ROI_DENSEPOSE_HEAD.CSE.EMBED_SIZE
|
|
schema = {
|
|
"coarse_segm": SizeData(dtype="float32", shape=(n_csc, hout, wout)),
|
|
"embedding": SizeData(dtype="float32", shape=(embed_size, hout, wout)),
|
|
}
|
|
else:
|
|
raise ValueError(f"Unknown evaluator type: {evaluator_type}")
|
|
|
|
if storage_spec == "ram":
|
|
storage = SingleProcessRamTensorStorage(schema, io.BytesIO())
|
|
elif storage_spec == "file":
|
|
fpath = os.path.join(output_folder, f"DensePoseEvaluatorStorage.{get_rank()}.bin")
|
|
PathManager.mkdirs(output_folder)
|
|
storage = SingleProcessFileTensorStorage(schema, fpath, "wb")
|
|
else:
|
|
raise ValueError(f"Unknown storage specification: {storage_spec}")
|
|
return storage
|
|
|