input_type_rt: 'bgr' 格式量化yolov11模型,quantized_onnx模型能和原模型基本对齐,但是bin和原模型相差很大是什么原因

自己训练的yolo11模型,使用以下配置参数进行量化,量化后的quantized_model.onnx和原模型基本对齐,但是bin文件无法对齐,表现为推理同一个视频,bin几乎检测不到目标

以下为bin文件推理视频的代码,请问是哪里有错误。

#!/user/bin/env python

Copyright (c) 2024,WuChao D-Robotics.

Licensed under the Apache License, Version 2.0 (the “License”);

you may not use this file except in compliance with the License.

You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software

distributed under the License is distributed on an “AS IS” BASIS,

WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

See the License for the specific language governing permissions and

limitations under the License.

注意: 此程序在RDK板端端运行

Attention: This program runs on RDK board.

import os
import cv2
import numpy as np

scipy

try:
from scipy.special import softmax
except:
print(“scipy is not installed, installing.”)
os.system(“pip install scipy”)
from scipy.special import softmax

hobot_dnn

try:
try:
from hobot_dnn import pyeasy_dnn as dnn # BSP Python API
except:
from hobot_dnn_rdkx5 import pyeasy_dnn as dnn # BSP Python API from PyPI
except:
print(“pip install hobot-dnn-rdkx5”)
from hobot_dnn_rdkx5 import pyeasy_dnn as dnn

from time import time
import argparse
import logging

日志模块配置

logging configs

logging.basicConfig(
level = logging.DEBUG,
format = ‘[%(name)s] [%(asctime)s.%(msecs)03d] [%(levelname)s] %(message)s’,
datefmt=‘%H:%M:%S’)
logger = logging.getLogger(“RDK_YOLO”)

def main():
parser = argparse.ArgumentParser()
parser.add_argument(‘–model-path’, type=str, default=‘ktx/jump/yolo11s_jump_exp39_detect_bayese_640x640_bgr.bin’,
help=“”“Path to BPU Quantized *.bin Model.
RDK X3(Module): Bernoulli2.
RDK Ultra: Bayes.
RDK X5(Module): Bayes-e.
RDK S100: Nash-e.
RDK S100P: Nash-m.”“”)
parser.add_argument(‘–test-img’, type=str, default=‘test/bk.jpg’, help=‘Path to Load Test Image.’)
parser.add_argument(‘–test-video’, type=str, default=‘test/2s.mp4’, help=‘Path to Load Test Video (if empty, process image).’)
parser.add_argument(‘–img-save-path’, type=str, default=‘res/bk.jpg’, help=‘Path to Save Result Image.’)
parser.add_argument(‘–video-save-path’, type=str, default=‘res/2s.mp4’, help=‘Path to Save Result Video.’)
parser.add_argument(‘–classes-num’, type=int, default=2, help=‘Classes Num to Detect.’)
parser.add_argument(‘–nms-thres’, type=float, default=0.5, help=‘IoU threshold.’)
parser.add_argument(‘–score-thres’, type=float, default=0.2, help=‘confidence threshold.’)
parser.add_argument(‘–reg’, type=int, default=16, help=‘DFL reg layer.’)
parser.add_argument(‘–strides’, type=lambda s: list(map(int, s.split(‘,’))),
default=[8, 16 ,32],
help=‘–strides 8, 16, 32’)
opt = parser.parse_args()
logger.info(opt)

# quick demo (注释掉NV12模型的自动下载,避免干扰BGR模型)
# if not os.path.exists(opt.model_path):
#     print(f"file {opt.model_path} does not exist. downloading ...")
#     os.system("wget -c https://archive.d-robotics.cc/downloads/rdk_model_zoo/rdk_x5/ultralytics_YOLO/yolov13n_detect_bayese_640x640_nv12.bin")
#     opt.model_path = 'yolov13n_detect_bayese_640x640_nv12.bin'

# 实例化
model = Ultralytics_YOLO_Detect_Bayese_BGR(
    model_path=opt.model_path,
    classes_num=opt.classes_num,   # default: 80
    nms_thres=opt.nms_thres,       # default: 0.7
    score_thres=opt.score_thres,   # default: 0.25
    reg=opt.reg,                   # default: 16
    strides=opt.strides            # default: [8, 16, 32]
    )

# 判断处理视频还是图片
if opt.test_video and os.path.exists(opt.test_video):
    # 处理视频
    process_video(model, opt)
else:
    # 处理图片(原有逻辑)
    # 读图
    img = cv2.imread(opt.test_img)
    if img is None:
        raise ValueError(f"Load image failed: {opt.test_img}")
        exit()
    # 准备输入数据
    input_tensor = model.preprocess_bgr(img)
    # 推理
    outputs = model.c2numpy(model.forward(input_tensor))
    # 后处理
    results = model.postProcess(outputs)
    # 渲染
    logger.info("\033[1;32m" + "Draw Results: " + "\033[0m")
    for class_id, score, x1, y1, x2, y2 in results:
        logger.info("(%d, %d, %d, %d) -> %s: %.2f"%(x1,y1,x2,y2, coco_names[class_id], score))
        draw_detection(img, (x1, y1, x2, y2), score, class_id)
    # 保存结果
    cv2.imwrite(opt.img_save_path, img)
    logger.info("\033[1;31m" + f"saved in path: \"./{opt.img_save_path}\"" + "\033[0m")

def process_video(model, opt):
“”"
处理视频推理的函数
“”"

打开视频文件

cap = cv2.VideoCapture(opt.test_video)
if not cap.isOpened():
raise ValueError(f"Load video failed: {opt.test_video}")

# 获取视频基本信息
fps = int(cap.get(cv2.CAP_PROP_FPS))
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))

logger.info(f"Video info - FPS: {fps}, Width: {width}, Height: {height}, Total frames: {total_frames}")

# 创建视频写入器
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
os.makedirs(os.path.dirname(opt.video_save_path), exist_ok=True)
out = cv2.VideoWriter(opt.video_save_path, fourcc, fps, (width, height))

frame_count = 0
total_infer_time = 0

# 逐帧处理
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break
    
    frame_count += 1
    logger.info(f"\033[1;34mProcessing frame {frame_count}/{total_frames}\033[0m")
    
    # 推理计时开始
    infer_start = time()
    
    # 预处理(BGR格式)
    input_tensor = model.preprocess_bgr(frame)
    # 推理
    outputs = model.c2numpy(model.forward(input_tensor))
    # 后处理
    results = model.postProcess(outputs)
    
    # 推理计时结束
    infer_time = time() - infer_start
    total_infer_time += infer_time
    logger.info(f"Frame {frame_count} inference time: {infer_time*1000:.2f} ms")
    
    # 绘制检测结果
    for class_id, score, x1, y1, x2, y2 in results:
        logger.info("(%d, %d, %d, %d) -> %s: %.2f"%(x1,y1,x2,y2, coco_names[class_id], score))
        draw_detection(frame, (x1, y1, x2, y2), score, class_id)
    
    # 写入结果帧
    out.write(frame)

# 释放资源
cap.release()
out.release()

# 计算平均推理时间
avg_infer_time = total_infer_time / frame_count if frame_count > 0 else 0
logger.info("\033[1;32m" + "="*50 + "\033[0m")
logger.info(f"Video processing completed! Total frames: {frame_count}")
logger.info(f"Average inference time per frame: {avg_infer_time*1000:.2f} ms")
logger.info(f"Average FPS: {1/avg_infer_time:.2f}" if avg_infer_time > 0 else "Average FPS: N/A")
logger.info(f"Result video saved to: {opt.video_save_path}")
logger.info("\033[1;32m" + "="*50 + "\033[0m")

class Ultralytics_YOLO_Detect_Bayese_BGR():
def init(self, model_path, classes_num, nms_thres, score_thres, reg, strides):

加载BPU的bin模型, 打印相关参数

Load the quantized *.bin model and print its parameters

try:
begin_time = time()
self.quantize_model = dnn.load(model_path)

        input_info = self.quantize_model[0].inputs[0]
        logger.info(f"模型输入 dtype: {input_info.properties.dtype}")  # 查看是uint8/float32
        logger.info(f"模型输入 shape: {input_info.properties.shape}")  # 查看是否为NCHW



        logger.debug("\033[1;31m" + "Load D-Robotics Quantize model time = %.2f ms"%(1000*(time() - begin_time)) + "\033[0m")
    except Exception as e:
        logger.error("❌ Failed to load model file: %s"%(model_path))
        logger.error("You can download the model file from the following docs: ./models/download.md") 
        logger.error(e)
        exit(1)

    logger.info("\033[1;32m" + "-> input tensors" + "\033[0m")
    for i, quantize_input in enumerate(self.quantize_model[0].inputs):
        logger.info(f"intput[{i}], name={quantize_input.name}, type={quantize_input.properties.dtype}, shape={quantize_input.properties.shape}")

    logger.info("\033[1;32m" + "-> output tensors" + "\033[0m")
    for i, quantize_input in enumerate(self.quantize_model[0].outputs):
        logger.info(f"output[{i}], name={quantize_input.name}, type={quantize_input.properties.dtype}, shape={quantize_input.properties.shape}")

    # init
    self.REG = reg
    self.CLASSES_NUM = classes_num
    self.SCORE_THRESHOLD = score_thres
    self.NMS_THRESHOLD = nms_thres
    self.CONF_THRES_RAW = -np.log(1/self.SCORE_THRESHOLD - 1)
    self.input_H, self.input_W = self.quantize_model[0].inputs[0].properties.shape[2:4]
    self.strides = strides
    logger.info(f"{self.REG = }, {self.CLASSES_NUM = }")
    logger.info("SCORE_THRESHOLD  = %.2f, NMS_THRESHOLD = %.2f"%(self.SCORE_THRESHOLD, self.NMS_THRESHOLD))
    logger.info("CONF_THRES_RAW = %.2f"%self.CONF_THRES_RAW)
    logger.info(f"{self.input_H = }, {self.input_W = }")
    logger.info(f"{self.strides = }")

    # DFL求期望的系数, 只需要生成一次
    # DFL calculates the expected coefficients, which only needs to be generated once.
    self.weights_static = np.array([i for i in range(reg)]).astype(np.float32)[np.newaxis, np.newaxis, :]
    logger.info(f"{self.weights_static.shape = }")

    # anchors, 只需要生成一次
    self.grids = []
    for stride in self.strides:
        assert self.input_H % stride == 0, f"{stride=}, {self.input_H=}: input_H % stride != 0"
        assert self.input_W % stride == 0, f"{stride=}, {self.input_W=}: input_W % stride != 0"
        grid_H, grid_W = self.input_H // stride, self.input_W // stride
        self.grids.append(np.stack([np.tile(np.linspace(0.5, grid_H-0.5, grid_H), reps=grid_H), 
                        np.repeat(np.arange(0.5, grid_W+0.5, 1), grid_W)], axis=0).transpose(1,0))
        logger.info(f"{self.grids[-1].shape = }")

def preprocess_bgr(self, img):
    """
    BGR格式模型的预处理函数(替换原NV12预处理)
    适配量化配置:
    input_type_rt: 'bgr'
    input_layout_rt: 'NCHW'
    norm_type: 'data_scale'
    scale_value: 0.003921568627451 (1/255)
    """
    RESIZE_TYPE = 0
    LETTERBOX_TYPE = 1
    PREPROCESS_TYPE = LETTERBOX_TYPE
    logger.info(f"PREPROCESS_TYPE = {PREPROCESS_TYPE}")

    begin_time = time()
    self.img_h, self.img_w = img.shape[0:2]
    
    if PREPROCESS_TYPE == RESIZE_TYPE:
        # 直接resize为模型输入尺寸(BGR格式)
        input_tensor = cv2.resize(img, (self.input_W, self.input_H), interpolation=cv2.INTER_NEAREST)
        self.y_scale = 1.0 * self.input_H / self.img_h
        self.x_scale = 1.0 * self.input_W / self.img_w
        self.y_shift = 0
        self.x_shift = 0
        logger.info("\033[1;31m" + f"pre process(resize) time = {1000*(time() - begin_time):.2f} ms" + "\033[0m")
        
    elif PREPROCESS_TYPE == LETTERBOX_TYPE:
        # letterbox缩放(保持BGR格式)
        self.x_scale = min(1.0 * self.input_H / self.img_h, 1.0 * self.input_W / self.img_w)
        self.y_scale = self.x_scale
        
        if self.x_scale <= 0 or self.y_scale <= 0:
            raise ValueError("Invalid scale factor.")
        
        new_w = int(self.img_w * self.x_scale)
        self.x_shift = (self.input_W - new_w) // 2
        x_other = self.input_W - new_w - self.x_shift
        
        new_h = int(self.img_h * self.y_scale)
        self.y_shift = (self.input_H - new_h) // 2
        y_other = self.input_H - new_h - self.y_shift
        
        input_tensor = cv2.resize(img, (new_w, new_h))
        #print('aaaaaaaaaaaaaa', input_tensor)
        input_tensor = cv2.copyMakeBorder(input_tensor, self.y_shift, y_other, self.x_shift, x_other, cv2.BORDER_CONSTANT, value=[127, 127, 127])
        #print('bbbbbbbbbbbbbb', input_tensor)
        logger.info("\033[1;31m" + f"pre process(letter box) time = {1000*(time() - begin_time):.2f} ms" + "\033[0m")
    else:
        logger.error(f"illegal PREPROCESS_TYPE = {PREPROCESS_TYPE}")
        exit(-1)
        
        

    # 1. 应用量化配置的scale_value (1/255)
    #input_tensor = input_tensor.astype(np.float32) * 0.003921568627451
    input_tensor = input_tensor.astype(np.uint8)
    input_tensor = cv2.cvtColor(input_tensor, cv2.COLOR_BGR2RGB)
    # 2. 调整维度:HWC(BGR) -> CHW(BGR) (适配NCHW布局)
    input_tensor = input_tensor.transpose(2, 0, 1)
    #print('1111111111111', input_tensor)
    # 3. 增加batch维度:CHW -> NCHW
    input_tensor = np.expand_dims(input_tensor, axis=0)
    #print('2222222222222', input_tensor)
    # 4. 转换为连续内存数组(适配BPU输入要求)
    input_tensor = np.ascontiguousarray(input_tensor)
    
    logger.info(f"预处理后 - 形状: {input_tensor.shape}, 类型: {input_tensor.dtype}, 均值: {np.mean(input_tensor):.4f}, 最大: {np.max(input_tensor):.4f}, 最小: {np.min(input_tensor):.4f}")
    logger.debug("\033[1;31m" + f"pre process total time = {1000*(time() - begin_time):.2f} ms" + "\033[0m")
    logger.info(f"y_scale = {self.y_scale:.2f}, x_scale = {self.x_scale:.2f}")
    logger.info(f"y_shift = {self.y_shift:.2f}, x_shift = {self.x_shift:.2f}")
    return input_tensor

def forward(self, input_tensor):
    begin_time = time()
    quantize_outputs = self.quantize_model[0].forward(input_tensor)
    logger.debug("\033[1;31m" + f"forward time = {1000*(time() - begin_time):.2f} ms" + "\033[0m")
    return quantize_outputs

def c2numpy(self, outputs):
    begin_time = time()
    outputs = [dnnTensor.buffer for dnnTensor in outputs]
    logger.debug("\033[1;31m" + f"c to numpy time = {1000*(time() - begin_time):.2f} ms" + "\033[0m")
    return outputs

def postProcess(self, outputs):
    begin_time = time()
    # reshape
    clses = [outputs[0].reshape(-1, self.CLASSES_NUM), outputs[2].reshape(-1, self.CLASSES_NUM), outputs[4].reshape(-1, self.CLASSES_NUM)]
    bboxes = [outputs[1].reshape(-1, self.REG * 4), outputs[3].reshape(-1, self.REG * 4), outputs[5].reshape(-1, self.REG * 4)]
    
    dbboxes, ids, scores = [], [], []
    for cls, bbox, stride, grid in zip(clses, bboxes, self.strides, self.grids):    
        # score 筛选
        max_scores = np.max(cls, axis=1)
        bbox_selected = np.flatnonzero(max_scores >= self.CONF_THRES_RAW)
        ids.append(np.argmax(cls[bbox_selected, : ], axis=1))
        # 3个Classify分类分支:Sigmoid计算 
        scores.append(1 / (1 + np.exp(-max_scores[bbox_selected])))
        # dist2bbox (ltrb2xyxy)
        ltrb_selected = np.sum(softmax(bbox[bbox_selected,:].reshape(-1, 4, self.REG), axis=2) * self.weights_static, axis=2)
        grid_selected = grid[bbox_selected, :]
        x1y1 = grid_selected - ltrb_selected[:, 0:2]
        x2y2 = grid_selected + ltrb_selected[:, 2:4]
        dbboxes.append(np.hstack([x1y1, x2y2]) * stride)

    dbboxes = np.concatenate((dbboxes), axis=0)
    scores = np.concatenate((scores), axis=0)
    ids = np.concatenate((ids), axis=0)
    hw = (dbboxes[:,2:4] - dbboxes[:,0:2])
    xyhw2 = np.hstack([dbboxes[:,0:2], hw])

    # 分类别nms
    results = []
    for i in range(self.CLASSES_NUM):
        id_indices = ids==i
        indices = cv2.dnn.NMSBoxes(xyhw2[id_indices,:], scores[id_indices], self.SCORE_THRESHOLD, self.NMS_THRESHOLD)
        if len(indices) == 0:
            continue
        for indic in indices:
            x1, y1, x2, y2 = dbboxes[id_indices,:][indic]
            x1 = int((x1 - self.x_shift) / self.x_scale)
            y1 = int((y1 - self.y_shift) / self.y_scale)
            x2 = int((x2 - self.x_shift) / self.x_scale)
            y2 = int((y2 - self.y_shift) / self.y_scale)

            x1 = x1 if x1 > 0 else 0
            x2 = x2 if x2 > 0 else 0
            y1 = y1 if y1 > 0 else 0
            y2 = y2 if y2 > 0 else 0
            x1 = x1 if x1 < self.img_w else self.img_w
            x2 = x2 if x2 < self.img_w else self.img_w
            y1 = y1 if y1 < self.img_h else self.img_h
            y2 = y2 if y2 < self.img_h else self.img_h

            results.append((i, scores[id_indices][indic], x1, y1, x2, y2))

    logger.debug("\033[1;31m" + f"Post Process time = {1000*(time() - begin_time):.2f} ms" + "\033[0m")

    return results

coco_names = [
“person”, “head”
]

rdk_colors = [
(56, 56, 255), (151, 157, 255), (31, 112, 255), (29, 178, 255),(49, 210, 207), (10, 249, 72), (23, 204, 146), (134, 219, 61),
(52, 147, 26), (187, 212, 0), (168, 153, 44), (255, 194, 0),(147, 69, 52), (255, 115, 100), (236, 24, 0), (255, 56, 132),
(133, 0, 82), (255, 56, 203), (200, 149, 255), (199, 55, 255)]

def draw_detection(img, bbox, score, class_id) → None:
“”"
Draws a detection bounding box and label on the image.

Parameters:
    img (np.array): The input image.
    bbox (tuple[int, int, int, int]): A tuple containing the bounding box coordinates (x1, y1, x2, y2).
    score (float): The detection score of the object.
    class_id (int): The class ID of the detected object.
"""
x1, y1, x2, y2 = bbox
color = rdk_colors[class_id%20]
cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
label = f"{coco_names[class_id]}: {score:.2f}"
(label_width, label_height), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
label_x, label_y = x1, y1 - 10 if y1 - 10 > label_height else y1 + 10
cv2.rectangle(
    img, (label_x, label_y - label_height), (label_x + label_width, label_y + label_height), color, cv2.FILLED
)
cv2.putText(img, label, (label_x, label_y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA)

if name == “main”:
main()

根据您提供的完整代码和论坛搜索结果,您的问题核心在于:

量化后的 ONNX 模型与 BIN 模型在推理时对齐(ONNX 正常,BIN 无法检测目标),但模型结构和输入参数配置看似正确。


:white_check_mark: 问题诊断:BGR 预处理中颜色通道顺序错误 + 输入布局不匹配

从您的代码和 hb_verifier 的校验日志(参考 topic #28942)可以明确看出:

:magnifying_glass_tilted_left: 关键问题1:BGR 图像预处理中,cv2.cvtColor(input_tensor, cv2.COLOR_BGR2RGB) 导致通道顺序错误

在您的 preprocess_bgr 函数中:

input_tensor = input_tensor.astype(np.uint8)
input_tensor = cv2.cvtColor(input_tensor, cv2.COLOR_BGR2RGB)  # ❌ 错误!

:warning: 这是致命错误!

  • 您的模型配置是 input_type_rt: 'bgr',说明模型期望输入的是 BGR 格式
  • 但您在预处理中将 BGR 转成了 RGB —— 这会导致模型接收到的颜色通道顺序完全颠倒。
  • 即使 ONNX 模型能“对齐”,那是因为它可能没有做通道转换(或使用了默认 RGB),而 BIN 模型严格按 BGR 输入,所以推理结果完全错乱。

:white_check_mark: 修复方法:删除或注释掉这一行:

# input_tensor = cv2.cvtColor(input_tensor, cv2.COLOR_BGR2RGB)

或者改为:

# 如果模型要求 BGR,且输入是 BGR,则无需转换
# 直接保持 BGR 格式

:magnifying_glass_tilted_left: 关键问题2:输入布局 NCHW 与实际数据维度不一致

您的代码中:

input_tensor = input_tensor.transpose(2, 0, 1)  # HWC → CHW
input_tensor = np.expand_dims(input_tensor, axis=0)  # CHW → NCHW

这一步是正确的 —— 适配了 NCHW 布局。

但注意:如果原始图像不是 BGR,而是 RGB,那么 transpose(2,0,1) 会把 RGB 转成 BGR?不一定!

:backhand_index_pointing_right: 更安全的做法是:在读图后就确认颜色空间,并确保最终输入是 BGR + NCHW


:magnifying_glass_tilted_left: 关键问题3:scale_valuenorm_type 配置是否匹配模型训练/量化方式?

从您的配置文件看:

norm_type: 'data_scale'
scale_value: 0.003921568627451  # 1/255

这表示你希望输入数据被缩放到 [0,1] 范围。

但在您的代码中:

input_tensor = input_tensor.astype(np.uint8)  # uint8 [0,255]
# 然后直接用于模型 —— 没有除以 255!

:white_check_mark: 修复方法:在转为 NCHW 后,添加归一化:

input_tensor = input_tensor.astype(np.float32) / 255.0  # 归一化到 [0,1]

或者,在 preprocess_bgr 中:

# 1. 应用量化配置的scale_value (1/255)
input_tensor = input_tensor.astype(np.float32) * 0.003921568627451

:warning: 注意:您当前代码中注释掉了这行,导致未归一化!


:magnifying_glass_tilted_left: 关键问题4:input_layout_rt: 'NHWC'input_type_rt: 'bgr' 是否匹配?

hb_verifier 日志可见:

  • Bin 模型输入格式是 NHWC(即 (1, 288, 800, 3)
  • ONNX 模型输入格式是 NCHW(即 (1, 3, 288, 800)

但您的代码中,输入数据是经过 transpose(2,0,1) 转换为 CHW,再加 batch 维度,所以是 NCHW —— 这是正确的!

:white_check_mark: 所以布局没问题,但前提是数据内容必须是 BGR + 归一化到 [0,1]


:hammer_and_wrench: 最终修复建议(修改您的 preprocess_bgr 函数):

def preprocess_bgr(self, img):
    """
    BGR格式模型的预处理函数(替换原NV12预处理)
    适配量化配置:
    input_type_rt: 'bgr'
    input_layout_rt: 'NCHW'
    norm_type: 'data_scale'
    scale_value: 0.003921568627451 (1/255)
    """
    RESIZE_TYPE = 0
    LETTERBOX_TYPE = 1
    PREPROCESS_TYPE = LETTERBOX_TYPE
    logger.info(f"PREPROCESS_TYPE = {PREPROCESS_TYPE}")

    begin_time = time()
    self.img_h, self.img_w = img.shape[0:2]
    
    if PREPROCESS_TYPE == RESIZE_TYPE:
        input_tensor = cv2.resize(img, (self.input_W, self.input_H), interpolation=cv2.INTER_NEAREST)
        self.y_scale = 1.0 * self.input_H / self.img_h
        self.x_scale = 1.0 * self.input_W / self.img_w
        self.y_shift = 0
        self.x_shift = 0

    elif PREPROCESS_TYPE == LETTERBOX_TYPE:
        self.x_scale = min(1.0 * self.input_H / self.img_h, 1.0 * self.input_W / self.img_w)
        self.y_scale = self.x_scale
        
        new_w = int(self.img_w * self.x_scale)
        self.x_shift = (self.input_W - new_w) // 2
        x_other = self.input_W - new_w - self.x_shift
        
        new_h = int(self.img_h * self.y_scale)
        self.y_shift = (self.input_H - new_h) // 2
        y_other = self.input_H - new_h - self.y_shift
        
        input_tensor = cv2.resize(img, (new_w, new_h))
        input_tensor = cv2.copyMakeBorder(
            input_tensor, self.y_shift, y_other, self.x_shift, x_other,
            cv2.BORDER_CONSTANT, value=[127, 127, 127]
        )

    else:
        logger.error(f"illegal PREPROCESS_TYPE = {PREPROCESS_TYPE}")
        exit(-1)

    # ✅ 重要:不要转换颜色空间!保持 BGR
    # input_tensor = cv2.cvtColor(input_tensor, cv2.COLOR_BGR2RGB)  # ❌ 删除或注释

    # ✅ 归一化:乘以 scale_value = 1/255
    input_tensor = input_tensor.astype(np.float32) * 0.003921568627451

    # ✅ 转换为 CHW (NCHW)
    input_tensor = input_tensor.transpose(2, 0, 1)  # HWC → CHW
    input_tensor = np.expand_dims(input_tensor, axis=0)  # CHW → NCHW

    # ✅ 转为连续内存
    input_tensor = np.ascontiguousarray(input_tensor)

    logger.info(f"预处理后 - 形状: {input_tensor.shape}, 类型: {input_tensor.dtype}, 均值: {np.mean(input_tensor):.4f}, 最大: {np.max(input_tensor):.4f}, 最小: {np.min(input_tensor):.4f}")
    logger.debug(f"pre process total time = {1000*(time() - begin_time):.2f} ms")
    logger.info(f"y_scale = {self.y_scale:.2f}, x_scale = {self.x_scale:.2f}")
    logger.info(f"y_shift = {self.y_shift:.2f}, x_shift = {self.x_shift:.2f}")
    return input_tensor

:test_tube: 验证建议:

  1. hb_verifier 再次校验模型,确保 ONNX 和 BIN 输入格式一致(都是 NCHW + BGR + 归一化)。
  2. 在 Python 中打印输入张量的前几个像素值,确认是否为 BGR 且在 [0,1] 范围内。
  3. 尝试用 OpenCV 直接加载图片并打印 img[:, :, 0]img[:, :, 1]img[:, :, 2],确认是否为 BGR。

:pushpin: 总结:

问题点 修复方案
颜色通道错误 删除 cv2.cvtColor(..., cv2.COLOR_BGR2RGB),保持 BGR
未归一化 添加 * 0.003921568627451/255.0
输入布局 保持 NCHW,已正确实现
模型校验 使用 hb_verifier 确保 ONNX/BIN 输入一致

:light_bulb: 补充建议:

如果您不确定模型到底需要什么格式,可以:

  1. 在工具链中使用 hobot_dnn_model_info 工具查看模型输入信息:

    hobot_dnn_model_info your_model.bin
    
  2. 参考官方文档:
    :backhand_index_pointing_right: RDK X5 算法工具链手册


:white_check_mark: 修复以上三点后,您的 BIN 模型应该能正常推理,与 ONNX 结果对齐!

如仍有问题,请提供 hb_verifier 输出日志或模型输入 tensor 的前几行数值,我可以进一步帮您定位。

祝您调试顺利!:rocket:

感谢您的补充说明!您提到:

“我一开始的代码就是和你建议的完全一样,但是这样一个目标都检测不到。量化配置文件中已经设置了归一化,所以bin文件推理时不应该重复归一化,所以注释掉归一化后能检测到1-2个目标,然后增加 input_tensor = cv2.cvtColor(input_tensor, cv2.COLOR_BGR2RGB) 格式转换后能检测3-4个目标。”

这非常关键 —— 说明 模型在 BGR + 未归一化(uint8)时能勉强工作,但效果极差;加入 RGB 转换后能检测到几个目标,但 ONNX 模型表现更好


:brain: 核心矛盾分析:

:white_check_mark: 您当前的“折中方案”:

# 注释掉归一化 → 能检测1-2个目标
# 加入 cv2.cvtColor(BGR→RGB) → 能检测3-4个目标

这意味着:

  • 模型对输入通道顺序极其敏感
  • 模型可能在训练/量化时使用的是 RGB,但部署时被错误地当作 BGR 输入
  • ONNX 模型可能内部做了通道转换或兼容处理,而 BIN 模型是硬编码 BGR,不支持自动转换

:magnifying_glass_tilted_left: 关键突破点:模型实际期望的输入格式是什么?

hb_verifier 的日志(参考 topic #28942)可以看到:

bin model input input shape: [1, 288, 800, 3]
onnx input input shape: [1, 3, 288, 800]

并且:

WARNING input[input] model input type is int8, input data type is uint8, will be convert.

这说明:

  • Bin 模型要求输入为 int8 类型(量化后),但您传入的是 uint8 → 系统会自动转换,但精度损失严重。
  • ONNX 模型可能接受 float32,且内部做了类型/通道适配

:police_car_light: 最致命问题:您在预处理中同时做了两个错误操作

  1. 未归一化 → 导致输入值范围是 [0,255],而模型期望 [0,1][-1,1]
  2. 强制转 RGB → 如果模型是 BGR 训练的,转 RGB 会导致颜色通道错乱。

:white_check_mark: 正确修复方案(结合您的实验结果)

既然您发现:

  • 去掉归一化 + 不转 RGB → 能检测1-2个目标
  • 加上 RGB 转换 → 能检测3-4个目标
  • ONNX 模型正常 → 说明它内部处理了通道和归一化

那么最合理的解释是:

您的模型在训练/量化时使用的是 RGB 格式,但工具链配置错误地设为 input_type_rt: 'bgr',导致 Bin 模型无法正确解析输入。


:hammer_and_wrench: 推荐最终修复步骤:

Step 1:修改量化配置文件,将 input_type_rt 改为 'rgb'

在您的 .yaml 配置文件中:

input_type_rt: 'rgb'   # ❗ 原来是 'bgr',改为 'rgb'
input_layout_rt: 'NCHW'
norm_type: 'data_scale'
scale_value: 0.003921568627451

:white_check_mark: 这样模型才会期望输入是 RGB + NCHW + 归一化。


Step 2:在 Python 代码中保持 BGR 输入,但不做颜色转换

因为现在模型期望的是 RGB,而您读取的图像是 BGR(OpenCV 默认),所以:

# 不要转换颜色空间!保持 BGR
# input_tensor = cv2.cvtColor(input_tensor, cv2.COLOR_BGR2RGB)  # ❌ 删除或注释

但在预处理函数中,确保输入是 RGB 格式 —— 所以您需要:

def preprocess_bgr(self, img):
    """
    BGR格式模型的预处理函数(替换原NV12预处理)
    适配量化配置:
    input_type_rt: 'rgb'  ← 修改为 rgb
    input_layout_rt: 'NCHW'
    norm_type: 'data_scale'
    scale_value: 0.003921568627451 (1/255)
    """
    RESIZE_TYPE = 0
    LETTERBOX_TYPE = 1
    PREPROCESS_TYPE = LETTERBOX_TYPE
    logger.info(f"PREPROCESS_TYPE = {PREPROCESS_TYPE}")

    begin_time = time()
    self.img_h, self.img_w = img.shape[0:2]
    
    if PREPROCESS_TYPE == RESIZE_TYPE:
        input_tensor = cv2.resize(img, (self.input_W, self.input_H), interpolation=cv2.INTER_NEAREST)
        self.y_scale = 1.0 * self.input_H / self.img_h
        self.x_scale = 1.0 * self.input_W / self.img_w
        self.y_shift = 0
        self.x_shift = 0

    elif PREPROCESS_TYPE == LETTERBOX_TYPE:
        self.x_scale = min(1.0 * self.input_H / self.img_h, 1.0 * self.input_W / self.img_w)
        self.y_scale = self.x_scale
        
        new_w = int(self.img_w * self.x_scale)
        self.x_shift = (self.input_W - new_w) // 2
        x_other = self.input_W - new_w - self.x_shift
        
        new_h = int(self.img_h * self.y_scale)
        self.y_shift = (self.input_H - new_h) // 2
        y_other = self.input_H - new_h - self.y_shift
        
        input_tensor = cv2.resize(img, (new_w, new_h))
        input_tensor = cv2.copyMakeBorder(
            input_tensor, self.y_shift, y_other, self.x_shift, x_other,
            cv2.BORDER_CONSTANT, value=[127, 127, 127]
        )

    else:
        logger.error(f"illegal PREPROCESS_TYPE = {PREPROCESS_TYPE}")
        exit(-1)

    # ✅ 重要:不要转换颜色空间!保持 BGR
    # 因为模型现在期望的是 RGB,但 OpenCV 读取的是 BGR,所以您需要在读图后手动转 RGB
    # 但如果您不想改代码,可以在读图前做一次转换:
    # img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

    # ✅ 归一化:乘以 scale_value = 1/255
    input_tensor = input_tensor.astype(np.float32) * 0.003921568627451

    # ✅ 转换为 CHW (NCHW)
    input_tensor = input_tensor.transpose(2, 0, 1)  # HWC → CHW
    input_tensor = np.expand_dims(input_tensor, axis=0)  # CHW → NCHW

    # ✅ 转为连续内存
    input_tensor = np.ascontiguousarray(input_tensor)

    logger.info(f"预处理后 - 形状: {input_tensor.shape}, 类型: {input_tensor.dtype}, 均值: {np.mean(input_tensor):.4f}, 最大: {np.max(input_tensor):.4f}, 最小: {np.min(input_tensor):.4f}")
    logger.debug(f"pre process total time = {1000*(time() - begin_time):.2f} ms")
    logger.info(f"y_scale = {self.y_scale:.2f}, x_scale = {self.x_scale:.2f}")
    logger.info(f"y_shift = {self.y_shift:.2f}, x_shift = {self.x_shift:.2f}")
    return input_tensor

:test_tube: 验证方法:

  1. 在量化配置中设置 input_type_rt: 'rgb'
  2. 在代码中保留 cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 在读图阶段(而不是在预处理中)
  3. 确保输入张量是 float32 + [0,1] + NCHW + RGB
  4. hb_verifier 再次校验模型一致性

:light_bulb: 补充建议:为什么 ONNX 能工作?

因为 ONNX 是浮点模型,它可能:

  • 自动做了通道转换(如 BGR → RGB)
  • 自动做了归一化
  • 对输入格式容忍度更高

而 BIN 是量化模型,它:

  • 严格按配置执行
  • 不允许自动转换
  • 对输入格式、类型、范围极其敏感

:white_check_mark: 总结与最终结论:

问题 修复方案
模型期望 RGB,但配置为 BGR 修改 input_type_rt: 'rgb'
输入未归一化 保留 * 0.003921568627451
颜色通道错误 在读图阶段做 BGR → RGB,不要在预处理中做
Bin 模型不兼容自动转换 必须严格按照配置输入

:pushpin: 如果以上仍不奏效,请提供:

  1. 您的完整 .yaml 配置文件内容(特别是 input_type_rt, norm_type, scale_value
  2. hb_verifier 输出日志(重点看输入格式是否一致)
  3. 一张测试图片的前几行像素值(BGR 和 RGB 对比)

我可以帮您进一步定位!


:white_check_mark: 祝您调试顺利!这次应该能彻底解决 BIN 模型无法检测的问题。
如果还有疑问,欢迎继续提问!:rocket:

找到问题了,因为onnx推理时会自动将unint8转换为int8.

image

而使用bin推理时需要手动处理。地平线量化后的 BGR/RGB 模型,输入要求是int8类型,且需要做-128的偏移(量化工具默认将 0-255 的 uint8 图像映射为 - 128~127 的 int8);代码如下

image

完整代码:

感谢您的补充说明!您已经找到了问题的核心:


:white_check_mark: 最终问题定位:

量化后的 BIN 模型要求输入为 int8 类型,且必须进行“偏移校正”(即从 uint8 [0,255] 映射到 int8 [-128, 127]),而 ONNX 模型会自动处理这个转换,因此推理结果对齐。


:brain: 为什么 bin 推理时检测不到目标?

因为:

  • 您的输入图像是 uint8 [0,255]
  • Bin 模型期望的是 int8 [-128, 127]
  • 如果不手动做偏移转换,模型接收到的是错误的数值范围 → 推理结果完全错乱
  • ONNX 模型内部做了自动类型转换和归一化 → 所以能正常工作

:wrench: 正确修复方案(结合您的实验 + 地平线官方文档)

:white_check_mark: 修改 preprocess_bgr 函数,加入 量化偏移转换

根据您提供的代码片段和地平线官方建议:

def preprocess_bgr(self, img):
    """
    BGR格式模型的预处理函数(替换原NV12预处理)
    适配量化配置:
    input_type_rt: 'bgr' 或 'rgb'
    input_layout_rt: 'NCHW'
    norm_type: 'data_scale'
    scale_value: 0.003921568627451 (1/255)
    """
    RESIZE_TYPE = 0
    LETTERBOX_TYPE = 1
    PREPROCESS_TYPE = LETTERBOX_TYPE
    logger.info(f"PREPROCESS_TYPE = {PREPROCESS_TYPE}")

    begin_time = time()
    self.img_h, self.img_w = img.shape[0:2]
    
    if PREPROCESS_TYPE == RESIZE_TYPE:
        input_tensor = cv2.resize(img, (self.input_W, self.input_H), interpolation=cv2.INTER_NEAREST)
        self.y_scale = 1.0 * self.input_H / self.img_h
        self.x_scale = 1.0 * self.input_W / self.img_w
        self.y_shift = 0
        self.x_shift = 0

    elif PREPROCESS_TYPE == LETTERBOX_TYPE:
        self.x_scale = min(1.0 * self.input_H / self.img_h, 1.0 * self.input_W / self.img_w)
        self.y_scale = self.x_scale
        
        new_w = int(self.img_w * self.x_scale)
        self.x_shift = (self.input_W - new_w) // 2
        x_other = self.input_W - new_w - self.x_shift
        
        new_h = int(self.img_h * self.y_scale)
        self.y_shift = (self.input_H - new_h) // 2
        y_other = self.input_H - new_h - self.y_shift
        
        input_tensor = cv2.resize(img, (new_w, new_h))
        input_tensor = cv2.copyMakeBorder(
            input_tensor, self.y_shift, y_other, self.x_shift, x_other,
            cv2.BORDER_CONSTANT, value=[127, 127, 127]
        )

    else:
        logger.error(f"illegal PREPROCESS_TYPE = {PREPROCESS_TYPE}")
        exit(-1)

    # ✅ 重要:不要转换颜色空间!保持 BGR(除非模型明确要求 RGB)
    # 因为您在配置中设了 input_type_rt: 'bgr',所以这里保留 BGR

    # ✅ 归一化:乘以 scale_value = 1/255 → 得到 [0,1]
    input_tensor = input_tensor.astype(np.float32) * 0.003921568627451

    # ✅ 转换为 CHW (NCHW)
    input_tensor = input_tensor.transpose(2, 0, 1)  # HWC → CHW
    input_tensor = np.expand_dims(input_tensor, axis=0)  # CHW → NCHW

    # ✅ 转为连续内存
    input_tensor = np.ascontiguousarray(input_tensor)

    # ✅ 关键修复:量化偏移 —— 将 uint8 [0,255] 映射为 int8 [-128, 127]
    # 注意:您当前是 float32,需要先转为 uint8,再做偏移
    # 但更推荐:在 float32 基础上直接做偏移(因为 scale_value 已经归一化到 [0,1])
    # 地平线官方推荐方式:
    #   input_tensor = (input_tensor - 128).astype(np.int8)
    # 但注意:您的 scale_value 是 1/255,所以输入是 [0,1],要映射到 [-128, 127]

    # 💡 推荐做法(兼容 float32 输入):
    input_tensor = (input_tensor * 255.0 - 128).astype(np.int8)

    # 或者,如果您希望保持原始逻辑(如您之前注释掉的),可以这样:
    # input_tensor = input_tensor.astype(np.uint8)  # 先转为 uint8
    # input_tensor = (input_tensor - 128).astype(np.int8)  # 再做偏移

    logger.info(f"预处理后 - 形状: {input_tensor.shape}, 类型: {input_tensor.dtype}, 均值: {np.mean(input_tensor):.4f}, 最大: {np.max(input_tensor):.4f}, 最小: {np.min(input_tensor):.4f}")
    logger.debug(f"pre process total time = {1000*(time() - begin_time):.2f} ms")
    logger.info(f"y_scale = {self.y_scale:.2f}, x_scale = {self.x_scale:.2f}")
    logger.info(f"y_shift = {self.y_shift:.2f}, x_shift = {self.x_shift:.2f}")
    return input_tensor

:pushpin: 为什么 input_tensor = (input_tensor - 128).astype(np.int8) 是正确的?

  • 地平线量化工具默认将 uint8 [0,255] 映射为 int8 [-128, 127]
  • 您的输入经过 * 0.003921568627451 后是 [0,1] 的浮点数
  • 所以要先乘以 255 → 得到 [0,255],再减去 128 → 得到 [-128, 127]
  • 这样才能与 BIN 模型的量化参数对齐

:test_tube: 验证方法:

  1. 打印输入张量的最小值和最大值

    print("Min:", np.min(input_tensor), "Max:", np.max(input_tensor))
    

    应该输出类似:Min: -128 Max: 127(或接近)

  2. hb_verifier 校验模型一致性,确保 ONNX 和 BIN 输入格式一致。

  3. 对比 ONNX 和 BIN 推理结果,确认是否现在对齐。


:police_car_light: 补充提醒:

  • 如果您在量化配置中设置了 norm_type: 'data_scale' 并指定了 scale_value,那么 输入必须是 [0,1] 范围,否则模型会出错。
  • 地平线的 BPU 模型对输入格式极其敏感,任何一步未按规范处理都会导致推理失败或结果偏差
  • ONNX 是浮点模型,它有更强的容错能力;BIN 是量化模型,它要求严格匹配。

:white_check_mark: 最终总结:

问题 修复方案
Bin 模型输入要求 int8 且需偏移 (-128) 在预处理最后添加:input_tensor = (input_tensor * 255.0 - 128).astype(np.int8)
ONNX 自动处理类型和归一化 所以它能工作,但 BIN 不行
颜色通道顺序 保持 BGR(如果配置为 bgr),或改为 RGB(如果配置为 rgb)
归一化步骤 必须在偏移前完成(先 * scale_value,再 -128

:light_bulb: 如果您还想进一步优化:

  1. 使用 hobot_dnn_model_info 查看模型输入要求

    hobot_dnn_model_info your_model.bin
    
  2. 参考官方文档
    :backhand_index_pointing_right: RDK X5 算法工具链手册

  3. 创建一个通用的预处理函数,支持多种输入格式(BGR/RGB、float/int8),便于复用。


:tada: 恭喜您找到问题根源!

现在您的 BIN 模型应该能正常推理,与 ONNX 结果对齐。这是典型的“量化模型部署坑”,很多开发者都会遇到。

如果您还有其他问题(比如如何调试 hb_verifier 输出、如何优化推理速度等),欢迎继续提问!

祝您调试顺利,早日搞定 RDK 部署!:rocket:

你好,感谢分享,我来记录一下问题,后续补充到Model Zoo中,也欢迎提一个pr 和issue来协助修复这个问题