EXPERIMENT / 配套代码
prepare_yunet.py
← 返回文章"""生成 YuNet 320 的输入、浮点基准及 input255 等价图;不运行 ATC。"""
import argparse
import copy
import hashlib
import json
from pathlib import Path
import cv2
import numpy as np
import onnx
from onnx import numpy_helper
import onnxruntime as ort
OUTPUT_NAMES = [f'{kind}_{stride}' for kind in ('cls', 'obj', 'bbox', 'kps') for stride in (8, 16, 32)]
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def fixed_yunet(path):
model = onnx.load(str(path))
if [x.name for x in model.graph.output] != OUTPUT_NAMES:
raise ValueError('仅支持本系列所述 YuNet 2023mar 十二输出模型')
if len(model.graph.input) != 1 or model.graph.input[0].name != 'input':
raise ValueError('YuNet 输入接口不匹配')
dims = model.graph.input[0].type.tensor_type.shape.dim
if len(dims) != 4:
raise ValueError('输入必须为四维')
for dim, value in zip(dims, (1, 3, 320, 320)):
dim.ClearField('dim_param')
dim.dim_value = value
# 该 YuNet 为全卷积路径;同步修正 640 输入遗留的输出声明。
del model.graph.value_info[:]
channels = {'cls': 1, 'obj': 1, 'bbox': 4, 'kps': 10}
for output in model.graph.output:
kind, stride = output.name.rsplit('_', 1)
dims = output.type.tensor_type.shape.dim
if len(dims) != 3:
raise ValueError('YuNet 输出必须为三维')
for dim, value in zip(dims, (1, (320 // int(stride)) ** 2, channels[kind])):
dim.ClearField('dim_param')
dim.dim_value = value
onnx.checker.check_model(model)
return model
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--model', required=True, type=Path)
parser.add_argument('--output', required=True, type=Path)
parser.add_argument('--container-output', required=True, help='输出目录在转换容器内的绝对路径')
sources = parser.add_mutually_exclusive_group(required=True)
sources.add_argument('--images', nargs='+', type=Path, help='真实头部 ROI 图片,按左上对齐缩放到 320')
sources.add_argument('--synthetic', action='store_true', help='仅验证数值与文件格式,不用于模型质量或生产校准')
args = parser.parse_args()
if args.output.exists():
parser.error('输出目录必须不存在,避免覆盖旧实验')
if not args.container_output.startswith('/'):
parser.error('container-output 必须是绝对路径')
original = fixed_yunet(args.model)
scaled = copy.deepcopy(original)
first = scaled.graph.node[0]
if first.op_type != 'Conv':
raise ValueError('首节点不是预期 Conv')
for item in scaled.graph.initializer:
if item.name == first.input[1]:
value = numpy_helper.to_array(item).astype(np.float32) * np.float32(255)
item.CopyFrom(numpy_helper.from_array(value, item.name))
break
else:
raise ValueError('首卷积权重不是预期 initializer')
onnx.checker.check_model(scaled)
args.output.mkdir(parents=True)
original_path = args.output / 'yunet320_reference.onnx'
scaled_path = args.output / 'yunet320_input255.onnx'
onnx.save(original, original_path)
onnx.save(scaled, scaled_path)
options = ort.SessionOptions()
options.intra_op_num_threads = 2
options.inter_op_num_threads = 1
a = ort.InferenceSession(str(original_path), sess_options=options, providers=['CPUExecutionProvider'])
b = ort.InferenceSession(str(scaled_path), sess_options=options, providers=['CPUExecutionProvider'])
if args.synthetic:
y, x = np.indices((240, 300))
image = np.stack(((x * 3 + y) % 256, (x + y * 2) % 256, (x * 2 + y * 3) % 256), axis=-1).astype(np.uint8)
inputs = [('synthetic-pattern', image)]
else:
inputs = [(str(path), cv2.imread(str(path), cv2.IMREAD_COLOR)) for path in args.images]
report = {'source_model_sha256': digest(args.model), 'onnx_sha256': digest(scaled_path),
'synthetic': args.synthetic, 'input': {'name': 'input', 'shape': [1, 3, 320, 320],
'runtime_dtype': 'uint8', 'layout': 'BGR_PLANAR', 'geometry': 'top-left / black padding / bilinear'},
'versions': {'numpy': np.__version__, 'opencv': cv2.__version__, 'onnx': onnx.__version__, 'onnxruntime': ort.__version__},
'samples': []}
calibration = []
for index, (name, image) in enumerate(inputs):
if image is None or not image.size:
raise ValueError(f'无法读取图像:{name}')
h, w = image.shape[:2]
scale = 320 / max(h, w)
rw, rh = max(1, round(w * scale)), max(1, round(h * scale))
canvas = np.zeros((320, 320, 3), dtype=np.uint8)
canvas[:rh, :rw] = cv2.resize(image, (rw, rh), interpolation=cv2.INTER_LINEAR)
image_path = args.output / f'prepared_{index}.png'
if not cv2.imwrite(str(image_path), canvas):
raise IOError('PNG 写出失败')
planar = np.ascontiguousarray(canvas.transpose(2, 0, 1))
input_path = args.output / f'input_{index}.bgr'
planar.tofile(input_path)
assert input_path.stat().st_size == 307200
floats = planar[None].astype(np.float32)
ra = a.run(OUTPUT_NAMES, {'input': floats})
rb = b.run(OUTPUT_NAMES, {'input': floats / np.float32(255)})
diffs = []
for output, original_values, scaled_values in zip(OUTPUT_NAMES, ra, rb):
if not (np.isfinite(original_values).all() and np.isfinite(scaled_values).all()):
raise ValueError('浮点参考出现非有限值')
np.testing.assert_allclose(scaled_values, original_values, rtol=1e-3, atol=1e-3)
diffs.append({'name': output, 'shape': list(original_values.shape),
'max_abs': float(np.max(np.abs(original_values - scaled_values))),
'mae': float(np.mean(np.abs(original_values - scaled_values)))})
np.savez(args.output / f'reference_{index}.npz', **dict(zip(OUTPUT_NAMES, ra)))
np.savez(args.output / f'input255_{index}.npz', **dict(zip(OUTPUT_NAMES, rb)))
report['samples'].append({'source': name, 'source_size': [w, h], 'resized_size': [rw, rh],
'input_file': input_path.name, 'input_sha256': digest(input_path),
'prepared_sha256': digest(image_path), 'outputs': diffs})
calibration.append(args.container_output.rstrip('/') + '/' + image_path.name)
(args.output / 'calibration.txt').write_text('\n'.join(calibration) + '\n')
(args.output / 'manifest.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n')
print(json.dumps(report, ensure_ascii=False, indent=2))
if __name__ == '__main__':
main()