はい、承知いたしました。Python OpenCV(cv2)を使った画像処理の基本について、約5000語の詳細な解説記事を作成します。
Python OpenCV(cv2)入門:画像処理の基本を徹底解説
画像処理は、デジタル画像を操作し、解析する技術分野です。コンピュータビジョン、機械学習、医療画像、エンターテイメントなど、様々な分野で不可欠な技術となっています。そして、この画像処理の分野で最も広く使われているライブラリの一つが OpenCV (Open Source Computer Vision Library) です。
OpenCVは、C++、Java、Pythonなど多くの言語に対応しており、特にPythonとの組み合わせは、その手軽さと豊富なNumPyライブラリとの連携の良さから、多くの研究者や開発者に選ばれています。
この記事では、Pythonを使ったOpenCV(cv2
モジュール)の入門として、画像処理の基本的な操作や概念を徹底的に解説します。環境構築から始まり、画像の読み込み、表示、保存、様々な変換、フィルタリング、特徴抽出といった基礎の基礎から丁寧に見ていきましょう。
1. はじめに:OpenCVとは何か?
1.1 OpenCVの概要
OpenCVは、リアルタイムのコンピュータビジョンアプリケーション開発を目的としてIntelによって開発された、クロスプラットフォームのオープンソースライブラリです。非常に高速に動作するように最適化されており、C++で書かれていますが、Pythonを含む様々な言語から利用できるようになっています。
OpenCVには、以下のような非常に幅広い機能が含まれています。
- 画像処理: 画素単位の操作、色空間変換、フィルタリング、幾何学的変換、しきい値処理、モルフォロジー変換、ヒストグラム処理など
- 特徴点検出と記述: SIFT, SURF, ORBなどのアルゴリズム
- 物体検出: Haar-like特徴を用いたカスケード分類器(顔検出など)、HOG特徴を用いた物体検出
- 動画解析: オプティカルフロー、背景差分
- 3D再構成: ステレオビジョン、Structure from Motion
- 機械学習: K-Meansクラスタリング、SVM、CNNなど、基本的な機械学習アルゴリズムの一部
1.2 なぜPythonでOpenCVを使うのか?
OpenCVの主要な実装はC++ですが、Python APIが非常に充実しており、多くのユーザーに利用されています。PythonでOpenCVを使うことの利点は以下の通りです。
- 開発効率: Pythonはコードが簡潔で記述が容易なため、素早くプロトタイプを作成したり、実験を行ったりするのに適しています。
- NumPyとの連携: OpenCVの画像データは、NumPy配列として扱われます。これにより、NumPyが提供する高速な数値計算機能や配列操作機能をそのまま画像処理に応用できます。画素値へのアクセスや加工が非常に直感的に行えます。
- 豊富なエコシステム: Pythonには、Matplotlib(データ可視化)、scikit-image(別の画像処理ライブラリ)、TensorFlow/PyTorch(深層学習)など、画像処理やコンピュータビジョンに関連する強力なライブラリが多数存在します。これらのライブラリとOpenCVを組み合わせて使うことで、より高度な処理が可能になります。
一方で、パフォーマンスが最優先されるリアルタイムシステムや組み込みシステムなどでは、C++版のOpenCVが選ばれることもあります。しかし、多くのアプリケーションにおいて、Python版のOpenCVは十分なパフォーマンスを発揮します。
1.3 この記事で学ぶこと
この記事では、PythonとOpenCVを使って画像処理の基本的な操作を行う方法を学びます。具体的には、以下のトピックを扱います。
- OpenCV環境の構築
- 画像の読み込み、表示、保存
- 画像の色空間変換
- 画像のサイズ変更と切り抜き
- 画像の回転と平行移動
- 画像への図形やテキストの描画
- 画素へのアクセスと編集、画像間の算術演算
- しきい値処理(画像の2値化)
- 画像の平滑化(フィルタリング)
- モルフォロジー変換(膨張、収縮など)
- 輪郭検出
- エッジ検出
- ヒストグラムとその応用(ヒストグラム平坦化)
これらの基本をしっかりと理解することで、より高度な画像処理やコンピュータビジョンの技術を学ぶための土台ができます。
2. 環境構築
PythonでOpenCVを使うための環境構築は非常に簡単です。Pythonがインストールされていることを前提に説明します。推奨されるPythonのバージョンは3.6以降です。
2.1 Pythonのインストール確認
ターミナルまたはコマンドプロンプトを開き、以下のコマンドを実行してPythonがインストールされているか、またそのバージョンを確認してください。
“`bash
python –version
または
python3 –version
“`
表示されたバージョンが3.6以上であれば問題ありません。もしPythonがインストールされていない場合は、公式ウェブサイトからダウンロードしてインストールしてください。
2.2 OpenCVのインストール
Pythonのパッケージ管理ツールであるpip
を使って、OpenCVをインストールします。Python版OpenCVにはいくつか種類がありますが、一般的にはopencv-python
パッケージで十分です。追加でGUI機能や動画コーデックなどが欲しい場合は、opencv-contrib-python
などを使用することもありますが、ここでは基本的なopencv-python
をインストールします。
ターミナルまたはコマンドプロンプトで以下のコマンドを実行します。
bash
pip install opencv-python numpy matplotlib
numpy
はOpenCVが画像データをNumPy配列として扱うために必須のライブラリです。matplotlib
は、後のヒストグラム描画などで利用すると便利なので一緒にインストールしておきます。
2.3 インストールの確認
インストールが完了したら、Pythonの対話環境やスクリプト内でcv2
モジュールをインポートできるか確認します。
python
import cv2
print(cv2.__version__)
エラーが発生せずにOpenCVのバージョンが表示されれば、インストールは成功です。
3. 画像の読み込み、表示、保存
画像処理の最初のステップは、画像をメモリに読み込むことです。OpenCVでは、画像はNumPyの多次元配列として扱われます。
3.1 画像の読み込み: cv2.imread()
cv2.imread()
関数を使って、画像ファイルを読み込みます。
“`python
import cv2
画像ファイルのパスを指定
例: ‘input_image.jpg’ という名前の画像ファイルがある場合
(このファイルを記事と同じディレクトリに置くか、絶対パスを指定してください)
image_path = ‘input_image.jpg’
画像を読み込み
cv2.IMREAD_COLOR: カラー画像として読み込む (デフォルト)
cv2.IMREAD_GRAYSCALE: グレースケール画像として読み込む
cv2.IMREAD_UNCHANGED: アルファチャンネルを含めて読み込む
img_color = cv2.imread(image_path, cv2.IMREAD_COLOR)
img_gray = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
img_unchanged = cv2.imread(image_path, cv2.IMREAD_UNCHANGED) # アルファチャンネルがある場合
画像が正常に読み込めたか確認
if img_color is None:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
else:
print(“画像を正常に読み込みました。”)
print(f”カラー画像の形状: {img_color.shape}”) # (高さ, 幅, チャンネル数)
print(f”グレースケール画像の形状: {img_gray.shape}”) # (高さ, 幅)
# チャンネル数を確認することもできる
print(f”カラー画像の次元数: {img_color.ndim}”) # 3 (高さ, 幅, チャンネル)
print(f”グレースケール画像の次元数: {img_gray.ndim}”) # 2 (高さ, 幅)
“`
cv2.imread()
の第2引数には、画像をどのように読み込むかを指定するフラグを渡します。
* cv2.IMREAD_COLOR
または 1
: カラー画像として読み込みます。画像の透明度(アルファチャンネル)は無視されます。デフォルト値です。
* cv2.IMREAD_GRAYSCALE
または 0
: グレースケール画像として読み込みます。
* cv2.IMREAD_UNCHANGED
または -1
: 画像ファイルをそのまま読み込みます。アルファチャンネルが含まれていれば、それも読み込まれます。
読み込んだ画像データはNumPy配列です。カラー画像の場合、形状は(高さ, 幅, 3)
となります(OpenCVではBGRの順にチャンネルが並びます)。グレースケール画像の場合、形状は(高さ, 幅)
となります。
注意: 指定したパスに画像ファイルが存在しない場合や、ファイルが破損している場合、cv2.imread()
はNone
を返します。画像を読み込んだ後は、必ずNone
でないか確認することが重要です。
3.2 画像の表示: cv2.imshow()
読み込んだ画像を画面に表示するには、cv2.imshow()
関数を使います。
“`python
import cv2
image_path = ‘input_image.jpg’
img = cv2.imread(image_path, cv2.IMREAD_COLOR)
if img is not None:
# ウィンドウに画像を表示
# 第1引数: ウィンドウの名前 (文字列)
# 第2引数: 表示する画像データ (NumPy配列)
cv2.imshow(‘Color Image’, img)
# ウィンドウを表示したまま待機
# cv2.waitKey(0): 何かキーが押されるまで無限に待機
# cv2.waitKey(n): nミリ秒待機し、その間にキーが押されればそのキーのASCIIコードを返す。押されなければ-1を返す。
cv2.waitKey(0)
# 開いたウィンドウを全て閉じる
cv2.destroyAllWindows()
# 特定のウィンドウだけ閉じる場合は cv2.destroyWindow('ウィンドウ名')
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
cv2.imshow()
は新しいウィンドウを作成して画像を表示します。ウィンドウ名は一意にする必要があります。
cv2.waitKey()
は非常に重要です。この関数を呼び出さないと、cv2.imshow()
で表示されたウィンドウはすぐに閉じられてしまい、画像を見ることができません。通常、画像を一時停止して見たい場合はcv2.waitKey(0)
を使います。動画処理などでは、特定のフレームごとに数ミリ秒待つためにcv2.waitKey(n)
(n > 0)を使用します。
cv2.destroyAllWindows()
は、OpenCVによって作成された全てのウィンドウを閉じます。プログラムの終了時に呼び出すのが一般的です。
3.3 画像の保存: cv2.imwrite()
処理した画像をファイルとして保存するには、cv2.imwrite()
関数を使います。
“`python
import cv2
image_path = ‘input_image.jpg’
img = cv2.imread(image_path, cv2.IMREAD_COLOR)
if img is not None:
# グレースケールに変換(例として)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 処理した画像を保存
# 第1引数: 保存するファイル名 (拡張子でフォーマットを指定)
# 第2引数: 保存する画像データ (NumPy配列)
cv2.imwrite('gray_image.jpg', img_gray)
cv2.imwrite('gray_image.png', img_gray) # PNG形式で保存
print("画像を保存しました。")
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
cv2.imwrite()
の第1引数に指定するファイル名の拡張子(.jpg
, .png
, .bmp
など)によって、保存される画像フォーマットが決まります。OpenCVは様々なフォーマットに対応しています。JPEGのような非可逆圧縮形式で保存すると、画像データは劣化する可能性があることに注意してください。PNGのような可逆圧縮形式は劣化しません。
4. 画像の色空間変換
色空間は、色を表現するためのモデルです。一般的な色空間には、人間の知覚に近いRGB、印刷やペイントに使われるCMY(K)、ビデオ信号に使われるYUV/YCbCrなどがあります。画像処理では、目的に応じて異なる色空間に変換することがよくあります。
4.1 色空間とは?
- BGR: OpenCVがデフォルトで使用するカラー画像の色空間です。青 (Blue)、緑 (Green)、赤 (Red) の順にチャンネルが並んでいます。各チャンネルは通常0から255までの値をとります。
- グレースケール: 明度(輝度)情報のみを持つ単一チャンネルの色空間です。各画素は0(黒)から255(白)までの値をとります。
- HSV: 色相 (Hue)、彩度 (Saturation)、明度 (Value) の3つの要素で色を表現する色空間です。特定の色を検出する際に非常に便利です。
- H (Hue): 色の種類(赤、黄、緑、青など)。0〜180または0〜360の範囲で表現されることが多いですが、OpenCVでは8ビット画像の場合0〜179の範囲で表現されます。
- S (Saturation): 色の鮮やかさ。0に近いほど灰色になり、255に近いほど鮮やかになります。
- V (Value): 色の明るさ。0に近いほど暗く(黒く)なり、255に近いほど明るく(白く)なります。
4.2 色空間変換: cv2.cvtColor()
cv2.cvtColor()
関数を使って、画像を異なる色空間に変換できます。
“`python
import cv2
import numpy as np
image_path = ‘input_color_image.jpg’ # カラー画像を準備してください
img_bgr = cv2.imread(image_path, cv2.IMREAD_COLOR)
if img_bgr is not None:
# BGRからグレースケールへ変換
img_gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
# BGRからHSVへ変換
img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
# HSV画像は直接表示しても色は分かりにくい(通常H, S, Vチャンネルを分離して使う)
# 元画像、グレースケール画像、HSV画像の一部を表示(確認用)
cv2.imshow('Original BGR', img_bgr)
cv2.imshow('Grayscale', img_gray)
# HSV画像を表示しても色の見た目は変わるので、HSVチャンネルを分けてみる
h, s, v = cv2.split(img_hsv) # HSVチャンネルを分離
cv2.imshow('HSV - Hue channel', h)
cv2.imshow('HSV - Saturation channel', s)
cv2.imshow('HSV - Value channel', v)
print("色空間変換を行いました。")
print(f"元の画像形状: {img_bgr.shape}")
print(f"グレースケール画像形状: {img_gray.shape}")
print(f"HSV画像形状: {img_hsv.shape}")
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
cv2.cvtColor()
の第2引数には、変換元の色空間と変換先の組み合わせを指定するフラグを渡します。よく使われるフラグには以下があります。
cv2.COLOR_BGR2GRAY
: BGRからグレースケールへcv2.COLOR_GRAY2BGR
: グレースケールからBGRへ(これは単に1チャンネルを3チャンネルに複製するだけで、カラー情報は復元されません)cv2.COLOR_BGR2HSV
: BGRからHSVへcv2.COLOR_HSV2BGR
: HSVからBGRへ
4.3 HSV色空間を用いた特定色抽出
HSV色空間は、特定の色を範囲指定して抽出するのに非常に適しています。例えば、「赤い物体だけを検出したい」といった場合に利用します。HSVの各要素は色相、彩度、明度という人間の感覚に近い意味を持つため、直感的に色の範囲を指定しやすいのです。
特定の色範囲の画素だけを抽出するには、cv2.inRange()
関数を使います。
“`python
import cv2
import numpy as np
image_path = ‘input_color_image.jpg’ # 特定の色(例:青)を含む画像を準備してください
img_bgr = cv2.imread(image_path, cv2.IMREAD_COLOR)
if img_bgr is not None:
# BGRからHSVへ変換
img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
# 抽出したい色のHSV範囲を定義
# 例: 青色の範囲 (H: 100-120, S: 50-255, V: 50-255)
# 色のHSV値は、カラーピッカーツールやオンラインのHSVカラーチャートなどで調べることができます。
# Hの範囲はOpenCVでは0-179なので、通常の0-360範囲の半分になります。
# 例: 青 (通常H: 200-240) -> OpenCV H: 100-120
lower_blue = np.array([100, 50, 50])
upper_blue = np.array([120, 255, 255])
# 指定したHSV範囲内の画素を白(255)、それ以外を黒(0)とするマスクを作成
# マスクは元の画像と同じサイズで、単一チャンネル(グレースケール)の画像になります
mask_blue = cv2.inRange(img_hsv, lower_blue, upper_blue)
# マスクを使って元の画像から特定色の部分だけを抽出 (ビット演算)
# 結果画像は、マスクが白の部分だけ元の画像が表示され、黒の部分は0になります
# 第1引数: 元画像
# 第2引数: 元画像 (同じものを指定することが多い)
# mask: マスク画像
result_blue = cv2.bitwise_and(img_bgr, img_bgr, mask=mask_blue)
# 結果を表示
cv2.imshow('Original', img_bgr)
cv2.imshow('Blue Mask', mask_blue)
cv2.imshow('Blue Objects', result_blue)
print("特定色抽出を行いました。")
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
cv2.inRange(src, lowerb, upperb)
は、入力画像 src
の各要素が lowerb
から upperb
の範囲内にある場合に255、そうでない場合に0となるようなマスク画像を生成します。カラー画像の場合は、lowerb
とupperb
はそれぞれ3要素(HSVならH, S, V)のタプルまたはNumPy配列で指定します。
cv2.bitwise_and(src1, src2, mask=mask)
は、2つの画像 src1
と src2
に対してビット単位のAND演算を行います。mask
引数を指定すると、マスク画像の値が0である画素に対しては演算を行わず(結果は0)、マスク画像の値が0でない画素に対してのみ演算を行います。ここではsrc1
とsrc2
に同じ元画像を指定し、マスクによって特定色領域だけを残す操作を行っています。
5. 画像のサイズ変更
画像のサイズを変更することは、画像処理パイプラインの最初や、特定のアルゴリズムへの入力サイズを揃える際によく行われます。
5.1 サイズ変更: cv2.resize()
cv2.resize()
関数は、画像のサイズを変更するために使用されます。
“`python
import cv2
image_path = ‘input_image.jpg’ # 任意の画像を準備してください
img = cv2.imread(image_path)
if img is not None:
height, width = img.shape[:2]
print(f”元の画像サイズ: 幅={width}, 高さ={height}”)
# サイズを指定してリサイズ
# 例1: 幅100ピクセル、高さ50ピクセルに強制的に変更
resized_fixed = cv2.resize(img, (100, 50)) # (幅, 高さ)の順序に注意
# 拡大縮小率を指定してリサイズ
# 例2: 元のサイズの半分のサイズに縮小
scale_factor = 0.5
resized_scale = cv2.resize(img, None, fx=scale_factor, fy=scale_factor) # 出力サイズはNoneで、fx, fyで指定
# 例3: 元のサイズの2倍のサイズに拡大
scale_factor_large = 2.0
resized_large = cv2.resize(img, None, fx=scale_factor_large, fy=scale_factor_large)
# 補間方法を指定
# 例4: 線形補間 (デフォルト)、縮小に適している
resized_linear = cv2.resize(img, None, fx=scale_factor, fy=scale_factor, interpolation=cv2.INTER_LINEAR)
# 例5: キュービック補間、拡大に適している(計算コストは高い)
resized_cubic = cv2.resize(img, None, fx=scale_factor_large, fy=scale_factor_large, interpolation=cv2.INTER_CUBIC)
# 例6: 最近傍補間、最も高速だが画質は低い(特に拡大時)
resized_nearest = cv2.resize(img, None, fx=scale_factor_large, fy=scale_factor_large, interpolation=cv2.INTER_NEAREST)
print(f"固定サイズリサイズ形状: {resized_fixed.shape}")
print(f"スケールリサイズ形状 (0.5): {resized_scale.shape}")
print(f"スケールリサイズ形状 (2.0): {resized_large.shape}")
cv2.imshow('Original', img)
cv2.imshow('Resized Fixed (100x50)', resized_fixed)
cv2.imshow('Resized Scale (0.5)', resized_scale)
cv2.imshow('Resized Scale (2.0) - Linear', resized_large)
cv2.imshow('Resized Scale (2.0) - Cubic', resized_cubic)
cv2.imshow('Resized Scale (2.0) - Nearest', resized_nearest)
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
cv2.resize(src, dsize, fx=0, fy=0, interpolation)
:
* src
: 入力画像
* dsize
: 出力画像のサイズをタプル(幅, 高さ)
で指定します。(width, height)
の順序であることに注意してください(NumPy配列の形状は(height, width)
ですが、ここでは逆になります)。もしdsize
が(0, 0)
の場合、fx
とfy
に基づいてサイズが計算されます。
* fx
, fy
: x方向、y方向の拡大/縮小率です。dsize
が(0, 0)
でない場合は無視されます。
* interpolation
: 補間方法を指定します。画像の拡大/縮小において、新しい画素の値をどのように計算するかを決定します。
* cv2.INTER_NEAREST
: 最近傍補間。最も高速ですが、ジャギー(ギザギザ)が発生しやすいです。
* cv2.INTER_LINEAR
: 線形補間。デフォルトです。ある程度の滑らかさがあり、縮小に適しています。
* cv2.INTER_CUBIC
: キュービック補間。より滑らかですが、計算コストが高くなります。拡大に適しています。
* cv2.INTER_AREA
: エリア補間。縮小に適しています。画素領域の平均を取るため、エイリアシング(折り返し雑音)を抑える効果があります。
アスペクト比を維持してリサイズしたい場合は、dsize=(0,0)
とし、fx
とfy
に同じ拡大縮小率を指定するのが最も簡単です。
6. 画像の切り抜き
画像の一部を切り抜く(クロップする)操作は、NumPy配列のスライシング機能を使って簡単に行えます。画像はNumPy配列なので、配列のインデクシングやスライシングがそのまま適用できます。
NumPy配列は[行の範囲, 列の範囲]
または[y座標の範囲, x座標の範囲]
でスライスします。
“`python
import cv2
image_path = ‘input_image.jpg’ # 任意の画像を準備してください
img = cv2.imread(image_path)
if img is not None:
# 元画像のサイズを取得
height, width = img.shape[:2]
# 切り抜き範囲を定義 (開始y座標:終了y座標, 開始x座標:終了x座標)
# 例: 上から100ピクセル、左から50ピクセルの位置を左上角とし、
# 高さが200ピクセル、幅が250ピクセルの領域を切り抜く
start_y = 100
end_y = start_y + 200 # 100 + 200 = 300
start_x = 50
end_x = start_x + 250 # 50 + 250 = 300
# 切り抜きを実行 (NumPyスライシング)
# img[yの範囲, xの範囲]
cropped_img = img[start_y:end_y, start_x:end_x]
print(f"元の画像形状: {img.shape}")
print(f"切り抜いた画像形状: {cropped_img.shape}")
cv2.imshow('Original', img)
cv2.imshow('Cropped Image', cropped_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
NumPyのスライシングは非常に効率的で高速です。img[y1:y2, x1:x2]
と指定すると、y座標がy1
からy2-1
まで、x座標がx1
からx2-1
までの領域が切り抜かれます。
7. 画像の回転と平行移動
画像を回転させたり、平行移動させたりといった幾何学的変換も、OpenCVを使って行うことができます。これらの変換はアフィン変換と呼ばれる一般的な変換の一部として扱われます。アフィン変換は、平行線が平行に保たれる変換です。
7.1 平行移動: cv2.warpAffine()
画像をある方向に一定量移動させるのが平行移動です。平行移動を行うには、まず移動量を表す2×3の変換行列を作成し、次にcv2.warpAffine()
関数を使ってその行列を画像に適用します。
変換行列は以下の形式になります。
$$
M = \begin{pmatrix}
1 & 0 & tx \
0 & 1 & ty
\end{pmatrix}
$$
ここで、$tx$はx方向(水平方向)の移動量、$ty$はy方向(垂直方向)の移動量です。移動量はピクセル単位で指定します。正の値はそれぞれ右、下への移動を表します。
“`python
import cv2
import numpy as np
image_path = ‘input_image.jpg’ # 任意の画像を準備してください
img = cv2.imread(image_path)
if img is not None:
height, width = img.shape[:2]
# 平行移動量を定義 (tx, ty)
# 例: 右に50ピクセル、下に30ピクセル移動
tx = 50
ty = 30
# 2x3の変換行列 M を作成
# 行列のデータ型は float32 である必要がある
M_translate = np.float32([[1, 0, tx],
[0, 1, ty]])
# cv2.warpAffine() を使って平行移動を適用
# 第1引数: 入力画像
# 第2引数: 変換行列 (float32型)
# 第3引数: 出力画像のサイズ (幅, 高さ) - 元画像と同じサイズにすることが多い
translated_img = cv2.warpAffine(img, M_translate, (width, height))
print(f"元の画像形状: {img.shape}")
print(f"平行移動後の画像形状: {translated_img.shape}") # サイズは変わらない
cv2.imshow('Original', img)
cv2.imshow('Translated Image', translated_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
cv2.warpAffine(src, M, dsize)
:
* src
: 入力画像
* M
: 2×3の変換行列。データ型はnp.float32
である必要があります。
* dsize
: 出力画像のサイズを(幅, 高さ)
のタプルで指定します。
7.2 回転: cv2.getRotationMatrix2D()
と cv2.warpAffine()
画像を回転させる場合も、アフィン変換として扱われます。回転の中心、回転角度、スケール(拡大/縮小率)を指定して、回転のための2×3変換行列を取得します。これにはcv2.getRotationMatrix2D()
関数を使用します。
“`python
import cv2
import numpy as np
image_path = ‘input_image.jpg’ # 任意の画像を準備してください
img = cv2.imread(image_path)
if img is not None:
height, width = img.shape[:2]
# 回転の中心 (画像の中心を回転の中心とすることが多い)
center = (width // 2, height // 2)
# 回転角度 (時計回りが正の値)
# 例: 反時計回りに90度回転 -> 角度は -90 または 270
angle = 90
# スケール (1.0でそのまま、0.5で半分、2.0で2倍など)
scale = 1.0
# 回転のための2x3変換行列を取得
# 第1引数: 回転の中心 (x, y)
# 第2引数: 回転角度 (度数法)
# 第3引数: スケール
M_rotate = cv2.getRotationMatrix2D(center, angle, scale)
# cv2.warpAffine() を使って回転を適用
# サイズは回転によって画像全体が収まるように調整することも多いですが、
# ここでは簡単のため元画像と同じサイズに出力します。
# その場合、画像の一部が切り取られる可能性があります。
rotated_img = cv2.warpAffine(img, M_rotate, (width, height))
# 回転後の画像全体を収めるためのサイズ計算(応用)
# 参考: https://www.pyimagesearch.com/2017/01/02/rotate-images-correctly-with-opencv-and-python/
# cols = width
# rows = height
# M = cv2.getRotationMatrix2D(center, angle, scale)
# cos = np.abs(M[0, 0])
# sin = np.abs(M[0, 1])
# nW = int((rows * sin) + (cols * cos))
# nH = int((rows * cos) + (cols * sin))
# M[0, 2] += (nW / 2) - center[0]
# M[1, 2] += (nH / 2) - center[1]
# rotated_img_fit = cv2.warpAffine(img, M, (nW, nH))
print(f"元の画像形状: {img.shape}")
print(f"回転後の画像形状: {rotated_img.shape}") # サイズは変わらない (出力サイズ指定による)
cv2.imshow('Original', img)
cv2.imshow(f'Rotated {angle} degrees', rotated_img)
# cv2.imshow('Rotated (Fit)', rotated_img_fit) # 全体を収めた画像
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
cv2.getRotationMatrix2D(center, angle, scale)
:
* center
: 回転の中心となる座標(x, y)
のタプル。通常は画像の中心(width // 2, height // 2)
を指定します。
* angle
: 回転角度(度数法)。正の値は時計回りの回転を表します。
* scale
: スケールファクター。1.0を指定すると元のサイズ、0.5で半分、2.0で2倍になります。
取得した変換行列M
をcv2.warpAffine()
に渡すことで、回転後の画像が得られます。出力サイズdsize
を元画像と同じにすると、回転によって画像の一部がフレームからはみ出して失われる可能性があることに注意してください。画像の回転後に全体を収めたい場合は、上記コードのコメントアウト部分のように、回転後のサイズを計算してdsize
に指定する必要があります。
8. 画像への描画
OpenCVは、画像上に線、円、矩形、多角形、テキストなどの図形を描画するための関数を提供しています。これらの関数を使うと、物体検出の結果を可視化したり、画像にアノテーションを付けたりすることができます。
描画関数は、描画先の画像オブジェクトそのものを変更します。元の画像を保持したい場合は、描画前にimg.copy()
などで画像のコピーを作成してから描画する必要があります。
“`python
import cv2
import numpy as np
描画用のキャンバスとして真っ黒な画像を生成 (高さ400, 幅600, 3チャンネルのカラー画像)
または既存の画像を読み込む
image_path = ‘input_image.jpg’
img = cv2.imread(image_path)
if img is None:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
exit()
canvas = img.copy() # 元画像を残したい場合はコピー
真っ黒なキャンバスを作成する例
canvas = np.zeros((400, 600, 3), dtype=’uint8′) # uint8型は画素値0-255に対応
各色のBGR値を定義
BGRの順序に注意! (青, 緑, 赤)
blue = (255, 0, 0)
green = (0, 255, 0)
red = (0, 0, 255)
yellow = (0, 255, 255)
cyan = (255, 255, 0)
magenta = (255, 0, 255)
white = (255, 255, 255)
black = (0, 0, 0)
線の描画: cv2.line(img, pt1, pt2, color, thickness)
img: 描画先の画像
pt1, pt2: 線分の開始点と終了点の座標 (x, y)
color: 線の色 (BGRタプル)
thickness: 線の太さ (ピクセル)。負の値や cv2.FILLED を指定すると塗りつぶし。
cv2.line(canvas, (50, 50), (550, 50), blue, 5)
cv2.line(canvas, (50, 70), (550, 70), green, 3)
矩形の描画: cv2.rectangle(img, pt1, pt2, color, thickness)
pt1: 矩形の左上角の座標 (x, y)
pt2: 矩形の右下角の座標 (x, y)
cv2.rectangle(canvas, (100, 100), (200, 200), red, 2)
cv2.rectangle(canvas, (220, 100), (320, 200), yellow, -1) # -1またはcv2.FILLEDで塗りつぶし
円の描画: cv2.circle(img, center, radius, color, thickness)
center: 円の中心座標 (x, y)
radius: 円の半径 (ピクセル)
cv2.circle(canvas, (450, 150), 50, cyan, 3)
cv2.circle(canvas, (530, 150), 30, magenta, -1)
多角形の描画: cv2.polylines(img, pts, isClosed, color, thickness)
pts: 頂点座標の配列 (shape: (N, 1, 2))
isClosed: Trueなら閉じた多角形、Falseなら開いた折れ線
cv2.fillPoly(img, pts, color) で塗りつぶした多角形を描画
pts_triangle = np.array([[50, 250], [150, 250], [100, 350]], np.int32) # 頂点座標をNumPy配列で定義
polylinesは頂点配列のリストを受け付ける
pts_triangle = pts_triangle.reshape((-1, 1, 2)) # 形状を (N, 1, 2) に変更する必要がある
cv2.polylines(canvas, [pts_triangle], True, white, 2)
pts_pentagon = np.array([[200, 250], [250, 250], [280, 300], [250, 350], [200, 350]], np.int32)
pts_pentagon = pts_pentagon.reshape((-1, 1, 2))
cv2.fillPoly(canvas, [pts_pentagon], (128, 128, 128)) # 灰色で塗りつぶし
テキストの描画: cv2.putText(img, text, org, fontFace, fontScale, color, thickness, lineType)
text: 描画する文字列
org: テキストの左下角の座標 (x, y)
fontFace: フォントの種類 (cv2.FONT_HERSHEY_…)
fontScale: フォントサイズに対するスケールファクター
thickness: 文字の太さ
lineType: ラインの種類 (cv2.LINE_AA が滑らか)
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(canvas, ‘OpenCV Drawing’, (50, 390), font, 1, (255, 255, 255), 2, cv2.LINE_AA)
cv2.imshow(‘Drawing on Canvas’, canvas)
print(“画像に描画しました。”)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
各描画関数の引数について補足です。
* color
: 色はBGRのタプルで指定します。例えば青は(255, 0, 0)
、赤は(0, 0, 255)
、白は(255, 255, 255)
です。
* thickness
: 線の太さをピクセル単位で指定します。-1
やcv2.FILLED
を指定すると、図形が塗りつぶされます。
* 座標: 座標は(x, y)
のタプルで指定します。画像の左上角が原点(0, 0)
で、x座標は右方向、y座標は下方向に増加します。
* 多角形の頂点: cv2.polylines
やcv2.fillPoly
に渡す頂点配列は、NumPy配列で、形状が(N, 1, 2)
である必要があります。N
は頂点の数です。
これらの描画関数を組み合わせることで、検出された物体に矩形を描いたり、特徴点に円を描いたり、結果にラベルを付けたりといった、様々な可視化が可能になります。
9. 画像処理の基本演算
OpenCVとNumPyを使うと、画素レベルでの様々な演算や、画像間の算術演算を行うことができます。
9.1 画素値へのアクセスと編集
画像はNumPy配列なので、配列のインデクシングを使って個々の画素や画素ブロックの値にアクセスしたり、値を変更したりできます。
“`python
import cv2
import numpy as np
image_path = ‘input_image.jpg’ # 任意の画像を準備してください
img = cv2.imread(image_path)
if img is not None:
# 画素値へのアクセス (y座標, x座標)
# カラー画像の場合: (y, x, チャンネル)
# グレースケール画像の場合: (y, x)
# 例: 座標(50, 100)の画素の色を取得 (カラー画像の場合)
# OpenCVはBGR順なので、青, 緑, 赤 の値が得られる
pixel_color = img[50, 100]
print(f"座標 (100, 50) の画素値 (BGR): {pixel_color}") # NumPy配列 [B, G, R]
# 例: 座標(50, 100)の青チャンネルの値を取得
blue_value = img[50, 100, 0]
print(f"座標 (100, 50) の青チャンネル値: {blue_value}")
# グレースケール画像の場合
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
pixel_gray = img_gray[50, 100]
print(f"座標 (100, 50) の画素値 (グレースケール): {pixel_gray}")
# 画素値の編集 (特定の色に変えるなど)
# 例: 座標(50, 100)の画素を明るい緑色に変える
img[50, 100] = [0, 255, 0] # [B, G, R] = [0, 255, 0]
# 特定の領域を編集
# 例: 左上 (0, 0) から (100, 100) の領域を赤色にする
img[0:101, 0:101] = [0, 0, 255] # [B, G, R] = [0, 0, 255]
cv2.imshow('Original', cv2.imread(image_path)) # 変更前の元画像を表示(比較用)
cv2.imshow('Modified Image', img)
print("画素値を編集しました。")
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
画素値へのアクセスは非常に高速ですが、ループを使って1画素ずつ処理するのは非効率です。画像全体や大きな領域に対して一括で処理を行う場合は、NumPyのベクトル演算やOpenCVの関数を利用するのが推奨されます。
9.2 画像の算術演算
2枚の画像の間で加算、減算、乗算などの算術演算を行うことができます。これは、画像のブレンドや差分検出などに利用されます。
OpenCVで画像の加算を行う関数としてcv2.add()
があります。NumPyでも+
演算子で加算できますが、挙動が異なります。
- NumPy加算 (
+
): 画素値が255を超えると、値を255で割った余り(モジュロ演算)になります。例えば、200 + 100 = 300 ですが、NumPyでは300 % 256 = 44
となります。 - OpenCV加算 (
cv2.add
): 画素値が255を超えると、値を255にクリップ(飽和演算)します。例えば、200 + 100 = 300 ですが、OpenCVでは255となります。
画像処理では、飽和演算の方が自然な結果になることが多いため、cv2.add()
を使うのが一般的です。
“`python
import cv2
import numpy as np
例として、明るい画像と暗い画像を準備する
または、2枚の異なる画像を読み込む
image1_path = ‘input_image1.jpg’ # 1枚目の画像を準備してください
image2_path = ‘input_image2.jpg’ # 2枚目の画像を準備してください
img1 = cv2.imread(image1_path)
img2 = cv2.imread(image2_path)
if img1 is not None and img2 is not None:
# 2枚の画像のサイズとチャンネル数が同じである必要がある
# 必要に応じてリサイズやグレースケール変換を行う
# ここでは同じサイズのカラー画像を前提とする
height, width = img1.shape[:2]
img2 = cv2.resize(img2, (width, height)) # サイズを合わせる
# OpenCVでの加算 (飽和演算)
added_cv = cv2.add(img1, img2)
# NumPyでの加算 (モジュロ演算)
added_np = img1 + img2
# 画像のブレンド (重み付き加算)
# cv2.addWeighted(src1, alpha, src2, beta, gamma) = src1 * alpha + src2 * beta + gamma
# 0 <= alpha <= 1, 0 <= beta <= 1 が一般的。alpha + beta = 1 にすると、両方の画像を重ね合わせるような効果
# gamma は輝度調整のための値
alpha = 0.7 # img1 の重み
beta = 0.3 # img2 の重み
gamma = 0 # 輝度調整なし
blended_img = cv2.addWeighted(img1, alpha, img2, beta, gamma)
cv2.imshow('Image 1', img1)
cv2.imshow('Image 2', img2)
cv2.imshow('Added (OpenCV)', added_cv)
cv2.imshow('Added (NumPy)', added_np)
cv2.imshow('Blended Image', blended_img)
print("画像間の算術演算を行いました。")
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(“エラー: 画像ファイルを読み込めませんでした。”)
“`
cv2.addWeighted(src1, alpha, src2, beta, gamma)
関数は、2つの画像を特定の割合でブレンドするのに便利です。alpha
とbeta
はそれぞれの画像の重みを指定します。gamma
は結果に加算されるバイアス値で、画像の明るさを調整できます。
画像の減算は、背景差分による物体検出などに使われます。NumPyの-
演算子やcv2.subtract()
関数を使用できます。乗算や除算も同様にNumPy演算子または対応するOpenCV関数(例えばcv2.multiply()
)を使えます。
9.3 しきい値処理 (Thresholding)
しきい値処理は、画像を2値画像(白と黒の2色のみで構成される画像)に変換する基本的な技術です。画像の各画素に対して、あらかじめ定めたしきい値と比較し、その値より大きいか小さいかに応じて画素値を0(黒)または255(白)に設定します。
しきい値処理は、特定の対象領域を背景から分離したい場合や、輪郭検出やモルフォロジー変換の前処理としてよく行われます。通常、グレースケール画像に対して適用します。
“`python
import cv2
import numpy as np
image_path = ‘input_grayscale_image.jpg’ # グレースケール画像を準備するか、カラー画像を読み込んで変換する
img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) # グレースケールとして読み込み
if img is not None:
# 手動でしきい値を設定 (例: 127)
thresh_value = 127
# しきい値処理を行う関数: cv2.threshold()
# 第1引数: 入力画像 (通常はグレースケール)
# 第2引数: しきい値
# 第3引数: 画素値がしきい値を超えた場合に設定される最大値 (通常は255)
# 第4引数: しきい値処理の種類
# 戻り値: (しきい値, 結果の2値画像)
# THRESH_BINARY: 画素値 > thresh なら maxval (255), そうでなければ 0
ret, thresh_binary = cv2.threshold(img, thresh_value, 255, cv2.THRESH_BINARY)
# THRESH_BINARY_INV: 画素値 > thresh なら 0, そうでなければ maxval (255) (白黒反転)
ret, thresh_binary_inv = cv2.threshold(img, thresh_value, 255, cv2.THRESH_BINARY_INV)
# THRESH_TRUNC: 画素値 > thresh なら thresh, そうでなければそのまま
ret, thresh_trunc = cv2.threshold(img, thresh_value, 255, cv2.THRESH_TRUNC)
# THRESH_TOZERO: 画素値 > thresh ならそのまま, そうでなければ 0
ret, thresh_tozero = cv2.threshold(img, thresh_value, 255, cv2.THRESH_TOZERO)
# THRESH_TOZERO_INV: 画素値 > thresh なら 0, そうでなければそのまま
ret, thresh_tozero_inv = cv2.threshold(img, thresh_value, 255, cv2.THRESH_TOZERO_INV)
# 大津の方法 (Otsu's Binarization): 最適なしきい値を自動計算
# cv2.threshold()の第4引数に cv2.THRESH_OTSU を追加する。
# この場合、第2引数のしきい値は無視されるが、指定する必要がある(通常は0)。
ret_otsu, thresh_otsu = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f"Otsuの方法で計算されたしきい値: {ret_otsu}")
cv2.imshow('Original Grayscale', img)
cv2.imshow('THRESH_BINARY', thresh_binary)
cv2.imshow('THRESH_BINARY_INV', thresh_binary_inv)
cv2.imshow('THRESH_TRUNC', thresh_trunc)
cv2.imshow('THRESH_TOZERO', thresh_tozero)
cv2.imshow('THRESH_TOZERO_INV', thresh_tozero_inv)
cv2.imshow('THRESH_OTSU', thresh_otsu)
print("しきい値処理を行いました。")
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
cv2.threshold()
関数は、結果の画像だけでなく、使用されたしきい値も返します。手動で指定した場合も、大津の方法で自動計算された場合も、第1返り値としてしきい値が返ってきます。
様々なthreshold_type
フラグとその効果を理解することは重要です。
* cv2.THRESH_BINARY
: 最も基本的な2値化です。しきい値より明るい画素は白(maxval)、暗い画素は黒(0)になります。
* cv2.THRESH_BINARY_INV
: THRESH_BINARY
の反転です。しきい値より明るい画素は黒(0)、暗い画素は白(maxval)になります。
* cv2.THRESH_TRUNC
: しきい値より明るい画素はしきい値と同じ値になり、暗い画素はそのままです。画像を明るい側でクリッピングするような効果があります。
* cv2.THRESH_TOZERO
: しきい値より明るい画素はそのまま、暗い画素は0になります。
* cv2.THRESH_TOZERO_INV
: しきい値より明るい画素は0、暗い画素はそのままです。
大津の方法 (Otsu’s Binarization) は、画像ヒストグラムを分析して、最適なグローバルしきい値を自動的に見つけるアルゴリズムです。画像が明確な前景と背景の2つのピークを持つヒストグラムを持つ場合に特に有効です。cv2.threshold()
でcv2.THRESH_OTSU
フラグを指定すると、自動的にしきい値が計算されます。この場合、手動で指定したしきい値は使用されません(ただし引数として渡す必要があります)。
10. 平滑化 (Smoothing/Blurring)
平滑化、またはブラーリングは、画像のノイズを除去したり、細かいディテールをぼかしたりするために行われます。これは、後の処理(例えばエッジ検出や特徴抽出)の精度を向上させるための前処理としてよく使われます。
平滑化は、画像の各画素に対して、その周囲の画素の値を考慮して新しい画素値を計算することで行われます。これはフィルタリングや畳み込みと呼ばれる操作によって実現されます。画像に適用される周囲の画素の値を計算するための重み付けを定義する小さな行列をカーネルまたはフィルタマスクと呼びます。
10.1 様々な平滑化フィルタ
OpenCVにはいくつかの種類の平滑化フィルタが用意されています。
- 平均化フィルタ (Averaging Filter): カーネル内の全ての画素の平均を計算して新しい画素値とします。
- ガウシアンフィルタ (Gaussian Filter): カーネル内の画素に対して、中心からの距離に応じたガウス分布の重みを与えて平均を計算します。平均化フィルタより自然なぼかしが得られます。
- メディアンフィルタ (Median Filter): カーネル内の画素値の中央値を新しい画素値とします。外れ値(ノイズ)の影響を受けにくいため、ソルト&ペッパーノイズ(白い点や黒い点として現れるノイズ)の除去に非常に効果的です。
- バイラテラルフィルタ (Bilateral Filter): 画素間の空間的な距離に加えて、画素値の差も考慮して重み付けを行います。これにより、エッジを保持したまま平滑化を行うことができます。計算コストは高くなります。
“`python
import cv2
import numpy as np
image_path = ‘input_noise_image.jpg’ # ノイズが含まれている画像を用意するか、既存の画像にノイズを加える
img = cv2.imread(image_path)
if img is not None:
# ノイズを追加する例 (ソルト&ペッパーノイズ) – テスト用
# rows, cols, _ = img.shape
# noise_ratio = 0.05 # ノイズの割合
# num_pixels = int(rows * cols * noise_ratio)
#
# for i in range(num_pixels):
# y = np.random.randint(0, rows)
# x = np.random.randint(0, cols)
# color = np.random.choice([0, 255])
# if img.ndim == 3:
# img[y, x] = [color, color, color] # カラー画像なら3チャンネル全てに同じ値を設定
# else:
# img[y, x] = color # グレースケール画像なら単一チャンネル
# 平均化フィルタ: cv2.blur(src, ksize)
# ksize: カーネルのサイズ (幅, 高さ)
blur_avg = cv2.blur(img, (5, 5)) # 5x5のカーネルを使用
# ガウシアンフィルタ: cv2.GaussianBlur(src, ksize, sigmaX)
# ksize: カーネルのサイズ (幅, 高さ)。奇数である必要がある。Noneを指定するとsigmaXから自動計算。
# sigmaX: X方向のガウス分布の標準偏差。これを指定するとksizeは自動計算されることが多い。0を指定するとsigmaXはksizeから計算される。
# sigmaY: Y方向の標準偏差。0を指定するとsigmaXと同じになる。
blur_gaussian = cv2.GaussianBlur(img, (5, 5), 0)
# メディアンフィルタ: cv2.medianBlur(src, ksize)
# ksize: カーネルのサイズ (奇数)。画像がカラーでも各チャンネルに個別に適用される。
blur_median = cv2.medianBlur(img, 5) # 5x5のカーネル (サイズ指定は奇数のみ)
# バイラテラルフィルタ: cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace)
# d: フィルタリングに使用する近傍画素の直径。負の値の場合はsigmaSpaceから決定。
# sigmaColor: 色の標準偏差。この値が大きいほど、遠い色(画素値の差が大きい)も考慮して平滑化。
# sigmaSpace: 空間の標準偏差。この値が大きいほど、遠い画素(距離が離れている)も考慮して平滑化。
blur_bilateral = cv2.bilateralFilter(img, 9, 75, 75)
cv2.imshow('Original (possibly with noise)', img)
cv2.imshow('Averaging Blur (5x5)', blur_avg)
cv2.imshow('Gaussian Blur (5x5)', blur_gaussian)
cv2.imshow('Median Blur (5x5)', blur_median)
cv2.imshow('Bilateral Filter', blur_bilateral)
print("画像を平滑化しました。")
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
各フィルタの引数や特性を理解し、画像の種類や除去したいノイズの種類、目的に応じて適切なフィルタを選択することが重要です。例えば、JPEGノイズのような一般的なランダムノイズにはガウシアンフィルタ、ソルト&ペッパーノイズにはメディアンフィルタが効果的です。エッジをぼかしたくない場合はバイラテラルフィルタを検討します。
11. モルフォロジー変換 (Morphological Transformations)
モルフォロジー変換は、画像の形状に基づいた操作を行うための技術です。主に2値画像またはグレースケール画像に適用され、ノイズ除去、オブジェクトの分離や結合、輪郭検出の前処理などに利用されます。
モルフォロジー変換は、カーネル(構造要素) と呼ばれる小さな形状(矩形、円形、十字形など)を使用して行われます。カーネルは画像上をスライドし、カーネルの形状と中心画素の下にある画素の配置に基づいて、中心画素の新しい値を決定します。
基本的なモルフォロジー変換には、膨張 (Dilation) と 収縮 (Erosion) があります。
11.1 カーネルの作成: cv2.getStructuringElement()
モルフォロジー変換を行う前に、使用するカーネル(構造要素)を作成します。
“`python
import cv2
import numpy as np
カーネルの形状とサイズを指定
kernel_size = (5, 5)
矩形カーネル
kernel_rect = cv2.getStructuringElement(cv2.MORPH_RECT, kernel_size)
print(“矩形カーネル:\n”, kernel_rect)
楕円形カーネル
kernel_ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, kernel_size)
print(“楕円形カーネル:\n”, kernel_ellipse)
十字形カーネル
kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, kernel_size)
print(“十字形カーネル:\n”, kernel_cross)
“`
cv2.getStructuringElement(shape, ksize)
:
* shape
: カーネルの形状 (cv2.MORPH_RECT
, cv2.MORPH_ELLIPSE
, cv2.MORPH_CROSS
)
* ksize
: カーネルのサイズを(幅, 高さ)
で指定します。
11.2 膨張 (Dilation)
膨張処理は、画像の前景オブジェクト(通常は白画素)の領域を「拡大」させます。具体的には、カーネル内の画素の少なくとも1つが前景(白)であれば、中心画素も前景(白)になります。
効果:
* 前景オブジェクトを太くする。
* オブジェクト内の小さな穴や隙間を埋める。
* オブジェクト同士を結合させる可能性がある。
* ノイズ(黒い点)を除去する効果はないが、白いノイズは拡大する。
“`python
import cv2
import numpy as np
2値画像を読み込み (または作成)
例: テキストや線画など、白黒がはっきり分かれている画像が適しています
image_path = ‘input_binary_image.png’ # 白い文字や線がある画像を準備してください
img_binary = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) # グレースケールとして読み込み
2値化されていない場合はここで2値化する
if img_binary is not None:
# THRESH_BINARY + THRESH_OTSU で自動2値化する例
ret, img_binary = cv2.threshold(img_binary, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 膨張のためのカーネルを作成
kernel = np.ones((5, 5), np.uint8) # 5x5の矩形カーネル (全ての要素が1)
# 膨張を適用: cv2.dilate(src, kernel, iterations)
# src: 入力画像 (通常は2値画像またはグレースケール)
# kernel: 膨張に使用するカーネル
# iterations: 膨張処理を繰り返す回数。大きいほど効果が強くなる。
dilated_img = cv2.dilate(img_binary, kernel, iterations=1)
dilated_img_iter2 = cv2.dilate(img_binary, kernel, iterations=2)
cv2.imshow('Original Binary', img_binary)
cv2.imshow('Dilated (1 iteration)', dilated_img)
cv2.imshow('Dilated (2 iterations)', dilated_img_iter2)
print("膨張処理を行いました。")
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
11.3 収縮 (Erosion)
収縮処理は、画像の前景オブジェクトの領域を「縮小」させます。具体的には、カーネル内の全ての画素が前景(白)である場合にのみ、中心画素は前景(白)になります。カーネル内に1つでも背景(黒)画素があれば、中心画素は背景(黒)になります。
効果:
* 前景オブジェクトを細くする。
* オブジェクト内の小さな穴や隙間を広げる。
* オブジェクト同士の境界線を分離する。
* ノイズ(白い点)を除去するのに効果的。
“`python
import cv2
import numpy as np
image_path = ‘input_binary_image.png’ # 白い文字や線がある画像を準備してください
img_binary = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) # グレースケールとして読み込み
2値化されていない場合はここで2値化する
if img_binary is not None:
# THRESH_BINARY + THRESH_OTSU で自動2値化する例
ret, img_binary = cv2.threshold(img_binary, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 収縮のためのカーネルを作成
kernel = np.ones((5, 5), np.uint8) # 5x5の矩形カーネル
# 収縮を適用: cv2.erode(src, kernel, iterations)
# src: 入力画像
# kernel: 収縮に使用するカーネル
# iterations: 収縮処理を繰り返す回数
eroded_img = cv2.erode(img_binary, kernel, iterations=1)
eroded_img_iter2 = cv2.erode(img_binary, kernel, iterations=2)
cv2.imshow('Original Binary', img_binary)
cv2.imshow('Eroded (1 iteration)', eroded_img)
cv2.imshow('Eroded (2 iterations)', eroded_img_iter2)
print("収縮処理を行いました。")
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
11.4 複合的なモルフォロジー変換
膨張と収縮を組み合わせることで、さらに複雑な処理を行うことができます。
- オープニング (Opening): 収縮 -> 膨張 の順序で行います。小さな白いノイズ(前景の点)を除去し、オブジェクトの輪郭を滑らかにする効果があります。
cv2.morphologyEx(src, cv2.MORPH_OPEN, kernel)
で実行できます。 - クロージング (Closing): 膨張 -> 収縮 の順序で行います。オブジェクト内の小さな穴や隙間を埋め、オブジェクト間の小さな切れ目を結合する効果があります。
cv2.morphologyEx(src, cv2.MORPH_CLOSE, kernel)
で実行できます。 - モルフォロジカルグラディエント (Morphological Gradient): 膨張 – 収縮 の結果の差をとります。オブジェクトの外形線(輪郭)を抽出する効果があります。
cv2.morphologyEx(src, cv2.MORPH_GRADIENT, kernel)
で実行できます。 - トップハット (Top Hat): 元画像 – オープニング の結果の差をとります。元画像よりも明るい、小さな要素(白い点状のノイズや細い白い線など)を抽出する効果があります。
cv2.morphologyEx(src, cv2.MORPH_TOPHAT, kernel)
で実行できます。 - ブラックハット (Black Hat): クロージング – 元画像 の結果の差をとります。元画像よりも暗い、小さな要素(黒い点状のノイズや細い黒い線など)を抽出する効果があります。
cv2.morphologyEx(src, cv2.MORPH_BLACKHAT, kernel)
で実行できます。
これらの複合的な変換は、cv2.morphologyEx()
関数を使って一度に実行できます。
“`python
import cv2
import numpy as np
image_path = ‘input_binary_image.png’ # 2値化に適した画像を準備
img_binary = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if img_binary is not None:
ret, img_binary = cv2.threshold(img_binary, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 複合モルフォロジー変換のためのカーネルを作成
kernel = np.ones((5, 5), np.uint8)
# オープニング (ノイズ除去)
opening = cv2.morphologyEx(img_binary, cv2.MORPH_OPEN, kernel)
# クロージング (穴埋め、結合)
closing = cv2.morphologyEx(img_binary, cv2.MORPH_CLOSE, kernel)
# グラディエント (輪郭検出)
gradient = cv2.morphologyEx(img_binary, cv2.MORPH_GRADIENT, kernel)
# トップハット (明るい要素抽出)
# 注: トップハットとブラックハットはグレースケール画像に適用するのが一般的ですが、
# ここでは2値画像に適用しています。効果は限定的になる場合があります。
tophat = cv2.morphologyEx(img_binary, cv2.MORPH_TOPHAT, kernel)
# ブラックハット (暗い要素抽出)
blackhat = cv2.morphologyEx(img_binary, cv2.MORPH_BLACKHAT, kernel)
cv2.imshow('Original Binary', img_binary)
cv2.imshow('Opening', opening)
cv2.imshow('Closing', closing)
cv2.imshow('Gradient', gradient)
cv2.imshow('Top Hat', tophat)
cv2.imshow('Black Hat', blackhat)
print("複合モルフォロジー変換を行いました。")
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
モルフォロジー変換は、カーネルのサイズや形状、繰り返し回数によって結果が大きく変わります。目的に応じてパラメータを調整することが重要です。
12. 輪郭検出 (Contour Detection)
輪郭とは、画像内の同じ色や明るさを持つ連続した点の曲線、すなわちオブジェクトの境界線です。輪郭検出は、画像内のオブジェクトを特定したり、その形状を分析したりするために用いられる重要な技術です。
通常、輪郭検出は2値画像(またはグレースケール画像に適用後に2値化)に対して行われます。オブジェクトは白(前景)、背景は黒として表現されている画像が適しています。
12.1 輪郭検出: cv2.findContours()
cv2.findContours()
関数は、画像から輪郭を検出します。
“`python
import cv2
import numpy as np
輪郭検出に適した画像を準備 (白いオブジェクト、黒い背景など)
例: 図形が描かれた2値画像
image_path = ‘input_shapes_binary.png’
img_binary = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if img_binary is not None:
# 注: cv2.findContours() は入力画像を破壊する可能性があります。
# 元の画像を保持したい場合はコピーを渡すことを強く推奨します。
img_binary_copy = img_binary.copy()
# 輪郭検出
# 第1引数: 入力画像 (8ビットのグレースケールまたはカラー画像)
# 第2引数: 輪郭の取得モード
# 第3引数: 輪郭の近似手法
# 戻り値: (輪郭のリスト, 階層構造の情報)
# OpenCVのバージョンによって戻り値の形式が異なる場合があります (リストの先頭に元の画像があるかどうかなど)。
# 一般的なバージョンでは (contours, hierarchy) が返されます。
contours, hierarchy = cv2.findContours(img_binary_copy, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
print(f"検出された輪郭の数: {len(contours)}")
# 各輪郭は点のNumPy配列です。
# 輪郭を描画するキャンバスとして、元のカラー画像を読み込むか、背景画像を作成
# 元の2値画像と同じサイズで3チャンネルの真っ黒な画像を作成
img_display = np.zeros((img_binary.shape[0], img_binary.shape[1], 3), dtype='uint8')
# または元のカラー画像を読み込む
# img_display = cv2.imread('input_shapes_color.png') # 対応するカラー画像があれば
# 検出された輪郭を描画: cv2.drawContours(image, contours, contourIdx, color, thickness)
# image: 輪郭を描画する画像
# contours: findContours() で検出された輪郭のリスト
# contourIdx: 描画する輪郭のインデックス。-1を指定すると全ての輪郭を描画。
# color: 輪郭の色 (BGRタプル)
# thickness: 輪郭線の太さ。負の値や cv2.FILLED は内部を塗りつぶす。
# 全ての輪郭を緑色で太さ2ピクセルで描画
cv2.drawContours(img_display, contours, -1, (0, 255, 0), 2)
# 例えば、最初の輪郭だけを赤色で塗りつぶして描画
# if len(contours) > 0:
# cv2.drawContours(img_display, contours, 0, (0, 0, 255), -1)
cv2.imshow('Original Binary', img_binary)
cv2.imshow('Detected Contours', img_display)
print("輪郭検出を行いました。")
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
cv2.findContours()
の重要な引数と戻り値について補足です。
* image
: 入力画像。8ビットのシングルチャンネル画像(グレースケールまたは2値画像)が必要です。この関数は入力画像を内部で変更(破壊)する可能性があるため、コピーを渡すのが安全です。
* mode
: 輪郭の取得モードを指定します。
* cv2.RETR_EXTERNAL
: 最も外側の輪郭のみを取得します。
* cv2.RETR_LIST
: 全ての輪郭を取得し、階層構造は考慮しません。
* cv2.RETR_CCOMP
: 全ての輪郭を取得し、2レベルの階層構造(コンポーネントと穴)を構成します。
* cv2.RETR_TREE
: 全ての輪郭を取得し、完全な階層ツリーを構成します。
* method
: 輪郭の近似手法を指定します。検出された輪郭を構成する点をどのように表現するかを決定します。
* cv2.CHAIN_APPROX_NONE
: 輪郭を構成する全ての点を格納します。最も詳細ですが、メモリを多く消費します。
* cv2.CHAIN_APPROX_SIMPLE
: 水平、垂直、斜め方向の線分を圧縮し、終点のみを格納します。例えば、矩形の輪郭は4点で表現されます。メモリを節約できます。ほとんどの場合、この手法で十分です。
* contours
: 検出された輪郭のリストです。各輪郭は、それを構成する点の座標を格納したNumPy配列です。
* hierarchy
: 各輪郭の階層情報を含むNumPy配列です。mode
にRETR_LIST
以外を指定した場合に有効な情報が格納されます。(次の輪郭のインデックス, 前の輪郭のインデックス, 子の輪郭のインデックス, 親の輪郭のインデックス)
という形式で、それぞれ対応する輪郭がない場合は-1が格納されます。この情報を使うことで、オブジェクトの穴や入れ子構造などを解析できます。
12.2 輪郭の特徴量
検出された輪郭に対して、面積、周囲長、外接矩形などの様々な特徴量を計算することができます。これらはオブジェクトのサイズや形状を分析するのに役立ちます。
“`python
import cv2
import numpy as np
image_path = ‘input_shapes_binary.png’
img_binary = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if img_binary is not None:
img_binary_copy = img_binary.copy()
contours, hierarchy = cv2.findContours(img_binary_copy, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 外側の輪郭のみ取得
img_display = np.zeros((img_binary.shape[0], img_binary.shape[1], 3), dtype='uint8')
# 輪郭を順番に処理
for i, contour in enumerate(contours):
# 輪郭の面積を計算
area = cv2.contourArea(contour)
# 輪郭の周囲長(弧長)を計算 (閉じた曲線ならTrue)
perimeter = cv2.arcLength(contour, True)
# 輪郭の外接矩形 (bounding rectangle) を取得
x, y, w, h = cv2.boundingRect(contour) # 左上点の座標(x, y), 幅(w), 高さ(h)
# 外接矩形を描画
cv2.rectangle(img_display, (x, y), (x+w, y+h), (0, 255, 0), 2)
# 輪郭の中心座標 (モーメントから計算)
M = cv2.moments(contour) # モーメントを計算
if M['m00'] != 0:
cx = int(M['m10'] / M['m00']) # 中心x座標
cy = int(M['m01'] / M['m00']) # 中心y座標
cv2.circle(img_display, (cx, cy), 5, (0, 0, 255), -1) # 中心に赤い円を描画
# 輪郭のインデックスと特徴量を画像に表示
label = f'ID:{i} Area:{int(area)} Perim:{int(perimeter)}'
cv2.putText(img_display, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
print(f"輪郭 {i}: 面積={area}, 周囲長={perimeter}, 外接矩形=({x},{y},{w},{h}), 中心=({cx},{cy})")
cv2.imshow('Original Binary', img_binary)
cv2.imshow('Contours with Features', img_display)
print("輪郭の特徴量を計算し、表示しました。")
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
輪郭から得られる特徴量は他にもたくさんあります。例えば、最小外接円、楕円フィッティング、ポリゴン近似、凸包などが利用可能です。これらを組み合わせることで、画像内の様々なオブジェクトを識別したり、分類したりする基礎とすることができます。
13. エッジ検出 (Edge Detection)
エッジとは、画像内の明るさや色の変化が急峻な箇所、つまりオブジェクトの境界線やテクスチャの変化がある部分です。エッジ検出は、画像内の重要な構造情報を抽出するために行われ、物体認識、画像セグメンテーション、特徴抽出などの前処理として非常に重要です。
13.1 様々なエッジ検出器
OpenCVにはいくつかのエッジ検出アルゴリズムが実装されています。
- Sobel / Scharr フィルタ: 画像のX方向およびY方向の1階微分を計算し、画素値の変化率(勾配の大きさ)を求めます。これにより、エッジの強さや方向を知ることができます。
- Laplacian フィルタ: 画像の2階微分を計算します。明るさの変化が急峻な場所(エッジ)で値が大きくなります。ノイズに敏感です。
- Canny エッジ検出器: エッジ検出において最も広く使われているアルゴリズムの一つです。以下のステップでエッジを検出します。
- ノイズ除去: ガウシアンフィルタで画像を平滑化し、ノイズの影響を軽減します。
- 勾配計算: Sobelフィルタなどを使って、明るさの勾配の大きさ(
magnitude
)と方向(direction
)を計算します。 - 非最大値抑制 (Non-maximum Suppression): 勾配の方向に沿って、その画素が局所的な最大値であるかを確認し、そうでない画素はエッジではないと判断して除外します。これにより、エッジを細くします。
- ヒステリシスしきい値処理 (Hysteresis Thresholding): 2つのしきい値(
minVal
とmaxVal
)を使ってエッジを分類します。maxVal
より大きい勾配を持つ画素は、「強いエッジ」として確実にエッジと判断されます。minVal
より小さい勾配を持つ画素は、確実にエッジではないと判断されます。minVal
とmaxVal
の間の勾配を持つ画素は、「弱いエッジ」と見なされ、強いエッジに連結していればエッジと判断されます。これにより、ノイズによる偽のエッジを抑制しつつ、本物のエッジを途切れさせないようにします。
“`python
import cv2
import numpy as np
image_path = ‘input_image.jpg’ # 任意の画像を準備してください
img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) # エッジ検出は通常グレースケール画像に対して行う
if img is not None:
# Sobelフィルタ
# cv2.Sobel(src, ddepth, dx, dy, ksize)
# src: 入力画像 (グレースケール推奨)
# ddepth: 出力画像の深度。画素値の変化が負になる場合があるため、cv2.CV_64F (float64) などを指定することが多い。
# dx, dy: x方向またはy方向の微分次数 (0または1)。dx=1, dy=0ならx方向、dx=0, dy=1ならy方向の勾配。
# ksize: Sobelカーネルのサイズ (奇数)。-1を指定するとScharrフィルタが使用される。
sobelx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5) # X方向の勾配
sobely = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=5) # Y方向の勾配
# Sobelの結果はfloat64型なので、表示するためにuint8型に変換する必要がある。
# 絶対値を取ってからuint8に変換する。
sobelx_abs = np.uint8(np.absolute(sobelx))
sobely_abs = np.uint8(np.absolute(sobely))
# X方向とY方向の勾配を合成して、エッジの大きさを計算
sobel_combined = cv2.addWeighted(sobelx_abs, 0.5, sobely_abs, 0.5, 0) # 簡単な合成方法
# Laplacianフィルタ
# cv2.Laplacian(src, ddepth, ksize)
laplacian = cv2.Laplacian(img, cv2.CV_64F)
laplacian_abs = np.uint8(np.absolute(laplacian))
# Cannyエッジ検出器
# cv2.Canny(image, threshold1, threshold2)
# threshold1, threshold2: ヒステリシスしきい値処理のためのしきい値。
# 一般的に、大きい方が maxVal、小さい方が minVal として機能する。
# minVal <= edge_pixel_gradient <= maxVal の範囲にあるエッジは、
# maxVal より大きいエッジに連結している場合のみエッジと判断される。
canny_edges = cv2.Canny(img, 100, 200) # 例: minVal=100, maxVal=200
cv2.imshow('Original Grayscale', img)
cv2.imshow('Sobel X', sobelx_abs)
cv2.imshow('Sobel Y', sobely_abs)
cv2.imshow('Sobel Combined', sobel_combined)
cv2.imshow('Laplacian', laplacian_abs)
cv2.imshow('Canny Edges', canny_edges)
print("エッジ検出を行いました。")
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
SobelやLaplacianの結果は画素値が負になる可能性があるため、cv2.CV_64F
などの浮動小数点型で計算し、絶対値を取ってからuint8
型に変換して表示する必要があります。
Cannyエッジ検出は、minVal
とmaxVal
のパラメータ調整が重要です。これらの値を小さくするとより多くのエッジが検出されますが、ノイズによるエッジも増える可能性があります。大きくすると検出されるエッジは減りますが、より確実なエッジのみが検出されます。
14. ヒストグラム (Histograms)
画像のヒストグラムは、画像中に含まれる各画素値(輝度値や色値)の出現頻度を示すグラフです。ヒストグラムを見ることで、画像の輝度分布やコントラストの状態などを把握することができます。
14.1 ヒストグラムの計算: cv2.calcHist()
cv2.calcHist()
関数を使って、画像のヒストグラムを計算できます。通常、グレースケール画像の場合は輝度値(0-255)のヒストグラム、カラー画像の場合は各チャンネル(B, G, R)ごとのヒストグラムを計算します。
“`python
import cv2
import numpy as np
import matplotlib.pyplot as plt # ヒストグラム描画のためにmatplotlibを使用
image_path = ‘input_image.jpg’ # 任意の画像を準備してください
img = cv2.imread(image_path)
if img is not None:
# グレースケール画像として読み込み
img_gray = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
# グレースケール画像のヒストグラム計算
# cv2.calcHist(images, channels, mask, histSize, ranges)
# images: 入力画像リスト (画像はNumPy配列) - リストにする必要がある [img_gray]
# channels: ヒストグラムを計算するチャンネルのインデックスリスト [0] (グレースケールは0番目のチャンネルのみ)
# mask: マスク画像 (ヒストグラムを計算する領域を指定)。Noneの場合は画像全体。
# histSize: ヒストグラムのbin(ビンの数)のリスト [256] (0-255までの256階調を個別にカウントする場合)
# ranges: 画素値の範囲 [0, 256] (0から255までの値。256は含まない)
hist_gray = cv2.calcHist([img_gray], [0], None, [256], [0, 256])
# カラー画像の各チャンネルごとのヒストグラム計算
# チャンネルごとに計算し、リストに格納することが多い
color = ('b', 'g', 'r') # BGRチャンネルに対応する色をmatplotlib用に定義
hist_color = []
for i, col in enumerate(color):
hist = cv2.calcHist([img], [i], None, [256], [0, 256])
hist_color.append((hist, col)) # ヒストグラムデータと色をタプルで保存
# ヒストグラムの描画 (matplotlibを使用)
plt.figure(figsize=(10, 6))
# グレースケールヒストグラムの描画
plt.subplot(1, 2, 1) # 1行2列の1番目のプロット
plt.plot(hist_gray, color='black')
plt.title('Grayscale Histogram')
plt.xlabel('Pixel Value')
plt.ylabel('Frequency')
plt.xlim([0, 256])
# カラーヒストグラムの描画
plt.subplot(1, 2, 2) # 1行2列の2番目のプロット
for hist, col in hist_color:
plt.plot(hist, color=col)
plt.title('Color Histogram (BGR)')
plt.xlabel('Pixel Value')
plt.ylabel('Frequency')
plt.xlim([0, 256])
plt.legend(['Blue', 'Green', 'Red'])
plt.tight_layout() # レイアウト調整
plt.show() # グラフを表示
# 画像表示は別途行う
cv2.imshow('Original Grayscale', img_gray)
cv2.imshow('Original Color', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
cv2.calcHist()
関数は、計算されたヒストグラムデータを返します。このデータはNumPy配列であり、通常はMatplotlibなどのグラフ描画ライブラリを使って可視化します。
images
: ヒストグラムを計算したい画像のリストです。単一画像の場合もリスト[img]
として渡します。channels
: ヒストグラムを計算したいチャンネルのインデックスのリストです。グレースケール画像の場合は[0]
、カラー画像の場合はB, G, Rチャンネルそれぞれに対して計算する場合は[0]
(B),[1]
(G),[2]
(R) をループなどで指定します。mask
: ヒストグラムを計算したい画像の特定領域を指定するためのマスク画像です。None
を指定すると画像全体が対象となります。histSize
: 各チャンネルのヒストグラムのbinの数を含むリストです。例えば、画素値0-255を256個のビンに分けたい場合は[256]
とします。ranges
: 画素値の範囲を含むリストです。例えば、8ビット画像の場合は[0, 256]
と指定します(開始値は含む、終了値は含まない)。
14.2 ヒストグラム平坦化 (Histogram Equalization)
ヒストグラム平坦化は、画像のコントラストを改善するための手法です。画像の画素値分布を均一に近づけるように変換することで、暗すぎたり明るすぎたりして見えにくかった部分のディテールを強調することができます。これは、輝度分布が狭い範囲に集中している画像に特に効果的です。
OpenCVには、グレースケール画像に適用できるcv2.equalizeHist()
関数があります。
“`python
import cv2
import numpy as np
import matplotlib.pyplot as plt
image_path = ‘input_low_contrast.jpg’ # コントラストが低い画像を準備してください
img_gray = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if img_gray is not None:
# ヒストグラム平坦化を適用
equalized_img = cv2.equalizeHist(img_gray)
# 元画像と平坦化後の画像のヒストグラムを計算
hist_original = cv2.calcHist([img_gray], [0], None, [256], [0, 256])
hist_equalized = cv2.calcHist([equalized_img], [0], None, [256], [0, 256])
# ヒストグラムの描画
plt.figure(figsize=(10, 6))
plt.subplot(1, 2, 1)
plt.plot(hist_original, color='black')
plt.title('Original Histogram')
plt.xlabel('Pixel Value')
plt.ylabel('Frequency')
plt.xlim([0, 256])
plt.subplot(1, 2, 2)
plt.plot(hist_equalized, color='black')
plt.title('Equalized Histogram')
plt.xlabel('Pixel Value')
plt.ylabel('Frequency')
plt.xlim([0, 256])
plt.tight_layout()
plt.show()
# 画像の表示
cv2.imshow('Original Grayscale', img_gray)
cv2.imshow('Equalized Image', equalized_img)
print("ヒストグラム平坦化を行いました。")
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
cv2.equalizeHist(src)
は、入力画像src
(8ビットのグレースケール画像である必要があります)のヒストグラムを平坦化し、結果の画像を返します。
グローバルなヒストグラム平坦化は画像全体に適用されるため、部分的にコントラストが高すぎる領域ができてしまうことがあります。このような問題を解決するため、OpenCVには CLAHE (Contrast Limited Adaptive Histogram Equalization) という適応的なヒストグラム平坦化手法も用意されています。CLAHEは画像を小さなタイルに分割し、それぞれのタイル内でヒストグラム平坦化を適用します。これにより、ローカルなコントラストを改善しつつ、ノイズの過度な強調を防ぎます。
“`python
import cv2
import numpy as np
image_path = ‘input_low_contrast.jpg’ # コントラストが低い画像を準備してください
img_gray = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if img_gray is not None:
# CLAHEオブジェクトを作成
# cv2.createCLAHE(clipLimit, tileGridSize)
# clipLimit: コントラスト制限のためのしきい値。大きいほどコントラストが強くなる。
# tileGridSize: タイルに分割する際のグリッドサイズ (幅, 高さ)。
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
# CLAHEを適用
clahe_img = clahe.apply(img_gray)
cv2.imshow('Original Grayscale', img_gray)
cv2.imshow('Equalized (Global)', cv2.equalizeHist(img_gray)) # グローバル平坦化と比較
cv2.imshow('Equalized (CLAHE)', clahe_img)
print("CLAHEによるヒストグラム平坦化を行いました。")
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print(f”エラー: ファイル ‘{image_path}’ を読み込めませんでした。”)
“`
CLAHEは、特に医療画像や低照度環境で撮影された画像などのコントラスト改善に非常に有効です。
15. 応用例の紹介
これまでに学んだ基本的な画像処理技術は、単独で使われるだけでなく、組み合わせてより複雑なタスクを実行するために使われます。ここでは、いくつかの簡単な応用例に触れて、これまでの知識がどのように役立つかを見てみましょう。
15.1 簡単な色ベースの物体検出と追跡
HSV色空間での色抽出と、モルフォロジー変換、輪郭検出を組み合わせることで、特定の色のオブジェクトを検出・追跡することができます。
- 画像をHSV色空間に変換します。
cv2.inRange()
で対象の色の範囲を指定し、2値マスクを作成します。- マスク画像にモルフォロジー変換(オープニングやクロージング)を適用し、ノイズを除去したり、オブジェクトを結合したりします。
cv2.findContours()
でマスク画像の輪郭を検出します。- 検出された輪郭の中から、面積などの特徴量を使って対象のオブジェクトの輪郭を選び出します。
- 選ばれた輪郭の外接矩形や重心を計算し、元の画像に描画します。
このパイプラインは、例えばボールや特定の色の物体をカメラ映像から追跡するシステムの基礎となります。
15.2 エッジと輪郭を使った形状認識
エッジ検出と輪郭検出を組み合わせることで、画像内のオブジェクトの形状を分析できます。
- 画像をグレースケールに変換します。
cv2.Canny()
などでエッジを検出します。- 検出されたエッジ画像に対して
cv2.findContours()
を適用し、エッジをたどって輪郭を取得します。 - 検出された輪郭のポリゴン近似や円形度などの特徴量を計算し、既知の形状(例えば三角形、四角形、円など)と比較して形状を識別します。
この手法は、図形認識や特定の部品の検査などに利用できます。
これらの応用例は、ここで紹介した基本的な関数を組み合わせることで実現できます。もちろん、実際の応用ではさらに多くの技術や、場合によっては機械学習の手法も必要になりますが、OpenCVの基本的な画像処理機能はその出発点となります。
16. まとめと次のステップ
この記事では、PythonとOpenCVを使って画像処理を行うための基本的な知識と技術を幅広く解説しました。
- 環境構築から始め、画像の読み込み、表示、保存の方法を学びました。
- 画像の色空間変換(BGR, グレースケール, HSV)とその利用方法、特にHSV色空間での色抽出を見ました。
- 画像のサイズ変更、切り抜き、平行移動、回転といった幾何学的変換の方法を学びました。
- 画像上に線、矩形、円、テキストなどを描画する方法を習得しました。
- 画素値へのアクセスや画像間の算術演算、画像の2値化を行うしきい値処理について解説しました。
- 画像のノイズ除去や平滑化に使われる様々なフィルタ(平均化、ガウシアン、メディアン、バイラテラル)とその原理に触れました。
- 画像の形状に基づいた操作を行うモルフォロジー変換(膨張、収縮、オープニング、クロージングなど)を学びました。
- 画像内のオブジェクトの境界線を検出する輪郭検出と、輪郭の特徴量の取得方法を解説しました。
- 画像内の明るさの変化を検出するエッジ検出(Sobel, Laplacian, Canny)の手法を見ました。
- 画像の画素値分布を表すヒストグラムの計算と描画、およびコントラスト改善のためのヒストグラム平坦化(グローバル、CLAHE)を学びました。
これらはOpenCVの機能のほんの一部ですが、デジタル画像に対して行われるほとんどの基礎的な操作をカバーしています。これらの知識を組み合わせることで、様々な画像処理タスクの実現に向けた基盤ができます。
次のステップ
OpenCVには、この記事で紹介しきれなかった機能がまだたくさんあります。今後の学習では、以下のようなトピックに進むことをお勧めします。
- 特徴点検出とマッチング: SIFT, SURF (非フリー), ORB, AKAZEなどの特徴点検出アルゴリズムを使って、異なる画像間で対応する点を見つける方法。
- 物体検出: Haar-like特徴カスケード分類器(顔検出、目検出など)の使い方。
- 動画処理: カメラからのライブ映像や動画ファイルからフレームを読み込み、処理し、表示/保存する方法。
- キャリブレーションと3D再構成: カメラの内部パラメータや外部パラメータを推定し、ステレオカメラや複数の画像から3D情報を復元する方法。
- 機械学習モジュール: SVMやK-Meansなどの基本的な機械学習アルゴリズムがOpenCVにも実装されています。
- DNNモジュール: 最近のOpenCVには、深層学習モデル(CNNなど)を実行するためのDNNモジュールも含まれています。TensorFlow, PyTorch, Caffeなどのフレームワークで学習したモデルをOpenCVで推論することができます。
これらのさらに高度な機能は、ロボット工学、AR/VR、自動運転、監視システムなど、より複雑なコンピュータビジョンアプリケーションの開発に不可欠です。
学習を進める上では、以下のリソースが役立つでしょう。
- OpenCV公式ドキュメント: 最新かつ最も網羅的な情報源です。少し難解な場合もありますが、関数の詳細な説明や引数に関する正確な情報はここで得られます。
- OpenCV-Python Tutorials: 公式ドキュメントに含まれるPythonチュートリアルは、多くの基本的なトピックをカバーしており、コード例も豊富です。
- 書籍やオンラインコース: OpenCVや画像処理に関する様々な書籍やオンラインコースが提供されています。体系的に学びたい場合に有用です。
- コミュニティ: Stack OverflowやOpenCVフォーラムなどで質問したり、他のユーザーの解決策を参考にしたりできます。
画像処理は実践が重要です。様々な画像を使って、この記事で学んだ関数を実際に適用してみましょう。パラメータを変えて結果がどう変わるかを確認したり、複数の処理を組み合わせてみたりすることで、理解が深まります。
この入門記事が、PythonとOpenCVを使った画像処理の世界への第一歩となり、皆さんの学習やプロジェクトの助けになれば幸いです。