画像から特定の色の領域を抜き出す (OpenCV3 C++)
[履歴] [最終更新] (2020/02/02 13:15:34)
最近の投稿
注目の記事

概要

OpenCV3 C++ を用いて画像から特定の色の領域を取り出す方法のうち、HSV 色空間における色相を指定する方法と、バックプロジェクション (逆投影法) を利用する方法の二つを記載します。

HSV 色空間における色相を指定する方法

色を表現する空間には RGB の他に HSV (Hue 色相、Saturation 彩度、Value 明度) があります。HSV 色空間は人間の感覚に近い指標を利用しているため色を選びやすいという特徴があり、特定の色領域を抜き出すことも容易になります。関連ページ

例えば、以下の画像からオレンジ色の部分を抜き出したいとします。

Uploaded Image

Wikipedia によると、オレンジ色は RGB 値で (243, 152, 0) です。これを HSV に変換すると以下のようになります。OpenCV では RGB ではなく BGR の順番で指定する必要があることに注意します。

python3 -m IPython
In [1]: import numpy as np
In [2]: import cv2 as cv
In [3]: orange = np.uint8([[[0, 152, 243]]])
In [4]: cv.cvtColor(orange, cv.COLOR_BGR2HSV)
Out[4]: array([[[ 19, 255, 243]]], dtype=uint8)

オレンジ色の色相 Hue は、OpenCV においては約 19 であることが分かりました。色相についてプラスマイナス 10 程度のフィルタを作って画像を抜き出すと以下のようになります。

Uploaded Image

#include <opencv2/opencv.hpp>
#include <iostream>

int main() {

    // BGR 色空間
    cv::Mat src = cv::imread("aaa.png", cv::IMREAD_COLOR);

    // HSV 色空間での表現に変換
    cv::Mat hsv;
    cv::cvtColor(src, hsv, cv::COLOR_BGR2HSV);

    // inRange によって Hue が特定の範囲にある領域の mask を取得します。
    cv::Mat mask;
    cv::inRange(hsv, cv::Scalar(9, 100, 100), cv::Scalar(29, 255, 255), mask);

    // src と src の and 演算を、mask の領域 (0 以外の領域) についてのみ行うことで、画像を切り出します。
    double min, max;
    cv::minMaxLoc(mask, &min, &max);
    std::cout << min << std::endl; //=> 0
    std::cout << max << std::endl; //=> 255

    cv::Mat res;
    cv::bitwise_and(src, src, res, mask);

    cv::imshow("src", src);
    cv::imshow("mask", mask);
    cv::imshow("res", res);
    cv::waitKey(0);
    return 0;
}

色相の制限値を調整すると抜き出される領域が変化します。

cv::inRange(hsv, cv::Scalar(9, 100, 100), cv::Scalar(12, 255, 255), mask);

Uploaded Image

cv::inRange(hsv, cv::Scalar(13, 100, 100), cv::Scalar(14, 255, 255), mask);

Uploaded Image

バックプロジェクションによる方法

以下の画像について、特定の領域 ROI と似た色の領域を切り出すことを考えます。

Uploaded Image

ROI は画像の部分行列として用意できます。

Uploaded Image

ヒストグラムの計算について

cv::calcHist で各種ヒストグラムを計算できます。以下は画像に含まれる BGR 画素値のヒストグラムを計算する例です。

Uploaded Image

#include <opencv2/opencv.hpp>
#include <iostream>

int main() {
    cv::Mat src = cv::imread("aaa.png", cv::IMREAD_COLOR);

    // BGR チャンネルを配列として別々の Mat に分けます。
    std::vector<cv::Mat> bgr_planes;
    cv::split(src, bgr_planes);

    // BGR それぞれについて、画素値 [0, 255] のヒストグラムを計算します。
    cv::Mat b_hist, g_hist, r_hist;
    int histSize = 256;
    float range[] = { 0, 256 };
    const float* histRange = { range };
    bool uniform = true, accumulate = false;
    cv::calcHist( &bgr_planes[0], 1, 0, cv::Mat(), b_hist, 1, &histSize, &histRange, uniform, accumulate );
    cv::calcHist( &bgr_planes[1], 1, 0, cv::Mat(), g_hist, 1, &histSize, &histRange, uniform, accumulate );
    cv::calcHist( &bgr_planes[2], 1, 0, cv::Mat(), r_hist, 1, &histSize, &histRange, uniform, accumulate );

    // 以下はヒストグラムを可視化するための処理です。

    // 可視化するための画像 Mat を用意します。
    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(0, 0, 0) );

    std::cout << histImage.rows << std::endl; //=> 400

    double min, max;
    cv::minMaxLoc(b_hist, &min, &max);
    std::cout << max << std::endl; //=> 11725

    // ヒストグラムの各 bin について、値を [0, 400] に変換します。
    cv::normalize(b_hist, b_hist, 0, histImage.rows, cv::NORM_MINMAX);
    cv::normalize(g_hist, g_hist, 0, histImage.rows, cv::NORM_MINMAX);
    cv::normalize(r_hist, r_hist, 0, histImage.rows, cv::NORM_MINMAX);

    cv::minMaxLoc(b_hist, &min, &max);
    std::cout << max << std::endl; //=> 400

    // 折れ線グラフを描画します。
    for( int i = 1; i < histSize; i++ ) {
        cv::line(histImage, cv::Point( bin_w*(i-1), hist_h - cvRound(b_hist.at<float>(i-1)) ),
                  cv::Point( bin_w*(i), hist_h - cvRound(b_hist.at<float>(i)) ),
                  cv::Scalar(255, 0, 0), 2, 8, 0);
        cv::line(histImage, cv::Point( bin_w*(i-1), hist_h - cvRound(g_hist.at<float>(i-1)) ),
                  cv::Point( bin_w*(i), hist_h - cvRound(g_hist.at<float>(i)) ),
                  cv::Scalar(0, 255, 0), 2, 8, 0);
        cv::line(histImage, cv::Point( bin_w*(i-1), hist_h - cvRound(r_hist.at<float>(i-1)) ),
                  cv::Point( bin_w*(i), hist_h - cvRound(r_hist.at<float>(i)) ),
                  cv::Scalar(0, 0, 255), 2, 8, 0);
    }
    cv::imshow("Source image", src);
    cv::imshow("calcHist Demo", histImage);
    cv::waitKey(0);
    return 0;
}

バックプロジェクションを用いた画像の切り出し

ROI 画像についてヒストグラムを計算します。ここでは簡単のため Hue 値についてのみ考えます。もとの画像における各ピクセルの Hue 値について、ヒストグラムのどの bin に属するかを調べます。属する bin のヒストグラムの値 [0, 255] をそのピクセルのグレースケール画素値として記録することでバックプロジェクション画像を生成します。

バックプロジェクションは、各ピクセルが ROI 画像と同じ色相を持つ確率を表現しています。バックプロジェクションを mask としてもとの画像から ROI と似た色の領域を切り出すことができます。

#include <opencv2/opencv.hpp>
#include <iostream>

int main() {

    // BGR 色空間
    cv::Mat src = cv::imread("aaa.png", cv::IMREAD_COLOR);

    // ROI の指定
    cv::Mat roi = src.rowRange(100, 200).colRange(0, 50);

    // HSV 色空間
    cv::Mat hsvSrc, hsvRoi;
    cv::cvtColor(src, hsvSrc, cv::COLOR_BGR2HSV);
    cv::cvtColor(roi, hsvRoi, cv::COLOR_BGR2HSV);

    std::cout << hsvSrc.size() << std::endl; //=> [200 x 200]
    std::cout << hsvSrc.channels() << std::endl; //=> 3
    std::cout << hsvSrc.depth() << std::endl; //=> 0 (= CV_8U)

    // Hue を格納する行列を用意
    cv::Mat hueSrc;
    cv::Mat hueRoi;
    hueSrc.create(hsvSrc.size(), hsvSrc.depth());
    hueRoi.create(hsvRoi.size(), hsvRoi.depth());

    // ここでは簡単のため Hue 情報だけを用いたヒストグラムを考えます。
    // Saturation と Value を捨てます。
    int ch[] = {0, 0};
    cv::mixChannels(&hsvSrc, 1, &hueSrc, 1, ch, 1);
    cv::mixChannels(&hsvRoi, 1, &hueRoi, 1, ch, 1);

    // ROI について Hue 値のヒストグラムを作成します。
    cv::Mat histRoi;
    int histSize = 25;
    float range[] = {0, 180}; // OpenCV における Hue の範囲は [0, 180) です。
    const float* histRange = { range };
    cv::calcHist(&hueRoi, 1, 0, cv::Mat(), histRoi, 1, &histSize, &histRange, true, false);

    // ヒストグラムの各 bin について、値を [0, 255] に変換します。
    cv::normalize(histRoi, histRoi, 0, 255, cv::NORM_MINMAX);

    // src 画像の各ピクセルの Hue 値について、ヒストグラムのどの bin に属するかを調べます。
    // 属する bin のヒストグラムの値 [0, 255] をそのピクセルのグレースケール画素値として
    // backproj に記録します。
    cv::Mat backproj;
    cv::calcBackProject(&hueSrc, 1, 0, histRoi, backproj, &histRange);

    // 50 を閾値として黒 0 か白 255 に二値化します。
    cv::Mat mask;
    cv::threshold(backproj, mask, 50, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);

    // src と src の and 演算を、mask の領域 (0 以外の領域) についてのみ行うことで、画像を切り出します。
    cv::Mat res;
    cv::bitwise_and(src, src, res, mask);

    cv::imshow("src", src);
    cv::imshow("roi", roi);
    cv::imshow("backproj", backproj);
    cv::imshow("mask", mask);
    cv::imshow("res", res);
    cv::waitKey(0);
    return 0;
}

Uploaded Image

ROI を変更すると切り出される領域が変化します。

cv::Mat roi = src.rowRange(100, 150).colRange(100, 150);

Uploaded Image

バックプロジェクションについて、ノイズが多い場合はモルフォロジー変換を利用すると良い場合があります。

関連ページ