某滑塊驗證碼識別思路(附完整程式碼)

Python成长路發表於2024-12-10

思路

驗證碼型別如下:

大概搜尋了下,有兩種主流思路:yolo目標檢測演算法和opencv模版匹配。很明顯第二種成本遠小於第一種,也不需要訓練。

而且這種驗證碼有干擾(兩個目標點),yolo一次還不能直接到位,還得進一步處理。我在搜尋的時候還有用輪廓匹配做識別的,但是實測下來準確率很低,這裡就不說了。

識別

背景預處理

先對圖片做一些預處理,移除多餘的干擾項, 提高準確率。比如先簡單將圖片切割一下,只保留包含滑塊的那一部分。這麼說可能不太理解,不識別之前怎麼知道哪一部分包含滑塊?我截圖示一下大家就明白了,可以只保留中間這一塊。

網頁知道滑塊放置的位置,說明伺服器告訴了它準確的y座標,看了下介面返回的結果裡有一個tip_y應該是跟滑塊放置的y座標有關。滑塊的具體位置可以在元素一欄裡看到(em這個單位和px換算規則是 px = em * 字型大小,從網頁上看字型大小是100px)

但tip_y的值是69,和85px對不上。這裡可以打上屬性修改斷點,看一下屬性是怎麼生成的,但我找了半天沒找到,最後複製多個值發給gpt讓他說一下有什麼規律。它說比例是固定的,也就是tip_y乘以1.23就是放置的y座標

當然還有個簡單的方法就是用瀏覽器獲取滑塊的座標,這樣就不用關心兩個值有啥規律。

那就可以得到裁剪的位置了:

from PIL import Image


def crop_main_loc(background_path:Image, slide_path:Image, tip_y:int):
    background_img = Image.open(background_path)
    slide_img = Image.open(slide_path)
    top_y_212 = tip_y * (85 / 69)
    top_y_344 = int(top_y_212 * (344 / 212))
    crop_size = (0, top_y_344, background_img.width, top_y_344+slide_img.height)
    cropped_image = background_img.crop(crop_size)
    cropped_image.show()


if __name__ == '__main__':
    crop_main_loc('background.jpeg', 'slide.png', 69)

滑塊預處理

先提取一下滑塊的輪廓,抖音的滑塊特徵很明顯,可以不用用cv2.Canny來提取邊緣特徵。

具體步驟如下:

  1. 去除外圍透明畫素點(滑塊外層的畫素點的a值都是0)
  2. 將圖片轉成灰度圖並進行二值化操作(0和255)
  3. 只保留二值化為255的畫素點
  4. 去除多餘噪聲

程式碼

讀取rgba格式的滑塊

import cv2
input_img = cv2.imread("slide.png", cv2.IMREAD_UNCHANGED)

將透明值為0的畫素點設定為純黑色

# 取透明維度的值
alpha_channel = input_img[:, :, 3]
# 只使用rgb三個維度的值
rgb_image = input_img[:, :, :3]
rgb_image[alpha_channel == 0] = [0, 0, 0] 

提取白色邊緣並設定成黑色,將其他畫素點設定為白色

gray = cv2.cvtColor(rgb_image, cv2.COLOR_BGR2GRAY)
_, thresholded = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY)
white_img = np.ones_like(rgb_image) * 255
white_img[thresholded == 255] = [0, 0, 0]  

去除噪聲(判斷某個黑色畫素點周圍3x3範圍內有多少個黑色畫素點,少於閾值認為是噪聲)

def count_black_neighbors_by_cv2(gray_image):
    if gray_image.ndim == 3:
        gray_image = cv2.cvtColor(gray_image, cv2.COLOR_BGR2GRAY)
    _, binary_image = cv2.threshold(gray_image, 240, 255, cv2.THRESH_BINARY_INV)
    binary_image = binary_image // 255  
    kernel = np.ones((3, 3), dtype=np.uint8)
    kernel[1, 1] = 0 
    black_neighbors = cv2.filter2D(binary_image, -1, kernel)
    # 設定邊緣為0
    black_neighbors[:, 0] = 0
    black_neighbors[:, 109] = 0
    return black_neighbors

當然也可以透過遍歷來實現,這樣更容易理解點

def count_black_neighbors_by_range(gray_image):
    # 將影像轉換為灰度圖
    if len(gray_image.shape) == 3:
        gray_image = cv2.cvtColor(gray_image, cv2.COLOR_BGR2GRAY)
    # 二值化影像
    _, binary_image = cv2.threshold(gray_image, 240, 255, cv2.THRESH_BINARY_INV)
    binary_image = binary_image // 255 
    # 建立一個與輸入影像大小相同的全零陣列
    black_neighbors = np.zeros_like(binary_image)

    # 遍歷影像中的3x3鄰域,計算每個畫素
    neighbor_offsets = [(-1, -1), (-1, 0), (-1, 1),
                        (0, -1),          (0, 1),
                        (1, -1), (1, 0), (1, 1)]

    # 遍歷每個畫素
    rows, cols = binary_image.shape
    for row in range(1, rows - 1):
        for col in range(1, cols - 1):
            # 當它本身不是黑色畫素點的時候,就不計算
            if binary_image[row, col] != 1:
                continue
            count = 0
            for offset in neighbor_offsets:
                neighbor_row, neighbor_col = row + offset[0], col + offset[1]
                if binary_image[neighbor_row, neighbor_col] == 1:
                    count += 1
            black_neighbors[row, col] = count

    return black_neighbors
    
black_neighbors = count_black_neighbors_by_range(white_img)
output = np.ones_like(rgb_image) * 255
output[black_neighbors > 4] = 0

正題

好了,現在可以把上面看到的內容忘掉了,因為在實際識別的時候用不到(我發現不做處理比做處理識別的準確率要高很多),直接識別準確率甚至接近百分百了。

至於為啥還寫上面的內容,主要是我花時間研究了,總要寫出來,萬一下次用到又忘了呢。還有就是湊個字數。

完整程式碼

下面是識別的完整程式碼

import os
import cv2


def get_slide_distance(bg_path, slide_path):
    '''
    識別滑塊具體位置,返回位置比例: 位置/圖片寬度
    使用的時候再乘以實際圖片寬度即可
    '''
    bg_img = cv2.imread(bg_path)
    sd_img = cv2.imread(slide_path)
    bg_gray = cv2.cvtColor(bg_img, cv2.COLOR_BGR2GRAY)
    bg_gray = cv2.GaussianBlur(bg_gray, (5, 5), 0)
    bg_edge = cv2.Canny(bg_gray, 30, 100)
    rgb_bg_gray = cv2.cvtColor(bg_edge, cv2.COLOR_GRAY2RGB)
    
    sd_gray = cv2.cvtColor(sd_img, cv2.COLOR_BGR2GRAY)
    sd_gray = cv2.GaussianBlur(sd_gray, (5, 5), 0)
    sd_edge = cv2.Canny(sd_gray, 30, 100)
    rgb_sd_gray = cv2.cvtColor(sd_edge, cv2.COLOR_GRAY2RGB)
    result = cv2.matchTemplate(rgb_bg_gray, rgb_sd_gray, cv2.TM_CCORR_NORMED)
    _, _, _, max_loc = cv2.minMaxLoc(result)
    cv2.rectangle(bg_img, (max_loc[0], max_loc[1]), (max_loc[0]+110, max_loc[1] + 110),
        (0, 255, 0), 2)
    result_path = os.path.join(os.path.dirname(bg_path), "result.png")
    cv2.imwrite(result_path, bg_img)
    return max_loc[0]/bg_gray.shape[1]

cv2.matchTemplate

核心函式就是cv2.matchTemplate,它是用來做模版匹配的,通俗點說是在一個圖中找出另一張圖,看一下gpt的引數解釋:

不知道哪個引數更好,可以都測試一下。我看網上用的都是cv2.TM_CCORR_NORMED,效果如下:

TM_CCORR_NORMED

測試下來後面四個效果都不錯,只有cv2.TM_SQDIFFcv2.TM_SQDIFF_NORMED效果很差:

``

流程圖

為了更清晰的知道這段程式碼做了什麼,可以將中間步驟處理過程都儲存下來:

cv2.cvtColor(cv2.COLOR_BGR2GRAY是將bgr格式的圖片轉為灰度圖):

cv2.GaussianBlur(高斯濾波做模糊處理):

cv2.Canny(邊緣檢測,引數可以自己調節看看,第一個是最小值,第二個是最大值,如果值給的太高保留下來的線就很少):

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章