OpenCV実践ガイド:画像処理の基礎から応用まで

開発環境

  • Ubuntu 19.04
  • Visual Studio Code 1.37.0
  • OpenCV 3.4.7
  • CMake 3.13.4

OpenCV学習の道のりとして、まず基本的な画像処理から始めましょう。

画像の読み込みと表示

基本的な画像操作は次のようになります:

Mat inputImage = imread("画像ファイルパス");
namedWindow("表示ウィンドウ");
imshow("表示ウィンドウ", inputImage);

画像の読み込みに失敗した場合、以下のようなエラーが発生します:

error: (-215:Assertion failed) size.width>0 && size.height>0 in function 'imshow'

注意点として、画像ファイルは実行ファイルと同じディレクトリに配置する必要があります。

ビデオ処理

カメラやビデオファイルの取り扱い:

VideoCapture captureDevice;
captureDevice.open("ビデオファイルパス");
// カメラを開く場合
captureDevice.open(0);

読み込み確認の方法:

// 方法1
if(!inputImage.data) {
    cout << "画像の読み込みに失敗しました" << endl;
}
// 方法2
if(inputImage.empty()) {
    cout << "画像の読み込みに失敗しました" << endl;
}

関心領域(ROI)の指定

画像の特定領域を操作:

Mat roiRegion;
// 方法1:矩形で指定
roiRegion = sourceImage(Rect(100, 150, overlay.cols, overlay.rows));
// 方法2:範囲で指定
roiRegion = sourceImage(Range(150, 150 + overlay.rows), Range(100, 100 + overlay.cols));

画像の線形合成

addWeighted関数による画像のブレンド:

int main()
{
    double blendRatio = 0.6;
    double complementRatio;
    
    Mat basePicture = imread("base.jpg");
    Mat overlayImage = imread("overlay.png");

    if(basePicture.empty()) {
        cout << "ベース画像の読み込み失敗" << endl;
        return -1;
    }
    if(overlayImage.empty()) {
        cout << "オーバーレイ画像の読み込み失敗" << endl;
        return -1;
    }

    complementRatio = 1 - blendRatio;
    Mat selectedRegion = basePicture(Rect(0, 0, overlayImage.cols, overlayImage.rows));
    
    addWeighted(selectedRegion, blendRatio, overlayImage, complementRatio, 0.0, selectedRegion);

    namedWindow("ブレンド結果");
    imshow("ブレンド結果", basePicture);

    waitKey(0);
    return 0;
}

カラーチャンネルの操作

split()/merge()によるチャンネル処理:

#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include <iostream>

using namespace cv;
using namespace std;

int main()
{    
    Mat colorImage = imread("sample.jpg");
    Mat grayscaleImage = imread("mask.png", 0);
    vector<Mat> channelArray;
    Mat blueComponent;

    if(colorImage.empty()) {
        cout << "カラー画像の読み込み失敗" << endl;
        return -1;
    }
    if(grayscaleImage.empty()) {
        cout << "グレースケール画像の読み込み失敗" << endl;
        return -1;
    }

    split(colorImage, channelArray);
    blueComponent = channelArray.at(0);

    addWeighted(blueComponent(Rect(0,0,grayscaleImage.cols,grayscaleImage.rows)), 
                1.0, grayscaleImage, 0.5, 0, 
                blueComponent(Rect(0,0,grayscaleImage.cols,grayscaleImage.rows)));
    
    merge(channelArray, colorImage);

    namedWindow("処理結果");
    imshow("処理結果", colorImage);

    waitKey(0);
    return 0;
}

単色画像の生成:

#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include <iostream>

using namespace cv;
using namespace std;

int main()
{    
    Mat originalImage = imread("colorful.jpg");
    vector<Mat> colorChannels(originalImage.channels());
    vector<Mat> outputChannels(originalImage.channels());
    Mat resultImage;

    if(originalImage.empty()) {
        cout << "画像の読み込み失敗" << endl;
        return -1;
    }

    int imgWidth = originalImage.cols;
    int imgHeight = originalImage.rows;

    split(originalImage, colorChannels);

    Mat blackChannel;
    blackChannel.create(imgHeight, imgWidth, CV_8UC1);
    blackChannel = Scalar(0);

    outputChannels[0] = colorChannels[0];
    outputChannels[1] = blackChannel;
    outputChannels[2] = blackChannel;

    merge(outputChannels, resultImage);

    imshow("青チャンネルのみ", resultImage);

    waitKey(0);
    return 0;
}

画像フィルタリング

各種フィルタの比較:

#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>

using namespace cv;
using namespace std;

int main()
{    
    Mat sourceImg = imread("filter_test.jpg");
    if(sourceImg.empty()) {
        cout << "画像の読み込み失敗" << endl;
        return -1;
    }

    Mat boxResult, blurResult, gaussResult, medianResult, bilateralResult;

    boxResult = sourceImg.clone();
    blurResult = sourceImg.clone();
    gaussResult = sourceImg.clone();
    medianResult = sourceImg.clone();
    bilateralResult = sourceImg.clone();

    imshow("元画像", sourceImg);

    boxFilter(sourceImg, boxResult, -1, Size(7, 7));
    imshow("ボックスフィルタ", boxResult);

    blur(sourceImg, blurResult, Size(7, 7));
    imshow("平均化フィルタ", blurResult);
 
    GaussianBlur(sourceImg, gaussResult, Size(5, 5), 1.5, 1.5);
    imshow("ガウシアンフィルタ", gaussResult);

    medianBlur(sourceImg, medianResult, 7);
    imshow("メディアンフィルタ", medianResult);

    bilateralFilter(sourceImg, bilateralResult, 30, 60, 15);
    imshow "バイラテラルフィルタ", bilateralResult);

    waitKey(0);
    destroyAllWindows();

    return 0;
}

形態学演算

膨張・収縮処理:

#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>

using namespace cv;
using namespace std;

int kernelSize = 5;
int operationType;

Mat sourceImg, resultImg;

static void trackbarCallback(int, void*)
{
    Mat structuringElement = getStructuringElement(MORPH_RECT, 
                                Size(2*kernelSize+1, 2*kernelSize+1), 
                                Point(kernelSize, kernelSize));
    
    if(operationType == 0) {
        erode(sourceImg, resultImg, structuringElement);
    } else {
        dilate(sourceImg, resultImg, structuringElement);
    }
    
    imshow("形態学処理", resultImg);
}

int main()
{    
    sourceImg = imread("morphology_test.jpg");
    if(sourceImg.empty()) {
        cout << "画像の読み込み失敗" << endl;
        return -1;
    }

    namedWindow("形態学処理");
    
    createTrackbar("膨張/収縮", "形態学処理", &operationType, 1, trackbarCallback);
    createTrackbar("カーネルサイズ", "形態学処理", &kernelSize, 21, trackbarCallback);

    trackbarCallback(0, 0);

    while(char(waitKey(1)) != 'q');
    
    return 0;
}

エッジ検出

Cannyエッジ検出:

#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>

using namespace cv;
using namespace std;

int main()
{    
    Mat inputImg, edgeImg;

    inputImg = imread("edge_sample.jpg");
    if(inputImg.empty()) {
        cout << "画像の読み込み失敗" << endl;
        return -1;
    }

    Canny(inputImg, edgeImg, 150, 300);

    imshow("エッジ検出結果", edgeImg);
    
    while (char(waitKey(1)) != 'q');
    
    return 0;
}

Sobelオペレータ:

#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>

using namespace cv;
using namespace std;

int main()
{    
    Mat colorImg, grayImg, gradX, gradY, combinedGrad;

    colorImg = imread("gradient_test.jpg");
    if(colorImg.empty()) {
        cout << "画像の読み込み失敗" << endl;
        return -1;
    }

    imshow("元画像", colorImg);

    bilateralFilter(colorImg, grayImg, 25, 50, 12.5);
    cvtColor(grayImg, grayImg, CV_RGB2GRAY);

    Sobel(grayImg, gradX, CV_16S, 1, 0, 3, 1, 0, BORDER_DEFAULT);
    Sobel(grayImg, gradY, CV_16S, 0, 1, 3, 1, 0, BORDER_DEFAULT);

    convertScaleAbs(gradX, gradX);
    convertScaleAbs(gradY, gradY);

    addWeighted(gradX, 0.5, gradY, 0.5, 0, combinedGrad);

    imshow("X方向勾配", gradX);
    imshow("Y方向勾配", gradY);
    imshow("合成勾配", combinedGrad);

    while (char(waitKey(1)) != 'q');
    
    return 0;
}

ハフ変換

直線検出:

#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>

using namespace cv;
using namespace std;

int main()
{    
    Mat sourceImg, edgeImg, resultImg;

    sourceImg = imread("lines_test.jpg");
    if(sourceImg.empty()) {
        cout << "画像の読み込み失敗" << endl;
        return -1;
    }

    Canny(sourceImg, edgeImg, 100, 300, 3);
    cvtColor(edgeImg, resultImg, CV_GRAY2BGR);

    vector<Vec4i> lineSegments;
    HoughLinesP(edgeImg, lineSegments, 1, CV_PI/180, 80, 50, 10);

    for (size_t i = 0; i < lineSegments.size(); i++) {
        Vec4i segment = lineSegments[i];
        line(resultImg, Point(segment[0], segment[1]), 
             Point(segment[2], segment[3]), Scalar(0, 255, 0), 2, CV_AA);
    }

    imshow("直線検出結果", resultImg);

    while(char(waitKey(1)) != 'q');
    return 0;
}

円検出:

#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>

using namespace cv;
using namespace std;

int main()
{    
    Mat inputImg, grayImg, blurredImg;

    inputImg = imread("circles_test.jpg");
    if(inputImg.empty()) {
        cout << "画像の読み込み失敗" << endl;
        return -1;
    }

    cvtColor(inputImg, grayImg, CV_BGR2GRAY);
    GaussianBlur(grayImg, blurredImg, Size(9, 9), 2, 2);

    vector<Vec3f> detectedCircles;
    HoughCircles(blurredImg, detectedCircles, CV_HOUGH_GRADIENT, 
                 1, grayImg.rows/8, 200, 100, 0, 0);

    for (size_t i = 0; i < detectedCircles.size(); i++) {
        Point center(cvRound(detectedCircles[i][0]), 
                    cvRound(detectedCircles[i][1]));
        int radius = cvRound(detectedCircles[i][2]);

        circle(inputImg, center, 3, Scalar(0, 255, 0), -1);
        circle(inputImg, center, radius, Scalar(0, 0, 255), 3);
    }
    
    imshow("円検出結果", inputImg);

    while(char(waitKey(1)) != 'q');
    return 0;
}

特徴点検出

Harrisコーナー検出:

#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>

using namespace cv;
using namespace std;

int thresholdValue;
Mat srcImg, dstImg, normalizedImg;

static void thresholdCallback(int, void*)
{
    Mat binaryImg;
    threshold(normalizedImg, binaryImg, thresholdValue, 255, THRESH_BINARY);
    imshow("コーナー検出", binaryImg);
}

int main()
{    
    srcImg = imread("corner_test.jpg");
    if(srcImg.empty()) {
        cout << "画像の読み込み失敗" << endl;
        return -1;
    }

    cvtColor(srcImg, srcImg, CV_BGR2GRAY);

    Mat cornerResponse;
    cornerHarris(srcImg, cornerResponse, 7, 5, 0.05);
    
    normalize(cornerResponse, normalizedImg, 0, 255, NORM_MINMAX, CV_32FC1, Mat());

    namedWindow("コーナー検出");
    createTrackbar("閾値", "コーナー検出", &thresholdValue, 255, thresholdCallback);

    thresholdCallback(0, 0);

    while(char(waitKey(1)) != 'q');
    return 0;
}

まとめ

OpenCVの学習は実際にコードを動かしながら進めるのが効果的です。基本的な画像操作から始めて、徐々に複雑な処理へと進んでいくことで、画像処理の基礎を固めることができます。

さらに学習を進めるには、カメラキャリブレーションやsolvePnPの使用法、ビデオ処理の応用技術なども重要です。実プロジェクトに取り組みながら経験を積むことが、スキル向上の鍵となります。

タグ: OpenCV 画像処理 C++ コンピュータビジョン エッジ検出

8月13日 01:36 投稿