python+opencv 模板暴力匹配

背景

在做微博自动登陆部分的时候, 发现有的账号需要验证码, 微博使用的是极验验证码,开始尝试用css直接定位过去, 但是人家验证码这么简单也就不叫验证码了. 于是尝试换路子,看到不少人使用的超级鹰平台,但是啊,要钱啊。搞不起搞不起。看了下想法,他们也就是截图定位,于是打算自己搞。对了, 验证码这个样子的:

大图

开始搞

先观察,我发现每个验证码都一样,中间一堆文字,那干脆把那一堆文字截图下来作为识别的特征。


小图

那问题来了,怎么在大图片中找到小图片?简单搜索下,发现,opencv有个函数matchTemplate()叫做模板匹配,直接可以匹配,对了,事先记得装opencv模块

import cv2 as cv

#读取图片,第二个参数可以直接读取成灰度图,省的转换了。
big = cv.imread('big.png', cv.IMREAD_GRAYSCALE) 
small = cv.imread('small.png', cv.IMREAD_GRAYSCALE)

#第三个参数是用相关匹配方法,一共有六种具体看手册
res = cv.matchTemplate(big, small, cv.TM_CCOEFF)

# minMaxLoc 顾名思义,获得最小值最大值位置,返回四个值,分别是矩阵中最小值,最大值,最小值的位置,最大值的位置。
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)

matchTemplate手册在这里

结果识别成了这个样子


res.png

颜色越亮匹配程度越高。看来识别的不行啊。六种近似算法都尝试匹配了,结果都找不到。

自己撸一个吧

直接自己搞一份代码了,取小图片为窗口,逐个对比像素点,超过给定比例的像素相同就认为找到了。
由于文字空白部分比较多, 我们重新选特征图片,选小一点,特别一点的。


然后开始撸代码的就行了。

import cv2 as cv
import sys

def matchTemplate_bypixel(big, small, pctg):
    ''' 
        big是大图片,small是小图片,pctg是匹配百分百
        注意big 和small要传入灰度模式
    '''

    # pctg简单的合法性检查
    if pctg > 100 or pctg < 0:
        print('pctg should be in (0,100)')
        sys.exit()

    # 获取大小图片的宽度和高度
    big_h, big_w = big.shape[:2]
    small_h, small_w = small.shape[:2]
    
    # 设定相同像素的阈值,达到之后停止扫描
    threshold = int(small_h*small_w*pctg/100)
   
    # 获取大图片的比较窗口
    def get_window(left_top_point):
        x,y = left_top_point
        return big[x:x+small_h,y:y+small_w]
  
    # 比较两个窗口矩阵,返回相同像素的个数
    def same_pixel_num(m1, m2):
        if m1.shape != m2.shape:
            print('shape not same')
            print(m1.shape)
            print(m2.shape)
            return 
        return sum(sum(m1-m2==0))
  
    
    for i in range(big_h-small_h-1):
        # 输出行数,方便调试
        if i % 50 == 0:
            print(i)

        # 逐个像素移动窗口进行匹配
        for j in range(big_w-small_w-1):
            same_num = same_pixel_num(get_window((i,j)), small)
            if same_num > threshold:
                print('found match point: (',i,j, ') match value:', same_num)
                cv.rectangle(big, (j,i), (j+small_w,i+small_h), (0,0,255))
                cv.imwrite('aaaaa.png',big)
                print('write success')
                sys.exit()

if __name__ == '__main__':

    big = cv.imread('big.png', cv.IMREAD_GRAYSCALE)
    small = cv.imread('small.png', cv.IMREAD_GRAYSCALE)
    matchTemplate_bypixel(big, small, 20)

匹配到的位置还算可以,虽然有些偏差但是大体上可以找得到位置了,但是有个问题,原图的那个圆圈会随着鼠标转圈圈,所以我截图的部分不是百分百一样,但是就算那个阴影部分变成两倍,把百分百的参数调成50的话应该完全没有问题,但是在测试中发现,参数写成50根本匹配不到,20倒是可以,但是这可逼死强迫症啊。于是我决定找找问题出在哪里。


aaaaa.png

第一波优化

我尝试随便找两个矩阵模拟一下过程,似乎发现了什么

In [92]: m1
Out[92]: 
array([[255, 255, 255],
       [255, 255, 255],
       [255, 255, 255]], dtype=uint8)

In [93]: m2
Out[93]: 
array([[254, 254, 254],
       [254, 254, 254],
       [253, 254, 253]], dtype=uint8)

In [94]: m1-m2
Out[94]: 
array([[1, 1, 1],
       [1, 1, 1],
       [2, 1, 2]], dtype=uint8)

In [95]: m2-m1
Out[95]: 
array([[255, 255, 255],
       [255, 255, 255],
       [254, 255, 254]], dtype=uint8)

其实,这里的m1和m2我取的就是左上角的空白区域,但是,仔细观察发现灰度值有波动啊,这就说明,同样的颜色在转换之后可能会有轻微的改变,于是,我将same_pixel_num函数中的判定条件稍微修改了一下,将m1-m2==0改为m1-m2<3。

    def same_pixel_num(m1, m2):
        if m1.shape != m2.shape:
            print('shape not same')
            print(m1.shape)
            print(m2.shape)
            return 
        return sum(sum(m1-m2<3))

判定条件改了,那就有问题了, 矩阵类型是uint8,无符号!! 所以说当出现1-2这种情况的时候,会发生溢出啊。于是转个类型再减,顺便加个绝对值,于是,这个函数变成了

    def same_pixel_num(m1, m2):
        if m1.shape != m2.shape:
            print('shape not same')
            print(m1.shape)
            print(m2.shape)
            return 
        b1 = m1.astype('int16')
        b2 = m2.astype('int16')
        return sum(sum(abs(b1-b2)))

看看优化之后的效果,已经可以非常准确的找到位置了,所以总体上到这里已经可以识别啦。但是别急,还有问题。


fin.png

再优化

如果真的这样子找下去太慢了吧。识别一次需要耗时十几秒,这还不是全屏的截图,背景图片再大一些恐怕还要慢一点。点个验证码要十几秒,这效率堪比蜗牛。想办法优化咯。想到个办法,这个图形比较特别,他的颜色只有这一部分有,那不妨直接根据灰度值定位过来,反正,周围一片空白。

优化后的的代码:

import cv2 as cv 
import sys
from scipy import stats
import datetime

def printTime(f):
    def wrapper(*args, **kwargs):
        a = datetime.datetime.now()
        f(*args, **kwargs)
        b = datetime.datetime.now()
        print('running time: ', (b-a).seconds)

def matchTemplate_bypixel(big, small, pctg):

    if pctg > 100 or pctg < 0:
        print('pctg should be in (0,100)')
        sys.exit()

    big_h, big_w = big.shape[:2]
    small_h, small_w = small.shape[:2]
    threshold = int(small_h*small_w*pctg/100)
    print('threshold : ', threshold)

    def get_window(left_top_point):
        x,y = left_top_point
        return big[x:x+small_h,y:y+small_w]

    def same_pixel_num(m1, m2):
        if m1.shape != m2.shape:
            print('shape not same')
            print(m1.shape)
            print(m2.shape)
            return 
        b1 = m1.astype('int16')
        b2 = m2.astype('int16')
        return sum(sum(abs(b1-b2)<3))
  
  # 获取开始匹配的点
    def getStartPoint():
        range_h = range(big_h-small_h-1)
        range_w = range(big_w-small_w-1)
        # 利用 scipy的stats模块获得众数,即小图中最多的灰度值
        modenum = stats.mode(small.flatten())[0][0]
        startx = 0
        starty = 0
        for i in range_h:
            for j in range_w:
                if big[i,j] == modenum == big[i-1,j] == big[i+1,j] == big[i,j+1] == big[i,j-1]:
                    startx = i
                    starty = j
                    break
                if startx or starty:
                    break;
        startx = startx-small_w
        if startx < 0:
            satrtx = 0
        starty = starty-small_h
        if starty < 0:
            starty = 0
        return (startx,starty)

    startx,starty = getStartPoint()
    range_h = range(startx,big_h-small_h-1)
    range_w = range(starty,big_w-small_w-1)
    for i in range_h:
        if i % 50 == 0:
            print(i)
        for j in range_w:
            same_num = same_pixel_num(get_window((i,j)), small)
            if same_num > threshold:
                print('found match point: (',i,j, ') match value:', same_num)
                cv.rectangle(big, (j,i), (j+small_w,i+small_h), (0,0,255))
                cv.imwrite('aaaaa.png',big)
                print('write success')
                return

if __name__ == '__main__':

    big = cv.imread('big.png', cv.IMREAD_GRAYSCALE)
    small = cv.imread('small.png', cv.IMREAD_GRAYSCALE)
    a = datetime.datetime.now()
    matchTemplate_bypixel(big, small, 50)
    b = datetime.datetime.now()
    print('running time: ', (b-a).seconds)

测试了下,速度已经提升到了3s


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

推荐阅读更多精彩内容