很久没更新了,这一年来也有做一些东西,但是由于一些个人原因,人比较自闭,所以也不想写啥东西,最近开始干点活,状态好了不少,想着还是写点分享一下,如果能给大家带来点参考价值那最好不过了


由于演示需求,需要完成一个依靠姿态控制来进行游戏操作的游戏,我的初步想法还是依靠CMU原生openpose+Unity来实现,但是由于时间问题以及这边也没有熟悉Unity相关的开发人员,最终还是用了简陋的pygame。

pygamme很难实现一些比较好看的特效和动画,即使是打地鼠或者切西瓜这类游戏,网上pygame的版本也给我一种上世纪的感觉。拼图可以稍微削弱一下pygame在这方面的劣势。

首先是openpose的配置,老样子,优先官方文档和Github Issue,还有问题再google。
CMU-Openpose安装指南比较重要的几点:

  1. 官方指南里的先决条件这玩意一定得认真对着看下来,包括但不限于各种版本对齐,2019community虽然文档里写不被官方支持,但我实际使用是可用的。
  2. 确认一下系统环境内有没有cl.exe,这一问题不在当前版本的前置条件内提到,但是会导致编译报错。打开cmd输入cl查看即可,没有就装一个添加到系统变量
  3. 在openpose3rdpartywindows下找到四个.bat批处理文件分别运行一次以自动下载所需环境,虽然当前版本可以在c-make的时候自动下载,但是因为没有断点续传和完整性检测,中间中断了会很麻烦,建议手动下载
  4. 如果要使用python-api,记得在c-make configure后手动勾选,默认是没有添加python-api的
  5. 以下几篇文章较为详尽,搞不清楚官方文档或者一些细节有问题的时候可以参考:(windows版本
    【OpenPose-Windows】openpose1.4.0+vs2017+CUDA10.1+cuDNN v7.6.1配置教程
    windows编译openpose及在python中调用
    但是在实际使用python-api的时候有个问题,官方提供的python demo都是实现图片谷歌关键点检测的,我在使用视频流进行处理的时候,始终会报错,
    。提到的几个问题挨个排查过去,还是没定位出来,c++版本的视频骨骼关键点检测demo是没有任何问题的,个人感觉是c++编译出的python-api出了一些问题。如果有大佬能赐教,感激不尽。

鉴于此,我想就用纯python来实现了,看到pytorch-openpose提供了视频流处理的demo,遂决定先用pytorch-openpose来实现,跑了一下demo发现,pytorch-openpose很明显没有CMU-Openpose那么庞大的包和支持,代码量也不多,基本上我原生的环境就可以直接跑demo。但是问题也是很明显的,速度很慢,很明显的掉帧,而CMU-openpose能达到人眼感觉连贯的水平。

因为主要关注的是其中关键点识别部分的代码,所以主要关注util.py部分的代码

def draw_bodypose(canvas, candidate, subset):
stickwidth = 4
limbSeq = [[2, 3], [2, 6], [3, 4], [4, 5], [6, 7], [7, 8], [2, 9], [9, 10], \
           [10, 11], [2, 12], [12, 13], [13, 14], [2, 1], [1, 15], [15, 17], \
           [1, 16], [16, 18], [3, 17], [6, 18]]

colors = [[255, 0, 0], [255, 85, 0], [255, 170, 0], [255, 255, 0], [170, 255, 0], [85, 255, 0], [0, 255, 0], \
          [0, 255, 85], [0, 255, 170], [0, 255, 255], [0, 170, 255], [0, 85, 255], [0, 0, 255], [85, 0, 255], \
          [170, 0, 255], [255, 0, 255], [255, 0, 170], [255, 0, 85]]
RElbow,RWrist,LElbow,LWrist,R_vec,L_vec = np.array([]),np.array([]),np.array([]),np.array([]),np.array([]),np.array([])

for i in range(18):
    for n in range(len(subset)):
        index = int(subset[n][i])
        if index == -1:
            continue
        x, y = candidate[index][0:2]
        if i ==3:
            RElbow = np.array([x,y])
            #print(RElbow)
        #else:
            #print("RElbow missed")

        if i ==4:
            RWrist = np.array([x,y])
            #print(RWrist)
        #else:
            #print("RWrist missed")

        if i ==6:
            LElbow = np.array([x,y])
        #    print(LElbow)
        #else:
        #    print("LElbow misssed")
            

        if i ==7:
            LWrist = np.array([x,y])
            #print(LWrist.size)
        #else:
        #    print("LWrist misssed")
        #print(i,int(x),int(y))
        cv2.circle(canvas, (int(x), int(y)), 4, colors[i], thickness=-1)
if RElbow.size > 0 and RWrist.size > 0:
    R_vec = (RWrist - RElbow)
    #print("R_vec",R_vec)
if LElbow.size >0  and LWrist.size > 0:
    L_vec = (LWrist - LElbow)
    #print("L_vec",L_vec)
#rint(R_vec,L_vec)

        

for i in range(17):
    for n in range(len(subset)):
        index = subset[n][np.array(limbSeq[i]) - 1]
        #print(index)
        if -1 in index:
            continue
        cur_canvas = canvas.copy()
        Y = candidate[index.astype(int), 0]
        X = candidate[index.astype(int), 1]
        mX = np.mean(X)
        mY = np.mean(Y)
        #print(i,mX,mY)
        
      
  
        length = ((X[0] - X[1]) ** 2 + (Y[0] - Y[1]) ** 2) ** 0.5
        angle = math.degrees(math.atan2(X[0] - X[1], Y[0] - Y[1]))
        polygon = cv2.ellipse2Poly((int(mY), int(mX)), (int(length / 2), stickwidth), int(angle), 0, 360, 1)
        cv2.fillConvexPoly(cur_canvas, polygon, colors[i])
        canvas = cv2.addWeighted(canvas, 0.4, cur_canvas, 0.6, 0)
#R_vec = RWrist - RElbow
#print(R_vec)
# plt.imsave("preview.jpg", canvas[:, :, [2, 1, 0]])
# plt.imshow(canvas[:, :, [2, 1, 0]])
return canvas,R_vec,L_vec

pytorch-openpose的更新主要就是采用轮询摄像头获取图像,循环使用模型预测骨骼关键点,之后进行关节的预测和连接,实现绘制,参阅github上相关示意图,很容易就明白手腕和肘部对应的参数,获取对应位置的关键点坐标。

由于本身pytorch-openpose代码没有注释,阅读起来还是有一定难度,可以参考pytorch-openpose代码分析

为了实现通过手到肘部的移动,控制拼图移动,仅仅有关键点是不够的。或者说,如果仅通过关键点坐标位移,来实现计算,需要较为庞大的逻辑,由于每个人手肘长度不一,以及存在人走动的时候导致的关键点平移,都会导致漏检或误检等问题。因此我考虑使用前后两帧的向量夹角,来进行判定,逆时针为负,顺时针为正,逻辑大致如下:

        if q:
            Rangle = q.get()
            #time.sleep(1)
            print("queue_out",Rangle)
            if Rangle > -90 and Rangle <-40:
                blank_cell_idx = moveL(game_board, blank_cell_idx, num_cols)
            elif Rangle > 40 and Rangle <90:
                blank_cell_idx = moveR(game_board, blank_cell_idx, num_cols)
            elif Rangle < -120 :
                blank_cell_idx = moveU(game_board, blank_cell_idx, num_rows, num_cols)
            elif Rangle >120:
                blank_cell_idx = moveD(game_board, blank_cell_idx, num_cols)

由于没有摄像头,我使用了v cam进行模拟和验证,可以将视频在v cam中打开,自动播放到摄像头端口。
之后主要就是修改和完善拼图游戏的逻辑和界面,一开始我想通过next持续获取关键点向量参数,之后传入游戏逻辑代码实现控制,但是由于我重写了,但是没有完全重写,获取姿态向量和视频流绘制的代码是耦合的,如果通过这样的方法会导致视频流无法正常播放,因此还是得使用多线程。

因此,将pytorch-openpose中的demo_camera重写,封装成线程类,再将拼图游戏也进行同样处理,应该就可以实现了(理论上

但是现实很残酷,python的多线程是虚假的多线程,详情请参考
Python的GIL是什么鬼,多线程性能究竟如何
那没事了,多线程效率低,开多进程吧。
需要注意的是,多线程中使用的数据交互queue和多进程中的queue是不同的,分别从threading和multiprocessing中导入,改写的时候需要注意,不然会导致winerror:87


大概差不多了,测试
发现两边队列中读写速度不平衡,queue中快要塞满了,游戏还没获得几个控制参数,导致你可能会挥一下手,控制参数直接被压到队列底部,直到队列满了才全吐出来。

简单,我将队列设置为1不就完事了,强制同步。(然而事情并没有这么简单。现在视频流不是单纯的掉帧了,直接成为ppt,而且时不时就直接卡死。很明显,队列阻塞需要进一步的处理,仔细一想,这么简单的游戏循环不可能比模型预测还慢,我大概知道问题在哪了。

pygame.event.get(),原先的控制代码由键盘获取,通过pygame.event.get()获取对应控制参数,想过去也知道,事件获取肯定是有强制间隔的,肯定是这个导致了队列阻塞,于是我直接在游戏控制循环中把这玩意给删了。(但凡我稍微看看event的用法和逻辑,我都不会这么蠢。结果就是,数据全在更新,游戏界面不更新,而且随便一碰就无响应,我还以为是队列和进程出问题了。

后续重新在控制循环外补上事件获取,完成。
大概长这样

后续有空的话,可能在b站或者油管会补一个演示视频(
总的来讲就是糊墙糊出来的小玩意,像个水星杯比赛产物
太久没写代码了,这块也不熟,踩了不少坑
不过可拓展性还挺强的
优化搞搞其他应用感觉还是蛮有意思的,(就是学校里水国创必备产品