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')
この方法で変換するコードを書いてみましょう。
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秒間画像が表示され、自動で終了します。変換は正しく動作します。
変換速度
さて、上の方法で、解析した映像をリアルタイムに表示する場合を考えます。カメラから読み取った映像を解析・表示する場合など、そのような用途はよくあります。この場合、重要なのは変換速度です。計測してみましょう。
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)
この方法に変更し、再度計測を行います。
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で何か処理をした結果を表示してみましょう。例えば、二値化した結果を表示する場合を考えます。
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
に変更する分岐処理が必要になります。
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()
メソッドを使用する今までのやり方を計測してみます。
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()
メソッドを使用して高速化したやり方を計測してみます。
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