From 4ac5b5ef0a31166e66c4395ee025c3df13c54048 Mon Sep 17 00:00:00 2001 From: YuzukiTsuru Date: Thu, 21 Mar 2024 14:39:50 +0800 Subject: [PATCH] fix sidebar --- docs/README.md | 2309 +++++++++++++++++++++++------------------------- 1 file changed, 1128 insertions(+), 1181 deletions(-) diff --git a/docs/README.md b/docs/README.md index 3c57a6dee..8693d6edf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -423,144 +423,6 @@ C:\System> adb pull /mnt/UDISK/badapple.mp4 # 将 /mnt/UDISK/badapple.mp4 下 - Support mjpeg and h264 - 使用 win32diskimager 刷写到SD卡 -# 支持的系统与开发 SDK - -## Tina-SDK系统 - -- 此套构建系统基于全志单核 Arm Cortex-A7 SoC,搭载了 RISC-V 内核的V851s 芯片,适配了Tina 5.0主线版本,是专为智能 IP 摄像机设计的,支持人体检测和穿越报警等功能。 - -![](assets/post/README/OpenRemoved_Tina_Linux_System_software_development_Guide-3-1.jpg) - -* SDK 下载解压操作步骤请参考 Tina-SDK开发章节内容。 -* TinaSDK开发参考文档站点 https://tina.100ask.net/ - * 第一部分介绍了Tina-SDK源码的使用方式,包含源码目录功能,编译打包等命令。 - * 第二部分介绍了Bootloader相关的内容,主要包含uboot相关的使用说明。 - * 第三部分介绍了Linux所有的设备驱动开发的详细说明。 - * 第四部分介绍了Linux驱动之上的各类组件包库等的开发说明。 - * 第五部分介绍了Linux系统的相关操作,主要包含存储支持 打包 调试 优化等 - * 第六部分支持了一些应用demo示例,如LVGL GST等常用且较为丰富的综合项目 - -## SyterKit系统 - -* SyterKit源码位置: https://github.com/YuzukiHD/SyterKit - -SyterKit 是一个纯裸机框架,用于 TinyVision 或者其他 v851se/v851s/v851s3/v853 等芯片的开发板,SyterKit 使用 CMake 作为构建系统构建,支持多种应用与多种外设驱动。同时 SyterKit 也具有启动引导的功能,可以替代 U-Boot 实现快速启动(标准 Linux6.7 主线启动时间 1.02s,相较于传统 U-Boot 启动快 3s)。 - -目前已经支持如下功能 - -| 名称 | 功能 | 路径 | -| --------------- | ------------------------------------------------------------ | --------------------- | -| hello world | 最小程序示例,打印 Hello World | `app/hello_world` | -| init dram | 初始化串行端口和 DRAM | `app/init_dram` | -| read chip efuse | 读取芯片 efuse 信息 | `app/read_chip_efuse` | -| read chipsid | 读取芯片的唯一 ID | `app/read_chipsid` | -| load e907 | 读取 e907 核心固件,启动 e907 核心,并使用 V851s 作为大型 RISC-V 微控制器(E907 @ 600 MHz,64MB 内存) | `app/load_e907` | -| syter boot | 替代 U-Boot 的引导函数,为 Linux 启用快速系统启动 | `app/syter_boot` | -| syter amp | 读取 e907 核心固件,启动 e907 核心,加载内核,并在 e907 和 a7 系统上同时运行 Linux,系统是异构集成运行的 | `app/syter_amp` | -| fdt parser | 读取设备树二进制文件并解析打印输出 | `app/fdt_parser` | -| fdt cli | 使用支持 uboot fdt 命令的 CLI 读取设备树二进制文件 | `app/fdt_cli` | -| syter bootargs | 替代 U-Boot 引导,为 Linux 启用快速系统启动,支持在 CLI 中更改启动参数 | `app/syter_bootargs` | -| cli test | 测试基本 CLI 功能 | `app/cli_test` | - -## Linux Kernel - -基于Linus主线LinuxKernel 支持 tinyvision单板及驱动模块,支持多个内核版本,不同的内核版本支持的功能特性也不同,可以通过下述列表查看。 - -* 源码所在位置 https://github.com/YuzukiHD/TinyVision/tree/main/kernel/ - -| Kernel Version | Target ON | Core | Path | -| ------------------ | --------------------------------------- | -------------- | ------------------- | -| 4.9.191 | CV, Camera, NPU, MP, Video Encode, RTSP | Cortex-A7 Core | `kernel\linux-4.9` | -| 5.15.y | IoT, NPU, Router | Cortex-A7 Core | `kernel\linux-5.15` | -| 6.1.y | IoT | Cortex-A7 Core | `kernel\linux-6.1` | -| Mainline Linux 6.7 | Mainline | Cortex-A7 Core | `kernel\linux-6.7` | - - -## RTOS Kernel - -| Kernel Version | Target ON | Core | Path | -| -------------- | ----------------------- | -------------- | ----------------- | -| RT-Thread | Real-Time Control, Fast | RISC-V E907 | `kernel\rtos` | -| SyterKit | Baremetal ASM Code | Cortex-A7 Core | `kernel\SyterKit` | - -## Openwrt系统 - -TinyVision自带百兆网口接口+摄像头接口支持,支持 Current stable series: OpenWrt 23.05 系统,可以做一个 轻量级的IPC摄像头,里面运行主线系统,选择合适的内核版本 一键 编译生成系统镜像。 - -* openwrt-23.05源码: https://github.com/YuzukiHD/OpenWrt/tree/openwrt-23.05 -* OpenWrt-23.05目录结构,OpenWrt-23.05.tar.gz 压缩包 md5值 2b10a86405aa4d045bc2134e98d3f6d8 请确保压缩包文件一致性。 - -``` bash -ubuntu@ubuntu1804:~/$ md5sum OpenWrt-23.05.tar.gz -ubuntu@ubuntu1804:~/$ tree -L 1 -. -├── bin -├── BSDmakefile -├── build_dir -├── config -├── Config.in -├── COPYING -├── dl -├── feeds -├── feeds.conf.default -├── include -├── key-build -├── key-build.pub -├── key-build.ucert -├── key-build.ucert.revoke -├── LICENSES -├── Makefile -├── package -├── README.md -├── rules.mk -├── scripts -├── staging_dir -├── target -├── tmp -├── toolchain -└── tools - -14 directories, 11 files - -``` - -## Buildroot系统 - -buildroot系统是一套基于Makefile管理的构建系统框架 - -- 网盘链接:https://pan.baidu.com/s/19QFDR_ssy6SJeRMzm5lVDw?pwd=b4nh 提取码:b4nh - -``` ba -ubuntu@ubuntu1804:~/buildroot-2023.02.8$ tree -L 1 -. -├── arch -├── board -├── boot -├── CHANGES -├── Config.in -├── Config.in.legacy -├── configs -├── COPYING -├── defconfig -├── DEVELOPERS -├── dl -├── docs -├── fs -├── linux -├── Makefile -├── Makefile.legacy -├── output -├── package -├── README -├── support -├── system -├── toolchain -└── utils - -15 directories, 9 files -ubuntu@ubuntu1804:~/buildroot-2023.02.8$ -``` - # 安装并配置开发环境 ## 获取虚拟机系统 @@ -643,6 +505,21 @@ sudo apt install open-vm-tools-desktop # Tina 4.0 Linux 开发 +## Tina-SDK系统 + +- 此套构建系统基于全志单核 Arm Cortex-A7 SoC,搭载了 RISC-V 内核的V851s 芯片,适配了Tina 5.0主线版本,是专为智能 IP 摄像机设计的,支持人体检测和穿越报警等功能。 + +![](assets/post/README/OpenRemoved_Tina_Linux_System_software_development_Guide-3-1.jpg) + +* SDK 下载解压操作步骤请参考 Tina-SDK开发章节内容。 +* TinaSDK开发参考文档站点 https://tina.100ask.net/ + * 第一部分介绍了Tina-SDK源码的使用方式,包含源码目录功能,编译打包等命令。 + * 第二部分介绍了Bootloader相关的内容,主要包含uboot相关的使用说明。 + * 第三部分介绍了Linux所有的设备驱动开发的详细说明。 + * 第四部分介绍了Linux驱动之上的各类组件包库等的开发说明。 + * 第五部分介绍了Linux系统的相关操作,主要包含存储支持 打包 调试 优化等 + * 第六部分支持了一些应用demo示例,如LVGL GST等常用且较为丰富的综合项目 + Tina 4.0 Linux是基于Linux内核开发的针对智能硬件类产品的嵌入式软件系统。Tina Linux基于OpenWrt-14.07 版本的软件开发包,包含了 Linux 系统开发用到的内核源码、驱动、工具、系统中间件与应用程序包。(该系统较老,且学习难度较大,建议使用Tina 5.0 Linux 开发) **如果需要开发 Tina 4.0,请使用 Ubuntu 18.04 以下系统,高版本系统不支持!** @@ -1537,582 +1414,767 @@ U-Boot 默认配置的是使用 SDC2 也就是 TinyVision 的 SD-NAND 刷写固 出现 `try card 0` 开始下载到 TF 卡内 +# Tina Linux NPU 开发 +TinyVision V851s 使用 OpenCV + NPU 实现 Mobilenet v2 物体识别。上一篇已经介绍了如何使用 TinyVision 与 OpenCV 开摄像头,本篇将使用已经训练完成并且转换后的模型来介绍对接 NPU 实现物体识别的功能。 -# OpenWrt 编译开发 +## MobileNet V2 +MobileNet V2是一种轻量级的卷积神经网络(CNN)架构,专门设计用于在移动设备和嵌入式设备上进行计算资源受限的实时图像分类和目标检测任务。 -* 源码存放在百度网盘: https://pan.baidu.com/s/1a0uS7kqXiEdKFFgIJ3HF5g?pwd=qm83 提取码:qm83,打包的源码只是提供加速下载,Git上源码实时更新建议使用Github的源码,实在下载不下来再用这个。 -* git仓库位置 https://github.com/YuzukiHD/OpenWrt/ +以下是MobileNet V2的一些关键特点和创新之处: -## 编译 +1. Depthwise Separable Convolution(深度可分离卷积):MobileNet V2使用了深度可分离卷积,将标准卷积分解为两个步骤:depthwise convolution(深度卷积)和pointwise convolution(逐点卷积)。这种分解方式可以显著减少计算量和参数数量,从而提高模型的轻量化程度。 -获取镜像后,进行解压缩,建议使用百度网盘版本,因为网络问题,可能导致某些软件包无法正常下载,编译报错。 +2. Inverted Residuals with Linear Bottlenecks(带线性瓶颈的倒残差结构):MobileNet V2引入了带有线性瓶颈的倒残差结构,以增加模型的非线性表示能力。这种结构在每个残差块的中间层采用较低维度的逐点卷积来减少计算量,并使用扩张卷积来增加感受野,使网络能够更好地捕捉图像中的细节和全局信息。 -解压压缩包后,执行 make menuconfig 进入到配置界面, +3. Width Multiplier(宽度乘数):MobileNet V2提供了一个宽度乘数参数,可以根据计算资源的限制来调整模型的宽度。通过减少每个层的通道数,可以进一步减小模型的体积和计算量,适应不同的设备和应用场景。 -```shell -ubuntu@ubuntu1804:~/OpenWrt$ make menuconfig -``` +4. Linear Bottlenecks(线性瓶颈):为了减少非线性激活函数对模型性能的影响,MobileNet V2使用线性激活函数来缓解梯度消失问题。这种线性激活函数在倒残差结构的中间层中使用,有助于提高模型的收敛速度和稳定性。 -参考下图红框所示,三个选项选中 为 TinyVision开发板,保证一模一样。 -![image-20231216174136154](assets/post/README/Openwrt-config.jpg) -选中完成后,保存退出,继续执行make 命令即可开始编译。 +总体而言,MobileNet V2通过深度可分离卷积、倒残差结构和宽度乘数等技术,实现了较高的模型轻量化程度和计算效率,使其成为在资源受限的移动设备上进行实时图像分类和目标检测的理想选择。 -```shell -ubuntu@ubuntu1804:~/OpenWrt$ make +## NPU -``` +V851s 芯片内置一颗 NPU,其处理性能为最大 0.5 TOPS 并有 128KB 内部高速缓存用于高速数据交换 -如果你不想使用压缩包,而是从头获取源码,需要在 make menuconfig选中开发板之前 执行 ` ./scripts/feeds update -a ` 命令检查远端仓库和本地仓库的差异进行更新,之后执行 `./scripts/feeds install -a` 来安装远端更新。 +### NPU 系统架构 -```shell -ubuntu@ubuntu1804:~/OpenWrt$ ./scripts/feeds update -a -Updating feed 'packages' from 'https://github.com/openwrt/packages.git;openwrt-23.05' ... -remote: Enumerating objects: 101, done. -remote: Counting objects: 100% (101/101), done. -remote: Compressing objects: 100% (44/44), done. -remote: Total 68 (delta 44), reused 45 (delta 21), pack-reused 0 -Unpacking objects: 100% (68/68), done. -From https://github.com/openwrt/packages - 421e2c75a..d26bbd792 openwrt-23.05 -> origin/openwrt-23.05 -Updating 421e2c75a..d26bbd792 -Fast-forward - admin/btop/Makefile | 7 ++++--- - lang/rust/Makefile | 4 ++-- - lang/rust/patches/0001-Update-xz2-and-use-it-static.patch | 14 +++++++------- - lang/rust/patches/0002-rustc-bootstrap-cache.patch | 10 +++++----- - lang/rust/patches/0003-bump-libc-deps-to-0.2.146.patch | 28 ---------------------------- - lang/rust/rust-values.mk | 6 ++++++ - net/adblock-fast/Makefile | 2 +- - net/adblock-fast/files/etc/init.d/adblock-fast | 2 +- - net/dnsproxy/Makefile | 4 ++-- - net/sing-box/Makefile | 9 ++------- - net/travelmate/Makefile | 2 +- - net/travelmate/files/travelmate.sh | 12 +++++++++++- - net/uspot/Makefile | 10 ++++++---- - net/v2ray-geodata/Makefile | 12 ++++++------ - net/v2raya/Makefile | 6 +++--- - 15 files changed, 57 insertions(+), 71 deletions(-) -Updating feed 'luci' from 'https://github.com/openwrt/luci.git;openwrt-23.05' ... -remote: Enumerating objects: 49, done. -remote: Counting objects: 100% (49/49), done. -remote: Compressing objects: 100% (18/18), done. -remote: Total 31 (delta 12), reused 25 (delta 6), pack-reused 0 -Unpacking objects: 100% (31/31), done. -From https://github.com/openwrt/luci - 11a1a43..fa4b280 openwrt-23.05 -> origin/openwrt-23.05 -Updating 11a1a43..fa4b280 -Fast-forward - applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/rules.js | 3 +++ - applications/luci-app-https-dns-proxy/Makefile | 2 +- - applications/luci-app-https-dns-proxy/htdocs/luci-static/resources/view/https-dns-proxy/overview.js | 8 ++++---- - themes/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/cascade.css | 13 +++++++++++++ - 4 files changed, 21 insertions(+), 5 deletions(-) -Updating feed 'routing' from 'https://github.com/openwrt/routing.git;openwrt-23.05' ... -Already up to date. -Updating feed 'telephony' from 'https://github.com/openwrt/telephony.git;openwrt-23.05' ... -Already up to date. -Create index file './feeds/packages.index' -Collecting package info: done -Create index file './feeds/luci.index' -Collecting package info: done -Create index file './feeds/routing.index' -Create index file './feeds/telephony.index' -ubuntu@ubuntu1804:~/Downloads/TinyVision/OpenWrt$ ./scripts/feeds install -a -Installing all packages from feed packages. -Installing all packages from feed luci. -Installing all packages from feed routing. -Installing all packages from feed telephony. -ubuntu@ubuntu1804:~/OpenWrt$ +NPU 的系统架构如下图所示: -``` +![image-20220712100607889](assets/post/README/image-20220712100607889.png) -## 烧写镜像 +上层的应用程序可以通过加载模型与数据到 NPU 进行计算,也可以使用 NPU 提供的软件 API 操作 NPU 执行计算。 -系统编译完成后,镜像输出在 `build_dir/target-arm_cortex-a7+neon-vfpv4_musl_eabi/linux-yuzukihd_v851se/tmp/` 目录下,名称为 `openwrt-yuzukihd-v851se-yuzuki_tinyvision-squashfs-sysupgrade.img.gz` 需要先使用 `tar -xvf` 进行解压缩,之后 使用 `dd if` 命令 完整写入sd卡设备,或者 使用 `wind32diskimage`工具。 或者使用 `balenaEtcher` 等 进行烧录。 +NPU包括三个部分:可编程引擎(Programmable Engines,PPU)、神经网络引擎(Neural Network Engine,NN)和各级缓存。 -# Debian 12 构建与编译 +可编程引擎可以使用 EVIS 硬件加速指令与 Shader 语言进行编程,也可以实现激活函数等操作。 -## 构建 SyterKit 作为 Bootloader +神经网络引擎包含 NN 核心与 Tensor Process Fabric(TPF,图中简写为 Fabric) 两个部分。NN核心一般计算卷积操作, Tensor Process Fabric 则是作为 NN 核心中的高速数据交换的通路。算子是由可编程引擎与神经网络引擎共同实现的。 -SyterKit 是一个纯裸机框架,用于 TinyVision 或者其他 v851se/v851s/v851s3/v853 等芯片的开发板,SyterKit 使用 CMake 作为构建系统构建,支持多种应用与多种外设驱动。同时 SyterKit 也具有启动引导的功能,可以替代 U-Boot 实现快速启动 +NPU 支持 UINT8,INT8,INT16 三种数据格式。 -### 获取 SyterKit 源码 +### NPU 模型转换 -SyterKit 源码位于GitHub,可以前往下载。 +NPU 使用的模型是 NPU 自定义的一类模型结构,不能直接将网络训练出的模型直接导入 NPU 进行计算。这就需要将网络训练出的转换模型到 NPU 的模型上。 -```shell -git clone https://github.com/YuzukiHD/SyterKit.git -``` +NPU 的模型转换步骤如下图所示: -### 从零构建 SyterKit +![image-20220712113105463](assets/post/README/image-20220712112951142.png) -构建 SyterKit 非常简单,只需要在 Linux 操作系统中安装配置环境即可编译。SyterKit 需要的软件包有: +NPU 模型转换包括准备阶段、量化阶段与验证阶段。 -- `gcc-arm-none-eabi` -- `CMake` +#### 准备阶段 -对于常用的 Ubuntu 系统,可以通过如下命令安装 +首先我们把准备好模型使用工具导入,并创建配置文件。 -```shell -sudo apt-get update -sudo apt-get install gcc-arm-none-eabi cmake build-essential -y -``` +这时候工具会把模型导入并转换为 NPU 所使用的网络模型、权重模型与配置文件。 -然后新建一个文件夹存放编译的输出文件,并且进入这个文件夹 +配置文件用于对网络的输入和输出的参数进行描述以及配置。这些参数包括输入/输出 tensor 的形状、归一化系数 (均值/零点)、图像格式、tensor 的输出格式、后处理方式等等。 -```shell -mkdir build -cd build -``` +#### 量化阶段 -然后运行命令编译 SyterKit +由于训练好的神经网络对数据精度以及噪声的不敏感,因此可以通过量化将参数从浮点数转换为定点数。这样做有两个优点: -```shell -cmake .. -make -``` +(1)减少了数据量,进而可以使用容量更小的存储设备,节省了成本; -![f6cd8396-6b9e-4171-a32f-b6e908fa1fb9-image.png](assets/post/README/1702729920306-f6cd8396-6b9e-4171-a32f-b6e908fa1fb9-image.png) - -编译后的可执行文件位于 `build/app` 中,这里包括 SyterKit 的多种APP可供使用。 +(2)由于数据量减少,浮点转化为定点数也大大降低了系统的计算量,也提高了计算的速度。 -![ecd7330e-1281-4296-9de7-0433e12fef2f-image.png](assets/post/README/1702729933404-ecd7330e-1281-4296-9de7-0433e12fef2f-image.png) +但是量化也有一个致命缺陷——会导致精度的丢失。 -这里我们使用的是 `syter_boot` 作为启动引导。进入 syter_boot 文件夹,可以看到这些文件 +由于浮点数转换为定点数时会大大降低数据量,导致实际的权重参数准确度降低。在简单的网络里这不是什么大问题,但是如果是复杂的多层多模型的网络,每一层微小的误差都会导致最终数据的错误。 -![d631adb8-9d69-4f38-99f4-f080a3d04cc4-image.png](assets/post/README/1702729955121-d631adb8-9d69-4f38-99f4-f080a3d04cc4-image.png) +那么,可以不量化直接使用原来的数据吗?当然是可以的。 -由于 TinyVision 是 TF 卡启动,所以我们需要用到 `syter_boot_bin_card.bin` +但是由于使用的是浮点数,无法将数据导入到只支持定点运算的 NN 核心进行计算,这就需要可编程引擎来代替 NN 核进行计算,这样可以大大降低运算效率。 -![0bee1188-3372-4a0a-94c3-5ae19322eab3-image.png](assets/post/README/1702729964449-0bee1188-3372-4a0a-94c3-5ae19322eab3-image.png) +另外,在进行量化过程时,不仅对参数进行了量化,也会对输入输出的数据进行量化。如果模型没有输入数据,就不知道输入输出的数据范围。这时候我们就需要准备一些具有代表性的输入来参与量化。这些输入数据一般从训练模型的数据集里获得,例如图片数据集里的图片。 -## 编译 Linux-6.1 内核 +另外选择的数据集不一定要把所有训练数据全部加入量化,通常我们选择几百张能够代表所有场景的输入数据就即可。理论上说,量化数据放入得越多,量化后精度可能更好,但是到达一定阈值后效果增长将会非常缓慢甚至不再增长。 -由于 Debian 12 配套的内核是 Linux 6.1 LTS,所以这里我们选择构建 Linux 6.1 版本内核。 +#### 验证阶段 -### 搭建编译环境 +由于上一阶段对模型进行了量化导致了精度的丢失,就需要对每个阶段的模型进行验证,对比结果是否一致。 -安装一些必要的安装包 +首先我们需要使用非量化情况下的模型运行生成每一层的 tensor 作为 Golden tensor。输入的数据可以是数据集中的任意一个数据。然后量化后使用预推理相同的数据再次输出一次 tensor,对比这一次输出的每一层的 tensor 与 Golden tensor 的差别。 -```plaintext -sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf build-essential libncurses5-dev zlib1g-dev gawk flex bison quilt libssl-dev xsltproc libxml-parser-perl mercurial bzr ecj cvs unzip lsof -``` +如果差别较大可以尝试更换量化模型和量化方式。差别不大即可使用 IDE 进行仿真。也可以直接部署到 V851s 上进行测试。 -### 获取内核源码 +此时测试同样会输出 tensor 数据,对比这一次输出的每一层的 tensor 与 Golden tensor 的差别,差别不大即可集成到 APP 中了。 -内核源码托管在 Github 上,可以直接获取到,这里使用 `--depth=1` 指定 git 深度为 1 加速下载。 +### NPU 的开发流程 -```plaintext -git clone http://github.com/YuzukiHD/TinyVision --depth=1 -``` +NPU 开发完整的流程如下图所示: -然后进入内核文件夹 +![image-20240126194601436](assets/post/README/image-20240126194601436.png) -```plaintext -cd kernel/linux-6.1 -``` +#### 模型训练 -### 配置内核选项 +在模型训练阶段,用户根据需求和实际情况选择合适的框架(如Caffe、TensorFlow 等)使用数据集进行训练得到符合需求的模型,此模型可称为预训练模型。也可直接使用已经训练好的模型。V851s 的 NPU 支持包括分类、检测、跟踪、人脸、姿态估计、分割、深度、语音、像素处理等各个场景90 多个公开模型。 -应用 defconfig +#### 模型转换 -```plaintext -CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm tinyvision_defconfig -``` +在模型转化阶段,通过Acuity Toolkit 把预训练模型和少量训练数据转换为NPU 可用的模型NBG文件。 +一般步骤如下: -进入 `menuconfig` 配置选项 +1. 模型导入,生成网络结构文件、网络权重文件、输入描述文件和输出描述文件。 +2. 模型量化,生成量化描述文件和熵值文件,可改用不同的量化方式。 +3. 仿真推理,可逐一对比float 和其他量化精度的仿真结果的相似度,评估量化后的精度是否满足要求。 +4. 模型导出,生成端侧代码和*.nb 文件,可编辑输出描述文件的配置,配置是否添加后处理节点等。 -```plaintext -CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm menuconfig -``` +#### 模型部署及应用开发 -进入 `General Setup ->`,选中 `Control Group Support` +在模型部署阶段,就是基于VIPLite API 开发应用程序实现业务逻辑。 -![image-20231221104449523](assets/post/README/image-20231221104449523.png) +## OpenCV + NPU 源码解析 -![image-20231221122711591](assets/post/README/image-20231221122711591.png) +完整的代码已经上传Github开源,前往以下地址:https://github.com/YuzukiHD/TinyVision/tree/main/tina/openwrt/package/thirdparty/vision/opencv_camera_mobilenet_v2_ssd/src -前往 `File Systems` 找到 `FUSE (Filesystem in Userspace) support` +**【[注意,运行这里的DEMO请先移植LCD驱动!! ](#lcd-模组驱动)】** -![image-20231221104607368](assets/post/README/image-20231221104607368.png) +否则报错 `Unsupported depth of framebuffer` -前往 `File Systems` 找到 `Inotify support for userspace` +![image-20240320180529300](assets/post/README/image-20240320180529300.png) -![image-20231221122848948](assets/post/README/image-20231221122848948.png) +### Mobilenet v2 前处理 -编译内核 +```c +void get_input_data(const cv::Mat& sample, uint8_t* input_data, int input_h, int input_w, const float* mean, const float* scale){ + cv::Mat img; + if (sample.channels() == 1) + cv::cvtColor(sample, img, cv::COLOR_GRAY2RGB); + else + cv::cvtColor(sample, img, cv::COLOR_BGR2RGB); + cv::resize(img, img, cv::Size(input_h, input_w)); + uint8_t* img_data = img.data; + /* nhwc to nchw */ + for (int h = 0; h < input_h; h++) { + for (int w = 0; w < input_w; w++) { + for (int c = 0; c < 3; c++) { + int in_index = h * input_w * 3 + w * 3 + c; + int out_index = c * input_h * input_w + h * input_w + w; + input_data[out_index] = (uint8_t)(img_data[in_index]); //uint8 + } + } + } +} -```plaintext -CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm +uint8_t *mbv2_ssd_preprocess(const cv::Mat& sample, int input_size, int img_channel) { + const float mean[3] = {127, 127, 127}; + const float scale[3] = {0.0078125, 0.0078125, 0.0078125}; + int img_size = input_size * input_size * img_channel; + uint8_t *tensor_data = NULL; + tensor_data = (uint8_t *)malloc(1 * img_size * sizeof(uint8_t)); + get_input_data(sample, tensor_data, input_size, input_size, mean, scale); + return tensor_data; +} ``` -## 使用 debootstrap 构建 debian rootfs +这段C++代码是用于对输入图像进行预处理,以便输入到MobileNet V2 SSD模型中进行目标检测。 -### 准备环境,依赖 +1. `get_input_data`函数: + - 该函数对输入的图像进行预处理,将其转换为适合MobileNet V2 SSD模型输入的格式。 + - 首先,对输入图像进行通道格式的转换,确保图像通道顺序符合模型要求(RGB格式)。 + - 然后,将图像大小调整为指定的输入尺寸(`input_h * input_w`)。 + - 最后,将处理后的图像数据按照特定顺序(NCHW格式)填充到`input_data`数组中,以便作为模型的输入数据使用。 -下载安装依赖环境 +2. `mbv2_ssd_preprocess`函数: + - 该函数是对输入图像进行 MobileNet V2 SSD 模型的预处理,并返回处理后的数据。 + - 在函数内部,首先定义了图像各通道的均值(mean)和缩放比例(scale)。 + - 然后计算了输入图像的总大小,并分配了相应大小的内存空间用于存储预处理后的数据。 + - 调用了`get_input_data`函数对输入图像进行预处理,将处理后的数据存储在`tensor_data`中,并最终返回该数据指针。 -```plaintext -sudo apt install debootstrap qemu qemu-user-static qemu-system qemu-utils qemu-system-misc binfmt-support dpkg-cross debian-ports-archive-keyring --no-install-recommends -``` +总的来说,这段代码的功能是将输入图像进行预处理,以适应MobileNet V2 SSD模型的输入要求,并返回预处理后的数据供模型使用。同时需要注意,在使用完`tensor_data`后,需要在适当的时候释放相应的内存空间,以避免内存泄漏问题。 -生成目标镜像,配置环境,这里我们生成一个 1024M 的镜像文件用于存放 rootfs +### Mobilenet v2 后处理 -```shell -dd if=/dev/zero of=rootfs.img bs=1M count=1024 -mkdir rootfs -mkfs.ext4 rootfs.img -sudo mount rootfs.img rootfs -``` +这部分分为来讲: -### 开始构建基础 rootfs +```cpp +// 比较函数,用于按照分数对Bbox_t对象进行排序 +bool comp(const Bbox_t &a, const Bbox_t &b) { + return a.score > b.score; +} -这里我们选择最新的 debian12 (bookwarm) 作为目标镜像,使用清华源来构建,输出到目标目录 rootfs_data 文件夹中。新版本的 debootstrap 只需要运行一次即可完成两次 stage 的操作,相较于老版本方便许多。 +// 计算两个框之间的交集面积 +static inline float intersection_area(const Bbox_t& a, const Bbox_t& b) { + // 将框表示为cv::Rect_对象 + cv::Rect_ rect_a(a.xmin, a.ymin, a.xmax-a.xmin, a.ymax-a.ymin); + cv::Rect_ rect_b(b.xmin, b.ymin, b.xmax-b.xmin, b.ymax-b.ymin); + + // 计算两个矩形的交集 + cv::Rect_ inter = rect_a & rect_b; + + // 返回交集的面积 + return inter.area(); +} -```shell -sudo debootstrap --arch=armhf bookworm rootfs_data https://mirrors.tuna.tsinghua.edu.cn/debian/ +// 非极大值抑制算法(NMS) +static void nms_sorted_bboxes(const std::vector& bboxs, std::vector& picked, float nms_threshold) { + picked.clear(); + const int n = bboxs.size(); + + // 创建存储每个框面积的向量 + std::vector areas(n); + + // 计算每个框的面积并存储 + for (int i = 0; i < n; i++){ + areas[i] = (bboxs[i].xmax - bboxs[i].xmin) * (bboxs[i].ymax - bboxs[i].ymin); + } + + // 对每个框进行遍历 + for (int i = 0; i < n; i++) { + const Bbox_t& a = bboxs[i]; + int keep = 1; + + // 对已经选择的框进行遍历 + for (int j = 0; j < (int)picked.size(); j++) { + const Bbox_t& b = bboxs[picked[j]]; + + // 计算交集和并集面积 + float inter_area = intersection_area(a, b); + float union_area = areas[i] + areas[picked[j]] - inter_area; + + // 计算交并比 + if (inter_area / union_area > nms_threshold) + keep = 0; // 如果交并比大于阈值,则不选择该框 + } + + // 如果符合条件则选择该框,加入到结果向量中 + if (keep) + picked.push_back(i); + } +} ``` -![image-20231221093653561](assets/post/README/image-20231221093653561.png) +这段代码实现了目标检测中常用的非极大值抑制算法(NMS)。`comp`函数用于对`Bbox_t`对象按照分数进行降序排序。`intersection_area`函数用于计算两个框之间的交集面积。`nms_sorted_bboxes`函数是NMS算法的具体实现,它接受一个已经按照分数排序的框的向量`bboxs`,以及一个空的整数向量`picked`,用于存储保留下来的框的索引。`nms_threshold`是一个阈值,用于控制重叠度。 -看到 `I: Base system installed successfully.` 就是构建完成了 +算法的步骤如下: -![image-20231221094602269](assets/post/README/image-20231221094602269.png) +1. 清空存储结果的`picked`向量。 +2. 获取框的个数`n`,创建一个用于存储每个框面积的向量`areas`。 +3. 遍历每个框,计算其面积并存储到`areas`向量中。 +4. 对每个框进行遍历,通过计算交并比来判断是否选择该框。如果交并比大于阈值,则不选择该框。 +5. 如果符合条件,则选择该框,将其索引加入到`picked`向量中。 +6. 完成非极大值抑制算法,`picked`向量中存储了保留下来的框的索引。 -等待构建完成后,使用chroot进入到目录,这里编写一个挂载脚本方便挂载使用,新建文件 `ch-mount.sh` 并写入以下内容: +这个算法的作用是去除高度重叠的框,只保留得分最高的那个框,以减少冗余检测结果。 -```bash -#!/bin/bash +```c +cv::Mat detect_ssd(const cv::Mat& bgr, float **output) { + // 定义阈值和常数 + float iou_threshold = 0.45; + float conf_threshold = 0.5; + const int inputH = 300; + const int inputW = 300; + const int outputClsSize = 21; +#if MBV2_SSD + int output_dim_1 = 3000; +#else + int output_dim_1 = 8732; +#endif -function mnt() { - echo "MOUNTING" - sudo mount -t proc /proc ${2}proc - sudo mount -t sysfs /sys ${2}sys - sudo mount -o bind /dev ${2}dev - sudo mount -o bind /dev/pts ${2}dev/pts - sudo chroot ${2} -} + // 计算输出数据的大小 + int size0 = 1 * output_dim_1 * outputClsSize; + int size1 = 1 * output_dim_1 * 4; -function umnt() { - echo "UNMOUNTING" - sudo umount ${2}proc - sudo umount ${2}sys - sudo umount ${2}dev/pts - sudo umount ${2}dev + // 将输出数据转换为向量 + std::vector scores_data(output[0], &output[0][size0-1]); + std::vector boxes_data(output[1], &output[1][size1-1]); -} + // 获取分数和边界框的指针 + const float* scores = scores_data.data(); + const float* bboxes = boxes_data.data(); -if [ "$1" == "-m" ] && [ -n "$2" ] ; -then - mnt $1 $2 -elif [ "$1" == "-u" ] && [ -n "$2" ]; -then - umnt $1 $2 -else - echo "" - echo "Either 1'st, 2'nd or both parameters were missing" - echo "" - echo "1'st parameter can be one of these: -m(mount) OR -u(umount)" - echo "2'nd parameter is the full path of rootfs directory(with trailing '/')" - echo "" - echo "For example: ch-mount -m /media/sdcard/" - echo "" - echo 1st parameter : ${1} - echo 2nd parameter : ${2} -fi -``` + // 计算缩放比例 + float scale_w = bgr.cols / (float)inputW; + float scale_h = bgr.rows / (float)inputH; + bool pass = true; -然后赋予脚本执行的权限 + // 创建存储检测结果的向量 + std::vector BBox; -```shell -chmod 777 ch-mount.sh -``` + // 遍历每个框 + for(int i = 0; i < output_dim_1; i++) { + std::vector conf; + // 获取每个框的置信度 + for(int j = 0; j < outputClsSize; j++) { + conf.emplace_back(scores[i * outputClsSize + j]); + } + // 找到置信度最大的类别 + int max_index = std::max_element(conf.begin(), conf.end()) - conf.begin(); + // 如果类别不是背景类,并且置信度大于阈值,则选中该框 + if (max_index != 0) { + if(conf[max_index] < conf_threshold) + continue; + Bbox_t b; + // 根据缩放比例计算框的坐标和尺寸 + int left = bboxes[i * 4] * scale_w * 300; + int top = bboxes[i * 4 + 1] * scale_h * 300; + int right = bboxes[ i * 4 + 2] * scale_w * 300; + int bottom = bboxes[i * 4 + 3] * scale_h * 300; + // 确保坐标不超出图像范围 + b.xmin = std::max(0, left); + b.ymin = std::max(0, top); + b.xmax = right; + b.ymax = bottom; + b.score = conf[max_index]; + b.cls_idx = max_index; + BBox.emplace_back(b); + } + conf.clear(); + } -- 使用 `./ch-mount.sh -m rootfs_data` 挂载 -- 使用 `./ch-mount.sh -u rootfs_data` 卸载 + // 按照分数对框进行排序 + std::sort(BBox.begin(), BBox.end(), comp); -执行挂载,可以看到进入了 debian 的 rootfs + // 应用非极大值抑制算法,获取保留的框的索引 + std::vector keep_index; + nms_sorted_bboxes(BBox, keep_index, iou_threshold); -![image-20231221094725953](assets/post/README/image-20231221094725953.png) + // 创建存储框位置的向量 + std::vector bbox_per_frame; -配置系统字符集,选择 en_US 作为默认字符集 + // 遍历保留的框,绘制框和标签 + for(int i = 0; i < keep_index.size(); i++) { + int left = BBox[keep_index[i]].xmin; + int top = BBox[keep_index[i]].ymin; + int right = BBox[keep_index[i]].xmax; + int bottom = BBox[keep_index[i]].ymax; + int width = right - left; + int height = bottom - top; + int center_x = left + width / 2; + cv::rectangle(bgr, cv::Point(left, top), cv::Point(right, bottom), cv::Scalar(0, 0, 255), 1); + char text[256]; + sprintf(text, "%s %.1f%%", class_names[BBox[keep_index[i]].cls_idx], BBox[keep_index[i]].score * 100); + cv::putText(bgr, text, cv::Point(left, top), cv::FONT_HERSHEY_COMPLEX, 1, cv::Scalar(0, 255, 255), 1, 8, 0); + bbox_per_frame.emplace_back(left, top, width, height); + } -```shell -export LC_ALL=en_US.UTF-8 -apt-get install locales -dpkg-reconfigure locales + // 返回绘制了框和标签的图像 + return bgr; +} ``` -选择一个就可以 +这段代码主要用于处理模型的输出结果,将输出数据转换为向量,并计算缩放比例,然后创建一个向量来存储检测结果。 -![image-20231221095332517](assets/post/README/image-20231221095332517.png) +具体步骤如下: -直接 OK 下一步 +1. 定义了一些阈值和常数,包括IOU阈值(`iou_threshold`)、置信度阈值(`conf_threshold`)、输入图像的高度和宽度(`inputH`和`inputW`)、输出类别数量(`outputClsSize`)、输出维度(`output_dim_1`)。 +2. 计算输出数据的大小,分别为类别得分数据的大小(`size0`)和边界框数据的大小(`size1`)。 +3. 将输出数据转换为向量,分别为类别得分数据向量(`scores_data`)和边界框数据向量(`boxes_data`)。 +4. 获取类别得分和边界框的指针,分别为`scores`和`bboxes`。 +5. 计算图像的缩放比例,根据输入图像的尺寸和模型输入尺寸之间的比例计算得到。 +6. 创建一个向量`BBox`,用于存储检测结果。该向量的类型为`Bbox_t` +7. 遍历每一个框(共有`output_dim_1`个框)。 +8. 获取每一个框的各个类别的置信度,并将其存储在`conf`向量中。 +9. 找到置信度最大的类别,并记录其下标`max_index`。 +10. 如果最大置信度的类别不是背景类,并且置信度大于设定的阈值,则选中该框。 +11. 根据缩放比例计算框的坐标和尺寸,其中`left`、`top`、`right`和`bottom`分别表示框的左上角和右下角的坐标。 +12. 确保框的坐标不超出图像范围,并将目标框的信息(包括位置、置信度、类别等)存储在`Bbox_t`类型的变量`b`中。 +13. 将`b`加入到`BBox`向量中。 +14. 清空`conf`向量,为下一个框的检测做准备。 +15. 对所有检测到的目标框按照置信度从高到低排序; +16. 应用非极大值抑制算法,筛选出重叠度较小的目标框,并将保留的目标框的索引存储在`keep_index`向量中; +17. 遍历保留的目标框,对每个目标框进行绘制和标注; +18. 在图像上用矩形框标出目标框的位置和大小,并在矩形框内添加目标类别和置信度; +19. 将绘制好的目标框信息(包括左上角坐标、宽度和高度)存储在`bbox_per_frame`向量中; +20. 返回绘制好的图像。 -![image-20231221095409399](assets/post/README/image-20231221095409399.png) +需要注意的是,该代码使用了OpenCV库中提供的绘制矩形框和添加文字的相关函数。其中`cv::rectangle()`函数用于绘制矩形框,`cv::putText()`函数用于在矩形框内添加目标类别和置信度。 -安装 Linux 基础工具 +### 获取显示屏的参数信息 -```plaintext -apt install sudo ssh openssh-server net-tools ethtool wireless-tools network-manager iputils-ping rsyslog alsa-utils bash-completion gnupg busybox kmod wget git curl --no-install-recommends -``` +```c +// 帧缓冲器信息结构体,包括每个像素的位数和虚拟分辨率 +struct framebuffer_info { + uint32_t bits_per_pixel; + uint32_t xres_virtual; +}; -安装编译工具 +// 获取帧缓冲器的信息函数,接受设备路径作为参数 +struct framebuffer_info get_framebuffer_info(const char* framebuffer_device_path) +{ + struct framebuffer_info info; + struct fb_var_screeninfo screen_info; + int fd = -1; -```bash -apt install build-essential -``` + // 打开设备文件 + fd = open(framebuffer_device_path, O_RDWR); -安装 Linux nerd 工具 + // 如果成功打开设备文件,则使用 ioctl 函数获取屏幕信息 + if (fd >= 0) { + if (!ioctl(fd, FBIOGET_VSCREENINFO, &screen_info)) { + info.xres_virtual = screen_info.xres_virtual; // 虚拟分辨率 + info.bits_per_pixel = screen_info.bits_per_pixel; // 像素位数 + } + } -```plaintext -apt install vim nano neofetch + return info; +}; ``` -设置本机入口 ip 地址 - -```plaintext -cat < /etc/hosts -127.0.0.1 localhost -127.0.1.1 $HOST -::1 localhost ip6-localhost ip6-loopback -ff02::1 ip6-allnodes -ff02::2 ip6-allrouters -EOF -``` +这段代码的用途是获取帧缓冲器的信息。 -配置网卡 +具体来说: -```plaintext -mkdir -p /etc/network -cat >/etc/network/interfaces </etc/resolv.conf </etc/fstab < -/dev/mmcblk0p1 /boot vfat defaults 0 0 -/dev/mmcblk0p2 / ext4 defaults,noatime 0 1 -EOF +static void install_sig_handler(void) +{ + signal(SIGBUS, terminate); // 当程序访问一个不合法的内存地址时发送的信号 + signal(SIGFPE, terminate); // 浮点异常信号 + signal(SIGHUP, terminate); // 终端断开连接信号 + signal(SIGILL, terminate); // 非法指令信号 + signal(SIGINT, terminate); // 中断进程信号 + signal(SIGIOT, terminate); // IOT 陷阱信号 + signal(SIGPIPE, terminate); // 管道破裂信号 + signal(SIGQUIT, terminate); // 停止进程信号 + signal(SIGSEGV, terminate); // 无效的内存引用信号 + signal(SIGSYS, terminate); // 非法系统调用信号 + signal(SIGTERM, terminate); // 终止进程信号 + signal(SIGTRAP, terminate); // 跟踪/断点陷阱信号 + signal(SIGUSR1, terminate); // 用户定义信号1 + signal(SIGUSR2, terminate); // 用户定义信号2 +} ``` -配置 root 密码 +这段代码定义了两个函数,并给出了相应的注释说明。具体注释如下: -```plaintext -passwd -``` +- `static void terminate(int sig_no)`:信号处理函数。 + - `int sig_no`:接收到的信号编号。 + - `printf("Got signal %d, exiting ...\n", sig_no);`:打印接收到的信号编号。 + - `cap.release();`:释放视频流捕获对象。 + - `exit(1);`:退出程序。 +- `static void install_sig_handler(void)`:安装信号处理函数。 + - `signal(SIGBUS, terminate);`:为SIGBUS信号安装信号处理函数。 + - `signal(SIGFPE, terminate);`:为SIGFPE信号安装信号处理函数。 + - `signal(SIGHUP, terminate);`:为SIGHUP信号安装信号处理函数。 + - `signal(SIGILL, terminate);`:为SIGILL信号安装信号处理函数。 + - `signal(SIGINT, terminate);`:为SIGINT信号安装信号处理函数。 + - `signal(SIGIOT, terminate);`:为SIGIOT信号安装信号处理函数。 + - `signal(SIGPIPE, terminate);`:为SIGPIPE信号安装信号处理函数。 + - `signal(SIGQUIT, terminate);`:为SIGQUIT信号安装信号处理函数。 + - `signal(SIGSEGV, terminate);`:为SIGSEGV信号安装信号处理函数。 + - `signal(SIGSYS, terminate);`:为SIGSYS信号安装信号处理函数。 + - `signal(SIGTERM, terminate);`:为SIGTERM信号安装信号处理函数。 + - `signal(SIGTRAP, terminate);`:为SIGTRAP信号安装信号处理函数。 + - `signal(SIGUSR1, terminate);`:为SIGUSR1信号安装信号处理函数。 + - `signal(SIGUSR2, terminate);`:为SIGUSR2信号安装信号处理函数。 -配置主机名 +这段代码的功能是安装信号处理函数,用于捕获和处理不同类型的信号。当程序接收到指定的信号时,会调用`terminate`函数进行处理。 -```plaintext -echo TinyVision > /etc/hostname -``` +具体而言,`terminate`函数会打印接收到的信号编号,并释放视频流捕获对象`cap`,然后调用`exit(1)`退出程序。 -退出 chroot +`install_sig_handler`函数用于为多个信号注册同一个信号处理函数`terminate`,使得当这些信号触发时,都会执行相同的处理逻辑。 -```plaintext -exit -``` +### 主循环 -取消挂载 chroot +```cpp +int main(int argc, char *argv[]) +{ + const int frame_width = 480; // 视频帧宽度 + const int frame_height = 480; // 视频帧高度 + const int frame_rate = 30; // 视频帧率 -```plaintext -./ch-mount.sh -u rootfs_data/ -``` + char* nbg = "/usr/lib/model/mobilenet_v2_ssd.nb"; // 模型文件路径 -### 拷贝 rootfs 到镜像中 + install_sig_handler(); // 安装信号处理程序 -```plaintext -sudo cp -raf rootfs_data/* rootfs -``` + framebuffer_info fb_info = get_framebuffer_info("/dev/fb0"); // 获取帧缓冲区信息 -取消挂载 + cap.open(0); // 打开视频设备 -```plaintext -sudo umount rootfs -``` + if (!cap.isOpened()) { + std::cerr << "Could not open video device." << std::endl; // 如果打开视频设备失败,则输出错误信息并返回 + return 1; + } -至此 debian rootfs 就制作好了。 + std::cout << "Successfully opened video device." << std::endl; // 成功打开视频设备,输出成功信息 + cap.set(cv::CAP_PROP_FRAME_WIDTH, frame_width); // 设置视频帧宽度 + cap.set(cv::CAP_PROP_FRAME_HEIGHT, frame_height); // 设置视频帧高度 + cap.set(cv::CAP_PROP_FPS, frame_rate); // 设置视频帧率 + std::ofstream ofs("/dev/fb0"); // 打开帧缓冲区文件 + cv::Mat frame; // 创建用于存储视频帧的 Mat 对象 -## 打包固件 + awnn_init(7 * 1024 * 1024); // 初始化 AWNN 库 + Awnn_Context_t *context = awnn_create(nbg); // 创建 AWNN 上下文 + if (NULL == context){ + std::cerr << "fatal error, awnn_create failed." << std::endl; // 如果创建 AWNN 上下文失败,则输出致命错误信息并返回 + return -1; + } + /* copy input */ + uint32_t input_width = 300; // 输入图像宽度 + uint32_t input_height = 300; // 输入图像高度 + uint32_t input_depth = 3; // 输入图像通道数 + uint32_t sz = input_width * input_height * input_depth; // 输入图像数据总大小 -编译完成 bootloader,内核,rootfs 后,还需要打包固件成为可以 dd 写入的固件,这里我们使用 genimage 工具来生成构建。 + uint8_t* plant_data = NULL; // 定义输入图像数据指针,初始化为 NULL + + while (true) { + // 从视频设备中读取一帧图像 + cap >> frame; -## 生成刷机镜像 + // 检查图像的位深度是否为8位和通道数是否为3 + if (frame.depth() != CV_8U) { + std::cerr << "不是8位每像素和通道。" << std::endl; + } else if (frame.channels() != 3) { + std::cerr << "不是3个通道。" << std::endl; + } else { + // 转置和翻转图像以调整其方向 + cv::transpose(frame, frame); + cv::flip(frame, frame, 0); -编译内核后,可以在文件夹 `arch/arm/boot/dts/allwinner` 生成`sun8i-v851se-tinyvision.dtb` ,在文件夹`arch/arm/boot` 生成 `zImage` ,把他们拷贝出来。 + // 将图像大小调整为所需的输入宽度和高度 + cv::resize(frame, frame, cv::Size(input_width, input_height)); -![33140ec9-fd56-4cef-9250-ffa210b74178.png](assets/post/README/1702731217300-33140ec9-fd56-4cef-9250-ffa210b74178.png) + // 对MobileNetV2 SSD模型进行预处理 + plant_data = mbv2_ssd_preprocess(frame, input_width, input_depth); -然后将 `sun8i-v851se-tinyvision.dtb` 改名为 `sunxi.dtb` ,这个设备树名称是定义在 SyterKit 源码中的,如果之前修改了 SyterKit 的源码需要修改到对应的名称,SyterKit 会去读取这个设备树。 + // 设置AWNN上下文的输入缓冲区 + uint8_t *input_buffers[1] = {plant_data}; + awnn_set_input_buffers(context, input_buffers); -然后编写一个 `config.txt` 作为配置文件 + // 运行AWNN上下文进行模型推理 + awnn_run(context); -```plaintext -[configs] -bootargs=root=/dev/mmcblk0p2 earlyprintk=sunxi-uart,0x02500000 loglevel=2 initcall_debug=0 rootwait console=ttyS0 init=/sbin/init -mac_addr=4a:13:e4:f9:79:75 -bootdelay=3 -``` + // 从AWNN上下文中获取输出缓冲区 + float **results = awnn_get_output_buffers(context); -### 安装 GENIMAGE + // 使用SSD模型进行目标检测并更新图像 + frame = detect_ssd(frame, results); -这里我们使用 genimage 作为打包工具 + // 将图像大小调整为显示尺寸 + cv::resize(frame, frame, cv::Size(DISPLAY_X, DISPLAY_Y)); -```plaintext -sudo apt-get install libconfuse-dev #安装genimage依赖库 -sudo apt-get install genext2fs # 制作镜像时genimage将会用到 -git clone https://github.com/pengutronix/genimage.git -cd genimage -./autogen.sh # 配置生成configure -./configure # 配置生成makefile -make -sudo make install + // 获取帧缓冲区的宽度和位深度 + int framebuffer_width = fb_info.xres_virtual; + int framebuffer_depth = fb_info.bits_per_pixel; + + // 根据帧缓冲区的位深度将图像转换为兼容格式 + cv::Size2f frame_size = frame.size(); + cv::Mat framebuffer_compat; + switch (framebuffer_depth) { + case 16: + // 将BGR转换为BGR565格式以适用于16位帧缓冲区 + cv::cvtColor(frame, framebuffer_compat, cv::COLOR_BGR2BGR565); + + // 将转换后的图像写入帧缓冲区文件 + for (int y = 0; y < frame_size.height; y++) { + ofs.seekp(y * framebuffer_width * 2); + ofs.write(reinterpret_cast(framebuffer_compat.ptr(y)), frame_size.width * 2); + } + break; + case 32: + // 将图像分解为BGR通道并添加一个alpha通道以适用于32位帧缓冲区 + std::vector split_bgr; + cv::split(frame, split_bgr); + split_bgr.push_back(cv::Mat(frame_size, CV_8UC1, cv::Scalar(255))); + cv::merge(split_bgr, framebuffer_compat); + + // 将转换后的图像写入帧缓冲区文件 + for (int y = 0; y < frame_size.height; y++) { + ofs.seekp(y * framebuffer_width * 4); + ofs.write(reinterpret_cast(framebuffer_compat.ptr(y)), frame_size.width * 4); + } + break; + default: + std::cerr << "不支持的帧缓冲区位深度。" << std::endl; + } + + // 释放为plant_data分配的内存空间 + free(plant_data); + } +} ``` -编译后运行试一试,这里正常 +这段代码主要实现了以下功能: -![8dd643b9-5f40-4b9e-a355-457fd80d8c5b.png](assets/post/README/1702731225454-8dd643b9-5f40-4b9e-a355-457fd80d8c5b.png) +1. 定义了视频帧的宽度、高度和帧率。 +2. 指定了模型文件的路径。 +3. 安装信号处理程序。 +4. 获取帧缓冲区的信息。 +5. 打开视频设备,并设置视频帧的宽度、高度和帧率。 +6. 打开帧缓冲区文件,用于后续操作。 +7. 初始化 AWNN 库,并分配一定大小的内存。 +8. 创建 AWNN 上下文。 +9. 定义输入图像的宽度、高度和通道数,并计算输入图像数据的总大小。 +10. 声明一个输入图像数据指针。 -### 使用 GENIMAGE 打包固件 +11. 主循环函数,用于不断从视频设备中获取视频帧并进行处理和展示。 -编写 genimage.cfg 作为打包的配置 +具体的步骤如下: -```cfg -image boot.vfat { - vfat { - files = { - "zImage", - "sunxi.dtb", - "config.txt" - } - } - size = 32M -} +1. 使用`cap`对象从视频设备中获取一帧图像,并将其存储在`frame`中。 +2. 检查图像的位深度是否为8位(CV_8U),如果不是,则输出错误信息。 +3. 检查图像的通道数是否为3,如果不是,则输出错误信息。 +4. 对图像进行转置和翻转操作,以调整图像的方向。 +5. 将图像的大小调整为设定的输入宽度和高度。 +6. 调用`mbv2_ssd_preprocess`函数对图像进行预处理,并将结果存储在`plant_data`中。 +7. 将`plant_data`设置为AWNN上下文的输入缓冲区。 +8. 运行AWNN上下文,执行模型推理。 +9. 使用`detect_ssd`函数对图像进行目标检测,得到检测结果的可视化图像。 +10. 将图像的大小调整为设定的显示宽度和高度。 +11. 根据帧缓冲区的位深度,将图像转换为与帧缓冲区兼容的格式,并写入帧缓冲区文件。 +12. 释放`plant_data`的内存空间。 +13. 循环回到第1步,继续获取和处理下一帧图像。 -image sdcard.img { - hdimage {} +这段代码主要完成了从视频设备获取图像、预处理图像、执行模型推理、目标检测和将结果写入帧缓冲区文件等一系列操作,以实现实时目标检测并在显示设备上展示检测结果。 - partition boot0 { - in-partition-table = "no" - image = "syter_boot_bin_card.bin" - offset = 8K - } +## 效果展示 - partition boot0-gpt { - in-partition-table = "no" - image = "syter_boot_bin_card.bin" - offset = 128K - } +![image-20240126200516520](assets/post/README/image-20240126200516520.png) - partition kernel { - partition-type = 0xC - bootable = "true" - image = "boot.vfat" - } - - partition rootfs { - partition-type = 0x83 - bootable = "true" - image = "rootfs.img" - } -} -``` +# OpenWrt 编译开发 -由于genimage的脚本比较复杂,所以编写一个 `genimage.sh` 作为简易使用的工具 +TinyVision自带百兆网口接口+摄像头接口支持,支持 Current stable series: OpenWrt 23.05 系统,可以做一个 轻量级的IPC摄像头,里面运行主线系统,选择合适的内核版本 一键 编译生成系统镜像。 -```sh -#!/usr/bin/env bash +* openwrt-23.05源码: https://github.com/YuzukiHD/OpenWrt/tree/openwrt-23.05 +* OpenWrt-23.05目录结构,OpenWrt-23.05.tar.gz 压缩包 md5值 2b10a86405aa4d045bc2134e98d3f6d8 请确保压缩包文件一致性。 -die() { - cat <&2 -Error: $@ +``` bash +ubuntu@ubuntu1804:~/$ md5sum OpenWrt-23.05.tar.gz +ubuntu@ubuntu1804:~/$ tree -L 1 +. +├── bin +├── BSDmakefile +├── build_dir +├── config +├── Config.in +├── COPYING +├── dl +├── feeds +├── feeds.conf.default +├── include +├── key-build +├── key-build.pub +├── key-build.ucert +├── key-build.ucert.revoke +├── LICENSES +├── Makefile +├── package +├── README.md +├── rules.mk +├── scripts +├── staging_dir +├── target +├── tmp +├── toolchain +└── tools -Usage: ${0} -c GENIMAGE_CONFIG_FILE -EOF - exit 1 -} +14 directories, 11 files -# Parse arguments and put into argument list of the script -opts="$(getopt -n "${0##*/}" -o c: -- "$@")" || exit $? -eval set -- "$opts" +``` -GENIMAGE_TMP="${BUILD_DIR}/genimage.tmp" -while true ; do - case "$1" in - -c) - GENIMAGE_CFG="${2}"; - shift 2 ;; - --) # Discard all non-option parameters - shift 1; - break ;; - *) - die "unknown option '${1}'" ;; - esac -done +* 源码存放在百度网盘: https://pan.baidu.com/s/1a0uS7kqXiEdKFFgIJ3HF5g?pwd=qm83 提取码:qm83,打包的源码只是提供加速下载,Git上源码实时更新建议使用Github的源码,实在下载不下来再用这个。 +* git仓库位置 https://github.com/YuzukiHD/OpenWrt/ -[ -n "${GENIMAGE_CFG}" ] || die "Missing argument" +## 编译 -# Pass an empty rootpath. genimage makes a full copy of the given rootpath to -# ${GENIMAGE_TMP}/root so passing TARGET_DIR would be a waste of time and disk -# space. We don't rely on genimage to build the rootfs image, just to insert a -# pre-built one in the disk image. +获取镜像后,进行解压缩,建议使用百度网盘版本,因为网络问题,可能导致某些软件包无法正常下载,编译报错。 -trap 'rm -rf "${ROOTPATH_TMP}"' EXIT -ROOTPATH_TMP="$(mktemp -d)" -GENIMAGE_TMP="$(mktemp -d)" -rm -rf "${GENIMAGE_TMP}" +解压压缩包后,执行 make menuconfig 进入到配置界面, -genimage \ - --rootpath "${ROOTPATH_TMP}" \ - --tmppath "${GENIMAGE_TMP}" \ - --inputpath "${BINARIES_DIR}" \ - --outputpath "${BINARIES_DIR}" \ - --config "${GENIMAGE_CFG}" +```shell +ubuntu@ubuntu1804:~/OpenWrt$ make menuconfig ``` -准备完成,文件如下所示 - -![8986491d-003b-479e-9ef0-01f3c93ca43c.png](assets/post/README/1702731236382-8986491d-003b-479e-9ef0-01f3c93ca43c.png) +参考下图红框所示,三个选项选中 为 TinyVision开发板,保证一模一样。 +![image-20231216174136154](assets/post/README/Openwrt-config.jpg) +选中完成后,保存退出,继续执行make 命令即可开始编译。 -运行命令进行打包 +```shell +ubuntu@ubuntu1804:~/OpenWrt$ make -```plaintext -chmod 777 genimage.sh -./genimage.sh -c genimage.cfg ``` -![1ad6cdd4-59b6-4089-a5f4-2aac0e3538ef.png](assets/post/README/1702731309228-1ad6cdd4-59b6-4089-a5f4-2aac0e3538ef.png) +如果你不想使用压缩包,而是从头获取源码,需要在 make menuconfig选中开发板之前 执行 ` ./scripts/feeds update -a ` 命令检查远端仓库和本地仓库的差异进行更新,之后执行 `./scripts/feeds install -a` 来安装远端更新。 -打包完成,可以找到 `sdcard.img` +```shell +ubuntu@ubuntu1804:~/OpenWrt$ ./scripts/feeds update -a +Updating feed 'packages' from 'https://github.com/openwrt/packages.git;openwrt-23.05' ... +remote: Enumerating objects: 101, done. +remote: Counting objects: 100% (101/101), done. +remote: Compressing objects: 100% (44/44), done. +remote: Total 68 (delta 44), reused 45 (delta 21), pack-reused 0 +Unpacking objects: 100% (68/68), done. +From https://github.com/openwrt/packages + 421e2c75a..d26bbd792 openwrt-23.05 -> origin/openwrt-23.05 +Updating 421e2c75a..d26bbd792 +Fast-forward + admin/btop/Makefile | 7 ++++--- + lang/rust/Makefile | 4 ++-- + lang/rust/patches/0001-Update-xz2-and-use-it-static.patch | 14 +++++++------- + lang/rust/patches/0002-rustc-bootstrap-cache.patch | 10 +++++----- + lang/rust/patches/0003-bump-libc-deps-to-0.2.146.patch | 28 ---------------------------- + lang/rust/rust-values.mk | 6 ++++++ + net/adblock-fast/Makefile | 2 +- + net/adblock-fast/files/etc/init.d/adblock-fast | 2 +- + net/dnsproxy/Makefile | 4 ++-- + net/sing-box/Makefile | 9 ++------- + net/travelmate/Makefile | 2 +- + net/travelmate/files/travelmate.sh | 12 +++++++++++- + net/uspot/Makefile | 10 ++++++---- + net/v2ray-geodata/Makefile | 12 ++++++------ + net/v2raya/Makefile | 6 +++--- + 15 files changed, 57 insertions(+), 71 deletions(-) +Updating feed 'luci' from 'https://github.com/openwrt/luci.git;openwrt-23.05' ... +remote: Enumerating objects: 49, done. +remote: Counting objects: 100% (49/49), done. +remote: Compressing objects: 100% (18/18), done. +remote: Total 31 (delta 12), reused 25 (delta 6), pack-reused 0 +Unpacking objects: 100% (31/31), done. +From https://github.com/openwrt/luci + 11a1a43..fa4b280 openwrt-23.05 -> origin/openwrt-23.05 +Updating 11a1a43..fa4b280 +Fast-forward + applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/rules.js | 3 +++ + applications/luci-app-https-dns-proxy/Makefile | 2 +- + applications/luci-app-https-dns-proxy/htdocs/luci-static/resources/view/https-dns-proxy/overview.js | 8 ++++---- + themes/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/cascade.css | 13 +++++++++++++ + 4 files changed, 21 insertions(+), 5 deletions(-) +Updating feed 'routing' from 'https://github.com/openwrt/routing.git;openwrt-23.05' ... +Already up to date. +Updating feed 'telephony' from 'https://github.com/openwrt/telephony.git;openwrt-23.05' ... +Already up to date. +Create index file './feeds/packages.index' +Collecting package info: done +Create index file './feeds/luci.index' +Collecting package info: done +Create index file './feeds/routing.index' +Create index file './feeds/telephony.index' +ubuntu@ubuntu1804:~/Downloads/TinyVision/OpenWrt$ ./scripts/feeds install -a +Installing all packages from feed packages. +Installing all packages from feed luci. +Installing all packages from feed routing. +Installing all packages from feed telephony. +ubuntu@ubuntu1804:~/OpenWrt$ -使用软件烧录固件到TF卡上 +``` -![d06e037d-102f-46cc-80c1-49b47f72b8b1.png](assets/post/README/1702731317182-d06e037d-102f-46cc-80c1-49b47f72b8b1.png) +## 烧写镜像 -# 主线内核开发 +系统编译完成后,镜像输出在 `build_dir/target-arm_cortex-a7+neon-vfpv4_musl_eabi/linux-yuzukihd_v851se/tmp/` 目录下,名称为 `openwrt-yuzukihd-v851se-yuzuki_tinyvision-squashfs-sysupgrade.img.gz` 需要先使用 `tar -xvf` 进行解压缩,之后 使用 `dd if` 命令 完整写入sd卡设备,或者 使用 `wind32diskimage`工具。 或者使用 `balenaEtcher` 等 进行烧录。 -## SyterKit +# Debian 12 构建与编译 + +## 构建 SyterKit 作为 Bootloader SyterKit 是一个纯裸机框架,用于 TinyVision 或者其他 v851se/v851s/v851s3/v853 等芯片的开发板,SyterKit 使用 CMake 作为构建系统构建,支持多种应用与多种外设驱动。同时 SyterKit 也具有启动引导的功能,可以替代 U-Boot 实现快速启动 @@ -2152,190 +2214,294 @@ cmake .. make ``` -![image-20231216174136154](assets/post/README/image-20231216174136154.png) +![f6cd8396-6b9e-4171-a32f-b6e908fa1fb9-image.png](assets/post/README/1702729920306-f6cd8396-6b9e-4171-a32f-b6e908fa1fb9-image.png) 编译后的可执行文件位于 `build/app` 中,这里包括 SyterKit 的多种APP可供使用。 -![image-20231216173846369](assets/post/README/image-20231216173846369.png) +![ecd7330e-1281-4296-9de7-0433e12fef2f-image.png](assets/post/README/1702729933404-ecd7330e-1281-4296-9de7-0433e12fef2f-image.png) 这里我们使用的是 `syter_boot` 作为启动引导。进入 syter_boot 文件夹,可以看到这些文件 -![image-20231216174210790](assets/post/README/image-20231216174210790.png) +![d631adb8-9d69-4f38-99f4-f080a3d04cc4-image.png](assets/post/README/1702729955121-d631adb8-9d69-4f38-99f4-f080a3d04cc4-image.png) 由于 TinyVision 是 TF 卡启动,所以我们需要用到 `syter_boot_bin_card.bin` -![image-20231216174311727](assets/post/README/image-20231216174311727.png) +![0bee1188-3372-4a0a-94c3-5ae19322eab3-image.png](assets/post/README/1702729964449-0bee1188-3372-4a0a-94c3-5ae19322eab3-image.png) -## 移植 Linux 6.7 主线 +## 编译 Linux-6.1 内核 -有了启动引导,接下来是移植 Linux 6.7 主线,前往 https://kernel.org/ 找到 Linux 6.7,选择 `tarball` 下载 +由于 Debian 12 配套的内核是 Linux 6.1 LTS,所以这里我们选择构建 Linux 6.1 版本内核。 -![image-20231216174444070](assets/post/README/image-20231216174444070.png) +### 搭建编译环境 -下载后解压缩 +安装一些必要的安装包 -```shell -tar xvf linux-6.7-rc5.tar.gz +```plaintext +sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf build-essential libncurses5-dev zlib1g-dev gawk flex bison quilt libssl-dev xsltproc libxml-parser-perl mercurial bzr ecj cvs unzip lsof ``` -进入 linux 6.7 目录,开始移植相关驱动。 +### 获取内核源码 -### 搭建 Kernel 相关环境 +内核源码托管在 Github 上,可以直接获取到,这里使用 `--depth=1` 指定 git 深度为 1 加速下载。 -Kernel 编译需要一些软件包,需要提前安装。 +```plaintext +git clone http://github.com/YuzukiHD/TinyVision --depth=1 +``` + +然后进入内核文件夹 ```plaintext -sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf build-essential libncurses5-dev zlib1g-dev gawk flex bison quilt libssl-dev xsltproc libxml-parser-perl mercurial bzr ecj cvs unzip lsof +cd kernel/linux-6.1 ``` -安装完成后可以尝试编译一下,看看能不能编译通过,先应用配置文件 +### 配置内核选项 + +应用 defconfig ```plaintext -CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm sunxi_defconfig +CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm tinyvision_defconfig ``` -![image-20231216181640653](assets/post/README/image-20231216181640653.png) +进入 `menuconfig` 配置选项 -然后尝试编译 +```plaintext +CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm menuconfig +``` + +进入 `General Setup ->`,选中 `Control Group Support` + +![image-20231221104449523](assets/post/README/image-20231221104449523.png) + +![image-20231221122711591](assets/post/README/image-20231221122711591.png) + +前往 `File Systems` 找到 `FUSE (Filesystem in Userspace) support` + +![image-20231221104607368](assets/post/README/image-20231221104607368.png) + +前往 `File Systems` 找到 `Inotify support for userspace` + +![image-20231221122848948](assets/post/README/image-20231221122848948.png) + +编译内核 ```plaintext CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm ``` -可以用 `-j32` 来加速编译,`32` 指的是使用32线程编译,一般cpu有几个核心就设置几线程 +## 使用 debootstrap 构建 debian rootfs + +### 准备环境,依赖 + +下载安装依赖环境 ```plaintext -CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm -j32 +sudo apt install debootstrap qemu qemu-user-static qemu-system qemu-utils qemu-system-misc binfmt-support dpkg-cross debian-ports-archive-keyring --no-install-recommends ``` -正常编译 +生成目标镜像,配置环境,这里我们生成一个 1024M 的镜像文件用于存放 rootfs -![image-20231216183011911](assets/post/README/image-20231216183011911.png) +```shell +dd if=/dev/zero of=rootfs.img bs=1M count=1024 +mkdir rootfs +mkfs.ext4 rootfs.img +sudo mount rootfs.img rootfs +``` -### 移植 clk 驱动 +### 开始构建基础 rootfs -这里提供已经适配修改后的驱动:https://github.com/YuzukiHD/TinyVision/tree/main/kernel/linux-6.7-driver 可以直接使用。 +这里我们选择最新的 debian12 (bookwarm) 作为目标镜像,使用清华源来构建,输出到目标目录 rootfs_data 文件夹中。新版本的 debootstrap 只需要运行一次即可完成两次 stage 的操作,相较于老版本方便许多。 -也可以参考 https://github.com/YuzukiHD/TinyVision/tree/main/kernel/bsp/drivers/clk 中的驱动移植。 +```shell +sudo debootstrap --arch=armhf bookworm rootfs_data https://mirrors.tuna.tsinghua.edu.cn/debian/ +``` -进入文件夹 `include/dt-bindings/clock/` 新建文件 `sun8i-v851se-ccu.h` ,将 CLK 填入 +![image-20231221093653561](assets/post/README/image-20231221093653561.png) + +看到 `I: Base system installed successfully.` 就是构建完成了 + +![image-20231221094602269](assets/post/README/image-20231221094602269.png) + +等待构建完成后,使用chroot进入到目录,这里编写一个挂载脚本方便挂载使用,新建文件 `ch-mount.sh` 并写入以下内容: + +```bash +#!/bin/bash + +function mnt() { + echo "MOUNTING" + sudo mount -t proc /proc ${2}proc + sudo mount -t sysfs /sys ${2}sys + sudo mount -o bind /dev ${2}dev + sudo mount -o bind /dev/pts ${2}dev/pts + sudo chroot ${2} +} + +function umnt() { + echo "UNMOUNTING" + sudo umount ${2}proc + sudo umount ${2}sys + sudo umount ${2}dev/pts + sudo umount ${2}dev + +} + +if [ "$1" == "-m" ] && [ -n "$2" ] ; +then + mnt $1 $2 +elif [ "$1" == "-u" ] && [ -n "$2" ]; +then + umnt $1 $2 +else + echo "" + echo "Either 1'st, 2'nd or both parameters were missing" + echo "" + echo "1'st parameter can be one of these: -m(mount) OR -u(umount)" + echo "2'nd parameter is the full path of rootfs directory(with trailing '/')" + echo "" + echo "For example: ch-mount -m /media/sdcard/" + echo "" + echo 1st parameter : ${1} + echo 2nd parameter : ${2} +fi +``` -![image-20231216182350741](assets/post/README/image-20231216182350741.png) +然后赋予脚本执行的权限 -进入 `include/dt-bindings/reset` 新建文件 `sun8i-v851se-ccu.h` 将 RST 填入 +```shell +chmod 777 ch-mount.sh +``` -![image-20231216182941392](assets/post/README/image-20231216182941392.png) +- 使用 `./ch-mount.sh -m rootfs_data` 挂载 +- 使用 `./ch-mount.sh -u rootfs_data` 卸载 -进入 `drivers/clk/sunxi-ng` 找到 `sunxi-ng` clk 驱动,复制文件`ccu-sun20i-d1.c` 和 `ccu-sun20i-d1.h` 文件并改名为 `ccu-sun8i-v851se.c` ,`ccu-sun8i-v851se.h` 作为模板。 +执行挂载,可以看到进入了 debian 的 rootfs -![image-20231216180413415](assets/post/README/image-20231216180413415.png) +![image-20231221094725953](assets/post/README/image-20231221094725953.png) -将文件中的 `SUN20I_D1` 改为 `SUN8I_V851SE` +配置系统字符集,选择 en_US 作为默认字符集 -![image-20231216180653502](assets/post/README/image-20231216180653502.png) +```shell +export LC_ALL=en_US.UTF-8 +apt-get install locales +dpkg-reconfigure locales +``` -打开芯片数据手册[V851SX_Datasheet_V1.2.pdf](https://github.com/YuzukiHD/TinyVision/blob/main/docs/hardware/TinyVision/datasheet/V851SX_Datasheet_V1.2.pdf),找到 CCU 章节 +选择一个就可以 -![image-20231216180748419](assets/post/README/image-20231216180748419.png) +![image-20231221095332517](assets/post/README/image-20231221095332517.png) -对照手册编写驱动文件适配 V851se 平台。 +直接 OK 下一步 -然后找到 `drivers/clk/sunxi-ng/Kconfig` 文件,增加刚才编写的驱动的 Kconfig 说明 +![image-20231221095409399](assets/post/README/image-20231221095409399.png) -![image-20231216181118674](assets/post/README/image-20231216181118674.png) +安装 Linux 基础工具 ```plaintext -config SUN8I_V851SE_CCU - tristate "Support for the Allwinner V851se CCU" - default y - depends on MACH_SUN8I || COMPILE_TEST +apt install sudo ssh openssh-server net-tools ethtool wireless-tools network-manager iputils-ping rsyslog alsa-utils bash-completion gnupg busybox kmod wget git curl --no-install-recommends ``` -同时打开 `drivers/clk/sunxi-ng/Makefile` - -![image-20231216181248375](assets/post/README/image-20231216181248375.png) - -```plaintext -obj-$(CONFIG_SUN8I_V851SE_CCU) += sun8i-v851se-ccu.o +安装编译工具 -sun8i-v851se-ccu-y += ccu-sun8i-v851se.o +```bash +apt install build-essential ``` -来检查一下是否移植成功,先查看 `menuconfig`,找到 `Device Drivers > Common Clock Framework`,查看是否有 V851se 平台选项出现 +安装 Linux nerd 工具 ```plaintext -CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm menuconfig +apt install vim nano neofetch ``` -![image-20231216183207387](assets/post/README/image-20231216183207387.png) - -编译测试,有几处未使用的变量的警告,无视即可。 +设置本机入口 ip 地址 ```plaintext -CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm +cat < /etc/hosts +127.0.0.1 localhost +127.0.1.1 $HOST +::1 localhost ip6-localhost ip6-loopback +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters +EOF ``` -![image-20231216183406918](assets/post/README/image-20231216183406918.png) - -正常编译成功 - -### 移植 pinctrl 驱动 - -这里提供已经适配修改后的驱动:https://github.com/YuzukiHD/TinyVision/tree/main/kernel/linux-6.7-driver 可以直接使用。 +配置网卡 -前往`drivers/pinctrl/sunxi/` 新建文件 `pinctrl-sun8i-v851se.c` +```plaintext +mkdir -p /etc/network +cat >/etc/network/interfaces </etc/resolv.conf </etc/fstab < +/dev/mmcblk0p1 /boot vfat defaults 0 0 +/dev/mmcblk0p2 / ext4 defaults,noatime 0 1 +EOF +``` -修改 `drivers/pinctrl/sunxi/Makefile` 增加路径 +配置 root 密码 -![image-20231216184126988](assets/post/README/image-20231216184126988.png) +```plaintext +passwd +``` -来检查一下是否移植成功,先查看 `menuconfig`,找到 `> Device Drivers > Pin controllers`,查看是否有 V851se 平台选项出现 +配置主机名 ```plaintext -CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm menuconfig +echo TinyVision > /etc/hostname ``` -![image-20231216184259987](assets/post/README/image-20231216184259987.png) - -编译测试,编译通过 +退出 chroot ```plaintext -CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm +exit ``` -![image-20231216184649676](assets/post/README/image-20231216184649676.png) - -### 编写设备树 +取消挂载 chroot -这里提供已经适配修改后的驱动:https://github.com/YuzukiHD/TinyVision/tree/main/kernel/linux-6.7-driver/dts 可以直接使用。 +```plaintext +./ch-mount.sh -u rootfs_data/ +``` -![image-20231216185413254](assets/post/README/image-20231216185413254.png) +### 拷贝 rootfs 到镜像中 -这部分直接给结果了,把上面适配的设备树放到`/home/yuzuki/WorkSpace/aa/linux-6.7-rc5/arch/arm/boot/dts/allwinner/` ,修改 `/home/yuzuki/WorkSpace/aa/linux-6.7-rc5/arch/arm/boot/dts/allwinner/Makefile` +```plaintext +sudo cp -raf rootfs_data/* rootfs +``` -![image-20231216185113539](assets/post/README/image-20231216185113539.png) +取消挂载 ```plaintext -sun8i-v851se-tinyvision.dtb +sudo umount rootfs ``` -![image-20231216185530270](assets/post/README/image-20231216185530270.png) +至此 debian rootfs 就制作好了。 + +## 打包固件 + +编译完成 bootloader,内核,rootfs 后,还需要打包固件成为可以 dd 写入的固件,这里我们使用 genimage 工具来生成构建。 ## 生成刷机镜像 编译内核后,可以在文件夹 `arch/arm/boot/dts/allwinner` 生成`sun8i-v851se-tinyvision.dtb` ,在文件夹`arch/arm/boot` 生成 `zImage` ,把他们拷贝出来。 -![image-20231216191248458](assets/post/README/image-20231216191248458.png) +![33140ec9-fd56-4cef-9250-ffa210b74178.png](assets/post/README/1702731217300-33140ec9-fd56-4cef-9250-ffa210b74178.png) 然后将 `sun8i-v851se-tinyvision.dtb` 改名为 `sunxi.dtb` ,这个设备树名称是定义在 SyterKit 源码中的,如果之前修改了 SyterKit 的源码需要修改到对应的名称,SyterKit 会去读取这个设备树。 @@ -2343,12 +2509,12 @@ sun8i-v851se-tinyvision.dtb ```plaintext [configs] -bootargs=cma=4M root=/dev/mmcblk0p2 init=/sbin/init console=ttyS0,115200 earlyprintk=sunxi-uart,0x02500000 rootwait clk_ignore_unused +bootargs=root=/dev/mmcblk0p2 earlyprintk=sunxi-uart,0x02500000 loglevel=2 initcall_debug=0 rootwait console=ttyS0 init=/sbin/init mac_addr=4a:13:e4:f9:79:75 bootdelay=3 ``` -### 安装 genimage +### 安装 GENIMAGE 这里我们使用 genimage 作为打包工具 @@ -2365,9 +2531,9 @@ sudo make install 编译后运行试一试,这里正常 -![image-20231216192512837](assets/post/README/image-20231216192512837.png) +![8dd643b9-5f40-4b9e-a355-457fd80d8c5b.png](assets/post/README/1702731225454-8dd643b9-5f40-4b9e-a355-457fd80d8c5b.png) -### 使用 genimage 打包固件 +### 使用 GENIMAGE 打包固件 编写 genimage.cfg 作为打包的配置 @@ -2380,7 +2546,7 @@ image boot.vfat { "config.txt" } } - size = 8M + size = 32M } image sdcard.img { @@ -2403,6 +2569,12 @@ image sdcard.img { bootable = "true" image = "boot.vfat" } + + partition rootfs { + partition-type = 0x83 + bootable = "true" + image = "rootfs.img" + } } ``` @@ -2461,7 +2633,7 @@ genimage \ 准备完成,文件如下所示 -![image-20231216192612594](assets/post/README/image-20231216192612594.png) +![8986491d-003b-479e-9ef0-01f3c93ca43c.png](assets/post/README/1702731236382-8986491d-003b-479e-9ef0-01f3c93ca43c.png) 运行命令进行打包 @@ -2470,693 +2642,468 @@ chmod 777 genimage.sh ./genimage.sh -c genimage.cfg ``` -![image-20231216192702018](assets/post/README/image-20231216192702018.png) +![1ad6cdd4-59b6-4089-a5f4-2aac0e3538ef.png](assets/post/README/1702731309228-1ad6cdd4-59b6-4089-a5f4-2aac0e3538ef.png) 打包完成,可以找到 `sdcard.img` -![image-20231216192757467](assets/post/README/image-20231216192757467.png) - 使用软件烧录固件到TF卡上 -![image-20231216192825808](assets/post/README/image-20231216192825808.png) - -## 测试 - -插卡,上电,成功启动系统 - -![image-20231216193758046](assets/post/README/image-20231216193758046.png) - -可以看到 Linux 版本是 6.7.0 - -![image-20231216193814799](assets/post/README/image-20231216193814799.png) - -# Buildroot 开发 - -## 获取源码 - -- 网盘链接:https://pan.baidu.com/s/19QFDR_ssy6SJeRMzm5lVDw?pwd=b4nh 提取码:b4nh - -## 解压配置 +![d06e037d-102f-46cc-80c1-49b47f72b8b1.png](assets/post/README/1702731317182-d06e037d-102f-46cc-80c1-49b47f72b8b1.png) -```shell -ubuntu@ubuntu1804:~$ cd buildroot-2023.02.8/ -ubuntu@ubuntu1804:~/buildroot-2023.02.8$ ls -arch boot Config.in configs DEVELOPERS docs linux Makefile.legacy package support toolchain -board CHANGES Config.in.legacy COPYING dl fs Makefile output README system utils -ubuntu@ubuntu1804:~/buildroot-2023.02.8$ ls configs/tinyvision_defconfig -configs/tinyvision_defconfig -ubuntu@ubuntu1804:~/buildroot-2023.02.8$ -``` +# 主线内核开发 -## 编译 +## SyterKit -```shell -ubuntu@ubuntu1804:~/buildroot-2023.02.8$ make tinyvision_defconfig -# -# configuration written to /home/ubuntu/buildroot-2023.02.8/.config -# -ubuntu@ubuntu1804:~/buildroot-2023.02.8$ make -/usr/bin/make -j1 O=/home/ubuntu/buildroot-2023.02.8/output HOSTCC="/usr/bin/gcc" HOSTCXX="/usr/bin/g++" syncconfig -``` +SyterKit 是一个纯裸机框架,用于 TinyVision 或者其他 v851se/v851s/v851s3/v853 等芯片的开发板,SyterKit 使用 CMake 作为构建系统构建,支持多种应用与多种外设驱动。同时 SyterKit 也具有启动引导的功能,可以替代 U-Boot 实现快速启动 -注意: 不要使用 `make clean` 命令 清理仓库。 +### 获取 SyterKit 源码 -## 烧写 +SyterKit 源码位于GitHub,可以前往下载。 ```shell -ubuntu@ubuntu1804:~/buildroot-2023.02.8$ ls output/images/ -boot.vfat rootfs.ext4 sun8i-v851se-tinyvision.dtb sunxi.dtb tinyvision_sdcard.img -rootfs.ext2 rootfs.tar sun8i-v851s-tinyvision.dtb syter_boot_bin.bin zImage -ubuntu@ubuntu1804:~/buildroot-2023.02.8$ +git clone https://github.com/YuzukiHD/SyterKit.git ``` -系统编译完成后,镜像输出在 `output/images/` 目录下,名称为 `tinyvision_sdcard.img` 使用 `dd if` 命令 完整写入sd卡设备,或者 使用 Win32diskimage 工具。 或者使用 balenaEtcher 等 进行烧录。 - -# Tina Linux NPU 开发 - -TinyVision V851s 使用 OpenCV + NPU 实现 Mobilenet v2 物体识别。上一篇已经介绍了如何使用 TinyVision 与 OpenCV 开摄像头,本篇将使用已经训练完成并且转换后的模型来介绍对接 NPU 实现物体识别的功能。 - -## MobileNet V2 - -MobileNet V2是一种轻量级的卷积神经网络(CNN)架构,专门设计用于在移动设备和嵌入式设备上进行计算资源受限的实时图像分类和目标检测任务。 - -以下是MobileNet V2的一些关键特点和创新之处: - -1. Depthwise Separable Convolution(深度可分离卷积):MobileNet V2使用了深度可分离卷积,将标准卷积分解为两个步骤:depthwise convolution(深度卷积)和pointwise convolution(逐点卷积)。这种分解方式可以显著减少计算量和参数数量,从而提高模型的轻量化程度。 - -2. Inverted Residuals with Linear Bottlenecks(带线性瓶颈的倒残差结构):MobileNet V2引入了带有线性瓶颈的倒残差结构,以增加模型的非线性表示能力。这种结构在每个残差块的中间层采用较低维度的逐点卷积来减少计算量,并使用扩张卷积来增加感受野,使网络能够更好地捕捉图像中的细节和全局信息。 - -3. Width Multiplier(宽度乘数):MobileNet V2提供了一个宽度乘数参数,可以根据计算资源的限制来调整模型的宽度。通过减少每个层的通道数,可以进一步减小模型的体积和计算量,适应不同的设备和应用场景。 - -4. Linear Bottlenecks(线性瓶颈):为了减少非线性激活函数对模型性能的影响,MobileNet V2使用线性激活函数来缓解梯度消失问题。这种线性激活函数在倒残差结构的中间层中使用,有助于提高模型的收敛速度和稳定性。 - -总体而言,MobileNet V2通过深度可分离卷积、倒残差结构和宽度乘数等技术,实现了较高的模型轻量化程度和计算效率,使其成为在资源受限的移动设备上进行实时图像分类和目标检测的理想选择。 - -## NPU - -V851s 芯片内置一颗 NPU,其处理性能为最大 0.5 TOPS 并有 128KB 内部高速缓存用于高速数据交换 +### 从零构建 SyterKit -### NPU 系统架构 +构建 SyterKit 非常简单,只需要在 Linux 操作系统中安装配置环境即可编译。SyterKit 需要的软件包有: -NPU 的系统架构如下图所示: +- `gcc-arm-none-eabi` +- `CMake` -![image-20220712100607889](assets/post/README/image-20220712100607889.png) +对于常用的 Ubuntu 系统,可以通过如下命令安装 -上层的应用程序可以通过加载模型与数据到 NPU 进行计算,也可以使用 NPU 提供的软件 API 操作 NPU 执行计算。 +```shell +sudo apt-get update +sudo apt-get install gcc-arm-none-eabi cmake build-essential -y +``` -NPU包括三个部分:可编程引擎(Programmable Engines,PPU)、神经网络引擎(Neural Network Engine,NN)和各级缓存。 +然后新建一个文件夹存放编译的输出文件,并且进入这个文件夹 -可编程引擎可以使用 EVIS 硬件加速指令与 Shader 语言进行编程,也可以实现激活函数等操作。 +```shell +mkdir build +cd build +``` -神经网络引擎包含 NN 核心与 Tensor Process Fabric(TPF,图中简写为 Fabric) 两个部分。NN核心一般计算卷积操作, Tensor Process Fabric 则是作为 NN 核心中的高速数据交换的通路。算子是由可编程引擎与神经网络引擎共同实现的。 +然后运行命令编译 SyterKit -NPU 支持 UINT8,INT8,INT16 三种数据格式。 +```shell +cmake .. +make +``` -### NPU 模型转换 +![image-20231216174136154](assets/post/README/image-20231216174136154.png) -NPU 使用的模型是 NPU 自定义的一类模型结构,不能直接将网络训练出的模型直接导入 NPU 进行计算。这就需要将网络训练出的转换模型到 NPU 的模型上。 +编译后的可执行文件位于 `build/app` 中,这里包括 SyterKit 的多种APP可供使用。 -NPU 的模型转换步骤如下图所示: +![image-20231216173846369](assets/post/README/image-20231216173846369.png) -![image-20220712113105463](assets/post/README/image-20220712112951142.png) +这里我们使用的是 `syter_boot` 作为启动引导。进入 syter_boot 文件夹,可以看到这些文件 -NPU 模型转换包括准备阶段、量化阶段与验证阶段。 +![image-20231216174210790](assets/post/README/image-20231216174210790.png) -#### 准备阶段 +由于 TinyVision 是 TF 卡启动,所以我们需要用到 `syter_boot_bin_card.bin` -首先我们把准备好模型使用工具导入,并创建配置文件。 +![image-20231216174311727](assets/post/README/image-20231216174311727.png) -这时候工具会把模型导入并转换为 NPU 所使用的网络模型、权重模型与配置文件。 +## 移植 Linux 6.7 主线 -配置文件用于对网络的输入和输出的参数进行描述以及配置。这些参数包括输入/输出 tensor 的形状、归一化系数 (均值/零点)、图像格式、tensor 的输出格式、后处理方式等等。 +有了启动引导,接下来是移植 Linux 6.7 主线,前往 https://kernel.org/ 找到 Linux 6.7,选择 `tarball` 下载 -#### 量化阶段 +![image-20231216174444070](assets/post/README/image-20231216174444070.png) -由于训练好的神经网络对数据精度以及噪声的不敏感,因此可以通过量化将参数从浮点数转换为定点数。这样做有两个优点: +下载后解压缩 -(1)减少了数据量,进而可以使用容量更小的存储设备,节省了成本; +```shell +tar xvf linux-6.7-rc5.tar.gz +``` -(2)由于数据量减少,浮点转化为定点数也大大降低了系统的计算量,也提高了计算的速度。 +进入 linux 6.7 目录,开始移植相关驱动。 -但是量化也有一个致命缺陷——会导致精度的丢失。 +### 搭建 Kernel 相关环境 -由于浮点数转换为定点数时会大大降低数据量,导致实际的权重参数准确度降低。在简单的网络里这不是什么大问题,但是如果是复杂的多层多模型的网络,每一层微小的误差都会导致最终数据的错误。 +Kernel 编译需要一些软件包,需要提前安装。 -那么,可以不量化直接使用原来的数据吗?当然是可以的。 +```plaintext +sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf build-essential libncurses5-dev zlib1g-dev gawk flex bison quilt libssl-dev xsltproc libxml-parser-perl mercurial bzr ecj cvs unzip lsof +``` -但是由于使用的是浮点数,无法将数据导入到只支持定点运算的 NN 核心进行计算,这就需要可编程引擎来代替 NN 核进行计算,这样可以大大降低运算效率。 +安装完成后可以尝试编译一下,看看能不能编译通过,先应用配置文件 -另外,在进行量化过程时,不仅对参数进行了量化,也会对输入输出的数据进行量化。如果模型没有输入数据,就不知道输入输出的数据范围。这时候我们就需要准备一些具有代表性的输入来参与量化。这些输入数据一般从训练模型的数据集里获得,例如图片数据集里的图片。 +```plaintext +CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm sunxi_defconfig +``` -另外选择的数据集不一定要把所有训练数据全部加入量化,通常我们选择几百张能够代表所有场景的输入数据就即可。理论上说,量化数据放入得越多,量化后精度可能更好,但是到达一定阈值后效果增长将会非常缓慢甚至不再增长。 +![image-20231216181640653](assets/post/README/image-20231216181640653.png) -#### 验证阶段 +然后尝试编译 -由于上一阶段对模型进行了量化导致了精度的丢失,就需要对每个阶段的模型进行验证,对比结果是否一致。 +```plaintext +CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm +``` -首先我们需要使用非量化情况下的模型运行生成每一层的 tensor 作为 Golden tensor。输入的数据可以是数据集中的任意一个数据。然后量化后使用预推理相同的数据再次输出一次 tensor,对比这一次输出的每一层的 tensor 与 Golden tensor 的差别。 +可以用 `-j32` 来加速编译,`32` 指的是使用32线程编译,一般cpu有几个核心就设置几线程 -如果差别较大可以尝试更换量化模型和量化方式。差别不大即可使用 IDE 进行仿真。也可以直接部署到 V851s 上进行测试。 +```plaintext +CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm -j32 +``` -此时测试同样会输出 tensor 数据,对比这一次输出的每一层的 tensor 与 Golden tensor 的差别,差别不大即可集成到 APP 中了。 +正常编译 -### NPU 的开发流程 +![image-20231216183011911](assets/post/README/image-20231216183011911.png) -NPU 开发完整的流程如下图所示: +### 移植 clk 驱动 -![image-20240126194601436](assets/post/README/image-20240126194601436.png) +这里提供已经适配修改后的驱动:https://github.com/YuzukiHD/TinyVision/tree/main/kernel/linux-6.7-driver 可以直接使用。 -#### 模型训练 +也可以参考 https://github.com/YuzukiHD/TinyVision/tree/main/kernel/bsp/drivers/clk 中的驱动移植。 -在模型训练阶段,用户根据需求和实际情况选择合适的框架(如Caffe、TensorFlow 等)使用数据集进行训练得到符合需求的模型,此模型可称为预训练模型。也可直接使用已经训练好的模型。V851s 的 NPU 支持包括分类、检测、跟踪、人脸、姿态估计、分割、深度、语音、像素处理等各个场景90 多个公开模型。 +进入文件夹 `include/dt-bindings/clock/` 新建文件 `sun8i-v851se-ccu.h` ,将 CLK 填入 -#### 模型转换 +![image-20231216182350741](assets/post/README/image-20231216182350741.png) -在模型转化阶段,通过Acuity Toolkit 把预训练模型和少量训练数据转换为NPU 可用的模型NBG文件。 -一般步骤如下: +进入 `include/dt-bindings/reset` 新建文件 `sun8i-v851se-ccu.h` 将 RST 填入 -1. 模型导入,生成网络结构文件、网络权重文件、输入描述文件和输出描述文件。 -2. 模型量化,生成量化描述文件和熵值文件,可改用不同的量化方式。 -3. 仿真推理,可逐一对比float 和其他量化精度的仿真结果的相似度,评估量化后的精度是否满足要求。 -4. 模型导出,生成端侧代码和*.nb 文件,可编辑输出描述文件的配置,配置是否添加后处理节点等。 +![image-20231216182941392](assets/post/README/image-20231216182941392.png) -#### 模型部署及应用开发 +进入 `drivers/clk/sunxi-ng` 找到 `sunxi-ng` clk 驱动,复制文件`ccu-sun20i-d1.c` 和 `ccu-sun20i-d1.h` 文件并改名为 `ccu-sun8i-v851se.c` ,`ccu-sun8i-v851se.h` 作为模板。 -在模型部署阶段,就是基于VIPLite API 开发应用程序实现业务逻辑。 +![image-20231216180413415](assets/post/README/image-20231216180413415.png) -## OpenCV + NPU 源码解析 +将文件中的 `SUN20I_D1` 改为 `SUN8I_V851SE` -完整的代码已经上传Github开源,前往以下地址:https://github.com/YuzukiHD/TinyVision/tree/main/tina/openwrt/package/thirdparty/vision/opencv_camera_mobilenet_v2_ssd/src +![image-20231216180653502](assets/post/README/image-20231216180653502.png) -**【[注意,运行这里的DEMO请先移植LCD驱动!! ](#lcd-模组驱动)】** +打开芯片数据手册[V851SX_Datasheet_V1.2.pdf](https://github.com/YuzukiHD/TinyVision/blob/main/docs/hardware/TinyVision/datasheet/V851SX_Datasheet_V1.2.pdf),找到 CCU 章节 -否则报错 `Unsupported depth of framebuffer` +![image-20231216180748419](assets/post/README/image-20231216180748419.png) -![image-20240320180529300](assets/post/README/image-20240320180529300.png) +对照手册编写驱动文件适配 V851se 平台。 -### Mobilenet v2 前处理 +然后找到 `drivers/clk/sunxi-ng/Kconfig` 文件,增加刚才编写的驱动的 Kconfig 说明 -```c -void get_input_data(const cv::Mat& sample, uint8_t* input_data, int input_h, int input_w, const float* mean, const float* scale){ - cv::Mat img; - if (sample.channels() == 1) - cv::cvtColor(sample, img, cv::COLOR_GRAY2RGB); - else - cv::cvtColor(sample, img, cv::COLOR_BGR2RGB); - cv::resize(img, img, cv::Size(input_h, input_w)); - uint8_t* img_data = img.data; - /* nhwc to nchw */ - for (int h = 0; h < input_h; h++) { - for (int w = 0; w < input_w; w++) { - for (int c = 0; c < 3; c++) { - int in_index = h * input_w * 3 + w * 3 + c; - int out_index = c * input_h * input_w + h * input_w + w; - input_data[out_index] = (uint8_t)(img_data[in_index]); //uint8 - } - } - } -} +![image-20231216181118674](assets/post/README/image-20231216181118674.png) -uint8_t *mbv2_ssd_preprocess(const cv::Mat& sample, int input_size, int img_channel) { - const float mean[3] = {127, 127, 127}; - const float scale[3] = {0.0078125, 0.0078125, 0.0078125}; - int img_size = input_size * input_size * img_channel; - uint8_t *tensor_data = NULL; - tensor_data = (uint8_t *)malloc(1 * img_size * sizeof(uint8_t)); - get_input_data(sample, tensor_data, input_size, input_size, mean, scale); - return tensor_data; -} +```plaintext +config SUN8I_V851SE_CCU + tristate "Support for the Allwinner V851se CCU" + default y + depends on MACH_SUN8I || COMPILE_TEST ``` -这段C++代码是用于对输入图像进行预处理,以便输入到MobileNet V2 SSD模型中进行目标检测。 +同时打开 `drivers/clk/sunxi-ng/Makefile` -1. `get_input_data`函数: - - 该函数对输入的图像进行预处理,将其转换为适合MobileNet V2 SSD模型输入的格式。 - - 首先,对输入图像进行通道格式的转换,确保图像通道顺序符合模型要求(RGB格式)。 - - 然后,将图像大小调整为指定的输入尺寸(`input_h * input_w`)。 - - 最后,将处理后的图像数据按照特定顺序(NCHW格式)填充到`input_data`数组中,以便作为模型的输入数据使用。 +![image-20231216181248375](assets/post/README/image-20231216181248375.png) -2. `mbv2_ssd_preprocess`函数: - - 该函数是对输入图像进行 MobileNet V2 SSD 模型的预处理,并返回处理后的数据。 - - 在函数内部,首先定义了图像各通道的均值(mean)和缩放比例(scale)。 - - 然后计算了输入图像的总大小,并分配了相应大小的内存空间用于存储预处理后的数据。 - - 调用了`get_input_data`函数对输入图像进行预处理,将处理后的数据存储在`tensor_data`中,并最终返回该数据指针。 +```plaintext +obj-$(CONFIG_SUN8I_V851SE_CCU) += sun8i-v851se-ccu.o -总的来说,这段代码的功能是将输入图像进行预处理,以适应MobileNet V2 SSD模型的输入要求,并返回预处理后的数据供模型使用。同时需要注意,在使用完`tensor_data`后,需要在适当的时候释放相应的内存空间,以避免内存泄漏问题。 +sun8i-v851se-ccu-y += ccu-sun8i-v851se.o +``` -### Mobilenet v2 后处理 +来检查一下是否移植成功,先查看 `menuconfig`,找到 `Device Drivers > Common Clock Framework`,查看是否有 V851se 平台选项出现 -这部分分为来讲: +```plaintext +CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm menuconfig +``` -```cpp -// 比较函数,用于按照分数对Bbox_t对象进行排序 -bool comp(const Bbox_t &a, const Bbox_t &b) { - return a.score > b.score; -} +![image-20231216183207387](assets/post/README/image-20231216183207387.png) -// 计算两个框之间的交集面积 -static inline float intersection_area(const Bbox_t& a, const Bbox_t& b) { - // 将框表示为cv::Rect_对象 - cv::Rect_ rect_a(a.xmin, a.ymin, a.xmax-a.xmin, a.ymax-a.ymin); - cv::Rect_ rect_b(b.xmin, b.ymin, b.xmax-b.xmin, b.ymax-b.ymin); - - // 计算两个矩形的交集 - cv::Rect_ inter = rect_a & rect_b; - - // 返回交集的面积 - return inter.area(); -} +编译测试,有几处未使用的变量的警告,无视即可。 -// 非极大值抑制算法(NMS) -static void nms_sorted_bboxes(const std::vector& bboxs, std::vector& picked, float nms_threshold) { - picked.clear(); - const int n = bboxs.size(); - - // 创建存储每个框面积的向量 - std::vector areas(n); - - // 计算每个框的面积并存储 - for (int i = 0; i < n; i++){ - areas[i] = (bboxs[i].xmax - bboxs[i].xmin) * (bboxs[i].ymax - bboxs[i].ymin); - } - - // 对每个框进行遍历 - for (int i = 0; i < n; i++) { - const Bbox_t& a = bboxs[i]; - int keep = 1; - - // 对已经选择的框进行遍历 - for (int j = 0; j < (int)picked.size(); j++) { - const Bbox_t& b = bboxs[picked[j]]; - - // 计算交集和并集面积 - float inter_area = intersection_area(a, b); - float union_area = areas[i] + areas[picked[j]] - inter_area; - - // 计算交并比 - if (inter_area / union_area > nms_threshold) - keep = 0; // 如果交并比大于阈值,则不选择该框 - } - - // 如果符合条件则选择该框,加入到结果向量中 - if (keep) - picked.push_back(i); - } -} +```plaintext +CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm ``` -这段代码实现了目标检测中常用的非极大值抑制算法(NMS)。`comp`函数用于对`Bbox_t`对象按照分数进行降序排序。`intersection_area`函数用于计算两个框之间的交集面积。`nms_sorted_bboxes`函数是NMS算法的具体实现,它接受一个已经按照分数排序的框的向量`bboxs`,以及一个空的整数向量`picked`,用于存储保留下来的框的索引。`nms_threshold`是一个阈值,用于控制重叠度。 - -算法的步骤如下: - -1. 清空存储结果的`picked`向量。 -2. 获取框的个数`n`,创建一个用于存储每个框面积的向量`areas`。 -3. 遍历每个框,计算其面积并存储到`areas`向量中。 -4. 对每个框进行遍历,通过计算交并比来判断是否选择该框。如果交并比大于阈值,则不选择该框。 -5. 如果符合条件,则选择该框,将其索引加入到`picked`向量中。 -6. 完成非极大值抑制算法,`picked`向量中存储了保留下来的框的索引。 +![image-20231216183406918](assets/post/README/image-20231216183406918.png) -这个算法的作用是去除高度重叠的框,只保留得分最高的那个框,以减少冗余检测结果。 +正常编译成功 -```c -cv::Mat detect_ssd(const cv::Mat& bgr, float **output) { - // 定义阈值和常数 - float iou_threshold = 0.45; - float conf_threshold = 0.5; - const int inputH = 300; - const int inputW = 300; - const int outputClsSize = 21; -#if MBV2_SSD - int output_dim_1 = 3000; -#else - int output_dim_1 = 8732; -#endif +### 移植 pinctrl 驱动 - // 计算输出数据的大小 - int size0 = 1 * output_dim_1 * outputClsSize; - int size1 = 1 * output_dim_1 * 4; +这里提供已经适配修改后的驱动:https://github.com/YuzukiHD/TinyVision/tree/main/kernel/linux-6.7-driver 可以直接使用。 - // 将输出数据转换为向量 - std::vector scores_data(output[0], &output[0][size0-1]); - std::vector boxes_data(output[1], &output[1][size1-1]); +前往`drivers/pinctrl/sunxi/` 新建文件 `pinctrl-sun8i-v851se.c` - // 获取分数和边界框的指针 - const float* scores = scores_data.data(); - const float* bboxes = boxes_data.data(); +![image-20231216183716548](assets/post/README/image-20231216183716548.png) - // 计算缩放比例 - float scale_w = bgr.cols / (float)inputW; - float scale_h = bgr.rows / (float)inputH; - bool pass = true; +打开 [V851SE_PINOUT_V1.0.xlsx](https://github.com/YuzukiHD/TinyVision/blob/main/docs/hardware/TinyVision/datasheet/V851SE_PINOUT_V1.0.xlsx) 对照填入PIN的值与功能。 - // 创建存储检测结果的向量 - std::vector BBox; +![image-20231216183825726](assets/post/README/image-20231216183825726.png) - // 遍历每个框 - for(int i = 0; i < output_dim_1; i++) { - std::vector conf; - // 获取每个框的置信度 - for(int j = 0; j < outputClsSize; j++) { - conf.emplace_back(scores[i * outputClsSize + j]); - } - // 找到置信度最大的类别 - int max_index = std::max_element(conf.begin(), conf.end()) - conf.begin(); - // 如果类别不是背景类,并且置信度大于阈值,则选中该框 - if (max_index != 0) { - if(conf[max_index] < conf_threshold) - continue; - Bbox_t b; - // 根据缩放比例计算框的坐标和尺寸 - int left = bboxes[i * 4] * scale_w * 300; - int top = bboxes[i * 4 + 1] * scale_h * 300; - int right = bboxes[ i * 4 + 2] * scale_w * 300; - int bottom = bboxes[i * 4 + 3] * scale_h * 300; - // 确保坐标不超出图像范围 - b.xmin = std::max(0, left); - b.ymin = std::max(0, top); - b.xmax = right; - b.ymax = bottom; - b.score = conf[max_index]; - b.cls_idx = max_index; - BBox.emplace_back(b); - } - conf.clear(); - } +同样的,修改 `drivers/pinctrl/sunxi/Kconfig` 增加选项 - // 按照分数对框进行排序 - std::sort(BBox.begin(), BBox.end(), comp); +![image-20231216184038601](assets/post/README/image-20231216184038601.png) - // 应用非极大值抑制算法,获取保留的框的索引 - std::vector keep_index; - nms_sorted_bboxes(BBox, keep_index, iou_threshold); +修改 `drivers/pinctrl/sunxi/Makefile` 增加路径 - // 创建存储框位置的向量 - std::vector bbox_per_frame; +![image-20231216184126988](assets/post/README/image-20231216184126988.png) - // 遍历保留的框,绘制框和标签 - for(int i = 0; i < keep_index.size(); i++) { - int left = BBox[keep_index[i]].xmin; - int top = BBox[keep_index[i]].ymin; - int right = BBox[keep_index[i]].xmax; - int bottom = BBox[keep_index[i]].ymax; - int width = right - left; - int height = bottom - top; - int center_x = left + width / 2; - cv::rectangle(bgr, cv::Point(left, top), cv::Point(right, bottom), cv::Scalar(0, 0, 255), 1); - char text[256]; - sprintf(text, "%s %.1f%%", class_names[BBox[keep_index[i]].cls_idx], BBox[keep_index[i]].score * 100); - cv::putText(bgr, text, cv::Point(left, top), cv::FONT_HERSHEY_COMPLEX, 1, cv::Scalar(0, 255, 255), 1, 8, 0); - bbox_per_frame.emplace_back(left, top, width, height); - } +来检查一下是否移植成功,先查看 `menuconfig`,找到 `> Device Drivers > Pin controllers`,查看是否有 V851se 平台选项出现 - // 返回绘制了框和标签的图像 - return bgr; -} +```plaintext +CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm menuconfig ``` -这段代码主要用于处理模型的输出结果,将输出数据转换为向量,并计算缩放比例,然后创建一个向量来存储检测结果。 +![image-20231216184259987](assets/post/README/image-20231216184259987.png) -具体步骤如下: +编译测试,编译通过 -1. 定义了一些阈值和常数,包括IOU阈值(`iou_threshold`)、置信度阈值(`conf_threshold`)、输入图像的高度和宽度(`inputH`和`inputW`)、输出类别数量(`outputClsSize`)、输出维度(`output_dim_1`)。 -2. 计算输出数据的大小,分别为类别得分数据的大小(`size0`)和边界框数据的大小(`size1`)。 -3. 将输出数据转换为向量,分别为类别得分数据向量(`scores_data`)和边界框数据向量(`boxes_data`)。 -4. 获取类别得分和边界框的指针,分别为`scores`和`bboxes`。 -5. 计算图像的缩放比例,根据输入图像的尺寸和模型输入尺寸之间的比例计算得到。 -6. 创建一个向量`BBox`,用于存储检测结果。该向量的类型为`Bbox_t` -7. 遍历每一个框(共有`output_dim_1`个框)。 -8. 获取每一个框的各个类别的置信度,并将其存储在`conf`向量中。 -9. 找到置信度最大的类别,并记录其下标`max_index`。 -10. 如果最大置信度的类别不是背景类,并且置信度大于设定的阈值,则选中该框。 -11. 根据缩放比例计算框的坐标和尺寸,其中`left`、`top`、`right`和`bottom`分别表示框的左上角和右下角的坐标。 -12. 确保框的坐标不超出图像范围,并将目标框的信息(包括位置、置信度、类别等)存储在`Bbox_t`类型的变量`b`中。 -13. 将`b`加入到`BBox`向量中。 -14. 清空`conf`向量,为下一个框的检测做准备。 -15. 对所有检测到的目标框按照置信度从高到低排序; -16. 应用非极大值抑制算法,筛选出重叠度较小的目标框,并将保留的目标框的索引存储在`keep_index`向量中; -17. 遍历保留的目标框,对每个目标框进行绘制和标注; -18. 在图像上用矩形框标出目标框的位置和大小,并在矩形框内添加目标类别和置信度; -19. 将绘制好的目标框信息(包括左上角坐标、宽度和高度)存储在`bbox_per_frame`向量中; -20. 返回绘制好的图像。 +```plaintext +CROSS_COMPILE=arm-linux-gnueabihf- make ARCH=arm +``` -需要注意的是,该代码使用了OpenCV库中提供的绘制矩形框和添加文字的相关函数。其中`cv::rectangle()`函数用于绘制矩形框,`cv::putText()`函数用于在矩形框内添加目标类别和置信度。 +![image-20231216184649676](assets/post/README/image-20231216184649676.png) -### 获取显示屏的参数信息 +### 编写设备树 -```c -// 帧缓冲器信息结构体,包括每个像素的位数和虚拟分辨率 -struct framebuffer_info { - uint32_t bits_per_pixel; - uint32_t xres_virtual; -}; +这里提供已经适配修改后的驱动:https://github.com/YuzukiHD/TinyVision/tree/main/kernel/linux-6.7-driver/dts 可以直接使用。 -// 获取帧缓冲器的信息函数,接受设备路径作为参数 -struct framebuffer_info get_framebuffer_info(const char* framebuffer_device_path) -{ - struct framebuffer_info info; - struct fb_var_screeninfo screen_info; - int fd = -1; +![image-20231216185413254](assets/post/README/image-20231216185413254.png) - // 打开设备文件 - fd = open(framebuffer_device_path, O_RDWR); +这部分直接给结果了,把上面适配的设备树放到`/home/yuzuki/WorkSpace/aa/linux-6.7-rc5/arch/arm/boot/dts/allwinner/` ,修改 `/home/yuzuki/WorkSpace/aa/linux-6.7-rc5/arch/arm/boot/dts/allwinner/Makefile` - // 如果成功打开设备文件,则使用 ioctl 函数获取屏幕信息 - if (fd >= 0) { - if (!ioctl(fd, FBIOGET_VSCREENINFO, &screen_info)) { - info.xres_virtual = screen_info.xres_virtual; // 虚拟分辨率 - info.bits_per_pixel = screen_info.bits_per_pixel; // 像素位数 - } - } +![image-20231216185113539](assets/post/README/image-20231216185113539.png) - return info; -}; +```plaintext +sun8i-v851se-tinyvision.dtb ``` -这段代码的用途是获取帧缓冲器的信息。 +![image-20231216185530270](assets/post/README/image-20231216185530270.png) -具体来说: +## 生成刷机镜像 -1. `framebuffer_info` 是一个结构体,用于存储帧缓冲器的信息,包括每个像素的位数和虚拟分辨率。 +编译内核后,可以在文件夹 `arch/arm/boot/dts/allwinner` 生成`sun8i-v851se-tinyvision.dtb` ,在文件夹`arch/arm/boot` 生成 `zImage` ,把他们拷贝出来。 -2. `get_framebuffer_info` 是一个函数,用于获取帧缓冲器的信息。它接受帧缓冲器设备路径作为参数,打开设备文件并使用 ioctl 函数获取屏幕信息,然后将信息存储在 `framebuffer_info` 结构体中并返回。 +![image-20231216191248458](assets/post/README/image-20231216191248458.png) -### 信号处理函数 +然后将 `sun8i-v851se-tinyvision.dtb` 改名为 `sunxi.dtb` ,这个设备树名称是定义在 SyterKit 源码中的,如果之前修改了 SyterKit 的源码需要修改到对应的名称,SyterKit 会去读取这个设备树。 -注册信号处理函数,用于 `ctrl-c` 之后关闭摄像头,防止下一次使用摄像头出现摄像头仍被占用的情况。 +然后编写一个 `config.txt` 作为配置文件 -```c++ -/* Signal handler */ -static void terminate(int sig_no) -{ - printf("Got signal %d, exiting ...\n", sig_no); - cap.release(); - exit(1); -} +```plaintext +[configs] +bootargs=cma=4M root=/dev/mmcblk0p2 init=/sbin/init console=ttyS0,115200 earlyprintk=sunxi-uart,0x02500000 rootwait clk_ignore_unused +mac_addr=4a:13:e4:f9:79:75 +bootdelay=3 +``` -static void install_sig_handler(void) -{ - signal(SIGBUS, terminate); // 当程序访问一个不合法的内存地址时发送的信号 - signal(SIGFPE, terminate); // 浮点异常信号 - signal(SIGHUP, terminate); // 终端断开连接信号 - signal(SIGILL, terminate); // 非法指令信号 - signal(SIGINT, terminate); // 中断进程信号 - signal(SIGIOT, terminate); // IOT 陷阱信号 - signal(SIGPIPE, terminate); // 管道破裂信号 - signal(SIGQUIT, terminate); // 停止进程信号 - signal(SIGSEGV, terminate); // 无效的内存引用信号 - signal(SIGSYS, terminate); // 非法系统调用信号 - signal(SIGTERM, terminate); // 终止进程信号 - signal(SIGTRAP, terminate); // 跟踪/断点陷阱信号 - signal(SIGUSR1, terminate); // 用户定义信号1 - signal(SIGUSR2, terminate); // 用户定义信号2 -} +### 安装 genimage + +这里我们使用 genimage 作为打包工具 + +```plaintext +sudo apt-get install libconfuse-dev #安装genimage依赖库 +sudo apt-get install genext2fs # 制作镜像时genimage将会用到 +git clone https://github.com/pengutronix/genimage.git +cd genimage +./autogen.sh # 配置生成configure +./configure # 配置生成makefile +make +sudo make install ``` -这段代码定义了两个函数,并给出了相应的注释说明。具体注释如下: +编译后运行试一试,这里正常 -- `static void terminate(int sig_no)`:信号处理函数。 - - `int sig_no`:接收到的信号编号。 - - `printf("Got signal %d, exiting ...\n", sig_no);`:打印接收到的信号编号。 - - `cap.release();`:释放视频流捕获对象。 - - `exit(1);`:退出程序。 -- `static void install_sig_handler(void)`:安装信号处理函数。 - - `signal(SIGBUS, terminate);`:为SIGBUS信号安装信号处理函数。 - - `signal(SIGFPE, terminate);`:为SIGFPE信号安装信号处理函数。 - - `signal(SIGHUP, terminate);`:为SIGHUP信号安装信号处理函数。 - - `signal(SIGILL, terminate);`:为SIGILL信号安装信号处理函数。 - - `signal(SIGINT, terminate);`:为SIGINT信号安装信号处理函数。 - - `signal(SIGIOT, terminate);`:为SIGIOT信号安装信号处理函数。 - - `signal(SIGPIPE, terminate);`:为SIGPIPE信号安装信号处理函数。 - - `signal(SIGQUIT, terminate);`:为SIGQUIT信号安装信号处理函数。 - - `signal(SIGSEGV, terminate);`:为SIGSEGV信号安装信号处理函数。 - - `signal(SIGSYS, terminate);`:为SIGSYS信号安装信号处理函数。 - - `signal(SIGTERM, terminate);`:为SIGTERM信号安装信号处理函数。 - - `signal(SIGTRAP, terminate);`:为SIGTRAP信号安装信号处理函数。 - - `signal(SIGUSR1, terminate);`:为SIGUSR1信号安装信号处理函数。 - - `signal(SIGUSR2, terminate);`:为SIGUSR2信号安装信号处理函数。 +![image-20231216192512837](assets/post/README/image-20231216192512837.png) + +### 使用 genimage 打包固件 + +编写 genimage.cfg 作为打包的配置 + +```cfg +image boot.vfat { + vfat { + files = { + "zImage", + "sunxi.dtb", + "config.txt" + } + } + size = 8M +} + +image sdcard.img { + hdimage {} + + partition boot0 { + in-partition-table = "no" + image = "syter_boot_bin_card.bin" + offset = 8K + } + + partition boot0-gpt { + in-partition-table = "no" + image = "syter_boot_bin_card.bin" + offset = 128K + } + + partition kernel { + partition-type = 0xC + bootable = "true" + image = "boot.vfat" + } +} +``` -这段代码的功能是安装信号处理函数,用于捕获和处理不同类型的信号。当程序接收到指定的信号时,会调用`terminate`函数进行处理。 +由于genimage的脚本比较复杂,所以编写一个 `genimage.sh` 作为简易使用的工具 -具体而言,`terminate`函数会打印接收到的信号编号,并释放视频流捕获对象`cap`,然后调用`exit(1)`退出程序。 +```sh +#!/usr/bin/env bash -`install_sig_handler`函数用于为多个信号注册同一个信号处理函数`terminate`,使得当这些信号触发时,都会执行相同的处理逻辑。 +die() { + cat <&2 +Error: $@ -### 主循环 +Usage: ${0} -c GENIMAGE_CONFIG_FILE +EOF + exit 1 +} -```cpp -int main(int argc, char *argv[]) -{ - const int frame_width = 480; // 视频帧宽度 - const int frame_height = 480; // 视频帧高度 - const int frame_rate = 30; // 视频帧率 +# Parse arguments and put into argument list of the script +opts="$(getopt -n "${0##*/}" -o c: -- "$@")" || exit $? +eval set -- "$opts" - char* nbg = "/usr/lib/model/mobilenet_v2_ssd.nb"; // 模型文件路径 +GENIMAGE_TMP="${BUILD_DIR}/genimage.tmp" - install_sig_handler(); // 安装信号处理程序 +while true ; do + case "$1" in + -c) + GENIMAGE_CFG="${2}"; + shift 2 ;; + --) # Discard all non-option parameters + shift 1; + break ;; + *) + die "unknown option '${1}'" ;; + esac +done - framebuffer_info fb_info = get_framebuffer_info("/dev/fb0"); // 获取帧缓冲区信息 +[ -n "${GENIMAGE_CFG}" ] || die "Missing argument" - cap.open(0); // 打开视频设备 +# Pass an empty rootpath. genimage makes a full copy of the given rootpath to +# ${GENIMAGE_TMP}/root so passing TARGET_DIR would be a waste of time and disk +# space. We don't rely on genimage to build the rootfs image, just to insert a +# pre-built one in the disk image. - if (!cap.isOpened()) { - std::cerr << "Could not open video device." << std::endl; // 如果打开视频设备失败,则输出错误信息并返回 - return 1; - } +trap 'rm -rf "${ROOTPATH_TMP}"' EXIT +ROOTPATH_TMP="$(mktemp -d)" +GENIMAGE_TMP="$(mktemp -d)" +rm -rf "${GENIMAGE_TMP}" - std::cout << "Successfully opened video device." << std::endl; // 成功打开视频设备,输出成功信息 - cap.set(cv::CAP_PROP_FRAME_WIDTH, frame_width); // 设置视频帧宽度 - cap.set(cv::CAP_PROP_FRAME_HEIGHT, frame_height); // 设置视频帧高度 - cap.set(cv::CAP_PROP_FPS, frame_rate); // 设置视频帧率 - std::ofstream ofs("/dev/fb0"); // 打开帧缓冲区文件 - cv::Mat frame; // 创建用于存储视频帧的 Mat 对象 +genimage \ + --rootpath "${ROOTPATH_TMP}" \ + --tmppath "${GENIMAGE_TMP}" \ + --inputpath "${BINARIES_DIR}" \ + --outputpath "${BINARIES_DIR}" \ + --config "${GENIMAGE_CFG}" +``` - awnn_init(7 * 1024 * 1024); // 初始化 AWNN 库 - Awnn_Context_t *context = awnn_create(nbg); // 创建 AWNN 上下文 - if (NULL == context){ - std::cerr << "fatal error, awnn_create failed." << std::endl; // 如果创建 AWNN 上下文失败,则输出致命错误信息并返回 - return -1; - } - /* copy input */ - uint32_t input_width = 300; // 输入图像宽度 - uint32_t input_height = 300; // 输入图像高度 - uint32_t input_depth = 3; // 输入图像通道数 - uint32_t sz = input_width * input_height * input_depth; // 输入图像数据总大小 +准备完成,文件如下所示 - uint8_t* plant_data = NULL; // 定义输入图像数据指针,初始化为 NULL - - while (true) { - // 从视频设备中读取一帧图像 - cap >> frame; +![image-20231216192612594](assets/post/README/image-20231216192612594.png) - // 检查图像的位深度是否为8位和通道数是否为3 - if (frame.depth() != CV_8U) { - std::cerr << "不是8位每像素和通道。" << std::endl; - } else if (frame.channels() != 3) { - std::cerr << "不是3个通道。" << std::endl; - } else { - // 转置和翻转图像以调整其方向 - cv::transpose(frame, frame); - cv::flip(frame, frame, 0); +运行命令进行打包 - // 将图像大小调整为所需的输入宽度和高度 - cv::resize(frame, frame, cv::Size(input_width, input_height)); +```plaintext +chmod 777 genimage.sh +./genimage.sh -c genimage.cfg +``` - // 对MobileNetV2 SSD模型进行预处理 - plant_data = mbv2_ssd_preprocess(frame, input_width, input_depth); +![image-20231216192702018](assets/post/README/image-20231216192702018.png) - // 设置AWNN上下文的输入缓冲区 - uint8_t *input_buffers[1] = {plant_data}; - awnn_set_input_buffers(context, input_buffers); +打包完成,可以找到 `sdcard.img` - // 运行AWNN上下文进行模型推理 - awnn_run(context); +![image-20231216192757467](assets/post/README/image-20231216192757467.png) - // 从AWNN上下文中获取输出缓冲区 - float **results = awnn_get_output_buffers(context); +使用软件烧录固件到TF卡上 - // 使用SSD模型进行目标检测并更新图像 - frame = detect_ssd(frame, results); +![image-20231216192825808](assets/post/README/image-20231216192825808.png) - // 将图像大小调整为显示尺寸 - cv::resize(frame, frame, cv::Size(DISPLAY_X, DISPLAY_Y)); +## 测试 - // 获取帧缓冲区的宽度和位深度 - int framebuffer_width = fb_info.xres_virtual; - int framebuffer_depth = fb_info.bits_per_pixel; +插卡,上电,成功启动系统 - // 根据帧缓冲区的位深度将图像转换为兼容格式 - cv::Size2f frame_size = frame.size(); - cv::Mat framebuffer_compat; - switch (framebuffer_depth) { - case 16: - // 将BGR转换为BGR565格式以适用于16位帧缓冲区 - cv::cvtColor(frame, framebuffer_compat, cv::COLOR_BGR2BGR565); +![image-20231216193758046](assets/post/README/image-20231216193758046.png) - // 将转换后的图像写入帧缓冲区文件 - for (int y = 0; y < frame_size.height; y++) { - ofs.seekp(y * framebuffer_width * 2); - ofs.write(reinterpret_cast(framebuffer_compat.ptr(y)), frame_size.width * 2); - } - break; - case 32: - // 将图像分解为BGR通道并添加一个alpha通道以适用于32位帧缓冲区 - std::vector split_bgr; - cv::split(frame, split_bgr); - split_bgr.push_back(cv::Mat(frame_size, CV_8UC1, cv::Scalar(255))); - cv::merge(split_bgr, framebuffer_compat); +可以看到 Linux 版本是 6.7.0 - // 将转换后的图像写入帧缓冲区文件 - for (int y = 0; y < frame_size.height; y++) { - ofs.seekp(y * framebuffer_width * 4); - ofs.write(reinterpret_cast(framebuffer_compat.ptr(y)), frame_size.width * 4); - } - break; - default: - std::cerr << "不支持的帧缓冲区位深度。" << std::endl; - } +![image-20231216193814799](assets/post/README/image-20231216193814799.png) - // 释放为plant_data分配的内存空间 - free(plant_data); - } -} +# Buildroot 开发 + +buildroot系统是一套基于Makefile管理的构建系统框架 + +``` ba +ubuntu@ubuntu1804:~/buildroot-2023.02.8$ tree -L 1 +. +├── arch +├── board +├── boot +├── CHANGES +├── Config.in +├── Config.in.legacy +├── configs +├── COPYING +├── defconfig +├── DEVELOPERS +├── dl +├── docs +├── fs +├── linux +├── Makefile +├── Makefile.legacy +├── output +├── package +├── README +├── support +├── system +├── toolchain +└── utils + +15 directories, 9 files +ubuntu@ubuntu1804:~/buildroot-2023.02.8$ ``` -这段代码主要实现了以下功能: +## 获取源码 -1. 定义了视频帧的宽度、高度和帧率。 -2. 指定了模型文件的路径。 -3. 安装信号处理程序。 -4. 获取帧缓冲区的信息。 -5. 打开视频设备,并设置视频帧的宽度、高度和帧率。 -6. 打开帧缓冲区文件,用于后续操作。 -7. 初始化 AWNN 库,并分配一定大小的内存。 -8. 创建 AWNN 上下文。 -9. 定义输入图像的宽度、高度和通道数,并计算输入图像数据的总大小。 -10. 声明一个输入图像数据指针。 +- 网盘链接:https://pan.baidu.com/s/19QFDR_ssy6SJeRMzm5lVDw?pwd=b4nh 提取码:b4nh -11. 主循环函数,用于不断从视频设备中获取视频帧并进行处理和展示。 +## 解压配置 -具体的步骤如下: +```shell +ubuntu@ubuntu1804:~$ cd buildroot-2023.02.8/ +ubuntu@ubuntu1804:~/buildroot-2023.02.8$ ls +arch boot Config.in configs DEVELOPERS docs linux Makefile.legacy package support toolchain +board CHANGES Config.in.legacy COPYING dl fs Makefile output README system utils +ubuntu@ubuntu1804:~/buildroot-2023.02.8$ ls configs/tinyvision_defconfig +configs/tinyvision_defconfig +ubuntu@ubuntu1804:~/buildroot-2023.02.8$ +``` -1. 使用`cap`对象从视频设备中获取一帧图像,并将其存储在`frame`中。 -2. 检查图像的位深度是否为8位(CV_8U),如果不是,则输出错误信息。 -3. 检查图像的通道数是否为3,如果不是,则输出错误信息。 -4. 对图像进行转置和翻转操作,以调整图像的方向。 -5. 将图像的大小调整为设定的输入宽度和高度。 -6. 调用`mbv2_ssd_preprocess`函数对图像进行预处理,并将结果存储在`plant_data`中。 -7. 将`plant_data`设置为AWNN上下文的输入缓冲区。 -8. 运行AWNN上下文,执行模型推理。 -9. 使用`detect_ssd`函数对图像进行目标检测,得到检测结果的可视化图像。 -10. 将图像的大小调整为设定的显示宽度和高度。 -11. 根据帧缓冲区的位深度,将图像转换为与帧缓冲区兼容的格式,并写入帧缓冲区文件。 -12. 释放`plant_data`的内存空间。 -13. 循环回到第1步,继续获取和处理下一帧图像。 +## 编译 -这段代码主要完成了从视频设备获取图像、预处理图像、执行模型推理、目标检测和将结果写入帧缓冲区文件等一系列操作,以实现实时目标检测并在显示设备上展示检测结果。 +```shell +ubuntu@ubuntu1804:~/buildroot-2023.02.8$ make tinyvision_defconfig +# +# configuration written to /home/ubuntu/buildroot-2023.02.8/.config +# +ubuntu@ubuntu1804:~/buildroot-2023.02.8$ make +/usr/bin/make -j1 O=/home/ubuntu/buildroot-2023.02.8/output HOSTCC="/usr/bin/gcc" HOSTCXX="/usr/bin/g++" syncconfig +``` -## 效果展示 +注意: 不要使用 `make clean` 命令 清理仓库。 -![image-20240126200516520](assets/post/README/image-20240126200516520.png) +## 烧写 + +```shell +ubuntu@ubuntu1804:~/buildroot-2023.02.8$ ls output/images/ +boot.vfat rootfs.ext4 sun8i-v851se-tinyvision.dtb sunxi.dtb tinyvision_sdcard.img +rootfs.ext2 rootfs.tar sun8i-v851s-tinyvision.dtb syter_boot_bin.bin zImage +ubuntu@ubuntu1804:~/buildroot-2023.02.8$ +``` + +系统编译完成后,镜像输出在 `output/images/` 目录下,名称为 `tinyvision_sdcard.img` 使用 `dd if` 命令 完整写入sd卡设备,或者 使用 Win32diskimage 工具。 或者使用 balenaEtcher 等 进行烧录。 # LCD 模组驱动