This commit is contained in:
8
benchmark/PaddleOCR_DBNet/utils/__init__.py
Normal file
8
benchmark/PaddleOCR_DBNet/utils/__init__.py
Normal 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
|
||||
6
benchmark/PaddleOCR_DBNet/utils/cal_recall/__init__.py
Normal file
6
benchmark/PaddleOCR_DBNet/utils/cal_recall/__init__.py
Normal 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"]
|
||||
@@ -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)
|
||||
402
benchmark/PaddleOCR_DBNet/utils/cal_recall/script.py
Normal file
402
benchmark/PaddleOCR_DBNet/utils/cal_recall/script.py
Normal 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"]
|
||||
47
benchmark/PaddleOCR_DBNet/utils/compute_mean_std.py
Normal file
47
benchmark/PaddleOCR_DBNet/utils/compute_mean_std.py
Normal 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 读取的图像格式为BGR,PIL/Skimage读取到的都是RGB不用转
|
||||
means.reverse() # BGR --> RGB
|
||||
stdevs.reverse()
|
||||
|
||||
print("normMean = {}".format(means))
|
||||
print("normStd = {}".format(stdevs))
|
||||
print("transforms.Normalize(normMean = {}, normStd = {})".format(means, stdevs))
|
||||
21
benchmark/PaddleOCR_DBNet/utils/make_trainfile.py
Normal file
21
benchmark/PaddleOCR_DBNet/utils/make_trainfile.py
Normal 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()
|
||||
60
benchmark/PaddleOCR_DBNet/utils/metrics.py
Normal file
60
benchmark/PaddleOCR_DBNet/utils/metrics.py
Normal 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))
|
||||
19
benchmark/PaddleOCR_DBNet/utils/ocr_metric/__init__.py
Normal file
19
benchmark/PaddleOCR_DBNet/utils/ocr_metric/__init__.py
Normal 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
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Time : 2019/12/5 15:36
|
||||
# @Author : zhoujun
|
||||
|
||||
from .quad_metric import QuadMetric
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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}
|
||||
112
benchmark/PaddleOCR_DBNet/utils/profiler.py
Normal file
112
benchmark/PaddleOCR_DBNet/utils/profiler.py
Normal 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
|
||||
72
benchmark/PaddleOCR_DBNet/utils/schedulers.py
Normal file
72
benchmark/PaddleOCR_DBNet/utils/schedulers.py
Normal 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
|
||||
365
benchmark/PaddleOCR_DBNet/utils/util.py
Normal file
365
benchmark/PaddleOCR_DBNet/utils/util.py
Normal 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()
|
||||
Reference in New Issue
Block a user