当前位置: 首页 > ops >正文

ReID/OSNet 算法模型量化转换实践

ReID 算法,全称是“行人再识别(Person Re-Identification)”算法,可应用于智能驾驶或智能机器人陪伴等领域。它的目标是在不同摄像头或不同时间拍摄的多张图像中,准确识别出是否为同一个人。本文以 ReID 中比较有名的 OSNet 模型为例,讲解如何在地平线计算平台量化该模型。

OSNet(Omni-Scale Network)是 行人再识别 任务中非常流行的一种深度学习架构,旨在高效提取行人图像中不同尺度的判别特征。

核心思想:Omni-Scale Feature Learning

OSNet 的核心思想是:在一个卷积块中同时捕捉多种尺度的信息,而不是像传统方法那样堆叠多个尺度层。

传统 ReID 网络可能在不同层捕捉不同尺度(如局部细节 vs 整体结构),但 OSNet 尝试在每一层同时捕捉小尺度和大尺度的信息。

osnet_ain_x1_0OSNet 系列中性能较强的一个变体,其全称是:

OSNet-AIN-x1.0:Omni-Scale Network with Adaptive Instance Normalization(自适应实例归一化)

特点解析

  1. 多尺度特征学习(OSNet 核心)
  • OSBlock 模块引入多个不同感受野的卷积分支。
  • 实现:局部细节与全局上下文同时建模。
  • 相比传统 ResNet,有更强的判别性和更少的参数。
  1. AIN:自适应实例****归一化
  • Adaptive Instance Normalization 是一种可以调节风格的归一化方法,常用于领域自适应。
  • 在 ReID 中,行人图像来自不同摄像头或环境,AIN 可以自适应不同图像风格,减轻 域偏移(domain shift) 的影响。
  • 提升跨摄像头、跨数据集的识别能力。

一、源码导出 ONNX

1.1 克隆代码仓库

克隆代码仓库的 master 分支。

git clone https://github.com/KaiyangZhou/deep-person-reid.git

之后按照官方指导搭建好环境。

1.2 下载预训练权重

使用 Multi-source domain generalization 章节 osnet_ain_x1_0 的 MS+D+C->M 权重 osnet_ain_ms_d_c.pth.tar。

https://kaiyangzhou.github.io/deep-person-reid/MODEL_ZOO

img

1.3 导出 onnx

将 osnet_ain_ms_d_c.pth.tar 权重文件存放到项目根目录,编写并运行 onnx 导出脚本。

import torch
from torchreid.utils.feature_extractor import FeatureExtractordef export_onnx(model, im):f = 'osnet_ain_x1_0.onnx'torch.onnx.export(model.cpu(),im.cpu(),f,opset_version=11,do_constant_folding=True,input_names=['input'],output_names=['output'])return 0if __name__ == "__main__":extractor = FeatureExtractor(model_name="osnet_ain_x1_0",model_path="./osnet_ain_ms_d_c.pth.tar",device=str('cpu'))im = torch.zeros(1, 3, 256, 128).to('cpu')export_onnx(extractor.model.eval(), im)

1.4 检查 onnx 和 pytorch 一致性

import onnx
import torch
import onnxruntime as ort
import numpy as np
from pathlib import Path
from torchreid.utils.feature_extractor import FeatureExtractordef cosine_similarity(a, b):a = np.array(a)b = np.array(b)return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))def max_difference(a, b):a = np.array(a).ravel()b = np.array(b).ravel()diff = np.abs(a - b)max_diff = np.max(diff)max_idx = np.argmax(diff)print(f"最大差值: {max_diff},位置索引: {max_idx}")return max_diff, max_idxif __name__ == "__main__":extractor = FeatureExtractor(model_name="osnet_ain_x1_0",model_path="./osnet_ain_ms_d_c.pth.tar",device=str('cpu'))im = torch.randn(1, 3, 256, 128).to('cpu')with torch.no_grad():pt_model = extractor.model.eval()output = pt_model(im)onnx_path = 'osnet_ain_x1_0.onnx'session = ort.InferenceSession(onnx_path, providers=['CPUExecutionProvider'])input_tensor =im.numpy()input_name = session.get_inputs()[0].nameoutputs = session.run(None, {input_name: input_tensor})output_tensor = outputs[0]print(cosine_similarity(output.numpy().reshape(-1), output_tensor.reshape(-1)))max_difference(output.numpy().reshape(-1), output_tensor.reshape(-1))

如果多次运行,相似度打印均为 1,且最大差值均在 e-5 以内,则可认为 onnx 和 pytorch 的输出保持一致。

二、PTQ 量化

2.1 生成校准数据

可以从官方数据集(Market-1501-v15.09.15)中选择数百张 jpg 图片用来生成校准数据。

在生成校准数据前,需要明确预处理参数,在源码 deep-person-reid-master/torchreid/data/transforms.py 中,可以得知训练时会将图片直接 resize 成模型输入尺寸,并使用 imagenet 数据集的标准归一化参数,即:

norm_mean = [0.485, 0.456, 0.406]
norm_std = [0.229, 0.224, 0.225]

且由归一化参数可知,模型训练时使用的色彩通道顺序是 rgb。

此时可以基于 horizon_model_convert_sample 提供的脚本生成校准数据,只需基于 02_preprocess.sh 和 preprocess.py 脚本做如下改动:

python3 ../../../data_preprocess.py \--src_dir ./market_1501_jpg \    # 从Market-1501-v15.09.15数据集挑选的jpg图片--dst_dir ./calib_data_rgb_f32 \ # 生成的校准数据文件夹--pic_ext .rgb \                 # 色彩通道顺序--read_mode opencv \             # 读图方式使用opencv--saved_data_type float32 \      # 校准数据的数据类型使用float32--cal_img_num 300                # 生成300份校准数据
from horizon_tc_ui.data.transformer import (BGR2RGBTransformer, HWC2CHWTransformer, ResizeTransformer)def calibration_transformers():transformers = [ResizeTransformer(target_size=(256, 128)),  # 直接对图像做resizeHWC2CHWTransformer(),                       # 从HWC更改为CHWBGR2RGBTransformer(data_format="CHW"),      # 将色彩通道顺序更改为RGB]return transformers

由于归一化计算会集成到 PTQ 生成模型的预处理节点中,因此校准数据无需做归一化处理。

2.2 量化编译

推荐使用如下 YAML 配置:

calibration_parameters:cal_data_dir: calib_data_rgb_f32cal_data_type: float32calibration_type: maxmax_percentile: 0.99999optimization: set_all_nodes_int16run_on_cpu: "/conv1/bn/InstanceNormalization_mean1;/conv1/bn/InstanceNormalization_sub1;/conv1/bn/InstanceNormalization_mul1;/conv1/bn/InstanceNormalization_mean2;/conv1/bn/InstanceNormalization_reciprocal;/conv1/bn/InstanceNormalization_mul2;/conv1/bn/InstanceNormalization_mul3;/conv2/conv2.0/IN/InstanceNormalization_mean1;/conv2/conv2.0/IN/InstanceNormalization_sub1;/conv2/conv2.0/IN/InstanceNormalization_mul1;/conv2/conv2.0/IN/InstanceNormalization_mean2;/conv2/conv2.0/IN/InstanceNormalization_reciprocal;/conv2/conv2.0/IN/InstanceNormalization_mul2;/conv2/conv2.0/IN/InstanceNormalization_mul3"
compiler_parameters:jobs: 32optimize_level: O3
input_parameters:input_name: inputinput_shape: 1x3x256x128input_type_rt: nv12input_type_train: rgbinput_layout_train: NCHWnorm_type: 'data_mean_and_scale'mean_value: 123.675 116.28 103.53scale_value: 0.01712475383 0.0175070028 0.0174291938998
model_parameters:march: bayes-eonnx_model: osnet_ain_x1_0.onnxoutput_model_file_prefix: reidworking_dir: model_output

这里使用 nv12 作为板端模型的输入,mean_value 和 scale_value 由归一化参数计算得到。同时,考虑到 InstanceNormalization 算子的量化风险较高,因此为模型中的前两个 InstanceNormalization 配置浮点计算精度,YAML 中 run_on_cpu 配置的算子较多是因为 InstanceNormalization 会被 PTQ 拆分成多个算子。

可以使用以下命令编译:

hb_mapper makertbin --config osnet_config.yaml --model-type onnx

编译完成后,日志打印的相似度如下(quantized.onnx 对比 optimized.onnx):

=========================================================================
Output  Cosine Similarity  L1 Distance  L2 Distance  Chebyshev Distance  
-------------------------------------------------------------------------
output  0.995540           0.057718     0.008716     0.482978

余弦相似度>0.99,可初步认为量化精度满足需求。

2.3 验证 PTQ 生成 onnx 的输出相似度

在编译流程结束后,可以手动验证各 PTQ 生成物和原始 onnx 的输出相似度,及时发现可能的精度问题。

*请根据不同的计算平台修改代码。

import cv2 
import numpy as np 
from PIL import Image 
from horizon_tc_ui import HB_ONNXRuntime        def cosine_similarity(a, b):a = np.array(a)b = np.array(b)return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))def bgr2nv12(image):image = image.astype(np.uint8) height, width = image.shape[0], image.shape[1] yuv420p = cv2.cvtColor(image, cv2.COLOR_BGR2YUV_I420).reshape((height * width * 3 // 2, )) y = yuv420p[:height * width] uv_planar = yuv420p[height * width:].reshape((2, height * width // 4)) uv_packed = uv_planar.transpose((1, 0)).reshape((height * width // 2, )) nv12 = np.zeros_like(yuv420p) nv12[:height * width] = y nv12[height * width:] = uv_packed return nv12 def nv12Toyuv444(nv12, target_size): height = target_size[0] width = target_size[1] nv12_data = nv12.flatten() yuv444 = np.empty([height, width, 3], dtype=np.uint8) yuv444[:, :, 0] = nv12_data[:width * height].reshape(height, width) u = nv12_data[width * height::2].reshape(height // 2, width // 2) yuv444[:, :, 1] = Image.fromarray(u).resize((width, height),resample=0) v = nv12_data[width * height + 1::2].reshape(height // 2, width // 2) yuv444[:, :, 2] = Image.fromarray(v).resize((width, height),resample=0) return yuv444 def rgb_preprocess(img_path):bgr_input = cv2.imread(img_path)bgr_input = cv2.resize(bgr_input, (128,256), interpolation=cv2.INTER_LINEAR)rgb_input = cv2.cvtColor(bgr_input, cv2.COLOR_BGR2RGB)mean = np.array([123.675, 116.28, 103.53], dtype=np.float32)scale = np.array([0.01712475383, 0.0175070028, 0.0174291938998], dtype=np.float32)rgb_input = (rgb_input - mean) * scalergb_input = rgb_input[np.newaxis,:,:,:]rgb_input = np.transpose(rgb_input, (0, 3, 1, 2))return rgb_inputdef yuv444_preprocess(img_path):bgr_input = cv2.imread(img_path)bgr_input = cv2.resize(bgr_input, (128,256), interpolation=cv2.INTER_LINEAR)nv12_input = bgr2nv12(bgr_input)yuv444 = nv12Toyuv444(nv12_input, (256,128))yuv444_nhwc = yuv444[np.newaxis,:,:,:]yuv444_nchw = np.transpose(yuv444_nhwc, (0, 3, 1, 2))return yuv444_nhwc, yuv444_nchwdef main(): rgb_input = rgb_preprocess("test_img.jpg")yuv444_nhwc, yuv444_nchw = yuv444_preprocess("test_img.jpg")sess = HB_ONNXRuntime(model_file="./osnet_ain_x1_0.onnx")input_names = sess.input_namesoutput_names = sess.output_namesinput_info = {input_names[0]: rgb_input}onnx_output = sess.run(output_names, input_info)sess = HB_ONNXRuntime(model_file="./model_output/reid_original_float_model.onnx")input_info = {input_names[0]: yuv444_nchw}ori_output = sess.run(output_names, input_info)sess = HB_ONNXRuntime(model_file="./model_output/reid_optimized_float_model.onnx")input_info = {input_names[0]: yuv444_nchw}opt_output = sess.run(output_names, input_info)sess = HB_ONNXRuntime(model_file="./model_output/reid_calibrated_model.onnx")input_info = {input_names[0]: yuv444_nchw}calib_output = sess.run(output_names, input_info)sess = HB_ONNXRuntime(model_file="./model_output/reid_quantized_model.onnx")input_info = {input_names[0]: yuv444_nhwc}quant_output = sess.run(output_names, input_info)print("onnx_ori ", cosine_similarity(onnx_output[0].reshape(-1), ori_output[0].reshape(-1)))print("onnx_opt ", cosine_similarity(onnx_output[0].reshape(-1), opt_output[0].reshape(-1)))print("onnx_calib ", cosine_similarity(onnx_output[0].reshape(-1), calib_output[0].reshape(-1)))print("onnx_quant ", cosine_similarity(onnx_output[0].reshape(-1), quant_output[0].reshape(-1)))if __name__ == '__main__':main()

PTQ 各阶段生成的 onnx 和原始 onnx 的相似度打印结果如下:

onnx_ori  0.99779123
onnx_opt  0.9977911
onnx_calib  0.99509
onnx_quant  0.9942628

余弦相似度均>0.99,可进一步认为量化精度满足需求。

三、精度评测

3.1 评测结果

测试数据集:Market-1501-v15.09.15

1.距离度量方式:cosine

img

2.距离度量方式:euclidean

img

3.2 评测方法

3.2.1 预训练权重

在第一章配置好的官方运行环境中,使用代码仓库的现有方法对预训练权重做精度评测。

PYTHONPATH=. python3 scripts/main.py \
--config-file configs/im_osnet_ain_x1_0_softmax_256x128_amsgrad_cosine.yaml \
--root ./ \
model.load_weights ./osnet_ain_ms_d_c.pth.tar \
test.evaluate True

3.2.2 导出的 onnx

需要基于第一章配置好的官方运行环境,额外安装 onnxruntime 和 tqdm 模块。

pip install onnxruntime
pip install tqdm

测试时,datamanager 参数需要和 configs/im_osnet_ain_x1_0_softmax_256x128_amsgrad_cosine.yaml 对齐。

import cv2
import torch
import numpy as np
import onnxruntime as ort
from PIL import Image
from tqdm import tqdmfrom torchreid.data import ImageDataManager
from torchreid.metrics import compute_distance_matrix, evaluate_rankdef extract_feature(img_path):img = cv2.imread(img_path)img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)img = Image.fromarray(img)img = transform(img)img = img.unsqueeze(0).numpy()feat = session.run(None, {input_name: img})[0]return feat[0]def extract_features(dataset):features, pids, camids = [], [], []for img_path, pid, camid, _ in tqdm(dataset, desc="Extracting features"):feat = extract_feature(img_path)features.append(feat)pids.append(pid)camids.append(camid)return np.stack(features), np.array(pids), np.array(camids)datamanager = ImageDataManager(use_gpu=False,root="./",sources=['market1501'],height=256,width=128,batch_size_train=64,batch_size_test=1,transforms=['random_flip', 'color_jitter']
)if __name__ == "__main__":query_loader = datamanager.test_loader['market1501']['query']gallery_loader = datamanager.test_loader['market1501']['gallery']query = query_loader.dataset.querygallery = gallery_loader.dataset.gallerytransform = query_loader.dataset.transformsession = ort.InferenceSession('osnet_ain_x1_0.onnx', providers=['CPUExecutionProvider'])input_name = session.get_inputs()[0].namequery_features, query_pids, query_camids = extract_features(query)gallery_features, gallery_pids, gallery_camids = extract_features(gallery)distmat = compute_distance_matrix(torch.tensor(query_features),torch.tensor(gallery_features),metric='cosine').numpy()cmc, mAP = evaluate_rank(distmat, query_pids, gallery_pids, query_camids, gallery_camids, use_cython=False)print("\n=== Market1501 Evaluation Results (ONNX model) ===")print(f"mAP     : {mAP:.2%}")print(f"Rank-1  : {cmc[0]:.2%}")print(f"Rank-5  : {cmc[4]:.2%}")print(f"Rank-10 : {cmc[9]:.2%}")

3.2.3 PTQ 量化 onnx

需要基于第一章配置好的官方运行环境,额外安装 onnxruntime 和 tqdm 模块,并安装 horizon-nn/hmct 和 horizon-tc-ui。

pip install onnxruntime
pip install tqdm
pip install horizon_nn-1.1.0.post0.dev202506060002+840a0a685d053923c1a577c5032b7e0cfc84e6ac-cp310-cp310-linux_x86_64.whl --no-deps
pip install horizon_tc_ui-1.24.4-cp310-cp310-linux_x86_64.whl --no-deps

测试时,datamanager 参数需要和 configs/im_osnet_ain_x1_0_softmax_256x128_amsgrad_cosine.yaml 对齐,且需要把输入数据处理成 yuv444 类型。

import cv2
import torch
import numpy as np
from PIL import Image
from tqdm import tqdm
from horizon_tc_ui import HB_ONNXRuntimefrom torchreid.data import ImageDataManager
from torchreid.metrics import compute_distance_matrix, evaluate_rankdef bgr2nv12(image):image = image.astype(np.uint8) height, width = image.shape[0], image.shape[1] yuv420p = cv2.cvtColor(image, cv2.COLOR_BGR2YUV_I420).reshape((height * width * 3 // 2, )) y = yuv420p[:height * width] uv_planar = yuv420p[height * width:].reshape((2, height * width // 4)) uv_packed = uv_planar.transpose((1, 0)).reshape((height * width // 2, )) nv12 = np.zeros_like(yuv420p) nv12[:height * width] = y nv12[height * width:] = uv_packed return nv12 def nv12Toyuv444(nv12, target_size): height = target_size[0] width = target_size[1] nv12_data = nv12.flatten() yuv444 = np.empty([height, width, 3], dtype=np.uint8) yuv444[:, :, 0] = nv12_data[:width * height].reshape(height, width) u = nv12_data[width * height::2].reshape(height // 2, width // 2) yuv444[:, :, 1] = Image.fromarray(u).resize((width, height),resample=0) v = nv12_data[width * height + 1::2].reshape(height // 2, width // 2) yuv444[:, :, 2] = Image.fromarray(v).resize((width, height),resample=0) return yuv444 def preprocess_nhwc(img_path):bgr_input = cv2.imread(img_path)bgr_input = cv2.resize(bgr_input, (128,256), interpolation=cv2.INTER_LINEAR)nv12_input = bgr2nv12(bgr_input)yuv444 = nv12Toyuv444(nv12_input, (256,128))yuv444 = yuv444[np.newaxis,:,:,:]return yuv444def extract_feature(img_path):for input_name in input_names:feed_dict[input_name] = preprocess_nhwc(img_path)feat = sess.run(output_names, feed_dict)return feat[0]def extract_features(dataset):features, pids, camids = [], [], []for img_path, pid, camid, _ in tqdm(dataset, desc="Extracting features"):feat = extract_feature(img_path)features.append(feat)pids.append(pid)camids.append(camid)return np.stack(features), np.array(pids), np.array(camids)if __name__ == "__main__":datamanager = ImageDataManager(use_gpu=False,root="./",sources=['market1501'],height=256,width=128,batch_size_train=64,batch_size_test=1,transforms=['random_flip', 'color_jitter'])query_loader = datamanager.test_loader['market1501']['query']gallery_loader = datamanager.test_loader['market1501']['gallery']query = query_loader.dataset.querygallery = gallery_loader.dataset.gallerytransform = query_loader.dataset.transformsess = HB_ONNXRuntime(model_file='./model_output_inx2_cpu/reid_quantized_model.onnx')input_names = [input.name for input in sess.get_inputs()]output_names = [output.name for output in sess.get_outputs()]feed_dict = dict()query_features, query_pids, query_camids = extract_features(query)gallery_features, gallery_pids, gallery_camids = extract_features(gallery)distmat = compute_distance_matrix(torch.tensor(query_features.squeeze(1)),torch.tensor(gallery_features.squeeze(1)),metric='cosine').numpy()cmc, mAP = evaluate_rank(distmat, query_pids, gallery_pids, query_camids, gallery_camids, use_cython=False)print("\n=== Market1501 Evaluation Results (ONNX model) ===")print(f"mAP     : {mAP:.2%}")print(f"Rank-1  : {cmc[0]:.2%}")print(f"Rank-5  : {cmc[4]:.2%}")print(f"Rank-10 : {cmc[9]:.2%}")

四、性能评测

4.1 评测结果

Running condition:Thread number is: 1Frame count   is: 1000Program run time: 51433.128000 ms
Perf result:Frame totally latency is: 51417.804688 msAverage    latency    is: 51.417805 msFrame      rate       is: 19.442722 FPS

以 X5 为例,该模型的单线程推理延时为 51.4ms,对应 FPS 为 19.4

Latency 是指单流程推理模型所耗费的平均时间,重在表示在资源充足的情况下推理一帧的平均耗时,体现在上板运行是单核单线程统计。

FPS 是指多流程同时进行模型推理平均一秒推理的帧数,重在表示充分使用资源情况下模型的吞吐,体现在上板运行为单核多线程;统计方法是同时起多个线程进行模型推理,计算平均 1s 推理的总帧数。

Latency 与 FPS 的统计情景不同,Latency 为单流程(单核单线程)推理,FPS 为多流程(单核多线程)推理,因此推算不一致;若统计 FPS 时将流程(线程)数量设置为 1 ,则通过 Latency 推算 FPS 和测出的一致。

4.2 评测方法

可板端使用 hrt_model_exec 工具获取模型的实测性能数据,测试命令如下:

hrt_model_exec perf --model-file reid.bin --frame-count 1000 --thread-num 1
417.804688 msAverage    latency    is: 51.417805 msFrame      rate       is: 19.442722 FPS
http://www.xdnf.cn/news/17970.html

相关文章:

  • 芋道RBAC实现介绍
  • 基于Node.js+Express的电商管理平台的设计与实现/基于vue的网上购物商城的设计与实现/基于Node.js+Express的在线销售系统
  • css: word pacing属性
  • 【原理】C#构造函数可以标记为Static吗
  • Oracle Undo Tablespace 使用率暴涨案例分析
  • Java 方法引用详解
  • Vue.js 路由/redirect重定向刷新机制详解
  • 新的“MadeYouReset”方法利用 HTTP/2 进行隐秘的 DoS 攻击
  • linux-高级IO(上)
  • 数据结构4线性表——顺序栈
  • Microsoft WebView2
  • Java 大视界 -- 基于 Java 的大数据分布式计算在气象灾害预警与应急响应中的应用
  • 【lucene】SegmentInfos
  • 系统思考—啤酒游戏经营决策沙盘认证
  • 论文推荐|迁移学习+多模态特征融合
  • 二叉树的三种遍历方法
  • ZKmall开源商城的数据校验之道:用规范守护业务基石
  • 【论文笔记】STORYWRITER: A Multi-Agent Framework for Long Story Generation
  • lcx、netcat、powercat--安装、使用
  • [go] 桥接模式
  • 分布式存储与存储阵列:从传统到现代的存储革命
  • Tello无人机与LLM模型控制 ROS
  • 安全审计-iptales防火墙设置
  • 立体匹配中的稠密匹配和稀疏匹配
  • 教材采购管理系统(java)
  • 力扣(接雨水)——基于最高柱分割的双指针
  • Python - 100天从新手到大师:第十一天常用数据结构之字符串
  • Flink Stream API 源码走读 - 总结
  • 双指针和codetop复习
  • Day56 Java面向对象10 方法重写