将编译生成的luckfox_rtsp_opencv_demo/luckfox_rtsp_opencv 和、lib目录以及模型权重model.rknn都上传到 luckfox-pico 上,进入文件夹运行.
chmod 755 luckfox_rtsp_opencv
./luckfox_rtsp_opencv ./model.rknn
代码实现细节:
定义结构体存储预测的数字及其概率
// 定义一个结构体储存预测到的数字和其对应的概率
struct Prediction
{
int digit;
float probability;
};
// 定义全局变量简单队列用于存储预测到的数字和其对应的概率
std::vector<Prediction> predictions_queue;
定义了加载模型的函数,用于从指定的二进制文件中加载模型数据,并确保在操作完成后释放资源。
// 函数名称: load_model
// 输入参数: filename - 要加载的模型文件名,model_size - 用于存储模型大小的指针
// 返回值: 加载成功则返回模型数据指针,失败则返回NULL
static unsigned char *load_model(const char *filename, int *model_size)
{
// 以二进制模式打开文件
FILE *fp = fopen(filename, "rb");
if (fp == nullptr)
{
// 打开文件失败,输出错误信息并返回NULL
printf("fopen %s fail!\\n", filename);
return NULL;
}
// 获取文件长度
fseek(fp, 0, SEEK_END);
int model_len = ftell(fp);
// 分配model_len字节大小的内存,类型为unsigned char
unsigned char *model = (unsigned char *)malloc(model_len);
// 重新定位文件指针到文件开始
fseek(fp, 0, SEEK_SET);
// 读取文件中的内容到分配的内存中
if (model_len != fread(model, 1, model_len, fp))
{
// 读取失败,输出错误信息并释放内存
printf("fread %s fail!\\n", filename);
free(model);
return NULL;
}
// 将实际读取到的模型长度赋给*model_size
*model_size = model_len;
// 关闭文件
if (fp)
{
fclose(fp);
}
// 返回模型数据的内存地址
return model;
}
定义函数find_digit_contour,在输入图像中查找数字的轮廓,并进行抖动减小和形状过滤。
// 函数名称:find_digit_contour
// 功能:在输入图像中查找数字的轮廓,并进行抖动减小和形状过滤
// 输入参数:image - 输入图像
// 返回值:bounding_box - 数字的边界框
cv::Rect find_digit_contour(const cv::Mat &image) {
// 预处理图像
cv::Mat gray, blurred, edged;
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY); // 转换为灰度图像
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0); // 高斯模糊去噪
cv::Canny(blurred, edged, 30, 150); // 边缘检测
// 应用形态学操作
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));
cv::dilate(edged, edged, kernel); // 膨胀操作
cv::erode(edged, edged, kernel); // 腐蚀操作
// 查找轮廓
std::vector<std::vector<cv::Point>> contours;
cv::findContours(edged, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
if (contours.empty()) {
return cv::Rect();
}
// 找到最大的轮廓
auto largest_contour = std::max_element(contours.begin(), contours.end(),
[](const std::vector<cv::Point>& a, const std::vector<cv::Point>& b) {
return cv::contourArea(a) < cv::contourArea(b);
});
// 轮廓面积过滤
if (cv::contourArea(*largest_contour) < 10) {
return cv::Rect();
}
// 轮廓形状过滤
cv::Rect bounding_box = cv::boundingRect(*largest_contour);
float aspect_ratio = static_cast<float>(bounding_box.width) / bounding_box.height;
if (aspect_ratio < 0.2 || aspect_ratio > 3) {
return cv::Rect();
}
// 轮廓稳定性检测
static std::vector<cv::Rect> prev_bounding_boxes;
if (prev_bounding_boxes.size() > 5) {
prev_bounding_boxes.erase(prev_bounding_boxes.begin());
}
prev_bounding_boxes.push_back(bounding_box);
if (prev_bounding_boxes.size() == 5) {
float avg_width = 0.0;
float avg_height = 0.0;
for (const auto& box : prev_bounding_boxes) {
avg_width += box.width;
avg_height += box.height;
}
avg_width /= prev_bounding_boxes.size();
avg_height /= prev_bounding_boxes.size();
float width_diff = std::abs(bounding_box.width - avg_width) / avg_width;
float height_diff = std::abs(bounding_box.height - avg_height) / avg_height;
if (width_diff > 0.1 || height_diff > 0.1) {
return cv::Rect();
}
}
// 扩大边界框,以包含更多相关像素
bounding_box.x = std::max(0, bounding_box.x - 15);
bounding_box.y = std::max(0, bounding_box.y - 15);
bounding_box.width = std::min(image.cols - bounding_box.x, bounding_box.width + 30);
bounding_box.height = std::min(image.rows - bounding_box.y, bounding_box.height + 30);
// 返回最大轮廓的边界框
return bounding_box;
}
这段代码的主要功能是在输入图像中查找数字的轮廓,并进行抖动减小和形状过滤。主要包括以下步骤:
- 图像被预处理,包括灰度化、高斯模糊和边缘检测。
- 形态学操作(膨胀和腐蚀)用于去除噪声和连接轮廓。
- 查找所有外部轮廓,并选择最大的轮廓。对于最大的轮廓,进行面积和形状过滤,以减少不必要的小轮廓。
- 对图像边框每个方向扩大15个像素,以包含更多相关像素。
- 返回最终的边界框。
定义图像预处理函数,其目的是对输入的数字区域图像进行预处理,以便于后续的数字识别。
// 函数名称:preprocess_digit_region
// 功能:对输入的数字区域图像进行预处理,包括灰度转换、二值化、颜色反转和大小调整
// 输入参数:region - 输入的数字区域图像
// 返回值:resized - 预处理后的图像,大小为28x28,像素值归一化为0到1之间的浮点数
cv::Mat preprocess_digit_region(const cv::Mat ®ion) {
// 将输入图像转换为灰度图像
cv::Mat gray;
cv::cvtColor(region, gray, cv::COLOR_BGR2GRAY);
// 使用OTSU方法自动确定阈值,将灰度图像二值化
cv::threshold(gray, gray, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
// 再次应用阈值处理,将灰度值低于127的像素设置为0,高于200的像素设置为255
cv::threshold(gray, gray, 127, 255, cv::THRESH_BINARY_INV);
// 创建一个与gray相同大小的Mat对象,用于存储颜色反转后的图像
cv::Mat bitwized = cv::Mat::zeros(gray.size(), gray.type());
// 对图像进行颜色反转,将黑色变成白色,白色变成黑色
cv::bitwise_not(gray, bitwized);
// 手动实现颜色反转,以验证bitwise_not函数的效果
for (int i = 0; i < bitwized.rows; i++) {
for (int j = 0; j < bitwized.cols; j++) {
bitwized.at<uchar>(i, j) = 255 - bitwized.at<uchar>(i, j);
}
}
// 将处理后的图像大小调整为28x28,使用INTER_AREA插值方法以保持图像细节
cv::Mat resized;
cv::resize(bitwized, resized, cv::Size(28, 28), 0, 0, cv::INTER_AREA);
// 返回调整大小后的图像
return resized;
}
这段代码的主要步骤包括:
- 将输入的彩色图像转换为灰度图像。
- 使用OTSU方法自动确定阈值,将灰度图像二值化。
- 再次应用阈值处理,调整图像的颜色分布。
- 对二值化后的图像进行颜色反转。
- 手动验证颜色反转的效果。
- 将图像大小调整为28x28,使用INTER_AREA插值方法以保持图像细节。
- 返回预处理后的图像
后处理阶段:
deqnt_affine_to_f32函数:将量化后的INT8数据转化为浮点数。
output_normalization函数:将模型的输出进行归一化,并计算输出的概率分布。
// 将量化的INT8数据转换为浮点数
// Parameters:
// qnt: 量化后的整数数据
// zp: 零点(zero point)值,用于零点偏移(zero-point offset)
// scale: 缩放因子,用于缩放量化后的整数数据到浮点数范围
// Returns:
// 浮点数,表示经过反量化(dequantization)后的数据
static float deqnt_affine_to_f32(int8_t qnt, int32_t zp, float scale) {
// 返回((float)qnt - (float)zp) * scale,即量化值减去零点值后乘以缩放因子
return ((float)qnt - (float)zp) * scale;
}
// 将模型输出进行归一化,并计算输出的概率分布
// Parameters:
// output_attrs: 输出张量属性,包含了零点(zero point)值和缩放因子等信息
// output: 模型输出的数据,以INT8格式存储
// out_fp32: 存储归一化后的浮点数输出数据
static void output_normalization(rknn_tensor_attr* output_attrs, uint8_t *output, float *out_fp32)
{
int32_t zp = output_attrs->zp; // 从输出张量属性中获取零点值
float scale = output_attrs->scale; // 从输出张量属性中获取缩放因子
// 将INT8格式的输出数据进行反量化为浮点数,并进行存储
for(int i = 0; i < 10; i ++)
out_fp32[i] = deqnt_affine_to_f32(output[i],zp,scale);
// 计算输出数据的L2范数
float sum = 0;
for(int i = 0; i < 10; i++)
sum += out_fp32[i] * out_fp32[i];
// 对归一化后的浮点数输出进行归一化处理,确保输出数据的范围在[0,1]之间
float norm = sqrt(sum);
for(int i = 0; i < 10; i++)
out_fp32[i] /= norm;
// 打印输出数据的值
printf("\\n===================Output data values:===================\\n");
for (int i = 0; i < 10; ++i)
{
printf("%f ", out_fp32[i]);
}
printf("\\n");
// 找出最大概率对应的数字,并记录最大概率及其对应的数字
float max_prob = -1.0;
int predicted_digit = -1;
// 计算最大值的索引
for (int i = 0; i < 10; ++i)
{
if (out_fp32[i] > max_prob)
{
max_prob = out_fp32[i];
predicted_digit = i;
}
}
// 将预测的数字及其对应的概率记录到队列中
predictions_queue.push_back({predicted_digit, max_prob});
// 打印预测的数字与其对应的概率
printf("========Predicted digit: %d, Probability: %.2f========\\n\\n", predicted_digit, max_prob);
}
- 使用deqnt_affine_to_f32函数将INT8格式的数据转换为浮点数。
- 在output_normalization函数中,对转换后的浮点数数据进行归一化处理,使其概率分布的总和为1。计算并打印每个数字的概率值,找出最大概率对应的数字,并将其预测结果和概率记录到队列中。
定义推理函数,主要用于处理输入的图像数据,将其转换为模型所需的格式,并进行推理以获取输出结果。包括以下功能:
- 获取模型的输入输出属性。
- 处理输入图像,将其转换为模型所需的格式。
- 创建并设置输入输出张量的内存。
- 运行模型推理。
- 处理输出结果,获取预测的数字。
- 释放分配的内存。
// 定义函数run_inference,接收一个cv::Mat类型的图像帧作为输入
int run_inference(cv::Mat &frame)
{
int ret = 0; // 初始化返回值为0
rknn_input_output_num io_num; // 定义结构体用于存储输入输出通道数
// 获取模型的输入输出通道数
rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
// 初始化输入属性数组
rknn_tensor_attr input_attrs[io_num.n_input];
memset(input_attrs, 0, io_num.n_input * sizeof(rknn_tensor_attr));
for (uint32_t i = 0; i < io_num.n_input; i++)
{
input_attrs[i].index = i; // 设置输入属性的索引
// 查询输入属性的详细信息
ret = rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &(input_attrs[i]), sizeof(rknn_tensor_attr));
if (ret < 0)
{
printf("rknn_init error! ret=%d\\n", ret);
return -1; // 如果查询失败,返回错误
}
dump_tensor_attr(&input_attrs[i]); // 打印输入属性信息
}
printf("output tensors:\\n");
// 初始化输出属性数组
rknn_tensor_attr output_attrs[io_num.n_output];
memset(output_attrs, 0, io_num.n_output * sizeof(rknn_tensor_attr));
for (uint32_t i = 0; i < io_num.n_output; i++)
{
output_attrs[i].index = i; // 设置输出属性的索引
// 查询输出属性的详细信息
ret = rknn_query(ctx, RKNN_QUERY_NATIVE_OUTPUT_ATTR, &(output_attrs[i]), sizeof(rknn_tensor_attr));
if (ret != RKNN_SUCC)
{
printf("rknn_query fail! ret=%d\\n", ret);
return -1; // 如果查询失败,返回错误
}
dump_tensor_attr(&output_attrs[i]); // 打印输出属性信息
}
printf("Gray image size: %dx%d\\n", frame.rows, frame.cols);
printf("Gray image type: %d\\n", frame.type());
// 计算并分配用于存储调整大小后图像的内存
int mem_size = MODEL_WIDTH * MODEL_HEIGHT * CHANNEL_NUM;
unsigned char *resize_buf = (unsigned char *)malloc(mem_size);
memset(resize_buf, 0, mem_size);
// 创建输入张量内存
rknn_tensor_mem *input_mems[1];
input_attrs[0].type = input_type; // 设置输入类型
input_attrs[0].fmt = input_layout; // 设置输入格式
input_mems[0] = rknn_create_mem(ctx, input_attrs[0].size_with_stride);
// 将输入数据复制到输入张量内存
int width = input_attrs[0].dims[2];
int stride = input_attrs[0].w_stride;
if (width == stride)
{
memcpy(input_mems[0]->virt_addr, frame.data, width * input_attrs[0].dims[1] * input_attrs[0].dims[3]);
}
else
{
int height = input_attrs[0].dims[1];
int channel = input_attrs[0].dims[3];
uint8_t *src_ptr = frame.data;
uint8_t *dst_ptr = (uint8_t *)input_mems[0]->virt_addr;
int src_wc_elems = width * channel;
int dst_wc_elems = stride * channel;
for (int h = 0; h < height; ++h)
{
memcpy(dst_ptr, src_ptr, src_wc_elems);
src_ptr += src_wc_elems;
dst_ptr += dst_wc_elems;
}
}
// 创建输出张量内存
rknn_tensor_mem *output_mems[io_num.n_output];
for (uint32_t i = 0; i < io_num.n_output; ++i)
{
output_mems[i] = rknn_create_mem(ctx, output_attrs[i].size_with_stride);
}
// 设置输入张量内存
ret = rknn_set_io_mem(ctx, input_mems[0], &input_attrs[0]);
if (ret < 0)
{
printf("rknn_set_io_mem fail! ret=%d\\n", ret);
return -1;
}
// 设置输出张量内存
for (uint32_t i = 0; i < io_num.n_output; ++i)
{
ret = rknn_set_io_mem(ctx, output_mems[i], &output_attrs[i]);
if (ret < 0)
{
printf("rknn_set_io_mem fail! ret=%d\\n", ret);
return -1;
}
}
// 运行推理
ret = rknn_run(ctx, nullptr);
if (ret < 0)
{
printf("rknn_run failed! %s\\n", ret);
return -1;
}
uint8_t *output= (uint8_t*)malloc(sizeof(uint8_t) * 10);
float *out_fp32 = (float*)malloc(sizeof(float) * 10);
output = (uint8_t *)output_mems[0]->virt_addr;
// 获取预测的数字
output_normalization(&output_attrs[0], output, out_fp32);
// 释放内存
rknn_destroy_mem(ctx, input_mems[0]);
for (uint32_t i = 0; i < io_num.n_output; ++i)
{
rknn_destroy_mem(ctx, output_mems[i]);
}
}
主函数主要目的是在实时视频流中检测并显示数字,同时将视频编码后发送到rtsp服务器。包括以下步骤:
- 初始化必要的模块,如rkaiq、rkmpi、rtsp等。
- 通过rkmpi绑定Vi(Video Interface)和VPSS(Video Processing System)模块,用于视频处理和编码。
- 初始化VENC(Video Encoder)模块,用于视频编码。
- 加载并初始化rknn模型,进行手写数字识别。
- 在一个循环中,从VPSS获取帧,进行物体识别和数字预测,然后编码成H264并发送到rtsp流。
- 更新帧率(fps)并显示在图像上。
- 在每次循环结束时,释放帧资源和内存。
- 最后,释放所有模块的资源,销毁rknn模型,并退出程序。
首先我们需要,定义图像分辨率
int width = 640;
int height = 480;
在这里,我们主要介绍while循环里的逻辑代码
1.opencv获取摄像头帧,并调用find_digit_contour函数在其中查找数字的轮廓,并进行抖动减小和形状过滤。
void *data = RK_MPI_MB_Handle2VirAddr(stVpssFrame.stVFrame.pMbBlk);
cv::Mat frame(height, width, CV_8UC3, data);
cv::Rect digit_rect = find_digit_contour(frame);
2.if (digit_rect.area() > 0),opencv截取数字的区域并进行预处理,然后将预处理后的数据送入run_inference函数进行数据推理。
cv::Mat digit_region = frame(digit_rect);
cv::Mat preprocessed = preprocess_digit_region(digit_region);
int prediction = run_inference(preprocessed);
3.对当前帧上显示识别结果。
// 从predictions_queue中获取预测到的数字和其对应的概率
//检查predictions_queue是否为空,如果不为空,则取出最后一个元素作为当前帧的识别结果。
if (!predictions_queue.empty())
{
Prediction prediction = predictions_queue.back();
cv::rectangle(frame, digit_rect, cv::Scalar(0, 255, 0), 2);
// 在图像上显示预测结果,显示字号为1,颜色为红色,粗细为2
cv::putText(frame, std::to_string(prediction.digit), cv::Point(digit_rect.x, digit_rect.y - 10),
cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(255, 0, 0), 2);
// 在图像上显示预测概率
cv::putText(frame, std::to_string(prediction.probability), cv::Point(digit_rect.x+ 30, digit_rect.y - 10),
cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(230, 0, 0), 2);
// 从predictions_queue中删除最旧的元素,以便下一次迭代时可以取出新的识别结果
predictions_queue.pop_back();
}
4.最后当前帧图像数据复制到rtsp帧中
memcpy(data, frame.data, width * height * 3);