Python跳一跳:使用Cython加速opencv像素级访问

简要概述

网上已经有很多Python实现的跳一跳辅助程序,有基于模版匹配的,还有基于深度学习端到端的方法,都很厉害。但是没有一种算法和我自己想的一样:寻找一行上与背景不一样的像素,找出其最值,当最值连续不变化三次时,即认为找到了中心点的y坐标,而x坐标选择第一行存在于背景色不一致的像素x值得平均值。 所以自己写代码把想法实现了出来。
主要的算法如下:
1 使用模版匹配寻找棋子的位置
2 根据棋子坐标截取篮框部分用以识别吓一跳的中心坐标
3 用Cython实现的子程序识别篮筐部分的中心:x坐标为第一行存在于背景色不一致的像素x值得平均值, y坐标为连续三次与背景色颜色不一致像素x坐标的最值不产生变化时的y值;在寻找中心时,兼顾寻找RGB=(245, 245, 245)的像素区域中心,用以纠正识别误差;如图中Out所示。
4 最后根据识别的棋子和块中心计算出像素距离,计算跳跃时间;跳跃时间先是使用简单的线性模型,然后不断地记录调到正中心的距离和时间,最后使用KNN算法给出下一跳的时间。

识别过程示意

Code

首先是Cython写的像素级访问函数,文件名为fastLocation.pyx,注意后缀是.pyx而非py

import numpy as np
cimport numpy as np
cimport cython

DTYPE = np.uint8
ctypedef np.uint8_t DTYPE_t


cdef unsigned char absSub(unsigned char v1, unsigned char v2):
    return v1-v2 if v1>v2 else v2-v1

@cython.boundscheck(False)
@cython.wraparound(False)
def chessDetect(np.ndarray[DTYPE_t, ndim=3] image):
    cdef int height, width, i, j, xmin, xmax, prexmin=0, prexmax=0, x, y, rcount=0, lcount=0, xcount = 0, xsum=0,whitex=0, whitey = 0, whitecount=0, ai, aj
    cdef bint Foundx=False, Foundxmin
    cdef unsigned int diff
    height = image.shape[0]
    width = image.shape[1]
    cdef np.ndarray[DTYPE_t, ndim=2] out = np.zeros([height, width], dtype=DTYPE)
    cdef np.ndarray[DTYPE_t, ndim=1] backgroundColor, t
    backgroundColor = image[0, 0]
    for i in range(height):
        xmin = 0
        xmax = 0
        Foundxmin = False
        for j in range(1, width):
            t = image[i, j]
            if t[0] == 245 and t[1] == 245 and t[2] == 245:
                whitex += j
                whitey += i
                whitecount += 1
            diff = absSub(t[0], backgroundColor[0]) + absSub(t[1], backgroundColor[1]) + absSub(t[2], backgroundColor[2])
            if diff > 30:
                out[i, j] = 255
                if not Foundx:
                    xsum += j
                    xcount += 1
                if not Foundxmin:
                    xmin = j
                    Foundxmin = True
                xmax = j
        if xcount != 0:
            x = xsum // xcount
            Foundx = True
        if (xmin == prexmin or xmax == prexmax) and Foundx and (xmax-x>50 or x-xmin>50):
            # print(xmax, xmin, xmax-xmin) 
            if xmin == prexmin and xmax == prexmax:
                lcount += 1
            if xmax == prexmax:
                rcount += 1
        if lcount >= 2 or rcount >= 6: 
            y = i
            break
        prexmin = xmin
        prexmax = xmax
    for ai in range(i, min(height, i+20)):
        for aj in range(1, width):
            t = image[ai, aj]
            if t[0] == 245 and t[1] == 245 and t[2] == 245:
                whitex += aj
                whitey += ai
                whitecount += 1
            diff = absSub(t[0], backgroundColor[0]) + absSub(t[1], backgroundColor[1]) + absSub(t[2], backgroundColor[2])
            if diff > 30:
                out[ai, aj] = 255
    if whitecount != 0:
        # print("Here", whitex, whitey, whitecount)
        whitex = int(whitex/whitecount)
        whitey = int(whitey/whitecount)
    return out, x, y, whitex, whitey

关于如何使用Python与numpy交互,请参阅Cython文档。然后再同目录下建立setup.py

from distutils.core import setup, Extension
from Cython.Build import cythonize
import numpy

setup(ext_modules=cythonize("fastGetLocation.pyx"),  include_dirs=[numpy.get_include()])

然后,在命令行使用

python setup.py build_ext --inplace

编译Cython生成对应的C代码和可以被Python调用的库,这样Cython的像素级访问就完成啦。简单对比一下性能(基于Intel core i7 3630QM),直接使用Python访问numpy进行处理需要8秒;使用Cython之后只需要400ms,提速约20倍;使用C++版本的OpenCV实现,处理一张图像仅需20ms。由此可见,还是C++的速度更快更好。
下面进入主题部分:

# encoding=utf-8
import cv2
import numpy as np
import pandas as pd
from sklearn.neighbors import KNeighborsRegressor
from fastGetLocation import chessDetect
import time
import os
import glob

class AutoJumper():

    def __init__(self):
        self.player = cv2.imread("player.jpg")
        if self.player is None:
            print("player.jpg lost, exiting...")
            exit(0)
        self.player_height, self.player_width, _ = self.player.shape
        self.screen_width, self.screen_height = 1080, 1920
        self.player_bias = 40 # 减去棋子宽度,缩小检测范围
        self.BEGIN_Y = 540 # 检索开始行
        self.delayTime = 1000
        self.debug = False
        self.paths = glob.glob(".\\backup\*.png")

        cv2.namedWindow("Auto_Jump^_^", 0)

        self.count, self.predistance, self.pretime = 0, 0, 0

        data = pd.read_csv("data.csv")
        print("%d pre data loaded!" % len(data))
        if len(data) > 500:
            data = data[len(data)-500:len(data)]
        reg_X = data['distance'].values.reshape(-1, 1)
        reg_y = data['time']
        self.knnreg = KNeighborsRegressor(n_neighbors=2).fit(reg_X, reg_y)

        # Running Parameter
        self.player_x, self.player_y = 0, 0
        self.chess_x, self.chess_y = 0, 0
        self.count = 0
        self.predistance, self.pretime = 0, 0
        self.currdistance, self.currtime = 0, 0
        self.jumpRight = False #恰好调到中央Flag

    def get_screenshot(self, id):
        os.system('adb shell screencap -p /sdcard/%s.png' % str(id))
        os.system('adb pull /sdcard/%s.png .' % str(id))


    def makeJump(self):
        press_x = int(320 + np.random.randint(20))    
        press_y = int(410 + np.random.randint(20))
        cmd = 'adb shell input swipe %d %d %d %d ' % (press_x, press_y, press_x, press_y) + str(self.currtime)
        os.system(cmd)

    def detectPlayer(self):
        res1 = cv2.matchTemplate(self.image, self.player, cv2.TM_CCOEFF_NORMED)
        min_val1, max_val1, min_loc1, max_loc1 = cv2.minMaxLoc(res1)
        top_left = max_loc1
        bottom_right = (top_left[0] + self.player_width//2, top_left[1] + self.player_height) 
        cv2.circle(self.image, bottom_right, 10, 255, 10)
        self.player_x, self.player_y = bottom_right

    def detectChess(self):
        if self.player_x >= self.screen_width/2:
            startx, endx, starty, endy = 0, max(self.player_x-self.player_bias, 10), self.BEGIN_Y, self.player_y
        else:
            startx, endx, starty, endy = self.player_x+self.player_bias, self.screen_width, self.BEGIN_Y, self.player_y
        out, x, y, whitex, whitey = chessDetect(self.image[starty:endy, startx:endx])
        cv2.rectangle(self.image, (startx, starty), (endx, endy), 255, 10)
        cv2.circle(self.image, (whitex+startx, whitey+starty), 20, (0, 255, 0), 10)
        cv2.circle(self.image, (x+startx, y+starty), 10, (0, 0, 255), 10)
        # if self.count % 5 != 0:
        #     y = self.player_y - abs(x-self.player_x)*1.732/3
        if abs(x-whitex) + abs(y-whitey) < 30:
            x = whitex
            y = whitey
            self.jumpRight = True
        self.chess_x, self.chess_y = x+startx, y+starty

    def calDistanceAndTime(self):
        self.currdistance = np.sqrt((self.chess_x-self.player_x)**2+(self.chess_y-self.player_y)**2)
        self.currtime = int(self.knnreg.predict(self.currdistance))

    def showImage(self):
        cv2.imshow("Auto_Jump^_^", self.image)
        if cv2.waitKey(self.delayTime) & 0xFF == 27:
            print("Ese key pressed, exiting")
            exit(0)
    def parameterUpdate(self):
        self.count += 1
        self.predistance, self.pretime = self.currdistance, self.currtime
        if self.jumpRight:
            f = open("data.csv", 'a')
            print("Writing log: (%f, %d)" % (self.predistance, self.pretime))
            f.write("%f,%d\n" % (self.predistance, self.pretime))
            f.close()
        self.jumpRight = False

    def jump(self):
        t = time.time()
        self.get_screenshot(0)
        if self.debug:
            self.image = cv2.imread(self.paths[self.count])
            self.delayTime = 0
        else:
            self.image = cv2.imread("0.png")
        self.detectPlayer()
        self.detectChess()
        self.calDistanceAndTime()
        self.makeJump()
        self.showImage()
        self.parameterUpdate()
        print("\nStep %d:" % self.count, time.time()-t)

if __name__ == '__main__':
    jumper = AutoJumper()
    while True:
        jumper.jump()

主体部分的代码和其他作者的代码大同小异,所以没怎么写注释。这里使用了KNN算法去计算距离,并且在收集数据较多时,只取后500项数据进行训练,理论上具有一定的自学习能力。

距离时间模型

根据我自己手机的数据(小米Note标准版),绘制成一下时间距离图像,横轴为像素距离,纵轴为跳跃时间。


距离-时间图像

从图中可以看出,距离时间大体上呈线性关系,但是在两端具有截面效应,而且由于高距离的样本偏少,会导致距离较远时跳跃时间样本不足,从而导致并不能一直跳在中心。

不足

其实我一直想实现能够一直调到正中心的算法,但是后来发现这个目标比较难。此算法目前达到的最高分是1538分。

最高分

每跳得分大致在6分左右(衡量不同算法的优劣的指标之一),与我理想中的32还相差甚远。识别正方体时还是比较准确的,但是对于圆筒就有差距了,虽然已经做了差异化处理,但是还是不够准确;另外一点就是距离时间的映射模型还有待提升。我想,实现这个算法最大的收获便是学习了Cython的使用吧。这也让我觉得,Python+C的技能储备应该是比较好的,这也会是我之后的技能发展方向。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容