- 2024-11-06
-
发表了主题帖:
嘉楠K230AI开发板测评7--AI Demo开发框架
本帖最后由 dfjs 于 2024-11-6 21:01 编辑
嘉楠科K230AI开发板测评7--AI视觉篇
1、AI视觉开发框架
更高级的机器视觉(AI视觉)需要使用KPU。可以简单类别比计算机的GPU(显卡),本质是实现高速的图像数据运算。
KPU是K230内部一个神经网络处理器,它可以在低功耗的情况下实现卷积神经网络计算,实时获取被检测目标的大小、坐标和种类,对人脸或者物体进行检测和分类。K230 KPU支持INT8和INT16, 典型网络下实测推理能力可达K210的13.7倍,MAC利用率超70%。
CanMV官方基于K230专门搭建了配套的AI视觉开发框架,框架结构如下图所示:
这个框架简单来说就是Sensor(摄像头)默认输出两路图像,一路格式为YUV420,直接给到Display显示;另一路格式为RGB888,给到AI部分进行处理。AI主要实现任务的前处理、推理和后处理流程,得到后处理结果后将其绘制在osd image实例上,并送给Display叠加,最后在HDMI、LCD或IDE缓冲区显示识别结果。
这套框架的优势是用户可以直接基于处理结果编程实现自己的功能,同时AI主要实现任务的前处理、推理和后处理流程也是通过Python代码实现,方便用户深入二次开发。充分满足不同用户和开发者的需求。
AI视觉开发框架主要API接口有:
PineLine : 将sensor、display封装成固定接口,用于采集图像、画图以及结果图片显示。
Ai2d : 预处理(Preprocess)相关接口。
AIBase : 模型推理主要接口。
2、相关接口
可在该网址查看:https://developer.canaan-creative.com/k230_canmv/main/zh/example/ai/AI_Demo%E8%AF%B4%E6%98%8E%E6%96%87%E6%A1%A3.html
2.1 PipeLine
将Media部分的代码封装在PipeLine类型中,通过固定的接口实现整个流程操作。
PipeLine类提供的接口包括:
初始化参数
rgb888p_size:list类型,预设给到AI部分的图像分辨率;如rgb888p_size=[1920,1080]。
display_size:list类型,显示部分Display的分辨率;如display_size=[1920,1080]。
display_mode:str类型,显示模式,包括”hdmi“和”lcd“;如display_mode=”hdmi“。
debug_mode:int类型,耗时调试模式,如果大于0,打印操作耗时;如debug_mode=0。
creat(sensor=None,hmirror=None,vfilp=None)
sensor:参数为可选参数,类型为Sensor对象,可自主配置现有CanMV、01Studio和k230d zero开发板实现了自动探测,可以默认使用create()实现。
hmirror:默认为None,当主动设置时为bool类型(True/False),表示是否实现水平方向镜像显示。
vflip: 默认为None,当主动设置时为bool类型(True/False),表示是否实现垂直方向翻转。
get_frame
返回一帧ulab.numpy.ndarray类型图像数据,分辨率为rgb888p_size,排布为CHW。
show_image
PipeLine实例中预设一帧OSD图像,该接口将成员变量osd_img显示在屏幕上。
destroy
销毁PipeLine实例。
示例代码:
from libs.PipeLine import PipeLine, ScopedTiming
from media.media import *
import gc
import sys,os
if __name__ == "__main__":
# 显示模式,默认"hdmi",可以选择"hdmi"和"lcd"
display_mode="hdmi"
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
# 初始化PipeLine,用于图像处理流程
pl = PipeLine(rgb888p_size=[1920,1080], display_size=display_size, display_mode=display_mode)
pl.create() # 创建PipeLine实例
try:
while True:
os.exitpoint() # 检查是否有退出信号
with ScopedTiming("total",1):
img = pl.get_frame() # 获取当前帧数据
print(img.shape)
gc.collect() # 垃圾回收
except Exception as e:
sys.print_exception(e) # 打印异常信息
finally:
pl.destroy() # 销毁PipeLine实例
通过pl.get_frame()接口获取一帧分辨率为rgb888p_size的图像,类型为ulab.numpy.ndarray,排布为CHW。基于上面的代码得到了一帧图像给AI处理,只关注AI推理部分的操作即可。
图像AI开发过程包括:图像预处理、模型推理、输出后处理的过程,整个过程封装在Ai2d类和AIBase类中。
2.2 Ai2d
对于Ai2d类,我们给出了常见的几种预处理方法,包括crop/shift/pad/resize/affine。该类别提供的接口包括:
初始化参数
debug_mode:int类型,耗时调试模式,如果大于0,打印操作耗时;如debug_mode=0。
Set_ai2d_dtype(input_format,output_format,input_type,output_type)
设置ai2d计算过程中的输入输出数据类型,输入输出数据格式。
Crop(start_x,start_y,width,height)预处理crop函数:
start_x:宽度方向的起始像素,int类型
start_y: 高度方向的起始像素,int类型
width: 宽度方向的crop长度,int类型
height: 高度方向的crop长度,int类型
Shift(shift_va预处理shift函数:
shift_val:右移的比特数,int类型
Pad(paddings,pad_mode,pad_val)预处理padding函数:
paddings:各个维度的padding, size=8,分别表示dim0到dim4的前后padding的个数,其中dim0/dim1固定配置{0, 0},list类型
pad_mode:只支持pad constant,配置0即可,int类型
pad_val:每个channel的padding value,list类型
Resize(interp_method,interp_mode)预处理resize函数:
interp_method:resize插值方法,ai2d_interp_method类型
interp_mode:resize模式,ai2d_interp_mode类型
Affine(interp_method,crop_round,bound_ind,bound_val,bound_smooth,M)预处理affine函数:
interp_method:Affine采用的插值方法,ai2d_interp_method类型
cord_round:整数边界0或者1,uint32_t类型
bound_ind:边界像素模式0或者1,uint32_t类型
bound_val:边界填充值,uint32_t类型
bound_smooth:边界平滑0或者1,uint32_t类型
M:仿射变换矩阵对应的vector,仿射变换为Y=[a_0, a_1; a_2, a_3] \cdot X + [b_0, b_1] $, 则 M=[a_0,a_1,b_0,a_2,a_3,b_1 ],list类型
Build(ai2d_input_shape,ai2d_output_shape):ai2d构造函数,前面配置的预处理方法起作用。
Run使用ai2d完成预处理
注意:
(1) Affine和Resize功能是互斥的,不能同时开启; (2) Shift功能的输入格式只能是Raw16; (3) Pad value是按通道配置的,对应的list元素个数要与channel数相等; (4) 当配置了多个功能时,执行顺序是Crop->Shift->Resize/Affine->Pad, 配置参数时注意要匹配;如果不符合该顺序,需要初始化多个Ai2d实例实现预处理过程;
示例代码如下:
from libs.PipeLine import PipeLine, ScopedTiming
from libs.AI2D import Ai2d
from media.media import *
import nncase_runtime as nn
import gc
import sys,os
if __name__ == "__main__":
# 显示模式,默认"hdmi",可以选择"hdmi"和"lcd"
display_mode="hdmi"
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
# 初始化PipeLine,用于图像处理流程
pl = PipeLine(rgb888p_size=[512,512], display_size=display_size, display_mode=display_mode)
pl.create() # 创建PipeLine实例
my_ai2d=Ai2d(debug_mode=0) #初始化Ai2d实例
# 配置resize预处理方法
my_ai2d.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel)
# 构建预处理过程
my_ai2d.build([1,3,512,512],[1,3,640,640])
try:
while True:
os.exitpoint() # 检查是否有退出信号
with ScopedTiming("total",1):
img = pl.get_frame() # 获取当前帧数据
print(img.shape) # 原图shape为[1,3,512,512]
ai2d_output_tensor=my_ai2d.run(img) # 执行resize预处理
ai2d_output_np=ai2d_output_tensor.to_numpy() # 类型转换
print(ai2d_output_np.shape) # 预处理后的shape为[1,3,640,640]
gc.collect() # 垃圾回收
except Exception as e:
sys.print_exception(e) # 打印异常信息
finally:
pl.destroy() # 销毁PipeLine实例
2.3 AIBase
AIBase部分封装了实现模型推理的主要接口,也是进行AI开发主要关注的部分。用户需要按照自己demo的要求实现前处理和后处理部分。
AIBase提供的接口包括:
初始化参数
kmodel_path:str类型,kmodel路径,用于初始化kpu对象并加载kmodel;
model_input_size:list类型,可选,模型输入分辨率,在单输入时起作用,格式为[width,height],如:model_input_size=[512,512];
rgb888p_size:list类型,可选,AI得到的图像的分辨率,在单输入时起作用,格式为[width,height],如:rgb888p_size=[640,640];
debug_mode:int类型,耗时调试模式,如果大于0,打印操作耗时;如debug_mode=0。
get_kmodel_inputs_num():返回当前模型的输入个数
get_kmodel_outputs_num():返回当前模型的输出个数
preprocess(input_np):使用ai2d对input_np做预处理,,如果不使用单个ai2d实例做预处理,需要在子类重写该函数。
inference(tensors):对预处理后得到的kmodel的输入(类型为tensor)进行推理,得到多个输出(类型为ulab.numpy.ndarray)
postprocess(results):模型输出后处理函数,该函数需要用户在任务子类重写,因为不同AI任务的后处理是不同的
run(input_np):模型的前处理、推理、后处理流程,适用于单ai2d实例能解决的前处理的AI任务,其他任务需要用户在子类重写。
deinit():AIBase销毁函数。
2.4 ScopedTiming
ScopedTiming 类在PipeLine.py模块内,是一个用来测量代码块执行时间的上下文管理器。上下文管理器通过定义包含 __enter__ 和 __exit__ 方法的类来创建。当在 with 语句中使用该类的实例时,__enter__ 在进入 with 块时被调用,__exit__ 在离开时被调用。
示例代码:
from libs.PipeLine import ScopedTiming
def test_time():
with ScopedTiming("test",1):
#####代码#####
# ...
##############
3、应用方法和示例
用户可根据具体的AI场景自写任务类继承AIBase,可以将任务分为如下四类:单模型任务、多模型任务,自定义预处理任务、无预处理任务。不同任务需要编写不同的代码实现,具体如下图所示:
3.1 单模型任务
该任务只有一个模型,只需要关注该模型的前处理、推理、后处理过程,此类任务的前处理使用Ai2d实现,可能使用一个Ai2d实例,也可能使用多个Ai2d实例,后处理基于场景自定义。
编写自定义任务类,主要关注任务类的config_preprocess、postprocess、以及该任务需要的其他方法如:draw_result等。
如果该任务包含多个Ai2d实例,则需要重写preprocess,按照预处理的顺序设置预处理阶段的计算过程。
单模型任务的伪代码结构如下:
from libs.PipeLine import PipeLine, ScopedTiming
from libs.AIBase import AIBase
from libs.AI2D import Ai2d
import os
from media.media import *
import nncase_runtime as nn
import ulab.numpy as np
import image
import gc
import sys
# 自定义AI任务类,继承自AIBase基类
class MyAIApp(AIBase):
def __init__(self, kmodel_path, model_input_size, 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
# sensor给到AI的图像分辨率,并对宽度进行16的对齐
self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]]
# 显示分辨率,并对宽度进行16的对齐
self.display_size = [ALIGN_UP(display_size[0], 16), display_size[1]]
# 是否开启调试模式
self.debug_mode = debug_mode
# 实例化Ai2d,用于实现模型预处理
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)
# 配置预处理操作,这里使用了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):
# 初始化ai2d预处理配置,默认为sensor给到AI的尺寸,可以通过设置input_image_size自行修改输入尺寸
ai2d_input_size = input_image_size if input_image_size else self.rgb888p_size
# 配置resize预处理方法
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列表,需要根据实际任务重写
def postprocess(self, results):
with ScopedTiming("postprocess", self.debug_mode > 0):
pass
# 绘制结果到画面上,需要根据任务自己写
def draw_result(self, pl, dets):
with ScopedTiming("display_draw", self.debug_mode > 0):
pass
if __name__ == "__main__":
# 显示模式,默认"hdmi",可以选择"hdmi"和"lcd"
display_mode="hdmi"
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
# 设置模型路径,这里要替换成当前任务模型
kmodel_path = "example_test.kmodel"
rgb888p_size = [1920, 1080]
###### 其它参数########
...
######################
# 初始化PipeLine,用于图像处理流程
pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode)
pl.create() # 创建PipeLine实例
# 初始化自定义AI任务实例
my_ai = MyAIApp(kmodel_path, model_input_size=[320, 320],rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0)
my_ai.config_preprocess() # 配置预处理
try:
while True:
os.exitpoint() # 检查是否有退出信号
with ScopedTiming("total",1):
img = pl.get_frame() # 获取当前帧数据
res = my_ai.run(img) # 推理当前帧
my_ai.draw_result(pl, res) # 绘制结果
pl.show_image() # 显示结果
gc.collect() # 垃圾回收
except Exception as e:
sys.print_exception(e) # 打印异常信息
finally:
my_ai.deinit() # 反初始化
pl.destroy() # 销毁PipeLine实例
多个Ai2d实例时的伪代码如下:
from libs.PipeLine import PipeLine, ScopedTiming
from libs.AIBase import AIBase
from libs.AI2D import Ai2d
import os
from media.media import *
import nncase_runtime as nn
import ulab.numpy as np
import image
import gc
import sys
# 自定义AI任务类,继承自AIBase基类
class MyAIApp(AIBase):
def __init__(self, kmodel_path, model_input_size, 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
# sensor给到AI的图像分辨率,并对宽度进行16的对齐
self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]]
# 显示分辨率,并对宽度进行16的对齐
self.display_size = [ALIGN_UP(display_size[0], 16), display_size[1]]
# 是否开启调试模式
self.debug_mode = debug_mode
# 实例化Ai2d,用于实现模型预处理
self.ai2d_resize = Ai2d(debug_mode)
# 设置Ai2d的输入输出格式和类型
self.ai2d_resize.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT, nn.ai2d_format.NCHW_FMT, np.uint8, np.uint8)
# 实例化Ai2d,用于实现模型预处理
self.ai2d_resize = Ai2d(debug_mode)
# 设置Ai2d的输入输出格式和类型
self.ai2d_resize.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT, nn.ai2d_format.NCHW_FMT, np.uint8, np.uint8)
# 实例化Ai2d,用于实现模型预处理
self.ai2d_crop = Ai2d(debug_mode)
# 设置Ai2d的输入输出格式和类型
self.ai2d_crop.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT, nn.ai2d_format.NCHW_FMT, np.uint8, np.uint8)
# 配置预处理操作,这里使用了resize和crop,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):
# 初始化ai2d预处理配置,默认为sensor给到AI的尺寸,可以通过设置input_image_size自行修改输入尺寸
ai2d_input_size = input_image_size if input_image_size else self.rgb888p_size
# 配置resize预处理方法
self.ai2d_resize.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel)
# 构建预处理流程
self.ai2d_resize.build([1,3,ai2d_input_size[1],ai2d_input_size[0]],[1,3,640,640])
# 配置crop预处理方法
self.ai2d_crop.crop(0,0,320,320)
# 构建预处理流程
self.ai2d_crop.build([1,3,640,640],[1,3,320,320])
# 假设该任务需要crop和resize预处理,顺序是先resize再crop,该顺序不符合ai2d的处理顺序,因此需要设置两个Ai2d实例分别处理
def preprocess(self,input_np):
resize_tensor=self.ai2d_resize.run(input_np)
resize_np=resize_tensor.to_numpy()
crop_tensor=self.ai2d_crop.run(resize_np)
return [crop_tensor]
# 自定义当前任务的后处理,results是模型输出array列表,需要根据实际任务重写
def postprocess(self, results):
with ScopedTiming("postprocess", self.debug_mode > 0):
pass
# 绘制结果到画面上,需要根据任务自己写
def draw_result(self, pl, dets):
with ScopedTiming("display_draw", self.debug_mode > 0):
pass
# 重写deinit,释放多个ai2d资源
def deinit(self):
with ScopedTiming("deinit",self.debug_mode > 0):
del self.ai2d_resize
del self.ai2d_crop
super().deinit()
if __name__ == "__main__":
# 显示模式,默认"hdmi",可以选择"hdmi"和"lcd"
display_mode="hdmi"
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
# 设置模型路径,这里要替换成当前任务模型
kmodel_path = "example_test.kmodel"
rgb888p_size = [1920, 1080]
###### 其它参数########
...
######################
# 初始化PipeLine,用于图像处理流程
pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode)
pl.create() # 创建PipeLine实例
# 初始化自定义AI任务实例
my_ai = MyAIApp(kmodel_path, model_input_size=[320, 320],rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0)
my_ai.config_preprocess() # 配置预处理
try:
while True:
os.exitpoint() # 检查是否有退出信号
with ScopedTiming("total",1):
img = pl.get_frame() # 获取当前帧数据
res = my_ai.run(img) # 推理当前帧
my_ai.draw_result(pl, res) # 绘制结果
pl.show_image() # 显示结果
gc.collect() # 垃圾回收
except Exception as e:
sys.print_exception(e) # 打印异常信息
finally:
my_ai.deinit() # 反初始化
pl.destroy() # 销毁PipeLine实例
3.2 自定义预处理任务
该任务只有一个模型,只需要关注该模型的前处理、推理、后处理过程,此类任务的前处理不使用Ai2d实现,可以使用ulab.numpy自定义,后处理基于场景自定义。
编写自定义任务类,主要关注任务类的preprocess、postprocess、以及该任务需要的其他方法如:draw_result等
对于需要重写前处理(不使用提供的ai2d类,自己手动写预处理)的AI任务伪代码如下:
from libs.PipeLine import PipeLine, ScopedTiming
from libs.AIBase import AIBase
from libs.AI2D import Ai2d
import os
from media.media import *
import nncase_runtime as nn
import ulab.numpy as np
import image
import gc
import sys
# 自定义AI任务类,继承自AIBase基类
class MyAIApp(AIBase):
def __init__(self, kmodel_path, model_input_size, 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
# sensor给到AI的图像分辨率,并对宽度进行16的对齐
self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]]
# 显示分辨率,并对宽度进行16的对齐
self.display_size = [ALIGN_UP(display_size[0], 16), display_size[1]]
# 是否开启调试模式
self.debug_mode = debug_mode
# 实例化Ai2d,用于实现模型预处理
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完成预处理的AI任务,使用封装的接口或者ulab.numpy实现预处理,需要在子类中重写该函数
def preprocess(self,input_np):
#############
#注意自定义预处理过程
#############
return [tensor]
# 自定义当前任务的后处理,results是模型输出array列表,需要根据实际任务重写
def postprocess(self, results):
with ScopedTiming("postprocess", self.debug_mode > 0):
pass
# 绘制结果到画面上,需要根据任务自己写
def draw_result(self, pl, dets):
with ScopedTiming("display_draw", self.debug_mode > 0):
pass
if __name__ == "__main__":
# 显示模式,默认"hdmi",可以选择"hdmi"和"lcd"
display_mode="hdmi"
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
# 设置模型路径,这里要替换成当前任务模型
kmodel_path = "example_test.kmodel"
rgb888p_size = [1920, 1080]
###### 其它参数########
...
######################
# 初始化PipeLine,用于图像处理流程
pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode)
pl.create() # 创建PipeLine实例
# 初始化自定义AI任务实例
my_ai = MyAIApp(kmodel_path, model_input_size=[320, 320],rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0)
my_ai.config_preprocess() # 配置预处理
try:
while True:
os.exitpoint() # 检查是否有退出信号
with ScopedTiming("total",1):
img = pl.get_frame() # 获取当前帧数据
res = my_ai.run(img) # 推理当前帧
my_ai.draw_result(pl, res) # 绘制结果
pl.show_image() # 显示结果
gc.collect() # 垃圾回收
except Exception as e:
sys.print_exception(e) # 打印异常信息
finally:
my_ai.deinit() # 反初始化
pl.destroy() # 销毁PipeLine实例
3.3 无预处理任务
该任务只有一个模型且不需要预处理,只需要关注该模型的推理和后处理过程,此类任务一般作为多模型任务的一部分,直接对前一个模型的输出做为输入推理,后处理基于需求自定义。
编写自定义任务类,主要关注任务类的run(模型推理的整个过程,包括preprocess、inference、postprocess中的全部或某一些步骤)、postprocess、以及该任务需要的其他方法如:draw_results等
对于不需要预处理(直接输入推理)的AI任务伪代码如下:
from libs.PipeLine import PipeLine, ScopedTiming
from libs.AIBase import AIBase
from libs.AI2D import Ai2d
import os
from media.media import *
import nncase_runtime as nn
import ulab.numpy as np
import image
import gc
import sys
# 自定义AI任务类,继承自AIBase基类
class MyAIApp(AIBase):
def __init__(self, kmodel_path, model_input_size, 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
# sensor给到AI的图像分辨率,并对宽度进行16的对齐
self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]]
# 显示分辨率,并对宽度进行16的对齐
self.display_size = [ALIGN_UP(display_size[0], 16), display_size[1]]
# 是否开启调试模式
self.debug_mode = debug_mode
# 自定义当前任务的后处理,results是模型输出array列表,需要根据实际任务重写
def postprocess(self, results):
with ScopedTiming("postprocess", self.debug_mode > 0):
pass
# 对于用预处理的AI任务,需要在子类中重写该函数
def run(self,inputs_np):
# 先将ulab.numpy.ndarray列表转换成tensor列表
tensors=[]
for input_np in inputs_np:
tensors.append(nn.from_numpy(input_np))
# 调用AIBase内的inference函数进行模型推理
results=self.inference(tensors)
# 调用当前子类的postprocess方法进行自定义后处理
outputs=self.postprocess(results)
return outputs
# 绘制结果到画面上,需要根据任务自己写
def draw_result(self, pl, dets):
with ScopedTiming("display_draw", self.debug_mode > 0):
pass
if __name__ == "__main__":
# 显示模式,默认"hdmi",可以选择"hdmi"和"lcd"
display_mode="hdmi"
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
# 设置模型路径,这里要替换成当前任务模型
kmodel_path = "example_test.kmodel"
rgb888p_size = [1920, 1080]
###### 其它参数########
...
######################
# 初始化PipeLine,用于图像处理流程
pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode)
pl.create() # 创建PipeLine实例
# 初始化自定义AI任务实例
my_ai = MyAIApp(kmodel_path, model_input_size=[320, 320],rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0)
my_ai.config_preprocess() # 配置预处理
try:
while True:
os.exitpoint() # 检查是否有退出信号
with ScopedTiming("total",1):
img = pl.get_frame() # 获取当前帧数据
res = my_ai.run(img) # 推理当前帧
my_ai.draw_result(pl, res) # 绘制结果
pl.show_image() # 显示结果
gc.collect() # 垃圾回收
except Exception as e:
sys.print_exception(e) # 打印异常信息
finally:
my_ai.deinit() # 反初始化
pl.destroy() # 销毁PipeLine实例
3.4 多模型任务
该任务包含多个模型,可能是串联,也可能是其他组合方式。对于每个模型基本上属于前三种模型中的一种,最后通过一个完整的任务类将上述模型子任务统一起来。
编写多个子模型任务类,不同子模型任务参照前三种任务定义。不同任务关注不同的方法。
编写多模型任务类,将子模型任务类统一起来实现整个场景。
以双模型串联推理为例,给出的伪代码如下:
from libs.PipeLine import PipeLine, ScopedTiming
from libs.AIBase import AIBase
from libs.AI2D import Ai2d
import os
from media.media import *
import nncase_runtime as nn
import ulab.numpy as np
import image
import gc
import sys
# 自定义AI任务类,继承自AIBase基类
class MyAIApp_1(AIBase):
def __init__(self, kmodel_path, model_input_size, 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
# sensor给到AI的图像分辨率,并对宽度进行16的对齐
self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]]
# 显示分辨率,并对宽度进行16的对齐
self.display_size = [ALIGN_UP(display_size[0], 16), display_size[1]]
# 是否开启调试模式
self.debug_mode = debug_mode
# 实例化Ai2d,用于实现模型预处理
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)
# 配置预处理操作,这里使用了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):
# 初始化ai2d预处理配置,默认为sensor给到AI的尺寸,可以通过设置input_image_size自行修改输入尺寸
ai2d_input_size = input_image_size if input_image_size else self.rgb888p_size
# 配置resize预处理方法
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列表,需要根据实际任务重写
def postprocess(self, results):
with ScopedTiming("postprocess", self.debug_mode > 0):
pass
# 自定义AI任务类,继承自AIBase基类
class MyAIApp_2(AIBase):
def __init__(self, kmodel_path, model_input_size, 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
# sensor给到AI的图像分辨率,并对宽度进行16的对齐
self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]]
# 显示分辨率,并对宽度进行16的对齐
self.display_size = [ALIGN_UP(display_size[0], 16), display_size[1]]
# 是否开启调试模式
self.debug_mode = debug_mode
# 实例化Ai2d,用于实现模型预处理
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)
# 配置预处理操作,这里使用了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):
# 初始化ai2d预处理配置,默认为sensor给到AI的尺寸,可以通过设置input_image_size自行修改输入尺寸
ai2d_input_size = input_image_size if input_image_size else self.rgb888p_size
# 配置resize预处理方法
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列表,需要根据实际任务重写
def postprocess(self, results):
with ScopedTiming("postprocess", self.debug_mode > 0):
pass
class MyApp:
def __init__(kmodel1_path,kmodel2_path,kmodel1_input_size,kmodel2_input_size,rgb888p_size,display_size,debug_mode):
# 创建两个模型推理的实例
self.app_1=MyApp_1(kmodel1_path,kmodel1_input_size,rgb888p_size,display_size,debug_mode)
self.app_2=MyApp_2(kmodel2_path,kmodel2_input_size,rgb888p_size,display_size,debug_mode)
self.app_1.config_preprocess()
# 编写run函数,具体代码根据AI任务的需求编写,此处只是给出一个示例
def run(self,input_np):
outputs_1=self.app_1.run(input_np)
outputs_2=[]
for out in outputs_1:
self.app_2.config_preprocess(out)
out_2=self.app_2.run(input_np)
outputs_2.append(out_2)
return outputs_1,outputs_2
# 绘制
def draw_result(self,pl,outputs_1,outputs_2):
pass
######其他函数########
# 省略
####################
if __name__ == "__main__":
# 显示模式,默认"hdmi",可以选择"hdmi"和"lcd"
display_mode="hdmi"
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
rgb888p_size = [1920, 1080]
# 设置模型路径,这里要替换成当前任务模型
kmodel1_path = "test_kmodel1.kmodel"
kmdoel1_input_size=[320,320]
kmodel2_path = "test_kmodel2.kmodel"
kmodel2_input_size=[48,48]
###### 其它参数########
# 省略
######################
# 初始化PipeLine,用于图像处理流程
pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode)
pl.create() # 创建PipeLine实例
# 初始化自定义AI任务实例
my_ai = MyApp(kmodel1_path,kmodel2_path, kmodel1_input_size,kmodel2_input_size,rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0)
my_ai.config_preprocess() # 配置预处理
try:
while True:
os.exitpoint() # 检查是否有退出信号
with ScopedTiming("total",1):
img = pl.get_frame() # 获取当前帧数据
outputs_1,outputs_2 = my_ai.run(img) # 推理当前帧
my_ai.draw_result(pl, outputs_1,outputs_2) # 绘制结果
pl.show_image() # 显示结果
gc.collect() # 垃圾回收
except Exception as e:
sys.print_exception(e) # 打印异常信息
finally:
my_ai.app_1.deinit() # 反初始化
my_ai.app_2.deinit()
pl.destroy() # 销毁PipeLine实例
- 2024-11-04
-
发表了主题帖:
嘉楠K230AI开发板测评6--条形码、二维码与AprilTag标签识别
嘉楠科K230AI开发板测评6--机器视觉篇
1、条形码识别
条形码(barcode)是将宽度不等的多个黑条和空白,按照一定的编码规则排列,用以表达一组信息的图形标识符。常见的条形码是由反射率相差很大的黑条(简称条)和白条(简称空)排成的平行线图案。条形码可以标出物品的生产国、制造厂家、商品名称、生产日期、图书分类号、邮件起止地点、类别、日期等许多信息,因而在商品流通、图书管理、邮政管理、银行系统等许多领域都得到广泛的应用。
编程实现条形码识别,并将识别到的信息通过串口终端打印出来。
对于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
条形码对象是由 image.find_barcodes 返回的。
barcode.corners()返回一个由该对象的四个角组成的四个元组(x,y)的列表。四个角通常是按照从左上角开始沿顺时针顺序返回的。
barcode.rect()返回一个矩形元组(x, y, w, h),用于如数据矩阵的边界框的 image.draw_rectangle 等其他的 image 方法。
barcode.payload()返回条形码的有效载荷的字符串。例:数量。
barcode.type()返回条形码的列举类型 (int)。
barcode.rotation()返回以弧度计的条形码的旋度(浮点数)。
barcode.quality()返回条形码在图像中被检测到的次数(int)。
调用find_barcodes()函数,对得到的结果再进行处理即可,代码编写流程如下:
参考代码如下:
'''
实验名称:条形码识别
实验平台:01Studio CanMV K230
说明:编程实现摄像头识别各类条形码
'''
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"
try:
sensor = Sensor() #构建摄像头对象
sensor.reset() #复位和初始化摄像头
#sensor.set_framesize(Sensor.FHD) #设置帧大小FHD(1920x1080),默认通道0
sensor.set_framesize(width=800, height=480) #设置帧大小VGA,默认通道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:
os.exitpoint() #检测IDE中断
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, 255, 255)) #图像显示条码信息
Display.show_image(img) #显示图片
print(clock.fps()) #打印帧率
###################
# IDE中断释放资源代码
###################
except KeyboardInterrupt as e:
print(f"user stop")
except BaseException as e:
print(f"Exception '{e}'")
finally:
# sensor stop run
if isinstance(sensor, Sensor):
sensor.stop()
# deinit display
Display.deinit()
os.exitpoint(os.EXITPOINT_ENABLE_SLEEP)
time.sleep_ms(100)
# release media buffer
MediaManager.deinit()
实验结果如下,为了更好地识别,图像上条形码需比较平展,不能太小;在线生成一个条形码值位“ABC-abc-1234”的Code 128型条形码,运行程序,打开条形码图片。摄像头正对条形码,识别成功后可以看到图片出现方框以及在串口终端打印出条形码信息。
2、二维码识别
二维码又称二维条码,常见的二维码为QR Code,QR全称Quick Response,是一个近几年来移动设备上超流行的一种编码方式,它比传统的Bar Code条形码能存更多的信息,也能表示更多的数据类型。
二维条码/二维码(2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的、黑白相间的、记录数据符号信息的图形;在代码编制上巧妙地利用构成计算机内部逻辑基础的“0”、“1”比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息,通过图象输入设备或光电扫描设备自动识读以实现信息自动处理:它具有条码技术的一些共性:每种码制有其特定的字符集;每个字符占有一定的宽度;具有一定的校验功能等。同时还具有对不同行的信息自动识别功能、及处理图形旋转变化点。
MicroPython中的find_qrcodes()即可获取摄像头采集图像中二维码的相关信息。
二维码对象是由 image.find_qrcodes 返回的。
qrcode.corners()返回一个由该对象的四个角组成的四个元组(x,y)的列表。四个角通常是按照从左上角开始沿顺时针顺序返回的。
qrcode.rect()返回一个矩形元组(x, y, w, h),用于如二维码的边界框的 image.draw_rectangle 等其他的 image 方法。
qrcode.payload()返回二维码有效载荷的字符串,例如URL 。
qrcode.version()返回二维码的版本号(int)。
qrcode.ecc_level()返回二维码的ECC水平(int)。
qrcode.mask()返回二维码的掩码(int)。
qrcode.data_type()返回二维码的数据类型。
qrcode.eci()返回二维码的ECI。ECI储存了QR码中存储数据字节的编码。若想要处理包含超过标准ASCII文本的二维码,您需要查看这一数值。
MicroPython编程我们只需要简单地调用find_qrcodes()函数,对得到的结果再进行处理即可,代码编写流程如下:
核心代码如下:
''''''''''''
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, 255, 255))
print(res[0].payload()) #串口终端打印
Display.show_image(img) #显示图片
print(clock.fps()) #打印帧率
''''''''''''
实验结果,为了更好地识别,图像上二维码需比较平展,不能太小,在线生成一个值为“https://www.bing.com”的QR码,运行程序,打开二维码图片;摄像头正对二维码,识别成功后可以看到图片出现方框以及在串口终端打印出二维码信息。
3、AprilTag标签识别
AprilTag是一种视觉基准系统,可用于多种任务,包括增强现实、机器人和相机校准。可以通过普通打印机创建目标,AprilTag 检测软件可以计算标签相对于相机的精确3D位置、方向和标识。
AprilTag通过特定的标志(与二维码相似,但是降低了复杂度以满足实时性要求),可以快速地检测标志,并计算相对位置。
AprilTag内容主要包含三个步骤:
第一步是如何根据梯度检测出图像中的各种边缘。
第二步即如何在边缘图像中找出需要的四边形图案并进行筛选,AprilTag尽可能的对检测出的边缘检测,首先剔除非直线边缘,在直线边缘进行邻接边缘查找,最终若形成闭环则为检测到一个四边形。
最后一个步便是如何进行二维码编码和二维码解码,编码方式分为三种,其黑边色块长度分别为8,7,6三个色块长度,对于解码内容,要在检测到的四边形内生成点阵列用于计算每色块的值,再根据局部二值模式(Local Binary Patterns)构造简单分类器对四边形内的色块进行分类,将正例色块编码为1将负例色块编码为0,就可以得到该二维码的编码。得到编码以后再与已知库内的编码进行匹配,确定解码出的二维码是否为正确。
可以将AprilTag简单地理解为一个特定信息的二维码,有family和ID两个概念:
TAG16H5 → 0 to 29
TAG25H7 → 0 to 241
TAG25H9 → 0 to 34
TAG36H10 → 0 to 2319
TAG36H11 → 0 to 586 (CanMV K230推荐使用)
ARTOOLKIT → 0 to 511
以【TAG36H11 → 0 to 586】为例,family信息就是:TAG36H11 , ID可以是“0 到 586” ,也就是一共有587种标记码。
不同家族区别:TAG16H5的有效区域是 4x4 的方块,那么它比TAG36H11看的更远(因为他有 6x6 个方块)。 但是内容少,所以TAG16H5的错误率比TAG36H11 高很多,因为TAG36H11的校验信息多。CanMV K210推荐使用TAG36H11家族的标记码。
可以在CanMV IDE生成AprilTag。点击工具--机器视觉--AprilTag生成器--TAG36H11家族:最小输入0 ,最大输入9 ,制作id从0-9共10张标签。点击OK后选择要生成的位置文件夹即可,如下图:
识别apriltag使用find_apriltags对象函数,返回一个 image.apriltag 对象的列表。
与二维码相比,AprilTags可在更远距离、较差光线和更扭曲的图像环境下被检测到。 AprilTags可应对所有种类的图像失真问题,而二维码并不能。也就是说,AprilTags只能将数字ID编码作为其有效载荷。
AprilTags也可用于本地化。每个 image.apriltag 对象都从摄像机返回其三维位置信息和旋转角度。 位置信息由 fx 、 fy 、 cx 和 cy 决定,分别为X和Y方向上图像的焦距和中心点。
tag.rect()返回一个矩形元组(x,y,w,h),二维码的边界。可以通过索引[0-3]来获得单个值。
tag.family()家族信息。
tag.id()ID信息。
tag.rotation()方向。
代码编写流程如下:
参考代码如下:
'''
实验名称:AprilTags标签识别
实验平台:01Studio CanMV K230
教程:wiki.01studio.cc
说明:推荐使用QVGA(320x240)分辨率,分辨率太高帧率会下降。
'''
import time, math, os, gc
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
# apriltag代码最多支持可以同时处理6种tag家族。
# 返回的tag标记对象,将有其tag标记家族及其在tag标记家族内的id。
tag_families = 0
tag_families |= image.TAG16H5 # 注释掉,禁用这个家族
tag_families |= image.TAG25H7 # 注释掉,禁用这个家族
tag_families |= image.TAG25H9 # 注释掉,禁用这个家族
tag_families |= image.TAG36H10 # 注释掉,禁用这个家族
tag_families |= image.TAG36H11 # 注释掉以禁用这个家族(默认家族)
tag_families |= image.ARTOOLKIT # 注释掉,禁用这个家族
#标签系列有什么区别? 那么,例如,TAG16H5家族实际上是一个4x4的方形标签。
#所以,这意味着可以看到比6x6的TAG36H11标签更长的距离。
#然而,较低的H值(H5对H11),意味着4x4标签的假阳性率远高于6x6标签。
#所以,除非你有理由使用其他标签系列,否则使用默认族TAG36H11。
def family_name(tag):
if(tag.family() == image.TAG16H5):
return "TAG16H5"
if(tag.family() == image.TAG25H7):
return "TAG25H7"
if(tag.family() == image.TAG25H9):
return "TAG25H9"
if(tag.family() == image.TAG36H10):
return "TAG36H10"
if(tag.family() == image.TAG36H11):
return "TAG36H11"
if(tag.family() == image.ARTOOLKIT):
return "ARTOOLKIT"
try:
sensor = Sensor(width=1280, height=960) #构建摄像头对象,将摄像头长宽设置为4:3
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:
os.exitpoint() #检测IDE中断
clock.tick()
img = sensor.snapshot() #拍摄图片
for tag in img.find_apriltags(families=tag_families): # 如果没有给出家族,默认TAG36H11。
img.draw_rectangle(tag.rect(), color = (255, 0, 0), thickness=4)
img.draw_cross(tag.cx(), tag.cy(), color = (0, 255, 0), thickness=2)
print_args = (family_name(tag), tag.id(), (180 * tag.rotation()) / math.pi) #打印标签信息
print("Tag Family %s, Tag ID %d, rotation %f (degrees)" % print_args)
#img.draw_string_advanced(0, 0, 30, code.payload(), color = (255, 255, 255)) #图像显示条码信息
#Display.show_image(img) #显示图片
#显示图片,LCD居中方式显示
Display.show_image(img, x=round((800-sensor.width())/2),y=round((480-sensor.height())/2)) #显示图片
print(clock.fps()) #打印帧率
###################
# IDE中断释放资源代码
###################
except KeyboardInterrupt as e:
print(f"user stop")
except BaseException as e:
print(f"Exception '{e}'")
finally:
# sensor stop run
if isinstance(sensor, Sensor):
sensor.stop()
# deinit display
Display.deinit()
os.exitpoint(os.EXITPOINT_ENABLE_SLEEP)
time.sleep_ms(100)
# release media buffer
MediaManager.deinit()
实验结果如下,打开family: TAG36H11 , id: 0的标签图片测试:
- 2024-11-03
-
回复了主题帖:
嘉楠K230AI开发板测评4--图像显示、画图、边缘/线段/圆形/矩形检测、线性回归
秦天qintian0303 发表于 2024-11-2 23:09
这个学习怎么喂图啊?拍摄好的?还是喂视频?
直接电脑截图,哈哈
-
回复了主题帖:
嘉楠K230AI开发板测评4--图像显示、画图、边缘/线段/圆形/矩形检测、线性回归
littleshrimp 发表于 2024-11-2 21:58
这个开发板这么强大吗?如果不用micropython能实现相同的功能吗?或者micropython在商用产品上使用适不 ...
micropython都是封装好的函数,比较容易上手,也可以用linux开发,不过比较麻烦,没有micropython的生态好,个人感觉拿来商用的话成本还是太高了
-
发表了主题帖:
嘉楠K230AI开发板测评5---颜色识别、摄像头物体计数与巡线
本帖最后由 dfjs 于 2024-11-3 21:29 编辑
嘉楠科K230AI开发板测评5--机器视觉篇
1.单一颜色识别
预先设定颜色阈值,如红、绿、蓝,这样K230摄像头采集图像后就能自动识别了。
CanMV集成了RGB565颜色块识别find_blobs函数(其位于image模块下),主要是基于LAB(L:亮度,取值0-100,表示从纯黑到纯白的变化;A代表从绿色到红色的范围,取值是-128--127;B代表从蓝色到黄色的范围,取值是-128--127)颜色模型,每个颜色都是用一组LAB阈值表示。
image.find_blobs(thresholds[, invert=False[, roi[, x_stride=2[, y_stride=1[, area_threshold=10 [, pixels_threshold=10[, merge=False[, margin=0[, threshold_cb=None[, merge_cb=None]]]]]]]]]])函数查找图像中指定的色块,返回image.blog对象列表。
thresholds: 必须是元组列表。 [(lo, hi), (lo, hi), ..., (lo, hi)] 定义你想追踪的颜色范围。 对于灰度图像,每个元组需要包含两个值 - 最小灰度值和最大灰度值。 仅考虑落在这些阈值之间的像素区域。 对于RGB565图像,每个元组需要有六个值(l_lo,l_hi,a_lo,a_hi,b_lo,b_hi) - 分别是LAB L,A和B通道的最小值和最大值;
area_threshold: 若色块的边界框区域小于此参数值,则会被过滤掉;
pixels_threshold: 若色块的像素数量小于此参数值,则会被过滤掉;
merge: 若为True,则合并所有没有被过滤的色块;
margin: 调整合并色块的边缘。
blob.rect()函数返回一个矩形元组(x,y,w,h),如色块边界。可以通过索引[0-3]来获得这些值。
blob.cx()返回色块(int)的中心x位置。可以通过索引[5]来获得这个值。
blob.cy()返回色块(int)的中心y位置。可以通过索引[6]来获得这个值。
代码编写流程如下:
参考代码:
'''
实验名称:单一颜色识别
实验平台:01Studio CanMV K230
教程:wiki.01studio.cc
'''
import time, os, sys
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
# 颜色识别阈值 (L Min, L Max, A Min, A Max, B Min, B Max) LAB模型
# 下面的阈值元组是用来识别 红、绿、蓝三种颜色,当然你也可以调整让识别变得更好。
thresholds = [(30, 100, 15, 127, 15, 127), # 红色阈值
(30, 100, -64, -8, 50, 70), # 绿色阈值
(0, 40, 0, 90, -128, -20)] # 蓝色阈值
try:
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:
os.exitpoint() #检测IDE中断
################
## 这里编写代码 ##
################
clock.tick()
img = sensor.snapshot() #拍摄一张图片
blobs = img.find_blobs([thresholds[0]]) # 0,1,2分别表示红,绿,蓝色。
if blobs:
for b in blobs: #画矩形和箭头表示
tmp=img.draw_rectangle(b[0:4], thickness = 2)
tmp=img.draw_cross(b[5], b[6], thickness = 2)
img.draw_string_advanced(0, 0, 30, 'FPS: '+str("%.3f"%(clock.fps())), color = (255, 255, 255))
Display.show_image(img) #显示图片
print(clock.fps()) #打印FPS
###################
# IDE中断释放资源代码
###################
except KeyboardInterrupt as e:
print("user stop: ", e)
except BaseException as e:
print(f"Exception {e}")
finally:
# sensor stop run
if isinstance(sensor, Sensor):
sensor.stop()
# deinit display
Display.deinit()
os.exitpoint(os.EXITPOINT_ENABLE_SLEEP)
time.sleep_ms(100)
# release media buffer
MediaManager.deinit()
实验结果,如下图1,也可通过阈值编辑器来手动调节LAB的阈值范围,如下图2。
2.多种颜色识别
基于单一颜色识别,加以修改,即可实现多种颜色识别。
代码编写流程如下:
核心代码如下,与单一颜色识别例程相比,修改的代码如下,在颜色识别前中加入了for循环,识别预设的3种颜色:
# 颜色识别阈值 (L Min, L Max, A Min, A Max, B Min, B Max) LAB模型
# 下面的阈值元组是用来识别 红、绿、蓝三种颜色,当然你也可以调整让识别变得更好。
thresholds = [(30, 100, 15, 127, 15, 127), # 红色阈值
(30, 100, -64, -8, 50, 70), # 绿色阈值
(0, 40, 0, 90, -128, -20)] # 蓝色阈值
colors1 = [(255,0,0), (0,255,0), (0,0,255)]
colors2 = ['RED', 'GREEN', 'BLUE']
..............
img = sensor.snapshot() #拍摄一张图片
for i in range(3):
blobs = img.find_blobs([thresholds[i]]) # 0,1,2分别表示红,绿,蓝色。
if blobs:
for b in blobs: #画矩形、箭头和字符表示
tmp=img.draw_rectangle(b[0:4], thickness = 4, color = colors1[i])
tmp=img.draw_cross(b[5], b[6], thickness = 2)
tmp=img.draw_string_advanced(b[0], b[1]-35, 30, colors2[i],color = colors1[i])
img.draw_string_advanced(0, 0, 30, 'FPS: '+str("%.3f"%(clock.fps())), color = (255, 255, 255))
Display.show_image(img) #显示图片
print(clock.fps()) #打印FPS
实验结果如下,将每个颜色的圆形用矩形画出并表明颜色,与单一颜色识别例程相比,修改的代码如下,在颜色识别前中加入了for循环,识别预设的3种颜色:
3.物体计数(相同颜色)
基于上一节颜色识别我们看到可以识别出色块的数量,来学习如何识别指定颜色的物体,计算其数量。
针对不同颜色的物体我们如何获取它的阈值呢?
先使用 摄像头代码采集物体图像,在IDE右上角缓冲区点击“禁用”将要识别的物体确认下来;点击 工具—机器视觉—阈值编辑器 。在弹出的对话框选择“帧缓冲区”。通过调整下方6个LAB值,使得物体颜色在右边为白色,其余背景为黑色。记录颜色的LAB值,在后面代码中使用,如下图。
代码编写流程如下图:
核心代码如下:
.............
thresholds = [(18, 72, -13, 31, 18, 83)] #黄色跳线帽阈值
.............
img = sensor.snapshot()
blobs = img.find_blobs([thresholds[0]])
if blobs: #画框显示
for b in blobs:
tmp=img.draw_rectangle(b[0:4])
tmp=img.draw_cross(b[5], b[6])
#显示计算信息
img.draw_string_advanced(0, 0, 30, 'FPS: '+str("%.3f"%(clock.fps()))+' Num: '
+str(len(blobs)), color = (255, 255, 255))
Display.show_image(img)
print(clock.fps()) #打印FPS
.............
实验结果如下图,在阈值准确的情况下,统计出跳线帽的数量:
4.机器人巡线(实线)
机器人巡线依然基于颜色识别,根据摄像头采集到的图像直线与中心偏离的位置计算出偏离角度。
对画面是有一定要求的,也就是摄像头采集图像一定要出现唯一1条连续的黑色直线。程序通过对画面切割成三部分,计算每个部分黑色线的中心点X坐标,然后采用加权平均算法估算出直线的偏离位置。通常情况下越靠近底部的地方离摄像头越近,顶部表示远方线段。因此底部的图形权重高。
假设摄像头当前画面的像素是例程的QQVGA分辨率:160(宽)X120(高),左上角坐标为(0,0),然后当前出现直线坐标为(80,120)至(160,0)偏右的直线。上中下三个部分的权重分别为0.1、0.3、0.7(底部图像靠近机器人,权重大,权重总和可以不是1),我们来计算一下其中心值,如下图:
上图中Y轴的中点坐标就是60,X坐标加权平均值计算如下:
X=(80*0.7+120*0.3+160*0.1)/(0.7+0.3+0.1)=98
那么直线偏离坐标可以认为是(98,60),图中绿色“+”位置。那么利用反正切函数可以求出偏离角度:a = atan((98-80)/60)=16.7°,机器人相当于实线的位置往左偏了,所以加一个负号,即 -16.7°;偏离角度就是这么计算出来的。得到偏离角度后就可以自己编程去调整小车或者机器人的运动状态,直到0°为没有偏离。
代码编写思路如下:
参考代码如下:
'''
实验名称:机器人巡线(实线)
实验平台:01Studio CanMV K230
教程:wiki.01studio.cc
# 黑色灰度线巡线跟踪示例
#
#做一个跟随机器人的机器人需要很多的努力。这个示例脚本
#演示了如何做机器视觉部分的线跟随机器人。你
#可以使用该脚本的输出来驱动一个差分驱动机器人
#跟着一条线走。这个脚本只生成一个表示的旋转值(偏离角度)
#你的机器人向左或向右。
#
# 为了让本示例正常工作,你应该将摄像头对准一条直线(实线)
#并将摄像头调整到水平面45度位置。请保证画面内只有1条直线。
'''
import time, os, sys, math
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
# 追踪黑线。使用 [(128, 255)] 追踪白线.
GRAYSCALE_THRESHOLD = [(0, 64)]
# 下面是一个roi【区域】元组列表。每个 roi 用 (x, y, w, h)表示的矩形。
'''
#采样图像QQVGA 160*120,列表把roi把图像分成3个矩形,越靠近的摄像头视野(通常为图像下方)的矩形权重越大。
ROIS = [ # [ROI, weight]
(0, 100, 160, 20, 0.7), # 可以根据不同机器人情况进行调整。
(0, 50, 160, 20, 0.3),
(0, 0, 160, 20, 0.1)
]
'''
#采样图像为QVGA 320*240,列表把roi把图像分成3个矩形,越靠近的摄像头视野(通常为图像下方)的矩形权重越大。
ROIS = [ # [ROI, weight]
(0, 200, 320, 40, 0.7), # 可以根据不同机器人情况进行调整。
(0, 100, 320, 40, 0.3),
(0, 0, 320, 40, 0.1)
]
# 计算以上3个矩形的权值【weight】的和,和不需要一定为1.
weight_sum = 0
for r in ROIS: weight_sum += r[4] # r[4] 为矩形权重值.
try:
sensor = Sensor(width=1280, height=960) #构建摄像头对象,将摄像头长宽设置为4:3
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:
os.exitpoint() #检测IDE中断
################
## 这里编写代码 ##
################
clock.tick()
img = sensor.snapshot() #拍摄一张图片
centroid_sum = 0
for r in ROIS:
blobs = img.find_blobs(GRAYSCALE_THRESHOLD, roi=r[0:4], merge=True) # r[0:4] 是上面定义的roi元组.
if blobs:
# Find the blob with the most pixels.
largest_blob = max(blobs, key=lambda b: b.pixels())
# Draw a rect around the blob.
img.draw_rectangle(largest_blob.rect())
img.draw_cross(largest_blob.cx(),
largest_blob.cy())
centroid_sum += largest_blob.cx() * r[4] # r[4] 是每个roi的权重值.
center_pos = (centroid_sum / weight_sum) # 确定直线的中心.
# 将直线中心位置转换成角度,便于机器人处理.
deflection_angle = 0
# 使用反正切函数计算直线中心偏离角度。可以自行画图理解
#权重X坐标落在图像左半部分记作正偏,落在右边部分记为负偏,所以计算结果加负号。
#deflection_angle = -math.atan((center_pos-80)/60) #采用图像为QQVGA 160*120时候使用
deflection_angle = -math.atan((center_pos-160)/120) #采用图像为QVGA 320*240时候使用
# 将偏离值转换成偏离角度.
deflection_angle = math.degrees(deflection_angle)
# 计算偏离角度后可以控制机器人进行调整.
print("Turn Angle: %f" % deflection_angle)
# LCD显示偏移角度,scale参数可以改变字体大小
img.draw_string_advanced(2,2,20, str('%.1f' % deflection_angle), color=(255,255,255))
#Display.show_image(img) #显示图片
#显示图片,仅用于LCD居中方式显示
Display.show_image(img, x=round((800-sensor.width())/2),y=round((480-sensor.height())/2))
print(clock.fps()) #打印FPS
###################
# IDE中断释放资源代码
###################
except KeyboardInterrupt as e:
print("user stop: ", e)
except BaseException as e:
print(f"Exception {e}")
finally:
# sensor stop run
if isinstance(sensor, Sensor):
sensor.stop()
# deinit display
Display.deinit()
os.exitpoint(os.EXITPOINT_ENABLE_SLEEP)
time.sleep_ms(100)
# release media buffer
MediaManager.deinit()
实验结果如下,以手机做黑色直线用,分别观察摄像头采集到没偏移、左偏和右偏各个直线的实验结果,可以看出效果良好。手机偏移角度分别为接近0°,负数,正数。获取到的偏移角度可以通过串口发送给其他外设或者主控。
- 2024-11-02
-
发表了主题帖:
嘉楠K230AI开发板测评4--图像显示、画图、边缘/线段/圆形/矩形检测、线性回归
本帖最后由 dfjs 于 2024-11-2 18:13 编辑
嘉楠科K230AI开发板测评4--机器视觉篇
摄像头
摄像头是整个机器视觉应用的基础,K230的引出了3路摄像头,接口如下图:
CanMV K230使用camera模块实现摄像头采集图像功能,K230硬件支持3路sensor输入(CSI接口),每个sensor设备均可独立完成图像数据采集捕获处理,并可以同时输出3路图像数据,sensor 0,sensor 1,sensor 2表示三个图像传感器;Camera Device 0,Camera Device 1,Camera Device 2表示三个sensor设备;output channel 0,output channel 1,output channel 2表示sensor设备的三个输出通道。三个图像传感器可以通过软件配置映射到不同的sensor 设备,示意图如下图。
摄像头(sensor)位于media模块下,通过from media.sensor import * #导入sensor模块,使用摄像头相关接口,sensor = Sensor(id,[width, height, fps])构建摄像头对象,id为CSI输入号,默认值为CSI2即开发板上的摄像头,width、height和fps为可选参数,分别表示sensor采集图像宽度,高度和帧率。
sensor.reset()复位和初始化摄像头。sensor.set_framesize(framesize = FRAME_SIZE_INVAILD, [width, height],chn = CAM_CHN_ID_0, alignment=0, **kwargs)设置每个通道的图像输出尺寸,framesize: 通道图像输出尺寸。chn: 通道编号,每个摄像头设备有3个通道。
sensor.set_pixformat(pixformat, chn = CAM_CHN_ID_0)设置图像像素格式。pixformat: 格式。chn: 通道编号,每个摄像头设备有3个通道。
sensor.set_hmirror(enable)设置摄像头画面水平镜像。sensor.set_vflip(enable)设置摄像头画面垂直翻转。
sensor.run()启动摄像头。
sensor.snapshot()使用相机拍摄一张照片,并返回 image 对象。
然后使用计算FPS(每秒帧数)的clock模块。clock=time.clock()构建一个时钟对象。clock.tick()开始追踪运行时间。clock.fps()停止追踪运行时间,并返回当前FPS(每秒帧数)。在调用该函数前始终首先调用 clock.tick(),完整代码编写流程如下。
参考代码如下,摄像头实时拍摄并显示在IDE缓冲区,由于CanMV K230 MicroPython底层基于Linux + RTOS实现,因此可以看到代码中出现一些辅助中断等代码,这些代码相对固定。
'''
实验名称:摄像头使用
实验平台:01Studio CanMV K230
说明:实现摄像头图像采集显示
'''
import time, os, sys
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
try:
sensor = Sensor() #构建摄像头对象
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(Sensor.FHD) #设置帧大小FHD(1920x1080),默认通道0
sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0
#使用IDE缓冲区输出图像,显示尺寸和sensor配置一致。
Display.init(Display.VIRT, sensor.width(), sensor.height())
MediaManager.init() #初始化media资源管理器
sensor.run() #启动sensor
clock = time.clock()
while True:
os.exitpoint() #检测IDE中断
################
## 这里编写代码 ##
################
clock.tick()
img = sensor.snapshot() #拍摄一张图
Display.show_image(img) #显示图片
print(clock.fps()) #打印FPS
###################
# IDE中断释放资源代码
###################
except KeyboardInterrupt as e:
print("user stop: ", e)
except BaseException as e:
print(f"Exception {e}")
finally:
# sensor stop run
if isinstance(sensor, Sensor):
sensor.stop()
# deinit display
Display.deinit()
os.exitpoint(os.EXITPOINT_ENABLE_SLEEP)
time.sleep_ms(100)
# release media buffer
MediaManager.deinit()
实验结果,点击运行代码,右边显示摄像头实时拍摄情况,下方则显示RGB颜色直方图。
图像的3种显示方式
在摄像头拍摄图像后我们需要观察图像,这就涉及如何显示的问题,目前CanMV K230支持3种显示方式。分别是:IDE缓冲区显示、外接HDMI显示器或MIPI显示屏,3种图像显示方式,各有特点:
IDE缓冲区显示:性价比最高,图像质量有一定下降,但能满足大部分场合调试使用。最大支持1920x1080分辨率。
HDMI:外接HDMI显示屏,清晰度最高。最大支持1920x1080分辨率。
MIPI显示屏:外接01Studio 3.5寸MiPi显示屏,可以一体化组装,适合离线部署调试使用。最大支持800x480分辨率。
首先导入Display模块,Display.init(type = None, width = None, height = None, osd_num = 1, to_ide = False, fps = None)初始化Display模块,type: 显示设备类型,VIRT : IDE缓冲区显示;LT9611 : HDMI显示;ST7701 : mipi显示屏。width: 可选参数,显示图像宽度;height: 可选参数,显示图像高度;to_ide: 同时在IDE显示,仅用于设置为HDMI或MIPI屏显示时使用。
Display.show_image(img, x = 0, y = 0, layer = None, alpha = 255, flag = 0),img为显示图像对象,x: 起始横坐标;y: 起始纵坐标。
Display.deinit(),注销Display模块,必须在MediaManager.deinit()之前, 在sensor.stop()之后调用。代码编写流程图如下图:
参考代码如下,只展示与摄像头节不同的地方(核心代码):
#################################
## 图像3种不同显示方式(修改注释实现)
#################################
Display.init(Display.VIRT, sensor.width(), sensor.height()) #通过IDE缓冲区显示图像
#Display.init(Display.LT9611, to_ide=True) #通过HDMI显示图像
#Display.init(Display.ST7701, to_ide=True) #通过01Studio 3.5寸mipi显示屏显示图像
实验结果,分别为IDE缓冲区,HDMI显示器和MIPI屏幕
画图
通过摄像头采集到照片后,我们会进行一些处理,而这时候往往需要一些图形来指示,比如在图CanMV已经将图片处理(包含画图)封装成各类模块,我们 只需要熟悉其构造函数和使用方法即可片某个位置标记箭头、人脸识别后用矩形框提示等。
img=sensor.snapshot()通过摄像头拍摄方式返回image对象。
image.draw_line()对图像进行画线段,参数为起始坐标,终点坐标,颜色与线条粗细。
image.draw_rectangle()画矩形,参数为起始坐标,宽度,高度,颜色,边框粗细,是否填充。
image.draw_circle()画圆,参数为圆心,宽度,高度,颜色,线条粗细,是否填充。
image.draw_arrow()画箭头,参数为起始坐标,终点坐标,颜色,箭头位置大小,线条粗细。
image.draw_cross()画十字交叉,参数为交叉中点坐标,颜色,大小,线条粗细。
image.draw_string()写字符,参数为起始坐标,字符内容,颜色,字体大小,强制间隔。
image.draw_string_advanced()写字符,支持中文,参数为起始坐标,字体大小,字符内容,颜色,字体类型。
代码编写思路如下:
核心代码如下:
img = sensor.snapshot()
# 画线段:从 x0, y0 到 x1, y1 坐标的线段,颜色红色,线宽度 2。
img.draw_line(20, 20, 100, 20, color = (255, 0, 0), thickness = 2)
#画矩形:绿色不填充。
img.draw_rectangle(150, 20, 100, 30, color = (0, 255, 0), thickness = 2, fill = False)
#画圆:蓝色不填充。
img.draw_circle(60, 120, 30, color = (0, 0, 255), thickness = 2, fill = False)
#画箭头:白色。
img.draw_arrow(150, 120, 250, 120, color = (255, 255, 255), size = 20, thickness = 2)
#画十字交叉。
img.draw_cross(60, 200, color = (255, 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(150, 180, 30, "Hello 01Studio", color = (255, 255, 255))
img.draw_string_advanced(40, 300, 30, "人生苦短, 我用Python", color = (255, 255, 255))
Display.show_image(img)
实验结果如下,在合适位置依次画出线段、矩形、圆形、箭头、十字交叉和字符:
图像检测
边缘检测
生活中每个物体都有一个边缘, 简单来说就是轮廓,使用MicroPython 结合 CanMV K230 自带的库来做图像轮廓检测。
CanMV集成了RGB565颜色块识别find_edges函数,位于 image 模块下,因此直接将拍摄到的图片进行处理即可。
直接通过image.find_edges(edge_type[, threshold])即可对图像进行边缘检测,参数edge_type为处理方式,image.EDGE_SIMPLE : 简单的阈值高通滤波算法(其基本原理是设置一个频率阈值,将高于该阈值的频率成分保留或增强,而将低于该阈值的频率成分抑制或去除,从而实现图像的锐化或边缘检测。); image.EDGE_CANNY: Canny 边缘检测算法(核心思想是找寻图像中灰度强度变化最强的位置,这些位置即边缘);threshold: 包含高、低阈值的二元组,默认是(100,200),仅支持灰度图像。
代码编写思路如下:
核心代码如下,对图片对象进行边缘:
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=round((800-sensor.width())/2),y=round((480-sensor.height())/2))
print(clock.fps()) #打印FPS
实验结果如下,对任务画像进行了边缘化:
线段检测
CanMV集成了线段识别 find_line_segments 函数,位于 image 模块下,因此我们直接将拍摄到的图片进行处理即可。
image.find_line_segments([roi[,merge_distance=0[,max_theta_difference=15]]])线段识别函数,返回image.line线段对象列表。参数roi: 识别区域(x,y,w,h),未指定则默认整张图片。参数merge_distance: 两条线段间可以相互分开而不被合并的最大像素。参数max_theta_difference: 将少于这个角度值的线段合并。大部分参数使用默认即可,不支持压缩图像和bayer图像。
代码编写流程如下:
核心代码如下:
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))
print(clock.fps()) #打印FPS
实验结果如下,IDE缓冲区标出图像中的横线。
圆形检测
CanMV集成了圆形识别find_circles函数,位于image模块下,因此我们直接将拍摄到的图片进行处理即可。
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]]]]]]]]]])找圆函数。返回一个image.circle圆形对象,该圆形对象有4个值: x, y(圆心), r (半径)和magnitude(量级);量级越大说明识别到的圆可信度越高。
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:控制识别步骤。
代码编写思路如下:
核心代码如下:
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, 0),thickness=2)
print(c) #打印圆形的信息
#Display.show_image(img) #显示图片
#显示图片,仅用于LCD居中方式显示
Display.show_image(img, x=round((800-sensor.width())/2),y=round((480-sensor.height())/2))
实验结果如下,图片检测识别结果如图。
矩形检测
CanMV集成了矩形识别find_rects函数,位于image模块下,因此我们直接将拍摄到的图片进行处理即可。
image.find_rects([roi=Auto, threshold=10000])矩形识别函数。返回一个image.rect矩形对象列表。
roi: 识别区域(x,y,w,h),未指定则默认整张图片;
threshold: 阈值。返回大于或等于threshold的矩形,调整识别可信度。
代码编写思路如下:
核心代码如下:
img = sensor.snapshot() #拍摄一张图片
# `threshold` 需要设置一个比价大的值来过滤掉噪声。
#这样在图像中检测到边缘亮度较低的矩形。矩形
#边缘量级越大,对比越强…
for r in img.find_rects(threshold = 10000):
img.draw_rectangle(r.rect(), color = (255, 0, 0),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=round((800-sensor.width())/2),y=round((480-sensor.height())/2))
print(clock.fps()) #打印FPS
实验结果,左边矩形识别结果如图。
快速线性回归(巡线)
快速线性回归的用途非常广泛,如比赛经常用到的小车、机器人巡线,可以通过线性回归的方式判断虚线和实线的轨迹,从而做出判断和响应。
CanMV集成了快速线性回归get_regression函数,位于image模块下。
mage.get_regression(thresholds[, invert=False[, roi[, x_stride=2[, y_stride=1[, area_threshold=10[, pixels_threshold=10[, robust=False]]]]]]])对图像所有阈值像素进行线性回
归计算。这一计算通过最小二乘法进行,通常速度较快,但不能处理任何异常值。若 robust 为True,则将使用泰尔指数。泰尔指数计算图像中所有阈值像素间的所有斜率的中值。
若在阈值转换后设定太多像素,即使在80x60的图像上,这一N^2操作也可能将您的FPS降到5以下。 但是,只要阈值转换后的进行设置的像素数量较少,即使在超过30%的阈值像素
为异常值的情况下,线性回归也依然有效。
threshold: 必须是元组列表。 (lo, hi) 定义你想追踪的颜色范围。对于灰度图像,每个元组需要包含两个值:最小灰度值和最大灰度值。
代码编写流程如下:
核心代码如下:
#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=round((800-sensor.width())/2),y=round((480-sensor.height())/2))
print("FPS %f, mag = %s" % (clock.fps(), str(line.magnitude()) if (line) else "N/A"))
实验结果,为了标明线性的变化趋势,取多组实验结果如下,串口结果包含拟合线段的两个点坐标,长度,以及非常重要的theta角度信息:
- 2024-10-28
-
发表了主题帖:
嘉楠K230 AI开发板测评3--RTC、ADC、PWM、UART、Thread、看门狗、文件读写
嘉楠科技CanMV 勘智(Kendryte)K230嵌入式AI开发板--测评3
----基础实验篇
RTC(实时时钟)
MicroPyrhon已经集成了内置时钟函数模块,位于machine的RTC模块中(https://docs.micropython.org/en/latest/library/machine.RTC.html#machine-rtc),实验的原理是读取RTC时钟数据。
首先构造RTC对象,rtc=machine.RTC(),设置RTC日期和时间,rtc.datetime(year,mon,
day,hour,min,sec,microsec),其中datetime有8个参数,按顺序分别为年、月、日、星期、时、分、秒、微妙,其中星期使用0-6表示星期一到星期日。同时rtc.datetime返回当前RTC时间元组。
通过代码首次上电如果检测到未设置时间先设置时间,然后周期打印获取的时间信息,代码流程图如图所示
参考代码:
'''
实验名称:RTC实时时钟
说明:实时时钟使用
教程:wiki.01studio.cc
'''
# 导入相关模块
from machine import RTC
import time
# 构建RTC对象
rtc = RTC()
# 首次上电设置RTC日期和时间。(2024, 1, 1, 0, 0, 0, 0, 0)按顺序分别表示(年,月,日,星期,时,分,秒,微妙),
# 其中星期使用0-6表示星期一到星期日。
if rtc.datetime()[0] != 2024:
rtc.datetime((2024, 1, 1, 0, 0, 0, 0, 0))
while True:
print(rtc.datetime()) #打印时间
time.sleep(1) #延时1秒
实验结果如下图,运行代码,可以看到终端打印当前RTC时间信息,注:RTC时间是断电丢失的,想要RTC时间连续运行需要持续对开发板供电。
ADC(电压测量)
ADC(analog to digital conversion) 模拟数字转换,意思就是将模拟信号转化成数字信号,由于单片机只能识别二级制数字,所以外界模拟信号常常会通过ADC转换成其可以识别的数字信息,常见的应用就是将变化的电压转成数字信号实现对电压值测量。
K230内部包含一个ADC硬件模块,有6个通道,采样分辨率为12bit(0-4095),采样速率为1M。01Studio CanMV K230开发板引出0~3共4个通道。其中通道0、1量程为0-3.6V(原理图如下左图),通道2、3量程为0-1.8V,引脚定义如下右图。
首先构造函数对象adc=machine.ADC(channel),channel为0-3,接着adc.read_u16()获取ADC值,测量精度是12位,返回0-4095,adc.read_uv()获取ADC电压值,返回0-1.8,通道0、1要乘以2,对应0-3.6V,通道2、3返回值与实际电压值一一对应,代码编写流程如下:
参考代码如下:
'''
实验名称:ADC(电压测量)
版本:v1.0
作者:01Studio
实验平台:01Studio CanMV K230
说明:ADC共4个通道,其中通道0、1实际量程为0-3.6V,通道2、3量程为0-1.8V。
(请勿超出测量量程, 可能导致主控芯片烧坏!)
'''
from machine import ADC
import time
'''
构建ADC对象:
ADC0(排针32引脚,量程0-3.6V), ADC1(排针36引脚,量程0-3.6V),
ADC2(排针38引脚,量程0-1.8V), ADC3(排针40引脚,量程0-1.8V)。
'''
adc = ADC(0) #通道0
while True:
print(adc.read_u16()) # 获取ADC通道采样值
# 获取ADC通道电压值,保留2为小数。通道0、1实际量程为0-3.6V,返回值x2。
print('%.2f'%(adc.read_uv()/1000000*2), "V")
time.sleep(1) #延时1秒
实验结果如下图,通过杜邦线将CanMV K230 ADC通道0引脚分别与和GND引脚和3.3V引脚短接分别测量,可以看到在一定误差范围内,结果正确。
PWM(蜂鸣器)
PWM(脉冲宽度调制)就是一个特定信号输出,主要用于输出不同频率、占空比(一个周期内高电平出现时间占总时间比例)的方波,以实现固定频率或平均电压输出,如下图占空比为75%的5V方波等效于3.75V,占空比为50%的5V方波等效于2.5V,占空比为20%的5V方波等效于1V。
蜂鸣器分为有源蜂鸣器与无源蜂鸣器,有源蜂鸣器只需要接上电源,蜂鸣器就能发声,断开电源就停止发声。而无源蜂鸣器,需要给指定的频率,才能发声,通过改变频率来改变蜂鸣器的发声音色。
首先构造PWM函数对象,PWM对象位于machine模块下,pwm = machine.PWM(channel, freq, duty, enable=False),channel为通道编号,取值0-3,PWM0对应GPIO42,PWM1对应GPIO43,PWM2对应GPIO46,PWM3对应GPIO47,frep为PWM频率,duty为PWM占空比,enable为PWM输出使能。可以通过pwm.freq([value]),pwm.duty([value]),直接设置频率与占空比。无源蜂鸣器我们可以用特定频率的方波来驱动,方波的原理很简单,就是一定频率的高低电平转换,可以简单理解成占空比为50%的PWM输出。代码流程如下:
参考代码如下
'''
实验名称:PWM
版本:v1.0
作者:01Studio
实验平台:01Studio CanMV K230
说明:通过不同频率的PWM信号输出,驱动无源蜂鸣器发出不同频率的声音。
'''
from machine import Pin, PWM
from machine import FPIOA
import time
#配置引脚42为PWM0功能
#通道0:GPIO42,通道1:GPIO43,通道2:GPIO46,通道3:GPIO47,
fpioa = FPIOA()
fpioa.set_function(42,FPIOA.PWM0)
#构建蜂鸣器PWM对象,通道0,频率为200Hz,占空比为50%,默认使能输出
Beep = PWM(0,200, 50, enable=True) # 在同一语句下创建和配置PWM,占空比50%
#蜂鸣器发出频率200Hz响声
Beep.freq(200)
time.sleep(1)
#蜂鸣器发出频率400Hz响声
Beep.freq(400)
time.sleep(1)
#蜂鸣器发出频率600Hz响声
Beep.freq(600)
time.sleep(1)
#蜂鸣器发出频率800Hz响声
Beep.freq(800)
time.sleep(1)
#蜂鸣器发出频率1000Hz响声
Beep.freq(1000)
time.sleep(1)
#停止
Beep.enable(False)
实验结果如下图,使用引脚GPIO42输出PWM波形,通过示波器测量PWM波形,在图一中,设置初始频率为200Hz,最终频率为800Hz;在图二中,设置默认频率为200Hz,初始占空比为50%,最终占空比为80%。
UART(串口通信)
K230内部包含五个UART硬件模块,其中UART0被小核终端占用,UART3被大核终端占用,剩余UART1,UART2,UART4。 01Studio CanMV K230开发板通过排针引出了UART1和UART2共2个UART供用户使用。
首先构造函数,machine.UART (id, baudrate=115200, bits=UART.EIGHTBITS, parity=UART.PARITY_NONE, stop=UART.STOPBITS_ONE ),id为串口编号,串口1(UART.UART1:TX1(GPIO3), RX1(GPIO4))和串口2(UART.UART2:TX2(GPIO11), RX2(GPIO12))可用,baudrate为波特率,常用的115200、9600,bits为数据位,默认8位,parity为奇偶校验位,默认None,stop为停止位,默认1。通过UART.read(num)和UART.write(buf)分别读取和写入缓冲数据,同时可通过UART.readline(num)整行读取。
需要使用一个USB转TTL工具,然后配合串口助手与K230通信,串口1的引脚与原理图如下图,IO3--TX1,IO4--RX1:
使用3.3V串口转TTL工具,接线示意图与实物图分别如下:
先初始化串口,然后给串口发去一条信息,这样PC机的串口助手就会在接收区显示出来,然后进入循环,当检测到有数据可以接收时候就将数据接收并打印,并通过REPL打印显示。代码编写流程图如下:
参考代码如下:
'''
实验名称:UART(串口通信)
作者:01Studio
实验平台:01Studio CanMV K230
说明:通过编程实现串口通信,跟电脑串口助手实现数据收发。
'''
#导入串口模块
from machine import UART
from machine import FPIOA
import time
fpioa = FPIOA()
# UART1代码
fpioa.set_function(3,FPIOA.UART1_TXD)
fpioa.set_function(4,FPIOA.UART1_RXD)
uart=UART(UART.UART1,115200) #设置串口号1和波特率
'''
# UART2代码
fpioa.set_function(11,FPIOA.UART2_TXD)
fpioa.set_function(12,FPIOA.UART2_RXD)
uart=UART(UART.UART2,115200) #设置串口号2和波特率
'''
uart.write('Hello 01Studio!')#发送一条数据
while True:
text=uart.read(128) #接收128个字符
if text != b'':
print(text) #通过REPL打印串口3接收的数据
time.sleep(0.1) #100ms
实验结果如下图,将串口助手调为COM6,波特率为115200,可以看到串口成功输出(如左图),同时使用串口助手向K230发送一串字符“http://www.cmsoft.cn”,也可以看到K230的UART1正确输出(如右图)。
Thread(线程)
当我们需要分时完成不同任务时候,线程编程就派上用场了,这有点像RTOS(实时操作系统),编程实现多线程同时运行任务。
CanMV K230的MicroPython固件已经集成了_thread线程模块。我们直接调用即可。该模块衍生于python3(https://docs.python.org/3.5/library/_thread.html#module-thread),属于低级线程,代码编写流程如下:
参考代码如下:
'''
实验名称:线程
版本: v1.0
作者:01Studio
实验平台:01Studio CanMV K230
说明:通过编程实现多线程。
'''
import _thread #导入线程模块
import time
#线程函数
def func(name):
while True:
print("hello {}".format(name))
time.sleep(1)
_thread.start_new_thread(func,("1",)) #开启线程1,参数必须是元组
_thread.start_new_thread(func,("2",)) #开启线程2,参数必须是元组
while True:
time.sleep(0.01) #防止CPU满跑
实验结果,运行代码,可以看到串口终端重复执行2个线程,如下图所示。
看门狗
任何代码在运行过程中都可能出现崩溃的情况,这时候就可以加入看门狗代码。看门狗的用途是在应用程序崩溃并最终进入不可恢复状态时自动重新启动系统。一旦启动,就无法以任何方式停止或重新配置。启用后,应用程序必须定期“喂食”看门狗,以防止其过期并重置系统。
CanMV K230的MicroPython固件已经集成了看门狗WDT模块,我们直接调用即可。
首先构造WDT对象,wdt=WDT(id,timeout)创建看门狗对象,id为看门狗编号,timeout为超时时间,wdt.feed()喂狗,需要在构建看门狗对象时指定的超时时间内执行该指令。代码编写流程如图所示:
参考代码如下:
'''
实验名称:看门狗
版本: v1.0
作者:01Studio
实验平台:01Studio CanMV K230
说明:看门狗测试。
'''
from machine import WDT #导入线程模块
import time
#构建看门狗对象。
wdt = WDT(1,3) #看门狗编号1,超时时间3秒。
#每隔1秒喂一次狗,执行3次。
for i in range(3):
time.sleep(1)
print(i)
wdt.feed() #喂狗
#停止喂狗,系统会重启。
while True:
time.sleep(0.01) #防止CPU满跑
实验结果,运行代码,可以看到串口终端打印了3次信息后自动重启,断开了IDE连接。有了看门狗,当开发板死机时候就可以自动重启了。
文件读写
在嵌入式编程中我们经常会遇到需要将某些数据实现掉电保存功能,如传感器数据等。往往会用到EEPROM、flash等一些储存方式。而CanMV K230 MicroPython自带文件系统,我们 只 需要将数据直接用文件方式保存即可。
micropython的文件操作大部分指令兼容CPython。因此我们可以直接使用Python编程来实现文件读写。CanMV K230盘符对应的目录为/sdcard/路径下,代码编写流程如下所示:
参考代码如下:
'''
实验名称:文件读写
版本: v1.0
作者:01Studio
实验平台:01Studio CanMV K230
说明:文件读写,将字符“01Studio”写入文件后再读取出来。
'''
###########
## 写文件
###########
f = open('/sdcard/1.txt', 'w') #以写的方式打开一个文件,没有该文件就自动新建
f.write('01Studio') #写入数据
f.close() #每次操作完记得关闭文件
###########
## 读文件
###########
f = open('/sdcard/1.txt', 'r') #以读方式打开一个文件
text = f.read()
print(text) #读取数据并在终端打印
f.close() #每次操作完记得关闭文件
实验结果,运行代码,可以看到串口终端打印了文件内容。打开CanMV盘符,在sdcard目录下可以看到刚刚代码新建的1.txt文件,里面的内容就是我们写入的01Studio。除了txt,你可以写入任何后缀的文件,如json、csv或者无后缀的文件。
- 2024-10-13
-
发表了主题帖:
嘉楠K230 AI开发板测评2--GPIO、LED、按键、定时器
GPIO介绍
K230开发板总共引出了46个引脚,有常见的通用GPIO口、4路PWM、2路串口、1路SPI、I2C、I2S和4路的ADC,提供5V和3.3V的电源输出引脚对外供电,用常见的type-c引脚对开发板供电,方便简洁,GPIO引脚图如图1所示。
图 1 K230 GPIO引脚图
点亮第一个LED
CanMV K230有一个与GPIO直接连接的可控制LED蓝灯,其连接到GPIO52,其原理图如图2所示,从电路图可以看出,当GPIO口为高电平时,蓝灯被点亮。
图 2 LED灯原理图
由于K230功能多,在引脚有限的情况下,会复用引脚功能,通过FPIO库(也叫现场可编程IO阵列,库介绍网址2.8 FPIOA 模块API手册 — K230 CanMV (canaan-creative.com))实现对引脚的不同功能的选择。
代码编写流程如下:
首先引入FPIO模块,对GPIO52引脚的功能设置为IO口功能。引入Pin对象设置IO口,通过Pin(id, mode, pull) 设置IO口的引脚,模式与电阻配置,value([X])设置输出电平。
完整代码如下:
'''
实验名称:点亮LED蓝灯
版本:v1.0
作者:01Studio
实验平台:01Studio CanMV K230
教程:wiki.01studio.cc
'''
from machine import Pin #导入Pin模块
from machine import FPIOA
import time
#将GPIO52配置为普通GPIO
fpioa = FPIOA()
fpioa.set_function(52,FPIOA.GPIO52)
LED=Pin(52,Pin.OUT) #构建led对象,GPIO52,输出
LED.value(1) #点亮LED,也可以使用led.on()
实验结果如图3所示:
图 3 点亮led灯
按键
按键是最常见的输入设备,通过检测按键被按下之后,改变LED灯的亮灭状态。按键对应的IO口引脚原理图如下图4所示,按键的一段接到GPIO21,另一端接到GND,所以按键在没按下的时候输入高电平1,按下的时候输入低电平0。
图 4 按键原理图
按键按下时的电平变化如下图5所示,会发生抖动,有可能会造成误判,因此需要延时函数来进行消抖。常见的方法就是当检测到按键值为0时,延时一段时间,大约10ms,再次判断按键引脚至仍然是0,是的话说明按键被按下,延时使用time模块(time – time related functions — MicroPython latest documentation)。
图 5 按键一次出现的电平变化
首先将IO21配置为GPIO功能(使用FPIO模块),需要将按键的GPIO21引脚配置为输入模式(使用pin模块),当K230检测到按键被按下时led灯点亮,松开时led灯熄灭,代码编写流程图如图6所示。
图 6 按键代码流程图
完整代码如下所示:
'''
实验名称:按键
版本:v1.0
作者:01Studio
实验平台:01Studio CanMV K230
说明:通过按键改变LED的亮灭状态
'''
from machine import Pin
from machine import FPIOA
import time
#将GPIO52、GPIO21配置为普通GPIO模式
fpioa = FPIOA()
fpioa.set_function(52,FPIOA.GPIO52)
fpioa.set_function(21,FPIOA.GPIO21)
LED=Pin(52,Pin.OUT) #构建LED对象,开始熄灭
KEY=Pin(21,Pin.IN,Pin.PULL_UP) #构建KEY对象
state=0 #LED引脚状态
while True:
if KEY.value()==0: #按键被按下
time.sleep_ms(10) #消除抖动
if KEY.value()==0: #确认按键被按下
state=not state #使用not语句而非~语句
LED.value(state) #LED状态翻转
print('KEY')
while not KEY.value(): #检测按键是否松开
pass
实验结果如图7所示,当按键KEY每次被按下时,LED灯的亮灭状态发生反转。
图 7 按键控制LED灯结果图
定时器
定时器常常用来计时,通过定时器来周期性的执行各种任务。
引用machine的Timer模块(2.11 Timer 模块API手册 — K230 CanMV (canaan-creative.com))用定时器让LED灯周期性每秒闪烁一次。
首先构造函数tim = machine.Timer(id),接着初始化tim.init(mode, freq, period, callback)就可以轻松调用定时器模块。
代码编写流程如图8所示,定时器到达预定设计时间后,产生中断,跟外部中断的编程方式类似。
图 8 定时器调用代码流程图
完整代码如下:
'''
实验名称:定时器
版本:v1.0
作者:01Studio
实验平台:01Studio CanMV K230
说明:通过定时器让LED周期性每秒闪烁1次。
'''
from machine import Pin,Timer
import time
led=Pin(52,Pin.OUT)
Counter = 0
Fun_Num = 0
def fun(tim):
global Counter
Counter = Counter + 1
print(Counter)
led.value(Counter%2)
#使用软件定时器,编号-1
tim = Timer(-1)
tim.init(period=1000, mode=Timer.PERIODIC,callback=fun) #周期为1000ms
while True:
time.sleep(0.01) #避免CPU满跑
实验结果:可以看到LED灯每隔一秒闪烁一次
- 2024-09-26
-
发表了主题帖:
嘉楠K230 AI开发板测评1--开发板初始篇
嘉楠科技CanMV 勘智(Kendryte)K230嵌入式AI开发板--测评1
1、开发板介绍:
嘉楠科技CanMV 勘智(Kendryte)K230嵌入式AI开发板基于Micropython语言(https://docs.micropython.org/en/latest/index.html),内置多个高清视频图像输入处理和智能硬件处理单元,兼顾高性能、低功耗和高安全性特点,实测推理能力可达K210的13.7倍,广泛应用于各类智能产品。
2、开发板照片
开发板配件安装完成之后,正面图片如图1所示,精密的PCB布局到高质量的元器件选型,再到无缝衔接的接口与外壳设计,无不透露出制造商对品质的不懈追求。
开发板背面如下图2所示,
3、资料介绍
CanMV K230 AI开发板基于嘉楠科技边缘计算芯片K230和CanMV开源项目。有着丰富的学习资料与开发手册,支持在线查看和线下学习。在线网站(https://wiki.01studio.cc/docs/canmv_k230)如下图3所示,
线下资料(百度网盘链接:https://pan.baidu.com/s/1zVOi2lHbaQhlQEqK-7s-Nw?pwd=01KJ提取码:01KJ)目录如下图4所示:
有着丰富的社区论坛(https://forum.01studio.cc/),论坛里面包含各个开发者分享的各种AI项目,问题描述和群组供大家学习。
4、环境搭建
根据文档( https://wiki.01studio.cc/docs/category/开发环境搭建-1)介绍,你可以轻松的一直点击下一步,下一步直到环境搭建完成,通过软件安装,镜像烧录固件,代码测试,串口交互,离线运行,你已经完成所有的环境测试,下一步可以进入基础实验。
5、初步测试
拿到开发板的第一件事,当然是了解板子的功能模块与IO口,CanMV K230支持常用的I/O(输入输出引脚)、UART(串口)、I2C、SPI、PWM、ADC等功能,
第二件事就是最令人开心的点亮灯环节了,当你点亮一个LED灯的时候,你已经成功了一半。通过machine模块的Pin类,设置IO口的引脚输入/输出功能和IO口的值,点灯成功,如下5所示。
我们可以在micropython的官方文档中查看machine模块的具体内容(https://docs.micropython.org/en/latest/library/machine.html#module-machine),在(https://docs.micropython.org/en/latest/library/machine.Pin.html)中查看Pin类的所有函数解释,以此提高阅读英文文档的能力。
- 2024-09-19
-
回复了主题帖:
测评入围名单: 嘉楠科技CanMV 勘智(Kendryte)K230嵌入式AI开发板
个人信息无误,确认可以完成测评分享计划。