目录
概要
最近小编在做学校工作室的往年招新题目,也是做到了这道很有意思的题目,所以想把学到的东西分享一下,如果有错的话,还请大佬们指正。
首先基于Python+Opencv+mediapipe可以实现手势的识别,这里就只是识别数字,识别后得出的数据通过串口通信(Python实现)向stm32单片机传输数据,单片机收到数据后点亮相应数量的LED灯。
这里先简单介绍一下什么是Opencv和mediapipe
Opencv:OpenCV是一个基于Apache2.0许可(开源)发行的跨平台计算机视觉和机器学习软件库,可以运行在Linux、Windows、Android和Mac OS操作系统上。它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。
在这里我们只是用来获取电脑的摄像头,以便后续识别的进行。
mediapipe:mediapipe是一款由 Google Research 开发并开源的多媒体机器学习模型应用框架。在谷歌,一系列重要产品,如 YouTube、Google Lens、ARCore、Google Home 以及 Nest,都已深度整合了 MediaPipe。MediaPipe大有用武之地,可以做物体检测、自拍分割、头发分割、人脸检测、手部检测、运动追踪,等等。基于此可以实现更高级的功能。
手势识别的功能就靠mediapipe中的手部检测实现的。
还有串口通信的模块pySerial:
pySerial 是 Python 中用于操作串口的第三方模块,它支持 Windows、Linux、OSX、BSD等多个平台。如果要使用 pySerial 模块,首先必须保证 Python 版本高于 Python 2.7 或者 Python 3.4。另外,如果你是用的是 Windows 系统,那必须使用 Win7 及以上的版本。
pySerial 的安装很简单,只需要执行一条命令:pip install pyserial
安装完成后,只需要在 Python 代码中使用 import serial 语句导入该模块即可。
然后通过电脑向stm32单片机通信传输数据,单片机接收到数据后点亮对应数量得LED灯。怎么样是不听起来很轻松?其实也不是很难,一步一来就好。
整体架构流程
1.Python的代码
这里先给出效果图
关于Opencv和mediapipe的下载这里就不说了,网上有很多的教程。具体的代码框架如下,具体的思路写在注释里了。
import cv2
import mediapipe as mp
import warnings
import serial
import math
ser = serial.Serial('COM7', 9600, bytesize=8,parity=serial.PARITY_NONE,stopbits=1,timeout=None) #打开串口
if ser.isOpen(): #判断串口是否打开成功
print("Serial port opened successfully")
print(ser.name)
else:
print("Error in opening serial port")
hand_mod = mp.solutions.hands #导入手部模型
my_hands = hand_mod.Hands() #创建手部实例
my_drawing = mp.solutions.drawing_utils #导入绘图工具
my_camera = cv2.VideoCapture(0) #打开摄像头
while True:
ret, frame = my_camera.read()#读取摄像头数据 ret:返回值 frame:图像数据
if ret: #ret为True表示读取成功
img = cv2.flip(frame, 1) #镜像显示 因为获取到的摄像头不是镜像的,所以要转为镜像
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) #转换为RGB格式
#detect_hand_num(img)后面会解释
cv2.imshow("My Camera", img) #显示图像
k = cv2.waitKey(1) #等待按键输入
if k == ord('q'): #按q键退出程序
break
my_camera.release() #释放摄像头
cv2.destroyAllWindows() #关闭所有窗口
这里只是写出来了大概的框架,这里已经得到了手部实例和摄像头,主要的detect_hand_num()函数下面会说。
然后就是如何实现上图的手部节点检测和连接实现上面显示的效果图,直接上代码
def detect_hand_num(img):
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) #转换为RGB格式
results = my_hands.process(img_rgb) #手部识别
if results.multi_hand_landmarks: #识别到手
for land_marks in results.multi_hand_landmarks: #遍历每一只手的标记点
my_drawing.draw_landmarks(img, land_marks, hand_mod.HAND_CONNECTIONS) #绘制手部标记点
mlist = [] #标记点列表,用于判断手势,列表元素为(x,y)坐标,共20个
for point in land_marks.landmark: #遍历每一个标记点
x = int(point.x * img.shape[1]) #获取标记点x坐标
y = int(point.y * img.shape[0]) #获取标记点y坐标
cv2.circle(img, (x, y), 5, (255, 0, 0), cv2.FILLED) #绘制关节圆点,半径为5
if point!= land_marks.landmark[0]: #除去第一点,因为它是手掌中心点,不代表手指
mlist.append((x,y)) #添加到列表中
is_straight_list = [] #用于储存判断每一组四个点是否为直线的结果的列表
for i in range(0,20): #判断每一组四个点是否为直线
if i%4==0: #每四个点为一组
is_straight_list.append(is_straight(mlist[i],mlist[i+1],mlist[i+2],mlist[i+3])) #判断是否为直线
num = idt_finger(is_straight_list) #判断手势,返回数字
my_num = int(num) #转换为整数
ser.write(my_num.to_bytes(1, byteorder='big')) #发送数据到串口
print("num:",num) #打印数字
cv2.putText(img, num, (10, 100), cv2.FONT_HERSHEY_PLAIN, 3, (255, 0, 255), 3)#在窗口显示手势数字
先解释下这段代码,先遍历各个节点并将各个节点连接起来,然后因为我是通过手指的弯曲程度来实现数字的识别,所以用一个链表将20个节点(一个手指是4个节点)坐标存储起来,通过is_straight函数判断手指是否弯曲,再将判断结果存进is_straitght_list列表,通过idt_finger函数识别数字,得到数字后就可以传输数据给串口了。
再来就给一下上述几个函数的代码
def is_straight(point_1,point_2,point_3,point_4): #判断四个点是否为直线
x1 = int(point_1[0]) #x1,y1为第一个点的坐标
y1 = int(point_1[1])
x2 = int(point_2[0]) #x2,y2为第二个点的坐标
y2 = int(point_2[1])
x3 = int(point_3[0]) #x3,y3为第三个点的坐标
y3 = int(point_3[1])
x4 = int(point_4[0]) #x4,y4为第四个点的坐标
y4 = int(point_4[1])
degree1 = math.degrees(math.atan2(y2-y1,x2-x1)) #计算夹角
degree2 = math.degrees(math.atan2(y4-y3,x4-x3)) #计算夹角
if abs(degree1-degree2) > 20: #误差大于20度则视为弯曲
is_str = 0
else:
is_str = 1
return is_str
def idt_finger(is_straight_list): #判断手势,返回数字
if is_straight_list[0]==0 and is_straight_list[1]==1 and is_straight_list[2]==0 and is_straight_list[3]==0 and is_straight_list[4]==0:
return '1'
elif is_straight_list[0]==0 and is_straight_list[1]==1 and is_straight_list[2]==1 and is_straight_list[3]==0 and is_straight_list[4]==0:
return '2'
elif is_straight_list[0]==0 and is_straight_list[1]==1 and is_straight_list[2]==1 and is_straight_list[3]==1 and is_straight_list[4]==0:
return '3'
elif is_straight_list[0]==0 and is_straight_list[1]==1 and is_straight_list[2]==1 and is_straight_list[3]==1 and is_straight_list[4]==1:
return '4'
elif is_straight_list[0]==1 and is_straight_list[1]==1 and is_straight_list[2]==1 and is_straight_list[3]==1 and is_straight_list[4]==1:
return '5'
elif is_straight_list[0]==1 and is_straight_list[1]==0 and is_straight_list[2]==0 and is_straight_list[3]==0 and is_straight_list[4]==0:
return '6'
elif is_straight_list[0]==1 and is_straight_list[1]==1 and is_straight_list[2]==0 and is_straight_list[3]==0 and is_straight_list[4]==0:
return '8'
else :
return '0'
注意is_straight_list中的0、1、2、3、4分别对应大拇指、食指、中指、无名指、小指曲直状态。
2.stm32代码
初始化串口和中断
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP; //复用推挽输出,由USART外设控制输出
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;//PA9 - TX
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;//用上拉输入模式,因为串口空闲状态是低电平
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;//PA10 - RX
GPIO_Init(GPIOA,&GPIO_InitStructure);
USART_InitTypeDef USARET_InitStructure;
USARET_InitStructure.USART_BaudRate=9600;
USARET_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None ;//这里不需要流控
USARET_InitStructure.USART_Mode=USART_Mode_Tx|USART_Mode_Rx;
USARET_InitStructure.USART_Parity=USART_Parity_No; //校验位设置为0
USARET_InitStructure.USART_StopBits=USART_StopBits_1; //停止位设置1
USARET_InitStructure.USART_WordLength=USART_WordLength_8b; //8位数据位
USART_Init(USART1,&USARET_InitStructure);
USART_Cmd(USART1,ENABLE);
//中断初始化
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//配置RXNE的NVIC中断
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //优先级分组
NVIC_InitTypeDef NVIC_InitStructure; //初始化结构体
NVIC_InitStructure.NVIC_IRQChannel=USART1_IRQn; //设置中断通道
NVIC_InitStructure.NVIC_IRQChannelCmd= ENABLE; //使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority= 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority= 1;
NVIC_Init(&NVIC_InitStructure);
中断函数
uint8_t Serial_Rxdata;
uint8_t Serial_Rxflag;
uint8_t Serial_GetRxFlag(void) //获取标志位并设置为0
{
if(Serial_Rxflag==1)
{
Serial_Rxflag = 0;
return 1;
}
return 0;
}
uint8_t Serial_GetRxdata(void) //获取得到的数据
{
return Serial_Rxdata;
}
void USART1_IRQHandler(void) //中断函数
{
if(USART_GetFlagStatus(USART1,USART_IT_RXNE)==SET) //接受到数据
{
Serial_Rxdata = USART_ReceiveData(USART1); //保存数据
OLED_ShowNum(1,6,Serial_Rxdata,2); //在OLED上显示
Serial_Rxflag = 1; //设置标志位为1说明接收到数据
USART_ClearITPendingBit(USART1,USART_IT_RXNE); //清除RXNE位
}
}
这里我是将串口及中断初始化和中断函数代码封装在了Serial.c文件中,所以要设置 Serial_GetRxdata和Serial_GetRxflag给外界提供接口,才能让主函数获得信息和数据。
主函数
uint8_t Rdata;
int main(void)
{
led_init();
OLED_Init();
OLED_ShowString(1,1,"Data:");
Serial_Init(); //串口初始化
while(1)
{
if(Serial_GetRxFlag()==1)
{
Rdata = Serial_GetRxdata(); //接受数据
if(Rdata!=0)
strled(Rdata); //数据是非0则点亮对应数量的LED灯
else
streamled(); //数据是0则输出流水灯
}
}
}
点亮LED灯的代码比较简单,这里就不再赘述了。
文章最后
文章最后我展示一下完整的python代码(我觉得自己写的有点烂,因为自己不常用python,对python不是特别熟悉,希望大佬们能给点改进的建议)
import cv2
import mediapipe as mp
import warnings
import serial
import math
ser = serial.Serial('COM7', 9600, bytesize=8,parity=serial.PARITY_NONE,stopbits=1,timeout=None) #打开串口
if ser.isOpen(): #判断串口是否打开成功
print("Serial port opened successfully")
print(ser.name)
else:
print("Error in opening serial port")
warnings.filterwarnings("ignore")
hand_mod = mp.solutions.hands #导入手部模型
my_hands = hand_mod.Hands() #创建手部实例
my_drawing = mp.solutions.drawing_utils #导入绘图工具
my_camera = cv2.VideoCapture(0) #打开摄像头
def is_straight(point_1,point_2,point_3,point_4): #判断四个点是否为直线
x1 = int(point_1[0]) #x1,y1为第一个点的坐标
y1 = int(point_1[1])
x2 = int(point_2[0]) #x2,y2为第二个点的坐标
y2 = int(point_2[1])
x3 = int(point_3[0]) #x3,y3为第三个点的坐标
y3 = int(point_3[1])
x4 = int(point_4[0]) #x4,y4为第四个点的坐标
y4 = int(point_4[1])
degree1 = math.degrees(math.atan2(y2-y1,x2-x1)) #计算夹角
degree2 = math.degrees(math.atan2(y4-y3,x4-x3)) #计算夹角
if abs(degree1-degree2) > 20: #误差大于20度则视为弯曲
is_str = 0
else:
is_str = 1
return is_str
def idt_finger(is_straight_list): #判断手势,返回数字
if is_straight_list[0]==0 and is_straight_list[1]==1 and is_straight_list[2]==0 and is_straight_list[3]==0 and is_straight_list[4]==0:
return '1'
elif is_straight_list[0]==0 and is_straight_list[1]==1 and is_straight_list[2]==1 and is_straight_list[3]==0 and is_straight_list[4]==0:
return '2'
elif is_straight_list[0]==0 and is_straight_list[1]==1 and is_straight_list[2]==1 and is_straight_list[3]==1 and is_straight_list[4]==0:
return '3'
elif is_straight_list[0]==0 and is_straight_list[1]==1 and is_straight_list[2]==1 and is_straight_list[3]==1 and is_straight_list[4]==1:
return '4'
elif is_straight_list[0]==1 and is_straight_list[1]==1 and is_straight_list[2]==1 and is_straight_list[3]==1 and is_straight_list[4]==1:
return '5'
elif is_straight_list[0]==1 and is_straight_list[1]==0 and is_straight_list[2]==0 and is_straight_list[3]==0 and is_straight_list[4]==0:
return '6'
elif is_straight_list[0]==1 and is_straight_list[1]==1 and is_straight_list[2]==0 and is_straight_list[3]==0 and is_straight_list[4]==0:
return '8'
else :
return '0'
def detect_hand_num(img):
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) #转换为RGB格式
results = my_hands.process(img_rgb) #手部识别
if results.multi_hand_landmarks: #识别到手
for land_marks in results.multi_hand_landmarks: #遍历每一只手的标记点
my_drawing.draw_landmarks(img, land_marks, hand_mod.HAND_CONNECTIONS) #绘制手部标记点
mlist = [] #标记点列表,用于判断手势,列表元素为(x,y)坐标,共20个
for point in land_marks.landmark: #遍历每一个标记点
x = int(point.x * img.shape[1]) #获取标记点x坐标
y = int(point.y * img.shape[0]) #获取标记点y坐标
cv2.circle(img, (x, y), 5, (255, 0, 0), cv2.FILLED) #绘制关节圆点,半径为5
if point!= land_marks.landmark[0]: #除去第一点,因为它是手掌中心点,不代表手指
mlist.append((x,y)) #添加到列表中
is_straight_list = [] #用于储存判断每一组四个点是否为直线的结果的列表
for i in range(0,20): #判断每一组四个点是否为直线
if i%4==0: #每四个点为一组
is_straight_list.append(is_straight(mlist[i],mlist[i+1],mlist[i+2],mlist[i+3])) #判断是否为直线
num = idt_finger(is_straight_list) #判断手势,返回数字
my_num = int(num) #转换为整数
ser.write(my_num.to_bytes(1, byteorder='big')) #发送数据到串口
print("num:",num) #打印数字
cv2.putText(img, num, (10, 100), cv2.FONT_HERSHEY_PLAIN, 3, (255, 0, 255), 3)#在窗口显示手势数字
while True:
ret, frame = my_camera.read()#读取摄像头数据 ret:返回值 frame:图像数据
if ret: #ret为True表示读取成功
img = cv2.flip(frame, 1) #镜像显示
detect_hand_num(img) #手部识别
cv2.imshow("My Camera", img) #显示图像
k = cv2.waitKey(1) #等待按键输入
if k == ord('q'): #按q键退出程序
break
my_camera.release() #释放摄像头
cv2.destroyAllWindows() #关闭所有窗口