tech.sinayaka.com

pythonで文字captcha画像の読み取り

2023-02-15
2025-07-13
7分
1234語
Python botpythonopencv

よくある文字形式のcaptcha画像の読み取りを「OpenCV」と「Tesseract OCR」で試してみました。
今回扱う画像はこちらです。

captcha
captcha

試したい場合はダウンロードしてプログラムと同じディレクトリに置いてください。

事前準備

windowsの場合は事前にTesseractOCRの exeファイルをインストールする必要があります。 初回はJupyterNotebook上で下記コードを実行してライブラリをインストールしておきます。

!pip install opencv-python pyocr tesseract

正常にインストールできていれば下記コードで画像が表示されるはずです。

import cv2
import matplotlib.pyplot as plt

img = cv2.imread('captcha.png')
plt.title('orginal')
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.show()
original
original

2値化

文字認識しやすいように白黒のみの画像に変換します。

img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
plt.title('img_gray')
plt.imshow(cv2.cvtColor(img_gray, cv2.COLOR_BGR2RGB))
plt.show()

ret, img_binary = cv2.threshold(img_gray, 0, 255,  cv2.THRESH_BINARY + cv2.THRESH_TRIANGLE)
plt.title('img_binary THRESH_TRIANGLE')
plt.imshow(cv2.cvtColor(img_binary, cv2.COLOR_BGR2RGB))
plt.show()

ret, img_binary = cv2.threshold(img_gray, 0, 255,  cv2.THRESH_BINARY + cv2.THRESH_OTSU)
plt.title('img_binary THRESH_OTSU')
plt.imshow(cv2.cvtColor(img_binary, cv2.COLOR_BGR2RGB))
plt.show()
binary
binary

大津の2値化のほうがよさげです(# 名前もカッコイイ)

ゴミ除去

ハフ変換を用いて画像内の線分を検出し、検出した線分を黒く塗ることで線を除去します。
小さいゴミは後で除去できるので「細い線」で「砕き」ます。

import numpy as np

output =img_binary.copy()
color_output = cv2.cvtColor(output, cv2.COLOR_GRAY2BGR)
for a in range(6) :
    lines = cv2.HoughLinesP(output, rho=0.6, theta=np.pi/360, threshold=10, minLineLength=21-a, maxLineGap=1.0)
    if lines is not None :
        for line in lines:
            x1, y1, x2, y2 = line[0]
            cv2.line(output, (x1, y1), (x2, y2), (0, 0, 0), 1)
            cv2.line(color_output, (x1, y1), (x2, y2), (0, 255, 0), 1)
        break

plt.title('color_output')
plt.imshow(cv2.cvtColor(color_output, cv2.COLOR_BGR2RGB))
plt.show()

plt.title('removed_line')
plt.imshow(cv2.cvtColor(output, cv2.COLOR_BGR2RGB))
plt.show()
removed_line
removed_line

エッジ検出を行い、小さい領域は除去します。
その後、検出したエッジにマスクをかけ、その領域以外を除去します。

# エッジ検出
contours, hierarchy = cv2.findContours(output, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# ちいさい領域削除 the region of interest (ROI)
roi = [obj for obj in contours if not cv2.contourArea(obj)<=12]

# 検出枠表示用に入力画像をカラーデータに変換する
output_color = cv2.cvtColor(output, cv2.COLOR_GRAY2BGR)
for contour in roi:
    (x, y, w, h) = cv2.boundingRect(contour)
    
    region = output_color[y:y+h, x:x+w]
    
    # 最小外接矩形を求める
    rect = cv2.minAreaRect(contour)
    
    # 矩形の4つの角の座標を求める
    box = cv2.boxPoints(rect)
    box = np.int0(box)
    
    # 矩形を描画する
    cv2.drawContours(output_color, [box], 0, (0, 255, 0), 1)
    
plt.title(f'roi length={len(roi)}')
plt.imshow(cv2.cvtColor(output_color, cv2.COLOR_BGR2RGB))
plt.show()

# 興味ある部分のみ白マスク
mask = np.zeros(output.shape[:2], dtype=np.uint8)
cv2.fillPoly(mask, roi, 255)

# 白マスクしたとこだけ残す
output = cv2.bitwise_and(output, output, mask=mask)

plt.title('result')
plt.imshow(cv2.cvtColor(output, cv2.COLOR_BGR2RGB))
plt.show()
result
result

OCRでテキスト読み取り

テキストを読み取るための下準備ができましたのでOCRで読み取ってみます。
事前に検出する文字が決まっている場合はwhitelistを指定すると検出しやすいです。
OCRインストール先のconfigフォルダにalphabetというファイル名で保存しておきます。

tessedit_char_whitelist ABCDEFGHIJKLMNOPQRSTUVWXYZ

「tesseract_layout」は「10」の「画像を1つの文字として扱う」と指定しておきます。

import pyocr
from PIL import Image

# ndarrayをpillowのImageに変換
pil_img = Image.fromarray(cv2.cvtColor(output, cv2.COLOR_BGR2RGB))

# OCRエンジンを取得(事前にOSでインストールしておく必要がある)
pyocr.tesseract.TESSERACT_CMD = 'C:\\Program Files\\Tesseract-OCR\\tesseract.exe'
engines = pyocr.get_available_tools()
engine = engines[0]

builder = pyocr.builders.TextBuilder(tesseract_layout=10)
builder.tesseract_configs.append("alphabet")

# 画像の文字を読み込む
text = engine.image_to_string(pil_img, lang="eng", builder=builder)

print(f'"{text}"')
OCR
OCR

一応傾き補正もしてみましたが、あまり結果が変わらないので取り入れませんでした。

adjust_skew = output.copy()
contours, hierarchy = cv2.findContours(adjust_skew, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# 検出枠表示用に入力画像をカラーデータに変換する
output_color = cv2.cvtColor(output, cv2.COLOR_GRAY2BGR)

for contour in contours:
    (x, y, w, h) = cv2.boundingRect(contour)
    
    # ある程度横長のものは無視する
    if w >= 35 : continue
    region = adjust_skew[y:y+h, x:x+w]

    # 最小外接矩形を求める
    rect = cv2.minAreaRect(contour)

    # 矩形の4つの角の座標を求める
    box = cv2.boxPoints(rect)
    box = np.int0(box)
    
    # 矩形を描画する
    cv2.drawContours(output_color, [box], 0, (0, 255, 0), 1)
    
    # 矩形の傾きを計算する
    angle = rect[2]
    
    if angle > 45:
        angle = -(angle - 90)

    else :
        angle = -angle
    center = (w / 2, h / 2)

    # 回転のための変換行列の生成 
    # cv2.getRotationMatrix2D(入力画像の回転中心, 回転角度 単位は度 - 正の値:反時計回り, 等方性スケーリング係数 - 拡大縮小の倍率)
    M = cv2.getRotationMatrix2D(center, -angle, 1.0)

    # v2.warpAffine(元の画像, cv2.getRotationMatrix2Dで生成した2*3の変換行列, 出力する画像サイズ(縦の高さ, 横の幅))
    rotated_region = cv2.warpAffine(region, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
    adjust_skew[y:y+h, x:x+w] = rotated_region
    
plt.title('before')
plt.imshow(cv2.cvtColor(output_color, cv2.COLOR_BGR2RGB))
plt.show()

contours, hierarchy = cv2.findContours(adjust_skew, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 検出枠表示用に入力画像をカラーデータに変換する
output_color = cv2.cvtColor(adjust_skew, cv2.COLOR_GRAY2BGR)
for contour in contours:
    (x, y, w, h) = cv2.boundingRect(contour)
    region = output_color[y:y+h, x:x+w]
    rect = cv2.minAreaRect(contour)
    box = cv2.boxPoints(rect)
    box = np.int0(box)
    cv2.drawContours(output_color, [box], 0, (0, 255, 0), 1)

plt.title('after')
plt.imshow(cv2.cvtColor(output_color, cv2.COLOR_BGR2RGB))
plt.show()
skew
skew

おわりに

今回は比較的簡単な文字captcha画像を扱いましたが、ノイズが文字にかかった場合などは誤認識が多発します。
OCRの部分を機械学習データを自作することで、より精度を上げることができると思います。




Copyright 2025
サイトマップ