scikit-imageで画像を等間隔で分割(ブロック / グリッド分割)
scikit-imageのskimage.util.view_as_blocks()
を使うと、配列numpy.ndarray
で表された画像を等間隔で分割(同じサイズのブロックに分割)できる。
ここでは以下の内容について説明する。
skimage.util.view_as_blocks()
の使い方- カラー画像(三次元配列)を分割
- 所望の分割数で割り切れない場合
- 余分な部分を無視する
- 異なるサイズのブロックで分割
等間隔ではなく任意の位置で分割したい場合はNumPyの関数np.hsplit()
, np.vsplit()
やスライスなどを使えばよい。以下の記事を参照。
- 関連記事: Python, NumPyで画像処理(読み込み、演算、保存)
- 関連記事: NumPy配列ndarrayを分割(split, array_split, hsplit, vsplit, dsplit)
skimage.util.view_as_blocks()の使い方
以下の二次元配列を例とする。
import numpy as np
import skimage.util
a = np.arange(24).reshape(4, 6)
print(a)
# [[ 0 1 2 3 4 5]
# [ 6 7 8 9 10 11]
# [12 13 14 15 16 17]
# [18 19 20 21 22 23]]
skimage.util.view_as_blocks()
の第一引数に対象となる配列numpy.ndarray
、第二引数にブロックの形状shape
をタプルで指定する。分割数ではなくブロックの形状を指定する必要があるので注意。分割数を指定する例は後述。
返り値はnumpy.ndarray
。
blocks = skimage.util.view_as_blocks(a, (2, 3))
print(blocks)
# [[[[ 0 1 2]
# [ 6 7 8]]
#
# [[ 3 4 5]
# [ 9 10 11]]]
#
#
# [[[12 13 14]
# [18 19 20]]
#
# [[15 16 17]
# [21 22 23]]]]
print(type(blocks))
# <class 'numpy.ndarray'>
print(blocks.shape)
# (2, 2, 2, 3)
インデックス[]
を指定すると各ブロックにアクセスできる。
print(blocks[0, 0])
# [[0 1 2]
# [6 7 8]]
print(blocks[0, 1])
# [[ 3 4 5]
# [ 9 10 11]]
print(blocks[1, 0])
# [[12 13 14]
# [18 19 20]]
print(blocks[1, 1])
# [[15 16 17]
# [21 22 23]]
指定したブロックの形状で割り切れない場合はエラーとなる。
# blocks = skimage.util.view_as_blocks(a, (2, 4))
# ValueError: 'block_shape' is not compatible with 'arr_in'
skimage.util.view_as_blocks()
が返すのは元のnumpy.ndarray
のビュー。メモリを共有するので、いずれかの要素を変更すると、他方の要素も変更される。
print(np.shares_memory(a, blocks))
# True
a[0, 0] = 100
print(blocks[0, 0])
# [[100 1 2]
# [ 6 7 8]]
別々に処理したい場合はcopy()
でコピーを生成する。
a = np.arange(24).reshape(4, 6)
blocks_copy = skimage.util.view_as_blocks(a, (2, 3)).copy()
print(np.shares_memory(a, blocks_copy))
# False
a[0, 0] = 100
print(blocks_copy[0, 0])
# [[0 1 2]
# [6 7 8]]
元のnumpy.ndarray
のコピーを生成してそれを処理してもよい。
blocks_copy2 = skimage.util.view_as_blocks(a.copy(), (2, 3))
print(np.shares_memory(a, blocks_copy2))
# False
numpy.ndarray
のビューとコピーについては以下の記事を参照。
カラー画像(三次元配列)を分割
カラー画像は三次元配列として読み込まれる。
import numpy as np
import skimage.io
import skimage.util
img = skimage.io.imread('data/src/lena_square.png')
print(img.shape)
# (512, 512, 3)
三次元配列を分割する場合、skimage.util.view_as_blocks()
の第二引数にも三次元の形状を指定する必要がある。色(チャンネル)では分割しなくても省略するとエラーになるので注意。
blocks = skimage.util.view_as_blocks(img, (256, 256, 3))
print(blocks.shape)
# (2, 2, 1, 256, 256, 3)
# blocks = skimage.util.view_as_blocks(img, (256, 256))
# ValueError: 'block_shape' must have the same length as 'arr_in.shape'
各ブロックには以下のようにアクセスできる。ここでも3つ指定する必要がある。
print(blocks[0, 0, 0].shape)
# (256, 256, 3)
それぞれを別々に画像として保存すると以下の通り。
skimage.io.imsave('data/dst/skimage_block_00.jpg', blocks[0, 0, 0])
skimage.io.imsave('data/dst/skimage_block_01.jpg', blocks[0, 1, 0])
skimage.io.imsave('data/dst/skimage_block_10.jpg', blocks[1, 0, 0])
skimage.io.imsave('data/dst/skimage_block_11.jpg', blocks[1, 1, 0])
上述のように、skimage.util.view_as_blocks()
は元の配列のビューを返す。例えば、各ブロックの要素を変更すると元の画像も変更されてしまうので注意。
print(np.shares_memory(img, blocks))
# True
blocks[0, 0, 0] = 0
blocks[1, 1, 0] //= 2
skimage.io.imsave('data/dst/skimage_block_change.jpg', img)
別々に処理したい場合は上述の2次元配列の例と同様にcopy()
を使う。
なお、numpy.ndarray
のsqueeze()
メソッドを使うとサイズが1
の次元を削除できる。ブロックの選択が多少楽になる。
blocks_s = skimage.util.view_as_blocks(img, (256, 256, 3)).squeeze()
print(blocks_s.shape)
# (2, 2, 256, 256, 3)
print(blocks_s[0, 0].shape)
# (256, 256, 3)
所望の分割数で割り切れない場合
上述のように、skimage.util.view_as_blocks()
では第二引数に指定した形状で割り切れないとエラーとなる。
そのような場合の対処法は、何を優先するかによっていくつか考えられる。元画像を割り切れるサイズにリサイズするなどの方法もあるが、ここでは元の画素値を保持する例を示す。
これまでと同じ画像を例とする。
import numpy as np
import skimage.io
import skimage.util
img = skimage.io.imread('data/src/lena_square.png')
print(img.shape)
# (512, 512, 3)
余分な部分を無視する
できる限り大きいブロックで分割し、残りの部分は無視する(切り落とす)例は以下の通り。
div_v
には縦の分割数、div_h
には横の分割数を指定する。組み込み関数divmod()
と三項演算子を使っている。白黒画像(二次元配列)にもカラー画像(三次元配列)にも対応。
def split_image_cut(img, div_v, div_h):
h, w = img.shape[:2]
block_h, out_h = divmod(h, div_v)
block_w, out_w = divmod(w, div_h)
block_shape = (block_h, block_w, 3) if len(img.shape) == 3 else (block_h, block_w)
return skimage.util.view_as_blocks(img[:h - out_h, :w - out_w], block_shape)
最終的にはskimage.util.view_as_blocks()
を実行しているので、返り値はこれまでの例の通りnumpy.ndarray
。ブロックのサイズはすべて同じになる。
blocks = split_image_cut(img, 2, 3)
print(blocks.shape)
# (2, 3, 1, 256, 170, 3)
print(blocks[0, 0, 0].shape)
# (256, 170, 3)
print(blocks[0, 1, 0].shape)
# (256, 170, 3)
print(blocks[0, 2, 0].shape)
# (256, 170, 3)
print(blocks[1, 0, 0].shape)
# (256, 170, 3)
print(blocks[1, 1, 0].shape)
# (256, 170, 3)
print(blocks[1, 2, 0].shape)
# (256, 170, 3)
この例では画像(配列)の右側および下側を無視しているが、中央を重視したい場合はスライスの開始値・終了値を調整して上下左右を切り取ってもよい。
異なるサイズのブロックで分割
np.array_split()
を使うと、numpy.ndarray
を可能な限り等間隔で分割できる。等間隔にできない場合は分割サイズを適当に調整してくれる。
これを縦横に適用すると以下の通り。リスト内包表記を使っている。
- 関連記事: Pythonリスト内包表記の使い方
def split_image_unequal(img, div_v, div_h):
l_v = np.array_split(img, div_v)
return [np.array_split(img_v, div_h, 1) for img_v in l_v]
np.array_split()
はnumpy.ndarray
を要素とするリストを返す。上で定義した関数はnumpy.ndarray
を要素とする二次元リスト(リストのリスト)を返す。
l = split_image_unequal(img, 2, 3)
print(type(l))
# <class 'list'>
print(len(l))
# 2
print(type(l[0]))
# <class 'list'>
print(len(l[0]))
# 3
print(type(l[0][0]))
# <class 'numpy.ndarray'>
元の画像のすべての部分が使われるが、割り切れない場合はサイズがブロックごとに異なる。
print(l[0][0].shape)
# (256, 171, 3)
print(l[0][1].shape)
# (256, 171, 3)
print(l[0][2].shape)
# (256, 170, 3)
print(l[1][0].shape)
# (256, 171, 3)
print(l[1][1].shape)
# (256, 171, 3)
print(l[1][2].shape)
# (256, 170, 3)
なお、np.array_split()
でも返り値の各要素のnumpy.ndarray
は元のnumpy.ndarray
のビュー。いずれかを変更すると他方も変更される。別々に処理したい場合は、元のnumpy.ndarray
のコピーを渡せばよい。
l = split_image_unequal(img, 2, 3)
print(np.shares_memory(img, l[0][0]))
# True
l_copy = split_image_unequal(img.copy(), 2, 3)
print(np.shares_memory(img, l_copy[0][0]))
# False