本帖最后由 qiao--- 于 2024-5-25 14:50 编辑
前言:
经过前面两位贴友铺路,我的模型也是成功的部署。与前面两位贴友不同的是,我是将显示结果显示到屏幕上,所以多了一步驱动适配的过程。
因为我前面已经发布了st7735屏幕的适配过程,我这里就不在赘述了。有兴趣的贴友可以去看看我的这一篇帖子:【Luckfox幸狐 RV1106 Linux 开发板】4-SPI测试__驱动RGB_TFT正常显示 https://bbs.eeworld.com.cn/thread-1271812-1-1.html
然后模型的训练可以去看我之前的两个帖子:
模型训练:#AI挑战营第一站#MNIST手写数字识别模型训练 https://bbs.eeworld.com.cn/thread-1277782-1-1.html
模型转换:#AI挑战营第二站#Ubuntu22上将ONNX模型转换成RKNN模型 https://bbs.eeworld.com.cn/thread-1280156-1-1.html
如果有贴友对烧录过程和使用过程不熟悉的也可去看我之前的测评,这里我来个汇总:
【Luckfox幸狐 RV1106 Linux 开发板】1-开箱及上电测评
【Luckfox幸狐 RV1106 Linux 开发板】2-搭建开发环境和镜像烧录
【Luckfox幸狐 RV1106 Linux 开发板】3-PWM测试__控制舵机任意旋转
【Luckfox幸狐 RV1106 Linux 开发板】4-SPI测试__驱动RGB_TFT正常显示
【Luckfox幸狐 RV1106 Linux 开发板】5-给系统移植QT环境__显示电子时钟界面
【Luckfox幸狐 RV1106 Linux 开发板】6-ADC测试
【Luckfox幸狐 RV1106 Linux 开发板】7-基于ffmpeg的视频播放测试
【Luckfox幸狐 RV1106 Linux 开发板】8-opencv人脸识别测评
本次工程的代码我上传到了我的仓库,有需要可自取:
下面开始正题,如何将自己训练的手写模型部署到开发板上呢。
1.熟悉API
参考内容:RKNN 推理测试 | LUCKFOX WIKI
下面是模型部署的API使用流程图,我们可以参考这个来使用官方的API
2.实战操作
定义模型上下文
rknn_app_context_t rknn_app_ctx;
memset(&rknn_app_ctx, 0, sizeof(rknn_app_context_t));
init_mnist_model(model_path, &rknn_app_ctx);
其中rknn_app_context_t结构体如下,存储rknn的上下文
typedef struct {
rknn_context rknn_ctx;
rknn_tensor_mem* max_mem;
rknn_tensor_mem* net_mem;
rknn_input_output_num io_num;
rknn_tensor_attr* input_attrs;
rknn_tensor_attr* output_attrs;
rknn_tensor_mem* input_mems[3];
rknn_tensor_mem* output_mems[3];
int model_channel;
int model_width;
int model_height;
bool is_quant;
} rknn_app_context_t;
因为我们要把结果显示到屏幕上,所以我们还要初始化屏幕,代码如下
//Init fb
int fb = open("/dev/fb0", O_RDWR);
if(fb == -1)
{
close(fb);
return -1;
}
size_t screensize = FB_WIDTH * FB_HEIGHT * 2;
uint16_t* framebuffer = (uint16_t*)mmap(NULL, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fb, 0);
这里我们使用mmap函数将帧缓冲设备文件/dev/fb0映射到内存中的一段区域,这里我们就可以操作这块内存来进行显示了。
就下来我们要初始化摄像头,和帧数据,其中bgr565用于显示到我们屏幕上的帧数据。
//Init Opencv-mobile
cv::VideoCapture cap;
cv::Mat bgr(disp_height, disp_width, CV_8UC3);
cv::Mat bgr565(disp_height, disp_width, CV_16UC1);
cv::Mat bgr640(model_height, model_width, CV_8UC3, rknn_app_ctx.input_mems[0]->virt_addr);
cap.set(cv::CAP_PROP_FRAME_WIDTH, disp_width);
cap.set(cv::CAP_PROP_FRAME_HEIGHT, disp_height);
cap.open(0);
下面为输入图像的操作,我们直接可以用>>进行输入图像,因为opencv-mobile库对这个操作符进行重载了。
start_time = clock();
cap >> bgr;
cv::resize(bgr, bgr640, cv::Size(model_width, model_height), 0, 0, cv::INTER_LINEAR);
下面进行对输入的图片帧进行预处理,因为我们需要的灰度图像(训练模型是灰度),下面这部分参考的luyism兄弟的。这里代码写的一次只能对最大的轮廓的数字进行识别,因为返回了最大轮廓的区域。
// 在图像中找到数字的轮廓,同时减小找到轮廓时的抖动
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();
}
}
// 对图像边框每个方向扩大15个像素
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;
}
这里对所选轮廓进行预处理,让这个帧变成28*28*1的图像
// 预处理数字区域
cv::Mat preprocess_digit_region(const cv::Mat region)
{
// 将图像转换为灰度图像,然后调整大小为28x28,最后将像素值归一化为0到1之间的浮点数
cv::Mat gray, resized, bitwized, normalized;
cv::cvtColor(region, gray, cv::COLOR_BGR2GRAY);
// 扩大图像中的数字轮廓,使其更容易识别
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);
// 对图像黑白进行反转,黑色变成白色,白色变成黑色
cv::bitwise_not(gray, bitwized);
// 手动实现黑白反转
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,图片形状不发生畸变,过短的部分使用黑色填充
cv::resize(bitwized, resized, cv::Size(28, 28), 0, 0, cv::INTER_AREA);
//定义一个局部静态变量,让其只初始化一次,让其在等于200时将图片保存到本目录下
static int count = 0;
if (count == 5)
{
cv::imwrite("pre.jpg", resized);
}
count++;
printf("count=%d\n", count);
return resized;
}
紧接着开始对这个区域进行识别
if (digit_rect.area() > 0)
{
cv::Mat digit_region = bgr(digit_rect);
cv::Mat preprocessed = preprocess_digit_region(digit_region);
// 运行推理
run_inference(&rknn_app_ctx, preprocessed);
// 从predictions_queue中获取预测到的数字和其对应的概率
if (!predictions_queue.empty())
{
Prediction prediction = predictions_queue.back();
cv::rectangle(bgr, digit_rect, cv::Scalar(0, 255, 0), 2);
// 在图像上显示预测结果,显示字号为1,颜色为红色,粗细为2
cv::putText(bgr, 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(bgr, 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);
// 打印预测到的数字和其对应的概率
// printf("****** Predicted digit: %d, Probability: %.2f ******\n", prediction.digit, prediction.probability);
// 从predictions_queue中删除最旧的元素
predictions_queue.pop_back();
}
}
打上帧率
sprintf(text,"fps=%.1f",fps);
cv::putText(bgr,text,cv::Point(0, 20),cv::FONT_HERSHEY_SIMPLEX,0.5, cv::Scalar(0,255,0),1);
最后就是将我们结果帧显示到fb设备映射的内存区域就行了,如下:
//LCD Show
cv::cvtColor(bgr, bgr565, cv::COLOR_BGR2BGR565);
memcpy(framebuffer, bgr565.data, disp_width * disp_height * 2);
#if USE_DMA
dma_sync_cpu_to_device(framebuffer_fd);
#endif
//Update Fps
end_time = clock();
fps= (float) (CLOCKS_PER_SEC / (end_time - start_time)) ;
memset(text,0,16);
在程序结尾记得释放资源,代码如下
deinit_post_process();
#if USE_DMA
dma_buf_free(disp_width*disp_height*2, &framebuffer_fd, bgr.data);
#endif
ret = release_yolov5_model(&rknn_app_ctx);
if (ret != 0)
{
printf("release_yolov5_model fail! ret=%d\n", ret);
}
这样我们的主要代码部分就编写完了。
3.编译
工程目录是使用cmake进行管理的。
我们进入cpp目录,创建build目录存放编译结果。然后运行下面步骤进行编译。
cd build
export LUCKFOX_SDK_PATH=<Your Luckfox-pico Sdk Path>
cmake ..
make && make install
最后就可以看到我们可执行和库文件就出现在下面这个目录
我们将上面这个目录打包scp到我们的板子上就OK了
4.运行与运行结果
装上摄像头,系统是有一个默认的rkipc进行占用了摄像头,我们可以运行关闭脚本,也可以暴力杀死这个进程。
我使用的是暴力法,先看看进程的 PID
然后用kill 终止这个进程就OK了
运行输入./luckfox_yolov5_demo_test model/mnist.rknn即可运行,如下
运行效果如下,我照了几张识别图片,然后拍了一个视频效果,如下所示。
视频效果如下所示
VID_20240525_023139
总结:从上面的效果来看准确率还是可以的,能达到70左右的准确率。代码在: ,其中也包含了我跑的yolo代码。
|