この記事ではpythonとOpenCVを使用して、ホワイトボードや文書の写真の傾きを補正する方法を記載します。利用シーン分析→要求仕様設定→ツール構成→pythonコード生成・解説を順を追って説明します。
よく台形補正や射影変換などと呼ばれている、傾いて撮影された絵や文書を補正する方法です。
スマホアプリを使えば簡単ですが、データの機密性からPCでスタンドアロンで実施したい場合を想定します。
利用シーン
シナリオとしては以下の感じです。
利用シーン
セキュリティやデータ機密保持のため、写真加工アプリやクラウド利用ができない…。
けど使用しているPCに保存されているホワイトボードや書類の写真を補正したい。
まずはこれをユースケース分析して要求仕様を設定します。
ユースケース
ユースケースをアクティビティ図で示すと以下のような感じ。
tool側に書いている角丸がtoolのアクションであり、実現すべきことになります。
アクティビティ図については以下が参考になります。
要求仕様
ツールとして実現すべきこと(ユースケースに記載のアクティビティ図の右側)は以下です。
- ユーザーのパスの指定を元に、パス先に保存された画像を取得する
- 補正の基準となる頂点の入力を受け付ける
- 入力された頂点座標をもとに、画像を射影変換する
- 補正後の画像の出力先のパス指定を受け付ける
- 指定先に補正後の画像を保存する
また、ユーザーの操作(ユースケースに記載のアクティビティ図の左側)に対して求められることを以下とします。(いわゆる非機能要求っぽいやつです。ここでは使用性(ユーザビリティ)が該当します。)
- 画像のパス指定(補正する画像と補正後の画像)はコードでの指定ではなく、任意で選択したい
- 頂点座標の入力は実際の画像を見ながら指定できるようにしたい
- 指定した頂点や補正する部分がわかるようにしたい
上記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()
解説
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については後段で解説します。こちらについては、以下を参考にさせていただきました。
射影変換
## 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で作成する方法は以下で紹介しています!
-
PythonとOpenCVのコードをChatGPTで自動生成する
この記事では、ChatGPTを使ってpythonのコードを作成する事例を紹介します。 https://openai.com/chatgpt/ ChatGPTでコード生成するもの 今回は以下のPytho ...
続きを見る
実行結果
入力画像を以下のように斜めに撮った文書とします。
コードを実行し、画像ファイルを指定すると以下のように画像に対して座標を指定・表示することができます。
補正後の画像はこちら。いい感じですね。
まとめ
実行結果なかなかうまくいったのではと思います。今後の改良ポイントとしては以下が考えられます。
- 座標の指定の順序を順不同でも良いようにする
- いっそのこと座標の取得は自動で実施する
- exeファイル化してpython実行の知識がなくても使えるようにする