keypoint_metrics.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import os
  15. import json
  16. from collections import defaultdict, OrderedDict
  17. import numpy as np
  18. import paddle
  19. from pycocotools.coco import COCO
  20. from pycocotools.cocoeval import COCOeval
  21. from ..modeling.keypoint_utils import oks_nms
  22. from scipy.io import loadmat, savemat
  23. from ppdet.utils.logger import setup_logger
  24. logger = setup_logger(__name__)
  25. __all__ = ['KeyPointTopDownCOCOEval', 'KeyPointTopDownMPIIEval']
  26. class KeyPointTopDownCOCOEval(object):
  27. """refer to
  28. https://github.com/leoxiaobin/deep-high-resolution-net.pytorch
  29. Copyright (c) Microsoft, under the MIT License.
  30. """
  31. def __init__(self,
  32. anno_file,
  33. num_samples,
  34. num_joints,
  35. output_eval,
  36. iou_type='keypoints',
  37. in_vis_thre=0.2,
  38. oks_thre=0.9,
  39. save_prediction_only=False):
  40. super(KeyPointTopDownCOCOEval, self).__init__()
  41. self.coco = COCO(anno_file)
  42. self.num_samples = num_samples
  43. self.num_joints = num_joints
  44. self.iou_type = iou_type
  45. self.in_vis_thre = in_vis_thre
  46. self.oks_thre = oks_thre
  47. self.output_eval = output_eval
  48. self.res_file = os.path.join(output_eval, "keypoints_results.json")
  49. self.save_prediction_only = save_prediction_only
  50. self.reset()
  51. def reset(self):
  52. self.results = {
  53. 'all_preds': np.zeros(
  54. (self.num_samples, self.num_joints, 3), dtype=np.float32),
  55. 'all_boxes': np.zeros((self.num_samples, 6)),
  56. 'image_path': []
  57. }
  58. self.eval_results = {}
  59. self.idx = 0
  60. def update(self, inputs, outputs):
  61. kpts, _ = outputs['keypoint'][0]
  62. num_images = inputs['image'].shape[0]
  63. self.results['all_preds'][self.idx:self.idx + num_images, :, 0:
  64. 3] = kpts[:, :, 0:3]
  65. self.results['all_boxes'][self.idx:self.idx + num_images, 0:2] = inputs[
  66. 'center'].numpy()[:, 0:2] if isinstance(
  67. inputs['center'], paddle.Tensor) else inputs['center'][:, 0:2]
  68. self.results['all_boxes'][self.idx:self.idx + num_images, 2:4] = inputs[
  69. 'scale'].numpy()[:, 0:2] if isinstance(
  70. inputs['scale'], paddle.Tensor) else inputs['scale'][:, 0:2]
  71. self.results['all_boxes'][self.idx:self.idx + num_images, 4] = np.prod(
  72. inputs['scale'].numpy() * 200,
  73. 1) if isinstance(inputs['scale'], paddle.Tensor) else np.prod(
  74. inputs['scale'] * 200, 1)
  75. self.results['all_boxes'][
  76. self.idx:self.idx + num_images,
  77. 5] = np.squeeze(inputs['score'].numpy()) if isinstance(
  78. inputs['score'], paddle.Tensor) else np.squeeze(inputs['score'])
  79. if isinstance(inputs['im_id'], paddle.Tensor):
  80. self.results['image_path'].extend(inputs['im_id'].numpy())
  81. else:
  82. self.results['image_path'].extend(inputs['im_id'])
  83. self.idx += num_images
  84. def _write_coco_keypoint_results(self, keypoints):
  85. data_pack = [{
  86. 'cat_id': 1,
  87. 'cls': 'person',
  88. 'ann_type': 'keypoints',
  89. 'keypoints': keypoints
  90. }]
  91. results = self._coco_keypoint_results_one_category_kernel(data_pack[0])
  92. if not os.path.exists(self.output_eval):
  93. os.makedirs(self.output_eval)
  94. with open(self.res_file, 'w') as f:
  95. json.dump(results, f, sort_keys=True, indent=4)
  96. logger.info(f'The keypoint result is saved to {self.res_file}.')
  97. try:
  98. json.load(open(self.res_file))
  99. except Exception:
  100. content = []
  101. with open(self.res_file, 'r') as f:
  102. for line in f:
  103. content.append(line)
  104. content[-1] = ']'
  105. with open(self.res_file, 'w') as f:
  106. for c in content:
  107. f.write(c)
  108. def _coco_keypoint_results_one_category_kernel(self, data_pack):
  109. cat_id = data_pack['cat_id']
  110. keypoints = data_pack['keypoints']
  111. cat_results = []
  112. for img_kpts in keypoints:
  113. if len(img_kpts) == 0:
  114. continue
  115. _key_points = np.array(
  116. [img_kpts[k]['keypoints'] for k in range(len(img_kpts))])
  117. _key_points = _key_points.reshape(_key_points.shape[0], -1)
  118. result = [{
  119. 'image_id': img_kpts[k]['image'],
  120. 'category_id': cat_id,
  121. 'keypoints': _key_points[k].tolist(),
  122. 'score': img_kpts[k]['score'],
  123. 'center': list(img_kpts[k]['center']),
  124. 'scale': list(img_kpts[k]['scale'])
  125. } for k in range(len(img_kpts))]
  126. cat_results.extend(result)
  127. return cat_results
  128. def get_final_results(self, preds, all_boxes, img_path):
  129. _kpts = []
  130. for idx, kpt in enumerate(preds):
  131. _kpts.append({
  132. 'keypoints': kpt,
  133. 'center': all_boxes[idx][0:2],
  134. 'scale': all_boxes[idx][2:4],
  135. 'area': all_boxes[idx][4],
  136. 'score': all_boxes[idx][5],
  137. 'image': int(img_path[idx])
  138. })
  139. # image x person x (keypoints)
  140. kpts = defaultdict(list)
  141. for kpt in _kpts:
  142. kpts[kpt['image']].append(kpt)
  143. # rescoring and oks nms
  144. num_joints = preds.shape[1]
  145. in_vis_thre = self.in_vis_thre
  146. oks_thre = self.oks_thre
  147. oks_nmsed_kpts = []
  148. for img in kpts.keys():
  149. img_kpts = kpts[img]
  150. for n_p in img_kpts:
  151. box_score = n_p['score']
  152. kpt_score = 0
  153. valid_num = 0
  154. for n_jt in range(0, num_joints):
  155. t_s = n_p['keypoints'][n_jt][2]
  156. if t_s > in_vis_thre:
  157. kpt_score = kpt_score + t_s
  158. valid_num = valid_num + 1
  159. if valid_num != 0:
  160. kpt_score = kpt_score / valid_num
  161. # rescoring
  162. n_p['score'] = kpt_score * box_score
  163. keep = oks_nms([img_kpts[i] for i in range(len(img_kpts))],
  164. oks_thre)
  165. if len(keep) == 0:
  166. oks_nmsed_kpts.append(img_kpts)
  167. else:
  168. oks_nmsed_kpts.append([img_kpts[_keep] for _keep in keep])
  169. self._write_coco_keypoint_results(oks_nmsed_kpts)
  170. def accumulate(self):
  171. self.get_final_results(self.results['all_preds'],
  172. self.results['all_boxes'],
  173. self.results['image_path'])
  174. if self.save_prediction_only:
  175. logger.info(f'The keypoint result is saved to {self.res_file} '
  176. 'and do not evaluate the mAP.')
  177. return
  178. coco_dt = self.coco.loadRes(self.res_file)
  179. coco_eval = COCOeval(self.coco, coco_dt, 'keypoints')
  180. coco_eval.params.useSegm = None
  181. coco_eval.evaluate()
  182. coco_eval.accumulate()
  183. coco_eval.summarize()
  184. keypoint_stats = []
  185. for ind in range(len(coco_eval.stats)):
  186. keypoint_stats.append((coco_eval.stats[ind]))
  187. self.eval_results['keypoint'] = keypoint_stats
  188. def log(self):
  189. if self.save_prediction_only:
  190. return
  191. stats_names = [
  192. 'AP', 'Ap .5', 'AP .75', 'AP (M)', 'AP (L)', 'AR', 'AR .5',
  193. 'AR .75', 'AR (M)', 'AR (L)'
  194. ]
  195. num_values = len(stats_names)
  196. print(' '.join(['| {}'.format(name) for name in stats_names]) + ' |')
  197. print('|---' * (num_values + 1) + '|')
  198. print(' '.join([
  199. '| {:.3f}'.format(value) for value in self.eval_results['keypoint']
  200. ]) + ' |')
  201. def get_results(self):
  202. return self.eval_results
  203. class KeyPointTopDownMPIIEval(object):
  204. def __init__(self,
  205. anno_file,
  206. num_samples,
  207. num_joints,
  208. output_eval,
  209. oks_thre=0.9,
  210. save_prediction_only=False):
  211. super(KeyPointTopDownMPIIEval, self).__init__()
  212. self.ann_file = anno_file
  213. self.res_file = os.path.join(output_eval, "keypoints_results.json")
  214. self.save_prediction_only = save_prediction_only
  215. self.reset()
  216. def reset(self):
  217. self.results = []
  218. self.eval_results = {}
  219. self.idx = 0
  220. def update(self, inputs, outputs):
  221. kpts, _ = outputs['keypoint'][0]
  222. num_images = inputs['image'].shape[0]
  223. results = {}
  224. results['preds'] = kpts[:, :, 0:3]
  225. results['boxes'] = np.zeros((num_images, 6))
  226. results['boxes'][:, 0:2] = inputs['center'].numpy()[:, 0:2]
  227. results['boxes'][:, 2:4] = inputs['scale'].numpy()[:, 0:2]
  228. results['boxes'][:, 4] = np.prod(inputs['scale'].numpy() * 200, 1)
  229. results['boxes'][:, 5] = np.squeeze(inputs['score'].numpy())
  230. results['image_path'] = inputs['image_file']
  231. self.results.append(results)
  232. def accumulate(self):
  233. self._mpii_keypoint_results_save()
  234. if self.save_prediction_only:
  235. logger.info(f'The keypoint result is saved to {self.res_file} '
  236. 'and do not evaluate the mAP.')
  237. return
  238. self.eval_results = self.evaluate(self.results)
  239. def _mpii_keypoint_results_save(self):
  240. results = []
  241. for res in self.results:
  242. if len(res) == 0:
  243. continue
  244. result = [{
  245. 'preds': res['preds'][k].tolist(),
  246. 'boxes': res['boxes'][k].tolist(),
  247. 'image_path': res['image_path'][k],
  248. } for k in range(len(res))]
  249. results.extend(result)
  250. with open(self.res_file, 'w') as f:
  251. json.dump(results, f, sort_keys=True, indent=4)
  252. logger.info(f'The keypoint result is saved to {self.res_file}.')
  253. def log(self):
  254. if self.save_prediction_only:
  255. return
  256. for item, value in self.eval_results.items():
  257. print("{} : {}".format(item, value))
  258. def get_results(self):
  259. return self.eval_results
  260. def evaluate(self, outputs, savepath=None):
  261. """Evaluate PCKh for MPII dataset. refer to
  262. https://github.com/leoxiaobin/deep-high-resolution-net.pytorch
  263. Copyright (c) Microsoft, under the MIT License.
  264. Args:
  265. outputs(list(preds, boxes)):
  266. * preds (np.ndarray[N,K,3]): The first two dimensions are
  267. coordinates, score is the third dimension of the array.
  268. * boxes (np.ndarray[N,6]): [center[0], center[1], scale[0]
  269. , scale[1],area, score]
  270. Returns:
  271. dict: PCKh for each joint
  272. """
  273. kpts = []
  274. for output in outputs:
  275. preds = output['preds']
  276. batch_size = preds.shape[0]
  277. for i in range(batch_size):
  278. kpts.append({'keypoints': preds[i]})
  279. preds = np.stack([kpt['keypoints'] for kpt in kpts])
  280. # convert 0-based index to 1-based index,
  281. # and get the first two dimensions.
  282. preds = preds[..., :2] + 1.0
  283. if savepath is not None:
  284. pred_file = os.path.join(savepath, 'pred.mat')
  285. savemat(pred_file, mdict={'preds': preds})
  286. SC_BIAS = 0.6
  287. threshold = 0.5
  288. gt_file = os.path.join(
  289. os.path.dirname(self.ann_file), 'mpii_gt_val.mat')
  290. gt_dict = loadmat(gt_file)
  291. dataset_joints = gt_dict['dataset_joints']
  292. jnt_missing = gt_dict['jnt_missing']
  293. pos_gt_src = gt_dict['pos_gt_src']
  294. headboxes_src = gt_dict['headboxes_src']
  295. pos_pred_src = np.transpose(preds, [1, 2, 0])
  296. head = np.where(dataset_joints == 'head')[1][0]
  297. lsho = np.where(dataset_joints == 'lsho')[1][0]
  298. lelb = np.where(dataset_joints == 'lelb')[1][0]
  299. lwri = np.where(dataset_joints == 'lwri')[1][0]
  300. lhip = np.where(dataset_joints == 'lhip')[1][0]
  301. lkne = np.where(dataset_joints == 'lkne')[1][0]
  302. lank = np.where(dataset_joints == 'lank')[1][0]
  303. rsho = np.where(dataset_joints == 'rsho')[1][0]
  304. relb = np.where(dataset_joints == 'relb')[1][0]
  305. rwri = np.where(dataset_joints == 'rwri')[1][0]
  306. rkne = np.where(dataset_joints == 'rkne')[1][0]
  307. rank = np.where(dataset_joints == 'rank')[1][0]
  308. rhip = np.where(dataset_joints == 'rhip')[1][0]
  309. jnt_visible = 1 - jnt_missing
  310. uv_error = pos_pred_src - pos_gt_src
  311. uv_err = np.linalg.norm(uv_error, axis=1)
  312. headsizes = headboxes_src[1, :, :] - headboxes_src[0, :, :]
  313. headsizes = np.linalg.norm(headsizes, axis=0)
  314. headsizes *= SC_BIAS
  315. scale = headsizes * np.ones((len(uv_err), 1), dtype=np.float32)
  316. scaled_uv_err = uv_err / scale
  317. scaled_uv_err = scaled_uv_err * jnt_visible
  318. jnt_count = np.sum(jnt_visible, axis=1)
  319. less_than_threshold = (scaled_uv_err <= threshold) * jnt_visible
  320. PCKh = 100. * np.sum(less_than_threshold, axis=1) / jnt_count
  321. # save
  322. rng = np.arange(0, 0.5 + 0.01, 0.01)
  323. pckAll = np.zeros((len(rng), 16), dtype=np.float32)
  324. for r, threshold in enumerate(rng):
  325. less_than_threshold = (scaled_uv_err <= threshold) * jnt_visible
  326. pckAll[r, :] = 100. * np.sum(less_than_threshold,
  327. axis=1) / jnt_count
  328. PCKh = np.ma.array(PCKh, mask=False)
  329. PCKh.mask[6:8] = True
  330. jnt_count = np.ma.array(jnt_count, mask=False)
  331. jnt_count.mask[6:8] = True
  332. jnt_ratio = jnt_count / np.sum(jnt_count).astype(np.float64)
  333. name_value = [ #noqa
  334. ('Head', PCKh[head]),
  335. ('Shoulder', 0.5 * (PCKh[lsho] + PCKh[rsho])),
  336. ('Elbow', 0.5 * (PCKh[lelb] + PCKh[relb])),
  337. ('Wrist', 0.5 * (PCKh[lwri] + PCKh[rwri])),
  338. ('Hip', 0.5 * (PCKh[lhip] + PCKh[rhip])),
  339. ('Knee', 0.5 * (PCKh[lkne] + PCKh[rkne])),
  340. ('Ankle', 0.5 * (PCKh[lank] + PCKh[rank])),
  341. ('PCKh', np.sum(PCKh * jnt_ratio)),
  342. ('PCKh@0.1', np.sum(pckAll[11, :] * jnt_ratio))
  343. ]
  344. name_value = OrderedDict(name_value)
  345. return name_value
  346. def _sort_and_unique_bboxes(self, kpts, key='bbox_id'):
  347. """sort kpts and remove the repeated ones."""
  348. kpts = sorted(kpts, key=lambda x: x[key])
  349. num = len(kpts)
  350. for i in range(num - 1, 0, -1):
  351. if kpts[i][key] == kpts[i - 1][key]:
  352. del kpts[i]
  353. return kpts