- 2024-10-15
-
加入了学习《【Follow me第二季第1期】任务提交-使用makecode开发(JavaScript)》,观看 【Follow me第二季第1期】任务提交-使用makecode开发(JavaScript)
- 2024-08-12
-
加入了学习《【Follow me第二季第1期】使用Makecode图形化完成任务》,观看 【Follow me第二季第1期】使用Makecode图形化完成任务
-
加入了学习《FollowMe 第二季: 1 Adafruit Circuit Playground Express及任务讲解》,观看 Adafruit Circuit Playground Express 及任务讲解
- 2024-08-06
-
回复了主题帖:
【Sipeed MAix BiT AIoT 开发套件】4,自动瞄准跟拍的系统
厉害
- 2024-08-02
-
加入了学习《【Follow me第二季第1期】全部任务演示》,观看 全部任务演示2.0
- 2024-08-01
-
回复了主题帖:
解锁功能强大且灵活的【Arduino UNO R4 WiFi】,快来报名得捷Follow me第二季第1期!
板子有点意思
- 2024-07-30
-
回复了主题帖:
【Follow me第二季第1期】创意任务二:害怕声音的纸章鱼~
非常巧妙
- 2024-07-27
-
上传了资料:
【Follow me第二季第1期】任务提交
-
发表了主题帖:
【Follow me第二季第1期】任务提交-使用makecode开发(JavaScript)
本帖最后由 knv 于 2024-7-27 12:55 编辑
【Follow me第二季第1期】作品提交
大家好,我是knv,很高兴参加这次follow me活动,这次的开发板很有意思,自带10个RGB灯,以及充足的传感器。这次活动我除了必做任务,选做了 创意任务二:章鱼哥——章鱼哥的触角根据环境声音的大小,章鱼哥的触角可舒展或者收缩。搭配器件: Adafruit Circuit Playground Express、舵机。
使用到的器件内容:
设计思路:
主控为Circuit Playground Express
其中选做任务使用了舵机。
主要使用Circuit Playground Express的传感器进行数据采集,然后通过灯光显示。
入门任务(必做):开发环境搭建,板载LED点亮
1:搭建开发环境,由于到手的版本是最新的express版本,所以我选择使用Code.org CSD来制作应用程序。
浏览网站发现还有版本区别,我们收到的应该都是express版本,资源充足,玩micropython都可以。
设备上电后的效果,默认就是七彩灯光,非常好看。
简单介绍一下刷入固件的方法:
首先PC通过USB线链接开发板,等待开机成功后,轻按RESET键并松开, 进入刷机模式,当所有RGB灯变绿后,电脑会弹出一个磁盘,
将固件拖入磁盘即可完成固件升级。
跑第一个工程,点亮所有RGB。只需要拖动一个show ring 手动选择灯光效果,就可以点亮灯光了。
编写代码点亮RGB灯
基础任务一(必做):控制板载炫彩LED,跑马灯点亮和颜色变换
流程图:
let r = 0
input.onGesture(Gesture.Shake, () => {
r = Math.randomRange(0, 4)
if (r == 0) {
light.showAnimation(light.runningLightsAnimation, 2000)
} else if (r == 1) {
light.showAnimation(light.rainbowAnimation, 2000)
} else if (r == 2) {
light.showAnimation(light.cometAnimation, 2000)
} else {
light.showAnimation(light.theaterChaseAnimation, 2000)
}
})
代码内容:
摇一摇触发亮灯,每次随机生成一个数字,然后触发不同的灯效2秒。
基础任务二(必做):监测环境温度和光线,通过板载LED展示舒适程度
任务预期,根据温度和光线亮度来生成舒适指数,使用光线传感器获取环境亮度,使用温度传感器获取温度。当温度舒适时 灯光 绿色,温度过高 灯光红色 温度过低 灯光蓝色,亮度过高 黄色灯光闪烁
代码如下:
let currentBrightness = 0
// 持续更新
forever(function () {
// 获取光线和温度
let lightLevel = input.lightLevel()
let temperature = input.temperature(TemperatureUnit.Celsius)
// 根据光线条件决定是否显示黄色
let isYellow = lightLevel > 200
let primaryColor = isYellow ? 0xffff00 : getColorBasedOnTemperature(temperature)
let secondaryColor = isYellow ? getColorBasedOnTemperature(temperature) : 0xffff00
// 执行呼吸灯效果
if (isYellow) {
showBreathingColor(primaryColor, secondaryColor)
} else {
showStaticColor(primaryColor)
}
})
// 根据温度条件返回对应的颜色
function getColorBasedOnTemperature(temperature: number): number {
if (temperature > 25) {
return 0xff0000 // 红色
} else if (temperature <= 25 && temperature > 16) {
return 0x00ff00 // 绿色
} else {
return 0x0000ff // 蓝色
}
}
// 使用呼吸灯效果显示交替的颜色
function showBreathingColor(primaryColor: number, secondaryColor: number) {
let steps = 10
let stepSize = 255 / steps
let pauseTime = 20
// 从 primaryColor 渐变到 secondaryColor
for (let step = 0; step <= steps; step++) {
let brightness = Math.round(step * stepSize)
let color = interpolateColor(primaryColor, secondaryColor, step / steps)
light.setAll(color)
light.setBrightness(brightness)
pause(pauseTime)
}
// 从 secondaryColor 渐变回 primaryColor
for (let step = steps; step >= 0; step--) {
let brightness = Math.round(step * stepSize)
let color = interpolateColor(primaryColor, secondaryColor, step / steps)
light.setAll(color)
light.setBrightness(brightness)
pause(pauseTime)
}
}
// 使用静态颜色显示LED
function showStaticColor(color: number) {
light.setAll(color)
light.setBrightness(255) // 设置最大亮度
}
// 插值函数:计算两种颜色之间的渐变颜色
function interpolateColor(color1: number, color2: number, ratio: number): number {
let r1 = (color1 >> 16) & 0xff
let g1 = (color1 >> 8) & 0xff
let b1 = color1 & 0xff
let r2 = (color2 >> 16) & 0xff
let g2 = (color2 >> 8) & 0xff
let b2 = color2 & 0xff
let r = Math.round(r1 + (r2 - r1) * ratio)
let g = Math.round(g1 + (g2 - g1) * ratio)
let b = Math.round(b1 + (b2 - b1) * ratio)
return (r << 16) | (g << 8) | b
}
效果图:
当光线大于200时:
当温度大于16,并小于25时(在冰箱测试的,感觉温度传感器并不是很灵敏)
基础任务三(必做):接近检测——设定安全距离并通过板载LED展示,检测到入侵时,发起声音报警
任务目标,当光线检测传感器检测到较黑(亮度传感器<50)后,触发入侵报警。
流程图:
代码如下:
// 持续更新
forever(function () {
// 获取光线值
let lightLevel = input.lightLevel()
if (lightLevel < 50) {
// 当光线小于50时,闪烁红灯并发出声音报警
flashRedLight()
soundAlarm()
} else {
// 光线大于或等于50时,关闭所有LED灯
light.clear()
}
})
// 使用闪烁的红色灯光显示LED
function flashRedLight() {
let flashCount = 5 // 闪烁次数
let flashDuration = 200 // 每次闪烁的时间,单位:毫秒
for (let i = 0; i < flashCount; i++) {
light.setAll(0xff0000) // 红色
pause(flashDuration)
light.clear()
pause(flashDuration)
}
}
// 发起声音报警
function soundAlarm() {
// 通过扬声器发出声音
music.baDing.play()
pause(200) // 每200毫秒发出一次声音
}
遮挡时:
无遮挡时:
进阶任务(必做):制作不倒翁——展示不倒翁运动过程中的不同灯光效果
任务目标,将设备贴到不倒翁上,当X Y加速度增加,则计算灯光速度,加速度越快,灯光速度越快。
// 持续更新
forever(function () {
// 获取 X 和 Y 轴的加速度值
let accelX = input.acceleration(Dimension.X)
let accelY = input.acceleration(Dimension.Y)
// 计算平面上的综合加速度
let strength2D = Math.sqrt(Math.pow(accelX, 2) + Math.pow(accelY, 2))
// 计算跑马灯的速度
let normalizedStrength = Math.min(strength2D / 1023, 1) // 标准化到 [0, 1] 范围
let speed = 100 - (90 * normalizedStrength) // 速度范围从 10 毫秒到 100 毫秒
// 执行七彩跑马灯效果
runColorMarquee(speed)
})
// 运行七彩跑马灯效果
function runColorMarquee(speed: number) {
const colors = [0xff0000, 0xffa500, 0xffff00, 0x008000, 0x0000ff, 0x4b0082, 0xee82ee] // 红、橙、黄、绿、蓝、靛、紫
const numLEDs = 10
// 清除所有 LED
light.clear()
// 循环显示 LED
for (let i = 0; i < numLEDs; i++) {
// 先关闭所有 LED
light.clear()
// 点亮当前 LED 和下一个 LED
light.setPixelColor(i, colors[i % colors.length])
light.setPixelColor((i + 1) % numLEDs, colors[(i + 1) % colors.length])
pause(speed) // 等待时间,根据速度调整
}
}
当晃动时,跑马灯速度更快:
■ (选做)创意任务二:章鱼哥——章鱼哥的触角根据环境声音的大小,章鱼哥的触角可舒展或者收缩
搭配器件: Adafruit Circuit Playground Express、舵机
舵机链接:
橙色 链接--> A1
棕色 链接--> GND
红色 链接--> VOUT
任务目标,使用设备的麦克风,获取音量,音量越大,舵机转动越快。同时启动线程,读取音量,使开发板上的RGB灯光以跑马灯形式点亮。代码开发完成后,用A4纸剪出章鱼触角的样子,贴到舵机上,完成收缩或者舒展功能。
流程图:
代码如下:
let servo = servos.A1;
let lastLevel = 0;
// 设置音量的阈值
let threshold = 120; // 小于此音量时,停止伺服电机
// 伺服电机控制
forever(function () {
let level = input.soundLevel();
if (lastLevel != level) {
if (level < threshold) {
servo.run(0);
} else {
// 将音量映射到伺服电机速度
let speed = Math.map(level, threshold, 255, 0, 100);
// 确保映射后的值在有效范围内
speed = Math.max(0, Math.min(speed, 100));
// 随机决定正转或反转
let direction = Math.randomRange(0, 1) === 0 ? 1 : -1;
let finalSpeed = speed * direction;
console.log(finalSpeed);
servo.run(finalSpeed);
pause(100)
}
lastLevel = level;
}
});
// 跑马灯效果
forever(function () {
let level = input.soundLevel();
if (level >= threshold) {
// 计算跑马灯的速度
let lightSpeed = Math.map(level, threshold, 255, 500, 50); // 数字越大速度越慢
runLights(lightSpeed);
} else {
clearLights();
}
});
function runLights(delay: number) {
const colors = [
0xff0000, // Red
0xffa500, // Orange
0xffff00, // Yellow
0x008000, // Green
0x0000ff, // Blue
0x4b0082, // Indigo
0xee82ee // Violet
];
for (let i = 0; i < 10; i++) {
let level = input.soundLevel();
if (level < threshold) {
clearLights();
return;
}
for (let j = 0; j < 10; j++) {
light.setPixelColor((i + j) % 10, colors[j % colors.length]); // 设置当前灯光为多彩颜色
}
pause(delay); // 根据音量调整速度
}
}
function clearLights() {
light.clear(); // 清除所有灯光
}
运行图片:
任务汇总视频:
链接:【Follow me第二季第1期】任务提交-使用makecode开发(JavaScript)-【Follow me第二季第1期】任务提交-使用makecode开发(JavaScript)-EEWORLD大学堂
源码下载:
download.eeworld.com.cn/detail/knv/633848
心得体会:
本次Follow me 第二季第1期任务活动的目标是利用 Adafruit Circuit Playground Express 开发一些灯光效果的功能,该项目不仅增强了我对传感器数据的处理能力,还培养了我们在硬件编程中的综合应用能力。学习了如何使用 Circuit Playground Express 的 LED 环实现多彩的视觉效果,并结合传感器数据动态控制这些效果。感谢得捷电子,电子工程世界提供的活动。
-
加入了学习《【得捷电子Follow me第2期】从显示中文到电子天气日历》,观看 【得捷电子Follow me第2期】从显示中文到电子天气日历
- 2024-07-08
-
回复了主题帖:
颁奖:嵌入式工程师AI挑战营(初阶),致敬敢于将边缘AI收入统治领域的你们
本帖最后由 knv 于 2024-7-8 17:47 编辑
个人信息已确认,请安排邮寄。感谢luckfox和eeworld的活动
- 2024-05-24
-
回复了主题帖:
【AI挑战营终点站】应用落地:部署手写数字识别应用到幸狐RV1106开发板
本帖最后由 knv 于 2024-5-24 17:55 编辑
完成打卡:) 大家有问题可以群里讨论或者帖子留言
https://bbs.eeworld.com.cn/thread-1282455-1-1.html
-
回复了主题帖:
#AI挑战营终点站# rv1106数字识别模型部署与性能优化
大佬牛皮
- 2024-05-23
-
回复了主题帖:
#AI挑战营终点站#将模型部署到RV1106中通过摄像头采集进行手写数字识别
aramy 发表于 2024-5-22 16:40
大佬:求助!按您这个流程跑下来。提示:E RKNN: failed to decode config data!
求指导!谢 ...
将编译后生成的 luckfox_rtsp_opencv_demo 目录下面的 lib也复制到开发板运行目录中,再尝试运行
- 2024-05-21
-
发表了主题帖:
#AI挑战营终点站#将模型部署到RV1106中通过摄像头采集进行手写数字识别
本帖最后由 knv 于 2024-5-24 10:16 编辑
感谢eeworld提供的开发板,,东西还挺精致的。检查一下直接插上USB。
开发板都自带的 SPI NAND FLASH,并在出厂时搭载了测试系统。然而,用户在收到板子后需要自行烧录网盘系统,以确保正常使用 PWM、I2C、UART 等功能。
1.下载和解压烧录工具(戳我下载)。
https://files.luckfox.com/wiki/Luckfox-Pico/Software/SocToolKit.zip
安装驱动
https://files.luckfox.com/wiki/Luckfox-Pico/Software/DriverAssitant_v5.12.zip
2.选择rv1106
3.按住boot键链接usb,设备管理器增加下面的设备
4:开始刷机
建议使用buildroot系统
因为https://forums.luckfox.com/viewtopic.php?t=22
刷机完毕后,重新链接开发板,电脑会多一个rndis网口,配置电脑 rndis网口的ip地址 为172.32.0.100 子网掩码 255.255.0.0
此时可以通过ssh 或者telnet 访问 172.32.0.93
登录账号:root
登录密码:luckfox
静态IP地址:172.32.0.93
测试摄像头
LuckFox Pico Pro/Max 开发板连接摄像头时,请确保摄像头排线的金属面朝向开发板芯片。
查看摄像头,若成功识别摄像头会生成 rkipc.ini 文件。
```
ls /userdata/
ethaddr.txt lost+found image.bmp video0 video2 rkipc . ini video1
```
此时可以通过vlc 拉流看到摄像头内容
默认的 IP 地址:rtsp://172.32.0.93/live/0
接下来配置开发环境
参考
https://wiki.luckfox.com/zh/Luckfox-Pico/Luckfox-Pico-SDK
此处不再赘述
我这种小白肯定是不会使用太复杂的代码,所以直接照抄历程
在ubuntu中新建一个文件夹
运行
```
git clone https://github.com/luckfox-eng29/luckfox_pico_rtsp_opencv
export LUCKFOX_SDK_PATH=
mkdir build
cd build
cmake ..
make && make install
```
将编译生成的luckfox_rtsp_opencv_demo/luckfox_rtsp_opencv 和 lib目录都上传到 luckfox-pico 上,进入文件夹运行
运行目录下应有 luckfox_rtsp_opencv lib model.rknn 三个文件
```
RkLunch-stop.sh
./luckfox_rtsp_opencv
```
使用 vlc 打开网络串流 rtsp://172.32.0.93/live/0(按实际情况修改IP地址拉取图像) 注意:运行前请关闭系统默认 rkipc 程序,执行 RkLunch-stop.sh 关闭 在 luckfox-pico / mini / plus 上尝试降低推流分辩率保存程序运行。
如何降低分辨率
设置main方法的width height 即可
配置rknpu库引用
在根目录的 CMakeLists.txt
加入这行
首先我的思路就是将摄像头采集到的帧,直接进行模型分析。
直接开始编写代码 ,因为 rv1106不支持get,set, 所以需要使用 rknn_query及其后续函数进行模型推理。
~~此处大坑~~
参考项目:https://github.com/MontaukLaw/rv1106_night_vision
```
int run_inference(cv::Mat &frame)
{
int ret = 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.index = i;
// query info
ret = rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &(input_attrs), sizeof(rknn_tensor_attr));
if (ret < 0)
{
printf("rknn_init error! ret=%d\n", ret);
return -1;
}
dump_tensor_attr(&input_attrs);
}
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.index = i;
// query info
ret = rknn_query(ctx, RKNN_QUERY_NATIVE_OUTPUT_ATTR, &(output_attrs), sizeof(rknn_tensor_attr));
if (ret != RKNN_SUCC)
{
printf("rknn_query fail! ret=%d\n", ret);
return -1;
}
dump_tensor_attr(&output_attrs);
}
cv::Mat gray;
cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
cv::resize(gray, gray, cv::Size(28, 28), 0, 0, cv::INTER_LINEAR);
// printf("Gray image size: %dx%d\n", gray.rows, gray.cols);
// printf("Gray image type: %d\n", gray.type());
// 为resize后的图形申请内存
int mem_size = MODEL_WIDTH * MODEL_HEIGHT * CHANNEL_NUM;
unsigned char *resize_buf = (unsigned char *)malloc(mem_size);
memset(resize_buf, 0, mem_size);
// Create input tensor memory
rknn_tensor_mem *input_mems[1];
// default input type is int8 (normalize and quantize need compute in outside)
// if set uint8, will fuse normalize and quantize to npu
input_attrs[0].type = input_type;
// default fmt is NHWC, npu only support NHWC in zero copy mode
input_attrs[0].fmt = input_layout;
input_mems[0] = rknn_create_mem(ctx, input_attrs[0].size_with_stride);
// Copy input data to input tensor memory
int width = input_attrs[0].dims[2];
int stride = input_attrs[0].w_stride;
if (width == stride)
{
memcpy(input_mems[0]->virt_addr, gray.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];
// copy from src to dst with stride
uint8_t *src_ptr = gray.data;
uint8_t *dst_ptr = (uint8_t *)input_mems[0]->virt_addr;
// width-channel elements
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;
}
}
// Create output tensor memory
rknn_tensor_mem *output_mems[io_num.n_output];
for (uint32_t i = 0; i < io_num.n_output; ++i)
{
output_mems = rknn_create_mem(ctx, output_attrs.size_with_stride);
}
// Set input tensor memory
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;
}
// Set output tensor memory
for (uint32_t i = 0; i < io_num.n_output; ++i)
{
// set output memory and attribute
ret = rknn_set_io_mem(ctx, output_mems, &output_attrs);
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;
}
printf("output origin tensors:\n");
rknn_tensor_attr orig_output_attrs[io_num.n_output];
memset(orig_output_attrs, 0, io_num.n_output * sizeof(rknn_tensor_attr));
for (uint32_t i = 0; i < io_num.n_output; i++)
{
orig_output_attrs.index = i;
// query info
ret = rknn_query(ctx, RKNN_QUERY_OUTPUT_ATTR, &(orig_output_attrs), sizeof(rknn_tensor_attr));
if (ret != RKNN_SUCC)
{
printf("rknn_query fail! ret=%d\n", ret);
return -1;
}
dump_tensor_attr(&orig_output_attrs);
}
// 创建存储模型输出的NCHW格式数据的向量
std::vector output_mems_nchw;
for (uint32_t i = 0; i < io_num.n_output; ++i)
{
int size = orig_output_attrs.size_with_stride;
int8_t *output_mem = new int8_t[size];
output_mems_nchw.push_back(output_mem);
}
// 进行NC1HWC2_int8_to_NCHW_int8转换
// for (uint32_t i = 0; i < io_num.n_output; i++) {
// int channel = orig_output_attrs.dims[1];
// int h = orig_output_attrs.n_dims > 2 ? orig_output_attrs.dims[2] : 1;
// int w = orig_output_attrs.n_dims > 3 ? orig_output_attrs.dims[3] : 1;
// int hw = h * w;
// NC1HWC2_int8_to_NCHW_int8((int8_t*)output_mems->virt_addr, (int8_t*)output_mems_nchw,
// (int*)output_attrs.dims, channel, h, w);
// }
// 获取预测的数字
int predicted_digit = get_predicted_digit(output_mems_nchw);
// 释放内存
for (uint32_t i = 0; i < io_num.n_output; ++i)
{
delete[] output_mems_nchw;
}
rknn_destroy_mem(ctx, input_mems[0]);
for (uint32_t i = 0; i < io_num.n_output; ++i)
{
rknn_destroy_mem(ctx, output_mems);
}
return predicted_digit;
}
```
将编译生成的luckfox_rtsp_opencv_demo/luckfox_rtsp_opencv 和 lib目录都上传到 luckfox-pico 上,进入文件夹运行
运行目录下应有 luckfox_rtsp_opencv lib model.rknn 三个文件
```
# chmod 777 luckfox rtsp opencv
# ./luckfox rtsp opencv model.rknn
```
运行效果
# 调优:
此时我们已经可以通过摄像头进行图片获取,并且识别了。
但是识别准确率极低,因为有太多的干扰项目了。我们调整一下代码,增加一个裁剪视频帧的功能,
下面是两个裁剪函数。
```
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, 50, 150);
std::vector 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 &a, const std::vector &b)
{
return cv::contourArea(a) < cv::contourArea(b);
});
return cv::boundingRect(*largest_contour);
}
cv::Mat preprocess_digit_region(const cv::Mat ®ion)
{
cv::Mat gray, resized, normalized;
cv::cvtColor(region, gray, cv::COLOR_BGR2GRAY);
cv::resize(gray, resized, cv::Size(28, 28), 0, 0, cv::INTER_LINEAR);
resized.convertTo(normalized, CV_32F, 1.0 / 255.0);
return normalized;
}
```
接下来调整调用逻辑 ,有文本裁剪数据时才使用模型推理。
```
cv::Mat frame(height, width, CV_8UC3, data);
// 复制一个帧,然后使用模型推理,获取结果,传递到resdata中。
cv::Rect digit_rect = find_digit_contour(frame);
if (digit_rect.area() > 0)
{
cv::Mat digit_region = frame(digit_rect);
cv::Mat preprocessed = preprocess_digit_region(digit_region);
int prediction = run_inference(preprocessed);
// 在图像上显示预测结果
cv::rectangle(frame, digit_rect, cv::Scalar(0, 255, 0), 2);
cv::putText(frame, std::to_string(prediction), cv::Point(digit_rect.x, digit_rect.y - 10),
cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 0, 255), 2);
}
```
[localvideo]87f05bc08ca9bbb4de2aa68d31b91b64[/localvideo]
# 最后结果:
识别率还是很差,识别不是很稳定,该模型也没什么区别,等待大佬来带了。
## 二次调整及优化
感谢luyism [luyism](https://gitee.com/luyism/luckfox_rtsp_mnist "luyism") 大佬做出的开源优化
通过阅读代码,了解到了之前的代码中的不足,
1:预处理问题
2:后处理问题
通过修正这部分代码,来重新编译程序,完成手写数字的识别。
增加预处理部分
```
// 在图像中找到数字的轮廓,同时减小找到轮廓时的抖动
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 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& a, const std::vector& 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(bounding_box.width) / bounding_box.height;
if (aspect_ratio < 0.2 || aspect_ratio > 3) {
return cv::Rect();
}
// **轮廓稳定性检测**:
// 通过比较当前帧和之前几帧的轮廓位置来判断轮廓的稳定性。
// 如果多帧之间的轮廓位置变化较小,则可以认为轮廓比较稳定,不需要进行过多的调整。
static std::vector 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;
}
// 预处理数字区域
cv::Mat preprocess_digit_region(const cv::Mat ®ion)
{
// 将图像转换为灰度图像,然后调整大小为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(i, j) = 255 - bitwized.at(i, j);
}
}
// 将图片大小调整为28x28,图片形状不发生畸变,过短的部分使用黑色填充
cv::resize(bitwized, resized, cv::Size(28, 28), 0, 0, cv::INTER_AREA);
return resized;
}
```
增加将量化的INT8数据转换为浮点数
```
// 将量化的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) { 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 = deqnt_affine_to_f32(output,zp,scale);
// 计算输出数据的L2范数
float sum = 0;
for(int i = 0; i < 10; i++)
sum += out_fp32 * out_fp32;
// 对归一化后的浮点数输出进行归一化处理,确保输出数据的范围在[0,1]之间
float norm = sqrt(sum);
for(int i = 0; i < 10; i++)
out_fp32 /= norm;
// 打印输出数据的值
printf("\n===================Output data values:===================\n");
for (int i = 0; i < 10; ++i)
{
printf("%f ", out_fp32);
}
printf("\n");
// 找出最大概率对应的数字,并记录最大概率及其对应的数字
float max_prob = -1.0;
int predicted_digit = -1;
// 计算最大值的索引
for (int i = 0; i < 10; ++i)
{
if (out_fp32 > max_prob)
{
max_prob = out_fp32;
predicted_digit = i;
}
}
// 将预测的数字及其对应的概率记录到队列中
predictions_queue.push_back({predicted_digit, max_prob});
// 打印预测的数字与其对应的概率
printf("========Predicted digit: %d, Probability: %.2f========\n\n", predicted_digit, max_prob);
}
```
# 最终演示:
视频演示:
可以看到这个版本比上个版本稳定很多。
[localvideo]668efdd254b7a0c7f8735129b1c0a64c[/localvideo]
识别率测试:
将识别率获取增加到代码中,下面是测试结果,可以看到识别率大约60%左右。
# 代码提交
代码放到了github仓库中,有兴趣的朋友看看
https://github.com/knva/luckfox_pico_rtsp_opencv
单独识别图片的
https://github.com/knva/luckfox_pico_rknn_test
- 2024-05-08
-
回复了主题帖:
#AI挑战营第一站# PC基于PyTorch的MNIST模型训练过程与模型转换
本帖最后由 knv 于 2024-5-8 18:10 编辑
补充准确率: 验证准确率 99.16%
新版本代码:
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
class OptimizedConvNet(nn.Module):
def __init__(self,num_classes=10):
super(OptimizedConvNet, 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
# 定义超参数
learning_rate = 0.001
batch_size = 64
num_epochs = 10
# 检查是否有可用的GPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 创建模型,并将模型移动到GPU上
model = OptimizedConvNet(num_classes=10).to(device)
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# 加载MNIST数据集
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
# 加载验证集
valid_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
valid_loader = DataLoader(valid_dataset, batch_size=64, shuffle=False)
# 初始化最优验证损失为无穷大
best_valid_loss = float('inf')
import matplotlib.pyplot as plt
# 初始化准确率列表
train_accuracies = []
valid_accuracies = []
for epoch in range(num_epochs):
# 在训练集上训练
model.train()
total_train_loss = 0
correct_train_preds = 0
total_train_preds = 0
for i, (images, labels) in enumerate(train_loader):
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
_, predicted = torch.max(outputs.data, 1)
total_train_preds += labels.size(0)
correct_train_preds += (predicted == labels).sum().item()
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (i + 1) % 100 == 0:
print(f'Epoch [{epoch + 1}/{num_epochs}], Step [{i + 1}/{len(train_loader)}], Loss: {loss.item()}')
total_train_loss += loss.item()
average_train_loss = total_train_loss / len(train_loader)
train_accuracy = correct_train_preds / total_train_preds
train_accuracies.append(train_accuracy)
print(f'Epoch: {epoch + 1}, Training Loss: {average_train_loss:.4f}, Training Accuracy: {train_accuracy:.4f}')
# 在验证集上验证
model.eval()
valid_loss = 0.0
correct_valid_preds = 0
total_valid_preds = 0
with torch.no_grad():
for images, labels in valid_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
_, predicted = torch.max(outputs.data, 1)
total_valid_preds += labels.size(0)
correct_valid_preds += (predicted == labels).sum().item()
valid_loss += loss.item()
valid_loss /= len(valid_loader)
valid_accuracy = correct_valid_preds / total_valid_preds
valid_accuracies.append(valid_accuracy)
print(f'Epoch: {epoch+1}, Validation Loss: {valid_loss:.4f}, Validation Accuracy: {valid_accuracy:.4f}')
# 如果验证损失有所下降,则保存模型
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), 'best_model.pth')
print('Model saved.')
# 绘制训练和验证准确率
plt.plot(range(1, num_epochs + 1), train_accuracies, label='Train')
plt.plot(range(1, num_epochs + 1), valid_accuracies, label='Valid')
plt.title('Model Accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()
plt.show()
-
回复了主题帖:
入围名单公布:嵌入式工程师AI挑战营(初阶),获RV1106 Linux 板+摄像头的名单
本帖最后由 knv 于 2024-5-9 09:12 编辑
个人信息已确认,领取板卡,可继续完成&分享挑战营第二站和第三站任务。
训练结果已补充到原帖回复中。
-
回复了主题帖:
#AI挑战营第二站# 基于RV1106芯片的RKNN环境部署及模型转换过程
xianhangCheng 发表于 2024-5-8 11:31
发现有一个这个错误,
AssertionError: target_platform set as ['RV1106'] is not support
跟我的步骤走的肯定没问题,手动创建一下data.txt
内容是一张测试图片的路径
我的是28*28像素的png
比如:
6.png
- 2024-04-28
-
回复了主题帖:
(已颁奖)嵌入式工程师AI挑战营(初阶):基于RV1106,动手部署手写数字识别落地
和和123 发表于 2024-4-26 11:32
什么时候公布获取幸狐RV1106开发板名单、开发板派发?
五月十号
-
回复了主题帖:
【AI挑战营第二站】算法工程化部署打包成SDK
RKNN模型是RK公司设计的一套模型格式,用于在rknpu上加速运行
ONNX模型是通用的开放模型格式,可以用于直接使用或模型转换。在专用npu上可能无法加速运行
#AI挑战营第二站# 基于RV1106芯片的RKNN环境部署及模型转换过程 https://bbs.eeworld.com.cn/thread-1280038-1-1.html