STEP 3:OpenCVによる画像処理

🔧 STEP 3: OpenCVによる画像処理

フィルタリング、エッジ検出、モルフォロジー変換、色検出など
OpenCVの実践的な技術を習得します

📋 このステップで学ぶこと

  • OpenCVの基礎と基本的な画像操作
  • フィルタリング(ブラー、シャープニング、ガウシアン)
  • エッジ検出(Sobel、Canny、Laplacian)
  • モルフォロジー変換(膨張、収縮、オープニング、クロージング)
  • 輪郭検出(findContours、drawContours)
  • 色検出とトラッキング(HSV色空間の活用)

🔧 1. OpenCVとは何か?

OpenCVは、「コンピュータに目を与える」ためのライブラリです。

名前の由来は「Open Source Computer Vision Library」の略で、画像や動画を処理するための機能が2500以上も詰まっています。

1-1. OpenCVが選ばれる理由

🚀 OpenCVの5つの強み

① 高速:C++で書かれており、Pythonから呼び出しても非常に速い
② 無料:オープンソースで商用利用もOK
③ 豊富な機能:2500以上のアルゴリズム(フィルタ、検出、追跡など)
④ 実績:4700万回以上ダウンロードされた業界標準
⑤ 互換性:Windows、Mac、Linux、スマホ(Android/iOS)で動作

1-2. OpenCVのインストール

Google Colabを使う場合、OpenCVは最初から入っています。ローカル環境の場合は以下でインストールします。

# OpenCVの基本パッケージをインストール pip install opencv-python # 追加機能(SIFT、SURFなど)が必要な場合 pip install opencv-contrib-python

バージョンの確認

# OpenCVが正しくインストールされているか確認 import cv2 print(cv2.__version__) # 例:4.8.0

1-3. 基本操作の復習(STEP 2の振り返り)

STEP 2で学んだ内容を簡単に復習します。OpenCVでの画像操作の基本パターンです。

# 基本パターン:読み込み → 変換 → 処理 → 表示/保存 import cv2 import numpy as np import matplotlib.pyplot as plt # 1. 画像読み込み(BGRで読み込まれる) img = cv2.imread(‘sample.jpg’) # 2. BGR → RGB変換(表示用) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 3. 画像情報の確認 print(f”形状: {img.shape}”) # (高さ, 幅, チャンネル) print(f”データ型: {img.dtype}”) # uint8 # 4. matplotlibで表示 plt.imshow(img_rgb) plt.axis(‘off’) plt.show() # 5. 画像保存 cv2.imwrite(‘output.jpg’, img) # BGRのまま保存
⚠️ OpenCVの注意点(STEP 2の復習)

OpenCVはBGR順序で画像を読み込みます。matplotlibで表示する場合は、必ずRGB順序に変換してください。変換を忘れると、赤と青が入れ替わった画像が表示されます。

🌫️ 2. フィルタリング – 画像のノイズを取り除く

フィルタリングとは、画像に対して「処理のフィルター」をかけることです。

カメラで撮影した画像には、どうしてもノイズ(ざらつき)が入ります。フィルタリングを使うと、このノイズを除去したり、逆にエッジ(輪郭)を強調したりできます。

2-1. ブラー(ぼかし)の仕組み

ブラー(ぼかし)は、画像を意図的にぼやけさせる処理です。なぜぼかすのでしょうか?

🎯 ブラーを使う理由

① ノイズ除去:細かいざらつきを平均化して消す
② エッジ検出の前処理:ノイズをエッジと誤検出しないようにする
③ 背景ぼかし:ポートレート写真のように背景をぼかす
④ プライバシー保護:顔や情報を隠す

カーネルとは?

フィルタリングでは「カーネル」という小さな窓を使います。この窓を画像の上で動かしながら、窓内のピクセル値を計算して新しい値を作ります。

【カーネルのイメージ:3×3の場合】 画像上の1ピクセルを処理するとき、 周囲3×3(合計9ピクセル)を見る [120] [130] [125] [128] [■] [132] → 9ピクセルの平均 = 127 [121] [126] [129] ■の位置に127が入る(平均値ブラーの場合)

2-2. 4種類のブラーを使い分ける

① 平均値ブラー(Average Blur)

最もシンプルなブラー。カーネル内のピクセルの平均値を計算します。

# cv2.blur(画像, カーネルサイズ) # カーネルサイズ:(幅, 高さ) – 奇数で指定 img_blur = cv2.blur(img, (5, 5)) # (5, 5)は「5×5ピクセルの範囲で平均を取る」という意味 # 数字が大きいほど、強くぼかす

② ガウシアンブラー(Gaussian Blur)- 最もよく使う

最も一般的に使われるブラーです。単純な平均ではなく、中心に近いピクセルほど重要度(重み)を高くして計算します。

# cv2.GaussianBlur(画像, カーネルサイズ, シグマ) # シグマ:ぼかしの強さ(0を指定するとカーネルサイズから自動計算) img_gaussian = cv2.GaussianBlur(img, (5, 5), 0) # なぜガウシアンが人気? # → 自然なぼかしになる(中心を重視するため) # → エッジ検出の前処理として最適

③ メディアンブラー(Median Blur)- ノイズに強い

カーネル内のピクセルを並べ替えて、中央値を取ります。「ごま塩ノイズ」(点々のノイズ)の除去に特に効果的です。

# cv2.medianBlur(画像, カーネルサイズ) # カーネルサイズは奇数の整数1つ(5なら5×5) img_median = cv2.medianBlur(img, 5) # なぜメディアンがノイズに強い? # → 極端に明るい/暗いピクセル(ノイズ)は中央値から外れる # → ノイズの影響を受けにくい

④ バイラテラルフィルタ – エッジを保護

エッジ(輪郭)を残しながらノイズを除去する高品質なフィルタ。肌の加工などに使われます。ただし処理が遅いです。

# cv2.bilateralFilter(画像, d, sigmaColor, sigmaSpace) # d:カーネル直径(-1で自動) # sigmaColor:色の類似度を判断する範囲 # sigmaSpace:距離の影響範囲 img_bilateral = cv2.bilateralFilter(img, 9, 75, 75) # エッジ(色が大きく変わる場所)は保持される # → 肌は滑らかに、目や口の輪郭はくっきり

2-3. ブラーの比較コード(実行用)

以下のコードで4種類のブラーを比較できます。

※ コードが横に長い場合は横スクロールできます

# 各種ブラーを比較するコード import cv2 import numpy as np import matplotlib.pyplot as plt # サンプル画像をダウンロード import urllib.request url = “https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg” urllib.request.urlretrieve(url, “sample.jpg”) # 画像読み込み img = cv2.imread(‘sample.jpg’) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 4種類のブラーを適用 img_average = cv2.blur(img, (9, 9)) img_gaussian = cv2.GaussianBlur(img, (9, 9), 0) img_median = cv2.medianBlur(img, 9) img_bilateral = cv2.bilateralFilter(img, 9, 75, 75) # BGR → RGB変換 img_average_rgb = cv2.cvtColor(img_average, cv2.COLOR_BGR2RGB) img_gaussian_rgb = cv2.cvtColor(img_gaussian, cv2.COLOR_BGR2RGB) img_median_rgb = cv2.cvtColor(img_median, cv2.COLOR_BGR2RGB) img_bilateral_rgb = cv2.cvtColor(img_bilateral, cv2.COLOR_BGR2RGB) # 2×2で表示 fig, axes = plt.subplots(2, 2, figsize=(12, 12)) axes[0, 0].imshow(img_average_rgb) axes[0, 0].set_title(‘Average Blur (9×9)’) axes[0, 0].axis(‘off’) axes[0, 1].imshow(img_gaussian_rgb) axes[0, 1].set_title(‘Gaussian Blur (9×9)’) axes[0, 1].axis(‘off’) axes[1, 0].imshow(img_median_rgb) axes[1, 0].set_title(‘Median Blur (9)’) axes[1, 0].axis(‘off’) axes[1, 1].imshow(img_bilateral_rgb) axes[1, 1].set_title(‘Bilateral Filter’) axes[1, 1].axis(‘off’) plt.tight_layout() plt.show()

2-4. シャープニング – 画像を鮮明にする

ブラーとは逆に、画像を鮮明(シャープ)にする処理です。ぼやけた写真をくっきりさせたいときに使います。

シャープニングの仕組み

シャープニングは、カーネル(フィルタ)を使ってエッジ(輪郭)を強調します。

【シャープニングカーネルの例】 標準的なシャープニングカーネル: [[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]] 中心が9、周囲が-1 → 中心のピクセルを強調し、周囲との差を際立たせる

シャープニングの実装

# シャープニングカーネルを定義 # np.arrayで2次元配列として作成 kernel_sharpen = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]]) # cv2.filter2D(画像, 出力の深度, カーネル) # -1:入力と同じ深度を使用 img_sharpened = cv2.filter2D(img, -1, kernel_sharpen)

より控えめなシャープニング

# 強すぎるときは、より穏やかなカーネルを使う kernel_sharpen_light = np.array([[ 0, -1, 0], [-1, 5, -1], [ 0, -1, 0]]) img_sharpen_light = cv2.filter2D(img, -1, kernel_sharpen_light) # 中心が5(標準は9)なので、効果が穏やか
🎯 ブラーとシャープニングの使い分け

ガウシアンブラー:ノイズ除去、エッジ検出の前処理
メディアンブラー:点々のノイズ(ごま塩ノイズ)除去
バイラテラル:肌の加工、エッジを保持したい場合
シャープニング:ぼやけた写真を鮮明に

📐 3. エッジ検出 – 輪郭を見つける

エッジ検出は、画像の中で「明るさが急に変わる場所」を見つける技術です。物体の輪郭を抽出するときに使います。

3-1. エッジとは何か?

📏 エッジ = 明るさが急に変わる場所

例えば、白い紙の上に黒いペンで線を引くと、紙(明るい)とペン(暗い)の境界がエッジになります。

【エッジのイメージ】 明るさの変化: ■■■□□□□□ ← 黒から白への急な変化 = エッジ ↑ ここがエッジ グラフで表すと: 明るさ ↑ | ___ | / |__/ ← 急な傾斜の場所 = エッジ +——-→ 位置

3-2. Sobelフィルタ – 方向別エッジ検出

Sobelフィルタは、縦方向横方向のエッジを別々に検出できます。

ステップ1:グレースケールに変換

# エッジ検出は通常グレースケールで行う # なぜ? → 色情報は不要、明るさの変化だけを見たいから gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

ステップ2:X方向(縦のエッジ)を検出

# cv2.Sobel(画像, 出力深度, X方向の微分, Y方向の微分, カーネルサイズ) # X方向の微分 = 縦のエッジを検出 sobel_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3) # cv2.CV_64F:64ビット浮動小数点(負の値も保持するため) # 1, 0:X方向に1回微分、Y方向は微分しない # ksize=3:3×3カーネルを使用

ステップ3:Y方向(横のエッジ)を検出

# Y方向の微分 = 横のエッジを検出 sobel_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3) # 0, 1:X方向は微分しない、Y方向に1回微分

ステップ4:XとYを合成

# X方向とY方向を合成する方法 # 方法1:絶対値を取って加算 sobel_x_abs = cv2.convertScaleAbs(sobel_x) # 絶対値 + uint8変換 sobel_y_abs = cv2.convertScaleAbs(sobel_y) sobel_combined = cv2.addWeighted(sobel_x_abs, 0.5, sobel_y_abs, 0.5, 0) # 方法2:二乗和の平方根(より正確) # sobel_combined = np.sqrt(sobel_x**2 + sobel_y**2) # sobel_combined = np.uint8(sobel_combined)

3-3. Cannyエッジ検出 – 最も人気のある方法

Cannyエッジ検出は、実務で最もよく使われるエッジ検出手法です。OpenCVでは1行で実行できます。

🌟 Cannyが優れている4つの理由

① ノイズに強い:内部でガウシアンブラーを適用
② 高精度:Sobelでエッジの方向と強度を計算
③ 細い線:Non-Maximum Suppressionで1ピクセル幅に
④ 連続性:ヒステリシス閾値処理でエッジがつながる

Cannyエッジ検出の使い方

# cv2.Canny(画像, 低い閾値, 高い閾値) # グレースケール画像を準備 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Cannyエッジ検出 edges = cv2.Canny(gray, 100, 200) # 低い閾値(100):これ以下はエッジでない # 高い閾値(200):これ以上は確実にエッジ # その間:強いエッジにつながっていればエッジ

推奨:ブラーを前処理として追加

# より良い結果のために、事前にブラーをかける gray_blur = cv2.GaussianBlur(gray, (5, 5), 0) edges = cv2.Canny(gray_blur, 50, 150) # ブラーでノイズを減らしてからCannyを適用 # → よりきれいなエッジが得られる

閾値の決め方

🎯 Cannyの閾値ガイドライン

低い閾値:50〜100(これ以下の弱いエッジは無視)
高い閾値:150〜250(これ以上の強いエッジは確実に採用)
比率:高い閾値 = 低い閾値 × 2〜3 が目安

調整のコツ:
・エッジが少なすぎる → 閾値を下げる
・ノイズが多すぎる → 閾値を上げる

3-4. Laplacianフィルタ

Laplacianは、全方向のエッジを一度に検出します。ただしノイズに弱いので、事前にブラーが必要です。

# Laplacianフィルタ laplacian = cv2.Laplacian(gray, cv2.CV_64F) laplacian = np.uint8(np.absolute(laplacian)) # CV_64F:64ビット浮動小数点 # np.absolute:絶対値を取る(負の値を正に) # np.uint8:0-255の整数に変換

3-5. エッジ検出の比較コード(実行用)

# 3種類のエッジ検出を比較するコード import cv2 import numpy as np import matplotlib.pyplot as plt # サンプル画像をダウンロード import urllib.request url = “https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg” urllib.request.urlretrieve(url, “sample.jpg”) # 画像読み込み・グレースケール変換 img = cv2.imread(‘sample.jpg’) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # ブラーで前処理 gray_blur = cv2.GaussianBlur(gray, (5, 5), 0) # Sobel(X方向とY方向を合成) sobel_x = cv2.Sobel(gray_blur, cv2.CV_64F, 1, 0, ksize=3) sobel_y = cv2.Sobel(gray_blur, cv2.CV_64F, 0, 1, ksize=3) sobel_x_abs = cv2.convertScaleAbs(sobel_x) sobel_y_abs = cv2.convertScaleAbs(sobel_y) sobel = cv2.addWeighted(sobel_x_abs, 0.5, sobel_y_abs, 0.5, 0) # Canny canny = cv2.Canny(gray_blur, 50, 150) # Laplacian laplacian = cv2.Laplacian(gray_blur, cv2.CV_64F) laplacian = np.uint8(np.absolute(laplacian)) # 2×2で表示 fig, axes = plt.subplots(2, 2, figsize=(12, 12)) axes[0, 0].imshow(gray, cmap=’gray’) axes[0, 0].set_title(‘Original (Grayscale)’) axes[0, 0].axis(‘off’) axes[0, 1].imshow(sobel, cmap=’gray’) axes[0, 1].set_title(‘Sobel (X + Y)’) axes[0, 1].axis(‘off’) axes[1, 0].imshow(canny, cmap=’gray’) axes[1, 0].set_title(‘Canny’) axes[1, 0].axis(‘off’) axes[1, 1].imshow(laplacian, cmap=’gray’) axes[1, 1].set_title(‘Laplacian’) axes[1, 1].axis(‘off’) plt.tight_layout() plt.show()
手法 特徴 メリット デメリット
Sobel X方向、Y方向を個別に検出 方向別の分析が可能 ノイズに敏感
Canny 最も一般的な手法 高精度、細い線、連続性 閾値の調整が必要
Laplacian 全方向を一度に検出 シンプルで高速 ノイズに非常に敏感

🔲 4. モルフォロジー変換 – 形を操作する

モルフォロジー変換は、バイナリ画像(白黒2値の画像)の形を変える処理です。「モルフォロジー」は「形態学」という意味です。

4-1. バイナリ画像とは?

モルフォロジー変換を適用する前に、画像をバイナリ画像(白黒2値)に変換する必要があります。

# バイナリ画像の作成 # グレースケールに変換 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二値化(しきい値127で白黒に分ける) # cv2.threshold(画像, しきい値, 最大値, 方法) _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # しきい値127:これより暗いピクセル → 0(黒) # これより明るいピクセル → 255(白)

4-2. 膨張(Dilation)- 白を膨らませる

膨張は、白い部分を「膨らませる」処理です。

📈 膨張の効果

・白い領域が大きくなる
・黒い小さな穴が埋まる
・離れた白い物体同士がつながる

# カーネル(構造要素)を作成 # np.ones:全て1の配列を作る # (5, 5):5×5のサイズ # np.uint8:8ビット整数型 kernel = np.ones((5, 5), np.uint8) # 膨張を適用 # cv2.dilate(画像, カーネル, 繰り返し回数) dilated = cv2.dilate(binary, kernel, iterations=1) # iterations=1:1回だけ膨張 # iterations=3:3回連続で膨張(より大きく膨らむ)

4-3. 収縮(Erosion)- 白を削る

収縮は、白い部分を「削る」処理です。膨張の逆です。

📉 収縮の効果

・白い領域が小さくなる
・白い小さなノイズが消える
・つながった白い物体同士が分離

# 収縮を適用 # cv2.erode(画像, カーネル, 繰り返し回数) eroded = cv2.erode(binary, kernel, iterations=1) # 白い小さな点(ノイズ)が消える # 白い物体が細くなる

4-4. オープニング(Opening)- ノイズ除去

オープニング = 収縮 → 膨張という2段階の処理です。

【オープニングの動作】 元画像: ●● ・ ●●●● ●● ●●●● ↓ 収縮(小さいものが消える) ・ ●●●● ●●●● ↓ 膨張(元のサイズに戻る) ●●●● ●●●● 結果:小さなノイズ(・)が消えた!
# オープニング # cv2.morphologyEx(画像, 処理タイプ, カーネル) opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 用途:白い小さなノイズを除去

4-5. クロージング(Closing)- 穴埋め

クロージング = 膨張 → 収縮という2段階の処理です。オープニングの逆順です。

【クロージングの動作】 元画像: ●●●● ●・・● (中に黒い穴がある) ●●●● ↓ 膨張(穴が埋まる) ●●●● ●●●● ●●●● ↓ 収縮(元のサイズに戻る) ●●●● ●●●● ●●●● 結果:黒い穴が埋まった!
# クロージング closing = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 用途:白い物体の中の黒い小さな穴を埋める

4-6. モルフォロジー変換の比較コード(実行用)

# モルフォロジー変換を比較するコード import cv2 import numpy as np import matplotlib.pyplot as plt # サンプル画像をダウンロード import urllib.request url = “https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg” urllib.request.urlretrieve(url, “sample.jpg”) # 画像読み込み img = cv2.imread(‘sample.jpg’) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # バイナリ画像に変換 _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # カーネルを定義 kernel = np.ones((5, 5), np.uint8) # 各種モルフォロジー変換 dilated = cv2.dilate(binary, kernel, iterations=2) eroded = cv2.erode(binary, kernel, iterations=2) opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) closing = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 2×3で表示 fig, axes = plt.subplots(2, 3, figsize=(15, 10)) axes[0, 0].imshow(binary, cmap=’gray’) axes[0, 0].set_title(‘Original (Binary)’) axes[0, 0].axis(‘off’) axes[0, 1].imshow(dilated, cmap=’gray’) axes[0, 1].set_title(‘Dilation (膨張)’) axes[0, 1].axis(‘off’) axes[0, 2].imshow(eroded, cmap=’gray’) axes[0, 2].set_title(‘Erosion (収縮)’) axes[0, 2].axis(‘off’) axes[1, 0].imshow(opening, cmap=’gray’) axes[1, 0].set_title(‘Opening (オープニング)’) axes[1, 0].axis(‘off’) axes[1, 1].imshow(closing, cmap=’gray’) axes[1, 1].set_title(‘Closing (クロージング)’) axes[1, 1].axis(‘off’) axes[1, 2].axis(‘off’) # 空きスペース plt.tight_layout() plt.show()
🎯 モルフォロジー変換の使い分け

オープニング:白い小さなノイズを除去したい
クロージング:白い物体の中の黒い穴を埋めたい
膨張:白い物体を大きくしたい、物体同士をつなげたい
収縮:白い物体を小さくしたい、物体同士を分離したい

🔍 5. 輪郭検出 – 物体の形を見つける

輪郭検出は、バイナリ画像から物体の外形(輪郭)を検出する技術です。物体のカウント、形状分析、物体検出の前処理などに使います。

5-1. findContours – 輪郭を見つける

ステップ1:バイナリ画像を準備

# グレースケール → 二値化 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

ステップ2:輪郭を検出

# cv2.findContours(画像, 検索モード, 近似方法) contours, hierarchy = cv2.findContours( binary, cv2.RETR_EXTERNAL, # 外側の輪郭のみ検出 cv2.CHAIN_APPROX_SIMPLE # 輪郭を単純化 ) # 戻り値: # contours:輪郭のリスト(各輪郭は点の配列) # hierarchy:輪郭の階層構造(入れ子関係) # 検出された輪郭数 print(f”検出された輪郭数: {len(contours)}”)

検索モードの種類

検索モード 説明
RETR_EXTERNAL 最も外側の輪郭のみ(よく使う)
RETR_LIST すべての輪郭(階層なし)
RETR_TREE すべての輪郭(階層あり)

5-2. drawContours – 輪郭を描画

# 元の画像をコピー(描画用) img_contours = img.copy() # すべての輪郭を描画 # cv2.drawContours(画像, 輪郭リスト, 輪郭インデックス, 色, 線の太さ) cv2.drawContours(img_contours, contours, -1, (0, 255, 0), 2) # -1:すべての輪郭を描画(特定の輪郭だけ描くなら0, 1, 2…) # (0, 255, 0):緑色(BGR形式) # 2:線の太さ(ピクセル)

5-3. 輪郭の情報を取得

# 各輪郭の面積と周長を計算 for i, contour in enumerate(contours): # 面積を計算 area = cv2.contourArea(contour) # 周長を計算 # 第2引数:True = 閉じた輪郭として計算 perimeter = cv2.arcLength(contour, True) print(f”輪郭{i}: 面積={area:.1f}, 周長={perimeter:.1f}”)

5-4. バウンディングボックスを描画

輪郭を囲む四角形(バウンディングボックス)を描画できます。物体検出の基本です。

# 輪郭ごとにバウンディングボックスを描画 img_boxes = img.copy() for contour in contours: # 面積が小さいものは無視(ノイズ除去) area = cv2.contourArea(contour) if area < 500: # 500ピクセル未満は無視 continue # バウンディングボックスの座標を取得 # x, y:左上の座標 # w, h:幅と高さ x, y, w, h = cv2.boundingRect(contour) # 四角形を描画 # cv2.rectangle(画像, 左上座標, 右下座標, 色, 線の太さ) cv2.rectangle(img_boxes, (x, y), (x+w, y+h), (0, 255, 0), 2) # 面積をテキストで表示 cv2.putText(img_boxes, f"Area: {int(area)}", (x, y-10), # テキストの位置 cv2.FONT_HERSHEY_SIMPLEX, # フォント 0.5, # フォントサイズ (0, 255, 0), # 色 2) # 線の太さ

5-5. 輪郭検出の完成コード(実行用)

# 輪郭検出とバウンディングボックスの描画 import cv2 import numpy as np import matplotlib.pyplot as plt # サンプル画像をダウンロード import urllib.request url = “https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg” urllib.request.urlretrieve(url, “sample.jpg”) # 画像読み込み img = cv2.imread(‘sample.jpg’) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # グレースケール → 二値化 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # ノイズ除去(オープニング) kernel = np.ones((5, 5), np.uint8) binary_clean = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 輪郭検出 contours, _ = cv2.findContours(binary_clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 描画用画像を準備 img_result = img_rgb.copy() # 輪郭とバウンディングボックスを描画 object_count = 0 for contour in contours: area = cv2.contourArea(contour) if area < 500: continue object_count += 1 x, y, w, h = cv2.boundingRect(contour) cv2.rectangle(img_result, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText(img_result, f"#{object_count}", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) print(f"検出された物体数: {object_count}個") # 表示 fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(img_rgb) axes[0].set_title('Original') axes[0].axis('off') axes[1].imshow(binary_clean, cmap='gray') axes[1].set_title('Binary (Cleaned)') axes[1].axis('off') axes[2].imshow(img_result) axes[2].set_title(f'Detected: {object_count} objects') axes[2].axis('off') plt.tight_layout() plt.show()

実行結果(例):

検出された物体数: 3個

🌈 6. 色検出とトラッキング – HSV色空間を活用

特定の色の物体を検出するには、HSV色空間を使うと効果的です。STEP 2で学んだHSVをここで実践します。

6-1. なぜHSVで色検出するのか?

🎨 RGBとHSVの違い(復習)

RGB:赤・緑・青の3色の混合で表現
→ 照明が変わると3つの値すべてが変化する(検出が難しい)

HSV:色相・彩度・明度で表現
→ 色相(H)は照明が変わっても比較的安定(検出しやすい)

【HSVの値の範囲(OpenCV)】 H(色相):0〜180 ← 色の種類(赤、緑、青など) S(彩度):0〜255 ← 鮮やかさ(0=グレー、255=ビビッド) V(明度):0〜255 ← 明るさ(0=黒、255=明るい) 主な色のH値: ・赤:0〜10 または 160〜180(両端にある) ・オレンジ:10〜25 ・黄色:25〜35 ・緑:35〜85 ・青:85〜130 ・紫:130〜160

6-2. 赤い物体を検出する

赤色はHSVで特殊な位置にあります。H=0とH=180が同じ赤なので、2つの範囲を指定する必要があります。

ステップ1:RGB → HSV変換

# BGR → HSV変換 img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

ステップ2:赤色の範囲を定義

# 赤は0度付近と180度付近の2箇所にある # 範囲1:0〜10度 lower_red1 = np.array([0, 100, 100]) # [H下限, S下限, V下限] upper_red1 = np.array([10, 255, 255]) # [H上限, S上限, V上限] # 範囲2:160〜180度 lower_red2 = np.array([160, 100, 100]) upper_red2 = np.array([180, 255, 255]) # S, Vの下限を100に設定:薄い色や暗い色を除外

ステップ3:マスクを作成

# cv2.inRange(画像, 下限, 上限) # 範囲内のピクセル → 255(白) # 範囲外のピクセル → 0(黒) mask1 = cv2.inRange(img_hsv, lower_red1, upper_red1) mask2 = cv2.inRange(img_hsv, lower_red2, upper_red2) # 2つのマスクを合成(OR演算) mask_red = cv2.bitwise_or(mask1, mask2)

ステップ4:ノイズ除去

# モルフォロジー変換でノイズ除去 kernel = np.ones((5, 5), np.uint8) mask_red = cv2.morphologyEx(mask_red, cv2.MORPH_OPEN, kernel) # 小さなノイズ除去 mask_red = cv2.morphologyEx(mask_red, cv2.MORPH_CLOSE, kernel) # 穴埋め

ステップ5:検出結果を表示

# マスクを使って元の画像から赤い部分だけ抽出 result = cv2.bitwise_and(img_rgb, img_rgb, mask=mask_red) # bitwise_and:両方が1(白)の場所だけ残す

6-3. 色検出の完成コード(実行用)

# 赤い物体を検出するコード import cv2 import numpy as np import matplotlib.pyplot as plt # サンプル画像をダウンロード(赤い物体が含まれる画像を用意) import urllib.request url = “https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Sunflower_from_Silesia2.jpg/800px-Sunflower_from_Silesia2.jpg” urllib.request.urlretrieve(url, “sample_color.jpg”) # 画像読み込み img = cv2.imread(‘sample_color.jpg’) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # BGR → HSV変換 img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 黄色の範囲を定義(ひまわりの例) lower_yellow = np.array([20, 100, 100]) upper_yellow = np.array([35, 255, 255]) # マスクを作成 mask_yellow = cv2.inRange(img_hsv, lower_yellow, upper_yellow) # ノイズ除去 kernel = np.ones((5, 5), np.uint8) mask_yellow = cv2.morphologyEx(mask_yellow, cv2.MORPH_OPEN, kernel) mask_yellow = cv2.morphologyEx(mask_yellow, cv2.MORPH_CLOSE, kernel) # 検出結果 result = cv2.bitwise_and(img_rgb, img_rgb, mask=mask_yellow) # 輪郭検出してバウンディングボックスを描画 contours, _ = cv2.findContours(mask_yellow, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) img_detected = img_rgb.copy() for contour in contours: area = cv2.contourArea(contour) if area > 1000: x, y, w, h = cv2.boundingRect(contour) cv2.rectangle(img_detected, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText(img_detected, ‘Yellow’, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) # 表示 fig, axes = plt.subplots(2, 2, figsize=(12, 12)) axes[0, 0].imshow(img_rgb) axes[0, 0].set_title(‘Original’) axes[0, 0].axis(‘off’) axes[0, 1].imshow(mask_yellow, cmap=’gray’) axes[0, 1].set_title(‘Yellow Mask’) axes[0, 1].axis(‘off’) axes[1, 0].imshow(result) axes[1, 0].set_title(‘Extracted Yellow’) axes[1, 0].axis(‘off’) axes[1, 1].imshow(img_detected) axes[1, 1].set_title(‘Detected Yellow Objects’) axes[1, 1].axis(‘off’) plt.tight_layout() plt.show()

6-4. 各色の検出範囲一覧

下限 [H, S, V] 上限 [H, S, V] 備考
赤(低) [0, 100, 100] [10, 255, 255] 両端の合成必要
赤(高) [160, 100, 100] [180, 255, 255] 両端の合成必要
オレンジ [10, 100, 100] [25, 255, 255]
黄色 [20, 100, 100] [35, 255, 255]
[35, 40, 40] [85, 255, 255]
[100, 40, 40] [140, 255, 255]
[130, 40, 40] [160, 255, 255]
[0, 0, 200] [180, 30, 255] S低く、V高い
[0, 0, 0] [180, 255, 50] V低い

📝 練習問題

問題1:フィルタの選択(基礎)

以下のケースでは、どのフィルタを使うべきですか?理由とともに答えてください。

  1. ノイズが多い画像をエッジ検出する前に前処理したい
  2. 写真の肌を滑らかにしたいが、輪郭は保持したい
  3. ぼやけた写真を鮮明にしたい
解答:

1. ガウシアンブラー(Gaussian Blur)

・エッジ検出の前処理として最も一般的
・ノイズを効果的に除去しつつ、エッジ情報を保持
・Cannyエッジ検出も内部でガウシアンブラーを使用

gray_blur = cv2.GaussianBlur(gray, (5, 5), 0) edges = cv2.Canny(gray_blur, 50, 150)

2. バイラテラルフィルタ(Bilateral Filter)

・エッジを保持しながら平滑化できる
・肌のレタッチに最適
・処理は遅いが、高品質

img_smooth = cv2.bilateralFilter(img, 9, 75, 75)

3. シャープニングフィルタ

・エッジを強調し、ぼやけを軽減
・カーネルで高周波成分を強調

kernel_sharpen = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]]) img_sharp = cv2.filter2D(img, -1, kernel_sharpen)

問題2:エッジ検出の実装(中級)

画像に対して以下の処理を順番に実行するコードを書いてください。

  1. グレースケールに変換
  2. ガウシアンブラーでノイズ除去(5×5カーネル)
  3. Cannyエッジ検出(閾値:50, 150)
  4. 元の画像にエッジを緑色で重ねて表示
解答:
import cv2 import numpy as np import matplotlib.pyplot as plt # 画像読み込み img = cv2.imread(‘sample.jpg’) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 1. グレースケール変換 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 2. ガウシアンブラー(ノイズ除去) gray_blur = cv2.GaussianBlur(gray, (5, 5), 0) # 3. Cannyエッジ検出 edges = cv2.Canny(gray_blur, 50, 150) # 4. エッジを元の画像に重ねる # 方法:緑色でエッジを描画 img_overlay = img_rgb.copy() img_overlay[edges != 0] = [0, 255, 0] # エッジの部分を緑色に # 表示 fig, axes = plt.subplots(2, 2, figsize=(12, 12)) axes[0, 0].imshow(img_rgb) axes[0, 0].set_title(‘Original’) axes[0, 0].axis(‘off’) axes[0, 1].imshow(gray, cmap=’gray’) axes[0, 1].set_title(‘Grayscale’) axes[0, 1].axis(‘off’) axes[1, 0].imshow(edges, cmap=’gray’) axes[1, 0].set_title(‘Canny Edges’) axes[1, 0].axis(‘off’) axes[1, 1].imshow(img_overlay) axes[1, 1].set_title(‘Edges Overlay (Green)’) axes[1, 1].axis(‘off’) plt.tight_layout() plt.show()

ポイント:

edges != 0でエッジのピクセル(白=255の場所)を選択
[0, 255, 0]は緑色(RGB形式)
img_rgb.copy()で元の画像を壊さないようにコピー

問題3:物体カウント(応用)

画像内の白い物体の数を数えるプログラムを作成してください。以下の手順で実装してください。

  1. グレースケールに変換
  2. 二値化(閾値:127)
  3. モルフォロジー変換でノイズ除去(オープニング)
  4. 輪郭検出
  5. 面積が100以上の輪郭のみをカウント
  6. 各物体にバウンディングボックスと番号を表示
解答:
import cv2 import numpy as np import matplotlib.pyplot as plt # 画像読み込み img = cv2.imread(‘sample.jpg’) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 1. グレースケール変換 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 2. 二値化 _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # 3. モルフォロジー変換(オープニング)でノイズ除去 kernel = np.ones((5, 5), np.uint8) binary_clean = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 4. 輪郭検出 contours, _ = cv2.findContours(binary_clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 5 & 6. 面積100以上をカウントし、バウンディングボックスと番号を表示 img_result = img_rgb.copy() object_count = 0 for contour in contours: area = cv2.contourArea(contour) # 面積が100以上のみ if area >= 100: object_count += 1 # バウンディングボックス x, y, w, h = cv2.boundingRect(contour) cv2.rectangle(img_result, (x, y), (x+w, y+h), (0, 255, 0), 2) # 番号を表示 text = f”#{object_count}” cv2.putText(img_result, text, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) # 結果を表示 print(f”検出された物体数: {object_count}個”) fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(gray, cmap=’gray’) axes[0].set_title(‘Grayscale’) axes[0].axis(‘off’) axes[1].imshow(binary_clean, cmap=’gray’) axes[1].set_title(‘Binary (Cleaned)’) axes[1].axis(‘off’) axes[2].imshow(img_result) axes[2].set_title(f’Detected: {object_count} objects’) axes[2].axis(‘off’) plt.tight_layout() plt.show()

ポイント:

MORPH_OPENで小さなノイズを除去してから輪郭検出
・面積でフィルタリングして、小さすぎる輪郭を除外
object_countで番号を管理

問題4:複数色の検出(応用)

画像から「青い物体」と「黄色い物体」を同時に検出し、それぞれ異なる色でバウンディングボックスを描画するプログラムを作成してください。

解答:
import cv2 import numpy as np import matplotlib.pyplot as plt # 画像読み込み img = cv2.imread(‘sample.jpg’) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # BGR → HSV変換 img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 青の範囲 lower_blue = np.array([100, 40, 40]) upper_blue = np.array([140, 255, 255]) # 黄色の範囲 lower_yellow = np.array([20, 100, 100]) upper_yellow = np.array([35, 255, 255]) # マスク作成 mask_blue = cv2.inRange(img_hsv, lower_blue, upper_blue) mask_yellow = cv2.inRange(img_hsv, lower_yellow, upper_yellow) # ノイズ除去 kernel = np.ones((5, 5), np.uint8) mask_blue = cv2.morphologyEx(mask_blue, cv2.MORPH_OPEN, kernel) mask_yellow = cv2.morphologyEx(mask_yellow, cv2.MORPH_OPEN, kernel) # 結果用画像 img_result = img_rgb.copy() # 青い物体を検出 contours_blue, _ = cv2.findContours(mask_blue, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) blue_count = 0 for contour in contours_blue: area = cv2.contourArea(contour) if area > 500: blue_count += 1 x, y, w, h = cv2.boundingRect(contour) cv2.rectangle(img_result, (x, y), (x+w, y+h), (0, 0, 255), 2) # 青 cv2.putText(img_result, f’Blue #{blue_count}’, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) # 黄色い物体を検出 contours_yellow, _ = cv2.findContours(mask_yellow, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) yellow_count = 0 for contour in contours_yellow: area = cv2.contourArea(contour) if area > 500: yellow_count += 1 x, y, w, h = cv2.boundingRect(contour) cv2.rectangle(img_result, (x, y), (x+w, y+h), (255, 255, 0), 2) # 黄色 cv2.putText(img_result, f’Yellow #{yellow_count}’, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 2) # 統計情報 print(f”青い物体: {blue_count}個”) print(f”黄色い物体: {yellow_count}個”) print(f”合計: {blue_count + yellow_count}個”) # 表示 fig, axes = plt.subplots(2, 2, figsize=(12, 12)) axes[0, 0].imshow(img_rgb) axes[0, 0].set_title(‘Original’) axes[0, 0].axis(‘off’) axes[0, 1].imshow(mask_blue, cmap=’gray’) axes[0, 1].set_title(f’Blue Mask ({blue_count} objects)’) axes[0, 1].axis(‘off’) axes[1, 0].imshow(mask_yellow, cmap=’gray’) axes[1, 0].set_title(f’Yellow Mask ({yellow_count} objects)’) axes[1, 0].axis(‘off’) axes[1, 1].imshow(img_result) axes[1, 1].set_title(f’Blue: {blue_count}, Yellow: {yellow_count}’) axes[1, 1].axis(‘off’) plt.tight_layout() plt.show()

ポイント:

・色ごとに別々のマスクを作成
cv2.inRange()で指定した色の範囲内のピクセルだけを抽出
・バウンディングボックスの色を変えて、どの色の物体か区別

📝 STEP 3 のまとめ

✅ このステップで学んだこと

1. OpenCVはコンピュータビジョンの最も重要なライブラリ

2. フィルタリング
・ガウシアンブラー:ノイズ除去(最も一般的)
・メディアンブラー:点々のノイズに強い
・バイラテラル:エッジを保持しながらぼかす
・シャープニング:画像を鮮明にする

3. エッジ検出
・Canny:最も人気(高精度、細い線、連続性)
・Sobel:方向別(縦・横)に検出
・Laplacian:全方向を一度に検出

4. モルフォロジー変換
・膨張:白を膨らませる
・収縮:白を削る
・オープニング:白いノイズを除去
・クロージング:黒い穴を埋める

5. 輪郭検出
・findContours:輪郭を見つける
・drawContours:輪郭を描画
・boundingRect:バウンディングボックス

6. 色検出
・HSV色空間を使うと色検出が簡単
・inRange()でマスクを作成
・複数色の同時検出も可能

💡 このステップの技術は「古典的CV」

今回学んだ技術は、深層学習が登場する前から使われている「古典的CV」です。

深層学習の時代でも、これらの技術は前処理・後処理として頻繁に使われます。

特にCannyエッジ検出HSV色検出は、実務でも非常によく使う技術なので、しっかりマスターしましょう。

🎯 次のステップの準備

次のSTEP 4では、「データ拡張とバッチ処理」を学びます。

torchvision.transformsとAlbumentationsでデータ拡張を実装し、カスタムデータセットの作成、DataLoaderの使い方など、深層学習に必要な効率的なデータパイプラインを構築します。

CVの基礎から、いよいよ深層学習への橋渡しです!

📝

学習メモ

コンピュータビジョン(CV) - Step 3

📋 過去のメモ一覧
#artnasekai #学習メモ
LINE