CUBE SUGAR CONTAINER

技術系のこと書きます。

Homebrew で OpenCV の Python バインディングを試す

OpenCV はオープンソースのコンピュータビジョンライブラリ。 画像だけでなく動画も処理できたり、それ関連の機械学習アルゴリズムまで実装してたりする。 本体は C++ で書かれているけど Python バインディングもあるので、今回はそれを試してみる。

表題の通り Homebrew を使って OpenCV をインストールするため、環境には Mac OS X を使用している。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.4
BuildVersion:   18E226

OpenCV をインストールする

OpenCV を Homebrew からインストールする。

$ brew install opencv glog

尚、そのままでは OpenCV のパッケージが Python のパスに追加されないので、以下のようにしてパスを通す必要がある。

$ echo /usr/local/opt/opencv/lib/python2.7/site-packages >> /usr/local/lib/python2.7/site-packages/opencv.pth

Python 3 を使っている場合にはバージョンを書き換える。

$ echo /usr/local/opt/opencv/lib/python3.7/site-packages >> /usr/local/lib/python3.7/site-packages/opencv.pth

Homebrew 以外の Python を使っている場合

この場合は、上記に加えて下記の手順も必要となる。

これは例えば pyenv や pythonz といった Python マネージャや、あるいは手動でビルドした Python を使っている場合が該当する。

$ mkdir -p ~/Library/Python/2.7/lib/python/site-packages
$ echo 'import site; site.addsitedir("/usr/local/lib/python2.7/site-packages")' >> ~/Library/Python/2.7/lib/python/site-packages/homebrew.pth

これも使用する Python に併せてバージョンは書き換える。

$ mkdir -p ~/Library/Python/3.7/lib/python/site-packages
$ echo 'import site; site.addsitedir("/usr/local/lib/python3.7/site-packages")' >> ~/Library/Python/3.7/lib/python/site-packages/homebrew.pth

特に、先ほどのインストール手順のように --with-python3 オプションを付けた状態では Homebrew 経由で python3 だけが依存パッケージとしてインストールされる (python は入らない) ようなので注意が必要になる。

virtualenv 環境で使用する場合

もし、virtualenv 環境で OpenCV を使いたいという場合には以下のようにする。

$ echo /usr/local/opt/opencv/lib/python2.7/site-packages >>  $(python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")/opencv.pth

当然これも使用する Python に併せてバージョンを書き換える。

$ echo /usr/local/opt/opencv/lib/python3.7/site-packages >>  $(python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")/opencv.pth

ちなみに、上記のインラインで実行しているコマンドでは現在の Python 実行環境の site-packages ディレクトリを取得している。

$ python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())"
/Users/amedama/.virtualenvs/py37/lib/python3.7/site-packages

また、Homebrew 経由でインストールされた依存パッケージの NumPy にパスが通らなくなるので、改めてインストールが必要になる。

$ pip install numpy

Python から OpenCV をインポートする

次のようにして OpenCV のパッケージ (cv2) がインポートできれば成功。

$ python -c "import cv2;print(cv2.__version__)"
4.1.0

画像を扱う

まずは動作確認用の画像をダウンロードしておく。

$ wget --header="User-Agent:" https://pixabay.com/static/uploads/photo/2014/04/03/11/55/snake-312561_640.png -O image.png

こんなやつ。 f:id:momijiame:20151001214640p:plain 尚、この画像は上記のサイトで CC0 ライセンスで配布されているもの。

Python の REPL を使って動作を確認してみよう。 cv2.imread() 関数でファイルから画像を読み込むことができる。 それを表示する場合には cv2.imshow() 関数を使う。

>>> import cv2
>>> img = cv2.imread('image.png')
>>> cv2.imshow('python', img)

実行すると以下のようにウィンドウが表示されるはず。 f:id:momijiame:20151001214449p:plain

ウィンドウを閉じるには以下のように cv2.destroyWindow() 関数を使う。

>>> cv2.destroyWindow('python')

あるいは cv2.destroyAllWindows() 関数を使っても手っ取り早くていい。

>>> cv2.destroyAllWindows()

ちなみに、読み込まれた画像は NumPy の配列オブジェクトになっている。

>>> type(img)
<type 'numpy.ndarray'>

この配列は、画像の縦横のピクセルを表した二次元に、RGB の要素を加えた三次元になっているようだ。 つまり、OpenCV はデータを上手いことビットマップデータに変換してくれている。

>>> img.ndim
3
>>> len(img)
572
>>> len(img[0])
640
>>> img[0][0]
array([255, 255, 255], dtype=uint8)

なので、例えば手動で明るい (255 になっている) ところをこんな感じに塗りつぶす、みたいなことも楽にできてしまう。

>>> img[img == 255] = 0
>>> cv2.imshow('python', img)

上記を実行すると塗りつぶされたヘビが見える。 f:id:momijiame:20151001215011p:plain

ウィンドウを閉じるには先程と同様の手順を踏む。

>>> cv2.destroyWindow('python')

スクリプトファイルにする

ひとまず、ここまでの内容を Python のスクリプトファイルにすると以下のようになる。 このファイルは、先ほどダウンロードしてきた画像ファイルと同じ場所に作る。 cv2.waitKey() 関数は初登場だけど、これはウィンドウにフォーカスが当たった状態で何らかのキーが押されたら終了させるために入れている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cv2


def main():
    img = cv2.imread('image.png')
    cv2.imshow('python', img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


if __name__ == '__main__':
    main()

実行するには以下のようにする。

$ python helloworld.py

表示される内容は先ほどと変わらないので省略する。

輪郭線を抽出する

OpenCV っぽい感じの機能を試すために、今度は Canny というフィルタを使って輪郭線を抽出してみよう。 使い方は至ってシンプルで、読み込んだ画像データをフィルタに通すだけ。 cv2.imread() 関数の第二引数に 0 を指定することでグレイスケールで読み込んでいる点に注意。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cv2


def main():
    img = cv2.imread('image.png', 0)
    edge_img = cv2.Canny(img, 100, 200)
    cv2.imshow('python', edge_img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


if __name__ == '__main__':
    main()

上記を実行してみる。

$ python edge.py

すると以下のような画像が表示される。 見事に輪郭線が抽出されている。 f:id:momijiame:20151001221120p:plain

動画を扱う

次は動画を扱ってみる。 先ほどと同様に CC0 ライセンスの動画を動作確認用にダウンロードしておく。

$ wget http://mirrors.creativecommons.org/movingimages/Building_on_the_Past.mp4 -O movie.mp4

動画を再生する

動画を扱う場合には cv2.VideoCapture() 関数で読み込む。 動画の情報は cv2.get() 関数経由で手に入れることができる。 読み込んだ動画からは VideoCapture#read() メソッドを使うことで 1 フレーム分のイメージデータ (要するに NumPy 配列) が取り出せる。 なんとシンプル! 再生スピードの制御には、取り出したフレームレートを元に各フレームを表示した後に time.sleep() メソッドでスレッドを休止させている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import division

import time

import cv2


def main():
    video = cv2.VideoCapture('movie.mp4')

    fps = video.get(cv2.CAP_PROP_FPS)

    while video.isOpened():
        success, frame = video.read()

        if not success:
            break

        cv2.imshow('video', frame)

        if cv2.waitKey(1) != -1:
            break

        time.sleep(1 / fps)

    video.release()
    cv2.destroyAllWindows()


if __name__ == '__main__':
    main()

上記を実行する。

$ python movie.py

すると以下のような感じで動画が再生される。 f:id:momijiame:20151001222044p:plain

動画を逆再生する

上記だけでは何だか寂しいので、先ほどの動画を逆再生してみよう。 といっても、やっていることは VideoCapture#set() を使って再生ポジションを切り替えているだけ。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import division

import time

import cv2


def main():
    video = cv2.VideoCapture('movie.mp4')

    fps = video.get(cv2.CAP_PROP_FPS)
    pos = video.get(cv2.CAP_PROP_FRAME_COUNT)
    video.set(cv2.CAP_PROP_POS_FRAMES, pos)

    while video.isOpened():
        success, frame = video.read()

        if not success:
            break

        cv2.imshow('video', frame)

        if cv2.waitKey(1) != -1:
            break

        time.sleep(1 / fps)

        pos = pos - 1
        video.set(cv2.CAP_PROP_POS_FRAMES, pos)

    video.release()
    cv2.destroyAllWindows()


if __name__ == '__main__':
    main()

実行してみる。

$ python reverse_play.py

すると動画が末尾から逆方向に再生される。 f:id:momijiame:20151001222425p:plain

ばっちり。

まとめ

今回は Homebrew でインストールした OpenCV を軽く使ってみた。 OpenCV の Python バインディングでは、画像から取り出したイメージデータが単なる NumPy 配列だったり、動画は 1 フレームずつをイメージデータとして処理できたりと、とてもシンプルで使いやすい API になっていると感じた。