硬核王同学

  • 2024-07-14
  • 发表了主题帖: 【超小型 Linux 开发套件Quantum Tiny Linux】- 扩展外设使用

    我们还是先跟着官方wiki学着试一下外设的使用:https://wiki.seeedstudio.com/cn/Quantum-Mini-Linux-Development-Kit/#%E4%BA%A7%E5%93%81%E7%89%B9%E6%80%A7   使用EMMC启动 需要使用EMMC启动夸克的镜像,首先需要将镜像拷贝到TF卡上。 通过SSH或远程桌面连接进入夸克的终端,输入如下命令: sudo dd if=/dev/mmcblk0 of=/dev/mmcblk1 bs=512 count="EMMC的Block数+1" & sudo watch -n 5 pkill -USR1 ^dd$ 其中EMMC的Block数可以使用fdisk -l查看。 sudo fdisk -l 查看mmcblk1的sectors数为30777344 pi@Quark-N:~$ sudo fdisk -l Disk /dev/mmcblk0: 29.8 GiB, 31927042048 bytes, 62357504 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0x41efd092 Device Boot Start End Sectors Size Id Type /dev/mmcblk0p1 49152 131071 81920 40M 83 Linux /dev/mmcblk0p2 131072 2588671 2457600 1.2G 83 Linux /dev/mmcblk0p3 2588672 19455999 16867328 8G 83 Linux Disk /dev/mmcblk1: 14.7 GiB, 15758000128 bytes, 30777344 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disk /dev/mmcblk1boot1: 4 MiB, 4194304 bytes, 8192 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disk /dev/mmcblk1boot0: 4 MiB, 4194304 bytes, 8192 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes sudo dd if=/dev/mmcblk0 of=/dev/mmcblk1 bs=512 count="30777345" & sudo watch -n 5 pkill -USR1 ^dd$   定制命令行欢迎信息 欢迎信息主要是这个目录下的脚本来打印的: /etc/update-motd.d/ 比如要修改 FriendlyELEC 的大字LOGO,可以修改/etc/update-motd.d/10-header 这个文件,比如要将LOGO改为Seeed,可将以下行: TERM=linux toilet -f standard -F metal $BOARD_VENDOR 改为: TERM=linux toilet -f standard -F metal Seeed 我改了串口上显示没啥效果   更换软件包源和pip源 wget http://112.124.9.243/aptsouce.sh chmod 755 aptsouce.sh sudo -H ./aptsouce.sh sudo apt-get update wget后要加这个地址http://112.124.9.243/aptsouce.sh,否则下载不了aptsouce.sh   蓝牙 系统镜像中具有内置的蓝牙驱动程序,您可以按照以下步骤启动蓝牙: $ bluetoothctl 进入 bluetoothctl 界面后。运行scan扫描列出附近所有的蓝牙设备: $ scan on 复制设备的MAC地址,然后使用以下命令连接到设备: $ pair A4:xx:xx:xx:xx:30 $ trust A4:xx:xx:xx:xx:30 $ connect A4:xx:xx:xx:xx:30 现在您的开发板已连接到蓝牙设备。输入quit回终端。在开发板上播放音乐,然后您将在蓝牙扬声器设备上听到音乐! 您也可以在远程桌面中通过状态栏的蓝牙图标进行图形化的连接配对。   我测试可以连接,但不能播放音乐,可能驱动有问题   麦克风 Atom-N(载板)上还有一个内置麦克风,使其具备机器学习功能!我们提供了简单的测试示例,可以轻松使用随镜像预先安装的 Audacity 软件。 使用远程桌面登录开发板。 打开Audacity软件。 开始录制并与麦克风交谈,您会看到声波! 注意 你可以使用 arecord -l命令来寻找麦克风. 如果想基于麦克风进行进一步开发,可以在 WorkSpace/WuKong 中找到并使用 WuKong Robot 和 snowboy. Audacity 没玩明白。   LCD屏幕 LCD屏幕使用SPI与CPU进行通信,并且其驱动程序内置于系统镜像中,因此在启动系统时应该能够看到系统消息日志:   如果将USB键盘和鼠标连接到开发板,则这将成为最小体积的电脑! 有一个使用python编写的pygame示例,演示了LCD使用。 进入文件: $ cd WorkSpace/PyGame 运行示例: $ sudo python hello_world.py 您也可以在LCD屏幕下输入s-tui命令,可以将屏幕用于显示系统的监控信息。   这个可以跑,点击绿色点退出。   OpenCV 系统镜像中还内置了OpenCV ,可以通过接入USB 摄像头实现人脸识别功能 您将需要一个USB相机。将USB相机连接到开发板的USB端口A。 进入项目文件夹: $ cd WorkSpace/OpenCV 运行demo: $ python FaceDetectOnTft.py 您可以在开发板的LCD屏幕上看到视频!! 测试过也不行!卡掉了。   GPIO 对于GPIO访问,因为是基于Linux所以十分方便,并且可以使用Python轻松控制。 鉴于上面很多外设都不能使用,所以我们直接转到quark-n实现外设的操控,GPIO是可以正常使用的,下一篇文章分享下如何操作 进入到项目文件夹: $ cd WorkSpace/GPIO 运行GPIO示例: $ sudo python gpio_key_led.py 改成按键按下灭蓝灯:   整体来说,官方给的wiki问题点还不少,实际玩耍还要看quark-n,下一篇文章就给大家分享下如何正确玩这块板子。

  • 2024-07-12
  • 发表了主题帖: 【超小型 Linux 开发套件Quantum Tiny Linux】-开发资源分享与登录系统

    一、开发板介绍 别看这块板子这么小,实际可玩性非常高!下面给大家介绍下这块板子: 硬件总览       规格参数 td {white-space:nowrap;border:1px solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;} 规格参数 详情 Quark-N SoM   CPU Allwinner H3, 四核Cortex-A7 @ 1GHz GPU ARM Mali400 MP2 GPU 内存 512MB LPDDR3 RAM 存储 16GB eMMC 接口 以太网, SPI, I2C, UART, 可复用的GPIO, MIC, LINEOUT GPIO 2.0mm间距26针式接头连接器,包括USB OTG,USB串口,I2C,UART,SPI,I2S,GPIO PCB 6层高密度沉金设计 工作温度 0-80°C 尺寸 31mmx22mm Atom-N Carrier Board   插槽 Quark-N的m.2接口 USB USB 2.0×2 USB Type-C×1 无线连接 RTL8723BU: Wi-Fi: IEEE 802.11 b/g/n @2.4GHz 蓝牙: BT V2.1/ BT V3.0/ BT V4.0 板载外设 1 x 麦克风 1 x MPU6050运动传感器(陀螺仪 + 加速度计) 4 x 按钮 (GPIO-KEY, Uboot, Recovery, Reset) 1 x TFT 显示屏 外部存储 Micro SD卡插槽 尺寸 40mm*35mm   产品特性 体积超小 (31mm x 22mm) 和高度集成的四核Cortex-A7 Linux SoM 带有丰富外围设备和接口的底板(40mmx35mm) : 麦克风,陀螺仪,加速度计,4个按钮和TFT显示屏等 集成了完整的ARM-Linux系统以进行高级开发 无线连接能力(Wi-Fi + 蓝牙) 具有m.2接口可以轻松设计您的底板 广泛的应用场景,例如个人服务器,智能语音助手和机器人开发等 USB Type-C功能:带USB-TTL可以直接用于串口登录终端   二、开发资源分享 在网上找了一些教程和资源,给大家分享下:   教程: https://wiki.seeedstudio.com/cn/Quantum-Mini-Linux-Development-Kit/#%E4%BB%8Eubuntu-1604-%E5%8D%87%E7%BA%A7%E5%88%B0ubuntu-1804-lts   评测: https://www.cnblogs.com/WATER-code/p/17508420.html   克隆代码地址: https://gitee.com/coolflyreg163/quark-n   三、环境搭建 启动开发板 首先把板子启动起来吧,电脑Type-C供电就可以,刚开始供电以为板子没有任何反应,还以为板子坏了(正常来说都会先烧一个版本测试板子功能的) 后面看教程才知道还要自己手动烧。 首先,下载镜像文件:https://files.seeedstudio.com/wiki/Quantum-Mini-Linux-Dev-Kit/quark-n-21-1-11.zip 下载完成后解压出来.img文件,再下载烧录工具:https://www.balena.io/etcher/ 下载完直接双击运行,把镜像文件烧录到sd卡中(至少16G,烧录完后sd卡只能用于启动,记得提前备份自己的文件!)     烧录完成,有时候电脑会提示是否格式化,这个点取消,直接拔出sd卡插到板子上。     给开发板供电,稍等一下开发板上的灯会亮起。并且可以看到一个可爱的小图标,这时我们的开发板就启动成功了! 黄灯常量代表上电。 白灯的为系统心跳LED,根据CPU的负载会有规律的闪烁。   串口登录系统 系统启动完成后,会看到电脑上识别到一个USB串口,可以用这个串口登录Quark-N进行终端交互,或者用相关命令连接WiFi后通过SSH登录。     本来想通过usb串口连接的,但是发现没有这个驱动,给大家分享一下如何安装CP21X的驱动。 首先,进入官网:https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers?tab=downloads     选择DOWNLOADS界面,windows10、11选择第一个,windows7、8选择第4个 下载完成后,解压压缩包会出现这个界面,右击.inf文件选择安装         安装成功后,设备管理器就可以看到COM口啦     用串口工具(我一般常用SecureCRT)登录开发板,记得波特率选为115200           系统默认用户和密码 普通用户: users: pi password: quark Root用户: users: root password: quark   系统配置 你可以使用这个npi-config命令来配置系统,如用户、系统语言、时区、ssh等等.     注意,系统已经默认配置好, 如果你不知道如何配置系统,请保持默认配置.   开启WiFi 首先我们要用串口登录系统,配置连接WiFi后通过SSH登录。 这里有一个快捷的脚本: cd /home/pi/WorkSpace/System/net/ sudo python connect_wifi.py SSID PASSWORD 其中SSID是WIFI名字,PASSWORD为密码   下面是正常使用,我们使用NetworkManager来管理网络,但请按照以下步骤连接到Wi-Fi: 切换到root用户: $ su root 开启Wi-Fi: $ nmcli r wifi on 扫描附近的Wi-Fi: $ nmcli dev wifi     连接到特定的Wi-Fi: $ nmcli dev wifi connect "SSID" password "PASSWORD" ifname wlan0 重启网卡设备 sudo ifconfig wlan0 down sudo ifconfig wlan0 up 连接成功后,下次开机,WiFi 也会自动连接。 如果你的USB WiFi无法正常工作, 大概率是因为文件系统里缺少了对应的USB WiFi固件。 可以通过下列命令安装所有的USB WiFi固件: apt-get install linux-firmware 有关NetworkManager的更多参考,请阅读here.   SSH登录系统 可以通过ifconfig查看网络 ifconfig     通过SSH工具连接,我习惯用PUTTY     输入用户名和登录密码就可以登录系统了       软件更新 (可选择) 第一次连接到网络后,最好先更新软件: $ sudo apt-get update   远程桌面连接 系统镜像中包含xrdp服务 并在正常情况下默认运行,可以使用Windows自带的远程桌面连接 软件进入Quark-N的桌面: 使用 ifconfig 获取开发板的ip地址 在Windows电脑上搜索远程桌面连接,打开软件 在同一局域网中,输入开发板的IP地址并登录,这样就可以进行远程桌面控制了! 不知道为什么,我的桌面壁纸和别人的不一样哈哈      

  • 2024-07-10
  • 发表了主题帖: 一起读《机器学习算法与实现 —— Python编程与应用实例》- 第1章 绪论

    本书介绍   本书的名称是《机器学习算法与实现——Python编程与应用实例》,作者布树辉是西北工业大学的教授,我浏览了他的gitee,里面都有很全的自学机器学习及编程的资料,大家可以去看看:https://gitee.com/pi-lab   本书总共有11章,可以分为3大块,第一部分是Python语言;第二部分是机器学习的基础算法及实现;第三部分为深度学习及目标检测。   从第一章绪论就可以看出来,本书实践性很强,可以边做实验边理解书中的概念,而且本书还配有视频教程和电子档的资料,b站就能免费观看!   最后,本书质量非常不错,彩版印刷,看起来相当舒服,真心建议大家买一本回去仔细学习!   第一章介绍   第一章介绍了一些机器学习的来历、基本的专业术语和机器学习的类型,还有机器学习在现实生活中的应用,以及实现机器学习应用的具体步骤,另外又讲了一下评估机器学习模型好坏的方法和学习机器学习的方法。   总的来说,第一章主要是向还不熟悉机器学习的同学介绍和认识机器学习,并且扩展讲了一下现实生活中对机器学习的应用步骤,以及告诉我们,新手该如何正确学习机器学习,和学习机器学习正确的方法。   下面我就摘取一些我认为很有价值的东西,分享给大家:   人类想要构建智能机器,让机器具备和人一样的学习能力,就需要让机器自己具备智能。而使机器自己具备智能这样的学习理论和方法就是重要的研究课题。我们正好可以让机器模仿人类的学习方式实现”机器学习“:输入、整合和输出。   实际上人类学习的定义就是:人类的学习是根据过往的知识或经验,对一类问题形成某种认识或总结出一定的规律,然后利用这些知识对新问题进行判断、处理的过程。   而真正的现实生活中,具有海量的信息,挖掘其所蕴含的知识,突破人类认知能力的瓶颈,是人类目前面临的主要挑战之一,所以“机器学习”就是解决这一理念的产物。   机器学习的定义如下:机器学习是一类算法的总称,这些算法能够从大量历史数据中挖掘出隐含的规律,并用于分类、回归和聚类。   机器学习更简单和形象的定义是:寻找从输入样本数据到期望输出结果之间的映射函数。注意,机器学习的目标是使学习得到的函数很好地适用于“新样本”,而不是仅适用于训练样本。   机器学习的基本思路如下:1.收集并整理数据;2.将显示问题抽象成数学模型;3.利用机器学习方法求解数学问题;4.评估求解得到的数学模型。 无论使用什么算法和数据,机器学习的应用都包含上述几个步骤。   一、机器学习的发展历程 从20世纪50年代至今,人工智能的发展经历了“推理期”“知识期”和“机器学习时期”   机器学习时期大致也分为三个阶段。20世纪80年代初,连接主义较为流行。20世纪90年代,统计学习方法占据主流舞台。进入21世纪,深度神经网络被提出,连接主义思想又受到大家关注。   具体机器学习的发展历程可以百度搜索,我们主要为了学习技术,对于历史背景在学习技术的过程中再详细了解。   二、机器学习的基本术语 本章及以下内容不必强求理解,只需记住这些名词和概念即可,后续章节中深入讲解设计的概念和内涵。   1.特征:特征是被观测目标的一个独立可观测的属性或特点,即某个东西可直接被观察到的属性。很多个特征可以定义出一个标签,这个标签是无法直接观察到或难以被观察到的属性,比如可以通过一个草莓的大小、颜色、成熟度等等的特征来判断出这个草莓味道是否甜的标签。   2.样本:样本是数据的特定实例,分为两类,有标签样本和无标签样本。无标签样本不包含标签,需要通过算法根据数据在特征空间中的分布关系自动找到样本之间的关系。样本的集合构成数据集,在不同的阶段对算法模型进行训练、测试和验证,数据集也正好被分为训练集、测试集、验证集。   3.模型:模型定义特征与标签之间的关系,一般可以是为一个由参数定义的函数或逻辑操作的集合。模型的生命周期包括两个阶段:1.训练阶段,创建或学习模型。2.推理阶段,将训练后的模型应用于无标签样本,对其做出有用的预测。   4.回归、分类与聚类:机器学习的基本应用是回归、分类和聚类。回归和分类都属于监督学习,其主要特点是根据输入的特征,分析并得到数值或类别。回归问题常用于预测一个连续值,如预测房价、未来气温等。分类问题的目的是将事物打上标签,结果通常为离散值。聚类问题属于无监督学习,用于在数据中寻找隐藏的模式或分组。   5.泛化与过拟合:通过数据学习得到模型的过程就是学习,也称训练。先根据训练数据集构建一个模型,然后将该模型用于此前未见过的新数据的过程,被称为模型的泛化。构建一个对现有信息量来说过于复杂的模型,如果该模型在拟合训练数据集时表现得非常好,但在测试数据集上表现非常差,就说明模型出现了过拟合。与之相反,模型如果过于简单,就可能无法掌握数据的全部内容及数据中的变化,机器学习模型甚至在训练集上表现都很差,不能拟合复杂的数据,这种成为欠拟合。只有在训练集和测试机的精度都较高的情况下,才认为模型对数据拟合的成都刚刚好,模型的泛化表现出色,这才是最期望的模型。   三、机器学习的基本分类 1.监督学习:监督学习是指在训练机器学习模型时,使用有标签的训练样本数据。首先建立数据样本特征和已知结果之间的练习来求解最优模型参数,然后同各国模型和求解的的模型参数对新数据进行结果预测。监督学习通常用于分类和回归问题。监督学习的难点是,获取具有目标值的标签样本数据的成本较高,而成本高的原因是这些训练集要一路来人工进行标注。   2.无监督学习:根据类别位置(未被标记)的训练样本解决模式识别中的各种问题,称为无监督学习。无监督学习与监督学习的区别是选取的样本数据无需标签信息,而只需分析这些数据的内在规律。此外无监督学习也适用于降维。无监督学习的优点是,数据不需要进行人工标注,且数据获取成本低。   3.半监督学习:半监督学习是监督学习和无监督学习相结合的一种学习方法,半监督学习方法可以结合分类、回归和聚类,只需少量有带标签的样本和和大量无标签的样本即可获得较好的应用效果。   4.深度学习:根据人工神经网络的层数或者机器学习方法的非线性处理深度,可以将机器学习算法分为浅层学习算法和深度学习算法。浅层学习算法的典层代表是感知机,深度神经网络主要是指处理层数较多的神经网络。   5.强化学习:强化学习又称再励学习、评价学习或增强学习,用于描述和解决智能体与环境交互的学习策略,以达成回报最大化或者实现特定目标的问题。   6.总结:机器学习与人工智能:人工智能是一种概念,主要目标是使机器能够胜任通常人类智能才能完成的复杂工作。机器学习是一种实现人工智能的方法,是一种实现机器学习的技术,也是当前最热门的机器学习方法。下图展示了机器学习和人工智能之间的关系。       四、机器学习的应用 以机器学习为代表的各种智能方法不仅应用于图像识别、语音识别、自然语言处理等传统领域,在其他方向也有其重要的作用,下面几种是比较常见且重要的应用: 1.图像识别与处理 2.语音识别与自然语言处理 3.环境感知与智能决策 4.融合物理信息的工程设计   五、机器学习应用的步骤 使用机器学习解决实际问题时,遵循的基本流程如下: 选择合适的模型。这通常需要视实际问题而定,即要针对不同的问题和任务选取恰当的模型,而模型就是一组函数的集合。 判断函数的好坏。这需要确定一个衡量标准,即通常所说的损失函数,损失函数的确定也需要视具体问题而定,如回归问题一般采用欧式距离,分类问题一般采用交叉熵代价函数。 找出“最好”的函数的模型参数。如何从高维参数空间最快地找出最优的那组参数是最大的难点。常用的方法有梯度下降法、最小二乘法等。 学习得到“最好”的函数的参数后,需要在新样本上进行测试,只有在新样本上表现很好时,才算是一个最优模型。   机器学习的流程本质上是数据准备、数据分析、数据处理和结果反馈的过程,如下列排序所示: 1.应用场景分析 2.数据处理 3.特征工程 4.算法模型训练与评估 5.应用服务   六、机器学习的评估方法 当模型训练完成时,需要对模型进行跟踪与评估。为了让得到的模型适用于新样本,即使模型获得一个最期望的模型。为了充分验证机器学习模型的性能和泛化能力,需要选择与问题匹配的评估方法,才能发现模型选择或训练过程中的问题,进而迭代地优化模型。下面是两种常见的评估方法: 1.数据集划分方法:数据集一般需要分割为训练集、测试集和验证集,划分方法如下:1.留出法2.交叉验证法3.自助法 2.性能度量:性能度量是衡量模型泛化能力的数值评价标准,反映了所求解模型的性能。   七、如何学习机器学习 主要有两个中心思想: 1.由浅入深:机器学习综合了诸多学科知识,应由浅入深,循序渐进的方法学习机器学习,并基于Python应用建立感性认识,在学和做的过程中不断强化理解机器学习的深度与广度。 2.形成于思:机器学习是一门实践性很强的学科,需要理论与实践相结合,不仅要理解还要实践,学习机器学习时应先快速建立感性认识去解决一些实际问题,并在解决问题的过程中加深理论、算法、编程、验证等方面的理解。   总结 最后,攀登机器学习这座“高山”,不必强求一步就学会,而在做与学的过程中不断强化感性认识,进而牵引理论学习。所以对于机器学习这些多而复杂的概念,可以先认识了解,在实验中学习和加深这些概念的印象即可。后面几章就是在实践中进行学习,希望大家可以和我一起跟着布树辉老师做实验,从而真正掌握机器学习这门学科!!!  

  • 2024-07-05
  • 加入了学习《C++零基础入门到精通》,观看 C++入门-01-代码、编译和程序

  • 加入了学习《C++零基础入门到精通》,观看 导学视频

  • 2024-06-20
  • 回复了主题帖: 共读入围:《机器学习算法与实现 —— Python编程与应用实例》

    个人信息确认无误,确认可以完成测评分享计划

  • 2024-06-17
  • 加入了学习《泰克 4 系列 B MSO》,观看 泰克4系列B MSO混合信号示波器简介

  • 2024-06-15
  • 加入了学习《用树莓派和Python开发机器人教程》,观看 控制电机:用软硬件让马达动起来

  • 2024-06-06
  • 回复了主题帖: 【AI挑战营终点站】应用落地:部署手写数字识别应用到幸狐RV1106开发板

    完成打卡 #AI挑战营终点站#基于RV1106通过摄像头采集实现手写数字识别部署 https://bbs.eeworld.com.cn/thread-1283321-1-1.html  

  • 2024-05-29
  • 发表了主题帖: #AI挑战营终点站#基于RV1106通过摄像头采集实现手写数字识别部署

    本帖最后由 硬核王同学 于 2024-6-6 10:41 编辑   之前搞这个部署识别没搞通,部署环境的时候总是报错,最后发现是硬盘空间不够了!而且我直接在vm里面扩容导致数据损坏,Ubuntu直接开不开机了,实在没时间写程序跑模型,这次先跟着论坛大佬走一遍!   环境部署很详细,新手可以直接跟着我做。   一、初识开发板   这是幸狐的 LuckFox Pico Max 开发板,基于 瑞芯微 RV1106 芯片 。 到手开发板就是这个样子,一块板子,一个SC3336摄像头,一个RTC电池,后面的杜邦线是我自己配的,用于串口收发。 注意摄像头座子要反接,将接口拔起来,排线蓝色的一面朝向网口,再按下去。 详细产品介绍就不多做解释了,可以去官网查看:https://wiki.luckfox.com/zh/Luckfox-Pico/Luckfox-Pico-quick-start   二、镜像烧录   LuckFox Pico Max自带的 SPI NAND FLASH,并在出厂时搭载了测试系统。但是我们也要学下镜像烧录,万一有什么问题,可以详细排查。   1)环境准备   安装驱动,下载RK驱动助手 DriverAssitant(去官网下载)。打开RK驱动助手 DriverAssitant 安装 USB 驱动程序,此过程无需连接,安装完成后重启电脑。 镜像下载(网盘下载),太大了10个G,挑专用的luckfox_pico_pro_max_image下载。专门为用户提供了适配 LuckFox Pico 和 LuckFox Pico Mini A 的 SD 卡固件以及 LuckFox Pico Mini B 和 LuckFox Pico Plus/Pro/Max 的 SPI FLASH 固件。 镜像烧录,下载和解压烧录工具(去官网下载)。   2)烧录详细步骤   按住 BOOT 键后连接电脑后,松开 BOOT 键,瑞芯微刷机工具就会显示 MaskRom 设备。 加载固件的存放目录,重载 env 文件,勾选所有项。 点击下载。     这样就成功了。   3)检查环境(这步可以跳过,一般都对)   如果烧录成功,此时开发板会闪红灯,并且可以实现SSH、Telnet 登录、串口调试、ADB 登录、文件传输,这里就不再过多赘述,我习惯使用ADB,所以一般用ADB。详情看这个:https://wiki.luckfox.com/zh/Luckfox-Pico/SSH-Telnet-Login 另外还要检查下摄像头,需要配置下网卡地址为 172.32.0.100,详情看这个:https://wiki.luckfox.com/zh/Luckfox-Pico/CSI-Camera         三、SDK 环境部署(PC端)   打开Ubuntu22.04,这个环境安装第一章讲过。 1)搭建编译环境 安装依赖环境: sudo apt update sudo apt-get install -y git ssh make gcc gcc-multilib g++-multilib module-assistant expect g++ gawk texinfo libssl-dev bison flex fakeroot cmake unzip gperf autoconf device-tree-compiler libncurses5-dev pkg-config bc python-is-python3 passwd openssl openssh-server openssh-client vim file cpio rsync   获取最新的 SDK : git clone https://gitee.com/LuckfoxTECH/luckfox-pico.git   git clone https://gitee.com/LuckfoxTECH/luckfox-pico.git 2)SDK 目录说明 SDK目录结构 ├── build.sh - project/build.sh ---- SDK编译脚本 ├── media --------------------------- 多媒体编解码、ISP等算法相关(可独立SDK编译) ├── sysdrv -------------------------- U-Boot、kernel、rootfs目录(可独立SDK编译) ├── project ------------------------- 参考应用、编译配置以及脚本目录 ├── output -------------------------- SDK编译后镜像文件存放目录 └── tools --------------------------- 烧录镜像打包工具以及烧录工具 镜像存放目录 output/ ├── image │ ├── download.bin ---------------- 烧录工具升级通讯的设备端程序,只会下载到板子内存 │ ├── env.img --------------------- 包含分区表和启动参数 │ ├── uboot.img ------------------- uboot镜像 │ ├── idblock.img ----------------- loader镜像 │ ├── boot.img -------------------- kernel镜像 │ ├── rootfs.img ------------------ kernel镜像 │ └── userdata.img ---------------- userdata镜像 └── out ├── app_out --------------------- 参考应用编译后的文件 ├── media_out ------------------- media相关编译后的文件 ├── rootfs_xxx ------------------ 文件系统打包目录 ├── S20linkmount ---------------- 分区挂载脚本 ├── sysdrv_out ------------------ sysdrv编译后的文件 └── userdata -------------------- userdata   3)编译镜像文件   制作镜像不是必要操作。但是下载luckfox-pico这个源码环境包是必须的,后边很多都依赖这里边的工具。   安装交叉编译工具链 cd tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/ source env_install_toolchain.sh 编译 cd luckfox-pico ./build.sh lanch //我烧录进SPI_flash ,选择8 ./build.sh allsave   不知道有没有和我一样编译不成功的,后来排查了一下,没空间了,删了点东西,就可以了。   ./build.sh clean ./build.sh lanch ./build.sh allsave 第一次编译需要花点时间,等待好几个小时,而且需要编译的空间很大,我的40G盘根本放不下,搞得Ubuntu直接崩溃了,还要重新装系统,太痛苦了!!! 编译成功后,生成的固件存放在 SDK目录/output/image 目录下,还是老样子,直接用这个目录就可以烧录镜像。   四、在开发板上跑RKNN模型 1)测试官方给的例子 先用官方给的例子测试一下能不能用 opencv-mobile 添加在摄像头采集图像,左上角添加 fps 并进行 rtsp 推流。 创建个文件夹,克隆下来。(要翻墙,翻不了的老铁自行找码云,或克隆其他大佬的) mkdir workspace cd workspace/ git clone https://github.com/luckfox-eng29/luckfox_pico_rtsp_opencv.git 编译下,使用绝对路径! cd luckfox_pico_rtsp_opencv/ //export LUCKFOX_SDK_PATH=<Your Luckfox-pico Sdk Path> export LUCKFOX_SDK_PATH=/home/linux/Desktop/luckfox-pico mkdir build cd build cmake .. make && make install 将编译生成的luckfox_rtsp_opencv_demo/luckfox_rtsp_opencv 和 lib目录都上传到开发板上,我这里用的是Smaba+ADB。 一根数据线就可连接传输文件,很方便。 adb shell ls mkdir work exit adb push \\192.168.44.129\share\Desktop\workspace\luckfox_pico_rtsp_opencv\luckfox_rtsp_opencv_demo /work adb push \\192.168.44.129\share\Desktop\workspace\luckfox_pico_rtsp_opencv\lib /work/luckfox_rtsp_opencv_demo 登录进板子里,先停止默认运行的rtsp,这个是用于传输视频流的。 adb shell RkLunch-stop.sh 要先更改下权限,才可以运行,luckfox_rtsp_opencv是一个Linux可执行文件。 chmod 777 luckfox_rtsp_opencv ./luckfox_rtsp_opencv 最后用VLC拉流看视频即可,但有的时候板子的网卡没有自动打开,可以重启下。     2)测试大佬的例子   到这里我们就可以尝试使用上一章生成的RKNN模型进行推理测试,模型生成过程就不再过多赘述。 因为官方给的测试代码跑数字识别模型识别率会稍微低一些,所以先试着用大佬改好的代码跑一下。 大佬原帖:https://bbs.eeworld.com.cn/thread-1282745-1-1.html   先把大佬的代码克隆下来 git clone https://gitee.com/luyism/luckfox_rtsp_mnist.git 克隆完就有luckfox_rtsp_mnist文件了,直接进去看下 linux@linux-virtual-machine:~/Desktop/workspace$ cd luckfox_rtsp_mnist/ linux@linux-virtual-machine:~/Desktop/workspace/luckfox_rtsp_mnist$ ls 3rdparty common lib README_CN.md build image_show.png luckfox_rtsp_mnist_dir README.md CMakeLists.txt include model 和官方测试代码一样,都可以编译后上传到开发板上运行,这里的 luckfox_rtsp_mnist_dir 文件夹是可以直接运行的完整文件,那我们先用大佬的模型测试下识别率,看下效果 cd luckfox_rtsp_mnist/ //export LUCKFOX_SDK_PATH=<Your Luckfox-pico Sdk Path> export LUCKFOX_SDK_PATH=/home/linux/Desktop/luckfox-pico mkdir build cd build cmake .. make && make install 提前把文件传到板子上 adb push \\192.168.44.129\share\Desktop\workspace\luckfox_rtsp_mnist\luckfox_rtsp_mnist_dir /work 先停止默认运行的rtsp,这个是用于传输视频流的。 adb shell RkLunch-stop.sh 也是先更改下权限,再输入下面这个即可运行.其中luckfox_rtsp_mnist是可执行文件, model.rknn是对应的模型 chmod 777 luckfox_rtsp_mnist ./luckfox_rtsp_mnist ./model/model.rknn 这里手写的3识别不出来,8还可以,所以朋友们识别的时候把线图粗一些     3)跑通我们自己生成的RKNN模型 这里开始用我们自己生成的模型,可以跟大佬的模型做对比。 直接在开发板上,拷贝一份大佬的工程文件,用于给我们自己测试。 cp luckfox_rtsp_mnist_dir/ luckfox_mnist_test/ -r cd luckfox_mnist_test/ 直接运行,带上我们自己的mnist模型 ./luckfox_rtsp_mnist ./model/mnist.rknn   从我这里运行,可以看出来,应该是模型初始化失败了,我又扒了下别人的模型运行,发现应该就是我自己的模型出问题了,所以我就滚去再生成模型去,此处再省略1W字...... 和原模型作者合影留念     五、代码详解 1)官方例子 1.代码框架   3rdparty: 这个目录通常用于存放第三方库或依赖。在这里,它包含 allocator、librga 和 rknpu2 三个子目录。 build: 这个目录用于存放构建(编译)过程中生成的文件。 common: 这个目录包含项目中多个地方会用到的通用代码、库或资源。 include: 这个目录通常用于存放项目中用到的头文件。 lib: 这个目录通常用于存放库文件(如 .a 或 .so 文件)。 luckfox_rtsp_opencv_demo: 这个目录包含与项目演示或示例相关的代码或资源。 src: 这个目录通常包含源代码文件。   所以我们重点关注src这个文件就可以了,进入src文件夹,只有luckfox_mpi.cc main.cc这两个文件。 文件1: luckfox_mpi.cc 这个文件包含了一些与Rockchip多媒体处理接口(MPI)相关的函数实现。这些函数主要用于初始化和配置视频输入(VI)、视频处理子系统(VPSS)和视频编码(VENC)模块。 TEST_COMM_GetNowUs(): 这个函数用于获取当前的时间戳,以微秒为单位。 vi_dev_init(): 初始化视频输入设备。它首先检查设备是否已配置,如果没有,则进行配置。然后检查设备是否启用,如果没有,则启用它,并绑定到特定的管道。 vi_chn_init(): 初始化视频输入通道,设置缓冲区数量、内存类型、分辨率、像素格式等。 vpss_init(): 初始化视频处理子系统(VPSS),设置通道属性,如模式、动态范围、像素格式、分辨率等,并启动VPSS组。 venc_init(): 初始化视频编码器,设置编码类型、像素格式、配置文件、分辨率、帧宽度、帧高度、缓冲区数量、比特率等,并启动接收帧。 文件2: main.cc 这个文件是程序的主入口点,它使用OpenCV库来显示视频帧,并使用Rockchip的MPI来处理视频流。 包含了一系列标准C库和Rockchip特定的头文件,以及OpenCV的头文件。 main函数首先初始化ISP(图像信号处理器)、MPI系统、RTSP会话,并设置视频输入和VPSS。 使用vi_dev_init和vi_chn_init函数初始化视频输入设备和通道。 使用vpss_init函数初始化VPSS,并绑定VI到VPSS。 使用venc_init函数初始化视频编码器。 在一个无限循环中,程序从VPSS获取帧,使用OpenCV显示帧,并计算帧率。 编码H264视频流,并通过RTSP会话发送。 在循环结束后,程序释放资源并退出。 这两个文件共同工作,实现了一个视频捕获、处理和编码的流程,并通过RTSP协议流式传输视频数据。   2.代码解析 我们这里只需要了解main.cc文件即可。一共只有174行,我们只需要关注下几个重点的部分:   /***************************************************************************** * | Author : Luckfox team * | Function : * | Info : * *---------------- * | This version: V1.0 * | Date : 2024-04-07 * | Info : Basic version * ******************************************************************************/ /*头文件包含: 包含了标准C库头文件、Rockchip平台专用的头文件、OpenCV库的头文件。*/ #include <assert.h> #include <errno.h> #include <fcntl.h> #include <getopt.h> #include <pthread.h> #include <signal.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/poll.h> #include <time.h> #include <unistd.h> #include <vector> #include "rtsp_demo.h" #include "luckfox_mpi.h" #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> int main(int argc, char *argv[]) { /* 全局变量定义: s32Ret: 用于存储函数调用的返回值。 width 和 height: 视频帧的宽度和高度。 fps_text 和 fps: 用于显示帧率。 stFrame 和 h264_frame: 用于存储编码后的视频流数据和视频帧信息。 */ RK_S32 s32Ret = 0; int sX,sY,eX,eY; int width = 2304; int height = 1296; char fps_text[16]; float fps = 0; memset(fps_text,0,16); //h264_frame VENC_STREAM_S stFrame; stFrame.pstPack = (VENC_PACK_S *)malloc(sizeof(VENC_PACK_S)); VIDEO_FRAME_INFO_S h264_frame; VIDEO_FRAME_INFO_S stVpssFrame; /* 初始化ISP: SAMPLE_COMM_ISP_Init 和 SAMPLE_COMM_ISP_Run 用于初始化和运行图像信号处理器。 */ // rkaiq init RK_BOOL multi_sensor = RK_FALSE; const char *iq_dir = "/etc/iqfiles"; rk_aiq_working_mode_t hdr_mode = RK_AIQ_WORKING_MODE_NORMAL; //hdr_mode = RK_AIQ_WORKING_MODE_ISP_HDR2; SAMPLE_COMM_ISP_Init(0, hdr_mode, multi_sensor, iq_dir); SAMPLE_COMM_ISP_Run(0); /* 初始化MPI系统: RK_MPI_SYS_Init 初始化Rockchip的多媒体处理接口。 */ // rkmpi init if (RK_MPI_SYS_Init() != RK_SUCCESS) { RK_LOGE("rk mpi sys init fail!"); return -1; } /* RTSP会话初始化: 创建RTSP演示处理程序和会话,设置视频编码格式,并同步时间戳。 */ // rtsp init rtsp_demo_handle g_rtsplive = NULL; rtsp_session_handle g_rtsp_session; g_rtsplive = create_rtsp_demo(554); g_rtsp_session = rtsp_new_session(g_rtsplive, "/live/0"); rtsp_set_video(g_rtsp_session, RTSP_CODEC_ID_VIDEO_H264, NULL, 0); rtsp_sync_video_ts(g_rtsp_session, rtsp_get_reltime(), rtsp_get_ntptime()); /* 视频输入设备初始化: vi_dev_init 和 vi_chn_init 初始化视频输入设备和通道。 */ // vi init vi_dev_init(); vi_chn_init(0, width, height); /* VPSS初始化: vpss_init 初始化视频处理子系统。 */ // vpss init vpss_init(0, width, height); /* 绑定VI到VPSS: 使用RK_MPI_SYS_Bind将视频输入绑定到视频处理子系统。 */ // bind vi to vpss MPP_CHN_S stSrcChn, stvpssChn; stSrcChn.enModId = RK_ID_VI; stSrcChn.s32DevId = 0; stSrcChn.s32ChnId = 0; stvpssChn.enModId = RK_ID_VPSS; stvpssChn.s32DevId = 0; stvpssChn.s32ChnId = 0; printf("====RK_MPI_SYS_Bind vi0 to vpss0====\n"); s32Ret = RK_MPI_SYS_Bind(&stSrcChn, &stvpssChn); if (s32Ret != RK_SUCCESS) { RK_LOGE("bind 0 ch venc failed"); return -1; } /* 视频编码器初始化: venc_init 初始化视频编码器,设置编码参数。 */ // venc init RK_CODEC_ID_E enCodecType = RK_VIDEO_ID_AVC; venc_init(0, width, height, enCodecType); while(1) { /* 获取VPSS帧:从VPSS中获取一个视频帧。0,0指定了组和通道的ID,&stVpssFrame是存储获取到帧的变量,-1是超时时间,表示无限等待直到获取帧。 */ // get vpss frame s32Ret = RK_MPI_VPSS_GetChnFrame(0,0, &stVpssFrame,-1); /* 处理和显示帧:将获取到的视频帧的内存块地址转换为虚拟地址,然后使用OpenCV创建一个Mat对象,用于处理和显示。这里,将帧率信息显示在帧上,并更新帧的内容(例如,将处理后的图像数据复制回帧的内存空间)。 */ if(s32Ret == RK_SUCCESS) { void *data = RK_MPI_MB_Handle2VirAddr(stVpssFrame.stVFrame.pMbBlk); cv::Mat frame(height,width,CV_8UC3, data); sprintf(fps_text,"fps = %.2f",fps); cv::putText(frame,fps_text, cv::Point(40, 40), cv::FONT_HERSHEY_SIMPLEX,1, cv::Scalar(0,255,0),2); memcpy(data, frame.data, width * height * 3); } /* 编码H264视频帧:将处理后的帧发送给视频编码器进行编码。 */ // send stream // encode H264 RK_MPI_VENC_SendFrame(0, &stVpssFrame,-1); /* 获取编码后的视频流:从视频编码器获取编码后的视频流 */ // rtsp s32Ret = RK_MPI_VENC_GetStream(0, &stFrame, -1); /* RTSP传输:如果已经初始化了RTSP会话,将编码后的视频流通过RTSP传输出去。 */ if(s32Ret == RK_SUCCESS) { if(g_rtsplive && g_rtsp_session) { //printf("len = %d PTS = %d \n",stFrame.pstPack->u32Len, stFrame.pstPack->u64PTS); void *pData = RK_MPI_MB_Handle2VirAddr(stFrame.pstPack->pMbBlk); rtsp_tx_video(g_rtsp_session, (uint8_t *)pData, stFrame.pstPack->u32Len, stFrame.pstPack->u64PTS); rtsp_do_event(g_rtsplive); } /* 计算帧率:计算当前的帧率,TEST_COMM_GetNowUs 用于获取当前时间戳,然后根据前后帧的时间差来计算。 */ RK_U64 nowUs = TEST_COMM_GetNowUs(); fps = (float) 1000000 / (float)(nowUs - stVpssFrame.stVFrame.u64PTS); } /* 释放资源:释放VPSS帧和编码流占用的资源,这是非常重要的,以避免内存泄漏。 */ // release frame s32Ret = RK_MPI_VPSS_ReleaseChnFrame(0, 0, &stVpssFrame); if (s32Ret != RK_SUCCESS) { RK_LOGE("RK_MPI_VI_ReleaseChnFrame fail %x", s32Ret); } s32Ret = RK_MPI_VENC_ReleaseStream(0, &stFrame); if (s32Ret != RK_SUCCESS) { RK_LOGE("RK_MPI_VENC_ReleaseStream fail %x", s32Ret); } } /* 清理阶段: 解除VI和VPSS的绑定。 停止并销毁VPSS组,停止ISP,停止编码器接收帧,销毁编码器通道。 释放分配的内存,退出MPI系统。 */ RK_MPI_SYS_UnBind(&stSrcChn, &stvpssChn); RK_MPI_VI_DisableChn(0, 0); RK_MPI_VI_DisableDev(0); RK_MPI_VPSS_StopGrp(0); RK_MPI_VPSS_DestroyGrp(0); SAMPLE_COMM_ISP_Stop(0); RK_MPI_VENC_StopRecvFrame(0); RK_MPI_VENC_DestroyChn(0); free(stFrame.pstPack); if (g_rtsplive) rtsp_del_demo(g_rtsplive); RK_MPI_SYS_Exit(); return 0; }   最后我们总结下整体的代码逻辑: 初始化阶段: 设置视频帧的宽度和高度。 初始化帧率显示文本和计算变量。 初始化编码后视频流和视频帧信息的结构体。 初始化ISP模块。 初始化Rockchip MPI系统。 初始化RTSP会话和设置视频流参数。 初始化视频输入设备和VPSS。 将视频输入(VI)绑定到视频处理子系统(VPSS)。 初始化视频编码器,设置编码参数。 主循环阶段: 获取VPSS帧: 从VPSS获取视频帧。 处理和显示帧: 将VPSS帧的内存块地址转换为虚拟地址。 使用OpenCV创建Mat对象,处理和显示视频帧。 更新帧的内容,如将处理后的图像数据复制回帧的内存空间。 编码H264视频帧: 发送处理后的帧到视频编码器进行编码。 获取编码后的视频流: 从视频编码器获取编码后的视频流。 RTSP传输: 如果存在有效的RTSP会话,将编码后的视频流通过RTSP传输。 计算帧率: 根据当前时间和上一帧的时间戳计算帧率。 释放资源: 释放VPSS帧和编码流占用的资源。 清理阶段: 解除VI和VPSS的绑定。 停止并销毁VPSS组。 停止ISP模块。 停止视频编码器接收帧,销毁编码器通道。 释放分配的内存。 退出MPI系统。 基本上可以看成这张图:     2)大佬例子 1.代码框架   3rdparty: 此目录通常用于存放项目中使用的第三方库或依赖。 build: 此目录用于存放构建(编译)过程中生成的文件。 common: 此目录包含项目中多个地方会用到的通用代码、库或资源。 include: 此目录通常用于存放项目中用到的头文件。 lib: 此目录通常用于存放库文件(如 .a 或 .so 文件)。 luckfox_rtsp_mnist_dir: 这个目录与MNIST数据集的处理或识别有关,包含模型文件和库文件。 model: 此目录用于存放项目中使用的模型文件,这些文件可以是配置文件或数据模型。 src: 此目录通常包含源代码文件,是编写项目代码的地方。   可以看到比官方给的例子多了一个model文件夹,是因为我们需要用这个文件夹存放我们自己生成的RKNN模型,为后续的编译起到作用。   这部分修改可以查看CMakeLists.txt文件,跳转到最后。 set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/luckfox_rtsp_mnist_dir") file(GLOB RKNN_FILES "${CMAKE_CURRENT_SOURCE_DIR}/model/model.rknn") install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_PREFIX}) install(FILES ${RKNN_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/model)   这部分是大佬修改的,重新定义了项目的安装规则。 设置安装前缀: set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/luckfox_rtsp_mnist_dir") 这条命令设置了项目安装的目标路径。CMAKE_INSTALL_PREFIX 是CMake中的一个变量,用于定义项目构建结果的安装根目录。这里,它被设置为 ${CMAKE_CURRENT_SOURCE_DIR}/luckfox_rtsp_mnist_dir,意味着安装路径将是项目源代码目录下名为 luckfox_rtsp_mnist_dir 的子目录。 收集文件: file(GLOB RKNN_FILES "${CMAKE_CURRENT_SOURCE_DIR}/model/model.rknn") 这条命令使用 file(GLOB ...) 来收集与模式匹配的文件列表。GLOB 是 "global" 的缩写,用于查找所有与给定路径模式匹配的文件。这里,它查找位于 ${CMAKE_CURRENT_SOURCE_DIR}/model/ 目录下所有名为 model.rknn 的文件,并将这些文件的路径存储在变量 RKNN_FILES 中。 安装目标: install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_PREFIX}) 这条命令指定了项目构建生成的目标(可能是可执行文件或库文件)的安装规则。install() 函数定义了如何将构建目标安装到系统中。${PROJECT_NAME} 是在 project() 命令中定义的项目名称,这里假设它是一个构建目标。DESTINATION 指定了这些目标文件的安装目录,即上面设置的 CMAKE_INSTALL_PREFIX。 安装文件: install(FILES ${RKNN_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/model) 这条命令同样使用 install() 函数,但它用于安装文件。${RKNN_FILES} 是上面通过 file(GLOB ...) 收集的文件列表,DESTINATION 指定了这些文件的安装目录,这里是 ${CMAKE_INSTALL_PREFIX}/model,意味着所有匹配的 model.rknn 文件将被安装到 luckfox_rtsp_mnist_dir/model 目录下。   2.代码解析 我们这里也只需要了解main.cc文件即可。一共只有723行,我们看下大佬如何修改的代码:   /***************************************************************************** * | Author : Luckfox team * | Modified By : knv luyism * | Function : * | Info : * *---------------- * | This version: V1.1 * | Date : 2024-05-23 * | Info : Basic version * | Function Add: 1. Add the function of recognizing numbers in the image * | 2. Add the function of displaying the recognized number and its probability * | 3. Add the function of displaying the frame rate * ******************************************************************************/ /*头文件包含: 包含了标准C库头文件、Rockchip平台专用的头文件、OpenCV库的头文件。*/ #include <assert.h> #include <errno.h> #include <fcntl.h> #include <getopt.h> #include <pthread.h> #include <signal.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/poll.h> #include <time.h> #include <unistd.h> #include <vector> #include "rtsp_demo.h" #include "luckfox_mpi.h" #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <rknn_api.h> #define MODEL_WIDTH 28 #define MODEL_HEIGHT 28 #define CHANNEL_NUM 1 // 默认 RKNN_TENSOR_UINT8 rknn_tensor_type input_type = RKNN_TENSOR_UINT8; // 默认 RKNN_TENSOR_NHWC rknn_tensor_format input_layout = RKNN_TENSOR_NHWC; // rknn_context ctx = 0; // 定义一个结构体储存预测到的数字和其对应的概率 struct Prediction { int digit; float probability; }; // 定义全局变量简单队列用于存储预测到的数字和其对应的概率 std::vector<Prediction> predictions_queue; // 定义一个结构体用于存储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; // 从文件中加载模型 static unsigned char *load_model(const char *filename, int *model_size) { // 打开指定的文件以读取二进制数据 FILE *fp = fopen(filename, "rb"); if (fp == nullptr) { printf("fopen %s fail!\n", filename); return NULL; } fseek(fp, 0, SEEK_END); int model_len = ftell(fp); 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_len; if (fp) { fclose(fp); } return model; } // 打印张量属性 static void dump_tensor_attr(rknn_tensor_attr *attr) { printf(" index=%d, name=%s, n_dims=%d, dims=[%d, %d, %d, %d], n_elems=%d, size=%d, fmt=%s, type=%s, qnt_type=%s, " "zp=%d, scale=%f\n", attr->index, attr->name, attr->n_dims, attr->dims[0], attr->dims[1], attr->dims[2], attr->dims[3], attr->n_elems, attr->size, get_format_string(attr->fmt), get_type_string(attr->type), get_qnt_type_string(attr->qnt_type), attr->zp, attr->scale); } // 在图像中找到数字的轮廓,同时减小找到轮廓时的抖动 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; } // 预处理数字区域 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 == 200) { cv::imwrite("pre.jpg", resized); } count++; printf("count=%d\n", count); return resized; } // 将量化的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[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); } //定义init_mnist_model函数,用于初始化mnist模型 int init_mnist_model(const char *model_path,rknn_app_context_t *app_mnist_ctx) { int ret; int model_len = 0; rknn_context ctx_mnist = 0; // char *model; // ret = rknn_init(&ctx_mnist, (char *)model_path, 0, 0, NULL); // if (ret < 0) // { // printf("rknn_init fail! ret=%d\n", ret); // return -1; // } unsigned char * model = load_model(model_path, &model_len); ret = rknn_init(&ctx_mnist, model, model_len, 0, NULL); if (ret < 0) { printf("rknn_init failed! ret=%d", ret); return -1; } // Get sdk and driver version rknn_sdk_version sdk_ver; ret = rknn_query(ctx_mnist, RKNN_QUERY_SDK_VERSION, &sdk_ver, sizeof(sdk_ver)); if (ret != RKNN_SUCC) { printf("rknn_query fail! ret=%d\n", ret); return -1; } printf("rknn_api/rknnrt version: %s, driver version: %s\n", sdk_ver.api_version, sdk_ver.drv_version); // Get Model Input Output Info rknn_input_output_num io_num; ret = rknn_query(ctx_mnist, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num)); if (ret != RKNN_SUCC) { printf("rknn_query fail! ret=%d\n", ret); return -1; } printf("model input num: %d, output num: %d\n", io_num.n_input, io_num.n_output); // 打印输入张量 printf("\ninput tensors:\n"); rknn_tensor_attr input_attrs[io_num.n_input]; memset(input_attrs, 0, sizeof(input_attrs)); for (uint32_t i = 0; i < io_num.n_input; i++) { input_attrs[i].index = i; // query info ret = rknn_query(ctx_mnist, RKNN_QUERY_NATIVE_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("\noutput tensors:\n"); rknn_tensor_attr output_attrs[io_num.n_output]; memset(output_attrs, 0, sizeof(output_attrs)); for (uint32_t i = 0; i < io_num.n_output; i++) { output_attrs[i].index = i; // When using the zero-copy API interface, query the native output tensor attribute ret = rknn_query(ctx_mnist, RKNN_QUERY_NATIVE_NHWC_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]); } // 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; printf("input_attrs[0].size_with_stride=%d\n", input_attrs[0].size_with_stride); // Create input tensor memory app_mnist_ctx->input_mems[0] = rknn_create_mem(ctx_mnist, input_attrs[0].size_with_stride); // 设置输入张量内存 ret = rknn_set_io_mem(ctx_mnist, app_mnist_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) { app_mnist_ctx->output_mems[i] = rknn_create_mem(ctx_mnist, output_attrs[i].size_with_stride); // printf("output_attrs[%d].size_with_stride=%d\n", i, output_attrs[i].size_with_stride); // set output memory and attribute ret = rknn_set_io_mem(ctx_mnist, app_mnist_ctx->output_mems[i], &output_attrs[i]); if (ret < 0) { printf("rknn_set_io_mem fail! ret=%d\n", ret); return -1; } } // 将模型的上下文信息存储到app_mnist_ctx中 app_mnist_ctx->rknn_ctx = ctx_mnist; // TODO if (output_attrs[0].qnt_type == RKNN_TENSOR_QNT_AFFINE_ASYMMETRIC) { app_mnist_ctx->is_quant = true; } else { app_mnist_ctx->is_quant = false; } app_mnist_ctx->io_num = io_num; app_mnist_ctx->input_attrs = (rknn_tensor_attr *)malloc(io_num.n_input * sizeof(rknn_tensor_attr)); memcpy(app_mnist_ctx->input_attrs, input_attrs, io_num.n_input * sizeof(rknn_tensor_attr)); app_mnist_ctx->output_attrs = (rknn_tensor_attr *)malloc(io_num.n_output * sizeof(rknn_tensor_attr)); memcpy(app_mnist_ctx->output_attrs, output_attrs, io_num.n_output * sizeof(rknn_tensor_attr)); printf("model is NHWC input fmt\n"); app_mnist_ctx->model_height = input_attrs[0].dims[1]; app_mnist_ctx->model_width = input_attrs[0].dims[2]; app_mnist_ctx->model_channel = input_attrs[0].dims[3]; // 打印模型输出信息 printf("model input height=%d, width=%d, channel=%d\n", app_mnist_ctx->model_height, app_mnist_ctx->model_width, app_mnist_ctx->model_channel); printf("Init success \n"); return 0; } // 定义内存释放函数,用于释放模型的内存 int release_mnist_model(rknn_app_context_t *app_ctx) { if (app_ctx->rknn_ctx != 0) { rknn_destroy(app_ctx->rknn_ctx); app_ctx->rknn_ctx = 0; } if (app_ctx->net_mem != NULL) { printf("destory mem\n"); rknn_destroy_mem(app_ctx->rknn_ctx, app_ctx->net_mem); free(app_ctx->net_mem); } if (app_ctx->max_mem != NULL) { printf("destory mem\n"); rknn_destroy_mem(app_ctx->rknn_ctx, app_ctx->max_mem); free(app_ctx->max_mem); } if (app_ctx->input_attrs != NULL) { free(app_ctx->input_attrs); app_ctx->input_attrs = NULL; } if (app_ctx->output_attrs != NULL) { free(app_ctx->output_attrs); app_ctx->output_attrs = NULL; } for (int i = 0; i < app_ctx->io_num.n_input; i++) { if (app_ctx->input_mems[i] != NULL) { rknn_destroy_mem(app_ctx->rknn_ctx, app_ctx->input_mems[i]); free(app_ctx->input_mems[i]); } } for (int i = 0; i < app_ctx->io_num.n_output; i++) { if (app_ctx->output_mems[i] != NULL) { rknn_destroy_mem(app_ctx->rknn_ctx, app_ctx->output_mems[i]); free(app_ctx->output_mems[i]); } } return 0; } int run_inference(rknn_app_context_t *app_ctx, cv::Mat &frame) { int ret; //****************传入图像至推理内存区******************// int width = app_ctx->input_attrs[0].dims[2]; int stride = app_ctx->input_attrs[0].w_stride; if (width == stride) { memcpy(app_ctx->input_mems[0]->virt_addr, frame.data, width * app_ctx->input_attrs[0].dims[1] * app_ctx->input_attrs[0].dims[3]); } else { int height = app_ctx->input_attrs[0].dims[1]; int channel = app_ctx->input_attrs[0].dims[3]; // copy from src to dst with stride uint8_t *src_ptr = frame.data; uint8_t *dst_ptr = (uint8_t *)app_ctx->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; } } // 运行推理 ret = rknn_run(app_ctx->rknn_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 *)app_ctx->output_mems[0]->virt_addr; output_normalization(&app_ctx->output_attrs[0], output, out_fp32); return 0; } int main(int argc, char *argv[]) { // rknn init if (argc != 2) { printf("Usage: %s <model.rknn>", argv[0]); return -1; } char *model_path = argv[1]; RK_S32 s32Ret = 0; int sX, sY, eX, eY; int width = 640; int height = 480; char fps_text[16]; float fps = 0; memset(fps_text, 0, 16); // h264_frame VENC_STREAM_S stFrame; stFrame.pstPack = (VENC_PACK_S *)malloc(sizeof(VENC_PACK_S)); VIDEO_FRAME_INFO_S h264_frame; VIDEO_FRAME_INFO_S stVpssFrame; // rkaiq init RK_BOOL multi_sensor = RK_FALSE; const char *iq_dir = "/etc/iqfiles"; rk_aiq_working_mode_t hdr_mode = RK_AIQ_WORKING_MODE_NORMAL; // hdr_mode = RK_AIQ_WORKING_MODE_ISP_HDR2; SAMPLE_COMM_ISP_Init(0, hdr_mode, multi_sensor, iq_dir); SAMPLE_COMM_ISP_Run(0); // rkmpi init if (RK_MPI_SYS_Init() != RK_SUCCESS) { RK_LOGE("rk mpi sys init fail!"); return -1; } // rtsp init rtsp_demo_handle g_rtsplive = NULL; rtsp_session_handle g_rtsp_session; g_rtsplive = create_rtsp_demo(554); g_rtsp_session = rtsp_new_session(g_rtsplive, "/live/0"); rtsp_set_video(g_rtsp_session, RTSP_CODEC_ID_VIDEO_H264, NULL, 0); rtsp_sync_video_ts(g_rtsp_session, rtsp_get_reltime(), rtsp_get_ntptime()); // vi init vi_dev_init(); vi_chn_init(0, width, height); // vpss init vpss_init(0, width, height); // bind vi to vpss MPP_CHN_S stSrcChn, stvpssChn; stSrcChn.enModId = RK_ID_VI; stSrcChn.s32DevId = 0; stSrcChn.s32ChnId = 0; stvpssChn.enModId = RK_ID_VPSS; stvpssChn.s32DevId = 0; stvpssChn.s32ChnId = 0; printf("====RK_MPI_SYS_Bind vi0 to vpss0====\n"); s32Ret = RK_MPI_SYS_Bind(&stSrcChn, &stvpssChn); if (s32Ret != RK_SUCCESS) { RK_LOGE("bind 0 ch venc failed"); return -1; } // venc init RK_CODEC_ID_E enCodecType = RK_VIDEO_ID_AVC; venc_init(0, width, height, enCodecType); // rknn结构体变量 rknn_app_context_t app_mnist_ctx; memset(&app_mnist_ctx, 0, sizeof(rknn_app_context_t)); init_mnist_model(model_path, &app_mnist_ctx); while (1) { // get vpss frame s32Ret = RK_MPI_VPSS_GetChnFrame(0, 0, &stVpssFrame, -1); if (s32Ret == RK_SUCCESS) { 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); if (digit_rect.area() > 0) { cv::Mat digit_region = frame(digit_rect); cv::Mat preprocessed = preprocess_digit_region(digit_region); // 运行推理 run_inference(&app_mnist_ctx, preprocessed); // 从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); // 打印预测到的数字和其对应的概率 // printf("****** Predicted digit: %d, Probability: %.2f ******\n", prediction.digit, prediction.probability); // 从predictions_queue中删除最旧的元素 predictions_queue.pop_back(); } } sprintf(fps_text, "fps:%.2f", fps); cv::putText(frame, fps_text, cv::Point(40, 40), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0), 2); memcpy(data, frame.data, width * height * 3); } // send stream // encode H264 RK_MPI_VENC_SendFrame(0, &stVpssFrame, -1); // rtsp s32Ret = RK_MPI_VENC_GetStream(0, &stFrame, -1); if (s32Ret == RK_SUCCESS) { if (g_rtsplive && g_rtsp_session) { // printf("len = %d PTS = %d \n",stFrame.pstPack->u32Len, stFrame.pstPack->u64PTS); void *pData = RK_MPI_MB_Handle2VirAddr(stFrame.pstPack->pMbBlk); rtsp_tx_video(g_rtsp_session, (uint8_t *)pData, stFrame.pstPack->u32Len, stFrame.pstPack->u64PTS); rtsp_do_event(g_rtsplive); } RK_U64 nowUs = TEST_COMM_GetNowUs(); fps = (float)1000000 / (float)(nowUs - stVpssFrame.stVFrame.u64PTS); } // release frame s32Ret = RK_MPI_VPSS_ReleaseChnFrame(0, 0, &stVpssFrame); if (s32Ret != RK_SUCCESS) { RK_LOGE("RK_MPI_VI_ReleaseChnFrame fail %x", s32Ret); } s32Ret = RK_MPI_VENC_ReleaseStream(0, &stFrame); if (s32Ret != RK_SUCCESS) { RK_LOGE("RK_MPI_VENC_ReleaseStream fail %x", s32Ret); } } RK_MPI_SYS_UnBind(&stSrcChn, &stvpssChn); RK_MPI_VI_DisableChn(0, 0); RK_MPI_VI_DisableDev(0); RK_MPI_VPSS_StopGrp(0); RK_MPI_VPSS_DestroyGrp(0); SAMPLE_COMM_ISP_Stop(0); RK_MPI_VENC_StopRecvFrame(0); RK_MPI_VENC_DestroyChn(0); free(stFrame.pstPack); if (g_rtsplive) rtsp_del_demo(g_rtsplive); RK_MPI_SYS_Exit(); // 释放模型内存 release_mnist_model(&app_mnist_ctx); return 0; }   可以看出当前的 main.cc 是在官方例子的基础上进行了扩展,增加了使用RKNN模型进行图像中数字识别的功能。最主要的区别是: RKNN模型加载与推理: 加载RKNN模型到内存。 定义结构体存储预测的数字及其概率。 在图像中检测数字轮廓,并进行预处理。 执行模型推理,并将预处理后的图像数据传入模型。 将模型输出进行归一化,计算概率,并存储预测结果。 数字识别与显示: 在主循环中,检测图像中的数字,并使用RKNN模型进行识别。 在图像上绘制识别的数字及其概率。 维护一个预测结果队列,并在图像上显示最新预测结果。   下面我们详细分析下在main函数中都做了那些事情:   int main(int argc, char *argv[]) { // ... 省略了共同的初始化代码 ... // rknn init if (argc != 2) { printf("Usage: %s <model.rknn>", argv[0]); return -1; } char *model_path = argv[1]; // ------- 初始化 RKNN 模型 rknn_app_context_t app_mnist_ctx; memset(&app_mnist_ctx, 0, sizeof(rknn_app_context_t)); init_mnist_model(model_path, &app_mnist_ctx); while (1) { // get vpss frame s32Ret = RK_MPI_VPSS_GetChnFrame(0, 0, &stVpssFrame, -1); if (s32Ret == RK_SUCCESS) { 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); if (digit_rect.area() > 0) { cv::Mat digit_region = frame(digit_rect); cv::Mat preprocessed = preprocess_digit_region(digit_region); // ------- 运行推理 run_inference(&app_mnist_ctx, preprocessed); // ------- 从predictions_queue中获取预测到的数字和其对应的概率 if (!predictions_queue.empty()) { Prediction prediction = predictions_queue.back(); cv::rectangle(frame, digit_rect, cv::Scalar(0, 255, 0), 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(); } } sprintf(fps_text, "fps:%.2f", fps); cv::putText(frame, fps_text, cv::Point(40, 40), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0), 2); memcpy(data, frame.data, width * height * 3); } // send stream // ... 省略了共同的编码和发送代码 ... // release frame // ... 省略了共同的释放帧代码 ... } // ... 省略了共同的清理代码 ... // ------- 释放 RKNN 模型内存 release_mnist_model(&app_mnist_ctx); return 0; }   可以看出,最主要是使用了零拷贝的API,想了解的可以看下大佬原帖:https://bbs.eeworld.com.cn/thread-1282745-1-1.html 大致框架也就是下面这个样子:     六、总结 最后总结下:   模型识别率在电脑中测试可以高达98%,但实时用摄像头采集并用模型识别准确率只能达到60%左右。   可能的原因: 光照和图像质量:实时摄像头采集的图像可能受到光照条件的影响,导致图像质量不佳。 图像分辨率和缩放:实时采集的图像分辨率与模型训练时使用的图像分辨率不同,图像缩放导致信息丢失。。 模型输入不一致:实时图像可能与训练模型时使用的图像在尺寸、颜色空间或数据归一化方面不一致。 摄像头硬件限制:摄像头本身的硬件限制,比如分辨率、对焦、镜头质量等,可能影响图像的清晰度和准确率。 改进措施: 改善摄像头硬件限制条件:确保摄像头在充足的光照下工作,或使用更好的分辨率、对焦、镜头的质量。 调整图像预处理:对实时采集的图像进行更细致的预处理,如直方图均衡化、对比度增强等。 优化模型输入:确保实时图像的分辨率、颜色空间和归一化方法与模型训练时保持一致。 模型重训练:使用实时采集的图像对模型进行进一步的训练或微调,以适应实际应用场景。 增加模型复杂度:可以尝试使用更复杂的模型结构来提高识别准确率。 性能优化:更换芯片提高计算能力,优化代码和算法,减少实时处理的延迟,确保有足够的时间进行图像处理。 数据增强:在模型训练阶段应用数据增强技术,使模型更适应各种不同的图像条件。 后处理改进:应用后处理技术,如非极大值抑制(NMS)或阈值处理,以提高识别准确率。

  • 2024-05-13
  • 回复了主题帖: 都实现自动驾驶了,有没有给盲人用的智能导航产品?

    真别说,我还想做这种产品,智能拐杖。 多个摄像头识别物体+激光雷达测量直径1m外物体的距离啥的,技术上能简单实现,感觉最主要的是不挣钱,没人愿意做

  • 2024-05-10
  • 回复了主题帖: 【AI挑战营第二站】算法工程化部署打包成SDK

    1、跟帖回复:什么是ONNX模型、RKNN模型 ONNX模型和RKNN模型都是用于深度学习模型部署和推理的格式或框架。 ONNX(Open Neural Network Exchange)是一个开放的模型表示格式,旨在使不同深度学习框架之间的模型转换和部署更加容易。通过使用ONNX,您可以在不同的深度学习框架之间轻松地转换和共享模型,从而提高模型的可移植性和复用性。 RKNN(Rockchip Neural Network)则是Rockchip推出的用于在其自家芯片上进行模型部署和推理的框架。RKNN框架为Rockchip的芯片定制了针对深度学习模型的优化加速器,可以实现在RK芯片上高效地运行深度学习模型。 因此,当需要在Rockchip的芯片上部署和运行深度学习模型时,可以将模型转换为RKNN格式,以利用Rockchip提供的优化硬件加速器来提高模型的性能和效率。而ONNX则可以作为一个通用的模型表示格式,方便在不同框架之间进行模型转换和共享。   2.发帖链接:#AI挑战营第二站# 基于RKNN toolkit的模型转换与部署 - 嵌入式系统 - 电子工程世界-论坛 (eeworld.com.cn)

  • 发表了主题帖: #AI挑战营第二站# 基于RKNN toolkit的模型转换与部署

    本来想按照之前在windows上继续转换模型,谁知道直接不支持?那就直接在Ubuntu上搞。   一、什么是ONNX模型、RKNN模型? ONNX模型和RKNN模型都是用于深度学习模型部署和推理的格式或框架。 ONNX(Open Neural Network Exchange)是一个开放的模型表示格式,旨在使不同深度学习框架之间的模型转换和部署更加容易。通过使用ONNX,您可以在不同的深度学习框架之间轻松地转换和共享模型,从而提高模型的可移植性和复用性。 RKNN(Rockchip Neural Network)则是Rockchip推出的用于在其自家芯片上进行模型部署和推理的框架。RKNN框架为Rockchip的芯片定制了针对深度学习模型的优化加速器,可以实现在RK芯片上高效地运行深度学习模型。 因此,当需要在Rockchip的芯片上部署和运行深度学习模型时,可以将模型转换为RKNN格式,以利用Rockchip提供的优化硬件加速器来提高模型的性能和效率。而ONNX则可以作为一个通用的模型表示格式,方便在不同框架之间进行模型转换和共享。   RV1106文档地址如下: https://github.com/airockchip/rknn-toolkit2/blob/master/doc/01_Rockchip_RV1106_RV1103_Quick_Start_RKNN_SDK_V2.0.0beta0_CN.pdf   二、准备开发环境 1)下载RNKK相关仓库 新建文件夹 cd Desktop mkdir rv1106 cd rv1106 下载 RKNN-Toolkit2 仓库 git clone https://github.com/airockchip/rknn-toolkit2.git --depth 1 下载 RKNN Model Zoo 仓库 git clone https://github.com/airockchip/rknn_model_zoo.git --depth 1 注意: 1.参数 --depth 1 表示只克隆最近一次 commit 2.如果遇到 git clone 失败的情况,也可以直接在 github 中下载压缩包到本地,然后解压至该目录   2)安装conda 在计算机的终端窗口中执行以下命令,检查是否安装 Conda,若已安装则可省略此节步骤。 conda -V 参考输出信息:conda 23.9.0 ,表示 conda 版本为 23.9.0 如果提示 conda: command not found,则表示未安装 conda 如果没有安装 Conda,可以通过下面的链接下载 Conda 安装包: wget -c https://mirrors.bfsu.edu.cn/anaconda/miniconda/Miniconda3-latest-Linux-x86_64.sh 然后通过以下命令安装 Conda: chmod 777 Miniconda3-latest-Linux-x86_64.sh bash Miniconda3-latest-Linux-x86_64.sh   3)使用 Conda 创建 Python 环境 在计算机的终端窗口中,执行以下命令进入 Conda base 环境:miniconda3 安装的目录 source ~/miniconda3/bin/activate 成功后,命令行提示符会变成以下形式: (base) xxx@xxx:~$ 通过以下命令创建名称为 toolkit2 的 Python 3.8 环境: conda create -n toolkit2 python=3.8 激活 toolkit2 环境,后续将在此环境中安装 RKNN-Toolkit2: conda activate toolkit2 成功后,命令行提示符会变成以下形式: (toolkit2) xxx@xxx:~   4)安装依赖库和 RKNN-Toolkit2 进入 rknn-toolkit2 目录 cd rv1106/rknn-toolkit2/rknn-toolkit2 请根据不同的 python 版本,选择不同的 requirements 文件 pip install -r packages/requirements_cp38-2.0.0b0.txt 安装 RKNN-Toolkit2 请根据不同的 python 版本及处理器架构,选择不同的 wheel 安装包文件: 其中 x.x.x 是 RKNN-Toolkit2 版本号,xxxxxxxx 是提交号,cpxx 是 python 版 pip install packages/rknn_toolkit2-2.0.0b0+9bab5682-cp38-cp38-linux_x86_64.whl       5)验证是否安装成功 执行以下命令,若没有报错,则代表 RKNN-Toolkit2 环境安装成功。 进入 Python 交互模式 python   python   导入 RKNN 类   from rknn.api import RKNN     6)提前准备转换需要的文件 提前新建文件目录:   convert.py是我们的python代码 data.txt是用于指定用于量化优化的数据集文件,里面写成这样就行:   ./pic/0-label-0.png   0-label-0.png是28*28的图片素材   7)编写代码完成转换 以下是convert.py的代码,其中mnt/hgfs/VMwork-2/mnist_101_model.onnx 是我们上一步生成的ONNX模型。 from rknn.api import RKNN # Create RKNN object rknn = RKNN(verbose=True) # pre-process config print('--> config model') rknn.config(target_platform='rv1106', mean_values=[[28]], std_values=[[28]]) print('done') # Load model print('--> Loading model') ret = rknn.load_onnx(model='/mnt/hgfs/VMwork-2/mnist_101_model.onnx') if ret != 0: print('Load model failed!') exit(ret) print('done') rknn.build(do_quantization=True, dataset='./data.txt') # 构建RKNN模型,可选参数量化 if ret != 0: print('Build model failed!') exit(ret) print('done') ret = rknn.export_rknn('./mnist.rknn') # 导出RKNN模型文件 if ret != 0: print('Export rknn model failed!') exit(ret) print('done') # 释放 RKNN 对象 rknn.release() 直接启动生成RKNN! python convert.py       可以看到生成mnist.rknn成功! 完整目录放到了test压缩包里。  

  • 2024-05-08
  • 回复了主题帖: 入围名单公布:嵌入式工程师AI挑战营(初阶),获RV1106 Linux 板+摄像头的名单

    本帖最后由 硬核王同学 于 2024-5-9 15:13 编辑 个人信息已确认,领取板卡,可继续完成&分享挑战营第二站和第三站任务。

  • 2024-04-30
  • 回复了主题帖: #AI挑战营第二站# Ubuntu平台下ONNX模型转RKNN

    安装的toolkit2版本很高,必须要求转出的onnx版本为19  如何解决的呢  

  • 2024-04-24
  • 回复了主题帖: #AI挑战营第一站#基于PyTorch,在PC上完成MNIST手写数字识别模型训练

    wangerxian 发表于 2024-4-24 09:15 用cuda训练,速度应该很快,一套下来用了多久时间? 3轮训练不到一分钟

  • 回复了主题帖: 免费申请:幸狐 RV1106 Linux 开发板(带摄像头),助力AI挑战营应用落地

    #AI挑战营第一站#基于PyTorch,在PC上完成MNIST手写数字识别模型训练 - 嵌入式系统 - 电子工程世界-论坛 (eeworld.com.cn) 预期应用:实现数字识别车牌识别

  • 发表了主题帖: #AI挑战营第一站#基于PyTorch,在PC上完成MNIST手写数字识别模型训练

    本帖最后由 硬核王同学 于 2024-4-24 10:20 编辑 注意!网上有很多版本无法实现手写数字模型训练,我这个亲手实验,是可以成功的!   前几天一直在做环境的安装,我想在Ubuntu14上运行,发现python都不支持,后来升级到22可以了,但是在那个环境里没有显卡,所以还是玩不起来。   索性还是在windows里面训练吧,也记录下如何在PC上完成手写数字模型训练。   1.安装环境 1.1安装CUDA 首先查看CUDA环境: cmd命令行内输入nvidia-smi.exe,查看本机显卡驱动版本及可用的CUDA版本。     首先要注意查看,Pytorch对CUDA版本要求,不能乱装,所以得到官网看看匹配的版本。 https://pytorch.org/get-started/locally/   这里可以看到,最新稳定版本的PyTorch能支持到CUDA12.1版本。   CUDA官网下载链接:https://developer.nvidia.com/cuda-toolkit-archive     根据系统选择windows、64、11、exe版本,下载比较慢稍微等一下。     直接双击安装,下一步就好了。       重启cmd,查看nvcc --version软件版本     1.2安装Anaconda 登录anaconda的官网下载,anaconda是一个集成的工具软件不需要我们再次下载。 anaconda官网       输入你的邮箱注册       点击立即下载,选择windows版本        直接双击安装,下一步就好了。       windows搜索框里找一下anaconda prompt     打开,出现(base),安装成功。     1.3配置pytorch环境 在Anaconda Prompt下敲命令:conda create -n python python=3.11   此时需要下载导入一些包,输入y。   导入完成,切换虚拟环境名字叫python:conda activate python,再pip list查看当前环境,发现没有PyTorch,那么下面我们就需要安装它。     1.4安装pytorch 进入pytorch官网,选择本机CUDA12.1版本安装   复制下面的命令: pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 在原来的python环境中输入命令,开始下载:   下载完成后我们再次输入pip list,查看到已经有torch的存在。   先输入python,然后输入import torch,如果输入后没有任何报错,没有任何显示那就是成功了,然后再输入torch.cuda.is_available(),返回的是True,那便是完成了整个操作。     2.训练模型 接下来就可以正式开始模型训练工作了,这里我参考的实现是下面的教程,非常详细。 https://blog.csdn.net/qq_45588019/article/details/120935828   提前安装好相关包文件,在python环境下执行以下命令: pip install matplotlib pip install onnx   2.1导入包: import torch import torchvision from torch.utils.data import DataLoader import numpy as np from matplotlib import pyplot as plt from torchvision import transforms from torchvision import datasets import torch.nn.functional as F import torch.optim as optim     2.2定义超参数: n_epochs = 3 batch_size_train = 64 batch_size_test = 1000 learning_rate = 0.01 momentum = 0.5 log_interval = 10 random_seed = 1 torch.manual_seed(random_seed)     2.3载入数据集: train_loader = torch.utils.data.DataLoader( torchvision.datasets.MNIST('./data/', train=True, download=True, transform=torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize( (0.1307,), (0.3081,)) ])), batch_size=batch_size_train, shuffle=True) test_loader = torch.utils.data.DataLoader( torchvision.datasets.MNIST('./data/', train=False, download=True, transform=torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize( (0.1307,), (0.3081,)) ])), batch_size=batch_size_test, shuffle=True)   运行上面的程序后,会自动将数据集下载到目录下的data文件夹。   2.4查看MNIST数据集: examples = enumerate(test_loader) batch_idx, (example_data, example_targets) = next(examples) print(example_targets) print(example_data.shape)   fig = plt.figure() for i in range(6): plt.subplot(2,3,i+1) plt.tight_layout() plt.imshow(example_data[i][0], cmap='gray', interpolation='none') plt.title("Ground Truth: {}".format(example_targets[i])) plt.xticks([]) plt.yticks([])   这里记得跳出循环后再敲plt.show()   plt.show()       2.5构建网络: 现在让我们开始建立我们的网络。我们将使用两个2d卷积层,然后是两个全连接(或线性)层。作为激活函数,我们将选择整流线性单元(简称ReLUs),作为正则化的手段,我们将使用两个dropout层。在PyTorch中,构建网络的一个好方法是为我们希望构建的网络创建一个新类。让我们在这里导入一些子模块,以获得更具可读性的代码。 import torch.nn as nn import torch.nn.functional as F import torch.optim as optim class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 10, kernel_size=5) self.conv2 = nn.Conv2d(10, 20, kernel_size=5) self.conv2_drop = nn.Dropout2d() self.fc1 = nn.Linear(320, 50) self.fc2 = nn.Linear(50, 10) def forward(self, x): x = F.relu(F.max_pool2d(self.conv1(x), 2)) x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) x = x.view(-1, 320) x = F.relu(self.fc1(x)) x = F.dropout(x, training=self.training) x = self.fc2(x) x = F.log_softmax(x, dim=1) return x     具体各部分的含义,在下面详细讲! 广义地说,我们可以想到torch.nn层中包含可训练的参数,而torch.nn.functional就是纯粹的功能性。forward()传递定义了使用给定的层和函数计算输出的方式。为了便于调试,在前向传递中打印出张量是完全可以的。在试验更复杂的模型时,这就派上用场了。请注意,前向传递可以使用成员变量甚至数据本身来确定执行路径——它还可以使用多个参数!   2.6实例化模型: network = Net() device = torch.device("cuda" if torch.cuda.is_available() else "cpu") network.to(device)     2.7初始化网络及优化器: criterion = torch.nn.CrossEntropyLoss() # 交叉熵损失 optimizer = torch.optim.SGD(network.parameters(), lr=learning_rate, momentum=momentum) # lr学习率,momentum冲量   2.8模型训练: 建立我们的训练循环了。 首先,我们要确保我们的网络处于训练模式。然后,每个epoch对所有训练数据进行一次迭代。加载单独批次由DataLoader处理。 我们需要使用optimizer.zero_grad()手动将梯度设置为零,因为PyTorch在默认情况下会累积梯度。 然后,我们生成网络的输出(前向传递),并计算输出与真值标签之间的负对数概率损失。 现在,我们收集一组新的梯度,并使用optimizer.step()将其传播回每个网络参数。 有关PyTorch自动渐变系统内部工作方式的详细信息,请参阅autograd的官方文档(强烈推荐)。 我们还将使用一些打印输出来跟踪进度。为了在以后创建一个良好的培训曲线,我们还创建了两个列表来节省培训和测试损失。在x轴上,我们希望显示网络在培训期间看到的培训示例的数量。 train_losses = [] train_counter = [] test_losses = [] test_counter = [i*len(train_loader.dataset) for i in range(n_epochs + 1)]   在开始训练之前,我们将运行一次测试循环,看看仅使用随机初始化的网络参数可以获得多大的精度/损失。你能猜出我们的准确度是多少吗? def train(epoch): network.train() for batch_idx, (data, target) in enumerate(train_loader): data, target = data.to(device), target.to(device) # 将数据和标签移动到设备上 optimizer.zero_grad() output = network(data) loss = criterion(output, target) # 使用交叉熵损失函数 loss.backward() optimizer.step() if batch_idx % log_interval == 0: print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format( epoch, batch_idx * len(data), len(train_loader.dataset), 100. * batch_idx / len(train_loader), loss.item())) train_losses.append(loss.item()) train_counter.append( (batch_idx * 64) + ((epoch - 1) * len(train_loader.dataset))) torch.save(network.state_dict(), './model.pth') torch.save(optimizer.state_dict(), './optimizer.pth')   train(1)     神经网络模块以及优化器能够使用.state_dict()保存和加载它们的内部状态。这样,如果需要,我们就可以继续从以前保存的状态dict中进行训练——只需调用.load_state_dict(state_dict)。   2.9循环测试: 现在进入循环测试。在这里,我们总结了测试损失,并跟踪正确分类的数字来计算网络的精度。 def test(): network.eval() test_loss = 0 correct = 0 with torch.no_grad(): for data, target in test_loader: data, target = data.to(device), target.to(device) output = network(data) test_loss += F.nll_loss(output, target, reduction='sum').item() pred = output.data.max(1, keepdim=True)[1] correct += pred.eq(target.data.view_as(pred)).sum() test_loss /= len(test_loader.dataset) test_losses.append(test_loss) print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(test_loss, correct, len(test_loader.dataset),100. * correct / len(test_loader.dataset))) test()     开始多次训练! 我们将在循环遍历n_epochs之前手动添加test()调用,以使用随机初始化的参数来评估我们的模型。 for epoch in range(1, n_epochs + 1): train(epoch) test()       2.10评估模型的性能:   经过几个阶段的训练,我们已经能够达到测试集97%的准确率!我们开始使用随机初始化的参数。 我们来画一下训练曲线。 import matplotlib.pyplot as plt fig = plt.figure() plt.plot(train_counter, train_losses, color='blue') plt.scatter(test_counter, test_losses, color='red') plt.legend(['Train Loss', 'Test Loss'], loc='upper right') plt.xlabel('number of training examples seen') plt.ylabel('negative log likelihood loss') plt.show()       2.11模型导出为pth文件: torch.save(network.state_dict(), 'mnist_101_model.pth')   2.12导出Onnx文件: dummy_input = torch.randn(1, 1, 28, 28) torch.onnx.export(network, dummy_input.to(device), "mnist_101_model.onnx", verbose=False)       3.完整代码: import torch import torchvision from torch.utils.data import DataLoader import numpy as np from matplotlib import pyplot as plt from torchvision import transforms from torchvision import datasets import torch.nn.functional as F import torch.optim as optim n_epochs = 3 batch_size_train = 64 batch_size_test = 1000 learning_rate = 0.01 momentum = 0.5 log_interval = 10 random_seed = 1 torch.manual_seed(random_seed) train_loader = torch.utils.data.DataLoader( torchvision.datasets.MNIST('./data/', train=True, download=True, transform=torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize( (0.1307,), (0.3081,)) ])), batch_size=batch_size_train, shuffle=True) test_loader = torch.utils.data.DataLoader( torchvision.datasets.MNIST('./data/', train=False, download=True, transform=torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize( (0.1307,), (0.3081,)) ])), batch_size=batch_size_test, shuffle=True) import torch.nn as nn import torch.nn.functional as F import torch.optim as optim class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 10, kernel_size=5) self.conv2 = nn.Conv2d(10, 20, kernel_size=5) self.conv2_drop = nn.Dropout2d() self.fc1 = nn.Linear(320, 50) self.fc2 = nn.Linear(50, 10) def forward(self, x): x = F.relu(F.max_pool2d(self.conv1(x), 2)) x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) x = x.view(-1, 320) x = F.relu(self.fc1(x)) x = F.dropout(x, training=self.training) x = self.fc2(x) x = F.log_softmax(x, dim=1) return x network = Net() device = torch.device("cuda" if torch.cuda.is_available() else "cpu") network.to(device) criterion = torch.nn.CrossEntropyLoss() # 交叉熵损失 optimizer = torch.optim.SGD(network.parameters(), lr=learning_rate, momentum=momentum) # lr学习率,momentum冲量 train_losses = [] train_counter = [] test_losses = [] test_counter = [i*len(train_loader.dataset) for i in range(n_epochs + 1)] def train(epoch): network.train() for batch_idx, (data, target) in enumerate(train_loader): data, target = data.to(device), target.to(device) # 将数据和标签移动到设备上 optimizer.zero_grad() output = network(data) loss = criterion(output, target) # 使用交叉熵损失函数 loss.backward() optimizer.step() if batch_idx % log_interval == 0: print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format( epoch, batch_idx * len(data), len(train_loader.dataset), 100. * batch_idx / len(train_loader), loss.item())) train_losses.append(loss.item()) train_counter.append( (batch_idx * 64) + ((epoch - 1) * len(train_loader.dataset))) torch.save(network.state_dict(), './model.pth') torch.save(optimizer.state_dict(), './optimizer.pth') train(1) def test(): network.eval() test_loss = 0 correct = 0 with torch.no_grad(): for data, target in test_loader: data, target = data.to(device), target.to(device) output = network(data) test_loss += F.nll_loss(output, target, reduction='sum').item() pred = output.data.max(1, keepdim=True)[1] correct += pred.eq(target.data.view_as(pred)).sum() test_loss /= len(test_loader.dataset) test_losses.append(test_loss) print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(test_loss, correct, len(test_loader.dataset),100. * correct / len(test_loader.dataset))) test() for epoch in range(1, n_epochs + 1): train(epoch) test() import matplotlib.pyplot as plt fig = plt.figure() plt.plot(train_counter, train_losses, color='blue') plt.scatter(test_counter, test_losses, color='red') plt.legend(['Train Loss', 'Test Loss'], loc='upper right') plt.xlabel('number of training examples seen') plt.ylabel('negative log likelihood loss') plt.show() torch.save(network.state_dict(), 'mnist_101_model.pth') dummy_input = torch.randn(1, 1, 28, 28) torch.onnx.export(network, dummy_input.to(device), "mnist_101_model.onnx", verbose=False)        

  • 2024-04-21
  • 发表了主题帖: 一起读《奔跑吧Linux内核(第2版)卷2:调试与案例分析》- 内核调试

      这一章一起动手做一下内核的调试。   环境这块折腾了很久,之前都是Ubuntu14的,现在这个需要更新到Ubuntu22才可以~,大家动手做实验的时候注意啦!   一、搭建QEMU+Debian实验平台   一、安装工具 1.1、Linux主机安装需要的工具包   sudo apt-get install qemu libncurses5-dev gcc-aarch64-linux-gnu build-essential git bison flex libssl-dev qemu-system-arm   1.2、安装完成后,检查QEMU版本   qemu-system-aarch64 --version       二、下载源码 下载runninglinuxkernel_5.0源码   git clone https://gitee.com/zhang-ge/runninglinuxkernel_5.0.git       二、运行实验平台   一、编译内核   cd runninglinuxkernel_5.0   如果在编译内核之前想进入menuconfig界面来配置内核:   git checkout rlk_5.0   ./run_debian_arm64.sh menuconfig   编译内核。   $ ./run_debian_arm64.sh build_kernel   执行上述脚本需要几十分钟,依赖于主机的计算能力。接着,编译根文件系统。   $ sudo ./run_debian_arm64.sh build_rootfs   编译根文件系统需要管理员权限,而编译内核则不需要。 执行完成后会生成一个名为rootfs_arm64.ext4的根文件系统。       二、运行系统 运行刚才编译好的ARM64版本的Linux系统。 运行run_debian_arm64.sh脚本,输入run参数即可。   $./run_debian_arm64.sh run 系统登录名: benshushu 密码:123 切换到root用户:su root       三、实验平台初体验 一、在线安装软件包 QEMU虚拟机可以通过VirtIO-Net技术来生成一个虚拟的网卡,通过NAT网络桥接技术和主机进行网络共享。使用ifconfig命令来检查网络配置。   但这里我发现ifconfig不起作用,系统返回 bash: ifconfig: command not found 错误说明系统无法找到 ifconfig 命令。这可能是因为最新版本的Linux发行版中已经不推荐使用ifconfig命令了,而应该使用ip命令来替代。   可以尝试使用以下命令来代替ifconfig:   ip addr show       可以看到生成了一个名为enp0s1的网卡设备,分配的IP地址为:10.0.2.15。   提前更改系统时间   root@ubuntu:~# date -s 2020-03-29 #假设最新日期是2020年3月29日Sun Mar 29 00:00:00 UTC 2020   这里需要提前导入新的 GPG 密钥,使用以下命令:   sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 0E98404D386FA1D9   通过apt update命令来更新Debian系统的软件仓库。   apt update       使用apt install命令来安装软件包。比如,可以在线安装gcc。   root@ubuntu:~# apt install gcc   二、共享文件夹 在主机和QEMU虚拟机之间共享文件。 主机和QEMU虚拟机可以通过NET_9P技术进行文件共享,这个需要QEMU虚拟机和主机的Linux内核都使能NET_9P的内核模块。   本实验平台已经支持主机和QEMU虚拟机的共享文件,可以通过如下简单方法来测试。   复制一个文件到runninglinuxkernel_5.0/kmodules目录下面。   $ cp test.c runninglinuxkernel_5.0/kmodules   启动QEMU虚拟机之后,首先检查一下/mnt目录是否有test.c文件。   root@ubuntu:/# cd /mnt root@ubuntu:/mnt # ls README test.c   我们在后续的实验中会经常利用这个特性,比如把编译好的内核模块或者内核模块源代码放入QEMU虚拟机。   四、编译加载模块及单步调试内核   一、编译加载内核模块   在本书中,常常需要编译内核模块并放入QEMU虚拟机中以加载内核模块。我们这里提供两种编译内核模块的方法。一种是在主机上交叉编译,然后共享到QEMU虚拟机,另一种方法是在QEMU虚拟机里本地编译。   首先,在QEMU虚拟机中安装必要的软件包。   root@ubuntu: # apt install build-essential   在QEMU虚拟机里编译内核模块时需要指定QEMU虚拟机本地的内核路径,例如BASEINCLUDE变量指向了本地内核路径。   “/lib/modules/$(shell uname -r)/build”是一个链接文件,用来指向具体内核源代码路径,通常是指向已经编译过的内核路径。   BASEINCLUDE ?= /lib/modules/$(shell uname -r)/build   编译内核模块,下面以最简单的hello_world内核模块程序为例。   cd hello_world #进入内核模块代码所在的目录 export ARCH=arm64 export CROSS_COMPILE=aarch64-linux-gnu- root@ubuntu:/mnt/hello_world# make make -C /lib/modules/5.0.0+/build M=/mnt/hello_world modules; make[1]: Entering directory '/usr/src/linux' CC [M] /mnt/hello_world/test-1.o LD [M] /mnt/hello_world/test.o Building modules, stage 2. MODPOST 1 modules CC /mnt/hello_world/test.mod.o LD [M] /mnt/hello_world /test.ko make[1]: Leaving directory '/usr/src/linux' root@ubuntu: /mnt/hello_world#   提前编写modules_helloworld.c、Makefile文件 #include <linux/init.h> #include <linux/module.h> int hello_init(void) { printk("enter hello world!\n"); return 0; } void hello_exit(void) { printk("exit hello world!\n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");   BASEINCLUDE ?= /lib/modules/$(shell uname -r)/build CURRENT_PATH := $(shell pwd) obj-m := modules_helloworld.o build: kernel_modules kernel_modules: $(MAKE) -C $(BASEINCLUDE) M=$(CURRENT_PATH) modules clean: $(MAKE) -C $(BASEINCLUDE) M=$(CURRENT_PATH) clean   编译       运行,这里可能insmod不成功,而insmod和modprobe命令的可执行文件都可以在/sbin目录中找到,这意味着这两个命令应该已经正确安装在系统中了。   根据您之前尝试过的加载模块的命令和报错信息,看起来问题可能是命令使用方式不正确。   使用以下命令加载模块:   /sbin/insmod modules_helloworld.ko       二、单步调试ARM64 Linux内核 Ubuntu提前安装gdb-multiarch   sudo apt install gdb-multiarch   再进入实验平台时,加上启动GDB   ./run_debian_arm64.sh run debug   上述脚本会运行如下命令。   -S:表示QEMU虚拟机会冻结CPU,直到在远程的GDB中输入相应控制命令。 -s:表示在1234端口接受GDB的调试连接。   接下来,在另外一个超级终端中启动GDB。   linux@linux-virtual-machine:~$ cd Desktop/work/runninglinuxkernel_5.0/ linux@linux-virtual-machine:~/Desktop/work/runninglinuxkernel_5.0$ gdb-multiarch --tui vmlinux   此时会来到GDB端口:     (gdb) set architecture aarch64 //设置aarch64架构 (gdb) target remote localhost:1234 //通过1234端口远程连接到QEMU虚拟机 (gdb) b start_kernel //在内核的start_kernel处设置断点 (gdb) c   如图所示,GDB开始接管Linux内核运行,并且到断点处暂停,这时即可使用GDB命令来调试内核。   五、总结   总之,这部分实验还是比较轻松的,实现了QEMU+Debian实验平台 的搭建和编译加载模块及单步调试内核。 以后可以基于此来调试Linux,当系统发生段错误或崩溃,都可以使用GDB来排查错误了,是工作中非常实用的技巧!  

  • 2024-04-20
  • 回复了主题帖: 阅读打卡终点站:安全漏洞分析——《奔跑吧Linux内核2:调试与案例分析》

    1.请简述高速侧信道攻击的原理。 答:测信道攻击是密码学中常见的暴力攻击技术。它是针对加密电子设备在运行过程中的消耗、功率消耗或电磁辐射之类的测信道信息泄露而对加密设备进行攻击的方法。 2.在CPU熔断漏洞攻击中,攻击者在用户态访问内核空间时会发生异常,攻击者进程会被终止,这样导致后续无法进行侧信道攻击,那么如何解决这个问题? 答:我们可以在攻击者进程中设置异常处理信号。当发生异常时调用该信号的回调函数,从而抑制异常导致的攻击过程失败。 3.请简述熔断漏洞攻击的原理和过程。 答:CPU熔断漏洞巧妙地利用了现代处理器中乱序执行的副作用进行侧信道攻击,破坏了基于地址空间隔离的安全机制,使得用户态程序可以读出内核空间的数据,包括个人私有数据和密码等。熔断漏洞和计算机架构知识紧密相关,特别是乱序执行、异常处理以及地址空间。

最近访客

< 1/4 >

统计信息

已有48人来访过

  • 芯积分:258
  • 好友:1
  • 主题:20
  • 回复:35

留言

你需要登录后才可以留言 登录 | 注册


现在还没有留言