- 2024-11-18
-
回复了主题帖:
【嘉楠科技 CanMV K230测评】应用项目——寻找最大色块,并使用串口返回
Jacktang 发表于 2024-11-18 07:35
寻找最大色块,如果没有检测到,就会返回一组默认值,是这个效果
是的,如果返回默认值的话,串口那边接收到(用的是32),就会控制云台和电机左右转动,来找,要是能找到的话,会再次进行定位。
- 2024-11-17
-
发表了主题帖:
【嘉楠科技 CanMV K230测评】应用项目——寻找最大色块,并使用串口返回
import time, os, sys
from machine import UART
from machine import FPIOA
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 = [(15, 85, 31, 72, -28, 92), # 红色阈值
(30, 100, -64, -8, 50, 70), # 橙色阈值
(37, 100, -56, 67, -92, 91), # 黑色阈值
(47, 100, -128, 127, -45, 89)] # 蓝色阈值
colors1 = [(255,0,0), (255,165,0), (0,0,255)]
colors2 = ["Red","Orange","Blue"]
fpioa = FPIOA()
fpioa.set_function(11,FPIOA.UART2_TXD)
fpioa.set_function(12,FPIOA.UART2_RXD)
uart = UART(UART.UART2,115200)
#寻找最大色块函数
def find_max(blobs):
max_size=0
for blob in blobs:
if blob.pixels() > max_size:
max_blob=blob
max_size = blob.pixels()
return max_blob
try:
sensor = Sensor() #构建摄像头对象
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(Sensor.FHD) #设置帧大小FHD(1920x1080),缓冲区和HDMI用,默认通道0
sensor.set_framesize(width=400,height=240) #设置帧大小800x480,LCD专用,默认通道0
sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0
# Display.init(Display.VIRT, sensor.width(), sensor.height()) #只使用IDE缓冲区显示图像
Display.init(Display.ST7701, to_ide=True) #通过01Studio 3.5寸mipi显示屏显示图像
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: #画矩形和箭头表示、
b = find_max(blobs)
tmp = img.draw_rectangle(b[0:4],thickness = 4, color=(255,255,255),fill = False)
tmp = img.draw_cross(b[5], b[6], thickness = 4)
x256 =b.cx()//256
x = b.cx()%256
y =b.cy()
pixels256 = b.pixels()//256
pixels = b.pixels()
Txdata = bytearray([0xa3,0xb3,x256,x,y,1,pixels256,pixels,0xc3])
uart.write(Txdata)
print(b.cx(),b.cy(),b.pixels())
#目前判断车是否距离球体 的 想法:
#对象的像素数量是否大于某一阈值、对象的质心坐标是否位于一帧图像的中点附近、
else:
x = 260
y = 180
x256 =y//256
pixels = 0
pixels256 = pixels
Txdata = bytearray([0xa3,0xb3,x256,x,y,0,pixels256,pixels,0xc3])
uart.write(Txdata)
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()
使用寻找色块的程序,在其基础上进行更改。实现寻找图片中最大的色块,并通过串口返回给其他单片机,控制小车及云台进行追球。上面是工程的完整代码。
def find_max(blobs):
max_size=0
for blob in blobs:
if blob.pixels() > max_size:
max_blob=blob
max_size = blob.pixels()
return max_blob
这个是寻找最大色块的代码。首先轮询检测到的所有blobs,提取其中的pixels参数,进行找最大值的处理,并最终返回找到的最大值。
if blobs:
for b in blobs: #画矩形和箭头表示、
b = find_max(blobs)
tmp = img.draw_rectangle(b[0:4],thickness = 4, color=(255,255,255),fill = False)
tmp = img.draw_cross(b[5], b[6], thickness = 4)
x256 =b.cx()//256
x = b.cx()%256
y =b.cy()
pixels256 = b.pixels()//256
pixels = b.pixels()
Txdata = bytearray([0xa3,0xb3,x256,x,y,1,pixels256,pixels,0xc3])
uart.write(Txdata)
print(b.cx(),b.cy(),b.pixels())
#目前判断车是否距离球体 的 想法:
#对象的像素数量是否大于某一阈值、对象的质心坐标是否位于一帧图像的中点附近、
else:
x = 260
y = 180
x256 =y//256
pixels = 0
pixels256 = pixels
Txdata = bytearray([0xa3,0xb3,x256,x,y,0,pixels256,pixels,0xc3])
uart.write(Txdata)
这里是通过串口发送的代码。
首先是对数据进行一下处理,来获取色块的横纵坐标,以及像素点大小。然后调用串口将采集的数据发送出去。同时在前面加上帧头,在后面加上帧尾。
如果没有检测到,就会返回一组默认值。
- 2024-11-10
-
发表了主题帖:
【嘉楠科技 CanMV K230测评】车牌识别检测
车牌识别
车牌识别相比于其他的要进行检测外,还要多加的一个步骤就是要进行OCR的识别。就是将图片中的数字、汉字、字母等转换为机器可读文本的信息。目前在智能停车场、智慧交通等领域中,都有车牌识别的相关的应用。K230也提供了车牌识别的一个demo供我们学习使用。下面是官方提供的例程代码。(对最后的打印位置做出了一点修改,更改为只有检测到车牌才会开启打印,且不打印帧率信息。)
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 image
import aidemo
import random
import gc
import sys
# 自定义车牌检测类
class LicenceDetectionApp(AIBase):
# 初始化函数,设置车牌检测应用的参数
def __init__(self, kmodel_path, model_input_size, 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
# sensor给到AI的图像分辨率
self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]]
# 显示分辨率
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)
# 配置预处理操作,这里使用了pad和resize,Ai2d支持crop/shift/pad/resize/affine
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
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]])
# 自定义当前任务的后处理
def postprocess(self, results):
with ScopedTiming("postprocess", self.debug_mode > 0):
# 对检测结果进行后处理
det_res = aidemo.licence_det_postprocess(results, [self.rgb888p_size[1], self.rgb888p_size[0]], self.model_input_size, self.confidence_threshold, self.nms_threshold)
return det_res
# 自定义车牌识别任务类
class LicenceRecognitionApp(AIBase):
def __init__(self,kmodel_path,model_input_size,rgb888p_size=[1920,1080],display_size=[1920,1080],debug_mode=0):
super().__init__(kmodel_path,model_input_size,rgb888p_size,debug_mode)
# kmodel路径
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]]
# 视频输出VO分辨率,宽16字节对齐
self.display_size=[ALIGN_UP(display_size[0],16),display_size[1]]
# debug模式
self.debug_mode=debug_mode
# 车牌字符字典
self.dict_rec = ["挂", "使", "领", "澳", "港", "皖", "沪", "津", "渝", "冀", "晋", "蒙", "辽", "吉", "黑", "苏", "浙", "京", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "桂", "琼", "川", "贵", "云", "藏", "陕", "甘", "青", "宁", "新", "警", "学", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "_", "-"]
self.dict_size = len(self.dict_rec)
self.ai2d=Ai2d(debug_mode)
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
def config_preprocess(self,input_image_size=None):
with ScopedTiming("set preprocess config",self.debug_mode > 0):
ai2d_input_size=input_image_size if input_image_size else self.rgb888p_size
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):
output_data=results[0].reshape((-1,self.dict_size))
max_indices = np.argmax(output_data, axis=1)
result_str = ""
for i in range(max_indices.shape[0]):
index = max_indices[i]
if index > 0 and (i == 0 or index != max_indices[i - 1]):
result_str += self.dict_rec[index - 1]
return result_str
# 车牌识别任务类
class LicenceRec:
def __init__(self,licence_det_kmodel,licence_rec_kmodel,det_input_size,rec_input_size,confidence_threshold=0.25,nms_threshold=0.3,rgb888p_size=[1920,1080],display_size=[1920,1080],debug_mode=0):
# 车牌检测模型路径
self.licence_det_kmodel=licence_det_kmodel
# 车牌识别模型路径
self.licence_rec_kmodel=licence_rec_kmodel
# 人脸检测模型输入分辨率
self.det_input_size=det_input_size
# 人脸姿态模型输入分辨率
self.rec_input_size=rec_input_size
# 置信度阈值
self.confidence_threshold=confidence_threshold
# nms阈值
self.nms_threshold=nms_threshold
# sensor给到AI的图像分辨率,宽16字节对齐
self.rgb888p_size=[ALIGN_UP(rgb888p_size[0],16),rgb888p_size[1]]
# 视频输出VO分辨率,宽16字节对齐
self.display_size=[ALIGN_UP(display_size[0],16),display_size[1]]
# debug_mode模式
self.debug_mode=debug_mode
self.licence_det=LicenceDetectionApp(self.licence_det_kmodel,model_input_size=self.det_input_size,confidence_threshold=self.confidence_threshold,nms_threshold=self.nms_threshold,rgb888p_size=self.rgb888p_size,display_size=self.display_size,debug_mode=0)
self.licence_rec=LicenceRecognitionApp(self.licence_rec_kmodel,model_input_size=self.rec_input_size,rgb888p_size=self.rgb888p_size)
self.licence_det.config_preprocess()
# run函数
def run(self,input_np):
# 执行车牌检测
det_boxes=self.licence_det.run(input_np)
# 将车牌部分抠出来
imgs_array_boxes = aidemo.ocr_rec_preprocess(input_np,[self.rgb888p_size[1],self.rgb888p_size[0]],det_boxes)
imgs_array = imgs_array_boxes[0]
boxes = imgs_array_boxes[1]
rec_res = []
for img_array in imgs_array:
# 对每一个检测到的车牌进行识别
self.licence_rec.config_preprocess(input_image_size=[img_array.shape[3],img_array.shape[2]])
licence_str=self.licence_rec.run(img_array)
rec_res.append(licence_str)
gc.collect()
return det_boxes,rec_res
# 绘制车牌检测识别效果
def draw_result(self,pl,det_res,rec_res):
pl.osd_img.clear()
if det_res:
point_8 = np.zeros((8),dtype=np.int16)
for det_index in range(len(det_res)):
for i in range(4):
x = det_res[det_index][i * 2 + 0]/self.rgb888p_size[0]*self.display_size[0]
y = det_res[det_index][i * 2 + 1]/self.rgb888p_size[1]*self.display_size[1]
point_8[i * 2 + 0] = int(x)
point_8[i * 2 + 1] = int(y)
for i in range(4):
pl.osd_img.draw_line(point_8[i * 2 + 0],point_8[i * 2 + 1],point_8[(i+1) % 4 * 2 + 0],point_8[(i+1) % 4 * 2 + 1],color=(255, 0, 255, 0),thickness=4)
pl.osd_img.draw_string_advanced( point_8[6], point_8[7] + 20, 40,rec_res[det_index] , color=(255,255,153,18))
if __name__=="__main__":
# 显示模式,默认"hdmi",可以选择"hdmi"和"lcd"
display_mode="lcd"
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
# 车牌检测模型路径
licence_det_kmodel_path="/sdcard/app/tests/kmodel/LPD_640.kmodel"
# 车牌识别模型路径
licence_rec_kmodel_path="/sdcard/app/tests/kmodel/licence_reco.kmodel"
# 其它参数
rgb888p_size=[640,360]
licence_det_input_size=[640,640]
licence_rec_input_size=[220,32]
confidence_threshold=0.2
nms_threshold=0.2
# 初始化PipeLine,只关注传给AI的图像分辨率,显示的分辨率
pl=PipeLine(rgb888p_size=rgb888p_size,display_size=display_size,display_mode=display_mode)
pl.create()
lr=LicenceRec(licence_det_kmodel_path,licence_rec_kmodel_path,det_input_size=licence_det_input_size,rec_input_size=licence_rec_input_size,confidence_threshold=confidence_threshold,nms_threshold=nms_threshold,rgb888p_size=rgb888p_size,display_size=display_size)
clock = time.clock()
try:
while True:
os.exitpoint()
clock.tick()
img=pl.get_frame() # 获取当前帧
det_res,rec_res=lr.run(img) # 推理当前帧
lr.draw_result(pl,det_res,rec_res) # 绘制当前帧推理结果
pl.show_image() # 展示推理结果
gc.collect()
if det_res:
print(det_res,rec_res) #打印结果
# print(clock.fps()) #打印帧率
except Exception as e:
sys.print_exception(e)
finally:
lr.licence_det.deinit()
lr.licence_rec.deinit()
pl.destroy()
在官方提供的代码中,可以看到,将车牌识别的过程大致分为了两个过程。第一个过程是车牌检测,第二个过程是车牌识别。这和大多数的机器视觉系统的处理过程是类似的,都是先进行检测,然后再进行。针对这两个过程,分别定义了两个基于AIBase的类,分别用于做检测和识别的工作。
用于车牌检测的类叫做LicenceDetectionApp,里面主要对图像进行了预处理和后处理的方法的定义,其中预处理中主要调用了AI2D中的resize方法,对图片进行了适当的尺寸变换。
用于车牌识别的类叫做LicenceRecognitionApp,里面主要对图像进行了预处理和后处理的方法的定义,其中预处理中主要调用了AI2D中的resize方法,对图片进行了适当的尺寸变换。
最后定义了一个用于车牌检测和识别的类,包含这上面定义的两个类,从而实现更加简单的使用的方法。这个类中,定义了两个方法一个是run,另一个是draw_result,分别用于运行模型和显示结果。
在run中,首先调用了LicenceDetectionApp中的父类AIBase的run方法,将采集到的图像进行了车牌检测,并将返回的列表存入了det_boxes变量中。然后使用aidemo中的ocr相关的函数,将列表中的车牌抠出来。然后开始进行循环,对列表中被抠出的车牌进行检测。最终返回det_boxes和rec_res。
在draw_result中,首先判断是否有有检测到的信息,如果有,就会开始围着检测到的车牌进行画框,并写上ocr得出的字符串信息。
在主函数中,首先调用pl获取当前的图像帧,然后对该帧进行run,来获取车牌的位置信息以及OCR后得到的字符串信息。再调用draw_result方法将获取的两个信息传入到pl中在图像上进行显示。并通过串行终端进行数据的打印显示。最终调用pl中的show_image方法,实现最终的显示效果。
这样就完成了一个完整的车牌检测和识别的完整过程。
下面是效果的演示。找了一个标准的北京号牌,可以实现非常完美的框选和识别的功能。
可以利用这个车牌检测和识别的功能,做一个仿停车场的管理系统,可以实现车辆的出入管理,计时计费等功能。
- 2024-11-03
-
发表了主题帖:
【嘉楠科技 CanMV K230测评】人脸检测、手掌检测和手势识别
人脸检测
视觉应用最为广泛的领域可能就是人脸识别,不论是家用的门锁上安装的摄像头或者是小区、公司中的门禁机,甚至是机场等公共场所中用于追犯人的人脸检测系统,都是人脸识别的实际应用。而人脸识别的第一步就是要准确的在图像中检测到人脸。这也是人脸识别的第一步,人脸检测。
经过查询,最常用也最基础的人脸检测算法是Haar算子特征检测。但是K230将AI开发的过程进行了非常多的简化。如下图所示,是嘉楠官方提供的一个AI开发的框架,我们需要做的仅是完成AI部分中的下面五个步骤,分别是配置前处理、前处理、推理、后处理以及显示结果。后面的AI的代码开发也基本上会围绕着这些内容开展。通常是创建一个类,然后在类中分别定义几个方法,实现下面的几个步骤,然后实例这个类,并在合适的时候调用对应的方法即可实现对应的功能。
为了方便我们开发AI相关的内容,官方提供给我们几个封装好的API接口,分别是PineLine、Ai2d、AIBase。他们所做的事情是完成了图像的采集和显示、预处理的相关接口以及模型推理的相关接口。使用上述的接口来实现后面的AI的视觉例程。
Python中有类的概念,通常一个类的第一个方法都是init,这个方法会在实例化的时候自动进行初始化,同时将传入的参数赋值给这个实例化,如果没有这个init的函数的话,就像是它短暂的存在了一段时间,但是这个东西本身是不存在的一样。所以加入init函数还是很重要的。在后面的AI视觉中的Python知识较为复杂一些,所以有必要同时补充一些Python的基础知识。
下面我们直接参考官方提供的代码,进行分析。
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="hdmi"
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
# 设置模型路径和其他参数
kmodel_path = "/sdcard/app/tests/kmodel/face_detection_320.kmodel"
# 其它参数
confidence_threshold = 0.5
nms_threshold = 0.2
anchor_len = 4200
det_dim = 4
anchors_path = "/sdcard/app/tests/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()
try:
while True:
os.exitpoint() # 检查是否有退出信号
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()) #打印帧率
except Exception as e:
sys.print_exception(e) # 打印异常信息
finally:
face_det.deinit() # 反初始化
pl.destroy() # 销毁PipeLine实例
首先是在代码中引入了大量的库文件,包括了AI相关的库和一些基础的库文件。
然后紧接着就是定义了一个人脸检测的类。在这个类中一共创建了五个方法:初始化、配置预处理、后处理、显示结果、获取padding参数。
方法-初始化:在这个方法中,主要是利用了基类的构造函数进行了构造,然后将初始化的参数传入到创建的实例中,这些参数包括了模型的路径、模型的分辨率等等。
方法-配置预处理:在这个方法中主要就是调用了Ai2d相关的一些函数,进行了诸如crop/shift/pad/resize/affine等的操作,这些操作就是对采集的图像进行一些裁剪、平移等等。在这个实验中主要就是进行了pad和resize的操作。这个方法中调用了后面要定义的方法get_padding_param来获取想要pad的一些参数,然后使用这些参数进行pad。并完成resize缩放图像到一个合适的尺寸后,进行构建预处理,这样就完成了图像的一些预处理的工作。
方法-后处理:这个方法中主要就是调用了aidemo库中的一个人脸检测的接口,传入一些阈值信息进行检测,若有相关的数据,则返回数据,否则就返回空的。
方法-显示结果:这个方法就是去检测有没有检测到的人脸对象,如果有,就开始清除当前的OSD,即屏幕上显示的东西,然后将返回的数据转成在屏幕上显示的对应的数据,并调用pl即PineLine中关于绘制图像相关的方法,将检测的结果标识在图像上进行显示。
方法-获取padding参数:这个方法主要是为了给配置预处理中的pad提供一些参数,这里会获取到模型输入的高度和宽度然后对高度宽度进行缩放和处理后,将这个整理好的数据传给预处理过程中,实现一个pad的操作。
完成人脸检测类的创建后,下面就要开始实例,并调用对应的AI相关的操作,开始进行识别和检测了。
首先是给一些参数一些确定的数值,比如说模型的路径、设定的阈值、显示的尺寸等等。
然后首先是实例一个PineLine,叫做pl,并进行一个创建。这样就确保了图像的获取、显示、画图可以正常实现。
紧接着就是实例一个人脸检测的类,这里需要传入init方法中的参数,然后进行预处理的配置,这个预处理的配置实际上使用了两个方法,就是第二个和第五个方法。
然后开始进行图像的获取和推理。
首先使用pl中的方法get frame获取一个图像帧,传入到img中,并将img传入到之前实例的人脸检测类中,这里使用的方法是run,很明显没有定义这个方法,是因为这个run是继承自AIBase父类的。开始进行推理当前帧。
当检测到人脸的时候,就会打印出结果。然后执行一下人脸检测类中的第四个方法,就是绘制结果。同时调用pl将结果进行显示。
这样就完成了一个AI的人脸检测的过程。
下面我们来演示一下效果。在这里使用手机找了一张图片,可以看到准确的识别出了照片上的三个人,并使用框进行了准确的框选。
后面,我们就可以在这个的基础上进行更多的AI视觉相关的开发和学习。
手掌检测
对于手掌的检测和识别,可以实现手势的识别,从而用来使用手势实现一些控制的操作。
手掌检测和人脸检测的识别其实是大致类似的一个流程。在手掌检测程序中定义一个新的类,在里面实现前处理、配置等相关的函数后,再在主函数中实例类,然后初始化函数进行操作即可。
和人脸检测相比,需要更改的首先就是模型,不同的识别要使用不同的模型,这里我使用了官方提供的模型。
然后还有一个参数,叫做anchors,中文叫做锚点。其含义是存储了一组预设的边框,在使用的时候,会优先使用anchors中的参数进行边框绘制,在其基础上进行一些偏移,所以anchors中的参数越多所能检测到的情况也会越多。但是其参数的设置也要合理,要大概根据手掌的形状分配这个边框的长和宽。例程中提供的参数有这些[26,27, 53,52, 75,71, 80,99, 106,82, 99,134, 140,113, 161,172, 245,276]。当减少到仅剩26,27这组参数的时候,就会发现,只能在特定的一个角度,很困难的识别到手的存在。因此,这个列表中的参数多一些,会增加识别的概率,但是同时可能也会增加误检测的概率。
下面是手掌检测的主要代码部分。
if __name__=="__main__":
# 显示模式,默认"hdmi",可以选择"hdmi"和"lcd"
display_mode="hdmi"
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
# 模型路径
kmodel_path="/sdcard/app/tests/kmodel/hand_det.kmodel"
# 其它参数设置
confidence_threshold = 0.2
nms_threshold = 0.5
rgb888p_size=[1920,1080]
labels = ["hand"]
anchors = [26,27, 53,52, 75,71, 80,99, 106,82, 99,134, 140,113, 161,172, 245,276] #anchor设置
# 初始化PipeLine
pl=PipeLine(rgb888p_size=rgb888p_size,display_size=display_size,display_mode=display_mode)
pl.create()
# 初始化自定义手掌检测实例
hand_det=HandDetectionApp(kmodel_path,model_input_size=[512,512],labels=labels,anchors=anchors,confidence_threshold=confidence_threshold,nms_threshold=nms_threshold,nms_option=False,strides=[8,16,32],rgb888p_size=rgb888p_size,display_size=display_size,debug_mode=0)
hand_det.config_preprocess()
clock = time.clock()
try:
while True:
os.exitpoint() # 检查是否有退出信号
clock.tick()
img=pl.get_frame() # 获取当前帧数据
res=hand_det.run(img) # 推理当前帧
hand_det.draw_result(pl,res) # 绘制结果到PipeLine的osd图像
print(res) # 打印结果
pl.show_image() # 显示当前的绘制结果
gc.collect() # 垃圾回收
print(clock.fps()) #打印帧率
except Exception as e:
sys.print_exception(e)
finally:
hand_det.deinit() # 反初始化
pl.destroy() # 销毁PipeLine实例
下面是演示效果。
手势判断
实现手掌识别后,就要对每个手指进行划线。如下图所示,来获取五个手指头的弯曲的角度,并将这个值可以返回到一个列表中。
通过下面的代码,实现将角度返回到一个列表中。
for i in range(5):
angle = self.hk_vector_2d_angle([(results[0]-results[i*8+4]), (results[1]-results[i*8+5])],[(results[i*8+6]-results[i*8+8]),(results[i*8+7]-results[i*8+9])])
angle_list.append(angle)
然后对angle_list列表中的每一位进行单独的角度判断,从而可以实现手势的识别。
从0-5对应的依次是拇指、食指、中指、无名指、小拇指。
参数thr_angle为弯转角度阈值,即超过这个数值就认为手指发生了弯曲。
参数thr_angle_thumb为拇指弯转角度阈值,即超过这个数值就认为拇指发生了弯曲。
参数thr_angle_s为放松角度阈值,即小于这个数值就认为手指处于伸展状态。
参数gesture_str 为手势字符串,用来存储手势的名称,返回进行显示。
下面就针对gun和love手势进行一下分析。
gun手势,大拇指和食指处于伸展放松状态,剩余三个手指处于弯曲状态。因此大拇指和食指的条件应该是小于thr_angle_s,其余三指为大于thr_angle。可以看到代码中列表的前两位判断条件是小于thr_angle_s,后三位判断条件是大于thr_angle。
love手势,大拇指、食指和小拇指处于伸展放松状态,剩余两个手指处于弯曲状态。因此大拇指、食指和小拇指的条件应该是小于thr_angle_s,其余两指为大于thr_angle。可以看到代码中列表的前两位和最后一位判断条件是小于thr_angle_s,中间两位判断条件是大于thr_angle。
thr_angle,thr_angle_thumb,thr_angle_s,gesture_str = 65.,53.,49.,None
if 65535. not in angle_list:
if (angle_list[0]>thr_angle_thumb) and (angle_list[1]>thr_angle) and (angle_list[2]>thr_angle) and (angle_list[3]>thr_angle) and (angle_list[4]>thr_angle):
gesture_str = "fist"
elif (angle_list[0]<thr_angle_s) and (angle_list[1]<thr_angle_s) and (angle_list[2]<thr_angle_s) and (angle_list[3]<thr_angle_s) and (angle_list[4]<thr_angle_s):
gesture_str = "five"
elif (angle_list[0]<thr_angle_s) and (angle_list[1]<thr_angle_s) and (angle_list[2]>thr_angle) and (angle_list[3]>thr_angle) and (angle_list[4]>thr_angle):
gesture_str = "gun"
elif (angle_list[0]<thr_angle_s) and (angle_list[1]<thr_angle_s) and (angle_list[2]>thr_angle) and (angle_list[3]>thr_angle) and (angle_list[4]<thr_angle_s):
gesture_str = "love"
elif (angle_list[0]>5) and (angle_list[1]<thr_angle_s) and (angle_list[2]>thr_angle) and (angle_list[3]>thr_angle) and (angle_list[4]>thr_angle):
gesture_str = "one"
elif (angle_list[0]<thr_angle_s) and (angle_list[1]>thr_angle) and (angle_list[2]>thr_angle) and (angle_list[3]>thr_angle) and (angle_list[4]<thr_angle_s):
gesture_str = "six"
elif (angle_list[0]>thr_angle_thumb) and (angle_list[1]<thr_angle_s) and (angle_list[2]<thr_angle_s) and (angle_list[3]<thr_angle_s) and (angle_list[4]>thr_angle):
gesture_str = "three"
elif (angle_list[0]<thr_angle_s) and (angle_list[1]>thr_angle) and (angle_list[2]>thr_angle) and (angle_list[3]>thr_angle) and (angle_list[4]>thr_angle):
gesture_str = "thumbUp"
elif (angle_list[0]>thr_angle_thumb) and (angle_list[1]<thr_angle_s) and (angle_list[2]<thr_angle_s) and (angle_list[3]>thr_angle) and (angle_list[4]>thr_angle):
gesture_str = "yeah"
return gesture_str
下面我们尝试补充一个OK的手势。OK的手势为拇指和食指弯曲,剩余三指处于放松的状态。但是经过测试后,发现,拇指需要弯曲角度很大才能实现识别。因此更改一下设定的thr_angle_thumb阈值,通过将这个阈值改小一点,可以实现更好的识别。
elif (angle_list[0]>thr_angle_thumb) and (angle_list[1]>thr_angle) and (angle_list[2]<thr_angle_s) and (angle_list[3]<thr_angle_s) and (angle_list[4]<thr_angle_s):
gesture_str = "OK"
因为是凭借角度识别手势,所以对于手的翻转、旋转可能会出现不太准确的情况。下面再添加一个Four的手势。Four手势,拇指弯曲,剩余四指伸展放松。
elif (angle_list[0]>thr_angle_thumb) and (angle_list[1]<thr_angle_s) and (angle_list[2]<thr_angle_s) and (angle_list[3]<thr_angle_s) and (angle_list[4]<thr_angle_s):
gesture_str = "four"
- 2024-10-27
-
发表了主题帖:
【嘉楠科技 CanMV K230测评】多色识别以及颜色计数&AprilTag初次使用
多色识别
之前我们做过单一色彩识别的程序,多色识别实际上就是使用一个循环函数,在循环中,每次切换不同的判断标准,然后依次进行draw,就可以标识出不同的颜色的色块。
基本的程序逻辑是,在find blobs的时候,选取的阈值,不是单一的,而是在一个范围为3的循环中(已知仅有三种颜色),每一次的循环都会切换不同的阈值。在每一次的循环中,都会找到对应的阈值的所有blobs,返回一个元组列表,在这个元组列表中进行一个新的循环,对每一个元组列表里的色块进行单独的框选标识和打印对应的字符。
这里的话,需要单独的去建立两个新的元组,用来存放颜色信息和字符信息。分别来表示不同的颜色。
下面是代码的演示。
可以看到多出了两个新的变量元组,colors1和colors2,分别存储的是红绿蓝三色的颜色数据和颜色字符数据。
在拍照和显示的中间,加入了两个循环,其中第一个循环用来循环不同的颜色,第二个循环用来循环同一个颜色中的不同的色块。整体的程序与单一颜色识别的程序相差不大。
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)] # 蓝色阈值
colors1 = [(255,0,0), (0,255,0), (0,0,255)]
colors2 = ['RED', 'GREEN', 'BLUE']
try:
sensor = Sensor() #构建摄像头对象
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(width=800, height=480) #设置帧大小为LCD分辨率(800x480),默认通道0
sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0
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 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
###################
# 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()
效果演示。
颜色识别计数
在实际的生活和生产工程中,可能会存在同一个物品散落,需要进行计数的情况,这样的物体通常都有颜色一致的特征。find blobs函数最终返回的是一个元组列表,那么这个返回值就有一个很好的特性,就是可以计算出元组的长度用来表示检测到的色块的数量。同样例程中也给出了一个检测黄色跳线帽的一个例程,我们运行来实验一下。
代码仿照上面的单一颜色识别程序,需要改变的是颜色阈值的设定,这里我们可以导入一张包含有跳线帽的图片,通过阈值编辑器来进行阈值的获取。然后在获取执行完fing blobs的函数后,得到了一个blobs的元组列表对象。使用len函数(在python中有明确定义,用来获取一个对象的长度,这个对象可以是元组列表等也可以是一个字典)对blobs这个元组列表进行长度的获取,然后使用draw进行字符的打印。下面使用代码实现一下。
import time, os, sys
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
thresholds = [(18, 72, -13, 31, 18, 83)] #黄色跳线帽阈值
try:
sensor = Sensor() #构建摄像头对象
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(width=800, height=480) #设置帧大小为LCD分辨率(800x480),默认通道0
sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0
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]])
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
###################
# 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()
下面使用例程中提供的图片,进行效果的演示。
AprilTag识别
AprilTag是一种视觉标签,用于增强现实(AR)、机器人导航和计算机视觉等应用。它由一系列黑白图案组成,每个标签都有一个唯一的标识符。相机可以识别这些标签,并计算它们在三维空间中的位置和朝向。这个标签有更高的识别率,即使在环境较差的情况下,也能实现较为精准的识别,而且容易生成和使用,因此被广泛的应用在需要空间定位的应用中。
K230中可以使用find_apriltags进行AprilTag的识别。其函数原型是find_apriltags([roi[, families=image.TAG36H11[, fx[, fy[, cx[, cy]]]]]])。其中参数roi是识别的范围,families是要解码的标签家族的位掩码,fx和fy是以像素为单位的相机x和y方向的焦距。cx和cy是图像的中心。
下面进行代码的演示。
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.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分辨率
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)
#显示图片,LCD居中方式显示
Display.show_image(img, x=round((800-sensor.width())/2),y=round((480-sensor.height())/2)) #显示图片
print(clock.fps()) #打印帧率
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()
演示效果如下,可以看到在打印窗口中可以看到识别到的AprilTag以及角度的信息,或许可以借助这个角度的信息进行一些空间定位。(之前没有应用过这个AprilTag,最近比较忙,下周研究一下相关的文档,看看相关的原理是怎么实现的。)
- 2024-10-21
-
回复了主题帖:
【嘉楠科技 CanMV K230测评】单一颜色识别&简单二维码识别
wangerxian 发表于 2024-10-21 15:43
可以测试一下反色的二维码可以识别不?
使用QR生成器生成了两个完全一样,仅黑白反相的二维码进行测试。测试结果如下,可以看出反相后的二维码无法被正常识别。使用的生成工具QR生成器
二维码检测原理还没有具体的研究,后续有时间的话可以尝试能否检测出反色的二维码。01studio官网上提供的教程说只能检测简单的二维码,尝试过微信用户二维码,也是无法识别的。目前还不太清楚其检测的原理是啥。后续研究出来会分享。
- 2024-10-20
-
发表了主题帖:
【嘉楠科技 CanMV K230测评】单一颜色识别&简单二维码识别
单一颜色识别
在K230的颜色识别中,使用的是LAB色彩模型。有区别于我们人眼色RGB模型。其中的L、A、B分别代表的是颜色的亮度,红色到绿色的指示,黄色和蓝色之间的指示。其检测的原理可以简单的理解为,我们通过这三个参数设置了一个有限的范围,在整张图片中,计算出每个像素点的LAB数值,然后去和我们所设定的阈值进行对比,当在阈值范围的时候,我们就认为这个就是我们所要检测到的颜色,从而进行标记出来进行显示。那么实现这个过程,K230为我们提供了一个封好的函数叫做find_blobs。
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]]]]]]]]]])这个是find_blobs的函数定义,可以看到参数的数量较多。
thresholds参数需要输入一个元组列表,这个位置可以存放六个数据,用来表示LAB三个参数的低阈值和高阈值。顺序依次是l_lo,l_hi,a_lo,a_hi,b_lo,b_hi。如果图像是一个灰度图像,则仅需要两个参数值进行显示,即灰度最小值和灰度最大值。如果元组数值不足6个,则后面缺少的会自动补齐为最大,若超过六个,则超出的部分自动忽略。
invert参数为反相,即最终识别的是在阈值范围内还是阈值范围外,若为Flase则不进行反相,识别的内容为阈值范围内,若为True则进行反相,识别阈值范围外的所有像素点。
roi是检测的区域范围,若不设置默认为整张图像。
x_stride和y_stride是查找色块时跳过的像素数量。色块较大时,可以增加数值来加快查找色块的速度。
area_threshold为边框区域阈值,当边框区域小于这个数值的时候,会被过滤掉。pixels_threshold为像素数阈值,当色块的像素数少于这个数值的时候,会被过滤掉。
后面的四个参数用于表示合并相关的内容。
调用这个函数返回的对象是image.blob。这个是检测到的关于色块的信息。包含的内容有矩形元组(即能刚好框住该色块的矩形框,包含的数据有x、y、w、h,分别是横坐标、纵坐标、宽度和高度),像素数量,色块的中心横纵坐标,色块的旋转角度等信息。3.11 image 图像处理 API手册 — K230 CanMV
下面实际操作来试一下颜色识别的功能。
在程序中依旧是调用摄像头、拍照、显示的步骤。检测的程序放在了拍照和显示之间。这里是直接调用了find blobs的函数,并将返回的信息存在blobs变量中,然后针对这个变量元组中不同位置的信息,进行绘制标识的操作。这里结合之前画圆的函数,实现了把圆形表示出来的效果。需要注意的是,画圆函数输入的值是int类型,因此当b[2]这个数值为偶数的时候,除2可以得到int类型,但是当该数值为奇数的时候,除2就会得到小数,因此就需要进行一下强转,将其转为int类型,就可以实现。如果不进行强转出现的报错提醒是Exception can't convert float to int,需要注意一下。
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.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_circle(b[5],b[6],int(b[2]/2),color = (255, 255, 255),thickness = 2,fill = False)
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()
当然颜色阈值采用的是官方例程中提供的阈值,因为使用的检测图像也是官方例程中提供的。这里的阈值更改可以直接更改在程序引入相关接口后定义的元组中,进行更改。更改的流程首先要导入图像(可以选择帧缓冲区),然后点击IDE中的工具->机器视觉->阈值编辑器,然后选择帧缓冲区或导入图像,然后在下面的滑块中调整,确保只识别出你想要的颜色,然后复制参数,更改元组的数据即可。操作效果如下所示。注意白色的位置才是被跟踪的像素,若是采用下图的方式,则是刚好将蓝色区域排除了,所以可以选择使用反相来实现检测。
最终的演示效果如下所示:
原图
红色区域识别
蓝色区域识别(反相,自定义颜色阈值范围)
二维码识别
二维码现在被广泛的使用在我们的日常生活中,查找信息、结账支付、登陆账号等等。因此能够准确的识别二维码也是如今机器视觉的一个重要功能。
二维码是采用某种特定的几何图形按照一定的规律进行分布的图形,利用了计算机中的01概念,从而实现了使用图形来表示众多的信息。
在K230中使用函数find_qrcodes()即可实现二维码的检测和采集信息。
这个函数的参数仅有感兴趣的范围一个参数,即roi,如果不进行设置则默认整张图片。
返回的信息是一个元组列表。[0]-[3]是起始点的横纵坐标,[4]是载荷信息,[5]是版本号。image - 图像处理 — K210 CanMV
下面是代码的演示。
import time, math, os, gc
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(width=800, height=480) #设置帧大小为LCD分辨率(800x480),默认通道0
sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0
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() #拍摄图片
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()) #打印帧率
###################
# 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()
效果演示
使用草料二维码生成一个包含EEWORLD字符信息的二维码,进行识别扫描,可以实现准确的识别,并可以将信息显示在屏幕上以及打印在串行终端中进行显示。
- 2024-10-14
-
回复了主题帖:
【嘉楠科技 CanMV K230测评】机器视觉进阶-Find something(边缘、直线、圆形、矩形)
wangerxian 发表于 2024-10-14 09:23
所以汽车视觉识别,也有检测图像轮廓是不,感觉这种图更适合计算机。
汽车视觉识别相关的内容,我不是很了解。大概查询了一下关于汽车视觉相关的帖子,发现大部分在网上公开分享的都是基于opencv使用Canny算法进行边缘检测(可能在实际的应用中有更为先进的算法),然后会使用Hough直线检测进行车道的识别。感觉和K230提供的方法差不太多,但是运算的硬件平台可能会更强,相应的算法原理相似,但是会更复杂。这是我一个外行人的认识,要是有懂行的欢迎在评论区解释一下!
- 2024-10-13
-
发表了主题帖:
【嘉楠科技 CanMV K230测评】机器视觉番外一——Canny算法和Hough算法
K230的代码进行了非常非常好的封装,通过一行代码,就可以实现诸多的操作,譬如边缘检测、直线检测以及圆形检测等等。但是一行代码虽然实现了功能,却没有真正理解其内部的原理,知其然知其所以然,根据API中的提示以及openCV相关的文档,经过网络的查找,找到了相关的算法的介绍,经过一段时间的理论学习(实际上可以通过matlab或者openCV进行验证,但是由于时间的问题,这里只是在理论上了解了算法的工作原理,实际的验证并没有动手尝试),对机器视觉中最基础的两个算法有了一定的认识和了解,在这里分享一下我的看法。所分享的内容均是个人的理解,若有疏漏,还望各位查缺补漏,共同进步!!!
Canny边缘检测算法
Canny边缘检测算法最早是在1986年开发出来的一个多级边缘检测算法。
其发展的目标是能够找到一个最优的边缘检测算法。其中关于最优边缘检测的定义有三项:好的检测、好的定位、最小响应。简单的说,就是最终计算得出的边缘要尽可能多标识出图像中的实际边缘,与图像中的真实边缘尽可能接近,尽可能减少图像噪声带来的影响。
Canny边缘检测算法的步骤简化为:降噪(高斯滤波)->计算图像中的亮度梯度->应用非最大抑制技术来消除边缘误检->利用双阈值来确定可能存在的边界->利用滞后技术进行边界的跟踪。
高斯滤波是将采集到的图像与高斯核进行卷积运算。从而可以让采集到的图像在保留边缘特性的情况下,尽可能减少了噪声的影响。
计算图像中的亮度梯度,计算亮度梯度的目的是明确边缘,那么在什么情况下,可以判断为边缘呢,对于程序来说,灰度变化最为剧烈的地方就是边缘,就是梯度最大的点,就可以被认为是边缘,因此要想找到这个最大的梯度,首先就要计算出图像中的亮度梯度。常见的方法是使用Sobel算子进行计算。
应用非最大抑制技术来消除边缘误检,是在梯度方向上的每一个像素点进行局部最大值(极大值)的检测。如果是,则保留该点为边缘,若不是则抑制该点为0,这一步让原本模糊的边界变得更加“清晰”。
利用双阈值来确定可能存在的边界,这一步和K230提供的find_edges中的两个参数密切相关。这一步的作用是为了真正的区分出来边缘还是噪声。在之前的三步中已经将图像转为了仅包含局部最大值的边缘图像。但是,这些局部最大值的范围在整体上也是较为离散的,因此还需要一个确定的值来确定究竟哪个才是边缘。双阈值中有一个高阈值和一个低阈值,两个阈值产生了三个区间,在高于高阈值的区间内,认为该点一定是边缘,被称为是强边缘;在低于低阈值的区间内,认为该点一定不是边缘;在低阈值和高阈值之间的区间内,被称为是弱边缘,需要进行进一步的计算来确定是噪声还是边缘。
利用滞后技术进行边界的跟踪,这一步就是用来进行弱边缘的计算,通过查看弱边缘像素点周围的多个像素点,看该弱边缘像素点是否存在强边缘像素点,只要存在就可以保留该弱边缘像素点为真是的边缘,否则认为其是噪声。
因此,在设置image.find_edges(edge_type[, threshold])函数中的高低阈值组时,若想显示更多的边缘,或者说边缘不太明显时,高阈值应该尽可能小一些。若想显示一个很明显的边缘,且要求尽可能只显示这一个边缘时,应设置的较大一些。当然具体的数值还是要根据摄像头的采集情况进行调整。
参考文章:openCV中关于Canny算法的介绍
霍夫变换直线检测
霍夫变换是图像处理中的一种特征提取技术,其主要的原理是利用点和线的对偶性,将原始图像空间中给定的曲线通过表达式转换为参数空间中的一个点。这样就可以把曲线检测问题转换为寻找参数空间中的峰值的问题。常见的可以检测的有直线、椭圆、圆、弧线。
霍夫变换的最重要的就是利用了点和线的对偶性。已知一条直线y=kx+b。如果在直角坐标系中表示,则是一条直线,斜率为k,截距为b。若以k和b为坐标系的两个轴,就会发现这条直线被表示成了一个点,且这条直线如果是线段的话,线段越长,这个点出现的“概率”就越大,我们把这个概率想象成Z轴,即这个线段越直,这个Z轴的数值对应的就会越高。这样就把寻找直线的问题转换成了在对应的参数空间中寻找峰值的问题。只要这个点在某个区域内是一个峰值,就可以认为这个点所还原回去的是一条直线。
但是使用斜截式的话,会出现垂直于X轴或垂直于Y轴的情况,此时对于K来说,就会出现0或者无穷大的情况,因此就将斜截式的方程使用参数方程表示,此时的方程就会表示为ρ=x*cosθ+y*sinθ。通过这个方程,若x和y是一个固定的常数,即一个固定的点,此时经过该点,环绕做多条直线,就可以在ρ-θ坐标系中得出一条曲线,此时这个坐标系中的曲线就可以表示参数坐标系中的一个点。若此时有另一个点,同时在ρ-θ坐标系中会得出另一条曲线,这两条曲线会产生一个交点,那么这个交点,就是在参数坐标系中两点的连线。若在这个连线上有更多的点做上述的转换操作,则会有更多的曲线交于这一点。
那么我们在实际的直线检测时,首先会对图像进行Canny变换,经过Canny变换后,会将边缘用点绘制出来,对每一个点,进行对偶变换,ρ-θ坐标系中得出多条曲线,这些曲线可能会产生多个交点。我们将ρ-θ坐标系分成均匀的小格,如下图所示,那么不同的交点就会落在不同的小格中,不同的小格也会有不同数量的交点。我们对每一个小格进行累加处理,即有一个点就进行一次累加,这样就可以算出每个格的交点个数,这样在一个格中的交点数量越多,则就越能说明这个交点对偶变换后,在参数坐标系中是一条直线。这样就实现了直线的检测。
这是对霍夫变换最简单的理解。当然目前依靠这个理解我们只能对直线进行检测。这也是经典霍夫变换最开始的作用。后来随着扩展,逐渐被应用在任意形状物体(能用数学公式进行表示)的识别。像圆形、椭圆等等。
参考文章:霍夫变换介绍 openCV中关于霍夫变换的介绍
霍夫变换圆形检测
经典霍夫变换主要被用来进行直线的检测,后来通过霍夫变换的扩展,可以将霍夫变换应用在几乎任意形状的物体上,但是这个形状要能用数学公式表示。
对于圆形来说,圆形的方程可以表示为r^2 = (x-x0)^2+(y-y0)^2其中圆心为x0和y0,半径为r。
这样的话,如果将这个方程转换到参数方程中去,就可以由三个参数进行表示,分别是r,x0和y0。这样构成的图像应该是一个三维的图像。即想象一个三维的坐标系,三个轴依次为x0、y0和r。在这个坐标系中的一个点,对应的就是在平面直角坐标系中的一个圆,这样线与点就构成了对偶的关系。在这个三维空间中构建出多个小格格,每个小格都是一个累加器,仿照霍夫变换中直线检测的方法,即可找到多个曲线交点,并将对应的累加器进行累加运算,这样就可以得到一个三维的具有不同尖峰的立体图像。在这个图像中找到局部尖峰值就可以找到对应的圆形。
但是根据opencv(目前在opencv相关的资料中找到了关于霍夫变化圆形检测的详细介绍,K230的圆形检测算法可能与opencv的不一致,所以此处仅是用来了解和学习相关的知识)相关书籍中提供的资料,使用霍夫变换进行圆形检测进行了两轮筛选。
首先是将三维的图像转换为二维的图像,在第一轮的筛选中,首先找出可能是圆的位置,圆周上的像素点的梯度方向与半径的方向是一致的,所以就可以找到所谓的半径方向(实际上是梯度方向)上的所有累加器进行处理,对圆形的圆周都进行一样的操作,最终会有一个累加器的数值高于周围的累加器,则这个点就被认为可能是圆的圆心。如图所示,对canny边缘检测后的点,结合梯度信息进行沿半径的累加处理,这样就会得到一个在局部区域内有最大值的累加器,认为该累加器可能是圆周的圆心。当某个累加器的数值超过了预定数量的投票后,就会将该以该圆心进行第二轮的筛选。
然后将检测到的圆形针对半径构建一个直方图,构成的直方图范围由定义中的半径最小值和最大值进行确定,然后在直方图中进行峰值的检测,检测到的峰值,就能检测到可能的圆形的半径。如下所示,假如设定的圆形半径最小为1最大为10,则会在这个范围内进行累加,在对应的半径上检测边缘点存在的个数,这样就会存在一个局部最大值,此处对应的是半径为6的情况,这样就可以认为该圆心上有一个半径为6的圆形。
通过上述的两个步骤,就可以实现圆形的检测。
在K230的参考例程中给出的函数是img.find_circles(threshold = 1300, x_margin = 10, y_margin= 10,r_margin = 10,r_min = 5, r_max = 50, r_step = 2) 其中猜测threshold可能与第一轮中的预设最小投票数值有关(但是在官方的API文档中,提出了叫做索贝尔滤波像素大小的总和的概念,所以该数值是否与第一轮中的预设最小投票数值有关还有待验证,此处只是个人的猜测)。r_min和r_max应该对应的就是第二轮中的设定的圆形的最小半径和最大半径。而r_step参数在K230文档中表示为控制识别步骤,但是在官方的API文档中并没有发现对应的解释,在此处猜测是否与第一轮和第二轮的检测顺序或者选取的坐标轴有关。当尝试更改该数值为0时,程序会出现报错,因此就选择了默认值2。
-
发表了主题帖:
【嘉楠科技 CanMV K230测评】机器视觉进阶-Find something(边缘、直线、圆形、矩形)
前面介绍了基于K230相关外设以及机器视觉相关的基础,像是摄像头的使用以及图像的显示和画图相关的内容。下面会逐渐开始机器视觉真正的内容,循序渐进的进行,首先是常见的边缘检测、直线检测等等,后面跟着例程学习颜色相关、码类识别以及后续的AI相关的内容。
边缘检测
边缘检测使用image.find_edges(edge_type[, threshold])函数,可以选择的边缘检测算法有两种,Simple和Canny两种。其中Canny检测算法更好一些。而后面的高低阈值参数设置,与Canny检测算法中的双阈值确定边界有关。阈值设置较高,则只能检测出较为明显的边缘,阈值设置较低,则可以检测出大部分的边缘。
代码演示如下,其基本内容为图像的采集和显示,主要就是在snapshot和show_image之间插入了find_edges函数。除此之外重要的是,边缘检测是需要图像转换为黑白图像,因此在输出图像格式的位置,应该选择的参数为GRAYSCALE。
import time, os, sys, gc
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
try:
sensor = Sensor(width=1280, height=960) #构建摄像头对象,将摄像头长宽设置为4:3
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(width=320, height=240) #设置帧大小为LCD分辨率(320x240),默认通道0
sensor.set_pixformat(Sensor.GRAYSCALE) #设置输出图像格式,默认通道0
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() #拍摄一张图片
#使用 Canny 边缘检测器
img.find_edges(image.EDGE_CANNY, threshold=(10, 20))
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()
下面使用同一张素材,对参数中的高低阈值进行更改,观察不同的效果。
素材原图及素材灰度图:
高阈值200、低阈值100:
高阈值20、低阈值10:
高阈值200、低阈值10:
可以明显的看出,阈值设置为10和20的时候,检测出的边缘数量远大于阈值设置为100和200的时候。可以更加清楚的看到人脸的轮廓。但是对应也产生了更多“噪声”,因此选择一个合适的阈值是十分重要的。
线段检测
线段检测使用的函数为image.find_line_segments([roi[, merge_distance=0[, max_theta_difference=15]]]),其返回值为一个线段对象列表image.line。其中参数roi为识别的区域范围,若无指定则默认是整张图片;merge_distance为两条线段不被合并的最大像素值;max_theta_difference两条线段不被合并的最大角度值。
代码展示如下。
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可以获得更直的线段
try:
sensor = Sensor(width=1280, height=960) #构建摄像头对象,将摄像头长宽设置为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,320,240) #只使用IDE缓冲区显示图像
MediaManager.init() #初始化media资源管理器
sensor.run() #启动sensor
clock = time.clock()
while True:
os.exitpoint() #检测IDE中断
################
## 这里编写代码 ##
################
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 = 5, max_theta_diff = 15):
img.draw_line(l.line(), color = (255, 0, 0), thickness=2)
print(l)
#Display.show_image(img) #显示图片
#显示图片,仅用于LCD居中方式显示
Display.show_image(img, x=0,y=0)
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()
在这个代码中,在拍摄图片和显示图片之间加入了find_line_segments函数。并将返回的数据存储在了l中。然后再将l列表中的信息,画成线表示在图片上,并将l列表的信息打印在串行终端。
下面进行效果的演示。
原图:
检测后效果:
可以看到公路上的线段就可以被识别出来,且在左下角的串行终端中进行了线段数据的打印。
圆形检测
圆形检测使用的函数为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]]]]]]]]]]),其返回值为一个圆形对象,有四个参数值,分别为圆心的X和Y,半径以及量级(量级数值越大圆的可信度越高)。其中参数roi为识别的区域范围,若无指定则默认是整张图片;threshold为阈值,返回大于或等于量级大于这个数值的圆;x_stride和y_stride为检测过程中跳过的像素量;x_margin 、y_margin、 r_margin为所检测圆的合并;r_min和r_max用于控制识别圆形的半径范围;r_step用于控制识别的步骤。
代码演示。
import time, os, sys
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
try:
sensor = Sensor(width=1280, height=960) #构建摄像头对象,将摄像头长宽设置为4:3
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(width=320, height=240) #设置帧大小,默认通道0
sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0
Display.init(Display.VIRT, 320, 240) #只使用IDE缓冲区显示图像
MediaManager.init() #初始化media资源管理器
sensor.run() #启动sensor
clock = time.clock()
while True:
os.exitpoint() #检测IDE中断
################
## 这里编写代码 ##
################
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 = 1300, x_margin = 10, y_margin= 10,
r_margin = 10,r_min = 5, r_max = 50, 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=0,y=0)
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()
效果演示。
原图:
检测后结果展示:
矩形检测
K230中进行矩形检测的算法采用的是二维码检测中的image.find_rects([roi=Auto, threshold=10000]),其中的参数roi表示的是检测的区域,不配置的话就是默认整张图片。threshold参数用来配置阈值,只有超过这个阈值的矩形,才会被识别并显示出来。使用这个函数同样也会返回一个列表,列表的信息有矩形的起点横纵坐标以及矩形的长度和宽度以及矩形的量级(即前面参数配置中对应的threshold)。
代码:
import time, os, sys
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
try:
sensor = Sensor(width=1280, height=960) #构建摄像头对象
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() #拍摄一张图片
# `threshold` 需要设置一个比价大的值来过滤掉噪声。
#这样在图像中检测到边缘亮度较低的矩形。矩形
#边缘量级越大,对比越强…
for r in img.find_rects(threshold = 40000):
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
###################
# 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()
效果演示:
原图:
效果图:
- 2024-10-11
-
回复了主题帖:
【嘉楠科技 CanMV K230测评】机器视觉基础——摄像头、显示以及画图
秦天qintian0303 发表于 2024-10-10 23:27
怎么进行分类训练啊?手动圈吗?
还没学习到那一步,但是之前看了一个例程,不知道符不符合您的意思。自分类学习 | 01Studio
- 2024-10-09
-
发表了主题帖:
【嘉楠科技 CanMV K230测评】机器视觉基础——摄像头、显示以及画图
Camera & Display
完成了K230基本外设的学习,下面才是真正能够发挥K230实力的部分。机器视觉以及AI相关的内容。
机器视觉首先最为重要的便是采集图像,为了方便我们进行调试,将采集到的图像进行实时的显示出来也是十分重要的。因此首先学习一下怎么在K230上使用摄像头进行图像采集,并将采集到的图像输出进行显示。
在K230中的摄像头相关的函数放在Sensor模块中,因此在使用的时候,首先要引入Sensor模块。K230有三个摄像头通道,每个摄像头都可以独立的完成图像的采集,并可以同时输出3路图像数据。其框架如下图所示。
构建函数,调用函数sensor = Sensor(id, [width, height, fps])进行构建。其中id为csi端口的选择,可选择的范围是0-2,在K230的开发板上带的摄像头的csi为2。width为最大输出图像的宽度。height为最大输出图像的高度。fps为最大输出图像的帧率。后三个参数的默认值为1920、1080、30。
复位函数sensor.reset()。在构建摄像头对象后,必须调用该函数,才能继续其他的操作。
在构建函数中的后三个参数都可以通过调用单独的函数进行更改。首先是输出图像的尺寸大小,可以使用函数sensor.set_framesize(framesize = FRAME_SIZE_INVAILD, chn = CAM_CHN_ID_0, alignment=0, **kwargs)进行输出图像尺寸的选择。
sensor.set_pixformat(pix_format, chn = CAM_CHN_ID_0)函数可以更改输出的图形格式。
sensor.set_hmirror(enable)用于设置图像的水平镜像,sensor.set_vflip(enable)用于设置图像的垂直翻转。
sensor.run()函数让摄像头开始输出,根据API手册,该函数必须在MediaManager.init()之前进行调用。sensor.stop()函数让摄像头停止输出,根据API手册,该函数必须在MediaManager.deinit()之前调用。注意!!!如果在使用多个摄像头的时候,run函数仅需要调用任意一个即可,stop函数需要每个都调用。
sensor.snapshot(chn = CAM_CHN_ID_0)用于获取一帧图像数据。
sensor.bind_info(x = 0, y = 0, chn = CAM_CHN_ID_0)用于将输出的图像绑定到Display的指定坐标。
在K230中关于图像显示这一块的内容主要存放在Display模块中,因此在使用的时候,首先要引入Display模块。K230的图像输出方式有三种,分别是HDMI、MIPI以及IDE内部缓冲区显示,三种方式相比IDE内部缓冲区成本较低,且调试方便,因此主要以IDE内部缓冲区进行介绍。
显示模块的初始化调用函数def init(type = None, width = None, height = None, osd_num = 1, to_ide = False, fps = None, quality = 90)其中参数type为必选项,内容为显示设备的类型,在这里我们选择使用VIRT即IDE内置的缓冲区域。参数width和height为显示图像的宽度和高度,这两个选项的默认值会根据type的改变而改变,在VIRT类型下,这两个值的默认值为640和480。osd_num为在show_image时可以支持的LAYER数量,跟默认保持一致,选择为1。to_ide为是否选择将图像同步显示在IDE内置的缓冲区中。fps为是否显示帧率。quality为设置IDE缓冲区显示的图像质量。 因为显示的方式仅有IDE内置显示,所以其中type只能选择VIRT,width和height根据默认分别选择为640和480,其余的可以保持默认,不进行配置。
图像的显示使用函数def show_image(img, x = 0, y = 0, layer = None, alpha = 255, flag = 0),其中参数img为摄像头拍摄的图片的信息,x和y为显示的起点坐标,layer为显示到指定的层。alpha为图层混合的参数,flag为显示标志。在使用的时候,主要就是使用前三个参数即可实现图像的显示。
def deinit()函数用于进行Display的反初始化,这个模块的使用会关闭Display的所有通路。其使用条件必须在MediaManager.deinit()之前调用且必须在sensor.stop()之后调用。
def bind_layer(src=(mod, dev, layer), dstlayer, rect = (x, y, w, h), pix_format, alpha, flag)函数用于将sensor的图像直接输出到display中,不需要用户手动参与即可将图像持续的显示在屏幕上(感觉有点像DMA进行数据的传输)。其中的参数,src为输出信息,可以通过调用函数sensor.bind_info()进行获取。dstlayer为绑定到display的指定层。rect为显示的区域。pix_format为图像像素格式,alpha为图层混合,flag为显示标志。
在K230中,摄像头和图像显示之间还有一个软件抽象层,叫做media模块,主要是针对K230 CanMV平台媒体数据链路以及媒体缓冲区相关操作的封装。其相关的API接口可以参考官方提供的API手册3.4 Media模块API手册 — K230 CanMV
首先是初始化和反初始化的函数,MediaManager.init()和MediaManager.deinit()。
配置的函数是MediaManager._config(config)。其中config参数为媒体缓冲区配置参数。
用于将输入和输出的连接的函数是MediaManager.link(src=(mod,dev,chn), dst = (mod,dev,chn))。其中src为输入源,dst为输出。
这三个模块的配合使用可以实现K230的摄像头采集图像,并在IDE上进行显示。下面结合代码进行详细的过程分析。
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()
实现图像的采集和显示的代码,相比于之前单纯的外设内容多了很多。下面尽可能进行逐行的分析。
首先引入了time、os、sys相关的模块。然后引入摄像头和图像显示相关的部分,media.sensor、media.display、media.media。
首先构建摄像头的函数sensor,然后对摄像头进行复位和初始化,设置显示大小和显示格式。
然后进行显示部分的初始化,配置参数为IDE显示,宽度和高度与sensor的宽度和高度保持一致。
然后初始化MediaManager。
完成三个模块的初始化和参数配置后,启动摄像头,使用sensor.run函数。
创建一个clock函数,主要用于fps的计算。
在循环中,拍摄图像前进行一次clock的tick获取。然后拍摄一次,并将数据传入到img变量中,使用display模块将拍摄的图像进行显示。调用clock中的fsp函数,并将返回值进行打印显示。
这样就可以实现图像的采集和显示。
下面的代码,是当上面的代码出现问题后,进行报错打印以及相关操作停止的代码。重点看一下最后的关闭的顺序。
首先进行摄像头sensor的停止,然后对display进行反初始化,最后再进行MediaManager的反初始化。
下面进行视频的演示。在视频中可以看到,使用摄像头拍摄的外面的画面,并在IDE中进行了显示。同时在下面的串行终端中进行了帧率的打印。
[localvideo]bea48a647fcad4660c52c4370d1849d9[/localvideo]
Drawing
在图像识别中,完成图像的采集和显示后,往往会在显示的图像上做一些标记,从而实现对目标的追踪和锁定。因此,就需要我们在图像上进行画图的操作。
在画图中,我们操作的主要对象是image对象,image是机器视觉中最基本的对象,因为后面进行的操作几乎都是基于image进行处理。所以要先简单介绍一下image对象,后面再逐渐深入去了解相关的API。
在摄像头相关内容中,会使用sensor.snapshot进行一帧图像的获取,并返回image对象。此时我们就获取了image对象。然后可以针对这个image对象进行一系列的画线、画圆圈、画方块等等的操作,这些操作被夹在拍摄一帧照片和进行显示之间。当然这只是一种创建image对象的方式,也可以新建一个image对象或者从从本地路径下去读取一张照片进行显示。这样的操作方式为:img=image.Image(w, h, format)和img=image.Image(path[, copy_to_fb=False])。其对应的例子为img = image.Image(640, 480, image.RGB565)和img = image.Image("01Studio.bmp", copy_to_fb=True)。其中format为图像格式,大部分都选择RGB565格式。copy_to_fb为是否加载大图片,可选项为True和False。
下面重点介绍一下画图的一些相关操作。
画线:image.draw_line(x0, y0, x1, y1[, color[, thickness=1]])参数依次为起点横坐标、起点纵坐标、终点横坐标、终点纵坐标、颜色、颜色粗细。
画矩形:image.draw_rectangle(x, y, w, h[, color[, thickness=1[, fill=False]]])参数依次为起点横坐标、起点纵坐标、矩形宽度、矩形高度、颜色、边框粗细、是否填充。
画圆形:image.draw_circle(x, y, radius[, color[, thickness=1[, fill=False]]])参数依次为圆心横坐标、圆心纵坐标、半径、颜色、线条粗细、是否填充。
画箭头:image.draw_arrow(x0, y0, x1, y1[, color[, size,[thickness=1]]])参数依次为起点横坐标、起点纵坐标、终点横坐标、终点纵坐标、颜色、箭头位置大小、线条粗细。
画十字:image.draw_cross(x, y[, color[, size=5[, thickness=1]]])参数依次为中点横坐标、中点纵坐标、颜色、大小、线条粗细。
写字符:image.draw_string(x, y, text[, color[, scale=1[,mono_space=True…]]]])参数依次为起点横坐标、起点纵坐标、文本内容、颜色、大小、是否有间隔。
写中文字符:image.draw_string_advanced(x, y, char_size,str,[color, font])参数依次为起点横坐标、起点纵坐标、字符大小、字符串信息、颜色、字体类型。
下面使用代码实现在采集的图像上进行一些简单的绘制操作。
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(width=800, height=480) #设置帧大小VGA,默认通道0
sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0
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()
# 画线段:从 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 = True)
#画圆:蓝色不填充。
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_advanced(150, 180, 30, "EEWORLD", color = (255, 255, 255))
img.draw_string_advanced(40, 300, 30, "电子工程世界", 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()
大体的代码和上一节的内容保持一致,只是在img = sensor.snapshot()和Display.show_image(img)之间对img对象进行了一些操作。
即画线段、画矩形、画圆、画箭头、画十字交叉以及字符的显示。
演示视频如下:
[localvideo]270c56c8908ce02eb22046d0eb79ae8f[/localvideo]
- 2024-10-08
-
加入了学习《模拟集成电路设计(上海交通大学李章全)》,观看 1
-
发表了主题帖:
【嘉楠科技 CanMV K230测评】基础外设学习二——PWM、UART、Thread、WDT、File
PWM
PWM称为脉冲宽度调制,用于产生一个特定的脉冲信号,当然这个信号的频率(周期)以及占空比都可以进行灵活的设定。PWM被广泛的应用在风扇调速、舵机控制、电机控制等等中。在其他的单片机中,PWM的产生通常都需要搭配定时器进行使用,通过定时器产生一定频率(周期)的中断,并在计数到达一定的数量后,进行电平的反转,从而实现PWM信号的产生。但是在K230中的PWM产生非常的方便,仅需要几个函数就可以实现PWM的快速生成。
PWM也位于machine模块下,因此在使用PWM的时候,要先从machine中进行引用。
通过pwm = machine.PWM(channel, freq, duty, enable=False)进行PWM的函数构建,其中参数channel用于选择PWM的通道,K230提供了6个PWM通道,因此可选的范围为0-5,在这块开发板上,仅有四个通道被引出,可选的范围为0-3。参数freq用于配置PWM的频率,对应的单位为Hz。参数duty用于配置PWM的占空比,取值范围为0-100,默认值为50。enable用于配置PWM输出是否使能,默认为false即不使能状态。
当然PWM的频率和占空比都可以进行单独的配置,通过调用函数pwm.freq([value])和pwm.duty([value])。
在不使用PWM的时候,可以调用pwn.deinit()函数,将PWM功能注销掉。
下面进行PWM代码的演示,产生变换的PWM波形以及控制无源蜂鸣器产生不同声调的声音。
from machine import Pin, PWM
from machine import FPIOA
import time
fpioa = FPIOA()
fpioa.set_function(42,FPIOA.PWM0)
Beep = PWM(0,200, 50, enable=True)
Beep.freq(200)
time.sleep(1)
Beep.freq(400)
time.sleep(1)
Beep.freq(600)
time.sleep(1)
Beep.freq(800)
time.sleep(1)
Beep.freq(1000)
time.sleep(1)
Beep.enable(False)
首先引入了PWM、Pin和FPIOA模块,然后先构建了FPIOA的函数,并将42号引脚配置为PWM0的功能。
然后构建了PWM的函数,命名为Beep,同时进行了初始化配置,配置为通道0,周期为200Hz,占空比为50,并进行使能。
在下面,进行频率的变换,依次变换为200、400、600、800、1000,每次变换完后进行一秒的延时,用于观察。
最终的演示结果如下所示。
[localvideo]a3e5afb6f2e5982ca25185c54fd09f19[/localvideo]
下面进行PWM第二个功能代码的演示,用于控制180°舵机,进行不同角度的变化。
舵机的工作原理:舵机的工作需要一个周期为20ms的脉冲信号,占空比的范围为2.5-12.5,所对应的时间为0.5ms-2.5ms,对应的角度为0-180或者-90~90。
from machine import Pin, PWM
from machine import FPIOA
import time
fpioa = FPIOA()
fpioa.set_function(42,FPIOA.PWM0)
Motor = PWM(0,50, 3, enable=True)
Motor.duty(5)
time.sleep(1)
Motor.duty(7)
time.sleep(1)
Motor.duty(9)
time.sleep(1)
Motor.duty(11)
time.sleep(1)
Motor.duty(4)
time.sleep(1)
Motor.enable(False)
首先引入了PWM、Pin和FPIOA模块,然后先构建了FPIOA的函数,并将42号引脚配置为PWM0的功能。
然后构建了PWM的函数,命名为Motor,同时进行了初始化配置,配置为通道0,周期为50Hz(周期为20ms),占空比为3,并进行使能。
在下面,进行频率的变换,依次变换为5、7、9、11、4,每次变换完后进行一秒的延时,用于观察。
最终的演示结果如下所示。
[localvideo]bde1059cc6423ef282f04ca32c7eccc5[/localvideo]
UART
串口是最常用的串行总线,具有全双工、异步的特性。被广泛的应用在无线透传模块以及工控相关的产品上。和其他开发板的通信,往往也是使用串口进行数据的收发。
K230内部有5个UART硬件模块,其中UART0被小核终端占用,UART3被大核终端占用。剩余的UART1、UART2、UART4可以被连接使用,但是在这块开发板上引出了UART1、UART2。
UART也位于machine模块下,因此在使用UART的时候,要先从machine中进行引用。
machine.UART(id, baudrate=115200, bits=UART.EIGHTBITS, parity=UART.PARITY_NONE, stop=UART.STOPBITS_ONE)函数用来构建函数。id为串口号,可以用的范围是UART.UART1和UART.UART2;baudrate为波特率,常用的数值为115200、9600等,使用串口通信时,首先要保证两个设备的串口波特率保持一致。bits为数据位,一般为8位,默认也为8位。parity为校验位,可以选择的有偶校验、奇校验和无校验,默认是无校验;stop为停止位设定,可以选的范围是1、1.5、2,默认为1。
K230使用串口与外面的设备进行通信时,配置完成后要进行数据的收发。
发送数据的函数为UART.write(buf),其中buf为要发送的数据。
接收数据的函数为UART.read(num),其中num为要接收的数据的位数。UART.readline(num)为接收整行的数据,其中num为读取的行数。
在不使用串口后可以使用UART.deinit()函数,将串口注销掉。
下面使用USB转TTL模块,将K230与电脑使用串口进行通信。
硬件接线方式。
软件代码展示。
from machine import UART
from machine import FPIOA
import time
fpioa = FPIOA()
fpioa.set_function(11,FPIOA.UART2_TXD)
fpioa.set_function(12,FPIOA.UART2_RXD)
uart = UART(UART.UART2,115200)
uart.write('Hello EEWORLD')
while True:
text = uart.read(128)
if text != b'':
print(text)
time.sleep(0.1)
首先引入UART和FPIOA的模块。
构建一个FPIOA的函数,并将11和12两个引脚设置为串口2的TXD和RXD工作模式。构建一个UART函数,并将这个串口配置为串口2通道,波特率配置为115200。
使用串口写入函数,发送Hello EEWORLD字符串。
在循环中,接收串口接收的数据到text变量中,判断变量内容,如果不是b'',则打印text内容。产生100ms的延时,避免满跑。
串口收发数据效果展示。在串口调试助手上,选择对应的串口号和波特率以及相应的设置,打开串口后,并运行程序,可以收到由K230发送的字符串。在数据发送编辑栏里发送数据,可以在IDE的串行终端中收到电脑通过串口发送给K230的123字符串。
Thread线程
单片机运行程序的方式是顺序运行,即代码从上至下依次进行程序的运行。但是为了实现程序的更快和更加实时,出现l实时操作系统这一概念,即RTOS。在MicroPython中有一个类似的概念,称之为线程,thread。
在MicroPython中对于线程的描述可以参考官方文档。17.9. _thread — Low-level threading API
使用线程最简单的方式就是通过调用_thread.start_new_thread(function, args[, kwargs])函数,新创建一个线程,便会开始自动运行。其中的参数function代表的是在这个线程中要被调用的函数。这个函数在线程创建函数之前,且其要完成一定的工作。参数args则是传入该函数的一些参数,这个参数的传入方式为元组。
使用多线程打印多条信息。代码演示。
import _thread
import time
def func(name):
while True:
print("hello {}".format(name))
time.sleep(1)
_thread.start_new_thread(func,("1",))
_thread.start_new_thread(func,("2",))
while True:
time.sleep(0.01)
首先引入thread模块。
定义一个要在多线程中运行函数,func,所执行的任务是打印一条信息,传入的参数为name。
在下面开启两个线程,并传入不同的参数。
在循环中写入一小段延时,避免单片机满跑。
下面进行效果延时。可以看到串行终端中不断打印定义的func函数中的数据。
WDT
看门狗是单片机用来防止软件程序跑飞,从而出现问题的一种内部机制。其本质可以看作是一个定时器,当开启看门狗后,定时器开始运行,在定时器到达设定的重启时间之前要进行喂狗(即重置一下定时器),若超过设定的时间没有进行喂狗,就会通过软件复位的方式,将程序进行复位,从而避免了程序跑飞出现的问题。
在K230单片机中也有WDT模块。在使用的时候,需要先引用WDT模块,然后构建一个WDT函数,使用函数wdt = WDT(id, timeout),并在构建函数的时候配置好看门狗的编号以及超时的时间,这里的时间单位是s。喂狗的方式是调用feed函数,wdt.feed()就可以实现喂狗。
下面就用代码演示一下看门狗的配置和喂狗的操作。
from machine import WDT
import time
wdt = WDT(1,3)
for i in range(3):
time.sleep(1)
print(i)
wdt.feed()
while True:
time.sleep(0.01)
首先引入WDT模块。构建一个WDT的函数,并设置超时时间为3s。
执行一个循环函数,循环的次数为3次,在每一次循环的时候进行1s的延时,打印当前的循环次数,并进行一次喂狗。完成3次喂狗后,就会跳出循环,在循环外没有喂狗的操作。因此进入死循环3s后,就会自动复位,此时的开发板就会和电脑断开连接并重新连接。
演示视频如下。
[localvideo]9d1a36b483c17b0d20bf71cea5edcb76[/localvideo]
File
在其他的单片机中我们要是想断电后保存一些数据的话,通常会使用EEPROM或者Flash进行数据的存储。在K230中自带一个文件系统,因此我们直接使用K230的文件系统就可以实现数据的存储和获取。
K230进行文件操作的方式和Python的文件操作方式是一样的。K230的文件路径为/sdcard,所创建的文件,直接存储在这个路径下即可。
代码展示。
f = open('/sdcard/1.txt', 'w')
f.write('EEWORLD')
f.close()
f = open('/sdcard/1.txt', 'r')
text = f.read()
print(text)
f.close()
构建文件函数f,并打开以写的方式,如果没有就会自动新建。使用write函数进行数据的写入。然后关闭文件。
打开文件以读的方式,使用read函数将数据读出并存储在text变量中,打印text,关闭文件。
效果演示。
[localvideo]a96345351865543210e461fa0b9b7077[/localvideo]
- 2024-10-02
-
发表了主题帖:
【嘉楠科技 CanMV K230测评】基础外设学习一——Pin、Timer、RTC、ADC
本帖最后由 王嘉辉 于 2024-10-2 23:16 编辑
点灯
“点亮LED灯”即点灯实验是我们学习嵌入式软件开发过程中必须要经历的一个demo,所以在学习这个开发板的时候,我们也从这个例程开始这块开发板的学习。
LED灯其本质上就是一个发光二极管,在这个二极管上加上一个合适的电压(注意二极管的单向导电性,不要接反,不过在开发板上不需要注意这些),下面是这个开发板的原理图上的LED灯的电路原理,可以看到二极管的负极接在了GND上,正极串联一个电阻接在了芯片的IO引脚上。串联电阻的目的是为了限制电流,可以延长LED灯的使用寿命,同时也可以控制LED灯的亮度。
根据提供的例程和文档可以发现点亮一个LED灯需要使用的内部外设有FPIOA和Pin两个部分。
K230的功能很多,所以大部分的IO都会复用多种功能,所以K230内部引入了FPIOA的概念,即Field Programmable Input and Output Array 现场可编程IO阵列,这个概念其实和IOMUX类似。从而实现更加灵活的IO功能的选择和使用。下面是API手册中关于FPIOA的相关介绍。
2.8 FPIOA 模块API手册 — K230 CanMV
在API手册中可以看到FPIOA类位于machine模块下,因此我们在使用这个类的时候要从machine模块中进行引用。
首先使用fpioa = FPIOA()构造一个函数。
下面简单介绍一下可能用到的函数,更详细的内容可以参考上面提供的API手册进行查找。
首先第一个是最最最最常用的函数,就是给一个引脚指定对应的功能,以及给这个功能配置一些基本的属性,比如上拉下拉、输入输出使能或者驱动能力等等。
FPIOA.set_function(pin, func, ie=-1, oe=-1, pu=-1, pd=-1, st=-1, sl=-1, ds=-1)
其中pin为引脚号,可选的范围0-63。func为功能号,可以被配置为普通的IO或者其可被配置的其他功能如串口等等。ie、oe、pu、pd分别对应着输入使能、输出使能、配置上拉和配置下拉。st和sl分别是st使能和sl使能。ds为配置驱动能力。(在后面的pin模块的使用中,也可以配置普通IO以及普通IO所能配置的输入输出模式、上下拉以及驱动能力。)
除此之外,FPIOA的部分还提供了一些可以用于查询的函数(通过引脚查询该引脚所配置的功能和通过功能查询配置该功能的引脚)。
PWM.get_pin_num(func)
PWM.get_pin_func(pin)
当然也可以使用PWM.help([number, func=false])函数进行引脚或功能的所有查询。
下面简单介绍一下Pin相关的函数。2.6 Pin 模块API手册 — K230 CanMV
Pin类也位于machine模块下,因此我们在使用这个类的时候要从machine模块中进行引用。
首先使用pin = Pin(index, mode, pull=Pin.PULL_NONE, drive=7)构造一个函数。其中的参数分别是引脚号、输入或输出模式、上拉或下拉选择、驱动能力选择。
当然也可以使用Pin.init(mode, pull=Pin.PULL_NONE, drive=7)进行重新的配置。其中参数的介绍参考上面的介绍。
也可以使用Pin.mode([mode])、Pin.pull([pull])、Pin.drive([drive])分别进行输入输出模式、上下拉和驱动能力的单独的配置。
Pin.value([value])函数用于配置引脚在输出模式下,输出高电平或低电平。也可以使用Pin.on()、Pin.off()、Pin.high()、Pin.low()进行电平高低的配置。如果这个函数在引脚的输入模式下,同时不传入参数,就可以获取引脚的电平。
现在开始进行点灯的实验。
在IDE中点击文件->新建文件,IDE中会打开一个新的文件,然后先保存一下(只有改动后才可以进行保存)。
先把提供的代码删掉,进行点灯的代码的编写。
from machine import Pin
from machine import FPIOA
import time
#将GPIO52配置为普通GPIO
fpioa = FPIOA()
fpioa.set_function(52,FPIOA.GPIO52)
LED=Pin(52,Pin.OUT)
LED.value(1)
time.sleep(3) # 睡眠3秒
LED.value(0)
time.sleep(3) # 睡眠3秒
LED.value(1)
首先在代码的前三行引入了三个模块,分别是来自machine模块下的FPIOA和Pin类以及一个time模块,用于产生一定的延时。
然后构建一个FPIOA的函数,并将引脚52配置为普通IO模式(根据上面的原理图可以看到LED被连接在52号引脚上)。
然后构建一个LED的函数,并将引脚52配置为输出模式。
然后使用LED.value(1)和LED.value(0)用于控制引脚输出高电平和低电平,从而实现LED的点亮和熄灭。
但是过于快速的切换,我们的肉眼无法观察到,所以要在切换的时候加入一定时间的延时,即time.sleep(3)函数。
连接开发板,并开始运行,观察开发板的LED。
[localvideo]44b4f3e52e4b1e5ac69067bb5d726979[/localvideo]
按键
按键被按下时相当于一段导线,松开时相当于是断路。这样按键被连接在GPIO上。另一端连接在GND上,同时在GPIO一侧连接一个上拉电阻。当按键按下时,GPIO被连接到GND上,读到的电平就是低电平,当按键松开时,GPIO的电平状态被上拉电阻稳定到高电平,读到的电平就是高电平。实现了通过GPIO检测按键是否按下。
按键的软件开发和LED的软件开发所使用的模块是一样的,都是使用FPIOA和Pin。
from machine import Pin
from machine import FPIOA
import time
fpioa = FPIOA()
fpioa.set_function(52,FPIOA.GPIO52)
fpioa.set_function(21,FPIOA.GPIO21)
LED=Pin(52,Pin.OUT)
KEY=Pin(21,Pin.IN,Pin.PULL_UP)
state=0
while True:
if KEY.value()==0:
time.sleep_ms(10)
if KEY.value()==0:
state=not state
LED.value(state)
while not KEY.value():
pass
首先在代码的前三行引入了三个模块,分别是来自machine模块下的FPIOA和Pin类以及一个time模块,用于产生一定的延时。
然后构建一个FPIOA的函数,并将引脚52配置为普通IO模式(根据上面的原理图可以看到LED被连接在52号引脚上),同时将引脚21配置为普通IO模式(根据上面的原理图可以看到KEY被连接在21号引脚上)。
然后构建一个LED的函数,并将引脚52配置为输出模式。同时构建一个KEY的函数,并将引脚21配置为输入模式,同时配置内部上拉。
定义一个新的变量state用来表示LED的状态。
然后写一个循环while True: 类似于C语言中的while(1)。
通过if语句判断按键是否按下,然后延时10ms用于按键消抖,再次判断按键是否按下,若按下,则将state进行翻转。再使用LED赋值函数将目前的state值赋给LED从而可以改变LED的状态,实现按键控制LED亮灭切换。
最后加上一个pass函数,用于避免按键连按。
下载代码的效果如下面的视频所示。每次按键按下都会切换LED的亮灭状态。
[localvideo]2b66891655d1075d3120f1d133a7f0c2[/localvideo]
Timer
定时器的作用是用来产生一个精准的计时,当到达我们设定的时间后可以提醒我们去做一些事情。
Timer类位于machine模块下,因此我们在使用这个类的时候要从machine模块中进行引用。
Timer的函数较少,仅有构造函数、初始化以及释放函数三个。
使用timer = Timer(index, mode=Timer.PERIODIC, freq=-1, period=-1, callback=None, arg=None)进行Timer函数的构造,其中的参数分别对应着Timer通道(可选的范围为-1~5,-1为软件定时器,但是在01studio的文档中解释的是目前仅有软件定时器可以使用);mode用于选择Timer的工作模式,是循环进行定时还是定时一次后就结束;freq用于设定定时器定时的频率;period用于设定定时器定时的周期(当freq和period同时被给出时,会优先使用freq,此时的period会被屏蔽);callback用于设置超时回调函数;arg用于设置超时回调函数的参数。
使用Timer.init(mode=Timer.PERIODIC, freq=-1, period=-1, callback=None, arg=None)可以进行Timer的初始化,这个函数中的参数可以参考构造函数的参数。
Timer.deinit()函数用于释放Timer。
下面我们使用Timer进行点灯的实验。 点灯的频率为5Hz,即200ms为一个周期。
from machine import Timer,Pin
import time
led = Pin(52,Pin.OUT)
state = 0
def fun(i):
global state
state = not state
led.value(state)
#使用软件定时器,编号-1
tim = Timer(-1)
tim.init(mode=Timer.PERIODIC, period=100, callback=fun) #周期为100ms
while True:
time.sleep(0.01) #避免CPU满跑
首先引入Timer和Pin。
然后构建一个LED的函数,并将引脚52配置为输出模式。创建一个全局变量state,用于表示LED的状态。
定义回调函数,在回调函数中首先先声明一下全局变量state,然后对state进行取反,然后将state的数值赋给LED。
初始化Timer使用软件定时器,传入参数为-1。并配置模式为循环模式,周期为100ms(此时的周期为LED闪烁周期的一半,是因为LED闪烁一个周期包含点亮和熄灭两个过程,因此应该是Timer的两倍),回调函数为fun,即刚才我们定义的回调函数。
然后在死循环中运行一个sleep的延时,避免CPU满跑。
效果演示如下。
[localvideo]6da730c27c4298ad62a3290a979e18e6[/localvideo]
RTC
RTC全称是Real Time Clock,可以用来进行精准的时间计时,我们现在常用的万年历或数字时钟常常就是使用类似的芯片DS1302等等实现计时的功能。K230内置一个RTC,但是这块板子上没有额外的电池,所以要想让RTC一直工作的话,需要板子一直进行上电。
RTC类位于machine模块下,因此我们在使用这个类的时候要从machine模块中进行引用。
RTC的主要功能就是获取实时的时间,当然也可以使用函数进行重新设定时间。
rtc.init(year,mon,day,hour,min,sec,microsec)函数用于时间的重新设定,其中的参数依次是年、月、日、小时、分钟、秒、微秒。
rtc.datetime()函数用于时间的获取。这个的返回量是一个数组,所以可以配置打印不同的位,从而单独的获取年、月、日等等。
像rtc.datetime()[0]、rtc.datetime()[1]这样可以获取年和月的数值。然后可以使用print进行打印或者赋值给其他的变量进行其他的处理。
下面进行RTC的初始化和时间打印。
from machine import RTC
import time
rtc = RTC()
if rtc.datetime()[1] != 10:
rtc.datetime((2024, 10, 1, 0, 0, 0, 0, 0))
while True:
print(rtc.datetime())
time.sleep(1)
首先引入RTC。
然后构建一个RTC的函数。
写一个判断语句,判断RTC的第二位即rtc.datetime()[1]不等于10的时候,给RTC初始化为2024年10月1日0点0分0秒。
然后在循环中,打印当前的RTC时间。time的作用是每隔一秒打印一次,防止高速的刷屏。
下面是RTC的演示视频。
[localvideo]80640f16b8b645883a3bf913d16c2aed[/localvideo]
ADC
物理世界中的大部分的信号都是模拟量,但是随着单片机的发展,越来越多需要数字信号的处理,因此将模拟信号转换成为数字信号就显得尤为重要。
K230内置了一个ADC,其具有6个通道(该开发板引出了四个ADC通道,分别是0-4,其中0和1的量程为0-3.6V,2和3的量程为0-1.8V),采样分辨率为12bit,采样的速率可以达到1MHz。
ADC类位于machine模块下,因此我们在使用这个类的时候要从machine模块中进行引用。
构造函数的方式是adc = ADC(channel),其中的参数channel为ADC通道的选择,可以选择的范围为0-5。
获取ADC数值的方式有两种,一种是直接读出寄存器的数值,不进行单位转换,对应的数据范围是0-4095。另一种是转换为电压值进行输出,返回值的范围是0-1800000。这两个函数分别是ADC.read_u16()和ADC.read_uv()。
下面进行代码的演示。
from machine import ADC
import time
adc = ADC(0)
while True:
print(adc.read_u16())
print('%.2f'%(adc.read_uv()/1000000*2), "V")
time.sleep(1)
首先引入ADC。
构建一个ADC的函数,并使用0通道,对应的引脚为32,量程为0-3.6V
在循环中持续打印两个数据。首先是直接打印ADC通道的采样值,得到的数据的范围是0-4095。
然后打印实际的电压值,函数的电压的单位是uV,因此首先要进行单位的转换,除上1000000将单位换成V。通道0的检测范围最大可以到3.6V,但是数值的显示最大仅有1.8V,因此还要进行乘2,从而实现准确数值的显示。
进行1s的延时。演示的效果如下所示。
[localvideo]0d8544e055bea7f22fb60a15e9f34ae0[/localvideo]
通过对K230的基本外设的熟悉,基本上掌握了Pin、Timer、ADC、RTC的使用,后面会进行PWM、UART等其他外设的介绍,以及图像识别和机器学习相关的分享。
-
回复了主题帖:
【Follow me第二季第2期】任务汇总
秦天qintian0303 发表于 2024-10-2 11:37
wave_sin[256] 这种方式是不是曲线更加的平滑啊啊
数组位数越多,能形成的波形应该会越还原。DAC应该也是可以调整到12位精度的,这两个方式应该都可以让波形更加的平滑一些。
- 2024-10-01
-
加入了学习《【Follow me第二季第2期】视频展示》,观看 Follow_me第二季第二期视频展示
- 2024-09-29
-
回复了主题帖:
【嘉楠科技 CanMV K230测评】开发板开箱,开发环境搭建,Demo程序烧录测试
freebsder 发表于 2024-9-29 14:28
谢谢分享,期待后续深度解析
-
发表了主题帖:
【Follow me第二季第2期】任务汇总
本帖最后由 王嘉辉 于 2024-10-31 22:40 编辑
UNO R4 WIFI
视频:【Follow me第二季第2期】视频展示-EEWORLD大学堂
代码:Follow_me第二季第二期代码-嵌入式开发相关资料下载-EEWORLD下载中心
文档:
【Follow me第二季第2期】+搭建环境并开启第一步Blink / 串口打印Hello EEWorld! - DigiKey得捷技术专区 - 电子工程世界-论坛
【Follow me第二季第2期】+LED矩阵、DAC、放大器、AD - DigiKey得捷技术专区 - 电子工程世界-论坛 (eeworld.com.cn)
【Follow me第二季第2期】+通过Wi-Fi,利用MQTT协议接入到开源的智能家居平台HA - DigiKey得捷技术专区 - 电子工程世界-论坛 (eeworld.com.cn)
【Follow me第二季第2期】+扩展任务二:通过外部SHT40温湿度传感器,上传温湿度到HA - DigiKey得捷技术专区 - 电子工程世界-论坛 (eeworld.com.cn)
零、写在前面
很荣幸参加此次Follow me第二季第二期活动,通过这次活动学到了Arduino的一些知识,以及HA相关的知识。完成了活动所要求的一些任务LED、UART、LED矩阵、WIFI连接HA通过MQTT传输数据、SHT40传感器数据获取、DAC、ADC等等。所用到的器件有Arduino R4 WIFI主板一块,SHT40传感器小板一块和4P连接线一根。
一、环境搭建
1.1 硬件环境及硬件介绍
此次参加FOLLOW ME第二季第二期的活动,使用的主控板子是Arduino R4 WIFI开发板,还买了一个SHT40温湿度传感器模块,还有一根4P的线。
根据官网以及网络上各个帖子的介绍,可以了解到这块开发板有了很大的升级,主控芯片升级为了瑞萨的32位Arm Cortex-M4内核的RA4M1微控制器,拥有256KB Flash和32KB SRAM,时钟频率来到了48MHz,接口可以与之前的R3进行兼容,但是这次与电脑的接口换上了更为先进的TYPE-C接口。这块板子让人眼前一新的地方就是12*8的LED矩阵,可以用来显示数字、字母、简单的图形甚至汉字。其有丰富的接口和片上外设,内置6个PWM接口、6个ADC接口、1个12bit的DAC以及1个SPI、2个IIC和1个CAN接口,可以连接非常多的外设传感器,可玩性极高。
1.2 软件环境
打开Arduino IDE搜索R4板子,下载对应的包,下载的时间较久,耐心等待一下。
1.3 上电测试
使用TYPE-C进行上电,上电后,可以看到Arduino官方烧录的Blink代码,但是这块板子有一个很大面积的LED矩阵,上电后演示了一段积木搭建的效果,并最终停留在了一颗爱心的图案。
二、点灯(Blink)
点灯需要使用到的外设为GPIO,查询相关的函数发现和GPIO相关的函数有pinMode,用来设置引脚的输入或输出模式,digitalRead用来读取引脚上的电平状态,digitalWrite用来写入引脚上的电平状态。
新建文件后,可以看到两个大函数,setup函数中用于写初始化配置的相关代码,这一部分的代码在上电后会执行一次。loop函数中用于写循环执行的代码,这一部分的代码会在setup执行完成后重复不断的执行。
因此要在setup里写pinMode函数,将LED对应的引脚配置成输出模式。然后在loop里写digitalWrite函数,分别配置写入高电平和低电平,在两个函数之间加入延时函数,实现LED灯闪烁的效果。
void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
}
最终呈现的代码如下所示,代码实现的效果是,板子上的LED以5Hz的频率进行闪烁。
void setup() {
// put your setup code here, to run once:
pinMode(LED_BUILTIN,OUTPUT);
}
void loop() {
// put your main code here, to run repeatedly:
digitalWrite(LED_BUILTIN,HIGH);
delay(100);
digitalWrite(LED_BUILTIN,LOW);
delay(100);
}
三、串口
串口的使用需要在setup中进行串口的初始化配置,主要是串口波特率的配置。
在loop中进行串口的数据发送,通过调用Serial.println("Hello EEWorld!")函数,可以将Hello EEWorld!通过串口发送到电脑的监视器窗口。
四、板载LED矩阵
首先应该包含Arduino提供的矩阵库文件#include "Arduino_LED_Matrix.h"
然后在这个文件中有一个ArduinoLEDMatrix类,我们利用这个类定义一个新的对象matrix。代码实现为ArduinoLEDMatrix matrix;
初始化矩阵matrix.begin();
定义矩阵显示,矩阵的显示无非就是控制每个LED处于高电平还是低电平,当对应的LED为高电平时,LED点亮,与此同时矩阵中对应的位置数字为1。反之为低电平,LED熄灭,矩阵对应的位置数字为0。
Arduino R4 WIFI的板子上的LED矩阵为8*12,因此我们定义一个二维数组用来存放显示图形的数据。这里设置了三个图形,分别是笑脸、眨眼和哭脸。
uint8_t smile[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};
uint8_t wink[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};
uint8_t cry[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};
最后调用matrix.renderBitmap(smile, 8, 12);函数便可以实现将数组数据加载到LED矩阵中去,在每个表情切换中间加上适当的延时,实现表情的显示。
同样的可以进行汉字、数字等的显示,更改对应的数组信息即可。
完整代码:
#include "Arduino_LED_Matrix.h"
ArduinoLEDMatrix matrix;
void setup()
{
Serial.begin(115200);
matrix.begin();
}
uint8_t smile[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};
uint8_t wink[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};
uint8_t cry[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};
void loop()
{
matrix.renderBitmap(smile, 8, 12);
delay(1000);
matrix.renderBitmap(wink, 8, 12);
delay(1000);
matrix.renderBitmap(cry, 8, 12);
delay(1000);
}
视频演示:
[localvideo]4a8ff528466c55a9b37f294e4ec3a9e0[/localvideo]
五、DAC产生正弦波形
DAC就是模数转换器,可以将数字信号转换为模拟信号,这个功能可以用来制作函数发生器也可以用来驱动蜂鸣器实现音频的播放,相比于PWM频率控制,可以实现更加连续的波形产生,比如正弦波、锯齿波等等。
首先是驱动DAC能够产生一个稳定的电压信号,然后通过每次更换输入的数字值,从而改变每次输出的电压信号,让电压信号的变化遵循正弦信号的变化,即可实现正弦信号的生成。下面我们一步一步来实现。
通过调用函数analogWriteResolution(8);设置了DAC的分辨率为8bit。然后调用函数analogWrite(DAC, 具体数值(0-255));就可以实现产生一个稳定的直流电压信号。我们分别设置数值为100和150来查看电压是否会发生变化。当为100时,使用万用表测量的数据为1.76V,当为150时,使用万用表测量的数据为2.637V。当为255时,即可以设置的最大数字时,电压表显示的电压值为4.46。因此可以得出当DAC设置为8bit模式时,每一个值代表的电压大约为0.01754V,根据你想要的电压值,除于该数值便可以得到要输入的数值。比如需要3V电压值,3/0.01754=171(省略小数位),输入171,则可以得到3V的电压值。
void setup()
{
analogWriteResolution(8); // set the analog output resolution to 12 bit (4096 levels)
}
void loop()
{
analogWrite(DAC, 171); // write the selected waveform on DAC0
}
那么我们将产生正弦信号的数值依次存放在数组中,间隔一定的时间(与频率有关)将数组的每一位依次传入该函数中,使用DAC将数据输出成模拟信号,便可以实现正弦波的生成。同时还找到一个锯齿波的数组,但是锯齿波的数据仅有120位,且要求DAC的分辨率为12,因此需要修改analogWriteResolution(8);中的8改为12,analogWrite(DAC, wave_sin);中的wave_sin改为waveformsTable,同时将if (i == 256)中的256修改为120即可。
#ifndef _Waves_h_
#define _Waves_h_
static int waveformsTable[120] = {
0x22, 0x44, 0x66, 0x88, 0xaa, 0xcc, 0xee, 0x110, 0x132, 0x154,
0x176, 0x198, 0x1ba, 0x1dc, 0x1fe, 0x220, 0x242, 0x264, 0x286, 0x2a8,
0x2ca, 0x2ec, 0x30e, 0x330, 0x352, 0x374, 0x396, 0x3b8, 0x3da, 0x3fc,
0x41e, 0x440, 0x462, 0x484, 0x4a6, 0x4c8, 0x4ea, 0x50c, 0x52e, 0x550,
0x572, 0x594, 0x5b6, 0x5d8, 0x5fa, 0x61c, 0x63e, 0x660, 0x682, 0x6a4,
0x6c6, 0x6e8, 0x70a, 0x72c, 0x74e, 0x770, 0x792, 0x7b4, 0x7d6, 0x7f8,
0x81a, 0x83c, 0x85e, 0x880, 0x8a2, 0x8c4, 0x8e6, 0x908, 0x92a, 0x94c,
0x96e, 0x990, 0x9b2, 0x9d4, 0x9f6, 0xa18, 0xa3a, 0xa5c, 0xa7e, 0xaa0,
0xac2, 0xae4, 0xb06, 0xb28, 0xb4a, 0xb6c, 0xb8e, 0xbb0, 0xbd2, 0xbf4,
0xc16, 0xc38, 0xc5a, 0xc7c, 0xc9e, 0xcc0, 0xce2, 0xd04, 0xd26, 0xd48,
0xd6a, 0xd8c, 0xdae, 0xdd0, 0xdf2, 0xe14, 0xe36, 0xe58, 0xe7a, 0xe9c,
0xebe, 0xee0, 0xf02, 0xf24, 0xf46, 0xf68, 0xf8a, 0xfac, 0xfce, 0xff0
};
static int wave_sin[256] = {
0x80,0x83,0x85,0x88,0x8A,0x8D,0x8F,0x92,
0x94,0x97,0x99,0x9B,0x9E,0xA0,0xA3,0xA5,
0xA7,0xAA,0xAC,0xAE,0xB1,0xB3,0xB5,0xB7,
0xB9,0xBB,0xBD,0xBF,0xC1,0xC3,0xC5,0xC7,
0xC9,0xCB,0xCC,0xCE,0xD0,0xD1,0xD3,0xD4,
0xD6,0xD7,0xD8,0xDA,0xDB,0xDC,0xDD,0xDE,
0xDF,0xE0,0xE1,0xE2,0xE3,0xE3,0xE4,0xE4,
0xE5,0xE5,0xE6,0xE6,0xE7,0xE7,0xE7,0xE7,
0xE7,0xE7,0xE7,0xE7,0xE6,0xE6,0xE5,0xE5,
0xE4,0xE4,0xE3,0xE3,0xE2,0xE1,0xE0,0xDF,
0xDE,0xDD,0xDC,0xDB,0xDA,0xD8,0xD7,0xD6,
0xD4,0xD3,0xD1,0xD0,0xCE,0xCC,0xCB,0xC9,
0xC7,0xC5,0xC3,0xC1,0xBF,0xBD,0xBB,0xB9,
0xB7,0xB5,0xB3,0xB1,0xAE,0xAC,0xAA,0xA7,
0xA5,0xA3,0xA0,0x9E,0x9B,0x99,0x97,0x94,
0x92,0x8F,0x8D,0x8A,0x88,0x85,0x83,0x80,
0x7D,0x7B,0x78,0x76,0x73,0x71,0x6E,0x6C,
0x69,0x67,0x65,0x62,0x60,0x5D,0x5B,0x59,
0x56,0x54,0x52,0x4F,0x4D,0x4B,0x49,0x47,
0x45,0x43,0x41,0x3F,0x3D,0x3B,0x39,0x37,
0x35,0x34,0x32,0x30,0x2E,0x2D,0x2C,0x2A,
0x29,0x28,0x26,0x25,0x24,0x23,0x22,0x21,
0x20,0x1F,0x1E,0x1D,0x1D,0x1C,0x1C,0x1B,
0x1B,0x1A,0x1A,0x1A,0x19,0x19,0x19,0x19,
0x19,0x19,0x19,0x19,0x1A,0x1A,0x1A,0x1B,
0x1B,0x1C,0x1C,0x1D,0x1D,0x1E,0x1F,0x20,
0x21,0x22,0x23,0x24,0x25,0x26,0x28,0x29,
0x2A,0x2C,0x2D,0x2F,0x30,0x32,0x34,0x35,
0x37,0x39,0x3B,0x3D,0x3F,0x41,0x43,0x45,
0x47,0x49,0x4B,0x4D,0x4F,0x52,0x54,0x56,
0x59,0x5B,0x5D,0x60,0x62,0x65,0x67,0x69,
0x6C,0x6E,0x71,0x73,0x76,0x78,0x7B,0x7D
};
#endif
#include "Waves.h"
int i = 0;
void setup()
{
analogWriteResolution(8); // set the analog output resolution to 12 bit (4096 levels)
}
void loop()
{
analogWrite(DAC, wave_sin); // write the selected waveform on DAC0
i++;
if (i == 256) // Reset the counter to repeat the wave
i = 0;
delayMicroseconds(10); // Hold the sample value for the sample time
}
六、板载放大器
Arduino R4 WIFI板子上自带一个内置的放大器,可以将A1作为正向输入、A2作为反向输入、A3作为输出。因此设计一个放大倍数两倍的同相比例放大器,来放大我们产生的正弦信号。
想要使用内置的放大器,首先要引入一个库,#include <OPAMP.h>然后开启放大器即可,使用函数OPAMP.begin(OPAMP_SPEED_HIGHSPEED);即可开启放大器。
除此之外的代码analogWrite(DAC, wave_sin/2);仅在此处加了一个除2的操作,若不加除2放大后的波形会出现削顶失真,当然也可以通过更改数组的数值进行调整。其余的均与DAC代码保持一致,包括Waves.h库文件。
#include "Waves.h"
#include <OPAMP.h>
int i = 0;
void setup()
{
OPAMP.begin(OPAMP_SPEED_HIGHSPEED);
analogWriteResolution(8); // set the analog output resolution to 12 bit (4096 levels)
}
void loop()
{
analogWrite(DAC, wave_sin/2); // write the selected waveform on DAC0
i++;
if (i == 256) // Reset the counter to repeat the wave
i = 0;
delayMicroseconds(10); // Hold the sample value for the sample time
}
其原理可由该图来解释。该图构成一个简易的同相比例放大器,其中放大倍数由黄色的公式给出。可以调整两个电阻的大小,更改放大倍数。红色为放大电路主体,将A0和A1短接,即将DAC产生的模拟信号输入到放大器的正向输入端,输出和反向输入端构成反馈。蓝色为波形的示意,A1输入的正弦峰值为1V,A3输出的正弦峰值为2V。
实物搭建较为简陋,但是通过示波器,可以明显看出输出波形相较于输入波形有了明显的放大,且放大倍数大约在2倍。
七、ADC采集数据并使用串口打印绘制波形。
ADC就是模数转换器,其工作方式与DAC恰好相反。可以将输入的模拟信号转换为数字信号,然后进行数字上的处理,更适应这个数字化发展的趋势。
在硬件上的连接,需要将放大器的A3输出引脚连接在A4的ADC采集引脚上。
使用ADC首先需要初始化ADC,调用函数analogReadResolution(8); 。并使用函数int reading = analogRead(A4);将A4引脚上的模拟数值转换为数字数值并存储在reading变量中。然后通过串口进行数据的打印以及波形图的绘制。
#include "Waves.h"
#include <OPAMP.h>
int i = 0;
void setup()
{
OPAMP.begin(OPAMP_SPEED_HIGHSPEED);
analogWriteResolution(8); // set the analog output resolution to 12 bit (4096 levels)
analogReadResolution(8); //change to 14-bit resolution
Serial.begin(9600);
}
void loop()
{
analogWrite(DAC, wave_sin/2); // write the selected waveform on DAC0
i++;
if (i == 256) // Reset the counter to repeat the wave
i = 0;
int reading = analogRead(A4); // returns a value between 0-16383
delayMicroseconds(10); // Hold the sample value for the sample time
Serial.print(reading);
Serial.print("\n");
}
八、HA平台搭建
此次主要就是想借此次活动来学习HA相关的知识,之前有了解到HA是一个功能非常庞大的物联网平台,但是由于自己的知识储备不足(大部分是没有学习的动力,hhh),一直没有接触到相关的知识。正好有这次活动,让我能够“被迫”学习HA相关的内容,发现要是实现数据的上传功能也不是很难,但是HA的庞大还需要继续进行探索。这篇就是简单介绍一下在Windows中使用VMware搭建HA的过程,以及怎么在HA中安装MQTT相关的内容和与Arduino R4 WIFI板子进行数据的交互。
首先是在Windows中安装VMware,我用的是VMware PRO 17的版本,这个的安装过程我参考了网上的一篇博客,相关的内容还是很多,大家可以自行去搜索安装。(当然其他版本的VMware应该也是可以的,网上相关的信息较多,大家可以自行尝试)
然后就是安装HomeAssistant,打开官网https://www.home-assistant.io/,按照下图中的顺序,依次点击,就可以下载到最新的版本。
然后打开安装好的VMware开始进行HA平台的搭建。详细的过程大家可以去网上搜索帖子学习搭建,需要注意的两个地方(被坑了一个多小时),第一个是网络类型要选择桥接网络,第二个是要修改虚拟机的固件类型为UEFI。(我是被卡了一个小时,然后找到一篇帖子改了这两项后,就可以用了,具体的原理还没有深究,有懂的大佬可以指点一下)完成虚拟机的创建和配置后,直接一键开机。。。然后就会虚拟机就会打印出当前虚拟机的网络相关的信息,找到ipv4的地址,然后在浏览器中输入ipv4地址:8123就会进入到虚拟机中创建的HA的网页,大概需要等待20分钟的时间(网上的普遍说法,但是我大概等了几分钟的样子)就会进入HA的登陆界面,点击创建账户,输入一些信息,就可以创建属于自己的HA账户。
进入HA网页后,点击设置,选择加载项
点击右下角的加载项商店,搜索Mosquitto进行下载。
下载完后,点击设置->设备与服务->MQTT->配置对MQTT进行一些配置。用户名和密码填写创建HA的用户名和密码即可,也可以添加成员,使用新成员的用户名和密码。其余的可以保持默认不变。完成后,可以在MQTT设置页面进行PUB和SUB的测试。
下图所示,配置两个的主题保持一致,然后开启监听主题的开始监听,在上面发送信息,就会在下面收到。
然后就是怎么把Arduino的数据上传到HA中了,这第一步就是要在HA中创建一个设备。(这一步也难了一个小时,因为网上大部分的方案都是用NAS或者树莓派搭建的HA,使用命令行和修改yaml文件就实现了新建设备的功能,但是我使用虚拟机加网页的形式,没有找到yaml文件,所以找了很多的资料)最终,发现可以开启MQTT的自发现功能,然后向这个代理发送符合规则的PUB就可以添加设备。
下载一个MQTTX(也可以使用其他的相关模拟软件),免费、有中文。然后建立一个新的连接。填入的信息和创建的HA信息保持一致即可。
然后发送一个标准的PUB就可以添加设备到HA中了。topic填写在输入栏上方的topic栏中,信息填写在输入栏中,然后点击右下角的小飞机,就可以了。
topic:homeassistant/sensor/sensorBedroomH/config
{
"device_class":"humidity",
"name":"Humidity",
"state_topic":"homeassistant/sensor/sensorBedroom/state",
"unit_of_measurement":"%",
"value_template":"{{ value_json.humidity}}",
"unique_id":"hum01ae",
"device":{
"identifiers":[
"bedroom01ae"
],
"name":"Bedroom"
}
}
然后打开HA的网页,设置->设备与服务->MQTT->1个设备就可以看到传感器栏上有一个新的条目信息。说明添加设备成功了。
然后我们开始使用Arduino更新这个条目信息。先安装两个MQTT相关的库。
复制下面的代码,记得更改WIFI和MQTT相关的配置信息,然后下载到板子上,打开串口查看打印信息。并打开HA查看数据是否更新。(数据应该是21.5)
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
// 替换为你的网络信息
const char* ssid = "WIFI名称";
const char* password = "WIFI密码";
// 替换为你的Mosquitto broker信息
const char* mqtt_server = "IPV4地址";
const int mqtt_port = 1883;
const char* mqtt_user = "HA用户名";
const char* mqtt_password = "HA密码";
// 初始化WiFi和MQTT客户端
WiFiClient espClient;
PubSubClient client(espClient);
// 传感器数据发布主题
const char* topic = "homeassistant/sensor/sensorBedroom/state";
void setup() {
Serial.begin(115200);
setup_wifi();
client.setServer(mqtt_server, mqtt_port);
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
// 假设你有一个温度传感器,获取温度数据
float temperature = readTemperature();
StaticJsonDocument<200> jsonDoc;
jsonDoc["humidity"] = temperature;
char jsonBuffer[512];
serializeJson(jsonDoc, jsonBuffer);
client.publish(topic, jsonBuffer);
// 每隔5秒发送一次数据
delay(5000);
}
void setup_wifi() {
delay(10);
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("Connected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
void reconnect() {
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
if (client.connect("ArduinoClient", mqtt_user, mqtt_password)) {
Serial.println("connected");
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
delay(5000);
}
}
}
float readTemperature() {
// 在这里读取实际传感器的数据
// 示例中返回一个固定值
return 21.5;
}
这样就完成了HA的搭建以及Arduino R4 WIFI上传数据到HA平台。
在HA学习过程中遇到很多问题,借助了强大的AI工具,帮助我解决了很多疑惑,大家可以擅用AI工具,帮助自己对代码进行分析以及对问题进行处理。
九、SHT40传感器以及上传温湿度数据到HA
这次的扩展任务选择了温湿度传感器SHT40,之前使用过SHT30,想体验一下SHT40与之前的传感器有什么区别。除此之外还选购了一条线(为了逼近300块钱,但是又不超过300,究极白嫖怪,hhh),但是就是这个多买的线,坑了我一个小时,还好查看了一下相关的文档,解决了这个问题。
SHT40的通讯协议是IIC,按照以前的思路,找一个SHT40的库,然后改一下代码,加入串口打印就可以实现SHT40数据的获取,但是我找了一个大佬的测试例程,串口打印SHT40设备无法找到,我以为我SHT40插反了,然后又重新接线测试,发现还是不行,我一度以为这个模块是不是有问题。
后来查看文档发现,Arduino R4 WIFI上能与这个线完美结合的那个接口是QWIIC,用的是wire1,我测试的例程使用的IIC是wire0。发现问题,开始解决问题。首先查询了一下QWIIC怎么使用,发现还是比较简单的。
只需要调用Wire1.begin();然后在传感器初始化的函数中添加wire1的参数即可。
#include <Wire.h>
#include "Adafruit_SHT4x.h"
Adafruit_SHT4x sht4 = Adafruit_SHT4x();
void setup()
{
// put your setup code here, to run once:
Serial.begin(115200);
Wire1.begin();
if(sht4.begin(&Wire1) == false)
{
Serial.println("SHT40 not detected. Please check wiring. Freezing.");
while (1)
;
}
sht4.setPrecision(SHT4X_HIGH_PRECISION);
sht4.setHeater(SHT4X_NO_HEATER);
Serial.println("SHT40 acknowledged.");
}
void loop() {
// put your main code here, to run repeatedly:
sensors_event_t humidity, temp;
sht4.getEvent(&humidity, &temp);// populate temp and humidity objects with fresh data
Serial.print("Temperature: ");
Serial.print(temp.temperature);
Serial.println("℃");
Serial.print("Humidity: ");
Serial.print(humidity.relative_humidity);
Serial.println("%");
delay(1000);
}
测试代码下载到板子上,打开串口监视器查看一下数据。
数据可以正常获取,完美!
下面开始将SHT40获取数据和HA结合在一起。首先是用MQTTX在创建一个条目。按照上一篇帖子的方法进行创建即可。(不仅要修改发送的信息,还要修改topic,别忘记)创建完成后检查一下HA有无问题。
然后将两个代码进行整合。得到最终的代码。依旧记得更改信息连接自己的WIFI和MQTT。
#include <Wire.h>
#include "Adafruit_SHT4x.h"
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
Adafruit_SHT4x sht4 = Adafruit_SHT4x();
// 替换为你的网络信息
const char* ssid = "WIFI名称";
const char* password = "WIFI密码";
// 替换为你的Mosquitto broker信息
const char* mqtt_server = "IPV4地址";
const int mqtt_port = 1883;
const char* mqtt_user = "HA用户名";
const char* mqtt_password = "HA密码";
// 初始化WiFi和MQTT客户端
WiFiClient espClient;
PubSubClient client(espClient);
// 传感器数据发布主题
const char* topic = "homeassistant/sensor/sensorBedroom/state";
void setup()
{
// put your setup code here, to run once:
Serial.begin(115200);
setup_wifi();
client.setServer(mqtt_server, mqtt_port);
Wire1.begin();
if(sht4.begin(&Wire1) == false)
{
Serial.println("SHT40 not detected. Please check wiring. Freezing.");
while (1)
;
}
sht4.setPrecision(SHT4X_HIGH_PRECISION);
sht4.setHeater(SHT4X_NO_HEATER);
Serial.println("SHT40 acknowledged.");
}
void loop() {
// put your main code here, to run repeatedly:
delay(1000);
if (!client.connected()) {
reconnect();
}
client.loop();
// 假设你有一个温度传感器,获取温度数据
float temperature = readTemperature();
float humidity = readHumidity();
StaticJsonDocument<200> jsonDoc;
jsonDoc["humidity"] = humidity;
jsonDoc["temperature"] = temperature;
char jsonBuffer[512];
serializeJson(jsonDoc, jsonBuffer);
client.publish(topic, jsonBuffer);
// 每隔5秒发送一次数据
delay(5000);
}
void setup_wifi() {
delay(10);
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("Connected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
void reconnect() {
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
if (client.connect("ArduinoClient", mqtt_user, mqtt_password)) {
Serial.println("connected");
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
}
}
}
float readTemperature() {
// 在这里读取实际传感器的数据
// 示例中返回一个固定值
sensors_event_t humidity,temp;
sht4.getEvent(&humidity,&temp);// populate temp and humidity objects with fresh data
// Serial.print("Temperature: ");
// Serial.print(temp.temperature);
// Serial.println("℃");
// Serial.print("Humidity: ");
// Serial.print(humidity.relative_humidity);
// Serial.println("%");
return temp.temperature;
}
float readHumidity() {
// 在这里读取实际传感器的数据
// 示例中返回一个固定值
sensors_event_t humidity,temp;
sht4.getEvent(&humidity,&temp);// populate temp and humidity objects with fresh data
// Serial.print("Temperature: ");
// Serial.print(temp.temperature);
// Serial.println("℃");
// Serial.print("Humidity: ");
// Serial.print(humidity.relative_humidity);
// Serial.println("%");
return humidity.relative_humidity;
}
下载程序,然后打开HA进行查看,发现数据已经可以正常进行上传了。测试一下SHT40的性能。发现数据变化的速度非常快,基本手指触碰到传感器后,数据就已经开始上涨了。
十、心得体会
非常荣幸能够参加到这次FM的活动,从这次活动中学习到了很多的知识,也对Arduino有了完完全全的重新的认识。之前使用51单片机或者32单片机关于模拟的功能仅使用过ADC和DAC,在这个板子上见到了内置的OPA,并使用内置的OPA实现了放大的功能。当然通过这次活动,最大的收获便是对HA有了初步的认识,并在网络的帮助下实现了HA环境的搭建,以及将传感器的数据上传到HA的平台中,遗憾的是由于目前身边没有低功耗的开发板(树莓派之类的,可以长时间运行HA的板子或设备),使用虚拟机搭建的HA环境也没有长时间的运行下去。希望后面有时间可以学习一下Linux的开发,搭建一套真正属于自己的可以长时间工作的HA的环境,并继续使用这块板子实现更多传感器的数据获取和上传。
在这次活动中,能提出的意见是,希望活动的直播可以早一些开始。在这次活动的制作过程中,大家的热情都很高涨,很多人在直播开始前基本就做完了大部分的内容,直播就没有了带大家入门的效果,当然自己从网上搜寻资料,进行开发也是一项必备的技能。但是还是希望如果有更多的活动,希望直播可以尽早的开启,这样也不会浪费官方的老师为我们大家精心准备的课程。
最后的最后,还是要希望论坛能够多多推出更多有趣有意思,能够带领大家学到更多知识的活动、课程、直播等等。也希望各位工程师朋友在今后的工作中顺利,学到更多的知识,掌握更多的本领。
-
上传了资料:
Follow_me第二季第二期代码