13
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-03-28

OpenCV と Python を使って、下のようなアニメ動画用紙の位置合わせ穴を検出し、スキャン画像の位置ずれを除くツールを作ってみました。OpenCV と Python 初心者向けの実用的な記事です。OpenCVについてはこちらなどを参照
peascan.gif

解決したい課題

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

2-2. プログラム実行

  1. 「test.py」のはいったフォルダを開く
  2. 何もないところで、右ボタンをクリック>「Open IPython console here」をクリック
    これで、Python用ターミナルが開きます。
  3. python test.py と入力して「Enter」
  4. 画像ファイルの指定が正しければ、画像が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= 以降の数字は、実際の画像にあわせて、適宜、調整してください。

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) #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に直接描画される

関連記事

  1. 無料ソフトでアニメを作ってみよう(9VAe きゅうべえ)
  1. 書き順アニメーションの作り方
  2. 9VAeきゅうべえ:長いアニメを作る方法
13
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?