Download src/infer/depthart_infer_axmodel.py from AXERA-TECH/DepthART: direct link, hf CLI and curl.
- Browser
- Download file 31.2 kB
-
https://huggingface.co/AXERA-TECH/DepthART/resolve/main/src/infer/depthart_infer_axmodel.py
- Command line
-
hf download hf://AXERA-TECH/DepthART/src/infer/depthart_infer_axmodel.py
-
curl -L -o depthart_infer_axmodel.py https://huggingface.co/AXERA-TECH/DepthART/resolve/main/src/infer/depthart_infer_axmodel.py
31.2 kB
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| DepthART 相对深度 axmodel 板端推理(AX650 + axengine)。 | |
| 随板部署文件(建议放在同一目录): | |
| relative_s_448_default_ax650.axmodel | |
| depthart_infer_axmodel.py | |
| 待测图片或图片目录 | |
| 模型接口(以运行时 get_inputs/get_outputs 实际返回为准): | |
| input [1, 3, 448, 448] uint8 NCHW RGB | |
| depth [1, 1, 448, 448] float32 仿射不变相对深度,数值越大表示越近 | |
| 预处理按编译配置 config_axonnx.json 的 input_processors 执行:tensor_format=RGB、 | |
| src_dtype=U8、mean/std 为 0-255 域的 ImageNet 值,归一化由编译前端完成。因此板端 | |
| 送 0-255 原始 RGB 像素即可,不要再做 /255 或减均值。若 axmodel 输入为 FP32(编译 | |
| 时未配置 input_processors),脚本默认改为 /255 + ImageNet mean/std,与 ONNX 基准 | |
| 一致;可用 --input-norm 显式覆盖。 | |
| 深度还原使用与 ONNX 脚本一致的 bilinear + align_corners=True(linspace + remap), | |
| 保证板端与开发机基准可以逐像素对比。 | |
| 板端计时分为 NPU(session.run)和 CPU(读取、预处理、输出提取/校验、后处理、 | |
| 可视化、写盘),两者分别统计,避免弱 CPU 上的后处理掩盖真实 NPU 性能。 | |
| 单图模式:打印深度统计、NPU/CPU 各阶段耗时,保存并排可视化,不产出评测文件。 | |
| 数据集模式:tqdm 进度 + npu_timing.json;默认快速模式只统计读取/预处理/NPU, | |
| --full-output 才做深度还原、可视化、原始 NPZ 和图片写盘。 | |
| 板端用法(所有路径均为必填): | |
| # 单图验证 | |
| python3 depthart_infer_axmodel.py --axmodel relative_s_448_default_ax650.axmodel \ | |
| --image ./host.jpg --output ./results/ax_single | |
| # 数据集快速性能模式 | |
| python3 depthart_infer_axmodel.py --axmodel relative_s_448_default_ax650.axmodel \ | |
| --dataset ./images --output ./results/board | |
| # 数据集完整输出(原始深度 NPZ、可视化和大图深度) | |
| python3 depthart_infer_axmodel.py --axmodel relative_s_448_default_ax650.axmodel \ | |
| --dataset ../../test_depth --output ./results/board_full --full-output | |
| # 与开发机 ONNX 基准对比(--ref-npz 指向 ONNX 脚本的汇总 NPZ) | |
| python3 depthart_infer_axmodel.py --axmodel relative_s_448_default_ax650.axmodel \ | |
| --dataset ../../test_depth --output ./results/board_check --ref-npz ./onnx_npz | |
| 开发机用法(仅用于脚本语法/参数检查,实际推理需要板端 axengine): | |
| ../../venv/bin/python src/infer/depthart_infer_axmodel.py \ | |
| --axmodel output_axonnx3/relative_s_448_default_ax650.axmodel \ | |
| --dataset /path/images --output results/axmodel_board --limit 50 | |
| 诊断: | |
| --debug 打印输入/输出 tensor 的 shape、dtype、min、max、mean、nan 计数 | |
| --dump-npz 单图时为 .npz 文件路径;数据集时为汇总 .npz 文件路径, | |
| 保存 names/depths/original_sizes,不再每张图一个小文件 | |
| --npz-chunk 数据集模式下每 N 张切一个文件(xxx_0000.npz 等), | |
| 0 表示全部写入同一个文件 | |
| --ref-npz 单图时为参考 .npz,数据集时为参考汇总 .npz(按 names 匹配), | |
| 报告 max/mean/p99 绝对误差、cosine 以及 scale-shift 对齐后误差 | |
| """ | |
| import argparse | |
| import json | |
| import logging | |
| import time | |
| from pathlib import Path | |
| import cv2 | |
| import numpy as np | |
| from tqdm import tqdm | |
| 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'} | |
| STAGES = ('read', 'preprocess', 'npu', 'diagnostics', | |
| 'postprocess', 'visualize', 'write', 'total') | |
| def meta_type(meta): | |
| value = getattr(meta, 'type', None) | |
| if value is None: | |
| value = getattr(meta, 'dtype', '') | |
| return str(value).lower() | |
| def input_spec(meta): | |
| """从运行时输入元信息推导尺寸、layout 和 dtype,不依赖固定名称。""" | |
| try: | |
| shape = [int(v) for v in meta.shape] | |
| except (TypeError, ValueError): | |
| raise ValueError(f'输入必须是静态 4D NCHW/NHWC: {meta.name} {meta.shape}') | |
| if len(shape) != 4 or any(v <= 0 for v in shape): | |
| raise ValueError(f'输入必须是静态 4D NCHW/NHWC: {meta.name} {meta.shape}') | |
| if shape[0] != 1: | |
| raise ValueError(f'仅支持 batch=1: {meta.name} {meta.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'输入必须含 3 通道: {meta.name} {meta.shape}') | |
| type_name = meta_type(meta) | |
| if 'float16' in type_name or 'fp16' in type_name: | |
| dtype = np.float16 | |
| elif 'float' in type_name or 'fp32' in type_name: | |
| dtype = np.float32 | |
| elif 'uint8' in type_name or type_name.endswith('u8'): | |
| dtype = np.uint8 | |
| elif 'int8' in type_name or type_name.endswith('s8'): | |
| dtype = np.int8 | |
| else: | |
| raise ValueError(f'不支持的输入类型: {meta.name} {type_name!r}') | |
| return size, layout, dtype | |
| def preprocess(image, size, color_order, layout, dtype, norm_mode): | |
| """按编译契约生成输入 tensor。 | |
| uint8 直送路径沿用官方 axmodel 流程,在 uint8 域直接 resize;ImageNet | |
| 归一化路径严格按 ONNX 基准的顺序(float 域 /255 -> resize -> mean/std)。 | |
| """ | |
| target_h, target_w = size | |
| if color_order == 'rgb': | |
| image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) | |
| if norm_mode == 'imagenet': | |
| canvas = image.astype(np.float32) / 255.0 | |
| canvas = cv2.resize(canvas, (target_w, target_h), | |
| interpolation=cv2.INTER_CUBIC) | |
| canvas = (canvas - MEAN) / STD | |
| elif dtype != np.uint8: | |
| canvas = cv2.resize(image.astype(np.float32), (target_w, target_h), | |
| interpolation=cv2.INTER_CUBIC) | |
| else: | |
| canvas = cv2.resize(image, (target_w, target_h), | |
| interpolation=cv2.INTER_CUBIC).astype(np.float32) | |
| if dtype == np.uint8: | |
| canvas = np.clip(np.rint(canvas), 0, 255).astype(np.uint8) | |
| elif dtype == np.int8: | |
| raise ValueError('当前脚本不支持 INT8 输入:缺少反量化 scale/zero-point') | |
| else: | |
| canvas = canvas.astype(dtype) | |
| tensor = canvas.transpose(2, 0, 1) if layout == 'NCHW' else canvas | |
| return np.ascontiguousarray(tensor[None]) | |
| def select_output(outputs, metas, input_size): | |
| """axmodel 输出名可能被编译器改写:先按名字,再按 shape 回退。""" | |
| if not isinstance(outputs, (list, tuple)): | |
| outputs = [outputs] | |
| if len(outputs) != len(metas): | |
| raise ValueError(f'输出数量不一致: run 返回 {len(outputs)},' | |
| f'接口声明 {len(metas)}') | |
| for index, meta in enumerate(metas): | |
| if 'depth' in str(meta.name).lower(): | |
| return outputs[index], str(meta.name) | |
| candidates = [] | |
| for index, array in enumerate(outputs): | |
| shape = np.asarray(array).shape | |
| if len(shape) == 4 and shape[1] == 1 and tuple(shape[2:]) == tuple(input_size): | |
| candidates.append(index) | |
| elif len(shape) == 3 and tuple(shape[1:]) == tuple(input_size): | |
| candidates.append(index) | |
| if len(candidates) != 1: | |
| actual = [(m.name, np.asarray(o).shape) for m, o in zip(metas, outputs)] | |
| raise ValueError(f'无法按 name 或 shape 唯一匹配 depth 输出;实际输出: {actual}') | |
| return outputs[candidates[0]], str(metas[candidates[0]].name) | |
| def read_depth(raw, input_size): | |
| array = np.asarray(raw) | |
| if array.ndim == 4 and array.shape[1] == 1: | |
| array = array[0] | |
| if array.ndim == 3 and array.shape[0] == 1: | |
| array = array[0] | |
| if array.shape != tuple(input_size): | |
| raise ValueError(f'depth 输出无效: shape={array.shape},' | |
| f'期望 {tuple(input_size)}') | |
| if not np.issubdtype(array.dtype, np.floating): | |
| raise ValueError(f'depth 输出应为浮点,实际 dtype={array.dtype},' | |
| '整数输出需要已知反量化参数') | |
| array = array.astype(np.float32, copy=False) | |
| if not np.isfinite(array).all(): | |
| raise ValueError('depth 输出包含 nan/inf') | |
| if float(array.std()) < 1e-6: | |
| logging.warning('depth 输出近似常数(std=%.3g):请检查输入归一化/颜色顺序' | |
| '是否与编译配置的 input_processors 一致。', float(array.std())) | |
| return np.ascontiguousarray(array) | |
| def restore_depth(depth, original_size): | |
| """按官方推理的 bilinear + align_corners=True 还原到原图尺寸。""" | |
| height, width = original_size | |
| src_h, src_w = depth.shape | |
| if (src_h, src_w) == (height, width): | |
| return depth.copy() | |
| xs = np.linspace(0.0, src_w - 1, width, dtype=np.float32) | |
| ys = np.linspace(0.0, src_h - 1, height, dtype=np.float32) | |
| map_x, map_y = np.meshgrid(xs, ys) | |
| return cv2.remap(depth, map_x, map_y, interpolation=cv2.INTER_LINEAR, | |
| borderMode=cv2.BORDER_REPLICATE) | |
| def visualize(image, depth): | |
| finite = depth[np.isfinite(depth)] | |
| low, high = np.percentile(finite, (2, 98)) | |
| value = np.clip((depth - low) / max(float(high - low), 1e-6), 0, 1) | |
| colored = cv2.applyColorMap((value * 255).astype(np.uint8), cv2.COLORMAP_INFERNO) | |
| panel = np.concatenate((image, colored), axis=1) | |
| legend = np.full((56, panel.shape[1], 3), 255, dtype=np.uint8) | |
| ramp = np.tile(np.linspace(0, 255, max(panel.shape[1] - 20, 1), | |
| dtype=np.uint8), (12, 1)) | |
| legend[5:17, 10:10 + ramp.shape[1]] = cv2.applyColorMap(ramp, cv2.COLORMAP_INFERNO) | |
| cv2.putText(legend, f'Image | Relative depth (2-98%): {low:.3f} -> {high:.3f}', | |
| (10, 42), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (30, 30, 30), 1, cv2.LINE_AA) | |
| return np.concatenate((panel, legend), axis=0) | |
| def log_tensor(name, value): | |
| logging.info('%s: shape=%s dtype=%s min=%.6g max=%.6g mean=%.6g nan=%d inf=%d', | |
| name, value.shape, value.dtype, float(np.min(value)), | |
| float(np.max(value)), float(np.mean(value)), | |
| int(np.isnan(value).sum()), int(np.isinf(value).sum())) | |
| def compare_arrays(depth, expected, source='', preprocessing=''): | |
| expected = np.asarray(expected) | |
| if expected.ndim == 4 and expected.shape[1] == 1: | |
| expected = expected[0] | |
| if expected.shape != depth.shape: | |
| raise ValueError(f'参考 depth shape 不符: {expected.shape} vs {depth.shape} ({source})') | |
| if not np.isfinite(expected).all(): | |
| raise ValueError(f'参考 depth 含 nan/inf: {source}') | |
| actual64 = depth.astype(np.float64).ravel() | |
| expected64 = expected.astype(np.float64).ravel() | |
| diff = np.abs(actual64 - expected64) | |
| cosine = float(np.dot(actual64, expected64) | |
| / (np.linalg.norm(actual64) * np.linalg.norm(expected64) + 1e-12)) | |
| # 相对深度存在全局 scale/offset 漂移,对齐后误差更能反映结构偏差。 | |
| slope, offset = np.polyfit(expected64, actual64, 1) | |
| aligned = np.abs(actual64 - (slope * expected64 + offset)) | |
| metrics = {'cosine': cosine, | |
| 'max_abs': float(diff.max()), | |
| 'mean_abs': float(diff.mean()), | |
| 'p99_abs': float(np.percentile(diff, 99)), | |
| 'scale': float(slope), 'offset': float(offset), | |
| 'aligned_mean_abs': float(aligned.mean()), | |
| 'aligned_p99_abs': float(np.percentile(aligned, 99)), | |
| 'reference_preprocessing': preprocessing} | |
| logging.info('参考对比: %s', metrics) | |
| return metrics | |
| def compare_reference(depth, reference_path): | |
| with np.load(reference_path, allow_pickle=False) as reference: | |
| if 'depth' not in reference: | |
| raise ValueError(f'参考 NPZ 缺少 depth: {reference_path}') | |
| expected = np.asarray(reference['depth']) | |
| preprocessing = (str(reference['preprocessing']) | |
| if 'preprocessing' in reference else '') | |
| return compare_arrays(depth, expected, str(reference_path), preprocessing) | |
| def load_reference_dataset(path): | |
| """读取汇总 NPZ,返回 {name: depth}。""" | |
| path = Path(path) | |
| if path.is_file(): | |
| with np.load(path, allow_pickle=False) as ref: | |
| if 'depths' not in ref or 'names' not in ref: | |
| raise ValueError(f'汇总 NPZ 需要 names/depths: {path}') | |
| return {str(n): d for n, d in zip(ref['names'], ref['depths'])} | |
| raise ValueError(f'数据集模式 --ref-npz 需要汇总 NPZ 文件: {path}') | |
| def preprocessing_description(color_order, norm_mode): | |
| norm = ('imagenet' if norm_mode == 'imagenet' else 'raw-0-255') | |
| return f'color={color_order}; resize=stretch; norm={norm}' | |
| def save_raw_npz(path, depth, original_size, input_size, color_order, norm_mode): | |
| # 板端 CPU 弱,savez_compressed 的 DEFLATE 对 float32 收益极小却耗时明显,用未压缩保存。 | |
| np.savez(path, depth=depth, | |
| original_size=np.array(original_size, dtype=np.int32), | |
| input_size=np.array(input_size, dtype=np.int32), | |
| preprocessing=preprocessing_description(color_order, norm_mode)) | |
| def flush_dataset_npz(path, records, chunk_index, input_size, color_order, norm_mode): | |
| """把缓冲区写成汇总 NPZ:names / depths / original_sizes。 | |
| 板端弱 CPU 上 DEFLATE 对 float32 收益极小却耗时明显,这里保持未压缩。 | |
| chunk_index 为 None 时写入 path 本身,否则按 chunk 编号追加后缀。 | |
| """ | |
| if not records: | |
| return | |
| target = path if chunk_index is None else path.with_name( | |
| f'{path.stem}_{chunk_index:04d}{path.suffix}') | |
| target.parent.mkdir(parents=True, exist_ok=True) | |
| np.savez( | |
| target, | |
| names=np.array([r[0] for r in records]), | |
| depths=np.stack([r[1] for r in records]), | |
| original_sizes=np.array([r[2] for r in records], dtype=np.int32), | |
| input_size=np.array(input_size, dtype=np.int32), | |
| preprocessing=preprocessing_description(color_order, norm_mode)) | |
| 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 summarize(samples): | |
| result = {} | |
| for stage in STAGES: | |
| values = np.array([sample[stage] for sample in samples], dtype=np.float64) | |
| result[stage] = {'mean_ms': float(values.mean()), | |
| 'p50_ms': float(np.percentile(values, 50)), | |
| 'p90_ms': float(np.percentile(values, 90)), | |
| 'p99_ms': float(np.percentile(values, 99))} | |
| return result | |
| def run_single(session, input_meta, output_metas, input_size, layout, dtype, | |
| norm_mode, args): | |
| stem = args.image.stem | |
| timings = dict.fromkeys(STAGES, 0.0) | |
| total_start = time.perf_counter() | |
| start = time.perf_counter() | |
| image = cv2.imread(str(args.image), cv2.IMREAD_COLOR) | |
| if image is None: | |
| raise ValueError(f'读取图片失败: {args.image}') | |
| timings['read'] = (time.perf_counter() - start) * 1000 | |
| start = time.perf_counter() | |
| tensor = preprocess(image, input_size, args.color_order, layout, dtype, norm_mode) | |
| timings['preprocess'] = (time.perf_counter() - start) * 1000 | |
| start = time.perf_counter() | |
| outputs = session.run(None, {input_meta.name: tensor}) | |
| timings['npu'] = (time.perf_counter() - start) * 1000 | |
| start = time.perf_counter() | |
| raw, output_name = select_output(outputs, output_metas, input_size) | |
| depth = read_depth(raw, input_size) | |
| comparison = None | |
| if args.debug: | |
| log_tensor(input_meta.name, tensor) | |
| log_tensor('depth', depth) | |
| if args.ref_npz: | |
| comparison = compare_reference(depth, args.ref_npz) | |
| timings['diagnostics'] = (time.perf_counter() - start) * 1000 | |
| start = time.perf_counter() | |
| restored = restore_depth(depth, image.shape[:2]) | |
| timings['postprocess'] = (time.perf_counter() - start) * 1000 | |
| start = time.perf_counter() | |
| panel = visualize(image, restored) | |
| timings['visualize'] = (time.perf_counter() - start) * 1000 | |
| start = time.perf_counter() | |
| args.output.mkdir(parents=True, exist_ok=True) | |
| image_path = args.output / f'{stem}_depth.jpg' | |
| if not cv2.imwrite(str(image_path), panel, [cv2.IMWRITE_JPEG_QUALITY, 90]): | |
| raise OSError(f'保存可视化失败: {image_path}') | |
| np.save(str(args.output / f'{stem}_depth.npy'), restored) | |
| npz_path = None | |
| if args.dump_npz: | |
| npz_path = Path(args.dump_npz) | |
| npz_path.parent.mkdir(parents=True, exist_ok=True) | |
| save_raw_npz(npz_path, depth, image.shape[:2], input_size, | |
| args.color_order, norm_mode) | |
| timings['write'] = (time.perf_counter() - start) * 1000 | |
| timings['total'] = (time.perf_counter() - total_start) * 1000 | |
| cpu_ms = (timings['read'] + timings['preprocess'] + timings['diagnostics'] | |
| + timings['postprocess'] + timings['visualize'] + timings['write']) | |
| logging.info('深度(模型分辨率): min=%.4f max=%.4f mean=%.4f p50=%.4f', | |
| float(depth.min()), float(depth.max()), float(depth.mean()), | |
| float(np.percentile(depth, 50))) | |
| logging.info('深度(原图分辨率): min=%.4f max=%.4f mean=%.4f', | |
| float(restored.min()), float(restored.max()), float(restored.mean())) | |
| logging.info('输出绑定: %s', output_name) | |
| logging.info('NPU 推理: %.2f ms;CPU 处理: %.2f ms;端到端: %.2f ms' | |
| '(单图为首次推理,含运行时首次执行开销;性能请用数据集模式)', | |
| timings['npu'], cpu_ms, timings['total']) | |
| logging.info('各阶段耗时: %s', | |
| ' '.join(f'{key}={value:.2f} ms' for key, value in timings.items())) | |
| if comparison is not None: | |
| logging.info('参考对比结果: %s', comparison) | |
| logging.info('结果目录: %s(可视化 %s%s)', args.output, image_path.name, | |
| f',原始 NPZ {npz_path.name}' if npz_path else '') | |
| def run_dataset(session, input_meta, output_metas, input_size, layout, dtype, | |
| norm_mode, runtime_version, args): | |
| images = collect_images(args.dataset) | |
| if args.limit is not None: | |
| images = images[:args.limit] | |
| args.output.mkdir(parents=True, exist_ok=True) | |
| full_output = args.full_output | |
| if full_output: | |
| logging.warning('--full-output: CPU 侧深度还原、可视化与写盘会拉高端到端耗时,' | |
| '不能只看 NPU 时间。') | |
| else: | |
| logging.info('数据集快速模式:只统计 read/preprocess/npu,' | |
| '跳过深度还原、可视化和结果写盘。') | |
| ref_map = load_reference_dataset(args.ref_npz) if args.ref_npz else None | |
| preprocessing = preprocessing_description(args.color_order, norm_mode) | |
| dump_path = Path(args.dump_npz) if args.dump_npz else None | |
| if dump_path is not None and dump_path.is_dir(): | |
| raise ValueError('数据集模式 --dump-npz 请传 .npz 文件路径,' | |
| '不再按目录为每张图生成小文件') | |
| dump_records, dump_chunk = [], 0 | |
| first = cv2.imread(str(images[0]), cv2.IMREAD_COLOR) | |
| if first is None: | |
| raise ValueError(f'读取图片失败: {images[0]}') | |
| warm = preprocess(first, input_size, args.color_order, layout, dtype, norm_mode) | |
| warmup_ms = 0.0 | |
| if args.warmup: | |
| start = time.perf_counter() | |
| for _ in range(args.warmup): | |
| session.run(None, {input_meta.name: warm}) | |
| warmup_ms = (time.perf_counter() - start) * 1000 | |
| samples = [] | |
| sample_info = [] | |
| for path in tqdm(images, desc='AX 相对深度', unit='image'): | |
| relative = path.relative_to(args.dataset) | |
| timings = dict.fromkeys(STAGES, 0.0) | |
| total_start = time.perf_counter() | |
| start = time.perf_counter() | |
| image = cv2.imread(str(path), cv2.IMREAD_COLOR) | |
| if image is None: | |
| raise ValueError(f'读取图片失败: {path}') | |
| timings['read'] = (time.perf_counter() - start) * 1000 | |
| start = time.perf_counter() | |
| tensor = preprocess(image, input_size, args.color_order, layout, dtype, norm_mode) | |
| timings['preprocess'] = (time.perf_counter() - start) * 1000 | |
| start = time.perf_counter() | |
| outputs = session.run(None, {input_meta.name: tensor}) | |
| timings['npu'] = (time.perf_counter() - start) * 1000 | |
| start = time.perf_counter() | |
| raw, output_name = select_output(outputs, output_metas, input_size) | |
| depth = read_depth(raw, input_size) | |
| comparison = None | |
| if args.debug and path == images[0]: | |
| log_tensor(input_meta.name, tensor) | |
| log_tensor('depth', depth) | |
| if dump_path is not None: | |
| dump_records.append((str(relative), depth, | |
| np.array(image.shape[:2], dtype=np.int32))) | |
| if args.npz_chunk and len(dump_records) >= args.npz_chunk: | |
| flush_dataset_npz(dump_path, dump_records, dump_chunk, | |
| input_size, args.color_order, norm_mode) | |
| dump_records.clear() | |
| dump_chunk += 1 | |
| if ref_map is not None: | |
| key = str(relative) | |
| if key not in ref_map: | |
| raise ValueError(f'参考 NPZ 缺少样本: {key}') | |
| comparison = compare_arrays(depth, ref_map[key], | |
| f'{args.ref_npz}:{key}', preprocessing) | |
| timings['diagnostics'] = (time.perf_counter() - start) * 1000 | |
| restored = None | |
| if full_output: | |
| start = time.perf_counter() | |
| restored = restore_depth(depth, image.shape[:2]) | |
| timings['postprocess'] = (time.perf_counter() - start) * 1000 | |
| start = time.perf_counter() | |
| panel = visualize(image, restored) | |
| timings['visualize'] = (time.perf_counter() - start) * 1000 | |
| start = time.perf_counter() | |
| prefix = args.output / relative | |
| prefix.parent.mkdir(parents=True, exist_ok=True) | |
| if not cv2.imwrite(str(prefix) + '.depth.jpg', panel, | |
| [cv2.IMWRITE_JPEG_QUALITY, 90]): | |
| raise OSError(f'保存可视化失败: {prefix}.depth.jpg') | |
| timings['write'] = (time.perf_counter() - start) * 1000 | |
| timings['total'] = (time.perf_counter() - total_start) * 1000 | |
| samples.append(timings) | |
| sample_info.append({'name': str(relative), | |
| 'original_size': list(image.shape[:2]), | |
| 'depth_min': float((restored if restored is not None | |
| else depth).min()), | |
| 'depth_max': float((restored if restored is not None | |
| else depth).max()), | |
| 'timing_ms': timings, | |
| 'reference_comparison': comparison}) | |
| if dump_path is not None: | |
| flush_dataset_npz(dump_path, dump_records, | |
| None if not args.npz_chunk else dump_chunk, | |
| input_size, args.color_order, norm_mode) | |
| stats = summarize(samples) | |
| total_ms = sum(sample['total'] for sample in samples) | |
| cpu_stages = ('read', 'preprocess', 'diagnostics', 'postprocess', 'visualize', 'write') | |
| cpu_total_ms = sum(stats[stage]['mean_ms'] for stage in cpu_stages) | |
| report = {'model': str(Path(args.axmodel).resolve()), | |
| 'runtime': runtime_version, | |
| 'task': 'relative-depth', | |
| 'input_name': input_meta.name, 'output_name': output_name, | |
| 'input_size': list(input_size), 'input_layout': layout, | |
| 'input_dtype': np.dtype(dtype).name, | |
| 'color_order': args.color_order, | |
| 'preprocessing': preprocessing_description(args.color_order, norm_mode), | |
| 'batch_size': 1, 'sample_count': len(samples), | |
| 'full_output': full_output, 'warmup_count': args.warmup, | |
| 'warmup_ms': warmup_ms, 'total_ms': total_ms, | |
| 'fps': len(samples) * 1000 / total_ms, | |
| 'npu_mean_ms': stats['npu']['mean_ms'], | |
| 'cpu_total_mean_ms': cpu_total_ms, | |
| 'timing_scope': 'npu=仅 session.run;cpu=读取+预处理+输出提取/校验+' | |
| '诊断+后处理+可视化+写盘;total=单样本端到端。排除' | |
| '模型加载、session 初始化和 warmup。快速模式的 ' | |
| 'postprocess/visualize/write 记为 0。', | |
| 'stages': stats, 'samples': sample_info} | |
| with (args.output / 'npu_timing.json').open('w', encoding='utf-8') as file: | |
| json.dump(report, file, ensure_ascii=False, indent=2, allow_nan=False) | |
| for stage, values in stats.items(): | |
| logging.info('%s: mean=%.2f p50=%.2f p90=%.2f p99=%.2f ms', | |
| stage, values['mean_ms'], values['p50_ms'], | |
| values['p90_ms'], values['p99_ms']) | |
| logging.info('%d 张图片,NPU mean=%.2f ms,CPU mean=%.2f ms,' | |
| 'total=%.2f s,FPS=%.3f,结果目录: %s', | |
| len(samples), stats['npu']['mean_ms'], cpu_total_ms, | |
| total_ms / 1000, report['fps'], args.output) | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='DepthART 相对深度 axmodel 板端推理(AX650)', | |
| formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__) | |
| parser.add_argument('--axmodel', type=Path, required=True, help='编译后的 axmodel') | |
| source = parser.add_mutually_exclusive_group(required=True) | |
| source.add_argument('--image', type=Path, help='单张图片') | |
| source.add_argument('--dataset', type=Path, help='图片目录(递归)') | |
| parser.add_argument('--output', type=Path, required=True, help='结果目录') | |
| parser.add_argument('--color-order', choices=('rgb', 'bgr'), default='rgb', | |
| help='rgb=与官方预处理一致(BGR->RGB);bgr=按编译配置' | |
| 'input_processors.src_format 直送') | |
| parser.add_argument('--input-norm', choices=('auto', 'raw', 'imagenet'), | |
| default='auto', | |
| help='auto=U8 送 0-255、浮点用 ImageNet mean/std;' | |
| 'raw=直送 0-255;imagenet=/255+mean/std(仅浮点输入)') | |
| parser.add_argument('--warmup', type=int, default=3, | |
| help='数据集模式 warmup 次数,默认 3') | |
| parser.add_argument('--limit', type=int, help='数据集模式只处理前 N 张') | |
| parser.add_argument('--full-output', action='store_true', | |
| help='数据集模式执行完整后处理、可视化和结果写盘(含 NPZ)') | |
| parser.add_argument('--debug', action='store_true', help='打印输入/输出统计') | |
| parser.add_argument('--dump-npz', | |
| help='单图:NPZ 文件;数据集:汇总 NPZ 文件(names/depths/original_sizes)') | |
| parser.add_argument('--npz-chunk', type=int, default=0, | |
| help='数据集模式每 N 张切一个 NPZ 文件,0 表示全部写入一个文件') | |
| parser.add_argument('--ref-npz', type=Path, help='参考 NPZ 文件或目录') | |
| args = parser.parse_args() | |
| logging.basicConfig(level=logging.INFO, format='%(message)s') | |
| if args.warmup < 0: | |
| parser.error('--warmup 不能为负数') | |
| if args.npz_chunk < 0: | |
| parser.error('--npz-chunk 不能为负数') | |
| if args.limit is not None and args.limit < 1: | |
| parser.error('--limit 必须为正数') | |
| if args.image: | |
| if not args.image.is_file(): | |
| parser.error(f'--image 不是文件: {args.image}') | |
| if args.ref_npz and not args.ref_npz.is_file(): | |
| parser.error('单图模式 --ref-npz 必须是文件') | |
| if args.limit is not None or args.full_output: | |
| logging.warning('单图模式忽略 --limit/--full-output') | |
| else: | |
| if not args.dataset.is_dir(): | |
| parser.error(f'--dataset 不是目录: {args.dataset}') | |
| try: | |
| import axengine | |
| except ImportError as error: | |
| raise SystemExit('未找到 axengine:该脚本需在板端运行(pip 安装 axengine/' | |
| 'pyaxengine 后重试)') from error | |
| session = axengine.InferenceSession(str(args.axmodel)) | |
| inputs = session.get_inputs() | |
| output_metas = session.get_outputs() | |
| logging.info('模型输入: %s', [(m.name, m.shape, meta_type(m)) for m in inputs]) | |
| logging.info('模型输出: %s', [(m.name, m.shape, meta_type(m)) for m in output_metas]) | |
| if len(inputs) != 1: | |
| raise ValueError(f'模型应有 1 个输入,实际 ' | |
| f'{[(m.name, m.shape) for m in inputs]}') | |
| input_meta = inputs[0] | |
| input_size, layout, dtype = input_spec(input_meta) | |
| if dtype == np.int8: | |
| raise ValueError('当前脚本不支持 INT8 输入(缺少反量化参数);请把编译配置的 ' | |
| 'input_processors.src_dtype 设为 U8 后重编译') | |
| if args.input_norm == 'auto': | |
| norm_mode = 'raw' if dtype == np.uint8 else 'imagenet' | |
| else: | |
| norm_mode = args.input_norm | |
| if dtype == np.uint8 and norm_mode == 'imagenet': | |
| raise ValueError('输入类型为 uint8,不能在推理时做 ImageNet 归一化;' | |
| '归一化必须由编译前端 input_processors 或校准口径保证') | |
| if dtype != np.uint8 and norm_mode == 'raw': | |
| logging.warning('输入为浮点但按 0-255 直送(--input-norm raw),' | |
| '仅用于诊断,需与编译前端/校准口径一致。') | |
| logging.info('batch=1,输入尺寸: %s,layout=%s,dtype=%s,颜色顺序=%s,' | |
| '预处理=%s,resize=stretch', input_size, layout, | |
| np.dtype(dtype).name, args.color_order, | |
| 'ImageNet mean/std (/255)' if norm_mode == 'imagenet' | |
| else '0-255 原始像素') | |
| if dtype == np.uint8 and norm_mode == 'raw': | |
| logging.info('提示:uint8 输入按编译契约送 0-255;ImageNet 归一化应由 ' | |
| 'config_axonnx.json 的 input_processors 在模型内完成。') | |
| runtime_version = getattr(axengine, '__version__', 'unknown') | |
| if args.image: | |
| run_single(session, input_meta, output_metas, input_size, layout, dtype, | |
| norm_mode, args) | |
| else: | |
| run_dataset(session, input_meta, output_metas, input_size, layout, dtype, | |
| norm_mode, runtime_version, args) | |
| if __name__ == '__main__': | |
| main() | |