目标检测是计算机视觉中的一个重要领域,它不仅要求模型能够识别图像中出现的对象属于哪一类(分类问题),还要求确定这些对象在图像中的具体位置(通常通过边界框来表示)。目标检测可以应用于多种场景,如自动驾驶、安防监控、医学影像分析等。
YOLO(You Only Look Once)是一种流行的目标检测算法系列,而YOLOv5是其中的一个版本。YOLOv5由Ultralytics公司开发,并不是正式的学术论文发布,但它因其高效性和易用性而在实践中得到了广泛应用。以下是YOLOv5的一些特点:
速度与精度:YOLOv5相比之前的版本,在保持较高检测精度的同时显著提升了处理速度。这使得它非常适合实时应用。
网络结构:YOLOv5的网络结构包括Backbone、Neck和Head三个主要部分。Backbone负责从输入图像中提取特征;Neck用于融合不同层次的特征图,以便更好地捕捉物体的多尺度信息;Head则基于融合后的特征图进行最终的边界框预测和类别判定。
预处理和增强:在输入网络之前,YOLOv5会对图片进行预处理,例如缩放、填充以保持宽高比等。此外,它还会应用数据增强技术,比如Mosaic,以增加训练集的多样性,从而提高模型的泛化能力。
多尺度预测:YOLOv5会在三个不同的尺度上进行预测,每个尺度对应着不同大小的目标。这样有助于提高对小目标或重叠目标的检测效果。
Anchor机制:YOLOv5使用了预定义的anchor boxes来帮助模型更好地适应不同形状和大小的目标。这些anchors是在训练过程中根据数据集自动生成的。
易于使用:YOLOv5提供了易于使用的接口和详细的文档,使得即使是初学者也能快速上手并将其应用于实际项目中。
完整代码可以前往 gitee 仓库查看
在边缘端设备部署神经网络模型时,通常需要配合剪枝/量化/蒸馏等手段加以辅助,提升模型的运行效率,目前在边缘端设备普遍被使用到的是模型量化,
模型量化是指将深度学习模型中的浮点参数和操作转换为定点数表示,如FLOAT32转换为INT8等。量化能够降低内存占用,实现模型压缩和推理加速,但会造成一定程度的精度损失。
以线性非对称量化为例,浮点数量化为有符号定点数的计算原理如下:
x i n t = clamp ( [ x s ] + z ; − 2 b − 1 , 2 b − 1 − 1 ) (1) x_{int} = \text{clamp}\left(\left[\frac{x}{s}\right] + z; -2^{b-1}, 2^{b-1} - 1\right) \tag{1} xint=clamp([sx]+z;−2b−1,2b−1−1)(1)
其中 $ x $ 为浮点数, $ x_{int} $ 为量化定点数, [ ⋅ ] [ \cdot ] [⋅] 为四舍五入运算, $ s $ 为量化比例因子, $ z $ 为量化零点, $ b $ 为量化位宽,如INT8数据类型中 $ b = 8 $; clamp 为截断运算,具体定义如下:
clamp ( x ; a , c ) = { a , x < a , x , a ≤ x ≤ c , c , x > c , (2) \text{clamp}(x; a, c) = \begin{cases} a, & x < a, \\ x, & a \leq x \leq c, \\ c, & x > c, \end{cases} \tag{2} clamp(x;a,c)=⎩ ⎨ ⎧a,x,c,x<a,a≤x≤c,x>c,(2)
从定点数转换为浮点数称为反量化过程,具体定义如下:
x ≈ x ^ = s ( x i n t − z ) (3) x \approx \hat{x} = s(x_{int} - z) \tag{3} x≈x^=s(xint−z)(3)
设量化范围为 ( q m i n , q m a x ) (q_{min}, q_{max}) (qmin,qmax),截断范围为 ( c m i n , c m a x ) (c_{min}, c_{max}) (cmin,cmax),量化参数 $ s $ 和 $ z $ 的计算公式如下:
s = q m a x − q m i n c m a x − c m i n = q m a x − q m i n 2 b − 1 (4) s = \frac{q_{max} - q_{min}}{c_{max} - c_{min}} = \frac{q_{max} - q_{min}}{2^b - 1} \tag{4} s=cmax−cminqmax−qmin=2b−1qmax−qmin(4)
z = c m a x − ⌊ q m a x s ⌋ 或 z = c m i n − ⌊ q m i n s ⌋ (5) z = c_{max} - \left\lfloor \frac{q_{max}}{s} \right\rfloor \quad \text{或} \quad z = c_{min} - \left\lfloor \frac{q_{min}}{s} \right\rfloor \tag{5} z=cmax−⌊sqmax⌋或z=cmin−⌊sqmin⌋(5)
其中截断范围是根据量化的数据类型决定,例如INT8的截断范围为(-128, 127); 量化范围根据不同的量化算法确定。
量化会造成模型一定程度的精度丢失。根据公式(1)可知,量化误差来源于舍入误差和截断误差,即 [ ⋅ ] [ \cdot ] [⋅] 和 $ \text{clamp} $ 运算。四舍五入的计算方式会产生舍入误差,误差范围为 ( − 1 2 s , 1 2 s ) \left(-\frac{1}{2}s, \frac{1}{2}s\right) (−21s,21s)。当浮点数 x x x 过大,比例因子 s s s 过小时,容易导致量化定点数超出截断范围,产生截断误差。理论上,比例因子 s s s 的增大可以减小截断误差,但会造成舍入误差的增大。因此为了权衡两种误差,需要设计合适的比例因子和零点,来减小量化误差。
线性量化中定点数之间的间隔是均匀的,例如INT8线性量化将量化范围均匀等分为256个数。线性对称量化中零点是根据量化数据类型确定并且零点 z z z 位于量化定点数范围上的中心对称点,例如INT8中零点为0。线性非对称量化中零点根据公式(5)计算确定并且零点 z z z 一般不在量化定点数范围上的中心对称点。
对称量化是非对称量化的简化版本,理论上非对称量化能够更好的处理数据分布不均匀的情况,因此实践中大多采用非对称量化方案。
量化比例因子 s s s 和零点 z z z 是影响量化误差的关键参数,而量化范围的求解对量化参数起到决定性作用。本文介绍三种关于量化范围求解的算法:Normal、KL-Divergence和MMSE。
Normal量化算法是通过计算浮点数中的最大值和最小值直接确定量化范围的最大值和最小值。从量化计算原理可知,Normal量化算法不会产生截断误差,但对异常值很敏感,因为大异常值可能会导致舍入误差过大。
q m i n = min V (6) q_{min} = \min \mathbf{V} \tag{6} qmin=minV(6)
q m a x = max V (7) q_{max} = \max \mathbf{V} \tag{7} qmax=maxV(7)
其中 V \mathbf{V} V 为浮点数Tensor。
KL-Divergence量化算法计算浮点数和定点数的分布,通过调整不同的阈值来更新浮点数和定点数的分布,并根据KL散度最小化两个分布的相似性来确定量化范围的最大值和最小值。KL-Divergence量化算法通过最小化浮点数和定点数之间的分布差异,能够更好地适应非均匀的数据分布并缓解少数异常值的影响。
arg min q m i n , q m a x H ( Ψ ( V ) , Ψ ( V i n t ) ) (8) \arg \min_{q_{min}, q_{max}} H(\Psi(\mathbf{V}), \Psi(\mathbf{V}_{int})) \tag{8} argqmin,qmaxminH(Ψ(V),Ψ(Vint))(8)
其中 H ( ⋅ , ⋅ ) H(\cdot, \cdot) H(⋅,⋅) 为KL散度计算公式, Ψ ( ⋅ ) \Psi(\cdot) Ψ(⋅) 为分布函数,将对应数据计算为离散分布, V i n t \mathbf{V}_{int} Vint 为量化定点数Tensor。
MMSE量化算法通过最小化浮点数与量化反量化后浮点数的均方误差损失,确定量化范围的最大值和最小值,在一定程度上缓解大异常值带来的量化精度丢失问题。由于MMSE量化算法的具体实现是采用暴力迭代搜索近似解,速度较慢,内存开销较大,但通常会比Normal量化算法具有更高的量化精度。
arg min q m i n , q m a x ∥ V − V ^ ( q m i n , q m a x ) ∥ F 2 (9) \arg \min_{q_{min}, q_{max}} \left\| \mathbf{V} - \widehat{\mathbf{V}}(q_{min}, q_{max}) \right\|_F^2 \tag{9} argqmin,qmaxmin V−V (qmin,qmax) F2(9)
其中 V ^ ( q m i n , q m a x ) \widehat{\mathbf{V}}(q_{min}, q_{max}) V (qmin,qmax) 为 V \mathbf{V} V 的量化、反量化形式, ∥ ⋅ ∥ F \left\| \cdot \right\|_F ∥⋅∥F 为F范数。
参考来源:RKNN Toolkit2
扯了这么多量化原理后,有童鞋可能在想废话这么多,那就直接上菜了
#include "yolov5.h"
int init_yolov5_model(const char* model_path, rknn_app_context_t* ctx);
int inference_yolov5_model(rknn_app_context_t* ctx,
object_detect_result_list* od_results);
void release_yolov5_model(rknn_app_context_t* ctx);
cv::Mat letterbox(cv::Mat& image);
void mapCoordinates(float& x, float& y);
void init_post_process();
void draw_detections(int count,
object_detect_result* results,
cv::Mat& frame,
void (*mapFunc)(float&, float&));
#include
#include
#include
#include "yolov5.h"
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include
#include
// output img size
#define DISP_WIDTH 320
#define DISP_HEIGHT 320
// disp size
int width = DISP_WIDTH;
int height = DISP_HEIGHT;
// model size
int model_width = 320;
int model_height = 320;
int leftPadding;
int topPadding;
// label size
extern int obj_class_num;
char *lable;
int main(int argc, char *argv[])
{
if (argc != 4)
{
LOGGER_INFO("Usage: %s ./yolov5_main model_path ./label size\n ./label_txt");
}
obj_class_num = atoi(argv[2]);
lable = argv[3];
// Rknn model
char text[16];
// rknn上下文结构体
rknn_app_context_t rknn_app_ctx;
object_detect_result_list od_results;
int ret;
const char *model_path = argv[1];
memset(&rknn_app_ctx, 0, sizeof(rknn_app_context_t));
// Step 1: Load RKNN model
if (init_yolov5_model(model_path, &rknn_app_ctx) != 0)
{
printf("❌ Failed to load RKNN model!\n");
return -1;
}
printf("✅ RKNN model loaded successfully.\n");
// 加载标签文件
init_post_process();
// 打开摄像头
lockzhiner_vision_module::edit::Edit edit;
if (!edit.StartAndAcceptConnection())
{
std::cerr << "Error: Failed to start and accept connection." << std::endl;
return EXIT_FAILURE;
}
cv::VideoCapture cap;
cap.set(cv::CAP_PROP_FRAME_WIDTH, 640);
cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480);
cap.open(0);
if (!cap.isOpened())
{
std::cerr << "Error: Could not open camera." << std::endl;
return 1;
}
cv::Mat frame;
// 在 while 循环外声明 start 和 end 变量
std::chrono::steady_clock::time_point start, end;
while (true)
{
// 记录开始时间
start = std::chrono::steady_clock::now();
// Step 2: Load image from command line
cap >> frame;
if (frame.empty())
{
LOGGER_INFO("❌ Failed to read frame from camera.\n");
continue;
}
cv::resize(frame, frame, cv::Size(width, height), 0, 0, cv::INTER_LINEAR);
cv::Mat letterboxImage = letterbox(frame);
if (letterboxImage.empty() || letterboxImage.total() * letterboxImage.elemSize() != model_width * model_height * 3)
{
LOGGER_ERROR("❌ Input image format or size mismatch!\n");
release_yolov5_model(&rknn_app_ctx);
return -1;
}
if (rknn_app_ctx.input_mems == nullptr || rknn_app_ctx.input_mems[0] == nullptr)
{
LOGGER_ERROR("❌ RKNN input memory not allocated!\n");
release_yolov5_model(&rknn_app_ctx);
return -1;
}
memcpy(rknn_app_ctx.input_mems[0]->virt_addr, letterboxImage.data, model_width * model_height * 3);
if (inference_yolov5_model(&rknn_app_ctx, &od_results) != 0)
{
LOGGER_ERROR("inference_yolov5_model failed");
release_yolov5_model(&rknn_app_ctx);
return -1;
}
draw_detections(od_results.count, // 传入结果数量
od_results.results, // 传入结果数组
frame, // 图像帧
mapCoordinates); // 直接使用现有坐标映射函数
edit.Print(frame);
// 记录结束时间
end = std::chrono::steady_clock::now();
// 计算耗时(秒)
double elapsed_time = std::chrono::duration<double>(end - start).count();
printf("Frame processed in %.4f seconds\n", elapsed_time);
}
release_yolov5_model(&rknn_app_ctx);
deinit_post_process();
cap.release();
return 0;
}
后处理代码可前往gitee 仓库查看,太懒了,不想 CV
文档版本:v1.0
最后更新时间:2025年5月15日