初心者向けOpenCV入門|画像処理をゼロから始める方法
はじめに:画像処理の世界へようこそ!
現代において、画像処理は私たちの日常生活のあらゆるところに浸透しています。スマートフォンのカメラアプリのフィルター、自動運転車の物体認識、医療分野での画像診断、工場の製品検査、さらにはSNSの顔認識機能など、数え上げればきりがありません。これらの技術の多くは、背後で高度な画像処理技術によって支えられています。
「画像処理」と聞くと、なんだか難しそう、数学や専門知識が必要そう、といった印象を持つ方もいるかもしれません。確かに、その応用分野は広く奥深いものですが、基本的な原理や操作は、プログラミング初心者でも十分に理解し、実際に手を動かして学ぶことができます。
この記事は、まさに「画像処理をゼロから始めてみたい」という完全な初心者の方を対象としています。数ある画像処理ライブラリの中でも、特に広く使われ、強力で、かつ比較的学びやすい「OpenCV」を使って、画像の読み込みから基本的な操作、フィルタリング、特徴抽出といった基礎の基礎を、コード例を交えながら丁寧に解説していきます。
OpenCV (Open Source Computer Vision Library) は、コンピュータビジョンに関する様々なアルゴリズムを集めた、オープンソースのライブラリです。C++, Python, Javaなど、様々なプログラミング言語から利用でき、特にPythonとの連携は非常にスムーズです。高速な処理が可能で、リアルタイムでのビデオ処理なども得意としています。
この記事を読むことで、あなたは以下のことができるようになります。
- OpenCVを使うための環境構築
- 画像を読み込み、表示し、保存する方法
- 画像のピクセル情報やサイズを取得・操作する方法
- 色空間を変換する方法(グレースケール、HSVなど)
- 画像のサイズ変更、回転、平行移動などの基本的な幾何学変換
- 画像のノイズを除去したり、エッジを強調したりするフィルタリング処理
- 画像を二値化する方法
- 画像の輪郭を検出する方法
さあ、一緒に画像処理の fascinating な世界への第一歩を踏み出しましょう!
開発環境の構築:OpenCVを使う準備をしよう
OpenCVを使った画像処理を行うためには、まずPythonとOpenCVライブラリをコンピュータにインストールする必要があります。ここでは、初心者の方にとって比較的簡単でおすすめの方法を紹介します。
1. Pythonのインストール
OpenCVはPythonから使うのが非常に一般的で、この記事もPythonを使用します。Pythonの実行環境がまだない場合は、まずPythonをインストールしましょう。
初心者の方には、Anaconda というPythonのディストリビューションをインストールすることをおすすめします。Anacondaには、Python本体だけでなく、科学技術計算やデータ分析に頻繁に使用されるライブラリ(NumPy, SciPy, pandasなど)があらかじめ含まれており、環境構築が非常に楽になります。OpenCVもこれらのライブラリと連携して使うことが多いので、最初からAnacondaを入れておくと便利です。
- Anacondaのインストール方法:
- Anacondaの公式サイト (https://www.anaconda.com/products/distribution) にアクセスします。
- お使いのOS (Windows, macOS, Linux) に合ったインストーラをダウンロードします。
- ダウンロードしたインストーラを実行し、指示に従ってインストールを進めます。特別な理由がなければ、デフォルトの設定で問題ありません。インストール場所はデフォルトのままで良いでしょう。環境変数へのPATHの追加については、インストーラでチェックボックスが表示される場合がありますが、チェックを入れておくとコマンドプロンプト(またはターミナル)から
pythonコマンドなどを直接実行できるようになります。
Anacondaをインストールしない場合は、Python公式サイト (https://www.python.org/) から標準インストーラをダウンロードしてインストールすることも可能です。その場合は、後述するNumPyなどのライブラリを個別にインストールする必要があります。
インストールが完了したら、コマンドプロンプト(Windows)またはターミナル(macOS, Linux)を開き、以下のコマンドを入力してPythonが正しくインストールされているか確認しましょう。
bash
python --version
あるいは、Anacondaをインストールした場合は
bash
conda --version
と入力して確認します。バージョン情報が表示されれば成功です。
2. OpenCVとNumPyのインストール
Pythonの実行環境が準備できたら、いよいよOpenCVライブラリをインストールします。Pythonのパッケージ管理システムである pip を使うのが最も簡単です。
コマンドプロンプトまたはターミナルを開き、以下のコマンドを実行します。
bash
pip install opencv-python
これにより、OpenCVのPythonラッパーがインストールされます。
画像データを効率的に扱うために、OpenCVは内部的に NumPy というライブラリを使用します。NumPyはPythonで数値計算を高速に行うためのライブラリで、特に多次元配列(行列)の操作に優れています。OpenCVが画像をNumPy配列として扱うため、OpenCVとNumPyはセットで使われることがほとんどです。
Anacondaをインストールした場合は、NumPyはすでに入っていることが多いですが、念のため確認するか、以下のコマンドでインストールしておきましょう。
bash
pip install numpy
これで、OpenCVとNumPyを使う準備が整いました。
3. 開発環境(IDE)の準備(任意)
Pythonコードを書くためのエディタや統合開発環境(IDE)があると、コードの記述や実行、デバッグがしやすくなります。初心者の方には、以下のようなIDEがおすすめです。
- VS Code (Visual Studio Code): 軽量で高機能なエディタ。Python拡張機能をインストールすることでIDEのように使えます。無料で利用でき、多くのプログラマーに愛用されています。
- PyCharm: Python開発に特化した強力なIDE。コード補完やデバッグ機能が非常に優れています。Community Editionは無料で利用できます。
- Jupyter Notebook / JupyterLab: ウェブブラウザ上でコードを記述・実行し、その結果(テキスト、画像、グラフなど)を一緒に表示できる環境。試行錯誤しながらコードを書いていくのに向いています。Anacondaには標準で含まれています。
お好みの環境を準備してください。もちろん、Pythonが実行できる環境であれば、シンプルなテキストエディタでも問題ありません。
これで、OpenCVを使った画像処理を始めるための環境構築は完了です!
OpenCVの基本操作:画像を読み込み、表示し、保存する
画像処理の第一歩は、コンピュータ上の画像ファイルをプログラムで扱えるようにすることです。OpenCVを使えば、簡単に画像を読み込み、画面に表示し、処理結果をファイルに保存することができます。
1. 画像の読み込み (cv2.imread())
cv2.imread() 関数は、指定したパスにある画像を読み込みます。
“`python
import cv2
import numpy as np # 画像はNumPy配列として扱われます
画像ファイルのパスを指定
ここでは同じフォルダにある ‘input.jpg’ という名前の画像を読み込むと仮定
image_path = ‘input.jpg’
画像を読み込む
cv2.imread(ファイルパス, 読み込みフラグ)
読み込みフラグについて:
cv2.IMREAD_COLOR (または 1): カラー画像として読み込む (透明度は無視)。デフォルト。
cv2.IMREAD_GRAYSCALE (または 0): グレースケール画像として読み込む。
cv2.IMREAD_UNCHANGED (または -1): アルファチャンネルを含む元の形式で読み込む。
img = cv2.imread(image_path, cv2.IMREAD_COLOR)
画像が正しく読み込まれたか確認
if img is None:
print(f”エラー: 画像ファイル ‘{image_path}’ を読み込めませんでした。パスを確認してください。”)
else:
print(f”画像 ‘{image_path}’ を読み込みました。”)
# 画像の情報を少し見てみましょう(後のセクションで詳しく解説)
print(f”画像の形状 (高さ, 幅, チャンネル数): {img.shape}”)
print(f”画像のデータ型: {img.dtype}”)
“`
cv2.imread() は、読み込んだ画像をNumPyの多次元配列(ndarray)として返します。画像が読み込めなかった場合(ファイルが存在しない、パスが間違っている、画像ファイルが破損しているなど)は None を返します。そのため、画像を読み込んだ後は、必ず None でないか確認する習慣をつけましょう。
デフォルトの読み込みフラグ cv2.IMREAD_COLOR でカラー画像を読み込むと、画像データは BGR の順序で格納されます。一般的な画像ファイル(PNG, JPEGなど)ではRGBの順序で色が表現されますが、OpenCVでは歴史的な理由からBGRがデフォルトです。これについては後のセクションで詳しく説明します。
2. 画像の表示 (cv2.imshow(), cv2.waitKey(), cv2.destroyAllWindows())
読み込んだ画像を画面上のウィンドウに表示するには、cv2.imshow() を使用します。しかし、cv2.imshow() は画像をウィンドウに表示するだけで、プログラムはすぐに次の行に進んでしまい、ウィンドウは瞬時に消えてしまいます。ウィンドウを表示し続けるためには、キー入力などを待つ必要があります。そのために cv2.waitKey() 関数を使います。また、表示したウィンドウを閉じるためには cv2.destroyAllWindows() 関数を使用します。
“`python
上記の画像読み込みコードに続けて実行
if img is not None:
# ウィンドウに画像を表示
# cv2.imshow(ウィンドウ名, 表示する画像データ)
cv2.imshow(‘Loaded Image’, img)
# キー入力を待つ
# cv2.waitKey(待ち時間 ミリ秒)
# 0を指定すると、何かキーが押されるまで無限に待つ
# 例: 1000を指定すると、1秒待つ
print("画像が表示されました。何かキーを押すとウィンドウが閉じます。")
cv2.waitKey(0)
# 全てのウィンドウを破棄する
cv2.destroyAllWindows()
print("ウィンドウを閉じました。")
“`
cv2.imshow('ウィンドウ名', img): ‘ウィンドウ名’ という名前のウィンドウを作成し、imgの画像データを表示します。同じ名前のウィンドウがすでに存在する場合は、そのウィンドウの内容が更新されます。cv2.waitKey(delay): キー入力を待ちます。引数delayは待ち時間をミリ秒で指定します。delay = 0: 何らかのキーが押されるまでプログラムの実行を一時停止します。ウィンドウを表示したままユーザーの操作を待つ場合によく使われます。delay > 0: 指定したミリ秒だけ待機します。この時間内にキーが押されれば、そのキーのASCIIコードを返します。時間が経過してもキーが押されなければ-1を返します。ビデオ処理のように、一定時間ごとにフレームを更新する際などに使用します。
cv2.destroyAllWindows(): OpenCVによって作成された全てのウィンドウを閉じ、関連するメモリを解放します。プログラムの終了前に呼び出すのが一般的です。特定のウィンドウだけを閉じたい場合はcv2.destroyWindow('ウィンドウ名')を使用します。
これらの関数をセットで使うことで、読み込んだ画像をユーザーが見て確認できるように表示することができます。
3. 画像の保存 (cv2.imwrite())
処理した結果の画像をファイルとして保存するには、cv2.imwrite() 関数を使用します。
“`python
上記の画像読み込み・表示コードに続けて実行
if img is not None:
# グレースケールに変換した画像を保存してみる(変換方法は後述)
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 画像をファイルに保存
# cv2.imwrite(保存先のファイルパス, 保存する画像データ)
output_path = 'output_gray.jpg'
success = cv2.imwrite(output_path, gray_img)
if success:
print(f"処理結果の画像を '{output_path}' に保存しました。")
else:
print(f"エラー: 画像を '{output_path}' に保存できませんでした。")
“`
cv2.imwrite(filename, img):imgの画像データを指定したfilenameでファイルに保存します。ファイルの拡張子(例:.jpg,.png,.bmp)によって、OpenCVが自動的に保存形式を判断します。
保存に成功すると True が、失敗すると False が返されます。保存失敗の原因としては、指定されたパスに書き込み権限がない、ファイル名に不正な文字が含まれている、指定された形式をOpenCVがサポートしていない、などが考えられます。
これらの基本的な読み込み、表示、保存の操作は、OpenCVを使った画像処理を行う上で最も頻繁に使用するものです。まずはこれらの操作に慣れることから始めましょう。
画像の基本情報取得と操作:ピクセルに触れてみよう
OpenCVが画像をNumPy配列として扱うことの利点は、NumPyが提供する強力な配列操作機能をそのまま画像データに適用できることです。ここでは、画像のサイズやデータ型を取得したり、個々のピクセル値にアクセスしたり、画像の一部を切り出したりする方法を見ていきます。
1. 画像のサイズ、データ型、ピクセル値の取得
画像をNumPy配列として読み込むと、その配列は画像の様々な情報を含んでいます。
“`python
import cv2
import numpy as np
image_path = ‘input.jpg’ # または他の画像ファイル
img = cv2.imread(image_path)
if img is not None:
# 画像の形状 (NumPy配列のshapeプロパティ)
# カラー画像の場合: (高さ, 幅, チャンネル数)
# グレースケール画像の場合: (高さ, 幅)
print(f”画像の形状: {img.shape}”)
# 画像の総ピクセル数 (NumPy配列のsizeプロパティ)
print(f"総ピクセル数: {img.size}")
# 画像のデータ型 (NumPy配列のdtypeプロパティ)
# 一般的には uint8 (符号なし8ビット整数) - 0から255の値
print(f"画像のデータ型: {img.dtype}")
# 特定のピクセル値にアクセス (NumPy配列のインデックス指定)
# 画素の座標は (行インデックス, 列インデックス) または (y座標, x座標) で指定
# OpenCVでは通常、座標は (x, y) で表現されることが多いが、NumPy配列としては (y, x) となる点に注意
# 例: 左上隅 (0, 0) のピクセル値
# カラー画像の場合: [B, G, R] のリストまたはタプル
# グレースケール画像の場合: 単一の輝度値
try:
# 画像の中心あたりのピクセル値にアクセスしてみる
height, width = img.shape[:2] # shapeから高さと幅を取得 (チャンネル数は不要なら[:2]で除外)
center_pixel = img[height // 2, width // 2]
print(f"画像中心 ({width // 2}, {height // 2}) のピクセル値: {center_pixel}")
# 特定のチャンネル(例: 青チャンネル)の値にアクセス
# カラー画像の場合のみ
if len(img.shape) == 3: # shapeの要素数が3ならカラー画像
blue_channel_center = img[height // 2, width // 2, 0] # BGRの0番目が青
print(f"画像中心の青チャンネル値: {blue_channel_center}")
except IndexError:
print("画像サイズが小さすぎて中心ピクセルにアクセスできませんでした。")
“`
img.shape: 画像の形状をタプルで返します。カラー画像なら(高さ, 幅, 3)、グレースケール画像なら(高さ, 幅)となります。3はBGRの3チャンネルを意味します。img.size: 画像の総ピクセル数 * チャンネル数を返します。カラー画像なら高さ * 幅 * 3、グレースケール画像なら高さ * 幅です。img.dtype: 画像データのデータ型を返します。一般的な画像ファイルから読み込んだ場合はuint8となります。これは各ピクセルが0から255の範囲の符号なし8ビット整数で表現されていることを意味します。img[y, x]またはimg[y, x, c]: NumPy配列のインデックス指定により、特定のピクセルまたは特定のピクセルの特定のチャンネル値にアクセスできます。yが行インデックス(高さ方向)、xが列インデックス(幅方向)です。カラー画像の場合はcでチャンネルを指定します(0: 青, 1: 緑, 2: 赤)。
NumPy配列のインデックス指定は非常に強力です。これを使うことで、画像の一部を簡単に操作できます。
2. 画素値の操作
取得したピクセル値は、NumPy配列の要素として直接変更することができます。
“`python
上記のコードに続けて実行
if img is not None:
# 画像をコピーしておくと元の画像を変更せずに作業できます
img_modified = img.copy()
# 例: 左上隅のピクセルを黒 (0, 0, 0) に設定
# カラー画像の場合
if len(img_modified.shape) == 3:
img_modified[0, 0] = [0, 0, 0] # B, G, R の順
print("左上隅のピクセルを黒に設定しました。")
# 例: 画像の中心にある10x10ピクセルの領域を白 (255, 255, 255) に設定
h, w = img_modified.shape[:2]
center_y, center_x = h // 2, w // 2
size = 10
# スライシングを使って領域を指定
# img[y_start : y_end, x_start : x_end]
y_start = max(0, center_y - size // 2)
y_end = min(h, center_y + size // 2)
x_start = max(0, center_x - size // 2)
x_end = min(w, center_x + size // 2)
img_modified[y_start:y_end, x_start:x_end] = [255, 255, 255] # 白
print(f"画像中心の {size}x{size} の領域を白に設定しました。")
# グレースケール画像の場合
# gray_img_modified = gray_img.copy() # 前のセクションで作成したgray_imgがある場合
# gray_img_modified[0, 0] = 0 # 左上隅を黒 (輝度0) に設定
# gray_img_modified[y_start:y_end, x_start:x_end] = 255 # 中心領域を白 (輝度255) に設定
# 変更後の画像を表示
cv2.imshow('Modified Image', img_modified)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
img_modified = img.copy(): 画像全体または画像の一部を変更する場合、元の画像データを保護するために.copy()メソッドを使ってコピーを作成することをおすすめします。NumPy配列は代入によって参照がコピーされるため、img_modified = imgとすると、img_modifiedへの変更がimgにも反映されてしまいます。img[y, x] = [B, G, R]: 特定のピクセルの色を直接設定します。img[y_start : y_end, x_start : x_end] = value: NumPyのスライシング機能を使って、矩形領域の全てのピクセル値を一度に設定できます。valueに単一の値を指定すると、その領域の全チャンネルがその値になります(グレースケール画像の場合)。BGRのリストまたはタプルを指定すると、各ピクセルのチャンネルごとにその値が設定されます。
NumPyのスライシングは非常に強力で、画像の一部(ROI: Region of Interest)を簡単に取得したり、操作したりするのに使われます。
3. 画像チャンネルの分離・結合
カラー画像(BGRまたはRGB)は、通常、青(B)、緑(G)、赤(R)の3つのチャンネルに分かれています。OpenCVでは、これらのチャンネルを分離したり、分離したチャンネルから再びカラー画像を結合したりする関数が用意されています。
“`python
上記のコードに続けて実行
if img is not None and len(img.shape) == 3: # カラー画像の場合のみ実行
# チャンネルを分離
# cv2.split() 関数は、各チャンネルのグレースケール画像を要素とするリストを返す
b, g, r = cv2.split(img)
print("チャンネルを分離しました。")
print(f"青チャンネル画像の形状: {b.shape}") # (高さ, 幅)
print(f"緑チャンネル画像の形状: {g.shape}") # (高さ, 幅)
print(f"赤チャンネル画像の形状: {r.shape}") # (高さ, 幅)
# 分離したチャンネルを個別に表示してみる
# 各チャンネルはグレースケール画像として表示される
cv2.imshow('Blue Channel', b)
cv2.imshow('Green Channel', g)
cv2.imshow('Red Channel', r)
cv2.waitKey(0)
# cv2.destroyAllWindows() # 一旦ここで閉じずに次に進む
# 例: 緑チャンネルと赤チャンネルをゼロにして、青成分だけを持つ画像を作成
# 各チャンネルは単一の値を保持するグレースケール画像なので、NumPy操作が可能
# np.zeros_like(配列) は、指定された配列と同じ形状とデータ型のゼロ配列を作成
zeros = np.zeros_like(b) # 青チャンネルと同じ形状のゼロ配列
# cv2.merge() 関数は、単一チャンネル画像を結合してカラー画像を生成
# cv2.merge([チャンネル1, チャンネル2, チャンネル3, ...])
# BGRの順序で結合
blue_only_img = cv2.merge([b, zeros, zeros]) # 青チャンネルのみ
green_only_img = cv2.merge([zeros, g, zeros]) # 緑チャンネルのみ
red_only_img = cv2.merge([zeros, zeros, r]) # 赤チャンネルのみ
cv2.imshow('Blue Only', blue_only_img)
cv2.imshow('Green Only', green_only_img)
cv2.imshow('Red Only', red_only_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.split(img): カラー画像を個々のチャンネルを表すグレースケール画像(NumPy配列)のリストに分割します。戻り値は[b, g, r]のようになります(BGR順)。cv2.merge([channel1, channel2, channel3, ...]): 複数の単一チャンネル画像(グレースケール画像)を結合して、多チャンネル画像を作成します。カラー画像を作成する場合、BGRの順序でチャンネルをリストに格納します。
チャンネルの分離・結合は、特定の色の成分だけを取り出したり、強調したりする場合などに役立ちます。また、NumPy配列として個々のチャンネルにアクセスすることでも同様の操作は可能ですが、cv2.split() と cv2.merge() はより直感的です。
これらの基本的な情報取得と操作を理解することで、画像の内部構造を把握し、ピクセルレベルでの操作の基礎を築くことができます。
色空間の変換:画像の色を別の方法で表現する
私たちは通常、色を「赤」「青」「緑」といった三原色の組み合わせで捉えます。これをRGB(Red, Green, Blue)色空間と呼びます。一般的な画像ファイル(JPEG, PNGなど)はこのRGB形式で保存されています。しかし、OpenCVがデフォルトで扱うのは BGR 色空間です(青、緑、赤の順)。そして、画像処理の目的によっては、RGBやBGR以外の色空間で画像を扱う方が便利な場合があります。
代表的な色空間の変換機能と、なぜそれが必要なのかを見ていきましょう。
1. BGRとは何か? なぜOpenCVはBGRを使うのか?
RGBは直感的で、ディスプレイやカメラなどのハードウェアで広く使われています。一方、OpenCVがBGRをデフォルトにしているのは、OpenCVが元々C++で開発されており、その黎明期に使われていた古いBMPファイル形式のピクセルデータの格納順序がBGRだったことに由来すると言われています。
ほとんどの画像処理アルゴリズムはBGRとRGBの順序の違いに影響を受けませんが、BGRとRGBの間で相互変換が必要な場合もあります。OpenCVでは、読み込んだカラー画像は特に指定しない限りBGRとして扱われる、ということを覚えておきましょう。
2. グレースケール変換 (cv2.cvtColor() – BGR to Gray)
最も基本的な色空間変換の一つが、カラー画像をグレースケール(白黒)画像に変換することです。グレースケール画像は色の情報を持たず、各ピクセルは明るさ(輝度)だけを持ちます。これにより、データ量が減り、色情報に左右されない処理(例:エッジ検出、輪郭検出など)を行うのに適しています。
グレースケールへの変換は、通常、RGB(またはBGR)の各チャンネル値から計算される輝度値に置き換えられます。一般的な計算式は 輝度 = 0.299 * R + 0.587 * G + 0.114 * B のような加重平均ですが、OpenCVは内部的に最適な計算を行います。
“`python
import cv2
import numpy as np
image_path = ‘input.jpg’
img = cv2.imread(image_path, cv2.IMREAD_COLOR) # カラー画像として読み込む
if img is not None:
# カラー画像の場合のみグレースケール変換
if len(img.shape) == 3:
# cv2.cvtColor(入力画像, 変換コード)
# 変換コードは cv2.COLOR_〇〇2△△ の形式
# ここでは BGR から GRAY へ変換
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
print("カラー画像をグレースケールに変換しました。")
print(f"グレースケール画像の形状: {gray_img.shape}") # (高さ, 幅)
print(f"グレースケール画像のデータ型: {gray_img.dtype}") # uint8
# 元の画像とグレースケール画像を表示
cv2.imshow('Original Color Image (BGR)', img)
cv2.imshow('Grayscale Image', gray_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
else:
print("読み込んだ画像はすでにグレースケールかもしれません。")
“`
cv2.cvtColor(src, code): 画像srcを指定したcodeに従って色空間変換します。cv2.COLOR_BGR2GRAY: BGR色空間からグレースケールへの変換を指定するコードです。他にもcv2.COLOR_RGB2GRAY,cv2.COLOR_BGR2RGB,cv2.COLOR_BGR2HSVなど、様々な変換コードが用意されています。
グレースケール画像は1チャンネル(輝度)のみを持つため、NumPy配列の形状は (高さ, 幅) となります。
3. HSV変換 (cv2.cvtColor() – BGR to HSV)
もう一つ、画像処理で非常によく使われる色空間が HSV (Hue, Saturation, Value) です。
- Hue (色相): 赤、黄、緑、シアン、青、マゼンタといった「色み」を表します。円環状になっており、0〜180(または0〜360)の角度で表現されることが多いです。OpenCVでは通常0〜179の範囲で正規化されます。
- Saturation (彩度): 色の鮮やかさを表します。彩度が高いほど鮮やかになり、低いほどくすんだ色(白や灰色に近い色)になります。0〜255の範囲で表現されることが多いです。
- Value (明度): 色の明るさを表します。明度が高いほど明るく(白に近く)なり、低いほど暗く(黒に近く)なります。0〜255の範囲で表現されることが多いです。
なぜHSVが便利なのでしょうか?それは、HSVが色相、彩度、明度を分離して表現しているためです。例えば、「特定の色の物体だけを検出したい」といった場合、RGB空間では光の明るさや影によってRGB値が大きく変動してしまいますが、HSV空間では色相(Hue)の値が比較的安定しているため、特定のHueの範囲で画像をフィルタリングすることで目的の色を容易に抽出できます。
“`python
上記のコードに続けて実行
if img is not None and len(img.shape) == 3: # カラー画像の場合のみ実行
# BGR から HSV へ変換
hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
print("カラー画像をHSVに変換しました。")
print(f"HSV画像の形状: {hsv_img.shape}") # (高さ, 幅, 3) - Hue, Saturation, Value の3チャンネル
print(f"HSV画像のデータ型: {hsv_img.dtype}") # uint8
# HSV画像をチャンネル分離して表示してみる
# 各チャンネルはグレースケール画像として表示されるが、
# Hueチャンネルは色の種類、Saturationは鮮やかさ、Valueは明るさを示す
h, s, v = cv2.split(hsv_img)
cv2.imshow('Original Color Image (BGR)', img)
cv2.imshow('HSV Image', hsv_img) # OpenCVはHSV画像をBGRに変換して表示する
cv2.imshow('Hue Channel', h)
cv2.imshow('Saturation Channel', s)
cv2.imshow('Value Channel', v)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.COLOR_BGR2HSV: BGR色空間からHSV色空間への変換を指定するコードです。
HSV画像もカラー画像と同様に3チャンネルを持ちますが、その内容はBGRとは全く異なります。HSV画像を cv2.imshow() で表示すると、OpenCVは自動的にBGR色空間に変換してから表示するため、一見元の画像と似て見えるかもしれませんが、データとしてはHSV値が格納されています。
補足: OpenCVのHSVにおけるHueの値は0〜179の範囲で表現されます。これは、Hueの円環(0〜360度)を2で割って8ビット(0〜255)に収まるようにしているためです。SaturationとValueは0〜255の範囲です。特定の色の範囲をHSVで指定する際は、このHueの範囲に注意が必要です。
例えば、「赤い部分」を抽出したい場合、赤のHue値は0付近と180付近(OpenCVの180度は360度に相当)の両方に存在します。例えば、Hue値が0〜10の範囲と170〜179の範囲を指定することで、画像中の赤い領域を抽出できます。
“`python
特定の色(例:赤)を抽出する簡単な例
if img is not None and len(img.shape) == 3:
hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 赤色の範囲を定義 (HSVで指定)
# OpenCVのHueは0-179
# 赤は0付近と180(実質179)付近
# 例: Hue 0-10 (赤), Saturation > 50, Value > 50
lower_red1 = np.array([0, 50, 50])
upper_red1 = np.array([10, 255, 255])
# 例: Hue 170-179 (赤), Saturation > 50, Value > 50
lower_red2 = np.array([170, 50, 50])
upper_red2 = np.array([179, 255, 255])
# cv2.inRange() 関数を使って、指定した範囲内のピクセルを抽出するマスクを作成
# マスクは、範囲内のピクセルが255 (白)、範囲外が0 (黒) のグレースケール画像
mask1 = cv2.inRange(hsv_img, lower_red1, upper_red1)
mask2 = cv2.inRange(hsv_img, lower_red2, upper_red2)
# 2つのマスクを結合(論理和 OR)
red_mask = mask1 + mask2
# 元画像にマスクを適用して、赤色以外の部分を黒にする
# cv2.bitwise_and() は、2つの画像のAND演算を行う。マスクを使って特定の領域を抽出する際によく使う
# 元画像 img と 元画像 img の AND を取るが、red_mask が 0 のピクセルは結果も 0 になる
result_img = cv2.bitwise_and(img, img, mask=red_mask)
cv2.imshow('Original Image', img)
cv2.imshow('Red Mask', red_mask) # マスクを表示
cv2.imshow('Red Only', result_img) # 赤色のみの画像を表示
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
この例のように、cv2.inRange() 関数を使うことで、HSVなどの色空間で指定した値の範囲内にあるピクセルを簡単に抽出できます。これは特定の色を検出したり、背景から物体を分離したりする際に非常に強力な手法です。
色空間の変換は、画像処理の目的や対象とする画像の種類に応じて適切に使い分けることが重要です。
基本的な画像変換:画像の形や位置を変える
画像処理における「変換」とは、画像のピクセル値を変更したり、ピクセルの位置を移動させたりして、画像の見かけや内容を変えることです。ここでは、画像のサイズを変更したり、平行移動、回転、アフィン変換、射影変換といった基本的な幾何学変換について学びます。
これらの変換は、画像の前処理(例:サイズを統一する、位置合わせを行う)や、コンピューターグラフィックス、AR/VRなどで広く利用されています。
1. リサイズ (cv2.resize())
画像のサイズを変更(拡大・縮小)する最も一般的な方法です。
“`python
import cv2
import numpy as np
image_path = ‘input.jpg’
img = cv2.imread(image_path)
if img is not None:
# 画像の元のサイズを取得
height, width = img.shape[:2]
print(f”元のサイズ: 幅={width}, 高さ={height}”)
# サイズを指定してリサイズ(例: 幅を200ピクセルにする、アスペクト比を維持)
new_width = 200
# アスペクト比を維持して新しい高さを計算
aspect_ratio = width / height
new_height = int(new_width / aspect_ratio)
# cv2.resize(入力画像, (新しい幅, 新しい高さ), 補間方法)
# 補間方法について:
# cv2.INTER_NEAREST: 最近傍補間 (最も高速だが画質は低い、拡大時にブロックノイズが出やすい)
# cv2.INTER_LINEAR: バイリニア補間 (デフォルト、一般的)
# cv2.INTER_CUBIC: バイキュービック補間 (より高品質だが処理は重い、縮小には向かない)
# cv2.INTER_AREA: ピクセル領域の関係で補間 (縮小に適している)
resized_img_by_size = cv2.resize(img, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
print(f"サイズ指定でリサイズ後のサイズ: 幅={new_width}, 高さ={new_height}")
# 拡大・縮小率を指定してリサイズ(例: 0.5倍に縮小、2倍に拡大)
scale_factor_x = 0.5
scale_factor_y = 0.5 # X方向とY方向で異なるスケールも可能
# cv2.resize(入力画像, 出力サイズ=(0,0), fx=X方向スケール, fy=Y方向スケール, 補間方法)
# 出力サイズを(0,0)にすると、fx, fyに基づいて自動計算される
resized_img_by_scale = cv2.resize(img, (0,0), fx=scale_factor_x, fy=scale_factor_y, interpolation=cv2.INTER_LINEAR)
print(f"スケール指定でリサイズ後のサイズ: 幅={resized_img_by_scale.shape[1]}, 高さ={resized_img_by_scale.shape[0]}")
cv2.imshow('Original Image', img)
cv2.imshow(f'Resized by Size ({new_width}x{new_height})', resized_img_by_size)
cv2.imshow(f'Resized by Scale ({scale_factor_x}x)', resized_img_by_scale)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.resize(src, dsize, fx=0, fy=0, interpolation): 画像srcのサイズを変更します。dsize: 出力画像の新しいサイズを(幅, 高さ)のタプルで指定します。fx,fy: 幅方向、高さ方向のスケールファクターを指定します。dsizeが(0,0)の場合にfx,fyが使用され、出力サイズは(元の幅 * fx, 元の高さ * fy)となります。dsizeが指定されている場合はfx,fyは無視されます。interpolation: 補間方法を指定します。拡大時にはcv2.INTER_LINEARやcv2.INTER_CUBIC、縮小時にはcv2.INTER_AREAが一般的に使われます。
リサイズは非常に頻繁に行われる操作です。特に、機械学習モデルへの入力画像サイズを統一する際などに必須となります。
2. 平行移動 (cv2.warpAffine())
画像を水平方向または垂直方向に移動させる変換です。この変換には アフィン変換 と呼ばれるより一般的な変換の一部として行われ、変換内容を表現する 変換行列 が必要になります。
平行移動の変換行列は以下の形式になります。
[ 1 0 tx ]
[ 0 1 ty ]
ここで tx はX方向(水平)の移動量、ty はY方向(垂直)の移動量です。正の値はそれぞれ右方向、下方向への移動を示します。
“`python
上記のコードに続けて実行
if img is not None:
height, width = img.shape[:2]
# 平行移動量 (右に50px, 下に30px)
tx = 50
ty = 30
# 変換行列 M を定義 (NumPyのfloat型である必要がある)
M_translate = np.float32([[1, 0, tx],
[0, 1, ty]])
# cv2.warpAffine(入力画像, 変換行列, 出力画像のサイズ)
# 出力画像のサイズは通常、元の画像サイズを指定
translated_img = cv2.warpAffine(img, M_translate, (width, height))
cv2.imshow('Original Image', img)
cv2.imshow(f'Translated Image (tx={tx}, ty={ty})', translated_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.warpAffine(src, M, dsize): 画像srcに変換行列Mを適用し、サイズdsizeの出力画像を生成します。アフィン変換は直線や平行線を維持する変換です。M: 2×3の変換行列です。NumPyのfloat32型配列である必要があります。
平行移動によって、画像の一部がウィンドウの外に出て見えなくなることがあります。出力画像のサイズを元の画像サイズより大きくすることで、画像の全てを表示させることも可能ですが、その場合は背景部分は通常黒になります。
3. 回転 (cv2.getRotationMatrix2D(), cv2.warpAffine())
画像を特定の中心点の周りで回転させる変換です。回転もアフィン変換の一種であり、やはり変換行列が必要です。OpenCVには回転変換行列を簡単に計算してくれる関数があります。
“`python
上記のコードに続けて実行
if img is not None:
height, width = img.shape[:2]
# 回転の中心 (画像の中心を基準にすることが多い)
center = (width // 2, height // 2)
# 回転角度 (度数法、反時計回りが正)
angle = 45
# スケールファクター (1.0 で元のサイズ、2.0 で2倍に拡大など)
scale = 1.0
# 回転変換行列を取得
# cv2.getRotationMatrix2D(中心点, 角度, スケール)
M_rotate = cv2.getRotationMatrix2D(center, angle, scale)
# 回転変換を適用
# 出力サイズは元の画像サイズで、回転によって一部が切り取られる
rotated_img = cv2.warpAffine(img, M_rotate, (width, height))
cv2.imshow('Original Image', img)
cv2.imshow(f'Rotated Image ({angle} degrees)', rotated_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.getRotationMatrix2D(center, angle, scale): 指定された中心点center、角度angle、スケールscaleに基づく2×3の回転変換行列を計算します。角度は度数法で指定し、正の値は反時計回りの回転になります。cv2.warpAffine(): 取得した回転変換行列を画像に適用します。
回転によって画像の一部が切り取られてしまうのを防ぐためには、回転後の画像全体が収まるように出力サイズ (dsize) を計算し直す必要があります。これは少し計算が必要になりますが、OpenCVのドキュメントなどで計算方法が紹介されています。
4. アフィン変換 (cv2.warpAffine())
平行移動、回転、拡大・縮小、せん断(Skew)といった変換は、すべてアフィン変換として表現できます。アフィン変換では、直線は直線のまま、平行線は平行線のまま維持されます。アフィン変換を適用するには、入力画像の3つの対応点と、それらが変換後に移動する出力画像の3つの点を指定します。この3つの点の情報から、変換行列が計算されます。
“`python
上記のコードに続けて実行
if img is not None:
height, width = img.shape[:2]
# 入力画像上の3つの点 (左上、右上、左下を例とする)
# NumPyのfloat32型である必要がある
pts1 = np.float32([[50, 50], [200, 50], [50, 200]])
# これらの点が移動する出力画像上の3つの点 (例: 少しずらす、せん断させる)
pts2 = np.float32([[10, 100], [200, 50], [100, 250]])
# 3つの対応点からアフィン変換行列を取得
# cv2.getAffineTransform(入力側の3点配列, 出力側の3点配列)
M_affine = cv2.getAffineTransform(pts1, pts2)
# アフィン変換を適用
affine_img = cv2.warpAffine(img, M_affine, (width, height))
cv2.imshow('Original Image', img)
# 対応点を描画してみる (オプション)
img_pts = img.copy()
for pt in pts1:
cv2.circle(img_pts, (int(pt[0]), int(pt[1])), 5, (0, 0, 255), -1) # 元画像に赤い点を描画
img_affine_pts = affine_img.copy()
for pt in pts2:
cv2.circle(img_affine_pts, (int(pt[0]), int(pt[1])), 5, (0, 0, 255), -1) # 変換後画像に赤い点を描画
cv2.imshow('Original with Points', img_pts)
cv2.imshow('Affine Transformed Image', affine_img)
cv2.imshow('Affine Transformed with Points', img_affine_pts)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.getAffineTransform(src_pts, dst_pts): 入力画像上の3つの点src_ptsと、それに対応する出力画像上の3つの点dst_ptsから、2×3のアフィン変換行列を計算します。src_ptsとdst_ptsはそれぞれ3つの(x, y)座標を持つNumPyのfloat32型配列である必要があります。cv2.warpAffine(): 取得したアフィン変換行列を画像に適用します。
アフィン変換は、カメラで斜めから撮影された矩形をまっすぐに見せる(ただし奥行きは無視)などに応用できます。
5. 射影変換 (cv2.getPerspectiveTransform(), cv2.warpPerspective())
射影変換(パースペクティブ変換)は、アフィン変換よりも一般的な変換で、画像に遠近感を持たせることができます。射影変換では直線は直線のまま維持されますが、平行線は必ずしも平行には維持されません(一点に収束することがあります)。射影変換を行うには、入力画像の4つの対応点と、それらが変換後に移動する出力画像の4つの点を指定します。
“`python
上記のコードに続けて実行
if img is not None:
height, width = img.shape[:2]
# 入力画像上の4つの点 (例: 書籍の角、平面上の4点など)
# 通常は左上、右上、右下、左下の順で指定することが多いが必須ではない
# NumPyのfloat32型である必要がある
pts1 = np.float32([[56, 65], [368, 52], [28, 387], [389, 390]]) # 例として適当な4点
# これらの点が変換後に移動する出力画像上の4つの点 (例: 矩形)
# 例として、元の画像のサイズに合わせてまっすぐな矩形にする
pts2 = np.float32([[0, 0], [width - 1, 0], [0, height - 1], [width - 1, height - 1]])
# 4つの対応点から射影変換行列を取得
# cv2.getPerspectiveTransform(入力側の4点配列, 出力側の4点配列)
M_perspective = cv2.getPerspectiveTransform(pts1, pts2)
# 射影変換を適用
# cv2.warpPerspective(入力画像, 変換行列, 出力画像のサイズ)
perspective_img = cv2.warpPerspective(img, M_perspective, (width, height))
cv2.imshow('Original Image', img)
# 対応点を描画してみる (オプション)
img_pts = img.copy()
for pt in pts1:
cv2.circle(img_pts, (int(pt[0]), int(pt[1])), 5, (0, 0, 255), -1) # 元画像に赤い点を描画
img_perspective_pts = perspective_img.copy()
for pt in pts2:
cv2.circle(img_perspective_pts, (int(pt[0]), int(pt[1])), 5, (0, 0, 255), -1) # 変換後画像に赤い点を描画
cv2.imshow('Original with Points', img_pts)
cv2.imshow('Perspective Transformed Image', perspective_img)
cv2.imshow('Perspective Transformed with Points', img_perspective_pts)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.getPerspectiveTransform(src_pts, dst_pts): 入力画像上の4つの点src_ptsと、それに対応する出力画像上の4つの点dst_ptsから、3×3の射影変換行列を計算します。src_ptsとdst_ptsはそれぞれ4つの(x, y)座標を持つNumPyのfloat32型配列である必要があります。cv2.warpPerspective(src, M, dsize): 画像srcに射影変換行列Mを適用し、サイズdsizeの出力画像を生成します。
射影変換は、斜めから撮影された文書画像をまっすぐにする(台形補正)といった用途によく使われます。
これらの幾何学変換を組み合わせることで、画像の見た目を様々に変化させることができます。
画像のフィルタリング:画像に「フィルター」をかける
画像フィルタリングは、画像の各ピクセルとその周辺のピクセル(局所領域)の値を参照して、新しいピクセル値を計算する処理です。これにより、ノイズの除去、エッジの強調、画像のぼかしなど、様々な効果を得ることができます。
フィルタリングの中心となる概念は 畳み込み (Convolution) です。畳み込みは、小さな行列である カーネル (Kernel) または フィルタ (Filter) を画像の上でスライドさせながら、カーネルの要素と画像データの対応する要素の積の和を計算し、その結果を新しい画像のピクセル値とする操作です。
1. 畳み込みとは(簡単な説明)
例として、3×3のカーネルを使った畳み込みを考えます。
“`
カーネル:
[ k11 k12 k13 ]
[ k21 k22 k23 ]
[ k31 k32 k33 ]
画像の一部:
[ p11 p12 p13 ]
[ p21 p22 p23 ]
[ p31 p32 p33 ]
``p22` に重なっていると仮定)
(カーネルの中心が画像上のピクセル
このとき、p22 に対応する新しい画像でのピクセル値は、以下の計算で求められます。
新しい p22 = k11*p11 + k12*p12 + k13*p13 + k21*p21 + k22*p22 + k23*p23 + k31*p31 + k32*p32 + k33*p33
この計算を、カーネルを画像全体にわたって1ピクセルずつ(または指定したストライドで)スライドさせながら行います。
カーネルの各要素の値によって、画像に様々な効果をもたらすことができます。OpenCVには、特定の効果を持つカーネルを使ったフィルタリング関数が多数用意されています。
2. 平滑化(ノイズ除去)フィルタ
画像のノイズを除去し、全体を滑らかにするためのフィルタです。主に平均フィルタやガウシアンフィルタが使われます。
a) 平均フィルタ (cv2.blur(), cv2.boxFilter())
カーネル内の全ピクセル値の平均を計算し、その値で中心ピクセルを置き換えるフィルタです。単純な平均化により、局所的なノイズを周囲の値にならして目立たなくする効果があります。
“`python
import cv2
import numpy as np
image_path = ‘input.jpg’ # ノイズの多い画像を使うと効果が分かりやすい
img = cv2.imread(image_path)
if img is not None:
# カーネルサイズを指定 (例: 5×5)
kernel_size = (5, 5)
# 平均フィルタ
# cv2.blur(入力画像, カーネルサイズ)
avg_blurred_img = cv2.blur(img, kernel_size)
cv2.imshow('Original Image', img)
cv2.imshow(f'Average Filter (Kernel {kernel_size})', avg_blurred_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.blur(src, ksize): 画像srcにksizeで指定されたサイズのカーネルで平均フィルタを適用します。ksizeは(幅, 高さ)のタプルで指定します。
b) ガウシアンフィルタ (cv2.GaussianBlur())
平均フィルタに似ていますが、カーネルの中心に近いピクセルほど重み(影響力)が大きくなるように計算されます。重み付けには正規分布(ガウス分布)が用いられます。これにより、平均フィルタよりも自然なぼかし効果が得られ、ノイズ除去と画像の詳細さの維持のバランスが良いとされています。
“`python
上記のコードに続けて実行
if img is not None:
# カーネルサイズを指定 (例: 5×5)。奇数である必要がある。
kernel_size = (5, 5)
# 標準偏差 σ を指定 (0を指定すると、カーネルサイズから自動計算される)
sigmaX = 0 # X方向の標準偏差
sigmaY = 0 # Y方向の標準偏差 (通常は sigmaX と同じか 0)
# ガウシアンフィルタ
# cv2.GaussianBlur(入力画像, カーネルサイズ, sigmaX, sigmaY=0, borderType=cv2.BORDER_DEFAULT)
gaussian_blurred_img = cv2.GaussianBlur(img, kernel_size, sigmaX, sigmaY)
cv2.imshow('Original Image', img)
cv2.imshow(f'Gaussian Filter (Kernel {kernel_size}, SigmaX {sigmaX})', gaussian_blurred_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.GaussianBlur(src, ksize, sigmaX, sigmaY=0, borderType=cv2.BORDER_DEFAULT): 画像srcにガウシアンフィルタを適用します。ksizeはカーネルサイズ(幅, 高さ)のタプルで、両方とも正の奇数であるか、両方とも0である必要があります。sigmaX,sigmaYはガウス分布の標準偏差です。通常はsigmaXだけ指定し、sigmaYは0とします(sigmaYはsigmaXと等しく設定されます)。sigmaX,sigmaYを0にすると、ksizeから自動的に計算されます。
c) メディアンフィルタ (cv2.medianBlur())
カーネル内のピクセル値の中央値(メディアン)を計算し、その値で中心ピクセルを置き換えるフィルタです。これは畳み込みではなく、非線形フィルタリングの一種です。特に、画像に飛び飛びの値を持つノイズ(塩胡椒ノイズ)が乗っている場合に非常に効果的です。メディアンフィルタはエッジを比較的よく保存する特性も持っています。
“`python
上記のコードに続けて実行
if img is not None:
# カーネルサイズを指定 (例: 5)。正の奇数である必要がある。
# メディアンフィルタのカーネルサイズは、通常、正方カーネルの1辺の長さで指定
kernel_size = 5
# メディアンフィルタ
# cv2.medianBlur(入力画像, カーネルサイズ)
median_blurred_img = cv2.medianBlur(img, kernel_size)
cv2.imshow('Original Image', img)
cv2.imshow(f'Median Filter (Kernel Size {kernel_size})', median_blurred_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.medianBlur(src, ksize): 画像srcにメディアンフィルタを適用します。ksizeはカーネルの1辺の長さ(正の奇数)を指定します。
d) バイラテラルフィルタ (cv2.bilateralFilter())
エッジを保存しながらノイズを除去したい場合に非常に有効なフィルタです。バイラテラルフィルタは、ピクセルの空間的な近さだけでなく、ピクセル値(色の違いや明るさの違い)の類似性も考慮して重みを計算します。つまり、近くにあっても色が大きく異なるピクセル(エッジ部分)には小さな重みをつけ、近くにあり色が似ているピクセル(平坦な領域)には大きな重みをつけることで、エッジをぼかさずにノイズだけを除去しようとします。
“`python
上記のコードに続けて実行
if img is not None:
# フィルタリングのパラメータを指定
d = 9 # フィルタリングに使用する近傍ピクセルの直径。負の値にするとsigmaSpaceから自動計算。
sigmaColor = 75 # 色の標準偏差。値が大きいほど、より離れた色も考慮してぼかす。
sigmaSpace = 75 # 空間の標準偏差。値が大きいほど、より遠くのピクセルも考慮してぼかす。
# バイラテラルフィルタ
# cv2.bilateralFilter(入力画像, 近傍直径, 色の標準偏差, 空間の標準偏差)
bilateral_filtered_img = cv2.bilateralFilter(img, d, sigmaColor, sigmaSpace)
cv2.imshow('Original Image', img)
cv2.imshow(f'Bilateral Filter (d={d}, sc={sigmaColor}, ss={sigmaSpace})', bilateral_filtered_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace, borderType=cv2.BORDER_DEFAULT): 画像srcにバイラテラルフィルタを適用します。dはフィルタリングに使用する近傍ピクセルの直径です。sigmaColorは色の空間での標準偏差、sigmaSpaceは座標空間での標準偏差です。これらのパラメータは試行錯誤して最適な値を見つけることが多いです。
バイラテラルフィルタは計算コストが比較的高いですが、高画質でエッジを維持したままノイズ除去を行いたい場合に有効です。
3. エッジ検出フィルタ
画像中の輝度値が急激に変化する場所、すなわち エッジ を検出するためのフィルタです。エッジは物体の輪郭や境界を示す重要な特徴量です。
a) Sobelフィルタ (cv2.Sobel())
画像の輝度勾配(明るさの変化率)を計算することでエッジを検出します。水平方向の勾配と垂直方向の勾配を別々に計算し、それらを組み合わせて最終的なエッジ強度や方向を求めます。
“`python
import cv2
import numpy as np
image_path = ‘input.jpg’
img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) # グレースケール画像で実行するのが一般的
if img is not None:
# 勾配を計算する方向とカーネルサイズを指定
# dx: X方向の微分次数 (1なら1次微分)
# dy: Y方向の微分次数 (1なら1次微分)
# ksize: Sobelカーネルのサイズ (1, 3, 5, 7のいずれか)
ksize = 3
# X方向の勾配を計算
# cv2.Sobel(入力画像, 出力画像の深さ, dx, dy, ksize)
# 出力画像の深さには cv2.CV_64F (64ビット浮動小数点型) を指定するのが一般的。
# 勾配計算結果は負の値になることがあるため、uint8では表現できない。
sobelx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=ksize)
# Y方向の勾配を計算
sobely = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=ksize)
# 勾配画像をuint8形式に戻す
# 勾配値の絶対値を取り、0-255の範囲にスケーリングする
sobelx_abs = cv2.convertScaleAbs(sobelx) # スケール変換と絶対値計算を同時に行う
sobely_abs = cv2.convertScaleAbs(sobely)
# X方向とY方向の勾配を合成してエッジ強度を計算 (L2ノルムまたはL1ノルム)
# ここではL2ノルムの近似(sqrt(x^2 + y^2))を使う関数 cv2.addWeighted もあるが、
# 単純な加算(L1ノルムの近似)でもエッジ強度がわかる
# sobel_combined = cv2.addWeighted(sobelx_abs, 0.5, sobely_abs, 0.5, 0) # 加重平均
sobel_combined = cv2.add(sobelx_abs, sobely_abs) # 単純加算
cv2.imshow('Original Grayscale Image', img)
cv2.imshow(f'Sobel X ({ksize})', sobelx_abs)
cv2.imshow(f'Sobel Y ({ksize})', sobely_abs)
cv2.imshow(f'Sobel Combined ({ksize})', sobel_combined)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.Sobel(src, ddepth, dx, dy, ksize): 画像srcに対してSobelフィルタを適用します。ddepthは出力画像のデータ型で、負の値も扱えるcv2.CV_64Fが推奨されます。dx,dyはX方向とY方向の微分次数で、通常はどちらか一方を1、もう一方を0とします。ksizeはカーネルサイズです。cv2.convertScaleAbs(src): 入力画像srcの各要素の絶対値を計算し、結果をuint8型にスケーリングして変換します。Sobelフィルタの出力(負の値や255を超える値を含む)を画像として表示するために必要です。
Sobelフィルタはエッジの存在とその方向を捉えることができます。
b) Scharrフィルタ (cv2.Scharr())
Sobelフィルタに似ていますが、より高い精度で勾配を計算できるフィルタです。通常、Sobelフィルタの ksize=3 の場合に代わりにScharrフィルタを使用できます。
“`python
上記のコードに続けて実行
if img is not None:
# Scharrフィルタはksize=3のSobelより精度が高い
# dx, dy は Sobel と同様に 1, 0 または 0, 1
# X方向の勾配を計算
scharrx = cv2.Scharr(img, cv2.CV_64F, 1, 0)
# Y方向の勾配を計算
scharry = cv2.Scharr(img, cv2.CV_64F, 0, 1)
# 勾配画像をuint8形式に戻す
scharrx_abs = cv2.convertScaleAbs(scharrx)
scharry_abs = cv2.convertScaleAbs(scharry)
# 合成
scharr_combined = cv2.add(scharrx_abs, scharry_abs)
cv2.imshow('Original Grayscale Image', img)
cv2.imshow('Scharr X', scharrx_abs)
cv2.imshow('Scharr Y', scharry_abs)
cv2.imshow('Scharr Combined', scharr_combined)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.Scharr(src, ddepth, dx, dy): 画像srcに対してScharrフィルタを適用します。ksizeは指定しません(内部的に3×3カーネルが使用されます)。
c) Laplacianフィルタ (cv2.Laplacian())
画像の2次微分を計算することで、エッジの位置(輝度変化が最大となる場所)を検出するフィルタです。単独で使用すると両方向のエッジを検出できますが、ノイズに敏感なため、通常は事前に平滑化処理を施してから使用します。
“`python
上記のコードに続けて実行
if img is not None:
# ラプラシアンフィルタ
# cv2.Laplacian(入力画像, 出力画像の深さ, ksize=1)
# ksizeはSobelと同様に正の奇数、デフォルトは1 (3×3カーネル)
laplacian = cv2.Laplacian(img, cv2.CV_64F, ksize=3)
# 結果をuint8に戻す
laplacian_abs = cv2.convertScaleAbs(laplacian)
cv2.imshow('Original Grayscale Image', img)
cv2.imshow(f'Laplacian ({ksize})', laplacian_abs)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.Laplacian(src, ddepth, ksize=1, scale=1, delta=0, borderType=cv2.BORDER_DEFAULT): 画像srcに対してラプラシアンフィルタを適用します。ksizeはカーネルサイズです。
d) Cannyエッジ検出 (cv2.Canny())
画像処理で最も広く使われているエッジ検出アルゴリズムの一つです。ジョン・キャニーによって提案され、多くの段階を経て高精度なエッジを検出します。その主な手順は以下の通りです。
- ガウス平滑化: 画像のノイズを除去します。
- 輝度勾配の計算: Sobelフィルタなどを使って、画像の輝度勾配(エッジの強度と方向)を計算します。
- 非最大抑制 (Non-maximum Suppression): 勾配方向に対して、エッジ強度が局所的な最大値であるピクセルのみを残し、それ以外をゼロにします。これにより、エッジが細くなります(細線化)。
- ヒステリシス閾値処理: 2つの閾値(上限閾値と下限閾値)を使って、最終的なエッジを決定します。
- 上限閾値よりも強い勾配を持つピクセルは、明確なエッジとして確定します。
- 下限閾値よりも弱い勾配を持つピクセルは、エッジではないとみなされ破棄されます。
- 下限閾値と上限閾値の間にあるピクセルは、明確なエッジ(上限閾値を超えたピクセル)に連結している場合にのみ、エッジとして採用されます。これにより、エッジの連続性が保たれます。
“`python
上記のコードに続けて実行
if img is not None:
# Cannyエッジ検出
# cv2.Canny(入力画像, 下限閾値, 上限閾値, apertureSize=3, L2gradient=False)
# 下限閾値と上限閾値の比率は 1:2 または 1:3 が推奨されることが多い
threshold1 = 100 # 下限閾値
threshold2 = 200 # 上限閾値
# apertureSize: Sobelフィルタのカーネルサイズ (3, 5, 7)
# L2gradient: 勾配計算方法 (True: L2ノルム, False: L1ノルム近似)
canny_edges = cv2.Canny(img, threshold1, threshold2)
cv2.imshow('Original Grayscale Image', img)
cv2.imshow(f'Canny Edges (T1={threshold1}, T2={threshold2})', canny_edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.Canny(image, threshold1, threshold2, apertureSize=3, L2gradient=False): 画像imageに対してCannyエッジ検出を適用し、二値のエッジ画像(エッジが255、それ以外が0)を返します。threshold1は下限閾値、threshold2は上限閾値です。apertureSizeは内部で使われるSobelフィルタのカーネルサイズ、L2gradientは勾配計算方法を制御します。
Cannyエッジ検出は、他のフィルタリング手法と組み合わせることで、より高精度なエッジ情報を取得することができます。
閾値処理:画像を単純な白黒に分ける
閾値処理(二値化)は、画像を前景と背景の2つの領域に分けるための最も単純な画像処理手法の一つです。グレースケール画像に対して行われるのが一般的です。ある閾値(しきいち)を定め、その閾値よりも明るいピクセルは白(または最大値)に、暗いピクセルは黒(または最小値)に変換します。
これは、画像の中から特定の対象物だけを抽出したり、後の処理を簡略化したりするのに非常に有効です。
1. 単純な閾値処理 (cv2.threshold())
画像全体に対して一つの固定された閾値を適用する方法です。
“`python
import cv2
import numpy as np
image_path = ‘input.jpg’
img_gray = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) # グレースケール画像で実行
if img_gray is not None:
# 閾値を設定 (例: 127)
threshold_value = 127
# cv2.threshold(入力画像, 閾値, 最大値, 閾値処理タイプ)
# 戻り値は (適用された閾値, 処理結果の画像) のタプル
# 閾値処理タイプについて:
# cv2.THRESH_BINARY: ピクセル値 > 閾値 なら 最大値, そうでなければ 0
# cv2.THRESH_BINARY_INV: ピクセル値 > 閾値 なら 0, そうでなければ 最大値
# cv2.THRESH_TRUNC: ピクセル値 > 閾値 なら 閾値, そうでなければ 元の値
# cv2.THRESH_TOZERO: ピクセル値 > 閾値 なら 元の値, そうでなければ 0
# cv2.THRESH_TOZERO_INV: ピクセル値 > 閾値 なら 0, そうでなければ 元の値
# 二値化 (BINARYタイプ)
ret, binary_img = cv2.threshold(img_gray, threshold_value, 255, cv2.THRESH_BINARY)
# 二値化 (BINARY_INVタイプ)
ret, binary_inv_img = cv2.threshold(img_gray, threshold_value, 255, cv2.THRESH_BINARY_INV)
# ret は cv2.THRESH_OTSU や cv2.THRESH_TRIANGLE タイプを使った場合に計算された閾値が返される。
# その他のタイプでは入力した閾値がそのまま返されることが多い。
print(f"適用された閾値 (BINARY): {ret}")
cv2.imshow('Original Grayscale Image', img_gray)
cv2.imshow(f'Binary Threshold (>{threshold_value})', binary_img)
cv2.imshow(f'Binary Inverse Threshold (>{threshold_value})', binary_inv_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.threshold(src, thresh, maxval, type): 画像srcに対して閾値処理を行います。threshは閾値、maxvalは二値化された場合の最大値(通常は255)、typeは閾値処理のタイプを指定します。- 戻り値の
retは、使用した閾値です。cv2.THRESH_OTSUやcv2.THRESH_TRIANGLEといった特別なタイプを指定すると、OpenCVが自動的に最適な閾値を計算してくれます。これらのタイプを使う場合は、手動で設定する閾値threshは0を指定し、typeにTHRESH_BINARY | THRESH_OTSUのようにビットORで結合して指定します。
2. 適応的閾値処理 (cv2.adaptiveThreshold())
画像全体の明るさが均一でない場合、一つの固定閾値ではうまくいかないことがあります。例えば、画像の左側が明るく、右側が暗いような場合、左側に合わせた閾値では右側の対象物が見えなくなったり、右側に合わせた閾値では左側に不要なノイズが現れたりします。
適応的閾値処理は、画像を小さな領域に分割し、各領域ごとにその局所的な特徴に基づいて閾値を計算し適用します。これにより、画像全体の明るさの変化に対応した二値化が可能になります。
“`python
上記のコードに続けて実行
if img_gray is not None:
# 適応的閾値処理
# cv2.adaptiveThreshold(入力画像, 最大値, 適応的方法, 閾値処理タイプ, ブロックサイズ, 定数C)
# 適応的方法について:
# cv2.ADAPTIVE_THRESH_MEAN_C: 近傍領域の平均値を閾値とする
# cv2.ADAPTIVE_THRESH_GAUSSIAN_C: 近傍領域の画素値にガウス分布の重みをつけて計算した加重平均値を閾値とする
# ブロックサイズ: 閾値を計算する近傍領域のサイズ (正の奇数)
# 定数C: 計算された平均値または加重平均値から差し引かれる値 (この値を調整することで、閾値を厳しくしたり緩やかにしたりできる)
# 例: 近傍領域11x11、定数C=2
block_size = 11
C = 2
# 平均値に基づく適応的閾値処理
adaptive_mean_thresh = cv2.adaptiveThreshold(img_gray, 255,
cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY, block_size, C)
# ガウス加重平均に基づく適応的閾値処理
adaptive_gaussian_thresh = cv2.adaptiveThreshold(img_gray, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, block_size, C)
cv2.imshow('Original Grayscale Image', img_gray)
cv2.imshow(f'Adaptive Mean Threshold (Block {block_size}, C {C})', adaptive_mean_thresh)
cv2.imshow(f'Adaptive Gaussian Threshold (Block {block_size}, C {C})', adaptive_gaussian_thresh)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.adaptiveThreshold(src, maxval, adaptiveMethod, thresholdType, blockSize, C): 画像srcに対して適応的閾値処理を行います。maxvalは最大値、adaptiveMethodは閾値の計算方法、thresholdTypeは閾値処理のタイプ(通常はcv2.THRESH_BINARYまたはcv2.THRESH_BINARY_INV)、blockSizeは近傍領域のサイズ(奇数)、Cは平均値または加重平均値から差し引く定数です。
適応的閾値処理は、スキャンされた文書画像のように、影や照明のムラがある場合に有効です。
閾値処理は、画像中の対象物を抽出し、次の処理(例:輪郭検出)に進むための重要な前処理ステップとなります。
モルフォロジー変換:画像の形状を変化させる
モルフォロジー変換(形態学的変換)は、画像中の前景領域(通常は白)の形状に基づいてオブジェクトを加工する画像処理手法です。主に二値画像に対して適用されますが、グレースケール画像に対しても応用できます。これらの変換は、ノイズの除去、オブジェクトの分離や結合、穴埋めなどに利用されます。
モルフォロジー変換の基本的な操作は、膨張 (Dilation) と 収縮 (Erosion) です。これらの操作は、構造要素 (Structuring Element) と呼ばれるカーネル(マスク)を用いて行われます。構造要素は、通常、矩形、楕円、十字形などの形状をしています。
1. 膨張 (Dilation) (cv2.dilate())
画像の前景領域(白い部分)を「膨らませる」操作です。構造要素を画像上でスライドさせ、構造要素内のどこか一つでも前景ピクセルがあれば、その構造要素の中心に対応する出力画像のピクセルを前景ピクセル(白)にします。これにより、前景領域のサイズが大きくなり、欠けている部分が埋められたり、隣接する前景領域が結合されたりします。
“`python
import cv2
import numpy as np
二値画像を読み込むか、作成する
ここではサンプルとして簡単な二値画像を生成
真っ黒な画像に白い矩形と点ノイズを描画する
img_size = (200, 200)
img_binary = np.zeros(img_size, dtype=np.uint8)
cv2.rectangle(img_binary, (50, 50), (150, 150), 255, -1) # 白い矩形
cv2.circle(img_binary, (20, 20), 3, 255, -1) # 白い点ノイズ
cv2.circle(img_binary, (180, 180), 3, 255, -1) # 白い点ノイズ
真っ白な背景に黒い点ノイズや穴がある画像でも試すと面白いです
構造要素を定義 (例: 5×5の矩形)
kernel_size = (5, 5)
cv2.getStructuringElement(形状, サイズ)
形状: cv2.MORPH_RECT (矩形), cv2.MORPH_ELLIPSE (楕円), cv2.MORPH_CROSS (十字)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, kernel_size)
膨張処理
cv2.dilate(入力画像, 構造要素, iterations=1)
iterations: 膨張処理を繰り返す回数 (デフォルトは1)
dilated_img = cv2.dilate(img_binary, kernel, iterations=1)
cv2.imshow(‘Original Binary Image’, img_binary)
cv2.imshow(f’Dilated Image (Kernel {kernel_size})’, dilated_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.getStructuringElement(shape, ksize): 指定されたshapeとksizeを持つ構造要素(NumPy配列)を作成します。cv2.dilate(src, kernel, iterations=1, borderType=cv2.BORDER_CONSTANT, borderValue=0): 画像srcに構造要素kernelを用いて膨張処理を適用します。iterationsは処理を繰り返す回数です。
2. 収縮 (Erosion) (cv2.erode())
画像の前景領域(白い部分)を「縮小させる」操作です。構造要素を画像上でスライドさせ、構造要素内の全てのピクセルが前景ピクセルである場合にのみ、その構造要素の中心に対応する出力画像のピクセルを前景ピクセル(白)にします。構造要素内に一つでも背景ピクセル(黒)が含まれていれば、中心ピクセルは背景ピクセル(黒)になります。これにより、前景領域のサイズが小さくなり、小さなノイズが除去されたり、隣接する前景領域が分離されたり、細い線が消えたりします。
“`python
上記のコードに続けて実行
収縮処理
cv2.erode(入力画像, 構造要素, iterations=1)
eroded_img = cv2.erode(img_binary, kernel, iterations=1)
cv2.imshow(‘Original Binary Image’, img_binary)
cv2.imshow(f’Eroded Image (Kernel {kernel_size})’, eroded_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.erode(src, kernel, iterations=1, borderType=cv2.BORDER_CONSTANT, borderValue=0): 画像srcに構造要素kernelを用いて収縮処理を適用します。
3. オープニング (Opening)
オープニングは、収縮の後に膨張を行う 処理です。収縮 → 膨張 の順で行います。これにより、前景にある小さなノイズ(収縮によって除去される)を除去しつつ、オブジェクトのサイズや形状への影響を最小限に抑えることができます。例えるなら、「細い首で繋がったコブ」のコブだけを取り除くような効果があります。
OpenCVでは、cv2.morphologyEx() 関数を使って様々なモルフォロジー変換を指定できます。
“`python
上記のコードに続けて実行
オープニング処理
cv2.morphologyEx(入力画像, 操作の種類, 構造要素)
操作の種類: cv2.MORPH_OPEN (オープニング)
opening_img = cv2.morphologyEx(img_binary, cv2.MORPH_OPEN, kernel)
cv2.imshow(‘Original Binary Image’, img_binary)
cv2.imshow(f’Opening (Kernel {kernel_size})’, opening_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.morphologyEx(src, op, kernel, iterations=1, borderType=cv2.BORDER_CONSTANT, borderValue=0): 画像srcに対して、opで指定されたモルフォロジー変換を構造要素kernelを用いて行います。opにcv2.MORPH_OPENを指定するとオープニングになります。
4. クロージング (Closing)
クロージングは、膨張の後に収縮を行う 処理です。膨張 → 収縮 の順で行います。これにより、前景オブジェクトの内部にある小さな穴や、オブジェクト間の狭い隙間を埋めることができます。例えるなら、「切れ込みのあるオブジェクト」の切れ込みを繋いで一体化するような効果があります。
“`python
上記のコードに続けて実行
クロージング処理
操作の種類: cv2.MORPH_CLOSE (クロージング)
closing_img = cv2.morphologyEx(img_binary, cv2.MORPH_CLOSE, kernel)
cv2.imshow(‘Original Binary Image’, img_binary)
cv2.imshow(f’Closing (Kernel {kernel_size})’, closing_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
opにcv2.MORPH_CLOSEを指定するとクロージングになります。
オープニングとクロージングは、二値画像のノイズ除去や形状の補正によく使われるペアとなる操作です。他にも、cv2.morphologyEx() 関数では、Gradient (膨張と収縮の差分)、Top Hat (元の画像とオープニングの差分)、Black Hat (クロージングと元の画像の差分) といった様々なモルフォロジー変換を行うことができます。
モルフォロジー変換は、二値画像からオブジェクトの基本的な構造を取り出したり、不要な要素を除去したりする際に非常に役立つツールです。
輪郭の検出と描画:オブジェクトの形を見つける
画像処理において、オブジェクトの境界線を検出することは非常に重要なタスクです。この境界線は 輪郭 (Contours) と呼ばれます。OpenCVは、画像中の輪郭を効率的に検出・取得するための強力な機能を提供しています。
輪郭検出は、オブジェクト認識、形状分析、物体追跡、画像分割など、様々な応用分野で利用されます。
1. 輪郭とは何か
輪郭は、同じ色や輝度を持つ連続した点(またはピクセル)を結んだ曲線のリストです。通常、背景から明確に分離されたオブジェクト(二値画像の前景)の境界を表現します。OpenCVで輪郭検出を行う際は、一般的にグレースケール画像または二値画像を入力として使用します。
2. 輪郭の検出 (cv2.findContours())
cv2.findContours() 関数は、画像中の輪郭を検出し、検出された輪郭をリストとして返します。
“`python
import cv2
import numpy as np
輪郭検出のための二値画像を準備
例: 白い図形がいくつか描かれた黒い画像
img_size = (300, 300)
img_binary = np.zeros(img_size, dtype=np.uint8)
cv2.rectangle(img_binary, (50, 50), (150, 150), 255, -1) # 矩形
cv2.circle(img_binary, (220, 100), 40, 255, -1) # 円
cv2.putText(img_binary, ‘OpenCV’, (10, 250), cv2.FONT_HERSHEY_SIMPLEX, 1, 255, 2) # テキスト
注意: findContoursは背景が黒、前景が白の二値画像で最も効果的に機能します。
白い背景に黒いオブジェクトの場合、白黒反転(cv2.bitwise_not)が必要になることがあります。
img_binary をコピーしておく(findContoursは入力画像を変更する可能性があるため)
img_binary_copy = img_binary.copy()
輪郭の検出
cv2.findContours(入力画像, 取得モード, 近似手法)
入力画像: 8ビットのグレースケール画像または二値画像。関数実行後、入力画像は変更される可能性があるため注意が必要。
取得モード (mode): 輪郭の階層構造をどのように取得するか
cv2.RETR_EXTERNAL: 最も外側の輪郭のみを取得
cv2.RETR_LIST: 全ての輪郭を取得し、階層構造は無視
cv2.RETR_CCOMP: 全ての輪郭を取得し、2レベルの階層構造 (外側の輪郭とその内側の穴の輪郭) を構築
cv2.RETR_TREE: 全ての輪郭を取得し、完全な階層構造 (木構造) を構築
近似手法 (method): 輪郭の形状をどのように近似するか
cv2.CHAIN_APPROX_NONE: 輪郭上の全ての点を格納する (メモリ消費大)
cv2.CHAIN_APPROX_SIMPLE: 水平、垂直、斜めの線分を圧縮し、端点のみを格納する (メモリ消費小)
戻り値: (輪郭リスト, 階層情報) または (画像, 輪郭リスト, 階層情報) – OpenCVのバージョンによって異なる
一般的なOpenCV 4.x では (輪郭リスト, 階層情報) が返されます
contours, hierarchy = cv2.findContours(img_binary_copy, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
print(f”検出された輪郭の数: {len(contours)}”)
contours は NumPy配列のリストです。各要素が1つの輪郭を表します。
各輪郭は、輪郭上の点群 (x, y) のリストとして格納されています。
hierarchy は 輪郭の階層情報を持つ NumPy配列です。
各行が1つの輪郭に対応し、[次の輪郭のインデックス, 前の輪郭のインデックス, 親の輪郭のインデックス, 子の輪郭のインデックス]
対応する輪郭がない場合は -1 が格納されます。
“`
cv2.findContours(image, mode, method): 画像imageから輪郭を検出します。戻り値のcontoursは検出された輪郭のリストで、各輪郭は点のNumPy配列です。hierarchyは輪郭の階層構造を表すNumPy配列です。
3. 輪郭の描画 (cv2.drawContours())
検出された輪郭を画像上に描画することができます。
“`python
上記のコードに続けて実行
輪郭を描画するためのカラー画像を作成 (元の二値画像は描画には向かない)
通常は、元の画像や真っ白な画像などに描画する
img_contours = np.zeros_like(img_binary_copy, dtype=np.uint8) # 元の二値画像と同じサイズの真っ黒な画像
または元のカラー画像に描画したい場合: cv2.imread(‘input.jpg’) を読み込んで使う
cv2.drawContours(描画対象の画像, 輪郭リスト, 描画する輪郭のインデックス, 色, 線の太さ)
輪郭リスト: cv2.findContours() の戻り値である contours
描画する輪郭のインデックス: 描画したい輪郭のリスト内のインデックスを指定。-1 を指定すると全ての輪郭を描画。
色: 線の色 (BGR形式のタプル)。グレースケール画像に描画する場合は単一の輝度値 (例: 255)
線の太さ: 線のピクセル単位の太さ。正の値は太さ、-1 (または cv2.FILLED) は内部を塗りつぶす。
全ての輪郭を白 (255) で描画
cv2.drawContours(img_contours, contours, -1, 255, 2) # グレースケール画像に描画するので色指定は単一値
cv2.imshow(‘Original Binary Image’, img_binary)
cv2.imshow(‘Contours Drawn’, img_contours)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
cv2.drawContours(image, contours, contourIdx, color, thickness, lineType=cv2.LINE_8, hierarchy=None, maxLevel=None, offset=None): 画像image上に、contoursリストから指定されたcontourIdxの輪郭を描画します。colorは描画色、thicknessは線の太さです。
4. 個別輪郭の描画とプロパティ
特定の輪郭だけを描画したり、検出された各輪郭のプロパティ(面積、周囲長、バウンディングボックスなど)を取得したりすることも可能です。
“`python
上記のコードに続けて実行
if contours: # 輪郭が検出された場合
# 個別の輪郭を描画してみる (例: 最初の輪郭)
img_first_contour = np.zeros_like(img_binary_copy, dtype=np.uint8)
cv2.drawContours(img_first_contour, contours, 0, 255, 2) # 最初の輪郭だけ描画 (インデックス0)
cv2.imshow(‘First Contour Only’, img_first_contour)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 輪郭のプロパティを取得
print("\n--- 輪郭のプロパティ ---")
for i, cnt in enumerate(contours):
print(f"\n輪郭 {i}:")
# 面積の計算
area = cv2.contourArea(cnt)
print(f" 面積: {area}")
# 周囲長(弧長)の計算
# closed=True は輪郭が閉じているか (通常は閉じてる)
perimeter = cv2.arcLength(cnt, closed=True)
print(f" 周囲長: {perimeter}")
# 外接する直線の矩形 (Bounding Rectangle)
# cv2.boundingRect() は (x, y, width, height) のタプルを返す
x, y, w, h = cv2.boundingRect(cnt)
print(f" バウンディングボックス (x, y, w, h): ({x}, {y}, {w}, {h})")
# バウンディングボックスを描画してみる
img_bbox = np.zeros_like(img_binary_copy, dtype=np.uint8)
cv2.drawContours(img_bbox, contours, i, 255, 1) # 元の輪郭も一緒に描画
cv2.rectangle(img_bbox, (x, y), (x+w, y+h), 150, 2) # バウンディングボックスをグレーで描画
cv2.imshow(f'Contour {i} with Bounding Box', img_bbox)
cv2.waitKey(0)
# 外接する回転を考慮した矩形 (Rotated Bounding Rectangle)
# cv2.minAreaRect() は ((中心x, 中心y), (幅, 高さ), 角度) のタプルを返す
rect = cv2.minAreaRect(cnt)
print(f" 回転を考慮したバウンディングボックス: {rect}")
# 矩形の4つの角を計算
box = cv2.boxPoints(rect) # float32の配列として角の座標を取得
box = np.intp(box) # 整数に変換
# 回転矩形を描画してみる
img_rotated_bbox = np.zeros_like(img_binary_copy, dtype=np.uint8)
cv2.drawContours(img_rotated_bbox, [box], 0, 255, 2) # 回転矩形は1つの輪郭として描画
cv2.imshow(f'Contour {i} with Rotated Bounding Box', img_rotated_bbox)
cv2.waitKey(0)
# 最小外接円
# cv2.minEnclosingCircle() は ((中心x, 中心y), 半径) のタプルを返す
(center_x, center_y), radius = cv2.minEnclosingCircle(cnt)
center = (int(center_x), int(center_y))
radius = int(radius)
print(f" 最小外接円 (中心, 半径): ({center_x:.2f}, {center_y:.2f}), {radius:.2f}")
# 最小外接円を描画してみる
img_min_circle = np.zeros_like(img_binary_copy, dtype=np.uint8)
cv2.drawContours(img_min_circle, contours, i, 255, 1) # 元の輪郭も一緒に描画
cv2.circle(img_min_circle, center, radius, 150, 2) # 円をグレーで描画
cv2.imshow(f'Contour {i} with Minimum Enclosing Circle', img_min_circle)
cv2.waitKey(0)
# 近似多角形 (輪郭をより少ない頂点を持つ多角形で近似)
# cv2.approxPolyDP(輪郭, ε (元の輪郭と近似多角形間の最大距離), closed)
# ε は近似精度を指定するパラメータ。元の輪郭の周囲長の数%程度を指定することが多い
epsilon = 0.01 * cv2.arcLength(cnt, True) # 周囲長の1%をεとする
approx = cv2.approxPolyDP(cnt, epsilon, True)
print(f" 近似多角形の頂点数: {len(approx)}")
# 近似多角形を描画してみる
img_approx_poly = np.zeros_like(img_binary_copy, dtype=np.uint8)
cv2.drawContours(img_approx_poly, [approx], 0, 255, 2) # 近似多角形は1つの輪郭として描画
cv2.imshow(f'Contour {i} Approximate Polygon ({len(approx)} vertices)', img_approx_poly)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
輪郭のプロパティを取得することで、検出したオブジェクトのサイズ、形状、位置などを定量的に分析することが可能になります。例えば、面積でフィルタリングして特定のサイズのオブジェクトだけを選び出したり、近似多角形の頂点数で形状を分類したりすることができます。
輪郭検出は、画像処理パイプラインの非常に重要なステップであり、後の物体認識や解析の基盤となります。
(発展)ビデオ処理の基本:動画像を扱ってみよう
これまでは静止画像を中心に扱ってきましたが、OpenCVは動画(ビデオファイルやカメラからのストリーム)を扱う機能も充実しています。動画は連続する静止画像(フレーム)の集まりとして扱われます。
1. ビデオファイルの読み込み (cv2.VideoCapture())
ビデオファイル(例: .mp4, .avi など)を読み込むには cv2.VideoCapture() を使用します。
“`python
import cv2
ビデオファイルのパスを指定
video_path = ‘input_video.mp4’ # 存在しない場合は、サンプルビデオなどを準備してください
ビデオファイルをオープン
cv2.VideoCapture(ファイルパス) または cv2.VideoCapture(カメラID)
cap = cv2.VideoCapture(video_path)
ビデオファイルが正しくオープンできたか確認
if not cap.isOpened():
print(f”エラー: ビデオファイル ‘{video_path}’ を開けませんでした。パスを確認してください。”)
else:
# ビデオの情報を取得
# cap.get(プロパティID)
# プロパティIDの例:
# cv2.CAP_PROP_FRAME_COUNT: 総フレーム数
# cv2.CAP_PROP_FPS: フレームレート (1秒あたりのフレーム数)
# cv2.CAP_PROP_FRAME_WIDTH: フレームの幅
# cv2.CAP_PROP_FRAME_HEIGHT: フレームの高さ
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS)
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(f"ビデオファイル '{video_path}' を開きました。")
print(f" 総フレーム数: {frame_count}")
print(f" フレームレート (FPS): {fps}")
print(f" フレームサイズ: {frame_width}x{frame_height}")
# フレームを1枚ずつ読み込んで表示するループ
while True:
# フレームを1枚読み込む
# cap.read() は (読み込み成功フラグ (bool), フレームデータ (NumPy配列)) のタプルを返す
ret, frame = cap.read()
# フレームが正しく読み込めなかった(動画の終わりなど)場合はループを抜ける
if not ret:
print("ビデオの終わりに到達しました。")
break
# 読み込んだフレームを表示
cv2.imshow('Video Frame', frame)
# キー入力を待つ (例えば 'q' キーで終了)
# cv2.waitKey(1) は約1ミリ秒待つ。この間にキーが押されるとそのキーのASCIIコードを返す。
# フレームレートに合わせて待機時間を調整することも可能: wait_time = int(1000 / fps)
key = cv2.waitKey(25) # 25ミリ秒待つ (約40 FPSに相当)
if key == ord('q'): # 'q' キーが押されたら終了
print("'q' キーが押されました。終了します。")
break
# キャプチャオブジェクトを解放する
cap.release()
# 全てのウィンドウを破棄する
cv2.destroyAllWindows()
print("ビデオ再生を終了しました。")
“`
cv2.VideoCapture(filename): 指定したビデオファイルを開きます。成功するとVideoCaptureオブジェクトを返します。cap.isOpened(): ビデオファイルが正しく開かれているかを確認します。cap.get(propId): 指定したプロパティIDpropIdの値を取得します。cap.read(): ビデオから次のフレームを読み込みます。成功するとTrueとフレームデータを、失敗するとFalseとNoneを返します。cap.release():VideoCaptureオブジェクトを解放し、リソースを閉じます。ビデオ処理の終了時に必ず呼び出す必要があります。
2. カメラ入力の取得 (cv2.VideoCapture(0))
PCに接続されたウェブカメラや内蔵カメラからの映像ストリームを取得するには、cv2.VideoCapture() の引数にカメラのIDを指定します。通常、デフォルトのカメラはID 0です。
“`python
import cv2
カメラをオープン (ID 0 がデフォルトのカメラ)
cap = cv2.VideoCapture(0)
カメラが正しくオープンできたか確認
if not cap.isOpened():
print(“エラー: カメラを開けませんでした。カメラが接続されているか、他のアプリケーションで使用されていないか確認してください。”)
else:
print(“カメラを開きました。”)
# カメラからのフレームを読み込んで表示するループ
while True:
# フレームを1枚読み込む
ret, frame = cap.read()
# フレームが正しく読み込めなかった場合はループを抜ける
if not ret:
print("フレームを読み込めませんでした。")
break
# 読み込んだフレームを表示
cv2.imshow('Camera Feed', frame)
# キー入力を待つ ('q' キーで終了)
key = cv2.waitKey(1) # ほぼリアルタイムで表示するために短い待ち時間
if key == ord('q'):
print("'q' キーが押されました。終了します。")
break
# キャプチャオブジェクトを解放する
cap.release()
# 全てのウィンドウを破棄する
cv2.destroyAllWindows()
print("カメラ処理を終了しました。")
“`
カメラからのフレームもビデオファイルと同様にNumPy配列として取得されるため、これまでに学んだ画像処理のテクニックをリアルタイムで適用することができます。例えば、カメラ映像から顔を検出して枠を描画したり、特定の色を追跡したり、エッジをリアルタイムで表示したりといった応用が考えられます。
3. ビデオファイルの保存 (cv2.VideoWriter())
処理したフレームを新しいビデオファイルとして保存することも可能です。
“`python
import cv2
カメラからの入力を使う例
cap = cv2.VideoCapture(0)
if cap.isOpened():
# ビデオの情報を取得(保存するビデオのフォーマットを決めるのに使う)
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS) # カメラがサポートするFPS
if fps <= 0: # もしFPSが取得できない場合はデフォルト値を設定
fps = 20.0
print(f”カメラ情報 – サイズ: {frame_width}x{frame_height}, FPS: {fps}”)
# 保存先のビデオファイル名を指定
output_filename = 'output_video.avi'
# ビデオライターオブジェクトを作成
# cv2.VideoWriter(ファイル名, コーデック, フレームレート, フレームサイズ)
# コーデック (FourCC - 4文字コード): ビデオ圧縮形式を指定。例: 'XVID', 'MJPG', 'mp4v' など
# FourCCを指定するには cv2.VideoWriter_fourcc(*'XXXX') を使う
# コーデックはシステムやインストールされているライブラリによって利用可能なものが異なる
# Windows: DIVX, XVID, MJPG, X264, WMV1, WMV2 など
# Linux: DIVX, XVID, MJPG, X264 (.mp4, .mkv), I420 (無圧縮), PIM1 (MPEG-1) など
# Mac OS X: DIVX, MJPG, MP4V, X264
# ここでは一般的な 'XVID' を使用 (codec = cv2.VideoWriter_fourcc(*'XVID'))
# または 'MJPG' (codec = cv2.VideoWriter_fourcc(*'MJPG'))
# または .mp4 ファイルとして保存する場合は 'mp4v' (codec = cv2.VideoWriter_fourcc(*'mp4v')) や 'X264' (.mp4) を試す
codec = cv2.VideoWriter_fourcc(*'XVID') # または他のコーデック
# フレームサイズは (幅, 高さ) の順序で指定
out = cv2.VideoWriter(output_filename, codec, fps, (frame_width, frame_height))
# フレームを処理して保存するループ
print(f"録画を開始します ('q'キーで終了)。保存先: {output_filename}")
while True:
ret, frame = cap.read()
if not ret:
break
# ここに画像処理のコードを記述
# 例: グレースケールに変換
# gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# OpenCVはカラーフレーム (3チャンネル) を期待する場合が多いので、
# グレースケールを保存する場合は、ファイルフォーマットやコーデックに注意するか、
# グレースケール画像を3チャンネルの画像に変換してから書き出す
# 処理結果のフレームを書き込む
out.write(frame) # この例では元のカラーフレームをそのまま保存
# フレームを表示
cv2.imshow('Recording...', frame)
# キー入力を待つ ('q' キーで終了)
key = cv2.waitKey(1)
if key == ord('q'):
print("'q' キーが押されました。録画を終了します。")
break
# オブジェクトを解放する
cap.release()
out.release() # VideoWriter オブジェクトも解放が必要
cv2.destroyAllWindows()
print(f"録画を終了し、'{output_filename}' に保存しました。")
else:
print(“カメラを開けませんでした。”)
“`
cv2.VideoWriter(filename, fourcc, fps, frameSize, isColor=True): 指定したファイル名、コーデック、フレームレート、フレームサイズでVideoWriterオブジェクトを作成します。isColorはカラー画像(True)かグレースケール画像(False)かを指定します。cv2.VideoWriter_fourcc(*'XXXX'): 4文字のコーデックコードから FourCC 値を作成します。out.write(image): 指定した画像imageをビデオファイルに書き込みます。入力画像は作成時に指定したframeSizeおよびisColorに合致している必要があります。out.release():VideoWriterオブジェクトを解放し、ビデオファイルの書き込みを完了します。
ビデオ処理では、フレームごとに画像処理を適用し、その結果をリアルタイムで表示したり、ファイルに保存したりすることができます。これにより、顔検出や物体追跡といったより高度な応用への道が開けます。
まとめ:画像処理の旅はまだ始まったばかり
この記事では、初心者の方を対象に、OpenCVを使った画像処理の基本的なステップをゼロから解説してきました。環境構築から始まり、画像の読み込み・表示・保存、ピクセル操作、色空間変換、幾何学変換、フィルタリング、閾値処理、モルフォロジー変換、輪郭検出、そしてビデオ処理の基本まで、幅広いトピックに触れました。
OpenCVで画像がNumPy配列として扱われること、様々な処理が専用の関数で提供されていること、そして基本的な操作を組み合わせることで多様な画像処理が可能になることを理解していただけたかと思います。
しかし、ここで紹介した内容はOpenCVの機能のほんの一部に過ぎません。OpenCVは、以下のようなさらに高度な画像処理やコンピュータビジョンの機能も豊富に提供しています。
- 特徴点検出と記述子: SIFT, SURF, ORBなどのアルゴリズムを使った画像の特徴点の検出とマッチング。
- 物体検出: Haarcascadesや深層学習モデル(YOLO, SSDなど)を使った画像中の特定の物体の検出。
- 顔認識: 画像中の顔を識別する技術。
- 物体追跡: 動画中の特定の物体を追跡する技術。
- 画像スティッチング: 複数の画像を繋ぎ合わせて一枚のパノラマ画像を生成する技術。
- キャリブレーション: カメラの歪みを補正する技術。
- 機械学習モジュール: SVM, K-Meansなどの機械学習アルゴリズム。
これらのより高度な機能は、この記事で学んだ基本的な操作の上に成り立っています。まずは基本をしっかりとマスターし、応用へと進んでいくのが良いでしょう。
次のステップへのアドバイス:
- 手を動かし続ける: 実際に様々な画像を使い、この記事で紹介したコードを繰り返し実行してみましょう。パラメータを変えてみて、結果がどのように変わるかを確認することが理解を深める上で非常に重要です。
- 公式ドキュメントを参照する: OpenCVの公式ドキュメント(特にPythonチュートリアル)は非常に充実しています。この記事で触れられなかった詳細な情報や、さらに多くの機能について学ぶことができます。
- 様々な画像処理の例を探す: Web上にはOpenCVを使った様々な画像処理のチュートリアルやコード例が公開されています。興味のある処理(例:特定の色の物体検出、顔のモザイク処理、QRコードの読み取りなど)を探して、コードを読んで実行してみましょう。
- NumPyの基本を学ぶ: OpenCVはNumPy配列を多用します。NumPyの基本的な操作(配列の生成、インデックス指定、スライシング、演算など)をしっかりと理解しておくと、OpenCVでの画像操作が格段にスムーズになります。
- 応用プロジェクトに挑戦する: 基本的な操作に慣れてきたら、小さなプロジェクトに挑戦してみましょう。例えば、「ウェブカメラを使って、画面に映った自分の顔にリアルタイムでモザイクをかける」「特定のフォルダにある全ての画像ファイルを自動でリサイズする」「手書きの数字画像を読み込んで二値化する」など、身近なテーマから始めるのがおすすめです。
画像処理は非常に広く、奥深い分野です。しかし、基本的な原理と強力なツールであるOpenCVがあれば、誰でもこの exciting な世界に飛び込むことができます。
この記事が、あなたの画像処理学習の素晴らしいスタートとなることを願っています!Happy Coding!