OpenCV

【C++】OpenCVを活用した画像処理:読み込みから回転、エッジ検出までの実用テクニック

C++とOpenCVを用いると、cv::imreadcv::imshowなどの関数で画像の読み込み、表示、保存が手軽に行えます。

回転、リサイズ、エッジ検出、トリミングなどの画像処理機能を組み合わせることで、シンプルなアプリから高度な解析まで多様な用途に対応可能です。

画像の読み込みと保存

画像ファイルのパス指定とデータ確認

画像ファイルのパスは正確に指定する必要があります。

画像の形式ごとにファイル名やディレクトリの区切りに注意してください。

例えば、絶対パスや相対パスを使い分けることで、プログラムの動作環境に合わせた画像の指定がしやすくなります。

また、読み込んだ後に画像データのサイズやチャンネル数などを確認することで、想定通りの画像が読み込めたかどうかをチェックできます。

以下は、画像ファイルのパス指定と確認の例です。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // 画像ファイルのパスを指定
    std::string filePath = "sample.jpg";
    // 画像の読み込み
    cv::Mat image = cv::imread(filePath);
    // 画像が読み込まれたか確認する処理
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました: " << filePath << std::endl;
        return -1;
    }
    // 画像データの確認(幅, 高さ, チャンネル数)
    std::cout << "画像の幅: " << image.cols << ", 高さ: " << image.rows
              << ", チャンネル数: " << image.channels() << std::endl;
    return 0;
}
画像の幅: 640, 高さ: 480, チャンネル数: 3

読み込み処理のエラーチェック

画像読み込み時にファイルが存在しなかったり、形式が正しくない場合、cv::imreadは空のMatを返します。

そのため、読み込み後は必ずデータが正しく取得できたかエラーチェックを行う必要があります。

エラーチェックを実装することで、プログラムが予期しない動作をすることを防ぐことができます。

以下はエラーチェックを含んだ例です。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    std::string filePath = "nonexistent.jpg";
    cv::Mat image = cv::imread(filePath);
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました: " << filePath << std::endl;
        return -1;
    }
    std::cout << "画像の読み込みに成功しました" << std::endl;
    return 0;
}
画像の読み込みに失敗しました: nonexistent.jpg

書き出しフォーマットと保存オプション

画像処理後は、結果を保存する際にファイル形式を指定することができます。

JPEG、PNG、BMPなどのフォーマットごとに圧縮率や透過性のオプションを扱えるため、用途に合わせて適切な形式を選びましょう。

例えば、cv::imwriteを使って保存する際、JPEGの場合は圧縮率を指定する引数を渡すことができます。

下記はJPEG画像の保存例です。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat image = cv::imread("sample.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return -1;
    }
    // JPEG保存時の圧縮パラメータ(例: 品質95)
    std::vector<int> compressionParams;
    compressionParams.push_back(cv::IMWRITE_JPEG_QUALITY);
    compressionParams.push_back(95);
    // 画像の書き出し
    if (!cv::imwrite("output.jpg", image, compressionParams)) {
        std::cerr << "画像の保存に失敗しました" << std::endl;
        return -1;
    }
    std::cout << "画像が正常に保存されました" << std::endl;
    return 0;
}
画像が正常に保存されました

画像表示

ウィンドウの設定と管理

画像を表示する際はcv::namedWindowを使ってウィンドウのサイズや表示モードを指定できます。

ウィンドウ名を付けることで、複数の画像を同時に表示する際に区別がしやすくなります。

ウィンドウはマウス操作にも対応できるため、インタラクティブなアプリケーションで便利です。

以下はウィンドウ設定の一例です。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat image = cv::imread("sample.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return -1;
    }
    // ウィンドウの名前と表示モードを指定
    cv::namedWindow("Display Window", cv::WINDOW_AUTOSIZE);
    cv::imshow("Display Window", image);
    // 表示ウィンドウが閉じられるまで待機
    cv::waitKey(0);
    return 0;
}

キー入力による表示制御

cv::waitKeyを利用すると、キー入力に基づいた画像表示の終了や制御が可能です。

指定した時間だけ待機したり、キーが押されたかどうかを判定したりできます。

例えば、特定のキー(例: ESCキー)でウィンドウを閉じるような処理がよく利用されます。

以下はキー入力に応じた画像表示制御の例です。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat image = cv::imread("sample.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return -1;
    }
    cv::namedWindow("Image Window", cv::WINDOW_AUTOSIZE);
    cv::imshow("Image Window", image);
    // キー入力待ち。ESCキー(27)が押されるとループを抜ける
    while (true) {
        int key = cv::waitKey(30);
        if (key == 27) {
            break;
        }
    }
    return 0;
}
(ESCキーを押すまで画像ウィンドウが表示)

画像変換処理

回転処理

固定角度回転(90度、180度など)

固定角度回転は、cv::rotateを利用することで手軽に実装できます。

回転方向や角度を指定するだけで、画像全体を回転させることが可能です。

90度や180度など、事前に定義された定数を使うとシンプルなコードになります。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat image = cv::imread("sample.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return -1;
    }
    // 画像を90度時計回りに回転
    cv::rotate(image, image, cv::ROTATE_90_CLOCKWISE);
    cv::imshow("Rotated Image", image);
    cv::waitKey(0);
    return 0;
}
(表示される画像は90度時計回りに回転されています)

任意角度への回転と中心・スケーリング設定

任意の角度で画像を回転させる場合は、cv::getRotationMatrix2Dで回転行列を作成し、cv::warpAffineを利用します。

この場合、回転の中心や拡大・縮小係数(スケール)も同時に指定することができます。

数学的には回転行列は次のように表されます。

R(θ)=(cosθsinθsinθcosθ)

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat image = cv::imread("sample.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return -1;
    }
    // 回転角度とスケールの設定
    double angle = 45.0;
    double scale = 1.0;
    // 回転中心を画像の中心に設定
    cv::Point2f center(image.cols / 2.0F, image.rows / 2.0F);
    // 回転行列の取得
    cv::Mat rotationMatrix = cv::getRotationMatrix2D(center, angle, scale);
    // アフィン変換を適用して画像の回転
    cv::Mat rotatedImage;
    cv::warpAffine(image, rotatedImage, rotationMatrix, image.size());
    cv::imshow("Rotated 45 Degrees Image", rotatedImage);
    cv::waitKey(0);
    return 0;
}
(表示される画像は45度回転されています)

リサイズ・拡大縮小

アスペクト比の維持と調整

画像のリサイズを行う際、アスペクト比(縦横比)を維持することが重要です。

アスペクト比を保ちながらサイズ変更をすることで、画像の歪みを防ぐことができます。

例えば、新しい幅に応じて高さを計算する方法などが考えられます。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat image = cv::imread("sample.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return -1;
    }
    // リサイズ後の幅を指定。高さは元のアスペクト比に合わせて算出
    int newWidth = 320;
    double aspectRatio = static_cast<double>(image.rows) / image.cols;
    int newHeight = static_cast<int>(newWidth * aspectRatio);
    cv::Mat resizedImage;
    cv::resize(image, resizedImage, cv::Size(newWidth, newHeight));
    cv::imshow("Resized Image", resizedImage);
    cv::waitKey(0);
    return 0;
}
(画像は指定した幅に合わせて、適切な高さにリサイズされています)

拡大縮小アルゴリズムの選択

OpenCVは複数のリサイズアルゴリズムを提供しており、用途に応じて選択することが可能です。

例えば、拡大時にはcv::INTER_CUBIC、縮小時にはcv::INTER_AREAがよく使われます。

下記の表は主要なアルゴリズムの特徴をまとめたものです。

  • INTER_LINEAR: 標準的な補間方法
  • INTER_CUBIC: 高品質な補間で、拡大時に滑らかさが向上します
  • INTER_AREA: 縮小時に適しており、画像のノイズが減少する場合が多い

適切なアルゴリズムを選ぶことで、リサイズ画像の画質を向上できる可能性があります。

トリミング処理

ROI(Region of Interest)の指定方法

ROIを利用することで、画像全体ではなく一部分を選んで処理することが可能になります。

ROIはcv::Rectで矩形領域を指定し、その領域だけを抽出する方法が一般的です。

関心領域を正確に指定することで、処理負荷を減らしながら必要な部分だけの画像解析を行えます。

矩形領域での切り出し

下記は矩形領域を指定して画像を切り出す例です。

ROIの座標やサイズは、画像の内容に応じて調整してください。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat image = cv::imread("sample.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return -1;
    }
    // ROIの設定(例: 左上から幅200、高さ150の領域)
    cv::Rect roi(0, 0, 200, 150);
    // 矩形領域での切り出し
    cv::Mat croppedImage = image(roi);
    cv::imshow("Cropped Image", croppedImage);
    cv::waitKey(0);
    return 0;
}
(切り出された区域のみが表示されます)

エッジ検出

Sobel法による微分処理

X方向とY方向の差分抽出

Sobel法は画像のエッジ検出に有効な手法です。

まずはX方向とY方向の勾配を計算することで、各方向の差分を抽出することができます。

微分のオペレーションはcv::Sobelを用いて実装できます。

下記の例では、X方向とY方向の両方を計算しています。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat image = cv::imread("sample.jpg", cv::IMREAD_GRAYSCALE);
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return -1;
    }
    cv::Mat gradX, gradY;
    // X方向の勾配
    cv::Sobel(image, gradX, CV_32F, 1, 0, 3);
    // Y方向の勾配
    cv::Sobel(image, gradY, CV_32F, 0, 1, 3);
    cv::imshow("Sobel X", gradX);
    cv::imshow("Sobel Y", gradY);
    cv::waitKey(0);
    return 0;
}

エッジ強調のパラメータ調整

Sobel法ではカーネルサイズや出力データ型を調整することで、エッジ強調の度合いや検出精度が変わります。

パラメータを変えると、エッジの細かさやノイズの影響を調整可能になるため、画像に合わせたチューニングが求められます。

Laplacian法の適用ポイント

Laplacian法はエッジ検出の際、画像全体の二階微分を計算する手法です。

Sobel法と組み合わせることで、細かいエッジ部分の強調に効果を発揮します。

下記はシンプルなLaplacian法を用いたエッジ検出の例です。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat image = cv::imread("sample.jpg", cv::IMREAD_GRAYSCALE);
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return -1;
    }
    cv::Mat laplacian;
    cv::Laplacian(image, laplacian, CV_32F, 3);
    cv::imshow("Laplacian Edge", laplacian);
    cv::waitKey(0);
    return 0;
}

Cannyエッジ検出

閾値設定とエッジ抽出の最適化

Canny法は2段階のしきい値処理をすることで、ノイズの影響を抑えながら確実にエッジを抽出します。

下記のコードでは、低い閾値と高い閾値を設定してエッジ検出を行っています。

適切な閾値を選定することで、精度の高いエッジ抽出が可能となります。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat image = cv::imread("sample.jpg", cv::IMREAD_GRAYSCALE);
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return -1;
    }
    cv::Mat cannyEdges;
    // Cannyエッジ検出、低閾値50, 高閾値150を指定
    cv::Canny(image, cannyEdges, 50, 150);
    cv::imshow("Canny Edges", cannyEdges);
    cv::waitKey(0);
    return 0;
}
(画像のエッジ部分が白く表示され、その他は黒い背景になります)

画像前処理とフィルタリング

ノイズ除去手法

平均化フィルタとガウシアンフィルタの比較

ノイズ除去には平均化フィルタとガウシアンフィルタがよく利用されます。

平均化フィルタはシンプルな手法で全体のノイズを均一に除去できますが、エッジ部分もぼやけがちです。

一方、ガウシアンフィルタは重み付けがあるためエッジを保護しながらノイズを低減する効果があります。

以下のコードは両フィルタの適用例を示します。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat image = cv::imread("noisy.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return -1;
    }
    cv::Mat averageFiltered;
    cv::Mat gaussianFiltered;
    // 平均化フィルタ(ブラー)
    cv::blur(image, averageFiltered, cv::Size(5, 5));
    // ガウシアンフィルタ
    cv::GaussianBlur(image, gaussianFiltered, cv::Size(5, 5), 1.5);
    cv::imshow("Original Image", image);
    cv::imshow("Average Filter", averageFiltered);
    cv::imshow("Gaussian Filter", gaussianFiltered);
    cv::waitKey(0);
    return 0;
}
(3つのウィンドウに元画像、平均化フィルタ処理画像、ガウシアンフィルタ処理画像が表示されます)

シャープ化処理

エッジ強調フィルタの利用方法

画像の細部をはっきりさせるために、シャープ化処理が有効です。

エッジ強調フィルタを適用することで、輪郭部分を強調し、見やすい画像にすることができます。

下記はシャープ化フィルタの一例です。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat image = cv::imread("blurred.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return -1;
    }
    cv::Mat sharpenedImage;
    // シャープ化用のカーネル(ラプラシアンフィルタに基づく)
    cv::Mat kernel = (cv::Mat_<float>(3,3) <<
                       0, -1, 0,
                      -1,  5, -1,
                       0, -1, 0);
    cv::filter2D(image, sharpenedImage, image.depth(), kernel);
    cv::imshow("Original Image", image);
    cv::imshow("Sharpened Image", sharpenedImage);
    cv::waitKey(0);
    return 0;
}
(元の画像とシャープ加工された画像が並んで表示されます)

応用画像処理技術

複数手法の連携による高度な処理

複数の画像処理技術を組み合わせることで、より高度な解析や編集が可能になります。

例えば、回転、リサイズ、エッジ検出の各処理を連携させることで、入力画像のコーナー部分を抽出するなど、柔軟な手法の実装が可能になります。

各手法間のパラメータ調整を念入りに行い、最適な結果が得られるように工夫する必要があります。

動画フレームへの応用

リアルタイム処理の注意点

動画処理の場合、各フレームに対してリアルタイムで画像変換を行う必要があります。

リアルタイム性を考慮して、処理速度や並列処理の導入を意識することが大切です。

また、フレームレートの低下が視覚的に認識できないように、処理の最適化を行う必要があります。

フレームごとの画像変換戦略

動画の各フレームに対し、個別に画像変換処理を行うことで、動きのある対象物の解析や追跡が可能です。

同一の処理を連続して適用する際は、フレーム間の連続性や整合性に注意しながら、パラメータを調整する工夫が求められます。

パフォーマンス最適化

並列処理の導入検討

並列処理を利用すると、画像処理の高速化が期待できます。

複数のスレッドを活用し、画像の各チャネルやフレームの一部を同時に処理することで、全体の処理速度が向上します。

ハードウェアの特性やライブラリの最適化を考慮しながら、並列処理の手法を取り入れると良いでしょう。

メモリ管理とリソース最適化

画像処理では大容量のデータを扱うため、メモリの管理が重要になります。

処理中に不要なデータを適宜解放したり、効率的なバッファの利用を意識することで、システム全体のパフォーマンスが向上します。

また、画像データのコピー回数を減らし、直接演算可能な方法を模索する工夫が求められます。

まとめ

今回の内容では、OpenCVを使った画像処理の基本的な操作から、回転やリサイズ、エッジ検出、フィルタリング、さらに応用技術に至るまで、幅広いテクニックを詳しく解説しました。

各処理ごとに適切なサンプルコードを通して、実際の利用方法やパラメータの調整方法を示しましたので、実際に手を動かしながら試していただくと良いと思います。

また、パフォーマンスや資源管理の工夫を取り入れることで、より快適に画像処理が行える環境を整えられる点も参考になれば幸いです。

関連記事

Back to top button
目次へ