目录
1 背景介绍
在上一篇博客中,我们已经介绍了如何单人驱动火柴人。详情可见(1条消息) mediapipe单人动捕驱动unity“火柴人”_痴生的博客-优快云博客
2 技术介绍
YOLOv5(You Only Look Once version 5)是一种目标检测模型,用于在视频或图像中识别和定位物体。它在YOLOv4的基础上进行了改进。
YOLOv5是一种端到端的深度学习模型,可以直接从原始图像中检测和定位目标。它使用卷积神经网络(CNN)来学习图像中物体的特征,并使用多尺度预测和网格分割来检测和定位目标。YOLOv5的优势在于它可以在高速运行,并且可以在不同的图像分辨率上很好地工作。
总的来说,YOLOv5是一种高效的目标检测模型,可以应用于许多不同的场景,包括自动驾驶,机器人感知,图像分析等。其具有合适于移动端部署,模型小,速度快的特点。
YOLOv5有YOLOv5s。YOLOv5m,YOLOV51、YOLOV5x四个版本,文件中,这几个模型的结构基本一样,不同的是depth multiple模型深度和width multioler模型宽度这两个参数。就和我们买衣服的尺码大小排席一样,YOLOV5S网络是YOLOV5系列中深度最小,特征图的宽度最小的网络。其他的三种都是在此基础上不断加深,不断加宽。
3 思路介绍
人体姿态估计(Human Pose Estimation)也称为人体关键点检测(Human Keypoints Detection)。对于人体姿态估计的研究,主要有两种思路:Top-down 和Bottom-up。
Top-down首先利用目标检测算法检测出单个人,得到边界框,然后在每一个边界框中检测人体关键点,连接成一个人形,缺点就是受检测框的影响太大,漏检,误检,IOU大小等都会对结果有影响,算法包括RMPE、Mask-RCNN 等,这种方法一般具有较高的准确率但是处理速度较低。
Bottom-up方法首先对整个图片进行每个人体关键点部件的检测,再将检测到的部件拼接成一个人形,这种方法一般准确率较差,会将不同人的不同部位按一个人进行拼接,但处理速度较快,代表方法就是OpenPose、DeepCut 、PAF。
在本项目中,我使用Top-down思路原因有三,如下所示:
- 我在上一节中已经实现了单人驱动“火柴人”,使用Top-down思路符合我之前所做工作的脉络并且省时省力。
- 对于体感游戏来说,准确性尤为重要,在我看来,即使损失部分性能也要确保识别动作的准确性。因此,准确性更高的Top-down思路是我的首选。
- 我选择通过一个端口传输一个人体姿态识别的数据,使用Bottom-up方法会增加将一个人的坐标信息拼凑起来再通过端口发送的工作,使工作变得更加复杂。
4 代码实现
4.1 环境
python3.9 安装mediapipe和opencv-python包
python和Unity通信使用socket
Unity2021.3
4.2 python代码
import os
import cv2
import mediapipe as mp
from mediapipe.python.solutions import pose as mp_pose
import socket
# PyTorch Hub
import torch
import cv2
import mediapipe as mp
import torch.nn as nn
import time
import onnxruntime as ort
class poseDetector():
def __init__(self, mode=False, upBody=False, smooth=True, detectionCon=0.5, trackCon=0.5):
self.mode = mode
self.upBody = upBody
self.smooth = smooth
self.detectionCon = detectionCon
self.trackCon = trackCon
self.mpDraw = mp.solutions.drawing_utils
self.mpPose = mp.solutions.pose
self.pose = self.mpPose.Pose(self.mode, self.upBody, self.smooth, False, True, # 这里的False 和True为默认
self.detectionCon, self.trackCon) # pose对象 1、是否检测静态图片,2、姿态模型的复杂度,3、结果看起来平滑(用于video有效),4、是否分割,5、减少抖动,6、检测阈值,7、跟踪阈值
'''
STATIC_IMAGE_MODE:如果设置为 false,该解决方案会将输入图像视为视频流。它将尝试在第一张图像中检测最突出的人,并在成功检测后进一步定位姿势地标。在随后的图像中,它只是简单地跟踪那些地标,而不会调用另一个检测,直到它失去跟踪,以减少计算和延迟。如果设置为 true,则人员检测会运行每个输入图像,非常适合处理一批静态的、可能不相关的图像。默认为false。
MODEL_COMPLEXITY:姿势地标模型的复杂度:0、1 或 2。地标准确度和推理延迟通常随着模型复杂度的增加而增加。默认为 1。
SMOOTH_LANDMARKS:如果设置为true,解决方案过滤不同的输入图像上的姿势地标以减少抖动,但如果static_image_mode也设置为true则忽略。默认为true。
UPPER_BODY_ONLY:是要追踪33个地标的全部姿势地标还是只有25个上半身的姿势地标。
ENABLE_SEGMENTATION:如果设置为 true,除了姿势地标之外,该解决方案还会生成分割掩码。默认为false。
SMOOTH_SEGMENTATION:如果设置为true,解决方案过滤不同的输入图像上的分割掩码以减少抖动,但如果 enable_segmentation设置为false或者static_image_mode设置为true则忽略。默认为true。
MIN_DETECTION_CONFIDENCE:来自人员检测模型的最小置信值 ([0.0, 1.0]),用于将检测视为成功。默认为 0.5。
MIN_TRACKING_CONFIDENCE:来自地标跟踪模型的最小置信值 ([0.0, 1.0]),用于将被视为成功跟踪的姿势地标,否则将在下一个输入图像上自动调用人物检测。将其设置为更高的值可以提高解决方案的稳健性,但代价是更高的延迟。如果 static_image_mode 为 true,则忽略,人员检测在每个图像上运行。默认为 0.5。
'''
def findPose(self, img, draw=True):
imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 将BGR格式转换成灰度图片
self.results = self.pose.process(imgRGB) # 处理 RGB 图像并返回检测到的最突出人物的姿势特征点。
if self.results.pose_landmarks:
if draw:
self.mpDraw.draw_landmarks(img, self.results.pose_landmarks, self.mpPose.POSE_CONNECTIONS)
# results.pose_landmarks画点 mpPose.POSE_CONNECTIONS连线
return img
def findPosition(self, img, draw = True):
#print(results.pose_landmarks)
lmList = []
if self.results.pose_landmarks:
for id, lm in enumerate(self.results.pose_landmarks.landmark): # enumerate()函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标
h, w, c = img.shape # 返回图片的(高,宽,位深)
cx, cy, cz = int(lm.x * w), int(lm.y * h), int(lm.z * w) # lm.x lm.y是比例 乘上总长度就是像素点位置
lmList.append([id, cx, cy, cz])
if draw:
cv2.circle(img, (cx, cy), 5, (255, 0, 0), cv2.FILLED) # 画蓝色圆圈
return lmList
detector = poseDetector()
lmLists = []
strdata = ""
y0min=100000
# Model
# 加载模型文件
#yolo_model = torch.hub.load('ultralytics/yolov5', 'yolov5s')
# 加载轻量级的模型文件
yolo_model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True)
# 检查是否有可用的GPU设备
if torch.cuda.is_available():
# 将yolo_model加载到GPU设备上
yolo_model = yolo_model.to('cuda')
else:
print("GPU device not found. Using CPU instead.")
# since we are only interested in detecting a person
yolo_model.classes = [0]
mp_drawing = mp.solutions.drawing_utils
# mp_pose = mp.solutions.pose detector.findPose
# cap = cv2.VideoCapture(1) # 使用默认摄像头设备
cap = cv2.VideoCapture('q3.mp4') # 视频
# 获取摄像头的尺寸
ret, frame = cap.read()
h, w, _ = frame.shape
size = (w, h)
# 创建用于保存视频的 VideoWriter 对象
out = cv2.VideoWriter("output.avi", cv2.VideoWriter_fourcc(*"MJPG"), 20, size)
num_people = 0 # 用于记录检测到的人数
# 构建两个实例,分别用于连接不同的监听端口
client1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client1.connect(('127.0.0.1', 9999))
client2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client2.connect(('127.0.0.1', 8888))
clients = {} # 使用字典存储多个客户端套接字对象
clients[0] = client1
clients[1] = client2
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# Recolor Feed from BGR to RGB
image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# making image writeable to False improves prediction
image.flags.writeable = False
result = yolo_model(image)
# Recolor image back to BGR for rendering
image.flags.writeable = True
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
# This array will contain crops of images in case we need it
img_list = []
# we need some extra margin bounding box for human crops to be properly detected
MARGIN = 10
num_people = 0 # 重置人数为0
for (xmin, ymin, xmax, ymax, confidence, clas) in result.xyxy[0].tolist():
with mp_pose.Pose(min_detection_confidence=0.3, min_tracking_confidence=0.3) as pose:
# Media pose prediction
cropped_image = image[int(ymin) + MARGIN:int(ymax) + MARGIN, int(xmin) + MARGIN:int(xmax) + MARGIN]
cropped_image = detector.findPose(cropped_image)
lmLists.append(detector.findPosition(cropped_image))
img_list.append(cropped_image)
num_people += 1 # 每检测到一个人,人数加1
#将关键点坐标映射回原始帧
if lmLists[num_people-1] is not None:
for az, lm in enumerate(lmLists[num_people-1]):
id, cx, cy, cz = lm # 解包关键点信息
if cy<y0min:
y0min=cy
if lmLists[num_people-1] is not None:
for i, lm in enumerate(lmLists[num_people-1]):
id, cx, cy, cz = lm # 解包关键点信息
cx = cx
cy = cy
cz = cz
# 更新lmLists中的关键点坐标
lmLists[num_people - 1][i] = [id, cx, cy, cz]
# Display the resulting image with person count
cv2.putText(image, f'People: {num_people}', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
cv2.imshow('Activity Recognition', image)
# Write the frame to video file
out.write(image)
if num_people == 2:
# 执行你的操作,例如打印一条消息
print("There are 2 people in the video.")
i=0
# 将每个人的坐标点信息发送到不同的端口上
for i in range(num_people):
if len(lmLists[i]) != 0:
for data in lmLists[i]:
print(data) # print(lmList[n]) 可以打印第n个
for a in range(1, 4):
if a == 2:
strdata = strdata + str(frame.shape[0] - data[a]) + ','
else:
strdata = strdata + str(data[a]) + ','
print(str(clients[i]) + ':' + strdata)
clients[i].send(strdata.encode('utf-8'))
strdata = ""
lmLists = []
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
out.release()
cv2.destroyAllWindows()
4.3 unity代码
在unity中,常常会因为出现主线程堵塞的问题而导致游戏画面停滞、响应停滞、帧率下降、内存泄漏等问题。这是因为主线程负责处理游戏的逻辑更新、渲染和用户输入响应等任务,如果主线程被堵塞,那么这些任务无法及时完成,导致游戏画面无法更新,玩家的输入无法响应,游戏逻辑无法进行。总的来说,在Unity中,主线程堵塞会导致游戏的停顿或卡顿现象,给玩家带来不流畅的游戏体验。
因此,在项目中,为了避免主进程堵塞,对于接收两个端口信息的server与server1文件,我在两个代码文件中,都使用了多线程来处理客户端的连接和数据接收,以避免主线程的阻塞。
在Server类中:
- listenThread是一个线程,通过调用ListenClientConnect方法来监听客户端的连接请求。
- ReceiveMessage方法也是在一个独立的线程中运行,用于接收客户端发送的数据。
这样,服务器可以在主线程中处理接收到的数据,并在更新物体位置之前等待数据的到达,而不会阻塞整个程序的执行。
4.4 游戏其他可玩性设定
因为我想做一款简易格斗游戏,我在火柴人的手部特征点上添加触发器,编写触发器脚本,打到敌方目标得分加一。
目前还比较简陋,只是得分页面,后续会将其完善成血条UI。
5 结果展示
6 后续优化
目前来讲,游戏仅仅是跑起来了而已,由于多目标检测的未绑定目标,时常两个火柴人就变成向外(正常格斗游戏都是两人面对面向内而不是背对背向外),这个问题需要解决。
此外还有将火柴人换成一些动漫模型、血条UI界面设置、游戏开始界面设置等工作仍需解决。
7 文件分享
emmm,等我回头有空上传github吧