3960|0

224

帖子

0

TA的资源

一粒金砂(高级)

楼主
 

#AI挑战营终点站#数字识别模型部署 [复制链接]

本帖最后由 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代码。

 

点赞(1) 关注

回复
举报
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/10 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表