Author:qyan.li
Date:2022.6.10
Topic:详解树莓派的使用及基于树莓派制作手势控制的小车
Reference:如何给树莓派安装操作系统 - 知乎 (zhihu.com)
一、写在前面
~~~~~~~~ 课程的结业项目要求基于树莓派做个小东西,无意中翻出大一刚入学时做项目剩下的小车架,就想着能不能基于此做一个小车的系统,将树莓派放置在上面,形成一个系统。后面又在思考,单纯的驱动难度太小,能不能添加一些控制的模块,比如语音控制,手势控制等等,于是就形成今天要写的项目:基于树莓派的手势控制的智能小车。
~~~~~~~~ 顺便提一下,为方便大家烧录树莓派系统,所有相关软件均会在文中提供下载连接,文章末尾会以网盘的形式将相关的安装包打包提供,大家可自行下载使用。凭这一点,你还不给我点个赞!开个玩笑,希望打动你的是文章的内容,有收获就好!
二、系统实现:
~~~~~~~~ 在正式开始之前,需要做一些准备工作,主要包括:
-
相关配件的购买:
SD
卡,其余视情况购买 -
树莓派系统的烧录
-
小车系统的构建(模块组装,系统接线等等)
~~~~~~~~
上面作为一个分界线,首先来说明raspberry
系统的烧录,网络上有关树莓派系统烧录的讲解视频和博文都比较多,但大多比较老,可以成功安装,但在安装的过程中可能会出现诸多问题。树莓派官网上提供系统的安装教程,为避免部分同学无法科学上网Youtube
,视频提供在下方,可按照教程安装。
~~~~~~~~
新版的树莓派系统安装提供一个imager_1.7.2.exe
的烧录工具,可以凭借于此完成树莓派系统的烧录和树莓派初始化的一些配置。
烧录工具下载连接:Raspberry Pi OS – Raspberry Pi
~~~~~~~~ OK,下面详细讲解工具的使用以及树莓派系统的安装。
-
将
SD
卡插入PC
机(SD
卡一般带有U
盘状或者扁平状的卡槽),运行下载好的imager
软件,界面如下:
-
分别选择目标烧录的操作系统(树莓派官方系统选择第一个),插入的储存卡,注意此时不要点击烧录
-
**划重点!!!**此处必须点击右下角设置进行树莓派初始化的设置,可以省去后面诸多的问题,必须配置的几个方面:
- 用户名和密码配置,新版本系统移除默认用户,因此必须自己设置用户名和密码,牢记(最好写在备忘录里),后续系统登录需要
Wifi
配置,Wifi
配置成功后,树莓派启动后会自动链接,便于后续操作和远程链接,Wifi
我配置为寝室网络,因此树莓派启动时仅能链接寝室网络,无法链接其他。如果树莓派需要不断更换位置使用,建议配置个人手机热点- SSH服务设置为开启,方便后续借助于
SSH
操作树莓派以及VNC
服务开启(重点:必须开启)
-
点击烧录
raspberry
系统,烧录过程中出现问题,可重新多烧录几次 -
SD卡插入树莓派背面的卡槽,树莓派上电
~~~~~~~~ 上面又是一个分界线,按照上面的步骤,理论上讲,树莓派的系统已经安装好,且此时树莓派应该已经链接上你事先设定的网络。
~~~~~~~~
是否成功链接,你可以通过查看链接在当前网络下的设备即可,手机热点通过手机查看;如果你和我一样,使用寝室WIFI
,可以通过浏览器管理查看,方法如下:
- 浏览器输入
192.168.1.1
进入路由器管理界面(前提:PC
机必须链接在寝室WIFI
下) - 输入寝室
WIFI
的用户名和密码,即可以查看当前网络下是否有Raspberry
链接,点击进入Raspberry
的管理界面,可以看到树莓派的IP
地址,后续有用
~~~~~~~~ 又是一个分界线,此时树莓派系统烧录完成,树莓派也成功联网,但是我们如何应用呢?由于树莓派是一个微型的计算机,你可以链接外设-显示器借助于图形化界面操作树莓派,这是一种解决的办法,但显然违背我们的初衷,微小便携,更何况后续我们还要嵌在小车上。那有没有其他的方式呢?
~~~~~~~~
这是就不得不提前面给大家强调的SSH
服务啦,由于树莓派官方系统是基于linux
的,所以它也具有linux
系统最大的特色,终端,而SSH
服务让我们可以通过命令行的方式操作树莓派:
~~~~~~~~
这里我们借助于的是putty
的软件,下载连接:https://www.putty.org/下载完成打开界面如下:
在其中输入IP
地址,点击open
即可进行树莓派的终端,借助于命令行操作Raspberry
小
Tips:
树莓派IP地址的查询方法,寝室
WIFI
可借助于192.168.1.1
路由器管理进行查询,手机热点由于系统不同方法也不同,请自行检索
~~~~~~~~
OK,此处又是一个分界线,因此此时我们已经可以实现和树莓派进行交互啦,但是上述有一个比较重要的问题,许多同学是没有接触过linux
终端的,就算知道,一般也是仅限于知道(我)。所以,上述交互方式显然不友好,那么可不可以像windows
一样借助于图形化界面操作呢?答案是可以,此时就要上场今天的第二个工具:VNC-Viewer
VNC-Viewer
下载连接:https://www.realvnc.com/en/connect/download/viewer
~~~~~~~~
VNC-Viewer
是一款远程链接的服务,可以将ipad
,PC
机等等作为树莓派的显示屏幕,而不必必须使用HDML
链接显示屏的方式。如果希望借助于VNC-Viewer
连接树莓派,必须事先在树莓派上开启VNC
服务,如何开启呢?借助于SSH
命令行进行操作,其实这才是我为什么强调必须在初始化时开启SSH
服务的原因。
~~~~~~~~ 但是由于自己配置的时间较为久远,因此在此不会展示详细的步骤,可参考下面的连接进行配置:
(6条消息) 使用VNC Viewer连接树莓派超时的原因_Lok’tar O’gar的博客-CSDN博客_树莓派vnc连接超时
里面包含有借助于SSH
命令行详细配置开启树莓派VNC的详细步骤。
~~~~~~~~
截止到现在,理论上讲,你已经可以借助于VNC-Viewer
连接树莓派,看到树莓派的主屏幕啦!接下来就可以为所欲为啦!!
~~~~~~~~ 下面提供一些相关的链接方便大家参考借鉴:
-
借助于路由器
192.168.1.1
查看树莓派的IP
地址: -
借助于
VNC-Viewer
使用电脑或者ipad
充当树莓派的显示屏 -
VNC
链接显示Timed out waiting for a response from the computer
(13条消息) 使用VNC Viewer连接树莓派超时的原因_Lok’tar O’gar的博客-CSDN博客_树莓派vnc连接超时
-
SSH
连接显示access denied
树莓派新系统默认移除
pi
用户,无法通过此进行登录 -
通过
wpa
和ssh
树莓派连接wifi
-
系统烧录成功后,树莓派显示
can not currently show the desktop
https://blog.csdn.net/LlHilo/article/details/106577069
小
Tips
:树莓派显示分辨率过低,按照上述方式设置后,一定要
reboot
,重启系统,否则不会生效
~~~~~~~~
此处提供相关的安装包,方便大家下载使用:(其中包含ssh
,vnc
,imager
下载和格式化工具),格式化工具自己没用到,但是部分博文有提及,需要同学自取。
链接:https://pan.baidu.com/s/1FwpFadmzbooNP27CMQPetg
提取码:8rdp
~~~~~~~~ 写不动啦,今天先暂时更新到树莓派系统的安装,后续系统相关内容持续更新,这是一场持久战!!
~~~~~~~~ 2022.6.18,今天更新小车的接线和系统的搭建,这部分相对来讲比较简单,更多的是傻瓜教程,准备好相关的配件,照着系统的线路图接线即可。首先,看一下系统搭建完成后的效果图:
系统搭建需要的主要模块:
- 树莓派:核心控制模块
- L298N:电机驱动模块
- 小车车架(包含两个直流电机)
- 充电宝:树莓派供电
- 电池盒:系统供电
- 夹子:固定万向轮
- 杜邦线若干:系统接线
准备好相关的配件后,即可按照如下的线路图进行系统的搭建:
~~~~~~~~ 这里作为一个分界线。上述已经完成系统的搭建,下面开始写代码测试系统搭建是否成功,老样子,还是先上代码,后面会具体讲解代码原理:
# -*-coding = utf-8-*-
# Author:qyan.li
# Date:2022/5/23 10:56
# Topic:借助于树莓派操作小车实现智能控制
# Reference:
import RPi.GPIO as GPIO #引入RPi.GPIO库函数命名为GPIO
import time #引入计时time函数
import socket
import sys
## 电机转动的驱动程序
GPIO.setmode(GPIO.BOARD) #将GPIO编程方式设置为BOARD模式(BOARD编号方式,基于插座引脚编号)
#接口定义
INT1 = 11 #将L298 INT1口连接到树莓派Pin11
INT2 = 12 #将L298 INT2口连接到树莓派Pin12
INT3 = 13 #将L298 INT3口连接到树莓派Pin13
INT4 = 15 #将L298 INT4口连接到树莓派Pin15
ENA = 16 #将L298 ENA口连接到树莓派Pin16
ENB = 18 #将L298 ENB口连接到树莓派Pin18
#输出模式
GPIO.setup(INT1,GPIO.OUT)
GPIO.setup(INT2,GPIO.OUT)
GPIO.setup(INT3,GPIO.OUT)
GPIO.setup(INT4,GPIO.OUT)
GPIO.setup(ENA,GPIO.OUT)
GPIO.setup(ENB,GPIO.OUT)
def motorSet():
## 电机驱动
## 设置ENA,ENB后必添加(转速初始化)
pwma.start(90) # 以占空比90开始启动
pwmb.start(90)
GPIO.output(INT1, GPIO.HIGH)
GPIO.output(INT2, GPIO.LOW)
GPIO.output(INT3, GPIO.LOW)
GPIO.output(INT4, GPIO.HIGH)
def motorStop():
## 电机停止
GPIO.output(INT1, GPIO.LOW)
GPIO.output(INT2, GPIO.LOW)
GPIO.output(INT3, GPIO.LOW)
GPIO.output(INT4, GPIO.LOW)
def turnRight():
## 电机右转
pwma.ChangeDutyCycle(10) # 占空比90
pwmb.ChangeDutyCycle(90)
def turnLeft():
## 电机左转
pwma.ChangeDutyCycle(90) # 占空比90
pwmb.ChangeDutyCycle(10)
pwma = GPIO.PWM(16,80) # 此处分别代表左右电机
pwmb = GPIO.PWM(18,80)
motorSet()
# motorStop()
# turnRight()
# turnLeft()
首先电机的驱动和停止:(仅就电机驱动和停止来讲,占空比的问题可以不必考虑)
- 驱动:电机的两个接口分别设置为low和high,电压差存在电机驱动(两管脚low和high互换,电机反转,本实验不需要)
- 停止:电机的两接口设置为同电平,同low或者同high,没有电压差,电机停止转动
其次是小车的左转和右转,是通过PWM改变输出波的占空比实现电机转速的调整(PWM详细了解,请自行检索),转速差使得小车转弯,左大于右,右转;左小于右,左转
- ChangeDutyCycle(num)函数改变占空比,num越大,转速越快,即可实现转速调整
- 本例中为效果明显,设置90和10两种占空比,差距过大,现象并非左转和右转,而是左转弯和右转弯(说明现象即可)
~~~~~~~~ 这里又作为一个分界线,读者可自行测试一下上述代码,在树莓派上运行,观察小车的运动情况,成功后可继续阅读,进行后续操作。看到这里,其实我们已经完成系统的所有硬件工作,接下来就是软件代码方面以及系统的总体调试,由于我们系统的目标是设计基于树莓派的手势控制系统,所以手势的识别其实是一个最核心的板块,手势识别借助于MediaPipe,由于较为复杂且问题较多,博主另起一篇文章讲述:
参考连接:(6条消息) 简单记录一次MediaPipe手势识别过程(附详细代码及问题解决办法)_隔壁李学长的博客-CSDN博客
~~~~~~~~ 这里又是一个分界线,手势识别,小车控制都已经实现,接下来的问题,如何让树莓派和PC机通信,由于手势识别在PC上运行,我们需要将手势识别代码产生的命令发送给树莓派以实现小车的控制,所以必须实现二者通信。
针对于此进行小小的说明:
为什么不将手势识别的代码直接放置在树莓派上运行,而是放在PC机上与树莓派通信实现:
- 担心树莓派的算力不够,MediaPipe框架在树莓派上能否实现
- 硬件局限,手势识别放置在树莓派上,必然在系统添加摄像头,而且在小车运动过程中始终保持摄像头可以捕捉到手势,难度大,不便于操作
但是这种思想降低实验的难度,但是不太符合嵌入式系统的特点或者要求,大家可自行调整系统框架
~~~~~~~~ 树莓派和PC机的通信,本实验中借助于TCP协议实现,由于代码并非自己所写,网络资源,所以并没有过多的原理性的部分可以讲解,仅粘贴实现代码,直接应用:
TCP参考代码:(6条消息) 树莓派和PC间TCP通信_qq_35130248的博客-CSDN博客_树莓派tcp通信
~~~~~~~~ TCP通信的代码在此处不进行粘贴,修改后的TCP代码嵌在手势识别和小车控制中,详细的代码见文章的最后部分:
有关于TCP通信做小的说明:
- 原始TCP通信中作者为增强代码的健壮性,增加诸多try,catch异常处理的版块,但是在本项目中自己测试,异常处理的存在可能会降低系统的灵敏度,所以在本项目张中删除异常处理部分的代码
~~~~~~~~ OK,这里作为最后一条分界线,以上我们已经将所有的分板块全部介绍完成,下面是系统的总体部署:将所有的代码串联起来形成完整的系统,主要是一些代码编写的注意点是,完成的代码放在下面,方便大家参考借鉴:
## 树莓派端代码(包含TCP通信和小车控制)
# -*-coding = utf-8-*-
# Author:qyan.li
# Date:2022/5/23 10:56
# Topic:借助于树莓派操作小车实现智能控制
# Reference:
import RPi.GPIO as GPIO #引入RPi.GPIO库函数命名为GPIO
import time #引入计时time函数
import socket
import sys
## 电机转动的驱动程序
GPIO.setmode(GPIO.BOARD) #将GPIO编程方式设置为BOARD模式(BOARD编号方式,基于插座引脚编号)
#接口定义
INT1 = 11 #将L298 INT1口连接到树莓派Pin11
INT2 = 12 #将L298 INT2口连接到树莓派Pin12
INT3 = 13 #将L298 INT3口连接到树莓派Pin13
INT4 = 15 #将L298 INT4口连接到树莓派Pin15
ENA = 16 #将L298 ENA口连接到树莓派Pin16
ENB = 18 #将L298 ENB口连接到树莓派Pin18
#输出模式
GPIO.setup(INT1,GPIO.OUT)
GPIO.setup(INT2,GPIO.OUT)
GPIO.setup(INT3,GPIO.OUT)
GPIO.setup(INT4,GPIO.OUT)
GPIO.setup(ENA,GPIO.OUT)
GPIO.setup(ENB,GPIO.OUT)
def motorSet():
## 电机驱动
## 设置ENA,ENB后必添加(转速初始化)
pwma.start(90) # 以占空比90开始启动
pwmb.start(90)
GPIO.output(INT1, GPIO.HIGH)
GPIO.output(INT2, GPIO.LOW)
GPIO.output(INT3, GPIO.LOW)
GPIO.output(INT4, GPIO.HIGH)
def motorStop():
## 电机停止
GPIO.output(INT1, GPIO.LOW)
GPIO.output(INT2, GPIO.LOW)
GPIO.output(INT3, GPIO.LOW)
GPIO.output(INT4, GPIO.LOW)
def turnRight():
## 电机右转
pwma.ChangeDutyCycle(10) # 占空比90
pwmb.ChangeDutyCycle(90)
def turnLeft():
## 电机左转
pwma.ChangeDutyCycle(90) # 占空比90
pwmb.ChangeDutyCycle(10)
pwma = GPIO.PWM(16,80) # 此处分别代表左右电机
pwmb = GPIO.PWM(18,80)
## 树莓派变量设定
HOST_IP = "192.168.1.107"
HOST_PORT = 8888
print("Starting socket: TCP...")
## 树莓派连接PC机
socket_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("TCP server listen @ %s:%d!" %(HOST_IP, HOST_PORT) )
host_addr = (HOST_IP, HOST_PORT)
socket_tcp.bind(host_addr)
socket_tcp.listen(3)
socket_con, (client_ip, client_port) = socket_tcp.accept()
print("Connection accepted from %s." %client_ip)
socket_con.send(str.encode("Welcome to RPi TCP server!"))
print("Receiving package...")
while True:
## 接收发送的消息
try:
data=socket_con.recv(512)
data = bytes.decode(data)
if len(data)>0:
print("Received:%s"%data)
## 根据接收消息执行对应的指令
if data == '11':
print("forward")
motorSet()
elif data == '00':
print("backward")
motorStop()
elif data == '10':
print('turn right')
turnRight()
elif data == '01':
print('turn left')
turnLeft()
else:
continue
except Exception:
socket_tcp.close()
sys.exit(1)
## PC机端代码(包含手势识别和TCP通信)
# -*-coding = utf-8-*-
# Author:qyan.li
# Date:2022/5/24 10:35
# Topic:借助于mediapipe实现手势识别(前进,后退,向左,向右四种操作)
# Reference:https://blog.csdn.net/weixin_41747193/article/details/122117629
# Reference:https://zhuanlan.zhihu.com/p/391844369
import cv2
import mediapipe as mp
import time
import traceback
import socket
import sys
# RPi's IP
SERVER_IP = "192.168.1.107"
SERVER_PORT = 8888
print("Starting socket: TCP...")
server_addr = (SERVER_IP, SERVER_PORT)
socket_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
commandDict = {'right':'10','left':'01','forward':'11','backforward':'00','unKnown':'-1'}
## videoCapture用于读取外部视频或者调用摄像头获取视频
## 报错:cv2.error: OpenCV(4.5.5) D:\a\opencv-python\opencv-python\opencv\modules\imgproc\src\color.cpp:182: error: (-215:Assertion failed) !_src.empty() in function 'cv::cvtColor'
## 解决方案:VideoCapture参数中添加cv2.CAP_DSHOW,可能是版本兼容问题
## 参考文献:https://blog.csdn.net/m0_38053897/article/details/111823537
cap = cv2.VideoCapture(0,cv2.CAP_DSHOW)
# cap = cv2.VideoCapture('./HandTest.mp4')
# cap = cv2.VideoCapture('./Test.MP4')
mpHands = mp.solutions.hands
hands = mpHands.Hands(static_image_mode=False,
max_num_hands=2,
min_detection_confidence=0.5,
min_tracking_confidence=0.5)
mpDraw = mp.solutions.drawing_utils
pTime = 0
cTime = 0
# 全局变量
frameNum = 0
commandLst = []
commandSending = ''
flag = 0
# 首先连接至树莓派,可发送消息
while True:
try:
print("Connecting to server @ %s:%d..." % (SERVER_IP, SERVER_PORT))
socket_tcp.connect(server_addr)
flag = 1
break
except Exception:
print("Can't connect to server,try it latter!")
time.sleep(1)
continue
if flag == 1:
while True:
# 初始化0关键点的坐标
lst = [0, 0, 0]
# 初始化字典
distanceDict = {}
success, img = cap.read()
imgRGB= cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
results = hands.process(imgRGB)
# print(results.multi_hand_landmarks)
if results.multi_hand_landmarks:
for handLms in results.multi_hand_landmarks:
for id, lm in enumerate(handLms.landmark):
## 存储0关键点的三个坐标
if id == 0:
lst = [lm.x,lm.y,lm.z]
h, w, c = img.shape
cx, cy = int(lm.x *w), int(lm.y*h)
## 绘制手部关键结点
cv2.circle(img, (cx,cy), 3, (255,0,255), cv2.FILLED)
## 分别检测4,8,12,20四个关键结点与0结点间的距离判断手指指向
if id == 4 or id == 8 or id == 12 or id == 20:
distanceSum = 0
distanceSum = (lst[0]-lm.x)**2 + (lst[1]-lm.y)**2 + (lst[2]-lm.z)**2
distanceDict[id] = distanceSum
## mediapipe中首部关键结点的连线
mpDraw.draw_landmarks(img, handLms, mpHands.HAND_CONNECTIONS)
command = 'UnKnown'
if distanceDict == {}:
MaxId = 0
else:
MaxId = [key for key,value in distanceDict.items() if value == max(distanceDict.values())][0]
## 判断哪个结点距离根节点最远,并由此给出相应的命令
if MaxId == 4:
command = 'right'
if MaxId == 8:
command = 'left'
if MaxId == 12:
command = 'forward'
if MaxId == 20:
command = 'backforward'
if MaxId == 0:
command = 'unKnown'
cv2.putText(img,command,(10,150), cv2.FONT_HERSHEY_PLAIN, 3, (255,0,255), 3)
## 计算并显示帧率
cTime = time.time()
fps = 1/(cTime-pTime)
pTime = cTime
cv2.putText(img,str(int(fps)), (10,70), cv2.FONT_HERSHEY_PLAIN, 3, (255,0,255), 3)
cv2.imshow("Image", img)
## 连续判断十帧图像
frameNum += 1
commandLst.append(command)
if frameNum == 5:
if max(commandLst) == min(commandLst):
commandSending = commandDict[command]
else:
commandSending = commandDict['unKnown']
socket_tcp.send(str.encode(commandSending))
## 数据重新置零
frameNum = 0
commandLst = []
print(commandSending)
else:
continue
## 此处设置退出键esc,按下esc退出窗口
key = cv2.waitKey(1)
if key == 27:
#通过esc键退出摄像
cv2.destroyAllWindows()
break
~~~~~~~~
最后附上手势识别及小车控制的效果,由于是分开录制的,可能会存在不同步的现像: