Edited at

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

More than 1 year has passed since last update.

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 きゅうべえ)

  2. スクラッチ、ビスケットの次は 9VAe=第3のプログラミング学習環境

  3. 書き順アニメーションの作り方

  4. 9VAeきゅうべえ:長いアニメを作る方法

  5. 動くLINEスタンプのAPNG作成:無料ソフト9VAeきゅうべえ

  6. 9VAeきゅうべえ」で絵を描かずに作れるGIFアニメ

  7. 9VAeきゅうべえで作成したSVGアニメーション

  8. アニメソフト 9VAe をカスタマイズする方法

  9. 9VAeをキッズプラザ大阪向けに改造する

  10. 9VAe / 9svg データフォーマット解説

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