Optie研

パソコンで絵や動画を作る方法について

Python3 & OpenCV で画像処理を学ぶ[0] 〜 OpenCVの導入

 これから何回かに分けて、Python3でOpenCVライブラリを使用しながら画像処理を僕が学んでいきたいと思います。 目標としては、「OpenCVでできること&その書き方を概略的に把握し、未知の実装も適当に調べれば書ける状態になること」といちおう定義しておきます。

1. 概要

1.1 動機

 画像処理ができたら……嬉しい!
 画像処理についてきちんと学んだ経験はありませんが、今のところ「特徴量抽出、(画素|領域|周波数)単位での(操作|変換|フィルタリング)、パターン認識」といったあたりに関心を持っています。

1.2 なぜPython

 Pythonが僕にとって一番慣れた言語であること、numpyなどとの連携ができること、jupyter が使えるとうれしい等といった理由があります。

1.3 これからやること

 今回は、OpenCVの初歩的な使い方を手を動かしながら覚えます。 具体的には、公式チュートリアルGui Features in OpenCV を(適宜省略などしつつ)進めます。
 適宜コメントをつけて理解を深めますが、コードはほぼ写経です。

 また、Python, numpy, matplotlibの書き方に関する基本的な知識と、CG映像ソフトのユーザー程度のディジタル画像に関する知識(R,G,Bチャンネルの数値を持つピクセルという概念や、ディジタル画像はピクセルの配列だと理解していることなど)は仮定します。

今回使用した環境は以下の通りです。

- MacOS High Sierra 10.13.3
- Python 3.6.2
- OpenCV 3.3.0_3
- Jupyterlab 0.27.0

諸々のインストールが完了しているところから話を始めます。

 それでは、やっていきます。必要ライブラリをインポートし、サンプルとして使用する素材のパスを変数にしておきます。

import cv2
import numpy as np
from matplotlib import pyplot as plt

%matplotlib inline  #jupyterなので

IMAGE_PATH = 'mopemope_title.jpg'
MOV_PATH = 'Lyrith_170906.mp4'

2. 画像の入出力と表示

 画像は cv2.imread(path, mode) 関数で読み込めるようです。画像なので、ピクセルの二重配列が返り値です。 カラーの場合は、横 * 縦 * [B,G,R]の三重配列になります。OpenCVでは、 Red, Green Blue ではなく Blue, Green, Red の順番のようです。 可視光線の短波長(紫外線方向)から長波長(赤外線方向)の順の並びなので、こちらの方が自然ということでしょうか。

画像の書き込みはcv2.imwrite(path, image) となります。

 「画像を読み込んでopenCVのウィンドウで表示し、sキーが入力されれば画像を保存する」コードが以下になります。

# 第二引数は1:RGB, 0:GrayScale, -1:RGBA
img = cv2.imread(IMAGE_PATH, 0)

# 第二引数 cv2.WINDOW_NORMAL : ウィンドウをリサイズできるようにする
cv2.namedWindow('image', cv2.WINDOW_NORMAL)

cv2.imshow('image',img)
k = cv2.waitKey(0) # any key inputを無限に待つ

if k==27:  #ESC
    cv2.destroyAllWindows()
elif k == ord('s'):  #sキー
    cv2.imwrite(OUT_IMAGE_PATH,img)
    cv2.destroyAllWindows()

出力:

f:id:Optie_f:20180212203215p:plain:w400

無事に、グレースケールで読み込んだもぺもぺのタイトル画像が表示されました。

次に、画像をmatplotlibで表示してみます。

img = cv2.imread(IMAGE_PATH, 0)
plt.imshow(img, cmap='gray',interpolation='bicubic')
# plt.xticks([]), plt.yticks([])  #目盛りをなくす
plt.show()

出力:

f:id:Optie_f:20180212203140p:plain

 問題なく表示できました。 このあたりは直感的でわかりやすいですね。

3. 映像の入出力と表示

 映像の入力の場合、 VideoCapture オブジェクトを呼び、.read() メソッドでフレームを一枚づつ読んでいくようです。 .read() メソッドは返り値として、フレーム「フレームがあるかどうか」の真偽値(終了判定) を持つため、 これを利用してループを回し、フレームを一枚づつ読み書きするという形になります。

 映像を出力する際は、 VideoWriter オブジェクトを使用します。インスタンスを作るときに、cv2.VideoWriter(filename, codec, fps, frame_size) という形で書き出し形式を引数で与えます。

 コーデックを選択するときなのですが、 Video Codecs by FOURCC - fourcc.org にある4バイトの識別コードを cv2.VideoWriter_fourcc() 関数に渡した返り値を用いるようです。今回 ANIM を利用しましたが、先ほどのページを見ても説明がなく、ANIMが実際何なのかはよくわからないのですが、これを.movに与えたものがQuickTime Animationに相当するのではないかと推察されます(雑)。また、cv2.VideoWriter_fourcc(*'ANIM') ないし cv2.VideoWriter_fourcc('A','N','I','M')と書く必要があるそうです。*'ANIM' という表現は何なのでしょうか、ポインタではないような……?

 VideoWriterオブジェクトの .write(frame) メソッドに、実際に保存したいフレームを次々に与えることで書き出されるようです。

 以下は、リリスの動画を読み込んで上下反転させて表示し、指定したファイルに書き込むコードです。

cap = cv2.VideoCapture(MOV_PATH)

#codec  http://www.fourcc.org/codecs.php
fourcc = cv2.VideoWriter_fourcc(*'ANIM')

#VideoWriterObj. args: name, codec, fps, frame size
out = cv2.VideoWriter('output.mov',fourcc, 20.0, (1920,1080)) 

while(cap.isOpened()):
    #ret : True/False(EOF判定), frame : 各フレーム
    ret, frame = cap.read()
    if ret:  #終了フレームまで
        frame = cv2.flip(frame,0)  #上下反転
        
        out.write(frame)

        cv2.imshow('frame', frame)
        
        #waitKey(ms)を挟み, キー入力 q がなければ引き続きwhileループを継続
        if cv2.waitKey(1) & 0xFF ==ord('q'):
            break
    else:
        break

cap.release()
out.release()
cv2.destroyAllWindows()

出力:

f:id:Optie_f:20180212204054j:plain:w400

上下が反転した迷宮リリスが表示されました。

4. 基本的な描画機能

 図形の描画です。はじめに背景となる画像(配列)を用意し、そこに図形が表現されるように配列の数値を書き換えていくという流れになります。 試しに黒平面に対角線を引いてみます。

# 0 埋めされたwidth * height * (B,G,R)の三重配列 (黒平面)
img = np.zeros((512,512,3), np.uint8)

# 線を引く args: array, start, end, (B,G,R), thickness 
# 返り値はimg配列の値を操作したような配列
img = cv2.line(img, (0,0), (511,511), (255,0,0), 5)

 これをmatplotlibで表示してみます。 先ほども述べたように、OpenCV上ではあくまで(B,G,R)として扱われているので、(R,G,B)で解釈するmatplotlibのためにcv2.cvtColor(img, cv2.COLOR_BGR2RGB)と変換して渡してあげます。

#matplotlibは(R,G,B)で解釈するので、変換して渡す
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.show()

出力:

f:id:Optie_f:20180212203437p:plain

このように描画されました。

以下、基本的な図形の描画です。長方形、円、楕円はコードのコメントを見てください。

cv2.polylines(img, [pts], isClosed, color, thickness)は与えられた座標を結ぶ線分を描画するのですが、 座標の与え方として、  [ [[x_1, y_1]], [[x_2, y_2]], ...] のnumpy配列を、さらに[]でくくって一次元リストの要素にしたものでないといけないようです。これは何故なのでしょう。

# 長方形 args: array, ↖︎, ↘︎, (B,G,R), thickness 
img = cv2.rectangle(img, (350, 350), (400, 500), (0,155,155), 10)

# 円 args: array, 中心, 半径, (B, G,R), thickness. 
# 閉じた図形のthicknessに-1 を与えるとfillとなる
img = cv2.circle(img, (447, 63), 63, (0,0,155), -1)

#楕円 args: array, 中心, (長半径, 短半径), angle, startAngle, endAngle, Color, thickness
# θ ∈ [startAngle, endAngle] の弧を作り、その後にangleで図形自体を回転させる
img = cv2.ellipse(img, (256,256), (100, 50), 45, 30, 210, (255,155,0), 10)

pts = np.array([[10,5], [200,30], [70, 200], [50, 100]], np.int32)
pts = pts.reshape((-1,1,2))
#折れ線 args: array, 点群, isClosed, Color, thickness
img = cv2.polylines(img, [pts], False, (0,255,255), 5)

テキストは以下のように書けます。HERSHEYというフォントのファミリーしか使えない

font = cv2.FONT_HERSHEY_SCRIPT_COMPLEX

# args : array, text, ↙︎, fontFace, font_Scale, Color, thickness, linetype(アンチエイリアス)
img = cv2.putText(img, 'OptieCV', (10,300), font, 4, (255,255,255), 2, cv2.LINE_AA)

以上の描画結果を表示します。

plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.show()

出力:

f:id:Optie_f:20180212203519p:plain

先ほどの対角線の上に、長方形,円,楕円,折れ線,テキストが描画されました。

こうして、OpenCVで基本的な図形を描画する方法をわかることができました。

5. GUIトラックバーの作成

 ユーザーがインタラクティブに値を操作できるスライダーを実装します。 まず cv2.namedWindow(window_name) 関数でウィンドウを定義し(名前をつけて)、cv2.createTrackbar(Trackbar_label, window_name, 初期値, 最大値, callback_func) 関数でウィンドウにトラックバーを割り当てます。callback_func はバーを動かすたびに呼ばれる関数です。何もないときは pass するだけの関数を渡しておけば良いです。

以下、R,G,Bそれぞれの値を操作可能なスライダーを与えて、その色を表示するウィンドウを作るコードです。

def nothing(x):
    pass

img = np.zeros((300, 512, 3), np.uint8)

cv2.namedWindow('image')

# args: label, window_name, min, max, callback_func
cv2.createTrackbar('R', 'image', 0, 255, nothing)
cv2.createTrackbar('G', 'image', 0, 255, nothing)
cv2.createTrackbar('B', 'image', 0, 255, nothing)

switch = '0 : OFF \n 1 : ON'
cv2.createTrackbar(switch, 'image', 0, 1, nothing)

while(1):
    cv2.imshow('image',img)
    k = cv2.waitKey(1) & 0xFF
    if k==27:
        break
    
    r = cv2.getTrackbarPos('R','image')
    g = cv2.getTrackbarPos('G','image')
    b = cv2.getTrackbarPos('B','image')
    s = cv2.getTrackbarPos(switch,'image')
    
    if s==0:
        img[:] = 0
    else:
        img[:] = [b, g, r]

cv2.destroyAllWindows()

出力:

f:id:Optie_f:20180212214741p:plain:w400

 ……チュートリアル通りのコードのはずなのですが、目盛りがおかしいことと、switchの改行が見切れていること、バーの現在の値が表示されていませんね。 一応、ちゃんと動いてはいるので今回は良しとします。

 動作も重いので、インタラクティブに操作をしたい場合は jupyter 側で ipywidgets などを導入した方が良いかもしれません。

6. まとめ

 今回は、OpenCVによる画像/映像の入出力と取り扱い、図形の描画に関して導入することができました。 今のところOpenCVGUI操作を利用する予定はないので、次回以降の方針として、画像への操作・変換・フィルタリングを重点的に見ていきたいと思います。

7. 参考文献

Gui Features in OpenCV — OpenCV-Python Tutorials 1 documentation

https://docs.opencv.org/2.4/modules/core/doc/drawing_functions.html#ellipse

[Python]PythonでOpenCVを使う (基本編) - Qiita