OpenCV と Python を使ったアニメーション動画用紙のスキャン画像位置合わせ

OpenCV と Python を使って、下のようなアニメ動画用紙の位置合わせ穴を検出し、スキャン画像の位置ずれを除くツールを作ってみました。OpenCV と Python 初心者向けの実用的な記事です。[OpenCVについてはこちらなどを参照](http://www.buildinsider.net/small/opencv/001)
![peascan.gif](https://qiita-image-store.s3.amazonaws.com/0/67734/5124f0bd-5aad-a82a-c258-6cc57560065e.gif)

# 解決したい課題
アニメーションを作るときに、[9VAeきゅうべえのような２Ｄキーフレームアプリ](http://qiita.com/danjiro/items/7fa584a4336cda9c0a05)を使えば簡単ですが、１枚ずつ動画用紙に描くとすると数１００枚以上の大変な枚数になります。これをオートフィーダつきスキャナでパソコンに入力すると、どうしても数ドットの位置ずれが生じ、再生したときに画像がぶれてしまいます。

そこで動画用紙の上にある穴を検出し、画像処理で位置ずれを補正するツール「Peascan.py」を作ってみましょう。

|入力|位置ずれがある連番 JPEG 画像 数１００枚|
|:--|:--|
|出力|位置ずれのない連番 JPEG 画像 数１００枚|
|環境|Windows|

穴の認識や画像の回転、位置ずれの補正は、OpenCV を使えば簡単にできるはずです。 

# １．OpenCV と Python のインストール

- 誰でも使えるように、インストールが簡単な Python(x,y)を使うことにしました。OpenCV と Python を同時にインストールできます。

|手順|内容|補足|
|:--|:--|:--|
|1. ダウンロード|[http://python-xy.github.io/downloads.html](http://python-xy.github.io/downloads.html) から Python(x,y)-2.7.10.0.exe|[詳しいインストール方法はこちら](http://windows.blogo.jp/programming/pythonxy_install)|
|2. インストール|「Next」ボタンをクリック<br>「Choose Components」画面では、「Custom」の右側の「↓」をクリックし「**Full**」をクリック。あとは「Next」「Install」|Custom の中をひらいて、OpenCVにチェックを入れてもよいです。|

インストールされるのは、Python 2.7.10, OpenCV 2.4.12 と少し前のバージョンですが、問題なく使えます。

# ２．画像を読み込んで表示

## 2-1. Python プログラムの作成

 簡単な Python + OpenCV のプログラムを作ってみましょう。

|項目|ポイント|
|:--|:--|
|文字コード|「UTF8」。メモ帳で保存する場合、「ファイル」＞「名前を付けて保存」＞下の「文字コード」を「UTF-8」にして保存。|
|拡張子|.py|
|実行|コマンドプロンプトから、`python xxx.py`|

以下のテキストを、「test.py」という名前で「UTF-8」で保存してください。パスに日本語がはいると動作しないことがあるので、`c:\pytest` といったフォルダを作って保存するとよいでしょう。xxサンプル画像xx のところには、適当な画像ファイルを自分で用意してパス名を入れてください（c:/pytest/test.jpg など）。日本語名は使えません。パスの区切は「/」です。

```py
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')|ファイル、パス名に日本語は使えない、パスの区切は「/」|
|変数表示|print|「,」で区切って並べるとなんでも表示|
|画像サイズ|img.shape|画像imgの(高さ、幅、チャンネル数)|
|画像表示|cv2.imshow('Title',img)|ウィンドウで画像imgを表示|
|一時停止|cv2.waitKey(5000)|５秒停止、0 ならキー入力待ち|

## 2-2. プログラム実行
1. 「test.py」のはいったフォルダを開く
2. 何もないところで、右ボタンをクリック＞「Open IPython console here」をクリック<br>これで、Python用ターミナルが開きます。
3. `python test.py` と入力して「Enter」
4. 画像ファイルの指定が正しければ、画像が５秒間表示されます。もし「None」「AttributeError」と表示されたら、(1) 画像のはいった場所やファイル名に日本語がはいっていないか、(2) パスの区切が「￥」になっていないか（「/」にしなければならない）、(3)ファイル名が違っていないか確かめてください。



# ３．画像の左上のマーク（穴）を２値化して重心を求める

画像表示ができたら「test.py」の「img = 」の次の行から以下のように書き換えてみましょう。画像の左上(0,0)-(200,200)の範囲にマークがあるとして、その重心を求めます。frmX,toX、frmY,toY の値は画像の大きさに合わせて調整してください。

```py
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) #２値化
cv2.imshow('out',bin) #マークの範囲
cv2.waitKey(1000) #1秒停止
contours, hierarchy = cv2.findContours(bin, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) #輪郭の抽出
cnt = contours[0] #１つめの輪郭
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) #５秒表示

```

|項目|使用例|説明|
|:--|:--|:--|
|複数の同時代入|frmX,toX = 200,600|frmX=200 toX=600と同じ<br>１つの関数で複数の値を代入できる|
|部分画像| img[frmY:toY, frmX:toX]|imgの(frmX,frmY)-(toX,toY)を取り出す|
|モノクロ化| gray = cv2.cvtColor(mark, cv2.COLOR_BGR2GRAY) |カラー画像 markからモノクロ grayを作成|
|２値化| ret, bin = cv2.threshold(gray, 127, 255, 0)|grayを２値化して bin を作成|
|白黒反転| `bin = ~bin`| 配列 bin 全体に Not 演算|
|輪郭の抽出|contours, hierarchy = cv2.findContours(bin, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)|contours に輪郭がはいる|
|輪郭の重心|M = cv2.moments(cnt) <br> cx = int(M['m10']/M['m00']) <br> cy = int(M['m01']/M['m00'])|M['m00']は輪郭の面積|
|円を描く|cv2.circle(img,(cx,cy), 10, (0,0,255), -1)|半径10ドットの赤い円を描く|

# ４．左上、右上にあるマーク（穴）の重心を求め、基準の画像の重心に一致するよう変形する

「test.py」を以下のように書き換えましょう。画像の左上と右上の 200x200 の範囲にあるマーク（穴）の重心を検出し、基準の画像の位置にあわせるように変形します。

```py
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) #２値化
	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 #右上のマーク（穴）が下に１０ドットずれていたとする 
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) #５秒表示
cv2.imshow('Title',dst) #変換後の画像を表示
cv2.waitKey(5000) #５秒表示

```

|項目|使用例|説明|
|:--|:--|:--|
|関数定義|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 は３点pts1をpts2に対応させる変換係数|
|アフィン変換|dst = cv2.warpAffine(img,M,(width,height))|画像imgを変換して画像dstを作成|

実行すると、わざとずらせた青い円が、元の赤い円に重なるように画像が変換されます。これでマーク（穴）の検出と変換処理は完成です。

# ５．フォルダの中の画像を読み込んで、位置合わせした画像を別のフォルダに出力する

基本処理が完成したので、次のような使い方にしましょう。これでフォルダの中に数100枚の画像がはいっていても、簡単に変換できます。このツールは「peascan.py」と名付けました。（Position Error correction After SCAN の略）


|使い方|画像がはいったフォルダを「peascan.py」アイコンの上にドラッグすると画像と同じ場所に「out」というフォルダを作成し、同じ画像の名前で修正結果を入れる|
|:--|:--|

位置合わせマーク（穴）の範囲 frmX,toX=, frmY,toY= 以降の数字は、実際の画像にあわせて、適宜、調整してください。

```py:peascan.py
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) #２値化
	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: #２番目以降の画像は最初の画像にあわせてアフィン変換
		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) #５秒表示

```

|項目|使用例|説明|
|:--|:--|:--|
|コマンド引数|import sys<br>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]の最後から２つめまで取り出す|
|ファイル/フォルダ名|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で書かれているため、簡単に修正できます。ぜひ、ご利用ください。

# ６．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)<br>出力は２個|
|描画|img = cv2.circle(img, center, radius,(0,255,0),2)|cv2.circle(img, center, radius, (0,255,0),2)<br>出力はなし、imgに直接描画される|


---
# 関連記事

1. [無料ソフトでアニメを作ってみよう（9VAe きゅうべえ）](http://qiita.com/danjiro/items/253e5a33a38599098274)
- [スクラッチ、ビスケットの次は 9VAe＝第３のプログラミング学習環境](http://qiita.com/danjiro/items/d856d393b3e138af123a)
6. [書き順アニメーションの作り方](http://qiita.com/danjiro/items/8dd0d7c7263f62189437)
7. [9VAeきゅうべえ：長いアニメを作る方法](http://qiita.com/danjiro/items/020cd885edb5eb3d9fb0)
- [動くLINEスタンプのAPNG作成：無料ソフト9VAeきゅうべえ](http://qiita.com/danjiro/items/e4d9eb2b22f90ebd1e91)
- [9VAeきゅうべえ」で絵を描かずに作れるGIFアニメ](http://qiita.com/danjiro/items/7fa584a4336cda9c0a05)
- [9VAeきゅうべえで作成したSVGアニメーション](http://qiita.com/danjiro/items/1b4f6b5a520ade762116)
-  [アニメソフト 9VAe をカスタマイズする方法](http://qiita.com/danjiro/items/c5dc07671ffdbbb8a9a9)
- [9VAeをキッズプラザ大阪向けに改造する](http://qiita.com/danjiro/items/bbb10e07a9775a938200)
- [9VAe / 9svg データフォーマット解説](http://qiita.com/danjiro/items/0d4ac334a7a97ddb4fe2)
- [OpenCV と Python を使ったアニメーション動画用紙のスキャン画像位置合わせ](http://qiita.com/danjiro/items/985afccd07722d4c21e9)