Skip to content

Commit 1ee7aa3

Browse files
committed
PythonAPI/cocoeval.py add keypoint evaluation tools!
1 parent 1a32502 commit 1ee7aa3

File tree

2 files changed

+154
-51
lines changed

2 files changed

+154
-51
lines changed

PythonAPI/pycocotools/coco.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def __init__(self, annotation_file=None):
7272
print 'loading annotations into memory...'
7373
tic = time.time()
7474
dataset = json.load(open(annotation_file, 'r'))
75+
assert type(dataset)==dict, "annotation file format %s not supported"%(type(dataset))
7576
print 'Done (t=%0.2fs)'%(time.time()- tic)
7677
self.dataset = dataset
7778
self.createIndex()
@@ -332,8 +333,10 @@ def loadRes(self, resFile):
332333
s = ann['keypoints']
333334
x = s[0::3]
334335
y = s[1::3]
335-
ann['area'] = float((np.max(x)-np.min(x))*(np.max(y)-np.min(y)))
336+
x0,x1,y0,y1 = np.min(x), np.max(x), np.min(y), np.max(y)
337+
ann['area'] = (x1-x0)*(y1-y0)
336338
ann['id'] = id + 1
339+
ann['bbox'] = [x0,y0,x1-x0,y1-y0]
337340
print 'DONE (t=%0.2fs)'%(time.time()- tic)
338341

339342
res.dataset['annotations'] = anns

PythonAPI/pycocotools/cocoeval.py

+150-50
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ class COCOeval:
2626
# recThrs - [0:.01:1] R=101 recall thresholds for evaluation
2727
# areaRng - [...] A=4 object area ranges for evaluation
2828
# maxDets - [1 10 100] M=3 thresholds on max detections per image
29-
# useSegm - [1] if true evaluate against ground-truth segments
30-
# useCats - [1] if true use category labels for evaluation # Note: if useSegm=0 the evaluation is run on bounding boxes.
29+
# iouType - ['segm'] set iouType to 'segm', 'bbox' or 'keypoints'
30+
# iouType replaced the now DEPRECATED useSegm parameter.
31+
# useCats - [1] if true use category labels for evaluation
3132
# Note: if useCats=0 category labels are ignored as in proposal scoring.
3233
# Note: multiple areaRngs [Ax2] and maxDets [Mx1] can be specified.
3334
#
@@ -56,21 +57,23 @@ class COCOeval:
5657
# Data, paper, and tutorials available at: http://mscoco.org/
5758
# Code written by Piotr Dollar and Tsung-Yi Lin, 2015.
5859
# Licensed under the Simplified BSD License [see coco/license.txt]
59-
def __init__(self, cocoGt=None, cocoDt=None):
60+
def __init__(self, cocoGt=None, cocoDt=None, iouType="segm"):
6061
'''
6162
Initialize CocoEval using coco APIs for gt and dt
6263
:param cocoGt: coco object with ground truth annotations
6364
:param cocoDt: coco object with detection results
6465
:return: None
6566
'''
67+
if not iouType:
68+
print("iouType not specified. use default iouType segm")
6669
self.cocoGt = cocoGt # ground truth COCO API
6770
self.cocoDt = cocoDt # detections COCO API
6871
self.params = {} # evaluation parameters
6972
self.evalImgs = defaultdict(list) # per-image per-category evaluation results [KxAxI] elements
7073
self.eval = {} # accumulated evaluation results
7174
self._gts = defaultdict(list) # gt for evaluation
7275
self._dts = defaultdict(list) # dt for evaluation
73-
self.params = Params() # parameters
76+
self.params = Params(iouType=iouType) # parameters
7477
self._paramsEval = {} # parameters for evaluation
7578
self.stats = [] # result summarization
7679
self.ious = {} # ious between all gts and dts
@@ -84,7 +87,6 @@ def _prepare(self):
8487
Prepare ._gts and ._dts for evaluation based on params
8588
:return: None
8689
'''
87-
#
8890
def _toMask(objs, coco):
8991
# modify segmentation by reference
9092
for obj in objs:
@@ -114,9 +116,16 @@ def _toMask(objs, coco):
114116
gts=self.cocoGt.loadAnns(self.cocoGt.getAnnIds(imgIds=p.imgIds))
115117
dts=self.cocoDt.loadAnns(self.cocoDt.getAnnIds(imgIds=p.imgIds))
116118

117-
if p.useSegm:
119+
# convert ground truth to mask if iouType == "segm"
120+
if p.iouType == "segm":
118121
_toMask(gts, self.cocoGt)
119122
_toMask(dts, self.cocoDt)
123+
# set ignore flag
124+
for gt in gts:
125+
gt["ignore"] = gt["ignore"] if "ignore" in gt else 0
126+
gt["ignore"] = "iscrowd" in gt and gt["iscrowd"]
127+
if p.iouType == "keypoints":
128+
gt["ignore"] = (gt["num_keypoints"] == 0) or gt["ignore"]
120129
self._gts = defaultdict(list) # gt for evaluation
121130
self._dts = defaultdict(list) # dt for evaluation
122131
for gt in gts:
@@ -134,6 +143,10 @@ def evaluate(self):
134143
tic = time.time()
135144
print 'Running per image evaluation... '
136145
p = self.params
146+
# add backward compatibility if useSegm is specified in params
147+
if not p.useSegm is None:
148+
p.iouType = "segm" if p.useSegm == 1 else "bbox"
149+
print("useSegm (deprecated) is not None. Running %s evaluation"%(p.iouType))
137150
p.imgIds = list(np.unique(p.imgIds))
138151
if p.useCats:
139152
p.catIds = list(np.unique(p.catIds))
@@ -144,7 +157,10 @@ def evaluate(self):
144157
# loop through images, area range, max detection number
145158
catIds = p.catIds if p.useCats else [-1]
146159

147-
computeIoU = self.computeIoU
160+
if p.iouType == "segm" or p.iouType == "bbox":
161+
computeIoU = self.computeIoU
162+
elif p.iouType == "keypoints":
163+
computeIoU = self.computeOks
148164
self.ious = {(imgId, catId): computeIoU(imgId, catId) \
149165
for imgId in p.imgIds
150166
for catId in catIds}
@@ -170,28 +186,73 @@ def computeIoU(self, imgId, catId):
170186
dt = [_ for cId in p.catIds for _ in self._dts[imgId,cId]]
171187
if len(gt) == 0 and len(dt) ==0:
172188
return []
173-
dt = sorted(dt, key=lambda x: -x['score'])
189+
inds = np.argsort(map(lambda x:-x["score"], dt),kind='mergesort')
190+
dt = [dt[i] for i in inds]
174191
if len(dt) > p.maxDets[-1]:
175192
dt=dt[0:p.maxDets[-1]]
176193

177-
if p.useSegm:
194+
if p.iouType == "segm":
178195
g = [g['segmentation'] for g in gt]
179196
d = [d['segmentation'] for d in dt]
180-
else:
197+
elif p.iouType == "bbox":
181198
g = [g['bbox'] for g in gt]
182199
d = [d['bbox'] for d in dt]
200+
else:
201+
raise Exception("unknown iouType for iou computation")
183202

184203
# compute iou between each dt and gt region
185204
iscrowd = [int(o['iscrowd']) for o in gt]
186205
ious = mask.iou(d,g,iscrowd)
187206
return ious
188207

208+
def computeOks(self, imgId, catId):
209+
p = self.params
210+
# dimention here should be Nxm
211+
gts = self._gts[imgId, catId]
212+
dts = self._dts[imgId, catId]
213+
inds = np.argsort(map(lambda x: -x["score"], dts), kind='mergesort')
214+
dts = [dts[i] for i in inds]
215+
if len(dts) > p.maxDets[-1]:
216+
dts = dts[0:p.maxDets[-1]]
217+
# if len(gts) == 0 and len(dts) == 0:
218+
if len(gts) == 0 or len(dts) == 0:
219+
return []
220+
ious = np.zeros((len(dts), len(gts)))
221+
sigmas = np.array([.26, .25, .25, .35, .35, .79, .79, .72, .72, .62,.62, 1.07, 1.07, .87, .87, .89, .89])/10.0
222+
vars = (sigmas * 2)**2
223+
k = len(sigmas)
224+
# compute oks between each detection and ground truth object
225+
for j, gt in enumerate(gts):
226+
# create bounds for ignore regions(double the gt bbox)
227+
g = np.array(gt["keypoints"])
228+
xg = g[0::3]; yg = g[1::3]; vg = g[2::3]
229+
k1 = np.count_nonzero(vg > 0)
230+
bb = gt["bbox"]
231+
x0 = bb[0] - bb[2]; x1 = bb[0] + bb[2] * 2
232+
y0 = bb[1] - bb[3]; y1 = bb[1] + bb[3] * 2
233+
for i, dt in enumerate(dts):
234+
d = np.array(dt["keypoints"])
235+
xd = d[0::3]; yd = d[1::3]
236+
if k1>0:
237+
# measure the per-keypoint distance if keypoints visible
238+
dx = xd - xg
239+
dy = yd - yg
240+
else:
241+
# measure minimum distance to keypoints in (x0,y0) & (x1,y1)
242+
z = np.zeros((k))
243+
dx = np.max((z, x0-xd),axis=0)+np.max((z, xd-x1),axis=0)
244+
dy = np.max((z, y0-yd),axis=0)+np.max((z, yd-y1),axis=0)
245+
e = (dx**2 + dy**2) / vars / (gt["area"]+np.spacing(1)) / 2
246+
if k1 > 0:
247+
e=e[vg > 0]
248+
ious[i, j] = np.sum(np.exp(-e)) / e.shape[0]
249+
return ious
250+
189251
def evaluateImg(self, imgId, catId, aRng, maxDet):
190252
'''
191253
perform evaluation for single category and image
192254
:return: dict (single image results)
193255
'''
194-
#
195256
p = self.params
196257
if p.useCats:
197258
gt = self._gts[imgId,catId]
@@ -203,23 +264,19 @@ def evaluateImg(self, imgId, catId, aRng, maxDet):
203264
return None
204265

205266
for g in gt:
206-
if 'ignore' not in g:
207-
g['ignore'] = 0
208-
if g['iscrowd'] == 1 or g['ignore'] or (g['area']<aRng[0] or g['area']>aRng[1]):
267+
if g['ignore'] or (g['area']<aRng[0] or g['area']>aRng[1]):
209268
g['_ignore'] = 1
210269
else:
211270
g['_ignore'] = 0
212271

213272
# sort dt highest score first, sort gt ignore last
214-
# gt = sorted(gt, key=lambda x: x['_ignore'])
215-
gtind = [ind for (ind, g) in sorted(enumerate(gt), key=lambda (ind, g): g['_ignore']) ]
216-
217-
gt = [gt[ind] for ind in gtind]
218-
dt = sorted(dt, key=lambda x: -x['score'])[0:maxDet]
273+
gtind = np.argsort([g['_ignore'] for g in gt], kind="mergesort")
274+
gt = map(lambda i: gt[i], gtind)
275+
dtind = np.argsort([-d['score'] for d in dt], kind="mergesort")
276+
dt = map(lambda i: dt[i], dtind[0:maxDet])
219277
iscrowd = [int(o['iscrowd']) for o in gt]
220278
# load computed ious
221-
N_iou = len(self.ious[imgId, catId])
222-
ious = self.ious[imgId, catId][0:maxDet, np.array(gtind)] if N_iou >0 else self.ious[imgId, catId]
279+
ious = self.ious[imgId, catId][:, gtind] if len(self.ious[imgId, catId]) > 0 else self.ious[imgId, catId]
223280

224281
T = len(p.iouThrs)
225282
G = len(gt)
@@ -244,7 +301,7 @@ def evaluateImg(self, imgId, catId, aRng, maxDet):
244301
# continue to next gt unless better match made
245302
if ious[dind,gind] < iou:
246303
continue
247-
# match successful and best so far, store appropriately
304+
# if match successful and best so far, store appropriately
248305
iou=ious[dind,gind]
249306
m=gind
250307
# if match made store id of match for both dt and gt
@@ -305,7 +362,6 @@ def accumulate(self, p = None):
305362
m_list = [m for n, m in enumerate(p.maxDets) if m in setM]
306363
a_list = [n for n, a in enumerate(map(lambda x: tuple(x), p.areaRng)) if a in setA]
307364
i_list = [n for n, i in enumerate(p.imgIds) if i in setI]
308-
# K0 = len(_pe.catIds)
309365
I0 = len(_pe.imgIds)
310366
A0 = len(_pe.areaRng)
311367
# retrieve E at each category, area range, and max number of detections
@@ -326,8 +382,8 @@ def accumulate(self, p = None):
326382

327383
dtm = np.concatenate([e['dtMatches'][:,0:maxDet] for e in E], axis=1)[:,inds]
328384
dtIg = np.concatenate([e['dtIgnore'][:,0:maxDet] for e in E], axis=1)[:,inds]
329-
gtIg = np.concatenate([e['gtIgnore'] for e in E])
330-
npig = len([ig for ig in gtIg if ig == 0])
385+
gtIg = np.concatenate([e['gtIgnore'] for e in E])
386+
npig = np.count_nonzero(gtIg==0 )
331387
if npig == 0:
332388
continue
333389
tps = np.logical_and( dtm, np.logical_not(dtIg) )
@@ -356,7 +412,7 @@ def accumulate(self, p = None):
356412
if pr[i] > pr[i-1]:
357413
pr[i-1] = pr[i]
358414

359-
inds = np.searchsorted(rc, p.recThrs)
415+
inds = np.searchsorted(rc, p.recThrs, side='left')
360416
try:
361417
for ri, pi in enumerate(inds):
362418
q[ri] = pr[pi]
@@ -387,43 +443,65 @@ def _summarize( ap=1, iouThr=None, areaRng='all', maxDets=100 ):
387443
areaStr = areaRng
388444
maxDetsStr = '%d'%(maxDets)
389445

390-
aind = [i for i, aRng in enumerate(['all', 'small', 'medium', 'large']) if aRng == areaRng]
391-
mind = [i for i, mDet in enumerate([1, 10, 100]) if mDet == maxDets]
446+
aind = [i for i, aRng in enumerate(p.areaRngLbl) if aRng == areaRng]
447+
mind = [i for i, mDet in enumerate(p.maxDets) if mDet == maxDets]
392448
if ap == 1:
393449
# dimension of precision: [TxRxKxAxM]
394450
s = self.eval['precision']
395451
# IoU
396452
if iouThr is not None:
397453
t = np.where(iouThr == p.iouThrs)[0]
398454
s = s[t]
399-
# areaRng
400455
s = s[:,:,:,aind,mind]
401456
else:
402457
# dimension of recall: [TxKxAxM]
403458
s = self.eval['recall']
459+
if iouThr is not None:
460+
t = np.where(iouThr == p.iouThrs)[0]
461+
s = s[t]
404462
s = s[:,:,aind,mind]
405463
if len(s[s>-1])==0:
406464
mean_s = -1
407465
else:
408466
mean_s = np.mean(s[s>-1])
409467
print iStr.format(titleStr, typeStr, iouStr, areaStr, maxDetsStr, '%.3f'%(float(mean_s)))
410468
return mean_s
411-
469+
def _summarizeDets():
470+
stats = np.zeros((12,))
471+
stats[0] = _summarize(1)
472+
stats[1] = _summarize(1, iouThr=.5)
473+
stats[2] = _summarize(1, iouThr=.75)
474+
stats[3] = _summarize(1, areaRng='small')
475+
stats[4] = _summarize(1, areaRng='medium')
476+
stats[5] = _summarize(1, areaRng='large')
477+
stats[6] = _summarize(0, maxDets=1)
478+
stats[7] = _summarize(0, maxDets=10)
479+
stats[8] = _summarize(0, maxDets=100)
480+
stats[9] = _summarize(0, areaRng='small')
481+
stats[10] = _summarize(0, areaRng='medium')
482+
stats[11] = _summarize(0, areaRng='large')
483+
return stats
484+
def _summarizeKps():
485+
stats = np.zeros((10,))
486+
stats[0] = _summarize(1, maxDets=20)
487+
stats[1] = _summarize(1, maxDets=20, iouThr=.5)
488+
stats[2] = _summarize(1, maxDets=20, iouThr=.75)
489+
stats[3] = _summarize(1, maxDets=20, areaRng='medium')
490+
stats[4] = _summarize(1, maxDets=20, areaRng='large')
491+
stats[5] = _summarize(0, maxDets=20)
492+
stats[6] = _summarize(0, maxDets=20, iouThr=.5)
493+
stats[7] = _summarize(0, maxDets=20, iouThr=.75)
494+
stats[8] = _summarize(0, maxDets=20, areaRng='medium')
495+
stats[9] = _summarize(0, maxDets=20, areaRng='large')
496+
return stats
412497
if not self.eval:
413498
raise Exception('Please run accumulate() first')
414-
self.stats = np.zeros((12,))
415-
self.stats[0] = _summarize(1)
416-
self.stats[1] = _summarize(1,iouThr=.5)
417-
self.stats[2] = _summarize(1,iouThr=.75)
418-
self.stats[3] = _summarize(1,areaRng='small')
419-
self.stats[4] = _summarize(1,areaRng='medium')
420-
self.stats[5] = _summarize(1,areaRng='large')
421-
self.stats[6] = _summarize(0,maxDets=1)
422-
self.stats[7] = _summarize(0,maxDets=10)
423-
self.stats[8] = _summarize(0,maxDets=100)
424-
self.stats[9] = _summarize(0,areaRng='small')
425-
self.stats[10] = _summarize(0,areaRng='medium')
426-
self.stats[11] = _summarize(0,areaRng='large')
499+
iouType = self.params.iouType
500+
if iouType == "segm" or iouType == "bbox":
501+
summarize = _summarizeDets
502+
elif iouType == "keypoints":
503+
summarize = _summarizeKps
504+
self.stats = summarize()
427505

428506
def __str__(self):
429507
self.summarize()
@@ -432,13 +510,35 @@ class Params:
432510
'''
433511
Params for coco evaluation api
434512
'''
435-
def __init__(self):
513+
def setDetParams(self):
436514
self.imgIds = []
437515
self.catIds = []
438516
# np.arange causes trouble. the data point on arange is slightly larger than the true value
439-
self.iouThrs = np.linspace(.5, 0.95, np.round((0.95-.5)/.05)+1, endpoint=True)
440-
self.recThrs = np.linspace(.0, 1.00, np.round((1.00-.0)/.01)+1, endpoint=True)
441-
self.maxDets = [1,10,100]
442-
self.areaRng = [ [0**2,1e5**2], [0**2, 32**2], [32**2, 96**2], [96**2, 1e5**2] ]
443-
self.useSegm = 0
444-
self.useCats = 1
517+
self.iouThrs = np.linspace(.5, 0.95, np.round((0.95 - .5) / .05) + 1, endpoint=True)
518+
self.recThrs = np.linspace(.0, 1.00, np.round((1.00 - .0) / .01) + 1, endpoint=True)
519+
self.maxDets = [1, 10, 100]
520+
self.areaRng = [[0 ** 2, 1e5 ** 2], [0 ** 2, 32 ** 2], [32 ** 2, 96 ** 2], [96 ** 2, 1e5 ** 2]]
521+
self.areaRngLbl = ['all', 'small', 'medium', 'large']
522+
self.useCats = 1
523+
524+
def setKpParams(self):
525+
self.imgIds = []
526+
self.catIds = []
527+
# np.arange causes trouble. the data point on arange is slightly larger than the true value
528+
self.iouThrs = np.linspace(.5, 0.95, np.round((0.95 - .5) / .05) + 1, endpoint=True)
529+
self.recThrs = np.linspace(.0, 1.00, np.round((1.00 - .0) / .01) + 1, endpoint=True)
530+
self.maxDets = [20]
531+
self.areaRng = [[0 ** 2, 1e5 ** 2], [32 ** 2, 96 ** 2], [96 ** 2, 1e5 ** 2]]
532+
self.areaRngLbl = ['all', 'medium', 'large']
533+
self.useCats = 1
534+
535+
def __init__(self, iouType="segm"):
536+
if iouType == "segm" or iouType == "bbox":
537+
self.setDetParams()
538+
elif iouType == "keypoints":
539+
self.setKpParams()
540+
else:
541+
raise Exception("iouType not supported")
542+
self.iouType = iouType
543+
# useSegm is deprecated
544+
self.useSegm = None

0 commit comments

Comments
 (0)