OpenCV3 C++ を用いて画像から特定の色の領域を取り出す方法のうち、HSV 色空間における色相を指定する方法と、バックプロジェクション (逆投影法) を利用する方法の二つを記載します。
色を表現する空間には RGB の他に HSV (Hue 色相、Saturation 彩度、Value 明度) があります。HSV 色空間は人間の感覚に近い指標を利用しているため色を選びやすいという特徴があり、特定の色領域を抜き出すことも容易になります。関連ページ。
例えば、以下の画像からオレンジ色の部分を抜き出したいとします。
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 程度のフィルタを作って画像を抜き出すと以下のようになります。
#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);
cv::inRange(hsv, cv::Scalar(13, 100, 100), cv::Scalar(14, 255, 255), mask);
以下の画像について、特定の領域 ROI と似た色の領域を切り出すことを考えます。
ROI は画像の部分行列として用意できます。
cv::calcHist
で各種ヒストグラムを計算できます。以下は画像に含まれる BGR 画素値のヒストグラムを計算する例です。
#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;
}
ROI を変更すると切り出される領域が変化します。
cv::Mat roi = src.rowRange(100, 150).colRange(100, 150);
バックプロジェクションについて、ノイズが多い場合はモルフォロジー変換を利用すると良い場合があります。