DepthART / src /tool /compare_onnx_axmodel.py
sunyijie
first commit
efaa3d7
Raw History Blame Contribute Delete
27.1 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Depth Anything V2:ONNX 与 AXMODEL 原始输出一致性对比。
对同一批图片分别运行 ONNX 和 axmodel,在模型分辨率下比较两路深度输出
(相对逆深度,数值越大越近),逐样本计算相似度指标,并汇总为 Markdown 文档。
两个后端的输入预处理可以不同,但 resize 与颜色顺序必须一致:
默认 ONNX 做 /255 + ImageNet mean/std,axmodel 送 0-255 原始像素
(对应编译时把归一化写进 input_processors / calibration_mean/std 的方案)。
如果 axmodel 重编译后仍需脚本侧归一化,使用 --axmodel-norm imagenet。
指标:
cosine 两路输出的余弦相似度
pearson 皮尔逊相关系数
mae / rmse 绝对误差均值 / 均方根
max_abs/p99_abs 最大误差 / 99 分位误差
aligned_mae 最小二乘 scale-shift 对齐后的 MAE(区分全局尺度漂移与结构误差)
aligned_rmse 对齐后的 RMSE
rel_error mae / mean(|onnx|)
依赖:numpy、opencv-python、onnxruntime、axengine(板端 pyaxengine)。
用法(在 hs/Depth-Anything-V2 目录下执行):
# 离线比较已有结果(开发机,无需 axengine 或模型文件)
python3 src/tool/compare_onnx_axmodel.py \
--onnx-results results/onnx_full --axmodel-results results/board_full \
--output results/compare_full/onnx_vs_axmodel.md
# 单图
python3 src/tool/compare_onnx_axmodel.py \
--onnx depth_anything_v2_vits.onnx \
--axmodel depth_anything_v2_vits_ax650.axmodel \
--image /path/a.jpg --output results/compare/single.md
# 数据集(递归收集图片),默认 axmodel 送 0-255
python3 src/tool/compare_onnx_axmodel.py \
--onnx depth_anything_v2_vits.onnx \
--axmodel depth_anything_v2_vits_ax650.axmodel \
--dataset /path/images --limit 50 \
--output results/compare/onnx_vs_axmodel.md
# axmodel 仍需要脚本侧做 ImageNet 归一化时
python3 src/tool/compare_onnx_axmodel.py \
--onnx depth_anything_v2_vits.onnx --axmodel other.axmodel \
--dataset /path/images --axmodel-norm imagenet \
--output results/compare/onnx_vs_axmodel.md
注意:
- axmodel 的输出名可能被编译器改写,脚本先按名字、再按 shape 回退绑定。
- 两个后端的输出 shape 必须一致;不一致时直接报错并打印实际接口。
- 结果默认输出到 results/compare/,不写预测 NPZ。
"""
import argparse
import json
import logging
import time
from datetime import datetime
from pathlib import Path
import cv2
import numpy as np
MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
EXTENSIONS = {'.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff'}
METRICS = ('cosine', 'pearson', 'mae', 'rmse', 'max_abs', 'p99_abs',
'aligned_mae', 'aligned_rmse', 'rel_error')
def meta_type(meta):
return str(getattr(meta, 'type', getattr(meta, 'dtype', ''))).lower()
def input_spec(shape, type_name, model_name):
shape = list(shape)
if len(shape) != 4 or not all(isinstance(v, int) and v > 0 for v in shape):
raise ValueError(f'{model_name} 输入必须是静态 4D NHWC/NCHW: {shape}')
if shape[0] != 1:
raise ValueError(f'{model_name} 仅支持 batch=1: {shape}')
if shape[1] == 3:
layout, size = 'NCHW', (shape[2], shape[3])
elif shape[3] == 3:
layout, size = 'NHWC', (shape[1], shape[2])
else:
raise ValueError(f'{model_name} 输入必须含 3 通道: {shape}')
if 'float' in type_name:
dtype = np.float32
elif 'uint8' in type_name or type_name.endswith('u8'):
dtype = np.uint8
elif 'int8' in type_name:
dtype = np.int8
else:
raise ValueError(f'{model_name} 不支持的输入类型: {type_name!r}')
return size, layout, dtype
def onnx_interface(session):
inputs, outputs = session.get_inputs(), session.get_outputs()
logging.info('ONNX 输入: %s', [(m.name, m.shape, m.type) for m in inputs])
logging.info('ONNX 输出: %s', [(m.name, m.shape, m.type) for m in outputs])
if len(inputs) != 1:
raise ValueError(f'ONNX 应有 1 个输入: {[(m.name, m.shape) for m in inputs]}')
meta = inputs[0]
if 'float' not in str(meta.type):
raise ValueError(f'ONNX 输入要求 float32: {meta.name} {meta.type}')
size, layout, dtype = input_spec(meta.shape, str(meta.type), 'ONNX')
return meta.name, size, layout, dtype, outputs[0].name
def axmodel_interface(session):
inputs, outputs = session.get_inputs(), session.get_outputs()
logging.info('AXMODEL 输入: %s', [(m.name, m.shape, meta_type(m)) for m in inputs])
logging.info('AXMODEL 输出: %s', [(m.name, m.shape, meta_type(m)) for m in outputs])
if len(inputs) != 1:
raise ValueError(f'axmodel 应有 1 个输入: {[(m.name, m.shape) for m in inputs]}')
meta = inputs[0]
size, layout, dtype = input_spec(meta.shape, meta_type(meta), 'AXMODEL')
return meta, size, layout, dtype, outputs
def preprocess(image, size, layout, dtype, resize_mode, color_order, norm):
target_h, target_w = size
original_h, original_w = image.shape[:2]
if color_order == 'rgb':
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
if resize_mode == 'pad':
scale = min(target_h / original_h, target_w / original_w)
new_h = max(1, min(target_h, int(round(original_h * scale))))
new_w = max(1, min(target_w, int(round(original_w * scale))))
resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
canvas = np.zeros((target_h, target_w, 3), dtype=np.float32)
canvas[:new_h, :new_w] = resized
image_size = (new_h, new_w)
else:
canvas = cv2.resize(image, (target_w, target_h),
interpolation=cv2.INTER_LINEAR).astype(np.float32)
image_size = (target_h, target_w)
canvas = canvas.astype(np.float32, copy=False)
if norm == 'imagenet':
if dtype != np.float32:
raise ValueError('整数输入不能使用 ImageNet 归一化')
canvas = (canvas / 255.0 - MEAN) / STD
if dtype != np.float32:
canvas = np.rint(np.clip(canvas, 0, 255)).astype(dtype)
tensor = canvas.transpose(2, 0, 1) if layout == 'NCHW' else canvas
return np.ascontiguousarray(tensor[None]), image_size
def to_2d(array, model_name):
array = np.asarray(array)
if array.ndim == 4:
if array.shape[0] != 1:
raise ValueError(f'{model_name} 输出 batch 必须为 1: {array.shape}')
if array.shape[1] == 1:
array = array[0, 0]
elif array.shape[3] == 1:
array = array[0, :, :, 0]
else:
raise ValueError(f'{model_name} 输出无法压缩为 2D: {array.shape}')
elif array.ndim == 3:
if array.shape[0] == 1:
array = array[0]
elif array.shape[2] == 1:
array = array[:, :, 0]
else:
raise ValueError(f'{model_name} 输出无法压缩为 2D: {array.shape}')
if array.ndim != 2:
raise ValueError(f'{model_name} 输出必须是 2D 深度图: {array.shape}')
array = array.astype(np.float32, copy=False)
if not np.isfinite(array).all():
raise ValueError(f'{model_name} 输出包含 nan/inf')
return array
def select_ax_output(outputs, metas, expected_shape):
if not isinstance(outputs, (list, tuple)):
outputs = [outputs]
if len(outputs) != len(metas):
raise ValueError(f'axmodel 输出数量不一致: {len(outputs)} vs {len(metas)}')
for index, meta in enumerate(metas):
if any(token in str(meta.name).lower() for token in ('depth', 'output')):
array = to_2d(outputs[index], 'AXMODEL')
if array.shape == expected_shape:
return array, str(meta.name)
candidates = []
for index, array in enumerate(outputs):
candidate = to_2d(array, 'AXMODEL')
if candidate.shape == expected_shape:
candidates.append((index, candidate))
if len(candidates) != 1:
raise ValueError('axmodel 输出无法按 name 或 shape 唯一匹配'
f' {expected_shape}: {[(m.name, np.asarray(o).shape) for m, o in zip(metas, outputs)]}')
index, candidate = candidates[0]
return candidate, str(metas[index].name)
def compare_arrays(reference, actual):
ref = reference.astype(np.float64).ravel()
act = actual.astype(np.float64).ravel()
diff = act - ref
ref_norm = np.linalg.norm(ref)
act_norm = np.linalg.norm(act)
cosine = float(np.dot(ref, act) / (ref_norm * act_norm + 1e-12))
if ref.std() > 1e-12 and act.std() > 1e-12:
pearson = float(np.corrcoef(ref, act)[0, 1])
else:
pearson = float('nan')
slope, offset = np.polyfit(ref, act, 1)
aligned = act - (slope * ref + offset)
abs_diff = np.abs(diff)
return {
'cosine': cosine,
'pearson': pearson,
'mae': float(abs_diff.mean()),
'rmse': float(np.sqrt(np.mean(diff ** 2))),
'max_abs': float(abs_diff.max()),
'p99_abs': float(np.percentile(abs_diff, 99)),
'aligned_mae': float(np.abs(aligned).mean()),
'aligned_rmse': float(np.sqrt(np.mean(aligned ** 2))),
'rel_error': float(abs_diff.mean() / (np.abs(ref).mean() + 1e-12)),
}
def summarize(records):
summary = {}
for key in METRICS:
values = np.array([record[key] for record in records], dtype=np.float64)
values = values[np.isfinite(values)]
if values.size == 0:
summary[key] = None
continue
summary[key] = {'mean': float(values.mean()),
'median': float(np.percentile(values, 50)),
'p90': float(np.percentile(values, 90)),
'p99': float(np.percentile(values, 99)),
'min': float(values.min()),
'max': float(values.max())}
return summary
def format_value(value):
if value is None:
return '-'
if isinstance(value, float) and not np.isfinite(value):
return '-'
return f'{value:.6g}'
def write_markdown(path, report):
lines = ['# Depth Anything V2:ONNX 与 AXMODEL 输出一致性', '']
lines.append(f'- 生成时间:{report["created"]}')
lines.append(f'- 样本数:{report["sample_count"]}')
lines.append(f'- 结果目录:`{report["output_dir"]}`')
if report['limit'] is not None:
lines.append(f'- 样本上限:{report["limit"]}')
lines.append('')
if report.get('mode') == 'results':
lines.extend([
'## 结果来源', '',
f'- ONNX:`{report["onnx_results"]}`',
f'- AXMODEL:`{report["axmodel_results"]}`',
'- 按相对路径匹配同名 `.depth.npy`,比较保存的深度图,不重新推理或 resize。',
'- 这些文件由推理脚本还原至原图尺寸,指标对应保存结果的像素空间。',
'- 离线比较不测量推理耗时;预处理配置以原推理记录为准。', '',
])
else:
lines.append('## 模型与预处理')
lines.append('')
lines.append('| 项 | ONNX | AXMODEL |')
lines.append('| --- | --- | --- |')
lines.append(f'| 模型 | `{report["onnx"]["path"]}` | `{report["axmodel"]["path"]}` |')
lines.append(f'| 运行时 | {report["onnx"]["runtime"]} | {report["axmodel"]["runtime"]} |')
lines.append(f'| 输入 | {report["onnx"]["input"]} | {report["axmodel"]["input"]} |')
lines.append(f'| 输出 | {report["onnx"]["output"]} | {report["axmodel"]["output"]} |')
lines.append(f'| 归一化 | {report["onnx"]["norm"]} | {report["axmodel"]["norm"]} |')
lines.append(f'| resize | {report["resize_mode"]} | {report["resize_mode"]} |')
lines.append(f'| 颜色顺序 | {report["color_order"]} | {report["color_order"]} |')
lines.append(f'| warmup | {report["warmup"]} | {report["warmup"]} |')
lines.append('')
lines.append('## 汇总指标')
lines.append('')
lines.append('| 指标 | mean | median | p90 | p99 | min | max |')
lines.append('| --- | --- | --- | --- | --- | --- | --- |')
for key in METRICS:
stats = report['summary'][key]
if stats is None:
lines.append(f'| {key} | - | - | - | - | - | - |')
else:
lines.append(f'| {key} | {format_value(stats["mean"])} | '
f'{format_value(stats["median"])} | {format_value(stats["p90"])} | '
f'{format_value(stats["p99"])} | {format_value(stats["min"])} | '
f'{format_value(stats["max"])} |')
lines.append('')
lines.append('## 逐样本结果')
lines.append('')
lines.append('| # | 图片 | cosine | pearson | mae | rmse | max_abs | p99_abs | '
'aligned_mae | aligned_rmse | rel_error | ONNX ms | AXMODEL ms |')
lines.append('| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |')
for index, sample in enumerate(report['samples'], 1):
metrics = sample['metrics']
lines.append(
f'| {index} | `{sample["name"]}` | {format_value(metrics["cosine"])} | '
f'{format_value(metrics["pearson"])} | {format_value(metrics["mae"])} | '
f'{format_value(metrics["rmse"])} | {format_value(metrics["max_abs"])} | '
f'{format_value(metrics["p99_abs"])} | {format_value(metrics["aligned_mae"])} | '
f'{format_value(metrics["aligned_rmse"])} | {format_value(metrics["rel_error"])} | '
f'{format_value(sample["onnx_ms"])} | {format_value(sample["axmodel_ms"])} |')
lines.append('')
lines.append('## 指标说明')
lines.append('')
lines.append('- `cosine`:两路输出的余弦相似度,1 表示方向完全一致。')
lines.append('- `pearson`:线性相关系数,衡量结构一致性,对全局尺度不敏感。')
lines.append('- `mae` / `rmse`:未对齐的绝对误差,包含量化误差与整体尺度漂移。')
lines.append('- `max_abs` / `p99_abs`:最大误差和 99 分位误差,反映局部离群点。')
lines.append('- `aligned_mae` / `aligned_rmse`:先做最小二乘 scale-shift 对齐再计算,'
'用于区分全局尺度漂移和真正的结构偏差。')
lines.append('- `rel_error`:`mae / mean(|ONNX|)`,便于跨图片比较。')
lines.append('')
lines.append('## 判定参考')
lines.append('')
lines.append(f'- 余弦相似度均值:{format_value(report["summary"]["cosine"]["mean"])}')
lines.append(f'- 对齐后 MAE 均值:{format_value(report["summary"]["aligned_mae"]["mean"])}')
lines.append(f'- 未对齐相对误差均值:'
f'{format_value(report["summary"]["rel_error"]["mean"])}')
lines.append('')
lines.append('若 `cosine` 接近 1 但 `mae` 明显偏大、`aligned_mae` 很小,说明两路输出只存在'
'整体尺度/偏移差异;若 `aligned_mae` 也明显偏大,则更可能是量化本身造成的'
'结构失真,需要结合逐层精度分析继续定位。')
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
def collect_images(dataset):
paths = sorted(p for p in dataset.rglob('*')
if p.is_file() and p.suffix.lower() in EXTENSIONS)
if not paths:
raise ValueError(f'目录中没有图片: {dataset}')
return paths
def compare_result_dirs(args):
from tqdm import tqdm
files = []
for directory in (args.onnx_results, args.axmodel_results):
if not directory.is_dir():
raise ValueError(f'结果目录不存在: {directory}')
mapping = {p.relative_to(directory).as_posix(): p
for p in directory.rglob('*.depth.npy') if p.is_file()}
if not mapping:
raise ValueError(f'结果目录中没有 *.depth.npy: {directory}')
files.append(mapping)
onnx_files, axmodel_files = files
if onnx_files.keys() != axmodel_files.keys():
onnx_only = sorted(onnx_files.keys() - axmodel_files.keys())
axmodel_only = sorted(axmodel_files.keys() - onnx_files.keys())
raise ValueError(f'结果文件未完全配对: ONNX 独有 {len(onnx_only)} 个 '
f'{onnx_only[:5]}, AXMODEL 独有 {len(axmodel_only)} 个 '
f'{axmodel_only[:5]}')
names = sorted(onnx_files)
if args.limit is not None:
names = names[:args.limit]
records = []
for name in tqdm(names, desc='比较深度输出', disable=len(names) == 1):
reference = to_2d(np.load(onnx_files[name], allow_pickle=False), f'ONNX {name}')
actual = to_2d(np.load(axmodel_files[name], allow_pickle=False), f'AXMODEL {name}')
if reference.size == 0 or reference.shape != actual.shape:
raise ValueError(f'{name} 深度图为空或 shape 不一致: '
f'ONNX {reference.shape} vs AXMODEL {actual.shape}')
metrics = compare_arrays(reference, actual)
if args.debug:
logging.info('%s: %s', name, metrics)
records.append({'name': name[:-len('.depth.npy')], 'metrics': metrics,
'onnx_ms': None, 'axmodel_ms': None})
report = {
'mode': 'results',
'created': datetime.now().astimezone().isoformat(timespec='seconds'),
'output_dir': str(args.output.resolve().parent),
'onnx_results': str(args.onnx_results.resolve()),
'axmodel_results': str(args.axmodel_results.resolve()),
'sample_count': len(records), 'limit': args.limit,
'summary': summarize([record['metrics'] for record in records]),
'samples': records,
}
write_markdown(args.output, report)
logging.info('样本数 %d,cosine mean=%.6f,aligned_mae mean=%.6g,报告: %s',
len(records), report['summary']['cosine']['mean'],
report['summary']['aligned_mae']['mean'], args.output)
def main():
parser = argparse.ArgumentParser(
description='计算 ONNX 与 AXMODEL 深度输出的相似度并生成 Markdown 报告',
formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__)
parser.add_argument('--onnx', type=Path, help='FP32 ONNX 模型')
parser.add_argument('--axmodel', type=Path, help='编译后的 axmodel')
parser.add_argument('--onnx-results', type=Path, help='ONNX 结果目录(递归读取 *.depth.npy)')
parser.add_argument('--axmodel-results', type=Path, help='AXMODEL 结果目录(相对路径必须完全配对)')
source = parser.add_mutually_exclusive_group()
source.add_argument('--image', type=Path, help='单张图片')
source.add_argument('--dataset', type=Path, help='图片目录(递归)')
parser.add_argument('--output', type=Path,
default=Path(__file__).resolve().parents[2] / 'results' /
'compare' / 'onnx_vs_axmodel.md',
help='Markdown 输出路径,默认 results/compare/onnx_vs_axmodel.md')
parser.add_argument('--resize-mode', choices=('stretch', 'pad'), default='stretch',
help='两个后端共用的 resize 方式,默认 stretch')
parser.add_argument('--color-order', choices=('rgb', 'bgr'), default='rgb',
help='两个后端共用的颜色顺序,默认 rgb')
parser.add_argument('--onnx-norm', choices=('imagenet', 'raw'), default='imagenet',
help='ONNX 输入归一化,默认 imagenet')
parser.add_argument('--axmodel-norm', choices=('imagenet', 'raw'), default='raw',
help='axmodel 输入归一化,默认 raw(编译时已吸收归一化)')
parser.add_argument('--warmup', type=int, default=3,
help='数据集模式每个后端的 warmup 次数,默认 3')
parser.add_argument('--limit', type=int, help='数据集模式只处理前 N 张')
parser.add_argument('--debug', action='store_true', help='打印逐样本输入/输出统计')
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format='%(message)s')
if args.warmup < 0:
parser.error('--warmup 不能为负数')
if args.limit is not None and args.limit < 1:
parser.error('--limit 必须为正数')
if args.onnx_results is not None or args.axmodel_results is not None:
if args.onnx_results is None or args.axmodel_results is None:
parser.error('--onnx-results 和 --axmodel-results 必须同时提供')
if any(value is not None for value in (args.onnx, args.axmodel, args.image, args.dataset)):
parser.error('结果目录模式不能同时指定模型或图片输入')
compare_result_dirs(args)
return
if args.onnx is None or args.axmodel is None or (args.image is None and args.dataset is None):
parser.error('推理模式必须提供 --onnx、--axmodel 和 --image/--dataset;'
'或使用 --onnx-results 和 --axmodel-results 进行离线比较')
if not args.onnx.is_file():
parser.error(f'--onnx 不存在: {args.onnx}')
if not args.axmodel.is_file():
parser.error(f'--axmodel 不存在: {args.axmodel}')
import onnxruntime as ort
import axengine
onnx_session = ort.InferenceSession(str(args.onnx))
axmodel_session = axengine.InferenceSession(str(args.axmodel))
onnx_input, onnx_size, onnx_layout, onnx_dtype, onnx_output = onnx_interface(onnx_session)
ax_meta, ax_size, ax_layout, ax_dtype, ax_outputs = axmodel_interface(axmodel_session)
if onnx_size != ax_size:
raise ValueError(f'两个后端输入尺寸不一致: ONNX {onnx_size} vs AXMODEL {ax_size}')
if args.image:
if not args.image.is_file():
parser.error(f'--image 不是文件: {args.image}')
images = [args.image]
root = args.image.parent
else:
if not args.dataset.is_dir():
parser.error(f'--dataset 不是目录: {args.dataset}')
images = collect_images(args.dataset)
if args.limit is not None:
images = images[:args.limit]
root = args.dataset
if args.axmodel_norm == 'imagenet' and ax_dtype != np.float32:
parser.error('axmodel 输入为整数类型,不能使用 --axmodel-norm imagenet')
# 数据集模式先 warmup,使用第一张图。
if len(images) > 1 and args.warmup:
first = cv2.imread(str(images[0]), cv2.IMREAD_COLOR)
if first is None:
raise ValueError(f'读取图片失败: {images[0]}')
onnx_tensor, _ = preprocess(first, onnx_size, onnx_layout, onnx_dtype,
args.resize_mode, args.color_order, args.onnx_norm)
ax_tensor, _ = preprocess(first, ax_size, ax_layout, ax_dtype,
args.resize_mode, args.color_order, args.axmodel_norm)
for _ in range(args.warmup):
onnx_session.run([onnx_output], {onnx_input: onnx_tensor})
axmodel_session.run(None, {ax_meta.name: ax_tensor})
logging.info('两个后端各 warmup %d 次', args.warmup)
records = []
for path in images:
name = str(path.relative_to(root)) if path != root else path.name
image = cv2.imread(str(path), cv2.IMREAD_COLOR)
if image is None:
raise ValueError(f'读取图片失败: {path}')
onnx_tensor, _ = preprocess(image, onnx_size, onnx_layout, onnx_dtype,
args.resize_mode, args.color_order, args.onnx_norm)
ax_tensor, _ = preprocess(image, ax_size, ax_layout, ax_dtype,
args.resize_mode, args.color_order, args.axmodel_norm)
start = time.perf_counter()
onnx_raw = onnx_session.run([onnx_output], {onnx_input: onnx_tensor})[0]
onnx_ms = (time.perf_counter() - start) * 1000
start = time.perf_counter()
ax_outputs_data = axmodel_session.run(None, {ax_meta.name: ax_tensor})
axmodel_ms = (time.perf_counter() - start) * 1000
reference = to_2d(onnx_raw, 'ONNX')
actual, ax_output_name = select_ax_output(ax_outputs_data, ax_outputs,
reference.shape)
metrics = compare_arrays(reference, actual)
if not np.isfinite(metrics['cosine']):
raise ValueError(f'{name} 无法计算相似度,请检查输出是否常数')
if args.debug:
logging.info('%s: ONNX min=%.4f max=%.4f mean=%.4f | AXMODEL min=%.4f '
'max=%.4f mean=%.4f | cosine=%.6f mae=%.6g',
name, float(reference.min()), float(reference.max()),
float(reference.mean()), float(actual.min()),
float(actual.max()), float(actual.mean()),
metrics['cosine'], metrics['mae'])
records.append({'name': name, 'metrics': metrics,
'onnx_ms': onnx_ms, 'axmodel_ms': axmodel_ms})
if len(images) == 1:
logging.info('输出绑定: ONNX=%s AXMODEL=%s', onnx_output, ax_output_name)
logging.info('指标: %s', json.dumps(metrics, ensure_ascii=False))
report = {
'created': datetime.now().astimezone().isoformat(timespec='seconds'),
'output_dir': str(args.output.resolve().parent),
'sample_count': len(records),
'limit': args.limit if not args.image else None,
'warmup': args.warmup if len(images) > 1 else 0,
'resize_mode': args.resize_mode,
'color_order': args.color_order,
'onnx': {'path': str(args.onnx.resolve()),
'runtime': 'onnxruntime ' + ort.__version__,
'input': f'{onnx_input} {onnx_tensor.shape}',
'output': onnx_output,
'norm': args.onnx_norm},
'axmodel': {'path': str(args.axmodel.resolve()),
'runtime': getattr(axengine, '__version__', 'unknown'),
'input': f'{ax_meta.name} {tuple(ax_meta.shape)} {np.dtype(ax_dtype).name}',
'output': ax_output_name,
'norm': args.axmodel_norm},
'summary': summarize([record['metrics'] for record in records]),
'samples': records,
}
write_markdown(args.output, report)
logging.info('样本数 %d,cosine mean=%.6f,aligned_mae mean=%.6g,报告: %s',
len(records), report['summary']['cosine']['mean'],
report['summary']['aligned_mae']['mean'], args.output)
if __name__ == '__main__':
main()