- 2024-05-31
-
回复了主题帖:
【AI挑战营终点站】应用落地:部署手写数字识别应用到幸狐RV1106开发板
打卡完成
#AI挑战营终点站#基于RV1106手写数字识别部署 - 嵌入式系统 - 电子工程世界-论坛 (eeworld.com.cn)
-
发表了主题帖:
#AI挑战营终点站#基于RV1106手写数字识别部署
本帖最后由 xianhangCheng 于 2024-5-30 15:57 编辑
实物图片
上图包括RV1106开发板+SC3336摄像头,这板子挺精致,自带的 SPI NAND FLASH,并在出厂时搭载了测试系统,收到板子后需要自行烧录网盘系统。
这个价格与性能来说,性价比很高,如此小巧的板子,板载256MB DDR3L,还可以直接运行yolov5。唯一的缺点就是在运行推理时发热挺严重的。
准备
参考手册:Luckfox官网教程SPI NAND Flash 镜像烧录 | LUCKFOX WIKI
提前准备工具:
1.下载RK驱动助手 DriverAssitant(戳我下载)。
2.下载和解压烧录工具(戳我下载)。
3.下载镜像文件镜像百度网盘链接。
4.下载VLC media player官方 VLC 媒體播放器下載,最好的開放原始碼播放器 - VideoLAN
注意:在下载RK驱动助手和烧录工具时最好关闭防火墙和病毒与威胁防护,否则可能导致下述状况:
驱动程序无法使用;
无法识别出板子。
如下图:
开启板子和摄像头
烧录镜像:
参考:SPI NAND Flash 镜像烧录 | LUCKFOX WIKI
首先我们要对板子进行烧录,官方提供了linux的镜像。提供的有ubuntu的镜像和buildroot镜像。这里我们选择官方推荐的buildroot镜像,活动的板子是Max系列,我们选择luckfox_pico_pro_max_image镜像。
登录:
参考文件:SSH/Telnet 登录 | LUCKFOX WIKI
Luckfox Pico 系列的最新固件默认启用了 SSH。这里我们通过 USB 连接使用静态 IP 进行登录。
然后我们关闭防火墙,配置电脑 rndis网口的ip地址 为172.32.0.100 子网掩码 255.255.0.0,此时可以通过ssh 访问 172.32.0.93。用自带的 Powershell 终端输入口令进行直接登录。格式是 ssh 客户端用户名@服务器ip地址
ssh root@172.32.0.93
登录账号:root
登录密码:luckfox
静态IP地址:172.32.0.93
通过ls命令查看都有哪些文件。
系统将自动识别摄像头,生成 rkipc.ini 文件。
下载并安装 VLC media player 。
打开 VLC media player 软件,在媒体—>打开网络串流,输入默认的 IP 地址:rtsp://172.32.0.93/live/0
这样子就可以看到摄像头拍摄的画面了。
注意:为了后续的部署,我们需要关闭系统默认 rkipc 程序,执行 RkLunch-stop.sh 命令。
配置开发环境:
参考文件:SDK 环境部署(PC端) | LUCKFOX WIKI
这里较为复杂,我也不懂,跟着教程照搬就可以了。
编译与运行:
export LUCKFOX_SDK_PATH=<Your Luckfox-pico Sdk Path>
mkdir build
cd build
cmake ..
make && make install
将编译生成的luckfox_rtsp_opencv_demo/luckfox_rtsp_opencv 和、lib目录以及模型权重model.rknn都上传到 luckfox-pico 上,进入文件夹运行.
文件传输命令:
# 传输文件
scp model.rknn root@172.32.0.93:/root
# 传输文件夹
scp -r luckfox_rtsp_opencv_demo root@172.32.0.93:/root
注意:首次连接需要输入 yes 确认,然后输入密码 luckfox 开始传输
运行目录下应有 luckfox_rtsp_opencv lib model.rknn 三个文件.
赋予权限并运行。
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);
实际演示效果
没有设备固定摄像头,一边录屏一边手拿摄像头,视频有点晃动。
但是数字识别的效果还是不错的,也没有数字识别错误的情况。数字0~9全部识别正确。
[localvideo]499910638be8d84fc57eadd6fd509e5e[/localvideo]
附参考代码:GitHub - knva/luckfox_pico_rtsp_opencv
- 2024-05-09
-
回复了主题帖:
入围名单公布:嵌入式工程师AI挑战营(初阶),获RV1106 Linux 板+摄像头的名单
个人信息已确认,领取板卡,可继续完成&分享挑战营第二站和第三站任务。
- 2024-05-08
-
回复了主题帖:
【AI挑战营第二站】算法工程化部署打包成SDK
1、跟帖回复:什么是ONNX模型、RKNN模型
OXXN模型:ONNX是一种用于表示深度学习模型的开放源代码格式,旨在促进不同深度学习框架(如pytorch、mxnet等)之间的模型互操作性和转换。通过ONNX,可以在一个深度学习框架中训练模型,然后将其转换为ONNX格式,最后在另一个支持ONNX的框架中加载和使用该模型,从而实现跨框架的模型迁移和共享。
RKNN模型:由瑞芯微(Rockchip)推出的一种神经网络模型转换工具。RKNN模型是为了在Rockchip的芯片上进行深度学习模型的高效部署而设计的。RKNN模型的主要功能包括将深度学习模型转换成Rockchip芯片上的可执行文件,以便在嵌入式设备、移动设备或其他Rockchip芯片支持的平台上进行推理。通过RKNN模型,用户可以将经过训练的神经网络模型(如TensorFlow、Caffe等)转换成Rockchip芯片所支持的格式,以实现在硬件上高效地运行神经网络模型。
2、以#AI挑战营第二站#为标题前缀,发帖分享ONNX模型转换成RKNN模型过程,包含环境部署、转换代码、解读代码,并附上RKNN模型。
#AI挑战营第二站# windows下基于RV1106开发板的ONNX转RKNN模型 - 嵌入式系统 - 电子工程世界-论坛 (eeworld.com.cn)
-
发表了主题帖:
#AI挑战营第二站# windows下基于RV1106开发板的ONNX转RKNN模型
本帖最后由 xianhangCheng 于 2024-5-8 11:54 编辑
介绍
OXXN模型:ONNX是一种用于表示深度学习模型的开放源代码格式,旨在促进不同深度学习框架(如pytorch、mxnet等)之间的模型互操作性和转换。通过ONNX,可以在一个深度学习框架中训练模型,然后将其转换为ONNX格式,最后在另一个支持ONNX的框架中加载和使用该模型,从而实现跨框架的模型迁移和共享。
RKNN模型:由瑞芯微(Rockchip)推出的一种神经网络模型转换工具。RKNN模型是为了在Rockchip的芯片上进行深度学习模型的高效部署而设计的。RKNN模型的主要功能包括将深度学习模型转换成Rockchip芯片上的可执行文件,以便在嵌入式设备、移动设备或其他Rockchip芯片支持的平台上进行推理。通过RKNN模型,用户可以将经过训练的神经网络模型(如TensorFlow、Caffe等)转换成Rockchip芯片所支持的格式,以实现在硬件上高效地运行神经网络模型。
模型转换:将ONNX模型转换为RKNN模型是通常需要搭建好python环境,并使用ONNX-RKNN转换工具,例如 rknn-toolkit,这是由RKNN官方提供的转换工具。可以在RKNN的GitHub上找到它瑞芯微-linux/rknn-工具包 --- rockchip-linux/rknn-toolkit (github.com)。其转换过程如下:
加载ONNX模型:使用Python的ONNX库(onnx)加载你的ONNX模型。
检查兼容性:确保ONNX模型的结构和层与RKNN支持的模型结构兼容。某些复杂架构可能需要调整。
转换模型:使用rknn-toolkit的convert命令将ONNX模型转换为RKNN模型。这通常涉及指定输入和输出格式,以及可能的模型优化参数。
配置RKNN:可能需要根据目标设备(如Raspberry Pi)的性能和内存限制进行模型配置。
编译和优化:生成RKNN模型文件,这个文件可以在嵌入式设备上运行。
环境配置
我们可以看到,根据介绍我们使用的是基于RV1106开发板,所以这里我们选择下载rknn-toolkit2.
我们根据提示从https://github.com/rockchip-linux/rknn-toolkit/releases下载 rknn-toolkit-v1.7.5-packages.tar.gz或者rknn-toolkit-v1.7.5-packages.zip。
下载完,解压后能看见多个平台的whl文件,这里因为用的是win所以选择rknn_toolkit-1.7.5-cp36-cp36m-win_amd64.whl这个文件,并选择requirements_cpu.txt作为我们的安装包。并且我们观察到这个whl文件是cp36,即win环境下支持python3.6。所以接下来我们开始配置环境。
使用 Conda 创建 Python 环境。
#创建conda虚拟环境
conda create --name=RKNN python=3.6
#激活环境
conda activate RKNN
进入解压的rknn-toolkit-v1.7.5-packages目录里,安装packages/requirements_cpu.txt文件里的依赖包,path_txt是自己解压的rknn-toolkit-v1.7.5-packages/packages目录下的requirements_cpu.txt文件在电脑硬盘里的路径。
pip install -r path_txt
最后安装rknn_toolkit-1.7.5-cp36-cp36m-win_amd64.whl文件,path_whl是自己解压的自己解压的rknn-toolkit-v1.7.5-packages/packages目录下的rknn_toolkit-1.7.5-cp36-cp36m-win_amd64.whl文件在电脑硬盘里的路径。
pip install path_whl
踩坑在安装whl文件时,可能会遇到错误,比如我遇到AssertionError: Could not find “cmake“ executable!导致的ERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full。(已解决,忘记截图了),这时候网上部分教程是更新pip、setuptools包;或者pip install xxx改为pip3 install xxx。但这些都不能解决!!!应该根据提示的具体错误加以解决。比如我遇到的AssertionError: Could not find “cmake“ executable!错误解决,需要安装cmake才能解决。安装命令:pip install cmake
ONNX模型转换为RKNN模型
from rknn.api import RKNN
# 创建对象
rknn = RKNN()
# 加载ONNX模型
print('--> Loading model')
ret = rknn.load_onnx(model="./minst_model.onnx")
if ret != 0:
print('Load model failed!')
exit(ret)
print('done')
# 配置平台
print('--> config model')
rknn.config(mean_values=[[128]], std_values=[[128]])
print('done')
# 建立模型
print('--> Building model')
ret = rknn.build(do_quantization=False)
if ret != 0:
print('Build model failed!')
exit(ret)
print('done')
# 保存RKNN模型
print('--> Export rknn model')
ret = rknn.export_rknn("best_model.rknn")
if ret != 0:
print('Export rknn model failed!')
exit(ret)
print('done')
运行代码,成功完成oxxn模型到rknn模型的转换。(红色的警告不必理会)
-
回复了主题帖:
#AI挑战营第二站# 基于RV1106芯片的RKNN环境部署及模型转换过程
xianhangCheng 发表于 2024-5-8 11:25
rknn.config加上target_platform='rv1106'参数,就会报错,PermissionError: [WinError 32] 另一个程序正在 ...
发现有一个这个错误,
AssertionError: target_platform set as ['RV1106'] is not support
-
回复了主题帖:
#AI挑战营第二站# Win10 ONNX转RKNN踩坑
求,rknn.config这种问题怎么解决呢,PermissionError: [WinError 32] 另一个程序正在使用此文件,进程无法访问。: 'C:\\Users\\24423\\AppData\\Local\\Temp\\tmpx00mgkkk\\log_feedback_to_the_rknn_toolkit_dev_team.log'
-
回复了主题帖:
#AI挑战营第二站# Windows下ONNX转RKNN模型过程
求,rknn.config这种问题怎么解决呢,PermissionError: [WinError 32] 另一个程序正在使用此文件,进程无法访问。: 'C:\\Users\\24423\\AppData\\Local\\Temp\\tmpx00mgkkk\\log_feedback_to_the_rknn_toolkit_dev_team.log'
- 2024-04-16
-
回复了主题帖:
免费申请:幸狐 RV1106 Linux 开发板(带摄像头),助力AI挑战营应用落地
本帖最后由 xianhangCheng 于 2024-4-16 13:58 编辑
#AI挑战营第一站# pytorch实现基于卷积神经网络的MNIST手写数.... - 嵌入式系统 - 电子工程世界-论坛 (eeworld.com.cn)
预期应用:部署智能家居应用的人脸识别解锁功能
- 2024-04-12
-
回复了主题帖:
【AI挑战营第一站】模型训练:在PC上完成手写数字模型训练,免费申请RV1106开发板
本帖最后由 xianhangCheng 于 2024-4-13 01:14 编辑
1.用自己的语言描述,模型训练的本质是什么,训练最终结果是什么
模型训练的本质是通过给定的数据集,利用机器学习算法来调整模型的参数,使其能够从数据中学习规律和模式。在训练过程中,模型会根据输入的数据进行计算,然后根据计算结果和实际标签之间的差异(损失),通过反向传播算法来调整参数,以最小化损失函数。这个过程不断迭代进行,直到模型收敛到一个满足要求的状态。训练的最终结果是一个经过优化的模型,可以用来对新的未见过的数据进行预测或分类。
2.PyTorch是什么?目前都支持哪些系统和计算平台?
PyTorch是一个开源的机器学习框架,提供了丰富的工具和库,用于构建深度学习模型、进行模型训练和部署。
PyTorch目前支持的系统和计算平台包括:
系统:Linux、Windows、macOS
计算平台:CPU、GPU(NVIDIA CUDA)、TPU(Google的Tensor Processing Unit)
3.动手实践链接: #AI挑战营第一站# pytorch实现基于卷积神经网络的MNIST手写数.... - 嵌入式系统 - 电子工程世界-论坛 (eeworld.com.cn)
-
发表了主题帖:
#AI挑战营第一站# pytorch实现基于卷积神经网络的MNIST手写数....
本帖最后由 xianhangCheng 于 2024-4-13 01:00 编辑
1.安装包
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
2.加载数据以及定义一些超参数(训练轮数/种类/学习率)
利用torchvision.datasets.MNIST函数下载数据集,其中root表示保存目录,train=True表示使用训练集,反之表示测试集。download表示如果本地没有MNIST就从服务器下载,transform是对图片的预处理工作(如尺寸resize、rgb转灰度图等)
# 加载MNIST数据集
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
train_dataset = torchvision.datasets.MNIST(root='./data', train=True, transform=transform, download=True)
test_dataset = torchvision.datasets.MNIST(root='./data', train=False, transform=transform, download=True)
#shuffle 参数设置为 True,在装载的过程会将数据随机打乱顺序并进行打包
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
#定义超参数
num_classes = 10 # 0-9数字分类
num_epochs = 20
learning_rate = 0.001
3.定义模型
我们定义了一个卷积神经网络,包含两个卷积层,2个全连接层,num_classes=10,代表数字0~9共10个总类。
# 定义模型
class model(nn.Module):
def __init__(self):
super(model, self).__init__()
self.layer1 = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=5, stride=1, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2))
self.layer2 = nn.Sequential(
nn.Conv2d(32, 64, kernel_size=5, stride=1, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2))
self.drop_out = nn.Dropout()
self.fc1 = nn.Linear(7 * 7 * 64, 1000)
self.fc2 = nn.Linear(1000, num_classes)
def forward(self, x):
out = self.layer1(x)
out = self.layer2(out)
out = out.reshape(out.size(0), -1)
out = self.drop_out(out)
out = self.fc1(out)
out = self.fc2(out)
return out
4.初始化模型,定义损失函数/优化器
# 初始化模型、损失函数和优化器
model = model()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
5.训练网络
指定训练次数epoch,对每一轮训练,都会进行:前向传播计算损失、反向传播梯度下降更新权重。在每一轮训练好之后,再计算 测试集的准确率,当指准确最大时保存该轮的模型权重。losses、accuracies、best_accuracy分别表示记录每一轮训练的loss值和准确率以及最高的准确率。
# 训练模型
# 在每一轮训练中记录损失和准确率
losses = []
accuracies = []
best_accuracy = 0.0
for epoch in range(num_epochs): # 假设训练20个epoch
model.train()
for images, labels in train_loader:
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 测试模型
model.eval()
total = 0
correct = 0
with torch.no_grad():
for images, labels in test_loader:
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
accuracy = correct / total
print(f'Epoch {epoch+1}, Loss: {loss.item():.4f}, Accuracy: {100 * accuracy:.2f}%')
losses.append(loss)
accuracies.append(accuracy)
# 保存准确率最高的模型权重
if accuracy > best_accuracy:
torch.save(model.state_dict(), 'best_model.pth')
best_accuracy = accuracy
6.结果及其可视化
准确率最高是第18轮训练,99.34%(红色框)
测试集准确率可视化:
训练集Loss可视化:
数据可视化代码如下:
# 绘制损失图
plt.figure()
plt.plot(range(1, num_epochs+1), losses, label='Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss')
plt.legend()
plt.savefig('loss_plot.png')
plt.show()
# 绘制准确率图
plt.figure()
plt.plot(range(1, num_epochs+1), accuracies, label='Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Training Accuracy')
plt.legend()
plt.savefig('accuracy_plot.png')
plt.show()
7.pth转换xoon
# 定义输入数据大小
dummy_input = torch.randn(1, 1, 28, 28)
# 导出为ONNX格式及其路径
onnx_path = "best_model.onnx"
# 导出模型为ONNX格式
torch.onnx.export(model, dummy_input, onnx_path,
input_names=['input'], output_names=['output'], # 输入和输出节点名称
do_constant_folding=True, # 是否执行常量折叠优化
export_params=True, # 是否包含权重
verbose=False, # 是否打印转换细节
dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}}) # 设置动态轴,允许在推理时调整batch_size