【Python】画像の自由変形(射影変換)での切り出し方法【OpenCV】

カメラで垂直に資料の平面を撮影したかった場合に、正対できずに上下左右の遠近感が出てしまうことがあります。しかし射影変換を行うことでそのずれを補正することが可能です。Python/OpenCVを使うことで誰でも簡単に画像の自由変形(射影変換)を使うことができます。本記事では射影元を画面から選択し、射影変換後ファイルを保存する処理について記載します。(日本語フルパス対応)

射影変換で画像を自由変形させるには?

射影変換とは、画像の形状を変形させる変換手法の1つです。コンピュータグラフィックスの分野では画像を様々な形状に変形させることで、2Dを3Dに見立てたり、表面にテクスチャを貼ったりしたりすることができます。例えば以下のように遠近感が上下で異なる画像に対して射影変換を行うことで正対した画像にすることが可能です。このように撮影時のちょっとしたブレを補正することが可能となります。

遠近感の異なる画像

遠近感の異なる画像

正対した画像(射影変換後)

正対した画像(射影変換後)

上で行った方法は射影変換ですが、画像の変換には下図のようないくつかの変換方法があります。
(a)合同変換  :変換前後にて画像は完全一致で回転のみを許す変換する方法
(b)相似変換  :(a)に加え、拡大と縮小を可能にした変換方法
(c)アフィン変換:(a)(b)に加えひし形の変形を可能にした変換方法
(d)射影変換  :(a)(b)(c)の全ての機能に対してさらにどんな四角形にでも変換できる方法

射影変換種類

射影変換のコード

インポートするパッケージ

%MINIFYHTMLf134d56892b6e605ea4fff3c04f76dc6131%
import cv2 import numpy as np import matplotlib.pyplot as plt import glob import configparser import os import sys

引数からのファイルフルパス配列取得

Pyinstallerなどでexe化し、Shell:SendToにショートカットを作成することで、右クリックの「送る」にて選択したファイルを順次処理することが可能となります。
引数のファイル名の取得と、ファイルパスの取得、拡張子のチェックを行い、メイン処理に渡しています。

if __name__ == "__main__": # 引数からのファイルフルパス配列取得 args = sys.argv for i,file_path in enumerate(args): # 拡張子取得と拡張子チェック処理 root, ext = os.path.splitext(file_path) if ext.lower() == '.png' or ext.lower() == '.jpg' or ext.lower() == '.jpeg' or ext.lower() == '.webp': # 現在のファイル名取得 file_Name = os.path.splitext(os.path.basename(file_path))[0] main = Main(file_path,file_Name)

画面での選択機能と射影変換処理

画面にて射影範囲を選択するための画像用意とパラメータ設定、そして射影変換処理と結果の保存処理となります。画面にて射影範囲を選択する際にはクリックした位置をウィンドウ上の座標から元の画像の座標に変換する必要があります。そして射影変換では以下の図のように変換前後の対応点p1-p4をそれぞれ対応している順番通りに選択が必要となります。画像は元の名称+”-fix”+拡張子にて保存されます。

画面での選択機能と射影変換処理
class Main(): def __init__(self,file_path,file_Name): # 画像読み込み処理と射影範囲選択処理用画像作成 self.img = imread(file_path,cv2.IMREAD_COLOR) self.orgHeight, self.orgWidth = self.img.shape[:2] # 幅の小さい方に合わせて画像作成を行う self.sizeH = (int(self.orgWidth*700/self.orgHeight),700) self.sizeW = (1000,int(self.orgHeight*1000/self.orgWidth)) if self.sizeH[0] > self.sizeW[0]: self.ratio = 1000/self.orgWidth # 拡大縮小用の関数 self.ImgShow = SizeCheck(self.sizeW[0],self.sizeW,self.img) else: self.ratio = 700/self.orgHeight # 拡大縮小用の関数 self.ImgShow = SizeCheck(self.sizeH[0],self.sizeH,self.img) # 画面からのマウスイベント取得用設定 self.wname = "MouseEvent" cv2.namedWindow(self.wname) self.npoints = 4 self.ptlist = PointList(self.npoints) cv2.setMouseCallback(self.wname, onMouse, [self.wname, self.ImgShow, self.ptlist]) cv2.imshow(self.wname, self.ImgShow) cv2.waitKey() cv2.destroyAllWindows() # 変換前後の対応点を設定 p1-p4はそれぞれ対応しているので順番通りに選択が必要 # クリックした位置をウィンドウ上の座標から元の画像の座標に変換 self.p_original = np.float32([[int(x/self.ratio) for x in self.ptlist.ptlist[0]], \ [int(x/self.ratio) for x in self.ptlist.ptlist[1]],\ [int(x/self.ratio) for x in self.ptlist.ptlist[2]],\ [int(x/self.ratio) for x in self.ptlist.ptlist[3]]]) self.diff_width1 = np.abs(int(self.ptlist.ptlist[0][0]/self.ratio) - int(self.ptlist.ptlist[1][0]/self.ratio)) self.diff_width2 = np.abs(int(self.ptlist.ptlist[2][0]/self.ratio) - int(self.ptlist.ptlist[3][0]/self.ratio)) self.diff_height1 = np.abs(int(self.ptlist.ptlist[0][1]/self.ratio) - int(self.ptlist.ptlist[3][1]/self.ratio)) self.diff_height2 = np.abs(int(self.ptlist.ptlist[1][1]/self.ratio) - int(self.ptlist.ptlist[2][1]/self.ratio)) # 幅と高さの小さい方に合わせる。 if self.diff_width1 < self.diff_width2: self.width = self.diff_width1 else: self.width = self.diff_width2 if self.diff_height1 < self.diff_height2: self.height = self.diff_height1 else: self.height = self.diff_height2 self.p_trans = np.float32([[0,0], [self.width,0], [self.width,self.height], [0,self.height]]) # 変換マトリクスと射影変換 self.M = cv2.getPerspectiveTransform(self.p_original, self.p_trans) self.img_trans = cv2.warpPerspective(self.img,self.M, (self.width,self.height)) self.ImgCut = self.img_trans self.ImgCutHeight, self.ImgCutWidth = self.ImgCut.shape[:2] # 射影した画像のサイズ調整 self.size = (780,int(self.ImgCutHeight*780/self.ImgCutWidth)) self.ImgShow = SizeCheck(780,self.size,self.ImgCut) # ファイルを元の名称+"-fix"+拡張子にて保存 basename = os.path.basename(file_path) dirname = os.path.dirname(file_path) root, ext = os.path.splitext(file_path) imwrite(dirname +'/'+ file_Name +'-fix' + ext, self.ImgShow)

マウスクリック時の座標保存処理

マウス移動時の座標表示、マウスクリックを行った際に4点までの座標のウィンドウ上への表示と保存を行う必要があります。追加時の表示、保存処理を以下に記載します。

# マウス位置のリスト作成処理 class PointList(): def __init__(self, npoints): self.npoints = npoints self.ptlist = np.empty((npoints, 2), dtype=int) self.pos = 0 # マウス位置追加保存処理 def add(self, x, y): if self.pos < self.npoints: self.ptlist[self.pos, :] = [x, y] self.pos += 1 return True return False if event == cv2.EVENT_LBUTTONDOWN: # レフトボタンをクリックしたとき、ptlist配列にx,y座標を格納する if ptlist.add(x, y): print('[%d] ( %d, %d )' % (ptlist.pos - 1, x, y)) cv2.circle(img, (x, y), 3, (0, 0, 255), 3) cv2.imshow(wname, img) else: print('All points have selected. Press ESC-key.') if(ptlist.pos == ptlist.npoints): cv2.line(img, (ptlist.ptlist[0][0], ptlist.ptlist[0][1]), (ptlist.ptlist[1][0], ptlist.ptlist[1][1]), (0, 255, 0), 3) cv2.line(img, (ptlist.ptlist[1][0], ptlist.ptlist[1][1]), (ptlist.ptlist[2][0], ptlist.ptlist[2][1]), (0, 255, 0), 3) cv2.line(img, (ptlist.ptlist[2][0], ptlist.ptlist[2][1]), (ptlist.ptlist[3][0], ptlist.ptlist[3][1]), (0, 255, 0), 3) cv2.line(img, (ptlist.ptlist[3][0], ptlist.ptlist[3][1]), (ptlist.ptlist[0][0], ptlist.ptlist[0][1]), (0, 255, 0), 3) cv2.imshow(wname, img)

cv2.imread、cv2.imwriteの日本語対応化

cv2.imread、cv2.imwriteは日本語を含むフルパスを引数として設定した場合、正常に処理ができません。そのため、以下のように新たに関数を用意してあげる必要があります。

# 日本語パス対応のためのimread関数作成 def imread(filename, flags=cv2.IMREAD_COLOR, dtype=np.uint8): try: n = np.fromfile(filename, dtype) img = cv2.imdecode(n, flags) return img except Exception as e: print(e) return None # 日本語パス対応のためのimwrite関数作成 def imwrite(filename, img, params=None): try: ext = os.path.splitext(filename)[1] result, n = cv2.imencode(ext, img, params) if result: with open(filename, mode='w+b') as f: n.tofile(f) return True else: return False except Exception as e: print(e) return False

コードのまとめ

上記で紹介したコードをまとめて記載すると以下のようになります。

# インポートするパッケージ import cv2 import numpy as np import matplotlib.pyplot as plt import glob import configparser import os import sys # 日本語パス対応のためのimread関数作成 def imread(filename, flags=cv2.IMREAD_COLOR, dtype=np.uint8): try: n = np.fromfile(filename, dtype) img = cv2.imdecode(n, flags) return img except Exception as e: print(e) return None # 日本語パス対応のためのimwrite関数作成 def imwrite(filename, img, params=None): try: ext = os.path.splitext(filename)[1] result, n = cv2.imencode(ext, img, params) if result: with open(filename, mode='w+b') as f: n.tofile(f) return True else: return False except Exception as e: print(e) return False # マウス位置のリスト作成処理 class PointList(): def __init__(self, npoints): self.npoints = npoints self.ptlist = np.empty((npoints, 2), dtype=int) self.pos = 0 # マウス位置追加保存処理 def add(self, x, y): if self.pos < self.npoints: self.ptlist[self.pos, :] = [x, y] self.pos += 1 return True return False # 拡大縮小にて処理方法を変更するためにチェックを行う処理 def SizeCheck(thresh,size,img): orgHeight, orgWidth = img.shape[:2] if thresh > orgWidth: # 拡大処理 INTER_CUBIC return cv2.resize(img, size, interpolation = cv2.INTER_CUBIC) else: # 縮小処理 IINTER_AREA return cv2.resize(img, size, interpolation = cv2.INTER_AREA) # マウス・クリック位置表示処理 def onMouse(event, x, y, flag, params): wname, img, ptlist = params if event == cv2.EVENT_MOUSEMOVE: # マウスが移動したときにx線とy線を更新する img2 = np.copy(img) h, w = img2.shape[0], img2.shape[1] cv2.line(img2, (x, 0), (x, h - 1), (255, 0, 0)) cv2.line(img2, (0, y), (w - 1, y), (255, 0, 0)) cv2.imshow(wname, img2) if event == cv2.EVENT_LBUTTONDOWN: # レフトボタンをクリックしたとき、ptlist配列にx,y座標を格納する if ptlist.add(x, y): print('[%d] ( %d, %d )' % (ptlist.pos - 1, x, y)) cv2.circle(img, (x, y), 3, (0, 0, 255), 3) cv2.imshow(wname, img) else: print('All points have selected. Press ESC-key.') if(ptlist.pos == ptlist.npoints): cv2.line(img, (ptlist.ptlist[0][0], ptlist.ptlist[0][1]), (ptlist.ptlist[1][0], ptlist.ptlist[1][1]), (0, 255, 0), 3) cv2.line(img, (ptlist.ptlist[1][0], ptlist.ptlist[1][1]), (ptlist.ptlist[2][0], ptlist.ptlist[2][1]), (0, 255, 0), 3) cv2.line(img, (ptlist.ptlist[2][0], ptlist.ptlist[2][1]), (ptlist.ptlist[3][0], ptlist.ptlist[3][1]), (0, 255, 0), 3) cv2.line(img, (ptlist.ptlist[3][0], ptlist.ptlist[3][1]), (ptlist.ptlist[0][0], ptlist.ptlist[0][1]), (0, 255, 0), 3) cv2.imshow(wname, img) class Main(): def __init__(self,file_path,file_Name): # 画像読み込み処理と射影範囲選択処理用画像作成 self.img = imread(file_path,cv2.IMREAD_COLOR) self.orgHeight, self.orgWidth = self.img.shape[:2] # 幅の小さい方に合わせて画像作成を行う self.sizeH = (int(self.orgWidth*700/self.orgHeight),700) self.sizeW = (1000,int(self.orgHeight*1000/self.orgWidth)) if self.sizeH[0] > self.sizeW[0]: self.ratio = 1000/self.orgWidth # 拡大縮小用の関数 self.ImgShow = SizeCheck(self.sizeW[0],self.sizeW,self.img) else: self.ratio = 700/self.orgHeight # 拡大縮小用の関数 self.ImgShow = SizeCheck(self.sizeH[0],self.sizeH,self.img) # 画面からのマウスイベント取得用設定 self.wname = "MouseEvent" cv2.namedWindow(self.wname) self.npoints = 4 self.ptlist = PointList(self.npoints) cv2.setMouseCallback(self.wname, onMouse, [self.wname, self.ImgShow, self.ptlist]) cv2.imshow(self.wname, self.ImgShow) cv2.waitKey() cv2.destroyAllWindows() # 変換前後の対応点を設定 p1-p4はそれぞれ対応しているので順番通りに選択が必要 self.p_original = np.float32([[int(x/self.ratio) for x in self.ptlist.ptlist[0]], \ [int(x/self.ratio) for x in self.ptlist.ptlist[1]],\ [int(x/self.ratio) for x in self.ptlist.ptlist[2]],\ [int(x/self.ratio) for x in self.ptlist.ptlist[3]]]) self.diff_width1 = np.abs(int(self.ptlist.ptlist[0][0]/self.ratio) - int(self.ptlist.ptlist[1][0]/self.ratio)) self.diff_width2 = np.abs(int(self.ptlist.ptlist[2][0]/self.ratio) - int(self.ptlist.ptlist[3][0]/self.ratio)) self.diff_height1 = np.abs(int(self.ptlist.ptlist[0][1]/self.ratio) - int(self.ptlist.ptlist[3][1]/self.ratio)) self.diff_height2 = np.abs(int(self.ptlist.ptlist[1][1]/self.ratio) - int(self.ptlist.ptlist[2][1]/self.ratio)) # 幅と高さの小さい方に合わせる。 if self.diff_width1 < self.diff_width2: self.width = self.diff_width1 else: self.width = self.diff_width2 if self.diff_height1 < self.diff_height2: self.height = self.diff_height1 else: self.height = self.diff_height2 self.p_trans = np.float32([[0,0], [self.width,0], [self.width,self.height], [0,self.height]]) # 変換マトリクスと射影変換 self.M = cv2.getPerspectiveTransform(self.p_original, self.p_trans) self.img_trans = cv2.warpPerspective(self.img,self.M, (self.width,self.height)) self.ImgCut = self.img_trans self.ImgCutHeight, self.ImgCutWidth = self.ImgCut.shape[:2] # 射影した画像のサイズ調整 self.size = (780,int(self.ImgCutHeight*780/self.ImgCutWidth)) self.ImgShow = SizeCheck(780,self.size,self.ImgCut) # ファイルを元の名称+"-fix"+拡張子にて保存 basename = os.path.basename(file_path) dirname = os.path.dirname(file_path) root, ext = os.path.splitext(file_path) imwrite(dirname +'/'+ file_Name +'-fix' + ext, self.ImgShow) if __name__ == "__main__": # 引数からのファイルフルパス配列取得 args = sys.argv for i,file_path in enumerate(args): # 拡張子取得と拡張子チェック処理 root, ext = os.path.splitext(file_path) if ext.lower() == '.png' or ext.lower() == '.jpg' or ext.lower() == '.jpeg' or ext.lower() == '.webp': # 現在のファイル名取得 file_Name = os.path.splitext(os.path.basename(file_path))[0] main = Main(file_path,file_Name)

最後に

射影変換を使用することで画像を正対した状態で表示することができます。応用として斜めからしか撮像できない物体等に対してカメラ座標を使用して正対した表示を動画等でも行うことが可能です。別途記事として作成時には紹介したいと思います。

UMilCLhttps://umilcl.com
兼業ブロガー。メインは大手製造業システム企画・設計・プロジェクト管理。工場のプロセス制御が主な業務。 得意分野: プロセス制御と統計、機械学習、画像処理、システム技術 保有資格: プロジェクトマネージャー、情報処理安全確保支援士、ネットワークスペシャリスト、応用情報処理、第一種衛生管理者

Similar Articles

Comments

返事を書く

コメントを入力してください!
ここに名前を入力してください

Follow US

0ファンいいね
3フォロワーフォロー
89フォロワーフォロー
4,103フォロワーフォロー
0SubscribersSubscribe

Archive

Most Popular

EWWW Image Optimizerの設定方法と使い方【Webp対応】

画像のサイズが大きいことによる速度インデックスの低下に対して画像のサイズを圧縮する「EWWW Image Optimizer」での対策を説明します。

【ブログ運営報告】雑記ブログ5ヶ月目のPV数・収益【WordPress】

雑記ブログ UMilCL を開始して5か月が経過しました。 ただの技術系の会社員が雑記ブログを始めると、5か月でアクセス数・収益がどのようになるか、初心者ブロガーが参考にするために見ていただければと思います。定期報告4回目となります。

【7月1日営業再開】ディズニーシー休止対象と営業予定一覧

私もぜひ行きたいのですが、休止対象設備だけ紹介されており、私みたいなディズニー初級者は何が営業しているのか全然わからなかったので、東京ディズニーシーの営業中と休止予定のアトラクション、ショップ、レストラン、ショーを纏めてみました。

WordPressでブログを100記事書くとPV・収益はどの程度になるか?

私も実際にWordPressで雑記ブログを始めて、本記事を合わせて100記事となりました。陸の孤島と呼ばれるWordPressで独自ドメインを新規で取得し、4か月半で100記事を書くとPV・収益はどの程度になるかについて解説していきたいと思います。

OPPO IDの登録方法と登録した際にできること

OPPO IDとは、テーマストア、オンラインストレージ拡張、データ復元などOPPO端末にプリインストールされているOPPO独自のアプリ・機能を利用する際に必要になります。本記事ではOPPO Reno A等のOPPOスマートフォンの設定を行う際に「OPPO ID」の登録・設定について、どのような情報を要求されており、手順はどのようになっているかについて説明します。