OpenCV

【C++】OpenCVで学ぶHoughCirclesによる円検出と精度向上テクニック

C++とOpenCVで円を検出するには画像をグレースケール変換しノイズをmedianBlurで抑えた後、HoughCirclesを実行し投票数や最小距離などのパラメータを調整します。

中心と半径はcv::Vec3fで取得できcircleで描画可能です。

Canny閾値と解像度比を最適化すると精度と速度が向上します。

HoughCirclesの基本原理

OpenCVのHoughCircles関数は、画像内の円を検出するための強力なツールです。

この関数はハフ変換(Hough変換)という手法を用いており、画像のエッジ情報から円の中心座標と半径を推定します。

ここでは、HoughCirclesの基本的な仕組みを理解するために、円のパラメータ空間や投票方式、そしてグレースケール変換の重要性について詳しく説明します。

円のパラメータ空間

円は2次元平面上で、中心座標 \((x, y)\) と半径 \(r\) の3つのパラメータで表現されます。

ハフ変換を用いた円検出では、これらのパラメータ空間を探索して、画像内の円を特定します。

具体的には、画像のエッジ点から「この点が円の一部である」と仮定し、可能性のある中心座標と半径の組み合わせに投票を行います。

すべてのエッジ点の投票結果を集計し、投票数が閾値を超えたパラメータの組み合わせが円として検出されます。

このパラメータ空間は3次元であり、計算量が膨大になるため、OpenCVのHoughCirclesでは効率的なアルゴリズムが採用されています。

特に、HOUGH_GRADIENT法は勾配情報を利用して計算を高速化しています。

投票方式と閾値

ハフ変換の投票方式は、画像のエッジ点が円のパラメータ空間に対して「投票」する仕組みです。

エッジ点の位置と勾配方向から、円の中心候補を計算し、その候補に投票します。

複数のエッジ点が同じ中心候補に投票すると、その候補の得票数が増えます。

OpenCVのHoughCircles関数では、投票数の閾値をparam2で指定します。

この値は、円として認識するために必要な最低投票数を意味します。

値が高いほど検出される円は厳選され、誤検出が減りますが、検出漏れのリスクも高まります。

逆に低い値に設定すると、より多くの円が検出されますが、ノイズや誤検出も増えやすくなります。

また、param1はCannyエッジ検出の高い閾値として使われ、エッジ検出の感度を調整します。

これにより、投票に使われるエッジ点の質が変わり、結果の精度に影響します。

グレースケール変換の重要性

HoughCirclesを使う前の画像処理で最も基本的かつ重要なステップが、カラー画像をグレースケール画像に変換することです。

カラー画像は3チャンネル(BGR)を持ちますが、円検出のアルゴリズムは輝度情報のみに基づいて動作します。

グレースケール変換を行うことで、画像の輝度成分だけを抽出し、エッジ検出やハフ変換の計算負荷を大幅に軽減できます。

さらに、カラー情報が混在しているとエッジ検出の結果が不安定になることがあるため、グレースケール化は安定した検出結果を得るために欠かせません。

OpenCVではcv::cvtColor関数を使って簡単にグレースケール変換が可能です。

例えば、

cv::cvtColor(srcImage, grayImage, cv::COLOR_BGR2GRAY);

のように記述します。

ここでsrcImageは元のカラー画像、grayImageは変換後のグレースケール画像です。

このグレースケール画像を使って、次にノイズ除去やエッジ検出を行い、円検出の精度を高めていきます。

グレースケール変換は、円検出の前処理として必ず実施することをおすすめします。

前処理で精度を高める方法

円検出の精度を向上させるためには、HoughCirclesを実行する前の画像処理、つまり前処理が非常に重要です。

ここでは、ノイズ除去やコントラスト調整、検出範囲の限定など、効果的な前処理手法を具体的に解説します。

この記事では主にこの写真を使用します

平滑化フィルタの種類と効果

画像のノイズを減らすために平滑化フィルタを使います。

ノイズが多いとエッジ検出が不安定になり、誤検出や検出漏れの原因となるため、適切な平滑化は円検出の精度向上に直結します。

代表的なフィルタにはmedianBlurGaussianBlurがあります。

medianBlur

medianBlurは中央値フィルタとも呼ばれ、画像の各ピクセルを周囲のピクセルの中央値で置き換えます。

この方法は特に「塩胡椒ノイズ」と呼ばれる点状のノイズに強く、エッジを比較的保持しながらノイズを除去できます。

OpenCVでの使い方は以下の通りです。

#include <opencv2/opencv.hpp>
int main() {
    cv::Mat grayImage = cv::imread("input.jpg", cv::IMREAD_GRAYSCALE);
    if (grayImage.empty()) return -1;
    cv::Mat medianBlurred;
    cv::medianBlur(grayImage, medianBlurred, 5); // カーネルサイズは奇数で指定
    cv::imshow("MedianBlurred", medianBlurred);
    cv::waitKey(0);
    return 0;
}

この例では、カーネルサイズ5の中央値フィルタを適用しています。

カーネルサイズが大きいほどノイズ除去効果は高まりますが、細かいエッジもぼやけるため、適切なサイズを選ぶことが重要です。

GaussianBlur

GaussianBlurはガウシアン分布に基づく平滑化で、画像全体を滑らかにぼかします。

ノイズ除去効果は高いですが、エッジも多少ぼやけるため、エッジ検出の前に適用する場合はパラメータ調整が必要です。

使い方は以下の通りです。

#include <opencv2/opencv.hpp>
int main() {
    cv::Mat grayImage = cv::imread("input.jpg", cv::IMREAD_GRAYSCALE);
    if (grayImage.empty()) return -1;
    cv::Mat gaussianBlurred;
    cv::GaussianBlur(grayImage, gaussianBlurred, cv::Size(7, 7), 1.5);
    cv::imshow("GaussianBlurred", gaussianBlurred);
    cv::waitKey(0);
    return 0;
}

ここでは7×7のカーネルと標準偏差1.5を指定しています。

GaussianBlurはエッジ検出前のノイズ除去に適しており、特に画像全体に均一なノイズがある場合に効果的です。

フィルタ名ノイズ除去の特徴エッジ保持の度合い適用例
medianBlur点状ノイズ(塩胡椒ノイズ)に強い高い点状ノイズが多い画像の前処理
GaussianBlur全体的なノイズを滑らかに除去中程度均一なノイズがある画像の前処理

ヒストグラム均一化でコントラスト強調

画像のコントラストが低いと、エッジ検出が難しくなり、円検出の精度が落ちます。

ヒストグラム均一化は、画像の輝度分布を均一化してコントラストを強調する手法です。

OpenCVではcv::equalizeHist関数を使います。

#include <opencv2/opencv.hpp>
int main() {
    cv::Mat grayImage = cv::imread("input.jpg", cv::IMREAD_GRAYSCALE);
    if (grayImage.empty()) return -1;
    cv::Mat equalizedImage;
    cv::equalizeHist(grayImage, equalizedImage);
    cv::imshow("Equalized", equalizedImage);
    cv::waitKey(0);
    return 0;
}

ヒストグラム均一化を行うと、暗い部分や明るい部分のコントラストが改善され、エッジがよりはっきりと検出されやすくなります。

ただし、ノイズも強調される場合があるため、平滑化フィルタと組み合わせて使うのが効果的です。

ROI設定で検出範囲を限定

画像全体を対象に円検出を行うと、計算コストが高くなり、誤検出のリスクも増えます。

特定の領域(ROI: Region of Interest)に絞って処理することで、効率的かつ精度の高い検出が可能です。

ROIはcv::Rectで矩形領域を指定し、画像から切り出して処理します。

#include <opencv2/opencv.hpp>
int main() {
    cv::Mat image = cv::imread("input.jpg");
    if (image.empty()) return -1;
    // ROIを設定(x=100, y=100, 幅=200, 高さ=200)
    cv::Rect roi(100, 100, 200, 200);
    cv::Mat roiImage = image(roi);
    // ROI内でグレースケール変換と平滑化
    cv::Mat grayROI;
    cv::cvtColor(roiImage, grayROI, cv::COLOR_BGR2GRAY);
    cv::medianBlur(grayROI, grayROI, 5);
    // ROI内で円検出などの処理を行う
    // ...
    // 検出結果を元画像に反映する場合は座標変換が必要
    // 例: 検出した円の中心座標にroi.x, roi.yを加算
    cv::imshow("ROI", roiImage);
    cv::waitKey(0);
    return 0;
}

ROIを限定することで、処理時間の短縮と誤検出の減少が期待できます。

特に動画処理やリアルタイム検出のシナリオでは有効です。

検出結果の座標はROIの左上座標を加算して元画像の座標系に変換することを忘れないようにしてください。

Cannyエッジ検出のチューニング

HoughCirclesで円を検出する際、エッジ検出の精度が結果に大きく影響します。

OpenCVのCanny関数はエッジ検出の代表的な手法ですが、パラメータの調整やオプションの変更によって検出結果が大きく変わります。

ここでは、Cannyの低閾値・高閾値の決め方、Sobelオプションの変更、そしてノイズ耐性を高めるテクニックを解説します。

低閾値と高閾値の決め方

Cannyエッジ検出は2つの閾値を使ってエッジを判定します。

低閾値threshold1は弱いエッジの検出に関わり、高閾値threshold2は強いエッジの判定に使われます。

一般的に、threshold1threshold2の約半分程度に設定することが多いです。

  • 高閾値(threshold2): エッジとして確実に認識される強度の閾値。これを超える勾配は強いエッジとみなされます
  • 低閾値(threshold1): 高閾値より低いが、弱いエッジとして検出される閾値。強いエッジに隣接している場合のみエッジとして保持されます

適切な閾値を選ぶには、画像のコントラストやノイズレベルを考慮します。

閾値が高すぎるとエッジが検出されにくくなり、低すぎるとノイズがエッジとして誤検出されます。

以下は閾値を調整しながらエッジ検出を行う例です。

#include <opencv2/opencv.hpp>
int main() {
    cv::Mat grayImage = cv::imread("input.jpg", cv::IMREAD_GRAYSCALE);
    if (grayImage.empty()) return -1;
    cv::Mat edges;
    double lowThreshold = 50;
    double highThreshold = 150;
    cv::Canny(grayImage, edges, lowThreshold, highThreshold);
    cv::imshow("Edges", edges);
    cv::waitKey(0);
    return 0;
}

この例では、低閾値を50、高閾値を150に設定しています。

画像によっては、低閾値を40〜70、高閾値を100〜200の範囲で調整すると良い結果が得られやすいです。

Sobelオプションの変更

Canny関数内部では、エッジの勾配を計算するためにSobelフィルタが使われています。

OpenCVのCanny関数はSobelのカーネルサイズをパラメータとして受け取ることができ、これを変更することでエッジ検出の感度や精度を調整できます。

カーネルサイズは奇数で、一般的には3、5、7が使われます。

カーネルサイズが大きいほど、より滑らかな勾配が計算され、ノイズに強くなりますが、細かいエッジがぼやける傾向があります。

以下はカーネルサイズを5に設定した例です。

#include <opencv2/opencv.hpp>
int main() {
    cv::Mat grayImage = cv::imread("input.jpg", cv::IMREAD_GRAYSCALE);
    if (grayImage.empty()) return -1;
    cv::Mat edges;
    double lowThreshold = 50;
    double highThreshold = 150;
    int apertureSize = 5; // Sobelカーネルサイズ
    cv::Canny(grayImage, edges, lowThreshold, highThreshold, apertureSize);
    cv::imshow("Edges with Sobel 5x5", edges);
    cv::waitKey(0);
    return 0;
}

カーネルサイズを大きくするとノイズに強くなりますが、細かい円の輪郭が検出しづらくなる場合があるため、画像の特性に応じて調整してください。

ノイズ耐性向上テクニック

Cannyエッジ検出はノイズに敏感なため、ノイズが多い画像では誤検出が増えます。

ノイズ耐性を高めるためには、以下のようなテクニックが有効です。

  • 前処理での平滑化

GaussianBlurmedianBlurでノイズを除去してからエッジ検出を行うと、誤検出が減ります。

特にmedianBlurは点状ノイズに強いです。

  • 閾値の適切な設定

ノイズが多い場合は高閾値を上げ、低閾値もそれに合わせて調整します。

これにより弱いノイズエッジを除外できます。

  • 画像のコントラスト調整

ヒストグラム均一化などでコントラストを強調し、エッジの強度差を明確にするとノイズとエッジの区別がつきやすくなります。

  • マルチスケール処理

画像を複数の解像度で処理し、共通して検出されるエッジのみを採用する方法もあります。

これによりノイズの影響を減らせます。

以下は平滑化とCannyエッジ検出を組み合わせた例です。

#include <opencv2/opencv.hpp>
int main() {
    cv::Mat grayImage = cv::imread("input.jpg", cv::IMREAD_GRAYSCALE);
    if (grayImage.empty()) return -1;
    // GaussianBlurでノイズ除去
    cv::Mat blurred;
    cv::GaussianBlur(grayImage, blurred, cv::Size(7, 7), 1.5);
    // Cannyエッジ検出
    cv::Mat edges;
    double lowThreshold = 60;
    double highThreshold = 180;
    cv::Canny(blurred, edges, lowThreshold, highThreshold);
    cv::imshow("Edges with Noise Reduction", edges);
    cv::waitKey(0);
    return 0;
}

このように、ノイズ除去と閾値調整を組み合わせることで、より安定したエッジ検出が可能になります。

円検出の前処理として、これらのチューニングを行うことが重要です。

HoughCirclesの主要パラメータ

OpenCVのHoughCircles関数は、円検出のために複数のパラメータを受け取ります。

これらのパラメータを適切に調整することで、検出精度や処理速度を最適化できます。

ここでは、特に重要なパラメータであるdpminDistparam1param2minRadiusmaxRadiusについて詳しく説明します。

dp 解像度比

dpは累積器(アキュムレータ)解像度の逆数を表すパラメータです。

具体的には、入力画像の解像度に対して、累積器の解像度がどの程度かを決めます。

  • dp = 1の場合、累積器の解像度は入力画像と同じです
  • dp > 1の場合、累積器の解像度は入力画像より低くなり、計算が高速になりますが、検出精度が落ちる可能性があります

例えば、dp = 2ならば累積器の解像度は入力画像の半分となり、計算量が減ります。

適切なdpの値は画像のサイズや検出対象の円の大きさによって異なります。

小さな円を正確に検出したい場合はdp=1が推奨されますが、大きな円や高速処理が必要な場合はdpを大きくしても良いでしょう。

minDist 最小中心距離

minDistは検出される円の中心同士の最小距離をピクセル単位で指定します。

この値は、近接した複数の円が重複して検出されるのを防ぐ役割を持ちます。

  • 小さすぎると、同じ円が複数回検出される可能性があります
  • 大きすぎると、近接した複数の円が検出されなくなることがあります

一般的には、検出したい円の半径の2倍程度を目安に設定します。

例えば、半径が30ピクセルの円を検出する場合、minDistは60ピクセル程度に設定すると良いです。

param1 Canny上限閾値

param1は内部で使われるCannyエッジ検出の高い閾値を指定します。

Cannyエッジ検出は2つの閾値を使いますが、HoughCirclesでは高い閾値のみを指定し、低い閾値はその半分に自動設定されます。

  • 高い閾値を大きくすると、エッジ検出が厳しくなり、ノイズが減りますが、弱いエッジが検出されにくくなります
  • 逆に小さくすると、エッジが多く検出されますが、ノイズも増えます

典型的な値は100〜200の範囲です。

画像のコントラストやノイズレベルに応じて調整してください。

param2 投票数閾値

param2は円の中心を検出する際の投票数の閾値です。

ハフ変換の累積器で得票数がこの値を超えた場合に円として認識されます。

  • 大きい値に設定すると、検出される円はより確実なものに限定され、誤検出が減りますが、検出漏れが増えます
  • 小さい値に設定すると、多くの円が検出されますが、誤検出も増えやすくなります

この値は画像の特性や検出したい円の明瞭さに応じて調整します。

一般的には30〜100の範囲で設定されることが多いです。

minRadius と maxRadius

minRadiusmaxRadiusは検出する円の最小半径と最大半径をピクセル単位で指定します。

これにより、検出対象の円のサイズを限定でき、誤検出の抑制や処理速度の向上に役立ちます。

  • minRadiusを0に設定すると、最小サイズの制限がなくなりますが、小さなノイズが円として誤検出される可能性があります
  • maxRadiusを0に設定すると、最大サイズの制限がなくなります

例えば、検出したい円の半径が20〜50ピクセルの範囲であれば、minRadius=20maxRadius=50と設定します。

これらのパラメータを適切に設定することで、検出精度が大幅に向上し、不要な円の検出を減らせます。

これらのパラメータは相互に影響し合うため、実際の画像や用途に応じて試行錯誤しながら調整することが重要です。

パラメータの調整は、円検出の成功に欠かせないステップです。

サンプルコード構成

円検出を実際に行う際の基本的なコード構成を、画像の読み込みから検出結果の描画、保存・表示までの流れで解説します。

OpenCVを使ったC++のサンプルコードを交えながら説明します。

画像読み込みと変換

まずは画像を読み込み、円検出に適した形式に変換します。

カラー画像をグレースケールに変換し、ノイズを軽減するために平滑化処理を行います。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // 画像の読み込み(カラー)
    cv::Mat srcImage = cv::imread("input.jpg");
    if (srcImage.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    // グレースケールに変換
    cv::Mat grayImage;
    cv::cvtColor(srcImage, grayImage, cv::COLOR_BGR2GRAY);
    // ノイズ除去のために中央値フィルタを適用
    cv::medianBlur(grayImage, grayImage, 5);
    // ここから円検出処理に進む
    // ...
}

このコードでは、cv::imreadで画像を読み込み、cv::cvtColorでグレースケールに変換しています。

さらにcv::medianBlurでノイズを減らし、エッジ検出や円検出の精度を高める準備をしています。

円描画と色指定

検出した円の情報は、中心座標と半径で表されます。

これらを元のカラー画像に描画して、視覚的に検出結果を確認します。

円の中心は小さな点で、円周は輪郭線で描きます。

色は見やすいように緑や赤などを使います。

// 検出された円を描画する例
std::vector<cv::Vec3f> circles; // ここにHoughCirclesの結果が格納されていると仮定
for (size_t i = 0; i < circles.size(); i++) {
    cv::Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
    int radius = cvRound(circles[i][2]);
    // 円の中心を緑色の塗りつぶし円で描画
    cv::circle(srcImage, center, 3, cv::Scalar(0, 255, 0), -1);
    // 円周を赤色の線で描画
    cv::circle(srcImage, center, radius, cv::Scalar(0, 0, 255), 2);
}

ここで使われているcv::circle関数は、第一引数に描画先の画像、第二引数に中心座標、第三引数に半径、第四引数に色(BGR形式)、第五引数に線の太さを指定します。

中心は太さ-1で塗りつぶし、円周は太さ2の線で描いています。

検出結果の保存と表示

描画した結果は画面に表示するだけでなく、ファイルに保存して後で確認できるようにすることも多いです。

OpenCVのcv::imshowでウィンドウ表示し、cv::imwriteで画像ファイルとして保存します。

#include <opencv2/opencv.hpp>
int main() {
    // 省略: 画像読み込み、円検出、描画処理
    // 検出結果を表示
    cv::imshow("Detected Circles", srcImage);
    // キー入力待ち(0は無限待機)
    cv::waitKey(0);
    // 画像をファイルに保存
    bool success = cv::imwrite("output.jpg", srcImage);
    if (!success) {
        std::cerr << "画像の保存に失敗しました。" << std::endl;
        return -1;
    }
    return 0;
}

cv::imshowはウィンドウ名と表示する画像を指定し、cv::waitKeyでキー入力を待ちます。

cv::imwriteはファイル名と保存する画像を指定し、保存に成功するとtrueを返します。

このように、画像の読み込みから前処理、円検出、描画、表示、保存までの一連の流れを組み合わせることで、円検出の結果を簡単に確認・活用できます。

複数フレームへの応用

画像内の円検出は静止画だけでなく、動画やリアルタイム映像のフレームごとに適用することが多いです。

ここでは、OpenCVの動画ストリーム処理の基本であるVideoCaptureの使い方、リアルタイム表示と遅延管理のポイント、さらにFPS(フレームレート)とバッファの最適化について詳しく解説します。

動画ストリーム処理

VideoCaptureの利用

OpenCVのVideoCaptureクラスは、カメラや動画ファイルからフレームを連続的に取得するためのインターフェースです。

これを使うことで、リアルタイム映像や動画ファイルの各フレームに対して円検出を適用できます。

以下はカメラ映像を取得し、フレームごとに処理する基本的な例です。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // カメラデバイスのオープン(0はデフォルトカメラ)
    cv::VideoCapture cap(0);
    if (!cap.isOpened()) {
        std::cerr << "カメラを開けませんでした。" << std::endl;
        return -1;
    }
    cv::Mat frame;
    while (true) {
        // フレームを取得
        cap >> frame;
        if (frame.empty()) break;
        // ここで円検出などの処理を行う
        // ...
        // フレームを表示
        cv::imshow("Camera Stream", frame);
        // 'q'キーで終了
        if (cv::waitKey(1) == 'q') break;
    }
    cap.release();
    cv::destroyAllWindows();
    return 0;
}

このコードでは、VideoCaptureでカメラを開き、cap >> frameでフレームを取得しています。

waitKey(1)は1ミリ秒待機し、キー入力をチェックします。

'q'が押されるとループを抜けて終了します。

動画ファイルを読み込む場合は、VideoCapture cap("video.mp4");のようにファイル名を指定します。

リアルタイム表示と遅延管理

リアルタイム処理では、フレームの取得から表示までの遅延を最小限に抑えることが重要です。

waitKeyの待機時間は表示のフレームレートに影響し、短すぎるとCPU負荷が高くなり、長すぎると映像がカクつきます。

  • waitKey(1)は約1000fps相当の待機時間で、ほぼ遅延なしの表示が可能です
  • 実際のカメラや動画のフレームレートに合わせてwaitKeyの値を調整すると滑らかな表示になります

また、処理時間が長い場合はフレームの取得が遅れ、映像が遅延します。

これを防ぐために、以下の対策があります。

  • 処理の軽量化

円検出のパラメータを調整し、計算負荷を下げます。

  • フレームスキップ

処理が間に合わない場合は一部のフレームをスキップして最新フレームを優先します。

  • マルチスレッド化

フレーム取得と処理を別スレッドで行い、処理遅延を分散します。

FPSとバッファの最適化

動画ストリームのFPS(Frames Per Second)は、映像の滑らかさと処理負荷のバランスを決める重要な指標です。

OpenCVのVideoCaptureでは、カメラのFPSを取得・設定できます。

double fps = cap.get(cv::CAP_PROP_FPS);
std::cout << "Camera FPS: " << fps << std::endl;

FPSが高いほど滑らかな映像になりますが、処理負荷も増大します。

円検出のような重い処理をリアルタイムで行う場合は、FPSを下げるか、処理を高速化する必要があります。

また、VideoCaptureは内部にフレームバッファを持っており、処理が遅いとバッファに古いフレームが溜まることがあります。

これにより表示が遅延し、リアルタイム性が損なわれます。

バッファの最適化方法は以下の通りです。

  • バッファをクリアする

フレーム取得前にバッファを空にすることで、最新フレームを優先できます。

OpenCVには直接バッファをクリアするAPIはありませんが、フレームを連続で読み捨てる方法があります。

while (cap.grab()) {
    // grabでフレームを取得し捨てる
}
  • フレームスキップ

処理が間に合わない場合は、grab()でフレームを取得し、retrieve()で処理するフレームを選択する方法もあります。

  • FPS制御

waitKeyの待機時間を調整し、処理速度に合わせて表示FPSを制御します。

これらの工夫により、リアルタイム映像での円検出をスムーズに行えます。

特にマルチスレッドやGPUを活用した高速化と組み合わせると、より高精度かつ高速な処理が可能です。

処理速度を向上させるアプローチ

円検出は計算負荷が高く、特に高解像度画像やリアルタイム処理では処理速度の最適化が重要です。

ここでは、OpenCVでのマルチスレッド活用、ROI分割によるタイル処理、そしてGPUを利用した高速化の方法について詳しく説明します。

マルチスレッド活用

CPUのマルチコアを活用して処理を並列化することで、処理速度を大幅に向上させることが可能です。

OpenCVはcv::parallel_for_という並列処理用のAPIを提供しており、これを使うと簡単にループ処理を並列化できます。

cv::parallel_for_での並列化

cv::parallel_for_は、指定した範囲のループを複数スレッドで分割して実行します。

円検出の後処理や複数画像の一括処理など、独立した処理単位がある場合に効果的です。

以下は、複数の画像に対して円検出を並列で実行する例です。

#include <opencv2/opencv.hpp>
#include <vector>
#include <iostream>
struct CircleDetector : public cv::ParallelLoopBody {
    std::vector<cv::Mat>& images;
    std::vector<std::vector<cv::Vec3f>>& results;
    CircleDetector(std::vector<cv::Mat>& imgs, std::vector<std::vector<cv::Vec3f>>& res)
        : images(imgs), results(res) {}
    void operator()(const cv::Range& range) const override {
        for (int i = range.start; i < range.end; i++) {
            cv::Mat gray;
            cv::cvtColor(images[i], gray, cv::COLOR_BGR2GRAY);
            cv::medianBlur(gray, gray, 5);
            std::vector<cv::Vec3f> circles;
            cv::HoughCircles(gray, circles, cv::HOUGH_GRADIENT, 1, gray.rows/8, 200, 100, 0, 0);
            results[i] = circles;
        }
    }
};
int main() {
    // 複数画像の読み込み例
    std::vector<cv::Mat> images = {
        cv::imread("image1.jpg"),
        cv::imread("image2.jpg"),
        cv::imread("image3.jpg")
    };
    for (auto& img : images) {
        if (img.empty()) {
            std::cerr << "画像の読み込みに失敗しました。" << std::endl;
            return -1;
        }
    }
    std::vector<std::vector<cv::Vec3f>> results(images.size());
    // 並列処理で円検出
    cv::parallel_for_(cv::Range(0, (int)images.size()), CircleDetector(images, results));
    // 結果表示(例として1枚目の画像の円を描画)
    for (const auto& circle : results[0]) {
        cv::Point center(cvRound(circle[0]), cvRound(circle[1]));
        int radius = cvRound(circle[2]);
        cv::circle(images[0], center, radius, cv::Scalar(0, 0, 255), 2);
    }
    cv::imshow("Detected Circles", images[0]);
    cv::waitKey(0);
    return 0;
}

この例では、CircleDetector構造体に処理内容をまとめ、parallel_for_で複数画像の円検出を並列化しています。

これにより、CPUの複数コアを効率的に活用できます。

ROI分割とタイル処理

大きな画像をそのまま処理すると計算負荷が高くなります。

画像を複数の小さな領域(タイル)に分割し、それぞれで円検出を行う方法があります。

これにより、メモリ使用量を抑えつつ、並列処理も組み合わせやすくなります。

ROI(Region of Interest)を設定して部分的に処理することで、不要な領域の処理を省略し、効率化が可能です。

#include <opencv2/opencv.hpp>
#include <vector>
int main() {
    cv::Mat src = cv::imread("large_image.jpg");
    if (src.empty()) return -1;
    int tileSize = 200; // タイルのサイズ
    std::vector<cv::Vec3f> allCircles;
    for (int y = 0; y < src.rows; y += tileSize) {
        for (int x = 0; x < src.cols; x += tileSize) {
            // タイル領域を設定(画像端でサイズ調整)
            int width = std::min(tileSize, src.cols - x);
            int height = std::min(tileSize, src.rows - y);
            cv::Rect roi(x, y, width, height);
            cv::Mat tile = src(roi);
            // グレースケール変換と平滑化
            cv::Mat gray;
            cv::cvtColor(tile, gray, cv::COLOR_BGR2GRAY);
            cv::medianBlur(gray, gray, 5);
            // 円検出
            std::vector<cv::Vec3f> circles;
            cv::HoughCircles(gray, circles, cv::HOUGH_GRADIENT, 1, gray.rows/8, 200, 100, 0, 0);
            // タイル内の座標を元画像座標に変換して保存
            for (auto& c : circles) {
                c[0] += x;
                c[1] += y;
                allCircles.push_back(c);
            }
        }
    }
    // 検出結果を描画
    for (const auto& c : allCircles) {
        cv::Point center(cvRound(c[0]), cvRound(c[1]));
        int radius = cvRound(c[2]);
        cv::circle(src, center, radius, cv::Scalar(0, 255, 0), 2);
    }
    cv::imshow("Tiled Circles", src);
    cv::waitKey(0);
    return 0;
}

この方法は、画像の一部だけに円が存在する場合や、並列処理と組み合わせて高速化したい場合に有効です。

GPU版HoughCirclesの利用

OpenCVはCUDA対応のGPUアクセラレーションをサポートしており、GPUを使うことでHoughCirclesの処理を大幅に高速化できます。

ただし、GPU版のAPIは標準のHoughCirclesとは異なり、CUDAモジュールを利用する必要があります。

GPU版の円検出はcv::cuda::HoughCirclesDetectorクラスを使います。

以下は基本的な使い方の例です。

#include <opencv2/opencv.hpp>
#include <opencv2/cudaimgproc.hpp>
#include <opencv2/cudafeatures2d.hpp>
#include <iostream>
int main() {
    cv::Mat src = cv::imread("input.jpg", cv::IMREAD_GRAYSCALE);
    if (src.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    // GPUメモリに画像をアップロード
    cv::cuda::GpuMat d_src;
    d_src.upload(src);
    // GPU版HoughCirclesDetectorの作成
    cv::Ptr<cv::cuda::HoughCirclesDetector> detector = cv::cuda::createHoughCirclesDetector(
        1,      // dp
        20,     // minDist
        100,    // param1 (Canny高閾値)
        30,     // param2 (投票数閾値)
        0,      // minRadius
        0       // maxRadius
    );
    // 円検出
    cv::cuda::GpuMat d_circles;
    detector->detect(d_src, d_circles);
    // 結果をCPUにダウンロード
    std::vector<cv::Vec3f> circles;
    d_circles.download(circles);
    // 結果表示
    cv::Mat colorSrc;
    cv::cvtColor(src, colorSrc, cv::COLOR_GRAY2BGR);
    for (const auto& c : circles) {
        cv::Point center(cvRound(c[0]), cvRound(c[1]));
        int radius = cvRound(c[2]);
        cv::circle(colorSrc, center, radius, cv::Scalar(0, 0, 255), 2);
    }
    cv::imshow("GPU HoughCircles", colorSrc);
    cv::waitKey(0);
    return 0;
}

GPU版はCUDA対応のGPUが必要で、環境構築やOpenCVのビルド時にCUDAサポートを有効にする必要があります。

GPUの並列処理能力を活かし、大量の画像や高解像度映像のリアルタイム処理に適しています。

これらのアプローチを組み合わせることで、円検出の処理速度を大幅に向上させることが可能です。

用途や環境に応じて最適な方法を選択してください。

精度検証と評価指標

円検出の性能を客観的に評価するためには、適切な指標を用いて検出結果の精度を測定することが重要です。

ここでは、真陽性率と偽陽性率、IoU(Intersection over Union)を用いた円の重なり評価、そしてパラメータ自動探索の方法について詳しく説明します。

真陽性率と偽陽性率

真陽性率(True Positive Rate, TPR)と偽陽性率(False Positive Rate, FPR)は、検出器の性能を評価する基本的な指標です。

  • 真陽性率(TPR)は、実際に存在する円のうち正しく検出された割合を示します

\[\text{TPR} = \frac{\text{真陽性数}}{\text{真陽性数} + \text{偽陰性数}}\]

真陽性数は正しく検出された円の数、偽陰性数は検出漏れした円の数です。

  • 偽陽性率(FPR)は、実際には存在しない円を誤って検出した割合を示します

\[\text{FPR} = \frac{\text{偽陽性数}}{\text{偽陽性数} + \text{真陰性数}}\]

偽陽性数は誤検出した円の数、真陰性数は正しく検出されなかった非円領域の数です。

これらの指標は、検出結果と正解データ(グラウンドトゥルース)を比較して算出します。

理想的にはTPRが高く、FPRが低いことが望まれます。

IoUを用いた円重なり評価

円検出の評価では、検出された円と正解円の重なり具合を定量的に評価することが重要です。

IoU(Intersection over Union)は、2つの領域の重なりの割合を示す指標で、円の重なり評価にも応用できます。

円のIoUは以下のように計算します。

  1. 2つの円の交差面積(Intersection)を求める。
  2. 2つの円の和集合面積(Union)を求める。
  3. IoUを計算します。

\[\text{IoU} = \frac{\text{Intersection}}{\text{Union}}\]

円の交差面積は、2つの円の中心間距離 \(d\)、半径 \(r_1, r_2\) を用いて以下の式で求められます。

\[\text{Intersection} = r_1^2 \cos^{-1}\left(\frac{d^2 + r_1^2 – r_2^2}{2 d r_1}\right) + r_2^2 \cos^{-1}\left(\frac{d^2 + r_2^2 – r_1^2}{2 d r_2}\right) – \frac{1}{2} \sqrt{(-d + r_1 + r_2)(d + r_1 – r_2)(d – r_1 + r_2)(d + r_1 + r_2)}\]

この計算はやや複雑ですが、実装例も多く公開されています。

IoUが一定の閾値(例えば0.5)以上であれば、検出円は正解円と一致しているとみなします。

これにより、真陽性・偽陽性の判定がより厳密になります。

パラメータ自動探索

HoughCirclesのパラメータは検出精度に大きく影響しますが、手動で最適値を見つけるのは時間がかかります。

そこで、パラメータ自動探索(ハイパーパラメータチューニング)を行うことで、最適な設定を効率的に見つけることが可能です。

代表的な方法は以下の通りです。

  • グリッドサーチ

複数のパラメータの候補値を組み合わせて総当たり的に検証し、最も良い評価指標を得られる組み合わせを選びます。

計算コストは高いですが、単純で確実です。

  • ランダムサーチ

パラメータ空間からランダムにサンプリングして評価します。

グリッドサーチより効率的に良いパラメータを見つけやすい場合があります。

  • ベイズ最適化

過去の評価結果を元に次に試すパラメータを賢く選択し、探索効率を高める手法です。

Pythonのscikit-optimizeなどでよく使われますが、C++でも実装可能です。

自動探索の際は、評価指標として真陽性率やIoUを用い、検出結果の品質を数値化します。

これにより、最適なdpminDistparam1param2minRadiusmaxRadiusなどのパラメータを効率的に決定できます。

これらの評価指標と自動探索を組み合わせることで、円検出の精度を客観的に評価し、最適なパラメータ設定を見つけることが可能です。

実際のアプリケーションでは、正解データを用意してこれらの手法を活用することをおすすめします。

よくある失敗例と解決策

円検出を行う際には、さまざまな失敗例が発生しやすく、それらを理解し適切に対処することが重要です。

ここでは、代表的な失敗例である「円が二重に検出される」「直線の角が円として誤検出される」「光量変化による抜け落ち」について、それぞれの原因と解決策を詳しく説明します。

円が二重に検出される

原因

同じ円が複数回検出される現象は、HoughCirclesのパラメータ設定や画像のノイズ、エッジの重複などが原因で起こります。

特に、検出される円の中心間の最小距離を指定するminDistパラメータが小さすぎる場合、近接した複数の円として認識されやすくなります。

また、エッジ検出の結果にノイズや細かい輪郭が多いと、同じ円の輪郭が複数の輪として検出されることもあります。

解決策

  • minDistの調整

円の中心同士の最小距離を適切に設定し、同じ円が複数回検出されないようにします。

一般的には検出したい円の半径の2倍程度を目安に設定します。

  • エッジ検出のノイズ除去

medianBlurGaussianBlurなどの平滑化フィルタを使い、エッジ検出前にノイズを減らします。

  • 投票数閾値param2の調整

投票数の閾値を上げることで、弱いエッジやノイズによる誤検出を減らせます。

  • 後処理での重複除去

検出後に、中心座標が近い円を統合または削除する処理を実装する方法もあります。

例えば、中心間距離が一定以下の円をまとめるなどの工夫が有効です。

直線の角が円として誤検出

原因

画像内の直線の角や曲がり角が、円の一部として誤って検出されることがあります。

これは、エッジ検出で角の部分が強いエッジとして認識され、ハフ変換の投票で円の中心候補に誤って投票されるためです。

特に、背景に多くの直線や角がある場合や、円以外の形状が複雑な場合に発生しやすいです。

解決策

  • ROIの限定

円が存在する可能性の高い領域だけを処理対象に限定し、誤検出の範囲を狭めます。

  • エッジ検出パラメータの調整

Cannyの閾値を調整し、角のエッジが強調されすぎないようにします。

  • 形状フィルタリング

検出後に円の形状を評価し、円らしくないものを除外します。

例えば、円の輪郭の均一性や円周率に近い形状かどうかを判定する方法があります。

  • 前処理での背景除去

画像の背景を単純化したり、直線や角を強調しないように前処理を工夫します。

光量変化による抜け落ち

原因

画像内の光量が不均一であったり、影や反射が強い場合、円の一部が暗くなったり明るくなったりして、エッジ検出が不十分になることがあります。

その結果、円の輪郭が途切れ、HoughCirclesで検出されない、または部分的にしか検出されないことがあります。

解決策

  • ヒストグラム均一化

cv::equalizeHistなどを使い、画像のコントラストを均一化してエッジ検出の安定性を向上させます。

  • 照明条件の改善

撮影環境で光源を均一に配置し、影や反射を減らす工夫を行います。

  • 前処理での局所的なコントラスト強調

CLAHE(Contrast Limited Adaptive Histogram Equalization)などの局所的なコントラスト強調手法を用いると、部分的な光量変化に強くなります。

  • パラメータの柔軟な調整

Cannyの閾値やHoughCirclesのパラメータを調整し、弱いエッジも検出できるように設定します。

ただし、ノイズが増える可能性もあるためバランスが重要です。

これらの失敗例は、パラメータ調整や前処理の工夫、後処理の実装によって多くの場合改善可能です。

円検出の精度を高めるために、これらのポイントを意識して対策を行うことが大切です。

他の形状検出との比較

円検出は画像処理における重要なタスクの一つですが、他にも直線検出やテンプレートマッチングなど、形状検出の手法は多岐にわたります。

ここでは、OpenCVでよく使われるHoughLinesによる直線検出との違い、そしてテンプレートマッチングとの住み分けについて詳しく説明します。

HoughLinesとの違い

HoughLinesは画像内の直線を検出するためのハフ変換ベースの手法であり、HoughCirclesと同じくハフ変換の一種ですが、検出対象の形状が異なります。

  • 検出対象の形状
    • HoughLinesは直線を検出します。画像のエッジ点をパラメータ空間(距離と角度)にマッピングし、直線のパラメータを特定します
    • HoughCirclesは円を検出します。円は中心座標と半径の3パラメータで表され、より複雑なパラメータ空間を扱います
  • パラメータ空間の次元
    • HoughLinesは2次元パラメータ空間(距離と角度)で処理します
    • HoughCirclesは3次元パラメータ空間(中心のx座標、y座標、半径)を扱うため、計算負荷が高くなります
  • 計算コストと精度
    • 直線検出は比較的高速で、画像内の明確な直線を効率的に検出できます
    • 円検出はパラメータ空間が広いため計算コストが高く、パラメータ調整が重要です
  • 用途の違い
    • HoughLinesは道路の車線検出や建築物の輪郭検出など、直線的な特徴の抽出に適しています
    • HoughCirclesはコイン検出やボール追跡、円形部品の検査など、円形状の検出に特化しています

このように、HoughLinesHoughCirclesは同じハフ変換の考え方を使いながらも、対象形状や処理内容が異なるため、用途に応じて使い分ける必要があります。

テンプレートマッチングとの住み分け

テンプレートマッチングは、あらかじめ用意したテンプレート画像と入力画像の類似度を計算し、特定のパターンを検出する手法です。

円検出とはアプローチが異なり、以下のような特徴があります。

  • 検出対象の柔軟性
    • テンプレートマッチングは任意の形状や模様を検出可能ですが、テンプレートと完全に一致する必要があります
    • HoughCirclesは円形という数学的に定義された形状を検出するため、多少の変形やノイズに強いです
  • 計算コスト
    • テンプレートマッチングはテンプレートサイズや検索範囲に依存し、計算量が大きくなることがあります
    • HoughCirclesはパラメータ空間の探索が必要ですが、円形状に特化しているため効率的に検出できます
  • スケールや回転の対応
    • テンプレートマッチングは基本的にスケールや回転に弱く、これらに対応するには複数のテンプレートを用意するか、拡張手法を使う必要があります
    • HoughCirclesは円の半径をパラメータとして検出できるため、スケール変化に対応しやすいです
  • 適用例の違い
    • テンプレートマッチングはロゴ検出や特定の模様認識、部品の位置検出などに向いています
    • HoughCirclesは円形の物体検出や計測、トラッキングに適しています

まとめると、テンプレートマッチングは形状や模様の具体的なパターン検出に強く、HoughCirclesは数学的に定義された円形状の検出に特化しています。

検出対象の性質や用途に応じて、これらの手法を使い分けることが効果的です。

応用事例

OpenCVのHoughCirclesを用いた円検出は、さまざまな分野で実用的な応用がなされています。

ここでは、代表的な応用例として「ボールトラッキング」「パイプ内径計測」「コイン選別システム」について具体的に解説します。

ボールトラッキング

スポーツやロボティクスの分野で、動いているボールの位置をリアルタイムに追跡する用途に円検出は非常に有効です。

HoughCirclesを使うことで、映像内のボールを検出し、その中心座標や半径を取得できます。

  • 処理の流れ
  1. 動画フレームを取得し、グレースケール変換とノイズ除去を行います。
  2. HoughCirclesでボールの円を検出。
  3. 検出した円の中心座標をトラッキングアルゴリズム(カルマンフィルタやMeanShiftなど)に渡し、連続フレームでの位置推定を行います。
  4. 位置情報を元にボールの軌跡や速度を計算。
  • ポイント
    • ボールの色や背景と区別しやすいように、前処理で色空間変換やマスク処理を組み合わせることが多いです
    • 照明変化や影の影響を受けにくいパラメータ調整が重要です
    • 高速処理が求められるため、ROI限定やマルチスレッド化も活用されます

この手法はサッカー、バスケットボール、卓球など多様なスポーツの映像解析に利用されています。

パイプ内径計測

製造業やメンテナンス分野では、パイプやチューブの内径を非接触で計測するニーズがあります。

画像内のパイプ断面はほぼ円形であるため、HoughCirclesを用いて内径を正確に測定できます。

  • 処理の流れ
  1. パイプ断面の画像を取得し、グレースケール化とノイズ除去を行います。
  2. HoughCirclesで内径の円を検出。
  3. 検出した円の半径をピクセル単位で取得し、カメラのキャリブレーション情報を用いて実際の寸法に換算。
  • ポイント
    • カメラの歪み補正やスケール変換が正確な計測には不可欠です
    • パイプの表面状態や照明条件によってエッジ検出の難易度が変わるため、前処理の工夫が重要です
    • 複数のパイプが映る場合はROI設定や後処理で対象を限定します

この方法は配管検査や品質管理、自動化検査装置に広く活用されています。

コイン選別システム

自動販売機や両替機などで、投入されたコインの種類を判別するために円検出が使われています。

コインは円形であり、サイズや模様の違いを組み合わせて識別します。

  • 処理の流れ
  1. 投入されたコインの画像を取得し、グレースケール化と平滑化を行います。
  2. HoughCirclesでコインの輪郭を検出し、半径を計測。
  3. 半径の大きさや位置情報を元にコインの種類を推定。
  4. 必要に応じてテンプレートマッチングや特徴量抽出で模様の識別を行います。
  • ポイント
    • コインの重なりや傾きによる検出漏れを防ぐため、複数角度からの撮影や複数カメラの利用が効果的です
    • 照明条件の均一化や反射防止の工夫が検出精度向上に寄与します
    • 高速かつ高精度な検出が求められるため、パラメータ調整や前処理の最適化が重要です

このシステムは自動販売機の信頼性向上や硬貨の偽造検出にも役立っています。

これらの応用事例は、HoughCirclesの円検出機能をベースに、前処理や後処理、他のアルゴリズムと組み合わせることで実現されています。

用途に応じた最適なパラメータ設定と処理設計が成功の鍵となります。

まとめ

本記事では、C++とOpenCVのHoughCirclesを用いた円検出の基本原理から、前処理やパラメータ調整、エッジ検出のチューニング方法まで詳しく解説しました。

さらに、複数フレーム処理や高速化のためのマルチスレッド・GPU活用、精度評価指標やよくある失敗例の対処法も紹介しています。

実践的なサンプルコードや応用事例を通じて、円検出の精度向上と効率化に役立つ知識が得られます。

これにより、画像処理や映像解析の幅広いシナリオで効果的に円検出を活用できるようになります。

関連記事

Back to top button