【Python/tkinter】CanvasにStretchDIBitsで画像を表示する

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を使って画像を表示する事も少なくなってきたので、情報が少ないのが難点でしょうか?

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください