Pythonで学ぶdilate(膨張処理)の基本を紹介


Pythonで学ぶ膨張処理(Dilate)の基本から応用まで:画像処理の強力なツールを使いこなす

はじめに:画像処理におけるモルフォロジー演算の世界

画像処理は、デジタル画像を操作、分析、理解するための技術分野です。その中でも、画像の形状や構造に基づいた処理を行う手法として「モルフォロジー(Morphology)演算」があります。モルフォロジー演算は、主にバイナリ画像(白と黒の2値画像)に対して適用され、ノイズ除去、オブジェクトの抽出、形状の解析、画像のセグメンテーションなど、多岐にわたるタスクで重要な役割を果たします。

モルフォロジー演算の基本的な操作は、主に「膨張(Dilation)」と「収縮(Erosion)」の二つです。これらの基本操作を組み合わせることで、「オープニング(Opening)」や「クロージング(Closing)」といったより複雑な演算が実現されます。

この記事では、モルフォロジー演算の中でも特に「膨張処理(Dilate)」に焦点を当て、その理論的な背景から、Pythonを使った具体的な実装方法、そして様々な応用例までを詳細に解説します。画像処理ライブラリとして広く使われているOpenCVと、科学技術計算で人気のscikit-imageを使った実装を中心に説明を進めます。約5000語というボリュームで、膨張処理の仕組みを深く理解し、Pythonで自在に使いこなせるようになることを目指します。

膨張処理(Dilate)とは? 理論と仕組み

膨張処理(Dilation)は、画像中の前景領域(通常は白または値が1のピクセル)を拡大するモルフォロジー演算です。名前の通り、前景オブジェクトの境界を「膨張」させる効果があります。この処理は、画像中の小さな穴を埋めたり、切断されたオブジェクト同士を結合したり、オブジェクトのサイズを大きく見せたりするために用いられます。

バイナリ画像と構造要素(Structuring Element/Kernel)

膨張処理を理解する上で重要な要素は二つあります。

  1. バイナリ画像(Binary Image): 膨張処理は、基本的に白と黒の2値画像に対して行われます。白が前景(オブジェクト)、黒が背景であることが多いです。数値で表すと、前景ピクセルが1、背景ピクセルが0となります。グレースケール画像に対しても適用可能ですが、その場合は最大値フィルタのような振る舞いをします(後述)。
  2. 構造要素(Structuring Element または Kernel): 膨張処理は、単にピクセルをランダムに増やすわけではありません。処理の形状や大きさを定義する小さなマトリックス(配列)を用います。これが構造要素です。構造要素は通常、前景ピクセルで構成され、その中心(原点)が定義されます。構造要素の形状(正方形、円形、十字形など)やサイズが、膨張処理の結果に大きく影響します。

膨張処理のメカニズム

膨張処理は、画像上の各ピクセルに対して、構造要素を用いた局所的な操作として定義されます。具体的には、以下の手順で行われます。

  1. 構造要素を画像の各ピクセル上に移動させます。 構造要素の中心が、現在処理している画像のピクセル(これを中心ピクセルと呼びます)に重なるように配置します。
  2. 構造要素が重なる範囲内にある画像のピクセルと、構造要素自身のピクセルを比較します。
  3. 構造要素内の少なくとも一つの前景ピクセル(値が1)が、画像の対応する位置のピクセル(構造要素が重なる範囲内)と重なった場合、新しい出力画像の中心ピクセルの値を前景(1または白)とします。
  4. そうでない場合(構造要素内の全ての前景ピクセルが、画像の対応する位置の背景ピクセルと重なった場合)、出力画像の中心ピクセルの値を背景(0または黒)とします。

簡単に言うと、「構造要素の形に沿って、前景ピクセルが”染み出す”」イメージです。構造要素の中心を基準に、その構造要素の形が前景ピクセルにかかると、その中心位置の出力ピクセルが前景になる、ということです。

数学的定義

数学的には、集合論を用いて膨張処理を定義できます。画像を前景ピクセル全体の集合 $A$、構造要素をその形状を表す点の集合 $B$ とします。構造要素 $B$ の原点を $o$ とします。

膨張処理 $A \oplus B$ は、以下のように定義されます。

$A \oplus B = {z \mid (B_z \cap A) \neq \emptyset }$

ここで、$B_z$ は構造要素 $B$ をベクトル $z$ だけ平行移動させた集合です。つまり、画像上の点 $z$ を中心に構造要素 $B$ を配置したとき、$B_z$ が元の画像の前景集合 $A$ と一つでも共通の点を持つならば、点 $z$ は膨張後の前景集合 $A \oplus B$ に含まれる、という意味です。

別の定義として、構造要素 $B$ を原点に対して点対称に反転させたもの $B’$(または $-B$ と書かれる)を用いることもあります。この場合、膨張処理は以下のように定義されます。

$A \oplus B = \bigcup_{b \in B} A_b$

ここで、$A_b$ は集合 $A$ をベクトル $b$ だけ平行移動させた集合です。これは、元の前景集合 $A$ を、構造要素 $B$ の各点 $b$ の位置に移動させた集合の和集合(Union)をとる、という意味になります。直感的にはこちらの定義の方が理解しやすいかもしれません。つまり、前景ピクセルの一つ一つが、構造要素の形に「広がっていく」と考えることができます。

例:画像 $A$ が単一の点 ${(0,0)}$ で、構造要素 $B$ が ${(0,0), (1,0), (0,1)}$ という3つの点の集合(L字型で原点が(0,0))だったとします。
$A \oplus B$ は、$A$ の各点 $(0,0)$ を構造要素 $B$ の各点に移動させた集合の和集合です。
$(0,0)$ を $(0,0)$ に移動 $\rightarrow {(0,0)}$
$(0,0)$ を $(1,0)$ に移動 $\rightarrow {(1,0)}$
$(0,0)$ を $(0,1)$ に移動 $\rightarrow {(0,1)}$
これらの和集合は ${(0,0), (1,0), (0,1)}$ となり、構造要素 $B$ そのものが出力されます。これは、一点を構造要素で膨張させると、構造要素の形になることを意味します。

もし画像 $A$ が二つの点 ${(0,0), (3,0)}$ で、構造要素 $B$ が $3 \times 3$ の正方形(中心(1,1))だったとします。
$A$ の点 $(0,0)$ を中心に $B$ で膨張させると、$(0,0)$ を中心とした $3 \times 3$ の正方形の領域が前景になります。
$A$ の点 $(3,0)$ を中心に $B$ で膨張させると、$(3,0)$ を中心とした $3 \times 3$ の正方形の領域が前景になります。
これら二つの領域が重なる部分は結合され、最終的な前景領域となります。もし二つの点が十分に近ければ、それぞれの膨張領域が重なり、一つの繋がった領域になるでしょう。

グレースケール画像への適用

膨張処理はグレースケール画像に対しても適用可能です。この場合、構造要素が重なる範囲内のピクセルの中で、最大値をとる操作に相当します。つまり、出力画像の中心ピクセルの値は、構造要素を重ねた際にその構造要素内のどこかにある最も明るいピクセルの値になります。

$$(A \oplus B)(x,y) = \max_{(i,j) \in B} {A(x-i, y-j)}$$

ここで、$A(x,y)$ は画像 $(x,y)$ のピクセル値、$B$ は構造要素の形状、$(i,j)$ は構造要素内の相対座標です。構造要素の中心を原点とします。

グレースケール画像への膨張は、画像の明るい特徴を拡大したり、画像の暗い領域にある小さな穴を埋めたり(周辺の明るいピクセルが最大値として膨張してくるため)する効果があります。バイナリ画像の場合、最大値は常に1になるため、結果的に「少なくとも一つ前景があれば前景になる」というバイナリ定義と一致します。

膨張処理の主な応用例

膨張処理は、それ単体で、あるいは他のモルフォロジー演算と組み合わせて、様々な画像処理タスクに利用されます。

  1. ノイズ除去: バイナリ画像に存在する小さな黒い点(前景中の背景ノイズ、pepper noise)を埋めるのに役立ちます。膨張によってこれらの小さな穴が閉じられます。
  2. オブジェクトの連結: 切断されたオブジェクト(例えば、細い線で描かれた文字が途切れている場合)を繋ぎ合わせるために使用できます。適切な構造要素を使うことで、隣接するオブジェクトを結合できます。
  3. オブジェクトの拡大: オブジェクトのサイズを意図的に大きくしたい場合に単純に適用します。
  4. 輪郭の強調: オブジェクトの輪郭を膨張させることで、より太く、目立つようにできます。
  5. 画像のセグメンテーションの前処理: オブジェクトを識別しやすくするために、前処理として膨張を適用することがあります。
  6. Opening (オープニング) および Closing (クロージング) 演算の構成要素:
    • Opening = Erosion ثم Dilation (収縮 → 膨張): 小さなノイズ(白い点、salt noise)を除去し、オブジェクトの境界を滑らかにする効果があります。収縮でノイズを除去し、その後に同じ構造要素で膨張して元のオブジェクトのサイズに戻そうとします(ノイズは除去されたまま)。
    • Closing = Dilation ثم Erosion (膨張 → 収縮): オブジェクト内の小さな穴や、近くにあるオブジェクト間の小さな隙間を埋める効果があります。膨張で隙間を埋め、その後に同じ構造要素で収縮してオブジェクトのサイズを元に戻そうとします(隙間は埋まったまま)。

このように、膨張処理は単なる拡大ではなく、画像の構造を操作する強力なツールであることがわかります。

Pythonでの実装:OpenCVを用いた膨張処理

画像処理において最も広く使われているライブラリの一つにOpenCV (cv2) があります。OpenCVは高速なC++で書かれており、Pythonを含む様々な言語から利用できます。OpenCVを使った膨張処理は非常に簡単です。

必要なライブラリのインポート

まず、OpenCVライブラリと数値計算ライブラリであるNumPyをインポートします。画像を可視化するためにMatplotlibもインポートしておくと便利です。

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

画像の読み込みと準備

膨張処理を適用する前に、画像を読み込み、必要に応じてバイナリ画像に変換します。ここでは簡単な例として、NumPyを使って手動でシンプルなバイナリ画像を作成してみましょう。

“`python

50×50の黒い画像を作成 (背景)

image = np.zeros((50, 50), dtype=np.uint8)

前景として白い正方形を描画 (中心付近)

image[20:30, 20:30] = 255 # 白は255 (uint8の場合)

前景として白い点を描画 (ノイズを模倣)

image[5, 5] = 255
image[5, 6] = 255 # 隣接する2ピクセルノイズ
image[45, 45] = 255

前景として細い線を描画 (途切れたオブジェクトを模倣)

image[10:15, 40] = 255
image[10:15, 42] = 255 # 間に隙間がある

元画像を表示

plt.figure(figsize=(6, 3))
plt.subplot(1, 2, 1)
plt.imshow(image, cmap=’gray’)
plt.title(‘Original Image (Binary)’)
plt.axis(‘off’)
“`

これで、白い正方形、いくつかのノイズ点、そして間に隙間のある細い線からなる50×50ピクセルのバイナリ画像が用意できました。

構造要素の作成

次に、膨張処理に使用する構造要素(カーネル)を作成します。OpenCVでは cv2.getStructuringElement() 関数を使って様々な形状の構造要素を生成できます。

構文: cv2.getStructuringElement(shape, ksize[, anchor])

  • shape: 構造要素の形状を指定します。
    • cv2.MORPH_RECT: 矩形(正方形/長方形)
    • cv2.MORPH_ELLIPSE: 楕円形(円形を含む)
    • cv2.MORPH_CROSS: 十字形
  • ksize: 構造要素のサイズを指定します。(幅, 高さ) のタプルで指定します。通常は幅と高さを同じにして正方形のカーネルを作成します。サイズが大きいほど膨張効果は強くなります。
  • anchor: 構造要素の原点(中心)を指定します。デフォルトは (-1, -1) で、これは中心を示す特別な値です。通常はデフォルトで問題ありません。

例:3×3の正方形カーネルを作成

python
kernel_square = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
print("3x3 Square Kernel:\n", kernel_square)

出力例:
3x3 Square Kernel:
[[1 1 1]
[1 1 1]
[1 1 1]]

例:5×5の楕円形カーネルを作成

python
kernel_ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
print("\n5x5 Ellipse Kernel:\n", kernel_ellipse)

出力例:
5x5 Ellipse Kernel:
[[0 0 1 0 0]
[0 1 1 1 0]
[1 1 1 1 1]
[0 1 1 1 0]
[0 0 1 0 0]]

(円形に近い形状になります)

例:3×3の十字形カーネルを作成

python
kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
print("\n3x3 Cross Kernel:\n", kernel_cross)

出力例:
3x3 Cross Kernel:
[[0 1 0]
[1 1 1]
[0 1 0]]

構造要素の形状によって、膨張の方向や度合いが異なります。矩形は全方向に均等に、十字形は水平・垂直方向に強く膨張させます。楕円形は円形に近い膨張効果をもたらします。

cv2.dilate() 関数の利用

いよいよ膨張処理を実行します。OpenCVでは cv2.dilate() 関数を使用します。

構文: cv2.dilate(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]])

  • src: 入力画像(NumPy配列)
  • kernel: 使用する構造要素(NumPy配列)。cv2.getStructuringElement() で作成したもの。None を指定すると、デフォルトの3×3の矩形カーネルが使用されます。
  • dst: 出力画像。省略可能。
  • anchor: 構造要素の原点。デフォルトは (-1,-1) で中心。
  • iterations: 膨張処理を繰り返す回数。デフォルトは1回です。複数回繰り返すと、その分だけ膨張効果が強くなります。
  • borderType, borderValue: 画像の境界処理に関する設定。通常はデフォルトで問題ありません。

最も基本的な使い方は、入力画像とカーネル、そして繰り返しの回数を指定することです。

例:元の画像に3×3の正方形カーネルで1回膨張を適用

“`python

3×3の正方形カーネル

kernel_square = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))

膨張処理の実行

dilated_image_square_1 = cv2.dilate(image, kernel_square, iterations=1)

結果の表示

plt.subplot(1, 2, 2)
plt.imshow(dilated_image_square_1, cmap=’gray’)
plt.title(‘Dilated Image (Square, 1 iter)’)
plt.axis(‘off’)
plt.tight_layout()
plt.show()
“`

このコードを実行すると、元の画像に加えて、膨張処理された画像が表示されます。
元の画像では20×20の正方形の輪郭が少し太くなり、5,5や45,45のノイズ点も少し大きくなっているのがわかります。また、10:15, 40と10:15, 42の間にあった隙間が埋まって、一本の線になっているはずです。これが膨張処理の効果です。

異なるカーネルと繰り返しの影響

構造要素の形状や繰り返し回数を変えると、結果がどのように変わるか見てみましょう。

例:5×5の楕円形カーネルで1回膨張

“`python
kernel_ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
dilated_image_ellipse_1 = cv2.dilate(image, kernel_ellipse, iterations=1)

plt.figure(figsize=(6, 3))
plt.subplot(1, 2, 1)
plt.imshow(image, cmap=’gray’)
plt.title(‘Original Image’)
plt.axis(‘off’)

plt.subplot(1, 2, 2)
plt.imshow(dilated_image_ellipse_1, cmap=’gray’)
plt.title(‘Dilated Image (Ellipse, 1 iter)’)
plt.axis(‘off’)
plt.tight_layout()
plt.show()
“`
5×5の楕円形カーネルを使うと、元の正方形は角が丸みを帯びて膨張します。ノイズ点や線も、より円形に近い形状で膨張するのが確認できます。

例:3×3の正方形カーネルで3回膨張

“`python
kernel_square = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
dilated_image_square_3 = cv2.dilate(image, kernel_square, iterations=3)

plt.figure(figsize=(6, 3))
plt.subplot(1, 2, 1)
plt.imshow(image, cmap=’gray’)
plt.title(‘Original Image’)
plt.axis(‘off’)

plt.subplot(1, 2, 2)
plt.imshow(dilated_image_square_3, cmap=’gray’)
plt.title(‘Dilated Image (Square, 3 iter)’)
plt.axis(‘off’)
plt.tight_layout()
plt.show()
“`
繰り返し回数を増やすと、膨張効果が累積されます。3回繰り返すことで、元の正方形はかなり大きくなり、ノイズ点もより大きな塊になり、切れていた線はより太く繋がっているはずです。

グレースケール画像の膨張(最大値フィルタとしての振る舞い)

先述の通り、膨張処理はグレースケール画像に対しても適用可能です。この場合、構造要素が重なる領域での最大値をとるフィルタとして機能します。

“`python

簡単なグレースケール画像を生成

gray_image = np.array([
[10, 20, 30, 40, 50],
[20, 30, 40, 50, 60],
[30, 100, 80, 70, 90], # 中心付近に明るいピクセル
[40, 50, 60, 70, 80],
[50, 60, 70, 80, 90]
], dtype=np.uint8)

3×3の正方形カーネル

kernel_square_3x3 = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))

グレースケール画像に膨張処理を適用

dilated_gray_image = cv2.dilate(gray_image, kernel_square_3x3, iterations=1)

print(“Original Grayscale Image:\n”, gray_image)
print(“\nDilated Grayscale Image (3×3 square):\n”, dilated_gray_image)

可視化 (ピクセル値を見やすくするため、敢えてimshowは使用しない)

もし画像として表示するなら cmap=’gray’ を使う

plt.imshow(gray_image, cmap=’gray’), plt.imshow(dilated_gray_image, cmap=’gray’)

“`

出力例(ピクセル値):
元の画像で中心付近の 100 という明るいピクセルが、膨張後の画像ではその周辺に広がっている(周囲のピクセル値が 100 になっている)ことが確認できます。例えば、元の画像で値が20だったピクセル位置でも、膨張後は隣接する明るいピクセル(30や100)の影響を受けて値が増加しています。これは最大値フィルタの典型的な振る舞いです。グレースケール画像の膨張は、明るい領域を拡大し、暗い領域にある小さな凹み(ピークではない谷)を埋める効果があります。

Pythonでの実装:scikit-imageを用いた膨張処理

scikit-imageは、科学技術計算によく使われる画像処理ライブラリです。モルフォロジー演算も充実しており、OpenCVと同様に膨張処理を簡単に行うことができます。

必要なライブラリのインポート

scikit-imageの morphology モジュールと、画像表示のための io (またはMatplotlib) をインポートします。NumPyも必要です。

python
import numpy as np
from skimage import io, morphology
import matplotlib.pyplot as plt

画像の読み込みと準備 (OpenCVの例と同じ画像を使用)

OpenCVの例と同じように、NumPyで手動で画像を作成します。scikit-imageのモルフォロジー関数は、入力画像をboolean型(True/False)として扱うのが一般的です。前景をTrue、背景をFalseとします。OpenCVでよく使う uint8 (0/255) の画像もそのまま入力できますが、内部でbooleanに変換されることが多いです。明示的にbooleanに変換する方が推奨されます。

“`python

50×50の黒い画像を作成 (背景)

image_uint8 = np.zeros((50, 50), dtype=np.uint8)

前景として白い正方形を描画 (中心付近)

image_uint8[20:30, 20:30] = 255 # 白は255

前景として白い点を描画 (ノイズを模倣)

image_uint8[5, 5] = 255
image_uint8[5, 6] = 255
image_uint8[45, 45] = 255

前景として細い線を描画 (途切れたオブジェクトを模倣)

image_uint8[10:15, 40] = 255
image_uint8[10:15, 42] = 255

boolean型に変換 (0以外をTrueとする)

image_bool = image_uint8 > 0

元画像を表示

plt.figure(figsize=(6, 3))
plt.subplot(1, 2, 1)
plt.imshow(image_bool, cmap=’gray’) # boolean画像もgray cmapで表示できる
plt.title(‘Original Image (Boolean)’)
plt.axis(‘off’)
“`

構造要素の作成

scikit-imageの morphology モジュールにも、様々な形状の構造要素を生成する関数があります。

  • morphology.square(radius): 半径 radius の正方形 ((2*radius+1) x (2*radius+1))。
  • morphology.disk(radius): 半径 radius の円形(ディスク)。
  • morphology.diamond(radius): 半径 radius のひし形。
  • morphology.star(radius): 半径 radius の星形。
  • NumPy配列で手動で作成することも可能です。

これらの関数は、直接 boolean 型の構造要素配列を返します。

例:半径1の正方形カーネルを作成(3×3正方形に相当)

python
kernel_square_sk = morphology.square(1)
print("3x3 Square Kernel (skimage, radius=1):\n", kernel_square_sk)

出力例:
3x3 Square Kernel (skimage, radius=1):
[[ True True True]
[ True True True]
[ True True True]]

例:半径2の円形カーネルを作成(5×5程度の円形に相当)

python
kernel_disk_sk = morphology.disk(2)
print("\n5x5 Disk Kernel (skimage, radius=2):\n", kernel_disk_sk)

出力例:
5x5 Disk Kernel (skimage, radius=2):
[[False False True False False]
[ True True True True True]
[ True True True True True]
[ True True True True True]
[False False True False False]]

(OpenCVの楕円形とは形状が少し異なりますが、同様に円形に近いものです)

skimage.morphology.binary_dilation() の利用

scikit-imageでバイナリ画像の膨張処理を行うには、skimage.morphology.binary_dilation() 関数を使用します。

構文: skimage.morphology.binary_dilation(image, selem=None, out=None)

  • image: 入力画像(boolean型または0/1を含む整数型/浮動小数点型NumPy配列)
  • selem: 使用する構造要素(boolean型または0/1を含むNumPy配列)。省略すると、デフォルトの3×3の正方形カーネルが使用されます。
  • out: 結果を格納する出力配列。省略可能。

繰り返し回数は、この関数には直接指定できません。複数回膨張したい場合は、関数を必要な回数だけ繰り返し呼び出す必要があります。

例:元の画像に半径1の正方形カーネルで1回膨張を適用

“`python

半径1の正方形カーネル (3×3)

kernel_square_sk = morphology.square(1)

膨張処理の実行

dilated_image_sk_square_1 = morphology.binary_dilation(image_bool, selem=kernel_square_sk)

結果の表示

plt.subplot(1, 2, 2)
plt.imshow(dilated_image_sk_square_1, cmap=’gray’)
plt.title(‘Dilated Image (skimage, Square, 1 iter)’)
plt.axis(‘off’)
plt.tight_layout()
plt.show()
“`
OpenCVの例と同様に、正方形の拡大、ノイズ点の拡大、線の連結が確認できます。

異なるカーネルと繰り返しの影響 (scikit-image)

scikit-imageでも、異なるカーネルや繰り返し回数で試してみましょう。繰り返しは関数をループで呼び出す必要があります。

例:半径2の円形カーネルで1回膨張

“`python
kernel_disk_sk = morphology.disk(2)
dilated_image_sk_disk_1 = morphology.binary_dilation(image_bool, selem=kernel_disk_sk)

plt.figure(figsize=(6, 3))
plt.subplot(1, 2, 1)
plt.imshow(image_bool, cmap=’gray’)
plt.title(‘Original Image (Boolean)’)
plt.axis(‘off’)

plt.subplot(1, 2, 2)
plt.imshow(dilated_image_sk_disk_1, cmap=’gray’)
plt.title(‘Dilated Image (skimage, Disk, 1 iter)’)
plt.axis(‘off’)
plt.tight_layout()
plt.show()
“`
円形カーネルを使った膨張は、オブジェクトの角を丸めます。

例:半径1の正方形カーネルで3回膨張

“`python
kernel_square_sk = morphology.square(1)
dilated_image_sk_square_3 = image_bool # 初期化
for _ in range(3):
dilated_image_sk_square_3 = morphology.binary_dilation(dilated_image_sk_square_3, selem=kernel_square_sk)

plt.figure(figsize=(6, 3))
plt.subplot(1, 2, 1)
plt.imshow(image_bool, cmap=’gray’)
plt.title(‘Original Image (Boolean)’)
plt.axis(‘off’)

plt.subplot(1, 2, 2)
plt.imshow(dilated_image_sk_square_3, cmap=’gray’)
plt.title(‘Dilated Image (skimage, Square, 3 iter)’)
plt.axis(‘off’)
plt.tight_layout()
plt.show()
``
ループを使って3回膨張を繰り返すことで、OpenCVの
iterations=3` と同様の効果が得られます。

グレースケール画像の膨張 (scikit-image)

scikit-imageでグレースケール画像の膨張を行うには、skimage.morphology.erosion()skimage.morphology.dilation() 関数を使用します(なぜかbinary_というプレフィックスがない関数がグレースケール用です)。ただし、erosiondilation はデフォルトでグレースケール用です。バイナリ画像を扱う場合は binary_erosionbinary_dilation を使う方が意図が明確になります。グレースケール膨張はOpenCVと同じく最大値フィルタとして機能します。

“`python

簡単なグレースケール画像を生成 (OpenCVの例と同じ)

gray_image_sk = np.array([
[10, 20, 30, 40, 50],
[20, 30, 40, 50, 60],
[30, 100, 80, 70, 90],
[40, 50, 60, 70, 80],
[50, 60, 70, 80, 90]
], dtype=np.uint8) # uint8型のまま

半径1の正方形カーネル (3×3)

kernel_square_sk_gray = morphology.square(1) # グレースケール膨張でも boolean カーネルを使用

グレースケール画像に膨張処理を適用

dilated_gray_image_sk = morphology.dilation(gray_image_sk, selem=kernel_square_sk_gray)

print(“Original Grayscale Image (skimage):\n”, gray_image_sk)
print(“\nDilated Grayscale Image (skimage, 3×3 square):\n”, dilated_gray_image_sk)
“`
出力結果はOpenCVのグレースケール膨張と同様に、最大値フィルタとして機能していることが確認できます。

OpenCVとscikit-imageはどちらも強力な画像処理ライブラリであり、膨張処理を提供しています。どちらを選ぶかは、プロジェクト全体の依存関係や個人の好みによるでしょう。OpenCVは処理速度に優れ、リアルタイム処理などに強い一方、scikit-imageはNumPyとの親和性が高く、研究開発やデータ分析のワークフローに組み込みやすいという特徴があります。バイナリ画像に対しては binary_dilation のように明示的な関数を持つscikit-imageの方が、バイナリ処理であることが分かりやすいかもしれません。

膨張処理の応用例:OpeningとClosing

膨張処理は、収縮処理(Erosion)と組み合わせることで、OpeningとClosingという重要な演算を構成します。これらの演算はノイズ除去やオブジェクトの整形によく用いられます。

  • オープニング(Opening): 収縮 → 膨張 の順で行います。
    • 効果: 小さな前景ノイズ(白い点)を除去し、オブジェクトの形状を滑らかにする。オブジェクトを構成する細い連結部を破断することがある。
    • イメージ: 構造要素がオブジェクト内を完全に収まる範囲を「開いて」、その開いた部分を構造要素で「満たす」感じ。
  • クロージング(Closing): 膨張 → 収縮 の順で行います。
    • 効果: オブジェクト内の小さな穴や、近くにあるオブジェクト間の小さな隙間を埋める。オブジェクトの外側の小さなノイズ(黒い点)を除去する。
    • イメージ: 構造要素がオブジェクトの穴や隙間を「閉じて」、その閉じられた領域を構造要素で「トリミング」する感じ。

どちらの演算も、収縮と膨張には同じ構造要素を使用するのが一般的です。

OpenCVとscikit-imageは、これらのOpening/Closing演算のための関数も提供しています。

OpenCVでのOpening/Closing

OpenCVでは、cv2.morphologyEx() 関数を使って、様々なモルフォロジー演算を一括で実行できます。

構文: cv2.morphologyEx(src, op, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]])

  • src: 入力画像
  • op: 実行するモルフォロジー演算の種類を指定します。
    • cv2.MORPH_OPEN: オープニング
    • cv2.MORPH_CLOSE: クロージング
    • cv2.MORPH_DILATE: 膨張(cv2.dilate() と同じ)
    • cv2.MORPH_ERODE: 収縮(cv2.erode() と同じ)
    • その他、Gradient, Top Hat, Black Hatなど
  • kernel: 使用する構造要素
  • iterations: 繰り返し回数 (Opening/Closingの場合は、収縮と膨張の両方が指定回数繰り返されます)

例:元のバイナリ画像に対して、3×3正方形カーネルでOpeningとClosingを適用

“`python

元画像 (OpenCVの例で使用した uint8 画像を再利用)

image = … (前述のコードで作成した np.uint8 の image 変数)

3×3の正方形カーネル

kernel_square_3x3 = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))

Opening処理

opening_image = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel_square_3x3)

Closing処理

closing_image = cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernel_square_3x3)

結果の表示

plt.figure(figsize=(9, 3))
plt.subplot(1, 3, 1)
plt.imshow(image, cmap=’gray’)
plt.title(‘Original Image’)
plt.axis(‘off’)

plt.subplot(1, 3, 2)
plt.imshow(opening_image, cmap=’gray’)
plt.title(‘Opening (Square 3×3)’)
plt.axis(‘off’)

plt.subplot(1, 3, 3)
plt.imshow(closing_image, cmap=’gray’)
plt.title(‘Closing (Square 3×3)’)
plt.axis(‘off’)
plt.tight_layout()
plt.show()
“`
この例では、Opening処理によって小さなノイズ点(5,5や45,45の白い点)が除去されていることが期待されます。また、Closing処理によって、切断されていた線(10:15, 40と10:15, 42)の間の隙間が埋まっていることが期待されます。

scikit-imageでのOpening/Closing

scikit-imageでも同様に関数が提供されています。

  • skimage.morphology.binary_opening(image, selem=None)
  • skimage.morphology.binary_closing(image, selem=None)
  • グレースケール用として skimage.morphology.opening() および skimage.morphology.closing() もあります。

例:元のバイナリ画像に対して、半径1正方形カーネルでOpeningとClosingを適用

“`python

元画像 (scikit-imageの例で使用した boolean 画像を再利用)

image_bool = … (前述のコードで作成した boolean の image_bool 変数)

半径1の正方形カーネル (3×3)

kernel_square_sk = morphology.square(1)

Opening処理

opening_image_sk = morphology.binary_opening(image_bool, selem=kernel_square_sk)

Closing処理

closing_image_sk = morphology.binary_closing(image_bool, selem=kernel_square_sk)

結果の表示 (OpenCVの例とほぼ同じ結果が得られるはずです)

plt.figure(figsize=(9, 3))
plt.subplot(1, 3, 1)
plt.imshow(image_bool, cmap=’gray’)
plt.title(‘Original Image (Boolean)’)
plt.axis(‘off’)

plt.subplot(1, 3, 2)
plt.imshow(opening_image_sk, cmap=’gray’)
plt.title(‘Opening (skimage, Square 3×3)’)
plt.axis(‘off’)

plt.subplot(1, 3, 3)
plt.imshow(closing_image_sk, cmap=’gray’)
plt.title(‘Closing (skimage, Square 3×3)’)
plt.axis(‘off’)
plt.tight_layout()
plt.show()
“`

OpeningとClosingは、膨張処理が単体で使われるだけでなく、他の基本的な演算と組み合わせてより高度なタスクを遂行する良い例です。膨張処理は、Closingにおける「隙間を埋める」段階で重要な役割を果たします。

構造要素の選択と設計

膨張処理の結果は、使用する構造要素の形状サイズに大きく依存します。適切な構造要素を選択することは、望む処理結果を得る上で非常に重要です。

  • 形状:
    • 正方形 (Rectangular): 全方向に均等に膨張させたい場合や、一般的なノイズ除去/隙間埋めに使われます。最もシンプルで汎用的な形状です。
    • 十字形 (Cross): 水平方向と垂直方向に強く膨張させたい場合に有効です。例えば、縦横に走る線やグリッド構造の処理に使われることがあります。
    • 円形/楕円形 (Disk/Elliptical): オブジェクトの角を丸めながら膨張させたい場合に使われます。円形のオブジェクトや、滑らかな輪郭を持つオブジェクトの処理に適しています。
  • サイズ:
    • 構造要素のサイズが大きいほど、膨張効果は強くなります。
    • 小さなノイズや隙間を処理したい場合は、それらのサイズよりもわずかに大きい程度の構造要素を選ぶのが一般的です。
    • 過度に大きい構造要素を使うと、目的のオブジェクト形状が大きく歪んだり、無関係なオブジェクト同士が結合してしまったりする可能性があります。
    • 適切なサイズは、処理対象となる画像やオブジェクトの特徴に応じて経験的に決定されることが多いです。

構造要素は、cv2.getStructuringElement()skimage.morphology の関数で生成できますが、特定の形状や効果が必要な場合はNumPy配列として手動で作成することも可能です。例えば、特定の角度の線形構造要素を作成して、特定の方向の線だけを強調・連結するような処理も考えられます。

例:斜め方向の線形構造要素

“`python

5×5の斜め線形構造要素 (右下がり)

kernel_diagonal = np.array([
[1, 0, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 0, 1]
], dtype=np.uint8) # OpenCV用ならuint8, skimage boolean_dilation用ならbool

この構造要素で膨張すると、右下がりの線を強調したり、途切れを繋いだりする効果が期待できる。

“`

膨張処理の計算コストと考慮事項

モルフォロジー演算、特に膨張と収縮は、カーネル処理の一種であり、一般的に計算コストは比較的低い部類に入ります。処理時間は主に以下の要因に依存します。

  1. 画像サイズ: 画像のピクセル数に比例します。
  2. 構造要素のサイズ: 構造要素が大きいほど、各ピクセルでの計算量が増えるため、処理時間は増加します。構造要素の面積(ピクセル数)にほぼ比例します。
  3. 繰り返し回数 (Iterations): 繰り返しの回数に比例して処理時間が増加します。
  4. 実装効率: 使用するライブラリ(OpenCVやscikit-image)の実装効率によっても異なります。OpenCVは通常、最適化されており高速です。

リアルタイム処理などで高速性が求められる場合は、構造要素のサイズを小さく抑えたり、繰り返し回数を最小限にしたりすることが重要です。必要に応じて、より高速なライブラリの実装(OpenCVなど)を選択することも考慮しましょう。

また、画像の端(境界)での処理にも注意が必要です。構造要素の全体が画像内に収まらないため、境界付近のピクセルは中心付近とは異なる影響を受ける可能性があります。多くのライブラリでは、境界外のピクセルを0(背景)とみなしたり、画像の端のピクセル値を繰り返したりするなど、境界処理の方法を選択できますが、デフォルト設定で問題ない場合が多いです。

収縮処理(Erosion)との双対性(Duality)

膨張処理と収縮処理は、互いに双対の関係にあります。これは、前景と背景を入れ替えた画像に対して一方の処理を行うと、元の画像に対して他方の処理を行った結果の前景と背景が入れ替わったものになる、ということです。

数学的には、画像 $A$ と構造要素 $B$ に対して、収縮処理 $A \ominus B$ は以下のように定義されます。

$A \ominus B = {z \mid (B_z \subseteq A) }$

これは、「構造要素 $B$ を中心 $z$ に置いたときに、構造要素全体が画像の前景集合 $A$ の中に完全に含まれるような点 $z$ の集合」です。膨張が「一つでも重なれば前景」だったのに対し、収縮は「完全に含まれなければ背景」となります。

双対性とは、以下の関係が成り立つことです。

$(A \oplus B)^c = A^c \ominus B’$
$(A \ominus B)^c = A^c \oplus B’$

ここで $A^c$ は画像 $A$ の補集合(前景と背景を反転させた画像)、$B’$ は構造要素 $B$ を原点に対して点対称に反転させたものです。通常、構造要素は中心に対して対称な形状(正方形、円形、十字形など)を選ぶことが多いため、$B’ = B$ となり、双対性は単純に以下のようになります。

$(A \oplus B)^c = A^c \ominus B$
$(A \ominus B)^c = A^c \oplus B$

これは、「画像を反転させて収縮すると、元の画像を膨張させてから反転したのと同じになる」「画像を反転させて膨張すると、元の画像を収縮させてから反転したのと同じになる」ことを意味します。

この双対性は、理論的な美しさだけでなく、実装上の工夫にもつながることがあります。例えば、膨張処理が定義されていないが収縮処理が定義されている特殊な構造要素を使う場合などに応用できる可能性があります。また、Openingが「収縮→膨張」であり、Closingが「膨張→収縮」であることも、この双対性と深く関連しています。

膨張処理のワークフローにおける位置づけ

実際の画像処理パイプラインにおいて、膨張処理は単独で使われることは少なく、他の前処理や後処理と組み合わせて利用されることが多いです。一般的なワークフローにおける膨張処理の位置づけを考えてみましょう。

  1. 前処理:
    • ノイズ除去: メディアンフィルタなどで初期ノイズを軽減した後、バイナリ化。その後、Closing(膨張→収縮)で小さな穴を埋める。
    • バイナリ化: グレースケール画像を閾値処理などでバイナリ画像に変換。この際、ノイズや細い線が失われたり、オブジェクトが分断されたりすることがあり、それを修正するために膨張処理やClosingが有効。
  2. 主要処理:
    • オブジェクト検出/セグメンテーション: オブジェクトの領域を特定する際に、膨張処理でオブジェクトを拡大し、近接するオブジェクトを結合したり、見つけにくい小さなオブジェクトを強調したりする前処理として使用。
    • 特徴抽出: オブジェクトの形状特徴を分析する前に、境界を膨張させて輪郭を明確にしたり、ノイズによる凹凸を滑らかにしたりする。
  3. 後処理:
    • マスク処理: 抽出したオブジェクト領域(マスク画像)に対して膨張を適用し、領域をわずかに拡大して、元の画像からオブジェクトを切り出す際に境界をきれいに含める。
    • テキスト処理: OCRなどで認識された文字領域(バウンディングボックスやマスク)に対して膨張を適用し、文字の一部欠損や文字間のスペースを修正する。

このように、膨張処理は画像のピクセルレベルでの操作でありながら、より高次のオブジェクトや構造に関する処理を助けるための重要なステップとして機能します。

まとめ:膨張処理を使いこなすために

この記事では、画像処理における膨張処理(Dilate)について、その理論からPythonでの実装方法、応用例までを詳しく見てきました。

  • 膨張処理は、画像中の前景領域を構造要素の形に沿って拡大するモルフォロジー演算です。
  • バイナリ画像に対しては前景ピクセルを拡張し、グレースケール画像に対しては最大値フィルタとして機能します。
  • 構造要素の形状とサイズ、そして繰り返し回数が処理結果に大きく影響します。適切なパラメータ選択が重要です。
  • OpenCV (cv2.dilate, cv2.morphologyEx) と scikit-image (skimage.morphology.binary_dilation, skimage.morphology.dilation) は、Pythonで膨張処理を実装するための主要なライブラリです。
  • 膨張処理は、OpeningやClosingといったより複雑なモルフォロジー演算の基礎となります。
  • ノイズ除去、オブジェクトの連結、隙間の充填など、様々な画像処理タスクの前処理や後処理として広く利用されます。

膨張処理は比較的シンプルな演算ですが、そのメカニズムを正しく理解し、構造要素の影響を把握することで、画像処理の可能性が大きく広がります。ぜひ様々な画像や異なる構造要素で実験を行い、膨張処理の効果を体感してみてください。そして、収縮処理や他のモルフォロジー演算と組み合わせて、より高度な画像処理タスクに挑戦してみてください。

この記事が、Pythonを使った膨張処理の学習の一助となれば幸いです。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール