行空板电机扩展板 apriltag小车跟踪项目
本帖最后由 daybreak21 于 2024-9-10 20:47 编辑https://www.bilibili.com/video/BV1dqpYeKE6q/?vd_source=882fcf4e33df5ebe16324a20bbb213c548cb7ad9779
一、项目简介
前不久,我拿到了df的行空板电机拓展版,让我觉得行空板的功能可以进一步拓展。我原来用哈士奇(二哈识图)做过小车跟随项目,用下来也挺稳定的。但是,大家知道,哈士奇就是个近视眼,对于高像素远距离的目标,它就很累;此外,哈士奇让我们快速的上手各种机器视觉项目,却封装的十分彻底,我无法了解检测的原理,作为一名教师,总觉得想让学生知道个所以然,所以,我在暑假研究了这样一个项目。能力有限,问题较多,请大家包含。
二、Apriltag介绍
AprilTag是由密歇根大学的APRIL Robotics实验室提出的视觉基准系统。它在增强现实、机器人和相机校准等领域广泛应用。通过识别AprilTag标签,可以确定计算标记相对于相机的精确3D位置、方向和标识,相对于传统QR二维码,它降低了复杂度以满足实时性需求,并且这些标签可以用打印机打印。以下是一个标签的示例图像:
Apriltag的编码原理
[*]Apriltag标签的数据有效承载通常为4到12位编码。
以下是每个标签家族的宽度(位数)和可用标签数量,在选择最复杂的设计之前,还要考虑到对于给定的物理标签尺寸,例如一个直径 25 厘米的标签,设计越复杂,数据位的尺寸越小,因此标签必须更近才能有足够的像素进行正确检测。
Family Name
Total Width
Width of Square
Fill Factor
Number Of Tags
16H5
8
6
0.75
30
21H7 Circle
9
5
0.55
38
25H9
9
7
0.78
35
36H11
10
8
0.8
587
41H12 Standard
9
5
0.56
2115
关于编码长度:
以AprilTag 16H5为例,“16”表示编码长度为 16 位,“H5”表示标签之间的最小汉明距离为 5。这意味着任意两个标签之间至少有 5 位是不同的。较大的汉明距离可以提高标签识别率,因为即使有一些位被错误识别,标签仍然可以被正确区分。每个标签都可以通过编号来识别。标签设计还包含一些错误检测和纠正功能,因此即使读取到一两个位的错误,也可以正确识别出标签。
假设我们有一个标签系统,每个标签用 16 位二进制数表示(即编码长度为 16)。可能的标签有:
[*]0000 0000 0000 0000
[*]0000 0000 0000 0001
[*]0000 0000 0000 0010
[*]0000 0000 0000 0011
[*]…
这样,我们可以生成 (2^{16}) 个不同的标签,每个标签都有一个唯一的 16 位二进制编码。
关于汉明距离:
汉明距离是指两个二进制数之间不同位的数量。举个例子:
[*]标签 A:0000 0000 0000 0101
[*]标签 B:0000 0000 0000 1100
我们比较这两个标签的每一位:
[*]第 1-12 位:相同
[*]第 13 位:0 和 1 不同
[*]第 14 位:1 和 1 相同
[*]第 15 位:0 和 1 不同
[*]第 16 位:1 和 0 不同
所以,标签 A 和标签 B 之间的汉明距离是 3,因为它们有 3 位不同。
为了在识别率和帧率之间取得平衡,本项目,我选用apriltag 25H9进行检测,一块4*4cm的标签,使用320*240摄像头分辨率,在一米以外仍可以检测,帧率在15fps,并且不出错。
三、认识行空板与电机扩展板
1、关于行空板
行空板是一款专为Python学习和使用设计的新一代国产开源硬件,采用单板计算机架构,集成LCD彩屏、WiFi蓝牙、多种常用传感器和丰富的拓展接口。同时,其自带Linux操作系统和Python环境,还预装了常用的Python库,让广大师生只需两步就能进行Python教学。
2、行空板双路电机扩展板
此产品是专为行空板(UNIHIKER)设计的适配扩展板,支持2路直流电机,简化控制流程。采用创新的倾斜金手指插槽设计,为屏幕提供最佳可视角度。集成了两路直流电机驱动,支持独立电源供电,并配备了RGB灯、红外发射与接收功能,以及10路3Pin口和4路I2C口的扩展能力,配合DFRobot强大的Gravity产品体系,让行空板的创意实现更加多样化。
选择双路电机扩展板的原因,是因为其为行空板专门设计,并且在小车控制中,双路差速控制是最简单的控制方法了,便于初学者学习。
3、行空板有线无线连接
有线连接:请看官网 https://www.unihiker.com.cn/wiki/get-started 快速使用教程
无线连接:请看官网 https://mc.dfrobot.com.cn/thread-316050-1-1.html
不需要使用typec,但需要wifi以及外部供电
四、环境搭建
环境搭建:请看官网 mind+ 库管理https://mindplus.dfrobot.com.cn/Python-code
完整环境搭建步骤
安装库文件需要行空板wifi联网
运行 pip install apriltag 安装apriltag
五、小车准备
为了适应行空板控制,我设计了一个小车底盘,摄像头安放在小车侧方,双前轮作为车头方向,使用高减速比的n20电机(减速比关乎到小车行驶的稳定性,太快或者太慢都会影响行驶体验,需要做些测试),2节锂电池串联装在小车底部。整体电压在8V左右,其左侧控制端口为P6、P16~,右侧控制端口为P5、P8~。
以下是我用的万向轮,0.5寸,我找了很久,大家可淘宝自寻
以下是小车车架 打印文件,大家可以下载
attach://177300.3mf
attach://177299.3mf
根据产品文档:https://wiki.dfrobot.com.cn/SKU_DFR1136_%E8%A1%8C%E7%A9%BA%E6%9D%BF%E5%8F%8C%E8%B7%AF%E7%94%B5%E6%9C%BA%E9%A9%B1%E5%8A%A8IO%E6%89%A9%E5%B1%95%E6%9D%BF_DC_Motor_Driver_Carrier#target_0
每个电机使用2根信号线控制,一个信号口输出高低电平控制电机转向,另外一个信号口输出pwm控制电机转速。P7控制M1转向,P16控制M1速度,P6控制M2转向,P8控制M2转速,PWM值为0~1023,比arduino精度高很多。
以下是文档中电机控制程序,可以先用来测试小车
static/image/hrline/5.gif
六、项目研究思路
如果要尽量接近实验结果,我们需要一步步解决问题,上一个没问题了,再进行下一个步骤。确认没问题的部分不要轻易改动。
1、使用python+opencv+摄像头组合,捕获图像,并调用apriltag检测画面中的标签。
2、如果发现一个或多个标签,则计算标签的最大外接矩形,挑选其中最大的。
3、在画面最大的标签中,进一步提取、计算标签的宽度和中心点坐标。
4、将关键信息与电机pwm值进行映射,获得左右电机的pwm当前值,并驱动小车行驶。
图像坐标与电机差速控制关系计算
OpenCV可以回传标识物的空间信息,当分辨率是320*240像素时。经过实验检测,它看到标识物的最大的像素宽度尺寸Width是80,最小的像素尺寸是11(可能会因为标识物不同有所区别)。标识物中心位置X,最左边达到50,最右边是270;
行空板控制电机转速PWM脉冲的最大值是1023,为了便于计算与行驶,我先把PWM限定在255内。此刻,我假设在直角坐标系中,当opencv反馈目标物宽度为65时,目标中心位置为200时,左右电机转速值PWM各是多少?
第一步:坐标映射
为了进行计算,需要将OpenCV反馈的目标物宽度和中心位置映射到0~255的坐标系中:
[*]对于宽度,给定范围是30到80像素,可以将其映射到[-25, 25]的坐标系。
[*]对于中心位置,给定范围是50到270像素,映射到[-255, 255]的坐标系。
Y轴(宽度到速度的映射)
假设目标物的宽度是65像素,宽度范围是30到80像素,目标物宽度的变化量是:
Y_range = 80 - 30 = 50
我们可以将其映射到[-25, 25]范围,并计算出相应的值:
Y_mapped = ((65 - 30) / 50) * 25 = 17.5
接下来将其映射到0~255的范围:
Y = (17.5 / 25) * 255 = 102
X轴(位置到转向的映射)
假设目标物中心位置为200像素,中心位置的变化范围是从50到270像素,总范围为:
X_range = 270 - 50 = 220
映射到[-255, 255]范围:
X_mapped = ((200 - 50) / 220) * 510 - 255 = 113.64
第二步:确定左右电机的速度关系
在这个系统中,左右电机的转速受两个因素的影响:
[*]Y轴值控制整体的前进速度(左右电机的基准速度相同)。
[*]X轴值控制转向时两侧电机的差速。X > 0 表示右转,X < 0 表示左转。
因此,左右电机的速度计算可以表达为:
[*]左电机转速:L = Y + (X / 255) * Y
[*]右电机转速:R = Y - (X / 255) * Y
为了保证速度值在0到255的范围内,需要使用 constrain() 函数来限制电机的转速不超出该范围。
第三步:公式推导
左右电机速度的最终计算公式为:
L = constrain(Y + (X / 255) * Y, 0, 255)R = constrain(Y - (X / 255) * Y, 0, 255)
其中:
[*]Y 是前进速度,范围是0~255。
[*]X 是转向值,范围是-255~255。X > 0 时左电机加速,右电机减速;X < 0 时左电机减速,右电机加速。
让我们总结一下:
基本原则是: 两个电机的基础速度由Y轴值确定。两个电机的速度差由X轴值确定。 如果X轴为200,则右侧电机R要减去g的部分,左侧电机L要加上i的部分。i、g的值不仅仅由x决定,而是要计算x的变化幅度在y上会改变多少(x/255*Y)
七、分步实施步骤:
本项目opencv部分,我用模块化编程做了个简易测试。然因为在模块化中编写复杂代码效率太低,并且一些opencv函数并为放在其中,所以,整体项目我还是使用代码编写。
opencv摄像头+apriltag+opencv文本显示+电机控制+小车调试
如果使用代码编写,可在行空板中新建py文件,并在mind+代码窗口中编译。程序编写完成,可以保存代码,并直接运行。
我的小车因为使用行空板wifi连接,所以就不用数据线了,调试也更加方便。
八 、调试改进
即便你在理论或解题思路上没有问题,但是,在测试过程中,最主要的还是需要调试。如果不经过调试,小车根本无法稳定运行。
以下是一些心得与建议:
1、观察小车运行状态,调节各种参数
起初运行时,小车无法跟随二维码,这会出现两种状况,一是小车转向不足,应该是左转或右转的车轮转速不够;二是转向过度,这应该是由于车轮行驶惯性较大,即便电机停机,其也会多滚动半圈,再加上如果用单线程的程序方法,响应不够及时,所以要多方处理。我的简单方法就是在pwm值低于200时(0~1023),我干脆就把PWM归零了。
2、多线程运行,用满cpu载荷。
许多静态项目在使用行空板运行时,并未考虑时效问题,但在opencv以及小车行驶项目中,需要更快的检测速度,否则经常错过目标。我在代码编写完成测试后,opencv帧率一直维持在16帧,这一度让我感到成功无望。行空板是4核处理器,询问AIGC,提示可以使用多线程方式,通过def u_thread2_function():函数,我将目标检测与小车行驶、屏幕信息显示放在不同的进程中。经过对比,多线程可以将帧率提高到24帧,这样小车灵敏多了。
还有许多参数调节的地方,我无法一一回忆,只希望大家能亲自尝试。
九、小结
除了apriltag,其实很多应用我还未能去深度实现,比如用opencv级联器的人脸识别功能跟踪,我尝试过用刘德华的脸,小车可以正常跟踪,只是并为进一步尝试提升帧率。还有就是如果想让小车高速行驶,那就要将PID控制方式嵌入其中,并且要增加摄像头的分辨率(640*480),这我将在后面继续尝试。其实,对于中小学的人工智能教学,这个项目的难度已经足够了,学生们自己完成挑战估计也要花费半个学期的时间。通过本项目,我验证了行空板控制小车的一些可能性。或许很多用户觉得行空板速度太慢了,比如运行深度学习的一些项目,我觉得在硬件型号确定的情况下,如何挖掘硬件的潜力,发挥其更大的价值时非常有意义的,希望和同行们一同交流分享经验。
https://mc.dfrobot.com.cn/static/image/hrline/4.gif
以下为项目完整代码:
电机测试代码
#-*- coding: UTF-8 -*-
# MindPlus
# Python
import time
from pinpong.board import Board,Pin
from pinpong.extension.unihiker import *
Board().begin()
p_p5_out=Pin(Pin.P5, Pin.OUT)
p_p8_pwm=Pin(Pin.P8, Pin.PWM)
p_p6_out=Pin(Pin.P6, Pin.OUT)
p_p16_pwm=Pin(Pin.P16, Pin.PWM)
while True:
p_p5_out.write_digital(1)
p_p8_pwm.write_analog(512)
p_p6_out.write_digital(1)
p_p16_pwm.write_analog(512)
time.sleep(1)
p_p8_pwm.write_analog(0)
p_p16_pwm.write_analog(0)
time.sleep(1)
p_p5_out.write_digital(0)
p_p8_pwm.write_analog(512)
p_p6_out.write_digital(0)
p_p16_pwm.write_analog(512)
time.sleep(1)
p_p8_pwm.write_analog(0)
p_p16_pwm.write_analog(0)
time.sleep(1)
单线程整体代码
import cv2# 导入OpenCV库,用于图像处理和摄像头捕获
import apriltag# 导入AprilTag库,用于二维码检测
import time# 导入time模块,用于时间相关功能
from pinpong.board import Board, Pin# 从pinpong库导入Board和Pin类,用于MindPlus板控制
# 初始化摄像头
cap = cv2.VideoCapture(0)# 创建摄像头对象,参数0表示使用第一个摄像头设备
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)# 设置摄像头捕获帧的宽度为320像素
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)# 设置摄像头捕获帧的高度为240像素
# 初始化AprilTag检测器
options = apriltag.DetectorOptions(families="tag25h9")# 定义AprilTag检测器的参数,选择tag25h9家族的二维码
detector = apriltag.Detector(options)# 创建AprilTag检测器实例
# 创建全屏显示窗口
cv2.namedWindow('AprilTag Detection', cv2.WND_PROP_FULLSCREEN)# 创建名为'AprilTag Detection'的窗口
cv2.setWindowProperty('AprilTag Detection', cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)# 将窗口设置为全屏显示
# 初始化帧率计算变量
fps = 0# 存储计算得到的帧率
frame_count = 0# 帧计数器,用于计算帧率
start_time = time.time()# 记录开始时间,用于计算帧率
# 初始化MindPlus板
Board().begin()# 初始化MindPlus板对象
p_p5_out = Pin(Pin.P5, Pin.OUT)# 创建P5引脚对象,并设置为输出模式
p_p6_out = Pin(Pin.P6, Pin.OUT)# 创建P6引脚对象,并设置为输出模式
p_p8_pwm = Pin(Pin.P8, Pin.PWM)# 创建P8引脚对象,并设置为PWM输出模式
p_p16_pwm = Pin(Pin.P16, Pin.PWM)# 创建P16引脚对象,并设置为PWM输出模式
# 定义PWM范围
PWM_MIN = 20# PWM最小值,对应电机控制的最小脉冲宽度
PWM_MAX = 150# PWM最大值,对应电机控制的最大脉冲宽度
# 映射函数:将输入值映射到指定的输出范围
def numberMap(x, in_min, in_max, out_min, out_max):
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
# 限制函数:将输入值限制在指定的范围内
def constrain(amt, low, high):
return max(low, min(amt, high))
# 控制小车前进函数
def forward(PWM_L, PWM_R):
if PWM_L < 20: PWM_L = 1023
if PWM_R < 20: PWM_R = 1023
# 将PWM值映射到允许的PWM信号范围内,并取整数部分
pwm_l_int = int(constrain(PWM_L, PWM_MIN, PWM_MAX))
pwm_r_int = int(constrain(PWM_R, PWM_MIN, PWM_MAX))
# 激活电机驱动引脚,设置为高电平
p_p5_out.write_digital(1)
p_p6_out.write_digital(1)
# 写入PWM值到电机控制引脚,控制电机转速
p_p8_pwm.write_analog(pwm_r_int)
p_p16_pwm.write_analog(pwm_l_int)
# 停止小车函数
def STOP():
# 写入0到PWM引脚,停止电机
p_p8_pwm.write_analog(0)
p_p16_pwm.write_analog(0)
# 主循环
while True:
# 读取摄像头的下一帧
ret, frame = cap.read()
# 如果读取失败,则退出循环
if not ret:
break
# 将当前帧转换为灰度图像,便于二维码检测
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 使用AprilTag检测器检测灰度图像中的二维码
tags = detector.detect(gray)
# 如果没有检测到二维码,调用STOP函数停止小车
if not tags:
STOP()
else:
# 如果检测到二维码,找到面积最大的二维码
max_tag = max(tags, key=lambda tag: tag.corners - tag.corners)
# 计算最大二维码的中心点横坐标
x = int(max_tag.center)
# 计算最大二维码的宽度
w = int(max_tag.corners - max_tag.corners)
# 使用映射函数将二维码中心点横坐标映射到PWM值
x_mapped = numberMap(x, 0, 320, -(PWM_MAX), (PWM_MAX ))
# 使用映射函数将二维码宽度映射到PWM值
w_mapped = numberMap(w, 40, 13, PWM_MIN, PWM_MAX)
# 根据映射结果计算左右电机的PWM值
PWM_L = int(constrain(w_mapped + ((x_mapped/PWM_MAX)*w_mapped), PWM_MIN, PWM_MAX))
PWM_R = int(constrain(w_mapped - ((x_mapped/PWM_MAX)*w_mapped), PWM_MIN, PWM_MAX))
# 调用forward函数,根据计算得到的PWM值驱动小车前进
forward(PWM_L, PWM_R)
# 遍历所有检测到的二维码,绘制边框并显示标签ID
for tag in tags:
for idx in range(len(tag.corners)):
# 绘制二维码边框
cv2.line(frame, tuple(tag.corners.astype(int)), tuple(tag.corners.astype(int)), (0, 255, 0), 2)
# 在二维码中心显示标签ID
cv2.putText(frame, str(tag.tag_id), (int(tag.center), int(tag.center)), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 255), 2)
# 更新帧率计算
frame_count += 1
elapsed_time = time.time() - start_time
# 每过一秒钟,计算一次帧率
if elapsed_time >= 1:
fps = frame_count / elapsed_time
frame_count = 0
start_time = time.time()
# 构建要显示的信息文本,包括FPS、标签数量、最大标签的ID、宽度和中心X坐标
info_text = f"FPS: {int(fps)}\n"
if tags:
info_text += f"Tag ID: {max_tag.tag_id}\nTag Width: {w}\nCenter X: {x}\nPWM L/R: {PWM_L}/{PWM_R}"
# 在窗口左下角显示信息文本
for i, line in enumerate(info_text.split('\n')):
cv2.putText(frame, line, (10, frame.shape - 25 - (i * 25)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 200, 64), 2)
# 在创建的窗口中显示处理后的帧
cv2.imshow('AprilTag Detection', frame)
# 检测按键,如果按下'q'键,则退出循环
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# 循环结束后,释放摄像头资源并关闭所有OpenCV窗口
cap.release()# 释放摄像头资源
cv2.destroyAllWindows()# 关闭所有OpenCV窗口
多线程整体代码
<font size="3">import cv2
import apriltag
import time
import threading
from pinpong.board import Board, Pin
from unihiker import GUI
# 初始化摄像头
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)
# 初始化AprilTag检测器
options = apriltag.DetectorOptions(families="tag25h9")
detector = apriltag.Detector(options)
# 初始化MindPlus板
Board().begin()
p_p5_out = Pin(Pin.P5, Pin.OUT)
p_p6_out = Pin(Pin.P6, Pin.OUT)
p_p8_pwm = Pin(Pin.P8, Pin.PWM)
p_p16_pwm = Pin(Pin.P16, Pin.PWM)
# 定义PWM范围
PWM_MIN = 0
PWM_MAX = 700
# 全局变量
frame = None
tags = []
fps = 0
max_tag, w, x, PWM_L, PWM_R = None, None, None, None, None
lock = threading.Lock()
# 创建GUI对象
u_gui = GUI()
# 映射函数
def numberMap(x, in_min, in_max, out_min, out_max):
return (x - in_min) * (out_max - out_min) / (in_max - in_max) + out_min
def numberMap(x, in_min, in_max, out_min, out_max):
# 避免分母为零的情况
if in_max == in_min:
raise ValueError("Input range cannot have zero width")
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
# 限制函数
def constrain(amt, low, high):
return max(low, min(amt, high))
# 电机控制函数
def forward(PWM_L, PWM_R):
#防止左右转走过头,所以在pwm低于某个数值时干脆停车
if PWM_L < 200:
PWM_L = 0
if PWM_R < 200:
PWM_R = 0
pwm_l_int = int(constrain(PWM_L, PWM_MIN, PWM_MAX))
pwm_r_int = int(constrain(PWM_R, PWM_MIN, PWM_MAX))
p_p5_out.write_digital(1)
p_p6_out.write_digital(1)
p_p8_pwm.write_analog(pwm_r_int)
p_p16_pwm.write_analog(pwm_l_int)
def STOP():
p_p8_pwm.write_analog(0)
p_p16_pwm.write_analog(0)
# 检测线程函数
def u_thread1_function():
global frame, tags, max_tag, w, x, PWM_L, PWM_R
while True:
ret, new_frame = cap.read()
if not ret:
break
gray = cv2.cvtColor(new_frame, cv2.COLOR_BGR2GRAY)
detected_tags = detector.detect(gray)
with lock:
frame = new_frame
tags = detected_tags
if not tags:
STOP()
max_tag, w, x, PWM_L, PWM_R = None, None, None, None, None
else:
max_tag = max(tags, key=lambda tag: tag.corners - tag.corners)
x = int(max_tag.center)
w = int(max_tag.corners - max_tag.corners)
# Ensure valid range
if 0 < x < 320:
x_mapped = numberMap(x, 0, 320, -PWM_MAX, PWM_MAX)
else:
x_mapped = 0
if 8 < w < 50:
w_mapped = numberMap(w, 50, 8, PWM_MIN, PWM_MAX)
else:
w_mapped = PWM_MIN
PWM_L = int(constrain(w_mapped + ((x_mapped / PWM_MAX) * w_mapped), PWM_MIN, PWM_MAX))
PWM_R = int(constrain(w_mapped - ((x_mapped / PWM_MAX) * w_mapped), PWM_MIN, PWM_MAX))
# 电机控制线程函数
def u_thread2_function():
global PWM_L, PWM_R
while True:
with lock:
if PWM_L is not None and PWM_R is not None:
forward(PWM_L, PWM_R)
else:
STOP()
# 显示线程函数
def u_thread3_function():
global frame, fps, tags, max_tag, w, x, PWM_L, PWM_R
frame_count = 0
start_time = time.time()
# 设置显示窗口为全屏模式
cv2.namedWindow('AprilTag Detection', cv2.WND_PROP_FULLSCREEN)
cv2.setWindowProperty('AprilTag Detection', cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
while True:
frame_count += 1
elapsed_time = time.time() - start_time
if elapsed_time >= 1:
fps = frame_count / elapsed_time
frame_count = 0
start_time = time.time()
with lock:
if frame is not None:
display_info(frame, fps, tags, max_tag, w, x, PWM_L, PWM_R)
cv2.imshow('AprilTag Detection', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# 显示信息函数
def display_info(frame, fps, tags, max_tag=None, w=None, x=None, PWM_L=None, PWM_R=None):
info_text = f"FPS: {int(fps)}\n"
if tags and max_tag:
info_text += f"Tag ID: {max_tag.tag_id}\nTag Width: {w}\nCenter X: {x}\nPWM L/R: {PWM_L}/{PWM_R}"
for i, line in enumerate(info_text.split('\n')):
cv2.putText(frame, line, (10, frame.shape - 25 - (i * 25)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 200, 64), 2)
for tag in tags:
for idx in range(len(tag.corners)):
cv2.line(frame, tuple(tag.corners.astype(int)), tuple(tag.corners.astype(int)), (0, 255, 0), 2)
cv2.putText(frame, str(tag.tag_id), (int(tag.center), int(tag.center)), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 255), 2)
# 启动线程
thread1 = u_gui.start_thread(u_thread1_function)
thread2 = u_gui.start_thread(u_thread2_function)
thread3 = u_gui.start_thread(u_thread3_function)
# 主循环,保持程序运行
while True:
time.sleep(1)</font>
学习了,非常棒的文章~ 666 老师,请问您使用的是什么样的万向轮?有没有购买链接? 感觉还可以用NFC 麦壳maikemaker 发表于 2024-9-10 09:51
老师,请问您使用的是什么样的万向轮?有没有购买链接?
您好,我已在帖子里更新,链接我就不提供了哈 1111111111111111111111111111111111111111111 daybreak21 发表于 2024-9-10 20:48
您好,我已在帖子里更新,链接我就不提供了哈
感谢!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
页:
[1]