【C++】OpenCVでエッジ検出を極める:Canny・Sobel・Houghで境界線を鮮明抽出
C++とOpenCVなら、cvtColor
でグレースケール化しGaussianBlur
でノイズを抑えた後、Canny
やSobel
を呼ぶだけで輪郭抽出が完了します。
パラメータを適切に調整すれば薄い線も強調でき、ハフ変換で直線抽出へ滑らかに接続できます。
OpenCVでのエッジ検出ワークフロー
画像のエッジ検出は、物体の輪郭や形状を抽出するための重要なステップです。
OpenCVを使うと、比較的簡単にエッジ検出を実装できますが、効果的な結果を得るためには一連の処理を正しく理解し、適切に組み合わせることが大切です。
ここでは、OpenCVでエッジ検出を行う際の基本的なワークフローを解説します。
読み込みから出力までの全体像
エッジ検出の処理は、主に以下のステップで構成されます。
- 画像の読み込み
まずは対象となる画像をOpenCVのcv::imread
関数で読み込みます。
カラー画像の場合はBGR形式で読み込まれます。
- グレースケール変換
エッジ検出は輝度の変化を検出するため、カラー画像をグレースケール画像に変換します。
cv::cvtColor
関数を使い、cv::COLOR_BGR2GRAY
を指定します。
- ノイズ除去(平滑化)
画像にノイズがあると誤検出が増えるため、ガウシアンブラーなどの平滑化処理でノイズを低減します。
cv::GaussianBlur
がよく使われます。
- エッジ検出
代表的な手法としてCannyエッジ検出やSobelフィルタがあります。
cv::Canny
やcv::Sobel
関数を用いてエッジを抽出します。
- ポストプロセッシング(必要に応じて)
モルフォロジー演算(膨張や収縮)でエッジの連結を強化したり、不要なノイズを除去したりします。
- 結果の表示・保存
cv::imshow
で結果を画面に表示したり、cv::imwrite
でファイルに保存したりします。
この流れを踏むことで、画像の境界線を鮮明に抽出できます。
以下に、ワークフローのイメージを表にまとめます。
ステップ番号 | 処理内容 | 主なOpenCV関数 | 目的・効果 |
---|---|---|---|
1 | 画像読み込み | cv::imread | 画像データの取得 |
2 | グレースケール変換 | cv::cvtColor | 輝度情報の抽出 |
3 | ノイズ除去 | cv::GaussianBlur | ノイズ低減、エッジ検出の精度向上 |
4 | エッジ検出 | cv::Canny 、cv::Sobel | 境界線の抽出 |
5 | ポストプロセッシング | cv::erode 、cv::dilate | エッジの強調やノイズ除去 |
6 | 表示・保存 | cv::imshow 、cv::imwrite | 結果の確認・保存 |
前処理で押さえるポイント
エッジ検出の精度を左右するのが前処理です。
特にノイズ除去とグレースケール変換は重要な役割を果たします。
グレースケール変換の注意点
カラー画像は3チャンネル(BGR)で構成されていますが、エッジ検出は輝度の変化を検出するため、1チャンネルのグレースケール画像に変換します。
cv::cvtColor
を使い、以下のように変換します。
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
この変換により、色情報の影響を排除し、エッジ検出の計算がシンプルかつ高速になります。
ノイズ除去の重要性
画像にはセンサーのノイズや圧縮アーティファクトなどが含まれていることが多く、そのままエッジ検出を行うと誤検出が増えます。
そこで、平滑化処理を行いノイズを低減します。
代表的な手法はガウシアンブラーです。
ガウシアンカーネルのサイズや標準偏差を調整することで、ノイズ除去の強さをコントロールできます。
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 1.5);
ここで、cv::Size(5, 5)
はカーネルサイズ、1.5
は標準偏差を表します。
カーネルサイズが大きいほど平滑化が強くなりますが、エッジのぼやけも増えるためバランスが重要です。
ノイズの種類に応じた前処理
- ガウシアンノイズにはガウシアンブラーが効果的です
- 塩胡椒ノイズにはメディアンフィルタ
cv::medianBlur
が適しています
例えば、メディアンフィルタは以下のように使います。
cv::medianBlur(gray, medianBlurred, 3);
カーネルサイズは奇数で指定し、3や5がよく使われます。
前処理のまとめ
ノイズ種類 | 推奨フィルタ | OpenCV関数 | 備考 |
---|---|---|---|
ガウシアンノイズ | ガウシアンブラー | cv::GaussianBlur | 平滑化効果が滑らか |
塩胡椒ノイズ | メディアンフィルタ | cv::medianBlur | ノイズ点の除去に強い |
その他 | バイラテラルフィルタ | cv::bilateralFilter | エッジを保持しつつ平滑化 |
前処理はエッジ検出の土台となる重要な工程です。
グレースケール変換で色情報を整理し、ノイズ除去で誤検出を減らすことで、後続のエッジ検出処理の精度と安定性が大きく向上します。
画像の特性に応じて適切なフィルタを選択し、パラメータを調整することがポイントです。
前処理テクニック
カラースペース変換
cvtColorでBGR→GRAY
OpenCVで画像を読み込むと、デフォルトでBGR形式の3チャンネルカラー画像として扱われます。
エッジ検出は輝度の変化を捉える処理なので、カラー情報を1チャンネルのグレースケール画像に変換する必要があります。
これにはcv::cvtColor
関数を使い、変換コードにcv::COLOR_BGR2GRAY
を指定します。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 画像の読み込み(BGRカラー)
cv::Mat colorImage = cv::imread("image.jpg");
if (colorImage.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
// BGRからグレースケールに変換
cv::Mat grayImage;
cv::cvtColor(colorImage, grayImage, cv::COLOR_BGR2GRAY);
// 結果の表示
cv::imshow("Original Image", colorImage);
cv::imshow("Grayscale Image", grayImage);
cv::waitKey(0);
return 0;
}
このコードでは、カラー画像をグレースケールに変換し、2つのウィンドウで表示しています。

グレースケール画像は1チャンネルで、輝度情報のみを持つため、エッジ検出の計算が効率的になります。
ノイズ低減
GaussianBlurのカーネルサイズ比較
ノイズを含む画像にそのままエッジ検出を適用すると、誤検出が増えます。
ガウシアンブラーはノイズ低減に広く使われる平滑化フィルタで、カーネルサイズ(フィルタの大きさ)を変えることで平滑化の強さを調整できます。
以下のコードは、同じ画像に対して異なるカーネルサイズのガウシアンブラーを適用し、結果を比較します。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat grayImage = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (grayImage.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
// カーネルサイズ3x3のガウシアンブラー
cv::Mat blurred3;
cv::GaussianBlur(grayImage, blurred3, cv::Size(3, 3), 0);
// カーネルサイズ5x5のガウシアンブラー
cv::Mat blurred5;
cv::GaussianBlur(grayImage, blurred5, cv::Size(5, 5), 0);
// カーネルサイズ9x9のガウシアンブラー
cv::Mat blurred9;
cv::GaussianBlur(grayImage, blurred9, cv::Size(9, 9), 0);
// 結果の表示
cv::imshow("Original", grayImage);
cv::imshow("GaussianBlur 3x3", blurred3);
cv::imshow("GaussianBlur 5x5", blurred5);
cv::imshow("GaussianBlur 9x9", blurred9);
cv::waitKey(0);
return 0;
}
このコードでは、カーネルサイズが大きくなるほど画像がより滑らかになり、ノイズが減少しますが、エッジの輪郭もぼやけやすくなります。




適切なカーネルサイズは画像の特性や目的に応じて選択してください。
MedianBlurによる塩胡椒ノイズ対策
塩胡椒ノイズ(ランダムに黒や白の点が散らばるノイズ)には、ガウシアンブラーよりもメディアンフィルタが効果的です。
メディアンフィルタはカーネル内の中央値をピクセル値として置き換えるため、ノイズ点を除去しつつエッジを比較的保持します。
以下のコードは、塩胡椒ノイズが混入した画像にメディアンフィルタを適用する例です。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat noisyImage = cv::imread("salt_pepper_noise.jpg", cv::IMREAD_GRAYSCALE);
if (noisyImage.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
// メディアンフィルタ適用(カーネルサイズ3)
cv::Mat medianFiltered;
cv::medianBlur(noisyImage, medianFiltered, 3);
// 結果の表示
cv::imshow("Noisy Image", noisyImage);
cv::imshow("Median Filtered", medianFiltered);
cv::waitKey(0);
return 0;
}
メディアンフィルタのカーネルサイズは奇数で指定し、3や5がよく使われます。
カーネルサイズが大きいほどノイズ除去効果は高まりますが、画像の細部も失われやすくなるため注意が必要です。
これらの前処理テクニックを適切に使い分けることで、エッジ検出の精度と安定性を大きく向上させられます。
画像のノイズ特性に応じて、ガウシアンブラーやメディアンフィルタを選択し、パラメータを調整してください。
Cannyエッジ検出
アルゴリズムの流れ
Cannyエッジ検出は、画像のエッジを高精度に抽出するための代表的な手法です。
以下のステップで処理が進みます。
- ノイズ除去
画像に含まれるノイズをガウシアンフィルタで平滑化し、誤検出を減らします。
- 勾配強度と方向の計算
Sobelフィルタを用いて画像の水平方向と垂直方向の勾配を計算し、勾配の大きさ(強度)と角度(方向)を求めます。
- 非最大抑制(Non-Maximum Suppression)
勾配強度の局所最大値のみをエッジとして残し、細くシャープなエッジを形成します。
- 二重しきい値処理(Double Thresholding)
2つのしきい値(低しきい値と高しきい値)を用いて、強いエッジ、弱いエッジ、非エッジに分類します。
- エッジの接続(Edge Tracking by Hysteresis)
強いエッジに隣接する弱いエッジをエッジとして確定し、孤立した弱いエッジは除去します。
この一連の処理により、ノイズに強く、細くて連続したエッジを抽出できます。
cv::Canny主要パラメータ
OpenCVのcv::Canny
関数は、以下の主要パラメータを指定してエッジ検出を行います。
- threshold1(低しきい値)
エッジの弱い候補を判定するための下限値です。
- threshold2(高しきい値)
エッジの強い候補を判定するための上限値です。
- apertureSize
Sobelフィルタのカーネルサイズで、勾配計算に使われます。
通常は3、5、7のいずれかを指定します。
- L2gradient
勾配の計算方法を指定します。
false
の場合はL1ノルム(絶対値の和)、true
の場合はL2ノルム(ユークリッド距離)で勾配強度を計算します。
低しきい値・高しきい値の選び方
低しきい値と高しきい値は、エッジ検出の感度に大きく影響します。
一般的な目安は以下の通りです。
- 高しきい値はエッジとして確実に検出したい強度の閾値です
- 低しきい値は高しきい値の約半分程度に設定することが多いです
例えば、高しきい値を100に設定した場合、低しきい値は50程度が適切です。
低しきい値が高すぎると弱いエッジが検出されにくくなり、低すぎるとノイズが多く検出されます。
高しきい値も同様に調整が必要です。
apertureSizeとL2gradientの効果
- apertureSize
Sobelフィルタのカーネルサイズを大きくすると、勾配計算がより滑らかになり、ノイズに対して強くなりますが、エッジの位置がぼやける可能性があります。
通常は3が使われますが、ノイズが多い場合は5や7を試すこともあります。
- L2gradient
false
(デフォルト)の場合は勾配強度を
true
にすると
精度を重視する場合に有効です。
自動しきい値算出アプローチ
しきい値の設定は手動で行うことが多いですが、画像ごとに最適値が異なるため自動化が望まれます。
代表的な方法は以下の通りです。
- ヒストグラム解析
画像の勾配強度のヒストグラムを作成し、ピークや分布を解析してしきい値を決定します。
- Otsuの二値化を応用
勾配強度画像に対してOtsuの方法で自動的にしきい値を決める手法です。
- パーセンタイル法
勾配強度の上位何パーセントをエッジとみなすかを基準にしきい値を設定します。
以下は、パーセンタイル法を用いて自動的に低しきい値と高しきい値を決めるサンプルコードです。
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
cv::Mat image = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (image.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
// ガウシアンブラーでノイズ除去
cv::Mat blurred;
cv::GaussianBlur(image, blurred, cv::Size(5, 5), 1.5);
// Sobelで勾配強度計算
cv::Mat grad_x, grad_y;
cv::Sobel(blurred, grad_x, CV_32F, 1, 0, 3);
cv::Sobel(blurred, grad_y, CV_32F, 0, 1, 3);
cv::Mat magnitude;
cv::magnitude(grad_x, grad_y, magnitude);
// 勾配強度を1次元配列に変換
std::vector<float> magVec;
magVec.assign((float*)magnitude.datastart, (float*)magnitude.dataend);
// 昇順にソート
std::sort(magVec.begin(), magVec.end());
// パーセンタイルからしきい値を決定
float highThresh = magVec[(int)(magVec.size() * 0.9)]; // 上位10%を高しきい値
float lowThresh = highThresh * 0.5f; // 低しきい値は高しきい値の半分
// Cannyエッジ検出
cv::Mat edges;
cv::Canny(blurred, edges, lowThresh, highThresh);
cv::imshow("Edges", edges);
cv::waitKey(0);
return 0;
}
この方法では、勾配強度の上位10%を高しきい値に設定し、その半分を低しきい値にしています。


画像の特性に応じてパーセンタイルの値を調整してください。
出力品質の評価指標
Cannyエッジ検出の結果を評価する際には、以下の指標が参考になります。
- エッジの連続性
エッジが途切れずに連続しているか。
非最大抑制とヒステリシス処理の効果を確認します。
- ノイズの抑制
ノイズによる誤検出が少ないか。
前処理のノイズ除去やしきい値設定の適切さが影響します。
- エッジの正確性
実際の物体境界に沿ったエッジが検出されているか。
過剰な平滑化やしきい値の誤設定でずれが生じることがあります。
- 計算コスト
処理速度も重要です。
L2gradient
をtrue
にすると精度は上がりますが計算時間が増えます。
これらの指標は主観的評価だけでなく、場合によってはグラウンドトゥルース(正解エッジ)との比較やF値(適合率と再現率の調和平均)を用いた定量評価も行います。
Cannyエッジ検出はパラメータ調整が結果に大きく影響するため、画像の特性や用途に応じてしきい値やフィルタサイズを適切に設定することが重要です。
自動しきい値算出や評価指標を活用しながら、最適な検出結果を目指してください。
Sobelフィルター
勾配画像の取得
Sobelフィルターは画像の輝度変化の勾配を計算し、エッジの方向や強度を検出するための基本的な手法です。
OpenCVではcv::Sobel
関数を使って勾配画像を取得します。
dxとdyの使い分け
cv::Sobel
関数のパラメータdx
とdy
は、それぞれ水平方向(x軸)と垂直方向(y軸)の微分次数を指定します。
dx = 1, dy = 0
水平方向の勾配を計算します。
画像の左右方向の輝度変化を捉え、垂直方向のエッジを強調します。
dx = 0, dy = 1
垂直方向の勾配を計算します。
上下方向の輝度変化を捉え、水平方向のエッジを強調します。
dx = 1, dy = 1
両方向の微分を同時に計算しますが、通常は勾配の合成を別途行うため、あまり使われません。
例えば、以下のコードは水平方向と垂直方向の勾配画像を取得する例です。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat gray = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (gray.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
cv::Mat grad_x, grad_y;
// 水平方向の勾配
cv::Sobel(gray, grad_x, CV_16S, 1, 0, 3);
// 垂直方向の勾配
cv::Sobel(gray, grad_y, CV_16S, 0, 1, 3);
// 絶対値を取り8ビットに変換
cv::Mat abs_grad_x, abs_grad_y;
cv::convertScaleAbs(grad_x, abs_grad_x);
cv::convertScaleAbs(grad_y, abs_grad_y);
cv::imshow("Gradient X", abs_grad_x);
cv::imshow("Gradient Y", abs_grad_y);
cv::waitKey(0);
return 0;
}

このコードでは、grad_x
が垂直エッジを、grad_y
が水平エッジを強調した画像になります。
カーネルサイズ別の特徴
cv::Sobel
の第6引数ksize
はSobelフィルターのカーネルサイズを指定します。
一般的に3、5、7が使われます。
カーネルサイズ | 特徴 |
---|---|
3 | 最も一般的。エッジ検出の感度と計算コストのバランスが良いでしょう。 |
5 | より広い範囲の勾配を計算。ノイズに強くなるがエッジがややぼやける。 |
7 | さらに広範囲の勾配を計算。ノイズ耐性は高いが細かいエッジが失われやすい。 |
カーネルサイズが大きくなるほど、平滑化効果が強まりノイズに強くなりますが、エッジの位置精度が低下する傾向があります。
用途に応じて使い分けてください。
勾配合成でできること
水平方向と垂直方向の勾配画像を合成することで、エッジの強度と方向を総合的に評価できます。
合成方法の代表例は以下の通りです。
- 勾配強度の合成
勾配の大きさをユークリッド距離で計算し、エッジの強度を表現します。
- 近似合成
計算コストを抑えたい場合に使われます。
OpenCVではcv::addWeighted
関数を使って簡単に合成できます。
cv::Mat grad;
cv::addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad);
この合成画像は、画像内の全方向のエッジを強調し、エッジ検出の結果としてよく使われます。
Sobel結果の応用例
Sobelフィルターの勾配画像は、様々な画像処理タスクに応用可能です。
- エッジ検出の基礎
Cannyエッジ検出の内部処理でもSobelフィルターが使われています。
単独でエッジの強度を把握する際にも有効です。
- 輪郭抽出
勾配強度画像を二値化して輪郭を抽出し、物体の形状解析に利用します。
- 特徴量抽出
勾配方向や強度を特徴量として、物体認識や画像分類の前処理に使います。
- 画像のシャープ化
勾配画像を元画像に加算することで、エッジを強調しシャープな画像を生成できます。
- 動き検出や変化検出
動画のフレーム間で勾配の変化を追うことで、動きのある領域を検出することが可能です。
これらの応用において、Sobelフィルターは計算コストが低く、リアルタイム処理にも適しているため、幅広く利用されています。
Scharrフィルター
Sobelとの違い
Scharrフィルターは、Sobelフィルターの改良版として位置づけられています。
どちらも画像の勾配を計算してエッジを検出するための微分フィルターですが、主に以下の点で違いがあります。
- カーネルの設計
Sobelフィルターは3×3のカーネルを使い、勾配を計算しますが、Scharrフィルターは同じ3×3サイズのカーネルでありながら、より高精度に勾配を近似するために係数が最適化されています。
具体的には、ScharrカーネルはSobelよりも中心付近の重みが大きく、エッジの検出精度が向上します。
- エッジ検出の精度
Scharrフィルターは特に細かいエッジや高周波成分の検出に優れており、Sobelフィルターよりもノイズに強く、エッジの位置ずれが少ない特徴があります。
- 計算コスト
両者とも3×3カーネルで計算量はほぼ同じですが、ScharrフィルターはOpenCVで専用の関数cv::Scharr
が用意されており、内部的に最適化されています。
OpenCVでの使い方はSobelとほぼ同様で、cv::Scharr
関数を使います。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat gray = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (gray.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
cv::Mat grad_x, grad_y;
// Scharrフィルターで水平方向の勾配を計算
cv::Scharr(gray, grad_x, CV_16S, 1, 0);
// Scharrフィルターで垂直方向の勾配を計算
cv::Scharr(gray, grad_y, CV_16S, 0, 1);
cv::Mat abs_grad_x, abs_grad_y;
cv::convertScaleAbs(grad_x, abs_grad_x);
cv::convertScaleAbs(grad_y, abs_grad_y);
cv::Mat grad;
cv::addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad);
cv::imshow("Scharr Gradient", grad);
cv::waitKey(0);
return 0;
}
このコードはSobelフィルターの代わりにScharrフィルターを使い、より精度の高い勾配画像を生成します。

高周波成分強調のポイント
Scharrフィルターは高周波成分、つまり画像の急激な輝度変化をより強調する特性があります。
これは以下の理由によります。
- 最適化されたカーネル係数
Scharrカーネルは、微分演算の誤差を最小化するように設計されており、特に高周波成分の検出に強い感度を持ちます。
- ノイズとエッジのバランス
高周波成分はノイズにも含まれますが、ScharrフィルターはSobelよりもノイズの影響を抑えつつ、細かいエッジを鮮明に抽出できます。
- エッジの位置精度向上
勾配の計算精度が高いため、エッジの位置ずれが少なく、細部の輪郭を正確に捉えられます。
このため、細かいテクスチャや微細な輪郭を検出したい場合にScharrフィルターは有効です。
ただし、ノイズが非常に多い画像では前処理でのノイズ除去が重要になります。
ScharrフィルターはSobelフィルターの代替として、より高精度なエッジ検出を実現します。
特に高周波成分の強調やエッジの位置精度を重視するシーンで活用すると効果的です。
Laplacianフィルター
二階微分で得られるメリット
Laplacianフィルターは画像の二階微分を計算することでエッジを検出します。
一次微分(SobelやScharrフィルター)と比較して、二階微分には以下のようなメリットがあります。
- エッジの強調が鋭くなる
一階微分は輝度の変化の大きさを捉えますが、二階微分はその変化の変化、つまり勾配の傾きの変化を検出します。
これにより、エッジの境界がよりシャープに強調されます。
- エッジの位置が明確になる
二階微分はエッジの位置で符号が変わる(ゼロ交差)特徴を持つため、エッジの正確な位置を特定しやすくなります。
- ノイズに対する感度が高い
ただし、二階微分はノイズにも敏感で、ノイズが多い画像では誤検出が増える傾向があります。
そのため、前処理でのノイズ除去が重要です。
OpenCVではcv::Laplacian
関数を使って二階微分を計算します。
以下は基本的な使い方の例です。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat gray = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (gray.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
cv::Mat laplacian;
// 二階微分を計算(カーネルサイズ3)
cv::Laplacian(gray, laplacian, CV_16S, 3);
// 絶対値を取り8ビットに変換
cv::Mat abs_laplacian;
cv::convertScaleAbs(laplacian, abs_laplacian);
cv::imshow("Laplacian", abs_laplacian);
cv::waitKey(0);
return 0;
}

このコードでは、グレースケール画像に対してLaplacianフィルターを適用し、エッジの強調画像を表示しています。
ゼロ交差を利用した輪郭抽出
Laplacianフィルターの特徴の一つに、エッジの位置で二階微分の値がゼロになる「ゼロ交差(zero-crossing)」があります。
これは、画像の輝度が急激に変化する境界で、二階微分の符号が正から負、または負から正に変わる点を指します。
ゼロ交差を検出することで、エッジの正確な輪郭を抽出できます。
具体的には、Laplacianフィルターを適用した後、隣接画素間で符号が変わる箇所を探し、その位置をエッジとしてマークします。
OpenCVにはゼロ交差を直接検出する関数はありませんが、以下のような手順で実装可能です。
- Laplacianフィルターを適用し、二階微分画像を得ります。
- 画像の各画素とその隣接画素の符号を比較し、符号が異なる箇所を検出します。
- 符号変化があれば、その位置をエッジとして抽出します。
以下は簡単なゼロ交差検出の例です。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat gray = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (gray.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
cv::Mat laplacian;
cv::Laplacian(gray, laplacian, CV_32F, 3);
cv::Mat zeroCross = cv::Mat::zeros(laplacian.size(), CV_8U);
for (int y = 1; y < laplacian.rows - 1; ++y) {
for (int x = 1; x < laplacian.cols - 1; ++x) {
float center = laplacian.at<float>(y, x);
// 隣接画素の符号をチェック
bool zeroCrossing = false;
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
if (dy == 0 && dx == 0) continue;
float neighbor = laplacian.at<float>(y + dy, x + dx);
if ((center > 0 && neighbor < 0) || (center < 0 && neighbor > 0)) {
zeroCrossing = true;
break;
}
}
if (zeroCrossing) break;
}
if (zeroCrossing) {
zeroCross.at<uchar>(y, x) = 255;
}
}
}
cv::imshow("Zero Crossing Edges", zeroCross);
cv::waitKey(0);
return 0;
}
このコードはLaplacian画像のゼロ交差を検出し、エッジとして白色で表示します。
ゼロ交差を利用することで、エッジの位置をより正確に特定できるため、輪郭抽出に有効です。
Laplacianフィルターは二階微分の特性を活かし、エッジの鋭い強調と正確な位置検出が可能です。
ゼロ交差を利用した輪郭抽出は、特に細かい輪郭や複雑な形状の検出に役立ちますが、ノイズに弱いため前処理でのノイズ除去が重要です。
ハフ変換による直線検出
標準ハフ変換と確率的ハフ変換の比較
ハフ変換は画像内の直線を検出するための古典的な手法で、エッジ画像から直線のパラメータを抽出します。
OpenCVでは主に2種類のハフ変換が利用されます。
- 標準ハフ変換(Standard Hough Transform)
cv::HoughLines
関数で実装されており、画像全体のエッジ点を用いて直線のパラメータ空間(ρ-θ空間)に投票を行います。
全てのエッジ点を考慮するため、検出精度は高いですが計算コストが大きく、処理時間が長くなる傾向があります。
- 確率的ハフ変換(Probabilistic Hough Transform)
cv::HoughLinesP
関数で実装されており、エッジ点のサブセットをランダムに抽出して投票を行います。
これにより計算量が大幅に削減され、リアルタイム処理に適しています。
さらに、検出されるのは線分の端点座標であり、直線の長さや位置が明確に得られます。
特徴 | 標準ハフ変換 (HoughLines) | 確率的ハフ変換 (HoughLinesP) |
---|---|---|
計算コスト | 高い | 低い |
出力 | (ρ, θ) のパラメータ | 線分の端点座標 (x1, y1, x2, y2) |
線分の長さ情報 | なし | あり |
実装の複雑さ | シンプル | やや複雑 |
適用シーン | 高精度が必要な場合 | リアルタイムや線分検出に最適 |
cv::HoughLinesとcv::HoughLinesPの選択基準
距離分解能・角度分解能
ハフ変換では、パラメータ空間の分解能を設定します。
- 距離分解能(ρ)
直線の距離パラメータρの分解能をピクセル単位で指定します。
小さい値にすると検出精度が上がりますが、計算量も増加します。
一般的には1ピクセルが使われます。
- 角度分解能(θ)
直線の角度パラメータθの分解能をラジアン単位で指定します。
小さい値にすると角度の精度が上がりますが、計算コストが増えます。
通常はCV_PI/180
(1度)を指定します。
これらのパラメータはcv::HoughLines
、cv::HoughLinesP
の両方で共通して設定します。
最小線分長と許容ギャップ
確率的ハフ変換cv::HoughLinesP
では、検出する線分の長さや線分間のギャップを制御するパラメータがあります。
- 最小線分長(minLineLength)
これより短い線分は検出されません。
ノイズや小さな断片的な線を除去するのに有効です。
- 許容ギャップ(maxLineGap)
線分を連結する際に許容される最大の隙間の長さです。
隙間がこの値以下なら線分として連結されます。
大きくすると断続的な線も一本の線分として検出されやすくなります。
これらのパラメータを適切に設定することで、誤検出を減らし、実際の直線に近い線分を抽出できます。
線分フィルタリングで誤検出抑制
ハフ変換で検出された線分には、ノイズや不要な線分が含まれることがあります。
誤検出を抑制するために、以下のようなフィルタリングを行います。
- 長さによるフィルタリング
最小線分長より短い線分を除去します。
短すぎる線分はノイズの可能性が高いためです。
- 角度によるフィルタリング
特定の角度範囲にある線分のみを抽出します。
例えば、水平線や垂直線だけを検出したい場合に有効です。
- 位置によるフィルタリング
画像の特定領域に存在する線分のみを対象にします。
ROI(Region of Interest)を設定して処理を限定できます。
- 重複線分の統合
近接した線分を統合し、冗長な検出を減らします。
線分の端点間距離や角度差を基準に統合判定を行います。
以下は角度フィルタリングの例です。
検出した線分の角度を計算し、指定範囲内の線分だけを描画します。
#include <opencv2/opencv.hpp>
#include <iostream>
#include <cmath>
int main() {
cv::Mat image = cv::imread("image.jpg");
if (image.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
cv::Mat gray, edges;
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
cv::Canny(gray, edges, 50, 150);
std::vector<cv::Vec4i> lines;
cv::HoughLinesP(edges, lines, 1, CV_PI / 180, 80, 30, 10);
cv::Mat result = image.clone();
for (const auto& line : lines) {
int x1 = line[0], y1 = line[1], x2 = line[2], y2 = line[3];
double angle = std::atan2(y2 - y1, x2 - x1) * 180.0 / CV_PI;
// 水平線に近い線分のみ描画(角度が-10度から10度の範囲)
if (angle > -10 && angle < 10) {
cv::line(result, cv::Point(x1, y1), cv::Point(x2, y2), cv::Scalar(0, 0, 255), 2);
}
}
cv::imshow("Filtered Lines", result);
cv::waitKey(0);
return 0;
}
このコードでは、水平線に近い線分だけを赤色で描画しています。
こうしたフィルタリングを組み合わせることで、誤検出を抑え、目的に合った直線検出が可能になります。
ハフ変換での円検出
cv::HoughCirclesパラメータ要点
OpenCVのcv::HoughCircles
関数は、画像内の円を検出するためのハフ変換ベースの手法を提供します。
円検出では、円の中心座標と半径をパラメータ空間で探索します。
cv::HoughCircles
の主なパラメータは以下の通りです。
- image
入力画像。
通常はグレースケール画像を指定します。
- circles
検出された円の情報を格納する出力ベクトル。
各円はVec3f
型で、(x座標, y座標, 半径)を表します。
- method
円検出のアルゴリズム。
OpenCVでは主にcv::HOUGH_GRADIENT
が使われます。
- dp
画像解像度と検出解像度の比率。
例えばdp=1
は入力画像と同じ解像度で検出し、dp=2
は半分の解像度で検出します。
大きい値にすると計算が高速になりますが、検出精度が落ちる可能性があります。
- minDist
検出される円の中心間の最小距離。
近接した円が複数検出されるのを防ぎます。
- param1
Cannyエッジ検出の上限しきい値。
円検出の前段階でエッジを検出する際に使われます。
- param2
円の検出に使う投票の閾値。
値が大きいほど厳密に円と判定され、誤検出が減りますが、検出数も減少します。
- minRadius
検出する円の最小半径。
これより小さい円は無視されます。
- maxRadius
検出する円の最大半径。
これより大きい円は無視されます。
以下は典型的な呼び出し例です。
cv::HoughCircles(grayImage, circles, cv::HOUGH_GRADIENT, 1, 50, 100, 30, 10, 100);
この例では、解像度比dp=1
、中心間距離minDist=50
、Cannyの上限しきい値param1=100
、投票閾値param2=30
、最小半径minRadius=10
、最大半径maxRadius=100
を指定しています。
パラメータの調整は検出結果に大きく影響するため、画像の特性や円の大きさに応じて適切に設定してください。
誤検出を減らす前処理の工夫
円検出の精度を高めるためには、前処理でノイズや不要な情報を除去し、エッジを鮮明にすることが重要です。
以下のポイントを押さえると誤検出を減らせます。
- グレースケール変換
カラー画像はグレースケールに変換し、輝度情報のみに絞ります。
cv::cvtColor
でcv::COLOR_BGR2GRAY
を指定します。
- ノイズ除去(平滑化)
ガウシアンブラーやメディアンフィルタでノイズを低減します。
ノイズが多いと誤検出が増えるため、適切なカーネルサイズを選びます。
cv::GaussianBlur(grayImage, blurred, cv::Size(9, 9), 2);
- コントラスト強調
ヒストグラム均一化cv::equalizeHist
などでコントラストを強調し、円の輪郭を際立たせます。
- エッジ強調
Cannyエッジ検出を前段階で行い、エッジを明確にする方法もあります。
ただし、cv::HoughCircles
内部でCannyが使われるため、過度なエッジ検出は逆効果になることもあります。
- ROIの設定
円が存在する可能性の高い領域だけを切り出して処理することで、誤検出を減らし計算効率も向上します。
- パラメータの適切な調整
param1
やparam2
の値を画像に合わせて調整し、誤検出を抑制します。
特にparam2
は投票の閾値なので、値を上げると誤検出が減りますが、検出漏れも増えるためバランスが必要です。
これらの前処理を組み合わせることで、円検出の精度が向上し、誤検出を効果的に抑えられます。
輪郭検出と輪郭近似
cv::findContoursのモード・メソッド
OpenCVのcv::findContours
関数は、二値化画像から輪郭(連結した境界線)を検出するための基本的な関数です。
輪郭は点の集合として表現され、物体の形状解析や領域抽出に利用されます。
cv::findContours
の主な引数に「モード」と「メソッド」があり、これらの設定によって検出される輪郭の種類や表現方法が変わります。
モード(Contour Retrieval Mode)
輪郭の検出方法や階層構造の扱いを指定します。
主なモードは以下の通りです。
モード名 | 説明 |
---|---|
cv::RETR_EXTERNAL | 最も外側の輪郭のみを検出します。内側の輪郭は無視されます。 |
cv::RETR_LIST | 全ての輪郭を検出し、階層構造は無視します。 |
cv::RETR_CCOMP | 輪郭を2レベルの階層構造で検出します。外側の輪郭と内側の輪郭を分けて管理します。 |
cv::RETR_TREE | 全ての輪郭を階層構造として検出します。親子関係を含む完全な階層情報を取得可能です。 |
メソッド(Contour Approximation Method)
輪郭の点の表現方法を指定します。
主なメソッドは以下の通りです。
メソッド名 | 説明 |
---|---|
cv::CHAIN_APPROX_NONE | 輪郭の全ての点を保存します。 |
cv::CHAIN_APPROX_SIMPLE | 直線部分の中間点を省略し、輪郭を圧縮して表現します。 |
cv::CHAIN_APPROX_TC89_L1 | Teh-Chinのアルゴリズム(L1距離)による近似。 |
cv::CHAIN_APPROX_TC89_KCOS | Teh-Chinのアルゴリズム(コサイン類似度)による近似。 |
通常はcv::CHAIN_APPROX_SIMPLE
が使われ、輪郭点数を減らして処理効率を高めます。
使用例
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat src = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (src.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
// 二値化
cv::Mat binary;
cv::threshold(src, binary, 100, 255, cv::THRESH_BINARY);
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;
// 輪郭検出(外側輪郭のみ、簡易近似)
cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
// 輪郭数を表示
std::cout << "検出された輪郭数: " << contours.size() << std::endl;
return 0;
}
検出された輪郭数: 267
近似多角形と面積抽出
輪郭は多数の点で構成されるため、そのままでは扱いにくいことがあります。
cv::approxPolyDP
関数を使うと、輪郭を近似多角形に変換し、点数を減らして形状を簡略化できます。
近似多角形の計算
cv::approxPolyDP
は、輪郭の曲線を指定した精度(ε)で近似します。
εは輪郭と近似多角形の最大距離の許容値で、値が小さいほど元の輪郭に近い形状になります。
std::vector<cv::Point> approx;
double epsilon = 0.01 * cv::arcLength(contour, true);
cv::approxPolyDP(contour, approx, epsilon, true);
面積の計算
近似多角形や輪郭の面積はcv::contourArea
で計算できます。
物体の大きさや領域の判定に利用されます。
double area = cv::contourArea(approx);
応用例
- 多角形近似で四角形を検出し、ドキュメントの輪郭抽出に使います
- 面積で小さなノイズ輪郭を除去します
輪郭階層の活用シーン
cv::findContours
で取得できる階層情報は、輪郭の親子関係を表します。
階層はstd::vector<cv::Vec4i>
型で、各輪郭の情報が格納されています。
階層の各要素は4つの整数で構成され、順に以下を示します。
- 次の輪郭のインデックス
- 前の輪郭のインデックス
- 子輪郭のインデックス
- 親輪郭のインデックス
活用例
- 穴の検出
親輪郭の内側にある子輪郭は穴や内部の空洞を表します。
例えば、ドーナツ形状の輪郭検出に使えます。
- 階層的な物体解析
複雑な形状の中に複数の輪郭がある場合、親子関係を利用して構造を解析できます。
- ノイズ除去
小さな子輪郭を無視することで、誤検出を減らせます。
階層情報の利用例
for (size_t i = 0; i < contours.size(); ++i) {
int parentIdx = hierarchy[i][3];
if (parentIdx < 0) {
std::cout << "輪郭 " << i << " は親輪郭です。" << std::endl;
} else {
std::cout << "輪郭 " << i << " は親輪郭 " << parentIdx << " の子輪郭です。" << std::endl;
}
}
階層情報を活用することで、画像内の複雑な輪郭構造を効率的に扱えます。
ポストプロセッシング
モルフォロジー演算
モルフォロジー演算は、画像の形状や構造を操作するための基本的な画像処理手法です。
主に二値画像に対して適用し、ノイズ除去やエッジの強調、穴埋めなどに利用されます。
OpenCVではcv::erode
やcv::dilate
などの関数で実装されています。
erodeとdilateの組み合わせ
erode
(収縮)
画像の白色領域(前景)を縮小させる処理です。
小さなノイズや細い線を消す効果があります。
具体的には、カーネル(構造要素)を画像上でスライドさせ、カーネル内の全ての画素が白でなければ中心画素を黒にします。
dilate
(膨張)
画像の白色領域を拡大させる処理です。
細い線を太くしたり、穴を埋めたりする効果があります。
カーネル内に白い画素が1つでもあれば中心画素を白にします。
これらを組み合わせることで、様々な効果を得られます。
- 収縮→膨張(Opening)
小さなノイズを除去し、物体の形状を保ちます。
- 膨張→収縮(Closing)
小さな穴や隙間を埋め、物体を滑らかにします。
以下はerode
とdilate
の基本的な使い方です。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat binary = cv::imread("binary_image.png", cv::IMREAD_GRAYSCALE);
if (binary.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
// カーネルの作成(3x3の正方形)
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
// 収縮処理
cv::Mat eroded;
cv::erode(binary, eroded, kernel);
// 膨張処理
cv::Mat dilated;
cv::dilate(binary, dilated, kernel);
cv::imshow("Original", binary);
cv::imshow("Eroded", eroded);
cv::imshow("Dilated", dilated);
cv::waitKey(0);
return 0;
}
openingとclosingでノイズ除去
OpenCVにはcv::morphologyEx
関数でモルフォロジー演算の複合処理を簡単に行えます。
- Opening(オープニング)
収縮→膨張の順で処理し、小さなノイズを除去します。
物体の形状はほぼ維持されます。
- Closing(クロージング)
膨張→収縮の順で処理し、小さな穴や隙間を埋めます。
cv::Mat opened, closed;
cv::morphologyEx(binary, opened, cv::MORPH_OPEN, kernel);
cv::morphologyEx(binary, closed, cv::MORPH_CLOSE, kernel);
これらの処理は、エッジ検出後のノイズ除去や輪郭の滑らかさ向上に効果的です。
細線化アルゴリズム
細線化(スケルトン化)は、物体の形状を保ちながら線幅を1ピクセルに細くする処理です。
エッジや輪郭の形状解析、パターン認識に役立ちます。
OpenCVには細線化の専用関数はありませんが、以下のような方法で実装できます。
- Zhang-Suenアルゴリズム
反復的に画素を削除し、形状を保ちながら細線化します。
- OpenCVの
ximgproc
モジュールのthinning
関数
OpenCVの拡張モジュールopencv_contrib
に含まれており、簡単に細線化が可能です。
#include <opencv2/opencv.hpp>
#include <opencv2/ximgproc.hpp>
int main() {
cv::Mat binary = cv::imread("binary_image.png", cv::IMREAD_GRAYSCALE);
if (binary.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
cv::Mat thinned;
cv::ximgproc::thinning(binary, thinned);
cv::imshow("Original", binary);
cv::imshow("Thinned", thinned);
cv::waitKey(0);
return 0;
}
細線化は、文字認識や指紋解析など、細かい形状の特徴抽出に有効です。
ROI抽出で計算コスト削減
ROI(Region of Interest)抽出は、画像の一部領域だけを対象に処理を行うことで、計算コストを削減し処理速度を向上させる手法です。
エッジ検出や輪郭検出の後、興味のある領域だけを切り出してポストプロセッシングを行うことが多いです。
cv::Rect roi(50, 50, 200, 200); // x, y, width, height
cv::Mat roiImage = image(roi);
// ROIに対して処理を実行
cv::Mat edges;
cv::Canny(roiImage, edges, 100, 200);
cv::imshow("ROI Edges", edges);
cv::waitKey(0);
ROIを使うことで、不要な領域の処理を省略し、リアルタイム処理や大規模画像の高速化に貢献します。
さらに、ROIの位置やサイズは動的に変更可能で、対象物の追跡や検出結果に応じて柔軟に対応できます。
パフォーマンス最適化
メモリアクセス削減テクニック
画像処理において、メモリアクセスは処理速度に大きな影響を与えます。
特に大きな画像やリアルタイム処理では、不要なメモリアクセスを減らすことが重要です。
以下のテクニックでメモリアクセスを最適化できます。
- 連続メモリの活用
OpenCVのcv::Mat
は連続したメモリ領域に画像データを格納しています。
isContinuous()
メソッドで確認し、連続している場合はポインタを使った高速アクセスが可能です。
- ポインタアクセスの活用
at<>()
関数は安全ですが、ループ内で大量に呼ぶとオーバーヘッドが大きいです。
代わりにポインタを使って直接アクセスすると高速化できます。
uchar* ptr = image.ptr<uchar>(row);
for (int col = 0; col < image.cols; ++col) {
uchar pixel = ptr[col];
// 処理
}
- ROIの活用
処理対象を必要な領域に限定し、不要な画素アクセスを減らします。
- キャッシュ効率の向上
画像の走査は行単位(row-major)で行い、キャッシュミスを減らします。
列単位のアクセスは避けるべきです。
- メモリコピーの削減
不要なコピーを避け、参照やポインタでデータを扱うことでメモリ帯域の節約になります。
cv::parallel_for_による並列化
OpenCVはマルチコアCPUを活用するために、cv::parallel_for_
という並列処理のためのAPIを提供しています。
これを使うと、ループ処理を複数スレッドで分割して高速化できます。
基本的な使い方
#include <opencv2/opencv.hpp>
#include <iostream>
class ParallelProcess : public cv::ParallelLoopBody {
public:
cv::Mat& image;
ParallelProcess(cv::Mat& img) : image(img) {}
void operator()(const cv::Range& range) const override {
for (int r = range.start; r < range.end; ++r) {
uchar* ptr = image.ptr<uchar>(r);
for (int c = 0; c < image.cols; ++c) {
// 例:画素値を反転
ptr[c] = 255 - ptr[c];
}
}
}
};
int main() {
cv::Mat image = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (image.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
ParallelProcess body(image);
cv::parallel_for_(cv::Range(0, image.rows), body);
cv::imshow("Processed Image", image);
cv::waitKey(0);
return 0;
}
この例では、画像の各行を複数スレッドで分割して処理しています。
cv::parallel_for_
は内部でスレッドプールを管理し、効率的に並列化を行います。
利点
- マルチコアCPUの性能を最大限に活用できます
- OpenMPやTBBなどの外部ライブラリに依存せず、OpenCV単体で利用可能です
- 既存のループ処理を簡単に並列化できます
CUDAモジュールでのGPU高速化
OpenCVはCUDA対応のモジュールを備えており、NVIDIAのGPUを活用して画像処理を高速化できます。
CUDAを使うと、膨大な並列演算能力を利用してリアルタイム処理や大規模画像処理が可能になります。
CUDAモジュールの特徴
- GPUメモリ上で画像を扱う
cv::cuda::GpuMat
クラスを使用 - 多くの画像処理関数がCUDA対応版として用意されている(例:
cv::cuda::cvtColor
、cv::cuda::Canny
など) - CPUとGPU間のデータ転送はコストが高いため、できるだけGPU上で連続処理を行うことが重要でしょう
基本的な使い方例
#include <opencv2/opencv.hpp>
#include <opencv2/cudaimgproc.hpp>
#include <opencv2/cudaarithm.hpp>
#include <iostream>
int main() {
cv::Mat src = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (src.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
// CPUからGPUへデータ転送
cv::cuda::GpuMat d_src(src);
cv::cuda::GpuMat d_blurred, d_edges;
// ガウシアンブラー(GPU版)
cv::cuda::GaussianBlur(d_src, d_blurred, cv::Size(5, 5), 1.5);
// Cannyエッジ検出(GPU版)
cv::Ptr<cv::cuda::CannyEdgeDetector> canny = cv::cuda::createCannyEdgeDetector(100.0, 200.0);
canny->detect(d_blurred, d_edges);
// GPUからCPUへ結果転送
cv::Mat edges;
d_edges.download(edges);
cv::imshow("Edges", edges);
cv::waitKey(0);
return 0;
}
注意点
- CUDA対応GPUと対応ドライバが必要でしょう
- GPUメモリへの転送回数を減らすことがパフォーマンス向上の鍵
- CUDAモジュールはOpenCVの
opencv_contrib
に含まれているため、ビルド時に有効化が必要でしょう
これらのパフォーマンス最適化手法を組み合わせることで、CPUとGPUのリソースを効率的に活用し、高速かつスケーラブルな画像処理を実現できます。
特にリアルタイム処理や大規模画像解析では不可欠な技術です。
実践ユースケース集
ドキュメントスキャナでの用紙輪郭抽出
ドキュメントスキャナでは、撮影した画像から用紙の輪郭を正確に抽出し、傾き補正やトリミングを行うことが重要です。
OpenCVのエッジ検出と輪郭検出機能を組み合わせて実装できます。
処理の流れ
- 画像の読み込みとグレースケール変換
カラー画像をグレースケールに変換し、輝度情報に絞ります。
- ノイズ除去
ガウシアンブラーでノイズを低減し、エッジ検出の精度を向上させます。
- エッジ検出
Cannyエッジ検出を用いて用紙の境界線を抽出します。
- 輪郭検出
cv::findContours
でエッジ画像から輪郭を検出し、最大の輪郭(用紙の輪郭)を特定します。
- 輪郭近似
cv::approxPolyDP
で輪郭を四角形に近似し、用紙の四隅を抽出します。
- 透視変換
抽出した四隅を基に透視変換を行い、用紙を正面から撮影したように補正します。
サンプルコード(輪郭検出部分)
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat image = cv::imread("document.jpg");
if (image.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
cv::Mat gray, blurred, edges;
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 1.5);
cv::Canny(blurred, edges, 50, 150);
std::vector<std::vector<cv::Point>> contours;
cv::findContours(edges, contours, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);
double maxArea = 0;
std::vector<cv::Point> maxContour;
for (const auto& contour : contours) {
double area = cv::contourArea(contour);
if (area > maxArea) {
maxArea = area;
maxContour = contour;
}
}
std::vector<cv::Point> approx;
double epsilon = 0.02 * cv::arcLength(maxContour, true);
cv::approxPolyDP(maxContour, approx, epsilon, true);
if (approx.size() == 4) {
cv::polylines(image, approx, true, cv::Scalar(0, 255, 0), 3);
}
cv::imshow("Document Contour", image);
cv::waitKey(0);
return 0;
}


このコードは用紙の輪郭を検出し、四角形として描画します。
透視変換を加えることで、スキャン品質の向上が可能です。
車線認識システムへの適用
自動運転や運転支援システムでは、車線の検出が安全運転の基盤となります。
OpenCVのエッジ検出とハフ変換を組み合わせて車線を認識します。
処理のポイント
- ROI設定
画像の下半分など車線が存在する領域に限定して処理し、計算効率を上げます。
- 前処理
グレースケール変換、ガウシアンブラーでノイズ除去を行います。
- エッジ検出
Cannyエッジ検出で車線の境界を抽出します。
- ハフ変換による直線検出
cv::HoughLinesP
で車線の直線を検出し、線分の長さや角度でフィルタリングします。
- 線分の統合と描画
複数の線分を車線としてまとめ、画像に描画します。
サンプルコード(車線検出の一部)
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat frame = cv::imread("road.jpg");
if (frame.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
cv::Mat gray, blurred, edges;
cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 1.5);
cv::Canny(blurred, edges, 50, 150);
// ROI設定(画像の下半分)
cv::Rect roi(0, frame.rows / 2, frame.cols, frame.rows / 2);
cv::Mat roiEdges = edges(roi);
std::vector<cv::Vec4i> lines;
cv::HoughLinesP(roiEdges, lines, 1, CV_PI / 180, 50, 50, 10);
for (const auto& line : lines) {
cv::line(frame, cv::Point(line[0], line[1] + roi.y), cv::Point(line[2], line[3] + roi.y), cv::Scalar(0, 0, 255), 3);
}
cv::imshow("Lane Detection", frame);
cv::waitKey(0);
return 0;
}
このコードは車線の直線を検出し、元画像に赤線で描画します。


簡易的なものであるため一部ガードレールもご検出していますが、実際のシステムでは、線分の角度や位置でさらにフィルタリングし、車線の左右を判別するなどより高精度な処理を行います。
監視カメラ映像のリアルタイム処理
監視カメラ映像の解析では、リアルタイムでエッジ検出や物体検出を行う必要があります。
OpenCVの高速処理機能やGPU活用を組み合わせて実装します。
実装のポイント
- 映像キャプチャ
cv::VideoCapture
でカメラ映像を取得します。
- 前処理の高速化
解像度を適切に下げ、ガウシアンブラーやCannyのパラメータを調整して処理負荷を軽減します。
- 並列処理・GPU活用
cv::parallel_for_
やCUDAモジュールを使い、処理を高速化します。
- 動的ROI設定
動きのある領域や関心領域だけを処理対象に絞り、無駄な計算を減らします。
サンプルコード(リアルタイムエッジ検出)
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::VideoCapture cap(0);
if (!cap.isOpened()) {
std::cerr << "カメラの起動に失敗しました。" << std::endl;
return -1;
}
cv::Mat frame, gray, blurred, edges;
while (true) {
cap >> frame;
if (frame.empty()) break;
cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 1.5);
cv::Canny(blurred, edges, 50, 150);
cv::imshow("Real-time Edges", edges);
if (cv::waitKey(1) == 27) break; // ESCキーで終了
}
return 0;
}
このコードはカメラ映像をリアルタイムで取得し、エッジ検出結果を表示します。
実際の監視システムでは、検出結果を基に動体検知や異常検知を行うことが多いです。
これらのユースケースは、OpenCVのエッジ検出技術を実際のアプリケーションに応用した例です。
各シナリオに応じて前処理やパラメータ調整を行い、最適な検出性能を目指してください。
失敗例とトラブルシューティング
入力画像が読み込めない場合
OpenCVで画像を読み込む際にcv::imread
が空のcv::Mat
を返すことがあります。
これは画像ファイルが正しく読み込めなかったことを意味し、以下の原因が考えられます。
- ファイルパスの誤り
ファイル名やパスが間違っている、または相対パスの基準が異なる場合があります。
絶対パスを使うか、実行ファイルのカレントディレクトリを確認してください。
- ファイル形式の非対応
OpenCVが対応していない画像形式や破損したファイルの場合、読み込みに失敗します。
JPEG、PNG、BMPなど一般的な形式を使い、ファイルの整合性を確認してください。
- 権限不足
ファイルにアクセス権限がない場合も読み込みに失敗します。
ファイルの読み取り権限を確認してください。
- OpenCVのビルド設定
OpenCVが特定の画像フォーマットのサポートをビルド時に有効化していない場合があります。
特にWindows環境で注意が必要です。
対策例
cv::Mat image = cv::imread("image.jpg");
if (image.empty()) {
std::cerr << "画像の読み込みに失敗しました。パスやファイル形式を確認してください。" << std::endl;
return -1;
}
ファイルパスを絶対パスに変更したり、ファイルの存在を事前に確認することも有効です。
Canny結果が真っ白・真っ黒になる要因
Cannyエッジ検出の結果が真っ白(全てエッジ)または真っ黒(エッジなし)になる場合、主に以下の原因が考えられます。
- しきい値の設定ミス
Cannyの低しきい値と高しきい値が不適切だと、全画素がエッジと判定されたり、逆に全く検出されなかったりします。
例:低しきい値が高すぎるとエッジが検出されにくくなり、低すぎるとノイズが多く検出されます。
- 入力画像の前処理不足
ノイズが多い画像やコントラストが低い画像では、エッジ検出がうまくいきません。
ガウシアンブラーなどの平滑化やコントラスト調整を行いましょう。
- 画像の型やチャンネル数の誤り
Cannyはグレースケール画像を想定しています。
カラー画像をそのまま渡すと正しく動作しません。
- 画像が空または不正なデータ
読み込み失敗や前処理で空の画像を渡すと、結果が不正になります。
対策例
cv::Mat gray;
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
cv::Mat blurred;
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 1.5);
double lowThresh = 50;
double highThresh = 150;
cv::Mat edges;
cv::Canny(blurred, edges, lowThresh, highThresh);
if (cv::countNonZero(edges) == 0) {
std::cerr << "エッジが検出されませんでした。しきい値や前処理を見直してください。" << std::endl;
}
しきい値は画像に応じて調整し、前処理を適切に行うことが重要です。
HoughLinesPで直線が検出されない時
確率的ハフ変換cv::HoughLinesP
で直線が検出されない場合、以下の原因が考えられます。
- エッジ画像の品質不足
Cannyなどのエッジ検出結果が不十分だと、ハフ変換で有効な線分が得られません。
前処理やしきい値の調整を行い、エッジを鮮明にしましょう。
- パラメータ設定の不適切さ
threshold
(投票数の閾値)が高すぎると線分が検出されにくくなりますminLineLength
が大きすぎると短い線分が無視されますmaxLineGap
が小さすぎると断続的な線分が分断されます
- ROIの設定ミス
処理対象の領域が間違っていると、線分が存在しない領域を処理している可能性があります。
- 画像の解像度やスケールの問題
画像が小さすぎたり、縮小されすぎていると線分が検出されにくくなります。
対策例
std::vector<cv::Vec4i> lines;
cv::HoughLinesP(edges, lines, 1, CV_PI / 180, 50, 30, 10);
if (lines.empty()) {
std::cerr << "直線が検出されませんでした。パラメータやエッジ検出結果を見直してください。" << std::endl;
}
パラメータを段階的に調整し、エッジ画像の品質を確認しながら最適値を探すことが重要です。
例えば、threshold
を下げたり、minLineLength
を短くすることで検出率が上がることがあります。
これらのトラブルは画像処理の基本的な部分でよく起こるため、原因を切り分けて順に対処することが成功の鍵です。
ログや表示を活用し、入力画像や中間結果を確認しながら調整してください。
カスタムフィルター設計
任意カーネルの作り方
画像処理において、特定の効果を狙ったフィルターを設計したい場合、任意のカーネル(フィルターマトリクス)を自作して適用することができます。
カーネルは画像の各画素に対して周囲の画素値を重み付けして合成する行列で、エッジ検出やぼかし、シャープ化など様々な効果を生み出します。
カーネルの基本構造
カーネルは通常、奇数サイズの正方行列(例:3×3、5×5)で表現されます。
各要素は重みを示し、画像の対応する画素に乗算されます。
例えば、3×3のカーネルは以下のように定義します。
[ k00, k01, k02 ]
[ k10, k11, k12 ]
[ k20, k21, k22 ]
代表的なカーネル例
- エッジ検出(ラプラシアン)
[ 0, 1, 0 ]
[ 1, -4, 1 ]
[ 0, 1, 0 ]
- シャープ化
[ 0, -1, 0 ]
[ -1, 5, -1 ]
[ 0, -1, 0 ]
- ぼかし(平均化)
[ 1/9, 1/9, 1/9 ]
[ 1/9, 1/9, 1/9 ]
[ 1/9, 1/9, 1/9 ]
OpenCVでのカーネル作成例
OpenCVではcv::Mat
を使ってカーネルを作成します。
例えば、3×3のシャープ化カーネルは以下のように定義します。
cv::Mat kernel = (cv::Mat_<float>(3, 3) <<
0, -1, 0,
-1, 5, -1,
0, -1, 0);
カーネルの要素はfloat
やdouble
型で指定し、効果に応じて値を調整します。
cv::filter2Dによる適用手順
作成したカーネルを画像に適用するには、OpenCVのcv::filter2D
関数を使います。
この関数は任意のカーネルを用いて画像の畳み込み処理を行います。
基本的な使い方
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 画像の読み込み(グレースケール推奨)
cv::Mat src = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (src.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
// カスタムカーネルの作成(シャープ化)
cv::Mat kernel = (cv::Mat_<float>(3, 3) <<
0, -1, 0,
-1, 5, -1,
0, -1, 0);
// 出力画像用のMat
cv::Mat dst;
// filter2Dで畳み込み処理
cv::filter2D(src, dst, -1, kernel);
// 結果の表示
cv::imshow("Original", src);
cv::imshow("Filtered", dst);
cv::waitKey(0);
return 0;
}
パラメータ説明
- src
入力画像。
カラー画像の場合は各チャンネルに対して処理されます。
- dst
出力画像。
src
と同じサイズ・タイプになります。
- ddepth
出力画像の深さ(データ型)。
-1
を指定すると入力画像と同じになります。
カーネルの値によってはオーバーフローを防ぐためにCV_16S
やCV_32F
を指定することもあります。
- kernel
適用するカーネル行列。
- anchor(省略可)
カーネルの基準点。
通常は(-1, -1)
でカーネルの中心が基準点になります。
- delta(省略可)
出力画像に加算する値。
明るさ調整などに使います。
- borderType(省略可)
画像端の処理方法。
cv::BORDER_DEFAULT
が一般的です。
注意点
- カーネルのサイズや値によっては、画像の明るさが変わったり、エッジが強調されすぎたりすることがあります。適宜正規化や値の調整を行ってください
- カーネルの合計が1になるように正規化すると、画像の明るさを保ちやすくなります
- カーネルのサイズは奇数が推奨されます。偶数サイズはアンカー位置の指定が複雑になるためです
任意カーネルの設計とcv::filter2D
の活用により、OpenCVで自由自在に画像フィルタリングが可能です。
エッジ検出やぼかし、シャープ化など、目的に応じてカーネルをカスタマイズしてみてください。
可視化とデバッグ
カラーマップで勾配強度を表示
画像処理でエッジ検出や勾配計算を行う際、勾配強度の分布を視覚的に把握することは重要です。
単純なグレースケール表示では微妙な強度差がわかりにくいため、カラーマップを使って勾配強度を色で表現すると、エッジの強弱や分布が直感的に理解できます。
OpenCVでのカラーマップ適用方法
OpenCVのcv::applyColorMap
関数を使うと、8ビットの単一チャンネル画像に対して様々なカラーマップを適用できます。
勾配強度画像を8ビットに正規化してからカラーマップを適用するのが一般的です。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 画像の読み込みとグレースケール変換
cv::Mat src = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (src.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
// Sobelフィルターで勾配強度を計算
cv::Mat grad_x, grad_y;
cv::Sobel(src, grad_x, CV_32F, 1, 0, 3);
cv::Sobel(src, grad_y, CV_32F, 0, 1, 3);
cv::Mat magnitude;
cv::magnitude(grad_x, grad_y, magnitude);
// 勾配強度を0-255に正規化して8ビットに変換
cv::Mat mag_8u;
cv::normalize(magnitude, mag_8u, 0, 255, cv::NORM_MINMAX);
mag_8u.convertTo(mag_8u, CV_8U);
// カラーマップを適用(JETカラーマップ)
cv::Mat colorMap;
cv::applyColorMap(mag_8u, colorMap, cv::COLORMAP_JET);
// 表示
cv::imshow("Gradient Magnitude (ColorMap)", colorMap);
cv::waitKey(0);
return 0;
}
cv::magnitude
で勾配の大きさを計算し、浮動小数点型で保持しますcv::normalize
で勾配強度を0〜255の範囲にスケーリングし、CV_8U
に変換しますcv::applyColorMap
でJETカラーマップを適用し、青から赤までのグラデーションで強度を色分けします- これにより、弱いエッジは青系、強いエッジは赤系で視覚的に判別しやすくなります
ヒストグラムでしきい値確認
エッジ検出や二値化処理のしきい値設定は結果に大きく影響します。
しきい値を適切に決めるために、画像の輝度や勾配強度のヒストグラムを確認することが有効です。
ヒストグラムを可視化することで、画素値の分布やピークを把握し、しきい値の候補を見つけやすくなります。
ヒストグラムの計算と表示
OpenCVのcv::calcHist
関数でヒストグラムを計算し、cv::line
やcv::plot
(OpenCVの拡張)で描画します。
ここでは基本的な方法を紹介します。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat src = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (src.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
// ヒストグラム計算
int histSize = 256;
float range[] = {0, 256};
const float* histRange = {range};
cv::Mat hist;
cv::calcHist(&src, 1, 0, cv::Mat(), hist, 1, &histSize, &histRange);
// ヒストグラム画像の作成
int hist_w = 512, hist_h = 400;
int bin_w = cvRound((double)hist_w / histSize);
cv::Mat histImage(hist_h, hist_w, CV_8UC3, cv::Scalar(255, 255, 255));
// ヒストグラムの正規化
cv::normalize(hist, hist, 0, histImage.rows, cv::NORM_MINMAX);
// ヒストグラム描画
for (int i = 1; i < histSize; i++) {
cv::line(histImage,
cv::Point(bin_w * (i - 1), hist_h - cvRound(hist.at<float>(i - 1))),
cv::Point(bin_w * i, hist_h - cvRound(hist.at<float>(i))),
cv::Scalar(0, 0, 0), 2);
}
cv::imshow("Histogram", histImage);
cv::waitKey(0);
return 0;
}
cv::calcHist
でグレースケール画像の輝度ヒストグラムを計算します- ヒストグラムの値を画像の高さに正規化し、見やすくします
cv::line
で各ビンの値を線でつなぎ、ヒストグラムを描画します- ヒストグラムのピークや谷を観察し、しきい値の候補を決める参考にします
これらの可視化手法は、画像処理のパラメータ調整やアルゴリズムの動作確認に役立ちます。
特にエッジ検出のしきい値設定や勾配強度の分布把握に活用し、より精度の高い処理を目指してください。
OpenCVバージョン差異
3系と4系のAPI変更点
OpenCVは長年にわたり進化を続けており、特に3系から4系へのメジャーアップデートでは多くのAPI変更や機能改善が行われました。
これにより、コードの互換性や書き方に違いが生じています。
主な変更点を以下にまとめます。
名前空間の整理
- 3系
多くの関数やクラスがグローバル名前空間やcv
名前空間に散在していました。
- 4系
モジュールごとに名前空間が整理され、特にcv::dnn
やcv::ml
などのサブ名前空間が明確化されました。
これにより、コードの可読性と管理性が向上しています。
モジュールの分割と統合
- 3系
一部の機能がopencv_contrib
に分かれており、ビルド時に明示的に有効化する必要がありました。
- 4系
多くのcontrib
モジュールが標準に統合され、利用しやすくなりました。
ただし、一部のモジュールは依然としてopencv_contrib
に残っています。
関数のシグネチャ変更
- いくつかの関数で引数の型や順序が変更されました。例えば、
cv::findContours
の戻り値が3系ではvoid
で輪郭と階層を引数で受け取る形でしたが、4系では輪郭を戻り値として返すようになりました
// OpenCV 3系
cv::findContours(binary, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
// OpenCV 4系
contours = cv::findContours(binary, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
デフォルトパラメータの変更
- 一部の関数でデフォルト引数が変更され、動作が微妙に異なる場合があります。例えば、
cv::resize
の補間方法のデフォルトが変わることがあります
C++11以降の機能活用
- 4系ではC++11以降の機能を積極的に活用し、スマートポインタやラムダ式、範囲ベースfor文などが推奨されています。これによりコードが簡潔かつ安全になりました
非推奨関数の代替策
OpenCV 4系では、3系で使われていた一部の関数やAPIが非推奨(deprecated)となり、将来的に削除される可能性があります。
これらの関数を使い続けると警告が出るため、代替策を知っておくことが重要です。
cv::findNonZero
- 非推奨理由
3系ではcv::findNonZero
が使われていましたが、4系ではより効率的な方法が推奨されています。
- 代替策
cv::findNonZero
自体は残っていますが、cv::Mat::forEach
やcv::countNonZero
と組み合わせて使う方法が増えています。
cv::imreadmulti
- 非推奨理由
複数ページの画像読み込みで使われていましたが、4系ではより安定したAPIが提供されています。
- 代替策
代わりにcv::VideoCapture
や外部ライブラリを使うことが推奨されます。
cv::createTrackbarのコールバック仕様
- 非推奨理由
コールバック関数のシグネチャが変更され、古い形式は非推奨です。
- 代替策
新しいコールバック形式に合わせて関数を修正してください。
cv::oclモジュール
- 非推奨理由
OpenCLベースのcv::ocl
モジュールは4系で非推奨となり、代わりにCUDAや他のGPUアクセラレーションが推奨されています。
cv::VideoWriterのAPI変更
- 一部のコーデック指定方法やパラメータが変更されているため、4系のドキュメントを参照して適切に修正してください
非推奨関数の確認方法
OpenCVのビルド時や実行時に非推奨関数を使うと警告が表示されます。
また、公式ドキュメントやリリースノートで非推奨リストを確認できます。
コードのメンテナンス時にはこれらを参照し、最新のAPIに置き換えることが推奨されます。
OpenCV 3系から4系への移行では、APIの変更点や非推奨関数の代替策を理解し、コードを適切に更新することが重要です。
これにより、最新の機能や最適化を活用しつつ、将来的な互換性を確保できます。
コードメンテナンスとテスト
例外安全なリソース管理
C++でOpenCVを使った画像処理プログラムを開発する際、例外安全なリソース管理は非常に重要です。
画像データやメモリ、ファイルハンドルなどのリソースを適切に管理しないと、例外発生時にリソースリークや不整合が起こりやすくなります。
RAII(Resource Acquisition Is Initialization)による管理
OpenCVのcv::Mat
は内部で参照カウントを持つスマートなクラスであり、RAIIの原則に従ってメモリ管理が行われています。
これにより、cv::Mat
オブジェクトのスコープを抜けると自動的にメモリが解放されるため、基本的にはメモリリークの心配は少ないです。
void processImage(const std::string& filename) {
cv::Mat image = cv::imread(filename);
if (image.empty()) {
throw std::runtime_error("画像の読み込みに失敗しました");
}
// imageはスコープ終了時に自動解放される
}
例外安全なファイル操作
ファイルや外部リソースを扱う場合は、std::ifstream
やstd::ofstream
などのRAII対応クラスを使い、例外発生時も自動的にクローズされるようにします。
#include <fstream>
void readConfig(const std::string& path) {
std::ifstream file(path);
if (!file) {
throw std::runtime_error("設定ファイルが開けません");
}
// ファイルはスコープ終了時に自動的に閉じられる
}
例外安全なOpenCVリソース管理のポイント
cv::VideoCapture
やcv::VideoWriter
これらもRAIIを採用しているため、オブジェクトの破棄時にリソースが解放されます。
ただし、明示的にrelease()
を呼ぶことで早期解放も可能です。
- 例外キャッチとリソース解放
例外が発生する可能性がある処理はtry-catch
ブロックで囲み、必要に応じてログ出力やリソースの明示的解放を行います。
try {
cv::Mat img = cv::imread("image.jpg");
if (img.empty()) throw std::runtime_error("画像が空です");
// 処理
} catch (const std::exception& e) {
std::cerr << "例外発生: " << e.what() << std::endl;
}
GoogleTestでの画像差分テスト
画像処理アルゴリズムの品質を保証するためには、単体テストや回帰テストが欠かせません。
GoogleTest(gtest)はC++向けのテストフレームワークで、OpenCVの画像処理結果の差分テストにも活用できます。
画像差分テストの基本アイデア
- 処理前後の画像や期待される結果画像と実際の出力画像を比較し、差分が許容範囲内かを判定します
- 差分画像を作成し、ピクセル単位での違いを検出します
#include <gtest/gtest.h>
#include <opencv2/opencv.hpp>
bool isImageSimilar(const cv::Mat& img1, const cv::Mat& img2, double maxDiff = 1e-5) {
if (img1.size() != img2.size() || img1.type() != img2.type()) {
return false;
}
cv::Mat diff;
cv::absdiff(img1, img2, diff);
double maxVal;
cv::minMaxLoc(diff, nullptr, &maxVal);
return maxVal <= maxDiff;
}
TEST(ImageProcessingTest, EdgeDetectionConsistency) {
cv::Mat input = cv::imread("test_input.jpg", cv::IMREAD_GRAYSCALE);
ASSERT_FALSE(input.empty());
cv::Mat expected = cv::imread("expected_edges.jpg", cv::IMREAD_GRAYSCALE);
ASSERT_FALSE(expected.empty());
cv::Mat blurred;
cv::GaussianBlur(input, blurred, cv::Size(5, 5), 1.5);
cv::Mat edges;
cv::Canny(blurred, edges, 50, 150);
EXPECT_TRUE(isImageSimilar(edges, expected, 10.0)) << "エッジ検出結果が期待値と異なります";
}
isImageSimilar
関数は2つの画像の最大ピクセル差を計算し、指定した閾値以下なら類似と判定します- テストケースでは、入力画像に対してエッジ検出を行い、期待される結果画像と比較しています
- 差分が大きい場合はテストが失敗し、メッセージを表示します
テストのポイント
- 閾値設定
画像処理は微小な差異が生じやすいため、完全一致ではなく適切な許容範囲を設定します。
- テストデータ管理
入力画像や期待画像はバージョン管理し、変更時は差分を確認します。
- 自動化
CI環境に組み込み、コード変更時に自動でテストを実行し品質を維持します。
例外安全なリソース管理と画像差分テストを組み合わせることで、堅牢で信頼性の高いOpenCVアプリケーションの開発が可能になります。
これらは長期的なメンテナンス性向上にも寄与します。
さらなる発展手法
マルチスケールエッジ検出
マルチスケールエッジ検出は、異なるスケール(解像度や平滑化レベル)で画像のエッジを検出し、それらを統合することで、より堅牢かつ詳細なエッジ情報を得る手法です。
単一スケールのエッジ検出では、小さなノイズや細かいテクスチャが誤検出されたり、大きな構造が見逃されたりすることがありますが、マルチスケール処理によりこれらの問題を緩和できます。
処理の流れ
- 複数のスケールで画像を生成
ガウシアンピラミッドや異なるカーネルサイズのガウシアンブラーを用いて、複数の平滑化レベルの画像を作成します。
- 各スケールでエッジ検出
それぞれのスケール画像に対してCannyやSobelなどのエッジ検出を実施します。
- エッジ情報の統合
各スケールのエッジマップを重み付けや論理和で統合し、最終的なエッジ画像を生成します。
メリット
- ノイズ耐性の向上
大きなスケールでの平滑化によりノイズが除去され、小さなスケールでの詳細なエッジも保持できます。
- 多様なエッジの検出
細かいテクスチャから大きな輪郭まで幅広く検出可能です。
サンプルコード例
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat src = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (src.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
std::vector<cv::Mat> edgeMaps;
std::vector<int> kernelSizes = {3, 5, 7};
for (int ksize : kernelSizes) {
cv::Mat blurred, edges;
cv::GaussianBlur(src, blurred, cv::Size(ksize, ksize), 0);
cv::Canny(blurred, edges, 50, 150);
edgeMaps.push_back(edges);
}
// 複数スケールのエッジを論理和で統合
cv::Mat combinedEdges = cv::Mat::zeros(src.size(), CV_8U);
for (const auto& edge : edgeMaps) {
combinedEdges |= edge;
}
cv::imshow("Multi-scale Edges", combinedEdges);
cv::waitKey(0);
return 0;
}
この例では、3種類の異なるガウシアンカーネルサイズで平滑化し、それぞれにCannyエッジ検出を適用。

最後に論理和で統合しています。
ディープラーニングとのハイブリッドアプローチ
近年、ディープラーニング(DL)を用いたエッジ検出が注目されています。
従来の手法(CannyやSobel)とDLを組み合わせることで、より高精度かつ頑健なエッジ検出が可能になります。
ハイブリッドアプローチの例
- 前処理にDLを活用
ノイズ除去や特徴抽出にCNN(畳み込みニューラルネットワーク)を用い、従来のエッジ検出の入力画像を最適化します。
- DLによるエッジマップ生成
HED(Holistically-Nested Edge Detection)やRCF(Richer Convolutional Features)などの深層学習モデルでエッジマップを生成し、従来手法の結果と組み合わせて精度向上を図ります。
- 後処理に従来手法を適用
DLで得たエッジマップに対して、モルフォロジー演算や輪郭抽出を行い、ノイズ除去やエッジの細線化を実施します。
メリット
- 複雑なパターンの検出
DLはテクスチャや照明変化に強く、従来手法では検出困難なエッジも抽出可能です。
- 適応性の向上
学習により特定のシーンや対象物に最適化でき、汎用性が高まります。
OpenCVでの活用例
OpenCVのdnn
モジュールを使い、学習済みのエッジ検出モデルを読み込んで推論できます。
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <iostream>
int main() {
cv::Mat src = cv::imread("image.jpg");
if (src.empty()) {
std::cerr << "画像の読み込みに失敗しました。" << std::endl;
return -1;
}
// 学習済みモデルの読み込み(例:HED)
cv::dnn::Net net = cv::dnn::readNetFromCaffe("deploy.prototxt", "hed_pretrained.caffemodel");
cv::Mat blob = cv::dnn::blobFromImage(src, 1.0, cv::Size(src.cols, src.rows),
cv::Scalar(104.00698793, 116.66876762, 122.67891434), false, false);
net.setInput(blob);
cv::Mat edges = net.forward();
// 出力を正規化して8ビット画像に変換
cv::Mat edgesResized, edges8U;
cv::resize(edges.reshape(1, src.rows), edgesResized, src.size());
edgesResized.convertTo(edges8U, CV_8U, 255);
cv::imshow("Deep Learning Edges", edges8U);
cv::waitKey(0);
return 0;
}
注意点
- 学習済みモデルの準備が必要で、モデルのサイズや推論速度に注意が必要です
- GPUを活用すると推論速度が大幅に向上します
- 従来手法との組み合わせで、ノイズ除去やエッジの細線化を行うとより良い結果が得られます
マルチスケールエッジ検出とディープラーニングのハイブリッドアプローチは、従来の単一スケール・単純手法を超えた高精度なエッジ検出を実現します。
用途や環境に応じて使い分け、最適なエッジ検出を目指してください。
まとめ
本記事では、OpenCVを用いたエッジ検出の基本から応用まで幅広く解説しました。
CannyやSobel、Laplacian、ハフ変換などの代表的手法の特徴やパラメータ調整、前処理・後処理のポイントを理解できます。
さらに、パフォーマンス最適化や実践的なユースケース、トラブルシューティング、最新の発展手法まで網羅。
これにより、画像内の境界線を効果的かつ高精度に抽出する技術を習得し、実務での応用に役立てられます。