にせねこメモ

はてなダイアリーがUTF-8じゃないので移ってきました。

Pillow, OpenCVなどでの画像の扱いの違い

Pythonには画像処理のために画像を読み書きするライブラリがあり、画像ファイルをnumpy.ndarrayの形で読み込んだりそれを表示・保存したりできるものがある。
一方で、各ライブラリによって画像の形式がまちまちであったりして、同じnumpy.ndarrayでも変換が必要だったりする。
ここでよく引っかかるので調べてまとめておく。

環境

  • Python 3
    • Python 3.5.3 |Anaconda custom (64-bit)| (default, May 15 2017, 10:43:23) [MSC v.1900 64 bit (AMD64)] on win32
  • NumPy: 1.13.1
  • OpenCV Python bindings (cv2): 3.1.0
  • Pillow (PIL): 4.2.1
  • SciPy: 0.19.0
  • matplotlib: 1.5.1

以下、RGBカラー画像(8 bit/channel)を読み込むことについて考える。

略記

  • w: 幅
  • h: 高さ
  • c: チャンネル数(RGB画像の場合は3)

例示するコードではnumpyはnpとしている:

import numpy as np

Pillow (PIL fork)

Imageはnumpy.ndarrayではないが、numpy.asarrayなどを使って相互変換できる。

import numpy as np
from PIL import Image

pil_img = Image.open(INFILE)
ary_img = np.asarray(pil_img)

Image.openで画像ファイルを開いて、numpy.asarrayでnumpy.ndarrayに変換する。
形は(h, w, c)。
numpy.asarrayで特に指定しないとdtype=uint8 [0, 255]で読み込まれる。また、dtype=np.floatなどと指定しても[0.0, 255.0]と値は変わらないので必要に応じて自分でスケールする必要がある。

numpy.ndarrayからPIL Imageへ

Image.fromarrayでできる。与えるのはuint8である必要があるらしい。

from PIL import Image
im = Image.fromarray(np.uint8(myarray*255))

Imageのリサイズ

画像をImageのインスタンスで保持している場合、Image.resizeでリサイズができる。
次は画像のサイズを半分にする例。

pil_img = Image.open(INFILE)
print(pil_img.size) # (w, h)
w_orig, h_orig =pil_img.size
resized_pil_img = pil_img.resize((w_orig//2, h_orig//2))

ここで、pil_img.resizeに指定するのは(w, h)の形である。


他、<Imageインスタンス>.show()で画像を表示できるっぽい。

OpenCV 3 (cv2)

OpenCVで扱うカラー画像は基本BGRである。これは歴史的な経緯があるらしい。

cv2.imread

import cv2
img_cv2 = cv2.imread(INFILE)

cv2.imreadで画像ファイルを開くとnumpy.ndarrayが得られる。
これは(h, w, c)の形ではあるが、チャンネルの並び順が他のライブラリと異なりBGRになっている。
また、何もしないとdtype=uint8 [0, 255]で読み込まれる。

同様に、cv2.imshow, cv2.imsave, cv2.VideoCapture, cv2.VideoWriterなどもBGRの順であることを前提としているため、cv2以外と組み合わせて利用する場合には、BGR-RGB間の変換をする必要がある。

cv2.imshowなどに渡す画素値の範囲

画像表示・出力用のcv2.imshow, cv2.imwriteなどの関数に与える画像の画素値の範囲は、uint8であれば[0, 255]、uint16は[0, 65535]、int16は[-32768, 32767]、floatであれば[0.0, 1.0]であるらしい(値の小さい方が黒、大きい方が白となる)。floatで[0.0, 1.0]の範囲からはみ出た値は、黒(0.0)または白(1.0)になる。

BGR画像→RGB画像への変換

img_bgrが(h, w, c=3)のBGRなnumpy.ndarrayとする。

img_rgb = img_bgr[:,:,::-1]

Pythonのスライスを使う。「第1, 2次元目はそのままで、第3次元目は逆方向に並べたもの」という風に指定している。

RGB画像→BGR画像への変換

img_rgbが(h, w, c=3)のRGBなnumpy.ndarrayのとき、

img_bgr = img_rgb[:,:,::-1]

項目を分けたが、BGR→RGBと全く同じ。

cv2.resize

一方で、画像のリサイズを行うcv2.resize関数に引数として渡すリサイズ後のサイズは(w, h)である。

resized_img = cv2.resize(img, (w, h))

これは、画像のnumpy.ndarrayの形が(h, w, c)であることを考えると、ちゃんと確認しないと引っかかりそう。
次は画像のサイズを半分にする例。

h,w,c = img.shape
resized_img = cv2.resize(img, (w//2, h//2))

cv2には空の画像を作成する関数はないっぽいので、numpy.zeros, numpy.onesなどを利用する。

matplotlib

matplotlib.image.imread

matplotlib.image.imreadが返す配列は(h, w, c)でRGB、dtype=float32、[0.0, 1.0]。

Return value is a numpy.array. For grayscale images, the return array is MxN. For RGB images, the return value is MxNx3. For RGBA images the return value is MxNx4.

https://matplotlib.org/api/image_api.html

matplotlib.pyplot.imshow

matplotlib.pyplot.imshowは、3チャンネルカラー画像であればRGB (float or uint8)を要求する。floatの場合は[0.0, 1.0]。

import matplotlib.pyplot as plt
import matplotlib.image as mpimg

img_plt = mpimg.imread(INFILE)

plt.figure()
plt.imshow(ary_img) 
plt.show()

SciPy

scipy.misc.imread

import scipy.misc
img_scp = scipy.misc.imread(INFILE)

PILを使って画像を読み込んでるっぽいのでPIL互換か。
dtype=uint8, (h, w, c), [0, 255].

scipy.misc.imsave

dtype=float32なnumpy.ndarray与えたら、[0.0, 1.0]でも[0.0, 255.0]でもどちらでもまともに出力された。
これは、uint8でないと、最小・最大の画素値を用いて正規化されるかららしい。そのため、画像の中身をちょっと見てみたい時にはいいと思うが、値が変わらないでほしい時には使えなさそう。

ChainerなどのCNNレイヤの入出力

CNN系のchainer.links.Convolution2Dは入力の形として(c, h, w)を前提とする。hとwは入れ替わっても一貫していれば問題ないが、チャンネルは最初に来る必要がある。
また、チャンネルの順番はRGBでもBGRでもなんでもいいが、一貫している必要がある。

次にあげる類のものは設計次第で何でもいいが、与えるデータを一貫して揃えておかないといけない。

  • 高さと幅の順番
  • チャンネルの並び順 (RGBかBGRか? など)
    • cv2とそれ以外を交ぜて使うと引っかかりそう
  • 画素値の範囲(とfloatにするかintにするかなど)
    • [0, 255], [-1.0, 1.0], [0.0, 1.0]、など。
(h, w, c)から(c, h, w)への変換

numpy.transposeを使う。img_hwcが(h, w, c)の形のnumpy.ndarrayだとすると、

img_chw = np.transpose(img_hwc, (2, 0, 1))
(c, h, w)から(h, w, c)への変換
img_hwc = np.transpose(img_chw, (1, 2, 0))

ミニバッチ対応

CNN系のchainer.links.Convolution2Dは入出力がミニバッチであるので、一つの画像を入力として与える場合にはnumpy.newaxisなどを使って先頭に次元を増やしてミニバッチサイズ1のミニバッチにしないといけない: (1, c=3, h, w)とか。

minibatch = img_chw[np.newaxis]

ミニバッチから取り出すのは

img_chw = minibatch[0]

とか。

値の範囲を変換

単にnumpyのブロードキャストの機能使って加減算や乗除算を行うだけ。一緒にnumpy.ndarray.astypeを使って形式の変換を行うことも多いと思う。

例: [0, 255] -> [0.0, 1.0]
a = a.astype(np.float)/255.0
例: [0, 255] -> [-1.0, 1.0)
a = a.astype(np.float)/128.0 - 1.0
例: [-1.0, 1.0] -> [0, 255]
a = ( (a + 1.0)*127.5 ).astype(np.uint8)

とか。