python

Bokehで画像をプロットする方法

Matplotlibで画像をプロットする場合はPillowの画像オブジェクトをimshow()関数に与えるだけでよかったのが、Bokehの場合は少しコツが必要です。

ということでBokehで画像をプロットする方法について、記事にまとめておきました。

Matplotlibで画像を表示する方法

まずはMatplotlibを使って画像を表示する方法ですが、PillowのImageオブジェクトをimshow()関数に渡せばイッパツでプロットすることができました。

from matplotlib import pyplot as plt
from PIL import Image

IMAGE_PATH = 'cat.jpg'
with Image.open(IMAGE_PATH) as im:
    plt.imshow(im)

Bokehのimage_url()関数を使ってプロット

Bokehの場合は、bokeh.plotting.figure.Figure.image_url()関数に画像パスを渡して画像を表示することができます。

from bokeh.plotting import figure, show
from bokeh.io import output_file
from PIL import Image

# Jupyter上ではローカルに保存されているイメージを
# URL参照で表示することができないため
# ファイルに出力してから表示しないといけない
output_file('plot.html')

# Pillowを使って画像を読み込み、の幅と高さを取得
with Image.open('cat.jpeg') as im:
    w, h = im.width, im.height

# 画像サイズに合わせた幅と高さのプロット作成
p = figure(width=w, height=h, x_range=(0, w), y_range=(0, h))

# 画像URLまたは相対パス文字列を渡すとプロットされる
# 画像のY座標は上から下に対し、Bokehの座標は下から上であるので
# y座標(描画の起点)は画像の高さを指定する必要がある
p.image_url(url=['cat.jpeg'], x=0, y=h)

show(p)

画像パスを渡すだけでプロットできちゃうんだからBokehのほうが画像プロット簡単じゃんとか思ちゃいそうなところですが、実はいろいろと利用できるシーンに制限があって苦労させられます。

苦労1つ目ですが、画像のサイズに応じてキャンバスサイズが調節されないので、figure()呼び出し時に適切なキャンバスサイズを指定する必要があります。適切なサイズを指定するためだけに、PillowやOpenCVなどを使って画像ファイルをロードすることになります。(縦横比気にしないとかはみ出してもいい場合は問題ない)

苦労2つ目は、Figure.image_url()はhttps://などのURL形式が前提のため、Jupyter上でローカルファイルをプロットすることができない、という制限です。Bokehを使うメインシチュエーションはローカル環境でのデータサイエンスじゃないかと思います。この制約はけっこう痛いですよね。

と、いうことでこれら苦労2つを考えると、次に紹介する方法が現実的なんじゃないかと思います。

Bokehのimage_rgba()関数を使ってプロット

bokeh.plotting.Figure.image_rgba()関数を使うと、Jupyter上でも画像をプロット表示することができます。

image_rgba()関数は画像データを直接渡すことになるワケですが、データの形式がRGBAを表現するUINT32の配列しか受け付けてくれません。

Numpyを使ってデータをいぢいぢすることになるワケですが、にわかデータサイエンティストなケンヂまるは何時間も悩む事になりました。。

from bokeh.plotting import figure, show
from bokeh.io import output_notebook
from PIL import Image
import numpy as np

# Jupyter内にプロットする場合は最初に呼び出し
output_notebook()

# PILイメージとして読み込む
pil_im = Image.open('cat.jpeg')
# RGBからRGBAに変換
pil_im_rgba = pil_im.convert('RGBA')
# PILイメージからndarrayに変換
im_rgba = np.asarray(pil_im_rgba)

# (高さ, 幅, [R,G,B,A])の配列から(高さ, 幅, RGBA)の配列に変換
im_uint32 = im_rgba.view(np.uint32).reshape(im_rgba.shape[:2])

# 画像の高さと幅を取得
h, w = im_rgba.shape[:2]
# 画像サイズと同じキャンバスを作成
p = figure(width=w, height=h, x_range=(0, w), y_range=(0, h))
# 画像をサイズ通りに描画
p.image_rgba(image=[im_uint32], x=0, y=0, dw=w, dh=h)

show(p)

プログラムの解説をします。

Jupyter上にプロット出力するために、output_notebook()関数を呼び出しています。

PILイメージとして読み込んだJPEG画像はRGB形式なので、RGBA形式に変換します。

さらにRGBA形式のPILイメージをndarrayに変換します。

さてここで、ndarrayは色情報を[255, 255, 255, 255]という形式になっています。つまり色情報は型がuint8、長さ4、の配列です。

Figure.image_rgba()関数は色情報がuint32のリテラルであることが前提なので、このまま渡では渡せません。

そこでndarray.view()関数を使って、(高さ, 幅, uint8×4配列)の形式から(高さ, 横, uint32×1値)の形式に変換しています。これでようやく、image_rgba()関数が受け付けてくれる形式になりました。

uint8×4配列からuint32×1値への変更イメージ

ということで情報をいぢいぢしたうえでimage_rgba()関数に渡すと、Jupyter上に表示することができました。(画像はDataSpellで実行したもの)

image_rgba()関数でプロットしたとき画像が逆さという問題

image_rgba()関数を使うと、Jupyter内に画像をプロットできました。けれども画像が上下逆さなんです。

逆さになる理由は、画像が左上が原点(0, 0)として扱われるのに対し、Bokehは左下が原点として扱われるためです。

Bokehのimage_rgba()のパラメータ設定では対処できなそうなので、渡してやる画像そのものを逆さまに加工します。

画像データを逆さまにするにはnumpy.flip()関数を使います。

この関数は指定した軸の順番を反転というもので、こんな感じで使います。

[[0, 0, 0], [1, 1, 1], [2, 2, 2]]のデータが[[2, 2, 2], [1, 1, 1], [0, 0, 0]]になりました。

これを画像情報に使えば、上下や左右を反転することができます。

# 画像情報を上下反転
im = np.flip(im_uint32, 0)

p2 = figure(width=500, height=int(500*h/w), x_range=(0, w), y_range=(0, h))
p2.image_rgba(image=[im], x=0, y=0, dw=w, dh=h)

show(p2)

ちゃんとプロットすることができました。

-python

© 2022 ヂまるBlog