OpenCV と Python を使って、下のようなアニメ動画用紙の位置合わせ穴を検出し、スキャン画像の位置ずれを除くツールを作ってみました。OpenCV と Python 初心者向けの実用的な記事です。OpenCVについてはこちらなどを参照
解決したい課題
アニメーションを作るときに、9VAeきゅうべえのような2Dキーフレームアプリを使えば簡単ですが、1枚ずつ動画用紙に描くとすると数100枚以上の大変な枚数になります。これをオートフィーダつきスキャナでパソコンに入力すると、どうしても数ドットの位置ずれが生じ、再生したときに画像がぶれてしまいます。
そこで動画用紙の上にある穴を検出し、画像処理で位置ずれを補正するツール「Peascan.py」を作ってみましょう。
入力 | 位置ずれがある連番 JPEG 画像 数100枚 |
---|---|
出力 | 位置ずれのない連番 JPEG 画像 数100枚 |
環境 | Windows |
穴の認識や画像の回転、位置ずれの補正は、OpenCV を使えば簡単にできるはずです。
1.OpenCV と Python のインストール
- 誰でも使えるように、インストールが簡単な Python(x,y)を使うことにしました。OpenCV と Python を同時にインストールできます。
手順 | 内容 | 補足 |
---|---|---|
1. ダウンロード | http://python-xy.github.io/downloads.html から Python(x,y)-2.7.10.0.exe | 詳しいインストール方法はこちら |
2. インストール | 「Next」ボタンをクリック 「Choose Components」画面では、「Custom」の右側の「↓」をクリックし「Full」をクリック。あとは「Next」「Install」 |
Custom の中をひらいて、OpenCVにチェックを入れてもよいです。 |
インストールされるのは、Python 2.7.10, OpenCV 2.4.12 と少し前のバージョンですが、問題なく使えます。
2.画像を読み込んで表示
2-1. Python プログラムの作成
簡単な Python + OpenCV のプログラムを作ってみましょう。
項目 | ポイント |
---|---|
文字コード | 「UTF8」。メモ帳で保存する場合、「ファイル」>「名前を付けて保存」>下の「文字コード」を「UTF-8」にして保存。 |
拡張子 | .py |
実行 | コマンドプロンプトから、python xxx.py
|
以下のテキストを、「test.py」という名前で「UTF-8」で保存してください。パスに日本語がはいると動作しないことがあるので、c:\pytest
といったフォルダを作って保存するとよいでしょう。xxサンプル画像xx のところには、適当な画像ファイルを自分で用意してパス名を入れてください(c:/pytest/test.jpg など)。日本語名は使えません。パスの区切は「/」です。
import numpy as np
import cv2
img = cv2.imread('C:/xxサンプル画像xx.jpg')
print img.shape
print img.shape[0], img.shape[1]
cv2.imshow('Title',img)
cv2.waitKey(5000)
それぞれ、以下の意味です。
項目 | 使用例 | 説明 |
---|---|---|
数値計算 | import numpy as np | 数値計算ライブラリを使う |
画像処理 | import cv2 | OpenCV 画像処理ライブラリを使う |
画像を読み込む | img = cv2.imread('C:/サンプル画像.jpg') | ファイル、パス名に日本語は使えない、パスの区切は「/」 |
変数表示 | 「,」で区切って並べるとなんでも表示 | |
画像サイズ | img.shape | 画像imgの(高さ、幅、チャンネル数) |
画像表示 | cv2.imshow('Title',img) | ウィンドウで画像imgを表示 |
一時停止 | cv2.waitKey(5000) | 5秒停止、0 ならキー入力待ち |
2-2. プログラム実行
- 「test.py」のはいったフォルダを開く
- 何もないところで、右ボタンをクリック>「Open IPython console here」をクリック
これで、Python用ターミナルが開きます。 -
python test.py
と入力して「Enter」 - 画像ファイルの指定が正しければ、画像が5秒間表示されます。もし「None」「AttributeError」と表示されたら、(1) 画像のはいった場所やファイル名に日本語がはいっていないか、(2) パスの区切が「¥」になっていないか(「/」にしなければならない)、(3)ファイル名が違っていないか確かめてください。
3.画像の左上のマーク(穴)を2値化して重心を求める
画像表示ができたら「test.py」の「img = 」の次の行から以下のように書き換えてみましょう。画像の左上(0,0)-(200,200)の範囲にマークがあるとして、その重心を求めます。frmX,toX、frmY,toY の値は画像の大きさに合わせて調整してください。
import numpy as np
import cv2
img = cv2.imread('C:/xxサンプル画像xx.jpg')
frmX,toX = 0,200 # マーク(穴)の範囲
frmY,toY = 0,200 # マーク(穴)の範囲
mark = img[frmY:toY, frmX:toX] #部分画像
gray = cv2.cvtColor(mark, cv2.COLOR_BGR2GRAY) #モノクロ化
ret, bin = cv2.threshold(gray, 127, 255, 0) #2値化
cv2.imshow('out',bin) #マークの範囲
cv2.waitKey(1000) #1秒停止
contours, hierarchy = cv2.findContours(bin, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) #輪郭の抽出
cnt = contours[0] #1つめの輪郭
M = cv2.moments(cnt) #モーメント
cx = int(M['m10']/M['m00']) #重心X
cy = int(M['m01']/M['m00']) #重心Y
cv2.circle(img,(cx,cy), 10, (0,0,255), -1)
print cx,cy
cv2.imshow('Title',img)
cv2.waitKey(5000) #5秒表示
項目 | 使用例 | 説明 |
---|---|---|
複数の同時代入 | frmX,toX = 200,600 | frmX=200 toX=600と同じ 1つの関数で複数の値を代入できる |
部分画像 | img[frmY:toY, frmX:toX] | imgの(frmX,frmY)-(toX,toY)を取り出す |
モノクロ化 | gray = cv2.cvtColor(mark, cv2.COLOR_BGR2GRAY) | カラー画像 markからモノクロ grayを作成 |
2値化 | ret, bin = cv2.threshold(gray, 127, 255, 0) | grayを2値化して bin を作成 |
白黒反転 | bin = ~bin |
配列 bin 全体に Not 演算 |
輪郭の抽出 | contours, hierarchy = cv2.findContours(bin, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) | contours に輪郭がはいる |
輪郭の重心 | M = cv2.moments(cnt) cx = int(M['m10']/M['m00']) cy = int(M['m01']/M['m00']) |
M['m00']は輪郭の面積 |
円を描く | cv2.circle(img,(cx,cy), 10, (0,0,255), -1) | 半径10ドットの赤い円を描く |
4.左上、右上にあるマーク(穴)の重心を求め、基準の画像の重心に一致するよう変形する
「test.py」を以下のように書き換えましょう。画像の左上と右上の 200x200 の範囲にあるマーク(穴)の重心を検出し、基準の画像の位置にあわせるように変形します。
import numpy as np
import cv2
img = cv2.imread('C:/xxサンプル画像xx.jpg')
frmX,toX = 0,200 # マーク(穴)の範囲
frmY,toY = 0,200 # マーク(穴)の範囲
def searchMark(img, left): #マーク(穴)を探す関数 left==1は左
if left==1: #左側のマーク(穴)を探す
mark = img[frmY:toY, frmX:toX]
else: #右側のマーク(穴)を探す
mark = img[frmY:toY, img.shape[1]-toX:img.shape[1]-frmX]
gray = cv2.cvtColor(mark, cv2.COLOR_BGR2GRAY) #モノクロ化
ret, bin = cv2.threshold(gray, 127, 255, 0) #2値化
cv2.imshow('out',bin) #マーク(穴)の範囲を表示
cv2.waitKey(1000) #1秒停止
contours, hierarchy = cv2.findContours(bin, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) #輪郭の抽出
ax = ay = sum = 0. #全体の重心の積算
for cnt in contours: #全体の重心を求める
M = cv2.moments(cnt)
ax += M['m10']
ay += M['m01']
sum += cv2.contourArea(cnt)
if left==1:
cx = ax/sum+frmX
cy = ay/sum+frmY
else:
cx = ax/sum + img.shape[1]-toX
cy = ay/sum + frmY
cv2.circle(img,(int(cx),int(cy)), 10, (0,0,255), -1) #重心に赤い円を描く
print cx,cy #求めた重心を表示
return cx,cy #関数の戻り値
# アフィン変換テスト
cx0,cy0 = searchMark(img,1) # 左上のマーク(穴)の重心(基準)
dx0,dy0 = searchMark(img,0) # 右上のマーク(穴)の重心(基準)
cx1,cy1 = cx0,cy0
dx1,dy1 = dx0,dy0+10 #右上のマーク(穴)が下に10ドットずれていたとする
cv2.circle(img,(int(dx1),int(dy1)), 10, (255,0,0), -1) #ずれた点に青い円を描く
pts2 = np.float32([[cx0,cy0],[dx0,dy0],[cx0-(dy0-cy0),cy0+(dx0-cx0)]])
pts1 = np.float32([[cx1,cy1],[dx1,dy1],[cx1-(dy1-cy1),cy1+(dx1-cx1)]])
height,width,ch = img.shape
M = cv2.getAffineTransform(pts1,pts2)
dst = cv2.warpAffine(img,M,(width,height))
cv2.imshow('Title',img) #変換前の画像を表示
cv2.waitKey(5000) #5秒表示
cv2.imshow('Title',dst) #変換後の画像を表示
cv2.waitKey(5000) #5秒表示
項目 | 使用例 | 説明 |
---|---|---|
関数定義 | def searchMark(img, left): | 関数の中は、一段下げる。return cx,cy のように複数の値を返せる |
If文 | if left==1: else: | If文の中は、一段下げる。 |
Forループ | for cnt in contours: | contoursの中身を全部実行。Forの中は、一段下げる。 |
輪郭の面積 | cv2.contourArea(cnt) | M['m00']と同じ値 |
アフィン変換係数 | M = cv2.getAffineTransform(pts1,pts2) | M は3点pts1をpts2に対応させる変換係数 |
アフィン変換 | dst = cv2.warpAffine(img,M,(width,height)) | 画像imgを変換して画像dstを作成 |
実行すると、わざとずらせた青い円が、元の赤い円に重なるように画像が変換されます。これでマーク(穴)の検出と変換処理は完成です。
5.フォルダの中の画像を読み込んで、位置合わせした画像を別のフォルダに出力する
基本処理が完成したので、次のような使い方にしましょう。これでフォルダの中に数100枚の画像がはいっていても、簡単に変換できます。このツールは「peascan.py」と名付けました。(Position Error correction After SCAN の略)
使い方 | 画像がはいったフォルダを「peascan.py」アイコンの上にドラッグすると画像と同じ場所に「out」というフォルダを作成し、同じ画像の名前で修正結果を入れる |
---|
位置合わせマーク(穴)の範囲 frmX,toX=, frmY,toY= 以降の数字は、実際の画像にあわせて、適宜、調整してください。
import numpy as np
import cv2
import sys # argv の取得
import os # ファイル操作
argv = sys.argv # コマンドライン引数を格納したリストの取得
argc = len(argv) # 引数の個数
if argc == 2: # フォルダかどうか調べる
if os.path.isdir(argv[1]) != True: #フォルダでない場合
argc = -1 #エラーにする
if argc != 2: # 使い方を表示
print 'Usage: Drag Image folder onto this icon.'
key = raw_input('Hit Enter')
quit() # 終了
# 位置合わせのマーク(穴)を調べる範囲 ★適宜調整してください★
frmX,toX = 10,200 # 水平方向端から 10 - 200 ドット(左右対称)
frmY,toY = 10,200 # 上から 10 - 200 ドット
def searchMark(img, left): #マーク(穴)を探す関数 left==1は左
if left==1: #左側のマーク(穴)を探す
mark = img[frmY:toY, frmX:toX]
else: #右側のマーク(穴)を探す
mark = img[frmY:toY, img.shape[1]-toX:img.shape[1]-frmX]
gray = cv2.cvtColor(mark, cv2.COLOR_BGR2GRAY) #モノクロ化
ret, bin = cv2.threshold(gray, 127, 255, 0) #2値化
cv2.imshow('out',bin) #マーク(穴)の範囲を表示
cv2.waitKey(1000) #1秒停止
contours, hierarchy = cv2.findContours(bin, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) #輪郭の抽出
ax = ay = sum = 0. #全体の重心の積算
for cnt in contours: #全体の重心を求める
M = cv2.moments(cnt)
ax += M['m10']
ay += M['m01']
sum += cv2.contourArea(cnt)
if left==1:
cx = ax/sum+frmX
cy = ay/sum+frmY
else:
cx = ax/sum + img.shape[1]-toX
cy = ay/sum + frmY
cv2.circle(img,(int(cx),int(cy)), 10, (0,0,255), -1) #重心に赤い円を描く
print cx,cy #求めた重心を表示
return cx,cy #関数の戻り値
# メインループ
inpFolder = argv[1] #入力画像フォルダ
parent = os.path.dirname(argv[1])
outFolder = os.path.join(parent,'out') #出力画像フォルダ
if os.path.exists(outFolder) != True: #存在しない場合
os.mkdir(outFolder) #出力フォルダを作成
files = [f for f in os.listdir(inpFolder)] #入力画像
files.sort(key=os.path.basename) #ファイル名でソート
cx0 = -1
for fn in files:
img = cv2.imread(os.path.join(inpFolder,fn))
if img is None: #読めなかったので次へ
continue
if cx0 == -1: #最初の画像はそのまま記憶
cx0,cy0=searchMark(img,1)
dx0,dy0=searchMark(img,0)
cv2.imwrite(os.path.join(outFolder,fn), img)
else: #2番目以降の画像は最初の画像にあわせてアフィン変換
cx1,cy1=searchMark(img,1)
dx1,dy1=searchMark(img,0)
pts2 = np.float32([[cx0,cy0],[dx0,dy0],[cx0-(dy0-cy0),cy0+(dx0-cx0)]])
pts1 = np.float32([[cx1,cy1],[dx1,dy1],[cx1-(dy1-cy1),cy1+(dx1-cx1)]])
height,width,ch = img.shape
M = cv2.getAffineTransform(pts1,pts2)
dst = cv2.warpAffine(img,M,(width,height))
cv2.imwrite(os.path.join(outFolder,fn), dst) #画像の書き込み
cv2.imshow('Title',dst) #最後の画像を表示
cv2.waitKey(5000) #5秒表示
項目 | 使用例 | 説明 |
---|---|---|
コマンド引数 | import sys argv = sys.argv |
コマンドライン引数を文字配列に入れる |
ファイル操作 | import os | |
配列の個数 | argc = len(argv) | argc は配列 argv の中身の個数 |
フォルダ判定 | os.path.isdir(argv[1]) | argv[1]がフォルダのパスなら True |
キー入力 | key = raw_input('Hit Enter') | Enterでkeyに文字列がはいる |
終了 | quit() | プログラムを終了する |
親のフォルダ | os.path.dirname(argv[1]) | パスargv[1]の最後から2つめまで取り出す |
ファイル/フォルダ名 | os.path.basename(argv[1]) | パスargv[1]の最後の名前を取り出す |
フォルダとファイルの結合 | os.path.join(parent,'out') | フォルダパス parentにファイル名'out'をつなげる |
存在チェック | os.path.exists(outFolder) | outFolder が存在すれば True |
フォルダ作成 | os.mkdir(outFolder) | フォルダ outFolder を作成 |
ファイルリスト作成 | files = [f for f in os.listdir(inpFolder)] | inpFolderフォルダの全ファイル名が配列 files にはいる |
ファイル名でソート | files.sort(key=os.path.basename) | 配列 files をファイル名でソート |
エラーの場合 | if img is None: | Noneと比較するときは、is か is not を使う |
Forの中断 | continue | For文の中で処理を中断して次に進む |
画像保存 | cv2.imwrite(os.path.join(outFolder,fn), img) | outFolder の中に、fn という名前で、画像 imgを保存 |
- 今回作成したツールは、画像の左上、右上にあるマークの重心を計算しているだけなので、形状はまったく関係ありません。トンボのような十字マークであっても、全画像に同じ形がはいっておれば、位置合わせに利用できます。
- プログラムが、Pythonで書かれているため、簡単に修正できます。ぜひ、ご利用ください。
6.OpenCV 3 と 2.4 の違いに注意
ネット上の OpenCV の情報は、OpenCV 3 のものが多く、OpenCV 2.4 ではエラーが出る場合がありました。
項目 | OpenCV 3 | OpenCV 2.4 |
---|---|---|
ラベリング | nLabels, labelImage = cv2.connectedComponents(bin) | connectedComponentsは使えません |
輪郭抽出 | image, contours, hierarchy = cv2.findContours( thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) | contours, hierarchy = cv2.findContours( bin, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) 出力は2個 |
描画 | img = cv2.circle(img, center, radius,(0,255,0),2) | cv2.circle(img, center, radius, (0,255,0),2) 出力はなし、imgに直接描画される |