|
|
|
|
|
|
|
|
|
|
|
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 |
|
|