🔤

Python 画面上の指定範囲から文字認識

2023/02/10に公開

概要

プログラムの全体の流れ

  1. 画面全体のキャプチャ
  2. 画像から取得したい文字の場所を選択(ドラッグ&ドロップ)
  3. 選択箇所の座標をもとにトリミングし、文字認識を行う
  4. クリップボードに保存

プレビューウィンドウの作成

全画面キャプチャした際の、画像をプレビューするためのウィンドウを設ける。今回はPythonの標準ライブラリであるTkinterを用いた。

# 画面全体をスクリーンショット
screenshot = gui.screenshot()

# GUI
root = tk.Tk()
root.attributes('-fullscreen', True) # フルスクリーン
width = root.winfo_screenwidth()   # 画面の横幅
height = root.winfo_screenheight() # 画面の縦幅

スクリーンショットはpyautoguiを使っています。ウィンドウは、フルスクリーンでプレビューを見れるようにします。
Macの設定解像度とディスプレイ本来の解像度で差があると、表示したプレビューウィンドウのサイズとスクリーンショットのサイズが合いません。そうすると、後ほど行うスクリーンショットから画像をトリミングする際に、座標にズレが生じます。なので、スクリーンショットのリサイズ用にウィンドウサイズを変数へ格納します。

# リサイズ
screenshot_resize = screenshot.resize((width, height))
# 元の画像が縮小サイズの何倍かを計算
width_dif = (screenshot_resize.width / width) + 1
height_dif = (screenshot_resize.height / height) + 1
# Tkinterで表示できるように変換
screenshot_tk = ImageTk.PhotoImage(screenshot_resize) 
# キャンバスの生成
canvas = tk.Canvas(root, width=screenshot_resize.width, height=screenshot_resize.height)
# キャンバスにイメージを表示
canvas.create_image(0, 0, image=screenshot_tk, anchor=tk.NW)
canvas.pack()

スクリーンショットをウィンドウサイズでリサイズします。その後、縮小率を計算し変数へ格納しておきます。また、画像をTkinterのウィンドウで表示できるように変換します。

画像から範囲を指定する

「クリックした時(lbutton_down)」「ドラッグしている時(drawing)」「ドラッグが終了した時(lbutton_up)」の3つで関数を作り、それぞれが発動するようにクリックイベントを設けます。

今回は、
左クリック:<ButtonPress-1>
ドラッグ:<Button1-Motion>
左クリックを離す:<ButtonRelease-1>
これらと、それぞれの関数が紐づくようにしています。
第1引数に、検知する対象、第2引数に関数を指定します。

プレビューウィンドウ作成後の、canvas.pack()の後に下記の3つを追記します。

# イベントを設定
canvas.bind("<ButtonPress-1>", lbutton_down)
canvas.bind("<Button1-Motion>", drawing)
canvas.bind("<ButtonRelease-1>", lbutton_up)

カーソルの座標取得(ドラッグ&ドロップ)

ドラッグを開始した時の処理

# ドラッグを開始した時の処理
def lbutton_down(event):
  global x1, y1
  x1, y1 = event.x, event.y
  # 前に描画した"rect"を削除
  canvas.delete("rect")
  # クリックした瞬間の小さい短形を描画
  canvas.create_rectangle(x1, y1, x1+1, y1+1, outline = "red", width = 2, tag="rect") 

ここでは、クリックした瞬間、小さい図形(ほぼ点)を描画するために、create_ractangleを使います。まず、クリックされた座標に関しては、lbutton_down関数の仮引数に、クリックイベントの座標が含まれています。初めに、そのx,y座標を分離し、それぞれ格納しています。

次に図形の描画についてです。create_rectangleの引数の第1第2は、図形の左上、第3第4は右下の座標を指しています。tagには図形に名前をつけ、判別がつくようにしています。

canvas.create_rectangle(x1, y1, x2, y2, outline = "線の色", width = 線の太さ, tag="図形の名前(タグ)") 

図形のイメージ図
図形の座標イメージ

図形描画前に、もし2回目以降のクリックだった場合、以前描画した図形はいらないのでcanvas.delete("rect")で図形の名前を指定して削除しています。

ドラッグしている時の処理

# ドラッグしている時の処理
def drawing(event):
  global x2, y2
  x2 = event.x
  y2 = event.y
  # 座標の変更
  canvas.coords("rect", x1, y1, x2, y2)

ドラッグして、カーソルが動くたびにdrawing関数は呼び出されます。そして、ここではそのたび今描画している図形の大きさを更新しています。

canvas.coords("図形の名前", x1, y1, x2, y2)

x1,y1は、クリックした最初の点なので変更はありません。x2,y2はドラッグ中に移動している点なので、取得した座標を指定しています。

ドラッグを離した時の処理

# ドラッグを離した時の処理
def lbutton_up(event):
  global x1, y1, x2, y2
  # 始点、終点の反転に対応
  if x1 > x2:
    x1, x2 = x2, x1
  if y1 > y2:
    y1, y2 = y2, y1
  # オリジナルサイズの画像からトリミング
  screenshot_crop = screenshot.crop((x1*width_dif, y1*height_dif, x2*width_dif, y2*height_dif)) 
  ...

最後のドラッグを離した時の処理では、クリックした時の始点と最後離した時の終点の位置を確認しています。この位置関係が真逆になった時。下のイメージ図のように小さい方に行った時に座標位置を始点と終点で入れ替えています。
これは、後ほどスクリーンショットをトリミングする時に正しい座標にするためです。
ドラッグのイメージ図

始点・終点の正しい座標が決まれば、あとはトリミングするだけです。Pythonの画像処理ライブラリPillowのImageモジュールにある画像を切り抜くメソッドcropを使います。一番初めに撮ったscreenshotの変数を指定しています。引数は、図形の描画同様x,y座標を2箇所指定します。

ここで気をつけるのが、スクリーンショットを切り抜く際に、今ある座標では縮小したサイズの座標なので、元の座標(実際のスクリーンショットの座標)を取得するため、初めに計算して用意しておいた、width_difheight_difをかけています。

screenshot.crop((x1*width_dif, y1*height_dif, x2*width_dif, y2*height_dif)) 

画像から文字認識

文字認識に関しては、以前書いた「Python 画像から文字認識(Tesseract)」に詳細を書いているので導入方法と、説明はそちらをご覧ください。

# ドラッグを離した時の処理
def lbutton_up(event):
  ...
  # 文字認識
  str = pytesseract.image_to_string(screenshot_crop, lang="jpn+jpn_vert")
  # クリップボードにコピー
  pyperclip.copy(str)
  # ウィンドウを閉じる
  root.destroy()

選択範囲からトリミングしたスクリーンショットをpytesseractに渡し、文字認識を行います。そのあとは、文字列をpyperclipcopyメソッドで、クリップボードに保存しています。


完成したコード

import tkinter as tk
from PIL import ImageTk # Tkinter用に画像変換
import pyautogui as gui # スクリーンショット
import pytesseract # 文字認識
import pyperclip # クリップボード


# ドラッグを開始した時の処理
def lbutton_down(event):
  global x1, y1
  x1, y1 = event.x, event.y
  # 前に描画した"rect"を削除
  canvas.delete("rect")
  # クリックした瞬間の小さい短形を描画
  canvas.create_rectangle(x1, y1, x1+1, y1+1, outline = "red", width = 2, tag="rect") 

# ドラッグしている時の処理
def drawing(event):
  global x2, y2
  x2 = event.x
  y2 = event.y
  # 座標の変更
  canvas.coords("rect", x1, y1, x2, y2)

# ドラッグを離した時の処理
def lbutton_up(event):
  global x1, y1, x2, y2
  # 始点、終点の反転に対応
  if x1 > x2:
    x1, x2 = x2, x1
  if y1 > y2:
    y1, y2 = y2, y1
  # オリジナルサイズの画像からトリミング
  screenshot_crop = screenshot.crop((x1*width_dif, y1*height_dif, x2*width_dif, y2*height_dif)) 
  # 文字認識
  str = pytesseract.image_to_string(screenshot_crop, lang="jpn+jpn_vert")
  # クリップボードにコピー
  pyperclip.copy(str)
  # ウィンドウを閉じる
  root.destroy()


# 画面全体をスクリーンショット
screenshot = gui.screenshot()

# GUI
root = tk.Tk()
root.attributes('-fullscreen', True) # フルスクリーン
width = root.winfo_screenwidth()   # 画面の横幅
height = root.winfo_screenheight() # 画面の縦幅
# リサイズ
screenshot_resize = screenshot.resize((width, height))
# 元の画像が縮小サイズの何倍かを計算
width_dif = (screenshot_resize.width / width) + 1
height_dif = (screenshot_resize.height / height) + 1
# Tkinterで表示できるように変換
screenshot_tk = ImageTk.PhotoImage(screenshot_resize) 
# キャンバスの生成
canvas = tk.Canvas(root, width=screenshot_resize.width, height=screenshot_resize.height)
# キャンバスにイメージを表示
canvas.create_image(0, 0, image=screenshot_tk, anchor=tk.NW)
canvas.pack()

# イベントを設定
canvas.bind("<ButtonPress-1>", lbutton_down)
canvas.bind("<Button1-Motion>", drawing)
canvas.bind("<ButtonRelease-1>", lbutton_up)

root.mainloop()

参考

Discussion