first commit
Some checks are pending
Build/Publish Develop Docs / deploy (push) Waiting to run

This commit is contained in:
2025-07-02 08:57:16 +03:00
commit 56532cc9a9
1901 changed files with 457695 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
# @Time : 2019/8/23 21:58
# @Author : zhoujun
from .util import *
from .metrics import *
from .schedulers import *
from .cal_recall.script import cal_recall_precision_f1
from .ocr_metric import get_metric

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# @Time : 1/16/19 6:40 AM
# @Author : zhoujun
from .script import cal_recall_precision_f1
__all__ = ["cal_recall_precision_f1"]

View File

@@ -0,0 +1,494 @@
#!/usr/bin/env python2
# encoding: UTF-8
import json
import sys
sys.path.append("./")
import zipfile
import re
import sys
import os
import codecs
import traceback
import numpy as np
from utils import order_points_clockwise
def print_help():
sys.stdout.write(
"Usage: python %s.py -g=<gtFile> -s=<submFile> [-o=<outputFolder> -p=<jsonParams>]"
% sys.argv[0]
)
sys.exit(2)
def load_zip_file_keys(file, fileNameRegExp=""):
"""
Returns an array with the entries of the ZIP file that match with the regular expression.
The key's are the names or the file or the capturing group defined in the fileNameRegExp
"""
try:
archive = zipfile.ZipFile(file, mode="r", allowZip64=True)
except:
raise Exception("Error loading the ZIP archive.")
pairs = []
for name in archive.namelist():
addFile = True
keyName = name
if fileNameRegExp != "":
m = re.match(fileNameRegExp, name)
if m == None:
addFile = False
else:
if len(m.groups()) > 0:
keyName = m.group(1)
if addFile:
pairs.append(keyName)
return pairs
def load_zip_file(file, fileNameRegExp="", allEntries=False):
"""
Returns an array with the contents (filtered by fileNameRegExp) of a ZIP file.
The key's are the names or the file or the capturing group defined in the fileNameRegExp
allEntries validates that all entries in the ZIP file pass the fileNameRegExp
"""
try:
archive = zipfile.ZipFile(file, mode="r", allowZip64=True)
except:
raise Exception("Error loading the ZIP archive")
pairs = []
for name in archive.namelist():
addFile = True
keyName = name
if fileNameRegExp != "":
m = re.match(fileNameRegExp, name)
if m == None:
addFile = False
else:
if len(m.groups()) > 0:
keyName = m.group(1)
if addFile:
pairs.append([keyName, archive.read(name)])
else:
if allEntries:
raise Exception("ZIP entry not valid: %s" % name)
return dict(pairs)
def load_folder_file(file, fileNameRegExp="", allEntries=False):
"""
Returns an array with the contents (filtered by fileNameRegExp) of a ZIP file.
The key's are the names or the file or the capturing group defined in the fileNameRegExp
allEntries validates that all entries in the ZIP file pass the fileNameRegExp
"""
pairs = []
for name in os.listdir(file):
addFile = True
keyName = name
if fileNameRegExp != "":
m = re.match(fileNameRegExp, name)
if m == None:
addFile = False
else:
if len(m.groups()) > 0:
keyName = m.group(1)
if addFile:
pairs.append([keyName, open(os.path.join(file, name)).read()])
else:
if allEntries:
raise Exception("ZIP entry not valid: %s" % name)
return dict(pairs)
def decode_utf8(raw):
"""
Returns a Unicode object on success, or None on failure
"""
try:
raw = codecs.decode(raw, "utf-8", "replace")
# extracts BOM if exists
raw = raw.encode("utf8")
if raw.startswith(codecs.BOM_UTF8):
raw = raw.replace(codecs.BOM_UTF8, "", 1)
return raw.decode("utf-8")
except:
return None
def validate_lines_in_file(
fileName,
file_contents,
CRLF=True,
LTRB=True,
withTranscription=False,
withConfidence=False,
imWidth=0,
imHeight=0,
):
"""
This function validates that all lines of the file calling the Line validation function for each line
"""
utf8File = decode_utf8(file_contents)
if utf8File is None:
raise Exception("The file %s is not UTF-8" % fileName)
lines = utf8File.split("\r\n" if CRLF else "\n")
for line in lines:
line = line.replace("\r", "").replace("\n", "")
if line != "":
try:
validate_tl_line(
line, LTRB, withTranscription, withConfidence, imWidth, imHeight
)
except Exception as e:
raise Exception(
(
"Line in sample not valid. Sample: %s Line: %s Error: %s"
% (fileName, line, str(e))
).encode("utf-8", "replace")
)
def validate_tl_line(
line, LTRB=True, withTranscription=True, withConfidence=True, imWidth=0, imHeight=0
):
"""
Validate the format of the line. If the line is not valid an exception will be raised.
If maxWidth and maxHeight are specified, all points must be inside the image bounds.
Possible values are:
LTRB=True: xmin,ymin,xmax,ymax[,confidence][,transcription]
LTRB=False: x1,y1,x2,y2,x3,y3,x4,y4[,confidence][,transcription]
"""
get_tl_line_values(line, LTRB, withTranscription, withConfidence, imWidth, imHeight)
def get_tl_line_values(
line,
LTRB=True,
withTranscription=False,
withConfidence=False,
imWidth=0,
imHeight=0,
):
"""
Validate the format of the line. If the line is not valid an exception will be raised.
If maxWidth and maxHeight are specified, all points must be inside the image bounds.
Possible values are:
LTRB=True: xmin,ymin,xmax,ymax[,confidence][,transcription]
LTRB=False: x1,y1,x2,y2,x3,y3,x4,y4[,confidence][,transcription]
Returns values from a textline. Points , [Confidences], [Transcriptions]
"""
confidence = 0.0
transcription = ""
points = []
numPoints = 4
if LTRB:
numPoints = 4
if withTranscription and withConfidence:
m = re.match(
r"^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-1].?[0-9]*)\s*,(.*)$",
line,
)
if m == None:
m = re.match(
r"^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-1].?[0-9]*)\s*,(.*)$",
line,
)
raise Exception(
"Format incorrect. Should be: xmin,ymin,xmax,ymax,confidence,transcription"
)
elif withConfidence:
m = re.match(
r"^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-1].?[0-9]*)\s*$",
line,
)
if m == None:
raise Exception(
"Format incorrect. Should be: xmin,ymin,xmax,ymax,confidence"
)
elif withTranscription:
m = re.match(
r"^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,(.*)$",
line,
)
if m == None:
raise Exception(
"Format incorrect. Should be: xmin,ymin,xmax,ymax,transcription"
)
else:
m = re.match(
r"^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,?\s*$",
line,
)
if m == None:
raise Exception("Format incorrect. Should be: xmin,ymin,xmax,ymax")
xmin = int(m.group(1))
ymin = int(m.group(2))
xmax = int(m.group(3))
ymax = int(m.group(4))
if xmax < xmin:
raise Exception("Xmax value (%s) not valid (Xmax < Xmin)." % (xmax))
if ymax < ymin:
raise Exception("Ymax value (%s) not valid (Ymax < Ymin)." % (ymax))
points = [float(m.group(i)) for i in range(1, (numPoints + 1))]
if imWidth > 0 and imHeight > 0:
validate_point_inside_bounds(xmin, ymin, imWidth, imHeight)
validate_point_inside_bounds(xmax, ymax, imWidth, imHeight)
else:
numPoints = 8
if withTranscription and withConfidence:
m = re.match(
r"^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-1].?[0-9]*)\s*,(.*)$",
line,
)
if m == None:
raise Exception(
"Format incorrect. Should be: x1,y1,x2,y2,x3,y3,x4,y4,confidence,transcription"
)
elif withConfidence:
m = re.match(
r"^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-1].?[0-9]*)\s*$",
line,
)
if m == None:
raise Exception(
"Format incorrect. Should be: x1,y1,x2,y2,x3,y3,x4,y4,confidence"
)
elif withTranscription:
m = re.match(
r"^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,(.*)$",
line,
)
if m == None:
raise Exception(
"Format incorrect. Should be: x1,y1,x2,y2,x3,y3,x4,y4,transcription"
)
else:
m = re.match(
r"^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*$",
line,
)
if m == None:
raise Exception("Format incorrect. Should be: x1,y1,x2,y2,x3,y3,x4,y4")
points = [float(m.group(i)) for i in range(1, (numPoints + 1))]
points = order_points_clockwise(np.array(points).reshape(-1, 2)).reshape(-1)
validate_clockwise_points(points)
if imWidth > 0 and imHeight > 0:
validate_point_inside_bounds(points[0], points[1], imWidth, imHeight)
validate_point_inside_bounds(points[2], points[3], imWidth, imHeight)
validate_point_inside_bounds(points[4], points[5], imWidth, imHeight)
validate_point_inside_bounds(points[6], points[7], imWidth, imHeight)
if withConfidence:
try:
confidence = float(m.group(numPoints + 1))
except ValueError:
raise Exception("Confidence value must be a float")
if withTranscription:
posTranscription = numPoints + (2 if withConfidence else 1)
transcription = m.group(posTranscription)
m2 = re.match(r"^\s*\"(.*)\"\s*$", transcription)
if (
m2 != None
): # Transcription with double quotes, we extract the value and replace escaped characters
transcription = m2.group(1).replace("\\\\", "\\").replace('\\"', '"')
return points, confidence, transcription
def validate_point_inside_bounds(x, y, imWidth, imHeight):
if x < 0 or x > imWidth:
raise Exception(
"X value (%s) not valid. Image dimensions: (%s,%s)"
% (xmin, imWidth, imHeight)
)
if y < 0 or y > imHeight:
raise Exception(
"Y value (%s) not valid. Image dimensions: (%s,%s) Sample: %s Line:%s"
% (ymin, imWidth, imHeight)
)
def validate_clockwise_points(points):
"""
Validates that the points that the 4 points that dlimite a polygon are in clockwise order.
"""
if len(points) != 8:
raise Exception("Points list not valid." + str(len(points)))
point = [
[int(points[0]), int(points[1])],
[int(points[2]), int(points[3])],
[int(points[4]), int(points[5])],
[int(points[6]), int(points[7])],
]
edge = [
(point[1][0] - point[0][0]) * (point[1][1] + point[0][1]),
(point[2][0] - point[1][0]) * (point[2][1] + point[1][1]),
(point[3][0] - point[2][0]) * (point[3][1] + point[2][1]),
(point[0][0] - point[3][0]) * (point[0][1] + point[3][1]),
]
summatory = edge[0] + edge[1] + edge[2] + edge[3]
if summatory > 0:
raise Exception(
"Points are not clockwise. The coordinates of bounding quadrilaterals have to be given in clockwise order. Regarding the correct interpretation of 'clockwise' remember that the image coordinate system used is the standard one, with the image origin at the upper left, the X axis extending to the right and Y axis extending downwards."
)
def get_tl_line_values_from_file_contents(
content,
CRLF=True,
LTRB=True,
withTranscription=False,
withConfidence=False,
imWidth=0,
imHeight=0,
sort_by_confidences=True,
):
"""
Returns all points, confindences and transcriptions of a file in lists. Valid line formats:
xmin,ymin,xmax,ymax,[confidence],[transcription]
x1,y1,x2,y2,x3,y3,x4,y4,[confidence],[transcription]
"""
pointsList = []
transcriptionsList = []
confidencesList = []
lines = content.split("\r\n" if CRLF else "\n")
for line in lines:
line = line.replace("\r", "").replace("\n", "")
if line != "":
points, confidence, transcription = get_tl_line_values(
line, LTRB, withTranscription, withConfidence, imWidth, imHeight
)
pointsList.append(points)
transcriptionsList.append(transcription)
confidencesList.append(confidence)
if withConfidence and len(confidencesList) > 0 and sort_by_confidences:
import numpy as np
sorted_ind = np.argsort(-np.array(confidencesList))
confidencesList = [confidencesList[i] for i in sorted_ind]
pointsList = [pointsList[i] for i in sorted_ind]
transcriptionsList = [transcriptionsList[i] for i in sorted_ind]
return pointsList, confidencesList, transcriptionsList
def main_evaluation(
p,
default_evaluation_params_fn,
validate_data_fn,
evaluate_method_fn,
show_result=True,
per_sample=True,
):
"""
This process validates a method, evaluates it and if it succeed generates a ZIP file with a JSON entry for each sample.
Params:
p: Dictionary of parameters with the GT/submission locations. If None is passed, the parameters send by the system are used.
default_evaluation_params_fn: points to a function that returns a dictionary with the default parameters used for the evaluation
validate_data_fn: points to a method that validates the correct format of the submission
evaluate_method_fn: points to a function that evaluated the submission and return a Dictionary with the results
"""
evalParams = default_evaluation_params_fn()
if "p" in p.keys():
evalParams.update(
p["p"] if isinstance(p["p"], dict) else json.loads(p["p"][1:-1])
)
resDict = {"calculated": True, "Message": "", "method": "{}", "per_sample": "{}"}
try:
# validate_data_fn(p['g'], p['s'], evalParams)
evalData = evaluate_method_fn(p["g"], p["s"], evalParams)
resDict.update(evalData)
except Exception as e:
traceback.print_exc()
resDict["Message"] = str(e)
resDict["calculated"] = False
if "o" in p:
if not os.path.exists(p["o"]):
os.makedirs(p["o"])
resultsOutputname = p["o"] + "/results.zip"
outZip = zipfile.ZipFile(resultsOutputname, mode="w", allowZip64=True)
del resDict["per_sample"]
if "output_items" in resDict.keys():
del resDict["output_items"]
outZip.writestr("method.json", json.dumps(resDict))
if not resDict["calculated"]:
if show_result:
sys.stderr.write("Error!\n" + resDict["Message"] + "\n\n")
if "o" in p:
outZip.close()
return resDict
if "o" in p:
if per_sample == True:
for k, v in evalData["per_sample"].iteritems():
outZip.writestr(k + ".json", json.dumps(v))
if "output_items" in evalData.keys():
for k, v in evalData["output_items"].iteritems():
outZip.writestr(k, v)
outZip.close()
if show_result:
sys.stdout.write("Calculated!")
sys.stdout.write(json.dumps(resDict["method"]))
return resDict
def main_validation(default_evaluation_params_fn, validate_data_fn):
"""
This process validates a method
Params:
default_evaluation_params_fn: points to a function that returns a dictionary with the default parameters used for the evaluation
validate_data_fn: points to a method that validates the correct format of the submission
"""
try:
p = dict([s[1:].split("=") for s in sys.argv[1:]])
evalParams = default_evaluation_params_fn()
if "p" in p.keys():
evalParams.update(
p["p"] if isinstance(p["p"], dict) else json.loads(p["p"][1:-1])
)
validate_data_fn(p["g"], p["s"], evalParams)
print("SUCCESS")
sys.exit(0)
except Exception as e:
print(str(e))
sys.exit(101)

View File

@@ -0,0 +1,402 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from collections import namedtuple
from . import rrc_evaluation_funcs
import Polygon as plg
import numpy as np
def default_evaluation_params():
"""
default_evaluation_params: Default parameters to use for the validation and evaluation.
"""
return {
"IOU_CONSTRAINT": 0.5,
"AREA_PRECISION_CONSTRAINT": 0.5,
"GT_SAMPLE_NAME_2_ID": "gt_img_([0-9]+).txt",
"DET_SAMPLE_NAME_2_ID": "res_img_([0-9]+).txt",
"LTRB": False, # LTRB:2points(left,top,right,bottom) or 4 points(x1,y1,x2,y2,x3,y3,x4,y4)
"CRLF": False, # Lines are delimited by Windows CRLF format
"CONFIDENCES": False, # Detections must include confidence value. AP will be calculated
"PER_SAMPLE_RESULTS": True, # Generate per sample results and produce data for visualization
}
def validate_data(gtFilePath, submFilePath, evaluationParams):
"""
Method validate_data: validates that all files in the results folder are correct (have the correct name contents).
Validates also that there are no missing files in the folder.
If some error detected, the method raises the error
"""
gt = rrc_evaluation_funcs.load_folder_file(
gtFilePath, evaluationParams["GT_SAMPLE_NAME_2_ID"]
)
subm = rrc_evaluation_funcs.load_folder_file(
submFilePath, evaluationParams["DET_SAMPLE_NAME_2_ID"], True
)
# Validate format of GroundTruth
for k in gt:
rrc_evaluation_funcs.validate_lines_in_file(
k, gt[k], evaluationParams["CRLF"], evaluationParams["LTRB"], True
)
# Validate format of results
for k in subm:
if (k in gt) == False:
raise Exception("The sample %s not present in GT" % k)
rrc_evaluation_funcs.validate_lines_in_file(
k,
subm[k],
evaluationParams["CRLF"],
evaluationParams["LTRB"],
False,
evaluationParams["CONFIDENCES"],
)
def evaluate_method(gtFilePath, submFilePath, evaluationParams):
"""
Method evaluate_method: evaluate method and returns the results
Results. Dictionary with the following values:
- method (required) Global method metrics. Ex: { 'Precision':0.8,'Recall':0.9 }
- samples (optional) Per sample metrics. Ex: {'sample1' : { 'Precision':0.8,'Recall':0.9 } , 'sample2' : { 'Precision':0.8,'Recall':0.9 }
"""
def polygon_from_points(points):
"""
Returns a Polygon object to use with the Polygon2 class from a list of 8 points: x1,y1,x2,y2,x3,y3,x4,y4
"""
resBoxes = np.empty([1, 8], dtype="int32")
resBoxes[0, 0] = int(points[0])
resBoxes[0, 4] = int(points[1])
resBoxes[0, 1] = int(points[2])
resBoxes[0, 5] = int(points[3])
resBoxes[0, 2] = int(points[4])
resBoxes[0, 6] = int(points[5])
resBoxes[0, 3] = int(points[6])
resBoxes[0, 7] = int(points[7])
pointMat = resBoxes[0].reshape([2, 4]).T
return plg.Polygon(pointMat)
def rectangle_to_polygon(rect):
resBoxes = np.empty([1, 8], dtype="int32")
resBoxes[0, 0] = int(rect.xmin)
resBoxes[0, 4] = int(rect.ymax)
resBoxes[0, 1] = int(rect.xmin)
resBoxes[0, 5] = int(rect.ymin)
resBoxes[0, 2] = int(rect.xmax)
resBoxes[0, 6] = int(rect.ymin)
resBoxes[0, 3] = int(rect.xmax)
resBoxes[0, 7] = int(rect.ymax)
pointMat = resBoxes[0].reshape([2, 4]).T
return plg.Polygon(pointMat)
def rectangle_to_points(rect):
points = [
int(rect.xmin),
int(rect.ymax),
int(rect.xmax),
int(rect.ymax),
int(rect.xmax),
int(rect.ymin),
int(rect.xmin),
int(rect.ymin),
]
return points
def get_union(pD, pG):
areaA = pD.area()
areaB = pG.area()
return areaA + areaB - get_intersection(pD, pG)
def get_intersection_over_union(pD, pG):
try:
return get_intersection(pD, pG) / get_union(pD, pG)
except:
return 0
def get_intersection(pD, pG):
pInt = pD & pG
if len(pInt) == 0:
return 0
return pInt.area()
def compute_ap(confList, matchList, numGtCare):
correct = 0
AP = 0
if len(confList) > 0:
confList = np.array(confList)
matchList = np.array(matchList)
sorted_ind = np.argsort(-confList)
confList = confList[sorted_ind]
matchList = matchList[sorted_ind]
for n in range(len(confList)):
match = matchList[n]
if match:
correct += 1
AP += float(correct) / (n + 1)
if numGtCare > 0:
AP /= numGtCare
return AP
perSampleMetrics = {}
matchedSum = 0
Rectangle = namedtuple("Rectangle", "xmin ymin xmax ymax")
gt = rrc_evaluation_funcs.load_folder_file(
gtFilePath, evaluationParams["GT_SAMPLE_NAME_2_ID"]
)
subm = rrc_evaluation_funcs.load_folder_file(
submFilePath, evaluationParams["DET_SAMPLE_NAME_2_ID"], True
)
numGlobalCareGt = 0
numGlobalCareDet = 0
arrGlobalConfidences = []
arrGlobalMatches = []
for resFile in gt:
gtFile = gt[resFile] # rrc_evaluation_funcs.decode_utf8(gt[resFile])
recall = 0
precision = 0
hmean = 0
detMatched = 0
iouMat = np.empty([1, 1])
gtPols = []
detPols = []
gtPolPoints = []
detPolPoints = []
# Array of Ground Truth Polygons' keys marked as don't Care
gtDontCarePolsNum = []
# Array of Detected Polygons' matched with a don't Care GT
detDontCarePolsNum = []
pairs = []
detMatchedNums = []
arrSampleConfidences = []
arrSampleMatch = []
sampleAP = 0
evaluationLog = ""
(
pointsList,
_,
transcriptionsList,
) = rrc_evaluation_funcs.get_tl_line_values_from_file_contents(
gtFile, evaluationParams["CRLF"], evaluationParams["LTRB"], True, False
)
for n in range(len(pointsList)):
points = pointsList[n]
transcription = transcriptionsList[n]
dontCare = transcription == "###"
if evaluationParams["LTRB"]:
gtRect = Rectangle(*points)
gtPol = rectangle_to_polygon(gtRect)
else:
gtPol = polygon_from_points(points)
gtPols.append(gtPol)
gtPolPoints.append(points)
if dontCare:
gtDontCarePolsNum.append(len(gtPols) - 1)
evaluationLog += (
"GT polygons: "
+ str(len(gtPols))
+ (
" (" + str(len(gtDontCarePolsNum)) + " don't care)\n"
if len(gtDontCarePolsNum) > 0
else "\n"
)
)
if resFile in subm:
detFile = subm[resFile] # rrc_evaluation_funcs.decode_utf8(subm[resFile])
(
pointsList,
confidencesList,
_,
) = rrc_evaluation_funcs.get_tl_line_values_from_file_contents(
detFile,
evaluationParams["CRLF"],
evaluationParams["LTRB"],
False,
evaluationParams["CONFIDENCES"],
)
for n in range(len(pointsList)):
points = pointsList[n]
if evaluationParams["LTRB"]:
detRect = Rectangle(*points)
detPol = rectangle_to_polygon(detRect)
else:
detPol = polygon_from_points(points)
detPols.append(detPol)
detPolPoints.append(points)
if len(gtDontCarePolsNum) > 0:
for dontCarePol in gtDontCarePolsNum:
dontCarePol = gtPols[dontCarePol]
intersected_area = get_intersection(dontCarePol, detPol)
pdDimensions = detPol.area()
precision = (
0 if pdDimensions == 0 else intersected_area / pdDimensions
)
if precision > evaluationParams["AREA_PRECISION_CONSTRAINT"]:
detDontCarePolsNum.append(len(detPols) - 1)
break
evaluationLog += (
"DET polygons: "
+ str(len(detPols))
+ (
" (" + str(len(detDontCarePolsNum)) + " don't care)\n"
if len(detDontCarePolsNum) > 0
else "\n"
)
)
if len(gtPols) > 0 and len(detPols) > 0:
# Calculate IoU and precision matrixs
outputShape = [len(gtPols), len(detPols)]
iouMat = np.empty(outputShape)
gtRectMat = np.zeros(len(gtPols), np.int8)
detRectMat = np.zeros(len(detPols), np.int8)
for gtNum in range(len(gtPols)):
for detNum in range(len(detPols)):
pG = gtPols[gtNum]
pD = detPols[detNum]
iouMat[gtNum, detNum] = get_intersection_over_union(pD, pG)
for gtNum in range(len(gtPols)):
for detNum in range(len(detPols)):
if (
gtRectMat[gtNum] == 0
and detRectMat[detNum] == 0
and gtNum not in gtDontCarePolsNum
and detNum not in detDontCarePolsNum
):
if (
iouMat[gtNum, detNum]
> evaluationParams["IOU_CONSTRAINT"]
):
gtRectMat[gtNum] = 1
detRectMat[detNum] = 1
detMatched += 1
pairs.append({"gt": gtNum, "det": detNum})
detMatchedNums.append(detNum)
evaluationLog += (
"Match GT #"
+ str(gtNum)
+ " with Det #"
+ str(detNum)
+ "\n"
)
if evaluationParams["CONFIDENCES"]:
for detNum in range(len(detPols)):
if detNum not in detDontCarePolsNum:
# we exclude the don't care detections
match = detNum in detMatchedNums
arrSampleConfidences.append(confidencesList[detNum])
arrSampleMatch.append(match)
arrGlobalConfidences.append(confidencesList[detNum])
arrGlobalMatches.append(match)
numGtCare = len(gtPols) - len(gtDontCarePolsNum)
numDetCare = len(detPols) - len(detDontCarePolsNum)
if numGtCare == 0:
recall = float(1)
precision = float(0) if numDetCare > 0 else float(1)
sampleAP = precision
else:
recall = float(detMatched) / numGtCare
precision = 0 if numDetCare == 0 else float(detMatched) / numDetCare
if (
evaluationParams["CONFIDENCES"]
and evaluationParams["PER_SAMPLE_RESULTS"]
):
sampleAP = compute_ap(arrSampleConfidences, arrSampleMatch, numGtCare)
hmean = (
0
if (precision + recall) == 0
else 2.0 * precision * recall / (precision + recall)
)
matchedSum += detMatched
numGlobalCareGt += numGtCare
numGlobalCareDet += numDetCare
if evaluationParams["PER_SAMPLE_RESULTS"]:
perSampleMetrics[resFile] = {
"precision": precision,
"recall": recall,
"hmean": hmean,
"pairs": pairs,
"AP": sampleAP,
"iouMat": [] if len(detPols) > 100 else iouMat.tolist(),
"gtPolPoints": gtPolPoints,
"detPolPoints": detPolPoints,
"gtDontCare": gtDontCarePolsNum,
"detDontCare": detDontCarePolsNum,
"evaluationParams": evaluationParams,
"evaluationLog": evaluationLog,
}
# Compute MAP and MAR
AP = 0
if evaluationParams["CONFIDENCES"]:
AP = compute_ap(arrGlobalConfidences, arrGlobalMatches, numGlobalCareGt)
methodRecall = 0 if numGlobalCareGt == 0 else float(matchedSum) / numGlobalCareGt
methodPrecision = (
0 if numGlobalCareDet == 0 else float(matchedSum) / numGlobalCareDet
)
methodHmean = (
0
if methodRecall + methodPrecision == 0
else 2 * methodRecall * methodPrecision / (methodRecall + methodPrecision)
)
methodMetrics = {
"precision": methodPrecision,
"recall": methodRecall,
"hmean": methodHmean,
"AP": AP,
}
resDict = {
"calculated": True,
"Message": "",
"method": methodMetrics,
"per_sample": perSampleMetrics,
}
return resDict
def cal_recall_precision_f1(gt_path, result_path, show_result=False):
p = {"g": gt_path, "s": result_path}
result = rrc_evaluation_funcs.main_evaluation(
p, default_evaluation_params, validate_data, evaluate_method, show_result
)
return result["method"]

View File

@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# @Time : 2019/12/7 14:46
# @Author : zhoujun
import numpy as np
import cv2
import os
import random
from tqdm import tqdm
# calculate means and std
train_txt_path = "./train_val_list.txt"
CNum = 10000 # 挑选多少图片进行计算
img_h, img_w = 640, 640
imgs = np.zeros([img_w, img_h, 3, 1])
means, stdevs = [], []
with open(train_txt_path, "r") as f:
lines = f.readlines()
random.shuffle(lines) # shuffle , 随机挑选图片
for i in tqdm(range(CNum)):
img_path = lines[i].split("\t")[0]
img = cv2.imread(img_path)
img = cv2.resize(img, (img_h, img_w))
img = img[:, :, :, np.newaxis]
imgs = np.concatenate((imgs, img), axis=3)
# print(i)
imgs = imgs.astype(np.float32) / 255.0
for i in tqdm(range(3)):
pixels = imgs[:, :, i, :].ravel() # 拉成一行
means.append(np.mean(pixels))
stdevs.append(np.std(pixels))
# cv2 读取的图像格式为BGRPIL/Skimage读取到的都是RGB不用转
means.reverse() # BGR --> RGB
stdevs.reverse()
print("normMean = {}".format(means))
print("normStd = {}".format(stdevs))
print("transforms.Normalize(normMean = {}, normStd = {})".format(means, stdevs))

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# @Time : 2019/8/24 12:06
# @Author : zhoujun
import os
import glob
import pathlib
data_path = r"test"
# data_path/img 存放图片
# data_path/gt 存放标签文件
f_w = open(os.path.join(data_path, "test.txt"), "w", encoding="utf8")
for img_path in glob.glob(data_path + "/img/*.jpg", recursive=True):
d = pathlib.Path(img_path)
label_path = os.path.join(data_path, "gt", ("gt_" + str(d.stem) + ".txt"))
if os.path.exists(img_path) and os.path.exists(label_path):
print(img_path, label_path)
else:
print("不存在", img_path, label_path)
f_w.write("{}\t{}\n".format(img_path, label_path))
f_w.close()

View File

@@ -0,0 +1,60 @@
# Adapted from score written by wkentaro
# https://github.com/wkentaro/pytorch-fcn/blob/master/torchfcn/utils.py
import numpy as np
class runningScore(object):
def __init__(self, n_classes):
self.n_classes = n_classes
self.confusion_matrix = np.zeros((n_classes, n_classes))
def _fast_hist(self, label_true, label_pred, n_class):
mask = (label_true >= 0) & (label_true < n_class)
if np.sum((label_pred[mask] < 0)) > 0:
print(label_pred[label_pred < 0])
hist = np.bincount(
n_class * label_true[mask].astype(int) + label_pred[mask],
minlength=n_class**2,
).reshape(n_class, n_class)
return hist
def update(self, label_trues, label_preds):
# print label_trues.dtype, label_preds.dtype
for lt, lp in zip(label_trues, label_preds):
try:
self.confusion_matrix += self._fast_hist(
lt.flatten(), lp.flatten(), self.n_classes
)
except:
pass
def get_scores(self):
"""Returns accuracy score evaluation result.
- overall accuracy
- mean accuracy
- mean IU
- fwavacc
"""
hist = self.confusion_matrix
acc = np.diag(hist).sum() / (hist.sum() + 0.0001)
acc_cls = np.diag(hist) / (hist.sum(axis=1) + 0.0001)
acc_cls = np.nanmean(acc_cls)
iu = np.diag(hist) / (
hist.sum(axis=1) + hist.sum(axis=0) - np.diag(hist) + 0.0001
)
mean_iu = np.nanmean(iu)
freq = hist.sum(axis=1) / (hist.sum() + 0.0001)
fwavacc = (freq[freq > 0] * iu[freq > 0]).sum()
cls_iu = dict(zip(range(self.n_classes), iu))
return {
"Overall Acc": acc,
"Mean Acc": acc_cls,
"FreqW Acc": fwavacc,
"Mean IoU": mean_iu,
}, cls_iu
def reset(self):
self.confusion_matrix = np.zeros((self.n_classes, self.n_classes))

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# @Time : 2019/12/5 15:36
# @Author : zhoujun
from .icdar2015 import QuadMetric
def get_metric(config):
try:
if "args" not in config:
args = {}
else:
args = config["args"]
if isinstance(args, dict):
cls = eval(config["type"])(**args)
else:
cls = eval(config["type"])(args)
return cls
except:
return None

View File

@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# @Time : 2019/12/5 15:36
# @Author : zhoujun
from .quad_metric import QuadMetric

View File

@@ -0,0 +1,474 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import math
from collections import namedtuple
import numpy as np
from shapely.geometry import Polygon
class DetectionDetEvalEvaluator(object):
def __init__(
self,
area_recall_constraint=0.8,
area_precision_constraint=0.4,
ev_param_ind_center_diff_thr=1,
mtype_oo_o=1.0,
mtype_om_o=0.8,
mtype_om_m=1.0,
):
self.area_recall_constraint = area_recall_constraint
self.area_precision_constraint = area_precision_constraint
self.ev_param_ind_center_diff_thr = ev_param_ind_center_diff_thr
self.mtype_oo_o = mtype_oo_o
self.mtype_om_o = mtype_om_o
self.mtype_om_m = mtype_om_m
def evaluate_image(self, gt, pred):
def get_union(pD, pG):
return Polygon(pD).union(Polygon(pG)).area
def get_intersection_over_union(pD, pG):
return get_intersection(pD, pG) / get_union(pD, pG)
def get_intersection(pD, pG):
return Polygon(pD).intersection(Polygon(pG)).area
def one_to_one_match(row, col):
cont = 0
for j in range(len(recallMat[0])):
if (
recallMat[row, j] >= self.area_recall_constraint
and precisionMat[row, j] >= self.area_precision_constraint
):
cont = cont + 1
if cont != 1:
return False
cont = 0
for i in range(len(recallMat)):
if (
recallMat[i, col] >= self.area_recall_constraint
and precisionMat[i, col] >= self.area_precision_constraint
):
cont = cont + 1
if cont != 1:
return False
if (
recallMat[row, col] >= self.area_recall_constraint
and precisionMat[row, col] >= self.area_precision_constraint
):
return True
return False
def num_overlaps_gt(gtNum):
cont = 0
for detNum in range(len(detRects)):
if detNum not in detDontCareRectsNum:
if recallMat[gtNum, detNum] > 0:
cont = cont + 1
return cont
def num_overlaps_det(detNum):
cont = 0
for gtNum in range(len(recallMat)):
if gtNum not in gtDontCareRectsNum:
if recallMat[gtNum, detNum] > 0:
cont = cont + 1
return cont
def is_single_overlap(row, col):
if num_overlaps_gt(row) == 1 and num_overlaps_det(col) == 1:
return True
else:
return False
def one_to_many_match(gtNum):
many_sum = 0
detRects = []
for detNum in range(len(recallMat[0])):
if (
gtRectMat[gtNum] == 0
and detRectMat[detNum] == 0
and detNum not in detDontCareRectsNum
):
if precisionMat[gtNum, detNum] >= self.area_precision_constraint:
many_sum += recallMat[gtNum, detNum]
detRects.append(detNum)
if round(many_sum, 4) >= self.area_recall_constraint:
return True, detRects
else:
return False, []
def many_to_one_match(detNum):
many_sum = 0
gtRects = []
for gtNum in range(len(recallMat)):
if (
gtRectMat[gtNum] == 0
and detRectMat[detNum] == 0
and gtNum not in gtDontCareRectsNum
):
if recallMat[gtNum, detNum] >= self.area_recall_constraint:
many_sum += precisionMat[gtNum, detNum]
gtRects.append(gtNum)
if round(many_sum, 4) >= self.area_precision_constraint:
return True, gtRects
else:
return False, []
def center_distance(r1, r2):
return ((np.mean(r1, axis=0) - np.mean(r2, axis=0)) ** 2).sum() ** 0.5
def diag(r):
r = np.array(r)
return (
(r[:, 0].max() - r[:, 0].min()) ** 2
+ (r[:, 1].max() - r[:, 1].min()) ** 2
) ** 0.5
perSampleMetrics = {}
recall = 0
precision = 0
hmean = 0
recallAccum = 0.0
precisionAccum = 0.0
gtRects = []
detRects = []
gtPolPoints = []
detPolPoints = []
gtDontCareRectsNum = (
[]
) # Array of Ground Truth Rectangles' keys marked as don't Care
detDontCareRectsNum = (
[]
) # Array of Detected Rectangles' matched with a don't Care GT
pairs = []
evaluationLog = ""
recallMat = np.empty([1, 1])
precisionMat = np.empty([1, 1])
for n in range(len(gt)):
points = gt[n]["points"]
# transcription = gt[n]['text']
dontCare = gt[n]["ignore"]
if not Polygon(points).is_valid or not Polygon(points).is_simple:
continue
gtRects.append(points)
gtPolPoints.append(points)
if dontCare:
gtDontCareRectsNum.append(len(gtRects) - 1)
evaluationLog += (
"GT rectangles: "
+ str(len(gtRects))
+ (
" (" + str(len(gtDontCareRectsNum)) + " don't care)\n"
if len(gtDontCareRectsNum) > 0
else "\n"
)
)
for n in range(len(pred)):
points = pred[n]["points"]
if not Polygon(points).is_valid or not Polygon(points).is_simple:
continue
detRect = points
detRects.append(detRect)
detPolPoints.append(points)
if len(gtDontCareRectsNum) > 0:
for dontCareRectNum in gtDontCareRectsNum:
dontCareRect = gtRects[dontCareRectNum]
intersected_area = get_intersection(dontCareRect, detRect)
rdDimensions = Polygon(detRect).area
if rdDimensions == 0:
precision = 0
else:
precision = intersected_area / rdDimensions
if precision > self.area_precision_constraint:
detDontCareRectsNum.append(len(detRects) - 1)
break
evaluationLog += (
"DET rectangles: "
+ str(len(detRects))
+ (
" (" + str(len(detDontCareRectsNum)) + " don't care)\n"
if len(detDontCareRectsNum) > 0
else "\n"
)
)
if len(gtRects) == 0:
recall = 1
precision = 0 if len(detRects) > 0 else 1
if len(detRects) > 0:
# Calculate recall and precision matrixes
outputShape = [len(gtRects), len(detRects)]
recallMat = np.empty(outputShape)
precisionMat = np.empty(outputShape)
gtRectMat = np.zeros(len(gtRects), np.int8)
detRectMat = np.zeros(len(detRects), np.int8)
for gtNum in range(len(gtRects)):
for detNum in range(len(detRects)):
rG = gtRects[gtNum]
rD = detRects[detNum]
intersected_area = get_intersection(rG, rD)
rgDimensions = Polygon(rG).area
rdDimensions = Polygon(rD).area
recallMat[gtNum, detNum] = (
0 if rgDimensions == 0 else intersected_area / rgDimensions
)
precisionMat[gtNum, detNum] = (
0 if rdDimensions == 0 else intersected_area / rdDimensions
)
# Find one-to-one matches
evaluationLog += "Find one-to-one matches\n"
for gtNum in range(len(gtRects)):
for detNum in range(len(detRects)):
if (
gtRectMat[gtNum] == 0
and detRectMat[detNum] == 0
and gtNum not in gtDontCareRectsNum
and detNum not in detDontCareRectsNum
):
match = one_to_one_match(gtNum, detNum)
if match is True:
# in deteval we have to make other validation before mark as one-to-one
if is_single_overlap(gtNum, detNum) is True:
rG = gtRects[gtNum]
rD = detRects[detNum]
normDist = center_distance(rG, rD)
normDist /= diag(rG) + diag(rD)
normDist *= 2.0
if normDist < self.ev_param_ind_center_diff_thr:
gtRectMat[gtNum] = 1
detRectMat[detNum] = 1
recallAccum += self.mtype_oo_o
precisionAccum += self.mtype_oo_o
pairs.append(
{"gt": gtNum, "det": detNum, "type": "OO"}
)
evaluationLog += (
"Match GT #"
+ str(gtNum)
+ " with Det #"
+ str(detNum)
+ "\n"
)
else:
evaluationLog += (
"Match Discarded GT #"
+ str(gtNum)
+ " with Det #"
+ str(detNum)
+ " normDist: "
+ str(normDist)
+ " \n"
)
else:
evaluationLog += (
"Match Discarded GT #"
+ str(gtNum)
+ " with Det #"
+ str(detNum)
+ " not single overlap\n"
)
# Find one-to-many matches
evaluationLog += "Find one-to-many matches\n"
for gtNum in range(len(gtRects)):
if gtNum not in gtDontCareRectsNum:
match, matchesDet = one_to_many_match(gtNum)
if match is True:
evaluationLog += "num_overlaps_gt=" + str(
num_overlaps_gt(gtNum)
)
# in deteval we have to make other validation before mark as one-to-one
if num_overlaps_gt(gtNum) >= 2:
gtRectMat[gtNum] = 1
recallAccum += (
self.mtype_oo_o
if len(matchesDet) == 1
else self.mtype_om_o
)
precisionAccum += (
self.mtype_oo_o
if len(matchesDet) == 1
else self.mtype_om_o * len(matchesDet)
)
pairs.append(
{
"gt": gtNum,
"det": matchesDet,
"type": "OO" if len(matchesDet) == 1 else "OM",
}
)
for detNum in matchesDet:
detRectMat[detNum] = 1
evaluationLog += (
"Match GT #"
+ str(gtNum)
+ " with Det #"
+ str(matchesDet)
+ "\n"
)
else:
evaluationLog += (
"Match Discarded GT #"
+ str(gtNum)
+ " with Det #"
+ str(matchesDet)
+ " not single overlap\n"
)
# Find many-to-one matches
evaluationLog += "Find many-to-one matches\n"
for detNum in range(len(detRects)):
if detNum not in detDontCareRectsNum:
match, matchesGt = many_to_one_match(detNum)
if match is True:
# in deteval we have to make other validation before mark as one-to-one
if num_overlaps_det(detNum) >= 2:
detRectMat[detNum] = 1
recallAccum += (
self.mtype_oo_o
if len(matchesGt) == 1
else self.mtype_om_m * len(matchesGt)
)
precisionAccum += (
self.mtype_oo_o
if len(matchesGt) == 1
else self.mtype_om_m
)
pairs.append(
{
"gt": matchesGt,
"det": detNum,
"type": "OO" if len(matchesGt) == 1 else "MO",
}
)
for gtNum in matchesGt:
gtRectMat[gtNum] = 1
evaluationLog += (
"Match GT #"
+ str(matchesGt)
+ " with Det #"
+ str(detNum)
+ "\n"
)
else:
evaluationLog += (
"Match Discarded GT #"
+ str(matchesGt)
+ " with Det #"
+ str(detNum)
+ " not single overlap\n"
)
numGtCare = len(gtRects) - len(gtDontCareRectsNum)
if numGtCare == 0:
recall = float(1)
precision = float(0) if len(detRects) > 0 else float(1)
else:
recall = float(recallAccum) / numGtCare
precision = (
float(0)
if (len(detRects) - len(detDontCareRectsNum)) == 0
else float(precisionAccum)
/ (len(detRects) - len(detDontCareRectsNum))
)
hmean = (
0
if (precision + recall) == 0
else 2.0 * precision * recall / (precision + recall)
)
numGtCare = len(gtRects) - len(gtDontCareRectsNum)
numDetCare = len(detRects) - len(detDontCareRectsNum)
perSampleMetrics = {
"precision": precision,
"recall": recall,
"hmean": hmean,
"pairs": pairs,
"recallMat": [] if len(detRects) > 100 else recallMat.tolist(),
"precisionMat": [] if len(detRects) > 100 else precisionMat.tolist(),
"gtPolPoints": gtPolPoints,
"detPolPoints": detPolPoints,
"gtCare": numGtCare,
"detCare": numDetCare,
"gtDontCare": gtDontCareRectsNum,
"detDontCare": detDontCareRectsNum,
"recallAccum": recallAccum,
"precisionAccum": precisionAccum,
"evaluationLog": evaluationLog,
}
return perSampleMetrics
def combine_results(self, results):
numGt = 0
numDet = 0
methodRecallSum = 0
methodPrecisionSum = 0
for result in results:
numGt += result["gtCare"]
numDet += result["detCare"]
methodRecallSum += result["recallAccum"]
methodPrecisionSum += result["precisionAccum"]
methodRecall = 0 if numGt == 0 else methodRecallSum / numGt
methodPrecision = 0 if numDet == 0 else methodPrecisionSum / numDet
methodHmean = (
0
if methodRecall + methodPrecision == 0
else 2 * methodRecall * methodPrecision / (methodRecall + methodPrecision)
)
methodMetrics = {
"precision": methodPrecision,
"recall": methodRecall,
"hmean": methodHmean,
}
return methodMetrics
if __name__ == "__main__":
evaluator = DetectionDetEvalEvaluator()
gts = [
[
{
"points": [(0, 0), (1, 0), (1, 1), (0, 1)],
"text": 1234,
"ignore": False,
},
{
"points": [(2, 2), (3, 2), (3, 3), (2, 3)],
"text": 5678,
"ignore": True,
},
]
]
preds = [
[
{
"points": [(0.1, 0.1), (1, 0), (1, 1), (0, 1)],
"text": 123,
"ignore": False,
}
]
]
results = []
for gt, pred in zip(gts, preds):
results.append(evaluator.evaluate_image(gt, pred))
metrics = evaluator.combine_results(results)
print(metrics)

View File

@@ -0,0 +1,417 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import math
from collections import namedtuple
import numpy as np
from shapely.geometry import Polygon
class DetectionICDAR2013Evaluator(object):
def __init__(
self,
area_recall_constraint=0.8,
area_precision_constraint=0.4,
ev_param_ind_center_diff_thr=1,
mtype_oo_o=1.0,
mtype_om_o=0.8,
mtype_om_m=1.0,
):
self.area_recall_constraint = area_recall_constraint
self.area_precision_constraint = area_precision_constraint
self.ev_param_ind_center_diff_thr = ev_param_ind_center_diff_thr
self.mtype_oo_o = mtype_oo_o
self.mtype_om_o = mtype_om_o
self.mtype_om_m = mtype_om_m
def evaluate_image(self, gt, pred):
def get_union(pD, pG):
return Polygon(pD).union(Polygon(pG)).area
def get_intersection_over_union(pD, pG):
return get_intersection(pD, pG) / get_union(pD, pG)
def get_intersection(pD, pG):
return Polygon(pD).intersection(Polygon(pG)).area
def one_to_one_match(row, col):
cont = 0
for j in range(len(recallMat[0])):
if (
recallMat[row, j] >= self.area_recall_constraint
and precisionMat[row, j] >= self.area_precision_constraint
):
cont = cont + 1
if cont != 1:
return False
cont = 0
for i in range(len(recallMat)):
if (
recallMat[i, col] >= self.area_recall_constraint
and precisionMat[i, col] >= self.area_precision_constraint
):
cont = cont + 1
if cont != 1:
return False
if (
recallMat[row, col] >= self.area_recall_constraint
and precisionMat[row, col] >= self.area_precision_constraint
):
return True
return False
def one_to_many_match(gtNum):
many_sum = 0
detRects = []
for detNum in range(len(recallMat[0])):
if (
gtRectMat[gtNum] == 0
and detRectMat[detNum] == 0
and detNum not in detDontCareRectsNum
):
if precisionMat[gtNum, detNum] >= self.area_precision_constraint:
many_sum += recallMat[gtNum, detNum]
detRects.append(detNum)
if round(many_sum, 4) >= self.area_recall_constraint:
return True, detRects
else:
return False, []
def many_to_one_match(detNum):
many_sum = 0
gtRects = []
for gtNum in range(len(recallMat)):
if (
gtRectMat[gtNum] == 0
and detRectMat[detNum] == 0
and gtNum not in gtDontCareRectsNum
):
if recallMat[gtNum, detNum] >= self.area_recall_constraint:
many_sum += precisionMat[gtNum, detNum]
gtRects.append(gtNum)
if round(many_sum, 4) >= self.area_precision_constraint:
return True, gtRects
else:
return False, []
def center_distance(r1, r2):
return ((np.mean(r1, axis=0) - np.mean(r2, axis=0)) ** 2).sum() ** 0.5
def diag(r):
r = np.array(r)
return (
(r[:, 0].max() - r[:, 0].min()) ** 2
+ (r[:, 1].max() - r[:, 1].min()) ** 2
) ** 0.5
perSampleMetrics = {}
recall = 0
precision = 0
hmean = 0
recallAccum = 0.0
precisionAccum = 0.0
gtRects = []
detRects = []
gtPolPoints = []
detPolPoints = []
gtDontCareRectsNum = (
[]
) # Array of Ground Truth Rectangles' keys marked as don't Care
detDontCareRectsNum = (
[]
) # Array of Detected Rectangles' matched with a don't Care GT
pairs = []
evaluationLog = ""
recallMat = np.empty([1, 1])
precisionMat = np.empty([1, 1])
for n in range(len(gt)):
points = gt[n]["points"]
# transcription = gt[n]['text']
dontCare = gt[n]["ignore"]
if not Polygon(points).is_valid or not Polygon(points).is_simple:
continue
gtRects.append(points)
gtPolPoints.append(points)
if dontCare:
gtDontCareRectsNum.append(len(gtRects) - 1)
evaluationLog += (
"GT rectangles: "
+ str(len(gtRects))
+ (
" (" + str(len(gtDontCareRectsNum)) + " don't care)\n"
if len(gtDontCareRectsNum) > 0
else "\n"
)
)
for n in range(len(pred)):
points = pred[n]["points"]
if not Polygon(points).is_valid or not Polygon(points).is_simple:
continue
detRect = points
detRects.append(detRect)
detPolPoints.append(points)
if len(gtDontCareRectsNum) > 0:
for dontCareRectNum in gtDontCareRectsNum:
dontCareRect = gtRects[dontCareRectNum]
intersected_area = get_intersection(dontCareRect, detRect)
rdDimensions = Polygon(detRect).area
if rdDimensions == 0:
precision = 0
else:
precision = intersected_area / rdDimensions
if precision > self.area_precision_constraint:
detDontCareRectsNum.append(len(detRects) - 1)
break
evaluationLog += (
"DET rectangles: "
+ str(len(detRects))
+ (
" (" + str(len(detDontCareRectsNum)) + " don't care)\n"
if len(detDontCareRectsNum) > 0
else "\n"
)
)
if len(gtRects) == 0:
recall = 1
precision = 0 if len(detRects) > 0 else 1
if len(detRects) > 0:
# Calculate recall and precision matrixes
outputShape = [len(gtRects), len(detRects)]
recallMat = np.empty(outputShape)
precisionMat = np.empty(outputShape)
gtRectMat = np.zeros(len(gtRects), np.int8)
detRectMat = np.zeros(len(detRects), np.int8)
for gtNum in range(len(gtRects)):
for detNum in range(len(detRects)):
rG = gtRects[gtNum]
rD = detRects[detNum]
intersected_area = get_intersection(rG, rD)
rgDimensions = Polygon(rG).area
rdDimensions = Polygon(rD).area
recallMat[gtNum, detNum] = (
0 if rgDimensions == 0 else intersected_area / rgDimensions
)
precisionMat[gtNum, detNum] = (
0 if rdDimensions == 0 else intersected_area / rdDimensions
)
# Find one-to-one matches
evaluationLog += "Find one-to-one matches\n"
for gtNum in range(len(gtRects)):
for detNum in range(len(detRects)):
if (
gtRectMat[gtNum] == 0
and detRectMat[detNum] == 0
and gtNum not in gtDontCareRectsNum
and detNum not in detDontCareRectsNum
):
match = one_to_one_match(gtNum, detNum)
if match is True:
# in deteval we have to make other validation before mark as one-to-one
rG = gtRects[gtNum]
rD = detRects[detNum]
normDist = center_distance(rG, rD)
normDist /= diag(rG) + diag(rD)
normDist *= 2.0
if normDist < self.ev_param_ind_center_diff_thr:
gtRectMat[gtNum] = 1
detRectMat[detNum] = 1
recallAccum += self.mtype_oo_o
precisionAccum += self.mtype_oo_o
pairs.append({"gt": gtNum, "det": detNum, "type": "OO"})
evaluationLog += (
"Match GT #"
+ str(gtNum)
+ " with Det #"
+ str(detNum)
+ "\n"
)
else:
evaluationLog += (
"Match Discarded GT #"
+ str(gtNum)
+ " with Det #"
+ str(detNum)
+ " normDist: "
+ str(normDist)
+ " \n"
)
# Find one-to-many matches
evaluationLog += "Find one-to-many matches\n"
for gtNum in range(len(gtRects)):
if gtNum not in gtDontCareRectsNum:
match, matchesDet = one_to_many_match(gtNum)
if match is True:
evaluationLog += "num_overlaps_gt=" + str(
num_overlaps_gt(gtNum)
)
gtRectMat[gtNum] = 1
recallAccum += (
self.mtype_oo_o if len(matchesDet) == 1 else self.mtype_om_o
)
precisionAccum += (
self.mtype_oo_o
if len(matchesDet) == 1
else self.mtype_om_o * len(matchesDet)
)
pairs.append(
{
"gt": gtNum,
"det": matchesDet,
"type": "OO" if len(matchesDet) == 1 else "OM",
}
)
for detNum in matchesDet:
detRectMat[detNum] = 1
evaluationLog += (
"Match GT #"
+ str(gtNum)
+ " with Det #"
+ str(matchesDet)
+ "\n"
)
# Find many-to-one matches
evaluationLog += "Find many-to-one matches\n"
for detNum in range(len(detRects)):
if detNum not in detDontCareRectsNum:
match, matchesGt = many_to_one_match(detNum)
if match is True:
detRectMat[detNum] = 1
recallAccum += (
self.mtype_oo_o
if len(matchesGt) == 1
else self.mtype_om_m * len(matchesGt)
)
precisionAccum += (
self.mtype_oo_o if len(matchesGt) == 1 else self.mtype_om_m
)
pairs.append(
{
"gt": matchesGt,
"det": detNum,
"type": "OO" if len(matchesGt) == 1 else "MO",
}
)
for gtNum in matchesGt:
gtRectMat[gtNum] = 1
evaluationLog += (
"Match GT #"
+ str(matchesGt)
+ " with Det #"
+ str(detNum)
+ "\n"
)
numGtCare = len(gtRects) - len(gtDontCareRectsNum)
if numGtCare == 0:
recall = float(1)
precision = float(0) if len(detRects) > 0 else float(1)
else:
recall = float(recallAccum) / numGtCare
precision = (
float(0)
if (len(detRects) - len(detDontCareRectsNum)) == 0
else float(precisionAccum)
/ (len(detRects) - len(detDontCareRectsNum))
)
hmean = (
0
if (precision + recall) == 0
else 2.0 * precision * recall / (precision + recall)
)
numGtCare = len(gtRects) - len(gtDontCareRectsNum)
numDetCare = len(detRects) - len(detDontCareRectsNum)
perSampleMetrics = {
"precision": precision,
"recall": recall,
"hmean": hmean,
"pairs": pairs,
"recallMat": [] if len(detRects) > 100 else recallMat.tolist(),
"precisionMat": [] if len(detRects) > 100 else precisionMat.tolist(),
"gtPolPoints": gtPolPoints,
"detPolPoints": detPolPoints,
"gtCare": numGtCare,
"detCare": numDetCare,
"gtDontCare": gtDontCareRectsNum,
"detDontCare": detDontCareRectsNum,
"recallAccum": recallAccum,
"precisionAccum": precisionAccum,
"evaluationLog": evaluationLog,
}
return perSampleMetrics
def combine_results(self, results):
numGt = 0
numDet = 0
methodRecallSum = 0
methodPrecisionSum = 0
for result in results:
numGt += result["gtCare"]
numDet += result["detCare"]
methodRecallSum += result["recallAccum"]
methodPrecisionSum += result["precisionAccum"]
methodRecall = 0 if numGt == 0 else methodRecallSum / numGt
methodPrecision = 0 if numDet == 0 else methodPrecisionSum / numDet
methodHmean = (
0
if methodRecall + methodPrecision == 0
else 2 * methodRecall * methodPrecision / (methodRecall + methodPrecision)
)
methodMetrics = {
"precision": methodPrecision,
"recall": methodRecall,
"hmean": methodHmean,
}
return methodMetrics
if __name__ == "__main__":
evaluator = DetectionICDAR2013Evaluator()
gts = [
[
{
"points": [(0, 0), (1, 0), (1, 1), (0, 1)],
"text": 1234,
"ignore": False,
},
{
"points": [(2, 2), (3, 2), (3, 3), (2, 3)],
"text": 5678,
"ignore": True,
},
]
]
preds = [
[
{
"points": [(0.1, 0.1), (1, 0), (1, 1), (0, 1)],
"text": 123,
"ignore": False,
}
]
]
results = []
for gt, pred in zip(gts, preds):
results.append(evaluator.evaluate_image(gt, pred))
metrics = evaluator.combine_results(results)
print(metrics)

View File

@@ -0,0 +1,300 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from collections import namedtuple
import numpy as np
from shapely.geometry import Polygon
import cv2
def iou_rotate(box_a, box_b, method="union"):
rect_a = cv2.minAreaRect(box_a)
rect_b = cv2.minAreaRect(box_b)
r1 = cv2.rotatedRectangleIntersection(rect_a, rect_b)
if r1[0] == 0:
return 0
else:
inter_area = cv2.contourArea(r1[1])
area_a = cv2.contourArea(box_a)
area_b = cv2.contourArea(box_b)
union_area = area_a + area_b - inter_area
if union_area == 0 or inter_area == 0:
return 0
if method == "union":
iou = inter_area / union_area
elif method == "intersection":
iou = inter_area / min(area_a, area_b)
else:
raise NotImplementedError
return iou
class DetectionIoUEvaluator(object):
def __init__(
self, is_output_polygon=False, iou_constraint=0.5, area_precision_constraint=0.5
):
self.is_output_polygon = is_output_polygon
self.iou_constraint = iou_constraint
self.area_precision_constraint = area_precision_constraint
def evaluate_image(self, gt, pred):
def get_union(pD, pG):
return Polygon(pD).union(Polygon(pG)).area
def get_intersection_over_union(pD, pG):
return get_intersection(pD, pG) / get_union(pD, pG)
def get_intersection(pD, pG):
return Polygon(pD).intersection(Polygon(pG)).area
def compute_ap(confList, matchList, numGtCare):
correct = 0
AP = 0
if len(confList) > 0:
confList = np.array(confList)
matchList = np.array(matchList)
sorted_ind = np.argsort(-confList)
confList = confList[sorted_ind]
matchList = matchList[sorted_ind]
for n in range(len(confList)):
match = matchList[n]
if match:
correct += 1
AP += float(correct) / (n + 1)
if numGtCare > 0:
AP /= numGtCare
return AP
perSampleMetrics = {}
matchedSum = 0
Rectangle = namedtuple("Rectangle", "xmin ymin xmax ymax")
numGlobalCareGt = 0
numGlobalCareDet = 0
arrGlobalConfidences = []
arrGlobalMatches = []
recall = 0
precision = 0
hmean = 0
detMatched = 0
iouMat = np.empty([1, 1])
gtPols = []
detPols = []
gtPolPoints = []
detPolPoints = []
# Array of Ground Truth Polygons' keys marked as don't Care
gtDontCarePolsNum = []
# Array of Detected Polygons' matched with a don't Care GT
detDontCarePolsNum = []
pairs = []
detMatchedNums = []
arrSampleConfidences = []
arrSampleMatch = []
evaluationLog = ""
for n in range(len(gt)):
points = gt[n]["points"]
# transcription = gt[n]['text']
dontCare = gt[n]["ignore"]
if not Polygon(points).is_valid or not Polygon(points).is_simple:
continue
gtPol = points
gtPols.append(gtPol)
gtPolPoints.append(points)
if dontCare:
gtDontCarePolsNum.append(len(gtPols) - 1)
evaluationLog += (
"GT polygons: "
+ str(len(gtPols))
+ (
" (" + str(len(gtDontCarePolsNum)) + " don't care)\n"
if len(gtDontCarePolsNum) > 0
else "\n"
)
)
for n in range(len(pred)):
points = pred[n]["points"]
if not Polygon(points).is_valid or not Polygon(points).is_simple:
continue
detPol = points
detPols.append(detPol)
detPolPoints.append(points)
if len(gtDontCarePolsNum) > 0:
for dontCarePol in gtDontCarePolsNum:
dontCarePol = gtPols[dontCarePol]
intersected_area = get_intersection(dontCarePol, detPol)
pdDimensions = Polygon(detPol).area
precision = (
0 if pdDimensions == 0 else intersected_area / pdDimensions
)
if precision > self.area_precision_constraint:
detDontCarePolsNum.append(len(detPols) - 1)
break
evaluationLog += (
"DET polygons: "
+ str(len(detPols))
+ (
" (" + str(len(detDontCarePolsNum)) + " don't care)\n"
if len(detDontCarePolsNum) > 0
else "\n"
)
)
if len(gtPols) > 0 and len(detPols) > 0:
# Calculate IoU and precision matrixs
outputShape = [len(gtPols), len(detPols)]
iouMat = np.empty(outputShape)
gtRectMat = np.zeros(len(gtPols), np.int8)
detRectMat = np.zeros(len(detPols), np.int8)
if self.is_output_polygon:
for gtNum in range(len(gtPols)):
for detNum in range(len(detPols)):
pG = gtPols[gtNum]
pD = detPols[detNum]
iouMat[gtNum, detNum] = get_intersection_over_union(pD, pG)
else:
# gtPols = np.float32(gtPols)
# detPols = np.float32(detPols)
for gtNum in range(len(gtPols)):
for detNum in range(len(detPols)):
pG = np.float32(gtPols[gtNum])
pD = np.float32(detPols[detNum])
iouMat[gtNum, detNum] = iou_rotate(pD, pG)
for gtNum in range(len(gtPols)):
for detNum in range(len(detPols)):
if (
gtRectMat[gtNum] == 0
and detRectMat[detNum] == 0
and gtNum not in gtDontCarePolsNum
and detNum not in detDontCarePolsNum
):
if iouMat[gtNum, detNum] > self.iou_constraint:
gtRectMat[gtNum] = 1
detRectMat[detNum] = 1
detMatched += 1
pairs.append({"gt": gtNum, "det": detNum})
detMatchedNums.append(detNum)
evaluationLog += (
"Match GT #"
+ str(gtNum)
+ " with Det #"
+ str(detNum)
+ "\n"
)
numGtCare = len(gtPols) - len(gtDontCarePolsNum)
numDetCare = len(detPols) - len(detDontCarePolsNum)
if numGtCare == 0:
recall = float(1)
precision = float(0) if numDetCare > 0 else float(1)
else:
recall = float(detMatched) / numGtCare
precision = 0 if numDetCare == 0 else float(detMatched) / numDetCare
hmean = (
0
if (precision + recall) == 0
else 2.0 * precision * recall / (precision + recall)
)
matchedSum += detMatched
numGlobalCareGt += numGtCare
numGlobalCareDet += numDetCare
perSampleMetrics = {
"precision": precision,
"recall": recall,
"hmean": hmean,
"pairs": pairs,
"iouMat": [] if len(detPols) > 100 else iouMat.tolist(),
"gtPolPoints": gtPolPoints,
"detPolPoints": detPolPoints,
"gtCare": numGtCare,
"detCare": numDetCare,
"gtDontCare": gtDontCarePolsNum,
"detDontCare": detDontCarePolsNum,
"detMatched": detMatched,
"evaluationLog": evaluationLog,
}
return perSampleMetrics
def combine_results(self, results):
numGlobalCareGt = 0
numGlobalCareDet = 0
matchedSum = 0
for result in results:
numGlobalCareGt += result["gtCare"]
numGlobalCareDet += result["detCare"]
matchedSum += result["detMatched"]
methodRecall = (
0 if numGlobalCareGt == 0 else float(matchedSum) / numGlobalCareGt
)
methodPrecision = (
0 if numGlobalCareDet == 0 else float(matchedSum) / numGlobalCareDet
)
methodHmean = (
0
if methodRecall + methodPrecision == 0
else 2 * methodRecall * methodPrecision / (methodRecall + methodPrecision)
)
methodMetrics = {
"precision": methodPrecision,
"recall": methodRecall,
"hmean": methodHmean,
}
return methodMetrics
if __name__ == "__main__":
evaluator = DetectionIoUEvaluator()
preds = [
[
{
"points": [(0.1, 0.1), (0.5, 0), (0.5, 1), (0, 1)],
"text": 1234,
"ignore": False,
},
{
"points": [(0.5, 0.1), (1, 0), (1, 1), (0.5, 1)],
"text": 5678,
"ignore": False,
},
]
]
gts = [
[
{
"points": [(0.1, 0.1), (1, 0), (1, 1), (0, 1)],
"text": 123,
"ignore": False,
}
]
]
results = []
for gt, pred in zip(gts, preds):
results.append(evaluator.evaluate_image(gt, pred))
metrics = evaluator.combine_results(results)
print(metrics)

View File

@@ -0,0 +1,398 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import math
from collections import namedtuple
import numpy as np
from shapely.geometry import Polygon
class DetectionMTWI2018Evaluator(object):
def __init__(
self,
area_recall_constraint=0.7,
area_precision_constraint=0.7,
ev_param_ind_center_diff_thr=1,
):
self.area_recall_constraint = area_recall_constraint
self.area_precision_constraint = area_precision_constraint
self.ev_param_ind_center_diff_thr = ev_param_ind_center_diff_thr
def evaluate_image(self, gt, pred):
def get_union(pD, pG):
return Polygon(pD).union(Polygon(pG)).area
def get_intersection_over_union(pD, pG):
return get_intersection(pD, pG) / get_union(pD, pG)
def get_intersection(pD, pG):
return Polygon(pD).intersection(Polygon(pG)).area
def one_to_one_match(row, col):
cont = 0
for j in range(len(recallMat[0])):
if (
recallMat[row, j] >= self.area_recall_constraint
and precisionMat[row, j] >= self.area_precision_constraint
):
cont = cont + 1
if cont != 1:
return False
cont = 0
for i in range(len(recallMat)):
if (
recallMat[i, col] >= self.area_recall_constraint
and precisionMat[i, col] >= self.area_precision_constraint
):
cont = cont + 1
if cont != 1:
return False
if (
recallMat[row, col] >= self.area_recall_constraint
and precisionMat[row, col] >= self.area_precision_constraint
):
return True
return False
def one_to_many_match(gtNum):
many_sum = 0
detRects = []
for detNum in range(len(recallMat[0])):
if (
gtRectMat[gtNum] == 0
and detRectMat[detNum] == 0
and detNum not in detDontCareRectsNum
):
if precisionMat[gtNum, detNum] >= self.area_precision_constraint:
many_sum += recallMat[gtNum, detNum]
detRects.append(detNum)
if round(many_sum, 4) >= self.area_recall_constraint:
return True, detRects
else:
return False, []
def many_to_one_match(detNum):
many_sum = 0
gtRects = []
for gtNum in range(len(recallMat)):
if (
gtRectMat[gtNum] == 0
and detRectMat[detNum] == 0
and gtNum not in gtDontCareRectsNum
):
if recallMat[gtNum, detNum] >= self.area_recall_constraint:
many_sum += precisionMat[gtNum, detNum]
gtRects.append(gtNum)
if round(many_sum, 4) >= self.area_precision_constraint:
return True, gtRects
else:
return False, []
def center_distance(r1, r2):
return ((np.mean(r1, axis=0) - np.mean(r2, axis=0)) ** 2).sum() ** 0.5
def diag(r):
r = np.array(r)
return (
(r[:, 0].max() - r[:, 0].min()) ** 2
+ (r[:, 1].max() - r[:, 1].min()) ** 2
) ** 0.5
perSampleMetrics = {}
recall = 0
precision = 0
hmean = 0
recallAccum = 0.0
precisionAccum = 0.0
gtRects = []
detRects = []
gtPolPoints = []
detPolPoints = []
gtDontCareRectsNum = (
[]
) # Array of Ground Truth Rectangles' keys marked as don't Care
detDontCareRectsNum = (
[]
) # Array of Detected Rectangles' matched with a don't Care GT
pairs = []
evaluationLog = ""
recallMat = np.empty([1, 1])
precisionMat = np.empty([1, 1])
for n in range(len(gt)):
points = gt[n]["points"]
# transcription = gt[n]['text']
dontCare = gt[n]["ignore"]
if not Polygon(points).is_valid or not Polygon(points).is_simple:
continue
gtRects.append(points)
gtPolPoints.append(points)
if dontCare:
gtDontCareRectsNum.append(len(gtRects) - 1)
evaluationLog += (
"GT rectangles: "
+ str(len(gtRects))
+ (
" (" + str(len(gtDontCareRectsNum)) + " don't care)\n"
if len(gtDontCareRectsNum) > 0
else "\n"
)
)
for n in range(len(pred)):
points = pred[n]["points"]
if not Polygon(points).is_valid or not Polygon(points).is_simple:
continue
detRect = points
detRects.append(detRect)
detPolPoints.append(points)
if len(gtDontCareRectsNum) > 0:
for dontCareRectNum in gtDontCareRectsNum:
dontCareRect = gtRects[dontCareRectNum]
intersected_area = get_intersection(dontCareRect, detRect)
rdDimensions = Polygon(detRect).area
if rdDimensions == 0:
precision = 0
else:
precision = intersected_area / rdDimensions
if precision > 0.5:
detDontCareRectsNum.append(len(detRects) - 1)
break
evaluationLog += (
"DET rectangles: "
+ str(len(detRects))
+ (
" (" + str(len(detDontCareRectsNum)) + " don't care)\n"
if len(detDontCareRectsNum) > 0
else "\n"
)
)
if len(gtRects) == 0:
recall = 1
precision = 0 if len(detRects) > 0 else 1
if len(detRects) > 0:
# Calculate recall and precision matrixs
outputShape = [len(gtRects), len(detRects)]
recallMat = np.empty(outputShape)
precisionMat = np.empty(outputShape)
gtRectMat = np.zeros(len(gtRects), np.int8)
detRectMat = np.zeros(len(detRects), np.int8)
for gtNum in range(len(gtRects)):
for detNum in range(len(detRects)):
rG = gtRects[gtNum]
rD = detRects[detNum]
intersected_area = get_intersection(rG, rD)
rgDimensions = Polygon(rG).area
rdDimensions = Polygon(rD).area
recallMat[gtNum, detNum] = (
0 if rgDimensions == 0 else intersected_area / rgDimensions
)
precisionMat[gtNum, detNum] = (
0 if rdDimensions == 0 else intersected_area / rdDimensions
)
# Find one-to-one matches
evaluationLog += "Find one-to-one matches\n"
for gtNum in range(len(gtRects)):
for detNum in range(len(detRects)):
if (
gtRectMat[gtNum] == 0
and detRectMat[detNum] == 0
and gtNum not in gtDontCareRectsNum
and detNum not in detDontCareRectsNum
):
match = one_to_one_match(gtNum, detNum)
if match is True:
# in deteval we have to make other validation before mark as one-to-one
rG = gtRects[gtNum]
rD = detRects[detNum]
normDist = center_distance(rG, rD)
normDist /= diag(rG) + diag(rD)
normDist *= 2.0
if normDist < self.ev_param_ind_center_diff_thr:
gtRectMat[gtNum] = 1
detRectMat[detNum] = 1
recallAccum += 1.0
precisionAccum += 1.0
pairs.append({"gt": gtNum, "det": detNum, "type": "OO"})
evaluationLog += (
"Match GT #"
+ str(gtNum)
+ " with Det #"
+ str(detNum)
+ "\n"
)
else:
evaluationLog += (
"Match Discarded GT #"
+ str(gtNum)
+ " with Det #"
+ str(detNum)
+ " normDist: "
+ str(normDist)
+ " \n"
)
# Find one-to-many matches
evaluationLog += "Find one-to-many matches\n"
for gtNum in range(len(gtRects)):
if gtNum not in gtDontCareRectsNum:
match, matchesDet = one_to_many_match(gtNum)
if match is True:
gtRectMat[gtNum] = 1
recallAccum += 1.0
precisionAccum += len(matchesDet) / (
1 + math.log(len(matchesDet))
)
pairs.append(
{
"gt": gtNum,
"det": matchesDet,
"type": "OO" if len(matchesDet) == 1 else "OM",
}
)
for detNum in matchesDet:
detRectMat[detNum] = 1
evaluationLog += (
"Match GT #"
+ str(gtNum)
+ " with Det #"
+ str(matchesDet)
+ "\n"
)
# Find many-to-one matches
evaluationLog += "Find many-to-one matches\n"
for detNum in range(len(detRects)):
if detNum not in detDontCareRectsNum:
match, matchesGt = many_to_one_match(detNum)
if match is True:
detRectMat[detNum] = 1
recallAccum += len(matchesGt) / (1 + math.log(len(matchesGt)))
precisionAccum += 1.0
pairs.append(
{
"gt": matchesGt,
"det": detNum,
"type": "OO" if len(matchesGt) == 1 else "MO",
}
)
for gtNum in matchesGt:
gtRectMat[gtNum] = 1
evaluationLog += (
"Match GT #"
+ str(matchesGt)
+ " with Det #"
+ str(detNum)
+ "\n"
)
numGtCare = len(gtRects) - len(gtDontCareRectsNum)
if numGtCare == 0:
recall = float(1)
precision = float(0) if len(detRects) > 0 else float(1)
else:
recall = float(recallAccum) / numGtCare
precision = (
float(0)
if (len(detRects) - len(detDontCareRectsNum)) == 0
else float(precisionAccum)
/ (len(detRects) - len(detDontCareRectsNum))
)
hmean = (
0
if (precision + recall) == 0
else 2.0 * precision * recall / (precision + recall)
)
numGtCare = len(gtRects) - len(gtDontCareRectsNum)
numDetCare = len(detRects) - len(detDontCareRectsNum)
perSampleMetrics = {
"precision": precision,
"recall": recall,
"hmean": hmean,
"pairs": pairs,
"recallMat": [] if len(detRects) > 100 else recallMat.tolist(),
"precisionMat": [] if len(detRects) > 100 else precisionMat.tolist(),
"gtPolPoints": gtPolPoints,
"detPolPoints": detPolPoints,
"gtCare": numGtCare,
"detCare": numDetCare,
"gtDontCare": gtDontCareRectsNum,
"detDontCare": detDontCareRectsNum,
"recallAccum": recallAccum,
"precisionAccum": precisionAccum,
"evaluationLog": evaluationLog,
}
return perSampleMetrics
def combine_results(self, results):
numGt = 0
numDet = 0
methodRecallSum = 0
methodPrecisionSum = 0
for result in results:
numGt += result["gtCare"]
numDet += result["detCare"]
methodRecallSum += result["recallAccum"]
methodPrecisionSum += result["precisionAccum"]
methodRecall = 0 if numGt == 0 else methodRecallSum / numGt
methodPrecision = 0 if numDet == 0 else methodPrecisionSum / numDet
methodHmean = (
0
if methodRecall + methodPrecision == 0
else 2 * methodRecall * methodPrecision / (methodRecall + methodPrecision)
)
methodMetrics = {
"precision": methodPrecision,
"recall": methodRecall,
"hmean": methodHmean,
}
return methodMetrics
if __name__ == "__main__":
evaluator = DetectionICDAR2013Evaluator()
gts = [
[
{
"points": [(0, 0), (1, 0), (1, 1), (0, 1)],
"text": 1234,
"ignore": False,
},
{
"points": [(2, 2), (3, 2), (3, 3), (2, 3)],
"text": 5678,
"ignore": True,
},
]
]
preds = [
[
{
"points": [(0.1, 0.1), (1, 0), (1, 1), (0, 1)],
"text": 123,
"ignore": False,
}
]
]
results = []
for gt, pred in zip(gts, preds):
results.append(evaluator.evaluate_image(gt, pred))
metrics = evaluator.combine_results(results)
print(metrics)

View File

@@ -0,0 +1,100 @@
import numpy as np
from .detection.iou import DetectionIoUEvaluator
class AverageMeter(object):
"""Computes and stores the average and current value"""
def __init__(self):
self.reset()
def reset(self):
self.val = 0
self.avg = 0
self.sum = 0
self.count = 0
def update(self, val, n=1):
self.val = val
self.sum += val * n
self.count += n
self.avg = self.sum / self.count
return self
class QuadMetric:
def __init__(self, is_output_polygon=False):
self.is_output_polygon = is_output_polygon
self.evaluator = DetectionIoUEvaluator(is_output_polygon=is_output_polygon)
def measure(self, batch, output, box_thresh=0.6):
"""
batch: (image, polygons, ignore_tags
batch: a dict produced by dataloaders.
image: tensor of shape (N, C, H, W).
polygons: tensor of shape (N, K, 4, 2), the polygons of objective regions.
ignore_tags: tensor of shape (N, K), indicates whether a region is ignorable or not.
shape: the original shape of images.
filename: the original filenames of images.
output: (polygons, ...)
"""
results = []
gt_polyons_batch = batch["text_polys"]
ignore_tags_batch = batch["ignore_tags"]
pred_polygons_batch = np.array(output[0])
pred_scores_batch = np.array(output[1])
for polygons, pred_polygons, pred_scores, ignore_tags in zip(
gt_polyons_batch, pred_polygons_batch, pred_scores_batch, ignore_tags_batch
):
gt = [
dict(points=np.int64(polygons[i]), ignore=ignore_tags[i])
for i in range(len(polygons))
]
if self.is_output_polygon:
pred = [
dict(points=pred_polygons[i]) for i in range(len(pred_polygons))
]
else:
pred = []
# print(pred_polygons.shape)
for i in range(pred_polygons.shape[0]):
if pred_scores[i] >= box_thresh:
# print(pred_polygons[i,:,:].tolist())
pred.append(
dict(points=pred_polygons[i, :, :].astype(np.int32))
)
# pred = [dict(points=pred_polygons[i,:,:].tolist()) if pred_scores[i] >= box_thresh for i in range(pred_polygons.shape[0])]
results.append(self.evaluator.evaluate_image(gt, pred))
return results
def validate_measure(self, batch, output, box_thresh=0.6):
return self.measure(batch, output, box_thresh)
def evaluate_measure(self, batch, output):
return (
self.measure(batch, output),
np.linspace(0, batch["image"].shape[0]).tolist(),
)
def gather_measure(self, raw_metrics):
raw_metrics = [
image_metrics
for batch_metrics in raw_metrics
for image_metrics in batch_metrics
]
result = self.evaluator.combine_results(raw_metrics)
precision = AverageMeter()
recall = AverageMeter()
fmeasure = AverageMeter()
precision.update(result["precision"], n=len(raw_metrics))
recall.update(result["recall"], n=len(raw_metrics))
fmeasure_score = (
2 * precision.val * recall.val / (precision.val + recall.val + 1e-8)
)
fmeasure.update(fmeasure_score)
return {"precision": precision, "recall": recall, "fmeasure": fmeasure}

View File

@@ -0,0 +1,112 @@
# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
import paddle
# A global variable to record the number of calling times for profiler
# functions. It is used to specify the tracing range of training steps.
_profiler_step_id = 0
# A global variable to avoid parsing from string every time.
_profiler_options = None
class ProfilerOptions(object):
"""
Use a string to initialize a ProfilerOptions.
The string should be in the format: "key1=value1;key2=value;key3=value3".
For example:
"profile_path=model.profile"
"batch_range=[50, 60]; profile_path=model.profile"
"batch_range=[50, 60]; tracer_option=OpDetail; profile_path=model.profile"
ProfilerOptions supports following key-value pair:
batch_range - a integer list, e.g. [100, 110].
state - a string, the optional values are 'CPU', 'GPU' or 'All'.
sorted_key - a string, the optional values are 'calls', 'total',
'max', 'min' or 'ave.
tracer_option - a string, the optional values are 'Default', 'OpDetail',
'AllOpDetail'.
profile_path - a string, the path to save the serialized profile data,
which can be used to generate a timeline.
exit_on_finished - a boolean.
"""
def __init__(self, options_str):
assert isinstance(options_str, str)
self._options = {
"batch_range": [10, 20],
"state": "All",
"sorted_key": "total",
"tracer_option": "Default",
"profile_path": "/tmp/profile",
"exit_on_finished": True,
}
self._parse_from_string(options_str)
def _parse_from_string(self, options_str):
for kv in options_str.replace(" ", "").split(";"):
key, value = kv.split("=")
if key == "batch_range":
value_list = value.replace("[", "").replace("]", "").split(",")
value_list = list(map(int, value_list))
if (
len(value_list) >= 2
and value_list[0] >= 0
and value_list[1] > value_list[0]
):
self._options[key] = value_list
elif key == "exit_on_finished":
self._options[key] = value.lower() in ("yes", "true", "t", "1")
elif key in ["state", "sorted_key", "tracer_option", "profile_path"]:
self._options[key] = value
def __getitem__(self, name):
if self._options.get(name, None) is None:
raise ValueError("ProfilerOptions does not have an option named %s." % name)
return self._options[name]
def add_profiler_step(options_str=None):
"""
Enable the operator-level timing using PaddlePaddle's profiler.
The profiler uses a independent variable to count the profiler steps.
One call of this function is treated as a profiler step.
Args:
profiler_options - a string to initialize the ProfilerOptions.
Default is None, and the profiler is disabled.
"""
if options_str is None:
return
global _profiler_step_id
global _profiler_options
if _profiler_options is None:
_profiler_options = ProfilerOptions(options_str)
if _profiler_step_id == _profiler_options["batch_range"][0]:
paddle.utils.profiler.start_profiler(
_profiler_options["state"], _profiler_options["tracer_option"]
)
elif _profiler_step_id == _profiler_options["batch_range"][1]:
paddle.utils.profiler.stop_profiler(
_profiler_options["sorted_key"], _profiler_options["profile_path"]
)
if _profiler_options["exit_on_finished"]:
sys.exit(0)
_profiler_step_id += 1

View File

@@ -0,0 +1,72 @@
from paddle.optimizer import lr
import logging
__all__ = ["Polynomial"]
class Polynomial(object):
"""
Polynomial learning rate decay
Args:
learning_rate (float): The initial learning rate. It is a python float number.
epochs(int): The decay epoch size. It determines the decay cycle, when by_epoch is set to true, it will change to epochs=epochs*step_each_epoch.
step_each_epoch: all steps in each epoch.
end_lr(float, optional): The minimum final learning rate. Default: 0.0001.
power(float, optional): Power of polynomial. Default: 1.0.
warmup_epoch(int): The epoch numbers for LinearWarmup. Default: 0, , when by_epoch is set to true, it will change to warmup_epoch=warmup_epoch*step_each_epoch.
warmup_start_lr(float): Initial learning rate of warm up. Default: 0.0.
last_epoch (int, optional): The index of last epoch. Can be set to restart training. Default: -1, means initial learning rate.
by_epoch: Whether the set parameter is based on epoch or iter, when set to true,, epochs and warmup_epoch will be automatically multiplied by step_each_epoch. Default: True
"""
def __init__(
self,
learning_rate,
epochs,
step_each_epoch,
end_lr=0.0,
power=1.0,
warmup_epoch=0,
warmup_start_lr=0.0,
last_epoch=-1,
by_epoch=True,
**kwargs,
):
super().__init__()
if warmup_epoch >= epochs:
msg = f'When using warm up, the value of "epochs" must be greater than value of "Optimizer.lr.warmup_epoch". The value of "Optimizer.lr.warmup_epoch" has been set to {epochs}.'
logging.warning(msg)
warmup_epoch = epochs
self.learning_rate = learning_rate
self.epochs = epochs
self.end_lr = end_lr
self.power = power
self.last_epoch = last_epoch
self.warmup_epoch = warmup_epoch
self.warmup_start_lr = warmup_start_lr
if by_epoch:
self.epochs *= step_each_epoch
self.warmup_epoch = int(self.warmup_epoch * step_each_epoch)
def __call__(self):
learning_rate = (
lr.PolynomialDecay(
learning_rate=self.learning_rate,
decay_steps=self.epochs,
end_lr=self.end_lr,
power=self.power,
last_epoch=self.last_epoch,
)
if self.epochs > 0
else self.learning_rate
)
if self.warmup_epoch > 0:
learning_rate = lr.LinearWarmup(
learning_rate=learning_rate,
warmup_steps=self.warmup_epoch,
start_lr=self.warmup_start_lr,
end_lr=self.learning_rate,
last_epoch=self.last_epoch,
)
return learning_rate

View File

@@ -0,0 +1,365 @@
# -*- coding: utf-8 -*-
# @Time : 2019/8/23 21:59
# @Author : zhoujun
import json
import pathlib
import time
import os
import glob
import cv2
import yaml
from typing import Mapping
import matplotlib.pyplot as plt
import numpy as np
from argparse import ArgumentParser, RawDescriptionHelpFormatter
def _check_image_file(path):
img_end = {"jpg", "bmp", "png", "jpeg", "rgb", "tif", "tiff", "gif", "pdf"}
return any([path.lower().endswith(e) for e in img_end])
def get_image_file_list(img_file):
imgs_lists = []
if img_file is None or not os.path.exists(img_file):
raise Exception("not found any img file in {}".format(img_file))
img_end = {"jpg", "bmp", "png", "jpeg", "rgb", "tif", "tiff", "gif", "pdf"}
if os.path.isfile(img_file) and _check_image_file(img_file):
imgs_lists.append(img_file)
elif os.path.isdir(img_file):
for single_file in os.listdir(img_file):
file_path = os.path.join(img_file, single_file)
if os.path.isfile(file_path) and _check_image_file(file_path):
imgs_lists.append(file_path)
if len(imgs_lists) == 0:
raise Exception("not found any img file in {}".format(img_file))
imgs_lists = sorted(imgs_lists)
return imgs_lists
def setup_logger(log_file_path: str = None):
import logging
logging._warn_preinit_stderr = 0
logger = logging.getLogger("DBNet.paddle")
formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")
ch = logging.StreamHandler()
ch.setFormatter(formatter)
logger.addHandler(ch)
if log_file_path is not None:
file_handle = logging.FileHandler(log_file_path)
file_handle.setFormatter(formatter)
logger.addHandler(file_handle)
logger.setLevel(logging.DEBUG)
return logger
# --exeTime
def exe_time(func):
def newFunc(*args, **args2):
t0 = time.time()
back = func(*args, **args2)
print("{} cost {:.3f}s".format(func.__name__, time.time() - t0))
return back
return newFunc
def load(file_path: str):
file_path = pathlib.Path(file_path)
func_dict = {".txt": _load_txt, ".json": _load_json, ".list": _load_txt}
assert file_path.suffix in func_dict
return func_dict[file_path.suffix](file_path)
def _load_txt(file_path: str):
with open(file_path, "r", encoding="utf8") as f:
content = [
x.strip().strip("\ufeff").strip("\xef\xbb\xbf") for x in f.readlines()
]
return content
def _load_json(file_path: str):
with open(file_path, "r", encoding="utf8") as f:
content = json.load(f)
return content
def save(data, file_path):
file_path = pathlib.Path(file_path)
func_dict = {".txt": _save_txt, ".json": _save_json}
assert file_path.suffix in func_dict
return func_dict[file_path.suffix](data, file_path)
def _save_txt(data, file_path):
"""
将一个list的数组写入txt文件里
:param data:
:param file_path:
:return:
"""
if not isinstance(data, list):
data = [data]
with open(file_path, mode="w", encoding="utf8") as f:
f.write("\n".join(data))
def _save_json(data, file_path):
with open(file_path, "w", encoding="utf-8") as json_file:
json.dump(data, json_file, ensure_ascii=False, indent=4)
def show_img(imgs: np.ndarray, title="img"):
color = len(imgs.shape) == 3 and imgs.shape[-1] == 3
imgs = np.expand_dims(imgs, axis=0)
for i, img in enumerate(imgs):
plt.figure()
plt.title("{}_{}".format(title, i))
plt.imshow(img, cmap=None if color else "gray")
plt.show()
def draw_bbox(img_path, result, color=(255, 0, 0), thickness=2):
if isinstance(img_path, str):
img_path = cv2.imread(img_path)
# img_path = cv2.cvtColor(img_path, cv2.COLOR_BGR2RGB)
img_path = img_path.copy()
for point in result:
point = point.astype(int)
cv2.polylines(img_path, [point], True, color, thickness)
return img_path
def cal_text_score(texts, gt_texts, training_masks, running_metric_text, thred=0.5):
training_masks = training_masks.numpy()
pred_text = texts.numpy() * training_masks
pred_text[pred_text <= thred] = 0
pred_text[pred_text > thred] = 1
pred_text = pred_text.astype(np.int32)
gt_text = gt_texts.numpy() * training_masks
gt_text = gt_text.astype(np.int32)
running_metric_text.update(gt_text, pred_text)
score_text, _ = running_metric_text.get_scores()
return score_text
def order_points_clockwise(pts):
rect = np.zeros((4, 2), dtype="float32")
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
return rect
def order_points_clockwise_list(pts):
pts = pts.tolist()
pts.sort(key=lambda x: (x[1], x[0]))
pts[:2] = sorted(pts[:2], key=lambda x: x[0])
pts[2:] = sorted(pts[2:], key=lambda x: -x[0])
pts = np.array(pts)
return pts
def get_datalist(train_data_path):
"""
获取训练和验证的数据list
:param train_data_path: 训练的dataset文件列表每个文件内以如下格式存储 path/to/img\tlabel
:return:
"""
train_data = []
for p in train_data_path:
with open(p, "r", encoding="utf-8") as f:
for line in f.readlines():
line = line.strip("\n").replace(".jpg ", ".jpg\t").split("\t")
if len(line) > 1:
img_path = pathlib.Path(line[0].strip(" "))
label_path = pathlib.Path(line[1].strip(" "))
if (
img_path.exists()
and img_path.stat().st_size > 0
and label_path.exists()
and label_path.stat().st_size > 0
):
train_data.append((str(img_path), str(label_path)))
return train_data
def save_result(result_path, box_list, score_list, is_output_polygon):
if is_output_polygon:
with open(result_path, "wt") as res:
for i, box in enumerate(box_list):
box = box.reshape(-1).tolist()
result = ",".join([str(int(x)) for x in box])
score = score_list[i]
res.write(result + "," + str(score) + "\n")
else:
with open(result_path, "wt") as res:
for i, box in enumerate(box_list):
score = score_list[i]
box = box.reshape(-1).tolist()
result = ",".join([str(int(x)) for x in box])
res.write(result + "," + str(score) + "\n")
def expand_polygon(polygon):
"""
对只有一个字符的框进行扩充
"""
(x, y), (w, h), angle = cv2.minAreaRect(np.float32(polygon))
if angle < -45:
w, h = h, w
angle += 90
new_w = w + h
box = ((x, y), (new_w, h), angle)
points = cv2.boxPoints(box)
return order_points_clockwise(points)
def _merge_dict(config, merge_dct):
"""Recursive dict merge. Inspired by :meth:``dict.update()``, instead of
updating only top-level keys, dict_merge recurses down into dicts nested
to an arbitrary depth, updating keys. The ``merge_dct`` is merged into
``dct``.
Args:
config: dict onto which the merge is executed
merge_dct: dct merged into config
Returns: dct
"""
for key, value in merge_dct.items():
sub_keys = key.split(".")
key = sub_keys[0]
if key in config and len(sub_keys) > 1:
_merge_dict(config[key], {".".join(sub_keys[1:]): value})
elif (
key in config
and isinstance(config[key], dict)
and isinstance(value, Mapping)
):
_merge_dict(config[key], value)
else:
config[key] = value
return config
def print_dict(cfg, print_func=print, delimiter=0):
"""
Recursively visualize a dict and
indenting acrrording by the relationship of keys.
"""
for k, v in sorted(cfg.items()):
if isinstance(v, dict):
print_func("{}{} : ".format(delimiter * " ", str(k)))
print_dict(v, print_func, delimiter + 4)
elif isinstance(v, list) and len(v) >= 1 and isinstance(v[0], dict):
print_func("{}{} : ".format(delimiter * " ", str(k)))
for value in v:
print_dict(value, print_func, delimiter + 4)
else:
print_func("{}{} : {}".format(delimiter * " ", k, v))
class Config(object):
def __init__(self, config_path, BASE_KEY="base"):
self.BASE_KEY = BASE_KEY
self.cfg = self._load_config_with_base(config_path)
def _load_config_with_base(self, file_path):
"""
Load config from file.
Args:
file_path (str): Path of the config file to be loaded.
Returns: global config
"""
_, ext = os.path.splitext(file_path)
assert ext in [".yml", ".yaml"], "only support yaml files for now"
with open(file_path) as f:
file_cfg = yaml.load(f, Loader=yaml.Loader)
# NOTE: cfgs outside have higher priority than cfgs in _BASE_
if self.BASE_KEY in file_cfg:
all_base_cfg = dict()
base_ymls = list(file_cfg[self.BASE_KEY])
for base_yml in base_ymls:
with open(base_yml) as f:
base_cfg = self._load_config_with_base(base_yml)
all_base_cfg = _merge_dict(all_base_cfg, base_cfg)
del file_cfg[self.BASE_KEY]
file_cfg = _merge_dict(all_base_cfg, file_cfg)
file_cfg["filename"] = os.path.splitext(os.path.split(file_path)[-1])[0]
return file_cfg
def merge_dict(self, args):
self.cfg = _merge_dict(self.cfg, args)
def print_cfg(self, print_func=print):
"""
Recursively visualize a dict and
indenting according by the relationship of keys.
"""
print_func("----------- Config -----------")
print_dict(self.cfg, print_func)
print_func("---------------------------------------------")
def save(self, p):
with open(p, "w") as f:
yaml.dump(dict(self.cfg), f, default_flow_style=False, sort_keys=False)
class ArgsParser(ArgumentParser):
def __init__(self):
super(ArgsParser, self).__init__(formatter_class=RawDescriptionHelpFormatter)
self.add_argument("-c", "--config_file", help="configuration file to use")
self.add_argument("-o", "--opt", nargs="*", help="set configuration options")
self.add_argument(
"-p",
"--profiler_options",
type=str,
default=None,
help="The option of profiler, which should be in format "
'"key1=value1;key2=value2;key3=value3".',
)
def parse_args(self, argv=None):
args = super(ArgsParser, self).parse_args(argv)
assert (
args.config_file is not None
), "Please specify --config_file=configure_file_path."
args.opt = self._parse_opt(args.opt)
return args
def _parse_opt(self, opts):
config = {}
if not opts:
return config
for s in opts:
s = s.strip()
k, v = s.split("=", 1)
if "." not in k:
config[k] = yaml.load(v, Loader=yaml.Loader)
else:
keys = k.split(".")
if keys[0] not in config:
config[keys[0]] = {}
cur = config[keys[0]]
for idx, key in enumerate(keys[1:]):
if idx == len(keys) - 2:
cur[key] = yaml.load(v, Loader=yaml.Loader)
else:
cur[key] = {}
cur = cur[key]
return config
if __name__ == "__main__":
img = np.zeros((1, 3, 640, 640))
show_img(img[0][0])
plt.show()