Pythonで画像処理!OpenCV入門ガイド


Pythonで画像処理!OpenCV入門ガイド:基礎から実践まで徹底解説

はじめに:OpenCVとは?なぜPythonで使うのか?

画像処理は、私たちの日常生活において非常に重要な役割を果たしています。スマートフォンのカメラアプリの顔認識、自動運転車の標識認識、医療画像の解析、製造ラインでの不良品検査など、様々な分野で活用されています。画像処理を学ぶことは、これらの技術を理解し、応用するための第一歩となります。

画像処理ライブラリは数多く存在しますが、その中でも世界的に最も広く使われているのが「OpenCV(Open Source Computer Vision Library)」です。OpenCVは、リアルタイムのコンピュータビジョンアプリケーション向けに設計された、クロスプラットフォームのライブラリであり、C++、Python、Javaなどの様々な言語に対応しています。

なぜPythonでOpenCVを使うのか?

OpenCVは元々C++で開発されましたが、Pythonとの組み合わせが非常に人気があります。その理由はいくつかあります。

  1. 使いやすさと開発速度: Pythonは文法がシンプルで習得しやすく、コードを素早く書くことができます。複雑な画像処理アルゴリズムも、Pythonの直感的な構文を使えば比較的容易に実装できます。これは、プロトタイプの開発やアイデアの検証において大きなメリットとなります。
  2. 豊富な科学計算ライブラリとの連携: Pythonには、NumPy、SciPy、Matplotlibといった強力な科学計算およびデータ分析ライブラリがあります。OpenCVは画像データをNumPyの多次元配列(ndarray)として扱うため、これらのライブラリとシームレスに連携できます。これにより、画像データの数値計算、統計処理、グラフ表示などが容易に行えます。特にNumPyの高速な配列操作は、画像処理のパフォーマンス向上に貢献します。
  3. コミュニティの活発さ: Pythonは非常に大きなユーザーコミュニティを持っています。OpenCVのPythonバインディングに関しても、多くのドキュメント、チュートリアル、サンプルコードがオンラインで公開されており、疑問点や問題が発生した際に助けを得やすい環境があります。
  4. OpenCVの機能の網羅性: Python版のOpenCVは、C++版の主要な機能のほとんどをカバーしています。基本的な画像操作から、高度な特徴点検出、機械学習モデル(例えばDNNモジュールを使ったディープラーニング推論)、3D処理まで、幅広い機能を利用できます。

このように、PythonとOpenCVの組み合わせは、画像処理の学習から研究開発、さらには実務アプリケーションの開発まで、非常に強力で効率的な環境を提供します。

本記事では、Python版OpenCVの基本的な使い方から、いくつかの重要な画像処理手法までを、具体的なコード例を交えながら詳細に解説していきます。画像処理の学習を始める方にとって、実践的な入門ガイドとなることを目指します。

開発環境のセットアップ

PythonでOpenCVを使うために必要なのは、Pythonの実行環境とOpenCVライブラリだけです。最も簡単なセットアップ方法は、pipパッケージマネージャーを使用することです。

ターミナルまたはコマンドプロンプトを開き、以下のコマンドを実行してください。

bash
pip install opencv-python numpy matplotlib

  • opencv-python: これがOpenCVライブラリ本体です。
  • numpy: OpenCVが内部で画像データをNumPy配列として扱うため、必須です。
  • matplotlib (オプション): 画像を表示する際にOpenCVの機能だけでなく、matplotlibを使うこともあります。特にJupyter Notebookなどで作業する場合に便利です。本記事では主にOpenCVの表示機能を使いますが、matplotlibも有用なのでインストールしておくと良いでしょう。

Anacondaを使用している場合は、以下のコマンドでもインストールできます。

bash
conda install opencv numpy matplotlib

インストールが完了したら、Pythonのインタラクティブシェルやスクリプト内でimport cv2としてエラーが出なければ成功です。

python
import cv2
print(cv2.__version__)

上記コードを実行してOpenCVのバージョンが表示されれば、準備完了です。

最初のステップ:画像の読み込み、表示、保存

画像処理を始めるにあたって、最も基本的な操作は、画像をファイルから読み込み、画面に表示し、処理結果をファイルに保存することです。OpenCVではこれらの操作を簡単に行うための関数が提供されています。

画像の読み込み (cv2.imread)

cv2.imread()関数は、指定したパスにある画像ファイルを読み込み、NumPy配列として返します。

“`python
import cv2

画像ファイルを指定

このスクリプトと同じディレクトリに ‘lena.jpg’ という名前の画像を置いてください

image_path = ‘lena.jpg’

画像を読み込む

第2引数で読み込み方法を指定できる

cv2.IMREAD_COLOR: カラー画像として読み込む (デフォルト)

cv2.IMREAD_GRAYSCALE: グレースケール画像として読み込む

cv2.IMREAD_UNCHANGED: アルファチャンネルを含めて読み込む (透過情報など)

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}”)
print(f”画素の合計数: {img.size}”)
“`

  • cv2.imread()の第1引数は画像ファイルのパスです。絶対パスでも相対パスでも指定できます。
  • 第2引数は読み込み方法を指定するフラグです。省略するとcv2.IMREAD_COLOR(カラー)になります。グレースケールで読み込みたい場合はcv2.IMREAD_GRAYSCALEを指定します。
  • 画像が読み込めなかった場合(ファイルが存在しない、破損しているなど)、cv2.imread()Noneを返します。そのため、読み込み後にNoneチェックを行うことが推奨されます。
  • 読み込まれた画像データはNumPyのndarrayオブジェクトになります。カラー画像の場合、形状は(高さ, 幅, チャンネル数)となります。OpenCVではデフォルトでB(青)、G(緑)、R(赤)の順番(BGR形式)でチャンネルが格納されます。グレースケール画像の場合は形状は(高さ, 幅)となり、チャンネル数はありません。
  • 画素値のデータ型(dtype)は、通常、各チャンネルあたり8ビットの符号なし整数(uint8)です。これは0から255までの値で画素の輝度や色を表します。

画像の表示 (cv2.imshow, cv2.waitKey, cv2.destroyAllWindows)

画像を画面に表示するには、cv2.imshow()関数を使用します。この関数だけではウィンドウはすぐに消えてしまうため、ユーザーがキーを押すまで待機させるcv2.waitKey()と組み合わせて使います。表示し終わったウィンドウを閉じるにはcv2.destroyAllWindows()またはcv2.destroyWindow()を使用します。

“`python
import cv2
import numpy as np # NumPyも必要なのでインポート

ダミー画像を生成 (例として500×500の青い画像)

実際の画像ファイルを読み込む場合も同様に扱えます

img = np.zeros((500, 500, 3), dtype=np.uint8)
img[:] = (255, 0, 0) # BGRで青を設定

if img is not None:
# 画像をウィンドウに表示
# 第1引数はウィンドウの名前 (任意の文字列)
# 第2引数は表示する画像データ (NumPy配列)
cv2.imshow(‘Blue Square’, img)

# キー入力を待つ
# 引数にミリ秒を指定すると、指定した時間経過するか、キー入力があれば関数が終了する
# 0を指定すると、何らかのキーが押されるまで無限に待機する
# 戻り値は押されたキーのASCIIコード。何も押されずにタイムアウトした場合は -1
print("ウィンドウが表示されました。何かキーを押してください。")
key = cv2.waitKey(0)

# ウィンドウを閉じる
# cv2.destroyWindow('ウィンドウの名前') で特定のウィンドウを閉じる
# cv2.destroyAllWindows() で作成された全てのOpenCVウィンドウを閉じる
cv2.destroyAllWindows()
print("ウィンドウが閉じられました。")

# 押されたキーを確認 (もしあれば)
# 特定のキーで処理を分岐させたい場合に利用できる
# 例: 'q'キーが押されたら終了する場合
# if key == ord('q'):
#     print("'q'キーが押されました。")
# elif key != -1:
#     print(f"ASCIIコード {key} のキーが押されました。")

else:
print(“表示する画像データがありません。”)

“`

  • cv2.imshow()の第1引数は、表示されるウィンドウのタイトルバーに表示される名前です。同じ名前で再度cv2.imshow()を呼び出すと、既存のウィンドウの内容が更新されます。
  • cv2.waitKey()は、OpenCVのウィンドウがイベント(キー入力、マウス操作など)を処理するための重要な関数です。これがないと、画像ウィンドウは正しく表示されません。引数に0を指定すると無限に待機し、正の値を指定するとそのミリ秒数待機します。キーが押されると、そのキーのASCIIコードを返します。タイムアウトした場合は-1を返します。
  • cv2.destroyAllWindows()は、OpenCVによって作成された全てのGUIウィンドウを閉じます。スクリプトの終了前に呼び出すのが一般的です。特定のウィンドウだけを閉じたい場合はcv2.destroyWindow('ウィンドウの名前')を使います。

画像の保存 (cv2.imwrite)

処理した画像をファイルに保存するには、cv2.imwrite()関数を使用します。

“`python
import cv2
import numpy as np

例としてグレースケール画像を生成

gray_img = np.zeros((400, 600), dtype=np.uint8)

中央に白い四角を描画 (NumPyのスライシングを利用)

gray_img[100:300, 200:400] = 255

画像をファイルに保存

第1引数は保存するファイルパス (拡張子でフォーマットを指定)

第2引数は保存する画像データ (NumPy配列)

output_path = ‘gray_square.png’ # PNG形式で保存
success = cv2.imwrite(output_path, gray_img)

if success:
print(f”画像を ‘{output_path}’ に保存しました。”)
else:
print(f”エラー: 画像を ‘{output_path}’ に保存できませんでした。”)

JPEG形式で保存する場合 (圧縮率を指定できる)

output_path_jpg = ‘gray_square.jpg’

flagsとして圧縮率を指定 (0-100, デフォルトは95)

success_jpg = cv2.imwrite(output_path_jpg, gray_img, [cv2.IMWRITE_JPEG_QUALITY, 90])

if success_jpg:
print(f”画像を ‘{output_path_jpg}’ に圧縮率90で保存しました。”)
else:
print(f”エラー: 画像を ‘{output_path_jpg}’ に保存できませんでした。”)

“`

  • cv2.imwrite()の第1引数は保存先のファイルパスです。指定した拡張子(.png, .jpg, .bmpなど)に基づいて、OpenCVが自動的に保存フォーマットを判断します。
  • 第2引数は保存する画像データ(NumPy配列)です。
  • 保存に成功した場合はTrue、失敗した場合はFalseを返します。
  • JPEGのような圧縮フォーマットの場合、第3引数で圧縮に関するパラメータを指定できます。例えば、[cv2.IMWRITE_JPEG_QUALITY, 90]はJPEGの圧縮率を90%に設定します(0-100の範囲、値が大きいほど高品質でファイルサイズが大きい)。

画像のプロパティとピクセルへのアクセス

画像がNumPy配列として読み込まれることで、PythonのNumPyの機能を使って画像データを操作できるようになります。画像の基本的なプロパティ(形状、データ型、サイズ)の取得や、個々のピクセル値へのアクセス、さらには部分領域(ROI: Region of Interest)の操作もNumPyのスライシングで行えます。

“`python
import cv2
import numpy as np

ダミーのカラー画像を生成 (幅400, 高さ300)

img = np.zeros((300, 400, 3), dtype=np.uint8) # 黒い画像
img[:] = (100, 150, 50) # 全体をBGR(50, 150, 100)の色にする

画像のプロパティ

print(f”形状 (高さ, 幅, チャンネル): {img.shape}”) # (300, 400, 3)
print(f”次元数: {img.ndim}”) # 3 (高さ、幅、チャンネル)
print(f”データ型: {img.dtype}”) # uint8
print(f”要素数 (ピクセル総数 * チャンネル数): {img.size}”) # 300 * 400 * 3 = 360000

個々のピクセル値へのアクセス (OpenCVのBGR形式に注意)

例えば、座標 (x=50, y=100) のピクセル値を取得

配列のインデックスは [行 (高さ方向), 列 (幅方向)]

カラー画像の場合は [y, x, チャンネル] となる

y, x = 100, 50
pixel_value = img[y, x]
print(f”座標 ({x}, {y}) のピクセル値 (BGR): {pixel_value}”) # 例: [ 50 150 100]

特定のチャンネルの値にアクセス

blue_value = img[y, x, 0] # Bチャンネル
green_value = img[y, x, 1] # Gチャンネル
red_value = img[y, x, 2] # Rチャンネル
print(f”座標 ({x}, {y}) の BGR 値: B={blue_value}, G={green_value}, R={red_value}”)

ピクセル値の変更

座標 (x=150, y=200) のピクセルを黄色 (BGRで(0, 255, 255)) に変更

img[200, 150] = (0, 255, 255)

部分領域 (ROI: Region of Interest) の操作

例として、画像の左上100×100ピクセルの領域を赤くする

NumPyのスライシングを使用: [y1:y2, x1:x2]

img[0:100, 0:100] = (0, 0, 255) # BGRで赤を設定

別の部分領域をコピーして貼り付け

例えば、先ほど赤くした領域を画像の中心付近にコピー

コピー元の領域 (左上100×100)

roi_source = img[0:100, 0:100]

貼り付け先の開始座標 (例えば x=200, y=150)

paste_y, paste_x = 150, 200

貼り付け先の領域を指定 (コピー元と同じ形状になるように終点座標を計算)

img[paste_y : paste_y+roi_source.shape[0], paste_x : paste_x+roi_source.shape[1]] = roi_source

変更後の画像を表示

cv2.imshow(‘Image with ROI and Pixel Changes’, img)
print(“変更後の画像が表示されました。何かキーを押してください。”)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`

  • 画像の形状はimg.shapeで取得できます。カラー画像は(高さ, 幅, チャンネル数)、グレースケール画像は(高さ, 幅)です。NumPy配列では一般的に(軸0の長さ, 軸1の長さ, ...)となりますが、画像処理では慣習的に高さが最初の次元、幅が2番目の次元として扱われます。
  • ピクセル座標を指定する際は、(y, x)または(高さ方向のインデックス, 幅方向のインデックス)の順序になることに注意してください。これは一般的なグラフの(x, y)とは逆になります。OpenCV関数の引数では(x, y)の順で指定するものもあるため、混同しないように注意が必要です。NumPy配列へのインデックス指定は常に(y, x)です。
  • img[y, x]で特定のピクセルの値(カラーなら3つの値の配列、グレースケールなら1つの値)を取得・設定できます。
  • img[y1:y2, x1:x2]のようにNumPyのスライシングを使うことで、画像の矩形領域(ROI)を簡単に抽出したり、その領域全体の画素値をまとめて変更したりできます。これは画像処理において非常に頻繁に使われるテクニックです。

色空間の変換

画像処理では、状況に応じて異なる色空間を利用することがあります。最も一般的な色空間はBGR(またはRGB)ですが、HSV(Hue, Saturation, Value)やグレースケールなど、他の色空間に変換することで、特定の処理が容易になる場合があります。

OpenCVでは、cv2.cvtColor()関数を使って色空間の変換を行います。

“`python
import cv2

画像を読み込む (カラーで)

image_path = ‘lena.jpg’
img_bgr = cv2.imread(image_path, cv2.IMREAD_COLOR)

if img_bgr is None:
print(f”エラー: 画像ファイル ‘{image_path}’ を読み込めませんでした。”)
else:
# BGR画像をグレースケールに変換
img_gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)

# BGR画像をHSVに変換
img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)

# グレースケール画像、HSV画像を表示
cv2.imshow('Original BGR Image', img_bgr)
cv2.imshow('Grayscale Image', img_gray)

# HSV画像はそのまま表示しても意図した色にならないことが多い
# 各チャンネルを分割して表示するなどする
# 例えば、V (Value) チャンネルを表示 (明るさ情報)
# Vチャンネルはグレースケール画像のように見える
h, s, v = cv2.split(img_hsv) # HSVチャンネルを分割
cv2.imshow('HSV - Hue Channel (H)', h)
cv2.imshow('HSV - Saturation Channel (S)', s)
cv2.imshow('HSV - Value Channel (V)', v)


print("画像が表示されました (BGR, Grayscale, HSVチャンネル)。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

# HSVからBGRに戻す
img_bgr_from_hsv = cv2.cvtColor(img_hsv, cv2.COLOR_HSV2BGR)
cv2.imshow('BGR from HSV', img_bgr_from_hsv)
print("HSVから戻した画像が表示されました。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

“`

  • cv2.cvtColor()の第1引数は変換元の画像データです。
  • 第2引数は変換コードです。cv2.COLOR_変換元_変換先のような形式で指定します。よく使うコードには、cv2.COLOR_BGR2GRAY, cv2.COLOR_BGR2HSV, cv2.COLOR_HSV2BGR, cv2.COLOR_BGR2RGB, cv2.COLOR_RGB2BGRなどがあります。
  • グレースケール: 画素が輝度情報のみを持つ単一チャンネルの画像です。色情報が不要な場合や、エッジ検出などの多くのアルゴリズムの前処理として使われます。
  • HSV: 色相(Hue)、彩度(Saturation)、明度(Value)の3つの要素で色を表します。
    • 色相 (H): 色の種類(赤、青、緑など)を表します。通常0-180の範囲(OpenCVの場合、180度系)または0-360の範囲で表されます。OpenCVのuint8画像では0-180にスケーリングされています。
    • 彩度 (S): 色の鮮やかさを表します。0(灰色)から最大値(純色)までの範囲をとります。
    • 明度 (V): 色の明るさを表します。0(黒)から最大値(最も明るい色)までの範囲をとります。
      HSV色空間は、特定の色を持つ物体を検出する際に非常に便利です。例えば、青い物体を検出したい場合、BGR空間でR, G, Bの複雑な組み合わせを条件にするよりも、HSV空間でHチャンネルの値が特定の範囲にあるかどうかをチェックする方が容易です。
  • HSV画像をOpenCVのimshowで直接表示しても、私たちが通常認識する色とは異なる見え方になります。これは、imshowがBGRまたはグレースケールを想定しているためです。HSV画像の各チャンネル(H, S, V)はそれぞれ単一チャンネルの画像として表示できます。特にVチャンネルは元の画像の明るさに対応するため、グレースケール画像のように見えます。
  • cv2.split()関数を使うと、カラー画像の各チャンネルを分離したNumPy配列のリストを取得できます。結合するにはcv2.merge()を使います。

基本的な画像変換

画像処理における基本的な変換として、サイズ変更、移動、回転、反転などがあります。これらの変換は、画像のサイズを揃えたり、特定の領域を抽出したり、画像を正規化したりするために使用されます。

サイズ変更 (リサイズ) (cv2.resize)

画像のサイズを変更します。拡大・縮小に使用します。

“`python
import cv2
import numpy as np

画像を読み込む

image_path = ‘lena.jpg’
img = cv2.imread(image_path)

if img is None:
print(f”エラー: 画像ファイル ‘{image_path}’ を読み込めませんでした。”)
else:
height, width = img.shape[:2]

# サイズを指定してリサイズ
# 新しいサイズは (幅, 高さ) の順で指定することに注意
new_width = 200
new_height = 150
resized_img_fixed = cv2.resize(img, (new_width, new_height), interpolation=cv2.INTER_AREA) # 縮小に適した補間方法

# 拡大率を指定してリサイズ
fx = 0.5 # 幅方向の拡大率 (50%に縮小)
fy = 0.5 # 高さ方向の拡大率 (50%に縮小)
resized_img_scale = cv2.resize(img, None, fx=fx, fy=fy, interpolation=cv2.INTER_LINEAR) # 一般的な補間方法

# 元の画像とリサイズ後の画像を表示
cv2.imshow('Original Image', img)
cv2.imshow(f'Resized (Fixed Size {new_width}x{new_height})', resized_img_fixed)
cv2.imshow(f'Resized (Scale {fx}x{fy})', resized_img_scale)

print("リサイズされた画像が表示されました。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

# 補間方法について
# 画像の拡大・縮小時には、新しいピクセル位置の色を計算する必要があります。これを補間と呼びます。
# cv2.INTER_NEAREST: 最近傍補間。最も高速だが、画像の質が低下しやすい(特に拡大時)。
# cv2.INTER_LINEAR: バイリニア補間。2x2ピクセルの線形補間。デフォルト。一般的にバランスが良い。
# cv2.INTER_CUBIC: バイキュービック補間。4x4ピクセルのキュービック補間。より高品質だが、計算コストが高い。
# cv2.INTER_AREA: 領域補間。画素領域の比率に基づくリサンプリング。縮小時に偽像を防ぐのに適している。
# cv2.INTER_LANCZOS4: ランチョシュ補間。8x8ピクセルのランチョシュ補間。より高品質だが、さらに計算コストが高い。
# 一般的に、縮小には cv2.INTER_AREA、拡大には cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_LANCZOS4 が適しています。

“`

  • cv2.resize()の第2引数dsizeで新しいサイズを(幅, 高さ)のタプルで指定します。もしNoneを指定した場合、第3引数fxと第4引数fyで拡大率を指定します。
  • interpolation引数で補間方法を指定します。画像の質や処理速度に影響するため、目的に応じて適切な方法を選択します。

移動 (並進) (cv2.warpAffine)

画像を平行移動させます。これはアフィン変換の一つであり、変換行列を使用して行います。

“`python
import cv2
import numpy as np

画像を読み込む

image_path = ‘lena.jpg’
img = cv2.imread(image_path)

if img is None:
print(f”エラー: 画像ファイル ‘{image_path}’ を読み込めませんでした。”)
else:
height, width = img.shape[:2]

# 移動量 (x方向に50ピクセル、y方向に30ピクセル移動)
tx = 50
ty = 30

# 移動の変換行列 M を作成
# 移動の変換行列は以下のような2x3行列
# [[1, 0, tx],
#  [0, 1, ty]]
M = np.float32([[1, 0, tx], [0, 1, ty]])

# アフィン変換を適用して画像を移動
# cv2.warpAffine(src, M, dsize)
# src: 入力画像
# M: 変換行列 (2x3)
# dsize: 出力画像のサイズ (幅, 高さ) - 通常は元の画像サイズを指定
translated_img = cv2.warpAffine(img, M, (width, height))

# 元の画像と移動後の画像を表示
cv2.imshow('Original Image', img)
cv2.imshow(f'Translated Image (tx={tx}, ty={ty})', translated_img)

print("移動された画像が表示されました。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

“`

  • 移動はアフィン変換の特別なケースです。cv2.warpAffine()関数を使用します。
  • アフィン変換には2×3の変換行列Mが必要です。移動の場合、行列はNumPy配列で[[1, 0, tx], [0, 1, ty]]と定義します。ここでtxtyがそれぞれx方向とy方向の移動量です。データ型はnp.float32である必要があります。
  • cv2.warpAffine()dsize引数には出力画像のサイズを(幅, 高さ)で指定します。通常は元の画像のサイズを指定しますが、移動によって画像の一部がフレームアウトする場合や、逆にフレーム内に余白を作るために大きなサイズを指定することも可能です。

回転 (cv2.getRotationMatrix2D, cv2.warpAffine)

画像を回転させます。回転もアフィン変換の一つであり、変換行列を使用します。OpenCVでは、回転のための変換行列を簡単に生成する関数が用意されています。

“`python
import cv2
import numpy as np

画像を読み込む

image_path = ‘lena.jpg’
img = cv2.imread(image_path)

if img is None:
print(f”エラー: 画像ファイル ‘{image_path}’ を読み込めませんでした。”)
else:
height, width = img.shape[:2]

# 回転の中心座標 (画像の中心を基準にするのが一般的)
center = (width // 2, height // 2)

# 回転角度 (時計回りを正とする)
angle = 45 # 例: 45度回転

# スケール (回転と同時に拡大縮小も可能。通常は1.0)
scale = 1.0

# 回転の変換行列 M を取得
# cv2.getRotationMatrix2D(center, angle, scale)
# center: 回転の中心座標 (x, y)
# angle: 回転角度 (度単位, 時計回りが正)
# scale: スケールファクター
M = cv2.getRotationMatrix2D(center, angle, scale)

# アフィン変換を適用して画像を回転
# cv2.warpAffine(src, M, dsize)
# src: 入力画像
# M: 変換行列 (2x3)
# dsize: 出力画像のサイズ (幅, 高さ) - 通常は元の画像サイズを指定
rotated_img = cv2.warpAffine(img, M, (width, height))

# 元の画像と回転後の画像を表示
cv2.imshow('Original Image', img)
cv2.imshow(f'Rotated Image ({angle} degrees)', rotated_img)

print("回転された画像が表示されました。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

# 注意点:
# 回転によって画像の一部が欠ける(フレームアウトする)可能性があります。
# これを防ぐには、出力画像のサイズ (dsize) を元の画像よりも大きく計算する必要があります。
# 欠けた部分や余白は通常、黒(画素値0)で埋められます。

“`

  • cv2.getRotationMatrix2D()関数は、指定した中心座標、角度、スケールで回転するための2×3変換行列を生成します。角度は度単位で指定し、時計回りが正の方向となります。
  • 得られた変換行列Mcv2.warpAffine()に渡すことで回転が適用されます。
  • 回転によって画像がフレームに収まりきらなくなることがよくあります。この場合、デフォルトでは元の画像サイズのフレームに収まる範囲で画像が表示され、はみ出した部分は切り取られます。欠けずに全体を表示するには、回転後の画像全体が収まるように出力サイズ(dsize)を計算し直す必要があります。

反転 (cv2.flip)

画像を垂直方向、水平方向、または両方向に反転させます。

“`python
import cv2

画像を読み込む

image_path = ‘lena.jpg’
img = cv2.imread(image_path)

if img is None:
print(f”エラー: 画像ファイル ‘{image_path}’ を読み込めませんでした。”)
else:
# 垂直方向に反転 (上下反転)
# flipCode = 0
flipped_vertical = cv2.flip(img, 0)

# 水平方向に反転 (左右反転)
# flipCode = 1
flipped_horizontal = cv2.flip(img, 1)

# 垂直方向と水平方向の両方に反転 (180度回転と同じ効果)
# flipCode = -1
flipped_both = cv2.flip(img, -1)

# 元の画像と反転後の画像を表示
cv2.imshow('Original Image', img)
cv2.imshow('Flipped Vertically', flipped_vertical)
cv2.imshow('Flipped Horizontally', flipped_horizontal)
cv2.imshow('Flipped Both', flipped_both)

print("反転された画像が表示されました。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

“`

  • cv2.flip()関数の第2引数flipCodeで反転の方向を指定します。
    • 0: 垂直方向(上下)に反転。
    • 1: 水平方向(左右)に反転。
    • -1: 垂直方向と水平方向の両方に反転。これは180度回転と同じ結果になります。

クロッピング (切り出し)

画像の特定の部分を切り出す操作です。これはOpenCVの特定の関数を使うのではなく、NumPy配列のスライシング機能で行います。

“`python
import cv2
import numpy as np

画像を読み込む

image_path = ‘lena.jpg’
img = cv2.imread(image_path)

if img is None:
print(f”エラー: 画像ファイル ‘{image_path}’ を読み込めませんでした。”)
else:
# 切り出す領域の座標を指定
# 開始座標 (x1, y1), 終了座標 (x2, y2)
x1, y1 = 100, 50
x2, y2 = 300, 250

# NumPyのスライシングで領域を切り出し
# 注意: NumPyスライシングは [y1:y2, x1:x2] の順
cropped_img = img[y1:y2, x1:x2]

# 元の画像とクロップされた画像を表示
cv2.imshow('Original Image', img)
cv2.imshow(f'Cropped Image ({x1},{y1}) to ({x2},{y2})', cropped_img)

print("クロップされた画像が表示されました。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

# グレースケール画像の場合も同様
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cropped_gray = img_gray[y1:y2, x1:x2]
cv2.imshow('Cropped Grayscale Image', cropped_gray)
cv2.waitKey(0)
cv2.destroyAllWindows()

“`

  • クロッピングはNumPy配列のスライシング[y1:y2, x1:x2]で行います。y1からy2-1までの行、x1からx2-1までの列が切り出されます。
  • カラー画像の場合はimg[y1:y2, x1:x2, :]となりますが、チャンネル次元は通常省略しても機能します。

画像への描画

画像上に線、円、矩形、テキストなどを描画する機能は、デバッグのためや、検出された物体をマークするために頻繁に使用されます。OpenCVにはこれらの描画を行う関数が用意されています。これらの関数は、多くの場合、元の画像を直接変更します(インプレース操作)。

描画関数共通の引数

多くの描画関数は以下の共通の引数を持ちます。

  • img: 描画対象の画像(NumPy配列)。描画は通常この画像に直接行われます。
  • color: 描画する色。BGR形式のタプルで指定します(例: 青=(255, 0, 0), 緑=(0, 255, 0), 赤=(0, 0, 255), 黒=(0, 0, 0), 白=(255, 255, 255))。グレースケール画像に描画する場合は、0-255の単一の値で指定します。
  • thickness: 描画する線の太さ。ピクセル単位で指定します。負の値を指定すると、塗りつぶされた図形が描画されます。
  • lineType: 線の種類。cv2.LINE_4, cv2.LINE_8 (デフォルト), cv2.LINE_AA (アンチエイリアス) などがあります。LINE_AAは滑らかな線を描画しますが、少し計算コストがかかります。

線を引く (cv2.line)

指定した2点間に線を引きます。

“`python
import cv2
import numpy as np

黒い画像を作成

img = np.zeros((400, 600, 3), dtype=np.uint8)

線の開始点と終了点

pt1 = (50, 50)
pt2 = (550, 350)

線を描画

cv2.line(img, pt1, pt2, color, thickness, lineType)

cv2.line(img, pt1, pt2, (0, 255, 0), 3, cv2.LINE_AA) # 緑色の線、太さ3、アンチエイリアス

cv2.imshow(‘Image with Line’, img)
print(“線が描画された画像が表示されました。何かキーを押してください。”)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`

  • pt1pt2は、それぞれ線の開始点と終了点の座標を(x, y)のタプルで指定します。

矩形を描く (cv2.rectangle)

指定した対角線上の2点を持つ矩形を描画します。

“`python
import cv2
import numpy as np

img = np.zeros((400, 600, 3), dtype=np.uint8)

矩形の左上と右下の座標

pt1 = (100, 100)
pt2 = (500, 300)

矩形を描画 (枠線)

cv2.rectangle(img, pt1, pt2, color, thickness, lineType)

cv2.rectangle(img, pt1, pt2, (0, 0, 255), 2) # 赤色の枠線、太さ2

別の矩形を塗りつぶしで描画

pt3 = (50, 50)
pt4 = (200, 200)
cv2.rectangle(img, pt3, pt4, (255, 0, 0), -1) # 青色で塗りつぶし (-1で塗りつぶし)

cv2.imshow(‘Image with Rectangles’, img)
print(“矩形が描画された画像が表示されました。何かキーを押してください。”)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`

  • pt1pt2は、矩形の対角線上の2点の座標を(x, y)のタプルで指定します。通常は左上と右下の座標を指定します。
  • thicknessに-1を指定すると、矩形が塗りつぶされます。

円を描く (cv2.circle)

指定した中心と半径を持つ円を描画します。

“`python
import cv2
import numpy as np

img = np.zeros((400, 600, 3), dtype=np.uint8)

円の中心座標と半径

center = (300, 200)
radius = 100

円を描画 (枠線)

cv2.circle(img, center, radius, color, thickness, lineType)

cv2.circle(img, center, radius, (0, 255, 255), 5) # 黄色の枠線、太さ5

別の円を塗りつぶしで描画

center2 = (100, 100)
radius2 = 50
cv2.circle(img, center2, radius2, (128, 0, 128), -1) # 紫色で塗りつぶし

cv2.imshow(‘Image with Circles’, img)
print(“円が描画された画像が表示されました。何かキーを押してください。”)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`

  • centerは円の中心座標を(x, y)のタプルで指定します。
  • radiusは円の半径をピクセル単位で指定します。

テキストを描く (cv2.putText)

画像上にテキストを描画します。

“`python
import cv2
import numpy as np

img = np.zeros((400, 600, 3), dtype=np.uint8)

描画するテキスト

text = ‘Hello, OpenCV!’

テキストの開始座標 (テキストの左下隅)

org = (50, 350)

フォント

fontFace = cv2.FONT_HERSHEY_SIMPLEX

フォントスケール

fontScale = 1.5

テキストの色 (BGR)

color = (255, 255, 255) # 白

線の太さ

thickness = 2

線の種類

lineType = cv2.LINE_AA

テキストを描画

cv2.putText(img, text, org, fontFace, fontScale, color, thickness, lineType)

cv2.putText(img, text, org, fontFace, fontScale, color, thickness, lineType)

別のテキストを描画 (例: 位置を変えて色を変える)

text2 = ‘Python Image Processing’
org2 = (50, 50)
color2 = (0, 255, 0) # 緑
thickness2 = 1
cv2.putText(img, text2, org2, cv2.FONT_HERSHEY_DUPLEX, 1.0, color2, thickness2, cv2.LINE_AA)

cv2.imshow(‘Image with Text’, img)
print(“テキストが描画された画像が表示されました。何かキーを押してください。”)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`

  • textは描画する文字列です。
  • orgはテキストの描画を開始する位置(通常は左下隅)を(x, y)のタプルで指定します。
  • fontFaceはフォントの種類を指定します(例: cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_PLAINなど)。
  • fontScaleはフォントの大きさを指定するスケールファクターです。
  • thicknessはテキストの線の太さです。
  • lineTypeは線の種類(アンチエイリアスなど)です。

注意点: OpenCVのputText関数は、基本的な英数字フォントしか内蔵していません。日本語などのマルチバイト文字を描画するには、PIL (Pillow) などの外部ライブラリと連携したり、より高度なテキスト描画手法を用いる必要があります。

マウスイベントとキーボードイベントの処理

インタラクティブな画像処理アプリケーションを作成する場合、ユーザーのマウス操作やキーボード入力を検出して処理する必要があります。OpenCVは、これらのイベントを処理するための仕組みを提供しています。

キーボードイベント (cv2.waitKey)

既に画像の表示で使用したcv2.waitKey()関数は、キーボードイベントの処理にも使用されます。この関数は押されたキーのASCIIコードを返すため、特定のキーが押されたかどうかを判定して処理を分岐させることができます。

“`python
import cv2
import numpy as np

黒い画像を作成

img = np.zeros((400, 600, 3), dtype=np.uint8)
cv2.putText(img, ‘Press Q to Quit, or any other key’, (50, 200), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)

cv2.imshow(‘Keyboard Event Example’, img)

print(“ウィンドウが表示されました。キーを押してください。”)

while True:
# キー入力を待つ (ここでは1ミリ秒だけ待つ)
# ループ内で常にイベントをチェックするために、短い時間待機させることが多い
key = cv2.waitKey(1) & 0xFF # ビットAND 0xFF は、環境による違いを吸収するためのおまじない

# 押されたキーのASCIIコードを判定
if key == ord('q'): # 'q' キーが押された場合
    print("'q' キーが押されました。終了します。")
    break # ループを終了
elif key != 255: # 何らかのキーが押されたが、255でない場合 (255は何も押されなかったときの一般的な値)
    print(f"ASCIIコード {key} ('{chr(key)}') のキーが押されました。")
    # ここに他のキーに応じた処理を追加

cv2.destroyAllWindows()
print(“終了しました。”)
“`

  • cv2.waitKey(milliseconds)は、指定したミリ秒数待機し、その間にキーが押されればそのASCIIコードを返します。何も押されないか、タイムアウトした場合は通常-1または255などの特定の値を返します。
  • 短い時間(例えば1ミリ秒)を指定してループ内でwaitKeyを呼び出すことで、ウィンドウを表示し続けつつ、継続的にキー入力をチェックすることができます。これはOpenCVのイベント処理ループの基本形です。
  • key & 0xFFという表現は、一部のシステムでwaitKeyの戻り値が32ビットになる場合があるため、下位8ビット(ASCIIコード)だけを取り出すための慣用句です。これにより、異なるプラットフォーム間でのコードの互換性が高まります。
  • 特定の文字のASCIIコードを取得するには、Pythonの組み込み関数ord()を使用します(例: ord('q'))。

マウスイベント (cv2.setMouseCallback)

マウスのクリック、移動、ドラッグなどのイベントを処理するには、cv2.setMouseCallback()関数とコールバック関数を使用します。

まず、マウスイベントが発生したときに呼び出される関数(コールバック関数)を定義します。この関数は特定のシグネチャを持つ必要があります。

python
def mouse_callback(event, x, y, flags, param):
# event: マウスイベントの種類 (cv2.EVENT_...)
# x, y: マウスカーソルの座標
# flags: Ctrl, Shiftなどの同時押下キー情報
# param: cv2.setMouseCallbackの第3引数で渡されたデータ (必要に応じて)
pass # ここにイベントに応じた処理を記述

次に、このコールバック関数を特定のウィンドウに関連付けます。

python
cv2.setMouseCallback('ウィンドウの名前', mouse_callback)

以下は、マウスの左ボタンがクリックされた座標を表示する例です。

“`python
import cv2
import numpy as np

マウスイベントを処理するコールバック関数を定義

def mouse_click_event(event, x, y, flags, param):
# 左ボタンが押されたイベントをチェック
if event == cv2.EVENT_LBUTTONDOWN:
print(f”左ボタンがクリックされました。座標: ({x}, {y})”)
# クリックされた場所に円を描画するなどの処理をここに追加できる
# param には画像データなどを渡すことができる
img = param
if img is not None:
cv2.circle(img, (x, y), 5, (0, 255, 255), -1) # 黄色の小さな円を描画
cv2.imshow(‘Mouse Event Example’, img) # ウィンドウを更新

黒い画像を作成

img = np.zeros((400, 600, 3), dtype=np.uint8)
cv2.putText(img, ‘Click anywhere with the left mouse button’, (50, 200), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)

ウィンドウを作成

window_name = ‘Mouse Event Example’
cv2.imshow(window_name, img)

マウスイベントコールバック関数をウィンドウに設定

第3引数に画像データ img を渡すことで、コールバック関数内で画像にアクセス・変更できる

cv2.setMouseCallback(window_name, mouse_click_event, img)

print(f”ウィンドウ ‘{window_name}’ が表示されました。左クリックしてください。’q’で終了。”)

イベントループ

while True:
key = cv2.waitKey(1) & 0xFF
if key == ord(‘q’):
break

cv2.destroyAllWindows()
print(“終了しました。”)
“`

  • cv2.EVENT_LBUTTONDOWNはマウスの左ボタンが押されたときのイベントです。他にも、cv2.EVENT_LBUTTONUP, cv2.EVENT_RBUTTONDOWN, cv2.EVENT_MOUSEMOVEなど様々なイベントタイプがあります。
  • cv2.setMouseCallback()の第1引数は、コールバックを設定するウィンドウの名前です。cv2.imshow()で指定した名前と一致させる必要があります。
  • 第2引数は定義したコールバック関数名です。
  • 第3引数paramはオプションで、コールバック関数に渡したい任意のデータ(例えば画像データ自体、オブジェクトのリストなど)を指定できます。これにより、コールバック関数がウィンドウの状態や関連データにアクセスできるようになります。

このイベント処理の仕組みを使うことで、ユーザーが指定した領域を切り出したり、特定の部分をクリックして情報を表示したり、簡単なペイントツールのようなものを作成したりすることが可能になります。

基本的な画像処理手法

ここからは、いくつかの基本的な画像処理アルゴリズムと、それらをOpenCVでどのように実装するかを見ていきます。

閾値処理 (二値化) (cv2.threshold)

閾値処理は、画像をグレースケール画像に変換した後、各ピクセルの輝度値があらかじめ定めた閾値より大きいか小さいかに基づいて、ピクセル値を0または最大値(通常255)に変換する処理です。これにより、画像を前景と背景の2つの領域に分割(二値化)することができます。

“`python
import cv2

グレースケール画像を読み込む

閾値処理は通常グレースケール画像に対して行われる

image_path = ‘lena.jpg’
img_gray = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

if img_gray is None:
print(f”エラー: 画像ファイル ‘{image_path}’ を読み込めませんでした。”)
else:
# シンプルな閾値処理 (固定閾値)
# cv2.threshold(src, thresh, maxval, type)
# src: 入力画像 (グレースケールまたはカラーだが、通常グレースケール)
# thresh: 閾値
# maxval: 閾値を超えた場合に割り当てる最大値 (通常255)
# type: 閾値処理のタイプ
# cv2.THRESH_BINARY: 画素値 > thresh => maxval, それ以外 => 0
# cv2.THRESH_BINARY_INV: 画素値 > thresh => 0, それ以外 => maxval (色の反転)
# cv2.THRESH_TRUNC: 画素値 > thresh => thresh, それ以外はそのまま
# cv2.THRESH_TOZERO: 画素値 > thresh => そのまま, それ以外 => 0
# cv2.THRESH_TOZERO_INV: 画素値 > thresh => 0, それ以外はそのまま
# 戻り値: (retVal, dst) – retValは実際に使用された閾値、dstは結果画像

ret, thresh1 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)
ret, thresh2 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY_INV)
ret, thresh3 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_TRUNC)
ret, thresh4 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_TOZERO)
ret, thresh5 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_TOZERO_INV)

# 大津の二値化 (Otsu's Binarization) - 自動的に最適な閾値を計算
# THRESH_OTSU フラグを追加し、閾値 thresh は 0 を指定する
ret_otsu, thresh_otsu = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f"大津の二値化で決定された閾値: {ret_otsu}")


# 結果を表示
cv2.imshow('Original Grayscale', img_gray)
cv2.imshow('THRESH_BINARY', thresh1)
cv2.imshow('THRESH_BINARY_INV', thresh2)
cv2.imshow('THRESH_TRUNC', thresh3)
cv2.imshow('THRESH_TOZERO', thresh4)
cv2.imshow('THRESH_TOZERO_INV', thresh5)
cv2.imshow('THRESH_OTSU (Binary)', thresh_otsu)


print("閾値処理の結果が表示されました。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

“`

  • cv2.threshold()は2つの値を返します。1つ目は実際に使用された閾値(retVal)、2つ目は閾値処理後の画像(dst)です。大津の二値化を使用する場合、retValにはOpenCVが計算した最適な閾値が格納されます。
  • cv2.THRESH_BINARYが最も一般的な二値化のタイプです。
  • cv2.THRESH_OTSUフラグをtypeに追加すると、OpenCVが画像のヒストグラムを分析し、最適な閾値を自動的に計算してくれます。この場合、手動で指定するthresh引数は0で構いません。

画像平滑化 (ぼかし) (cv2.GaussianBlur, cv2.medianBlur, etc.)

画像平滑化(スムージング)は、画像のノイズを低減したり、細部をぼかしたりするために使用されます。これは通常、畳み込み(Convolution)と呼ばれる操作によって行われます。特定の「カーネル」(またはフィルタ)を画像上の各ピクセルに適用し、そのピクセルとその周辺ピクセルの値を使って新しいピクセル値を計算します。

OpenCVにはいくつかの平滑化フィルタが用意されています。

  1. 平均化フィルタ (cv2.blur): カーネル内のピクセル値の平均を新しい値とする。
  2. ガウシアンフィルタ (cv2.GaussianBlur): カーネル内のピクセルに対し、中心に近いほど重みをつけて平均を計算する。ノイズ除去に広く使われる。
  3. メディアンフィルタ (cv2.medianBlur): カーネル内のピクセル値の中央値を新しい値とする。特に「ソルト&ペッパーノイズ」(白い点と黒い点)の除去に効果的。
  4. バイラテラルフィルタ (cv2.bilateralFilter): 画素値の差も考慮して平滑化を行うため、エッジを保持しながらノイズを低減できる。計算コストは高い。

ここでは、ガウシアンフィルタとメディアンフィルタの例を示します。

“`python
import cv2

画像を読み込む (ノイズがある画像を想定)

image_path = ‘lena.jpg’
img = cv2.imread(image_path)

if img is None:
print(f”エラー: 画像ファイル ‘{image_path}’ を読み込めませんでした。”)
else:
# ガウシアンフィルタを適用
# cv2.GaussianBlur(src, ksize, sigmaX, sigmaY=0, borderType=cv2.BORDER_DEFAULT)
# src: 入力画像
# ksize: ガウシアンカーネルのサイズ (幅, 高さ) のタプル。幅と高さは奇数で正の値である必要あり。例: (5, 5)
# sigmaX: x方向のガウシアン標準偏差。0を指定すると ksize から自動計算される。
# sigmaY: y方向のガウシアン標準偏差。通常 sigmaX と同じか 0 を指定。
gaussian_blurred = cv2.GaussianBlur(img, (5, 5), 0) # 5×5カーネルでガウシアンブラー

# メディアンフィルタを適用
# cv2.medianBlur(src, ksize)
# src: 入力画像
# ksize: メディアンフィルタのカーネルサイズ。1より大きい奇数である必要あり。例: 5
median_blurred = cv2.medianBlur(img, 5) # 5x5領域の中央値フィルタ

# 結果を表示
cv2.imshow('Original Image', img)
cv2.imshow('Gaussian Blurred (5x5)', gaussian_blurred)
cv2.imshow('Median Blurred (ksize=5)', median_blurred)


print("平滑化された画像が表示されました。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

“`

  • 平滑化は、エッジ検出などの後続の処理において、ノイズの影響を減らすための前処理としてよく行われます。
  • cv2.GaussianBlurksize(幅, 高さ)のタプルで指定し、両方とも正の奇数である必要があります。sigmaX(およびsigmaY)はガウス分布の広がりを制御し、大きいほどより強くぼかされます。sigmaX=0と指定すると、ksizeから自動的に計算されます。
  • cv2.medianBlurksizeは単一の整数で指定し、正の奇数である必要があります。これはksizexksizeの正方形領域を表します。

エッジ検出 (cv2.Canny)

エッジ検出は、画像の輝度値が急激に変化する場所(つまり、物体の境界線など)を見つける処理です。OpenCVで最も一般的に使われるエッジ検出アルゴリズムはCannyエッジ検出器です。

Cannyエッジ検出は、以下のステップで構成されます。

  1. ノイズ低減: 通常、ガウシアンフィルタを適用してノイズを減らす。
  2. 勾配計算: Sobelフィルタなどで画像の輝度勾配(変化の方向と大きさ)を計算する。
  3. 非最大抑制 (Non-maximum Suppression): 勾配の方向において、勾配の大きさがピークでないピクセルを非エッジとして除外する。
  4. ヒステリシス閾値処理 (Hysteresis Thresholding): 2つの閾値(上限と下限)を使用して、エッジを決定する。勾配が大きいピクセルが上限閾値を超えていれば「強いエッジ」、下限閾値は超えているが上限閾値は超えない場合は「弱いエッジ」とする。弱いエッジは、強いエッジと連結している場合にのみエッジとみなされる。これにより、連続したエッジを抽出できる。

cv2.Canny()関数は、これらのステップをまとめて実行してくれます。

“`python
import cv2

グレースケール画像を読み込む

image_path = ‘lena.jpg’
img_gray = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

if img_gray is None:
print(f”エラー: 画像ファイル ‘{image_path}’ を読み込めませんでした。”)
else:
# Cannyエッジ検出を適用
# cv2.Canny(image, threshold1, threshold2, apertureSize=3, L2gradient=False)
# image: 入力画像 (通常はグレースケール)
# threshold1: ヒステリシス閾値処理の下限閾値
# threshold2: ヒステリシス閾値処理の上限閾値 (通常 threshold1 より大きくする)
# apertureSize: Sobelオペレータのサイズ (3, 5, 7のいずれか)
# L2gradient: 勾配の大きさを計算する方法。Trueだとより正確だが遅い。デフォルトはFalse (L1ノルム)。

# 適切な閾値は画像によって異なるため、調整が必要
edges = cv2.Canny(img_gray, 100, 200) # 下限100、上限200でエッジ検出

# 結果を表示
cv2.imshow('Original Grayscale', img_gray)
cv2.imshow('Canny Edges', edges)

print("エッジが検出された画像が表示されました。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

# 閾値を変えて試してみる
edges_loose = cv2.Canny(img_gray, 50, 150) # より多くのエッジが検出される可能性がある
edges_strict = cv2.Canny(img_gray, 150, 250) # より少ない、強いエッジだけが検出される可能性がある

cv2.imshow('Canny Edges (Loose Threshold)', edges_loose)
cv2.imshow('Canny Edges (Strict Threshold)', edges_strict)
print("異なる閾値での結果が表示されました。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

“`

  • cv2.Canny関数はグレースケール画像を引数として取ります。カラー画像の場合は事前にcv2.COLOR_BGR2GRAYで変換してください。
  • threshold1threshold2はヒステリシス閾値処理における重要なパラメータです。これらの値の選択によって、検出されるエッジの量や質が大きく変化します。一般的には、threshold1 < threshold2 となるように設定します。最適な閾値は画像の内容によって異なるため、試行錯誤して調整する必要があります。
  • cv2.Cannyの出力は、検出されたエッジが白(255)、それ以外が黒(0)の単一チャンネルのグレースケール画像(または二値画像)になります。

カラーフィルタリング (cv2.inRange)

特定の色の範囲を持つ領域を画像から抽出(フィルタリング)する処理です。これは、特定の色の物体を検出したい場合などに非常に有効です。通常、HSV色空間で行われます。

“`python
import cv2
import numpy as np

画像を読み込む (カラーで)

image_path = ‘color_test.jpg’ # 特定の色を持つ画像を準備してください (例: 青いボールなど)
img_bgr = cv2.imread(image_path, cv2.IMREAD_COLOR)

if img_bgr is None:
print(f”エラー: 画像ファイル ‘{image_path}’ を読み込めませんでした。”)
else:
# BGR画像をHSV色空間に変換
img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)

# 検出したい色のHSV範囲を定義
# 例: 青色の範囲を定義
# H: 色相, S: 彩度, V: 明度
# HSVの値の範囲はライブラリや画像によって異なる場合がある (OpenCVのuint8では H:0-180, S:0-255, V:0-255)
# 以下の値はあくまで例です。検出したい色に合わせて調整が必要です。
# オンラインツールやOpenCVのGUIツールを使って目的の色のHSV値を確認すると良い。
lower_blue = np.array([100, 50, 50])  # 青色の下限 (H, S, V)
upper_blue = np.array([140, 255, 255]) # 青色の上限 (H, S, V)

# 定義したHSV範囲内のピクセルを抽出してマスク画像を生成
# cv2.inRange(src, lowerb, upperb)
# src: 入力画像 (通常HSV画像)
# lowerb: 下限の境界値 (NumPy配列またはリスト/タプル)
# upperb: 上限の境界値 (NumPy配列またはリスト/タプル)
# 戻り値: マスク画像 (範囲内のピクセルが白(255), それ以外が黒(0)の単一チャンネル画像)
mask_blue = cv2.inRange(img_hsv, lower_blue, upper_blue)

# 元画像とマスク画像を使って、青色の領域だけを抽出
# cv2.bitwise_and(src1, src2, mask=mask)
# src1, src2: 入力画像 (今回は同じ元画像 img_bgr を使う)
# mask: マスク画像。マスクが0でないピクセルに対応する src1 のピクセルのみが出力される。
blue_objects = cv2.bitwise_and(img_bgr, img_bgr, mask=mask_blue)

# 結果を表示
cv2.imshow('Original Image', img_bgr)
cv2.imshow('Blue Mask', mask_blue)
cv2.imshow('Extracted Blue Objects', blue_objects)

print("カラーフィルタリングの結果が表示されました。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

“`

  • 特定の色を抽出するには、まず画像をHSV色空間に変換するのが一般的です。これは、HSVが人間の色の感じ方に近く、色相(H)が明るさや彩度から分離されているため、照明条件の変化に比較的強いフィルタリングが可能だからです。
  • cv2.inRange()関数は、入力画像(通常HSV)の各ピクセルが指定された下限値と上限値の範囲内に収まっているかどうかをチェックし、その結果に基づいてマスク画像を生成します。範囲内のピクセルはマスク画像で255、それ以外は0となります。
  • 生成されたマスク画像を使って、cv2.bitwise_and()のようなビット演算関数で元画像をフィルタリングします。cv2.bitwise_and(img, img, mask=mask)とすることで、マスク画像が白(255)のピクセルに対応する元画像のピクセルだけが抽出され、マスク画像が黒(0)のピクセルに対応する元画像のピクセルは0(黒)になります。

注意点: 検出したい色のHSV範囲を正確に知る必要があります。これは画像や環境によって調整が必要な場合があります。カラーピッカーツールや、OpenCVのマウスイベントを使って特定のピクセルのHSV値を取得するスクリプトを作成すると、範囲決定の助けになります。

輪郭の検出と描画 (cv2.findContours, cv2.drawContours)

輪郭(Contours)は、同じ色や輝度を持つ連続した点の曲線で、物体の境界を表します。輪郭検出は、画像内の物体の形状を分析したり、物体を識別したりするために非常に重要です。

輪郭検出を行う前に、画像は通常、二値画像(白黒画像)である必要があります。閾値処理やCannyエッジ検出の結果がよく使われます。

“`python
import cv2
import numpy as np

グレースケール画像を読み込む

image_path = ‘shape_test.png’ # シンプルな図形 (円、四角など) が書かれた白背景/黒背景の画像を準備
img_gray = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

if img_gray is None:
print(f”エラー: 画像ファイル ‘{image_path}’ を読み込めませんでした。”)
else:
# 輪郭検出は二値画像に対して行うため、閾値処理を適用
# 白背景に黒い図形の場合:
ret, thresh = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY_INV) # 図形部分を白(255)にするため INV を使う

# 黒背景に白い図形の場合:
# ret, thresh = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY) # 図形部分を白(255)にする

# 輪郭を検出
# cv2.findContours(image, mode, method)
# image: 入力二値画像 (OpenCV 4.0以降では元の画像を変更しない)
# mode: 輪郭の取得モード
#   cv2.RETR_EXTERNAL: 最も外側の輪郭のみを取得 (穴の輪郭は無視)
#   cv2.RETR_LIST: 全ての輪郭を取得し、階層構造は考慮しない
#   cv2.RETR_CCOMP: 全ての輪郭を取得し、2レベルの階層構造を構築 (外側の輪郭と内側の穴の輪郭)
#   cv2.RETR_TREE: 全ての輪郭を取得し、完全な階層構造を構築
# method: 輪郭の近似方法
#   cv2.CHAIN_APPROX_NONE: 全ての輪郭点を格納する
#   cv2.CHAIN_APPROX_SIMPLE: 水平、垂直、斜めの線分の端点のみを圧縮して格納。メモリを節約できる。
# 戻り値 (OpenCVのバージョンによって異なる場合がある): contours, hierarchy
#   contours: 検出された輪郭のリスト。各輪郭は Numpy 配列 ([x, y]座標のリスト)
#   hierarchy: 各輪郭の親子関係を表す階層構造情報

# OpenCV 4.0以降の場合の戻り値の受け取り方
# 実際には img_gray を thresh に置き換えて実行
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 検出された輪郭を元の画像に描画
# カラー画像をコピーして描画するのが一般的 (元のグレースケール画像は1チャンネルなのでカラーで描画できない)
img_bgr = cv2.imread(image_path, cv2.IMREAD_COLOR)
if img_bgr is None: # カラー画像読み込み失敗の場合の代替
     img_bgr = cv2.cvtColor(img_gray, cv2.COLOR_GRAY2BGR) # グレースケールをBGRに変換

# cv2.drawContours(image, contours, contourIdx, color, thickness, lineType, hierarchy, maxLevel, offset)
# image: 描画対象の画像 (カラー画像が一般的)
# contours: 輪郭のリスト (findContoursの戻り値)
# contourIdx: 描画する輪郭のインデックス。-1を指定すると全ての輪郭を描画。
# color: 描画する色 (BGRタプル)
# thickness: 線の太さ (-1で塗りつぶし)

# 全ての輪郭を緑色で描画
cv2.drawContours(img_bgr, contours, -1, (0, 255, 0), 2) # 緑色の線、太さ2

# 個別の輪郭を描画する例 (最初の輪郭を赤色で描画)
# if len(contours) > 0:
#     cv2.drawContours(img_bgr, contours, 0, (0, 0, 255), 3)

# 輪郭の数を出力
print(f"検出された輪郭の数: {len(contours)}")

# 結果を表示
cv2.imshow('Original Grayscale', img_gray)
cv2.imshow('Thresholded', thresh)
cv2.imshow('Contours Detected', img_bgr)

print("輪郭検出と描画の結果が表示されました。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

# 輪郭のプロパティ (例: 面積、周囲長)
print("\n--- 輪郭のプロパティ ---")
for i, contour in enumerate(contours):
    # 輪郭の面積を計算
    area = cv2.contourArea(contour)
    # 輪郭の周囲長を計算 (Trueで閉じた輪郭を仮定)
    perimeter = cv2.arcLength(contour, True)
    print(f"輪郭 {i}: 面積={area:.2f}, 周囲長={perimeter:.2f}")

“`

  • cv2.findContours()関数は、入力の二値画像(通常8ビット単一チャンネル画像)から輪郭を検出します。この関数はOpenCV 4.0以降で戻り値の順番が変わったため注意が必要です。古いバージョンではimage, contours, hierarchy = cv2.findContours(...)の順でした。
  • mode引数は、輪郭をどのように取得し、輪郭間の階層構造をどのように扱うかを指定します。cv2.RETR_TREEは最も詳細な階層情報を取得します。
  • method引数は、輪郭の点のリストをどのように表現するかを指定します。cv2.CHAIN_APPROX_SIMPLEは、矩形の輪郭であれば4点のみ、円形であれば多くの点といったように、輪郭の形状を近似して必要な点だけを格納するため、メモリ効率が良いです。cv2.CHAIN_APPROX_NONEは全ての点を格納します。
  • cv2.findContours()の戻り値contoursは、検出された個々の輪郭を表すNumPy配列のリストです。各配列は輪郭上の点の座標([x, y]の形式)のリストです。
  • cv2.drawContours()関数は、指定した画像上に検出された輪郭を描画します。通常、元のカラー画像をコピーして、その上に描画します。contourIdx-1を指定すると全ての輪郭を描画し、特定のインデックスを指定するとその輪郭だけを描画します。
  • 輪郭が検出された後、cv2.contourArea()で輪郭が囲む領域の面積を、cv2.arcLength()で輪郭の周囲長を計算できます。これらのプロパティは、物体のサイズや形状を分析するのに役立ちます。

ちょっとした応用例:特定の色の物体を検出し、輪郭を描画する

これまでに学んだ「カラーフィルタリング」と「輪郭検出」を組み合わせることで、「画像の中から特定の色の物体を見つけ出し、その周囲に枠線を描く」といった応用が可能になります。

“`python
import cv2
import numpy as np

画像を読み込む (カラーで)

image_path = ‘color_and_shape.jpg’ # 背景が白で、青い物体がいくつか写っている画像を準備
img_bgr = cv2.imread(image_path, cv2.IMREAD_COLOR)

if img_bgr is None:
print(f”エラー: 画像ファイル ‘{image_path}’ を読み込めませんでした。”)
else:
# 1. BGR画像をHSV色空間に変換
img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)

# 2. 検出したい色 (例: 青色) のHSV範囲を定義
# 実際の画像に合わせてこれらの値を調整してください
lower_blue = np.array([100, 50, 50])
upper_blue = np.array([140, 255, 255])

# 3. HSV範囲に基づいてマスク画像を生成
mask = cv2.inRange(img_hsv, lower_blue, upper_blue)

# 4. 生成したマスク画像に対して輪郭を検出
# マスク画像は単一チャンネルの二値画像なので、直接 findContours に渡せる
# RETR_EXTERNAL で外側の輪郭のみ取得
# CHAIN_APPROX_SIMPLE で輪郭点を圧縮
contours, hierarchy = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Note: findContours は入力画像を変更することがあるため、mask.copy() を渡すのが安全です。

# 5. 元画像 (カラー) のコピーに検出された輪郭を描画
output_img = img_bgr.copy()
# 全ての輪郭を緑色で描画
cv2.drawContours(output_img, contours, -1, (0, 255, 0), 3) # 太さ3の緑色の線

# 6. 結果を表示
cv2.imshow('Original Image', img_bgr)
cv2.imshow('Blue Mask', mask)
cv2.imshow('Detected Blue Objects', output_img)


print("青色の物体検出と輪郭描画の結果が表示されました。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

# さらに、検出された各物体に対して処理を行う例
# 例えば、面積が一定以上の輪郭に対してだけ処理を行うなど
print("\n--- 検出された青色物体の情報 ---")
min_area = 500 # 最小面積の閾値 (ノイズによる小さな輪郭を除去するため)
for i, contour in enumerate(contours):
    area = cv2.contourArea(contour)
    if area > min_area:
        print(f"物体 {i}: 面積={area:.2f}")

        # 物体を囲む外接矩形を描画する例
        x, y, w, h = cv2.boundingRect(contour) # 外接矩形の座標と幅・高さを取得
        cv2.rectangle(output_img, (x, y), (x+w, y+h), (255, 0, 0), 2) # 青い矩形を描画

        # 物体の中心座標を計算する例 (モーメントを使用)
        M = cv2.moments(contour) # 輪郭のモーメントを計算
        if M["m00"] > 0: # 面積が0でない場合
            cx = int(M["m10"] / M["m00"])
            cy = int(M["m01"] / M["m00"])
            cv2.circle(output_img, (cx, cy), 5, (0, 0, 255), -1) # 中心に赤い円を描画
            print(f"  中心座標: ({cx}, {cy})")

# 検出された物体情報と外接矩形、中心が描画された画像を表示
cv2.imshow('Detected Blue Objects (with details)', output_img)
print("検出された物体の詳細が描画された画像が表示されました。何かキーを押してください。")
cv2.waitKey(0)
cv2.destroyAllWindows()

“`

この応用例は、多くの基本的な画像処理タスクの出発点となります。カラーフィルタリングで対象を絞り込み、輪郭検出で個々の物体を特定し、輪郭のプロパティ(面積、形状、外接矩形など)を使って物体を分析・識別することができます。

さらに学ぶために

本記事では、OpenCVの基本的な使い方として、画像の読み込み、表示、保存から、基本的な画像変換、描画、イベント処理、そして閾値処理、平滑化、エッジ検出、カラーフィルタリング、輪郭検出といった基礎的な画像処理手法を網羅しました。これらはOpenCVのほんの一部ですが、画像処理の様々なタスクをこなすための強力な基盤となります。

OpenCVライブラリは非常に広範な機能を持っています。さらに学習を進めるにあたっては、以下のトピックを探求することをお勧めします。

  • 形態学的変換 (Morphological Transformations): 画像の形状に基づいた操作(膨張、収縮、オープニング、クロージングなど)。ノイズ除去や物体の分離/結合に役立ちます。
  • ヒストグラム: 画像の画素値の分布。画像の輝度調整(ヒストグラム平坦化など)や比較に利用されます。
  • テンプレートマッチング: 画像の一部(テンプレート)を大きな画像の中から検出する手法。
  • 特徴点検出と記述: 画像内の特徴的な点(コーナーなど)を見つけ出し、その周辺の情報を数値化する。画像間の対応付けや物体認識に使われます(例: SIFT, SURF, ORB)。
  • 物体検出と認識: 画像内に写っている物体が何か、どこにあるかを特定する(例: Haar Cascades, HOG + SVM, YOLO, SSDなどの深層学習モデル)。
  • 動画処理: カメラからの入力や動画ファイルを処理する。
  • カメラキャリブレーションと3次元復元: カメラの歪みを補正したり、複数の画像から3次元情報を得たりする。
  • 機械学習(mlモジュール、dnnモジュール): サポートベクトルマシン(SVM)やK近傍法などの伝統的な機械学習アルゴリズムや、CNNなどの深層学習モデルを使った分類や検出。

OpenCVの公式ドキュメント(特にPythonチュートリアル)は非常に充実しており、これらの高度なトピックについても学ぶことができます。また、GitHub上には多くのOpenCVを使ったプロジェクトが公開されていますので、実際のコードを見て学ぶのも良いでしょう。

まとめ

本記事では、PythonでOpenCVを使った画像処理の入門として、以下の内容を詳細に解説しました。

  • OpenCVとは何か、Pythonで使うメリット
  • 開発環境のセットアップ方法
  • 画像の読み込み、表示、保存の基本 (cv2.imread, cv2.imshow, cv2.imwrite)
  • 画像データのNumPy配列としての扱い、プロパティ、ピクセルアクセス
  • 色空間の変換 (cv2.cvtColor)
  • 基本的な画像変換(リサイズ、移動、回転、反転、クロッピング) (cv2.resize, cv2.warpAffine, cv2.flip, NumPyスライシング)
  • 画像への図形やテキストの描画 (cv2.line, cv2.rectangle, cv2.circle, cv2.putText)
  • マウスイベントとキーボードイベントの処理 (cv2.waitKey, cv2.setMouseCallback)
  • 基本的な画像処理手法(閾値処理、平滑化、エッジ検出、カラーフィルタリング) (cv2.threshold, cv2.GaussianBlur, cv2.medianBlur, cv2.Canny, cv2.inRange)
  • 輪郭の検出と描画 (cv2.findContours, cv2.drawContours)
  • 応用例として、特定色の物体検出と輪郭描画

OpenCVは非常に強力で多機能なライブラリであり、画像処理の世界への扉を開く鍵となります。ぜひ、本記事で紹介したコードを実際に動かしてみて、パラメータを変えたり、自分の画像で試したりしながら、OpenCVを使った画像処理の面白さを体験してください。そして、さらに高度な画像処理技術や応用分野へと学びを進めていくことを願っています。

画像処理の旅は始まったばかりです。楽しみながら、一歩ずつ進んでいきましょう!


コメントする

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

上部へスクロール