python

JupyterにOpenCVの画像表示 シチュエーションによって使い分けよう

Jupyter上にOpenCVの画像データを表示する方法は、結構いろいろあります。ここ最近、ようやく自分なりに使いこなせてきたなと思えるようになってきました。

  • ただ画像を表示できればいい
  • 画像ピクセルの目盛り付きで表示したい
  • 表示した画像を拡大や縮小したい
  • 画像を並べて比較したい

色々なシチュエーションがあります。ということでそれぞれ適切な表示方法を紹介します。

OpenCVの画像データはnumpy配列

OpenCVを使って画像操作をしている方には説明するまでもないかもしれませんが、OpenCVの画像データはnumpy.ndarrayオブジェクトです。

これを普通にJupyterのセルのアウトプットにしても配列の数値が表示されるだけで、画像の可視化はできません。

このnumpy.ndarrayオブジェクトは、3次元の(高さピクセル数, 幅ピクセル数, 色情報)形式になっています。そして色情報の部分は基本的にBGR形式です。[0, 0, 0]は黒色を表すし、[255, 255, 255]は白を、[255, 0, 0]は赤を表す、といった具合のものです。

色情報は一般的にRGBの順番で表現されるのに対しOpenCVの場合はBGRで、互換性がありません。つまり、OpenCVの画像情報は基本的に、赤と青の情報が逆なわけです。その状態のままだと変な色で表示されちゃいます。

import cv2
data = cv2.imread("cat.jpeg")
Image.fromarray(data)

OpenCVの関数を使ってBGRデータをRGBデータに変換すれば、適切な色になります。

import cv2
from PIL import Image

data_bgr = cv2.imread("cat.jpeg")
# BGR → RGB
data_rgb = cv2.cvtColor(data_bgr, cv2.COLOR_BGR2RGB)
Image.fromarray(data_rgb)

本質的な部分を少し掘り下げると、cv2.COLOR_BGR2RGBオペレーションは、実際はaxis=2の情報の並び順を逆転しているだけです。なのでOpenCVの関数を使わなくても、numpy.flip()を使っても同じことができます。

import cv2
from PIL import Image

data_bgr = cv2.imread("cat.jpeg")
data_rgb = np.flip(data_bgr, axis=2)
Image.fromarray(data_rgb)

Pillowを使って表示

Jupyterで画像を表示するイチバン簡単な方法は、セルの戻り値にPillow.Imageオブジェクトを返してやることです。

この方法、実は前の章のサンプルで何の解説もなく既に使っちゃってました。😜

import cv2
from PIL import Image

data_bgr = cv2.imread("cat.jpeg")
data_rgb = cv2.cvtColor(data_bgr, cv2.COLOR_BGR2RGB)

# OpenCVの画像データからPillow.Imageを作成し、セルの戻り値に
Image.fromarray(data_rgb)

ちなみにPillow.Image.fromarray()の引数にnumpy.ndarrayのデータを指定することができるのは、buffer protocolを実装しているからです。

buffer protocolというのは僕もあまり知らないけれど、C実装が関係してくるかなり深い世界のようです。深みにハマると戻ってこられなくなる可能性が高いので、概要を押さえる程度にしておいたほうが幸せなままでいられそうです。

てことで概念的な部分だけ説明。

buffer protocolは2つ以上のオブジェクトから同一メモリ上のデータを利用できる規格です。Pillow.Image.fromarray()はこのbuffer protocolに対応しているオブジェクトなら受け取ることができます。もちろん、メモリ構造の互換性があるものに限りますけど。

Matplotlibを使って表示

かなり簡単な方法がもう一つあります。Matplotlib.pylot.imshow()を使う方法です。

import cv2
import matplotlib.pyplot as plt

data = cv2.imread("cat.jpeg")
data = cv2.cvtColor(data, cv2.COLOR_BGR2RGB)

plt.imshow(data)

こちらの方法だとピクセル目盛りも標準で表示されるし、お好みに応じて色々とアレンジもできます。

Bokehを使って表示

Bokehはさらに、拡大・縮小・パニングなどのインタラクティブな操作にも対応してくれます。具体的な方法は1つ前の記事「Bokehで画像をプロットする方法」で説明してあるので、見てみて下さい。

Matplotlibだとどうか

ちなみにMatplotlibでもipymplモジュールを使えばインタラクティブ操作はできます。ただ処理が重く、DataSpellでの表示もできません。てことで個人的にはあまり使わないです。

並べて表示

画像を縦や横に並べて表示したいってシチュにちょいちょい遭遇すると思います。画像の加工前後の比較とか解像度による劣化度合い確認なんかは並べるに限ります。

import cv2
import matplotlib.pyplot as plt
import numpy as np

cat1 = cv2.imread("cat.jpeg")
cat2 = cv2.imread("cat2.jpeg")

cats = np.hstack((cat1, cat2))
cats_rgb = cv2.cvtColor(cats, cv2.COLOR_BGR2RGB)
Image.fromarray(cats_rgb)

最初に説明したように、OpenCVの画像データというのは3次元のnumpy.ndarrayです。なので、axis=1の部分同士で結合してやれば、横並びにできます。numpy.hstack()を使うと画像を結合、numpy.vstack()を使うと画像を縦結合できます。

並べたうえで連動させて表示

単純に画像を縦や横に並べるだけじゃなく、ワシは細かい部分まで解析したいんじゃ!

そんな場合は、やはりBokehがオススメです。Bokehは画像を並べて表示したうえで拡大・縮小・パニングなどの操作を連動させることもできます。

まずは必要ライブラリのインポートと、画像ファイル名から単体のBokehプロットを作成する関数plot_cat()の作成部分を作成しました。この部分は「Bokehで画像をプロットする方法」で解説してあるので詳しい説明は省略します。

import cv2
import numpy as np

from bokeh.plotting import figure, show
from bokeh.io import output_notebook

# Jupyter内にBokehプロットを表示したい場合は最初に呼び出し
output_notebook()

def plot_cat(filename):
    """ ファイル名からbokeh.plotting.Figureを作成 """

    cat_bgr = cv2.imread(filename)
    # OpenCVのBGR形式からRGBA形式に変更
    cat_rgba = cv2.cvtColor(cat_bgr, cv2.COLOR_BGR2RGBA)
    # np.uint8型の[R,G,B,A]をまとめてnp.uint32型のRGBAとして参照
    cat_uint32_3dimensions = cat_rgba.view(np.uint32)
    # [[[RGBA], [RGBA]], [[RGBA], [RGBA]]]となっているのを
    # [[RGBA, RGBA], [RGBA, RGBA]]に
    cat = np.squeeze(cat_uint32_3dimensions)
    # 画像の上下を反転(画像のY座標は上から下なのに対してBokehのY座標は下から上なので)
    cat = np.flip(cat)
    # 画像サイズ取得
    h, w = cat.shape

    # 画像サイズのキャンバス作成
    p = figure(width=w, height=h, x_range=(0, w), y_range=(0, h))
    # 描画
    p.image_rgba([cat], x=0, y=0, dw=w, dh=h)

    return p

作成したplot_cat()を使用してcat1_plotとcat2_plotを作成しましょう。

cat1_plot = plot_cat("cat.jpeg")
show(cat1_plot)
cat2_plot = plot_cat("cat2.jpeg")
show(cat2_plot)

画像を1枚ずつ表示することができたので、ここから並べて表示する方法の解説です。

並べて表示するにはレイアウトを使います。単純に格子状にレイアウトしたい場合はbokeh.layouts.gridplotを使います。横に並べも縦に並べも格子状の一部分なので、gridplotでカバーすることができます。

拡大・縮小・パニングの連動は、bokeh.plotting.Figure.x_rangebokeh.plotting.Fiure.y_rangeを共有することで実現できます。

from bokeh.layouts import gridplot

# DataRange1dを共有して表示領域を連動
cat2_plot.x_range = cat1_plot.x_range
cat2_plot.y_range = cat1_plot.y_range

# sizing_mode='scale_both'を指定することで
# Jupyterの幅に収まるようにサイズを調整していくれる
g = gridplot([[cat1_plot, cat2_plot]], sizing_mode='scale_both')

show(g)

x_rangey_rangeは固定値ではなくbokeh.models.ranges.DataRange1dなので共有すると連動するようになります。

あとがき

Jupyter上でOpenCV画像を表示する方法といっても、いろいろあって、大変ですよね。。

何回も行き詰まって、調べて、試して、繰り返して。少しずつ可視化を進めていくことで、データの見えなかった一面が浮かび上がります。どんどん可視化していきましょう。

参考サイト

-python

© 2022 ヂまるBlog