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