Spaces:
Running
on
Zero
Running
on
Zero
Upload 114 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- densepose/__init__.py +20 -0
- densepose/config.py +277 -0
- densepose/converters/__init__.py +15 -0
- densepose/converters/base.py +93 -0
- densepose/converters/builtin.py +31 -0
- densepose/converters/chart_output_hflip.py +71 -0
- densepose/converters/chart_output_to_chart_result.py +188 -0
- densepose/converters/hflip.py +34 -0
- densepose/converters/segm_to_mask.py +150 -0
- densepose/converters/to_chart_result.py +70 -0
- densepose/converters/to_mask.py +49 -0
- densepose/data/__init__.py +25 -0
- densepose/data/build.py +736 -0
- densepose/data/combined_loader.py +44 -0
- densepose/data/dataset_mapper.py +168 -0
- densepose/data/datasets/__init__.py +5 -0
- densepose/data/datasets/builtin.py +16 -0
- densepose/data/datasets/chimpnsee.py +29 -0
- densepose/data/datasets/coco.py +432 -0
- densepose/data/datasets/dataset_type.py +11 -0
- densepose/data/datasets/lvis.py +257 -0
- densepose/data/image_list_dataset.py +72 -0
- densepose/data/inference_based_loader.py +172 -0
- densepose/data/meshes/__init__.py +5 -0
- densepose/data/meshes/builtin.py +101 -0
- densepose/data/meshes/catalog.py +71 -0
- densepose/data/samplers/__init__.py +8 -0
- densepose/data/samplers/densepose_base.py +203 -0
- densepose/data/samplers/densepose_confidence_based.py +108 -0
- densepose/data/samplers/densepose_cse_base.py +139 -0
- densepose/data/samplers/densepose_cse_confidence_based.py +119 -0
- densepose/data/samplers/densepose_cse_uniform.py +12 -0
- densepose/data/samplers/densepose_uniform.py +41 -0
- densepose/data/samplers/mask_from_densepose.py +28 -0
- densepose/data/samplers/prediction_to_gt.py +98 -0
- densepose/data/transform/__init__.py +3 -0
- densepose/data/transform/image.py +39 -0
- densepose/data/utils.py +38 -0
- densepose/data/video/__init__.py +17 -0
- densepose/data/video/frame_selector.py +87 -0
- densepose/data/video/video_keyframe_dataset.py +300 -0
- densepose/engine/__init__.py +3 -0
- densepose/engine/trainer.py +258 -0
- densepose/evaluation/__init__.py +3 -0
- densepose/evaluation/d2_evaluator_adapter.py +50 -0
- densepose/evaluation/densepose_coco_evaluation.py +1303 -0
- densepose/evaluation/evaluator.py +421 -0
- densepose/evaluation/mesh_alignment_evaluator.py +66 -0
- densepose/evaluation/tensor_storage.py +239 -0
- densepose/modeling/__init__.py +13 -0
densepose/__init__.py
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
from .data.datasets import builtin # just to register data
|
3 |
+
from .converters import builtin as builtin_converters # register converters
|
4 |
+
from .config import (
|
5 |
+
add_densepose_config,
|
6 |
+
add_densepose_head_config,
|
7 |
+
add_hrnet_config,
|
8 |
+
add_dataset_category_config,
|
9 |
+
add_bootstrap_config,
|
10 |
+
load_bootstrap_config,
|
11 |
+
)
|
12 |
+
from .structures import DensePoseDataRelative, DensePoseList, DensePoseTransformData
|
13 |
+
from .evaluation import DensePoseCOCOEvaluator
|
14 |
+
from .modeling.roi_heads import DensePoseROIHeads
|
15 |
+
from .modeling.test_time_augmentation import (
|
16 |
+
DensePoseGeneralizedRCNNWithTTA,
|
17 |
+
DensePoseDatasetMapperTTA,
|
18 |
+
)
|
19 |
+
from .utils.transform import load_from_cfg
|
20 |
+
from .modeling.hrfpn import build_hrfpn_backbone
|
densepose/config.py
ADDED
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding = utf-8 -*-
|
2 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
3 |
+
# pyre-ignore-all-errors
|
4 |
+
|
5 |
+
from detectron2.config import CfgNode as CN
|
6 |
+
|
7 |
+
|
8 |
+
def add_dataset_category_config(cfg: CN) -> None:
|
9 |
+
"""
|
10 |
+
Add config for additional category-related dataset options
|
11 |
+
- category whitelisting
|
12 |
+
- category mapping
|
13 |
+
"""
|
14 |
+
_C = cfg
|
15 |
+
_C.DATASETS.CATEGORY_MAPS = CN(new_allowed=True)
|
16 |
+
_C.DATASETS.WHITELISTED_CATEGORIES = CN(new_allowed=True)
|
17 |
+
# class to mesh mapping
|
18 |
+
_C.DATASETS.CLASS_TO_MESH_NAME_MAPPING = CN(new_allowed=True)
|
19 |
+
|
20 |
+
|
21 |
+
def add_evaluation_config(cfg: CN) -> None:
|
22 |
+
_C = cfg
|
23 |
+
_C.DENSEPOSE_EVALUATION = CN()
|
24 |
+
# evaluator type, possible values:
|
25 |
+
# - "iou": evaluator for models that produce iou data
|
26 |
+
# - "cse": evaluator for models that produce cse data
|
27 |
+
_C.DENSEPOSE_EVALUATION.TYPE = "iou"
|
28 |
+
# storage for DensePose results, possible values:
|
29 |
+
# - "none": no explicit storage, all the results are stored in the
|
30 |
+
# dictionary with predictions, memory intensive;
|
31 |
+
# historically the default storage type
|
32 |
+
# - "ram": RAM storage, uses per-process RAM storage, which is
|
33 |
+
# reduced to a single process storage on later stages,
|
34 |
+
# less memory intensive
|
35 |
+
# - "file": file storage, uses per-process file-based storage,
|
36 |
+
# the least memory intensive, but may create bottlenecks
|
37 |
+
# on file system accesses
|
38 |
+
_C.DENSEPOSE_EVALUATION.STORAGE = "none"
|
39 |
+
# minimum threshold for IOU values: the lower its values is,
|
40 |
+
# the more matches are produced (and the higher the AP score)
|
41 |
+
_C.DENSEPOSE_EVALUATION.MIN_IOU_THRESHOLD = 0.5
|
42 |
+
# Non-distributed inference is slower (at inference time) but can avoid RAM OOM
|
43 |
+
_C.DENSEPOSE_EVALUATION.DISTRIBUTED_INFERENCE = True
|
44 |
+
# evaluate mesh alignment based on vertex embeddings, only makes sense in CSE context
|
45 |
+
_C.DENSEPOSE_EVALUATION.EVALUATE_MESH_ALIGNMENT = False
|
46 |
+
# meshes to compute mesh alignment for
|
47 |
+
_C.DENSEPOSE_EVALUATION.MESH_ALIGNMENT_MESH_NAMES = []
|
48 |
+
|
49 |
+
|
50 |
+
def add_bootstrap_config(cfg: CN) -> None:
|
51 |
+
""" """
|
52 |
+
_C = cfg
|
53 |
+
_C.BOOTSTRAP_DATASETS = []
|
54 |
+
_C.BOOTSTRAP_MODEL = CN()
|
55 |
+
_C.BOOTSTRAP_MODEL.WEIGHTS = ""
|
56 |
+
_C.BOOTSTRAP_MODEL.DEVICE = "cuda"
|
57 |
+
|
58 |
+
|
59 |
+
def get_bootstrap_dataset_config() -> CN:
|
60 |
+
_C = CN()
|
61 |
+
_C.DATASET = ""
|
62 |
+
# ratio used to mix data loaders
|
63 |
+
_C.RATIO = 0.1
|
64 |
+
# image loader
|
65 |
+
_C.IMAGE_LOADER = CN(new_allowed=True)
|
66 |
+
_C.IMAGE_LOADER.TYPE = ""
|
67 |
+
_C.IMAGE_LOADER.BATCH_SIZE = 4
|
68 |
+
_C.IMAGE_LOADER.NUM_WORKERS = 4
|
69 |
+
_C.IMAGE_LOADER.CATEGORIES = []
|
70 |
+
_C.IMAGE_LOADER.MAX_COUNT_PER_CATEGORY = 1_000_000
|
71 |
+
_C.IMAGE_LOADER.CATEGORY_TO_CLASS_MAPPING = CN(new_allowed=True)
|
72 |
+
# inference
|
73 |
+
_C.INFERENCE = CN()
|
74 |
+
# batch size for model inputs
|
75 |
+
_C.INFERENCE.INPUT_BATCH_SIZE = 4
|
76 |
+
# batch size to group model outputs
|
77 |
+
_C.INFERENCE.OUTPUT_BATCH_SIZE = 2
|
78 |
+
# sampled data
|
79 |
+
_C.DATA_SAMPLER = CN(new_allowed=True)
|
80 |
+
_C.DATA_SAMPLER.TYPE = ""
|
81 |
+
_C.DATA_SAMPLER.USE_GROUND_TRUTH_CATEGORIES = False
|
82 |
+
# filter
|
83 |
+
_C.FILTER = CN(new_allowed=True)
|
84 |
+
_C.FILTER.TYPE = ""
|
85 |
+
return _C
|
86 |
+
|
87 |
+
|
88 |
+
def load_bootstrap_config(cfg: CN) -> None:
|
89 |
+
"""
|
90 |
+
Bootstrap datasets are given as a list of `dict` that are not automatically
|
91 |
+
converted into CfgNode. This method processes all bootstrap dataset entries
|
92 |
+
and ensures that they are in CfgNode format and comply with the specification
|
93 |
+
"""
|
94 |
+
if not cfg.BOOTSTRAP_DATASETS:
|
95 |
+
return
|
96 |
+
|
97 |
+
bootstrap_datasets_cfgnodes = []
|
98 |
+
for dataset_cfg in cfg.BOOTSTRAP_DATASETS:
|
99 |
+
_C = get_bootstrap_dataset_config().clone()
|
100 |
+
_C.merge_from_other_cfg(CN(dataset_cfg))
|
101 |
+
bootstrap_datasets_cfgnodes.append(_C)
|
102 |
+
cfg.BOOTSTRAP_DATASETS = bootstrap_datasets_cfgnodes
|
103 |
+
|
104 |
+
|
105 |
+
def add_densepose_head_cse_config(cfg: CN) -> None:
|
106 |
+
"""
|
107 |
+
Add configuration options for Continuous Surface Embeddings (CSE)
|
108 |
+
"""
|
109 |
+
_C = cfg
|
110 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE = CN()
|
111 |
+
# Dimensionality D of the embedding space
|
112 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.EMBED_SIZE = 16
|
113 |
+
# Embedder specifications for various mesh IDs
|
114 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.EMBEDDERS = CN(new_allowed=True)
|
115 |
+
# normalization coefficient for embedding distances
|
116 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.EMBEDDING_DIST_GAUSS_SIGMA = 0.01
|
117 |
+
# normalization coefficient for geodesic distances
|
118 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.GEODESIC_DIST_GAUSS_SIGMA = 0.01
|
119 |
+
# embedding loss weight
|
120 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.EMBED_LOSS_WEIGHT = 0.6
|
121 |
+
# embedding loss name, currently the following options are supported:
|
122 |
+
# - EmbeddingLoss: cross-entropy on vertex labels
|
123 |
+
# - SoftEmbeddingLoss: cross-entropy on vertex label combined with
|
124 |
+
# Gaussian penalty on distance between vertices
|
125 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.EMBED_LOSS_NAME = "EmbeddingLoss"
|
126 |
+
# optimizer hyperparameters
|
127 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.FEATURES_LR_FACTOR = 1.0
|
128 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.EMBEDDING_LR_FACTOR = 1.0
|
129 |
+
# Shape to shape cycle consistency loss parameters:
|
130 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.SHAPE_TO_SHAPE_CYCLE_LOSS = CN({"ENABLED": False})
|
131 |
+
# shape to shape cycle consistency loss weight
|
132 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.SHAPE_TO_SHAPE_CYCLE_LOSS.WEIGHT = 0.025
|
133 |
+
# norm type used for loss computation
|
134 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.SHAPE_TO_SHAPE_CYCLE_LOSS.NORM_P = 2
|
135 |
+
# normalization term for embedding similarity matrices
|
136 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.SHAPE_TO_SHAPE_CYCLE_LOSS.TEMPERATURE = 0.05
|
137 |
+
# maximum number of vertices to include into shape to shape cycle loss
|
138 |
+
# if negative or zero, all vertices are considered
|
139 |
+
# if positive, random subset of vertices of given size is considered
|
140 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.SHAPE_TO_SHAPE_CYCLE_LOSS.MAX_NUM_VERTICES = 4936
|
141 |
+
# Pixel to shape cycle consistency loss parameters:
|
142 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.PIX_TO_SHAPE_CYCLE_LOSS = CN({"ENABLED": False})
|
143 |
+
# pixel to shape cycle consistency loss weight
|
144 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.PIX_TO_SHAPE_CYCLE_LOSS.WEIGHT = 0.0001
|
145 |
+
# norm type used for loss computation
|
146 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.PIX_TO_SHAPE_CYCLE_LOSS.NORM_P = 2
|
147 |
+
# map images to all meshes and back (if false, use only gt meshes from the batch)
|
148 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.PIX_TO_SHAPE_CYCLE_LOSS.USE_ALL_MESHES_NOT_GT_ONLY = False
|
149 |
+
# Randomly select at most this number of pixels from every instance
|
150 |
+
# if negative or zero, all vertices are considered
|
151 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.PIX_TO_SHAPE_CYCLE_LOSS.NUM_PIXELS_TO_SAMPLE = 100
|
152 |
+
# normalization factor for pixel to pixel distances (higher value = smoother distribution)
|
153 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.PIX_TO_SHAPE_CYCLE_LOSS.PIXEL_SIGMA = 5.0
|
154 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.PIX_TO_SHAPE_CYCLE_LOSS.TEMPERATURE_PIXEL_TO_VERTEX = 0.05
|
155 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CSE.PIX_TO_SHAPE_CYCLE_LOSS.TEMPERATURE_VERTEX_TO_PIXEL = 0.05
|
156 |
+
|
157 |
+
|
158 |
+
def add_densepose_head_config(cfg: CN) -> None:
|
159 |
+
"""
|
160 |
+
Add config for densepose head.
|
161 |
+
"""
|
162 |
+
_C = cfg
|
163 |
+
|
164 |
+
_C.MODEL.DENSEPOSE_ON = True
|
165 |
+
|
166 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD = CN()
|
167 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.NAME = ""
|
168 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.NUM_STACKED_CONVS = 8
|
169 |
+
# Number of parts used for point labels
|
170 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.NUM_PATCHES = 24
|
171 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.DECONV_KERNEL = 4
|
172 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CONV_HEAD_DIM = 512
|
173 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.CONV_HEAD_KERNEL = 3
|
174 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.UP_SCALE = 2
|
175 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.HEATMAP_SIZE = 112
|
176 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.POOLER_TYPE = "ROIAlignV2"
|
177 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.POOLER_RESOLUTION = 28
|
178 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.POOLER_SAMPLING_RATIO = 2
|
179 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.NUM_COARSE_SEGM_CHANNELS = 2 # 15 or 2
|
180 |
+
# Overlap threshold for an RoI to be considered foreground (if >= FG_IOU_THRESHOLD)
|
181 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.FG_IOU_THRESHOLD = 0.7
|
182 |
+
# Loss weights for annotation masks.(14 Parts)
|
183 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.INDEX_WEIGHTS = 5.0
|
184 |
+
# Loss weights for surface parts. (24 Parts)
|
185 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.PART_WEIGHTS = 1.0
|
186 |
+
# Loss weights for UV regression.
|
187 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.POINT_REGRESSION_WEIGHTS = 0.01
|
188 |
+
# Coarse segmentation is trained using instance segmentation task data
|
189 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.COARSE_SEGM_TRAINED_BY_MASKS = False
|
190 |
+
# For Decoder
|
191 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.DECODER_ON = True
|
192 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.DECODER_NUM_CLASSES = 256
|
193 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.DECODER_CONV_DIMS = 256
|
194 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.DECODER_NORM = ""
|
195 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.DECODER_COMMON_STRIDE = 4
|
196 |
+
# For DeepLab head
|
197 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.DEEPLAB = CN()
|
198 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.DEEPLAB.NORM = "GN"
|
199 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.DEEPLAB.NONLOCAL_ON = 0
|
200 |
+
# Predictor class name, must be registered in DENSEPOSE_PREDICTOR_REGISTRY
|
201 |
+
# Some registered predictors:
|
202 |
+
# "DensePoseChartPredictor": predicts segmentation and UV coordinates for predefined charts
|
203 |
+
# "DensePoseChartWithConfidencePredictor": predicts segmentation, UV coordinates
|
204 |
+
# and associated confidences for predefined charts (default)
|
205 |
+
# "DensePoseEmbeddingWithConfidencePredictor": predicts segmentation, embeddings
|
206 |
+
# and associated confidences for CSE
|
207 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.PREDICTOR_NAME = "DensePoseChartWithConfidencePredictor"
|
208 |
+
# Loss class name, must be registered in DENSEPOSE_LOSS_REGISTRY
|
209 |
+
# Some registered losses:
|
210 |
+
# "DensePoseChartLoss": loss for chart-based models that estimate
|
211 |
+
# segmentation and UV coordinates
|
212 |
+
# "DensePoseChartWithConfidenceLoss": loss for chart-based models that estimate
|
213 |
+
# segmentation, UV coordinates and the corresponding confidences (default)
|
214 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.LOSS_NAME = "DensePoseChartWithConfidenceLoss"
|
215 |
+
# Confidences
|
216 |
+
# Enable learning UV confidences (variances) along with the actual values
|
217 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.UV_CONFIDENCE = CN({"ENABLED": False})
|
218 |
+
# UV confidence lower bound
|
219 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.UV_CONFIDENCE.EPSILON = 0.01
|
220 |
+
# Enable learning segmentation confidences (variances) along with the actual values
|
221 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.SEGM_CONFIDENCE = CN({"ENABLED": False})
|
222 |
+
# Segmentation confidence lower bound
|
223 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.SEGM_CONFIDENCE.EPSILON = 0.01
|
224 |
+
# Statistical model type for confidence learning, possible values:
|
225 |
+
# - "iid_iso": statistically independent identically distributed residuals
|
226 |
+
# with isotropic covariance
|
227 |
+
# - "indep_aniso": statistically independent residuals with anisotropic
|
228 |
+
# covariances
|
229 |
+
_C.MODEL.ROI_DENSEPOSE_HEAD.UV_CONFIDENCE.TYPE = "iid_iso"
|
230 |
+
# List of angles for rotation in data augmentation during training
|
231 |
+
_C.INPUT.ROTATION_ANGLES = [0]
|
232 |
+
_C.TEST.AUG.ROTATION_ANGLES = () # Rotation TTA
|
233 |
+
|
234 |
+
add_densepose_head_cse_config(cfg)
|
235 |
+
|
236 |
+
|
237 |
+
def add_hrnet_config(cfg: CN) -> None:
|
238 |
+
"""
|
239 |
+
Add config for HRNet backbone.
|
240 |
+
"""
|
241 |
+
_C = cfg
|
242 |
+
|
243 |
+
# For HigherHRNet w32
|
244 |
+
_C.MODEL.HRNET = CN()
|
245 |
+
_C.MODEL.HRNET.STEM_INPLANES = 64
|
246 |
+
_C.MODEL.HRNET.STAGE2 = CN()
|
247 |
+
_C.MODEL.HRNET.STAGE2.NUM_MODULES = 1
|
248 |
+
_C.MODEL.HRNET.STAGE2.NUM_BRANCHES = 2
|
249 |
+
_C.MODEL.HRNET.STAGE2.BLOCK = "BASIC"
|
250 |
+
_C.MODEL.HRNET.STAGE2.NUM_BLOCKS = [4, 4]
|
251 |
+
_C.MODEL.HRNET.STAGE2.NUM_CHANNELS = [32, 64]
|
252 |
+
_C.MODEL.HRNET.STAGE2.FUSE_METHOD = "SUM"
|
253 |
+
_C.MODEL.HRNET.STAGE3 = CN()
|
254 |
+
_C.MODEL.HRNET.STAGE3.NUM_MODULES = 4
|
255 |
+
_C.MODEL.HRNET.STAGE3.NUM_BRANCHES = 3
|
256 |
+
_C.MODEL.HRNET.STAGE3.BLOCK = "BASIC"
|
257 |
+
_C.MODEL.HRNET.STAGE3.NUM_BLOCKS = [4, 4, 4]
|
258 |
+
_C.MODEL.HRNET.STAGE3.NUM_CHANNELS = [32, 64, 128]
|
259 |
+
_C.MODEL.HRNET.STAGE3.FUSE_METHOD = "SUM"
|
260 |
+
_C.MODEL.HRNET.STAGE4 = CN()
|
261 |
+
_C.MODEL.HRNET.STAGE4.NUM_MODULES = 3
|
262 |
+
_C.MODEL.HRNET.STAGE4.NUM_BRANCHES = 4
|
263 |
+
_C.MODEL.HRNET.STAGE4.BLOCK = "BASIC"
|
264 |
+
_C.MODEL.HRNET.STAGE4.NUM_BLOCKS = [4, 4, 4, 4]
|
265 |
+
_C.MODEL.HRNET.STAGE4.NUM_CHANNELS = [32, 64, 128, 256]
|
266 |
+
_C.MODEL.HRNET.STAGE4.FUSE_METHOD = "SUM"
|
267 |
+
|
268 |
+
_C.MODEL.HRNET.HRFPN = CN()
|
269 |
+
_C.MODEL.HRNET.HRFPN.OUT_CHANNELS = 256
|
270 |
+
|
271 |
+
|
272 |
+
def add_densepose_config(cfg: CN) -> None:
|
273 |
+
add_densepose_head_config(cfg)
|
274 |
+
add_hrnet_config(cfg)
|
275 |
+
add_bootstrap_config(cfg)
|
276 |
+
add_dataset_category_config(cfg)
|
277 |
+
add_evaluation_config(cfg)
|
densepose/converters/__init__.py
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from .hflip import HFlipConverter
|
4 |
+
from .to_mask import ToMaskConverter
|
5 |
+
from .to_chart_result import ToChartResultConverter, ToChartResultConverterWithConfidences
|
6 |
+
from .segm_to_mask import (
|
7 |
+
predictor_output_with_fine_and_coarse_segm_to_mask,
|
8 |
+
predictor_output_with_coarse_segm_to_mask,
|
9 |
+
resample_fine_and_coarse_segm_to_bbox,
|
10 |
+
)
|
11 |
+
from .chart_output_to_chart_result import (
|
12 |
+
densepose_chart_predictor_output_to_result,
|
13 |
+
densepose_chart_predictor_output_to_result_with_confidences,
|
14 |
+
)
|
15 |
+
from .chart_output_hflip import densepose_chart_predictor_output_hflip
|
densepose/converters/base.py
ADDED
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from typing import Any, Tuple, Type
|
4 |
+
import torch
|
5 |
+
|
6 |
+
|
7 |
+
class BaseConverter:
|
8 |
+
"""
|
9 |
+
Converter base class to be reused by various converters.
|
10 |
+
Converter allows one to convert data from various source types to a particular
|
11 |
+
destination type. Each source type needs to register its converter. The
|
12 |
+
registration for each source type is valid for all descendants of that type.
|
13 |
+
"""
|
14 |
+
|
15 |
+
@classmethod
|
16 |
+
def register(cls, from_type: Type, converter: Any = None):
|
17 |
+
"""
|
18 |
+
Registers a converter for the specified type.
|
19 |
+
Can be used as a decorator (if converter is None), or called as a method.
|
20 |
+
|
21 |
+
Args:
|
22 |
+
from_type (type): type to register the converter for;
|
23 |
+
all instances of this type will use the same converter
|
24 |
+
converter (callable): converter to be registered for the given
|
25 |
+
type; if None, this method is assumed to be a decorator for the converter
|
26 |
+
"""
|
27 |
+
|
28 |
+
if converter is not None:
|
29 |
+
cls._do_register(from_type, converter)
|
30 |
+
|
31 |
+
def wrapper(converter: Any) -> Any:
|
32 |
+
cls._do_register(from_type, converter)
|
33 |
+
return converter
|
34 |
+
|
35 |
+
return wrapper
|
36 |
+
|
37 |
+
@classmethod
|
38 |
+
def _do_register(cls, from_type: Type, converter: Any):
|
39 |
+
cls.registry[from_type] = converter # pyre-ignore[16]
|
40 |
+
|
41 |
+
@classmethod
|
42 |
+
def _lookup_converter(cls, from_type: Type) -> Any:
|
43 |
+
"""
|
44 |
+
Perform recursive lookup for the given type
|
45 |
+
to find registered converter. If a converter was found for some base
|
46 |
+
class, it gets registered for this class to save on further lookups.
|
47 |
+
|
48 |
+
Args:
|
49 |
+
from_type: type for which to find a converter
|
50 |
+
Return:
|
51 |
+
callable or None - registered converter or None
|
52 |
+
if no suitable entry was found in the registry
|
53 |
+
"""
|
54 |
+
if from_type in cls.registry: # pyre-ignore[16]
|
55 |
+
return cls.registry[from_type]
|
56 |
+
for base in from_type.__bases__:
|
57 |
+
converter = cls._lookup_converter(base)
|
58 |
+
if converter is not None:
|
59 |
+
cls._do_register(from_type, converter)
|
60 |
+
return converter
|
61 |
+
return None
|
62 |
+
|
63 |
+
@classmethod
|
64 |
+
def convert(cls, instance: Any, *args, **kwargs):
|
65 |
+
"""
|
66 |
+
Convert an instance to the destination type using some registered
|
67 |
+
converter. Does recursive lookup for base classes, so there's no need
|
68 |
+
for explicit registration for derived classes.
|
69 |
+
|
70 |
+
Args:
|
71 |
+
instance: source instance to convert to the destination type
|
72 |
+
Return:
|
73 |
+
An instance of the destination type obtained from the source instance
|
74 |
+
Raises KeyError, if no suitable converter found
|
75 |
+
"""
|
76 |
+
instance_type = type(instance)
|
77 |
+
converter = cls._lookup_converter(instance_type)
|
78 |
+
if converter is None:
|
79 |
+
if cls.dst_type is None: # pyre-ignore[16]
|
80 |
+
output_type_str = "itself"
|
81 |
+
else:
|
82 |
+
output_type_str = cls.dst_type
|
83 |
+
raise KeyError(f"Could not find converter from {instance_type} to {output_type_str}")
|
84 |
+
return converter(instance, *args, **kwargs)
|
85 |
+
|
86 |
+
|
87 |
+
IntTupleBox = Tuple[int, int, int, int]
|
88 |
+
|
89 |
+
|
90 |
+
def make_int_box(box: torch.Tensor) -> IntTupleBox:
|
91 |
+
int_box = [0, 0, 0, 0]
|
92 |
+
int_box[0], int_box[1], int_box[2], int_box[3] = tuple(box.long().tolist())
|
93 |
+
return int_box[0], int_box[1], int_box[2], int_box[3]
|
densepose/converters/builtin.py
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from ..structures import DensePoseChartPredictorOutput, DensePoseEmbeddingPredictorOutput
|
4 |
+
from . import (
|
5 |
+
HFlipConverter,
|
6 |
+
ToChartResultConverter,
|
7 |
+
ToChartResultConverterWithConfidences,
|
8 |
+
ToMaskConverter,
|
9 |
+
densepose_chart_predictor_output_hflip,
|
10 |
+
densepose_chart_predictor_output_to_result,
|
11 |
+
densepose_chart_predictor_output_to_result_with_confidences,
|
12 |
+
predictor_output_with_coarse_segm_to_mask,
|
13 |
+
predictor_output_with_fine_and_coarse_segm_to_mask,
|
14 |
+
)
|
15 |
+
|
16 |
+
ToMaskConverter.register(
|
17 |
+
DensePoseChartPredictorOutput, predictor_output_with_fine_and_coarse_segm_to_mask
|
18 |
+
)
|
19 |
+
ToMaskConverter.register(
|
20 |
+
DensePoseEmbeddingPredictorOutput, predictor_output_with_coarse_segm_to_mask
|
21 |
+
)
|
22 |
+
|
23 |
+
ToChartResultConverter.register(
|
24 |
+
DensePoseChartPredictorOutput, densepose_chart_predictor_output_to_result
|
25 |
+
)
|
26 |
+
|
27 |
+
ToChartResultConverterWithConfidences.register(
|
28 |
+
DensePoseChartPredictorOutput, densepose_chart_predictor_output_to_result_with_confidences
|
29 |
+
)
|
30 |
+
|
31 |
+
HFlipConverter.register(DensePoseChartPredictorOutput, densepose_chart_predictor_output_hflip)
|
densepose/converters/chart_output_hflip.py
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
from dataclasses import fields
|
3 |
+
import torch
|
4 |
+
|
5 |
+
from densepose.structures import DensePoseChartPredictorOutput, DensePoseTransformData
|
6 |
+
|
7 |
+
|
8 |
+
def densepose_chart_predictor_output_hflip(
|
9 |
+
densepose_predictor_output: DensePoseChartPredictorOutput,
|
10 |
+
transform_data: DensePoseTransformData,
|
11 |
+
) -> DensePoseChartPredictorOutput:
|
12 |
+
"""
|
13 |
+
Change to take into account a Horizontal flip.
|
14 |
+
"""
|
15 |
+
if len(densepose_predictor_output) > 0:
|
16 |
+
|
17 |
+
PredictorOutput = type(densepose_predictor_output)
|
18 |
+
output_dict = {}
|
19 |
+
|
20 |
+
for field in fields(densepose_predictor_output):
|
21 |
+
field_value = getattr(densepose_predictor_output, field.name)
|
22 |
+
# flip tensors
|
23 |
+
if isinstance(field_value, torch.Tensor):
|
24 |
+
setattr(densepose_predictor_output, field.name, torch.flip(field_value, [3]))
|
25 |
+
|
26 |
+
densepose_predictor_output = _flip_iuv_semantics_tensor(
|
27 |
+
densepose_predictor_output, transform_data
|
28 |
+
)
|
29 |
+
densepose_predictor_output = _flip_segm_semantics_tensor(
|
30 |
+
densepose_predictor_output, transform_data
|
31 |
+
)
|
32 |
+
|
33 |
+
for field in fields(densepose_predictor_output):
|
34 |
+
output_dict[field.name] = getattr(densepose_predictor_output, field.name)
|
35 |
+
|
36 |
+
return PredictorOutput(**output_dict)
|
37 |
+
else:
|
38 |
+
return densepose_predictor_output
|
39 |
+
|
40 |
+
|
41 |
+
def _flip_iuv_semantics_tensor(
|
42 |
+
densepose_predictor_output: DensePoseChartPredictorOutput,
|
43 |
+
dp_transform_data: DensePoseTransformData,
|
44 |
+
) -> DensePoseChartPredictorOutput:
|
45 |
+
point_label_symmetries = dp_transform_data.point_label_symmetries
|
46 |
+
uv_symmetries = dp_transform_data.uv_symmetries
|
47 |
+
|
48 |
+
N, C, H, W = densepose_predictor_output.u.shape
|
49 |
+
u_loc = (densepose_predictor_output.u[:, 1:, :, :].clamp(0, 1) * 255).long()
|
50 |
+
v_loc = (densepose_predictor_output.v[:, 1:, :, :].clamp(0, 1) * 255).long()
|
51 |
+
Iindex = torch.arange(C - 1, device=densepose_predictor_output.u.device)[
|
52 |
+
None, :, None, None
|
53 |
+
].expand(N, C - 1, H, W)
|
54 |
+
densepose_predictor_output.u[:, 1:, :, :] = uv_symmetries["U_transforms"][Iindex, v_loc, u_loc]
|
55 |
+
densepose_predictor_output.v[:, 1:, :, :] = uv_symmetries["V_transforms"][Iindex, v_loc, u_loc]
|
56 |
+
|
57 |
+
for el in ["fine_segm", "u", "v"]:
|
58 |
+
densepose_predictor_output.__dict__[el] = densepose_predictor_output.__dict__[el][
|
59 |
+
:, point_label_symmetries, :, :
|
60 |
+
]
|
61 |
+
return densepose_predictor_output
|
62 |
+
|
63 |
+
|
64 |
+
def _flip_segm_semantics_tensor(
|
65 |
+
densepose_predictor_output: DensePoseChartPredictorOutput, dp_transform_data
|
66 |
+
):
|
67 |
+
if densepose_predictor_output.coarse_segm.shape[1] > 2:
|
68 |
+
densepose_predictor_output.coarse_segm = densepose_predictor_output.coarse_segm[
|
69 |
+
:, dp_transform_data.mask_label_symmetries, :, :
|
70 |
+
]
|
71 |
+
return densepose_predictor_output
|
densepose/converters/chart_output_to_chart_result.py
ADDED
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from typing import Dict
|
4 |
+
import torch
|
5 |
+
from torch.nn import functional as F
|
6 |
+
|
7 |
+
from detectron2.structures.boxes import Boxes, BoxMode
|
8 |
+
|
9 |
+
from ..structures import (
|
10 |
+
DensePoseChartPredictorOutput,
|
11 |
+
DensePoseChartResult,
|
12 |
+
DensePoseChartResultWithConfidences,
|
13 |
+
)
|
14 |
+
from . import resample_fine_and_coarse_segm_to_bbox
|
15 |
+
from .base import IntTupleBox, make_int_box
|
16 |
+
|
17 |
+
|
18 |
+
def resample_uv_tensors_to_bbox(
|
19 |
+
u: torch.Tensor,
|
20 |
+
v: torch.Tensor,
|
21 |
+
labels: torch.Tensor,
|
22 |
+
box_xywh_abs: IntTupleBox,
|
23 |
+
) -> torch.Tensor:
|
24 |
+
"""
|
25 |
+
Resamples U and V coordinate estimates for the given bounding box
|
26 |
+
|
27 |
+
Args:
|
28 |
+
u (tensor [1, C, H, W] of float): U coordinates
|
29 |
+
v (tensor [1, C, H, W] of float): V coordinates
|
30 |
+
labels (tensor [H, W] of long): labels obtained by resampling segmentation
|
31 |
+
outputs for the given bounding box
|
32 |
+
box_xywh_abs (tuple of 4 int): bounding box that corresponds to predictor outputs
|
33 |
+
Return:
|
34 |
+
Resampled U and V coordinates - a tensor [2, H, W] of float
|
35 |
+
"""
|
36 |
+
x, y, w, h = box_xywh_abs
|
37 |
+
w = max(int(w), 1)
|
38 |
+
h = max(int(h), 1)
|
39 |
+
u_bbox = F.interpolate(u, (h, w), mode="bilinear", align_corners=False)
|
40 |
+
v_bbox = F.interpolate(v, (h, w), mode="bilinear", align_corners=False)
|
41 |
+
uv = torch.zeros([2, h, w], dtype=torch.float32, device=u.device)
|
42 |
+
for part_id in range(1, u_bbox.size(1)):
|
43 |
+
uv[0][labels == part_id] = u_bbox[0, part_id][labels == part_id]
|
44 |
+
uv[1][labels == part_id] = v_bbox[0, part_id][labels == part_id]
|
45 |
+
return uv
|
46 |
+
|
47 |
+
|
48 |
+
def resample_uv_to_bbox(
|
49 |
+
predictor_output: DensePoseChartPredictorOutput,
|
50 |
+
labels: torch.Tensor,
|
51 |
+
box_xywh_abs: IntTupleBox,
|
52 |
+
) -> torch.Tensor:
|
53 |
+
"""
|
54 |
+
Resamples U and V coordinate estimates for the given bounding box
|
55 |
+
|
56 |
+
Args:
|
57 |
+
predictor_output (DensePoseChartPredictorOutput): DensePose predictor
|
58 |
+
output to be resampled
|
59 |
+
labels (tensor [H, W] of long): labels obtained by resampling segmentation
|
60 |
+
outputs for the given bounding box
|
61 |
+
box_xywh_abs (tuple of 4 int): bounding box that corresponds to predictor outputs
|
62 |
+
Return:
|
63 |
+
Resampled U and V coordinates - a tensor [2, H, W] of float
|
64 |
+
"""
|
65 |
+
return resample_uv_tensors_to_bbox(
|
66 |
+
predictor_output.u,
|
67 |
+
predictor_output.v,
|
68 |
+
labels,
|
69 |
+
box_xywh_abs,
|
70 |
+
)
|
71 |
+
|
72 |
+
|
73 |
+
def densepose_chart_predictor_output_to_result(
|
74 |
+
predictor_output: DensePoseChartPredictorOutput, boxes: Boxes
|
75 |
+
) -> DensePoseChartResult:
|
76 |
+
"""
|
77 |
+
Convert densepose chart predictor outputs to results
|
78 |
+
|
79 |
+
Args:
|
80 |
+
predictor_output (DensePoseChartPredictorOutput): DensePose predictor
|
81 |
+
output to be converted to results, must contain only 1 output
|
82 |
+
boxes (Boxes): bounding box that corresponds to the predictor output,
|
83 |
+
must contain only 1 bounding box
|
84 |
+
Return:
|
85 |
+
DensePose chart-based result (DensePoseChartResult)
|
86 |
+
"""
|
87 |
+
assert len(predictor_output) == 1 and len(boxes) == 1, (
|
88 |
+
f"Predictor output to result conversion can operate only single outputs"
|
89 |
+
f", got {len(predictor_output)} predictor outputs and {len(boxes)} boxes"
|
90 |
+
)
|
91 |
+
|
92 |
+
boxes_xyxy_abs = boxes.tensor.clone()
|
93 |
+
boxes_xywh_abs = BoxMode.convert(boxes_xyxy_abs, BoxMode.XYXY_ABS, BoxMode.XYWH_ABS)
|
94 |
+
box_xywh = make_int_box(boxes_xywh_abs[0])
|
95 |
+
|
96 |
+
labels = resample_fine_and_coarse_segm_to_bbox(predictor_output, box_xywh).squeeze(0)
|
97 |
+
uv = resample_uv_to_bbox(predictor_output, labels, box_xywh)
|
98 |
+
return DensePoseChartResult(labels=labels, uv=uv)
|
99 |
+
|
100 |
+
|
101 |
+
def resample_confidences_to_bbox(
|
102 |
+
predictor_output: DensePoseChartPredictorOutput,
|
103 |
+
labels: torch.Tensor,
|
104 |
+
box_xywh_abs: IntTupleBox,
|
105 |
+
) -> Dict[str, torch.Tensor]:
|
106 |
+
"""
|
107 |
+
Resamples confidences for the given bounding box
|
108 |
+
|
109 |
+
Args:
|
110 |
+
predictor_output (DensePoseChartPredictorOutput): DensePose predictor
|
111 |
+
output to be resampled
|
112 |
+
labels (tensor [H, W] of long): labels obtained by resampling segmentation
|
113 |
+
outputs for the given bounding box
|
114 |
+
box_xywh_abs (tuple of 4 int): bounding box that corresponds to predictor outputs
|
115 |
+
Return:
|
116 |
+
Resampled confidences - a dict of [H, W] tensors of float
|
117 |
+
"""
|
118 |
+
|
119 |
+
x, y, w, h = box_xywh_abs
|
120 |
+
w = max(int(w), 1)
|
121 |
+
h = max(int(h), 1)
|
122 |
+
|
123 |
+
confidence_names = [
|
124 |
+
"sigma_1",
|
125 |
+
"sigma_2",
|
126 |
+
"kappa_u",
|
127 |
+
"kappa_v",
|
128 |
+
"fine_segm_confidence",
|
129 |
+
"coarse_segm_confidence",
|
130 |
+
]
|
131 |
+
confidence_results = {key: None for key in confidence_names}
|
132 |
+
confidence_names = [
|
133 |
+
key for key in confidence_names if getattr(predictor_output, key) is not None
|
134 |
+
]
|
135 |
+
confidence_base = torch.zeros([h, w], dtype=torch.float32, device=predictor_output.u.device)
|
136 |
+
|
137 |
+
# assign data from channels that correspond to the labels
|
138 |
+
for key in confidence_names:
|
139 |
+
resampled_confidence = F.interpolate(
|
140 |
+
getattr(predictor_output, key),
|
141 |
+
(h, w),
|
142 |
+
mode="bilinear",
|
143 |
+
align_corners=False,
|
144 |
+
)
|
145 |
+
result = confidence_base.clone()
|
146 |
+
for part_id in range(1, predictor_output.u.size(1)):
|
147 |
+
if resampled_confidence.size(1) != predictor_output.u.size(1):
|
148 |
+
# confidence is not part-based, don't try to fill it part by part
|
149 |
+
continue
|
150 |
+
result[labels == part_id] = resampled_confidence[0, part_id][labels == part_id]
|
151 |
+
|
152 |
+
if resampled_confidence.size(1) != predictor_output.u.size(1):
|
153 |
+
# confidence is not part-based, fill the data with the first channel
|
154 |
+
# (targeted for segmentation confidences that have only 1 channel)
|
155 |
+
result = resampled_confidence[0, 0]
|
156 |
+
|
157 |
+
confidence_results[key] = result
|
158 |
+
|
159 |
+
return confidence_results # pyre-ignore[7]
|
160 |
+
|
161 |
+
|
162 |
+
def densepose_chart_predictor_output_to_result_with_confidences(
|
163 |
+
predictor_output: DensePoseChartPredictorOutput, boxes: Boxes
|
164 |
+
) -> DensePoseChartResultWithConfidences:
|
165 |
+
"""
|
166 |
+
Convert densepose chart predictor outputs to results
|
167 |
+
|
168 |
+
Args:
|
169 |
+
predictor_output (DensePoseChartPredictorOutput): DensePose predictor
|
170 |
+
output with confidences to be converted to results, must contain only 1 output
|
171 |
+
boxes (Boxes): bounding box that corresponds to the predictor output,
|
172 |
+
must contain only 1 bounding box
|
173 |
+
Return:
|
174 |
+
DensePose chart-based result with confidences (DensePoseChartResultWithConfidences)
|
175 |
+
"""
|
176 |
+
assert len(predictor_output) == 1 and len(boxes) == 1, (
|
177 |
+
f"Predictor output to result conversion can operate only single outputs"
|
178 |
+
f", got {len(predictor_output)} predictor outputs and {len(boxes)} boxes"
|
179 |
+
)
|
180 |
+
|
181 |
+
boxes_xyxy_abs = boxes.tensor.clone()
|
182 |
+
boxes_xywh_abs = BoxMode.convert(boxes_xyxy_abs, BoxMode.XYXY_ABS, BoxMode.XYWH_ABS)
|
183 |
+
box_xywh = make_int_box(boxes_xywh_abs[0])
|
184 |
+
|
185 |
+
labels = resample_fine_and_coarse_segm_to_bbox(predictor_output, box_xywh).squeeze(0)
|
186 |
+
uv = resample_uv_to_bbox(predictor_output, labels, box_xywh)
|
187 |
+
confidences = resample_confidences_to_bbox(predictor_output, labels, box_xywh)
|
188 |
+
return DensePoseChartResultWithConfidences(labels=labels, uv=uv, **confidences)
|
densepose/converters/hflip.py
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from typing import Any
|
4 |
+
|
5 |
+
from .base import BaseConverter
|
6 |
+
|
7 |
+
|
8 |
+
class HFlipConverter(BaseConverter):
|
9 |
+
"""
|
10 |
+
Converts various DensePose predictor outputs to DensePose results.
|
11 |
+
Each DensePose predictor output type has to register its convertion strategy.
|
12 |
+
"""
|
13 |
+
|
14 |
+
registry = {}
|
15 |
+
dst_type = None
|
16 |
+
|
17 |
+
@classmethod
|
18 |
+
# pyre-fixme[14]: `convert` overrides method defined in `BaseConverter`
|
19 |
+
# inconsistently.
|
20 |
+
def convert(cls, predictor_outputs: Any, transform_data: Any, *args, **kwargs):
|
21 |
+
"""
|
22 |
+
Performs an horizontal flip on DensePose predictor outputs.
|
23 |
+
Does recursive lookup for base classes, so there's no need
|
24 |
+
for explicit registration for derived classes.
|
25 |
+
|
26 |
+
Args:
|
27 |
+
predictor_outputs: DensePose predictor output to be converted to BitMasks
|
28 |
+
transform_data: Anything useful for the flip
|
29 |
+
Return:
|
30 |
+
An instance of the same type as predictor_outputs
|
31 |
+
"""
|
32 |
+
return super(HFlipConverter, cls).convert(
|
33 |
+
predictor_outputs, transform_data, *args, **kwargs
|
34 |
+
)
|
densepose/converters/segm_to_mask.py
ADDED
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from typing import Any
|
4 |
+
import torch
|
5 |
+
from torch.nn import functional as F
|
6 |
+
|
7 |
+
from detectron2.structures import BitMasks, Boxes, BoxMode
|
8 |
+
|
9 |
+
from .base import IntTupleBox, make_int_box
|
10 |
+
from .to_mask import ImageSizeType
|
11 |
+
|
12 |
+
|
13 |
+
def resample_coarse_segm_tensor_to_bbox(coarse_segm: torch.Tensor, box_xywh_abs: IntTupleBox):
|
14 |
+
"""
|
15 |
+
Resample coarse segmentation tensor to the given
|
16 |
+
bounding box and derive labels for each pixel of the bounding box
|
17 |
+
|
18 |
+
Args:
|
19 |
+
coarse_segm: float tensor of shape [1, K, Hout, Wout]
|
20 |
+
box_xywh_abs (tuple of 4 int): bounding box given by its upper-left
|
21 |
+
corner coordinates, width (W) and height (H)
|
22 |
+
Return:
|
23 |
+
Labels for each pixel of the bounding box, a long tensor of size [1, H, W]
|
24 |
+
"""
|
25 |
+
x, y, w, h = box_xywh_abs
|
26 |
+
w = max(int(w), 1)
|
27 |
+
h = max(int(h), 1)
|
28 |
+
labels = F.interpolate(coarse_segm, (h, w), mode="bilinear", align_corners=False).argmax(dim=1)
|
29 |
+
return labels
|
30 |
+
|
31 |
+
|
32 |
+
def resample_fine_and_coarse_segm_tensors_to_bbox(
|
33 |
+
fine_segm: torch.Tensor, coarse_segm: torch.Tensor, box_xywh_abs: IntTupleBox
|
34 |
+
):
|
35 |
+
"""
|
36 |
+
Resample fine and coarse segmentation tensors to the given
|
37 |
+
bounding box and derive labels for each pixel of the bounding box
|
38 |
+
|
39 |
+
Args:
|
40 |
+
fine_segm: float tensor of shape [1, C, Hout, Wout]
|
41 |
+
coarse_segm: float tensor of shape [1, K, Hout, Wout]
|
42 |
+
box_xywh_abs (tuple of 4 int): bounding box given by its upper-left
|
43 |
+
corner coordinates, width (W) and height (H)
|
44 |
+
Return:
|
45 |
+
Labels for each pixel of the bounding box, a long tensor of size [1, H, W]
|
46 |
+
"""
|
47 |
+
x, y, w, h = box_xywh_abs
|
48 |
+
w = max(int(w), 1)
|
49 |
+
h = max(int(h), 1)
|
50 |
+
# coarse segmentation
|
51 |
+
coarse_segm_bbox = F.interpolate(
|
52 |
+
coarse_segm,
|
53 |
+
(h, w),
|
54 |
+
mode="bilinear",
|
55 |
+
align_corners=False,
|
56 |
+
).argmax(dim=1)
|
57 |
+
# combined coarse and fine segmentation
|
58 |
+
labels = (
|
59 |
+
F.interpolate(fine_segm, (h, w), mode="bilinear", align_corners=False).argmax(dim=1)
|
60 |
+
* (coarse_segm_bbox > 0).long()
|
61 |
+
)
|
62 |
+
return labels
|
63 |
+
|
64 |
+
|
65 |
+
def resample_fine_and_coarse_segm_to_bbox(predictor_output: Any, box_xywh_abs: IntTupleBox):
|
66 |
+
"""
|
67 |
+
Resample fine and coarse segmentation outputs from a predictor to the given
|
68 |
+
bounding box and derive labels for each pixel of the bounding box
|
69 |
+
|
70 |
+
Args:
|
71 |
+
predictor_output: DensePose predictor output that contains segmentation
|
72 |
+
results to be resampled
|
73 |
+
box_xywh_abs (tuple of 4 int): bounding box given by its upper-left
|
74 |
+
corner coordinates, width (W) and height (H)
|
75 |
+
Return:
|
76 |
+
Labels for each pixel of the bounding box, a long tensor of size [1, H, W]
|
77 |
+
"""
|
78 |
+
return resample_fine_and_coarse_segm_tensors_to_bbox(
|
79 |
+
predictor_output.fine_segm,
|
80 |
+
predictor_output.coarse_segm,
|
81 |
+
box_xywh_abs,
|
82 |
+
)
|
83 |
+
|
84 |
+
|
85 |
+
def predictor_output_with_coarse_segm_to_mask(
|
86 |
+
predictor_output: Any, boxes: Boxes, image_size_hw: ImageSizeType
|
87 |
+
) -> BitMasks:
|
88 |
+
"""
|
89 |
+
Convert predictor output with coarse and fine segmentation to a mask.
|
90 |
+
Assumes that predictor output has the following attributes:
|
91 |
+
- coarse_segm (tensor of size [N, D, H, W]): coarse segmentation
|
92 |
+
unnormalized scores for N instances; D is the number of coarse
|
93 |
+
segmentation labels, H and W is the resolution of the estimate
|
94 |
+
|
95 |
+
Args:
|
96 |
+
predictor_output: DensePose predictor output to be converted to mask
|
97 |
+
boxes (Boxes): bounding boxes that correspond to the DensePose
|
98 |
+
predictor outputs
|
99 |
+
image_size_hw (tuple [int, int]): image height Himg and width Wimg
|
100 |
+
Return:
|
101 |
+
BitMasks that contain a bool tensor of size [N, Himg, Wimg] with
|
102 |
+
a mask of the size of the image for each instance
|
103 |
+
"""
|
104 |
+
H, W = image_size_hw
|
105 |
+
boxes_xyxy_abs = boxes.tensor.clone()
|
106 |
+
boxes_xywh_abs = BoxMode.convert(boxes_xyxy_abs, BoxMode.XYXY_ABS, BoxMode.XYWH_ABS)
|
107 |
+
N = len(boxes_xywh_abs)
|
108 |
+
masks = torch.zeros((N, H, W), dtype=torch.bool, device=boxes.tensor.device)
|
109 |
+
for i in range(len(boxes_xywh_abs)):
|
110 |
+
box_xywh = make_int_box(boxes_xywh_abs[i])
|
111 |
+
box_mask = resample_coarse_segm_tensor_to_bbox(predictor_output[i].coarse_segm, box_xywh)
|
112 |
+
x, y, w, h = box_xywh
|
113 |
+
masks[i, y : y + h, x : x + w] = box_mask
|
114 |
+
|
115 |
+
return BitMasks(masks)
|
116 |
+
|
117 |
+
|
118 |
+
def predictor_output_with_fine_and_coarse_segm_to_mask(
|
119 |
+
predictor_output: Any, boxes: Boxes, image_size_hw: ImageSizeType
|
120 |
+
) -> BitMasks:
|
121 |
+
"""
|
122 |
+
Convert predictor output with coarse and fine segmentation to a mask.
|
123 |
+
Assumes that predictor output has the following attributes:
|
124 |
+
- coarse_segm (tensor of size [N, D, H, W]): coarse segmentation
|
125 |
+
unnormalized scores for N instances; D is the number of coarse
|
126 |
+
segmentation labels, H and W is the resolution of the estimate
|
127 |
+
- fine_segm (tensor of size [N, C, H, W]): fine segmentation
|
128 |
+
unnormalized scores for N instances; C is the number of fine
|
129 |
+
segmentation labels, H and W is the resolution of the estimate
|
130 |
+
|
131 |
+
Args:
|
132 |
+
predictor_output: DensePose predictor output to be converted to mask
|
133 |
+
boxes (Boxes): bounding boxes that correspond to the DensePose
|
134 |
+
predictor outputs
|
135 |
+
image_size_hw (tuple [int, int]): image height Himg and width Wimg
|
136 |
+
Return:
|
137 |
+
BitMasks that contain a bool tensor of size [N, Himg, Wimg] with
|
138 |
+
a mask of the size of the image for each instance
|
139 |
+
"""
|
140 |
+
H, W = image_size_hw
|
141 |
+
boxes_xyxy_abs = boxes.tensor.clone()
|
142 |
+
boxes_xywh_abs = BoxMode.convert(boxes_xyxy_abs, BoxMode.XYXY_ABS, BoxMode.XYWH_ABS)
|
143 |
+
N = len(boxes_xywh_abs)
|
144 |
+
masks = torch.zeros((N, H, W), dtype=torch.bool, device=boxes.tensor.device)
|
145 |
+
for i in range(len(boxes_xywh_abs)):
|
146 |
+
box_xywh = make_int_box(boxes_xywh_abs[i])
|
147 |
+
labels_i = resample_fine_and_coarse_segm_to_bbox(predictor_output[i], box_xywh)
|
148 |
+
x, y, w, h = box_xywh
|
149 |
+
masks[i, y : y + h, x : x + w] = labels_i > 0
|
150 |
+
return BitMasks(masks)
|
densepose/converters/to_chart_result.py
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from typing import Any
|
4 |
+
|
5 |
+
from detectron2.structures import Boxes
|
6 |
+
|
7 |
+
from ..structures import DensePoseChartResult, DensePoseChartResultWithConfidences
|
8 |
+
from .base import BaseConverter
|
9 |
+
|
10 |
+
|
11 |
+
class ToChartResultConverter(BaseConverter):
|
12 |
+
"""
|
13 |
+
Converts various DensePose predictor outputs to DensePose results.
|
14 |
+
Each DensePose predictor output type has to register its convertion strategy.
|
15 |
+
"""
|
16 |
+
|
17 |
+
registry = {}
|
18 |
+
dst_type = DensePoseChartResult
|
19 |
+
|
20 |
+
@classmethod
|
21 |
+
# pyre-fixme[14]: `convert` overrides method defined in `BaseConverter`
|
22 |
+
# inconsistently.
|
23 |
+
def convert(cls, predictor_outputs: Any, boxes: Boxes, *args, **kwargs) -> DensePoseChartResult:
|
24 |
+
"""
|
25 |
+
Convert DensePose predictor outputs to DensePoseResult using some registered
|
26 |
+
converter. Does recursive lookup for base classes, so there's no need
|
27 |
+
for explicit registration for derived classes.
|
28 |
+
|
29 |
+
Args:
|
30 |
+
densepose_predictor_outputs: DensePose predictor output to be
|
31 |
+
converted to BitMasks
|
32 |
+
boxes (Boxes): bounding boxes that correspond to the DensePose
|
33 |
+
predictor outputs
|
34 |
+
Return:
|
35 |
+
An instance of DensePoseResult. If no suitable converter was found, raises KeyError
|
36 |
+
"""
|
37 |
+
return super(ToChartResultConverter, cls).convert(predictor_outputs, boxes, *args, **kwargs)
|
38 |
+
|
39 |
+
|
40 |
+
class ToChartResultConverterWithConfidences(BaseConverter):
|
41 |
+
"""
|
42 |
+
Converts various DensePose predictor outputs to DensePose results.
|
43 |
+
Each DensePose predictor output type has to register its convertion strategy.
|
44 |
+
"""
|
45 |
+
|
46 |
+
registry = {}
|
47 |
+
dst_type = DensePoseChartResultWithConfidences
|
48 |
+
|
49 |
+
@classmethod
|
50 |
+
# pyre-fixme[14]: `convert` overrides method defined in `BaseConverter`
|
51 |
+
# inconsistently.
|
52 |
+
def convert(
|
53 |
+
cls, predictor_outputs: Any, boxes: Boxes, *args, **kwargs
|
54 |
+
) -> DensePoseChartResultWithConfidences:
|
55 |
+
"""
|
56 |
+
Convert DensePose predictor outputs to DensePoseResult with confidences
|
57 |
+
using some registered converter. Does recursive lookup for base classes,
|
58 |
+
so there's no need for explicit registration for derived classes.
|
59 |
+
|
60 |
+
Args:
|
61 |
+
densepose_predictor_outputs: DensePose predictor output with confidences
|
62 |
+
to be converted to BitMasks
|
63 |
+
boxes (Boxes): bounding boxes that correspond to the DensePose
|
64 |
+
predictor outputs
|
65 |
+
Return:
|
66 |
+
An instance of DensePoseResult. If no suitable converter was found, raises KeyError
|
67 |
+
"""
|
68 |
+
return super(ToChartResultConverterWithConfidences, cls).convert(
|
69 |
+
predictor_outputs, boxes, *args, **kwargs
|
70 |
+
)
|
densepose/converters/to_mask.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from typing import Any, Tuple
|
4 |
+
|
5 |
+
from detectron2.structures import BitMasks, Boxes
|
6 |
+
|
7 |
+
from .base import BaseConverter
|
8 |
+
|
9 |
+
ImageSizeType = Tuple[int, int]
|
10 |
+
|
11 |
+
|
12 |
+
class ToMaskConverter(BaseConverter):
|
13 |
+
"""
|
14 |
+
Converts various DensePose predictor outputs to masks
|
15 |
+
in bit mask format (see `BitMasks`). Each DensePose predictor output type
|
16 |
+
has to register its convertion strategy.
|
17 |
+
"""
|
18 |
+
|
19 |
+
registry = {}
|
20 |
+
dst_type = BitMasks
|
21 |
+
|
22 |
+
@classmethod
|
23 |
+
# pyre-fixme[14]: `convert` overrides method defined in `BaseConverter`
|
24 |
+
# inconsistently.
|
25 |
+
def convert(
|
26 |
+
cls,
|
27 |
+
densepose_predictor_outputs: Any,
|
28 |
+
boxes: Boxes,
|
29 |
+
image_size_hw: ImageSizeType,
|
30 |
+
*args,
|
31 |
+
**kwargs
|
32 |
+
) -> BitMasks:
|
33 |
+
"""
|
34 |
+
Convert DensePose predictor outputs to BitMasks using some registered
|
35 |
+
converter. Does recursive lookup for base classes, so there's no need
|
36 |
+
for explicit registration for derived classes.
|
37 |
+
|
38 |
+
Args:
|
39 |
+
densepose_predictor_outputs: DensePose predictor output to be
|
40 |
+
converted to BitMasks
|
41 |
+
boxes (Boxes): bounding boxes that correspond to the DensePose
|
42 |
+
predictor outputs
|
43 |
+
image_size_hw (tuple [int, int]): image height and width
|
44 |
+
Return:
|
45 |
+
An instance of `BitMasks`. If no suitable converter was found, raises KeyError
|
46 |
+
"""
|
47 |
+
return super(ToMaskConverter, cls).convert(
|
48 |
+
densepose_predictor_outputs, boxes, image_size_hw, *args, **kwargs
|
49 |
+
)
|
densepose/data/__init__.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from .meshes import builtin
|
4 |
+
from .build import (
|
5 |
+
build_detection_test_loader,
|
6 |
+
build_detection_train_loader,
|
7 |
+
build_combined_loader,
|
8 |
+
build_frame_selector,
|
9 |
+
build_inference_based_loaders,
|
10 |
+
has_inference_based_loaders,
|
11 |
+
BootstrapDatasetFactoryCatalog,
|
12 |
+
)
|
13 |
+
from .combined_loader import CombinedDataLoader
|
14 |
+
from .dataset_mapper import DatasetMapper
|
15 |
+
from .inference_based_loader import InferenceBasedLoader, ScoreBasedFilter
|
16 |
+
from .image_list_dataset import ImageListDataset
|
17 |
+
from .utils import is_relative_local_path, maybe_prepend_base_path
|
18 |
+
|
19 |
+
# ensure the builtin datasets are registered
|
20 |
+
from . import datasets
|
21 |
+
|
22 |
+
# ensure the bootstrap datasets builders are registered
|
23 |
+
from . import build
|
24 |
+
|
25 |
+
__all__ = [k for k in globals().keys() if not k.startswith("_")]
|
densepose/data/build.py
ADDED
@@ -0,0 +1,736 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
import itertools
|
4 |
+
import logging
|
5 |
+
import numpy as np
|
6 |
+
from collections import UserDict, defaultdict
|
7 |
+
from dataclasses import dataclass
|
8 |
+
from typing import Any, Callable, Collection, Dict, Iterable, List, Optional, Sequence, Tuple
|
9 |
+
import torch
|
10 |
+
from torch.utils.data.dataset import Dataset
|
11 |
+
|
12 |
+
from detectron2.config import CfgNode
|
13 |
+
from detectron2.data.build import build_detection_test_loader as d2_build_detection_test_loader
|
14 |
+
from detectron2.data.build import build_detection_train_loader as d2_build_detection_train_loader
|
15 |
+
from detectron2.data.build import (
|
16 |
+
load_proposals_into_dataset,
|
17 |
+
print_instances_class_histogram,
|
18 |
+
trivial_batch_collator,
|
19 |
+
worker_init_reset_seed,
|
20 |
+
)
|
21 |
+
from detectron2.data.catalog import DatasetCatalog, Metadata, MetadataCatalog
|
22 |
+
from detectron2.data.samplers import TrainingSampler
|
23 |
+
from detectron2.utils.comm import get_world_size
|
24 |
+
|
25 |
+
from densepose.config import get_bootstrap_dataset_config
|
26 |
+
from densepose.modeling import build_densepose_embedder
|
27 |
+
|
28 |
+
from .combined_loader import CombinedDataLoader, Loader
|
29 |
+
from .dataset_mapper import DatasetMapper
|
30 |
+
from .datasets.coco import DENSEPOSE_CSE_KEYS_WITHOUT_MASK, DENSEPOSE_IUV_KEYS_WITHOUT_MASK
|
31 |
+
from .datasets.dataset_type import DatasetType
|
32 |
+
from .inference_based_loader import InferenceBasedLoader, ScoreBasedFilter
|
33 |
+
from .samplers import (
|
34 |
+
DensePoseConfidenceBasedSampler,
|
35 |
+
DensePoseCSEConfidenceBasedSampler,
|
36 |
+
DensePoseCSEUniformSampler,
|
37 |
+
DensePoseUniformSampler,
|
38 |
+
MaskFromDensePoseSampler,
|
39 |
+
PredictionToGroundTruthSampler,
|
40 |
+
)
|
41 |
+
from .transform import ImageResizeTransform
|
42 |
+
from .utils import get_category_to_class_mapping, get_class_to_mesh_name_mapping
|
43 |
+
from .video import (
|
44 |
+
FirstKFramesSelector,
|
45 |
+
FrameSelectionStrategy,
|
46 |
+
LastKFramesSelector,
|
47 |
+
RandomKFramesSelector,
|
48 |
+
VideoKeyframeDataset,
|
49 |
+
video_list_from_file,
|
50 |
+
)
|
51 |
+
|
52 |
+
__all__ = ["build_detection_train_loader", "build_detection_test_loader"]
|
53 |
+
|
54 |
+
|
55 |
+
Instance = Dict[str, Any]
|
56 |
+
InstancePredicate = Callable[[Instance], bool]
|
57 |
+
|
58 |
+
|
59 |
+
def _compute_num_images_per_worker(cfg: CfgNode) -> int:
|
60 |
+
num_workers = get_world_size()
|
61 |
+
images_per_batch = cfg.SOLVER.IMS_PER_BATCH
|
62 |
+
assert (
|
63 |
+
images_per_batch % num_workers == 0
|
64 |
+
), "SOLVER.IMS_PER_BATCH ({}) must be divisible by the number of workers ({}).".format(
|
65 |
+
images_per_batch, num_workers
|
66 |
+
)
|
67 |
+
assert (
|
68 |
+
images_per_batch >= num_workers
|
69 |
+
), "SOLVER.IMS_PER_BATCH ({}) must be larger than the number of workers ({}).".format(
|
70 |
+
images_per_batch, num_workers
|
71 |
+
)
|
72 |
+
images_per_worker = images_per_batch // num_workers
|
73 |
+
return images_per_worker
|
74 |
+
|
75 |
+
|
76 |
+
def _map_category_id_to_contiguous_id(dataset_name: str, dataset_dicts: Iterable[Instance]) -> None:
|
77 |
+
meta = MetadataCatalog.get(dataset_name)
|
78 |
+
for dataset_dict in dataset_dicts:
|
79 |
+
for ann in dataset_dict["annotations"]:
|
80 |
+
ann["category_id"] = meta.thing_dataset_id_to_contiguous_id[ann["category_id"]]
|
81 |
+
|
82 |
+
|
83 |
+
@dataclass
|
84 |
+
class _DatasetCategory:
|
85 |
+
"""
|
86 |
+
Class representing category data in a dataset:
|
87 |
+
- id: category ID, as specified in the dataset annotations file
|
88 |
+
- name: category name, as specified in the dataset annotations file
|
89 |
+
- mapped_id: category ID after applying category maps (DATASETS.CATEGORY_MAPS config option)
|
90 |
+
- mapped_name: category name after applying category maps
|
91 |
+
- dataset_name: dataset in which the category is defined
|
92 |
+
|
93 |
+
For example, when training models in a class-agnostic manner, one could take LVIS 1.0
|
94 |
+
dataset and map the animal categories to the same category as human data from COCO:
|
95 |
+
id = 225
|
96 |
+
name = "cat"
|
97 |
+
mapped_id = 1
|
98 |
+
mapped_name = "person"
|
99 |
+
dataset_name = "lvis_v1_animals_dp_train"
|
100 |
+
"""
|
101 |
+
|
102 |
+
id: int
|
103 |
+
name: str
|
104 |
+
mapped_id: int
|
105 |
+
mapped_name: str
|
106 |
+
dataset_name: str
|
107 |
+
|
108 |
+
|
109 |
+
_MergedCategoriesT = Dict[int, List[_DatasetCategory]]
|
110 |
+
|
111 |
+
|
112 |
+
def _add_category_id_to_contiguous_id_maps_to_metadata(
|
113 |
+
merged_categories: _MergedCategoriesT,
|
114 |
+
) -> None:
|
115 |
+
merged_categories_per_dataset = {}
|
116 |
+
for contiguous_cat_id, cat_id in enumerate(sorted(merged_categories.keys())):
|
117 |
+
for cat in merged_categories[cat_id]:
|
118 |
+
if cat.dataset_name not in merged_categories_per_dataset:
|
119 |
+
merged_categories_per_dataset[cat.dataset_name] = defaultdict(list)
|
120 |
+
merged_categories_per_dataset[cat.dataset_name][cat_id].append(
|
121 |
+
(
|
122 |
+
contiguous_cat_id,
|
123 |
+
cat,
|
124 |
+
)
|
125 |
+
)
|
126 |
+
|
127 |
+
logger = logging.getLogger(__name__)
|
128 |
+
for dataset_name, merged_categories in merged_categories_per_dataset.items():
|
129 |
+
meta = MetadataCatalog.get(dataset_name)
|
130 |
+
if not hasattr(meta, "thing_classes"):
|
131 |
+
meta.thing_classes = []
|
132 |
+
meta.thing_dataset_id_to_contiguous_id = {}
|
133 |
+
meta.thing_dataset_id_to_merged_id = {}
|
134 |
+
else:
|
135 |
+
meta.thing_classes.clear()
|
136 |
+
meta.thing_dataset_id_to_contiguous_id.clear()
|
137 |
+
meta.thing_dataset_id_to_merged_id.clear()
|
138 |
+
logger.info(f"Dataset {dataset_name}: category ID to contiguous ID mapping:")
|
139 |
+
for _cat_id, categories in sorted(merged_categories.items()):
|
140 |
+
added_to_thing_classes = False
|
141 |
+
for contiguous_cat_id, cat in categories:
|
142 |
+
if not added_to_thing_classes:
|
143 |
+
meta.thing_classes.append(cat.mapped_name)
|
144 |
+
added_to_thing_classes = True
|
145 |
+
meta.thing_dataset_id_to_contiguous_id[cat.id] = contiguous_cat_id
|
146 |
+
meta.thing_dataset_id_to_merged_id[cat.id] = cat.mapped_id
|
147 |
+
logger.info(f"{cat.id} ({cat.name}) -> {contiguous_cat_id}")
|
148 |
+
|
149 |
+
|
150 |
+
def _maybe_create_general_keep_instance_predicate(cfg: CfgNode) -> Optional[InstancePredicate]:
|
151 |
+
def has_annotations(instance: Instance) -> bool:
|
152 |
+
return "annotations" in instance
|
153 |
+
|
154 |
+
def has_only_crowd_anotations(instance: Instance) -> bool:
|
155 |
+
for ann in instance["annotations"]:
|
156 |
+
if ann.get("is_crowd", 0) == 0:
|
157 |
+
return False
|
158 |
+
return True
|
159 |
+
|
160 |
+
def general_keep_instance_predicate(instance: Instance) -> bool:
|
161 |
+
return has_annotations(instance) and not has_only_crowd_anotations(instance)
|
162 |
+
|
163 |
+
if not cfg.DATALOADER.FILTER_EMPTY_ANNOTATIONS:
|
164 |
+
return None
|
165 |
+
return general_keep_instance_predicate
|
166 |
+
|
167 |
+
|
168 |
+
def _maybe_create_keypoints_keep_instance_predicate(cfg: CfgNode) -> Optional[InstancePredicate]:
|
169 |
+
|
170 |
+
min_num_keypoints = cfg.MODEL.ROI_KEYPOINT_HEAD.MIN_KEYPOINTS_PER_IMAGE
|
171 |
+
|
172 |
+
def has_sufficient_num_keypoints(instance: Instance) -> bool:
|
173 |
+
num_kpts = sum(
|
174 |
+
(np.array(ann["keypoints"][2::3]) > 0).sum()
|
175 |
+
for ann in instance["annotations"]
|
176 |
+
if "keypoints" in ann
|
177 |
+
)
|
178 |
+
return num_kpts >= min_num_keypoints
|
179 |
+
|
180 |
+
if cfg.MODEL.KEYPOINT_ON and (min_num_keypoints > 0):
|
181 |
+
return has_sufficient_num_keypoints
|
182 |
+
return None
|
183 |
+
|
184 |
+
|
185 |
+
def _maybe_create_mask_keep_instance_predicate(cfg: CfgNode) -> Optional[InstancePredicate]:
|
186 |
+
if not cfg.MODEL.MASK_ON:
|
187 |
+
return None
|
188 |
+
|
189 |
+
def has_mask_annotations(instance: Instance) -> bool:
|
190 |
+
return any("segmentation" in ann for ann in instance["annotations"])
|
191 |
+
|
192 |
+
return has_mask_annotations
|
193 |
+
|
194 |
+
|
195 |
+
def _maybe_create_densepose_keep_instance_predicate(cfg: CfgNode) -> Optional[InstancePredicate]:
|
196 |
+
if not cfg.MODEL.DENSEPOSE_ON:
|
197 |
+
return None
|
198 |
+
|
199 |
+
use_masks = cfg.MODEL.ROI_DENSEPOSE_HEAD.COARSE_SEGM_TRAINED_BY_MASKS
|
200 |
+
|
201 |
+
def has_densepose_annotations(instance: Instance) -> bool:
|
202 |
+
for ann in instance["annotations"]:
|
203 |
+
if all(key in ann for key in DENSEPOSE_IUV_KEYS_WITHOUT_MASK) or all(
|
204 |
+
key in ann for key in DENSEPOSE_CSE_KEYS_WITHOUT_MASK
|
205 |
+
):
|
206 |
+
return True
|
207 |
+
if use_masks and "segmentation" in ann:
|
208 |
+
return True
|
209 |
+
return False
|
210 |
+
|
211 |
+
return has_densepose_annotations
|
212 |
+
|
213 |
+
|
214 |
+
def _maybe_create_specific_keep_instance_predicate(cfg: CfgNode) -> Optional[InstancePredicate]:
|
215 |
+
specific_predicate_creators = [
|
216 |
+
_maybe_create_keypoints_keep_instance_predicate,
|
217 |
+
_maybe_create_mask_keep_instance_predicate,
|
218 |
+
_maybe_create_densepose_keep_instance_predicate,
|
219 |
+
]
|
220 |
+
predicates = [creator(cfg) for creator in specific_predicate_creators]
|
221 |
+
predicates = [p for p in predicates if p is not None]
|
222 |
+
if not predicates:
|
223 |
+
return None
|
224 |
+
|
225 |
+
def combined_predicate(instance: Instance) -> bool:
|
226 |
+
return any(p(instance) for p in predicates)
|
227 |
+
|
228 |
+
return combined_predicate
|
229 |
+
|
230 |
+
|
231 |
+
def _get_train_keep_instance_predicate(cfg: CfgNode):
|
232 |
+
general_keep_predicate = _maybe_create_general_keep_instance_predicate(cfg)
|
233 |
+
combined_specific_keep_predicate = _maybe_create_specific_keep_instance_predicate(cfg)
|
234 |
+
|
235 |
+
def combined_general_specific_keep_predicate(instance: Instance) -> bool:
|
236 |
+
return general_keep_predicate(instance) and combined_specific_keep_predicate(instance)
|
237 |
+
|
238 |
+
if (general_keep_predicate is None) and (combined_specific_keep_predicate is None):
|
239 |
+
return None
|
240 |
+
if general_keep_predicate is None:
|
241 |
+
return combined_specific_keep_predicate
|
242 |
+
if combined_specific_keep_predicate is None:
|
243 |
+
return general_keep_predicate
|
244 |
+
return combined_general_specific_keep_predicate
|
245 |
+
|
246 |
+
|
247 |
+
def _get_test_keep_instance_predicate(cfg: CfgNode):
|
248 |
+
general_keep_predicate = _maybe_create_general_keep_instance_predicate(cfg)
|
249 |
+
return general_keep_predicate
|
250 |
+
|
251 |
+
|
252 |
+
def _maybe_filter_and_map_categories(
|
253 |
+
dataset_name: str, dataset_dicts: List[Instance]
|
254 |
+
) -> List[Instance]:
|
255 |
+
meta = MetadataCatalog.get(dataset_name)
|
256 |
+
category_id_map = meta.thing_dataset_id_to_contiguous_id
|
257 |
+
filtered_dataset_dicts = []
|
258 |
+
for dataset_dict in dataset_dicts:
|
259 |
+
anns = []
|
260 |
+
for ann in dataset_dict["annotations"]:
|
261 |
+
cat_id = ann["category_id"]
|
262 |
+
if cat_id not in category_id_map:
|
263 |
+
continue
|
264 |
+
ann["category_id"] = category_id_map[cat_id]
|
265 |
+
anns.append(ann)
|
266 |
+
dataset_dict["annotations"] = anns
|
267 |
+
filtered_dataset_dicts.append(dataset_dict)
|
268 |
+
return filtered_dataset_dicts
|
269 |
+
|
270 |
+
|
271 |
+
def _add_category_whitelists_to_metadata(cfg: CfgNode) -> None:
|
272 |
+
for dataset_name, whitelisted_cat_ids in cfg.DATASETS.WHITELISTED_CATEGORIES.items():
|
273 |
+
meta = MetadataCatalog.get(dataset_name)
|
274 |
+
meta.whitelisted_categories = whitelisted_cat_ids
|
275 |
+
logger = logging.getLogger(__name__)
|
276 |
+
logger.info(
|
277 |
+
"Whitelisted categories for dataset {}: {}".format(
|
278 |
+
dataset_name, meta.whitelisted_categories
|
279 |
+
)
|
280 |
+
)
|
281 |
+
|
282 |
+
|
283 |
+
def _add_category_maps_to_metadata(cfg: CfgNode) -> None:
|
284 |
+
for dataset_name, category_map in cfg.DATASETS.CATEGORY_MAPS.items():
|
285 |
+
category_map = {
|
286 |
+
int(cat_id_src): int(cat_id_dst) for cat_id_src, cat_id_dst in category_map.items()
|
287 |
+
}
|
288 |
+
meta = MetadataCatalog.get(dataset_name)
|
289 |
+
meta.category_map = category_map
|
290 |
+
logger = logging.getLogger(__name__)
|
291 |
+
logger.info("Category maps for dataset {}: {}".format(dataset_name, meta.category_map))
|
292 |
+
|
293 |
+
|
294 |
+
def _add_category_info_to_bootstrapping_metadata(dataset_name: str, dataset_cfg: CfgNode) -> None:
|
295 |
+
meta = MetadataCatalog.get(dataset_name)
|
296 |
+
meta.category_to_class_mapping = get_category_to_class_mapping(dataset_cfg)
|
297 |
+
meta.categories = dataset_cfg.CATEGORIES
|
298 |
+
meta.max_count_per_category = dataset_cfg.MAX_COUNT_PER_CATEGORY
|
299 |
+
logger = logging.getLogger(__name__)
|
300 |
+
logger.info(
|
301 |
+
"Category to class mapping for dataset {}: {}".format(
|
302 |
+
dataset_name, meta.category_to_class_mapping
|
303 |
+
)
|
304 |
+
)
|
305 |
+
|
306 |
+
|
307 |
+
def _maybe_add_class_to_mesh_name_map_to_metadata(dataset_names: List[str], cfg: CfgNode) -> None:
|
308 |
+
for dataset_name in dataset_names:
|
309 |
+
meta = MetadataCatalog.get(dataset_name)
|
310 |
+
if not hasattr(meta, "class_to_mesh_name"):
|
311 |
+
meta.class_to_mesh_name = get_class_to_mesh_name_mapping(cfg)
|
312 |
+
|
313 |
+
|
314 |
+
def _merge_categories(dataset_names: Collection[str]) -> _MergedCategoriesT:
|
315 |
+
merged_categories = defaultdict(list)
|
316 |
+
category_names = {}
|
317 |
+
for dataset_name in dataset_names:
|
318 |
+
meta = MetadataCatalog.get(dataset_name)
|
319 |
+
whitelisted_categories = meta.get("whitelisted_categories")
|
320 |
+
category_map = meta.get("category_map", {})
|
321 |
+
cat_ids = (
|
322 |
+
whitelisted_categories if whitelisted_categories is not None else meta.categories.keys()
|
323 |
+
)
|
324 |
+
for cat_id in cat_ids:
|
325 |
+
cat_name = meta.categories[cat_id]
|
326 |
+
cat_id_mapped = category_map.get(cat_id, cat_id)
|
327 |
+
if cat_id_mapped == cat_id or cat_id_mapped in cat_ids:
|
328 |
+
category_names[cat_id] = cat_name
|
329 |
+
else:
|
330 |
+
category_names[cat_id] = str(cat_id_mapped)
|
331 |
+
# assign temporary mapped category name, this name can be changed
|
332 |
+
# during the second pass, since mapped ID can correspond to a category
|
333 |
+
# from a different dataset
|
334 |
+
cat_name_mapped = meta.categories[cat_id_mapped]
|
335 |
+
merged_categories[cat_id_mapped].append(
|
336 |
+
_DatasetCategory(
|
337 |
+
id=cat_id,
|
338 |
+
name=cat_name,
|
339 |
+
mapped_id=cat_id_mapped,
|
340 |
+
mapped_name=cat_name_mapped,
|
341 |
+
dataset_name=dataset_name,
|
342 |
+
)
|
343 |
+
)
|
344 |
+
# second pass to assign proper mapped category names
|
345 |
+
for cat_id, categories in merged_categories.items():
|
346 |
+
for cat in categories:
|
347 |
+
if cat_id in category_names and cat.mapped_name != category_names[cat_id]:
|
348 |
+
cat.mapped_name = category_names[cat_id]
|
349 |
+
|
350 |
+
return merged_categories
|
351 |
+
|
352 |
+
|
353 |
+
def _warn_if_merged_different_categories(merged_categories: _MergedCategoriesT) -> None:
|
354 |
+
logger = logging.getLogger(__name__)
|
355 |
+
for cat_id in merged_categories:
|
356 |
+
merged_categories_i = merged_categories[cat_id]
|
357 |
+
first_cat_name = merged_categories_i[0].name
|
358 |
+
if len(merged_categories_i) > 1 and not all(
|
359 |
+
cat.name == first_cat_name for cat in merged_categories_i[1:]
|
360 |
+
):
|
361 |
+
cat_summary_str = ", ".join(
|
362 |
+
[f"{cat.id} ({cat.name}) from {cat.dataset_name}" for cat in merged_categories_i]
|
363 |
+
)
|
364 |
+
logger.warning(
|
365 |
+
f"Merged category {cat_id} corresponds to the following categories: "
|
366 |
+
f"{cat_summary_str}"
|
367 |
+
)
|
368 |
+
|
369 |
+
|
370 |
+
def combine_detection_dataset_dicts(
|
371 |
+
dataset_names: Collection[str],
|
372 |
+
keep_instance_predicate: Optional[InstancePredicate] = None,
|
373 |
+
proposal_files: Optional[Collection[str]] = None,
|
374 |
+
) -> List[Instance]:
|
375 |
+
"""
|
376 |
+
Load and prepare dataset dicts for training / testing
|
377 |
+
|
378 |
+
Args:
|
379 |
+
dataset_names (Collection[str]): a list of dataset names
|
380 |
+
keep_instance_predicate (Callable: Dict[str, Any] -> bool): predicate
|
381 |
+
applied to instance dicts which defines whether to keep the instance
|
382 |
+
proposal_files (Collection[str]): if given, a list of object proposal files
|
383 |
+
that match each dataset in `dataset_names`.
|
384 |
+
"""
|
385 |
+
assert len(dataset_names)
|
386 |
+
if proposal_files is None:
|
387 |
+
proposal_files = [None] * len(dataset_names)
|
388 |
+
assert len(dataset_names) == len(proposal_files)
|
389 |
+
# load datasets and metadata
|
390 |
+
dataset_name_to_dicts = {}
|
391 |
+
for dataset_name in dataset_names:
|
392 |
+
dataset_name_to_dicts[dataset_name] = DatasetCatalog.get(dataset_name)
|
393 |
+
assert len(dataset_name_to_dicts), f"Dataset '{dataset_name}' is empty!"
|
394 |
+
# merge categories, requires category metadata to be loaded
|
395 |
+
# cat_id -> [(orig_cat_id, cat_name, dataset_name)]
|
396 |
+
merged_categories = _merge_categories(dataset_names)
|
397 |
+
_warn_if_merged_different_categories(merged_categories)
|
398 |
+
merged_category_names = [
|
399 |
+
merged_categories[cat_id][0].mapped_name for cat_id in sorted(merged_categories)
|
400 |
+
]
|
401 |
+
# map to contiguous category IDs
|
402 |
+
_add_category_id_to_contiguous_id_maps_to_metadata(merged_categories)
|
403 |
+
# load annotations and dataset metadata
|
404 |
+
for dataset_name, proposal_file in zip(dataset_names, proposal_files):
|
405 |
+
dataset_dicts = dataset_name_to_dicts[dataset_name]
|
406 |
+
assert len(dataset_dicts), f"Dataset '{dataset_name}' is empty!"
|
407 |
+
if proposal_file is not None:
|
408 |
+
dataset_dicts = load_proposals_into_dataset(dataset_dicts, proposal_file)
|
409 |
+
dataset_dicts = _maybe_filter_and_map_categories(dataset_name, dataset_dicts)
|
410 |
+
print_instances_class_histogram(dataset_dicts, merged_category_names)
|
411 |
+
dataset_name_to_dicts[dataset_name] = dataset_dicts
|
412 |
+
|
413 |
+
if keep_instance_predicate is not None:
|
414 |
+
all_datasets_dicts_plain = [
|
415 |
+
d
|
416 |
+
for d in itertools.chain.from_iterable(dataset_name_to_dicts.values())
|
417 |
+
if keep_instance_predicate(d)
|
418 |
+
]
|
419 |
+
else:
|
420 |
+
all_datasets_dicts_plain = list(
|
421 |
+
itertools.chain.from_iterable(dataset_name_to_dicts.values())
|
422 |
+
)
|
423 |
+
return all_datasets_dicts_plain
|
424 |
+
|
425 |
+
|
426 |
+
def build_detection_train_loader(cfg: CfgNode, mapper=None):
|
427 |
+
"""
|
428 |
+
A data loader is created in a way similar to that of Detectron2.
|
429 |
+
The main differences are:
|
430 |
+
- it allows to combine datasets with different but compatible object category sets
|
431 |
+
|
432 |
+
The data loader is created by the following steps:
|
433 |
+
1. Use the dataset names in config to query :class:`DatasetCatalog`, and obtain a list of dicts.
|
434 |
+
2. Start workers to work on the dicts. Each worker will:
|
435 |
+
* Map each metadata dict into another format to be consumed by the model.
|
436 |
+
* Batch them by simply putting dicts into a list.
|
437 |
+
The batched ``list[mapped_dict]`` is what this dataloader will return.
|
438 |
+
|
439 |
+
Args:
|
440 |
+
cfg (CfgNode): the config
|
441 |
+
mapper (callable): a callable which takes a sample (dict) from dataset and
|
442 |
+
returns the format to be consumed by the model.
|
443 |
+
By default it will be `DatasetMapper(cfg, True)`.
|
444 |
+
|
445 |
+
Returns:
|
446 |
+
an infinite iterator of training data
|
447 |
+
"""
|
448 |
+
|
449 |
+
_add_category_whitelists_to_metadata(cfg)
|
450 |
+
_add_category_maps_to_metadata(cfg)
|
451 |
+
_maybe_add_class_to_mesh_name_map_to_metadata(cfg.DATASETS.TRAIN, cfg)
|
452 |
+
dataset_dicts = combine_detection_dataset_dicts(
|
453 |
+
cfg.DATASETS.TRAIN,
|
454 |
+
keep_instance_predicate=_get_train_keep_instance_predicate(cfg),
|
455 |
+
proposal_files=cfg.DATASETS.PROPOSAL_FILES_TRAIN if cfg.MODEL.LOAD_PROPOSALS else None,
|
456 |
+
)
|
457 |
+
if mapper is None:
|
458 |
+
mapper = DatasetMapper(cfg, True)
|
459 |
+
return d2_build_detection_train_loader(cfg, dataset=dataset_dicts, mapper=mapper)
|
460 |
+
|
461 |
+
|
462 |
+
def build_detection_test_loader(cfg, dataset_name, mapper=None):
|
463 |
+
"""
|
464 |
+
Similar to `build_detection_train_loader`.
|
465 |
+
But this function uses the given `dataset_name` argument (instead of the names in cfg),
|
466 |
+
and uses batch size 1.
|
467 |
+
|
468 |
+
Args:
|
469 |
+
cfg: a detectron2 CfgNode
|
470 |
+
dataset_name (str): a name of the dataset that's available in the DatasetCatalog
|
471 |
+
mapper (callable): a callable which takes a sample (dict) from dataset
|
472 |
+
and returns the format to be consumed by the model.
|
473 |
+
By default it will be `DatasetMapper(cfg, False)`.
|
474 |
+
|
475 |
+
Returns:
|
476 |
+
DataLoader: a torch DataLoader, that loads the given detection
|
477 |
+
dataset, with test-time transformation and batching.
|
478 |
+
"""
|
479 |
+
_add_category_whitelists_to_metadata(cfg)
|
480 |
+
_add_category_maps_to_metadata(cfg)
|
481 |
+
_maybe_add_class_to_mesh_name_map_to_metadata([dataset_name], cfg)
|
482 |
+
dataset_dicts = combine_detection_dataset_dicts(
|
483 |
+
[dataset_name],
|
484 |
+
keep_instance_predicate=_get_test_keep_instance_predicate(cfg),
|
485 |
+
proposal_files=[
|
486 |
+
cfg.DATASETS.PROPOSAL_FILES_TEST[list(cfg.DATASETS.TEST).index(dataset_name)]
|
487 |
+
]
|
488 |
+
if cfg.MODEL.LOAD_PROPOSALS
|
489 |
+
else None,
|
490 |
+
)
|
491 |
+
sampler = None
|
492 |
+
if not cfg.DENSEPOSE_EVALUATION.DISTRIBUTED_INFERENCE:
|
493 |
+
sampler = torch.utils.data.SequentialSampler(dataset_dicts)
|
494 |
+
if mapper is None:
|
495 |
+
mapper = DatasetMapper(cfg, False)
|
496 |
+
return d2_build_detection_test_loader(
|
497 |
+
dataset_dicts, mapper=mapper, num_workers=cfg.DATALOADER.NUM_WORKERS, sampler=sampler
|
498 |
+
)
|
499 |
+
|
500 |
+
|
501 |
+
def build_frame_selector(cfg: CfgNode):
|
502 |
+
strategy = FrameSelectionStrategy(cfg.STRATEGY)
|
503 |
+
if strategy == FrameSelectionStrategy.RANDOM_K:
|
504 |
+
frame_selector = RandomKFramesSelector(cfg.NUM_IMAGES)
|
505 |
+
elif strategy == FrameSelectionStrategy.FIRST_K:
|
506 |
+
frame_selector = FirstKFramesSelector(cfg.NUM_IMAGES)
|
507 |
+
elif strategy == FrameSelectionStrategy.LAST_K:
|
508 |
+
frame_selector = LastKFramesSelector(cfg.NUM_IMAGES)
|
509 |
+
elif strategy == FrameSelectionStrategy.ALL:
|
510 |
+
frame_selector = None
|
511 |
+
# pyre-fixme[61]: `frame_selector` may not be initialized here.
|
512 |
+
return frame_selector
|
513 |
+
|
514 |
+
|
515 |
+
def build_transform(cfg: CfgNode, data_type: str):
|
516 |
+
if cfg.TYPE == "resize":
|
517 |
+
if data_type == "image":
|
518 |
+
return ImageResizeTransform(cfg.MIN_SIZE, cfg.MAX_SIZE)
|
519 |
+
raise ValueError(f"Unknown transform {cfg.TYPE} for data type {data_type}")
|
520 |
+
|
521 |
+
|
522 |
+
def build_combined_loader(cfg: CfgNode, loaders: Collection[Loader], ratios: Sequence[float]):
|
523 |
+
images_per_worker = _compute_num_images_per_worker(cfg)
|
524 |
+
return CombinedDataLoader(loaders, images_per_worker, ratios)
|
525 |
+
|
526 |
+
|
527 |
+
def build_bootstrap_dataset(dataset_name: str, cfg: CfgNode) -> Sequence[torch.Tensor]:
|
528 |
+
"""
|
529 |
+
Build dataset that provides data to bootstrap on
|
530 |
+
|
531 |
+
Args:
|
532 |
+
dataset_name (str): Name of the dataset, needs to have associated metadata
|
533 |
+
to load the data
|
534 |
+
cfg (CfgNode): bootstrapping config
|
535 |
+
Returns:
|
536 |
+
Sequence[Tensor] - dataset that provides image batches, Tensors of size
|
537 |
+
[N, C, H, W] of type float32
|
538 |
+
"""
|
539 |
+
logger = logging.getLogger(__name__)
|
540 |
+
_add_category_info_to_bootstrapping_metadata(dataset_name, cfg)
|
541 |
+
meta = MetadataCatalog.get(dataset_name)
|
542 |
+
factory = BootstrapDatasetFactoryCatalog.get(meta.dataset_type)
|
543 |
+
dataset = None
|
544 |
+
if factory is not None:
|
545 |
+
dataset = factory(meta, cfg)
|
546 |
+
if dataset is None:
|
547 |
+
logger.warning(f"Failed to create dataset {dataset_name} of type {meta.dataset_type}")
|
548 |
+
return dataset
|
549 |
+
|
550 |
+
|
551 |
+
def build_data_sampler(cfg: CfgNode, sampler_cfg: CfgNode, embedder: Optional[torch.nn.Module]):
|
552 |
+
if sampler_cfg.TYPE == "densepose_uniform":
|
553 |
+
data_sampler = PredictionToGroundTruthSampler()
|
554 |
+
# transform densepose pred -> gt
|
555 |
+
data_sampler.register_sampler(
|
556 |
+
"pred_densepose",
|
557 |
+
"gt_densepose",
|
558 |
+
DensePoseUniformSampler(count_per_class=sampler_cfg.COUNT_PER_CLASS),
|
559 |
+
)
|
560 |
+
data_sampler.register_sampler("pred_densepose", "gt_masks", MaskFromDensePoseSampler())
|
561 |
+
return data_sampler
|
562 |
+
elif sampler_cfg.TYPE == "densepose_UV_confidence":
|
563 |
+
data_sampler = PredictionToGroundTruthSampler()
|
564 |
+
# transform densepose pred -> gt
|
565 |
+
data_sampler.register_sampler(
|
566 |
+
"pred_densepose",
|
567 |
+
"gt_densepose",
|
568 |
+
DensePoseConfidenceBasedSampler(
|
569 |
+
confidence_channel="sigma_2",
|
570 |
+
count_per_class=sampler_cfg.COUNT_PER_CLASS,
|
571 |
+
search_proportion=0.5,
|
572 |
+
),
|
573 |
+
)
|
574 |
+
data_sampler.register_sampler("pred_densepose", "gt_masks", MaskFromDensePoseSampler())
|
575 |
+
return data_sampler
|
576 |
+
elif sampler_cfg.TYPE == "densepose_fine_segm_confidence":
|
577 |
+
data_sampler = PredictionToGroundTruthSampler()
|
578 |
+
# transform densepose pred -> gt
|
579 |
+
data_sampler.register_sampler(
|
580 |
+
"pred_densepose",
|
581 |
+
"gt_densepose",
|
582 |
+
DensePoseConfidenceBasedSampler(
|
583 |
+
confidence_channel="fine_segm_confidence",
|
584 |
+
count_per_class=sampler_cfg.COUNT_PER_CLASS,
|
585 |
+
search_proportion=0.5,
|
586 |
+
),
|
587 |
+
)
|
588 |
+
data_sampler.register_sampler("pred_densepose", "gt_masks", MaskFromDensePoseSampler())
|
589 |
+
return data_sampler
|
590 |
+
elif sampler_cfg.TYPE == "densepose_coarse_segm_confidence":
|
591 |
+
data_sampler = PredictionToGroundTruthSampler()
|
592 |
+
# transform densepose pred -> gt
|
593 |
+
data_sampler.register_sampler(
|
594 |
+
"pred_densepose",
|
595 |
+
"gt_densepose",
|
596 |
+
DensePoseConfidenceBasedSampler(
|
597 |
+
confidence_channel="coarse_segm_confidence",
|
598 |
+
count_per_class=sampler_cfg.COUNT_PER_CLASS,
|
599 |
+
search_proportion=0.5,
|
600 |
+
),
|
601 |
+
)
|
602 |
+
data_sampler.register_sampler("pred_densepose", "gt_masks", MaskFromDensePoseSampler())
|
603 |
+
return data_sampler
|
604 |
+
elif sampler_cfg.TYPE == "densepose_cse_uniform":
|
605 |
+
assert embedder is not None
|
606 |
+
data_sampler = PredictionToGroundTruthSampler()
|
607 |
+
# transform densepose pred -> gt
|
608 |
+
data_sampler.register_sampler(
|
609 |
+
"pred_densepose",
|
610 |
+
"gt_densepose",
|
611 |
+
DensePoseCSEUniformSampler(
|
612 |
+
cfg=cfg,
|
613 |
+
use_gt_categories=sampler_cfg.USE_GROUND_TRUTH_CATEGORIES,
|
614 |
+
embedder=embedder,
|
615 |
+
count_per_class=sampler_cfg.COUNT_PER_CLASS,
|
616 |
+
),
|
617 |
+
)
|
618 |
+
data_sampler.register_sampler("pred_densepose", "gt_masks", MaskFromDensePoseSampler())
|
619 |
+
return data_sampler
|
620 |
+
elif sampler_cfg.TYPE == "densepose_cse_coarse_segm_confidence":
|
621 |
+
assert embedder is not None
|
622 |
+
data_sampler = PredictionToGroundTruthSampler()
|
623 |
+
# transform densepose pred -> gt
|
624 |
+
data_sampler.register_sampler(
|
625 |
+
"pred_densepose",
|
626 |
+
"gt_densepose",
|
627 |
+
DensePoseCSEConfidenceBasedSampler(
|
628 |
+
cfg=cfg,
|
629 |
+
use_gt_categories=sampler_cfg.USE_GROUND_TRUTH_CATEGORIES,
|
630 |
+
embedder=embedder,
|
631 |
+
confidence_channel="coarse_segm_confidence",
|
632 |
+
count_per_class=sampler_cfg.COUNT_PER_CLASS,
|
633 |
+
search_proportion=0.5,
|
634 |
+
),
|
635 |
+
)
|
636 |
+
data_sampler.register_sampler("pred_densepose", "gt_masks", MaskFromDensePoseSampler())
|
637 |
+
return data_sampler
|
638 |
+
|
639 |
+
raise ValueError(f"Unknown data sampler type {sampler_cfg.TYPE}")
|
640 |
+
|
641 |
+
|
642 |
+
def build_data_filter(cfg: CfgNode):
|
643 |
+
if cfg.TYPE == "detection_score":
|
644 |
+
min_score = cfg.MIN_VALUE
|
645 |
+
return ScoreBasedFilter(min_score=min_score)
|
646 |
+
raise ValueError(f"Unknown data filter type {cfg.TYPE}")
|
647 |
+
|
648 |
+
|
649 |
+
def build_inference_based_loader(
|
650 |
+
cfg: CfgNode,
|
651 |
+
dataset_cfg: CfgNode,
|
652 |
+
model: torch.nn.Module,
|
653 |
+
embedder: Optional[torch.nn.Module] = None,
|
654 |
+
) -> InferenceBasedLoader:
|
655 |
+
"""
|
656 |
+
Constructs data loader based on inference results of a model.
|
657 |
+
"""
|
658 |
+
dataset = build_bootstrap_dataset(dataset_cfg.DATASET, dataset_cfg.IMAGE_LOADER)
|
659 |
+
meta = MetadataCatalog.get(dataset_cfg.DATASET)
|
660 |
+
training_sampler = TrainingSampler(len(dataset))
|
661 |
+
data_loader = torch.utils.data.DataLoader(
|
662 |
+
dataset, # pyre-ignore[6]
|
663 |
+
batch_size=dataset_cfg.IMAGE_LOADER.BATCH_SIZE,
|
664 |
+
sampler=training_sampler,
|
665 |
+
num_workers=dataset_cfg.IMAGE_LOADER.NUM_WORKERS,
|
666 |
+
collate_fn=trivial_batch_collator,
|
667 |
+
worker_init_fn=worker_init_reset_seed,
|
668 |
+
)
|
669 |
+
return InferenceBasedLoader(
|
670 |
+
model,
|
671 |
+
data_loader=data_loader,
|
672 |
+
data_sampler=build_data_sampler(cfg, dataset_cfg.DATA_SAMPLER, embedder),
|
673 |
+
data_filter=build_data_filter(dataset_cfg.FILTER),
|
674 |
+
shuffle=True,
|
675 |
+
batch_size=dataset_cfg.INFERENCE.OUTPUT_BATCH_SIZE,
|
676 |
+
inference_batch_size=dataset_cfg.INFERENCE.INPUT_BATCH_SIZE,
|
677 |
+
category_to_class_mapping=meta.category_to_class_mapping,
|
678 |
+
)
|
679 |
+
|
680 |
+
|
681 |
+
def has_inference_based_loaders(cfg: CfgNode) -> bool:
|
682 |
+
"""
|
683 |
+
Returns True, if at least one inferense-based loader must
|
684 |
+
be instantiated for training
|
685 |
+
"""
|
686 |
+
return len(cfg.BOOTSTRAP_DATASETS) > 0
|
687 |
+
|
688 |
+
|
689 |
+
def build_inference_based_loaders(
|
690 |
+
cfg: CfgNode, model: torch.nn.Module
|
691 |
+
) -> Tuple[List[InferenceBasedLoader], List[float]]:
|
692 |
+
loaders = []
|
693 |
+
ratios = []
|
694 |
+
embedder = build_densepose_embedder(cfg).to(device=model.device) # pyre-ignore[16]
|
695 |
+
for dataset_spec in cfg.BOOTSTRAP_DATASETS:
|
696 |
+
dataset_cfg = get_bootstrap_dataset_config().clone()
|
697 |
+
dataset_cfg.merge_from_other_cfg(CfgNode(dataset_spec))
|
698 |
+
loader = build_inference_based_loader(cfg, dataset_cfg, model, embedder)
|
699 |
+
loaders.append(loader)
|
700 |
+
ratios.append(dataset_cfg.RATIO)
|
701 |
+
return loaders, ratios
|
702 |
+
|
703 |
+
|
704 |
+
def build_video_list_dataset(meta: Metadata, cfg: CfgNode):
|
705 |
+
video_list_fpath = meta.video_list_fpath
|
706 |
+
video_base_path = meta.video_base_path
|
707 |
+
category = meta.category
|
708 |
+
if cfg.TYPE == "video_keyframe":
|
709 |
+
frame_selector = build_frame_selector(cfg.SELECT)
|
710 |
+
transform = build_transform(cfg.TRANSFORM, data_type="image")
|
711 |
+
video_list = video_list_from_file(video_list_fpath, video_base_path)
|
712 |
+
keyframe_helper_fpath = getattr(cfg, "KEYFRAME_HELPER", None)
|
713 |
+
return VideoKeyframeDataset(
|
714 |
+
video_list, category, frame_selector, transform, keyframe_helper_fpath
|
715 |
+
)
|
716 |
+
|
717 |
+
|
718 |
+
class _BootstrapDatasetFactoryCatalog(UserDict):
|
719 |
+
"""
|
720 |
+
A global dictionary that stores information about bootstrapped datasets creation functions
|
721 |
+
from metadata and config, for diverse DatasetType
|
722 |
+
"""
|
723 |
+
|
724 |
+
def register(self, dataset_type: DatasetType, factory: Callable[[Metadata, CfgNode], Dataset]):
|
725 |
+
"""
|
726 |
+
Args:
|
727 |
+
dataset_type (DatasetType): a DatasetType e.g. DatasetType.VIDEO_LIST
|
728 |
+
factory (Callable[Metadata, CfgNode]): a callable which takes Metadata and cfg
|
729 |
+
arguments and returns a dataset object.
|
730 |
+
"""
|
731 |
+
assert dataset_type not in self, "Dataset '{}' is already registered!".format(dataset_type)
|
732 |
+
self[dataset_type] = factory
|
733 |
+
|
734 |
+
|
735 |
+
BootstrapDatasetFactoryCatalog = _BootstrapDatasetFactoryCatalog()
|
736 |
+
BootstrapDatasetFactoryCatalog.register(DatasetType.VIDEO_LIST, build_video_list_dataset)
|
densepose/data/combined_loader.py
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
import random
|
4 |
+
from collections import deque
|
5 |
+
from typing import Any, Collection, Deque, Iterable, Iterator, List, Sequence
|
6 |
+
|
7 |
+
Loader = Iterable[Any]
|
8 |
+
|
9 |
+
|
10 |
+
def _pooled_next(iterator: Iterator[Any], pool: Deque[Any]):
|
11 |
+
if not pool:
|
12 |
+
pool.extend(next(iterator))
|
13 |
+
return pool.popleft()
|
14 |
+
|
15 |
+
|
16 |
+
class CombinedDataLoader:
|
17 |
+
"""
|
18 |
+
Combines data loaders using the provided sampling ratios
|
19 |
+
"""
|
20 |
+
|
21 |
+
BATCH_COUNT = 100
|
22 |
+
|
23 |
+
def __init__(self, loaders: Collection[Loader], batch_size: int, ratios: Sequence[float]):
|
24 |
+
self.loaders = loaders
|
25 |
+
self.batch_size = batch_size
|
26 |
+
self.ratios = ratios
|
27 |
+
|
28 |
+
def __iter__(self) -> Iterator[List[Any]]:
|
29 |
+
iters = [iter(loader) for loader in self.loaders]
|
30 |
+
indices = []
|
31 |
+
pool = [deque()] * len(iters)
|
32 |
+
# infinite iterator, as in D2
|
33 |
+
while True:
|
34 |
+
if not indices:
|
35 |
+
# just a buffer of indices, its size doesn't matter
|
36 |
+
# as long as it's a multiple of batch_size
|
37 |
+
k = self.batch_size * self.BATCH_COUNT
|
38 |
+
indices = random.choices(range(len(self.loaders)), self.ratios, k=k)
|
39 |
+
try:
|
40 |
+
batch = [_pooled_next(iters[i], pool[i]) for i in indices[: self.batch_size]]
|
41 |
+
except StopIteration:
|
42 |
+
break
|
43 |
+
indices = indices[self.batch_size :]
|
44 |
+
yield batch
|
densepose/data/dataset_mapper.py
ADDED
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
3 |
+
|
4 |
+
import copy
|
5 |
+
import logging
|
6 |
+
from typing import Any, Dict, List, Tuple
|
7 |
+
import torch
|
8 |
+
|
9 |
+
from detectron2.data import MetadataCatalog
|
10 |
+
from detectron2.data import detection_utils as utils
|
11 |
+
from detectron2.data import transforms as T
|
12 |
+
from detectron2.layers import ROIAlign
|
13 |
+
from detectron2.structures import BoxMode
|
14 |
+
from detectron2.utils.file_io import PathManager
|
15 |
+
|
16 |
+
from densepose.structures import DensePoseDataRelative, DensePoseList, DensePoseTransformData
|
17 |
+
|
18 |
+
|
19 |
+
def build_augmentation(cfg, is_train):
|
20 |
+
logger = logging.getLogger(__name__)
|
21 |
+
result = utils.build_augmentation(cfg, is_train)
|
22 |
+
if is_train:
|
23 |
+
random_rotation = T.RandomRotation(
|
24 |
+
cfg.INPUT.ROTATION_ANGLES, expand=False, sample_style="choice"
|
25 |
+
)
|
26 |
+
result.append(random_rotation)
|
27 |
+
logger.info("DensePose-specific augmentation used in training: " + str(random_rotation))
|
28 |
+
return result
|
29 |
+
|
30 |
+
|
31 |
+
class DatasetMapper:
|
32 |
+
"""
|
33 |
+
A customized version of `detectron2.data.DatasetMapper`
|
34 |
+
"""
|
35 |
+
|
36 |
+
def __init__(self, cfg, is_train=True):
|
37 |
+
self.augmentation = build_augmentation(cfg, is_train)
|
38 |
+
|
39 |
+
# fmt: off
|
40 |
+
self.img_format = cfg.INPUT.FORMAT
|
41 |
+
self.mask_on = (
|
42 |
+
cfg.MODEL.MASK_ON or (
|
43 |
+
cfg.MODEL.DENSEPOSE_ON
|
44 |
+
and cfg.MODEL.ROI_DENSEPOSE_HEAD.COARSE_SEGM_TRAINED_BY_MASKS)
|
45 |
+
)
|
46 |
+
self.keypoint_on = cfg.MODEL.KEYPOINT_ON
|
47 |
+
self.densepose_on = cfg.MODEL.DENSEPOSE_ON
|
48 |
+
assert not cfg.MODEL.LOAD_PROPOSALS, "not supported yet"
|
49 |
+
# fmt: on
|
50 |
+
if self.keypoint_on and is_train:
|
51 |
+
# Flip only makes sense in training
|
52 |
+
self.keypoint_hflip_indices = utils.create_keypoint_hflip_indices(cfg.DATASETS.TRAIN)
|
53 |
+
else:
|
54 |
+
self.keypoint_hflip_indices = None
|
55 |
+
|
56 |
+
if self.densepose_on:
|
57 |
+
densepose_transform_srcs = [
|
58 |
+
MetadataCatalog.get(ds).densepose_transform_src
|
59 |
+
for ds in cfg.DATASETS.TRAIN + cfg.DATASETS.TEST
|
60 |
+
]
|
61 |
+
assert len(densepose_transform_srcs) > 0
|
62 |
+
# TODO: check that DensePose transformation data is the same for
|
63 |
+
# all the datasets. Otherwise one would have to pass DB ID with
|
64 |
+
# each entry to select proper transformation data. For now, since
|
65 |
+
# all DensePose annotated data uses the same data semantics, we
|
66 |
+
# omit this check.
|
67 |
+
densepose_transform_data_fpath = PathManager.get_local_path(densepose_transform_srcs[0])
|
68 |
+
self.densepose_transform_data = DensePoseTransformData.load(
|
69 |
+
densepose_transform_data_fpath
|
70 |
+
)
|
71 |
+
|
72 |
+
self.is_train = is_train
|
73 |
+
|
74 |
+
def __call__(self, dataset_dict):
|
75 |
+
"""
|
76 |
+
Args:
|
77 |
+
dataset_dict (dict): Metadata of one image, in Detectron2 Dataset format.
|
78 |
+
|
79 |
+
Returns:
|
80 |
+
dict: a format that builtin models in detectron2 accept
|
81 |
+
"""
|
82 |
+
dataset_dict = copy.deepcopy(dataset_dict) # it will be modified by code below
|
83 |
+
image = utils.read_image(dataset_dict["file_name"], format=self.img_format)
|
84 |
+
utils.check_image_size(dataset_dict, image)
|
85 |
+
|
86 |
+
image, transforms = T.apply_transform_gens(self.augmentation, image)
|
87 |
+
image_shape = image.shape[:2] # h, w
|
88 |
+
dataset_dict["image"] = torch.as_tensor(image.transpose(2, 0, 1).astype("float32"))
|
89 |
+
|
90 |
+
if not self.is_train:
|
91 |
+
dataset_dict.pop("annotations", None)
|
92 |
+
return dataset_dict
|
93 |
+
|
94 |
+
for anno in dataset_dict["annotations"]:
|
95 |
+
if not self.mask_on:
|
96 |
+
anno.pop("segmentation", None)
|
97 |
+
if not self.keypoint_on:
|
98 |
+
anno.pop("keypoints", None)
|
99 |
+
|
100 |
+
# USER: Implement additional transformations if you have other types of data
|
101 |
+
# USER: Don't call transpose_densepose if you don't need
|
102 |
+
annos = [
|
103 |
+
self._transform_densepose(
|
104 |
+
utils.transform_instance_annotations(
|
105 |
+
obj, transforms, image_shape, keypoint_hflip_indices=self.keypoint_hflip_indices
|
106 |
+
),
|
107 |
+
transforms,
|
108 |
+
)
|
109 |
+
for obj in dataset_dict.pop("annotations")
|
110 |
+
if obj.get("iscrowd", 0) == 0
|
111 |
+
]
|
112 |
+
|
113 |
+
if self.mask_on:
|
114 |
+
self._add_densepose_masks_as_segmentation(annos, image_shape)
|
115 |
+
|
116 |
+
instances = utils.annotations_to_instances(annos, image_shape, mask_format="bitmask")
|
117 |
+
densepose_annotations = [obj.get("densepose") for obj in annos]
|
118 |
+
if densepose_annotations and not all(v is None for v in densepose_annotations):
|
119 |
+
instances.gt_densepose = DensePoseList(
|
120 |
+
densepose_annotations, instances.gt_boxes, image_shape
|
121 |
+
)
|
122 |
+
|
123 |
+
dataset_dict["instances"] = instances[instances.gt_boxes.nonempty()]
|
124 |
+
return dataset_dict
|
125 |
+
|
126 |
+
def _transform_densepose(self, annotation, transforms):
|
127 |
+
if not self.densepose_on:
|
128 |
+
return annotation
|
129 |
+
|
130 |
+
# Handle densepose annotations
|
131 |
+
is_valid, reason_not_valid = DensePoseDataRelative.validate_annotation(annotation)
|
132 |
+
if is_valid:
|
133 |
+
densepose_data = DensePoseDataRelative(annotation, cleanup=True)
|
134 |
+
densepose_data.apply_transform(transforms, self.densepose_transform_data)
|
135 |
+
annotation["densepose"] = densepose_data
|
136 |
+
else:
|
137 |
+
# logger = logging.getLogger(__name__)
|
138 |
+
# logger.debug("Could not load DensePose annotation: {}".format(reason_not_valid))
|
139 |
+
DensePoseDataRelative.cleanup_annotation(annotation)
|
140 |
+
# NOTE: annotations for certain instances may be unavailable.
|
141 |
+
# 'None' is accepted by the DensePostList data structure.
|
142 |
+
annotation["densepose"] = None
|
143 |
+
return annotation
|
144 |
+
|
145 |
+
def _add_densepose_masks_as_segmentation(
|
146 |
+
self, annotations: List[Dict[str, Any]], image_shape_hw: Tuple[int, int]
|
147 |
+
):
|
148 |
+
for obj in annotations:
|
149 |
+
if ("densepose" not in obj) or ("segmentation" in obj):
|
150 |
+
continue
|
151 |
+
# DP segmentation: torch.Tensor [S, S] of float32, S=256
|
152 |
+
segm_dp = torch.zeros_like(obj["densepose"].segm)
|
153 |
+
segm_dp[obj["densepose"].segm > 0] = 1
|
154 |
+
segm_h, segm_w = segm_dp.shape
|
155 |
+
bbox_segm_dp = torch.tensor((0, 0, segm_h - 1, segm_w - 1), dtype=torch.float32)
|
156 |
+
# image bbox
|
157 |
+
x0, y0, x1, y1 = (
|
158 |
+
v.item() for v in BoxMode.convert(obj["bbox"], obj["bbox_mode"], BoxMode.XYXY_ABS)
|
159 |
+
)
|
160 |
+
segm_aligned = (
|
161 |
+
ROIAlign((y1 - y0, x1 - x0), 1.0, 0, aligned=True)
|
162 |
+
.forward(segm_dp.view(1, 1, *segm_dp.shape), bbox_segm_dp)
|
163 |
+
.squeeze()
|
164 |
+
)
|
165 |
+
image_mask = torch.zeros(*image_shape_hw, dtype=torch.float32)
|
166 |
+
image_mask[y0:y1, x0:x1] = segm_aligned
|
167 |
+
# segmentation for BitMask: np.array [H, W] of bool
|
168 |
+
obj["segmentation"] = image_mask >= 0.5
|
densepose/data/datasets/__init__.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from . import builtin # ensure the builtin datasets are registered
|
4 |
+
|
5 |
+
__all__ = [k for k in globals().keys() if "builtin" not in k and not k.startswith("_")]
|
densepose/data/datasets/builtin.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
from .chimpnsee import register_dataset as register_chimpnsee_dataset
|
3 |
+
from .coco import BASE_DATASETS as BASE_COCO_DATASETS
|
4 |
+
from .coco import DATASETS as COCO_DATASETS
|
5 |
+
from .coco import register_datasets as register_coco_datasets
|
6 |
+
from .lvis import DATASETS as LVIS_DATASETS
|
7 |
+
from .lvis import register_datasets as register_lvis_datasets
|
8 |
+
|
9 |
+
DEFAULT_DATASETS_ROOT = "datasets"
|
10 |
+
|
11 |
+
|
12 |
+
register_coco_datasets(COCO_DATASETS, DEFAULT_DATASETS_ROOT)
|
13 |
+
register_coco_datasets(BASE_COCO_DATASETS, DEFAULT_DATASETS_ROOT)
|
14 |
+
register_lvis_datasets(LVIS_DATASETS, DEFAULT_DATASETS_ROOT)
|
15 |
+
|
16 |
+
register_chimpnsee_dataset(DEFAULT_DATASETS_ROOT) # pyre-ignore[19]
|
densepose/data/datasets/chimpnsee.py
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from typing import Optional
|
4 |
+
|
5 |
+
from detectron2.data import DatasetCatalog, MetadataCatalog
|
6 |
+
|
7 |
+
from ..utils import maybe_prepend_base_path
|
8 |
+
from .dataset_type import DatasetType
|
9 |
+
|
10 |
+
CHIMPNSEE_DATASET_NAME = "chimpnsee"
|
11 |
+
|
12 |
+
|
13 |
+
def register_dataset(datasets_root: Optional[str] = None) -> None:
|
14 |
+
def empty_load_callback():
|
15 |
+
pass
|
16 |
+
|
17 |
+
video_list_fpath = maybe_prepend_base_path(
|
18 |
+
datasets_root,
|
19 |
+
"chimpnsee/cdna.eva.mpg.de/video_list.txt",
|
20 |
+
)
|
21 |
+
video_base_path = maybe_prepend_base_path(datasets_root, "chimpnsee/cdna.eva.mpg.de")
|
22 |
+
|
23 |
+
DatasetCatalog.register(CHIMPNSEE_DATASET_NAME, empty_load_callback)
|
24 |
+
MetadataCatalog.get(CHIMPNSEE_DATASET_NAME).set(
|
25 |
+
dataset_type=DatasetType.VIDEO_LIST,
|
26 |
+
video_list_fpath=video_list_fpath,
|
27 |
+
video_base_path=video_base_path,
|
28 |
+
category="chimpanzee",
|
29 |
+
)
|
densepose/data/datasets/coco.py
ADDED
@@ -0,0 +1,432 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
import contextlib
|
3 |
+
import io
|
4 |
+
import logging
|
5 |
+
import os
|
6 |
+
from collections import defaultdict
|
7 |
+
from dataclasses import dataclass
|
8 |
+
from typing import Any, Dict, Iterable, List, Optional
|
9 |
+
from fvcore.common.timer import Timer
|
10 |
+
|
11 |
+
from detectron2.data import DatasetCatalog, MetadataCatalog
|
12 |
+
from detectron2.structures import BoxMode
|
13 |
+
from detectron2.utils.file_io import PathManager
|
14 |
+
|
15 |
+
from ..utils import maybe_prepend_base_path
|
16 |
+
|
17 |
+
DENSEPOSE_MASK_KEY = "dp_masks"
|
18 |
+
DENSEPOSE_IUV_KEYS_WITHOUT_MASK = ["dp_x", "dp_y", "dp_I", "dp_U", "dp_V"]
|
19 |
+
DENSEPOSE_CSE_KEYS_WITHOUT_MASK = ["dp_x", "dp_y", "dp_vertex", "ref_model"]
|
20 |
+
DENSEPOSE_ALL_POSSIBLE_KEYS = set(
|
21 |
+
DENSEPOSE_IUV_KEYS_WITHOUT_MASK + DENSEPOSE_CSE_KEYS_WITHOUT_MASK + [DENSEPOSE_MASK_KEY]
|
22 |
+
)
|
23 |
+
DENSEPOSE_METADATA_URL_PREFIX = "https://dl.fbaipublicfiles.com/densepose/data/"
|
24 |
+
|
25 |
+
|
26 |
+
@dataclass
|
27 |
+
class CocoDatasetInfo:
|
28 |
+
name: str
|
29 |
+
images_root: str
|
30 |
+
annotations_fpath: str
|
31 |
+
|
32 |
+
|
33 |
+
DATASETS = [
|
34 |
+
CocoDatasetInfo(
|
35 |
+
name="densepose_coco_2014_train",
|
36 |
+
images_root="coco/train2014",
|
37 |
+
annotations_fpath="coco/annotations/densepose_train2014.json",
|
38 |
+
),
|
39 |
+
CocoDatasetInfo(
|
40 |
+
name="densepose_coco_2014_minival",
|
41 |
+
images_root="coco/val2014",
|
42 |
+
annotations_fpath="coco/annotations/densepose_minival2014.json",
|
43 |
+
),
|
44 |
+
CocoDatasetInfo(
|
45 |
+
name="densepose_coco_2014_minival_100",
|
46 |
+
images_root="coco/val2014",
|
47 |
+
annotations_fpath="coco/annotations/densepose_minival2014_100.json",
|
48 |
+
),
|
49 |
+
CocoDatasetInfo(
|
50 |
+
name="densepose_coco_2014_valminusminival",
|
51 |
+
images_root="coco/val2014",
|
52 |
+
annotations_fpath="coco/annotations/densepose_valminusminival2014.json",
|
53 |
+
),
|
54 |
+
CocoDatasetInfo(
|
55 |
+
name="densepose_coco_2014_train_cse",
|
56 |
+
images_root="coco/train2014",
|
57 |
+
annotations_fpath="coco_cse/densepose_train2014_cse.json",
|
58 |
+
),
|
59 |
+
CocoDatasetInfo(
|
60 |
+
name="densepose_coco_2014_minival_cse",
|
61 |
+
images_root="coco/val2014",
|
62 |
+
annotations_fpath="coco_cse/densepose_minival2014_cse.json",
|
63 |
+
),
|
64 |
+
CocoDatasetInfo(
|
65 |
+
name="densepose_coco_2014_minival_100_cse",
|
66 |
+
images_root="coco/val2014",
|
67 |
+
annotations_fpath="coco_cse/densepose_minival2014_100_cse.json",
|
68 |
+
),
|
69 |
+
CocoDatasetInfo(
|
70 |
+
name="densepose_coco_2014_valminusminival_cse",
|
71 |
+
images_root="coco/val2014",
|
72 |
+
annotations_fpath="coco_cse/densepose_valminusminival2014_cse.json",
|
73 |
+
),
|
74 |
+
CocoDatasetInfo(
|
75 |
+
name="densepose_chimps",
|
76 |
+
images_root="densepose_chimps/images",
|
77 |
+
annotations_fpath="densepose_chimps/densepose_chimps_densepose.json",
|
78 |
+
),
|
79 |
+
CocoDatasetInfo(
|
80 |
+
name="densepose_chimps_cse_train",
|
81 |
+
images_root="densepose_chimps/images",
|
82 |
+
annotations_fpath="densepose_chimps/densepose_chimps_cse_train.json",
|
83 |
+
),
|
84 |
+
CocoDatasetInfo(
|
85 |
+
name="densepose_chimps_cse_val",
|
86 |
+
images_root="densepose_chimps/images",
|
87 |
+
annotations_fpath="densepose_chimps/densepose_chimps_cse_val.json",
|
88 |
+
),
|
89 |
+
CocoDatasetInfo(
|
90 |
+
name="posetrack2017_train",
|
91 |
+
images_root="posetrack2017/posetrack_data_2017",
|
92 |
+
annotations_fpath="posetrack2017/densepose_posetrack_train2017.json",
|
93 |
+
),
|
94 |
+
CocoDatasetInfo(
|
95 |
+
name="posetrack2017_val",
|
96 |
+
images_root="posetrack2017/posetrack_data_2017",
|
97 |
+
annotations_fpath="posetrack2017/densepose_posetrack_val2017.json",
|
98 |
+
),
|
99 |
+
CocoDatasetInfo(
|
100 |
+
name="lvis_v05_train",
|
101 |
+
images_root="coco/train2017",
|
102 |
+
annotations_fpath="lvis/lvis_v0.5_plus_dp_train.json",
|
103 |
+
),
|
104 |
+
CocoDatasetInfo(
|
105 |
+
name="lvis_v05_val",
|
106 |
+
images_root="coco/val2017",
|
107 |
+
annotations_fpath="lvis/lvis_v0.5_plus_dp_val.json",
|
108 |
+
),
|
109 |
+
]
|
110 |
+
|
111 |
+
|
112 |
+
BASE_DATASETS = [
|
113 |
+
CocoDatasetInfo(
|
114 |
+
name="base_coco_2017_train",
|
115 |
+
images_root="coco/train2017",
|
116 |
+
annotations_fpath="coco/annotations/instances_train2017.json",
|
117 |
+
),
|
118 |
+
CocoDatasetInfo(
|
119 |
+
name="base_coco_2017_val",
|
120 |
+
images_root="coco/val2017",
|
121 |
+
annotations_fpath="coco/annotations/instances_val2017.json",
|
122 |
+
),
|
123 |
+
CocoDatasetInfo(
|
124 |
+
name="base_coco_2017_val_100",
|
125 |
+
images_root="coco/val2017",
|
126 |
+
annotations_fpath="coco/annotations/instances_val2017_100.json",
|
127 |
+
),
|
128 |
+
]
|
129 |
+
|
130 |
+
|
131 |
+
def get_metadata(base_path: Optional[str]) -> Dict[str, Any]:
|
132 |
+
"""
|
133 |
+
Returns metadata associated with COCO DensePose datasets
|
134 |
+
|
135 |
+
Args:
|
136 |
+
base_path: Optional[str]
|
137 |
+
Base path used to load metadata from
|
138 |
+
|
139 |
+
Returns:
|
140 |
+
Dict[str, Any]
|
141 |
+
Metadata in the form of a dictionary
|
142 |
+
"""
|
143 |
+
meta = {
|
144 |
+
"densepose_transform_src": maybe_prepend_base_path(base_path, "UV_symmetry_transforms.mat"),
|
145 |
+
"densepose_smpl_subdiv": maybe_prepend_base_path(base_path, "SMPL_subdiv.mat"),
|
146 |
+
"densepose_smpl_subdiv_transform": maybe_prepend_base_path(
|
147 |
+
base_path,
|
148 |
+
"SMPL_SUBDIV_TRANSFORM.mat",
|
149 |
+
),
|
150 |
+
}
|
151 |
+
return meta
|
152 |
+
|
153 |
+
|
154 |
+
def _load_coco_annotations(json_file: str):
|
155 |
+
"""
|
156 |
+
Load COCO annotations from a JSON file
|
157 |
+
|
158 |
+
Args:
|
159 |
+
json_file: str
|
160 |
+
Path to the file to load annotations from
|
161 |
+
Returns:
|
162 |
+
Instance of `pycocotools.coco.COCO` that provides access to annotations
|
163 |
+
data
|
164 |
+
"""
|
165 |
+
from pycocotools.coco import COCO
|
166 |
+
|
167 |
+
logger = logging.getLogger(__name__)
|
168 |
+
timer = Timer()
|
169 |
+
with contextlib.redirect_stdout(io.StringIO()):
|
170 |
+
coco_api = COCO(json_file)
|
171 |
+
if timer.seconds() > 1:
|
172 |
+
logger.info("Loading {} takes {:.2f} seconds.".format(json_file, timer.seconds()))
|
173 |
+
return coco_api
|
174 |
+
|
175 |
+
|
176 |
+
def _add_categories_metadata(dataset_name: str, categories: List[Dict[str, Any]]):
|
177 |
+
meta = MetadataCatalog.get(dataset_name)
|
178 |
+
meta.categories = {c["id"]: c["name"] for c in categories}
|
179 |
+
logger = logging.getLogger(__name__)
|
180 |
+
logger.info("Dataset {} categories: {}".format(dataset_name, meta.categories))
|
181 |
+
|
182 |
+
|
183 |
+
def _verify_annotations_have_unique_ids(json_file: str, anns: List[List[Dict[str, Any]]]):
|
184 |
+
if "minival" in json_file:
|
185 |
+
# Skip validation on COCO2014 valminusminival and minival annotations
|
186 |
+
# The ratio of buggy annotations there is tiny and does not affect accuracy
|
187 |
+
# Therefore we explicitly white-list them
|
188 |
+
return
|
189 |
+
ann_ids = [ann["id"] for anns_per_image in anns for ann in anns_per_image]
|
190 |
+
assert len(set(ann_ids)) == len(ann_ids), "Annotation ids in '{}' are not unique!".format(
|
191 |
+
json_file
|
192 |
+
)
|
193 |
+
|
194 |
+
|
195 |
+
def _maybe_add_bbox(obj: Dict[str, Any], ann_dict: Dict[str, Any]):
|
196 |
+
if "bbox" not in ann_dict:
|
197 |
+
return
|
198 |
+
obj["bbox"] = ann_dict["bbox"]
|
199 |
+
obj["bbox_mode"] = BoxMode.XYWH_ABS
|
200 |
+
|
201 |
+
|
202 |
+
def _maybe_add_segm(obj: Dict[str, Any], ann_dict: Dict[str, Any]):
|
203 |
+
if "segmentation" not in ann_dict:
|
204 |
+
return
|
205 |
+
segm = ann_dict["segmentation"]
|
206 |
+
if not isinstance(segm, dict):
|
207 |
+
# filter out invalid polygons (< 3 points)
|
208 |
+
segm = [poly for poly in segm if len(poly) % 2 == 0 and len(poly) >= 6]
|
209 |
+
if len(segm) == 0:
|
210 |
+
return
|
211 |
+
obj["segmentation"] = segm
|
212 |
+
|
213 |
+
|
214 |
+
def _maybe_add_keypoints(obj: Dict[str, Any], ann_dict: Dict[str, Any]):
|
215 |
+
if "keypoints" not in ann_dict:
|
216 |
+
return
|
217 |
+
keypts = ann_dict["keypoints"] # list[int]
|
218 |
+
for idx, v in enumerate(keypts):
|
219 |
+
if idx % 3 != 2:
|
220 |
+
# COCO's segmentation coordinates are floating points in [0, H or W],
|
221 |
+
# but keypoint coordinates are integers in [0, H-1 or W-1]
|
222 |
+
# Therefore we assume the coordinates are "pixel indices" and
|
223 |
+
# add 0.5 to convert to floating point coordinates.
|
224 |
+
keypts[idx] = v + 0.5
|
225 |
+
obj["keypoints"] = keypts
|
226 |
+
|
227 |
+
|
228 |
+
def _maybe_add_densepose(obj: Dict[str, Any], ann_dict: Dict[str, Any]):
|
229 |
+
for key in DENSEPOSE_ALL_POSSIBLE_KEYS:
|
230 |
+
if key in ann_dict:
|
231 |
+
obj[key] = ann_dict[key]
|
232 |
+
|
233 |
+
|
234 |
+
def _combine_images_with_annotations(
|
235 |
+
dataset_name: str,
|
236 |
+
image_root: str,
|
237 |
+
img_datas: Iterable[Dict[str, Any]],
|
238 |
+
ann_datas: Iterable[Iterable[Dict[str, Any]]],
|
239 |
+
):
|
240 |
+
|
241 |
+
ann_keys = ["iscrowd", "category_id"]
|
242 |
+
dataset_dicts = []
|
243 |
+
contains_video_frame_info = False
|
244 |
+
|
245 |
+
for img_dict, ann_dicts in zip(img_datas, ann_datas):
|
246 |
+
record = {}
|
247 |
+
record["file_name"] = os.path.join(image_root, img_dict["file_name"])
|
248 |
+
record["height"] = img_dict["height"]
|
249 |
+
record["width"] = img_dict["width"]
|
250 |
+
record["image_id"] = img_dict["id"]
|
251 |
+
record["dataset"] = dataset_name
|
252 |
+
if "frame_id" in img_dict:
|
253 |
+
record["frame_id"] = img_dict["frame_id"]
|
254 |
+
record["video_id"] = img_dict.get("vid_id", None)
|
255 |
+
contains_video_frame_info = True
|
256 |
+
objs = []
|
257 |
+
for ann_dict in ann_dicts:
|
258 |
+
assert ann_dict["image_id"] == record["image_id"]
|
259 |
+
assert ann_dict.get("ignore", 0) == 0
|
260 |
+
obj = {key: ann_dict[key] for key in ann_keys if key in ann_dict}
|
261 |
+
_maybe_add_bbox(obj, ann_dict)
|
262 |
+
_maybe_add_segm(obj, ann_dict)
|
263 |
+
_maybe_add_keypoints(obj, ann_dict)
|
264 |
+
_maybe_add_densepose(obj, ann_dict)
|
265 |
+
objs.append(obj)
|
266 |
+
record["annotations"] = objs
|
267 |
+
dataset_dicts.append(record)
|
268 |
+
if contains_video_frame_info:
|
269 |
+
create_video_frame_mapping(dataset_name, dataset_dicts)
|
270 |
+
return dataset_dicts
|
271 |
+
|
272 |
+
|
273 |
+
def get_contiguous_id_to_category_id_map(metadata):
|
274 |
+
cat_id_2_cont_id = metadata.thing_dataset_id_to_contiguous_id
|
275 |
+
cont_id_2_cat_id = {}
|
276 |
+
for cat_id, cont_id in cat_id_2_cont_id.items():
|
277 |
+
if cont_id in cont_id_2_cat_id:
|
278 |
+
continue
|
279 |
+
cont_id_2_cat_id[cont_id] = cat_id
|
280 |
+
return cont_id_2_cat_id
|
281 |
+
|
282 |
+
|
283 |
+
def maybe_filter_categories_cocoapi(dataset_name, coco_api):
|
284 |
+
meta = MetadataCatalog.get(dataset_name)
|
285 |
+
cont_id_2_cat_id = get_contiguous_id_to_category_id_map(meta)
|
286 |
+
cat_id_2_cont_id = meta.thing_dataset_id_to_contiguous_id
|
287 |
+
# filter categories
|
288 |
+
cats = []
|
289 |
+
for cat in coco_api.dataset["categories"]:
|
290 |
+
cat_id = cat["id"]
|
291 |
+
if cat_id not in cat_id_2_cont_id:
|
292 |
+
continue
|
293 |
+
cont_id = cat_id_2_cont_id[cat_id]
|
294 |
+
if (cont_id in cont_id_2_cat_id) and (cont_id_2_cat_id[cont_id] == cat_id):
|
295 |
+
cats.append(cat)
|
296 |
+
coco_api.dataset["categories"] = cats
|
297 |
+
# filter annotations, if multiple categories are mapped to a single
|
298 |
+
# contiguous ID, use only one category ID and map all annotations to that category ID
|
299 |
+
anns = []
|
300 |
+
for ann in coco_api.dataset["annotations"]:
|
301 |
+
cat_id = ann["category_id"]
|
302 |
+
if cat_id not in cat_id_2_cont_id:
|
303 |
+
continue
|
304 |
+
cont_id = cat_id_2_cont_id[cat_id]
|
305 |
+
ann["category_id"] = cont_id_2_cat_id[cont_id]
|
306 |
+
anns.append(ann)
|
307 |
+
coco_api.dataset["annotations"] = anns
|
308 |
+
# recreate index
|
309 |
+
coco_api.createIndex()
|
310 |
+
|
311 |
+
|
312 |
+
def maybe_filter_and_map_categories_cocoapi(dataset_name, coco_api):
|
313 |
+
meta = MetadataCatalog.get(dataset_name)
|
314 |
+
category_id_map = meta.thing_dataset_id_to_contiguous_id
|
315 |
+
# map categories
|
316 |
+
cats = []
|
317 |
+
for cat in coco_api.dataset["categories"]:
|
318 |
+
cat_id = cat["id"]
|
319 |
+
if cat_id not in category_id_map:
|
320 |
+
continue
|
321 |
+
cat["id"] = category_id_map[cat_id]
|
322 |
+
cats.append(cat)
|
323 |
+
coco_api.dataset["categories"] = cats
|
324 |
+
# map annotation categories
|
325 |
+
anns = []
|
326 |
+
for ann in coco_api.dataset["annotations"]:
|
327 |
+
cat_id = ann["category_id"]
|
328 |
+
if cat_id not in category_id_map:
|
329 |
+
continue
|
330 |
+
ann["category_id"] = category_id_map[cat_id]
|
331 |
+
anns.append(ann)
|
332 |
+
coco_api.dataset["annotations"] = anns
|
333 |
+
# recreate index
|
334 |
+
coco_api.createIndex()
|
335 |
+
|
336 |
+
|
337 |
+
def create_video_frame_mapping(dataset_name, dataset_dicts):
|
338 |
+
mapping = defaultdict(dict)
|
339 |
+
for d in dataset_dicts:
|
340 |
+
video_id = d.get("video_id")
|
341 |
+
if video_id is None:
|
342 |
+
continue
|
343 |
+
mapping[video_id].update({d["frame_id"]: d["file_name"]})
|
344 |
+
MetadataCatalog.get(dataset_name).set(video_frame_mapping=mapping)
|
345 |
+
|
346 |
+
|
347 |
+
def load_coco_json(annotations_json_file: str, image_root: str, dataset_name: str):
|
348 |
+
"""
|
349 |
+
Loads a JSON file with annotations in COCO instances format.
|
350 |
+
Replaces `detectron2.data.datasets.coco.load_coco_json` to handle metadata
|
351 |
+
in a more flexible way. Postpones category mapping to a later stage to be
|
352 |
+
able to combine several datasets with different (but coherent) sets of
|
353 |
+
categories.
|
354 |
+
|
355 |
+
Args:
|
356 |
+
|
357 |
+
annotations_json_file: str
|
358 |
+
Path to the JSON file with annotations in COCO instances format.
|
359 |
+
image_root: str
|
360 |
+
directory that contains all the images
|
361 |
+
dataset_name: str
|
362 |
+
the name that identifies a dataset, e.g. "densepose_coco_2014_train"
|
363 |
+
extra_annotation_keys: Optional[List[str]]
|
364 |
+
If provided, these keys are used to extract additional data from
|
365 |
+
the annotations.
|
366 |
+
"""
|
367 |
+
coco_api = _load_coco_annotations(PathManager.get_local_path(annotations_json_file))
|
368 |
+
_add_categories_metadata(dataset_name, coco_api.loadCats(coco_api.getCatIds()))
|
369 |
+
# sort indices for reproducible results
|
370 |
+
img_ids = sorted(coco_api.imgs.keys())
|
371 |
+
# imgs is a list of dicts, each looks something like:
|
372 |
+
# {'license': 4,
|
373 |
+
# 'url': 'http://farm6.staticflickr.com/5454/9413846304_881d5e5c3b_z.jpg',
|
374 |
+
# 'file_name': 'COCO_val2014_000000001268.jpg',
|
375 |
+
# 'height': 427,
|
376 |
+
# 'width': 640,
|
377 |
+
# 'date_captured': '2013-11-17 05:57:24',
|
378 |
+
# 'id': 1268}
|
379 |
+
imgs = coco_api.loadImgs(img_ids)
|
380 |
+
logger = logging.getLogger(__name__)
|
381 |
+
logger.info("Loaded {} images in COCO format from {}".format(len(imgs), annotations_json_file))
|
382 |
+
# anns is a list[list[dict]], where each dict is an annotation
|
383 |
+
# record for an object. The inner list enumerates the objects in an image
|
384 |
+
# and the outer list enumerates over images.
|
385 |
+
anns = [coco_api.imgToAnns[img_id] for img_id in img_ids]
|
386 |
+
_verify_annotations_have_unique_ids(annotations_json_file, anns)
|
387 |
+
dataset_records = _combine_images_with_annotations(dataset_name, image_root, imgs, anns)
|
388 |
+
return dataset_records
|
389 |
+
|
390 |
+
|
391 |
+
def register_dataset(dataset_data: CocoDatasetInfo, datasets_root: Optional[str] = None):
|
392 |
+
"""
|
393 |
+
Registers provided COCO DensePose dataset
|
394 |
+
|
395 |
+
Args:
|
396 |
+
dataset_data: CocoDatasetInfo
|
397 |
+
Dataset data
|
398 |
+
datasets_root: Optional[str]
|
399 |
+
Datasets root folder (default: None)
|
400 |
+
"""
|
401 |
+
annotations_fpath = maybe_prepend_base_path(datasets_root, dataset_data.annotations_fpath)
|
402 |
+
images_root = maybe_prepend_base_path(datasets_root, dataset_data.images_root)
|
403 |
+
|
404 |
+
def load_annotations():
|
405 |
+
return load_coco_json(
|
406 |
+
annotations_json_file=annotations_fpath,
|
407 |
+
image_root=images_root,
|
408 |
+
dataset_name=dataset_data.name,
|
409 |
+
)
|
410 |
+
|
411 |
+
DatasetCatalog.register(dataset_data.name, load_annotations)
|
412 |
+
MetadataCatalog.get(dataset_data.name).set(
|
413 |
+
json_file=annotations_fpath,
|
414 |
+
image_root=images_root,
|
415 |
+
**get_metadata(DENSEPOSE_METADATA_URL_PREFIX)
|
416 |
+
)
|
417 |
+
|
418 |
+
|
419 |
+
def register_datasets(
|
420 |
+
datasets_data: Iterable[CocoDatasetInfo], datasets_root: Optional[str] = None
|
421 |
+
):
|
422 |
+
"""
|
423 |
+
Registers provided COCO DensePose datasets
|
424 |
+
|
425 |
+
Args:
|
426 |
+
datasets_data: Iterable[CocoDatasetInfo]
|
427 |
+
An iterable of dataset datas
|
428 |
+
datasets_root: Optional[str]
|
429 |
+
Datasets root folder (default: None)
|
430 |
+
"""
|
431 |
+
for dataset_data in datasets_data:
|
432 |
+
register_dataset(dataset_data, datasets_root)
|
densepose/data/datasets/dataset_type.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from enum import Enum
|
4 |
+
|
5 |
+
|
6 |
+
class DatasetType(Enum):
|
7 |
+
"""
|
8 |
+
Dataset type, mostly used for datasets that contain data to bootstrap models on
|
9 |
+
"""
|
10 |
+
|
11 |
+
VIDEO_LIST = "video_list"
|
densepose/data/datasets/lvis.py
ADDED
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
import logging
|
3 |
+
import os
|
4 |
+
from typing import Any, Dict, Iterable, List, Optional
|
5 |
+
from fvcore.common.timer import Timer
|
6 |
+
|
7 |
+
from detectron2.data import DatasetCatalog, MetadataCatalog
|
8 |
+
from detectron2.data.datasets.lvis import get_lvis_instances_meta
|
9 |
+
from detectron2.structures import BoxMode
|
10 |
+
from detectron2.utils.file_io import PathManager
|
11 |
+
|
12 |
+
from ..utils import maybe_prepend_base_path
|
13 |
+
from .coco import (
|
14 |
+
DENSEPOSE_ALL_POSSIBLE_KEYS,
|
15 |
+
DENSEPOSE_METADATA_URL_PREFIX,
|
16 |
+
CocoDatasetInfo,
|
17 |
+
get_metadata,
|
18 |
+
)
|
19 |
+
|
20 |
+
DATASETS = [
|
21 |
+
CocoDatasetInfo(
|
22 |
+
name="densepose_lvis_v1_ds1_train_v1",
|
23 |
+
images_root="coco_",
|
24 |
+
annotations_fpath="lvis/densepose_lvis_v1_ds1_train_v1.json",
|
25 |
+
),
|
26 |
+
CocoDatasetInfo(
|
27 |
+
name="densepose_lvis_v1_ds1_val_v1",
|
28 |
+
images_root="coco_",
|
29 |
+
annotations_fpath="lvis/densepose_lvis_v1_ds1_val_v1.json",
|
30 |
+
),
|
31 |
+
CocoDatasetInfo(
|
32 |
+
name="densepose_lvis_v1_ds2_train_v1",
|
33 |
+
images_root="coco_",
|
34 |
+
annotations_fpath="lvis/densepose_lvis_v1_ds2_train_v1.json",
|
35 |
+
),
|
36 |
+
CocoDatasetInfo(
|
37 |
+
name="densepose_lvis_v1_ds2_val_v1",
|
38 |
+
images_root="coco_",
|
39 |
+
annotations_fpath="lvis/densepose_lvis_v1_ds2_val_v1.json",
|
40 |
+
),
|
41 |
+
CocoDatasetInfo(
|
42 |
+
name="densepose_lvis_v1_ds1_val_animals_100",
|
43 |
+
images_root="coco_",
|
44 |
+
annotations_fpath="lvis/densepose_lvis_v1_val_animals_100_v2.json",
|
45 |
+
),
|
46 |
+
]
|
47 |
+
|
48 |
+
|
49 |
+
def _load_lvis_annotations(json_file: str):
|
50 |
+
"""
|
51 |
+
Load COCO annotations from a JSON file
|
52 |
+
|
53 |
+
Args:
|
54 |
+
json_file: str
|
55 |
+
Path to the file to load annotations from
|
56 |
+
Returns:
|
57 |
+
Instance of `pycocotools.coco.COCO` that provides access to annotations
|
58 |
+
data
|
59 |
+
"""
|
60 |
+
from lvis import LVIS
|
61 |
+
|
62 |
+
json_file = PathManager.get_local_path(json_file)
|
63 |
+
logger = logging.getLogger(__name__)
|
64 |
+
timer = Timer()
|
65 |
+
lvis_api = LVIS(json_file)
|
66 |
+
if timer.seconds() > 1:
|
67 |
+
logger.info("Loading {} takes {:.2f} seconds.".format(json_file, timer.seconds()))
|
68 |
+
return lvis_api
|
69 |
+
|
70 |
+
|
71 |
+
def _add_categories_metadata(dataset_name: str) -> None:
|
72 |
+
metadict = get_lvis_instances_meta(dataset_name)
|
73 |
+
categories = metadict["thing_classes"]
|
74 |
+
metadata = MetadataCatalog.get(dataset_name)
|
75 |
+
metadata.categories = {i + 1: categories[i] for i in range(len(categories))}
|
76 |
+
logger = logging.getLogger(__name__)
|
77 |
+
logger.info(f"Dataset {dataset_name} has {len(categories)} categories")
|
78 |
+
|
79 |
+
|
80 |
+
def _verify_annotations_have_unique_ids(json_file: str, anns: List[List[Dict[str, Any]]]) -> None:
|
81 |
+
ann_ids = [ann["id"] for anns_per_image in anns for ann in anns_per_image]
|
82 |
+
assert len(set(ann_ids)) == len(ann_ids), "Annotation ids in '{}' are not unique!".format(
|
83 |
+
json_file
|
84 |
+
)
|
85 |
+
|
86 |
+
|
87 |
+
def _maybe_add_bbox(obj: Dict[str, Any], ann_dict: Dict[str, Any]) -> None:
|
88 |
+
if "bbox" not in ann_dict:
|
89 |
+
return
|
90 |
+
obj["bbox"] = ann_dict["bbox"]
|
91 |
+
obj["bbox_mode"] = BoxMode.XYWH_ABS
|
92 |
+
|
93 |
+
|
94 |
+
def _maybe_add_segm(obj: Dict[str, Any], ann_dict: Dict[str, Any]) -> None:
|
95 |
+
if "segmentation" not in ann_dict:
|
96 |
+
return
|
97 |
+
segm = ann_dict["segmentation"]
|
98 |
+
if not isinstance(segm, dict):
|
99 |
+
# filter out invalid polygons (< 3 points)
|
100 |
+
segm = [poly for poly in segm if len(poly) % 2 == 0 and len(poly) >= 6]
|
101 |
+
if len(segm) == 0:
|
102 |
+
return
|
103 |
+
obj["segmentation"] = segm
|
104 |
+
|
105 |
+
|
106 |
+
def _maybe_add_keypoints(obj: Dict[str, Any], ann_dict: Dict[str, Any]) -> None:
|
107 |
+
if "keypoints" not in ann_dict:
|
108 |
+
return
|
109 |
+
keypts = ann_dict["keypoints"] # list[int]
|
110 |
+
for idx, v in enumerate(keypts):
|
111 |
+
if idx % 3 != 2:
|
112 |
+
# COCO's segmentation coordinates are floating points in [0, H or W],
|
113 |
+
# but keypoint coordinates are integers in [0, H-1 or W-1]
|
114 |
+
# Therefore we assume the coordinates are "pixel indices" and
|
115 |
+
# add 0.5 to convert to floating point coordinates.
|
116 |
+
keypts[idx] = v + 0.5
|
117 |
+
obj["keypoints"] = keypts
|
118 |
+
|
119 |
+
|
120 |
+
def _maybe_add_densepose(obj: Dict[str, Any], ann_dict: Dict[str, Any]) -> None:
|
121 |
+
for key in DENSEPOSE_ALL_POSSIBLE_KEYS:
|
122 |
+
if key in ann_dict:
|
123 |
+
obj[key] = ann_dict[key]
|
124 |
+
|
125 |
+
|
126 |
+
def _combine_images_with_annotations(
|
127 |
+
dataset_name: str,
|
128 |
+
image_root: str,
|
129 |
+
img_datas: Iterable[Dict[str, Any]],
|
130 |
+
ann_datas: Iterable[Iterable[Dict[str, Any]]],
|
131 |
+
):
|
132 |
+
|
133 |
+
dataset_dicts = []
|
134 |
+
|
135 |
+
def get_file_name(img_root, img_dict):
|
136 |
+
# Determine the path including the split folder ("train2017", "val2017", "test2017") from
|
137 |
+
# the coco_url field. Example:
|
138 |
+
# 'coco_url': 'http://images.cocodataset.org/train2017/000000155379.jpg'
|
139 |
+
split_folder, file_name = img_dict["coco_url"].split("/")[-2:]
|
140 |
+
return os.path.join(img_root + split_folder, file_name)
|
141 |
+
|
142 |
+
for img_dict, ann_dicts in zip(img_datas, ann_datas):
|
143 |
+
record = {}
|
144 |
+
record["file_name"] = get_file_name(image_root, img_dict)
|
145 |
+
record["height"] = img_dict["height"]
|
146 |
+
record["width"] = img_dict["width"]
|
147 |
+
record["not_exhaustive_category_ids"] = img_dict.get("not_exhaustive_category_ids", [])
|
148 |
+
record["neg_category_ids"] = img_dict.get("neg_category_ids", [])
|
149 |
+
record["image_id"] = img_dict["id"]
|
150 |
+
record["dataset"] = dataset_name
|
151 |
+
|
152 |
+
objs = []
|
153 |
+
for ann_dict in ann_dicts:
|
154 |
+
assert ann_dict["image_id"] == record["image_id"]
|
155 |
+
obj = {}
|
156 |
+
_maybe_add_bbox(obj, ann_dict)
|
157 |
+
obj["iscrowd"] = ann_dict.get("iscrowd", 0)
|
158 |
+
obj["category_id"] = ann_dict["category_id"]
|
159 |
+
_maybe_add_segm(obj, ann_dict)
|
160 |
+
_maybe_add_keypoints(obj, ann_dict)
|
161 |
+
_maybe_add_densepose(obj, ann_dict)
|
162 |
+
objs.append(obj)
|
163 |
+
record["annotations"] = objs
|
164 |
+
dataset_dicts.append(record)
|
165 |
+
return dataset_dicts
|
166 |
+
|
167 |
+
|
168 |
+
def load_lvis_json(annotations_json_file: str, image_root: str, dataset_name: str):
|
169 |
+
"""
|
170 |
+
Loads a JSON file with annotations in LVIS instances format.
|
171 |
+
Replaces `detectron2.data.datasets.coco.load_lvis_json` to handle metadata
|
172 |
+
in a more flexible way. Postpones category mapping to a later stage to be
|
173 |
+
able to combine several datasets with different (but coherent) sets of
|
174 |
+
categories.
|
175 |
+
|
176 |
+
Args:
|
177 |
+
|
178 |
+
annotations_json_file: str
|
179 |
+
Path to the JSON file with annotations in COCO instances format.
|
180 |
+
image_root: str
|
181 |
+
directory that contains all the images
|
182 |
+
dataset_name: str
|
183 |
+
the name that identifies a dataset, e.g. "densepose_coco_2014_train"
|
184 |
+
extra_annotation_keys: Optional[List[str]]
|
185 |
+
If provided, these keys are used to extract additional data from
|
186 |
+
the annotations.
|
187 |
+
"""
|
188 |
+
lvis_api = _load_lvis_annotations(PathManager.get_local_path(annotations_json_file))
|
189 |
+
|
190 |
+
_add_categories_metadata(dataset_name)
|
191 |
+
|
192 |
+
# sort indices for reproducible results
|
193 |
+
img_ids = sorted(lvis_api.imgs.keys())
|
194 |
+
# imgs is a list of dicts, each looks something like:
|
195 |
+
# {'license': 4,
|
196 |
+
# 'url': 'http://farm6.staticflickr.com/5454/9413846304_881d5e5c3b_z.jpg',
|
197 |
+
# 'file_name': 'COCO_val2014_000000001268.jpg',
|
198 |
+
# 'height': 427,
|
199 |
+
# 'width': 640,
|
200 |
+
# 'date_captured': '2013-11-17 05:57:24',
|
201 |
+
# 'id': 1268}
|
202 |
+
imgs = lvis_api.load_imgs(img_ids)
|
203 |
+
logger = logging.getLogger(__name__)
|
204 |
+
logger.info("Loaded {} images in LVIS format from {}".format(len(imgs), annotations_json_file))
|
205 |
+
# anns is a list[list[dict]], where each dict is an annotation
|
206 |
+
# record for an object. The inner list enumerates the objects in an image
|
207 |
+
# and the outer list enumerates over images.
|
208 |
+
anns = [lvis_api.img_ann_map[img_id] for img_id in img_ids]
|
209 |
+
|
210 |
+
_verify_annotations_have_unique_ids(annotations_json_file, anns)
|
211 |
+
dataset_records = _combine_images_with_annotations(dataset_name, image_root, imgs, anns)
|
212 |
+
return dataset_records
|
213 |
+
|
214 |
+
|
215 |
+
def register_dataset(dataset_data: CocoDatasetInfo, datasets_root: Optional[str] = None) -> None:
|
216 |
+
"""
|
217 |
+
Registers provided LVIS DensePose dataset
|
218 |
+
|
219 |
+
Args:
|
220 |
+
dataset_data: CocoDatasetInfo
|
221 |
+
Dataset data
|
222 |
+
datasets_root: Optional[str]
|
223 |
+
Datasets root folder (default: None)
|
224 |
+
"""
|
225 |
+
annotations_fpath = maybe_prepend_base_path(datasets_root, dataset_data.annotations_fpath)
|
226 |
+
images_root = maybe_prepend_base_path(datasets_root, dataset_data.images_root)
|
227 |
+
|
228 |
+
def load_annotations():
|
229 |
+
return load_lvis_json(
|
230 |
+
annotations_json_file=annotations_fpath,
|
231 |
+
image_root=images_root,
|
232 |
+
dataset_name=dataset_data.name,
|
233 |
+
)
|
234 |
+
|
235 |
+
DatasetCatalog.register(dataset_data.name, load_annotations)
|
236 |
+
MetadataCatalog.get(dataset_data.name).set(
|
237 |
+
json_file=annotations_fpath,
|
238 |
+
image_root=images_root,
|
239 |
+
evaluator_type="lvis",
|
240 |
+
**get_metadata(DENSEPOSE_METADATA_URL_PREFIX),
|
241 |
+
)
|
242 |
+
|
243 |
+
|
244 |
+
def register_datasets(
|
245 |
+
datasets_data: Iterable[CocoDatasetInfo], datasets_root: Optional[str] = None
|
246 |
+
) -> None:
|
247 |
+
"""
|
248 |
+
Registers provided LVIS DensePose datasets
|
249 |
+
|
250 |
+
Args:
|
251 |
+
datasets_data: Iterable[CocoDatasetInfo]
|
252 |
+
An iterable of dataset datas
|
253 |
+
datasets_root: Optional[str]
|
254 |
+
Datasets root folder (default: None)
|
255 |
+
"""
|
256 |
+
for dataset_data in datasets_data:
|
257 |
+
register_dataset(dataset_data, datasets_root)
|
densepose/data/image_list_dataset.py
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
3 |
+
|
4 |
+
import logging
|
5 |
+
import numpy as np
|
6 |
+
from typing import Any, Callable, Dict, List, Optional, Union
|
7 |
+
import torch
|
8 |
+
from torch.utils.data.dataset import Dataset
|
9 |
+
|
10 |
+
from detectron2.data.detection_utils import read_image
|
11 |
+
|
12 |
+
ImageTransform = Callable[[torch.Tensor], torch.Tensor]
|
13 |
+
|
14 |
+
|
15 |
+
class ImageListDataset(Dataset):
|
16 |
+
"""
|
17 |
+
Dataset that provides images from a list.
|
18 |
+
"""
|
19 |
+
|
20 |
+
_EMPTY_IMAGE = torch.empty((0, 3, 1, 1))
|
21 |
+
|
22 |
+
def __init__(
|
23 |
+
self,
|
24 |
+
image_list: List[str],
|
25 |
+
category_list: Union[str, List[str], None] = None,
|
26 |
+
transform: Optional[ImageTransform] = None,
|
27 |
+
):
|
28 |
+
"""
|
29 |
+
Args:
|
30 |
+
image_list (List[str]): list of paths to image files
|
31 |
+
category_list (Union[str, List[str], None]): list of animal categories for
|
32 |
+
each image. If it is a string, or None, this applies to all images
|
33 |
+
"""
|
34 |
+
if type(category_list) == list:
|
35 |
+
self.category_list = category_list
|
36 |
+
else:
|
37 |
+
self.category_list = [category_list] * len(image_list)
|
38 |
+
assert len(image_list) == len(
|
39 |
+
self.category_list
|
40 |
+
), "length of image and category lists must be equal"
|
41 |
+
self.image_list = image_list
|
42 |
+
self.transform = transform
|
43 |
+
|
44 |
+
def __getitem__(self, idx: int) -> Dict[str, Any]:
|
45 |
+
"""
|
46 |
+
Gets selected images from the list
|
47 |
+
|
48 |
+
Args:
|
49 |
+
idx (int): video index in the video list file
|
50 |
+
Returns:
|
51 |
+
A dictionary containing two keys:
|
52 |
+
images (torch.Tensor): tensor of size [N, 3, H, W] (N = 1, or 0 for _EMPTY_IMAGE)
|
53 |
+
categories (List[str]): categories of the frames
|
54 |
+
"""
|
55 |
+
categories = [self.category_list[idx]]
|
56 |
+
fpath = self.image_list[idx]
|
57 |
+
transform = self.transform
|
58 |
+
|
59 |
+
try:
|
60 |
+
image = torch.from_numpy(np.ascontiguousarray(read_image(fpath, format="BGR")))
|
61 |
+
image = image.permute(2, 0, 1).unsqueeze(0).float() # HWC -> NCHW
|
62 |
+
if transform is not None:
|
63 |
+
image = transform(image)
|
64 |
+
return {"images": image, "categories": categories}
|
65 |
+
except (OSError, RuntimeError) as e:
|
66 |
+
logger = logging.getLogger(__name__)
|
67 |
+
logger.warning(f"Error opening image file container {fpath}: {e}")
|
68 |
+
|
69 |
+
return {"images": self._EMPTY_IMAGE, "categories": []}
|
70 |
+
|
71 |
+
def __len__(self):
|
72 |
+
return len(self.image_list)
|
densepose/data/inference_based_loader.py
ADDED
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
import random
|
4 |
+
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple
|
5 |
+
import torch
|
6 |
+
from torch import nn
|
7 |
+
|
8 |
+
SampledData = Any
|
9 |
+
ModelOutput = Any
|
10 |
+
|
11 |
+
|
12 |
+
def _grouper(iterable: Iterable[Any], n: int, fillvalue=None) -> Iterator[Tuple[Any]]:
|
13 |
+
"""
|
14 |
+
Group elements of an iterable by chunks of size `n`, e.g.
|
15 |
+
grouper(range(9), 4) ->
|
16 |
+
(0, 1, 2, 3), (4, 5, 6, 7), (8, None, None, None)
|
17 |
+
"""
|
18 |
+
it = iter(iterable)
|
19 |
+
while True:
|
20 |
+
values = []
|
21 |
+
for _ in range(n):
|
22 |
+
try:
|
23 |
+
value = next(it)
|
24 |
+
except StopIteration:
|
25 |
+
if values:
|
26 |
+
values.extend([fillvalue] * (n - len(values)))
|
27 |
+
yield tuple(values)
|
28 |
+
return
|
29 |
+
values.append(value)
|
30 |
+
yield tuple(values)
|
31 |
+
|
32 |
+
|
33 |
+
class ScoreBasedFilter:
|
34 |
+
"""
|
35 |
+
Filters entries in model output based on their scores
|
36 |
+
Discards all entries with score less than the specified minimum
|
37 |
+
"""
|
38 |
+
|
39 |
+
def __init__(self, min_score: float = 0.8):
|
40 |
+
self.min_score = min_score
|
41 |
+
|
42 |
+
def __call__(self, model_output: ModelOutput) -> ModelOutput:
|
43 |
+
for model_output_i in model_output:
|
44 |
+
instances = model_output_i["instances"]
|
45 |
+
if not instances.has("scores"):
|
46 |
+
continue
|
47 |
+
instances_filtered = instances[instances.scores >= self.min_score]
|
48 |
+
model_output_i["instances"] = instances_filtered
|
49 |
+
return model_output
|
50 |
+
|
51 |
+
|
52 |
+
class InferenceBasedLoader:
|
53 |
+
"""
|
54 |
+
Data loader based on results inferred by a model. Consists of:
|
55 |
+
- a data loader that provides batches of images
|
56 |
+
- a model that is used to infer the results
|
57 |
+
- a data sampler that converts inferred results to annotations
|
58 |
+
"""
|
59 |
+
|
60 |
+
def __init__(
|
61 |
+
self,
|
62 |
+
model: nn.Module,
|
63 |
+
data_loader: Iterable[List[Dict[str, Any]]],
|
64 |
+
data_sampler: Optional[Callable[[ModelOutput], List[SampledData]]] = None,
|
65 |
+
data_filter: Optional[Callable[[ModelOutput], ModelOutput]] = None,
|
66 |
+
shuffle: bool = True,
|
67 |
+
batch_size: int = 4,
|
68 |
+
inference_batch_size: int = 4,
|
69 |
+
drop_last: bool = False,
|
70 |
+
category_to_class_mapping: Optional[dict] = None,
|
71 |
+
):
|
72 |
+
"""
|
73 |
+
Constructor
|
74 |
+
|
75 |
+
Args:
|
76 |
+
model (torch.nn.Module): model used to produce data
|
77 |
+
data_loader (Iterable[List[Dict[str, Any]]]): iterable that provides
|
78 |
+
dictionaries with "images" and "categories" fields to perform inference on
|
79 |
+
data_sampler (Callable: ModelOutput -> SampledData): functor
|
80 |
+
that produces annotation data from inference results;
|
81 |
+
(optional, default: None)
|
82 |
+
data_filter (Callable: ModelOutput -> ModelOutput): filter
|
83 |
+
that selects model outputs for further processing
|
84 |
+
(optional, default: None)
|
85 |
+
shuffle (bool): if True, the input images get shuffled
|
86 |
+
batch_size (int): batch size for the produced annotation data
|
87 |
+
inference_batch_size (int): batch size for input images
|
88 |
+
drop_last (bool): if True, drop the last batch if it is undersized
|
89 |
+
category_to_class_mapping (dict): category to class mapping
|
90 |
+
"""
|
91 |
+
self.model = model
|
92 |
+
self.model.eval()
|
93 |
+
self.data_loader = data_loader
|
94 |
+
self.data_sampler = data_sampler
|
95 |
+
self.data_filter = data_filter
|
96 |
+
self.shuffle = shuffle
|
97 |
+
self.batch_size = batch_size
|
98 |
+
self.inference_batch_size = inference_batch_size
|
99 |
+
self.drop_last = drop_last
|
100 |
+
if category_to_class_mapping is not None:
|
101 |
+
self.category_to_class_mapping = category_to_class_mapping
|
102 |
+
else:
|
103 |
+
self.category_to_class_mapping = {}
|
104 |
+
|
105 |
+
def __iter__(self) -> Iterator[List[SampledData]]:
|
106 |
+
for batch in self.data_loader:
|
107 |
+
# batch : List[Dict[str: Tensor[N, C, H, W], str: Optional[str]]]
|
108 |
+
# images_batch : Tensor[N, C, H, W]
|
109 |
+
# image : Tensor[C, H, W]
|
110 |
+
images_and_categories = [
|
111 |
+
{"image": image, "category": category}
|
112 |
+
for element in batch
|
113 |
+
for image, category in zip(element["images"], element["categories"])
|
114 |
+
]
|
115 |
+
if not images_and_categories:
|
116 |
+
continue
|
117 |
+
if self.shuffle:
|
118 |
+
random.shuffle(images_and_categories)
|
119 |
+
yield from self._produce_data(images_and_categories) # pyre-ignore[6]
|
120 |
+
|
121 |
+
def _produce_data(
|
122 |
+
self, images_and_categories: List[Tuple[torch.Tensor, Optional[str]]]
|
123 |
+
) -> Iterator[List[SampledData]]:
|
124 |
+
"""
|
125 |
+
Produce batches of data from images
|
126 |
+
|
127 |
+
Args:
|
128 |
+
images_and_categories (List[Tuple[torch.Tensor, Optional[str]]]):
|
129 |
+
list of images and corresponding categories to process
|
130 |
+
|
131 |
+
Returns:
|
132 |
+
Iterator over batches of data sampled from model outputs
|
133 |
+
"""
|
134 |
+
data_batches: List[SampledData] = []
|
135 |
+
category_to_class_mapping = self.category_to_class_mapping
|
136 |
+
batched_images_and_categories = _grouper(images_and_categories, self.inference_batch_size)
|
137 |
+
for batch in batched_images_and_categories:
|
138 |
+
batch = [
|
139 |
+
{
|
140 |
+
"image": image_and_category["image"].to(self.model.device),
|
141 |
+
"category": image_and_category["category"],
|
142 |
+
}
|
143 |
+
for image_and_category in batch
|
144 |
+
if image_and_category is not None
|
145 |
+
]
|
146 |
+
if not batch:
|
147 |
+
continue
|
148 |
+
with torch.no_grad():
|
149 |
+
model_output = self.model(batch)
|
150 |
+
for model_output_i, batch_i in zip(model_output, batch):
|
151 |
+
assert len(batch_i["image"].shape) == 3
|
152 |
+
model_output_i["image"] = batch_i["image"]
|
153 |
+
instance_class = category_to_class_mapping.get(batch_i["category"], 0)
|
154 |
+
model_output_i["instances"].dataset_classes = torch.tensor(
|
155 |
+
[instance_class] * len(model_output_i["instances"])
|
156 |
+
)
|
157 |
+
model_output_filtered = (
|
158 |
+
model_output if self.data_filter is None else self.data_filter(model_output)
|
159 |
+
)
|
160 |
+
data = (
|
161 |
+
model_output_filtered
|
162 |
+
if self.data_sampler is None
|
163 |
+
else self.data_sampler(model_output_filtered)
|
164 |
+
)
|
165 |
+
for data_i in data:
|
166 |
+
if len(data_i["instances"]):
|
167 |
+
data_batches.append(data_i)
|
168 |
+
if len(data_batches) >= self.batch_size:
|
169 |
+
yield data_batches[: self.batch_size]
|
170 |
+
data_batches = data_batches[self.batch_size :]
|
171 |
+
if not self.drop_last and data_batches:
|
172 |
+
yield data_batches
|
densepose/data/meshes/__init__.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
|
2 |
+
|
3 |
+
from . import builtin
|
4 |
+
|
5 |
+
__all__ = [k for k in globals().keys() if "builtin" not in k and not k.startswith("_")]
|
densepose/data/meshes/builtin.py
ADDED
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
|
2 |
+
|
3 |
+
from .catalog import MeshInfo, register_meshes
|
4 |
+
|
5 |
+
DENSEPOSE_MESHES_DIR = "https://dl.fbaipublicfiles.com/densepose/meshes/"
|
6 |
+
|
7 |
+
MESHES = [
|
8 |
+
MeshInfo(
|
9 |
+
name="smpl_27554",
|
10 |
+
data="smpl_27554.pkl",
|
11 |
+
geodists="geodists/geodists_smpl_27554.pkl",
|
12 |
+
symmetry="symmetry/symmetry_smpl_27554.pkl",
|
13 |
+
texcoords="texcoords/texcoords_smpl_27554.pkl",
|
14 |
+
),
|
15 |
+
MeshInfo(
|
16 |
+
name="chimp_5029",
|
17 |
+
data="chimp_5029.pkl",
|
18 |
+
geodists="geodists/geodists_chimp_5029.pkl",
|
19 |
+
symmetry="symmetry/symmetry_chimp_5029.pkl",
|
20 |
+
texcoords="texcoords/texcoords_chimp_5029.pkl",
|
21 |
+
),
|
22 |
+
MeshInfo(
|
23 |
+
name="cat_5001",
|
24 |
+
data="cat_5001.pkl",
|
25 |
+
geodists="geodists/geodists_cat_5001.pkl",
|
26 |
+
symmetry="symmetry/symmetry_cat_5001.pkl",
|
27 |
+
texcoords="texcoords/texcoords_cat_5001.pkl",
|
28 |
+
),
|
29 |
+
MeshInfo(
|
30 |
+
name="cat_7466",
|
31 |
+
data="cat_7466.pkl",
|
32 |
+
geodists="geodists/geodists_cat_7466.pkl",
|
33 |
+
symmetry="symmetry/symmetry_cat_7466.pkl",
|
34 |
+
texcoords="texcoords/texcoords_cat_7466.pkl",
|
35 |
+
),
|
36 |
+
MeshInfo(
|
37 |
+
name="sheep_5004",
|
38 |
+
data="sheep_5004.pkl",
|
39 |
+
geodists="geodists/geodists_sheep_5004.pkl",
|
40 |
+
symmetry="symmetry/symmetry_sheep_5004.pkl",
|
41 |
+
texcoords="texcoords/texcoords_sheep_5004.pkl",
|
42 |
+
),
|
43 |
+
MeshInfo(
|
44 |
+
name="zebra_5002",
|
45 |
+
data="zebra_5002.pkl",
|
46 |
+
geodists="geodists/geodists_zebra_5002.pkl",
|
47 |
+
symmetry="symmetry/symmetry_zebra_5002.pkl",
|
48 |
+
texcoords="texcoords/texcoords_zebra_5002.pkl",
|
49 |
+
),
|
50 |
+
MeshInfo(
|
51 |
+
name="horse_5004",
|
52 |
+
data="horse_5004.pkl",
|
53 |
+
geodists="geodists/geodists_horse_5004.pkl",
|
54 |
+
symmetry="symmetry/symmetry_horse_5004.pkl",
|
55 |
+
texcoords="texcoords/texcoords_zebra_5002.pkl",
|
56 |
+
),
|
57 |
+
MeshInfo(
|
58 |
+
name="giraffe_5002",
|
59 |
+
data="giraffe_5002.pkl",
|
60 |
+
geodists="geodists/geodists_giraffe_5002.pkl",
|
61 |
+
symmetry="symmetry/symmetry_giraffe_5002.pkl",
|
62 |
+
texcoords="texcoords/texcoords_giraffe_5002.pkl",
|
63 |
+
),
|
64 |
+
MeshInfo(
|
65 |
+
name="elephant_5002",
|
66 |
+
data="elephant_5002.pkl",
|
67 |
+
geodists="geodists/geodists_elephant_5002.pkl",
|
68 |
+
symmetry="symmetry/symmetry_elephant_5002.pkl",
|
69 |
+
texcoords="texcoords/texcoords_elephant_5002.pkl",
|
70 |
+
),
|
71 |
+
MeshInfo(
|
72 |
+
name="dog_5002",
|
73 |
+
data="dog_5002.pkl",
|
74 |
+
geodists="geodists/geodists_dog_5002.pkl",
|
75 |
+
symmetry="symmetry/symmetry_dog_5002.pkl",
|
76 |
+
texcoords="texcoords/texcoords_dog_5002.pkl",
|
77 |
+
),
|
78 |
+
MeshInfo(
|
79 |
+
name="dog_7466",
|
80 |
+
data="dog_7466.pkl",
|
81 |
+
geodists="geodists/geodists_dog_7466.pkl",
|
82 |
+
symmetry="symmetry/symmetry_dog_7466.pkl",
|
83 |
+
texcoords="texcoords/texcoords_dog_7466.pkl",
|
84 |
+
),
|
85 |
+
MeshInfo(
|
86 |
+
name="cow_5002",
|
87 |
+
data="cow_5002.pkl",
|
88 |
+
geodists="geodists/geodists_cow_5002.pkl",
|
89 |
+
symmetry="symmetry/symmetry_cow_5002.pkl",
|
90 |
+
texcoords="texcoords/texcoords_cow_5002.pkl",
|
91 |
+
),
|
92 |
+
MeshInfo(
|
93 |
+
name="bear_4936",
|
94 |
+
data="bear_4936.pkl",
|
95 |
+
geodists="geodists/geodists_bear_4936.pkl",
|
96 |
+
symmetry="symmetry/symmetry_bear_4936.pkl",
|
97 |
+
texcoords="texcoords/texcoords_bear_4936.pkl",
|
98 |
+
),
|
99 |
+
]
|
100 |
+
|
101 |
+
register_meshes(MESHES, DENSEPOSE_MESHES_DIR)
|
densepose/data/meshes/catalog.py
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
|
2 |
+
|
3 |
+
import logging
|
4 |
+
from collections import UserDict
|
5 |
+
from dataclasses import dataclass
|
6 |
+
from typing import Iterable, Optional
|
7 |
+
|
8 |
+
from ..utils import maybe_prepend_base_path
|
9 |
+
|
10 |
+
|
11 |
+
@dataclass
|
12 |
+
class MeshInfo:
|
13 |
+
name: str
|
14 |
+
data: str
|
15 |
+
geodists: Optional[str] = None
|
16 |
+
symmetry: Optional[str] = None
|
17 |
+
texcoords: Optional[str] = None
|
18 |
+
|
19 |
+
|
20 |
+
class _MeshCatalog(UserDict):
|
21 |
+
def __init__(self, *args, **kwargs):
|
22 |
+
super().__init__(*args, **kwargs)
|
23 |
+
self.mesh_ids = {}
|
24 |
+
self.mesh_names = {}
|
25 |
+
self.max_mesh_id = -1
|
26 |
+
|
27 |
+
def __setitem__(self, key, value):
|
28 |
+
if key in self:
|
29 |
+
logger = logging.getLogger(__name__)
|
30 |
+
logger.warning(
|
31 |
+
f"Overwriting mesh catalog entry '{key}': old value {self[key]}"
|
32 |
+
f", new value {value}"
|
33 |
+
)
|
34 |
+
mesh_id = self.mesh_ids[key]
|
35 |
+
else:
|
36 |
+
self.max_mesh_id += 1
|
37 |
+
mesh_id = self.max_mesh_id
|
38 |
+
super().__setitem__(key, value)
|
39 |
+
self.mesh_ids[key] = mesh_id
|
40 |
+
self.mesh_names[mesh_id] = key
|
41 |
+
|
42 |
+
def get_mesh_id(self, shape_name: str) -> int:
|
43 |
+
return self.mesh_ids[shape_name]
|
44 |
+
|
45 |
+
def get_mesh_name(self, mesh_id: int) -> str:
|
46 |
+
return self.mesh_names[mesh_id]
|
47 |
+
|
48 |
+
|
49 |
+
MeshCatalog = _MeshCatalog()
|
50 |
+
|
51 |
+
|
52 |
+
def register_mesh(mesh_info: MeshInfo, base_path: Optional[str]) -> None:
|
53 |
+
geodists, symmetry, texcoords = mesh_info.geodists, mesh_info.symmetry, mesh_info.texcoords
|
54 |
+
if geodists:
|
55 |
+
geodists = maybe_prepend_base_path(base_path, geodists)
|
56 |
+
if symmetry:
|
57 |
+
symmetry = maybe_prepend_base_path(base_path, symmetry)
|
58 |
+
if texcoords:
|
59 |
+
texcoords = maybe_prepend_base_path(base_path, texcoords)
|
60 |
+
MeshCatalog[mesh_info.name] = MeshInfo(
|
61 |
+
name=mesh_info.name,
|
62 |
+
data=maybe_prepend_base_path(base_path, mesh_info.data),
|
63 |
+
geodists=geodists,
|
64 |
+
symmetry=symmetry,
|
65 |
+
texcoords=texcoords,
|
66 |
+
)
|
67 |
+
|
68 |
+
|
69 |
+
def register_meshes(mesh_infos: Iterable[MeshInfo], base_path: Optional[str]) -> None:
|
70 |
+
for mesh_info in mesh_infos:
|
71 |
+
register_mesh(mesh_info, base_path)
|
densepose/data/samplers/__init__.py
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from .densepose_uniform import DensePoseUniformSampler
|
4 |
+
from .densepose_confidence_based import DensePoseConfidenceBasedSampler
|
5 |
+
from .densepose_cse_uniform import DensePoseCSEUniformSampler
|
6 |
+
from .densepose_cse_confidence_based import DensePoseCSEConfidenceBasedSampler
|
7 |
+
from .mask_from_densepose import MaskFromDensePoseSampler
|
8 |
+
from .prediction_to_gt import PredictionToGroundTruthSampler
|
densepose/data/samplers/densepose_base.py
ADDED
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from typing import Any, Dict, List, Tuple
|
4 |
+
import torch
|
5 |
+
from torch.nn import functional as F
|
6 |
+
|
7 |
+
from detectron2.structures import BoxMode, Instances
|
8 |
+
|
9 |
+
from densepose.converters import ToChartResultConverter
|
10 |
+
from densepose.converters.base import IntTupleBox, make_int_box
|
11 |
+
from densepose.structures import DensePoseDataRelative, DensePoseList
|
12 |
+
|
13 |
+
|
14 |
+
class DensePoseBaseSampler:
|
15 |
+
"""
|
16 |
+
Base DensePose sampler to produce DensePose data from DensePose predictions.
|
17 |
+
Samples for each class are drawn according to some distribution over all pixels estimated
|
18 |
+
to belong to that class.
|
19 |
+
"""
|
20 |
+
|
21 |
+
def __init__(self, count_per_class: int = 8):
|
22 |
+
"""
|
23 |
+
Constructor
|
24 |
+
|
25 |
+
Args:
|
26 |
+
count_per_class (int): the sampler produces at most `count_per_class`
|
27 |
+
samples for each category
|
28 |
+
"""
|
29 |
+
self.count_per_class = count_per_class
|
30 |
+
|
31 |
+
def __call__(self, instances: Instances) -> DensePoseList:
|
32 |
+
"""
|
33 |
+
Convert DensePose predictions (an instance of `DensePoseChartPredictorOutput`)
|
34 |
+
into DensePose annotations data (an instance of `DensePoseList`)
|
35 |
+
"""
|
36 |
+
boxes_xyxy_abs = instances.pred_boxes.tensor.clone().cpu()
|
37 |
+
boxes_xywh_abs = BoxMode.convert(boxes_xyxy_abs, BoxMode.XYXY_ABS, BoxMode.XYWH_ABS)
|
38 |
+
dp_datas = []
|
39 |
+
for i in range(len(boxes_xywh_abs)):
|
40 |
+
annotation_i = self._sample(instances[i], make_int_box(boxes_xywh_abs[i]))
|
41 |
+
annotation_i[DensePoseDataRelative.S_KEY] = self._resample_mask( # pyre-ignore[6]
|
42 |
+
instances[i].pred_densepose
|
43 |
+
)
|
44 |
+
dp_datas.append(DensePoseDataRelative(annotation_i))
|
45 |
+
# create densepose annotations on CPU
|
46 |
+
dp_list = DensePoseList(dp_datas, boxes_xyxy_abs, instances.image_size)
|
47 |
+
return dp_list
|
48 |
+
|
49 |
+
def _sample(self, instance: Instances, bbox_xywh: IntTupleBox) -> Dict[str, List[Any]]:
|
50 |
+
"""
|
51 |
+
Sample DensPoseDataRelative from estimation results
|
52 |
+
"""
|
53 |
+
labels, dp_result = self._produce_labels_and_results(instance)
|
54 |
+
annotation = {
|
55 |
+
DensePoseDataRelative.X_KEY: [],
|
56 |
+
DensePoseDataRelative.Y_KEY: [],
|
57 |
+
DensePoseDataRelative.U_KEY: [],
|
58 |
+
DensePoseDataRelative.V_KEY: [],
|
59 |
+
DensePoseDataRelative.I_KEY: [],
|
60 |
+
}
|
61 |
+
n, h, w = dp_result.shape
|
62 |
+
for part_id in range(1, DensePoseDataRelative.N_PART_LABELS + 1):
|
63 |
+
# indices - tuple of 3 1D tensors of size k
|
64 |
+
# 0: index along the first dimension N
|
65 |
+
# 1: index along H dimension
|
66 |
+
# 2: index along W dimension
|
67 |
+
indices = torch.nonzero(labels.expand(n, h, w) == part_id, as_tuple=True)
|
68 |
+
# values - an array of size [n, k]
|
69 |
+
# n: number of channels (U, V, confidences)
|
70 |
+
# k: number of points labeled with part_id
|
71 |
+
values = dp_result[indices].view(n, -1)
|
72 |
+
k = values.shape[1]
|
73 |
+
count = min(self.count_per_class, k)
|
74 |
+
if count <= 0:
|
75 |
+
continue
|
76 |
+
index_sample = self._produce_index_sample(values, count)
|
77 |
+
sampled_values = values[:, index_sample]
|
78 |
+
sampled_y = indices[1][index_sample] + 0.5
|
79 |
+
sampled_x = indices[2][index_sample] + 0.5
|
80 |
+
# prepare / normalize data
|
81 |
+
x = (sampled_x / w * 256.0).cpu().tolist()
|
82 |
+
y = (sampled_y / h * 256.0).cpu().tolist()
|
83 |
+
u = sampled_values[0].clamp(0, 1).cpu().tolist()
|
84 |
+
v = sampled_values[1].clamp(0, 1).cpu().tolist()
|
85 |
+
fine_segm_labels = [part_id] * count
|
86 |
+
# extend annotations
|
87 |
+
annotation[DensePoseDataRelative.X_KEY].extend(x)
|
88 |
+
annotation[DensePoseDataRelative.Y_KEY].extend(y)
|
89 |
+
annotation[DensePoseDataRelative.U_KEY].extend(u)
|
90 |
+
annotation[DensePoseDataRelative.V_KEY].extend(v)
|
91 |
+
annotation[DensePoseDataRelative.I_KEY].extend(fine_segm_labels)
|
92 |
+
return annotation
|
93 |
+
|
94 |
+
def _produce_index_sample(self, values: torch.Tensor, count: int):
|
95 |
+
"""
|
96 |
+
Abstract method to produce a sample of indices to select data
|
97 |
+
To be implemented in descendants
|
98 |
+
|
99 |
+
Args:
|
100 |
+
values (torch.Tensor): an array of size [n, k] that contains
|
101 |
+
estimated values (U, V, confidences);
|
102 |
+
n: number of channels (U, V, confidences)
|
103 |
+
k: number of points labeled with part_id
|
104 |
+
count (int): number of samples to produce, should be positive and <= k
|
105 |
+
|
106 |
+
Return:
|
107 |
+
list(int): indices of values (along axis 1) selected as a sample
|
108 |
+
"""
|
109 |
+
raise NotImplementedError
|
110 |
+
|
111 |
+
def _produce_labels_and_results(self, instance: Instances) -> Tuple[torch.Tensor, torch.Tensor]:
|
112 |
+
"""
|
113 |
+
Method to get labels and DensePose results from an instance
|
114 |
+
|
115 |
+
Args:
|
116 |
+
instance (Instances): an instance of `DensePoseChartPredictorOutput`
|
117 |
+
|
118 |
+
Return:
|
119 |
+
labels (torch.Tensor): shape [H, W], DensePose segmentation labels
|
120 |
+
dp_result (torch.Tensor): shape [2, H, W], stacked DensePose results u and v
|
121 |
+
"""
|
122 |
+
converter = ToChartResultConverter
|
123 |
+
chart_result = converter.convert(instance.pred_densepose, instance.pred_boxes)
|
124 |
+
labels, dp_result = chart_result.labels.cpu(), chart_result.uv.cpu()
|
125 |
+
return labels, dp_result
|
126 |
+
|
127 |
+
def _resample_mask(self, output: Any) -> torch.Tensor:
|
128 |
+
"""
|
129 |
+
Convert DensePose predictor output to segmentation annotation - tensors of size
|
130 |
+
(256, 256) and type `int64`.
|
131 |
+
|
132 |
+
Args:
|
133 |
+
output: DensePose predictor output with the following attributes:
|
134 |
+
- coarse_segm: tensor of size [N, D, H, W] with unnormalized coarse
|
135 |
+
segmentation scores
|
136 |
+
- fine_segm: tensor of size [N, C, H, W] with unnormalized fine
|
137 |
+
segmentation scores
|
138 |
+
Return:
|
139 |
+
Tensor of size (S, S) and type `int64` with coarse segmentation annotations,
|
140 |
+
where S = DensePoseDataRelative.MASK_SIZE
|
141 |
+
"""
|
142 |
+
sz = DensePoseDataRelative.MASK_SIZE
|
143 |
+
S = (
|
144 |
+
F.interpolate(output.coarse_segm, (sz, sz), mode="bilinear", align_corners=False)
|
145 |
+
.argmax(dim=1)
|
146 |
+
.long()
|
147 |
+
)
|
148 |
+
I = (
|
149 |
+
(
|
150 |
+
F.interpolate(
|
151 |
+
output.fine_segm,
|
152 |
+
(sz, sz),
|
153 |
+
mode="bilinear",
|
154 |
+
align_corners=False,
|
155 |
+
).argmax(dim=1)
|
156 |
+
* (S > 0).long()
|
157 |
+
)
|
158 |
+
.squeeze()
|
159 |
+
.cpu()
|
160 |
+
)
|
161 |
+
# Map fine segmentation results to coarse segmentation ground truth
|
162 |
+
# TODO: extract this into separate classes
|
163 |
+
# coarse segmentation: 1 = Torso, 2 = Right Hand, 3 = Left Hand,
|
164 |
+
# 4 = Left Foot, 5 = Right Foot, 6 = Upper Leg Right, 7 = Upper Leg Left,
|
165 |
+
# 8 = Lower Leg Right, 9 = Lower Leg Left, 10 = Upper Arm Left,
|
166 |
+
# 11 = Upper Arm Right, 12 = Lower Arm Left, 13 = Lower Arm Right,
|
167 |
+
# 14 = Head
|
168 |
+
# fine segmentation: 1, 2 = Torso, 3 = Right Hand, 4 = Left Hand,
|
169 |
+
# 5 = Left Foot, 6 = Right Foot, 7, 9 = Upper Leg Right,
|
170 |
+
# 8, 10 = Upper Leg Left, 11, 13 = Lower Leg Right,
|
171 |
+
# 12, 14 = Lower Leg Left, 15, 17 = Upper Arm Left,
|
172 |
+
# 16, 18 = Upper Arm Right, 19, 21 = Lower Arm Left,
|
173 |
+
# 20, 22 = Lower Arm Right, 23, 24 = Head
|
174 |
+
FINE_TO_COARSE_SEGMENTATION = {
|
175 |
+
1: 1,
|
176 |
+
2: 1,
|
177 |
+
3: 2,
|
178 |
+
4: 3,
|
179 |
+
5: 4,
|
180 |
+
6: 5,
|
181 |
+
7: 6,
|
182 |
+
8: 7,
|
183 |
+
9: 6,
|
184 |
+
10: 7,
|
185 |
+
11: 8,
|
186 |
+
12: 9,
|
187 |
+
13: 8,
|
188 |
+
14: 9,
|
189 |
+
15: 10,
|
190 |
+
16: 11,
|
191 |
+
17: 10,
|
192 |
+
18: 11,
|
193 |
+
19: 12,
|
194 |
+
20: 13,
|
195 |
+
21: 12,
|
196 |
+
22: 13,
|
197 |
+
23: 14,
|
198 |
+
24: 14,
|
199 |
+
}
|
200 |
+
mask = torch.zeros((sz, sz), dtype=torch.int64, device=torch.device("cpu"))
|
201 |
+
for i in range(DensePoseDataRelative.N_PART_LABELS):
|
202 |
+
mask[I == i + 1] = FINE_TO_COARSE_SEGMENTATION[i + 1]
|
203 |
+
return mask
|
densepose/data/samplers/densepose_confidence_based.py
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
import random
|
4 |
+
from typing import Optional, Tuple
|
5 |
+
import torch
|
6 |
+
|
7 |
+
from densepose.converters import ToChartResultConverterWithConfidences
|
8 |
+
|
9 |
+
from .densepose_base import DensePoseBaseSampler
|
10 |
+
|
11 |
+
|
12 |
+
class DensePoseConfidenceBasedSampler(DensePoseBaseSampler):
|
13 |
+
"""
|
14 |
+
Samples DensePose data from DensePose predictions.
|
15 |
+
Samples for each class are drawn using confidence value estimates.
|
16 |
+
"""
|
17 |
+
|
18 |
+
def __init__(
|
19 |
+
self,
|
20 |
+
confidence_channel: str,
|
21 |
+
count_per_class: int = 8,
|
22 |
+
search_count_multiplier: Optional[float] = None,
|
23 |
+
search_proportion: Optional[float] = None,
|
24 |
+
):
|
25 |
+
"""
|
26 |
+
Constructor
|
27 |
+
|
28 |
+
Args:
|
29 |
+
confidence_channel (str): confidence channel to use for sampling;
|
30 |
+
possible values:
|
31 |
+
"sigma_2": confidences for UV values
|
32 |
+
"fine_segm_confidence": confidences for fine segmentation
|
33 |
+
"coarse_segm_confidence": confidences for coarse segmentation
|
34 |
+
(default: "sigma_2")
|
35 |
+
count_per_class (int): the sampler produces at most `count_per_class`
|
36 |
+
samples for each category (default: 8)
|
37 |
+
search_count_multiplier (float or None): if not None, the total number
|
38 |
+
of the most confident estimates of a given class to consider is
|
39 |
+
defined as `min(search_count_multiplier * count_per_class, N)`,
|
40 |
+
where `N` is the total number of estimates of the class; cannot be
|
41 |
+
specified together with `search_proportion` (default: None)
|
42 |
+
search_proportion (float or None): if not None, the total number of the
|
43 |
+
of the most confident estimates of a given class to consider is
|
44 |
+
defined as `min(max(search_proportion * N, count_per_class), N)`,
|
45 |
+
where `N` is the total number of estimates of the class; cannot be
|
46 |
+
specified together with `search_count_multiplier` (default: None)
|
47 |
+
"""
|
48 |
+
super().__init__(count_per_class)
|
49 |
+
self.confidence_channel = confidence_channel
|
50 |
+
self.search_count_multiplier = search_count_multiplier
|
51 |
+
self.search_proportion = search_proportion
|
52 |
+
assert (search_count_multiplier is None) or (search_proportion is None), (
|
53 |
+
f"Cannot specify both search_count_multiplier (={search_count_multiplier})"
|
54 |
+
f"and search_proportion (={search_proportion})"
|
55 |
+
)
|
56 |
+
|
57 |
+
def _produce_index_sample(self, values: torch.Tensor, count: int):
|
58 |
+
"""
|
59 |
+
Produce a sample of indices to select data based on confidences
|
60 |
+
|
61 |
+
Args:
|
62 |
+
values (torch.Tensor): an array of size [n, k] that contains
|
63 |
+
estimated values (U, V, confidences);
|
64 |
+
n: number of channels (U, V, confidences)
|
65 |
+
k: number of points labeled with part_id
|
66 |
+
count (int): number of samples to produce, should be positive and <= k
|
67 |
+
|
68 |
+
Return:
|
69 |
+
list(int): indices of values (along axis 1) selected as a sample
|
70 |
+
"""
|
71 |
+
k = values.shape[1]
|
72 |
+
if k == count:
|
73 |
+
index_sample = list(range(k))
|
74 |
+
else:
|
75 |
+
# take the best count * search_count_multiplier pixels,
|
76 |
+
# sample from them uniformly
|
77 |
+
# (here best = smallest variance)
|
78 |
+
_, sorted_confidence_indices = torch.sort(values[2])
|
79 |
+
if self.search_count_multiplier is not None:
|
80 |
+
search_count = min(int(count * self.search_count_multiplier), k)
|
81 |
+
elif self.search_proportion is not None:
|
82 |
+
search_count = min(max(int(k * self.search_proportion), count), k)
|
83 |
+
else:
|
84 |
+
search_count = min(count, k)
|
85 |
+
sample_from_top = random.sample(range(search_count), count)
|
86 |
+
index_sample = sorted_confidence_indices[:search_count][sample_from_top]
|
87 |
+
return index_sample
|
88 |
+
|
89 |
+
def _produce_labels_and_results(self, instance) -> Tuple[torch.Tensor, torch.Tensor]:
|
90 |
+
"""
|
91 |
+
Method to get labels and DensePose results from an instance, with confidences
|
92 |
+
|
93 |
+
Args:
|
94 |
+
instance (Instances): an instance of `DensePoseChartPredictorOutputWithConfidences`
|
95 |
+
|
96 |
+
Return:
|
97 |
+
labels (torch.Tensor): shape [H, W], DensePose segmentation labels
|
98 |
+
dp_result (torch.Tensor): shape [3, H, W], DensePose results u and v
|
99 |
+
stacked with the confidence channel
|
100 |
+
"""
|
101 |
+
converter = ToChartResultConverterWithConfidences
|
102 |
+
chart_result = converter.convert(instance.pred_densepose, instance.pred_boxes)
|
103 |
+
labels, dp_result = chart_result.labels.cpu(), chart_result.uv.cpu()
|
104 |
+
dp_result = torch.cat(
|
105 |
+
(dp_result, getattr(chart_result, self.confidence_channel)[None].cpu())
|
106 |
+
)
|
107 |
+
|
108 |
+
return labels, dp_result
|
densepose/data/samplers/densepose_cse_base.py
ADDED
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from typing import Any, Dict, List, Tuple
|
4 |
+
import torch
|
5 |
+
from torch.nn import functional as F
|
6 |
+
|
7 |
+
from detectron2.config import CfgNode
|
8 |
+
from detectron2.structures import Instances
|
9 |
+
|
10 |
+
from densepose.converters.base import IntTupleBox
|
11 |
+
from densepose.data.utils import get_class_to_mesh_name_mapping
|
12 |
+
from densepose.modeling.cse.utils import squared_euclidean_distance_matrix
|
13 |
+
from densepose.structures import DensePoseDataRelative
|
14 |
+
|
15 |
+
from .densepose_base import DensePoseBaseSampler
|
16 |
+
|
17 |
+
|
18 |
+
class DensePoseCSEBaseSampler(DensePoseBaseSampler):
|
19 |
+
"""
|
20 |
+
Base DensePose sampler to produce DensePose data from DensePose predictions.
|
21 |
+
Samples for each class are drawn according to some distribution over all pixels estimated
|
22 |
+
to belong to that class.
|
23 |
+
"""
|
24 |
+
|
25 |
+
def __init__(
|
26 |
+
self,
|
27 |
+
cfg: CfgNode,
|
28 |
+
use_gt_categories: bool,
|
29 |
+
embedder: torch.nn.Module,
|
30 |
+
count_per_class: int = 8,
|
31 |
+
):
|
32 |
+
"""
|
33 |
+
Constructor
|
34 |
+
|
35 |
+
Args:
|
36 |
+
cfg (CfgNode): the config of the model
|
37 |
+
embedder (torch.nn.Module): necessary to compute mesh vertex embeddings
|
38 |
+
count_per_class (int): the sampler produces at most `count_per_class`
|
39 |
+
samples for each category
|
40 |
+
"""
|
41 |
+
super().__init__(count_per_class)
|
42 |
+
self.embedder = embedder
|
43 |
+
self.class_to_mesh_name = get_class_to_mesh_name_mapping(cfg)
|
44 |
+
self.use_gt_categories = use_gt_categories
|
45 |
+
|
46 |
+
def _sample(self, instance: Instances, bbox_xywh: IntTupleBox) -> Dict[str, List[Any]]:
|
47 |
+
"""
|
48 |
+
Sample DensPoseDataRelative from estimation results
|
49 |
+
"""
|
50 |
+
if self.use_gt_categories:
|
51 |
+
instance_class = instance.dataset_classes.tolist()[0]
|
52 |
+
else:
|
53 |
+
instance_class = instance.pred_classes.tolist()[0]
|
54 |
+
mesh_name = self.class_to_mesh_name[instance_class]
|
55 |
+
|
56 |
+
annotation = {
|
57 |
+
DensePoseDataRelative.X_KEY: [],
|
58 |
+
DensePoseDataRelative.Y_KEY: [],
|
59 |
+
DensePoseDataRelative.VERTEX_IDS_KEY: [],
|
60 |
+
DensePoseDataRelative.MESH_NAME_KEY: mesh_name,
|
61 |
+
}
|
62 |
+
|
63 |
+
mask, embeddings, other_values = self._produce_mask_and_results(instance, bbox_xywh)
|
64 |
+
indices = torch.nonzero(mask, as_tuple=True)
|
65 |
+
selected_embeddings = embeddings.permute(1, 2, 0)[indices].cpu()
|
66 |
+
values = other_values[:, indices[0], indices[1]]
|
67 |
+
k = values.shape[1]
|
68 |
+
|
69 |
+
count = min(self.count_per_class, k)
|
70 |
+
if count <= 0:
|
71 |
+
return annotation
|
72 |
+
|
73 |
+
index_sample = self._produce_index_sample(values, count)
|
74 |
+
closest_vertices = squared_euclidean_distance_matrix(
|
75 |
+
selected_embeddings[index_sample], self.embedder(mesh_name)
|
76 |
+
)
|
77 |
+
closest_vertices = torch.argmin(closest_vertices, dim=1)
|
78 |
+
|
79 |
+
sampled_y = indices[0][index_sample] + 0.5
|
80 |
+
sampled_x = indices[1][index_sample] + 0.5
|
81 |
+
# prepare / normalize data
|
82 |
+
_, _, w, h = bbox_xywh
|
83 |
+
x = (sampled_x / w * 256.0).cpu().tolist()
|
84 |
+
y = (sampled_y / h * 256.0).cpu().tolist()
|
85 |
+
# extend annotations
|
86 |
+
annotation[DensePoseDataRelative.X_KEY].extend(x)
|
87 |
+
annotation[DensePoseDataRelative.Y_KEY].extend(y)
|
88 |
+
annotation[DensePoseDataRelative.VERTEX_IDS_KEY].extend(closest_vertices.cpu().tolist())
|
89 |
+
return annotation
|
90 |
+
|
91 |
+
def _produce_mask_and_results(
|
92 |
+
self, instance: Instances, bbox_xywh: IntTupleBox
|
93 |
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
94 |
+
"""
|
95 |
+
Method to get labels and DensePose results from an instance
|
96 |
+
|
97 |
+
Args:
|
98 |
+
instance (Instances): an instance of `DensePoseEmbeddingPredictorOutput`
|
99 |
+
bbox_xywh (IntTupleBox): the corresponding bounding box
|
100 |
+
|
101 |
+
Return:
|
102 |
+
mask (torch.Tensor): shape [H, W], DensePose segmentation mask
|
103 |
+
embeddings (Tuple[torch.Tensor]): a tensor of shape [D, H, W],
|
104 |
+
DensePose CSE Embeddings
|
105 |
+
other_values (Tuple[torch.Tensor]): a tensor of shape [0, H, W],
|
106 |
+
for potential other values
|
107 |
+
"""
|
108 |
+
densepose_output = instance.pred_densepose
|
109 |
+
S = densepose_output.coarse_segm
|
110 |
+
E = densepose_output.embedding
|
111 |
+
_, _, w, h = bbox_xywh
|
112 |
+
embeddings = F.interpolate(E, size=(h, w), mode="bilinear")[0]
|
113 |
+
coarse_segm_resized = F.interpolate(S, size=(h, w), mode="bilinear")[0]
|
114 |
+
mask = coarse_segm_resized.argmax(0) > 0
|
115 |
+
other_values = torch.empty((0, h, w), device=E.device)
|
116 |
+
return mask, embeddings, other_values
|
117 |
+
|
118 |
+
def _resample_mask(self, output: Any) -> torch.Tensor:
|
119 |
+
"""
|
120 |
+
Convert DensePose predictor output to segmentation annotation - tensors of size
|
121 |
+
(256, 256) and type `int64`.
|
122 |
+
|
123 |
+
Args:
|
124 |
+
output: DensePose predictor output with the following attributes:
|
125 |
+
- coarse_segm: tensor of size [N, D, H, W] with unnormalized coarse
|
126 |
+
segmentation scores
|
127 |
+
Return:
|
128 |
+
Tensor of size (S, S) and type `int64` with coarse segmentation annotations,
|
129 |
+
where S = DensePoseDataRelative.MASK_SIZE
|
130 |
+
"""
|
131 |
+
sz = DensePoseDataRelative.MASK_SIZE
|
132 |
+
mask = (
|
133 |
+
F.interpolate(output.coarse_segm, (sz, sz), mode="bilinear", align_corners=False)
|
134 |
+
.argmax(dim=1)
|
135 |
+
.long()
|
136 |
+
.squeeze()
|
137 |
+
.cpu()
|
138 |
+
)
|
139 |
+
return mask
|
densepose/data/samplers/densepose_cse_confidence_based.py
ADDED
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
import random
|
4 |
+
from typing import Optional, Tuple
|
5 |
+
import torch
|
6 |
+
from torch.nn import functional as F
|
7 |
+
|
8 |
+
from detectron2.config import CfgNode
|
9 |
+
from detectron2.structures import Instances
|
10 |
+
|
11 |
+
from densepose.converters.base import IntTupleBox
|
12 |
+
|
13 |
+
from .densepose_cse_base import DensePoseCSEBaseSampler
|
14 |
+
|
15 |
+
|
16 |
+
class DensePoseCSEConfidenceBasedSampler(DensePoseCSEBaseSampler):
|
17 |
+
"""
|
18 |
+
Samples DensePose data from DensePose predictions.
|
19 |
+
Samples for each class are drawn using confidence value estimates.
|
20 |
+
"""
|
21 |
+
|
22 |
+
def __init__(
|
23 |
+
self,
|
24 |
+
cfg: CfgNode,
|
25 |
+
use_gt_categories: bool,
|
26 |
+
embedder: torch.nn.Module,
|
27 |
+
confidence_channel: str,
|
28 |
+
count_per_class: int = 8,
|
29 |
+
search_count_multiplier: Optional[float] = None,
|
30 |
+
search_proportion: Optional[float] = None,
|
31 |
+
):
|
32 |
+
"""
|
33 |
+
Constructor
|
34 |
+
|
35 |
+
Args:
|
36 |
+
cfg (CfgNode): the config of the model
|
37 |
+
embedder (torch.nn.Module): necessary to compute mesh vertex embeddings
|
38 |
+
confidence_channel (str): confidence channel to use for sampling;
|
39 |
+
possible values:
|
40 |
+
"coarse_segm_confidence": confidences for coarse segmentation
|
41 |
+
(default: "coarse_segm_confidence")
|
42 |
+
count_per_class (int): the sampler produces at most `count_per_class`
|
43 |
+
samples for each category (default: 8)
|
44 |
+
search_count_multiplier (float or None): if not None, the total number
|
45 |
+
of the most confident estimates of a given class to consider is
|
46 |
+
defined as `min(search_count_multiplier * count_per_class, N)`,
|
47 |
+
where `N` is the total number of estimates of the class; cannot be
|
48 |
+
specified together with `search_proportion` (default: None)
|
49 |
+
search_proportion (float or None): if not None, the total number of the
|
50 |
+
of the most confident estimates of a given class to consider is
|
51 |
+
defined as `min(max(search_proportion * N, count_per_class), N)`,
|
52 |
+
where `N` is the total number of estimates of the class; cannot be
|
53 |
+
specified together with `search_count_multiplier` (default: None)
|
54 |
+
"""
|
55 |
+
super().__init__(cfg, use_gt_categories, embedder, count_per_class)
|
56 |
+
self.confidence_channel = confidence_channel
|
57 |
+
self.search_count_multiplier = search_count_multiplier
|
58 |
+
self.search_proportion = search_proportion
|
59 |
+
assert (search_count_multiplier is None) or (search_proportion is None), (
|
60 |
+
f"Cannot specify both search_count_multiplier (={search_count_multiplier})"
|
61 |
+
f"and search_proportion (={search_proportion})"
|
62 |
+
)
|
63 |
+
|
64 |
+
def _produce_index_sample(self, values: torch.Tensor, count: int):
|
65 |
+
"""
|
66 |
+
Produce a sample of indices to select data based on confidences
|
67 |
+
|
68 |
+
Args:
|
69 |
+
values (torch.Tensor): a tensor of length k that contains confidences
|
70 |
+
k: number of points labeled with part_id
|
71 |
+
count (int): number of samples to produce, should be positive and <= k
|
72 |
+
|
73 |
+
Return:
|
74 |
+
list(int): indices of values (along axis 1) selected as a sample
|
75 |
+
"""
|
76 |
+
k = values.shape[1]
|
77 |
+
if k == count:
|
78 |
+
index_sample = list(range(k))
|
79 |
+
else:
|
80 |
+
# take the best count * search_count_multiplier pixels,
|
81 |
+
# sample from them uniformly
|
82 |
+
# (here best = smallest variance)
|
83 |
+
_, sorted_confidence_indices = torch.sort(values[0])
|
84 |
+
if self.search_count_multiplier is not None:
|
85 |
+
search_count = min(int(count * self.search_count_multiplier), k)
|
86 |
+
elif self.search_proportion is not None:
|
87 |
+
search_count = min(max(int(k * self.search_proportion), count), k)
|
88 |
+
else:
|
89 |
+
search_count = min(count, k)
|
90 |
+
sample_from_top = random.sample(range(search_count), count)
|
91 |
+
index_sample = sorted_confidence_indices[-search_count:][sample_from_top]
|
92 |
+
return index_sample
|
93 |
+
|
94 |
+
def _produce_mask_and_results(
|
95 |
+
self, instance: Instances, bbox_xywh: IntTupleBox
|
96 |
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
97 |
+
"""
|
98 |
+
Method to get labels and DensePose results from an instance
|
99 |
+
|
100 |
+
Args:
|
101 |
+
instance (Instances): an instance of
|
102 |
+
`DensePoseEmbeddingPredictorOutputWithConfidences`
|
103 |
+
bbox_xywh (IntTupleBox): the corresponding bounding box
|
104 |
+
|
105 |
+
Return:
|
106 |
+
mask (torch.Tensor): shape [H, W], DensePose segmentation mask
|
107 |
+
embeddings (Tuple[torch.Tensor]): a tensor of shape [D, H, W]
|
108 |
+
DensePose CSE Embeddings
|
109 |
+
other_values: a tensor of shape [1, H, W], DensePose CSE confidence
|
110 |
+
"""
|
111 |
+
_, _, w, h = bbox_xywh
|
112 |
+
densepose_output = instance.pred_densepose
|
113 |
+
mask, embeddings, _ = super()._produce_mask_and_results(instance, bbox_xywh)
|
114 |
+
other_values = F.interpolate(
|
115 |
+
getattr(densepose_output, self.confidence_channel),
|
116 |
+
size=(h, w),
|
117 |
+
mode="bilinear",
|
118 |
+
)[0].cpu()
|
119 |
+
return mask, embeddings, other_values
|
densepose/data/samplers/densepose_cse_uniform.py
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from .densepose_cse_base import DensePoseCSEBaseSampler
|
4 |
+
from .densepose_uniform import DensePoseUniformSampler
|
5 |
+
|
6 |
+
|
7 |
+
class DensePoseCSEUniformSampler(DensePoseCSEBaseSampler, DensePoseUniformSampler):
|
8 |
+
"""
|
9 |
+
Uniform Sampler for CSE
|
10 |
+
"""
|
11 |
+
|
12 |
+
pass
|
densepose/data/samplers/densepose_uniform.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
import random
|
4 |
+
import torch
|
5 |
+
|
6 |
+
from .densepose_base import DensePoseBaseSampler
|
7 |
+
|
8 |
+
|
9 |
+
class DensePoseUniformSampler(DensePoseBaseSampler):
|
10 |
+
"""
|
11 |
+
Samples DensePose data from DensePose predictions.
|
12 |
+
Samples for each class are drawn uniformly over all pixels estimated
|
13 |
+
to belong to that class.
|
14 |
+
"""
|
15 |
+
|
16 |
+
def __init__(self, count_per_class: int = 8):
|
17 |
+
"""
|
18 |
+
Constructor
|
19 |
+
|
20 |
+
Args:
|
21 |
+
count_per_class (int): the sampler produces at most `count_per_class`
|
22 |
+
samples for each category
|
23 |
+
"""
|
24 |
+
super().__init__(count_per_class)
|
25 |
+
|
26 |
+
def _produce_index_sample(self, values: torch.Tensor, count: int):
|
27 |
+
"""
|
28 |
+
Produce a uniform sample of indices to select data
|
29 |
+
|
30 |
+
Args:
|
31 |
+
values (torch.Tensor): an array of size [n, k] that contains
|
32 |
+
estimated values (U, V, confidences);
|
33 |
+
n: number of channels (U, V, confidences)
|
34 |
+
k: number of points labeled with part_id
|
35 |
+
count (int): number of samples to produce, should be positive and <= k
|
36 |
+
|
37 |
+
Return:
|
38 |
+
list(int): indices of values (along axis 1) selected as a sample
|
39 |
+
"""
|
40 |
+
k = values.shape[1]
|
41 |
+
return random.sample(range(k), count)
|
densepose/data/samplers/mask_from_densepose.py
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from detectron2.structures import BitMasks, Instances
|
4 |
+
|
5 |
+
from densepose.converters import ToMaskConverter
|
6 |
+
|
7 |
+
|
8 |
+
class MaskFromDensePoseSampler:
|
9 |
+
"""
|
10 |
+
Produce mask GT from DensePose predictions
|
11 |
+
This sampler simply converts DensePose predictions to BitMasks
|
12 |
+
that a contain a bool tensor of the size of the input image
|
13 |
+
"""
|
14 |
+
|
15 |
+
def __call__(self, instances: Instances) -> BitMasks:
|
16 |
+
"""
|
17 |
+
Converts predicted data from `instances` into the GT mask data
|
18 |
+
|
19 |
+
Args:
|
20 |
+
instances (Instances): predicted results, expected to have `pred_densepose` field
|
21 |
+
|
22 |
+
Returns:
|
23 |
+
Boolean Tensor of the size of the input image that has non-zero
|
24 |
+
values at pixels that are estimated to belong to the detected object
|
25 |
+
"""
|
26 |
+
return ToMaskConverter.convert(
|
27 |
+
instances.pred_densepose, instances.pred_boxes, instances.image_size
|
28 |
+
)
|
densepose/data/samplers/prediction_to_gt.py
ADDED
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from dataclasses import dataclass
|
4 |
+
from typing import Any, Callable, Dict, List, Optional
|
5 |
+
|
6 |
+
from detectron2.structures import Instances
|
7 |
+
|
8 |
+
ModelOutput = Dict[str, Any]
|
9 |
+
SampledData = Dict[str, Any]
|
10 |
+
|
11 |
+
|
12 |
+
@dataclass
|
13 |
+
class _Sampler:
|
14 |
+
"""
|
15 |
+
Sampler registry entry that contains:
|
16 |
+
- src (str): source field to sample from (deleted after sampling)
|
17 |
+
- dst (Optional[str]): destination field to sample to, if not None
|
18 |
+
- func (Optional[Callable: Any -> Any]): function that performs sampling,
|
19 |
+
if None, reference copy is performed
|
20 |
+
"""
|
21 |
+
|
22 |
+
src: str
|
23 |
+
dst: Optional[str]
|
24 |
+
func: Optional[Callable[[Any], Any]]
|
25 |
+
|
26 |
+
|
27 |
+
class PredictionToGroundTruthSampler:
|
28 |
+
"""
|
29 |
+
Sampler implementation that converts predictions to GT using registered
|
30 |
+
samplers for different fields of `Instances`.
|
31 |
+
"""
|
32 |
+
|
33 |
+
def __init__(self, dataset_name: str = ""):
|
34 |
+
self.dataset_name = dataset_name
|
35 |
+
self._samplers = {}
|
36 |
+
self.register_sampler("pred_boxes", "gt_boxes", None)
|
37 |
+
self.register_sampler("pred_classes", "gt_classes", None)
|
38 |
+
# delete scores
|
39 |
+
self.register_sampler("scores")
|
40 |
+
|
41 |
+
def __call__(self, model_output: List[ModelOutput]) -> List[SampledData]:
|
42 |
+
"""
|
43 |
+
Transform model output into ground truth data through sampling
|
44 |
+
|
45 |
+
Args:
|
46 |
+
model_output (Dict[str, Any]): model output
|
47 |
+
Returns:
|
48 |
+
Dict[str, Any]: sampled data
|
49 |
+
"""
|
50 |
+
for model_output_i in model_output:
|
51 |
+
instances: Instances = model_output_i["instances"]
|
52 |
+
# transform data in each field
|
53 |
+
for _, sampler in self._samplers.items():
|
54 |
+
if not instances.has(sampler.src) or sampler.dst is None:
|
55 |
+
continue
|
56 |
+
if sampler.func is None:
|
57 |
+
instances.set(sampler.dst, instances.get(sampler.src))
|
58 |
+
else:
|
59 |
+
instances.set(sampler.dst, sampler.func(instances))
|
60 |
+
# delete model output data that was transformed
|
61 |
+
for _, sampler in self._samplers.items():
|
62 |
+
if sampler.src != sampler.dst and instances.has(sampler.src):
|
63 |
+
instances.remove(sampler.src)
|
64 |
+
model_output_i["dataset"] = self.dataset_name
|
65 |
+
return model_output
|
66 |
+
|
67 |
+
def register_sampler(
|
68 |
+
self,
|
69 |
+
prediction_attr: str,
|
70 |
+
gt_attr: Optional[str] = None,
|
71 |
+
func: Optional[Callable[[Any], Any]] = None,
|
72 |
+
):
|
73 |
+
"""
|
74 |
+
Register sampler for a field
|
75 |
+
|
76 |
+
Args:
|
77 |
+
prediction_attr (str): field to replace with a sampled value
|
78 |
+
gt_attr (Optional[str]): field to store the sampled value to, if not None
|
79 |
+
func (Optional[Callable: Any -> Any]): sampler function
|
80 |
+
"""
|
81 |
+
self._samplers[(prediction_attr, gt_attr)] = _Sampler(
|
82 |
+
src=prediction_attr, dst=gt_attr, func=func
|
83 |
+
)
|
84 |
+
|
85 |
+
def remove_sampler(
|
86 |
+
self,
|
87 |
+
prediction_attr: str,
|
88 |
+
gt_attr: Optional[str] = None,
|
89 |
+
):
|
90 |
+
"""
|
91 |
+
Remove sampler for a field
|
92 |
+
|
93 |
+
Args:
|
94 |
+
prediction_attr (str): field to replace with a sampled value
|
95 |
+
gt_attr (Optional[str]): field to store the sampled value to, if not None
|
96 |
+
"""
|
97 |
+
assert (prediction_attr, gt_attr) in self._samplers
|
98 |
+
del self._samplers[(prediction_attr, gt_attr)]
|
densepose/data/transform/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from .image import ImageResizeTransform
|
densepose/data/transform/image.py
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
import torch
|
4 |
+
|
5 |
+
|
6 |
+
class ImageResizeTransform:
|
7 |
+
"""
|
8 |
+
Transform that resizes images loaded from a dataset
|
9 |
+
(BGR data in NCHW channel order, typically uint8) to a format ready to be
|
10 |
+
consumed by DensePose training (BGR float32 data in NCHW channel order)
|
11 |
+
"""
|
12 |
+
|
13 |
+
def __init__(self, min_size: int = 800, max_size: int = 1333):
|
14 |
+
self.min_size = min_size
|
15 |
+
self.max_size = max_size
|
16 |
+
|
17 |
+
def __call__(self, images: torch.Tensor) -> torch.Tensor:
|
18 |
+
"""
|
19 |
+
Args:
|
20 |
+
images (torch.Tensor): tensor of size [N, 3, H, W] that contains
|
21 |
+
BGR data (typically in uint8)
|
22 |
+
Returns:
|
23 |
+
images (torch.Tensor): tensor of size [N, 3, H1, W1] where
|
24 |
+
H1 and W1 are chosen to respect the specified min and max sizes
|
25 |
+
and preserve the original aspect ratio, the data channels
|
26 |
+
follow BGR order and the data type is `torch.float32`
|
27 |
+
"""
|
28 |
+
# resize with min size
|
29 |
+
images = images.float()
|
30 |
+
min_size = min(images.shape[-2:])
|
31 |
+
max_size = max(images.shape[-2:])
|
32 |
+
scale = min(self.min_size / min_size, self.max_size / max_size)
|
33 |
+
images = torch.nn.functional.interpolate(
|
34 |
+
images,
|
35 |
+
scale_factor=scale,
|
36 |
+
mode="bilinear",
|
37 |
+
align_corners=False,
|
38 |
+
)
|
39 |
+
return images
|
densepose/data/utils.py
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
import os
|
4 |
+
from typing import Dict, Optional
|
5 |
+
|
6 |
+
from detectron2.config import CfgNode
|
7 |
+
|
8 |
+
|
9 |
+
def is_relative_local_path(path: str) -> bool:
|
10 |
+
path_str = os.fsdecode(path)
|
11 |
+
return ("://" not in path_str) and not os.path.isabs(path)
|
12 |
+
|
13 |
+
|
14 |
+
def maybe_prepend_base_path(base_path: Optional[str], path: str):
|
15 |
+
"""
|
16 |
+
Prepends the provided path with a base path prefix if:
|
17 |
+
1) base path is not None;
|
18 |
+
2) path is a local path
|
19 |
+
"""
|
20 |
+
if base_path is None:
|
21 |
+
return path
|
22 |
+
if is_relative_local_path(path):
|
23 |
+
return os.path.join(base_path, path)
|
24 |
+
return path
|
25 |
+
|
26 |
+
|
27 |
+
def get_class_to_mesh_name_mapping(cfg: CfgNode) -> Dict[int, str]:
|
28 |
+
return {
|
29 |
+
int(class_id): mesh_name
|
30 |
+
for class_id, mesh_name in cfg.DATASETS.CLASS_TO_MESH_NAME_MAPPING.items()
|
31 |
+
}
|
32 |
+
|
33 |
+
|
34 |
+
def get_category_to_class_mapping(dataset_cfg: CfgNode) -> Dict[str, int]:
|
35 |
+
return {
|
36 |
+
category: int(class_id)
|
37 |
+
for category, class_id in dataset_cfg.CATEGORY_TO_CLASS_MAPPING.items()
|
38 |
+
}
|
densepose/data/video/__init__.py
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from .frame_selector import (
|
4 |
+
FrameSelectionStrategy,
|
5 |
+
RandomKFramesSelector,
|
6 |
+
FirstKFramesSelector,
|
7 |
+
LastKFramesSelector,
|
8 |
+
FrameTsList,
|
9 |
+
FrameSelector,
|
10 |
+
)
|
11 |
+
|
12 |
+
from .video_keyframe_dataset import (
|
13 |
+
VideoKeyframeDataset,
|
14 |
+
video_list_from_file,
|
15 |
+
list_keyframes,
|
16 |
+
read_keyframes,
|
17 |
+
)
|
densepose/data/video/frame_selector.py
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
import random
|
4 |
+
from collections.abc import Callable
|
5 |
+
from enum import Enum
|
6 |
+
from typing import Callable as TCallable
|
7 |
+
from typing import List
|
8 |
+
|
9 |
+
FrameTsList = List[int]
|
10 |
+
FrameSelector = TCallable[[FrameTsList], FrameTsList]
|
11 |
+
|
12 |
+
|
13 |
+
class FrameSelectionStrategy(Enum):
|
14 |
+
"""
|
15 |
+
Frame selection strategy used with videos:
|
16 |
+
- "random_k": select k random frames
|
17 |
+
- "first_k": select k first frames
|
18 |
+
- "last_k": select k last frames
|
19 |
+
- "all": select all frames
|
20 |
+
"""
|
21 |
+
|
22 |
+
# fmt: off
|
23 |
+
RANDOM_K = "random_k"
|
24 |
+
FIRST_K = "first_k"
|
25 |
+
LAST_K = "last_k"
|
26 |
+
ALL = "all"
|
27 |
+
# fmt: on
|
28 |
+
|
29 |
+
|
30 |
+
class RandomKFramesSelector(Callable): # pyre-ignore[39]
|
31 |
+
"""
|
32 |
+
Selector that retains at most `k` random frames
|
33 |
+
"""
|
34 |
+
|
35 |
+
def __init__(self, k: int):
|
36 |
+
self.k = k
|
37 |
+
|
38 |
+
def __call__(self, frame_tss: FrameTsList) -> FrameTsList:
|
39 |
+
"""
|
40 |
+
Select `k` random frames
|
41 |
+
|
42 |
+
Args:
|
43 |
+
frames_tss (List[int]): timestamps of input frames
|
44 |
+
Returns:
|
45 |
+
List[int]: timestamps of selected frames
|
46 |
+
"""
|
47 |
+
return random.sample(frame_tss, min(self.k, len(frame_tss)))
|
48 |
+
|
49 |
+
|
50 |
+
class FirstKFramesSelector(Callable): # pyre-ignore[39]
|
51 |
+
"""
|
52 |
+
Selector that retains at most `k` first frames
|
53 |
+
"""
|
54 |
+
|
55 |
+
def __init__(self, k: int):
|
56 |
+
self.k = k
|
57 |
+
|
58 |
+
def __call__(self, frame_tss: FrameTsList) -> FrameTsList:
|
59 |
+
"""
|
60 |
+
Select `k` first frames
|
61 |
+
|
62 |
+
Args:
|
63 |
+
frames_tss (List[int]): timestamps of input frames
|
64 |
+
Returns:
|
65 |
+
List[int]: timestamps of selected frames
|
66 |
+
"""
|
67 |
+
return frame_tss[: self.k]
|
68 |
+
|
69 |
+
|
70 |
+
class LastKFramesSelector(Callable): # pyre-ignore[39]
|
71 |
+
"""
|
72 |
+
Selector that retains at most `k` last frames from video data
|
73 |
+
"""
|
74 |
+
|
75 |
+
def __init__(self, k: int):
|
76 |
+
self.k = k
|
77 |
+
|
78 |
+
def __call__(self, frame_tss: FrameTsList) -> FrameTsList:
|
79 |
+
"""
|
80 |
+
Select `k` last frames
|
81 |
+
|
82 |
+
Args:
|
83 |
+
frames_tss (List[int]): timestamps of input frames
|
84 |
+
Returns:
|
85 |
+
List[int]: timestamps of selected frames
|
86 |
+
"""
|
87 |
+
return frame_tss[-self.k :]
|
densepose/data/video/video_keyframe_dataset.py
ADDED
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
3 |
+
|
4 |
+
import csv
|
5 |
+
import logging
|
6 |
+
import numpy as np
|
7 |
+
from typing import Any, Callable, Dict, List, Optional, Union
|
8 |
+
import av
|
9 |
+
import torch
|
10 |
+
from torch.utils.data.dataset import Dataset
|
11 |
+
|
12 |
+
from detectron2.utils.file_io import PathManager
|
13 |
+
|
14 |
+
from ..utils import maybe_prepend_base_path
|
15 |
+
from .frame_selector import FrameSelector, FrameTsList
|
16 |
+
|
17 |
+
FrameList = List[av.frame.Frame] # pyre-ignore[16]
|
18 |
+
FrameTransform = Callable[[torch.Tensor], torch.Tensor]
|
19 |
+
|
20 |
+
|
21 |
+
def list_keyframes(video_fpath: str, video_stream_idx: int = 0) -> FrameTsList:
|
22 |
+
"""
|
23 |
+
Traverses all keyframes of a video file. Returns a list of keyframe
|
24 |
+
timestamps. Timestamps are counts in timebase units.
|
25 |
+
|
26 |
+
Args:
|
27 |
+
video_fpath (str): Video file path
|
28 |
+
video_stream_idx (int): Video stream index (default: 0)
|
29 |
+
Returns:
|
30 |
+
List[int]: list of keyframe timestaps (timestamp is a count in timebase
|
31 |
+
units)
|
32 |
+
"""
|
33 |
+
try:
|
34 |
+
with PathManager.open(video_fpath, "rb") as io:
|
35 |
+
container = av.open(io, mode="r")
|
36 |
+
stream = container.streams.video[video_stream_idx]
|
37 |
+
keyframes = []
|
38 |
+
pts = -1
|
39 |
+
# Note: even though we request forward seeks for keyframes, sometimes
|
40 |
+
# a keyframe in backwards direction is returned. We introduce tolerance
|
41 |
+
# as a max count of ignored backward seeks
|
42 |
+
tolerance_backward_seeks = 2
|
43 |
+
while True:
|
44 |
+
try:
|
45 |
+
container.seek(pts + 1, backward=False, any_frame=False, stream=stream)
|
46 |
+
except av.AVError as e:
|
47 |
+
# the exception occurs when the video length is exceeded,
|
48 |
+
# we then return whatever data we've already collected
|
49 |
+
logger = logging.getLogger(__name__)
|
50 |
+
logger.debug(
|
51 |
+
f"List keyframes: Error seeking video file {video_fpath}, "
|
52 |
+
f"video stream {video_stream_idx}, pts {pts + 1}, AV error: {e}"
|
53 |
+
)
|
54 |
+
return keyframes
|
55 |
+
except OSError as e:
|
56 |
+
logger = logging.getLogger(__name__)
|
57 |
+
logger.warning(
|
58 |
+
f"List keyframes: Error seeking video file {video_fpath}, "
|
59 |
+
f"video stream {video_stream_idx}, pts {pts + 1}, OS error: {e}"
|
60 |
+
)
|
61 |
+
return []
|
62 |
+
packet = next(container.demux(video=video_stream_idx))
|
63 |
+
if packet.pts is not None and packet.pts <= pts:
|
64 |
+
logger = logging.getLogger(__name__)
|
65 |
+
logger.warning(
|
66 |
+
f"Video file {video_fpath}, stream {video_stream_idx}: "
|
67 |
+
f"bad seek for packet {pts + 1} (got packet {packet.pts}), "
|
68 |
+
f"tolerance {tolerance_backward_seeks}."
|
69 |
+
)
|
70 |
+
tolerance_backward_seeks -= 1
|
71 |
+
if tolerance_backward_seeks == 0:
|
72 |
+
return []
|
73 |
+
pts += 1
|
74 |
+
continue
|
75 |
+
tolerance_backward_seeks = 2
|
76 |
+
pts = packet.pts
|
77 |
+
if pts is None:
|
78 |
+
return keyframes
|
79 |
+
if packet.is_keyframe:
|
80 |
+
keyframes.append(pts)
|
81 |
+
return keyframes
|
82 |
+
except OSError as e:
|
83 |
+
logger = logging.getLogger(__name__)
|
84 |
+
logger.warning(
|
85 |
+
f"List keyframes: Error opening video file container {video_fpath}, " f"OS error: {e}"
|
86 |
+
)
|
87 |
+
except RuntimeError as e:
|
88 |
+
logger = logging.getLogger(__name__)
|
89 |
+
logger.warning(
|
90 |
+
f"List keyframes: Error opening video file container {video_fpath}, "
|
91 |
+
f"Runtime error: {e}"
|
92 |
+
)
|
93 |
+
return []
|
94 |
+
|
95 |
+
|
96 |
+
def read_keyframes(
|
97 |
+
video_fpath: str, keyframes: FrameTsList, video_stream_idx: int = 0
|
98 |
+
) -> FrameList: # pyre-ignore[11]
|
99 |
+
"""
|
100 |
+
Reads keyframe data from a video file.
|
101 |
+
|
102 |
+
Args:
|
103 |
+
video_fpath (str): Video file path
|
104 |
+
keyframes (List[int]): List of keyframe timestamps (as counts in
|
105 |
+
timebase units to be used in container seek operations)
|
106 |
+
video_stream_idx (int): Video stream index (default: 0)
|
107 |
+
Returns:
|
108 |
+
List[Frame]: list of frames that correspond to the specified timestamps
|
109 |
+
"""
|
110 |
+
try:
|
111 |
+
with PathManager.open(video_fpath, "rb") as io:
|
112 |
+
container = av.open(io)
|
113 |
+
stream = container.streams.video[video_stream_idx]
|
114 |
+
frames = []
|
115 |
+
for pts in keyframes:
|
116 |
+
try:
|
117 |
+
container.seek(pts, any_frame=False, stream=stream)
|
118 |
+
frame = next(container.decode(video=0))
|
119 |
+
frames.append(frame)
|
120 |
+
except av.AVError as e:
|
121 |
+
logger = logging.getLogger(__name__)
|
122 |
+
logger.warning(
|
123 |
+
f"Read keyframes: Error seeking video file {video_fpath}, "
|
124 |
+
f"video stream {video_stream_idx}, pts {pts}, AV error: {e}"
|
125 |
+
)
|
126 |
+
container.close()
|
127 |
+
return frames
|
128 |
+
except OSError as e:
|
129 |
+
logger = logging.getLogger(__name__)
|
130 |
+
logger.warning(
|
131 |
+
f"Read keyframes: Error seeking video file {video_fpath}, "
|
132 |
+
f"video stream {video_stream_idx}, pts {pts}, OS error: {e}"
|
133 |
+
)
|
134 |
+
container.close()
|
135 |
+
return frames
|
136 |
+
except StopIteration:
|
137 |
+
logger = logging.getLogger(__name__)
|
138 |
+
logger.warning(
|
139 |
+
f"Read keyframes: Error decoding frame from {video_fpath}, "
|
140 |
+
f"video stream {video_stream_idx}, pts {pts}"
|
141 |
+
)
|
142 |
+
container.close()
|
143 |
+
return frames
|
144 |
+
|
145 |
+
container.close()
|
146 |
+
return frames
|
147 |
+
except OSError as e:
|
148 |
+
logger = logging.getLogger(__name__)
|
149 |
+
logger.warning(
|
150 |
+
f"Read keyframes: Error opening video file container {video_fpath}, OS error: {e}"
|
151 |
+
)
|
152 |
+
except RuntimeError as e:
|
153 |
+
logger = logging.getLogger(__name__)
|
154 |
+
logger.warning(
|
155 |
+
f"Read keyframes: Error opening video file container {video_fpath}, Runtime error: {e}"
|
156 |
+
)
|
157 |
+
return []
|
158 |
+
|
159 |
+
|
160 |
+
def video_list_from_file(video_list_fpath: str, base_path: Optional[str] = None):
|
161 |
+
"""
|
162 |
+
Create a list of paths to video files from a text file.
|
163 |
+
|
164 |
+
Args:
|
165 |
+
video_list_fpath (str): path to a plain text file with the list of videos
|
166 |
+
base_path (str): base path for entries from the video list (default: None)
|
167 |
+
"""
|
168 |
+
video_list = []
|
169 |
+
with PathManager.open(video_list_fpath, "r") as io:
|
170 |
+
for line in io:
|
171 |
+
video_list.append(maybe_prepend_base_path(base_path, str(line.strip())))
|
172 |
+
return video_list
|
173 |
+
|
174 |
+
|
175 |
+
def read_keyframe_helper_data(fpath: str):
|
176 |
+
"""
|
177 |
+
Read keyframe data from a file in CSV format: the header should contain
|
178 |
+
"video_id" and "keyframes" fields. Value specifications are:
|
179 |
+
video_id: int
|
180 |
+
keyframes: list(int)
|
181 |
+
Example of contents:
|
182 |
+
video_id,keyframes
|
183 |
+
2,"[1,11,21,31,41,51,61,71,81]"
|
184 |
+
|
185 |
+
Args:
|
186 |
+
fpath (str): File containing keyframe data
|
187 |
+
|
188 |
+
Return:
|
189 |
+
video_id_to_keyframes (dict: int -> list(int)): for a given video ID it
|
190 |
+
contains a list of keyframes for that video
|
191 |
+
"""
|
192 |
+
video_id_to_keyframes = {}
|
193 |
+
try:
|
194 |
+
with PathManager.open(fpath, "r") as io:
|
195 |
+
csv_reader = csv.reader(io)
|
196 |
+
header = next(csv_reader)
|
197 |
+
video_id_idx = header.index("video_id")
|
198 |
+
keyframes_idx = header.index("keyframes")
|
199 |
+
for row in csv_reader:
|
200 |
+
video_id = int(row[video_id_idx])
|
201 |
+
assert (
|
202 |
+
video_id not in video_id_to_keyframes
|
203 |
+
), f"Duplicate keyframes entry for video {fpath}"
|
204 |
+
video_id_to_keyframes[video_id] = (
|
205 |
+
[int(v) for v in row[keyframes_idx][1:-1].split(",")]
|
206 |
+
if len(row[keyframes_idx]) > 2
|
207 |
+
else []
|
208 |
+
)
|
209 |
+
except Exception as e:
|
210 |
+
logger = logging.getLogger(__name__)
|
211 |
+
logger.warning(f"Error reading keyframe helper data from {fpath}: {e}")
|
212 |
+
return video_id_to_keyframes
|
213 |
+
|
214 |
+
|
215 |
+
class VideoKeyframeDataset(Dataset):
|
216 |
+
"""
|
217 |
+
Dataset that provides keyframes for a set of videos.
|
218 |
+
"""
|
219 |
+
|
220 |
+
_EMPTY_FRAMES = torch.empty((0, 3, 1, 1))
|
221 |
+
|
222 |
+
def __init__(
|
223 |
+
self,
|
224 |
+
video_list: List[str],
|
225 |
+
category_list: Union[str, List[str], None] = None,
|
226 |
+
frame_selector: Optional[FrameSelector] = None,
|
227 |
+
transform: Optional[FrameTransform] = None,
|
228 |
+
keyframe_helper_fpath: Optional[str] = None,
|
229 |
+
):
|
230 |
+
"""
|
231 |
+
Dataset constructor
|
232 |
+
|
233 |
+
Args:
|
234 |
+
video_list (List[str]): list of paths to video files
|
235 |
+
category_list (Union[str, List[str], None]): list of animal categories for each
|
236 |
+
video file. If it is a string, or None, this applies to all videos
|
237 |
+
frame_selector (Callable: KeyFrameList -> KeyFrameList):
|
238 |
+
selects keyframes to process, keyframes are given by
|
239 |
+
packet timestamps in timebase counts. If None, all keyframes
|
240 |
+
are selected (default: None)
|
241 |
+
transform (Callable: torch.Tensor -> torch.Tensor):
|
242 |
+
transforms a batch of RGB images (tensors of size [B, 3, H, W]),
|
243 |
+
returns a tensor of the same size. If None, no transform is
|
244 |
+
applied (default: None)
|
245 |
+
|
246 |
+
"""
|
247 |
+
if type(category_list) == list:
|
248 |
+
self.category_list = category_list
|
249 |
+
else:
|
250 |
+
self.category_list = [category_list] * len(video_list)
|
251 |
+
assert len(video_list) == len(
|
252 |
+
self.category_list
|
253 |
+
), "length of video and category lists must be equal"
|
254 |
+
self.video_list = video_list
|
255 |
+
self.frame_selector = frame_selector
|
256 |
+
self.transform = transform
|
257 |
+
self.keyframe_helper_data = (
|
258 |
+
read_keyframe_helper_data(keyframe_helper_fpath)
|
259 |
+
if keyframe_helper_fpath is not None
|
260 |
+
else None
|
261 |
+
)
|
262 |
+
|
263 |
+
def __getitem__(self, idx: int) -> Dict[str, Any]:
|
264 |
+
"""
|
265 |
+
Gets selected keyframes from a given video
|
266 |
+
|
267 |
+
Args:
|
268 |
+
idx (int): video index in the video list file
|
269 |
+
Returns:
|
270 |
+
A dictionary containing two keys:
|
271 |
+
images (torch.Tensor): tensor of size [N, H, W, 3] or of size
|
272 |
+
defined by the transform that contains keyframes data
|
273 |
+
categories (List[str]): categories of the frames
|
274 |
+
"""
|
275 |
+
categories = [self.category_list[idx]]
|
276 |
+
fpath = self.video_list[idx]
|
277 |
+
keyframes = (
|
278 |
+
list_keyframes(fpath)
|
279 |
+
if self.keyframe_helper_data is None or idx not in self.keyframe_helper_data
|
280 |
+
else self.keyframe_helper_data[idx]
|
281 |
+
)
|
282 |
+
transform = self.transform
|
283 |
+
frame_selector = self.frame_selector
|
284 |
+
if not keyframes:
|
285 |
+
return {"images": self._EMPTY_FRAMES, "categories": []}
|
286 |
+
if frame_selector is not None:
|
287 |
+
keyframes = frame_selector(keyframes)
|
288 |
+
frames = read_keyframes(fpath, keyframes)
|
289 |
+
if not frames:
|
290 |
+
return {"images": self._EMPTY_FRAMES, "categories": []}
|
291 |
+
frames = np.stack([frame.to_rgb().to_ndarray() for frame in frames])
|
292 |
+
frames = torch.as_tensor(frames, device=torch.device("cpu"))
|
293 |
+
frames = frames[..., [2, 1, 0]] # RGB -> BGR
|
294 |
+
frames = frames.permute(0, 3, 1, 2).float() # NHWC -> NCHW
|
295 |
+
if transform is not None:
|
296 |
+
frames = transform(frames)
|
297 |
+
return {"images": frames, "categories": categories}
|
298 |
+
|
299 |
+
def __len__(self):
|
300 |
+
return len(self.video_list)
|
densepose/engine/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from .trainer import Trainer
|
densepose/engine/trainer.py
ADDED
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
|
2 |
+
|
3 |
+
import logging
|
4 |
+
import os
|
5 |
+
from collections import OrderedDict
|
6 |
+
from typing import List, Optional, Union
|
7 |
+
import torch
|
8 |
+
from torch import nn
|
9 |
+
|
10 |
+
from detectron2.checkpoint import DetectionCheckpointer
|
11 |
+
from detectron2.config import CfgNode
|
12 |
+
from detectron2.engine import DefaultTrainer
|
13 |
+
from detectron2.evaluation import (
|
14 |
+
DatasetEvaluator,
|
15 |
+
DatasetEvaluators,
|
16 |
+
inference_on_dataset,
|
17 |
+
print_csv_format,
|
18 |
+
)
|
19 |
+
from detectron2.solver.build import get_default_optimizer_params, maybe_add_gradient_clipping
|
20 |
+
from detectron2.utils import comm
|
21 |
+
from detectron2.utils.events import EventWriter, get_event_storage
|
22 |
+
|
23 |
+
from densepose import DensePoseDatasetMapperTTA, DensePoseGeneralizedRCNNWithTTA, load_from_cfg
|
24 |
+
from densepose.data import (
|
25 |
+
DatasetMapper,
|
26 |
+
build_combined_loader,
|
27 |
+
build_detection_test_loader,
|
28 |
+
build_detection_train_loader,
|
29 |
+
build_inference_based_loaders,
|
30 |
+
has_inference_based_loaders,
|
31 |
+
)
|
32 |
+
from densepose.evaluation.d2_evaluator_adapter import Detectron2COCOEvaluatorAdapter
|
33 |
+
from densepose.evaluation.evaluator import DensePoseCOCOEvaluator, build_densepose_evaluator_storage
|
34 |
+
from densepose.modeling.cse import Embedder
|
35 |
+
|
36 |
+
|
37 |
+
class SampleCountingLoader:
|
38 |
+
def __init__(self, loader):
|
39 |
+
self.loader = loader
|
40 |
+
|
41 |
+
def __iter__(self):
|
42 |
+
it = iter(self.loader)
|
43 |
+
storage = get_event_storage()
|
44 |
+
while True:
|
45 |
+
try:
|
46 |
+
batch = next(it)
|
47 |
+
num_inst_per_dataset = {}
|
48 |
+
for data in batch:
|
49 |
+
dataset_name = data["dataset"]
|
50 |
+
if dataset_name not in num_inst_per_dataset:
|
51 |
+
num_inst_per_dataset[dataset_name] = 0
|
52 |
+
num_inst = len(data["instances"])
|
53 |
+
num_inst_per_dataset[dataset_name] += num_inst
|
54 |
+
for dataset_name in num_inst_per_dataset:
|
55 |
+
storage.put_scalar(f"batch/{dataset_name}", num_inst_per_dataset[dataset_name])
|
56 |
+
yield batch
|
57 |
+
except StopIteration:
|
58 |
+
break
|
59 |
+
|
60 |
+
|
61 |
+
class SampleCountMetricPrinter(EventWriter):
|
62 |
+
def __init__(self):
|
63 |
+
self.logger = logging.getLogger(__name__)
|
64 |
+
|
65 |
+
def write(self):
|
66 |
+
storage = get_event_storage()
|
67 |
+
batch_stats_strs = []
|
68 |
+
for key, buf in storage.histories().items():
|
69 |
+
if key.startswith("batch/"):
|
70 |
+
batch_stats_strs.append(f"{key} {buf.avg(20)}")
|
71 |
+
self.logger.info(", ".join(batch_stats_strs))
|
72 |
+
|
73 |
+
|
74 |
+
class Trainer(DefaultTrainer):
|
75 |
+
@classmethod
|
76 |
+
def extract_embedder_from_model(cls, model: nn.Module) -> Optional[Embedder]:
|
77 |
+
if isinstance(model, nn.parallel.DistributedDataParallel):
|
78 |
+
model = model.module
|
79 |
+
if hasattr(model, "roi_heads") and hasattr(model.roi_heads, "embedder"):
|
80 |
+
return model.roi_heads.embedder
|
81 |
+
return None
|
82 |
+
|
83 |
+
# TODO: the only reason to copy the base class code here is to pass the embedder from
|
84 |
+
# the model to the evaluator; that should be refactored to avoid unnecessary copy-pasting
|
85 |
+
@classmethod
|
86 |
+
def test(
|
87 |
+
cls,
|
88 |
+
cfg: CfgNode,
|
89 |
+
model: nn.Module,
|
90 |
+
evaluators: Optional[Union[DatasetEvaluator, List[DatasetEvaluator]]] = None,
|
91 |
+
):
|
92 |
+
"""
|
93 |
+
Args:
|
94 |
+
cfg (CfgNode):
|
95 |
+
model (nn.Module):
|
96 |
+
evaluators (DatasetEvaluator, list[DatasetEvaluator] or None): if None, will call
|
97 |
+
:meth:`build_evaluator`. Otherwise, must have the same length as
|
98 |
+
``cfg.DATASETS.TEST``.
|
99 |
+
|
100 |
+
Returns:
|
101 |
+
dict: a dict of result metrics
|
102 |
+
"""
|
103 |
+
logger = logging.getLogger(__name__)
|
104 |
+
if isinstance(evaluators, DatasetEvaluator):
|
105 |
+
evaluators = [evaluators]
|
106 |
+
if evaluators is not None:
|
107 |
+
assert len(cfg.DATASETS.TEST) == len(evaluators), "{} != {}".format(
|
108 |
+
len(cfg.DATASETS.TEST), len(evaluators)
|
109 |
+
)
|
110 |
+
|
111 |
+
results = OrderedDict()
|
112 |
+
for idx, dataset_name in enumerate(cfg.DATASETS.TEST):
|
113 |
+
data_loader = cls.build_test_loader(cfg, dataset_name)
|
114 |
+
# When evaluators are passed in as arguments,
|
115 |
+
# implicitly assume that evaluators can be created before data_loader.
|
116 |
+
if evaluators is not None:
|
117 |
+
evaluator = evaluators[idx]
|
118 |
+
else:
|
119 |
+
try:
|
120 |
+
embedder = cls.extract_embedder_from_model(model)
|
121 |
+
evaluator = cls.build_evaluator(cfg, dataset_name, embedder=embedder)
|
122 |
+
except NotImplementedError:
|
123 |
+
logger.warn(
|
124 |
+
"No evaluator found. Use `DefaultTrainer.test(evaluators=)`, "
|
125 |
+
"or implement its `build_evaluator` method."
|
126 |
+
)
|
127 |
+
results[dataset_name] = {}
|
128 |
+
continue
|
129 |
+
if cfg.DENSEPOSE_EVALUATION.DISTRIBUTED_INFERENCE or comm.is_main_process():
|
130 |
+
results_i = inference_on_dataset(model, data_loader, evaluator)
|
131 |
+
else:
|
132 |
+
results_i = {}
|
133 |
+
results[dataset_name] = results_i
|
134 |
+
if comm.is_main_process():
|
135 |
+
assert isinstance(
|
136 |
+
results_i, dict
|
137 |
+
), "Evaluator must return a dict on the main process. Got {} instead.".format(
|
138 |
+
results_i
|
139 |
+
)
|
140 |
+
logger.info("Evaluation results for {} in csv format:".format(dataset_name))
|
141 |
+
print_csv_format(results_i)
|
142 |
+
|
143 |
+
if len(results) == 1:
|
144 |
+
results = list(results.values())[0]
|
145 |
+
return results
|
146 |
+
|
147 |
+
@classmethod
|
148 |
+
def build_evaluator(
|
149 |
+
cls,
|
150 |
+
cfg: CfgNode,
|
151 |
+
dataset_name: str,
|
152 |
+
output_folder: Optional[str] = None,
|
153 |
+
embedder: Optional[Embedder] = None,
|
154 |
+
) -> DatasetEvaluators:
|
155 |
+
if output_folder is None:
|
156 |
+
output_folder = os.path.join(cfg.OUTPUT_DIR, "inference")
|
157 |
+
evaluators = []
|
158 |
+
distributed = cfg.DENSEPOSE_EVALUATION.DISTRIBUTED_INFERENCE
|
159 |
+
# Note: we currently use COCO evaluator for both COCO and LVIS datasets
|
160 |
+
# to have compatible metrics. LVIS bbox evaluator could also be used
|
161 |
+
# with an adapter to properly handle filtered / mapped categories
|
162 |
+
# evaluator_type = MetadataCatalog.get(dataset_name).evaluator_type
|
163 |
+
# if evaluator_type == "coco":
|
164 |
+
# evaluators.append(COCOEvaluator(dataset_name, output_dir=output_folder))
|
165 |
+
# elif evaluator_type == "lvis":
|
166 |
+
# evaluators.append(LVISEvaluator(dataset_name, output_dir=output_folder))
|
167 |
+
evaluators.append(
|
168 |
+
Detectron2COCOEvaluatorAdapter(
|
169 |
+
dataset_name, output_dir=output_folder, distributed=distributed
|
170 |
+
)
|
171 |
+
)
|
172 |
+
if cfg.MODEL.DENSEPOSE_ON:
|
173 |
+
storage = build_densepose_evaluator_storage(cfg, output_folder)
|
174 |
+
evaluators.append(
|
175 |
+
DensePoseCOCOEvaluator(
|
176 |
+
dataset_name,
|
177 |
+
distributed,
|
178 |
+
output_folder,
|
179 |
+
evaluator_type=cfg.DENSEPOSE_EVALUATION.TYPE,
|
180 |
+
min_iou_threshold=cfg.DENSEPOSE_EVALUATION.MIN_IOU_THRESHOLD,
|
181 |
+
storage=storage,
|
182 |
+
embedder=embedder,
|
183 |
+
should_evaluate_mesh_alignment=cfg.DENSEPOSE_EVALUATION.EVALUATE_MESH_ALIGNMENT,
|
184 |
+
mesh_alignment_mesh_names=cfg.DENSEPOSE_EVALUATION.MESH_ALIGNMENT_MESH_NAMES,
|
185 |
+
)
|
186 |
+
)
|
187 |
+
return DatasetEvaluators(evaluators)
|
188 |
+
|
189 |
+
@classmethod
|
190 |
+
def build_optimizer(cls, cfg: CfgNode, model: nn.Module):
|
191 |
+
params = get_default_optimizer_params(
|
192 |
+
model,
|
193 |
+
base_lr=cfg.SOLVER.BASE_LR,
|
194 |
+
weight_decay_norm=cfg.SOLVER.WEIGHT_DECAY_NORM,
|
195 |
+
bias_lr_factor=cfg.SOLVER.BIAS_LR_FACTOR,
|
196 |
+
weight_decay_bias=cfg.SOLVER.WEIGHT_DECAY_BIAS,
|
197 |
+
overrides={
|
198 |
+
"features": {
|
199 |
+
"lr": cfg.SOLVER.BASE_LR * cfg.MODEL.ROI_DENSEPOSE_HEAD.CSE.FEATURES_LR_FACTOR,
|
200 |
+
},
|
201 |
+
"embeddings": {
|
202 |
+
"lr": cfg.SOLVER.BASE_LR * cfg.MODEL.ROI_DENSEPOSE_HEAD.CSE.EMBEDDING_LR_FACTOR,
|
203 |
+
},
|
204 |
+
},
|
205 |
+
)
|
206 |
+
optimizer = torch.optim.SGD(
|
207 |
+
params,
|
208 |
+
cfg.SOLVER.BASE_LR,
|
209 |
+
momentum=cfg.SOLVER.MOMENTUM,
|
210 |
+
nesterov=cfg.SOLVER.NESTEROV,
|
211 |
+
weight_decay=cfg.SOLVER.WEIGHT_DECAY,
|
212 |
+
)
|
213 |
+
# pyre-fixme[6]: For 2nd param expected `Type[Optimizer]` but got `SGD`.
|
214 |
+
return maybe_add_gradient_clipping(cfg, optimizer)
|
215 |
+
|
216 |
+
@classmethod
|
217 |
+
def build_test_loader(cls, cfg: CfgNode, dataset_name):
|
218 |
+
return build_detection_test_loader(cfg, dataset_name, mapper=DatasetMapper(cfg, False))
|
219 |
+
|
220 |
+
@classmethod
|
221 |
+
def build_train_loader(cls, cfg: CfgNode):
|
222 |
+
data_loader = build_detection_train_loader(cfg, mapper=DatasetMapper(cfg, True))
|
223 |
+
if not has_inference_based_loaders(cfg):
|
224 |
+
return data_loader
|
225 |
+
model = cls.build_model(cfg)
|
226 |
+
model.to(cfg.BOOTSTRAP_MODEL.DEVICE)
|
227 |
+
DetectionCheckpointer(model).resume_or_load(cfg.BOOTSTRAP_MODEL.WEIGHTS, resume=False)
|
228 |
+
inference_based_loaders, ratios = build_inference_based_loaders(cfg, model)
|
229 |
+
loaders = [data_loader] + inference_based_loaders
|
230 |
+
ratios = [1.0] + ratios
|
231 |
+
combined_data_loader = build_combined_loader(cfg, loaders, ratios)
|
232 |
+
sample_counting_loader = SampleCountingLoader(combined_data_loader)
|
233 |
+
return sample_counting_loader
|
234 |
+
|
235 |
+
def build_writers(self):
|
236 |
+
writers = super().build_writers()
|
237 |
+
writers.append(SampleCountMetricPrinter())
|
238 |
+
return writers
|
239 |
+
|
240 |
+
@classmethod
|
241 |
+
def test_with_TTA(cls, cfg: CfgNode, model):
|
242 |
+
logger = logging.getLogger("detectron2.trainer")
|
243 |
+
# In the end of training, run an evaluation with TTA
|
244 |
+
# Only support some R-CNN models.
|
245 |
+
logger.info("Running inference with test-time augmentation ...")
|
246 |
+
transform_data = load_from_cfg(cfg)
|
247 |
+
model = DensePoseGeneralizedRCNNWithTTA(
|
248 |
+
cfg, model, transform_data, DensePoseDatasetMapperTTA(cfg)
|
249 |
+
)
|
250 |
+
evaluators = [
|
251 |
+
cls.build_evaluator(
|
252 |
+
cfg, name, output_folder=os.path.join(cfg.OUTPUT_DIR, "inference_TTA")
|
253 |
+
)
|
254 |
+
for name in cfg.DATASETS.TEST
|
255 |
+
]
|
256 |
+
res = cls.test(cfg, model, evaluators) # pyre-ignore[6]
|
257 |
+
res = OrderedDict({k + "_TTA": v for k, v in res.items()})
|
258 |
+
return res
|
densepose/evaluation/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from .evaluator import DensePoseCOCOEvaluator
|
densepose/evaluation/d2_evaluator_adapter.py
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from detectron2.data.catalog import Metadata
|
4 |
+
from detectron2.evaluation import COCOEvaluator
|
5 |
+
|
6 |
+
from densepose.data.datasets.coco import (
|
7 |
+
get_contiguous_id_to_category_id_map,
|
8 |
+
maybe_filter_categories_cocoapi,
|
9 |
+
)
|
10 |
+
|
11 |
+
|
12 |
+
def _maybe_add_iscrowd_annotations(cocoapi) -> None:
|
13 |
+
for ann in cocoapi.dataset["annotations"]:
|
14 |
+
if "iscrowd" not in ann:
|
15 |
+
ann["iscrowd"] = 0
|
16 |
+
|
17 |
+
|
18 |
+
class Detectron2COCOEvaluatorAdapter(COCOEvaluator):
|
19 |
+
def __init__(
|
20 |
+
self,
|
21 |
+
dataset_name,
|
22 |
+
output_dir=None,
|
23 |
+
distributed=True,
|
24 |
+
):
|
25 |
+
super().__init__(dataset_name, output_dir=output_dir, distributed=distributed)
|
26 |
+
maybe_filter_categories_cocoapi(dataset_name, self._coco_api)
|
27 |
+
_maybe_add_iscrowd_annotations(self._coco_api)
|
28 |
+
# substitute category metadata to account for categories
|
29 |
+
# that are mapped to the same contiguous id
|
30 |
+
if hasattr(self._metadata, "thing_dataset_id_to_contiguous_id"):
|
31 |
+
self._maybe_substitute_metadata()
|
32 |
+
|
33 |
+
def _maybe_substitute_metadata(self):
|
34 |
+
cont_id_2_cat_id = get_contiguous_id_to_category_id_map(self._metadata)
|
35 |
+
cat_id_2_cont_id = self._metadata.thing_dataset_id_to_contiguous_id
|
36 |
+
if len(cont_id_2_cat_id) == len(cat_id_2_cont_id):
|
37 |
+
return
|
38 |
+
|
39 |
+
cat_id_2_cont_id_injective = {}
|
40 |
+
for cat_id, cont_id in cat_id_2_cont_id.items():
|
41 |
+
if (cont_id in cont_id_2_cat_id) and (cont_id_2_cat_id[cont_id] == cat_id):
|
42 |
+
cat_id_2_cont_id_injective[cat_id] = cont_id
|
43 |
+
|
44 |
+
metadata_new = Metadata(name=self._metadata.name)
|
45 |
+
for key, value in self._metadata.__dict__.items():
|
46 |
+
if key == "thing_dataset_id_to_contiguous_id":
|
47 |
+
setattr(metadata_new, key, cat_id_2_cont_id_injective)
|
48 |
+
else:
|
49 |
+
setattr(metadata_new, key, value)
|
50 |
+
self._metadata = metadata_new
|
densepose/evaluation/densepose_coco_evaluation.py
ADDED
@@ -0,0 +1,1303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
# All rights reserved.
|
3 |
+
#
|
4 |
+
# This source code is licensed under the license found in the
|
5 |
+
# LICENSE file in the root directory of this source tree.
|
6 |
+
# This is a modified version of cocoeval.py where we also have the densepose evaluation.
|
7 |
+
|
8 |
+
__author__ = "tsungyi"
|
9 |
+
|
10 |
+
import copy
|
11 |
+
import datetime
|
12 |
+
import logging
|
13 |
+
import numpy as np
|
14 |
+
import pickle
|
15 |
+
import time
|
16 |
+
from collections import defaultdict
|
17 |
+
from enum import Enum
|
18 |
+
from typing import Any, Dict, Tuple
|
19 |
+
import scipy.spatial.distance as ssd
|
20 |
+
import torch
|
21 |
+
import torch.nn.functional as F
|
22 |
+
from pycocotools import mask as maskUtils
|
23 |
+
from scipy.io import loadmat
|
24 |
+
from scipy.ndimage import zoom as spzoom
|
25 |
+
|
26 |
+
from detectron2.utils.file_io import PathManager
|
27 |
+
|
28 |
+
from densepose.converters.chart_output_to_chart_result import resample_uv_tensors_to_bbox
|
29 |
+
from densepose.converters.segm_to_mask import (
|
30 |
+
resample_coarse_segm_tensor_to_bbox,
|
31 |
+
resample_fine_and_coarse_segm_tensors_to_bbox,
|
32 |
+
)
|
33 |
+
from densepose.modeling.cse.utils import squared_euclidean_distance_matrix
|
34 |
+
from densepose.structures import DensePoseDataRelative
|
35 |
+
from densepose.structures.mesh import create_mesh
|
36 |
+
|
37 |
+
logger = logging.getLogger(__name__)
|
38 |
+
|
39 |
+
|
40 |
+
class DensePoseEvalMode(str, Enum):
|
41 |
+
# use both masks and geodesic distances (GPS * IOU) to compute scores
|
42 |
+
GPSM = "gpsm"
|
43 |
+
# use only geodesic distances (GPS) to compute scores
|
44 |
+
GPS = "gps"
|
45 |
+
# use only masks (IOU) to compute scores
|
46 |
+
IOU = "iou"
|
47 |
+
|
48 |
+
|
49 |
+
class DensePoseDataMode(str, Enum):
|
50 |
+
# use estimated IUV data (default mode)
|
51 |
+
IUV_DT = "iuvdt"
|
52 |
+
# use ground truth IUV data
|
53 |
+
IUV_GT = "iuvgt"
|
54 |
+
# use ground truth labels I and set UV to 0
|
55 |
+
I_GT_UV_0 = "igtuv0"
|
56 |
+
# use ground truth labels I and estimated UV coordinates
|
57 |
+
I_GT_UV_DT = "igtuvdt"
|
58 |
+
# use estimated labels I and set UV to 0
|
59 |
+
I_DT_UV_0 = "idtuv0"
|
60 |
+
|
61 |
+
|
62 |
+
class DensePoseCocoEval:
|
63 |
+
# Interface for evaluating detection on the Microsoft COCO dataset.
|
64 |
+
#
|
65 |
+
# The usage for CocoEval is as follows:
|
66 |
+
# cocoGt=..., cocoDt=... # load dataset and results
|
67 |
+
# E = CocoEval(cocoGt,cocoDt); # initialize CocoEval object
|
68 |
+
# E.params.recThrs = ...; # set parameters as desired
|
69 |
+
# E.evaluate(); # run per image evaluation
|
70 |
+
# E.accumulate(); # accumulate per image results
|
71 |
+
# E.summarize(); # display summary metrics of results
|
72 |
+
# For example usage see evalDemo.m and http://mscoco.org/.
|
73 |
+
#
|
74 |
+
# The evaluation parameters are as follows (defaults in brackets):
|
75 |
+
# imgIds - [all] N img ids to use for evaluation
|
76 |
+
# catIds - [all] K cat ids to use for evaluation
|
77 |
+
# iouThrs - [.5:.05:.95] T=10 IoU thresholds for evaluation
|
78 |
+
# recThrs - [0:.01:1] R=101 recall thresholds for evaluation
|
79 |
+
# areaRng - [...] A=4 object area ranges for evaluation
|
80 |
+
# maxDets - [1 10 100] M=3 thresholds on max detections per image
|
81 |
+
# iouType - ['segm'] set iouType to 'segm', 'bbox', 'keypoints' or 'densepose'
|
82 |
+
# iouType replaced the now DEPRECATED useSegm parameter.
|
83 |
+
# useCats - [1] if true use category labels for evaluation
|
84 |
+
# Note: if useCats=0 category labels are ignored as in proposal scoring.
|
85 |
+
# Note: multiple areaRngs [Ax2] and maxDets [Mx1] can be specified.
|
86 |
+
#
|
87 |
+
# evaluate(): evaluates detections on every image and every category and
|
88 |
+
# concats the results into the "evalImgs" with fields:
|
89 |
+
# dtIds - [1xD] id for each of the D detections (dt)
|
90 |
+
# gtIds - [1xG] id for each of the G ground truths (gt)
|
91 |
+
# dtMatches - [TxD] matching gt id at each IoU or 0
|
92 |
+
# gtMatches - [TxG] matching dt id at each IoU or 0
|
93 |
+
# dtScores - [1xD] confidence of each dt
|
94 |
+
# gtIgnore - [1xG] ignore flag for each gt
|
95 |
+
# dtIgnore - [TxD] ignore flag for each dt at each IoU
|
96 |
+
#
|
97 |
+
# accumulate(): accumulates the per-image, per-category evaluation
|
98 |
+
# results in "evalImgs" into the dictionary "eval" with fields:
|
99 |
+
# params - parameters used for evaluation
|
100 |
+
# date - date evaluation was performed
|
101 |
+
# counts - [T,R,K,A,M] parameter dimensions (see above)
|
102 |
+
# precision - [TxRxKxAxM] precision for every evaluation setting
|
103 |
+
# recall - [TxKxAxM] max recall for every evaluation setting
|
104 |
+
# Note: precision and recall==-1 for settings with no gt objects.
|
105 |
+
#
|
106 |
+
# See also coco, mask, pycocoDemo, pycocoEvalDemo
|
107 |
+
#
|
108 |
+
# Microsoft COCO Toolbox. version 2.0
|
109 |
+
# Data, paper, and tutorials available at: http://mscoco.org/
|
110 |
+
# Code written by Piotr Dollar and Tsung-Yi Lin, 2015.
|
111 |
+
# Licensed under the Simplified BSD License [see coco/license.txt]
|
112 |
+
def __init__(
|
113 |
+
self,
|
114 |
+
cocoGt=None,
|
115 |
+
cocoDt=None,
|
116 |
+
iouType: str = "densepose",
|
117 |
+
multi_storage=None,
|
118 |
+
embedder=None,
|
119 |
+
dpEvalMode: DensePoseEvalMode = DensePoseEvalMode.GPS,
|
120 |
+
dpDataMode: DensePoseDataMode = DensePoseDataMode.IUV_DT,
|
121 |
+
):
|
122 |
+
"""
|
123 |
+
Initialize CocoEval using coco APIs for gt and dt
|
124 |
+
:param cocoGt: coco object with ground truth annotations
|
125 |
+
:param cocoDt: coco object with detection results
|
126 |
+
:return: None
|
127 |
+
"""
|
128 |
+
self.cocoGt = cocoGt # ground truth COCO API
|
129 |
+
self.cocoDt = cocoDt # detections COCO API
|
130 |
+
self.multi_storage = multi_storage
|
131 |
+
self.embedder = embedder
|
132 |
+
self._dpEvalMode = dpEvalMode
|
133 |
+
self._dpDataMode = dpDataMode
|
134 |
+
self.evalImgs = defaultdict(list) # per-image per-category eval results [KxAxI]
|
135 |
+
self.eval = {} # accumulated evaluation results
|
136 |
+
self._gts = defaultdict(list) # gt for evaluation
|
137 |
+
self._dts = defaultdict(list) # dt for evaluation
|
138 |
+
self.params = Params(iouType=iouType) # parameters
|
139 |
+
self._paramsEval = {} # parameters for evaluation
|
140 |
+
self.stats = [] # result summarization
|
141 |
+
self.ious = {} # ious between all gts and dts
|
142 |
+
if cocoGt is not None:
|
143 |
+
self.params.imgIds = sorted(cocoGt.getImgIds())
|
144 |
+
self.params.catIds = sorted(cocoGt.getCatIds())
|
145 |
+
self.ignoreThrBB = 0.7
|
146 |
+
self.ignoreThrUV = 0.9
|
147 |
+
|
148 |
+
def _loadGEval(self):
|
149 |
+
smpl_subdiv_fpath = PathManager.get_local_path(
|
150 |
+
"https://dl.fbaipublicfiles.com/densepose/data/SMPL_subdiv.mat"
|
151 |
+
)
|
152 |
+
pdist_transform_fpath = PathManager.get_local_path(
|
153 |
+
"https://dl.fbaipublicfiles.com/densepose/data/SMPL_SUBDIV_TRANSFORM.mat"
|
154 |
+
)
|
155 |
+
pdist_matrix_fpath = PathManager.get_local_path(
|
156 |
+
"https://dl.fbaipublicfiles.com/densepose/data/Pdist_matrix.pkl", timeout_sec=120
|
157 |
+
)
|
158 |
+
SMPL_subdiv = loadmat(smpl_subdiv_fpath)
|
159 |
+
self.PDIST_transform = loadmat(pdist_transform_fpath)
|
160 |
+
self.PDIST_transform = self.PDIST_transform["index"].squeeze()
|
161 |
+
UV = np.array([SMPL_subdiv["U_subdiv"], SMPL_subdiv["V_subdiv"]]).squeeze()
|
162 |
+
ClosestVertInds = np.arange(UV.shape[1]) + 1
|
163 |
+
self.Part_UVs = []
|
164 |
+
self.Part_ClosestVertInds = []
|
165 |
+
for i in np.arange(24):
|
166 |
+
self.Part_UVs.append(UV[:, SMPL_subdiv["Part_ID_subdiv"].squeeze() == (i + 1)])
|
167 |
+
self.Part_ClosestVertInds.append(
|
168 |
+
ClosestVertInds[SMPL_subdiv["Part_ID_subdiv"].squeeze() == (i + 1)]
|
169 |
+
)
|
170 |
+
|
171 |
+
with open(pdist_matrix_fpath, "rb") as hFile:
|
172 |
+
arrays = pickle.load(hFile, encoding="latin1")
|
173 |
+
self.Pdist_matrix = arrays["Pdist_matrix"]
|
174 |
+
self.Part_ids = np.array(SMPL_subdiv["Part_ID_subdiv"].squeeze())
|
175 |
+
# Mean geodesic distances for parts.
|
176 |
+
self.Mean_Distances = np.array([0, 0.351, 0.107, 0.126, 0.237, 0.173, 0.142, 0.128, 0.150])
|
177 |
+
# Coarse Part labels.
|
178 |
+
self.CoarseParts = np.array(
|
179 |
+
[0, 1, 1, 2, 2, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8]
|
180 |
+
)
|
181 |
+
|
182 |
+
def _prepare(self):
|
183 |
+
"""
|
184 |
+
Prepare ._gts and ._dts for evaluation based on params
|
185 |
+
:return: None
|
186 |
+
"""
|
187 |
+
|
188 |
+
def _toMask(anns, coco):
|
189 |
+
# modify ann['segmentation'] by reference
|
190 |
+
for ann in anns:
|
191 |
+
# safeguard for invalid segmentation annotation;
|
192 |
+
# annotations containing empty lists exist in the posetrack
|
193 |
+
# dataset. This is not a correct segmentation annotation
|
194 |
+
# in terms of COCO format; we need to deal with it somehow
|
195 |
+
segm = ann["segmentation"]
|
196 |
+
if type(segm) == list and len(segm) == 0:
|
197 |
+
ann["segmentation"] = None
|
198 |
+
continue
|
199 |
+
rle = coco.annToRLE(ann)
|
200 |
+
ann["segmentation"] = rle
|
201 |
+
|
202 |
+
def _getIgnoreRegion(iid, coco):
|
203 |
+
img = coco.imgs[iid]
|
204 |
+
|
205 |
+
if "ignore_regions_x" not in img.keys():
|
206 |
+
return None
|
207 |
+
|
208 |
+
if len(img["ignore_regions_x"]) == 0:
|
209 |
+
return None
|
210 |
+
|
211 |
+
rgns_merged = [
|
212 |
+
[v for xy in zip(region_x, region_y) for v in xy]
|
213 |
+
for region_x, region_y in zip(img["ignore_regions_x"], img["ignore_regions_y"])
|
214 |
+
]
|
215 |
+
rles = maskUtils.frPyObjects(rgns_merged, img["height"], img["width"])
|
216 |
+
rle = maskUtils.merge(rles)
|
217 |
+
return maskUtils.decode(rle)
|
218 |
+
|
219 |
+
def _checkIgnore(dt, iregion):
|
220 |
+
if iregion is None:
|
221 |
+
return True
|
222 |
+
|
223 |
+
bb = np.array(dt["bbox"]).astype(int)
|
224 |
+
x1, y1, x2, y2 = bb[0], bb[1], bb[0] + bb[2], bb[1] + bb[3]
|
225 |
+
x2 = min([x2, iregion.shape[1]])
|
226 |
+
y2 = min([y2, iregion.shape[0]])
|
227 |
+
|
228 |
+
if bb[2] * bb[3] == 0:
|
229 |
+
return False
|
230 |
+
|
231 |
+
crop_iregion = iregion[y1:y2, x1:x2]
|
232 |
+
|
233 |
+
if crop_iregion.sum() == 0:
|
234 |
+
return True
|
235 |
+
|
236 |
+
if "densepose" not in dt.keys(): # filtering boxes
|
237 |
+
return crop_iregion.sum() / bb[2] / bb[3] < self.ignoreThrBB
|
238 |
+
|
239 |
+
# filtering UVs
|
240 |
+
ignoremask = np.require(crop_iregion, requirements=["F"])
|
241 |
+
mask = self._extract_mask(dt)
|
242 |
+
uvmask = np.require(np.asarray(mask > 0), dtype=np.uint8, requirements=["F"])
|
243 |
+
uvmask_ = maskUtils.encode(uvmask)
|
244 |
+
ignoremask_ = maskUtils.encode(ignoremask)
|
245 |
+
uviou = maskUtils.iou([uvmask_], [ignoremask_], [1])[0]
|
246 |
+
return uviou < self.ignoreThrUV
|
247 |
+
|
248 |
+
p = self.params
|
249 |
+
|
250 |
+
if p.useCats:
|
251 |
+
gts = self.cocoGt.loadAnns(self.cocoGt.getAnnIds(imgIds=p.imgIds, catIds=p.catIds))
|
252 |
+
dts = self.cocoDt.loadAnns(self.cocoDt.getAnnIds(imgIds=p.imgIds, catIds=p.catIds))
|
253 |
+
else:
|
254 |
+
gts = self.cocoGt.loadAnns(self.cocoGt.getAnnIds(imgIds=p.imgIds))
|
255 |
+
dts = self.cocoDt.loadAnns(self.cocoDt.getAnnIds(imgIds=p.imgIds))
|
256 |
+
|
257 |
+
imns = self.cocoGt.loadImgs(p.imgIds)
|
258 |
+
self.size_mapping = {}
|
259 |
+
for im in imns:
|
260 |
+
self.size_mapping[im["id"]] = [im["height"], im["width"]]
|
261 |
+
|
262 |
+
# if iouType == 'uv', add point gt annotations
|
263 |
+
if p.iouType == "densepose":
|
264 |
+
self._loadGEval()
|
265 |
+
|
266 |
+
# convert ground truth to mask if iouType == 'segm'
|
267 |
+
if p.iouType == "segm":
|
268 |
+
_toMask(gts, self.cocoGt)
|
269 |
+
_toMask(dts, self.cocoDt)
|
270 |
+
|
271 |
+
# set ignore flag
|
272 |
+
for gt in gts:
|
273 |
+
gt["ignore"] = gt["ignore"] if "ignore" in gt else 0
|
274 |
+
gt["ignore"] = "iscrowd" in gt and gt["iscrowd"]
|
275 |
+
if p.iouType == "keypoints":
|
276 |
+
gt["ignore"] = (gt["num_keypoints"] == 0) or gt["ignore"]
|
277 |
+
if p.iouType == "densepose":
|
278 |
+
gt["ignore"] = ("dp_x" in gt) == 0
|
279 |
+
if p.iouType == "segm":
|
280 |
+
gt["ignore"] = gt["segmentation"] is None
|
281 |
+
|
282 |
+
self._gts = defaultdict(list) # gt for evaluation
|
283 |
+
self._dts = defaultdict(list) # dt for evaluation
|
284 |
+
self._igrgns = defaultdict(list)
|
285 |
+
|
286 |
+
for gt in gts:
|
287 |
+
iid = gt["image_id"]
|
288 |
+
if iid not in self._igrgns.keys():
|
289 |
+
self._igrgns[iid] = _getIgnoreRegion(iid, self.cocoGt)
|
290 |
+
if _checkIgnore(gt, self._igrgns[iid]):
|
291 |
+
self._gts[iid, gt["category_id"]].append(gt)
|
292 |
+
for dt in dts:
|
293 |
+
iid = dt["image_id"]
|
294 |
+
if (iid not in self._igrgns) or _checkIgnore(dt, self._igrgns[iid]):
|
295 |
+
self._dts[iid, dt["category_id"]].append(dt)
|
296 |
+
|
297 |
+
self.evalImgs = defaultdict(list) # per-image per-category evaluation results
|
298 |
+
self.eval = {} # accumulated evaluation results
|
299 |
+
|
300 |
+
def evaluate(self):
|
301 |
+
"""
|
302 |
+
Run per image evaluation on given images and store results (a list of dict) in self.evalImgs
|
303 |
+
:return: None
|
304 |
+
"""
|
305 |
+
tic = time.time()
|
306 |
+
logger.info("Running per image DensePose evaluation... {}".format(self.params.iouType))
|
307 |
+
p = self.params
|
308 |
+
# add backward compatibility if useSegm is specified in params
|
309 |
+
if p.useSegm is not None:
|
310 |
+
p.iouType = "segm" if p.useSegm == 1 else "bbox"
|
311 |
+
logger.info("useSegm (deprecated) is not None. Running DensePose evaluation")
|
312 |
+
p.imgIds = list(np.unique(p.imgIds))
|
313 |
+
if p.useCats:
|
314 |
+
p.catIds = list(np.unique(p.catIds))
|
315 |
+
p.maxDets = sorted(p.maxDets)
|
316 |
+
self.params = p
|
317 |
+
|
318 |
+
self._prepare()
|
319 |
+
# loop through images, area range, max detection number
|
320 |
+
catIds = p.catIds if p.useCats else [-1]
|
321 |
+
|
322 |
+
if p.iouType in ["segm", "bbox"]:
|
323 |
+
computeIoU = self.computeIoU
|
324 |
+
elif p.iouType == "keypoints":
|
325 |
+
computeIoU = self.computeOks
|
326 |
+
elif p.iouType == "densepose":
|
327 |
+
computeIoU = self.computeOgps
|
328 |
+
if self._dpEvalMode in {DensePoseEvalMode.GPSM, DensePoseEvalMode.IOU}:
|
329 |
+
self.real_ious = {
|
330 |
+
(imgId, catId): self.computeDPIoU(imgId, catId)
|
331 |
+
for imgId in p.imgIds
|
332 |
+
for catId in catIds
|
333 |
+
}
|
334 |
+
|
335 |
+
self.ious = {
|
336 |
+
(imgId, catId): computeIoU(imgId, catId) for imgId in p.imgIds for catId in catIds
|
337 |
+
}
|
338 |
+
|
339 |
+
evaluateImg = self.evaluateImg
|
340 |
+
maxDet = p.maxDets[-1]
|
341 |
+
self.evalImgs = [
|
342 |
+
evaluateImg(imgId, catId, areaRng, maxDet)
|
343 |
+
for catId in catIds
|
344 |
+
for areaRng in p.areaRng
|
345 |
+
for imgId in p.imgIds
|
346 |
+
]
|
347 |
+
self._paramsEval = copy.deepcopy(self.params)
|
348 |
+
toc = time.time()
|
349 |
+
logger.info("DensePose evaluation DONE (t={:0.2f}s).".format(toc - tic))
|
350 |
+
|
351 |
+
def getDensePoseMask(self, polys):
|
352 |
+
maskGen = np.zeros([256, 256])
|
353 |
+
stop = min(len(polys) + 1, 15)
|
354 |
+
for i in range(1, stop):
|
355 |
+
if polys[i - 1]:
|
356 |
+
currentMask = maskUtils.decode(polys[i - 1])
|
357 |
+
maskGen[currentMask > 0] = i
|
358 |
+
return maskGen
|
359 |
+
|
360 |
+
def _generate_rlemask_on_image(self, mask, imgId, data):
|
361 |
+
bbox_xywh = np.array(data["bbox"])
|
362 |
+
x, y, w, h = bbox_xywh
|
363 |
+
im_h, im_w = self.size_mapping[imgId]
|
364 |
+
im_mask = np.zeros((im_h, im_w), dtype=np.uint8)
|
365 |
+
if mask is not None:
|
366 |
+
x0 = max(int(x), 0)
|
367 |
+
x1 = min(int(x + w), im_w, int(x) + mask.shape[1])
|
368 |
+
y0 = max(int(y), 0)
|
369 |
+
y1 = min(int(y + h), im_h, int(y) + mask.shape[0])
|
370 |
+
y = int(y)
|
371 |
+
x = int(x)
|
372 |
+
im_mask[y0:y1, x0:x1] = mask[y0 - y : y1 - y, x0 - x : x1 - x]
|
373 |
+
im_mask = np.require(np.asarray(im_mask > 0), dtype=np.uint8, requirements=["F"])
|
374 |
+
rle_mask = maskUtils.encode(np.array(im_mask[:, :, np.newaxis], order="F"))[0]
|
375 |
+
return rle_mask
|
376 |
+
|
377 |
+
def computeDPIoU(self, imgId, catId):
|
378 |
+
p = self.params
|
379 |
+
if p.useCats:
|
380 |
+
gt = self._gts[imgId, catId]
|
381 |
+
dt = self._dts[imgId, catId]
|
382 |
+
else:
|
383 |
+
gt = [_ for cId in p.catIds for _ in self._gts[imgId, cId]]
|
384 |
+
dt = [_ for cId in p.catIds for _ in self._dts[imgId, cId]]
|
385 |
+
if len(gt) == 0 and len(dt) == 0:
|
386 |
+
return []
|
387 |
+
inds = np.argsort([-d["score"] for d in dt], kind="mergesort")
|
388 |
+
dt = [dt[i] for i in inds]
|
389 |
+
if len(dt) > p.maxDets[-1]:
|
390 |
+
dt = dt[0 : p.maxDets[-1]]
|
391 |
+
|
392 |
+
gtmasks = []
|
393 |
+
for g in gt:
|
394 |
+
if DensePoseDataRelative.S_KEY in g:
|
395 |
+
# convert DensePose mask to a binary mask
|
396 |
+
mask = np.minimum(self.getDensePoseMask(g[DensePoseDataRelative.S_KEY]), 1.0)
|
397 |
+
_, _, w, h = g["bbox"]
|
398 |
+
scale_x = float(max(w, 1)) / mask.shape[1]
|
399 |
+
scale_y = float(max(h, 1)) / mask.shape[0]
|
400 |
+
mask = spzoom(mask, (scale_y, scale_x), order=1, prefilter=False)
|
401 |
+
mask = np.array(mask > 0.5, dtype=np.uint8)
|
402 |
+
rle_mask = self._generate_rlemask_on_image(mask, imgId, g)
|
403 |
+
elif "segmentation" in g:
|
404 |
+
segmentation = g["segmentation"]
|
405 |
+
if isinstance(segmentation, list) and segmentation:
|
406 |
+
# polygons
|
407 |
+
im_h, im_w = self.size_mapping[imgId]
|
408 |
+
rles = maskUtils.frPyObjects(segmentation, im_h, im_w)
|
409 |
+
rle_mask = maskUtils.merge(rles)
|
410 |
+
elif isinstance(segmentation, dict):
|
411 |
+
if isinstance(segmentation["counts"], list):
|
412 |
+
# uncompressed RLE
|
413 |
+
im_h, im_w = self.size_mapping[imgId]
|
414 |
+
rle_mask = maskUtils.frPyObjects(segmentation, im_h, im_w)
|
415 |
+
else:
|
416 |
+
# compressed RLE
|
417 |
+
rle_mask = segmentation
|
418 |
+
else:
|
419 |
+
rle_mask = self._generate_rlemask_on_image(None, imgId, g)
|
420 |
+
else:
|
421 |
+
rle_mask = self._generate_rlemask_on_image(None, imgId, g)
|
422 |
+
gtmasks.append(rle_mask)
|
423 |
+
|
424 |
+
dtmasks = []
|
425 |
+
for d in dt:
|
426 |
+
mask = self._extract_mask(d)
|
427 |
+
mask = np.require(np.asarray(mask > 0), dtype=np.uint8, requirements=["F"])
|
428 |
+
rle_mask = self._generate_rlemask_on_image(mask, imgId, d)
|
429 |
+
dtmasks.append(rle_mask)
|
430 |
+
|
431 |
+
# compute iou between each dt and gt region
|
432 |
+
iscrowd = [int(o.get("iscrowd", 0)) for o in gt]
|
433 |
+
iousDP = maskUtils.iou(dtmasks, gtmasks, iscrowd)
|
434 |
+
return iousDP
|
435 |
+
|
436 |
+
def computeIoU(self, imgId, catId):
|
437 |
+
p = self.params
|
438 |
+
if p.useCats:
|
439 |
+
gt = self._gts[imgId, catId]
|
440 |
+
dt = self._dts[imgId, catId]
|
441 |
+
else:
|
442 |
+
gt = [_ for cId in p.catIds for _ in self._gts[imgId, cId]]
|
443 |
+
dt = [_ for cId in p.catIds for _ in self._dts[imgId, cId]]
|
444 |
+
if len(gt) == 0 and len(dt) == 0:
|
445 |
+
return []
|
446 |
+
inds = np.argsort([-d["score"] for d in dt], kind="mergesort")
|
447 |
+
dt = [dt[i] for i in inds]
|
448 |
+
if len(dt) > p.maxDets[-1]:
|
449 |
+
dt = dt[0 : p.maxDets[-1]]
|
450 |
+
|
451 |
+
if p.iouType == "segm":
|
452 |
+
g = [g["segmentation"] for g in gt if g["segmentation"] is not None]
|
453 |
+
d = [d["segmentation"] for d in dt if d["segmentation"] is not None]
|
454 |
+
elif p.iouType == "bbox":
|
455 |
+
g = [g["bbox"] for g in gt]
|
456 |
+
d = [d["bbox"] for d in dt]
|
457 |
+
else:
|
458 |
+
raise Exception("unknown iouType for iou computation")
|
459 |
+
|
460 |
+
# compute iou between each dt and gt region
|
461 |
+
iscrowd = [int(o.get("iscrowd", 0)) for o in gt]
|
462 |
+
ious = maskUtils.iou(d, g, iscrowd)
|
463 |
+
return ious
|
464 |
+
|
465 |
+
def computeOks(self, imgId, catId):
|
466 |
+
p = self.params
|
467 |
+
# dimension here should be Nxm
|
468 |
+
gts = self._gts[imgId, catId]
|
469 |
+
dts = self._dts[imgId, catId]
|
470 |
+
inds = np.argsort([-d["score"] for d in dts], kind="mergesort")
|
471 |
+
dts = [dts[i] for i in inds]
|
472 |
+
if len(dts) > p.maxDets[-1]:
|
473 |
+
dts = dts[0 : p.maxDets[-1]]
|
474 |
+
# if len(gts) == 0 and len(dts) == 0:
|
475 |
+
if len(gts) == 0 or len(dts) == 0:
|
476 |
+
return []
|
477 |
+
ious = np.zeros((len(dts), len(gts)))
|
478 |
+
sigmas = (
|
479 |
+
np.array(
|
480 |
+
[
|
481 |
+
0.26,
|
482 |
+
0.25,
|
483 |
+
0.25,
|
484 |
+
0.35,
|
485 |
+
0.35,
|
486 |
+
0.79,
|
487 |
+
0.79,
|
488 |
+
0.72,
|
489 |
+
0.72,
|
490 |
+
0.62,
|
491 |
+
0.62,
|
492 |
+
1.07,
|
493 |
+
1.07,
|
494 |
+
0.87,
|
495 |
+
0.87,
|
496 |
+
0.89,
|
497 |
+
0.89,
|
498 |
+
]
|
499 |
+
)
|
500 |
+
/ 10.0
|
501 |
+
)
|
502 |
+
vars = (sigmas * 2) ** 2
|
503 |
+
k = len(sigmas)
|
504 |
+
# compute oks between each detection and ground truth object
|
505 |
+
for j, gt in enumerate(gts):
|
506 |
+
# create bounds for ignore regions(double the gt bbox)
|
507 |
+
g = np.array(gt["keypoints"])
|
508 |
+
xg = g[0::3]
|
509 |
+
yg = g[1::3]
|
510 |
+
vg = g[2::3]
|
511 |
+
k1 = np.count_nonzero(vg > 0)
|
512 |
+
bb = gt["bbox"]
|
513 |
+
x0 = bb[0] - bb[2]
|
514 |
+
x1 = bb[0] + bb[2] * 2
|
515 |
+
y0 = bb[1] - bb[3]
|
516 |
+
y1 = bb[1] + bb[3] * 2
|
517 |
+
for i, dt in enumerate(dts):
|
518 |
+
d = np.array(dt["keypoints"])
|
519 |
+
xd = d[0::3]
|
520 |
+
yd = d[1::3]
|
521 |
+
if k1 > 0:
|
522 |
+
# measure the per-keypoint distance if keypoints visible
|
523 |
+
dx = xd - xg
|
524 |
+
dy = yd - yg
|
525 |
+
else:
|
526 |
+
# measure minimum distance to keypoints in (x0,y0) & (x1,y1)
|
527 |
+
z = np.zeros(k)
|
528 |
+
dx = np.max((z, x0 - xd), axis=0) + np.max((z, xd - x1), axis=0)
|
529 |
+
dy = np.max((z, y0 - yd), axis=0) + np.max((z, yd - y1), axis=0)
|
530 |
+
e = (dx**2 + dy**2) / vars / (gt["area"] + np.spacing(1)) / 2
|
531 |
+
if k1 > 0:
|
532 |
+
e = e[vg > 0]
|
533 |
+
ious[i, j] = np.sum(np.exp(-e)) / e.shape[0]
|
534 |
+
return ious
|
535 |
+
|
536 |
+
def _extract_mask(self, dt: Dict[str, Any]) -> np.ndarray:
|
537 |
+
if "densepose" in dt:
|
538 |
+
densepose_results_quantized = dt["densepose"]
|
539 |
+
return densepose_results_quantized.labels_uv_uint8[0].numpy()
|
540 |
+
elif "cse_mask" in dt:
|
541 |
+
return dt["cse_mask"]
|
542 |
+
elif "coarse_segm" in dt:
|
543 |
+
dy = max(int(dt["bbox"][3]), 1)
|
544 |
+
dx = max(int(dt["bbox"][2]), 1)
|
545 |
+
return (
|
546 |
+
F.interpolate(
|
547 |
+
dt["coarse_segm"].unsqueeze(0),
|
548 |
+
(dy, dx),
|
549 |
+
mode="bilinear",
|
550 |
+
align_corners=False,
|
551 |
+
)
|
552 |
+
.squeeze(0)
|
553 |
+
.argmax(0)
|
554 |
+
.numpy()
|
555 |
+
.astype(np.uint8)
|
556 |
+
)
|
557 |
+
elif "record_id" in dt:
|
558 |
+
assert (
|
559 |
+
self.multi_storage is not None
|
560 |
+
), f"Storage record id encountered in a detection {dt}, but no storage provided!"
|
561 |
+
record = self.multi_storage.get(dt["rank"], dt["record_id"])
|
562 |
+
coarse_segm = record["coarse_segm"]
|
563 |
+
dy = max(int(dt["bbox"][3]), 1)
|
564 |
+
dx = max(int(dt["bbox"][2]), 1)
|
565 |
+
return (
|
566 |
+
F.interpolate(
|
567 |
+
coarse_segm.unsqueeze(0),
|
568 |
+
(dy, dx),
|
569 |
+
mode="bilinear",
|
570 |
+
align_corners=False,
|
571 |
+
)
|
572 |
+
.squeeze(0)
|
573 |
+
.argmax(0)
|
574 |
+
.numpy()
|
575 |
+
.astype(np.uint8)
|
576 |
+
)
|
577 |
+
else:
|
578 |
+
raise Exception(f"No mask data in the detection: {dt}")
|
579 |
+
raise ValueError('The prediction dict needs to contain either "densepose" or "cse_mask"')
|
580 |
+
|
581 |
+
def _extract_iuv(
|
582 |
+
self, densepose_data: np.ndarray, py: np.ndarray, px: np.ndarray, gt: Dict[str, Any]
|
583 |
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
584 |
+
"""
|
585 |
+
Extract arrays of I, U and V values at given points as numpy arrays
|
586 |
+
given the data mode stored in self._dpDataMode
|
587 |
+
"""
|
588 |
+
if self._dpDataMode == DensePoseDataMode.IUV_DT:
|
589 |
+
# estimated labels and UV (default)
|
590 |
+
ipoints = densepose_data[0, py, px]
|
591 |
+
upoints = densepose_data[1, py, px] / 255.0 # convert from uint8 by /255.
|
592 |
+
vpoints = densepose_data[2, py, px] / 255.0
|
593 |
+
elif self._dpDataMode == DensePoseDataMode.IUV_GT:
|
594 |
+
# ground truth
|
595 |
+
ipoints = np.array(gt["dp_I"])
|
596 |
+
upoints = np.array(gt["dp_U"])
|
597 |
+
vpoints = np.array(gt["dp_V"])
|
598 |
+
elif self._dpDataMode == DensePoseDataMode.I_GT_UV_0:
|
599 |
+
# ground truth labels, UV = 0
|
600 |
+
ipoints = np.array(gt["dp_I"])
|
601 |
+
upoints = upoints * 0.0
|
602 |
+
vpoints = vpoints * 0.0
|
603 |
+
elif self._dpDataMode == DensePoseDataMode.I_GT_UV_DT:
|
604 |
+
# ground truth labels, estimated UV
|
605 |
+
ipoints = np.array(gt["dp_I"])
|
606 |
+
upoints = densepose_data[1, py, px] / 255.0 # convert from uint8 by /255.
|
607 |
+
vpoints = densepose_data[2, py, px] / 255.0
|
608 |
+
elif self._dpDataMode == DensePoseDataMode.I_DT_UV_0:
|
609 |
+
# estimated labels, UV = 0
|
610 |
+
ipoints = densepose_data[0, py, px]
|
611 |
+
upoints = upoints * 0.0
|
612 |
+
vpoints = vpoints * 0.0
|
613 |
+
else:
|
614 |
+
raise ValueError(f"Unknown data mode: {self._dpDataMode}")
|
615 |
+
return ipoints, upoints, vpoints
|
616 |
+
|
617 |
+
def computeOgps_single_pair(self, dt, gt, py, px, pt_mask):
|
618 |
+
if "densepose" in dt:
|
619 |
+
ipoints, upoints, vpoints = self.extract_iuv_from_quantized(dt, gt, py, px, pt_mask)
|
620 |
+
return self.computeOgps_single_pair_iuv(dt, gt, ipoints, upoints, vpoints)
|
621 |
+
elif "u" in dt:
|
622 |
+
ipoints, upoints, vpoints = self.extract_iuv_from_raw(dt, gt, py, px, pt_mask)
|
623 |
+
return self.computeOgps_single_pair_iuv(dt, gt, ipoints, upoints, vpoints)
|
624 |
+
elif "record_id" in dt:
|
625 |
+
assert (
|
626 |
+
self.multi_storage is not None
|
627 |
+
), f"Storage record id encountered in detection {dt}, but no storage provided!"
|
628 |
+
record = self.multi_storage.get(dt["rank"], dt["record_id"])
|
629 |
+
record["bbox"] = dt["bbox"]
|
630 |
+
if "u" in record:
|
631 |
+
ipoints, upoints, vpoints = self.extract_iuv_from_raw(record, gt, py, px, pt_mask)
|
632 |
+
return self.computeOgps_single_pair_iuv(dt, gt, ipoints, upoints, vpoints)
|
633 |
+
elif "embedding" in record:
|
634 |
+
return self.computeOgps_single_pair_cse(
|
635 |
+
dt,
|
636 |
+
gt,
|
637 |
+
py,
|
638 |
+
px,
|
639 |
+
pt_mask,
|
640 |
+
record["coarse_segm"],
|
641 |
+
record["embedding"],
|
642 |
+
record["bbox"],
|
643 |
+
)
|
644 |
+
else:
|
645 |
+
raise Exception(f"Unknown record format: {record}")
|
646 |
+
elif "embedding" in dt:
|
647 |
+
return self.computeOgps_single_pair_cse(
|
648 |
+
dt, gt, py, px, pt_mask, dt["coarse_segm"], dt["embedding"], dt["bbox"]
|
649 |
+
)
|
650 |
+
raise Exception(f"Unknown detection format: {dt}")
|
651 |
+
|
652 |
+
def extract_iuv_from_quantized(self, dt, gt, py, px, pt_mask):
|
653 |
+
densepose_results_quantized = dt["densepose"]
|
654 |
+
ipoints, upoints, vpoints = self._extract_iuv(
|
655 |
+
densepose_results_quantized.labels_uv_uint8.numpy(), py, px, gt
|
656 |
+
)
|
657 |
+
ipoints[pt_mask == -1] = 0
|
658 |
+
return ipoints, upoints, vpoints
|
659 |
+
|
660 |
+
def extract_iuv_from_raw(self, dt, gt, py, px, pt_mask):
|
661 |
+
labels_dt = resample_fine_and_coarse_segm_tensors_to_bbox(
|
662 |
+
dt["fine_segm"].unsqueeze(0),
|
663 |
+
dt["coarse_segm"].unsqueeze(0),
|
664 |
+
dt["bbox"],
|
665 |
+
)
|
666 |
+
uv = resample_uv_tensors_to_bbox(
|
667 |
+
dt["u"].unsqueeze(0), dt["v"].unsqueeze(0), labels_dt.squeeze(0), dt["bbox"]
|
668 |
+
)
|
669 |
+
labels_uv_uint8 = torch.cat((labels_dt.byte(), (uv * 255).clamp(0, 255).byte()))
|
670 |
+
ipoints, upoints, vpoints = self._extract_iuv(labels_uv_uint8.numpy(), py, px, gt)
|
671 |
+
ipoints[pt_mask == -1] = 0
|
672 |
+
return ipoints, upoints, vpoints
|
673 |
+
|
674 |
+
def computeOgps_single_pair_iuv(self, dt, gt, ipoints, upoints, vpoints):
|
675 |
+
cVertsGT, ClosestVertsGTTransformed = self.findAllClosestVertsGT(gt)
|
676 |
+
cVerts = self.findAllClosestVertsUV(upoints, vpoints, ipoints)
|
677 |
+
# Get pairwise geodesic distances between gt and estimated mesh points.
|
678 |
+
dist = self.getDistancesUV(ClosestVertsGTTransformed, cVerts)
|
679 |
+
# Compute the Ogps measure.
|
680 |
+
# Find the mean geodesic normalization distance for
|
681 |
+
# each GT point, based on which part it is on.
|
682 |
+
Current_Mean_Distances = self.Mean_Distances[
|
683 |
+
self.CoarseParts[self.Part_ids[cVertsGT[cVertsGT > 0].astype(int) - 1]]
|
684 |
+
]
|
685 |
+
return dist, Current_Mean_Distances
|
686 |
+
|
687 |
+
def computeOgps_single_pair_cse(
|
688 |
+
self, dt, gt, py, px, pt_mask, coarse_segm, embedding, bbox_xywh_abs
|
689 |
+
):
|
690 |
+
# 0-based mesh vertex indices
|
691 |
+
cVertsGT = torch.as_tensor(gt["dp_vertex"], dtype=torch.int64)
|
692 |
+
# label for each pixel of the bbox, [H, W] tensor of long
|
693 |
+
labels_dt = resample_coarse_segm_tensor_to_bbox(
|
694 |
+
coarse_segm.unsqueeze(0), bbox_xywh_abs
|
695 |
+
).squeeze(0)
|
696 |
+
x, y, w, h = bbox_xywh_abs
|
697 |
+
# embedding for each pixel of the bbox, [D, H, W] tensor of float32
|
698 |
+
embedding = F.interpolate(
|
699 |
+
embedding.unsqueeze(0), (int(h), int(w)), mode="bilinear", align_corners=False
|
700 |
+
).squeeze(0)
|
701 |
+
# valid locations py, px
|
702 |
+
py_pt = torch.from_numpy(py[pt_mask > -1])
|
703 |
+
px_pt = torch.from_numpy(px[pt_mask > -1])
|
704 |
+
cVerts = torch.ones_like(cVertsGT) * -1
|
705 |
+
cVerts[pt_mask > -1] = self.findClosestVertsCse(
|
706 |
+
embedding, py_pt, px_pt, labels_dt, gt["ref_model"]
|
707 |
+
)
|
708 |
+
# Get pairwise geodesic distances between gt and estimated mesh points.
|
709 |
+
dist = self.getDistancesCse(cVertsGT, cVerts, gt["ref_model"])
|
710 |
+
# normalize distances
|
711 |
+
if (gt["ref_model"] == "smpl_27554") and ("dp_I" in gt):
|
712 |
+
Current_Mean_Distances = self.Mean_Distances[
|
713 |
+
self.CoarseParts[np.array(gt["dp_I"], dtype=int)]
|
714 |
+
]
|
715 |
+
else:
|
716 |
+
Current_Mean_Distances = 0.255
|
717 |
+
return dist, Current_Mean_Distances
|
718 |
+
|
719 |
+
def computeOgps(self, imgId, catId):
|
720 |
+
p = self.params
|
721 |
+
# dimension here should be Nxm
|
722 |
+
g = self._gts[imgId, catId]
|
723 |
+
d = self._dts[imgId, catId]
|
724 |
+
inds = np.argsort([-d_["score"] for d_ in d], kind="mergesort")
|
725 |
+
d = [d[i] for i in inds]
|
726 |
+
if len(d) > p.maxDets[-1]:
|
727 |
+
d = d[0 : p.maxDets[-1]]
|
728 |
+
# if len(gts) == 0 and len(dts) == 0:
|
729 |
+
if len(g) == 0 or len(d) == 0:
|
730 |
+
return []
|
731 |
+
ious = np.zeros((len(d), len(g)))
|
732 |
+
# compute opgs between each detection and ground truth object
|
733 |
+
# sigma = self.sigma #0.255 # dist = 0.3m corresponds to ogps = 0.5
|
734 |
+
# 1 # dist = 0.3m corresponds to ogps = 0.96
|
735 |
+
# 1.45 # dist = 1.7m (person height) corresponds to ogps = 0.5)
|
736 |
+
for j, gt in enumerate(g):
|
737 |
+
if not gt["ignore"]:
|
738 |
+
g_ = gt["bbox"]
|
739 |
+
for i, dt in enumerate(d):
|
740 |
+
#
|
741 |
+
dy = int(dt["bbox"][3])
|
742 |
+
dx = int(dt["bbox"][2])
|
743 |
+
dp_x = np.array(gt["dp_x"]) * g_[2] / 255.0
|
744 |
+
dp_y = np.array(gt["dp_y"]) * g_[3] / 255.0
|
745 |
+
py = (dp_y + g_[1] - dt["bbox"][1]).astype(int)
|
746 |
+
px = (dp_x + g_[0] - dt["bbox"][0]).astype(int)
|
747 |
+
#
|
748 |
+
pts = np.zeros(len(px))
|
749 |
+
pts[px >= dx] = -1
|
750 |
+
pts[py >= dy] = -1
|
751 |
+
pts[px < 0] = -1
|
752 |
+
pts[py < 0] = -1
|
753 |
+
if len(pts) < 1:
|
754 |
+
ogps = 0.0
|
755 |
+
elif np.max(pts) == -1:
|
756 |
+
ogps = 0.0
|
757 |
+
else:
|
758 |
+
px[pts == -1] = 0
|
759 |
+
py[pts == -1] = 0
|
760 |
+
dists_between_matches, dist_norm_coeffs = self.computeOgps_single_pair(
|
761 |
+
dt, gt, py, px, pts
|
762 |
+
)
|
763 |
+
# Compute gps
|
764 |
+
ogps_values = np.exp(
|
765 |
+
-(dists_between_matches**2) / (2 * (dist_norm_coeffs**2))
|
766 |
+
)
|
767 |
+
#
|
768 |
+
ogps = np.mean(ogps_values) if len(ogps_values) > 0 else 0.0
|
769 |
+
ious[i, j] = ogps
|
770 |
+
|
771 |
+
gbb = [gt["bbox"] for gt in g]
|
772 |
+
dbb = [dt["bbox"] for dt in d]
|
773 |
+
|
774 |
+
# compute iou between each dt and gt region
|
775 |
+
iscrowd = [int(o.get("iscrowd", 0)) for o in g]
|
776 |
+
ious_bb = maskUtils.iou(dbb, gbb, iscrowd)
|
777 |
+
return ious, ious_bb
|
778 |
+
|
779 |
+
def evaluateImg(self, imgId, catId, aRng, maxDet):
|
780 |
+
"""
|
781 |
+
perform evaluation for single category and image
|
782 |
+
:return: dict (single image results)
|
783 |
+
"""
|
784 |
+
|
785 |
+
p = self.params
|
786 |
+
if p.useCats:
|
787 |
+
gt = self._gts[imgId, catId]
|
788 |
+
dt = self._dts[imgId, catId]
|
789 |
+
else:
|
790 |
+
gt = [_ for cId in p.catIds for _ in self._gts[imgId, cId]]
|
791 |
+
dt = [_ for cId in p.catIds for _ in self._dts[imgId, cId]]
|
792 |
+
if len(gt) == 0 and len(dt) == 0:
|
793 |
+
return None
|
794 |
+
|
795 |
+
for g in gt:
|
796 |
+
# g['_ignore'] = g['ignore']
|
797 |
+
if g["ignore"] or (g["area"] < aRng[0] or g["area"] > aRng[1]):
|
798 |
+
g["_ignore"] = True
|
799 |
+
else:
|
800 |
+
g["_ignore"] = False
|
801 |
+
|
802 |
+
# sort dt highest score first, sort gt ignore last
|
803 |
+
gtind = np.argsort([g["_ignore"] for g in gt], kind="mergesort")
|
804 |
+
gt = [gt[i] for i in gtind]
|
805 |
+
dtind = np.argsort([-d["score"] for d in dt], kind="mergesort")
|
806 |
+
dt = [dt[i] for i in dtind[0:maxDet]]
|
807 |
+
iscrowd = [int(o.get("iscrowd", 0)) for o in gt]
|
808 |
+
# load computed ious
|
809 |
+
if p.iouType == "densepose":
|
810 |
+
# print('Checking the length', len(self.ious[imgId, catId]))
|
811 |
+
# if len(self.ious[imgId, catId]) == 0:
|
812 |
+
# print(self.ious[imgId, catId])
|
813 |
+
ious = (
|
814 |
+
self.ious[imgId, catId][0][:, gtind]
|
815 |
+
if len(self.ious[imgId, catId]) > 0
|
816 |
+
else self.ious[imgId, catId]
|
817 |
+
)
|
818 |
+
ioubs = (
|
819 |
+
self.ious[imgId, catId][1][:, gtind]
|
820 |
+
if len(self.ious[imgId, catId]) > 0
|
821 |
+
else self.ious[imgId, catId]
|
822 |
+
)
|
823 |
+
if self._dpEvalMode in {DensePoseEvalMode.GPSM, DensePoseEvalMode.IOU}:
|
824 |
+
iousM = (
|
825 |
+
self.real_ious[imgId, catId][:, gtind]
|
826 |
+
if len(self.real_ious[imgId, catId]) > 0
|
827 |
+
else self.real_ious[imgId, catId]
|
828 |
+
)
|
829 |
+
else:
|
830 |
+
ious = (
|
831 |
+
self.ious[imgId, catId][:, gtind]
|
832 |
+
if len(self.ious[imgId, catId]) > 0
|
833 |
+
else self.ious[imgId, catId]
|
834 |
+
)
|
835 |
+
|
836 |
+
T = len(p.iouThrs)
|
837 |
+
G = len(gt)
|
838 |
+
D = len(dt)
|
839 |
+
gtm = np.zeros((T, G))
|
840 |
+
dtm = np.zeros((T, D))
|
841 |
+
gtIg = np.array([g["_ignore"] for g in gt])
|
842 |
+
dtIg = np.zeros((T, D))
|
843 |
+
if np.all(gtIg) and p.iouType == "densepose":
|
844 |
+
dtIg = np.logical_or(dtIg, True)
|
845 |
+
|
846 |
+
if len(ious) > 0: # and not p.iouType == 'densepose':
|
847 |
+
for tind, t in enumerate(p.iouThrs):
|
848 |
+
for dind, d in enumerate(dt):
|
849 |
+
# information about best match so far (m=-1 -> unmatched)
|
850 |
+
iou = min([t, 1 - 1e-10])
|
851 |
+
m = -1
|
852 |
+
for gind, _g in enumerate(gt):
|
853 |
+
# if this gt already matched, and not a crowd, continue
|
854 |
+
if gtm[tind, gind] > 0 and not iscrowd[gind]:
|
855 |
+
continue
|
856 |
+
# if dt matched to reg gt, and on ignore gt, stop
|
857 |
+
if m > -1 and gtIg[m] == 0 and gtIg[gind] == 1:
|
858 |
+
break
|
859 |
+
if p.iouType == "densepose":
|
860 |
+
if self._dpEvalMode == DensePoseEvalMode.GPSM:
|
861 |
+
new_iou = np.sqrt(iousM[dind, gind] * ious[dind, gind])
|
862 |
+
elif self._dpEvalMode == DensePoseEvalMode.IOU:
|
863 |
+
new_iou = iousM[dind, gind]
|
864 |
+
elif self._dpEvalMode == DensePoseEvalMode.GPS:
|
865 |
+
new_iou = ious[dind, gind]
|
866 |
+
else:
|
867 |
+
new_iou = ious[dind, gind]
|
868 |
+
if new_iou < iou:
|
869 |
+
continue
|
870 |
+
if new_iou == 0.0:
|
871 |
+
continue
|
872 |
+
# if match successful and best so far, store appropriately
|
873 |
+
iou = new_iou
|
874 |
+
m = gind
|
875 |
+
# if match made store id of match for both dt and gt
|
876 |
+
if m == -1:
|
877 |
+
continue
|
878 |
+
dtIg[tind, dind] = gtIg[m]
|
879 |
+
dtm[tind, dind] = gt[m]["id"]
|
880 |
+
gtm[tind, m] = d["id"]
|
881 |
+
|
882 |
+
if p.iouType == "densepose":
|
883 |
+
if not len(ioubs) == 0:
|
884 |
+
for dind, d in enumerate(dt):
|
885 |
+
# information about best match so far (m=-1 -> unmatched)
|
886 |
+
if dtm[tind, dind] == 0:
|
887 |
+
ioub = 0.8
|
888 |
+
m = -1
|
889 |
+
for gind, _g in enumerate(gt):
|
890 |
+
# if this gt already matched, and not a crowd, continue
|
891 |
+
if gtm[tind, gind] > 0 and not iscrowd[gind]:
|
892 |
+
continue
|
893 |
+
# continue to next gt unless better match made
|
894 |
+
if ioubs[dind, gind] < ioub:
|
895 |
+
continue
|
896 |
+
# if match successful and best so far, store appropriately
|
897 |
+
ioub = ioubs[dind, gind]
|
898 |
+
m = gind
|
899 |
+
# if match made store id of match for both dt and gt
|
900 |
+
if m > -1:
|
901 |
+
dtIg[:, dind] = gtIg[m]
|
902 |
+
if gtIg[m]:
|
903 |
+
dtm[tind, dind] = gt[m]["id"]
|
904 |
+
gtm[tind, m] = d["id"]
|
905 |
+
# set unmatched detections outside of area range to ignore
|
906 |
+
a = np.array([d["area"] < aRng[0] or d["area"] > aRng[1] for d in dt]).reshape((1, len(dt)))
|
907 |
+
dtIg = np.logical_or(dtIg, np.logical_and(dtm == 0, np.repeat(a, T, 0)))
|
908 |
+
# store results for given image and category
|
909 |
+
# print('Done with the function', len(self.ious[imgId, catId]))
|
910 |
+
return {
|
911 |
+
"image_id": imgId,
|
912 |
+
"category_id": catId,
|
913 |
+
"aRng": aRng,
|
914 |
+
"maxDet": maxDet,
|
915 |
+
"dtIds": [d["id"] for d in dt],
|
916 |
+
"gtIds": [g["id"] for g in gt],
|
917 |
+
"dtMatches": dtm,
|
918 |
+
"gtMatches": gtm,
|
919 |
+
"dtScores": [d["score"] for d in dt],
|
920 |
+
"gtIgnore": gtIg,
|
921 |
+
"dtIgnore": dtIg,
|
922 |
+
}
|
923 |
+
|
924 |
+
def accumulate(self, p=None):
|
925 |
+
"""
|
926 |
+
Accumulate per image evaluation results and store the result in self.eval
|
927 |
+
:param p: input params for evaluation
|
928 |
+
:return: None
|
929 |
+
"""
|
930 |
+
logger.info("Accumulating evaluation results...")
|
931 |
+
tic = time.time()
|
932 |
+
if not self.evalImgs:
|
933 |
+
logger.info("Please run evaluate() first")
|
934 |
+
# allows input customized parameters
|
935 |
+
if p is None:
|
936 |
+
p = self.params
|
937 |
+
p.catIds = p.catIds if p.useCats == 1 else [-1]
|
938 |
+
T = len(p.iouThrs)
|
939 |
+
R = len(p.recThrs)
|
940 |
+
K = len(p.catIds) if p.useCats else 1
|
941 |
+
A = len(p.areaRng)
|
942 |
+
M = len(p.maxDets)
|
943 |
+
precision = -(np.ones((T, R, K, A, M))) # -1 for the precision of absent categories
|
944 |
+
recall = -(np.ones((T, K, A, M)))
|
945 |
+
|
946 |
+
# create dictionary for future indexing
|
947 |
+
logger.info("Categories: {}".format(p.catIds))
|
948 |
+
_pe = self._paramsEval
|
949 |
+
catIds = _pe.catIds if _pe.useCats else [-1]
|
950 |
+
setK = set(catIds)
|
951 |
+
setA = set(map(tuple, _pe.areaRng))
|
952 |
+
setM = set(_pe.maxDets)
|
953 |
+
setI = set(_pe.imgIds)
|
954 |
+
# get inds to evaluate
|
955 |
+
k_list = [n for n, k in enumerate(p.catIds) if k in setK]
|
956 |
+
m_list = [m for n, m in enumerate(p.maxDets) if m in setM]
|
957 |
+
a_list = [n for n, a in enumerate(map(lambda x: tuple(x), p.areaRng)) if a in setA]
|
958 |
+
i_list = [n for n, i in enumerate(p.imgIds) if i in setI]
|
959 |
+
I0 = len(_pe.imgIds)
|
960 |
+
A0 = len(_pe.areaRng)
|
961 |
+
# retrieve E at each category, area range, and max number of detections
|
962 |
+
for k, k0 in enumerate(k_list):
|
963 |
+
Nk = k0 * A0 * I0
|
964 |
+
for a, a0 in enumerate(a_list):
|
965 |
+
Na = a0 * I0
|
966 |
+
for m, maxDet in enumerate(m_list):
|
967 |
+
E = [self.evalImgs[Nk + Na + i] for i in i_list]
|
968 |
+
E = [e for e in E if e is not None]
|
969 |
+
if len(E) == 0:
|
970 |
+
continue
|
971 |
+
dtScores = np.concatenate([e["dtScores"][0:maxDet] for e in E])
|
972 |
+
|
973 |
+
# different sorting method generates slightly different results.
|
974 |
+
# mergesort is used to be consistent as Matlab implementation.
|
975 |
+
inds = np.argsort(-dtScores, kind="mergesort")
|
976 |
+
|
977 |
+
dtm = np.concatenate([e["dtMatches"][:, 0:maxDet] for e in E], axis=1)[:, inds]
|
978 |
+
dtIg = np.concatenate([e["dtIgnore"][:, 0:maxDet] for e in E], axis=1)[:, inds]
|
979 |
+
gtIg = np.concatenate([e["gtIgnore"] for e in E])
|
980 |
+
npig = np.count_nonzero(gtIg == 0)
|
981 |
+
if npig == 0:
|
982 |
+
continue
|
983 |
+
tps = np.logical_and(dtm, np.logical_not(dtIg))
|
984 |
+
fps = np.logical_and(np.logical_not(dtm), np.logical_not(dtIg))
|
985 |
+
tp_sum = np.cumsum(tps, axis=1).astype(dtype=float)
|
986 |
+
fp_sum = np.cumsum(fps, axis=1).astype(dtype=float)
|
987 |
+
for t, (tp, fp) in enumerate(zip(tp_sum, fp_sum)):
|
988 |
+
tp = np.array(tp)
|
989 |
+
fp = np.array(fp)
|
990 |
+
nd = len(tp)
|
991 |
+
rc = tp / npig
|
992 |
+
pr = tp / (fp + tp + np.spacing(1))
|
993 |
+
q = np.zeros((R,))
|
994 |
+
|
995 |
+
if nd:
|
996 |
+
recall[t, k, a, m] = rc[-1]
|
997 |
+
else:
|
998 |
+
recall[t, k, a, m] = 0
|
999 |
+
|
1000 |
+
# numpy is slow without cython optimization for accessing elements
|
1001 |
+
# use python array gets significant speed improvement
|
1002 |
+
pr = pr.tolist()
|
1003 |
+
q = q.tolist()
|
1004 |
+
|
1005 |
+
for i in range(nd - 1, 0, -1):
|
1006 |
+
if pr[i] > pr[i - 1]:
|
1007 |
+
pr[i - 1] = pr[i]
|
1008 |
+
|
1009 |
+
inds = np.searchsorted(rc, p.recThrs, side="left")
|
1010 |
+
try:
|
1011 |
+
for ri, pi in enumerate(inds):
|
1012 |
+
q[ri] = pr[pi]
|
1013 |
+
except Exception:
|
1014 |
+
pass
|
1015 |
+
precision[t, :, k, a, m] = np.array(q)
|
1016 |
+
logger.info(
|
1017 |
+
"Final: max precision {}, min precision {}".format(np.max(precision), np.min(precision))
|
1018 |
+
)
|
1019 |
+
self.eval = {
|
1020 |
+
"params": p,
|
1021 |
+
"counts": [T, R, K, A, M],
|
1022 |
+
"date": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
1023 |
+
"precision": precision,
|
1024 |
+
"recall": recall,
|
1025 |
+
}
|
1026 |
+
toc = time.time()
|
1027 |
+
logger.info("DONE (t={:0.2f}s).".format(toc - tic))
|
1028 |
+
|
1029 |
+
def summarize(self):
|
1030 |
+
"""
|
1031 |
+
Compute and display summary metrics for evaluation results.
|
1032 |
+
Note this function can *only* be applied on the default parameter setting
|
1033 |
+
"""
|
1034 |
+
|
1035 |
+
def _summarize(ap=1, iouThr=None, areaRng="all", maxDets=100):
|
1036 |
+
p = self.params
|
1037 |
+
iStr = " {:<18} {} @[ {}={:<9} | area={:>6s} | maxDets={:>3d} ] = {:0.3f}"
|
1038 |
+
titleStr = "Average Precision" if ap == 1 else "Average Recall"
|
1039 |
+
typeStr = "(AP)" if ap == 1 else "(AR)"
|
1040 |
+
measure = "IoU"
|
1041 |
+
if self.params.iouType == "keypoints":
|
1042 |
+
measure = "OKS"
|
1043 |
+
elif self.params.iouType == "densepose":
|
1044 |
+
measure = "OGPS"
|
1045 |
+
iouStr = (
|
1046 |
+
"{:0.2f}:{:0.2f}".format(p.iouThrs[0], p.iouThrs[-1])
|
1047 |
+
if iouThr is None
|
1048 |
+
else "{:0.2f}".format(iouThr)
|
1049 |
+
)
|
1050 |
+
|
1051 |
+
aind = [i for i, aRng in enumerate(p.areaRngLbl) if aRng == areaRng]
|
1052 |
+
mind = [i for i, mDet in enumerate(p.maxDets) if mDet == maxDets]
|
1053 |
+
if ap == 1:
|
1054 |
+
# dimension of precision: [TxRxKxAxM]
|
1055 |
+
s = self.eval["precision"]
|
1056 |
+
# IoU
|
1057 |
+
if iouThr is not None:
|
1058 |
+
t = np.where(np.abs(iouThr - p.iouThrs) < 0.001)[0]
|
1059 |
+
s = s[t]
|
1060 |
+
s = s[:, :, :, aind, mind]
|
1061 |
+
else:
|
1062 |
+
# dimension of recall: [TxKxAxM]
|
1063 |
+
s = self.eval["recall"]
|
1064 |
+
if iouThr is not None:
|
1065 |
+
t = np.where(np.abs(iouThr - p.iouThrs) < 0.001)[0]
|
1066 |
+
s = s[t]
|
1067 |
+
s = s[:, :, aind, mind]
|
1068 |
+
if len(s[s > -1]) == 0:
|
1069 |
+
mean_s = -1
|
1070 |
+
else:
|
1071 |
+
mean_s = np.mean(s[s > -1])
|
1072 |
+
logger.info(iStr.format(titleStr, typeStr, measure, iouStr, areaRng, maxDets, mean_s))
|
1073 |
+
return mean_s
|
1074 |
+
|
1075 |
+
def _summarizeDets():
|
1076 |
+
stats = np.zeros((12,))
|
1077 |
+
stats[0] = _summarize(1)
|
1078 |
+
stats[1] = _summarize(1, iouThr=0.5, maxDets=self.params.maxDets[2])
|
1079 |
+
stats[2] = _summarize(1, iouThr=0.75, maxDets=self.params.maxDets[2])
|
1080 |
+
stats[3] = _summarize(1, areaRng="small", maxDets=self.params.maxDets[2])
|
1081 |
+
stats[4] = _summarize(1, areaRng="medium", maxDets=self.params.maxDets[2])
|
1082 |
+
stats[5] = _summarize(1, areaRng="large", maxDets=self.params.maxDets[2])
|
1083 |
+
stats[6] = _summarize(0, maxDets=self.params.maxDets[0])
|
1084 |
+
stats[7] = _summarize(0, maxDets=self.params.maxDets[1])
|
1085 |
+
stats[8] = _summarize(0, maxDets=self.params.maxDets[2])
|
1086 |
+
stats[9] = _summarize(0, areaRng="small", maxDets=self.params.maxDets[2])
|
1087 |
+
stats[10] = _summarize(0, areaRng="medium", maxDets=self.params.maxDets[2])
|
1088 |
+
stats[11] = _summarize(0, areaRng="large", maxDets=self.params.maxDets[2])
|
1089 |
+
return stats
|
1090 |
+
|
1091 |
+
def _summarizeKps():
|
1092 |
+
stats = np.zeros((10,))
|
1093 |
+
stats[0] = _summarize(1, maxDets=20)
|
1094 |
+
stats[1] = _summarize(1, maxDets=20, iouThr=0.5)
|
1095 |
+
stats[2] = _summarize(1, maxDets=20, iouThr=0.75)
|
1096 |
+
stats[3] = _summarize(1, maxDets=20, areaRng="medium")
|
1097 |
+
stats[4] = _summarize(1, maxDets=20, areaRng="large")
|
1098 |
+
stats[5] = _summarize(0, maxDets=20)
|
1099 |
+
stats[6] = _summarize(0, maxDets=20, iouThr=0.5)
|
1100 |
+
stats[7] = _summarize(0, maxDets=20, iouThr=0.75)
|
1101 |
+
stats[8] = _summarize(0, maxDets=20, areaRng="medium")
|
1102 |
+
stats[9] = _summarize(0, maxDets=20, areaRng="large")
|
1103 |
+
return stats
|
1104 |
+
|
1105 |
+
def _summarizeUvs():
|
1106 |
+
stats = [_summarize(1, maxDets=self.params.maxDets[0])]
|
1107 |
+
min_threshold = self.params.iouThrs.min()
|
1108 |
+
if min_threshold <= 0.201:
|
1109 |
+
stats += [_summarize(1, maxDets=self.params.maxDets[0], iouThr=0.2)]
|
1110 |
+
if min_threshold <= 0.301:
|
1111 |
+
stats += [_summarize(1, maxDets=self.params.maxDets[0], iouThr=0.3)]
|
1112 |
+
if min_threshold <= 0.401:
|
1113 |
+
stats += [_summarize(1, maxDets=self.params.maxDets[0], iouThr=0.4)]
|
1114 |
+
stats += [
|
1115 |
+
_summarize(1, maxDets=self.params.maxDets[0], iouThr=0.5),
|
1116 |
+
_summarize(1, maxDets=self.params.maxDets[0], iouThr=0.75),
|
1117 |
+
_summarize(1, maxDets=self.params.maxDets[0], areaRng="medium"),
|
1118 |
+
_summarize(1, maxDets=self.params.maxDets[0], areaRng="large"),
|
1119 |
+
_summarize(0, maxDets=self.params.maxDets[0]),
|
1120 |
+
_summarize(0, maxDets=self.params.maxDets[0], iouThr=0.5),
|
1121 |
+
_summarize(0, maxDets=self.params.maxDets[0], iouThr=0.75),
|
1122 |
+
_summarize(0, maxDets=self.params.maxDets[0], areaRng="medium"),
|
1123 |
+
_summarize(0, maxDets=self.params.maxDets[0], areaRng="large"),
|
1124 |
+
]
|
1125 |
+
return np.array(stats)
|
1126 |
+
|
1127 |
+
def _summarizeUvsOld():
|
1128 |
+
stats = np.zeros((18,))
|
1129 |
+
stats[0] = _summarize(1, maxDets=self.params.maxDets[0])
|
1130 |
+
stats[1] = _summarize(1, maxDets=self.params.maxDets[0], iouThr=0.5)
|
1131 |
+
stats[2] = _summarize(1, maxDets=self.params.maxDets[0], iouThr=0.55)
|
1132 |
+
stats[3] = _summarize(1, maxDets=self.params.maxDets[0], iouThr=0.60)
|
1133 |
+
stats[4] = _summarize(1, maxDets=self.params.maxDets[0], iouThr=0.65)
|
1134 |
+
stats[5] = _summarize(1, maxDets=self.params.maxDets[0], iouThr=0.70)
|
1135 |
+
stats[6] = _summarize(1, maxDets=self.params.maxDets[0], iouThr=0.75)
|
1136 |
+
stats[7] = _summarize(1, maxDets=self.params.maxDets[0], iouThr=0.80)
|
1137 |
+
stats[8] = _summarize(1, maxDets=self.params.maxDets[0], iouThr=0.85)
|
1138 |
+
stats[9] = _summarize(1, maxDets=self.params.maxDets[0], iouThr=0.90)
|
1139 |
+
stats[10] = _summarize(1, maxDets=self.params.maxDets[0], iouThr=0.95)
|
1140 |
+
stats[11] = _summarize(1, maxDets=self.params.maxDets[0], areaRng="medium")
|
1141 |
+
stats[12] = _summarize(1, maxDets=self.params.maxDets[0], areaRng="large")
|
1142 |
+
stats[13] = _summarize(0, maxDets=self.params.maxDets[0])
|
1143 |
+
stats[14] = _summarize(0, maxDets=self.params.maxDets[0], iouThr=0.5)
|
1144 |
+
stats[15] = _summarize(0, maxDets=self.params.maxDets[0], iouThr=0.75)
|
1145 |
+
stats[16] = _summarize(0, maxDets=self.params.maxDets[0], areaRng="medium")
|
1146 |
+
stats[17] = _summarize(0, maxDets=self.params.maxDets[0], areaRng="large")
|
1147 |
+
return stats
|
1148 |
+
|
1149 |
+
if not self.eval:
|
1150 |
+
raise Exception("Please run accumulate() first")
|
1151 |
+
iouType = self.params.iouType
|
1152 |
+
if iouType in ["segm", "bbox"]:
|
1153 |
+
summarize = _summarizeDets
|
1154 |
+
elif iouType in ["keypoints"]:
|
1155 |
+
summarize = _summarizeKps
|
1156 |
+
elif iouType in ["densepose"]:
|
1157 |
+
summarize = _summarizeUvs
|
1158 |
+
self.stats = summarize()
|
1159 |
+
|
1160 |
+
def __str__(self):
|
1161 |
+
self.summarize()
|
1162 |
+
|
1163 |
+
# ================ functions for dense pose ==============================
|
1164 |
+
def findAllClosestVertsUV(self, U_points, V_points, Index_points):
|
1165 |
+
ClosestVerts = np.ones(Index_points.shape) * -1
|
1166 |
+
for i in np.arange(24):
|
1167 |
+
#
|
1168 |
+
if (i + 1) in Index_points:
|
1169 |
+
UVs = np.array(
|
1170 |
+
[U_points[Index_points == (i + 1)], V_points[Index_points == (i + 1)]]
|
1171 |
+
)
|
1172 |
+
Current_Part_UVs = self.Part_UVs[i]
|
1173 |
+
Current_Part_ClosestVertInds = self.Part_ClosestVertInds[i]
|
1174 |
+
D = ssd.cdist(Current_Part_UVs.transpose(), UVs.transpose()).squeeze()
|
1175 |
+
ClosestVerts[Index_points == (i + 1)] = Current_Part_ClosestVertInds[
|
1176 |
+
np.argmin(D, axis=0)
|
1177 |
+
]
|
1178 |
+
ClosestVertsTransformed = self.PDIST_transform[ClosestVerts.astype(int) - 1]
|
1179 |
+
ClosestVertsTransformed[ClosestVerts < 0] = 0
|
1180 |
+
return ClosestVertsTransformed
|
1181 |
+
|
1182 |
+
def findClosestVertsCse(self, embedding, py, px, mask, mesh_name):
|
1183 |
+
mesh_vertex_embeddings = self.embedder(mesh_name)
|
1184 |
+
pixel_embeddings = embedding[:, py, px].t().to(device="cuda")
|
1185 |
+
mask_vals = mask[py, px]
|
1186 |
+
edm = squared_euclidean_distance_matrix(pixel_embeddings, mesh_vertex_embeddings)
|
1187 |
+
vertex_indices = edm.argmin(dim=1).cpu()
|
1188 |
+
vertex_indices[mask_vals <= 0] = -1
|
1189 |
+
return vertex_indices
|
1190 |
+
|
1191 |
+
def findAllClosestVertsGT(self, gt):
|
1192 |
+
#
|
1193 |
+
I_gt = np.array(gt["dp_I"])
|
1194 |
+
U_gt = np.array(gt["dp_U"])
|
1195 |
+
V_gt = np.array(gt["dp_V"])
|
1196 |
+
#
|
1197 |
+
# print(I_gt)
|
1198 |
+
#
|
1199 |
+
ClosestVertsGT = np.ones(I_gt.shape) * -1
|
1200 |
+
for i in np.arange(24):
|
1201 |
+
if (i + 1) in I_gt:
|
1202 |
+
UVs = np.array([U_gt[I_gt == (i + 1)], V_gt[I_gt == (i + 1)]])
|
1203 |
+
Current_Part_UVs = self.Part_UVs[i]
|
1204 |
+
Current_Part_ClosestVertInds = self.Part_ClosestVertInds[i]
|
1205 |
+
D = ssd.cdist(Current_Part_UVs.transpose(), UVs.transpose()).squeeze()
|
1206 |
+
ClosestVertsGT[I_gt == (i + 1)] = Current_Part_ClosestVertInds[np.argmin(D, axis=0)]
|
1207 |
+
#
|
1208 |
+
ClosestVertsGTTransformed = self.PDIST_transform[ClosestVertsGT.astype(int) - 1]
|
1209 |
+
ClosestVertsGTTransformed[ClosestVertsGT < 0] = 0
|
1210 |
+
return ClosestVertsGT, ClosestVertsGTTransformed
|
1211 |
+
|
1212 |
+
def getDistancesCse(self, cVertsGT, cVerts, mesh_name):
|
1213 |
+
geodists_vertices = torch.ones_like(cVertsGT) * float("inf")
|
1214 |
+
selected = (cVertsGT >= 0) * (cVerts >= 0)
|
1215 |
+
mesh = create_mesh(mesh_name, "cpu")
|
1216 |
+
geodists_vertices[selected] = mesh.geodists[cVertsGT[selected], cVerts[selected]]
|
1217 |
+
return geodists_vertices.numpy()
|
1218 |
+
|
1219 |
+
def getDistancesUV(self, cVertsGT, cVerts):
|
1220 |
+
#
|
1221 |
+
n = 27554
|
1222 |
+
dists = []
|
1223 |
+
for d in range(len(cVertsGT)):
|
1224 |
+
if cVertsGT[d] > 0:
|
1225 |
+
if cVerts[d] > 0:
|
1226 |
+
i = cVertsGT[d] - 1
|
1227 |
+
j = cVerts[d] - 1
|
1228 |
+
if j == i:
|
1229 |
+
dists.append(0)
|
1230 |
+
elif j > i:
|
1231 |
+
ccc = i
|
1232 |
+
i = j
|
1233 |
+
j = ccc
|
1234 |
+
i = n - i - 1
|
1235 |
+
j = n - j - 1
|
1236 |
+
k = (n * (n - 1) / 2) - (n - i) * ((n - i) - 1) / 2 + j - i - 1
|
1237 |
+
k = (n * n - n) / 2 - k - 1
|
1238 |
+
dists.append(self.Pdist_matrix[int(k)][0])
|
1239 |
+
else:
|
1240 |
+
i = n - i - 1
|
1241 |
+
j = n - j - 1
|
1242 |
+
k = (n * (n - 1) / 2) - (n - i) * ((n - i) - 1) / 2 + j - i - 1
|
1243 |
+
k = (n * n - n) / 2 - k - 1
|
1244 |
+
dists.append(self.Pdist_matrix[int(k)][0])
|
1245 |
+
else:
|
1246 |
+
dists.append(np.inf)
|
1247 |
+
return np.atleast_1d(np.array(dists).squeeze())
|
1248 |
+
|
1249 |
+
|
1250 |
+
class Params:
|
1251 |
+
"""
|
1252 |
+
Params for coco evaluation api
|
1253 |
+
"""
|
1254 |
+
|
1255 |
+
def setDetParams(self):
|
1256 |
+
self.imgIds = []
|
1257 |
+
self.catIds = []
|
1258 |
+
# np.arange causes trouble. the data point on arange is slightly larger than the true value
|
1259 |
+
self.iouThrs = np.linspace(0.5, 0.95, int(np.round((0.95 - 0.5) / 0.05)) + 1, endpoint=True)
|
1260 |
+
self.recThrs = np.linspace(0.0, 1.00, int(np.round((1.00 - 0.0) / 0.01)) + 1, endpoint=True)
|
1261 |
+
self.maxDets = [1, 10, 100]
|
1262 |
+
self.areaRng = [
|
1263 |
+
[0**2, 1e5**2],
|
1264 |
+
[0**2, 32**2],
|
1265 |
+
[32**2, 96**2],
|
1266 |
+
[96**2, 1e5**2],
|
1267 |
+
]
|
1268 |
+
self.areaRngLbl = ["all", "small", "medium", "large"]
|
1269 |
+
self.useCats = 1
|
1270 |
+
|
1271 |
+
def setKpParams(self):
|
1272 |
+
self.imgIds = []
|
1273 |
+
self.catIds = []
|
1274 |
+
# np.arange causes trouble. the data point on arange is slightly larger than the true value
|
1275 |
+
self.iouThrs = np.linspace(0.5, 0.95, np.round((0.95 - 0.5) / 0.05) + 1, endpoint=True)
|
1276 |
+
self.recThrs = np.linspace(0.0, 1.00, np.round((1.00 - 0.0) / 0.01) + 1, endpoint=True)
|
1277 |
+
self.maxDets = [20]
|
1278 |
+
self.areaRng = [[0**2, 1e5**2], [32**2, 96**2], [96**2, 1e5**2]]
|
1279 |
+
self.areaRngLbl = ["all", "medium", "large"]
|
1280 |
+
self.useCats = 1
|
1281 |
+
|
1282 |
+
def setUvParams(self):
|
1283 |
+
self.imgIds = []
|
1284 |
+
self.catIds = []
|
1285 |
+
self.iouThrs = np.linspace(0.5, 0.95, int(np.round((0.95 - 0.5) / 0.05)) + 1, endpoint=True)
|
1286 |
+
self.recThrs = np.linspace(0.0, 1.00, int(np.round((1.00 - 0.0) / 0.01)) + 1, endpoint=True)
|
1287 |
+
self.maxDets = [20]
|
1288 |
+
self.areaRng = [[0**2, 1e5**2], [32**2, 96**2], [96**2, 1e5**2]]
|
1289 |
+
self.areaRngLbl = ["all", "medium", "large"]
|
1290 |
+
self.useCats = 1
|
1291 |
+
|
1292 |
+
def __init__(self, iouType="segm"):
|
1293 |
+
if iouType == "segm" or iouType == "bbox":
|
1294 |
+
self.setDetParams()
|
1295 |
+
elif iouType == "keypoints":
|
1296 |
+
self.setKpParams()
|
1297 |
+
elif iouType == "densepose":
|
1298 |
+
self.setUvParams()
|
1299 |
+
else:
|
1300 |
+
raise Exception("iouType not supported")
|
1301 |
+
self.iouType = iouType
|
1302 |
+
# useSegm is deprecated
|
1303 |
+
self.useSegm = None
|
densepose/evaluation/evaluator.py
ADDED
@@ -0,0 +1,421 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
3 |
+
|
4 |
+
import contextlib
|
5 |
+
import copy
|
6 |
+
import io
|
7 |
+
import itertools
|
8 |
+
import logging
|
9 |
+
import numpy as np
|
10 |
+
import os
|
11 |
+
from collections import OrderedDict
|
12 |
+
from typing import Dict, Iterable, List, Optional
|
13 |
+
import pycocotools.mask as mask_utils
|
14 |
+
import torch
|
15 |
+
from pycocotools.coco import COCO
|
16 |
+
from tabulate import tabulate
|
17 |
+
|
18 |
+
from detectron2.config import CfgNode
|
19 |
+
from detectron2.data import MetadataCatalog
|
20 |
+
from detectron2.evaluation import DatasetEvaluator
|
21 |
+
from detectron2.structures import BoxMode
|
22 |
+
from detectron2.utils.comm import gather, get_rank, is_main_process, synchronize
|
23 |
+
from detectron2.utils.file_io import PathManager
|
24 |
+
from detectron2.utils.logger import create_small_table
|
25 |
+
|
26 |
+
from densepose.converters import ToChartResultConverter, ToMaskConverter
|
27 |
+
from densepose.data.datasets.coco import maybe_filter_and_map_categories_cocoapi
|
28 |
+
from densepose.structures import (
|
29 |
+
DensePoseChartPredictorOutput,
|
30 |
+
DensePoseEmbeddingPredictorOutput,
|
31 |
+
quantize_densepose_chart_result,
|
32 |
+
)
|
33 |
+
|
34 |
+
from .densepose_coco_evaluation import DensePoseCocoEval, DensePoseEvalMode
|
35 |
+
from .mesh_alignment_evaluator import MeshAlignmentEvaluator
|
36 |
+
from .tensor_storage import (
|
37 |
+
SingleProcessFileTensorStorage,
|
38 |
+
SingleProcessRamTensorStorage,
|
39 |
+
SingleProcessTensorStorage,
|
40 |
+
SizeData,
|
41 |
+
storage_gather,
|
42 |
+
)
|
43 |
+
|
44 |
+
|
45 |
+
class DensePoseCOCOEvaluator(DatasetEvaluator):
|
46 |
+
def __init__(
|
47 |
+
self,
|
48 |
+
dataset_name,
|
49 |
+
distributed,
|
50 |
+
output_dir=None,
|
51 |
+
evaluator_type: str = "iuv",
|
52 |
+
min_iou_threshold: float = 0.5,
|
53 |
+
storage: Optional[SingleProcessTensorStorage] = None,
|
54 |
+
embedder=None,
|
55 |
+
should_evaluate_mesh_alignment: bool = False,
|
56 |
+
mesh_alignment_mesh_names: Optional[List[str]] = None,
|
57 |
+
):
|
58 |
+
self._embedder = embedder
|
59 |
+
self._distributed = distributed
|
60 |
+
self._output_dir = output_dir
|
61 |
+
self._evaluator_type = evaluator_type
|
62 |
+
self._storage = storage
|
63 |
+
self._should_evaluate_mesh_alignment = should_evaluate_mesh_alignment
|
64 |
+
|
65 |
+
assert not (
|
66 |
+
should_evaluate_mesh_alignment and embedder is None
|
67 |
+
), "Mesh alignment evaluation is activated, but no vertex embedder provided!"
|
68 |
+
if should_evaluate_mesh_alignment:
|
69 |
+
self._mesh_alignment_evaluator = MeshAlignmentEvaluator(
|
70 |
+
embedder,
|
71 |
+
mesh_alignment_mesh_names,
|
72 |
+
)
|
73 |
+
|
74 |
+
self._cpu_device = torch.device("cpu")
|
75 |
+
self._logger = logging.getLogger(__name__)
|
76 |
+
|
77 |
+
self._metadata = MetadataCatalog.get(dataset_name)
|
78 |
+
self._min_threshold = min_iou_threshold
|
79 |
+
json_file = PathManager.get_local_path(self._metadata.json_file)
|
80 |
+
with contextlib.redirect_stdout(io.StringIO()):
|
81 |
+
self._coco_api = COCO(json_file)
|
82 |
+
maybe_filter_and_map_categories_cocoapi(dataset_name, self._coco_api)
|
83 |
+
|
84 |
+
def reset(self):
|
85 |
+
self._predictions = []
|
86 |
+
|
87 |
+
def process(self, inputs, outputs):
|
88 |
+
"""
|
89 |
+
Args:
|
90 |
+
inputs: the inputs to a COCO model (e.g., GeneralizedRCNN).
|
91 |
+
It is a list of dict. Each dict corresponds to an image and
|
92 |
+
contains keys like "height", "width", "file_name", "image_id".
|
93 |
+
outputs: the outputs of a COCO model. It is a list of dicts with key
|
94 |
+
"instances" that contains :class:`Instances`.
|
95 |
+
The :class:`Instances` object needs to have `densepose` field.
|
96 |
+
"""
|
97 |
+
for input, output in zip(inputs, outputs):
|
98 |
+
instances = output["instances"].to(self._cpu_device)
|
99 |
+
if not instances.has("pred_densepose"):
|
100 |
+
continue
|
101 |
+
prediction_list = prediction_to_dict(
|
102 |
+
instances,
|
103 |
+
input["image_id"],
|
104 |
+
self._embedder,
|
105 |
+
self._metadata.class_to_mesh_name,
|
106 |
+
self._storage is not None,
|
107 |
+
)
|
108 |
+
if self._storage is not None:
|
109 |
+
for prediction_dict in prediction_list:
|
110 |
+
dict_to_store = {}
|
111 |
+
for field_name in self._storage.data_schema:
|
112 |
+
dict_to_store[field_name] = prediction_dict[field_name]
|
113 |
+
record_id = self._storage.put(dict_to_store)
|
114 |
+
prediction_dict["record_id"] = record_id
|
115 |
+
prediction_dict["rank"] = get_rank()
|
116 |
+
for field_name in self._storage.data_schema:
|
117 |
+
del prediction_dict[field_name]
|
118 |
+
self._predictions.extend(prediction_list)
|
119 |
+
|
120 |
+
def evaluate(self, img_ids=None):
|
121 |
+
if self._distributed:
|
122 |
+
synchronize()
|
123 |
+
predictions = gather(self._predictions)
|
124 |
+
predictions = list(itertools.chain(*predictions))
|
125 |
+
else:
|
126 |
+
predictions = self._predictions
|
127 |
+
|
128 |
+
multi_storage = storage_gather(self._storage) if self._storage is not None else None
|
129 |
+
|
130 |
+
if not is_main_process():
|
131 |
+
return
|
132 |
+
return copy.deepcopy(self._eval_predictions(predictions, multi_storage, img_ids))
|
133 |
+
|
134 |
+
def _eval_predictions(self, predictions, multi_storage=None, img_ids=None):
|
135 |
+
"""
|
136 |
+
Evaluate predictions on densepose.
|
137 |
+
Return results with the metrics of the tasks.
|
138 |
+
"""
|
139 |
+
self._logger.info("Preparing results for COCO format ...")
|
140 |
+
|
141 |
+
if self._output_dir:
|
142 |
+
PathManager.mkdirs(self._output_dir)
|
143 |
+
file_path = os.path.join(self._output_dir, "coco_densepose_predictions.pth")
|
144 |
+
with PathManager.open(file_path, "wb") as f:
|
145 |
+
torch.save(predictions, f)
|
146 |
+
|
147 |
+
self._logger.info("Evaluating predictions ...")
|
148 |
+
res = OrderedDict()
|
149 |
+
results_gps, results_gpsm, results_segm = _evaluate_predictions_on_coco(
|
150 |
+
self._coco_api,
|
151 |
+
predictions,
|
152 |
+
multi_storage,
|
153 |
+
self._embedder,
|
154 |
+
class_names=self._metadata.get("thing_classes"),
|
155 |
+
min_threshold=self._min_threshold,
|
156 |
+
img_ids=img_ids,
|
157 |
+
)
|
158 |
+
res["densepose_gps"] = results_gps
|
159 |
+
res["densepose_gpsm"] = results_gpsm
|
160 |
+
res["densepose_segm"] = results_segm
|
161 |
+
if self._should_evaluate_mesh_alignment:
|
162 |
+
res["densepose_mesh_alignment"] = self._evaluate_mesh_alignment()
|
163 |
+
return res
|
164 |
+
|
165 |
+
def _evaluate_mesh_alignment(self):
|
166 |
+
self._logger.info("Mesh alignment evaluation ...")
|
167 |
+
mean_ge, mean_gps, per_mesh_metrics = self._mesh_alignment_evaluator.evaluate()
|
168 |
+
results = {
|
169 |
+
"GE": mean_ge * 100,
|
170 |
+
"GPS": mean_gps * 100,
|
171 |
+
}
|
172 |
+
mesh_names = set()
|
173 |
+
for metric_name in per_mesh_metrics:
|
174 |
+
for mesh_name, value in per_mesh_metrics[metric_name].items():
|
175 |
+
results[f"{metric_name}-{mesh_name}"] = value * 100
|
176 |
+
mesh_names.add(mesh_name)
|
177 |
+
self._print_mesh_alignment_results(results, mesh_names)
|
178 |
+
return results
|
179 |
+
|
180 |
+
def _print_mesh_alignment_results(self, results: Dict[str, float], mesh_names: Iterable[str]):
|
181 |
+
self._logger.info("Evaluation results for densepose, mesh alignment:")
|
182 |
+
self._logger.info(f'| {"Mesh":13s} | {"GErr":7s} | {"GPS":7s} |')
|
183 |
+
self._logger.info("| :-----------: | :-----: | :-----: |")
|
184 |
+
for mesh_name in mesh_names:
|
185 |
+
ge_key = f"GE-{mesh_name}"
|
186 |
+
ge_str = f"{results[ge_key]:.4f}" if ge_key in results else " "
|
187 |
+
gps_key = f"GPS-{mesh_name}"
|
188 |
+
gps_str = f"{results[gps_key]:.4f}" if gps_key in results else " "
|
189 |
+
self._logger.info(f"| {mesh_name:13s} | {ge_str:7s} | {gps_str:7s} |")
|
190 |
+
self._logger.info("| :-------------------------------: |")
|
191 |
+
ge_key = "GE"
|
192 |
+
ge_str = f"{results[ge_key]:.4f}" if ge_key in results else " "
|
193 |
+
gps_key = "GPS"
|
194 |
+
gps_str = f"{results[gps_key]:.4f}" if gps_key in results else " "
|
195 |
+
self._logger.info(f'| {"MEAN":13s} | {ge_str:7s} | {gps_str:7s} |')
|
196 |
+
|
197 |
+
|
198 |
+
def prediction_to_dict(instances, img_id, embedder, class_to_mesh_name, use_storage):
|
199 |
+
"""
|
200 |
+
Args:
|
201 |
+
instances (Instances): the output of the model
|
202 |
+
img_id (str): the image id in COCO
|
203 |
+
|
204 |
+
Returns:
|
205 |
+
list[dict]: the results in densepose evaluation format
|
206 |
+
"""
|
207 |
+
scores = instances.scores.tolist()
|
208 |
+
classes = instances.pred_classes.tolist()
|
209 |
+
raw_boxes_xywh = BoxMode.convert(
|
210 |
+
instances.pred_boxes.tensor.clone(), BoxMode.XYXY_ABS, BoxMode.XYWH_ABS
|
211 |
+
)
|
212 |
+
|
213 |
+
if isinstance(instances.pred_densepose, DensePoseEmbeddingPredictorOutput):
|
214 |
+
results_densepose = densepose_cse_predictions_to_dict(
|
215 |
+
instances, embedder, class_to_mesh_name, use_storage
|
216 |
+
)
|
217 |
+
elif isinstance(instances.pred_densepose, DensePoseChartPredictorOutput):
|
218 |
+
if not use_storage:
|
219 |
+
results_densepose = densepose_chart_predictions_to_dict(instances)
|
220 |
+
else:
|
221 |
+
results_densepose = densepose_chart_predictions_to_storage_dict(instances)
|
222 |
+
|
223 |
+
results = []
|
224 |
+
for k in range(len(instances)):
|
225 |
+
result = {
|
226 |
+
"image_id": img_id,
|
227 |
+
"category_id": classes[k],
|
228 |
+
"bbox": raw_boxes_xywh[k].tolist(),
|
229 |
+
"score": scores[k],
|
230 |
+
}
|
231 |
+
results.append({**result, **results_densepose[k]})
|
232 |
+
return results
|
233 |
+
|
234 |
+
|
235 |
+
def densepose_chart_predictions_to_dict(instances):
|
236 |
+
segmentations = ToMaskConverter.convert(
|
237 |
+
instances.pred_densepose, instances.pred_boxes, instances.image_size
|
238 |
+
)
|
239 |
+
|
240 |
+
results = []
|
241 |
+
for k in range(len(instances)):
|
242 |
+
densepose_results_quantized = quantize_densepose_chart_result(
|
243 |
+
ToChartResultConverter.convert(instances.pred_densepose[k], instances.pred_boxes[k])
|
244 |
+
)
|
245 |
+
densepose_results_quantized.labels_uv_uint8 = (
|
246 |
+
densepose_results_quantized.labels_uv_uint8.cpu()
|
247 |
+
)
|
248 |
+
segmentation = segmentations.tensor[k]
|
249 |
+
segmentation_encoded = mask_utils.encode(
|
250 |
+
np.require(segmentation.numpy(), dtype=np.uint8, requirements=["F"])
|
251 |
+
)
|
252 |
+
segmentation_encoded["counts"] = segmentation_encoded["counts"].decode("utf-8")
|
253 |
+
result = {
|
254 |
+
"densepose": densepose_results_quantized,
|
255 |
+
"segmentation": segmentation_encoded,
|
256 |
+
}
|
257 |
+
results.append(result)
|
258 |
+
return results
|
259 |
+
|
260 |
+
|
261 |
+
def densepose_chart_predictions_to_storage_dict(instances):
|
262 |
+
results = []
|
263 |
+
for k in range(len(instances)):
|
264 |
+
densepose_predictor_output = instances.pred_densepose[k]
|
265 |
+
result = {
|
266 |
+
"coarse_segm": densepose_predictor_output.coarse_segm.squeeze(0).cpu(),
|
267 |
+
"fine_segm": densepose_predictor_output.fine_segm.squeeze(0).cpu(),
|
268 |
+
"u": densepose_predictor_output.u.squeeze(0).cpu(),
|
269 |
+
"v": densepose_predictor_output.v.squeeze(0).cpu(),
|
270 |
+
}
|
271 |
+
results.append(result)
|
272 |
+
return results
|
273 |
+
|
274 |
+
|
275 |
+
def densepose_cse_predictions_to_dict(instances, embedder, class_to_mesh_name, use_storage):
|
276 |
+
results = []
|
277 |
+
for k in range(len(instances)):
|
278 |
+
cse = instances.pred_densepose[k]
|
279 |
+
results.append(
|
280 |
+
{
|
281 |
+
"coarse_segm": cse.coarse_segm[0].cpu(),
|
282 |
+
"embedding": cse.embedding[0].cpu(),
|
283 |
+
}
|
284 |
+
)
|
285 |
+
return results
|
286 |
+
|
287 |
+
|
288 |
+
def _evaluate_predictions_on_coco(
|
289 |
+
coco_gt,
|
290 |
+
coco_results,
|
291 |
+
multi_storage=None,
|
292 |
+
embedder=None,
|
293 |
+
class_names=None,
|
294 |
+
min_threshold: float = 0.5,
|
295 |
+
img_ids=None,
|
296 |
+
):
|
297 |
+
logger = logging.getLogger(__name__)
|
298 |
+
|
299 |
+
densepose_metrics = _get_densepose_metrics(min_threshold)
|
300 |
+
if len(coco_results) == 0: # cocoapi does not handle empty results very well
|
301 |
+
logger.warn("No predictions from the model! Set scores to -1")
|
302 |
+
results_gps = {metric: -1 for metric in densepose_metrics}
|
303 |
+
results_gpsm = {metric: -1 for metric in densepose_metrics}
|
304 |
+
results_segm = {metric: -1 for metric in densepose_metrics}
|
305 |
+
return results_gps, results_gpsm, results_segm
|
306 |
+
|
307 |
+
coco_dt = coco_gt.loadRes(coco_results)
|
308 |
+
|
309 |
+
results = []
|
310 |
+
for eval_mode_name in ["GPS", "GPSM", "IOU"]:
|
311 |
+
eval_mode = getattr(DensePoseEvalMode, eval_mode_name)
|
312 |
+
coco_eval = DensePoseCocoEval(
|
313 |
+
coco_gt, coco_dt, "densepose", multi_storage, embedder, dpEvalMode=eval_mode
|
314 |
+
)
|
315 |
+
result = _derive_results_from_coco_eval(
|
316 |
+
coco_eval, eval_mode_name, densepose_metrics, class_names, min_threshold, img_ids
|
317 |
+
)
|
318 |
+
results.append(result)
|
319 |
+
return results
|
320 |
+
|
321 |
+
|
322 |
+
def _get_densepose_metrics(min_threshold: float = 0.5):
|
323 |
+
metrics = ["AP"]
|
324 |
+
if min_threshold <= 0.201:
|
325 |
+
metrics += ["AP20"]
|
326 |
+
if min_threshold <= 0.301:
|
327 |
+
metrics += ["AP30"]
|
328 |
+
if min_threshold <= 0.401:
|
329 |
+
metrics += ["AP40"]
|
330 |
+
metrics.extend(["AP50", "AP75", "APm", "APl", "AR", "AR50", "AR75", "ARm", "ARl"])
|
331 |
+
return metrics
|
332 |
+
|
333 |
+
|
334 |
+
def _derive_results_from_coco_eval(
|
335 |
+
coco_eval, eval_mode_name, metrics, class_names, min_threshold: float, img_ids
|
336 |
+
):
|
337 |
+
if img_ids is not None:
|
338 |
+
coco_eval.params.imgIds = img_ids
|
339 |
+
coco_eval.params.iouThrs = np.linspace(
|
340 |
+
min_threshold, 0.95, int(np.round((0.95 - min_threshold) / 0.05)) + 1, endpoint=True
|
341 |
+
)
|
342 |
+
coco_eval.evaluate()
|
343 |
+
coco_eval.accumulate()
|
344 |
+
coco_eval.summarize()
|
345 |
+
results = {metric: float(coco_eval.stats[idx] * 100) for idx, metric in enumerate(metrics)}
|
346 |
+
logger = logging.getLogger(__name__)
|
347 |
+
logger.info(
|
348 |
+
f"Evaluation results for densepose, {eval_mode_name} metric: \n"
|
349 |
+
+ create_small_table(results)
|
350 |
+
)
|
351 |
+
if class_names is None or len(class_names) <= 1:
|
352 |
+
return results
|
353 |
+
|
354 |
+
# Compute per-category AP, the same way as it is done in D2
|
355 |
+
# (see detectron2/evaluation/coco_evaluation.py):
|
356 |
+
precisions = coco_eval.eval["precision"]
|
357 |
+
# precision has dims (iou, recall, cls, area range, max dets)
|
358 |
+
assert len(class_names) == precisions.shape[2]
|
359 |
+
|
360 |
+
results_per_category = []
|
361 |
+
for idx, name in enumerate(class_names):
|
362 |
+
# area range index 0: all area ranges
|
363 |
+
# max dets index -1: typically 100 per image
|
364 |
+
precision = precisions[:, :, idx, 0, -1]
|
365 |
+
precision = precision[precision > -1]
|
366 |
+
ap = np.mean(precision) if precision.size else float("nan")
|
367 |
+
results_per_category.append((f"{name}", float(ap * 100)))
|
368 |
+
|
369 |
+
# tabulate it
|
370 |
+
n_cols = min(6, len(results_per_category) * 2)
|
371 |
+
results_flatten = list(itertools.chain(*results_per_category))
|
372 |
+
results_2d = itertools.zip_longest(*[results_flatten[i::n_cols] for i in range(n_cols)])
|
373 |
+
table = tabulate(
|
374 |
+
results_2d,
|
375 |
+
tablefmt="pipe",
|
376 |
+
floatfmt=".3f",
|
377 |
+
headers=["category", "AP"] * (n_cols // 2),
|
378 |
+
numalign="left",
|
379 |
+
)
|
380 |
+
logger.info(f"Per-category {eval_mode_name} AP: \n" + table)
|
381 |
+
|
382 |
+
results.update({"AP-" + name: ap for name, ap in results_per_category})
|
383 |
+
return results
|
384 |
+
|
385 |
+
|
386 |
+
def build_densepose_evaluator_storage(cfg: CfgNode, output_folder: str):
|
387 |
+
storage_spec = cfg.DENSEPOSE_EVALUATION.STORAGE
|
388 |
+
if storage_spec == "none":
|
389 |
+
return None
|
390 |
+
evaluator_type = cfg.DENSEPOSE_EVALUATION.TYPE
|
391 |
+
# common output tensor sizes
|
392 |
+
hout = cfg.MODEL.ROI_DENSEPOSE_HEAD.HEATMAP_SIZE
|
393 |
+
wout = cfg.MODEL.ROI_DENSEPOSE_HEAD.HEATMAP_SIZE
|
394 |
+
n_csc = cfg.MODEL.ROI_DENSEPOSE_HEAD.NUM_COARSE_SEGM_CHANNELS
|
395 |
+
# specific output tensors
|
396 |
+
if evaluator_type == "iuv":
|
397 |
+
n_fsc = cfg.MODEL.ROI_DENSEPOSE_HEAD.NUM_PATCHES + 1
|
398 |
+
schema = {
|
399 |
+
"coarse_segm": SizeData(dtype="float32", shape=(n_csc, hout, wout)),
|
400 |
+
"fine_segm": SizeData(dtype="float32", shape=(n_fsc, hout, wout)),
|
401 |
+
"u": SizeData(dtype="float32", shape=(n_fsc, hout, wout)),
|
402 |
+
"v": SizeData(dtype="float32", shape=(n_fsc, hout, wout)),
|
403 |
+
}
|
404 |
+
elif evaluator_type == "cse":
|
405 |
+
embed_size = cfg.MODEL.ROI_DENSEPOSE_HEAD.CSE.EMBED_SIZE
|
406 |
+
schema = {
|
407 |
+
"coarse_segm": SizeData(dtype="float32", shape=(n_csc, hout, wout)),
|
408 |
+
"embedding": SizeData(dtype="float32", shape=(embed_size, hout, wout)),
|
409 |
+
}
|
410 |
+
else:
|
411 |
+
raise ValueError(f"Unknown evaluator type: {evaluator_type}")
|
412 |
+
# storage types
|
413 |
+
if storage_spec == "ram":
|
414 |
+
storage = SingleProcessRamTensorStorage(schema, io.BytesIO())
|
415 |
+
elif storage_spec == "file":
|
416 |
+
fpath = os.path.join(output_folder, f"DensePoseEvaluatorStorage.{get_rank()}.bin")
|
417 |
+
PathManager.mkdirs(output_folder)
|
418 |
+
storage = SingleProcessFileTensorStorage(schema, fpath, "wb")
|
419 |
+
else:
|
420 |
+
raise ValueError(f"Unknown storage specification: {storage_spec}")
|
421 |
+
return storage
|
densepose/evaluation/mesh_alignment_evaluator.py
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
|
2 |
+
|
3 |
+
import json
|
4 |
+
import logging
|
5 |
+
from typing import List, Optional
|
6 |
+
import torch
|
7 |
+
from torch import nn
|
8 |
+
|
9 |
+
from detectron2.utils.file_io import PathManager
|
10 |
+
|
11 |
+
from densepose.structures.mesh import create_mesh
|
12 |
+
|
13 |
+
|
14 |
+
class MeshAlignmentEvaluator:
|
15 |
+
"""
|
16 |
+
Class for evaluation of 3D mesh alignment based on the learned vertex embeddings
|
17 |
+
"""
|
18 |
+
|
19 |
+
def __init__(self, embedder: nn.Module, mesh_names: Optional[List[str]]):
|
20 |
+
self.embedder = embedder
|
21 |
+
# use the provided mesh names if not None and not an empty list
|
22 |
+
self.mesh_names = mesh_names if mesh_names else embedder.mesh_names
|
23 |
+
self.logger = logging.getLogger(__name__)
|
24 |
+
with PathManager.open(
|
25 |
+
"https://dl.fbaipublicfiles.com/densepose/data/cse/mesh_keyvertices_v0.json", "r"
|
26 |
+
) as f:
|
27 |
+
self.mesh_keyvertices = json.load(f)
|
28 |
+
|
29 |
+
def evaluate(self):
|
30 |
+
ge_per_mesh = {}
|
31 |
+
gps_per_mesh = {}
|
32 |
+
for mesh_name_1 in self.mesh_names:
|
33 |
+
avg_errors = []
|
34 |
+
avg_gps = []
|
35 |
+
embeddings_1 = self.embedder(mesh_name_1)
|
36 |
+
keyvertices_1 = self.mesh_keyvertices[mesh_name_1]
|
37 |
+
keyvertex_names_1 = list(keyvertices_1.keys())
|
38 |
+
keyvertex_indices_1 = [keyvertices_1[name] for name in keyvertex_names_1]
|
39 |
+
for mesh_name_2 in self.mesh_names:
|
40 |
+
if mesh_name_1 == mesh_name_2:
|
41 |
+
continue
|
42 |
+
embeddings_2 = self.embedder(mesh_name_2)
|
43 |
+
keyvertices_2 = self.mesh_keyvertices[mesh_name_2]
|
44 |
+
sim_matrix_12 = embeddings_1[keyvertex_indices_1].mm(embeddings_2.T)
|
45 |
+
vertices_2_matching_keyvertices_1 = sim_matrix_12.argmax(axis=1)
|
46 |
+
mesh_2 = create_mesh(mesh_name_2, embeddings_2.device)
|
47 |
+
geodists = mesh_2.geodists[
|
48 |
+
vertices_2_matching_keyvertices_1,
|
49 |
+
[keyvertices_2[name] for name in keyvertex_names_1],
|
50 |
+
]
|
51 |
+
Current_Mean_Distances = 0.255
|
52 |
+
gps = (-(geodists**2) / (2 * (Current_Mean_Distances**2))).exp()
|
53 |
+
avg_errors.append(geodists.mean().item())
|
54 |
+
avg_gps.append(gps.mean().item())
|
55 |
+
|
56 |
+
ge_mean = torch.as_tensor(avg_errors).mean().item()
|
57 |
+
gps_mean = torch.as_tensor(avg_gps).mean().item()
|
58 |
+
ge_per_mesh[mesh_name_1] = ge_mean
|
59 |
+
gps_per_mesh[mesh_name_1] = gps_mean
|
60 |
+
ge_mean_global = torch.as_tensor(list(ge_per_mesh.values())).mean().item()
|
61 |
+
gps_mean_global = torch.as_tensor(list(gps_per_mesh.values())).mean().item()
|
62 |
+
per_mesh_metrics = {
|
63 |
+
"GE": ge_per_mesh,
|
64 |
+
"GPS": gps_per_mesh,
|
65 |
+
}
|
66 |
+
return ge_mean_global, gps_mean_global, per_mesh_metrics
|
densepose/evaluation/tensor_storage.py
ADDED
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
import io
|
4 |
+
import numpy as np
|
5 |
+
import os
|
6 |
+
from dataclasses import dataclass
|
7 |
+
from functools import reduce
|
8 |
+
from operator import mul
|
9 |
+
from typing import BinaryIO, Dict, Optional, Tuple
|
10 |
+
import torch
|
11 |
+
|
12 |
+
from detectron2.utils.comm import gather, get_rank
|
13 |
+
from detectron2.utils.file_io import PathManager
|
14 |
+
|
15 |
+
|
16 |
+
@dataclass
|
17 |
+
class SizeData:
|
18 |
+
dtype: str
|
19 |
+
shape: Tuple[int]
|
20 |
+
|
21 |
+
|
22 |
+
def _calculate_record_field_size_b(data_schema: Dict[str, SizeData], field_name: str) -> int:
|
23 |
+
schema = data_schema[field_name]
|
24 |
+
element_size_b = np.dtype(schema.dtype).itemsize
|
25 |
+
record_field_size_b = reduce(mul, schema.shape) * element_size_b
|
26 |
+
return record_field_size_b
|
27 |
+
|
28 |
+
|
29 |
+
def _calculate_record_size_b(data_schema: Dict[str, SizeData]) -> int:
|
30 |
+
record_size_b = 0
|
31 |
+
for field_name in data_schema:
|
32 |
+
record_field_size_b = _calculate_record_field_size_b(data_schema, field_name)
|
33 |
+
record_size_b += record_field_size_b
|
34 |
+
return record_size_b
|
35 |
+
|
36 |
+
|
37 |
+
def _calculate_record_field_sizes_b(data_schema: Dict[str, SizeData]) -> Dict[str, int]:
|
38 |
+
field_sizes_b = {}
|
39 |
+
for field_name in data_schema:
|
40 |
+
field_sizes_b[field_name] = _calculate_record_field_size_b(data_schema, field_name)
|
41 |
+
return field_sizes_b
|
42 |
+
|
43 |
+
|
44 |
+
class SingleProcessTensorStorage:
|
45 |
+
"""
|
46 |
+
Compact tensor storage to keep tensor data of predefined size and type.
|
47 |
+
"""
|
48 |
+
|
49 |
+
def __init__(self, data_schema: Dict[str, SizeData], storage_impl: BinaryIO):
|
50 |
+
"""
|
51 |
+
Construct tensor storage based on information on data shape and size.
|
52 |
+
Internally uses numpy to interpret the type specification.
|
53 |
+
The storage must support operations `seek(offset, whence=os.SEEK_SET)` and
|
54 |
+
`read(size)` to be able to perform the `get` operation.
|
55 |
+
The storage must support operation `write(bytes)` to be able to perform
|
56 |
+
the `put` operation.
|
57 |
+
|
58 |
+
Args:
|
59 |
+
data_schema (dict: str -> SizeData): dictionary which maps tensor name
|
60 |
+
to its size data (shape and data type), e.g.
|
61 |
+
```
|
62 |
+
{
|
63 |
+
"coarse_segm": SizeData(dtype="float32", shape=(112, 112)),
|
64 |
+
"embedding": SizeData(dtype="float32", shape=(16, 112, 112)),
|
65 |
+
}
|
66 |
+
```
|
67 |
+
storage_impl (BinaryIO): io instance that handles file-like seek, read
|
68 |
+
and write operations, e.g. a file handle or a memory buffer like io.BytesIO
|
69 |
+
"""
|
70 |
+
self.data_schema = data_schema
|
71 |
+
self.record_size_b = _calculate_record_size_b(data_schema)
|
72 |
+
self.record_field_sizes_b = _calculate_record_field_sizes_b(data_schema)
|
73 |
+
self.storage_impl = storage_impl
|
74 |
+
self.next_record_id = 0
|
75 |
+
|
76 |
+
def get(self, record_id: int) -> Dict[str, torch.Tensor]:
|
77 |
+
"""
|
78 |
+
Load tensors from the storage by record ID
|
79 |
+
|
80 |
+
Args:
|
81 |
+
record_id (int): Record ID, for which to load the data
|
82 |
+
|
83 |
+
Return:
|
84 |
+
dict: str -> tensor: tensor name mapped to tensor data, recorded under the provided ID
|
85 |
+
"""
|
86 |
+
self.storage_impl.seek(record_id * self.record_size_b, os.SEEK_SET)
|
87 |
+
data_bytes = self.storage_impl.read(self.record_size_b)
|
88 |
+
assert len(data_bytes) == self.record_size_b, (
|
89 |
+
f"Expected data size {self.record_size_b} B could not be read: "
|
90 |
+
f"got {len(data_bytes)} B"
|
91 |
+
)
|
92 |
+
record = {}
|
93 |
+
cur_idx = 0
|
94 |
+
# it's important to read and write in the same order
|
95 |
+
for field_name in sorted(self.data_schema):
|
96 |
+
schema = self.data_schema[field_name]
|
97 |
+
field_size_b = self.record_field_sizes_b[field_name]
|
98 |
+
chunk = data_bytes[cur_idx : cur_idx + field_size_b]
|
99 |
+
data_np = np.frombuffer(
|
100 |
+
chunk, dtype=schema.dtype, count=reduce(mul, schema.shape)
|
101 |
+
).reshape(schema.shape)
|
102 |
+
record[field_name] = torch.from_numpy(data_np)
|
103 |
+
cur_idx += field_size_b
|
104 |
+
return record
|
105 |
+
|
106 |
+
def put(self, data: Dict[str, torch.Tensor]) -> int:
|
107 |
+
"""
|
108 |
+
Store tensors in the storage
|
109 |
+
|
110 |
+
Args:
|
111 |
+
data (dict: str -> tensor): data to store, a dictionary which maps
|
112 |
+
tensor names into tensors; tensor shapes must match those specified
|
113 |
+
in data schema.
|
114 |
+
Return:
|
115 |
+
int: record ID, under which the data is stored
|
116 |
+
"""
|
117 |
+
# it's important to read and write in the same order
|
118 |
+
for field_name in sorted(self.data_schema):
|
119 |
+
assert (
|
120 |
+
field_name in data
|
121 |
+
), f"Field '{field_name}' not present in data: data keys are {data.keys()}"
|
122 |
+
value = data[field_name]
|
123 |
+
assert value.shape == self.data_schema[field_name].shape, (
|
124 |
+
f"Mismatched tensor shapes for field '{field_name}': "
|
125 |
+
f"expected {self.data_schema[field_name].shape}, got {value.shape}"
|
126 |
+
)
|
127 |
+
data_bytes = value.cpu().numpy().tobytes()
|
128 |
+
assert len(data_bytes) == self.record_field_sizes_b[field_name], (
|
129 |
+
f"Expected field {field_name} to be of size "
|
130 |
+
f"{self.record_field_sizes_b[field_name]} B, got {len(data_bytes)} B"
|
131 |
+
)
|
132 |
+
self.storage_impl.write(data_bytes)
|
133 |
+
record_id = self.next_record_id
|
134 |
+
self.next_record_id += 1
|
135 |
+
return record_id
|
136 |
+
|
137 |
+
|
138 |
+
class SingleProcessFileTensorStorage(SingleProcessTensorStorage):
|
139 |
+
"""
|
140 |
+
Implementation of a single process tensor storage which stores data in a file
|
141 |
+
"""
|
142 |
+
|
143 |
+
def __init__(self, data_schema: Dict[str, SizeData], fpath: str, mode: str):
|
144 |
+
self.fpath = fpath
|
145 |
+
assert "b" in mode, f"Tensor storage should be opened in binary mode, got '{mode}'"
|
146 |
+
if "w" in mode:
|
147 |
+
# pyre-fixme[6]: For 2nd argument expected `Union[typing_extensions.Liter...
|
148 |
+
file_h = PathManager.open(fpath, mode)
|
149 |
+
elif "r" in mode:
|
150 |
+
local_fpath = PathManager.get_local_path(fpath)
|
151 |
+
file_h = open(local_fpath, mode)
|
152 |
+
else:
|
153 |
+
raise ValueError(f"Unsupported file mode {mode}, supported modes: rb, wb")
|
154 |
+
super().__init__(data_schema, file_h) # pyre-ignore[6]
|
155 |
+
|
156 |
+
|
157 |
+
class SingleProcessRamTensorStorage(SingleProcessTensorStorage):
|
158 |
+
"""
|
159 |
+
Implementation of a single process tensor storage which stores data in RAM
|
160 |
+
"""
|
161 |
+
|
162 |
+
def __init__(self, data_schema: Dict[str, SizeData], buf: io.BytesIO):
|
163 |
+
super().__init__(data_schema, buf)
|
164 |
+
|
165 |
+
|
166 |
+
class MultiProcessTensorStorage:
|
167 |
+
"""
|
168 |
+
Representation of a set of tensor storages created by individual processes,
|
169 |
+
allows to access those storages from a single owner process. The storages
|
170 |
+
should either be shared or broadcasted to the owner process.
|
171 |
+
The processes are identified by their rank, data is uniquely defined by
|
172 |
+
the rank of the process and the record ID.
|
173 |
+
"""
|
174 |
+
|
175 |
+
def __init__(self, rank_to_storage: Dict[int, SingleProcessTensorStorage]):
|
176 |
+
self.rank_to_storage = rank_to_storage
|
177 |
+
|
178 |
+
def get(self, rank: int, record_id: int) -> Dict[str, torch.Tensor]:
|
179 |
+
storage = self.rank_to_storage[rank]
|
180 |
+
return storage.get(record_id)
|
181 |
+
|
182 |
+
def put(self, rank: int, data: Dict[str, torch.Tensor]) -> int:
|
183 |
+
storage = self.rank_to_storage[rank]
|
184 |
+
return storage.put(data)
|
185 |
+
|
186 |
+
|
187 |
+
class MultiProcessFileTensorStorage(MultiProcessTensorStorage):
|
188 |
+
def __init__(self, data_schema: Dict[str, SizeData], rank_to_fpath: Dict[int, str], mode: str):
|
189 |
+
rank_to_storage = {
|
190 |
+
rank: SingleProcessFileTensorStorage(data_schema, fpath, mode)
|
191 |
+
for rank, fpath in rank_to_fpath.items()
|
192 |
+
}
|
193 |
+
super().__init__(rank_to_storage) # pyre-ignore[6]
|
194 |
+
|
195 |
+
|
196 |
+
class MultiProcessRamTensorStorage(MultiProcessTensorStorage):
|
197 |
+
def __init__(self, data_schema: Dict[str, SizeData], rank_to_buffer: Dict[int, io.BytesIO]):
|
198 |
+
rank_to_storage = {
|
199 |
+
rank: SingleProcessRamTensorStorage(data_schema, buf)
|
200 |
+
for rank, buf in rank_to_buffer.items()
|
201 |
+
}
|
202 |
+
super().__init__(rank_to_storage) # pyre-ignore[6]
|
203 |
+
|
204 |
+
|
205 |
+
def _ram_storage_gather(
|
206 |
+
storage: SingleProcessRamTensorStorage, dst_rank: int = 0
|
207 |
+
) -> Optional[MultiProcessRamTensorStorage]:
|
208 |
+
storage.storage_impl.seek(0, os.SEEK_SET)
|
209 |
+
# TODO: overhead, pickling a bytes object, can just pass bytes in a tensor directly
|
210 |
+
# see detectron2/utils.comm.py
|
211 |
+
data_list = gather(storage.storage_impl.read(), dst=dst_rank)
|
212 |
+
if get_rank() != dst_rank:
|
213 |
+
return None
|
214 |
+
rank_to_buffer = {i: io.BytesIO(data_list[i]) for i in range(len(data_list))}
|
215 |
+
multiprocess_storage = MultiProcessRamTensorStorage(storage.data_schema, rank_to_buffer)
|
216 |
+
return multiprocess_storage
|
217 |
+
|
218 |
+
|
219 |
+
def _file_storage_gather(
|
220 |
+
storage: SingleProcessFileTensorStorage,
|
221 |
+
dst_rank: int = 0,
|
222 |
+
mode: str = "rb",
|
223 |
+
) -> Optional[MultiProcessFileTensorStorage]:
|
224 |
+
storage.storage_impl.close()
|
225 |
+
fpath_list = gather(storage.fpath, dst=dst_rank)
|
226 |
+
if get_rank() != dst_rank:
|
227 |
+
return None
|
228 |
+
rank_to_fpath = {i: fpath_list[i] for i in range(len(fpath_list))}
|
229 |
+
return MultiProcessFileTensorStorage(storage.data_schema, rank_to_fpath, mode)
|
230 |
+
|
231 |
+
|
232 |
+
def storage_gather(
|
233 |
+
storage: SingleProcessTensorStorage, dst_rank: int = 0
|
234 |
+
) -> Optional[MultiProcessTensorStorage]:
|
235 |
+
if isinstance(storage, SingleProcessRamTensorStorage):
|
236 |
+
return _ram_storage_gather(storage, dst_rank)
|
237 |
+
elif isinstance(storage, SingleProcessFileTensorStorage):
|
238 |
+
return _file_storage_gather(storage, dst_rank)
|
239 |
+
raise Exception(f"Unsupported storage for gather operation: {storage}")
|
densepose/modeling/__init__.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from .confidence import DensePoseConfidenceModelConfig, DensePoseUVConfidenceType
|
4 |
+
from .filter import DensePoseDataFilter
|
5 |
+
from .inference import densepose_inference
|
6 |
+
from .utils import initialize_module_params
|
7 |
+
from .build import (
|
8 |
+
build_densepose_data_filter,
|
9 |
+
build_densepose_embedder,
|
10 |
+
build_densepose_head,
|
11 |
+
build_densepose_losses,
|
12 |
+
build_densepose_predictor,
|
13 |
+
)
|