OpenCVのgrabCut関数によるLazy Snappingの実装

This page has been moved to tech0089.html


ソースコード

Lazy SnappingをOpenCV 2.4.11で実装する方法を紹介します.OpenCVのgrabCut関数を利用します.GUIは実装しません.前景と背景を表すストロークはあらかじめ与えられているものとします.

// Include directory C:\OpenCV2.4.11\build\include
// Library directory (x86) C:\OpenCV2.4.11\build\x86\vc12\lib
// Library directory (x64) C:\OpenCV2.4.11\build\x64\vc12\lib
// PATH (x86) C:\OpenCV2.4.11\build\x86\vc12\bin
// PATH (x64) C:\OpenCV2.4.11\build\x64\vc12\bin
#if _DEBUG
#pragma comment(lib, "opencv_core2411d.lib")
#pragma comment(lib, "opencv_highgui2411d.lib")
#pragma comment(lib, "opencv_imgproc2411d.lib")
#else
#pragma comment(lib, "opencv_core2411.lib")
#pragma comment(lib, "opencv_highgui2411.lib")
#pragma comment(lib, "opencv_imgproc2411.lib")
#endif
#include <opencv2/opencv.hpp>

int main()
{
  // Input
  cv::Mat inputImage = cv::imread("image.bmp", 1);
  cv::Mat inputMask = cv::imread("mask.bmp", 1);
  if (inputImage.data == NULL || inputMask.data == NULL) return 1;
  if (inputImage.size() != inputMask.size()) return 1;
  cv::Mat strokeImage = inputImage.clone();
  cv::Mat processMask = cv::Mat(inputMask.rows, inputMask.cols, CV_8UC1);
  cv::Mat regionImage = inputMask.clone();
  cv::Mat compositeImage = inputImage.clone();
  cv::Mat fgImage = inputImage.clone();

  // Mask
  for (int y = 0; y < inputMask.rows; y++) {
    for (int x = 0; x < inputMask.cols; x++) {
      cv::Vec<unsigned char, 3> color = inputMask.at<cv::Vec<unsigned char, 3>>(y, x);
      if (color[0] == 0 && color[1] == 0 && color[2] == 255) {
        processMask.at<unsigned char>(y, x) = cv::GC_FGD;
        strokeImage.at<cv::Vec<unsigned char, 3>>(y, x)[0] = 0;
        strokeImage.at<cv::Vec<unsigned char, 3>>(y, x)[1] = 0;
        strokeImage.at<cv::Vec<unsigned char, 3>>(y, x)[2] = 255;
      }
      else if (color[0] == 255 && color[1] == 0 && color[2] == 0) {
        processMask.at<unsigned char>(y, x) = cv::GC_BGD;
        strokeImage.at<cv::Vec<unsigned char, 3>>(y, x)[0] = 255;
        strokeImage.at<cv::Vec<unsigned char, 3>>(y, x)[1] = 0;
        strokeImage.at<cv::Vec<unsigned char, 3>>(y, x)[2] = 0;
      }
      else {
        processMask.at<unsigned char>(y, x) = (x + y) % 2 == 0 ? cv::GC_PR_FGD : cv::GC_PR_BGD;
      }
    }
  }

  // LazySnapping
  cv::Mat bgdModel;
  cv::Mat fgdModel;
  cv::Rect rect;
  const int iterCount = 4;
  cv::grabCut(inputImage, processMask, rect, bgdModel, fgdModel, iterCount, cv::GC_INIT_WITH_MASK);

  // GrabCut
  // rect = cv::Rect(13, 32, 302, 155);
  // cv::grabCut(inputImage, processMask, rect, bgdModel, fgdModel, iterCount, cv::GC_INIT_WITH_RECT);

  // Foreground
  for (int y = 0; y < inputMask.rows; y++) {
    for (int x = 0; x < inputMask.cols; x++) {
      compositeImage.at<cv::Vec<unsigned char, 3>>(y, x)[0] /= 2;
      compositeImage.at<cv::Vec<unsigned char, 3>>(y, x)[1] /= 2;
      compositeImage.at<cv::Vec<unsigned char, 3>>(y, x)[2] /= 2;
      int category = processMask.at<unsigned char>(y, x);
      if (category == cv::GC_FGD || category == cv::GC_PR_FGD) {
        regionImage.at<cv::Vec<unsigned char, 3>>(y, x)[0] = 0;
        regionImage.at<cv::Vec<unsigned char, 3>>(y, x)[1] = 0;
        regionImage.at<cv::Vec<unsigned char, 3>>(y, x)[2] = (category == cv::GC_FGD ? 255 : 127);
        compositeImage.at<cv::Vec<unsigned char, 3>>(y, x)[0] += 0;
        compositeImage.at<cv::Vec<unsigned char, 3>>(y, x)[1] += 0;
        compositeImage.at<cv::Vec<unsigned char, 3>>(y, x)[2] += 128;
      }
      else if (category == cv::GC_BGD || category == cv::GC_PR_BGD) {
        regionImage.at<cv::Vec<unsigned char, 3>>(y, x)[0] = (category == cv::GC_BGD ? 255 : 127);
        regionImage.at<cv::Vec<unsigned char, 3>>(y, x)[1] = 0;
        regionImage.at<cv::Vec<unsigned char, 3>>(y, x)[2] = 0;
        compositeImage.at<cv::Vec<unsigned char, 3>>(y, x)[0] += 128;
        compositeImage.at<cv::Vec<unsigned char, 3>>(y, x)[1] += 0;
        compositeImage.at<cv::Vec<unsigned char, 3>>(y, x)[2] += 0;
        fgImage.at<cv::Vec<unsigned char, 3>>(y, x)[0] = 255;
        fgImage.at<cv::Vec<unsigned char, 3>>(y, x)[1] = 255;
        fgImage.at<cv::Vec<unsigned char, 3>>(y, x)[2] = 255;
      }
    }
  }

  // Output
  cv::imwrite("stroke.bmp", strokeImage);
  cv::imwrite("region.bmp", regionImage);
  cv::imwrite("composite.bmp", compositeImage);
  cv::imwrite("foreground.bmp", fgImage);
  return 0;
}


画像ファイル


[入力] 対象画像 image.bmp


[入力] ユーザストローク(赤:前景,青:背景) mask.bmp


対象画像とストロークの合成画像(確認のための画像) stroke.bmp


[出力] 領域分割結果(赤:前景,青:背景) region.bmp


[出力] 領域分割結果と対象画像を重ね合わせた画像 composite.bmp


[出力] 入力画像に対して,背景の画素を白で表現した画像 foreground.bmp


説明

このページでは,OpenCVのgrabCut関数の実装例を紹介しています.GrabCutやLazy Snappingの説明は省きます.OpenCVのgrabCut関数をGrabCutとして使う方法は他のウェブページでも解説があるので,このページではその説明は省略します.このページでは,OpenCVのgrabCut関数をLazy Snappingとして使う方法を説明します.grabCut関数は以下のようなものです.

void cv::grabCut( InputArray _img, InputOutputArray _mask, Rect rect, InputOutputArray _bgdModel, InputOutputArray _fgdModel, int iterCount, int mode )

まず,_imgですが,対象の入力画像を指定します.3チャンネル8bitのcv::Mat形式の変数を指定します.
続いて,_maskですが,後で詳しく説明します.
rectについては,Lazy Snappingとして使う場合は,cv::Rect形式の変数を指定するだけでOKです.
_bgdModelと_fgdModelですが,cv::Mat形式の変数を与えてやるだけでOKです.
iterCountは反復回数です.1~10ぐらいの値を指定します.
modeですが,Lazy Snappingとして使う場合は,cv::GC_INIT_WITH_MASKを指定します.

さて,_maskについて説明します.これは,1チャンネル8bitのcv::Mat形式の変数です.対象入力画像と同じサイズです.各画素には,以下の4つの値のいずれかを指定します.
cv::GC_BGD // 値は0.ユーザが背景だと指定した画素を表します.
cv::GC_FGD // 値は1.ユーザが前景だと指定した画素を表します.
cv::GC_PR_BGD // 値は2.背景である可能性のある画素を表します.
cv::GC_PR_FGD // 値は3.前景である可能性のある画素を表します.

GC_BGDやGC_FGDを指定された画素は,その後の計算で値が変化することはありません.GC_PR_BGDやGC_PR_FGDが指定された画素は,その後の計算でgrabCut関数が適切にGC_PR_BGDかGC_PR_FGDの値を割り当てます.

よくある間違いとしては,_maskの全ての画素を0で初期化したままにしてはいけない,というものがあります.0はGC_BGDですので,その後にいくらgrabCut関数を呼び出しても,その画素はずっと背景のまま変化しません.前景か背景かをgrabCut関数に計算してもらいたい画素に対しては必ずGC_PR_BGDかGC_PR_FGDの値を設定する必要があります.

grabCut関数を呼び出す前,_maskの各画素には初期値を設定します.GC_BGDとGC_FGD以外の画素にはGC_PR_BGDかGC_PR_FGDのどちらかの値を設定します.背景である可能性のほうが高ければGC_PR_BGDを,前景である可能性のほうが高ければGC_PR_FGDを設定します.

grabCut関数を呼び出した後,領域分割の結果が_maskに上書きされます.元々GC_BGDやGC_FGDだった画素は変化しません.それ以外の画素にはGC_PR_BGDかGC_PR_FGDが入っています.ある画素がもしGC_FGDもしくはGC_PR_FGDであれば,その画素は前景です.ある画素がもしGC_BGDもしくはGC_PR_BGDであれば,その画素は背景です.これが領域分割結果となります.


もどる