File size: 10,594 Bytes
a9286c4 0e997e3 a9286c4 0e997e3 a9286c4 0e997e3 a9286c4 0e997e3 a9286c4 0e997e3 a9286c4 0e997e3 a9286c4 0e997e3 a9286c4 0e997e3 866ff8d 0e997e3 866ff8d 0e997e3 a9286c4 0e997e3 a9286c4 0e997e3 a9286c4 0e997e3 a9286c4 0e997e3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
import re
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
import yaml
# parse a '.md' file and find a table. return table information
def parse_table(filepath, cfg):
# parse benchmark data
def _parse_benchmark_data(lines):
raw_data = []
for l in lines:
l = l.strip()
# parse each line
m = re.match(r"(\d+\.?\d*)\s+(\d+\.?\d*)\s+(\d+\.?\d*)\s+\[([^]]*)]\s+(.*)", l)
if m:
raw_data.append(m.groups())
return raw_data
# find each cpu, gpu, npu block
def _find_all_platform_block(lines):
cur_start = None
cur_platform = None
platform_block = dict()
for i in range(len(lines)):
l = lines[i].strip()
# found start and end of a platform
if l.startswith("CPU") or l.startswith("GPU") or l.startswith("NPU"):
if cur_platform is not None:
platform_block[cur_platform] = (cur_start, i)
cur_platform = l[:-1]
cur_start = i + 1
continue
if cur_platform is not None and i == len(lines) - 1:
platform_block[cur_platform] = (cur_start, i)
for key in platform_block:
r = platform_block[key]
platform_block[key] = _parse_benchmark_data(lines[r[0]:r[1]])
return platform_block
# find device block
def _find_all_device_block(lines, level):
cur_start = None
cur_device_name = None
device_block = dict()
for i in range(len(lines)):
l = lines[i].strip()
m = re.match(r"^(#+)\s+(.*)", l)
# found start and end of a device
if m and len(m.group(1)) == level:
if cur_device_name is not None:
device_block[cur_device_name] = (cur_start, i)
cur_device_name = m.group(2)
cur_start = i + 1
continue
if cur_device_name is not None and i == len(lines) - 1:
device_block[cur_device_name] = (cur_start, i)
for key in device_block:
r = device_block[key]
device_block[key] = _find_all_platform_block(lines[r[0]:r[1]])
return device_block
# find detail block
def _find_detail_block(lines, title, level):
start = None
end = len(lines)
for i in range(len(lines)):
l = lines[i].strip()
m = re.match(r"^(#+)\s+(.*)", l)
# found start of detailed results block
if m and len(m.group(1)) == level and m.group(2) == title:
start = i + 1
continue
# found end of detailed results block
if start is not None and m and len(m.group(1)) <= level:
end = i
break
return _find_all_device_block(lines[start:end], level + 1)
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
lines = content.split("\n")
devices = cfg["Devices"]
models = cfg["Models"]
# display information of all devices
devices_display = [x['display_info'] for x in cfg["Devices"]]
header = ["Model", "Task", "Input Size"] + devices_display
body = [[x["name"], x["task"], x["input_size"]] + ["---"] * len(devices) for x in models]
table_raw_data = _find_detail_block(lines, title="Detailed Results", level=2)
device_name_header = [f"{x['name']}-{x['platform']}" for x in devices]
device_name_header = [""] * (len(header) - len(device_name_header)) + device_name_header
# device name map to model col idx
device_name_to_col_idx = {k: v for v, k in enumerate(device_name_header)}
# model name map to model row idx
model_name_to_row_idx = {k[0]: v for v, k in enumerate(body)}
# convert raw data to usage data
for device in devices:
raw_data = table_raw_data[device["name"]][device["platform"]]
col_idx = device_name_to_col_idx[f"{device['name']}-{device['platform']}"]
for model in models:
# find which row idx of this model
row_idx = model_name_to_row_idx[model["name"]]
model_idxs = [i for i in range(len(raw_data)) if model["keyword"] in raw_data[i][-1]]
if len(model_idxs) > 0:
# only choose the first one
model_idx = model_idxs[0]
# choose mean as value
body[row_idx][col_idx] = raw_data[model_idx][0]
# remove used data
for idx in sorted(model_idxs, reverse=True):
raw_data.pop(idx)
# handle suffix
for suffix in cfg["Suffixes"]:
row_idx = model_name_to_row_idx[suffix["model"]]
col_idx = device_name_to_col_idx[f"{suffix['device']}-{suffix['platform']}"]
body[row_idx][col_idx] += suffix["str"]
return header, body
# render table and save
def render_table(header, body, save_path, cfg, cmap_type):
# parse models information and return some data
def _parse_data(models_info, cmap, cfg):
min_list = []
max_list = []
colors = []
# model name map to idx
model_name_to_idx = {k["name"]: v for v, k in enumerate(cfg["Models"])}
for model in models_info:
# remove \*
data = [x.replace("\\*", "") for x in model]
# get max data
max_idx = -1
min_data = 9999999
min_idx = -1
for i in range(len(data)):
try:
d = float(data[i])
if d < min_data:
min_data = d
min_idx = i
except:
pass
# set all bigger than acceptable time to red color
idx = model_name_to_idx[model[0]]
acc_time = cfg["Models"][idx]["acceptable_time"]
min_list.append(min_idx)
max_list.append(max_idx)
# calculate colors
color = []
for t in data:
try:
t = float(t)
if t > acc_time:
# all bigger time will be set to red
color.append(cmap(1.))
else:
# sqrt to make the result non-linear
t = np.sqrt((t - min_data) / (acc_time - min_data))
color.append(cmap(t))
except:
color.append('white')
colors.append(color)
return colors, min_list, max_list
cmap = mpl.colormaps.get_cmap(cmap_type)
table_colors, min_list, max_list = _parse_data(body, cmap, cfg)
table_texts = [header] + body
table_colors = [['white'] * len(header)] + table_colors
# create a figure, base width set to 1000, height set to 80
fig, axs = plt.subplots(nrows=3, figsize=(10, 0.8))
# turn off labels and axis
for ax in axs:
ax.set_axis_off()
ax.set_xticks([])
ax.set_yticks([])
# create and add a color map
gradient = np.linspace(0, 1, 256)
gradient = np.vstack((gradient, gradient))
axs[0].imshow(gradient, aspect='auto', cmap=cmap)
axs[0].text(-0.01, 0.5, "Faster", va='center', ha='right', fontsize=11, transform=axs[0].transAxes)
axs[0].text(1.01, 0.5, "Slower", va='center', ha='left', fontsize=11, transform=axs[0].transAxes)
# initialize a table
table = axs[1].table(cellText=table_texts,
cellColours=table_colors,
cellLoc="left",
loc="upper left")
# set style of header, each url of hardware
ori_height = table[0, 0].get_height()
url_base = 'https://github.com/opencv/opencv_zoo/tree/main/benchmark#'
hw_urls = [f"{url_base}{x['name'].lower().replace(' ', '-')}" for x in cfg["Devices"]]
hw_urls = [""] * 3 + hw_urls
for col in range(len(header)):
cell = table[0, col]
cell.set_text_props(ha='center', weight='bold', linespacing=1.5, url=hw_urls[col])
cell.set_url(hw_urls[col])
cell.set_height(ori_height * 2.2)
url_base = 'https://github.com/opencv/opencv_zoo/tree/main/models/'
model_urls = [f"{url_base}{x['folder']}" for x in cfg["Models"]]
model_urls = [""] + model_urls
for row in range(len(body) + 1):
cell = table[row, 0]
cell.set_text_props(url=model_urls[row])
cell.set_url(model_urls[row])
# adjust table position
table_pos = axs[1].get_position()
axs[1].set_position([
table_pos.x0,
table_pos.y0 - table_pos.height,
table_pos.width,
table_pos.height
])
table.set_fontsize(11)
table.auto_set_font_size(False)
table.scale(1, 2)
table.auto_set_column_width(list(range(len(table_texts[0]))))
table.AXESPAD = 0 # cancel padding
# highlight the best number
for i in range(len(min_list)):
cell = table.get_celld()[(i + 1, min_list[i])]
cell.set_text_props(weight='bold', color='white')
# draw table and trigger changing the column width value
fig.canvas.draw()
# calculate table height and width
table_height = 0
table_width = 0
for i in range(len(table_texts)):
cell = table.get_celld()[(i, 0)]
table_height += cell.get_height()
for i in range(len(table_texts[0])):
cell = table.get_celld()[(0, i)]
table_width += cell.get_width()
# add notes for table
axs[2].text(0, -table_height - 1, "Units: All data in milliseconds (ms).", va='bottom', ha='left', fontsize=11, transform=axs[1].transAxes)
axs[2].text(0, -table_height - 2, "\\*: Models are quantized in per-channel mode, which run slower than per-tensor quantized models on NPU.", va='bottom', ha='left', fontsize=11, transform=axs[1].transAxes)
# adjust color map position to center
cm_pos = axs[0].get_position()
axs[0].set_position([
(table_width - 1) / 2,
cm_pos.y0,
cm_pos.width,
cm_pos.height
])
plt.rcParams['svg.fonttype'] = 'none'
plt.rcParams['svg.hashsalt'] = '11' # fix hash salt for avoiding id change
plt.savefig(save_path, format='svg', bbox_inches="tight", pad_inches=0, metadata={'Date': None, 'Creator': None})
if __name__ == '__main__':
with open("table_config.yaml", 'r') as f:
cfg = yaml.safe_load(f)
hw_info, model_info = parse_table("README.md", cfg)
render_table(hw_info, model_info, "color_table.svg", cfg, "RdYlGn_r")
|