Spaces:
Running
on
Zero
Running
on
Zero
ML-Motivators
commited on
Commit
β’
c804dfb
1
Parent(s):
ba6ca56
UPLOADS
Browse filesThis view is limited to 50 files because it contains too many changes. Β
See raw diff
- densepose/__init__.py +20 -20
- densepose/config.py +277 -277
- densepose/converters/__init__.py +15 -15
- densepose/converters/base.py +93 -93
- densepose/converters/builtin.py +31 -31
- densepose/converters/chart_output_hflip.py +71 -71
- densepose/converters/chart_output_to_chart_result.py +188 -188
- densepose/converters/hflip.py +34 -34
- densepose/converters/segm_to_mask.py +150 -150
- densepose/converters/to_chart_result.py +70 -70
- densepose/converters/to_mask.py +49 -49
- densepose/data/__init__.py +25 -25
- densepose/data/build.py +736 -736
- densepose/data/combined_loader.py +44 -44
- densepose/data/dataset_mapper.py +168 -168
- densepose/data/datasets/__init__.py +5 -5
- densepose/data/datasets/builtin.py +16 -16
- densepose/data/datasets/chimpnsee.py +29 -29
- densepose/data/datasets/coco.py +432 -432
- densepose/data/datasets/dataset_type.py +11 -11
- densepose/data/datasets/lvis.py +257 -257
- densepose/data/image_list_dataset.py +72 -72
- densepose/data/inference_based_loader.py +172 -172
- densepose/data/meshes/__init__.py +5 -5
- densepose/data/meshes/builtin.py +101 -101
- densepose/data/meshes/catalog.py +71 -71
- densepose/data/samplers/__init__.py +8 -8
- densepose/data/samplers/densepose_base.py +203 -203
- densepose/data/samplers/densepose_confidence_based.py +108 -108
- densepose/data/samplers/densepose_cse_base.py +139 -139
- densepose/data/samplers/densepose_cse_confidence_based.py +119 -119
- densepose/data/samplers/densepose_cse_uniform.py +12 -12
- densepose/data/samplers/densepose_uniform.py +41 -41
- densepose/data/samplers/mask_from_densepose.py +28 -28
- densepose/data/samplers/prediction_to_gt.py +98 -98
- densepose/data/transform/__init__.py +3 -3
- densepose/data/transform/image.py +39 -39
- densepose/data/utils.py +38 -38
- densepose/data/video/__init__.py +17 -17
- densepose/data/video/frame_selector.py +87 -87
- densepose/data/video/video_keyframe_dataset.py +300 -300
- densepose/engine/__init__.py +3 -3
- densepose/engine/trainer.py +258 -258
- densepose/evaluation/__init__.py +3 -3
- densepose/evaluation/d2_evaluator_adapter.py +50 -50
- densepose/evaluation/densepose_coco_evaluation.py +0 -0
- densepose/evaluation/evaluator.py +421 -421
- densepose/evaluation/mesh_alignment_evaluator.py +66 -66
- densepose/evaluation/tensor_storage.py +239 -239
- densepose/modeling/__init__.py +13 -13
densepose/__init__.py
CHANGED
@@ -1,20 +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
|
|
|
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
CHANGED
@@ -1,277 +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)
|
|
|
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
CHANGED
@@ -1,15 +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
|
|
|
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
CHANGED
@@ -1,93 +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]
|
|
|
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
CHANGED
@@ -1,31 +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)
|
|
|
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
CHANGED
@@ -1,71 +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
|
|
|
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
CHANGED
@@ -1,188 +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)
|
|
|
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
CHANGED
@@ -1,34 +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 |
-
)
|
|
|
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
CHANGED
@@ -1,150 +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)
|
|
|
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
CHANGED
@@ -1,70 +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 |
-
)
|
|
|
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
CHANGED
@@ -1,49 +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 |
-
)
|
|
|
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
CHANGED
@@ -1,25 +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("_")]
|
|
|
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
CHANGED
@@ -1,736 +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)
|
|
|
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
CHANGED
@@ -1,44 +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
|
|
|
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
CHANGED
@@ -1,168 +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
|
|
|
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
CHANGED
@@ -1,5 +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("_")]
|
|
|
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
CHANGED
@@ -1,16 +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]
|
|
|
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
CHANGED
@@ -1,29 +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 |
-
)
|
|
|
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
CHANGED
@@ -1,432 +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)
|
|
|
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
CHANGED
@@ -1,11 +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"
|
|
|
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
CHANGED
@@ -1,257 +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)
|
|
|
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
CHANGED
@@ -1,72 +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)
|
|
|
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
CHANGED
@@ -1,172 +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
|
|
|
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
CHANGED
@@ -1,5 +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("_")]
|
|
|
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
CHANGED
@@ -1,101 +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)
|
|
|
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
CHANGED
@@ -1,71 +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)
|
|
|
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
CHANGED
@@ -1,8 +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
|
|
|
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
CHANGED
@@ -1,203 +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
|
|
|
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
CHANGED
@@ -1,108 +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
|
|
|
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
CHANGED
@@ -1,139 +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
|
|
|
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
CHANGED
@@ -1,119 +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
|
|
|
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
CHANGED
@@ -1,12 +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
|
|
|
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
CHANGED
@@ -1,41 +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)
|
|
|
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
CHANGED
@@ -1,28 +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 |
-
)
|
|
|
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
CHANGED
@@ -1,98 +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)]
|
|
|
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
CHANGED
@@ -1,3 +1,3 @@
|
|
1 |
-
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
-
|
3 |
-
from .image import ImageResizeTransform
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from .image import ImageResizeTransform
|
densepose/data/transform/image.py
CHANGED
@@ -1,39 +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
|
|
|
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
CHANGED
@@ -1,38 +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 |
-
}
|
|
|
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
CHANGED
@@ -1,17 +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 |
-
)
|
|
|
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
CHANGED
@@ -1,87 +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 :]
|
|
|
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
CHANGED
@@ -1,300 +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)
|
|
|
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
CHANGED
@@ -1,3 +1,3 @@
|
|
1 |
-
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
-
|
3 |
-
from .trainer import Trainer
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from .trainer import Trainer
|
densepose/engine/trainer.py
CHANGED
@@ -1,258 +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
|
|
|
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
CHANGED
@@ -1,3 +1,3 @@
|
|
1 |
-
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
-
|
3 |
-
from .evaluator import DensePoseCOCOEvaluator
|
|
|
1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
2 |
+
|
3 |
+
from .evaluator import DensePoseCOCOEvaluator
|
densepose/evaluation/d2_evaluator_adapter.py
CHANGED
@@ -1,50 +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
|
|
|
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
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
densepose/evaluation/evaluator.py
CHANGED
@@ -1,421 +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
|
|
|
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
CHANGED
@@ -1,66 +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
|
|
|
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
CHANGED
@@ -1,239 +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}")
|
|
|
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
CHANGED
@@ -1,13 +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 |
-
)
|
|
|
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 |
+
)
|