- 2025-01-31
-
发表了主题帖:
《ROS2智能机器人开发实践》--2.ROS2了解与安装运行
本篇了解ROS/ROS2的发展历程及ROS2安装运行。
一.ROS/ROS2发展历程
ROS(机器人操作系统)发展历程如下:
全新的ROS2在2017年年底正式发布,到2024年5月,ROS2第二个长期支持版本ROS 2 Jazzy发布。
ROS的核心目标是提高机器人的软件复用率。ROS主要由由通信机制、开发工具、应用功能、生态系统四大部分组成,具备全球化社区、开源开放的生态、跨平台使用、工业应用支持的特点。下面了解下ROS2与ROS1系统架构,可以看到ROS2摒弃了Master节点管理器、引入更复杂也更完善的DDS系统通信机制,同时可选的操作系统更多更广泛。
ROS2设计了一个ROS Middleware,简称RMW,也就是指定一个标准的接口,如如何发数据,收数据,数据的各种属性配置等。厂家接入ROS社区,按照RMW标准写一个适配接口把自家DDS移植过来。对用户来说,可以安装使用适合的DDS而不需要更改应用程序,可以轻松更换底层的通信系统。
ROS2概念有如下:
●工作空间(Workspace):开发过程的大本营,放置各种开发文件。
●功能包(Package):功能源码的聚集地,用于组织某一机器人功能。
●节点(Node):机器人的工作细胞,是代码编译生成的一个可执行文件。
●话题(Topic):节点间传递数据的桥梁,周期传递各功能之间的信息。
●服务(Service):节点间的你问我答,用于某些机器人功能和参数的匹配置。
●通信接口(Interface):数据传递的标准结构,规范机器人的各种数据形态。
●参数(Parameter):机器人系统的全局字典,可定义或查询机器人的配置参数。
●动作(Action):完整行为的流程管理,控制机器人完成某些动作。
●分布式通信(DistributedCommunication):多计算平台的任务分配,实现快速组网。
●DDS(Data Distribution Service):机器人的神经网络,完成数据的高效安全传递。
二.ROS2 安装
这里自行准备好虚拟机安装上ubuntu,开始下面ROS2安装,在控制台下输入下列命令。可参考ROS官网ROS2安装。
1.设置编码
sudo apt update && sudo apt install locales
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
2.添加源
sudo apt update && sudo apt install curl gnupg lsb-release
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(source /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
3.安装ROS2
sudo apt update
sudo apt upgrade
sudo apt install ros-foxy-desktop
4.设置环境变量
source /opt/ros/foxy/setup.bash
echo " source /opt/ros/foxy/setup.bash" >> ~/.bashrc
这里查询到安装的是foxy版本,安装成功后如下:
三.运行小海龟仿真
打开两个终端,分别运行以下指令:
ros2 run turtlesim turtlesim_node
ros2 run turtlesim turtle_teleop_key
第一句指令将启动一个蓝色背景的海龟仿真器,第二句指令将启动一个键盘控制节点,在该终端中点击键盘上的“上下左右”按键,就可以控制小海龟运动。下图是指令运行与仿真效果。
至此,对ROS2有了初步了解,安装上了ROS2,并成功运行了一个仿真,为后面探索奠定了良好基础。
-
发表了主题帖:
《ROS2智能机器人开发实践》--1.开箱与全书概览
很有幸获得《ROS2智能机器人开发实践》这本书的阅读机会,这是一本不可多得的智能机器人学习书籍,下面看下书籍封面
这本书是今年刚刚出第一版,很新,作者是“古月居”创始人及负责人,在机器人操作系统(ROS)领域有丰富的开发经验与积累。这本书紧扣技术发展脉络,系统全面讲解ROS2,在人工智能时代,对学习开发者来说,是很好的入门及深入学习的不错选择。
拿到书籍,首先习惯性是翻看下序言和目录,对全书有一个大致全面的了解。本书分三部分,共九章。每部分及章节内容如下:
第一部分(第1~3 章)介绍 ROS2基础原理:主要讲解ROS2的发展历程、核心原理和组件工具,提供大量的编程和使用示例,为读者全面展示ROS2的基础原理和功能。
●第1章--ROS:智能机器人灵魂
●第2章--ROS 2核心原理:构建机器人的基石
●第3章--ROS 2常用工具:让机器人开发更便捷
第二部分(第4~6章)介绍ROS2机器人设计:主要讲解如何使用ROS2设计一个仿真机器人和实物机器人。
●第4章--ROS 2机器人仿真:零成本玩转机器人
●第5章--ROS 2机器人构建:从仿真到实物
●第6章--ROS 2控制与感知:让机器人动的了、看得见
第三部分(第7~9章)介绍ROS2机器人应用:主要讲解使用ROS2开发机器人视觉识别地图构建和自主导航等众多应用的方法。
●第7章--ROS 2视觉应用:让机器人看懂世界
●第8章--ROS 2地图构建:让机器人理解环境
●第9章--ROS 2自主导航:让机器人运动自由
本书采用最新稳定版本ROS2系统和全新一代的Gazebo机器人仿真平台,绝大部分功能和源码可以在单独的计算机和 Gazebo仿真平台上运行。同时,本书介绍实物机器人的搭建方法,并且在实物机器人上实现相应的功能。配套源码也很方便读者阅读学习。
可以看出,从这本书可学的内容还是挺多的,后面以章节主题为纲,逐一展开阅读学习与实践。
- 2025-01-26
-
回复了主题帖:
拼板PCB生成钢网文件?
lkh747566933 发表于 2025-1-26 10:56
我都是画个拼板的板框示意图,其他的交给板厂。
你这省事,让板厂那边弄也行
-
发表了主题帖:
【嘉楠K230开发板】人脸检测
本帖最后由 dirty 于 2025-1-26 14:54 编辑
本篇讲述使用K230进行人脸检测。
一.了解AI视觉开发框架
K230的KPU是一个内部神经网络处理器,它可以在低功耗的情况下实现卷积神经网络计算,实时获取被检测目标的大小、坐标和种类,对人脸或者物体进行检测和分类,支持INT8和INT16。CanMV官方基于K230专门搭建了配套的AI视觉开发框架。框架结构如下图所示:
这个框架简单来说就是Sensor(摄像头)默认输出两路图像,一路格式为YUV420,直接给到Display显示;另一路格式为RGB888,给到AI部分进行处理。AI主要实现任务的前处理、推理和后处理流程,得到后处理结果后将其绘制在osd image实例上,并送给Display叠加,最后在HDMI、LCD或IDE缓冲区显示识别结果。
AI视觉开发框架主要API接口有:
●PineLine:将sensor、display封装成固定接口,用于采集图像、画图以及结果图片显示。
●AI2D:预处理(Preprocess)相关接口。
●AIBase:模型推理主要接口。
二.人脸检测
人脸检测,是将一幅图片中人脸检测出来,支持单个和多个人脸。
本次人脸检测功能将摄像头拍摄到的画面中的人脸用矩形框标识出来。编程流程如下:
实现代码如下:
'''人脸检测
'''
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from libs.PipeLine import PipeLine, ScopedTiming
from libs.AIBase import AIBase
from libs.AI2D import Ai2d
import os
import ujson
from media.media import *
from time import *
import nncase_runtime as nn
import ulab.numpy as np
import time
import utime
import image
import random
import gc
import sys
import aidemo
# 自定义人脸检测类,继承自AIBase基类
class FaceDetectionApp(AIBase):
def __init__(self, kmodel_path, model_input_size, anchors, confidence_threshold=0.5, nms_threshold=0.2, rgb888p_size=[224,224], display_size=[1920,1080], debug_mode=0):
super().__init__(kmodel_path, model_input_size, rgb888p_size, debug_mode) # 调用基类的构造函数
self.kmodel_path = kmodel_path # 模型文件路径
self.model_input_size = model_input_size # 模型输入分辨率
self.confidence_threshold = confidence_threshold # 置信度阈值
self.nms_threshold = nms_threshold # NMS(非极大值抑制)阈值
self.anchors = anchors # 锚点数据,用于目标检测
self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]] # sensor给到AI的图像分辨率,并对宽度进行16的对齐
self.display_size = [ALIGN_UP(display_size[0], 16), display_size[1]] # 显示分辨率,并对宽度进行16的对齐
self.debug_mode = debug_mode # 是否开启调试模式
self.ai2d = Ai2d(debug_mode) # 实例化Ai2d,用于实现模型预处理
self.ai2d.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT, nn.ai2d_format.NCHW_FMT, np.uint8, np.uint8) # 设置Ai2d的输入输出格式和类型
# 配置预处理操作,这里使用了pad和resize,Ai2d支持crop/shift/pad/resize/affine,具体代码请打开/sdcard/app/libs/AI2D.py查看
def config_preprocess(self, input_image_size=None):
with ScopedTiming("set preprocess config", self.debug_mode > 0): # 计时器,如果debug_mode大于0则开启
ai2d_input_size = input_image_size if input_image_size else self.rgb888p_size # 初始化ai2d预处理配置,默认为sensor给到AI的尺寸,可以通过设置input_image_size自行修改输入尺寸
top, bottom, left, right = self.get_padding_param() # 获取padding参数
self.ai2d.pad([0, 0, 0, 0, top, bottom, left, right], 0, [104, 117, 123]) # 填充边缘
self.ai2d.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel) # 缩放图像
self.ai2d.build([1,3,ai2d_input_size[1],ai2d_input_size[0]],[1,3,self.model_input_size[1],self.model_input_size[0]]) # 构建预处理流程
# 自定义当前任务的后处理,results是模型输出array列表,这里使用了aidemo库的face_det_post_process接口
def postprocess(self, results):
with ScopedTiming("postprocess", self.debug_mode > 0):
post_ret = aidemo.face_det_post_process(self.confidence_threshold, self.nms_threshold, self.model_input_size[1], self.anchors, self.rgb888p_size, results)
if len(post_ret) == 0:
return post_ret
else:
return post_ret[0]
# 绘制检测结果到画面上
def draw_result(self, pl, dets):
with ScopedTiming("display_draw", self.debug_mode > 0):
if dets:
pl.osd_img.clear() # 清除OSD图像
for det in dets:
# 将检测框的坐标转换为显示分辨率下的坐标
x, y, w, h = map(lambda x: int(round(x, 0)), det[:4])
x = x * self.display_size[0] // self.rgb888p_size[0]
y = y * self.display_size[1] // self.rgb888p_size[1]
w = w * self.display_size[0] // self.rgb888p_size[0]
h = h * self.display_size[1] // self.rgb888p_size[1]
pl.osd_img.draw_rectangle(x, y, w, h, color=(255, 255, 0, 255), thickness=2) # 绘制矩形框
else:
pl.osd_img.clear()
# 获取padding参数
def get_padding_param(self):
dst_w = self.model_input_size[0] # 模型输入宽度
dst_h = self.model_input_size[1] # 模型输入高度
ratio_w = dst_w / self.rgb888p_size[0] # 宽度缩放比例
ratio_h = dst_h / self.rgb888p_size[1] # 高度缩放比例
ratio = min(ratio_w, ratio_h) # 取较小的缩放比例
new_w = int(ratio * self.rgb888p_size[0]) # 新宽度
new_h = int(ratio * self.rgb888p_size[1]) # 新高度
dw = (dst_w - new_w) / 2 # 宽度差
dh = (dst_h - new_h) / 2 # 高度差
top = int(round(0))
bottom = int(round(dh * 2 + 0.1))
left = int(round(0))
right = int(round(dw * 2 - 0.1))
return top, bottom, left, right
if __name__ == "__main__":
# 显示模式,默认"hdmi",可以选择"hdmi"和"lcd"
display_mode="lcd"
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
# 设置模型路径和其他参数
kmodel_path = "/sdcard/examples/kmodel/face_detection_320.kmodel"
# 其它参数
confidence_threshold = 0.5
nms_threshold = 0.2
anchor_len = 4200
det_dim = 4
anchors_path = "/sdcard/examples/utils/prior_data_320.bin"
anchors = np.fromfile(anchors_path, dtype=np.float)
anchors = anchors.reshape((anchor_len, det_dim))
rgb888p_size = [1920, 1080]
# 初始化PipeLine,用于图像处理流程
pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode)
pl.create() # 创建PipeLine实例
# 初始化自定义人脸检测实例
face_det = FaceDetectionApp(kmodel_path, model_input_size=[320, 320], anchors=anchors, confidence_threshold=confidence_threshold, nms_threshold=nms_threshold, rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0)
face_det.config_preprocess() # 配置预处理
clock = time.clock()
while True:
clock.tick()
img = pl.get_frame() # 获取当前帧数据
res = face_det.run(img) # 推理当前帧
# 当检测到人脸时,打印结果
if res:
print(res)
face_det.draw_result(pl, res) # 绘制结果
pl.show_image() # 显示结果
gc.collect() # 垃圾回收
print(clock.fps()) #打印帧率
主函数实现获取当前帧数据,AI推理当前帧,检测到人脸绘制、显示结果,这里用到SD卡路径下人脸检测模型/sdcard/examples/kmodel/face_detection_320.kmodel。AI处理阶段定义在FaceDetectionApp类下config_preprocess、postprocess、get_padding_param。
这里选用两张照片,运行摄像头正对照片,识别结果如下,可以看到图片中人脸均识别到。
至此,实现人脸检测功能。
-
回复了主题帖:
《Linux内核深度解析》--文件系统
qzgiky 发表于 2025-1-26 08:55
学习,Linux单单学会用还是不够,得会底层原理,然后裁剪定制
是的,学以致用
- 2025-01-24
-
回复了主题帖:
【Wio Lite AI STM32H725AE视觉开发板】--2.开发环境搭建与点灯
Jacktang 发表于 2025-1-24 09:31
用的版本V5.36.0.0,好吧,与STM32CubeMX里兼容匹配
STM32CubeMX 里有MDK-RAM (Keil)需要的版本(以上),导出的工程可用
-
发表了主题帖:
《Linux内核深度解析》--文件系统
本帖最后由 dirty 于 2025-1-24 10:18 编辑
本篇阅读学习文件系统及使用。
概述
在Linux 系统中,一切皆文件,除了通常所说的狭义的文件(文本文件和二进制文件)以外,目录、设备、套接字和管道等都是文件。
文件系统在不同的上下文中有不同的含义。
①在存储设备上组织文件的方法,包括数据结构和访问方法。
②按照某种文件系统类型格式化的一块存储介质。我们常说在某个目录下挂载或卸载文件系统,这里的文件系统就是这种意思。
③内核中负责管理和存储文件的模块,即文件系统模块。
Linux文件系统架构如下:
用户空间层面
应用程序可以直接使用内核提供的系统调用访问文件;
①一个存储设备上的文件系统,只有挂载到内存中目录树的某个目录下,进程才能诸问这个文件系统。
②系统调用umount用来卸载某个目录下挂载的文件系统。
③使用 open 打开文件。
④使用close 关闭文件。
⑤使用read 读文件。
⑥使用 write 写文件。
⑦使用lseek 设置文件偏移。
⑧当我们写文件的时候,内核的文件系统模块把数据保存在页缓存中,不会立即写到存储设备。
应用程序也可以使用 glibc 厍封装的标准 O 流函数访问文件,glibc 库封装的标准 O 流函数如下所示:
①使用 fopen 打开流。
②使用 fclose 关闭流
③使用 fead 读流。
④使用 fwrite 写流。
⑤使用 fseek 设置文件偏移。
⑥使用fwrite可以把数据写到用户空间缓冲区,但不会立即写到内核。我们可以使用fflush 冲刷流,即把写到用户空间缓冲区的数据立即写到内核。
硬件层面
外部存储设备分为块设备、闪存和NVDIMM 设备3类.
块设备主要有以下两种:机械硬盘、闪存类块设备。如SSD、eMMC、UFS。
闪存(Flash Memory)按存储结构分为NAND闪存和NOR闪存。
NVDIMM (Non-Volatile DIMM,非易失性内存; DIMM 是 Dual-Inline-Memory-Modules的缩写,表示双列直插式存储模块,是内存的一种规格)断电后数据不丢失。在断电的瞬间,超级电容提供电力,把内存中的数据转移到NAND闪存。
内核空间层面
在内核的目录下可以看到,内核支持多种文件系统类型。为了对用户程序提供统一的文件操作接口。
为了使不同的文件系统实现能够其存,内核实现了一个抽象层,称为虚拟文件系统(Vitual File System,VFS),也称为虚拟文件系统功换(virnal Filesystem Switch,VFS)。
文件系统分为以下4种。
①块设备文件系统,存储设备是机械硬盘和固态硬盘等块设备,常用的块设备文件系统是EXT和 btfs(读作|bAtS|)。EXT文件系统是Limux原创的文件系统,目前有3个版本:EXT2、EXT3 和 EXT4。
②闪存文件系统,存储设备是NAND闪存和NOR闪存,常用的闪存文件系统是JFFS2(日志型闪存文件系统版本2,Journalling Flash File System version2)和 UBIFS(无序区块镜像文件系统,Unsorted Block Image File System)。
③内存文件系统,文件在内存中,断电以后文件丢失,常用的内存文件系统是tmpfs,用来创建临时文件。
④伪文件系统,是假的文件系统,只是为了使用虚拟文件系统的编程接口。常用伪文件系统sockfs、proc 文件系统、sysf、hugetlbfs、cgroup、cgroup2
虚拟文件系统的数据结构
虽然不同文件系统类型的物理结构不同,但是虚拟文件系统定义了一套统一的数据结构。
①超级块。
②虚拟文件系统在内存中把目录组织为一棵树。
③每种文件系统的超级块的格式不同,需要向虚拟文件系统注册文件系统类型fle_system_type,并且实现 mount 方法用来读取和解析超级块。
④索引节点。
⑤目录项。
⑥当进程打开一个文件的时候,虚拟文件系统就会创建文件的一个打开实例:fle结构体,然后在进程的打开文件表中分配一个索引,这个索引称为文件描述符,最后把文件描述符和 fle 结构体的映射添加到打开文件表中。
注册文件系统类型
函数register flesystem用来注册文件系统类型:
int register filesystem(struct file_system type *fs);
函数unregister flesystem 用来注销文件系统类型:
int unregister filesystem(struct file_system_type *fs);
管理员可以执行命令“cat/proc/flesystems”来查看已经注册的文件系统类型。
挂载文件系统
虚拟文件系统在内存中把目录组织为一棵树。一个文件系统,只有挂载到内存中目树的一个目录下,进程才能访问这个文件系统。
glibc库封装了挂载文件系统的函数mount:
int mount(const char*dev_name, const char *dir_name,const char *type,unsigned long flags,const void *data);
glibc库封装了两个卸载文件系统的函数:
①函数umount,对应内核的系统调用oldumount。
int umount(const char *target);
②函数umount2,对应内核的系统调用umount。
int umount2(const char*target,int flags);
打开文件
进程读写文件之前需要打开文件,得到文件描述符,然后通过文件描述符读写文件。
内核提供了两个打开文件的系统调用。
①int open(const char *pathname, int flags, mode t mode);
②int openat(int dirfd, const char *pathname, int flags, mode t mode),
如果打开文件成功,那么返回文件描述符,值大于或等于0;如果打开文件失败,返回负的错误号。
创建文件
创建不同类型的文件,需要使用不同的命令。
①普通文件:touchFE,这条命今本来用来更新文件的访问时间和修改时间,如果文件不存在,创建文件。
②目录:mkdir DIRECTORY
③符号链接(也称为软链接):In-STARGETLINKNAME或In--symbolC TARGETLINK NAME
④字符或块设备文件:mknod NAME TYPE [MAJOR MINOR]
参数 TYPE:b表示带缓冲区的块设备文件,c表示带缓冲区的字符设备文件,u表示不带缓冲区的字符设备文件,p表示命名管道。
⑤命名管道:mkpipe NAME
⑥命令“In TARGETLINK NAME”用来创建硬链接,给已经存在的文件增加新的名文件的索引节点有一个硬链接计数,如果文件有几个名称,那么硬链接计数是n。
内核提供了下面这些创建文件的系统调用。
①创建普通文件。
int creat(const char*pathname,mode_t mode);
也可以使用open和openat创建普通文件
int open(const char *pathname,int flags,mode_t mode);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
②创建目录。
int mkdir(const char*pathname,mode_t mode);
int mkdirat(int dirfd,const char *pathname, mode_t mode);
③创建符号链接。
int symlink(const char *oldpath,const char *newpath);
int symlinkat(const char *oldpath, int newdirfd, const char *newpath);
④mknod通常用来创建字符设备文件和块设备文件,也可以创建普通文件、命名管道和套接字。
int mknod(const char *pathname,modet mode, dev_t dev);
int mknodat(int dirfd, const char *pathname, mode_t mode, dev_t dev);
⑤link用来创建硬链接,给已经存在的文件增加新的名称。
int link(const char *oldpath,const char*newpath);
int linkat(int olddfd, const char *oldpath, int newdfd, const char *newpath);
glibc库封装了和上面的系统调用同名的库函数,还封装了创建命名管道的库函数。库函数 mkffo 是通过调用系统调用 mknod 来实现的。
int mkfifo(const char*pathname,mode t mode);
删除文件
删除文件的命今如下。
①删除任何类型的文件:unlink PILE.
②m FJLE,默认不删除目录,如果使用选项“”“_只”或“_recursive”,可以删除目录和目录的内容。
③删除目录:mdirDIRECTORY.
内核提供了下面这些刑除文件的系统调用。
①unlink用来删除文件的名称,如果文件的硬链接计数变成0,并且没有进程打开这个文件,那么删除文件。
int unlinx(conet char*pathname);
int unlinrat(int dirfd, const char *pathname, int flags);
②删除目录。
int rmdir(const char *pathname);
设置文件权限
内核提供了下面这些设置文件权限的系统调用
①int chmod(const char *path, mode t mode),
②int fchmod(int fd, mode t mode);
③int fchmodat(int dfd, const char *filename, mode t mode);
读文件
进程读文件的方式有3种。
①调用内核提供的读文件的系统调用。
②调用glibc 库封装的读文件的标准 O 流函数。
③创建基于文件的内存映射,把文件的一个区间映射到进程的虚拟地址空国直接读内存。
内核提供了下面这些读文件的系统调用。
①系统调用read从文件的当前偏移读文件,把数据存放在一个缓冲区.
ssize_t read(int fd,void *buf,size_t count);
②系统调用pread64 从指定偏移开始读文件。
ssize_t pread64(int fd, void *buf, sizet count, off_t offset);
③系统调用readv从文件的当前偏移读文件,把数据存放在多个分散的缓冲区。
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
④系统调用preadv从指定偏移开始读文件,把数据存放在多个分散的缓冲区
ssize_t preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset);
⑤系统调用preadv2在系统调用preadv 的基础上增加了参数“int flags”。
ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags);
其中preadv和preadv2是Linux内核私有的系统调用。
写文件
进程写文件的方式有3种。
①调用内核提供的写文件的系统调用。
②调用 glibc 库封装的写文件的标准 IO 流函数。
③创建基于文件的内存映射,把文件的一个区间映射到进程的虚拟地址空间,然后直接写内存。
内核提供了下面这些写文件的系统调用。
①函数 write 从文件的当前偏移写文件,调用进程把要写入的数据存放在一个缓冲区
ssize_t write(int fd,const void *buf,size_t count);
②函数 pwrite64 从指定偏移开始写文件。
ssize_t pwrite64(int fd, const void *buf,size_t count, off_t offset);
③函数 writev从文件的当前偏移写文件,调用进程把要写入的数据存放在多个分散的缓冲区。
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
④函数pwritev从指定偏移开始写文件,调用进程把要写入的数据存放在多个分散的缓冲区。
ssize_t pwritev(int fd, const struct iovec *iov, int iovont, off_t offset);
⑤函数 pwritev2 在函数 pwritev 的基础上增加了参数“int flags”。
ssize_t pwritev2(int fd, const struct iovec *jov, int iovent, off_t offset, int flags) ;
其中 pwritev和pwritev2是Linux内核私有的系统调用。
文件回写
进程写文件时,内核的文件系统模块把数据写到文件的页缓存,没有立即写回到存储设备。文件系统模块会定期把脏页(即数据被修改过的文件页)写回到存储设备,进程也可以调用系统调用把脏页强制写回到存储设备。
内核提供了下面这些把文件同步到存储设备的系统调用。
①sync把内存中所有修改过的文件元数据和文件数据写回到存储设备。
void sync(void);
②syncfs把文件描述符fd引用的文件所属的文件系统写回到存储设备。
int syncfs(int fd);
③fsyc把文件描述符fd引用的文件修改过的元数据和数据写回到存储设备
int fsync(int fd);
④fdatasync把文件描述符d引用的文件修改过的数据写回到存储设备,还会把检索这些数据需要的元数据写回到存储设备。
int fdatasync(int fd);
⑤Linux 私有的系统调用sync fle range 把文件的一个区间修改过的数据写回到存储设备
int sync_file_range(int fd, off64_t offset, off64_t nbytes, unsigned int flags);
glibc库针对这些系统调用封装了同名的库函数,还封装了一个把数据从用户空间缓冲区写到内核的标准 I/O 流函数:
int fflush(FILE *stream);
下面以打开、读、写、关闭文件示例代码如下
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main()
{
char buf_w[30] = "HELLO WORLD!!!\n";
char buf_r[30] = "HELLO WORLD!!!\n";
int fd = open("./test", O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
printf("error: test open\n");
return -1;
}
if( write(fd, buf_w, strlen(buf)) < 0 )
{
printf("error: test write\n");
return -1;
}
if(read(fd, buf, 30) < 0)
{
printf("error: test read\n");
return -1;
}
printf("read fd:%s", buf);
ret = close(fd);
if (ret < 0)
{
printf("error: test close\n");
return -1;
}
if(ret = 0)
{
printf("succeed: test close\n");
return 0;
}
return 0;
}
至此,对文件系统进行了梳理学习与使用,对Linux文件系统加深了认识。
- 2025-01-23
-
发表了主题帖:
【Wio Lite AI STM32H725AE视觉开发板】--3.串口打印
本帖最后由 dirty 于 2025-1-23 22:54 编辑
本篇讲述实现串口打印功能。
一.硬件原理与准备
STM32H725AE有5个UART和一个LPUART,根据开发板原理图扩展接口,这里选择UART3 如下:
硬件板上接好串口如下:
二.代码准备
1.STM32CubeMX配置UART如下
2.生成代码后修改增加如下代码:
●添加串口重定向函数
#include "stdio.h"
#include "main.h"
/*
...
*/
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart3, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
●main函数加入串口打印,如下:
/**
* [url=home.php?mod=space&uid=159083]@brief[/url] The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* Enable I-Cache---------------------------------------------------------*/
SCB_EnableICache();
/* Enable D-Cache---------------------------------------------------------*/
SCB_EnableDCache();
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART3_UART_Init();
/* USER CODE BEGIN 2 */
printf("Welcome to Wio Lite AI STM32H725AE Develop Board !\r\n");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
if(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13))
{
printf("Red Led is On\r\n");
}
else
{
printf("Red Led is Off\r\n");
}
HAL_Delay(1000U);
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
3.编译烧录后运行,可以看到红灯闪烁,且有如下日志
至此,实现串口打印功能。
-
回复了主题帖:
【嘉楠K230开发板】码类识别
freebsder 发表于 2025-1-23 16:48
MicroPython 直接支持barcode?这有点意思了。
支持,就是条形码密集了、太小了,识别不大出来
-
发表了主题帖:
【Wio Lite AI STM32H725AE视觉开发板】--2.开发环境搭建与点灯
本帖最后由 dirty 于 2025-1-23 18:08 编辑
本篇讲述开发环境搭建与点亮LED闪烁。
一.开发环境搭建
1.ST官方IDE安装
首先安装ST官方软件STM32CubeMX、STM32CubeIDE、X-CUBE-AI,其中X-CUBE-AI可以在STM32CubeMX里选择安装,这里均选择Win版本。ST文档给的参考版本如下,
2.Keil 及芯片Pack包安装
Keil IDE留意下版本,主要与STM32CubeMX里兼容匹配,这里使用的版本V5.36.0.0。Keil.STM32H7xx_DFP.4.0.0.pack 可以在Keil官网找到,下载下来后安装上。
经过上面的安装,开发环境基本搭建起来。
二.点灯
下面在STM32CubeMX配置好生成工程,Keil 编译烧录,点亮LED闪烁。
1.LED原理图如下,这里选用PC13引脚,控制LED2红灯。
2.打开STM32CubeMX IDE,选择根据MCU创建工程,由于IDE没有集成STM32H725AEI6这板级选型,就根据MCU创建了,如下图所示:
3.RCC时钟打开,如下
4.LED控制引脚GPIO选择与配置,如下
5.内核配置,这里都给使能
6.系统时钟配置。这里Clock Configuration 里主频最大配置为64MHz,根据芯片手册这里配置为最大550MHz配置如下:
7.工程配置,包括工程名及路径,导出适配的IDE,这里选择Keil MDK-ARM,还有链接堆栈大小,配置如下
8.生成工程。
9.生成工程完后“Open Project”.就会Keil打开工程如下,前面已经安装了芯片pack包,这里开发板连接好ST-LINK,上电,选择配置好调试器。
10.编写点灯闪烁代码。这里主要在主循环下添加代码,如下
/**
* [url=home.php?mod=space&uid=159083]@brief[/url] The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* Enable I-Cache---------------------------------------------------------*/
SCB_EnableICache();
/* Enable D-Cache---------------------------------------------------------*/
SCB_EnableDCache();
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
HAL_Delay(1000U);
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
11.编译后烧录,可以看到开发板红灯闪烁,效果如下
[localvideo]44186c12fec4e39ad4b3baea2b8e847f[/localvideo]
至此,实现开发环境的搭建与点亮LED。
-
回复了主题帖:
【嘉楠K230开发板】码类识别
wangerxian 发表于 2025-1-23 09:17
二维码识别也能达到30帧,还不错!
嗯,还好
-
回复了主题帖:
【嘉楠K230开发板】图像检测
Jacktang 发表于 2025-1-23 08:32
比赛经常用到的小车、机器人巡线这些原来是快速线性回归(巡线)技术算法,,,
是的
- 2025-01-22
-
发表了主题帖:
【嘉楠K230开发板】码类识别
本帖最后由 dirty 于 2025-1-22 18:46 编辑
本篇讲述条形码与二维码识别。
条形码识别
1.了解条形码
条形码(barcode)是将宽度不等的多个黑条和空白,按照一定的编码规则排列,用以表达一组信息的图形标识符。常见的条形码是由反射率相差很大的黑条(简称条)和白条(简称空)排成的平行线图案。条形码可以标出物品的生产国、制造厂家、商品名称、生产日期、图书分类号、邮件起止地点、类别、日期等许多信息,因而在商品流通、图书管理、邮政管理、银行系统等许多领域都得到广泛的应用。
2.相关API函数
CanMV K230而言,直接使用MicroPython中的find_barcodes()即可获取摄像头采集图像中条形码的相关信息。该函数支持所有一维条形码:image.EAN2、image.EAN5、image.EAN8、image.UPCE、image.ISBN10、image.UPCA、image.EAN13、image.ISBN13、image.I25、image.DATABAR (RSS-14)、image.DATABAR_EXP (RSS-Expanded)、image.CODABAR image.CODE39、image.PDF417 image.CODE93、image.CODE128 。
构造函数
...
功能:查找roi区域内的所有条形码并返回一个image.barcode的对象列表
返回:image.barcode对象列表。
...
image.find_barcodes([roi])
使用方法
barcode.rect() #返回一个矩形元组(x,y,w,h),条形码的边界。可以通过索引[0-3]来获得单个值
barcode.payload() #返回条形码字符串信息。可以通过索引[4]来获得这个值。
barcode.type() #返回条形码类型。
3.条形码识别代码与运行结果
'''
条形码识别
'''
import time, math, os, gc
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
#定义条形码类型
def barcode_name(code):
if(code.type() == image.EAN2):
return "EAN2"
if(code.type() == image.EAN5):
return "EAN5"
if(code.type() == image.EAN8):
return "EAN8"
if(code.type() == image.UPCE):
return "UPCE"
if(code.type() == image.ISBN10):
return "ISBN10"
if(code.type() == image.UPCA):
return "UPCA"
if(code.type() == image.EAN13):
return "EAN13"
if(code.type() == image.ISBN13):
return "ISBN13"
if(code.type() == image.I25):
return "I25"
if(code.type() == image.DATABAR):
return "DATABAR"
if(code.type() == image.DATABAR_EXP):
return "DATABAR_EXP"
if(code.type() == image.CODABAR):
return "CODABAR"
if(code.type() == image.CODE39):
return "CODE39"
if(code.type() == image.PDF417):
return "PDF417"
if(code.type() == image.CODE93):
return "CODE93"
if(code.type() == image.CODE128):
return "CODE128"
sensor = Sensor() #构建摄像头对象
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(width=800, height=480) #设置帧大小为LCD分辨率(800x480),默认通道0
#sensor.set_framesize(Sensor.FHD) #设置帧大小FHD(1920x1080),默认通道0
sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0
#Display.init(Display.ST7701, to_ide=True) #同时使用3.5寸mipi屏和IDE缓冲区显示图像,800x480分辨率
Display.init(Display.VIRT, sensor.width(), sensor.height()) #只使用IDE缓冲区显示图像
MediaManager.init() #初始化media资源管理器
sensor.run() #启动sensor
clock = time.clock()
while True:
clock.tick()
img = sensor.snapshot() #拍摄图片
codes = img.find_barcodes() #查找图像中所有条形码
for code in codes:
#对条码画矩形表示
img.draw_rectangle(code.rect(),thickness=2)
#打印相关信息
print_args = (barcode_name(code), code.payload(), (180 * code.rotation()) / math.pi, code.quality())
print("Barcode %s, Payload \"%s\", rotation %f (degrees), quality %d" % print_args)
img.draw_string_advanced(0, 0, 30, code.payload(), color = (255, 0, 255)) #图像显示条码信息
Display.show_image(img) #显示图片
print(clock.fps()) #打印帧率
使用01Studio WIKI上条形码识别效果如下,用其他紧凑密集些的条形码识别不出,文档给了下说明不能太小,实际应用这里还是有优化处理空间。
二维码识别
1.了解二维码
二维码又称二维条码,常见的二维码为QR Code,QR全称Quick Response,它比传统的Bar Code条形码能存更多的信息,也能表示更多的数据类型。
二维条码/二维码(2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的、黑白相间的、记录数据符号信息的图形;在代码编制上巧妙地利用构成计算机内部逻辑基础的“0”、“1”比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息,通过图象输入设备或光电扫描设备自动识读以实现信息自动处理:它具有条码技术的一些共性:每种码制有其特定的字符集;每个字符占有一定的宽度;具有一定的校验功能等。同时还具有对不同行的信息自动识别功能、及处理图形旋转变化点。
2.相关API函数
CanMV K230使用MicroPython中的find_qrcodes()即可获取摄像头采集图像中二维码的相关信息。
构造函数
...
功能:查找roi区域内的所有二维码并返回一个image.qrcode的对象列表。
返回:image.qrcode对象列表。
...
image.find_qrcodes([roi])
使用方法
qrcode.rect() #返回一个矩形元组(x,y,w,h),二维码的边界。可以通过索引[0-3]来获得单个值。
qrcode.payload() #返回二维码字符串信息。可以通过索引[4]来获得这个值。
qrcode.verison() #返回二维码版本号。
3.二维码识别代码与运行结果
'''
二维码识别
'''
import time, math, os, gc
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
sensor = Sensor() #构建摄像头对象
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(width=800, height=480) #设置帧大小为LCD分辨率(800x480),默认通道0
sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0
#Display.init(Display.ST7701, to_ide=True) #同时使用3.5寸mipi屏和IDE缓冲区显示图像,800x480分辨率
Display.init(Display.VIRT, sensor.width(), sensor.height()) #只使用IDE缓冲区显示图像
MediaManager.init() #初始化media资源管理器
sensor.run() #启动sensor
clock = time.clock()
while True:
clock.tick()
img = sensor.snapshot() #拍摄图片
res = img.find_qrcodes() #寻找二维码
if len(res) > 0: #在图片和终端显示二维码信息
img.draw_rectangle(res[0].rect(), thickness=2)
img.draw_string_advanced(0, 0, 30, res[0].payload(), color = (255, 0, 0))
print(res[0].payload()) #串口终端打印
Display.show_image(img) #显示图片
print(clock.fps()) #打印帧率
使用二维码生成工具,这里使用EEWorld网址生成二维码。运行,摄像头对着二维码,识别结果如下:
至此,实现摄像头条形码、二维码识别功能。
-
回复了主题帖:
《Linux内核深度解析》--内核互斥技术
freebsder 发表于 2025-1-22 16:43
谢谢分享,期待后续!
好的
-
回复了主题帖:
【嘉楠K230开发板】摄像头显示与画图
懒猫爱飞 发表于 2025-1-22 14:57
加油
-
回复了主题帖:
【嘉楠K230开发板】点亮LED
懒猫爱飞 发表于 2025-1-22 14:56
加油
好的
-
发表了主题帖:
【嘉楠K230开发板】图像检测
本帖最后由 dirty 于 2025-1-22 16:18 编辑
本篇讲述使用开发板做图像检测,包含边缘检测、线段检测、圆形检测、矩形检测,快速线性回归。
边缘检测
简单来说就是轮廓检测。CanMV集成了RGB565颜色块识别find_edges函数,位于 image 模块下,下面了解其相关API函数
...
功能:边缘检测,将图像变为黑白,边缘保留白色像素。
参数:
edge_type: 处理方式。
image.EDGE_SIMPLE : 简单的阈值高通滤波算法;
image.EDGE_SIMPLE : 简单的阈值高通滤波算法;
threshold:包含高、低阈值的二元组,默认是(100,200),仅支持灰度图像。
...
image.find_edges(edge_type[, threshold])
下面是对图像进行边缘检测代码
'''
边缘检测
'''
import time, os, sys, gc
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
sensor = Sensor() #构建摄像头对象,将摄像头长宽设置为4:3
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(width=320, height=240) #设置帧大小为LCD分辨率(320x240),默认通道0
#sensor.set_framesize(Sensor.FHD) #设置帧大小FHD(1920x1080),默认通道0
sensor.set_pixformat(Sensor.GRAYSCALE) #设置输出图像格式,默认通道0
#Display.init(Display.ST7701, to_ide=True) #同时使用3.5寸mipi屏和IDE缓冲区显示图像,800x480分辨率
Display.init(Display.VIRT, sensor.width(), sensor.height()) #只使用IDE缓冲区显示图像
print(sensor.width(), sensor.height())
MediaManager.init() #初始化media资源管理器
sensor.run() #启动sensor
clock = time.clock()
while True:
clock.tick()
img = sensor.snapshot() #拍摄一张图片
#使用 Canny 边缘检测器
img.find_edges(image.EDGE_CANNY, threshold=(50, 80))
# 也可以使用简单快速边缘检测,效果一般,配置如下
#img.find_edges(image.EDGE_SIMPLE, threshold=(100, 255))
#Display.show_image(img) #显示图片
#显示图片,仅用于LCD居中方式显示
Display.show_image(img, x=0,y=0)
print(clock.fps()) #打印FPS
运行,将手掌放于摄像头前方,可以检测到手掌轮廓,摄像头上方门框边缘部分也被检测到,效果图如下:
线段检测
K230对图像中的线段进行检测识别,并画图指示。CanMV集成了线段识别 find_line_segments 函数,位于 image 模块下,下面了解下其相关API
...
功能:线段识别函数
参数:
roi:识别区域(x,y,w,h),未指定则默认整张图片。
merge_distance:两条线段间可以相互分开而不被合并的最大像素;
max_theta_difference:将少于这个角度值的线段合并。
返回:返回一个 image.line 线段对象列表
...
image.find_line_segments([roi[, merge_distance=0[, max_theta_difference=15]]])
下面是线段检测代码:
'''
线段检测
说明:推荐使用320x240以下分辨率,分辨率过大会导致帧率下降。
'''
import time, os, sys
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
enable_lens_corr = False # 设为True可以获得更直的线段
sensor = Sensor() #构建摄像头对象,将摄像头长宽设置为4:3
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(width=320, height=240) #设置帧大小,默认通道0
sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0
#Display.init(Display.ST7701, to_ide=True) #同时使用3.5寸mipi屏和IDE缓冲区显示图像,800x480分辨率
Display.init(Display.VIRT, sensor.width(), sensor.height()) #只使用IDE缓冲区显示图像
MediaManager.init() #初始化media资源管理器
sensor.run() #启动sensor
clock = time.clock()
while True:
clock.tick()
img = sensor.snapshot() #拍摄一张图片
if enable_lens_corr: img.lens_corr(1.8) # for 2.8mm lens...
# `merge_distance` 控制相近的线段是否合并. 数值 0 (默认值)表示不合并。数值
#为1时候表示相近1像素的线段被合并。因此你可以通过改变这个参数来控制检测到线
#段的数量。
# `max_theta_diff` 控制相差一定角度的线段合并,默认是15度,表示15度内的线
# 段都会合并
for l in img.find_line_segments(merge_distance = 0, max_theta_diff = 5):
img.draw_line(l.line(), color = (255, 0, 0), thickness=2)
print(l)
#Display.show_image(img) #显示图片
#显示图片,仅用于LCD居中方式显示
# Display.show_image(img, x=round((800-sensor.width())/2),y=round((480-sensor.height())/2))
Display.show_image(img, x=0,y=0)
print(clock.fps()) #打印FPS
这里在纸张上写上-XZ,运行检测结果如下,可以看到线段被标红:
圆形检测
K230对图像中的圆形进行检测识别,并画图指示。CanMV集成了圆形识别find_circles函数,位于image模块下,下面了解下其相关API.
...
功能:找圆函数
参数:
roi: 识别区域(x,y,w,h),未指定则默认整张图片。
threshold: 阈值。返回大于或等于threshold的圆,调整识别可信度。
x_stride y_stride : 霍夫变换时跳过x,y像素的量;
x_margin y_margin r_margin : 控制所检测圆的合并;
r_min r_max: 控制识别圆形的半径范围;
r_step:控制识别步骤。
返回:返回一个image.circle圆形对象,该圆形对象有4个值: x, y(圆心), r (半径)和magnitude(量级);量级越大说明识别到的圆可信度越高。
...
image.find_circles([roi[, x_stride=2[, y_stride=1[, threshold=2000[, x_margin=10[, y_margin=10
[, r_margin=10[, r_min=2[, r_max[, r_step=2]]]]]]]]]])
下面是圆形检测代码:
'''
实验名称:圆形检测
说明:推荐使用320x240以下分辨率,分辨率过大会导致帧率下降。
'''
import time, os, sys
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
sensor = Sensor() #构建摄像头对象
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(width=320, height=240) #设置帧大小,默认通道0
sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0
#Display.init(Display.ST7701, to_ide=True) #同时使用3.5寸mipi屏和IDE缓冲区显示图像,800x480分辨率
Display.init(Display.VIRT, sensor.width(), sensor.height()) #只使用IDE缓冲区显示图像
MediaManager.init() #初始化media资源管理器
sensor.run() #启动sensor
clock = time.clock()
while True:
clock.tick()
img = sensor.snapshot() #拍摄一张图片
# 圆形类有 4 个参数值: 圆心(x, y), r (半径)和 magnitude(量级);
# 量级越大说明识别到的圆可信度越高。
# `threshold` 参数控制找到圆的数量,数值的提升会降低识别圆形的总数。
# `x_margin`, `y_margin`, and `r_margin`控制检测到接近圆的合并调节.
# r_min, r_max, and r_step 用于指定测试圆的半径范围。
for c in img.find_circles(threshold = 2000, x_margin = 10, y_margin= 10,
r_margin = 10,r_min = 2, r_max = 100, r_step = 2):
#画红色圆做指示
img.draw_circle(c.x(), c.y(), c.r(), color = (255, 0, 255),thickness=2)
print(c) #打印圆形的信息
#Display.show_image(img) #显示图片
#显示图片,仅用于LCD居中方式显示
Display.show_image(img, x=0,y=0)
print(clock.fps()) #打印FPS
这里用一个小圆盒,放于摄像头前方,调整下角度光线,可以看到物体两个圆形均被标记识别出,运行监测效果如下图所示:
矩形检测
K230对图像中的矩形进行检测识别,并画图指示。CanMV集成了矩形识别find_rects函数,位于image模块下,下面了解下其相关API
...
功能:矩形识别函数
参数:
roi: 识别区域(x,y,w,h),未指定则默认整张图片。
threshold: 阈值。返回大于或等于threshold的矩形,调整识别可信度。
返回:返回一个image.rect矩形对象列表。
...
image.find_rects([roi=Auto, threshold=10000])
矩形检测代码如下:
'''
矩形检测
说明:推荐使用320x240以下分辨率,分辨率过大会导致帧率下降。
'''
import time, os, sys
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
sensor = Sensor() #构建摄像头对象
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(width=320, height=240) #设置帧大小为LCD分辨率(800x480),默认通道0
sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0
#Display.init(Display.ST7701, to_ide=True) #同时使用3.5寸mipi屏和IDE缓冲区显示图像,800x480分辨率
Display.init(Display.VIRT, sensor.width(), sensor.height()) #只使用IDE缓冲区显示图像
MediaManager.init() #初始化media资源管理器
sensor.run() #启动sensor
clock = time.clock()
while True:
clock.tick()
img = sensor.snapshot() #拍摄一张图片
# `threshold` 需要设置一个比价大的值来过滤掉噪声。
#这样在图像中检测到边缘亮度较低的矩形。矩形
#边缘量级越大,对比越强…
for r in img.find_rects(threshold = 10000):
img.draw_rectangle(r.rect(), color = (0, 0, 255),thickness=2) #画矩形显示
for p in r.corners(): img.draw_circle(p[0], p[1], 5, color = (0, 255, 0))#四角画小圆形
print(r)
#Display.show_image(img) #显示图片
#显示图片,仅用于LCD居中方式显示
Display.show_image(img, x=0,y=0)
print(clock.fps()) #打印FPS
这里在纸张上画上矩形,置于摄像头前方,运行可以看到矩形被识别标记,效果如下图所示:
快速线性回归(巡线)
快速线性回归的用途非常广泛,如比赛经常用到的小车、机器人巡线,可以通过线性回归的方式判断虚线和实线的轨迹。从而做出判断和响应。这里使用 K230对图像中的实线(或虚线)进行检测识别,并画图指示。CanMV集成了快速线性回归get_regression函数,位于image模块下,下面了解下其相关API
...
功能:线性回归计算.对图像所有阈值像素进行线性回归计算,通过最小二乘法进行,通常速度较快,但不能处理任何异常值;
参数:threshold: 必须是元组列表。 (lo, hi) 定义你想追踪的颜色范围。对于灰度图像,每个元组需要包含两个值:最小灰度值和最大灰度值。
返回:
...
image.get_regression(thresholds[, invert=False[, roi[, x_stride=2[, y_stride=1[,
area_threshold=10[, pixels_threshold=10[, robust=False]]]]]]])
为了提高处理效果,我们可以先将图像变成二值(黑白)图像,方法如下:
image.binary(thresholds[, invert=False[, zero=False[, mask=None[, to_bitmap=False[, copy=False]]]]])
巡线代码如下
'''
快速线性回归(巡线)
'''
import time, os, sys
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
THRESHOLD = (0, 100) # 黑白图像的灰度阈值
BINARY_VISIBLE = True # 使用二值化图像你可以看到什么是线性回归。
# 这可能降低 FPS(每秒帧数).
sensor = Sensor() #构建摄像头对象
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(width=320, height=240) #设置帧大小,默认通道0
sensor.set_pixformat(Sensor.GRAYSCALE) #设置输出图像格式,默认通道0
#Display.init(Display.ST7701, to_ide=True) #同时使用3.5寸mipi屏和IDE缓冲区显示图像,800x480分辨率
Display.init(Display.VIRT, sensor.width(), sensor.height()) #只使用IDE缓冲区显示图像
MediaManager.init() #初始化media资源管理器
sensor.run() #启动sensor
clock = time.clock()
while True:
clock.tick()
#image.binary([THRESHOLD])将灰度值在THRESHOLD范围变成了白色
img = sensor.snapshot().binary([THRESHOLD]) if BINARY_VISIBLE else sensor.snapshot()
# 返回一个类似 find_lines() 和find_line_segments()的对象.
# 有以下函数使用方法: x1(), y1(), x2(), y2(), length(),
# theta() (rotation in degrees), rho(), and magnitude().
#
# magnitude() 代表线性回归的指令,其值为(0, INF]。
# 0表示一个圆,INF数值越大,表示线性拟合的效果越好。
line = img.get_regression([(255,255) if BINARY_VISIBLE else THRESHOLD])
if (line):
img.draw_line(line.line(), color = 127,thickness=4)
print(line) #打印结果
#显示图片,仅用于LCD居中方式显示
Display.show_image(img, x=0,y=0)
print("FPS %f, mag = %s" % (clock.fps(), str(line.magnitude()) if (line) else "N/A"))
在纸张上画出轨迹,运行可以看到轨迹被白色线标记,效果如下图
至此,实现图像检测里的边缘检测、线段检测、圆形检测、矩形检测,快速线性回归(巡线)功能。
-
发表了主题帖:
《Linux内核深度解析》--内核互斥技术
本帖最后由 dirty 于 2025-1-22 16:55 编辑
本篇以笔记形式记录学习内核互斥技术,并做一定拓展学习。
在Linux内核中,可能出现多个进程、进程和硬中断、进程和软中断、多个处理器访问同一个对象等现象,我们需要使用互斥技术,确保在给定的时刻只有一个主体可以进入临界区访问对象。
如果临界区的执行时间比较长或者可能睡眠,可以使用下面这些互斥技术。
●信号量,大多数情况下我们使用互斥信号量。
●读写信号量。
●互斥锁。
●实时互斥锁。
如果临界区的执行时间很短,并且不会睡眠,可以使用下面这些互斥技术。
●原子变量。
●自旋锁。
●读写自旋锁,它是对自旋锁的改进,允许多个读者同时进入临界区。
●顺序锁,它是对读写自旋锁的改进,读者不会阻塞写者。
进程还可以使用下面的互斥技术。
●禁止内核抢占,防止被当前处理器上的其他进程抢占,实现和当前处理器上的他进程互斥。
●禁止软中断,防止被当前处理器上的软中断抢占,实现和当前处理器上的软中互斥。
●禁止硬中断,防止被当前处理器上的硬中断抢占,实现和当前处理器上的硬中互斥。
信号量
信号量(semaphore) 是进程和线程间控制共享资源访问的重要机制, 用于同步操作。信号量本质上是一个计数器, 用来跟踪资源的可用数量, 并通过增减信号量的值来控制对共享资源的访问权。 信号量的值可以理解为资源的数量, 信号量为 0 时表示资源已被占用, 当信号量为正数时表示资源可用。
内核使用的信号量结构体如下
include/linux/semaphore.h
struct semaphore {
raw_spinlock_t lock; //自旋锁,用来保护信号量的其他成员。
unsigned int count; //计数值,表示还可以允许多少个进程进入临界区
struct list_head wait_list; //等待进入临界区的进程链表。
};
初始化静态信号量的方法:
(1)SEMAPHORE INITIALIZER(name,n):指定名称和计数值,允许n个进程同时进入临界区。
(2)DEFINE SEMAPHORE(name):初始化一个互斥信号量。
在运行时动态初始化信号量的方法:
static inline void sema init(struct semaphore *sem, int val); //参数 val指定允许同时进入临界区的进程数量
获取信号量函数如下:
void down(struct semaphore *sem); //获取信号量,如果计数值是0,进程深度睡眠
int down_interruptible(struct semaphore *sem);//获取信号量,如果计数值是0,进程轻度睡眠。
int down_killable(struct semaphore *sem);//获取信号量,如果计数值是0,进程中度睡眠。
int down_trylock(struct semaphore *sem);//获取信号量,如果计数值是0,进程不等待。
int down_timeout(struct semaphore *sem, long jiffes),//获取信号量,指定等待的时间。
void up(struct semaphore*sem);//释放信号量
读写信号量
读写信号量是对互斥信号量的改进,允许多个读者同时进入临界区,读者和写者互斥猪和写者互斥,适合在以读为主的情况使用。
初始化静态读写信号量的方法:
DECLARE RWSEM(name);
在运行时动态初始化读写信号量的方法:
init rwsem(sem);
申请读锁的函数如下:
void down_read(struct rw semaphore *sem);//申请读锁,如果写者占有写锁或者正在等待写锁,那么进程深度睡眠。
int down_read_trylock(struct rw semaphore *sem);//尝试申请读锁,不会等待。如果申请成功,返回1;否则返回0。
void up_read(struct rwsemaphore *sem);//释放读锁
申请写锁的函数如下:
void down_write(struct rw_semaphore *sem);//申请写锁,如果写者占有写锁或者读者占有读锁,那么进程深度睡眼
int down_write_killable(struct rw_semaphore *sem);//申请写锁,如果写者占有写锁或者读者占有读锁,那么进程中度睡眠
int down_write_trylock(struct rw_semaphore *sem);//尝试申请写锁,不会等待。如果申请成功,返回1;否则返回0.
void downgrade_write(struct rw_semaphore *sem);//占有写锁以后,可以把写锁降级为读锁
void up_write(struct rw semaphore *sem);//释放写锁函数
互斥锁
互斥锁只允许一个进程进入临界区,适合保护比较长的临界区。尽管可以把二值信号量当作互斥锁使用,但是内核单独实现了互斥锁。 申请互斥锁的函数如下:
void mutex_lock(struct mutex *lock);//申请互斥锁,如果锁被占有,进程深度睡眠
int mutex_lock_interruptible(struct mutex *lock);//申请互斥锁,如果锁被占有,进程轻度睡眠。
int mutex_lock_killable(struct mutex *lock)//申请互斥锁,如果锁被占有,进程中度睡眠。
int mutex_tryock(struct mutex *lock);//申请互斥锁,如果申请成功,返回1:如果锁被其他进程占有,那么进程不等待,返回0.
void mutex_unlock(struct mutex *lock);//释放互斥锁
实时互斥锁
实时互斥锁是对互斥锁的改进,实现了优先级继承(priorityinheritance),解决了优先级反转(priority inversion)问题。
如果需要使用实时互斥锁,编译内核时需要开启配置宏CONFIGRTMUTEXES.
初始化静态实时互斥锁的方法:
DEFINE RT MUTEX(mutexname);
在运行时动态初始化实时互斥锁的方法:
rt mutex init(mutex);
申请实时互斥锁的函数如下:
void rt_mutex_lock(struct rt mutex *lock);//申请实时互斥锁,如果锁被占有,进程深度睡眠
int rt_mutex_lock_interruptible(struct rt mutex *lock);//申请实时互斥锁,如果锁被占有,进程轻度睡眠。
int rt_mutex_timed_lock(struct rt_mutex *lock, struct hrtimer sleeper *timeou//申请实时互斥锁,如果锁被占有,进程睡眠等待一段时间。
int rt_mutex_trylock(struct rt mutex *lock);//申请实时互斥锁,如果申请成功,返回1;如果锁被其他进程占有,进程不等待回 0.
void rt_mutex_unlock(struct rt mutex*lock);//释放实时互斥锁
原子变量用来实现对整数的互斥访问,通常用来实现计数器。
自旋锁
自旋锁用于处理器之间的互斥,适合保护很短的临界区,并且不允许在临界区睡眠申请自旋锁的时候,如果自旋锁被其他处理器占有,本处理器自旋等待(也称为忙等待)。进程、软中断和硬中断都可以使用自旋锁。
定义并且初始化静态自旋锁的方法如下:
DEFINE _SPINLOCK(x);
在运行时动态初始化自旋锁的方法如下:
spin_lock init(x);
申请自旋锁的函数如下:
void spin_lock(spinlock t *lock);//当前处理器自旋等待申请自旋锁,如果锁被其他处理器占有,
void spin_lock bh(spinlock t *lock);//申请自旋锁,并且禁止当前处理器的软中断。
void spin_lock irq(spinlock t *lock);//申请自旋锁,并且禁止当前处理器的硬中断。
spin_lock_irgsave(lock, flags);//申请自旋锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断
int spin_trylock(spinlock t *lock);//申请自旋锁,如果申请成功,返回1;如果锁被其他处理器占有,当前处理器不等待,立即返回 0。
释放自旋锁的函数如下:
(1)void spin unlock(spinlock t *lock);
(2)void spin unlock bh(spinlock t *lock);
释放自旋锁,并且开启当前处理器的软中断。
(3)void spin unlock irq(spinlock t *lock);
释放自旋锁,并且开启当前处理器的硬中断。
(4)void spin unlock irqrestore(spinlock t *lock, unsigned long flags);释放自旋锁,并且恢复当前处理器的硬中断状态。
读写自旋锁
读写自旋锁(通常简称读写锁)是对自旋锁的改进,区分读者和写者,允许多个读者同时进入临界区,读者和写者互斥,写者和写者互斥。如果读者占有读锁,写者申请写锁的时候自旋等待。如果写者占有写锁,读者申请读锁的时候自旋等待。
顺序锁
顺序锁区分读者和写者,和读写自旋锁相比,它的优点是不会出现写者饿死的情况。读者不会阻塞写者,读者读数据的时候写者可以写数据。顺序锁有序列号,写者把序列号加1,如果读者检测到序列号有变化,发现写者修改了数据,将会重试,读者的代价比较高。顺序锁支持两种类型的读者。
(1)顺序读者(sequencereaders):不会阻塞写者,但是如果读者检测到序列号有变化发现写者修改了数据,读者将会重试。
(2)持锁读者(lockingreaders):如果写者或另一个持锁读者正在访问临界区,持锁读者将会等待。持锁读者也会阻塞写者。这种情况下顺序锁退化为自旋锁。
如果使用顺序读者,那么互斥访问的资源不能是指针,因为写者可能使指针失效,读者访问失效的指针会出现致命的错误。
禁止内核抢占
内核抢占是指当进程在内核模式下运行的时候可以被其他进程抢占,编译内核时需要打开配置宏 CONFIG PREEMPT。
如果变量只会被本处理器上的进程访问,比如每处理器变量,可以使用禁止内核抢占的方法来保护,代价很低。如果变量可能被其他处理器上的进程访问,应该使用锁保护。
进程和软中断互斥
如果进程和软中断可能访问同一个对象,那么进程和软中断需要互斥,进程需要禁止软中新。
如果进程只需要和本处理器的软中断互斥,那么进程只需要禁止本处理器的软中断:如果进程要和所有处理器的软中断互斥,那么进程需要禁止本处理器的软中断,还要使用自旋锁和其他处理器的软中断互斥。
进程和硬中断互斥
如果进程和硬中断可能访问同一个对象,那么进程和硬中断需要互斥,进程需要禁止硬中断。如果进程只需要和本处理器的硬中断互斥,那么进程只需要禁止本处理器的硬中断;如果进程要和所有处理器的硬中断互斥,那么进程需要禁止本处理器的硬中断,还要使用自旋锁和其他处理器的硬中断互斥。
每处理器变量
在多处理器系统中,每处理器变量为每个处理器生成一个变量的副本,每个处理器访问自己的副本,从而避免了处理器之间的互斥和处理器缓存之间的同步,提高了程序的执行速度。每处理器变量分为静态和动态两种
内存屏障
内存屏障(memorybarrier)是一种保证内存访问顺序的方法,用来解决下面这些内存访问乱序问题。
内核支持3种内存屏障。
(1)编译器屏障。
(2)处理器内存屏障。
(3)内存映射I/O(Memory Mappping I/O,MMIO)写屏障。
RCU
RCU(Read-Copy Update)的意思是读-复制更新,它是根据原理命名的。写者修改对象的过程是:首先复制生成一个副本,然后更新这个副本,最后使用新的对象替换旧的对象。在写者执行复制更新的时候读者可以读数据。写者删除对象,必须等到所有访问被删除对象的读者访问结束,才能执行销毁操作。
RCU 的优点是读者没有任何同步开销:不需要获取何锁,不需要执行原子指令,(在除了阿尔法以外的处理器上)不需要执行内存屏障。但是写者的同步开销比较大,写者需要延迟对象的释放,复制被修改的对象,写者之间必须使用锁互斥。
RCU 根据数据结构可以分为以下两种:树型 RCU(tree RCU),微型 RCU(tinyRCU)。
死锁检测工具 lockdep
常见的死锁有以下4种情况。
(1)进程重复申请同一个锁,称为AA 死锁。例如,重复申请同一个自旋锁;使用读写锁,第一次申请读锁,第二次申请写锁。
(2)进程申请自旋锁时没有禁止硬中断,进程获取自旋锁以后,硬中断抢占,申请同一个自旋锁。这种 AA 死锁很隐蔽,人工审查很难发现。
(3)两个进程都要获取锁L1和L2,进程1持有锁L1,再去获取锁L2,如果这个时候进程2持有锁L2并且正在尝试获取锁L1,那么进程1和进程2就会死锁,称为AB-BA 死锁。
(4)在一个处理器上进程1持有锁L1,再去获取锁L2,在另一个处理器上进程2持有锁 L2,硬中断抢占进程2以后获取锁L1。这种AB-BA死锁很隐蔽,人工审查很难发现。
内核提供的死锁检测工具1ockdep用来发现内核的死锁风险。使用方法:死锁检测工具 lockdep 的配置宏如下:
(1)CONFIG_LOCKDEP:在配置菜单中看不到这个配置宏,打开配置宏CONFIGPROVE_LOCKING 或 CONFIG DEBUG LOCK ALLOC 的时候会自动打开这个配置宏。
(2)CONFIG PROVE LOCKING:允许内核报告死锁问题。
(3)CONFIG DEBUGLOCK ALLOC:检查内核是否错误地释放被持有的锁。
(4)CONFIG DEBUG LOCKING APISELFTESTS:内核在初始化的过程中运行一小段自我测试程序,自我测试程序检查调试机制是否可以发现常见的锁缺陷。
拓展
信号量的主要作用
控制共享资源的访问: 信号量充当标志, 控制对资源的并发访问, 保证只有一个进程或线程能占用资源。
进程与线程同步: 信号量可以协调进程或线程的执行顺序, 确保资源的访问按指定顺序进行。
Linux 中的信号量也分为 System V 和 POSIX 两种标准, 两者在用法和特性上有所不同; System V信号量提供了一个信号量集, 可以通过一个信号量标识符( semid) 来管理多个信号量。 其典型操作函数在 sys/sem.h 头文件中声明。 System V 信号量适用于需要多进程共享多个信号量的情况, 使用较为复杂,适合更细粒度的控制。 POSIX 信号量在 Linux 2.6 之后引入, 提供了单个信号量对象管理。 POSIX 信号量分为 命名信号量(可以在不同进程之间共享, 通过信号量的名称来标识) 和非命名信号量(只能在同一进程的线程间共享, 不支持跨进程) 两种。
System V 信号量的操作
创建和初始化信号量: 通过 semget 创建或获取信号量集。
信号量的 P(wait) 操作: 通过 semop 减少信号量的值, 如果信号量为 0, 则等待直到信号量大于0。
信号量的 V(signal) 操作: 通过 semop 增加信号量的值, 释放对资源的访问权。
控制和删除信号量: 通过 semctl 设置或获取信号量的值, 或删除信号量。
POSIX 信号量操作
相对而言, POSIX 信号量的操作更为简单, 主要使用以下函数:
创建和初始化信号量:
sem_open: 用于创建或打开命名信号量。
sem_init: 用于初始化非命名信号量。
信号量的 P(wait) 操作:
sem_wait: 等待信号量, 若信号量值为 0, 则阻塞。
信号量的 V(signal) 操作:
sem_post: 释放信号量, 增加信号量的值。
控制和删除信号量:
sem_close: 关闭信号量。
sem_unlink: 删除命名信号量。
本篇的内容特别概念原理是比较偏多较生涩,章节通过学习笔记了解内核互斥技术里所包含的术语定义、功能、原理以及内核实现函数,有一个较系统全面的轮廓。拓展是结合了实际工程应用需要一个点述。内核学习也是一个长期过程,也需要在实际工程应用中不断去学习领悟。
-
回复了主题帖:
【嘉楠K230开发板】基础功能汇总调测
Jacktang 发表于 2025-1-22 07:35
线程编程还是很有用的
是的,在API里回调函数实现线程功能
- 2025-01-21
-
发表了主题帖:
【嘉楠K230开发板】摄像头显示与画图
本篇讲述摄像头使用显示及简单的画图。
一.原理了解
K230 具备 3 路 MIPI-CSI 输入(3x2 lane/1x4+1x2 lane),最多可连接 3 路摄像头,每路摄像头支持输出 3 个通道,提供不同的分辨率和图像格式。开发板CSI2上已配有GC2093摄像头,查阅资料是支持200W像素(1920*1080),60fps。摄像头接口原理如下
关于K230显示,官网也做了些讲解。K230 配备 1 路 MIPI-DSI(1x4 lane),可驱动 MIPI 屏幕或通过接口芯片转换驱动 HDMI 显示器及支持虚拟IDE缓冲区显示。这里用于方便调试,选择支持虚拟IDE缓冲区显示。三者特点这里也了解下。
●MIPI显示屏:外接01Studio 3.5寸MiPi显示屏,可以一体化组装,适合离线部署调试使用。最大支持800x480分辨率。
●HDMI:外接HDMI显示屏,清晰度最高。最大支持1920x1080分辨率。
●IDE缓冲区显示:性价比最高,图像质量有一定下降,但能满足大部分场合调试使用。最大支持1920x1080分辨率。
二.代码准备
1.首先了解下摄像头及Display API使用函数及编码流程。
sensor.reset() ---复位和初始化摄像头。
sensor.set_framesize---设置每个通道的图像输出尺寸
sensor.set_pixformat---设置图像像素格式
sensor.set_hmirror---设置摄像头画面水平镜像
sensor.set_vflip---设置摄像头画面垂直翻转
Display.init---初始化Display模块
Display.deinit()---注销Display模块
2.关于画图API使用
sensor.snapshot()---通过摄像头拍摄方式返回image对象
image.Image---通过读取图片方式创建image对象
image.draw_line---画线段
image.draw_rectangle---画矩形
image.draw_circle---画圆
image.draw_arrow---画箭头
image.draw_cross---画十字交叉
image.draw_string---写字符
image.draw_string_advanced---写字符,支持中文。 这里实测“楠”字显示不出来。
3.代码编写
了解上面的API函数及构建流程,下面代码实现摄像头显示与画图功能。
'''
#1.实现摄像头图像采集显示
#2.画图
'''
import time, os, sys
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
sensor = Sensor() #构建摄像头对象
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(Sensor.FHD) #设置帧大小FHD(1920x1080),默认通道0
sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0
print(sensor.width(), sensor.height())
#使用IDE缓冲区输出图像,显示尺寸和sensor配置一致。
Display.init(Display.VIRT, sensor.width(), sensor.height())
MediaManager.init() #初始化media资源管理器
sensor.run() #启动sensor
clock = time.clock()
while True:
clock.tick()
'''摄像头图像采集显示'''
img = sensor.snapshot() #拍摄一张图
# Display.show_image(img) #显示图片
'''画图'''
# 画线段:从 x0, y0 到 x1, y1 坐标的线段,颜色红色,线宽度 2。
img.draw_line(20, 220, 100, 220, color = (255, 0, 0), thickness = 2)
#画矩形:绿色不填充。
img.draw_rectangle(150, 150, 200, 240, color = (0, 255, 0), thickness = 2, fill = False)
#画圆:蓝色不填充。
img.draw_circle(60, 320, 50, color = (0, 0, 255), thickness = 5, fill = False)
#画箭头:白色。
img.draw_arrow(150, 120, 250, 120, color = (255, 0, 255), size = 20, thickness = 5)
#画十字交叉。
img.draw_cross(60, 120, color = (0, 255, 255), size = 20, thickness = 2)
#写字符。
#img.draw_string(150, 200, "Hello 01Studio!", color = (255, 255, 255), scale = 4, mono_space = False)
#写字符,支持中文。
img.draw_string_advanced(100, 400, 50, "EEWorld & 01Studio", color = (255, 0, 255))
img.draw_string_advanced(100, 500, 50, "K230开发板", color = (255, 255, 0))
img.draw_string_advanced(100, 600, 66, "祝2025蛇年新春快乐", color = (255, 0, 0))
Display.show_image(img)
print(clock.fps()) #打印FPS
三.调试测验
IDE运行,帧缓冲区点选“缩放”,可以看到显示摄像头画面,以及画图,效果如下,可以看到帧率在45fps左右,动态显示视频,画图也是按设计显现。
至此,实现摄像头显示与画图功能。