PythonとOpenCVで始めるホワイトボード・文書画像の傾き補正

python

この記事ではpythonとOpenCVを使用して、ホワイトボードや文書の写真の傾きを補正する方法を記載します。利用シーン分析→要求仕様設定→ツール構成→pythonコード生成・解説を順を追って説明します。

所長
所長

よく台形補正や射影変換などと呼ばれている、傾いて撮影された絵や文書を補正する方法です。
スマホアプリを使えば簡単ですが、データの機密性からPCでスタンドアロンで実施したい場合を想定します。

利用シーン

シナリオとしては以下の感じです。

user
user

セキュリティやデータ機密保持のため、写真加工アプリやクラウド利用ができない…。
けど使用しているPCに保存されているホワイトボードや書類の写真を補正したい。

まずはこれをユースケース分析して要求仕様を設定します。

ユースケース

ユースケースをアクティビティ図で示すと以下のような感じ。
tool側に書いている角丸がtoolのアクションであり、実現すべきことになります。

ユースケース分析

アクティビティ図については以下が参考になります。

https://www.itsenka.com/contents/development/uml/activity.html

要求仕様

ツールとして実現すべきこと(ユースケースに記載のアクティビティ図の右側)は以下です。

  • ユーザーのパスの指定を元に、パス先に保存された画像を取得する
  • 補正の基準となる頂点の入力を受け付ける
  • 入力された頂点座標をもとに、画像を射影変換する
  • 補正後の画像の出力先のパス指定を受け付ける
  • 指定先に補正後の画像を保存する

また、ユーザーの操作(ユースケースに記載のアクティビティ図の左側)に対して求められることを以下とします。(いわゆる非機能要求っぽいやつです。ここでは使用性(ユーザビリティ)が該当します。)

  • 画像のパス指定(補正する画像と補正後の画像)はコードでの指定ではなく、任意で選択したい
  • 頂点座標の入力は実際の画像を見ながら指定できるようにしたい
  • 指定した頂点や補正する部分がわかるようにしたい

上記8項目を要求仕様とします。

ツール構成

ツールのin/outの構想は以下です。
受け渡し相手としてはuserとPC内のディレクトリとし、userとのやりとりはGUIを想定します。

pythonコード

前置きが長くなりましたが、いよいよpythonコードです。先にコードを示して後半で解説します。
なお、pythonのバージョンおよび使用ライブラリは以下です。

  • python : 3.9.9
  • numpy : 1.24.2
  • opencv-python : 4.7.0.68
  • Pillow : 9.4.0
## Import library
import numpy as np
import cv2
from PIL import Image
import tkinter
from tkinter import filedialog

## Function to get contour of the picture
def getContour(event, x, y, flg, params):
    raw_img = params["img"]
    wname = params["wname"]
    cor_list = params["cor_list"]
    cor_num=params["cor_num"]

    if event == cv2.EVENT_LBUTTONDOWN:
        if len(cor_list) < cor_num:
            cor_list.append([x, y])

    img = raw_img.copy()
    ## Show lines of the cousor
    h, w = img.shape[0], img.shape[1]
    cv2.line(img, (x, 0), (x, h), (255, 0, 0), 1)
    cv2.line(img, (0, y), (w, y), (255, 0, 0), 1)

    ## Show points and lines of selected contours
    for i in range(len(cor_list)):
        cv2.circle(img, (cor_list[i][0], cor_list[i][1]), 3, (0, 0, 255), 3)
        if 0 < i:
            cv2.line(img, (cor_list[i][0], cor_list[i][1]),
                    (cor_list[i-1][0], cor_list[i-1][1]), (0, 255, 0), 2)
        if i == cor_num -1:
            cv2.line(img, (cor_list[i][0], cor_list[i][1]),
                    (cor_list[0][0], cor_list[0][1]), (0, 255, 0), 2)
    if 0 < len(cor_list) < cor_num:
        cv2.line(img, (x, y), (cor_list[len(cor_list)-1][0], cor_list[len(cor_list)-1][1]), (0, 255, 0), 2)

    cv2.imshow(wname, img)

## main function
def main():
    ## Get path of target picture
    img_path = tkinter.filedialog.askopenfilename()
    src_img = Image.open(img_path)
    src_img = np.array(src_img)

    ## reshape picture
    h, w = src_img.shape[0], src_img.shape[1]
    ratio = 800 / w
    img = cv2.resize(src_img, None, None, ratio, ratio)

    ## set parameters
    wname = "ProjectiveTrans"
    cor_num = 4
    cor_list = []
    params = {
        "img": img,
        "wname": wname,  
        "cor_list": cor_list,
        "cor_num": cor_num,
    }

    ## show image and get contours
    cv2.namedWindow(wname)
    cv2.setMouseCallback(wname, getContour, params)
    cv2.imshow(wname, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

    ## perspective transform
    src = np.float32(cor_list) / ratio
    o_width = int(np.linalg.norm(src[1] - src[0]))
    o_height = int(np.linalg.norm(src[3] - src[0]))
    dst = np.float32([[0, 0], [o_width, 0], [o_width, o_height], [0, o_height]])
    M = cv2.getPerspectiveTransform(src, dst)
    out_img = cv2.warpPerspective(src_img, M, (o_width, o_height))

    ## output image
    out_name = tkinter.filedialog.asksaveasfilename()
    cv2.imwrite(out_name, out_img)


if __name__ == "__main__":
    main()
https://github.com/ChikuwaLab/open_playground/blob/main/python/ProjectiveTrans.py

解説

main / メイン関数

対象画像の取得
    ## Get path of target picture
    img_path = tkinter.filedialog.askopenfilename()
    src_img = Image.open(img_path)
    src_img = np.array(src_img)

まず、画像ファイルのパスをGUIより取得します。(tkinter.filedialog.askopenfilename())
画像データをsrc_imgとしています。このとき、PillowのImage.open()を使用して画像データ取得後、numpyで扱う形式に変換しています。理由はOpenCVのcv2.imreadですと、日本語のパスだと読み込めないためです。

座標取得前処理
    ## reshape picture
    h, w = src_img.shape[0], src_img.shape[1]
    ratio = 800 / w
    img = cv2.resize(src_img, None, None, ratio, ratio)

    ## set parameters
    wname = "ProjectiveTrans"
    cor_num = 4
    cor_list = []
    params = {
        "img": img,
        "wname": wname,  
        "cor_list": cor_list,
        "cor_num": cor_num,
    }

座標取得のための前処理およびパラメータ設定です。
まず、読み込んだ画像データを縮小します。後で使用するcv2.imshowで表示した際に大きな画像だと表示できない場合があるためです。画像データの幅と高さを取得し、幅が800になるように補正します。
写真のサイズ設定が4:3であれば、800×600となりますね。

次の## set parametersの部分は、次で使用する座標取得処理のためのパラメータ設定です。いろいろありますが、cor_num=4で座標を4個取得する設定をしています。

座標取得
    ## show image and get contours
    cv2.namedWindow(wname)
    cv2.setMouseCallback(wname, getContour, params)
    cv2.imshow(wname, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

対象となる画像を表示しながら、補正のための座標(写真の中で歪んでいる四角形の頂点)の入力を受け付けます。マウス操作でポチポチしながら4つの頂点を指定することができます。指定の順番は左上、右上、右下、左下の順で固定です。cv2.setMouseCallbackとそれで呼び出しているgetContourがポイントです。getContourについては後段で解説します。こちらについては、以下を参考にさせていただきました。

Kaggle Note
概要 画像処理関連のことをしていると画像のここの座標を取得したい!!!という場面が結構出てきますよね。 今回は視覚的に矩形の座標を取得したいと思|Kaggleのnotebookを中心に機械学習技術を紹介します。
射影変換
## perspective transform
    src = np.float32(cor_list) / ratio
    o_width = int(np.linalg.norm(src[1] - src[0]))
    o_height = int(np.linalg.norm(src[3] - src[0]))
    dst = np.float32([[0, 0], [o_width, 0], [o_width, o_height], [0, o_height]])
    M = cv2.getPerspectiveTransform(src, dst)
    out_img = cv2.warpPerspective(src_img, M, (o_width, o_height))

取得した座標をもとに、画像を射影変換します。
srcが取得した座標です。最初に入力画像を縮小している分を補正しています。その後、座標を元に、補正部分の実効的な大きさ(歪んだ四角形を長方形にした際の大きさ)を求めています。
あとはOpenCVの関数を使って、入力画像を補正します。

補正画像出力
## output image
    out_name = tkinter.filedialog.asksaveasfilename()
    cv2.imwrite(out_name, out_img)

出力先のファイル名を指定して補正画像を保存します。
なんの工夫もしていないです。ファイル名.jpgを手入力する必要があります。

getContour / 座標取得関数

対象画像を表示し、その画像上で補正ようの座標の指定をマウス操作で受け付けます。
その処理自体は以下のみです。

    if event == cv2.EVENT_LBUTTONDOWN:
        if len(cor_list) < cor_num:
            cor_list.append([x, y])

残りの処理は選択結果の表示です。マウス位置を青い十字線、選択した座標を赤丸、選択した座標で決まる四角形を緑線で表示します。見た目は実行結果をご覧ください。

ChatGPTを使ったコード自動生成

ここまでのコードをChatGPTで作成する方法は以下で紹介しています!

実行結果

入力画像を以下のように斜めに撮った文書とします。

コードを実行し、画像ファイルを指定すると以下のように画像に対して座標を指定・表示することができます。

補正後の画像はこちら。いい感じですね。

まとめ

実行結果なかなかうまくいったのではと思います。今後の改良ポイントとしては以下が考えられます。

  • 座標の指定の順序を順不同でも良いようにする
  • いっそのこと座標の取得は自動で実施する
  • exeファイル化してpython実行の知識がなくても使えるようにする

コメント

タイトルとURLをコピーしました