12
11

More than 3 years have passed since last update.

OpenCVの画像をPygameで描画する方法

Last updated at Posted at 2019-11-04

PythonでOpenCVを動かし、解析・加工した結果を表示したい場合、cv2.imshow()メソッドを使うのが一番簡単です。
しかし、ただ表示するだけ以上のことをしたい場合、OpenCVのウィンドウでは機能不足です。
このような問題を解決する方法の一つとして、PythonのGUIフレームワークであるPygameを使うやり方があります。
この投稿では、OpenCVの画像をPygame向けの画像に変換するやり方について書きます。

環境

ハードウェア
Raspberry Pi 3 Model B+
OS
Raspbian Buster Lite / 2019-09-26
Python
3.7.3
OpenCV
3.2.0
Pygame
1.9.4.post1

インストールコマンド

# OSの書き込み後、sshでログイン

# パッケージ・リストを更新
sudo apt update

# Python3用のOpenCVをインストール
sudo apt install python3-opencv

# Python3用のPygameをインストール
sudo apt install python3-pygame

# Pythonのバージョンを確認
python3 --version

# OpenCVのバージョンを確認
python3 -c 'import cv2; print(cv2.__version__)'

# Pygameのバージョンを確認
python3 -c 'import pygame; print(pygame.version.ver)'

Web上で見つかるやり方

opencvで作った画像をpygameで描画する。 - BlankTar」によれば、以下のような方法で変換することができるようです。

opencv_image = opencv_image[:,:,::-1]  # OpenCVはBGR、pygameはRGBなので変換してやる必要がある。
shape = opencv_image.shape[1::-1]  # OpenCVは(高さ, 幅, 色数)、pygameは(幅, 高さ)なのでこれも変換。
pygame_image = pygame.image.frombuffer(opencv_image.tostring(), shape, 'RGB')

この方法で変換するコードを書いてみましょう。

show-image.py
import time

import cv2
import pygame


def get_opencv_img_res(opencv_image):
    height, width = opencv_image.shape[:2]
    return width, height

def convert_opencv_img_to_pygame(opencv_image):
    """
    OpenCVの画像をPygame用に変換.

    see https://blanktar.jp/blog/2016/01/pygame-draw-opencv-image.html
    """
    opencv_image = opencv_image[:,:,::-1]  # OpenCVはBGR、pygameはRGBなので変換してやる必要がある。
    shape = opencv_image.shape[1::-1]  # OpenCVは(高さ, 幅, 色数)、pygameは(幅, 高さ)なのでこれも変換。
    pygame_image = pygame.image.frombuffer(opencv_image.tostring(), shape, 'RGB')

    return pygame_image

def main():
    # OpenCVで画像を読み込む
    image_path = '/usr/share/info/gnupg-module-overview.png'  # Raspbian Buster Liteに最初から含まれていた画像ファイルのパス
    opencv_image = cv2.imread(image_path)

    # Pygameを初期化
    pygame.init()
    width, height = get_opencv_img_res(opencv_image)
    screen = pygame.display.set_mode((width, height))

    # OpenCVの画像をPygame用に変換
    pygame_image = convert_opencv_img_to_pygame(opencv_image)

    # 画像を描画
    screen.blit(pygame_image, (0, 0))
    pygame.display.update()  # 画面を更新

    # 5秒待って終了する
    time.sleep(5)
    pygame.quit()


if __name__ == '__main__':
    main()
起動コマンド
sudo python3 show-image.py
# Note: SDLライブラリを使用して表示させるために、「sudo」を追加してroot権限で実行する必要があります。
#       Liteではない「with desktop」版Raspbianや、sshコマンドに「-X」オプションを追加して接続している場合などは、
#       X Window Systemを使用して表示できるため、「sudo」コマンドは不要です。
#       see https://www.subthread.co.jp/blog/20181206/

これを起動すると、5秒間画像が表示され、自動で終了します。変換は正しく動作します。

変換速度

さて、上の方法で、解析した映像をリアルタイムに表示する場合を考えます。カメラから読み取った映像を解析・表示する場合など、そのような用途はよくあります。この場合、重要なのは変換速度です。計測してみましょう。

show-image.py
import time

import cv2
import pygame


def get_opencv_img_res(opencv_image):
    height, width = opencv_image.shape[:2]
    return width, height

def convert_opencv_img_to_pygame(opencv_image):
    """
    OpenCVの画像をPygame用に変換.

    see https://blanktar.jp/blog/2016/01/pygame-draw-opencv-image.html
    """
    opencv_image = opencv_image[:,:,::-1]  # OpenCVはBGR、pygameはRGBなので変換してやる必要がある。
    shape = opencv_image.shape[1::-1]  # OpenCVは(高さ, 幅, 色数)、pygameは(幅, 高さ)なのでこれも変換。
    pygame_image = pygame.image.frombuffer(opencv_image.tostring(), shape, 'RGB')

    return pygame_image

def main():
    # OpenCVで画像を読み込む
    image_path = '/usr/share/info/gnupg-module-overview.png'  # Raspbian Buster Liteに最初から含まれていた画像ファイルのパス
    opencv_image = cv2.imread(image_path)

    # Pygameを初期化
    pygame.init()
    width, height = get_opencv_img_res(opencv_image)
    screen = pygame.display.set_mode((width, height))

    # OpenCVの画像をPygame用に変換
    time_start = time.perf_counter()  # 計測開始
    pygame_image = convert_opencv_img_to_pygame(opencv_image)
    time_end = time.perf_counter()  # 計測終了
    print(f'変換時間: {time_end - time_start}秒 / {1/(time_end - time_start)}fps')

    # 画像を描画
    screen.blit(pygame_image, (0, 0))
    pygame.display.update()  # 画面を更新

    # 5秒待って終了する
    time.sleep(5)
    pygame.quit()


if __name__ == '__main__':
    main()
変換時間: 0.14485926300017127秒 / 6.903252020540914fps

遅い!!!

ただ表示するだけのコードで0.14秒。フレームレート換算で7fpsと書けば、この遅さがよく分かると思います。

これがOpenCV+Pygameの限界なのでしょうか?いいえ、違います。やり方を間違えているだけです。ここまで遅いのは、画像データをtostring()メソッドでわざわざ文字列化しているせいです。計測すればわかりますが、この重さの9割はtostring()メソッドのせいです。

もっと高速なやり方

Python向けOpenCVの画像データの実態は、NumPyのndarrayです。Pygameには、NumPyの配列から画像データを読み込む関数が存在します。これを駆使したやり方が、「OpenCV VideoCapture running on PyGame」に記載されています。

frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frame = np.rot90(frame)
frame = pygame.surfarray.make_surface(frame)

また、「OpenCV VideoCapture running on PyGame」のコメント欄では、この方法で生じる画像の反転問題の解決策+さらなる高速化についても記載されています。

 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
-frame = np.rot90(frame)
+frame = frame.swapaxes(0,1)
 frame = pygame.surfarray.make_surface(frame)

この方法に変更し、再度計測を行います。

show-image.py
import time

import cv2
import pygame


def get_opencv_img_res(opencv_image):
    height, width = opencv_image.shape[:2]
    return width, height

def convert_opencv_img_to_pygame(opencv_image):
    """
    OpenCVの画像をPygame用に変換.

    see https://gist.github.com/radames/1e7c794842755683162b
    """
    rgb_image = cv2.cvtColor(opencv_image, cv2.COLOR_BGR2RGB).swapaxes(0, 1)
    # OpenCVの画像を元に、Pygameで画像を描画するためのSurfaceを生成する
    pygame_image = pygame.surfarray.make_surface(rgb_image)

    return pygame_image

def main():
    # OpenCVで画像を読み込む
    image_path = '/usr/share/info/gnupg-module-overview.png'  # Raspbian Buster Liteに最初から含まれていた画像ファイルのパス
    opencv_image = cv2.imread(image_path)

    # Pygameを初期化
    pygame.init()
    width, height = get_opencv_img_res(opencv_image)
    screen = pygame.display.set_mode((width, height))

    # OpenCVの画像をPygame用に変換
    time_start = time.perf_counter()  # 計測開始
    pygame_image = convert_opencv_img_to_pygame(opencv_image)
    time_end = time.perf_counter()  # 計測終了
    print(f'変換時間: {time_end - time_start}秒 / {1/(time_end - time_start)}fps')

    # 画像を描画
    screen.blit(pygame_image, (0, 0))
    pygame.display.update()  # 画面を更新

    # 5秒待って終了する
    time.sleep(5)
    pygame.quit()


if __name__ == '__main__':
    main()
変換時間: 0.030075492999912967秒 / 33.24966277370395fps

ずいぶんと改善しました。

グレイスケール画像の描画

速度の問題が改善したため、OpenCVで何か処理をした結果を表示してみましょう。例えば、二値化した結果を表示する場合を考えます。

show-image.py
import time

import cv2
import pygame


def get_opencv_img_res(opencv_image):
    height, width = opencv_image.shape[:2]
    return width, height

def convert_opencv_img_to_pygame(opencv_image):
    """
    OpenCVの画像をPygame用に変換.

    see https://gist.github.com/radames/1e7c794842755683162b
    """
    rgb_image = cv2.cvtColor(opencv_image, cv2.COLOR_BGR2RGB).swapaxes(0, 1)
    # OpenCVの画像を元に、Pygameで画像を描画するためのSurfaceを生成する
    pygame_image = pygame.surfarray.make_surface(rgb_image)

    return pygame_image

def main():
    # OpenCVで画像を読み込む
    image_path = '/usr/share/info/gnupg-module-overview.png'  # Raspbian Buster Liteに最初から含まれていた画像ファイルのパス
    opencv_image = cv2.imread(image_path)

    # 画像を処理
    opencv_gray_image = cv2.cvtColor(opencv_image, cv2.COLOR_BGR2GRAY)
    ret, opencv_threshold_image = cv2.threshold(opencv_gray_image, 128, 255, cv2.THRESH_BINARY)
    opencv_image = opencv_threshold_image

    # Pygameを初期化
    pygame.init()
    width, height = get_opencv_img_res(opencv_image)
    screen = pygame.display.set_mode((width, height))

    # OpenCVの画像をPygame用に変換
    pygame_image = convert_opencv_img_to_pygame(opencv_image)

    # 画像を描画
    screen.blit(pygame_image, (0, 0))
    pygame.display.update()  # 画面を更新

    # 5秒待って終了する
    time.sleep(5)
    pygame.quit()


if __name__ == '__main__':
    main()

しかし、次のエラーが発生して実行は失敗します。

OpenCV Error: Assertion failed (scn == 3 || scn == 4) in cvtColor, file /build/opencv-L65chJ/opencv-3.2.0+dfsg/modules/imgproc/src/color.cpp, line 9716
Traceback (most recent call last):
  File "show-image.py", line 51, in <module>
    main()
  File "show-image.py", line 39, in main
    pygame_image = convert_opencv_img_to_pygame(opencv_image)
  File "show-image.py", line 17, in convert_opencv_img_to_pygame
    rgb_image = cv2.cvtColor(opencv_image, cv2.COLOR_BGR2RGB).swapaxes(0, 1)
cv2.error: /build/opencv-L65chJ/opencv-3.2.0+dfsg/modules/imgproc/src/color.cpp:9716: error: (-215) scn == 3 || scn == 4 in function cvtColor

二値化された画像は、色成分の無いグレイスケール画像となります。しかし、Pygame用画像に変換する処理では、入力された画像の色をBGRからRGBに変換するコードが存在します。よって、この部分でエラーが発生してしまいます。

この問題を解決するために、グレイスケール画像の場合はcv2.cvtColor()メソッドの第二引数をcv2.COLOR_GRAY2RGBに変更する分岐処理が必要になります。

show-image.py
import time

import cv2
import pygame


def get_opencv_img_res(opencv_image):
    height, width = opencv_image.shape[:2]
    return width, height

def convert_opencv_img_to_pygame(opencv_image):
    """
    OpenCVの画像をPygame用に変換.

    see https://gist.github.com/radames/1e7c794842755683162b
    see https://github.com/atinfinity/lab/wiki/%5BOpenCV-Python%5D%E7%94%BB%E5%83%8F%E3%81%AE%E5%B9%85%E3%80%81%E9%AB%98%E3%81%95%E3%80%81%E3%83%81%E3%83%A3%E3%83%B3%E3%83%8D%E3%83%AB%E6%95%B0%E3%80%81depth%E5%8F%96%E5%BE%97
    """
    if len(opencv_image.shape) == 2:
        # グレイスケール画像の場合
        cvt_code = cv2.COLOR_GRAY2RGB
    else:
        # その他の場合:
        cvt_code = cv2.COLOR_BGR2RGB
    rgb_image = cv2.cvtColor(opencv_image, cvt_code).swapaxes(0, 1)
    # OpenCVの画像を元に、Pygameで画像を描画するためのSurfaceを生成する
    pygame_image = pygame.surfarray.make_surface(rgb_image)

    return pygame_image

def main():
    # OpenCVで画像を読み込む
    image_path = '/usr/share/info/gnupg-module-overview.png'  # Raspbian Buster Liteに最初から含まれていた画像ファイルのパス
    opencv_image = cv2.imread(image_path)

    # 画像を処理
    opencv_gray_image = cv2.cvtColor(opencv_image, cv2.COLOR_BGR2GRAY)
    ret, opencv_threshold_image = cv2.threshold(opencv_gray_image, 128, 255, cv2.THRESH_BINARY)
    opencv_image = opencv_threshold_image

    # Pygameを初期化
    pygame.init()
    width, height = get_opencv_img_res(opencv_image)
    screen = pygame.display.set_mode((width, height))

    # OpenCVの画像をPygame用に変換
    pygame_image = convert_opencv_img_to_pygame(opencv_image)

    # 画像を描画
    screen.blit(pygame_image, (0, 0))
    pygame.display.update()  # 画面を更新

    # 5秒待って終了する
    time.sleep(5)
    pygame.quit()


if __name__ == '__main__':
    main()

これで、二値化されたグレイスケール画像も表示できるはずです。

さらに高速化

カメラの映像を連続して処理する場合など、同じ大きさ・色の画像を何度も変換する場合は、pygame.surfarray.make_surface()メソッドの代わりにpygame.surfarray.blit_array()メソッドを使用して高速化できます

まず、変換のたびにpygame.surfarray.make_surface()メソッドを使用する今までのやり方を計測してみます。

show-image.py
import statistics
import time

import cv2
import pygame


def get_opencv_img_res(opencv_image):
    height, width = opencv_image.shape[:2]
    return width, height

def convert_opencv_img_to_pygame(opencv_image):
    """
    OpenCVの画像をPygame用に変換.

    see https://gist.github.com/radames/1e7c794842755683162b
    see https://github.com/atinfinity/lab/wiki/%5BOpenCV-Python%5D%E7%94%BB%E5%83%8F%E3%81%AE%E5%B9%85%E3%80%81%E9%AB%98%E3%81%95%E3%80%81%E3%83%81%E3%83%A3%E3%83%B3%E3%83%8D%E3%83%AB%E6%95%B0%E3%80%81depth%E5%8F%96%E5%BE%97
    """
    if len(opencv_image.shape) == 2:
        # グレイスケール画像の場合
        cvt_code = cv2.COLOR_GRAY2RGB
    else:
        # その他の場合:
        cvt_code = cv2.COLOR_BGR2RGB
    rgb_image = cv2.cvtColor(opencv_image, cvt_code).swapaxes(0, 1)
    # OpenCVの画像を元に、Pygameで画像を描画するためのSurfaceを生成する
    pygame_image = pygame.surfarray.make_surface(rgb_image)

    return pygame_image

def main():
    # OpenCVで画像を読み込む
    image_path = '/usr/share/info/gnupg-module-overview.png'  # Raspbian Buster Liteに最初から含まれていた画像ファイルのパス
    opencv_image = cv2.imread(image_path)

    # Pygameを初期化
    pygame.init()
    width, height = get_opencv_img_res(opencv_image)
    screen = pygame.display.set_mode((width, height))

    # OpenCVの画像をPygame用に変換
    time_diff_list = []
    for _ in range(500):
      time_start = time.perf_counter()  # 計測開始
      pygame_image = convert_opencv_img_to_pygame(opencv_image)
      time_end = time.perf_counter()  # 計測終了
      time_diff_list.append(time_end - time_start)
    time_diff = statistics.mean(time_diff_list)
    print(f'平均変換時間: {time_diff}秒 / {1/time_diff}fps')

    # 画像を描画
    screen.blit(pygame_image, (0, 0))
    pygame.display.update()  # 画面を更新

    # 1秒待って終了する
    time.sleep(1)
    pygame.quit()


if __name__ == '__main__':
    main()
平均変換時間: 0.01808052065800075秒 / 55.30814177950641fps

60fpsに迫ろうかという速さです。これでも十分に早いですが、ただ変換しているだけだと考えると少し気になる速度です。

続いて、pygame.surfarray.blit_array()メソッドを使用して高速化したやり方を計測してみます。

show-image.py
import statistics
import time

import cv2
import pygame


def get_opencv_img_res(opencv_image):
    height, width = opencv_image.shape[:2]
    return width, height

pygame_surface_cache = {}

def convert_opencv_img_to_pygame(opencv_image):
    """
    OpenCVの画像をPygame用に変換.

    see https://gist.github.com/radames/1e7c794842755683162b
    see https://github.com/atinfinity/lab/wiki/%5BOpenCV-Python%5D%E7%94%BB%E5%83%8F%E3%81%AE%E5%B9%85%E3%80%81%E9%AB%98%E3%81%95%E3%80%81%E3%83%81%E3%83%A3%E3%83%B3%E3%83%8D%E3%83%AB%E6%95%B0%E3%80%81depth%E5%8F%96%E5%BE%97
    see https://stackoverflow.com/a/42589544/4907315
    """
    if len(opencv_image.shape) == 2:
        # グレイスケール画像の場合
        cvt_code = cv2.COLOR_GRAY2RGB
    else:
        # その他の場合:
        cvt_code = cv2.COLOR_BGR2RGB
    rgb_image = cv2.cvtColor(opencv_image, cvt_code).swapaxes(0, 1)

    # 同じ画像サイズで生成済のSurfaceをキャッシュから取得する
    cache_key = rgb_image.shape
    cached_surface = pygame_surface_cache.get(cache_key)

    if cached_surface is None:
        # OpenCVの画像を元に、Pygameで画像を描画するためのSurfaceを生成する
        cached_surface = pygame.surfarray.make_surface(rgb_image)
        # Surfaceをキャッシュに追加
        pygame_surface_cache[cache_key] = cached_surface
    else:
        # 同じ画像サイズのSurfaceが見つかった場合は、すでに生成したSurfaceを使い回す
        pygame.surfarray.blit_array(cached_surface, rgb_image)

    return cached_surface

def main():
    # OpenCVで画像を読み込む
    image_path = '/usr/share/info/gnupg-module-overview.png'  # Raspbian Buster Liteに最初から含まれていた画像ファイルのパス
    opencv_image = cv2.imread(image_path)

    # Pygameを初期化
    pygame.init()
    width, height = get_opencv_img_res(opencv_image)
    screen = pygame.display.set_mode((width, height))

    # OpenCVの画像をPygame用に変換
    time_diff_list = []
    for _ in range(500):
      time_start = time.perf_counter()  # 計測開始
      pygame_image = convert_opencv_img_to_pygame(opencv_image)
      time_end = time.perf_counter()  # 計測終了
      time_diff_list.append(time_end - time_start)
    time_diff = statistics.mean(time_diff_list)
    print(f'平均変換時間: {time_diff}秒 / {1/time_diff}fps')

    # 画像を描画
    screen.blit(pygame_image, (0, 0))
    pygame.display.update()  # 画面を更新

    # 1秒待って終了する
    time.sleep(1)
    pygame.quit()


if __name__ == '__main__':
    main()
平均変換時間: 0.013679669161995207秒 / 73.1011830884182fps

60fpsを超えました!ここまで早ければ、十分実用的に使えるでしょう。

OpenCVの画像をPygame向けに変換するやり方

def convert_opencv_img_to_pygame(opencv_image):
    """
    OpenCVの画像をPygame用に変換.

    see https://gist.github.com/radames/1e7c794842755683162b
    see https://github.com/atinfinity/lab/wiki/%5BOpenCV-Python%5D%E7%94%BB%E5%83%8F%E3%81%AE%E5%B9%85%E3%80%81%E9%AB%98%E3%81%95%E3%80%81%E3%83%81%E3%83%A3%E3%83%B3%E3%83%8D%E3%83%AB%E6%95%B0%E3%80%81depth%E5%8F%96%E5%BE%97
    """
    if len(opencv_image.shape) == 2:
        # グレイスケール画像の場合
        cvt_code = cv2.COLOR_GRAY2RGB
    else:
        # その他の場合:
        cvt_code = cv2.COLOR_BGR2RGB
    rgb_image = cv2.cvtColor(opencv_image, cvt_code).swapaxes(0, 1)
    # OpenCVの画像を元に、Pygameで画像を描画するためのSurfaceを生成する
    pygame_image = pygame.surfarray.make_surface(rgb_image)

    return pygame_image

同じ大きさの画像を何度も変換するなら:

pygame_surface_cache = {}

def convert_opencv_img_to_pygame(opencv_image):
    """
    OpenCVの画像をPygame用に変換.

    see https://gist.github.com/radames/1e7c794842755683162b
    see https://github.com/atinfinity/lab/wiki/%5BOpenCV-Python%5D%E7%94%BB%E5%83%8F%E3%81%AE%E5%B9%85%E3%80%81%E9%AB%98%E3%81%95%E3%80%81%E3%83%81%E3%83%A3%E3%83%B3%E3%83%8D%E3%83%AB%E6%95%B0%E3%80%81depth%E5%8F%96%E5%BE%97
    see https://stackoverflow.com/a/42589544/4907315
    """
    if len(opencv_image.shape) == 2:
        # グレイスケール画像の場合
        cvt_code = cv2.COLOR_GRAY2RGB
    else:
        # その他の場合:
        cvt_code = cv2.COLOR_BGR2RGB
    rgb_image = cv2.cvtColor(opencv_image, cvt_code).swapaxes(0, 1)

    # 同じ画像サイズで生成済のSurfaceをキャッシュから取得する
    cache_key = rgb_image.shape
    cached_surface = pygame_surface_cache.get(cache_key)

    if cached_surface is None:
        # OpenCVの画像を元に、Pygameで画像を描画するためのSurfaceを生成する
        cached_surface = pygame.surfarray.make_surface(rgb_image)
        # Surfaceをキャッシュに追加
        pygame_surface_cache[cache_key] = cached_surface
    else:
        # 同じ画像サイズのSurfaceが見つかった場合は、すでに生成したSurfaceを使い回す
        pygame.surfarray.blit_array(cached_surface, rgb_image)

    return cached_surface

参考

12
11
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
12
11