scikit-imageで画像を等間隔で分割(ブロック / グリッド分割)

Posted: | Tags: Python, scikit-image, 画像処理

scikit-imageのskimage.util.view_as_blocks()を使うと、配列numpy.ndarrayで表された画像を等間隔で分割(同じサイズのブロックに分割)できる。

ここでは以下の内容について説明する。

  • skimage.util.view_as_blocks()の使い方
  • カラー画像(三次元配列)を分割
  • 所望の分割数で割り切れない場合
    • 余分な部分を無視する
    • 異なるサイズのブロックで分割

等間隔ではなく任意の位置で分割したい場合はNumPyの関数np.hsplit(), np.vsplit()やスライスなどを使えばよい。以下の記事を参照。

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のビューとコピーについては以下の記事を参照。

カラー画像(三次元配列)を分割

カラー画像は三次元配列として読み込まれる。

lena square

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() lena 00

skimage.util.view_as_blocks() lena 01

skimage.util.view_as_blocks() lena 10

skimage.util.view_as_blocks() lena 11

上述のように、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)

skimage.util.view_as_blocks() lena change

別々に処理したい場合は上述の2次元配列の例と同様にcopy()を使う。

なお、numpy.ndarraysqueeze()メソッドを使うとサイズが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を可能な限り等間隔で分割できる。等間隔にできない場合は分割サイズを適当に調整してくれる。

これを縦横に適用すると以下の通り。リスト内包表記を使っている。

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

関連カテゴリー

関連記事