tkinterでOpenCVなどの画像データ(numpyのndarray)をCanvasに表示する場合、画像がカラーだと、BGRからRGBに変換し、numpy→Pillow→PhotoImageと変換して、ようやくCanvasに画像を表示します。
さらに内部的にはおそらくRGBからBGRに変換している?と思います。
そのためnumpyの画像データをCanvasに表示するのは、かなり遅い。
VB6.0をメインで使っていた時代は遅い処理はWin32APIを使って高速に処理をするのが定番でしたが、Pythonで出来ないのか?調べてみました。
tkinterのCanvasにWin32APIのStretchDIBitsで画像を表示するとき、よく分からなかったポイントは2つ。
●Canvasのウィンドウハンドルをどのように取得するのか?
●numpyの配列(ndarray)のポインタの取得方法は?
これさえクリアできれば、なんとかなります。
Canvasのウィンドウハンドルの取得方法
Canvasに限らす、ウィジェットのウィンドウハンドルを取得するにはwinfo_id()関数を使います。
(例)
# Canvasの作成
canvas = tk.Canvas()
# キャンバスのウィンドウハンドルを取得
hWnd = canvas.winfo_id()
numpy配列のポインタの取得方法
numpy配列(ndarray)のデータのポイントを取得するには、ctypes.data_as()関数でポインタを取得します。
(例)
# 画像データの読み込み
img = cv2.imread("image.bmp", cv2.IMREAD_UNCHANGED)
# 画像データ(numpayのndarrayの配列)からポインタの取得
ptr_img = img.ctypes.data_as(ctypes.POINTER(ctypes.c_byte))
StretchDIBitsでCanvasに画像を表示するサンプル
できるだけシンプルなサンプルを作成しました。
imreadの部分のコメントを切り替えると、カラー/グレースケールの表示が可能になります。
import ctypes
import tkinter as tk
import cv2
# ----------------------------------------------------------------------
# 構造体などの定義
# ----------------------------------------------------------------------
SRCCOPY = 0x00CC0020
DIB_RGB_COLORS = 0
# StretchBlt() Modes
BLACKONWHITE = 1
WHITEONBLACK = 2
COLORONCOLOR = 3
HALFTONE = 4
class RECT(ctypes.Structure):
_fields_ = [
('left', ctypes.wintypes.LONG),
('top', ctypes.wintypes.LONG),
('right', ctypes.wintypes.LONG),
('bottom', ctypes.wintypes.LONG)
]
class BITMAPINFOHEADER(ctypes.Structure):
_fields_ = [
('biSize', ctypes.wintypes.DWORD),
('biWidth', ctypes.wintypes.LONG),
('biHeight', ctypes.wintypes.LONG),
('biPlanes', ctypes.wintypes.WORD),
('biBitCount', ctypes.wintypes.WORD),
('biCompression', ctypes.wintypes.DWORD),
('biSizeImage', ctypes.wintypes.DWORD),
('biXPelsPerMeter', ctypes.wintypes.LONG),
('biYPelsPerMeter', ctypes.wintypes.LONG),
('biClrUsed', ctypes.wintypes.DWORD),
('biClrImportant', ctypes.wintypes.DWORD)
]
class RGBQUAD(ctypes.Structure):
_fields_ = [
('rgbBlue', ctypes.wintypes.BYTE),
('rgbGreen', ctypes.wintypes.BYTE),
('rgbRed', ctypes.wintypes.BYTE),
('rgbReserved', ctypes.wintypes.BYTE)
]
class BITMAPINFO(ctypes.Structure):
_fields_ = [
('bmiHeader', BITMAPINFOHEADER),
('bmiColors', RGBQUAD * 256)
]
# ----------------------------------------------------------------------
root = tk.Tk()
root.title("StretchDIBitsで画像の表示") # ウィンドウタイトル
root.geometry("500x300") # ウィンドウサイズ(幅x高さ)
# ----------------------------------------------------------------------
# Canvasの作成
canvas = tk.Canvas()
# Canvasを配置
canvas.pack(expand = True, fill = tk.BOTH)
# 画像の読込
#img = cv2.imread("Parrots.bmp", cv2.IMREAD_GRAYSCALE) # グレースケールとして読み込む
img = cv2.imread("Parrots.bmp", cv2.IMREAD_COLOR) # カラーとして読み込む
if len(img.shape) >= 3:
# カラーの場合
src_height, src_width, src_ch = img.shape
else:
# グレースケールの場合
src_height, src_width = img.shape
src_ch = 1
# 画像データ(numpayのndarrayの配列)のポインタ
ptr_img = img.ctypes.data_as(ctypes.POINTER(ctypes.c_byte))
# ----------------------------------------------------------------------
# ヘッダの作成
bi = BITMAPINFO()
bi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
bi.bmiHeader.biWidth = src_width
bi.bmiHeader.biHeight = -src_height
bi.bmiHeader.biPlanes = 1
bi.bmiHeader.biBitCount = 8 * src_ch
# カラーパレット
for i in range(256):
bi.bmiColors[i].rgbBlue = i
bi.bmiColors[i].rgbGreen = i
bi.bmiColors[i].rgbRed = i
bi.bmiColors[i].rgbReserved = 255
# ----------------------------------------------------------------------
# キャンバスのウィンドウハンドルを取得
hWnd = canvas.winfo_id()
# キャンバスの領域取得
canvas.update() # 領域を取得するために一旦更新しておく
win_rect = RECT()
ctypes.windll.user32.GetClientRect(hWnd, ctypes.byref(win_rect))
# デバイスコンテキストハンドルの取得
hDC = ctypes.windll.user32.GetDC(hWnd)
# ストレッチモードの設定(BLACKONWHITE, WHITEONBLACK, COLORONCOLOR, HALFTONE)
ctypes.windll.gdi32.SetStretchBltMode(hDC , COLORONCOLOR)
# 描画
ctypes.windll.gdi32.StretchDIBits(
hDC,
win_rect.left, win_rect.top, win_rect.right, win_rect.bottom, # 描画先の領域
0, 0, src_width, src_height, # 描画元(画像)の領域
ptr_img, ctypes.byref(bi), DIB_RGB_COLORS, SRCCOPY)
# 解放
ctypes.windll.user32.ReleaseDC(hWnd, hDC)
# ----------------------------------------------------------------------
root.mainloop()
(実行結果)
カラー画像の場合
グレースケールの場合
まとめ
カメラの画像データはOpenCVに限らず、numpyのndarrayで取得される事が多く、カラー画像の場合はデータの並びがBGRなので、StretchDIBitsを使うと、BGR→RGBの変換など無く、ダイレクトにCanvasに画像を表示することができます。
これで、高速に画像を表示できるようになるはず?!
ただ、最近はベタにWin32APIを使って画像を表示する事も少なくなってきたので、情報が少ないのが難点でしょうか?