とある科学の備忘録

とある科学の備忘録

CやPythonのプログラミング、Arduino等を使った電子工作をメインに書いています。また、木製CNCやドローンの自作製作記も更新中です。たまに機械学習とかもやってます。

【OpenCV/C++】画像の一部分を修復する(cv::inpaint)

今回すること

OpenCVを用いて画像の一部を修復します。例えば、下の画像の様に、電線だけ消す、というようなことができます。(下の方は失敗していますが、一番上4本の電線はかなりきれいに消えていると思います)
f:id:pythonjacascript:20200903020610j:plain
 

このような画像修復を行うために、cv::inpaintを用います。

// inpaintMaskのピクセル値が0でないピクセルを修復します。
void inpaint(const Mat& src, const Mat& inpaintMask, Mat& dst, double inpaintRadius, int flags)

  
引数:

src 入力画像。8ビット,1あるいは3チャンネル
inpaintMask 8ビット,1チャンネル。修復領域を指定する
dst src と同じサイズ,同じタイプの画像。修復画像が出力される
inpaintRadius 修復される各点を中心とする近傍円形領域の半径。この領域がアルゴリズムに考慮される
flags 修復手法。cv::INPAINT_NS (ナビエ・ストークス(Navier-Stokes)ベースの手法)または、cv::INPAINT_TELEA( Alexandru Telea による手法)の2通り


qiita.com
このブログでとても詳しく説明されているので、今回は適当にコード書いて終わります。


プログラム

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

using namespace cv;
using namespace std;

bool LbtnPushed, need_update;
cv::Mat src, mask, result_img;


static void on_mouse(int event, int x, int y, int flags, void* param){
  const int radius = 2;

  switch (event){
  case EVENT_LBUTTONDOWN: 
    LbtnPushed = true;
    break;
  
  case EVENT_LBUTTONUP:
    LbtnPushed = false;
    break;

  case EVENT_MOUSEMOVE:
    if (LbtnPushed){
      circle(mask, cv::Point(x, y), radius, 255, -1);// 消去する部分のmaskの画素値を1以上にする(←のプログラムだと255)
      need_update = true;
    }
    break;
  }
}


bool loop() {
  if (need_update) {
    Mat binMask;
    binMask = mask & 1; //binMaskは、maskがGC_FGD(=1)とGC_PR_FGD(=3)のピクセルが1になり、それ以外は0になる
    result_img = cv::Mat(src.size(), CV_8UC3, cv::Scalar(0, 255, 0)); //全面緑の画像作成
    src.copyTo(result_img, binMask); //binMaskが1の部分だけをresult_imgにコピーする
    cv::inpaint(src, mask, result_img, 2, cv::INPAINT_TELEA);

    need_update = false;

  }
  cv::imshow("result", result_img);
  cv::imshow("mask", mask);
  return !(cv::waitKey(1) == 27);
}

int main(){
  namedWindow("result", WINDOW_AUTOSIZE);
  setMouseCallback("result", on_mouse, 0);
  need_update = true;

  src = cv::imread("test.jpg");

  mask.create(src.size(), CV_8UC1);
  mask.setTo(Scalar::all(0));
  //mask = cv::imread("mask.png", 0);
  while(loop()){}

  return 0;
}

実行結果

上のプログラムを実行して、src画像の上でマウスを左クリック→ドラッグすると、その部分が修復領域に指定され、修復された画像が再描画されます。
手作業で頑張って修復させると下画像のようになります。
f:id:pythonjacascript:20200903020610j:plain
 

参考

qiita.com


cv::xphoto::inpaintというのもあるようです:
taktak.jp

【OpenCV/C++】GrabCutで対話的な前景抽出を行う(AEのロトブラシの画像版)

この記事では、
f:id:pythonjacascript:20200830131125j:plain
↑画像を、
f:id:pythonjacascript:20200830131140j:plain
↑こんな風にするエフェクトをC++で作っていきます。

Rotoscoping、AEで言うところのRotobrush、日本語だと前景抽出とかセグメンテーションとかそういう類の技術です。


実装方法

細かいことを言うと、セグメンテーションのアルゴリズムにはGMM(Gaussian mixture model、ガウス混合モデル)でminCutアルゴリズムを使って前景と背景を分けて...という理屈はあるのですが、幸いにも、それらのアルゴリズムGrabCutとしてOpenCV内に実装があるので、それを使います。

//セグメンテーションの初期化
cv::grabCut(src, // 入力画像
  mask,   // セグメンテーションされたマスク
  myrect,// 入力画像の前景の位置を示した矩形
  bgModel, fgModel, // GMMモデル
  1, // 繰り返しの回数
  cv::GC_INIT_WITH_RECT); // use myrectを使ってmaskを初期化する

のようにすることで、srcの前景と思われる部分を判断し、前景/背景のセグメンテーションの結果をmaskに格納します。

第5引数は、以下の3通りがあります。
矩形領域内の情報を基に初期値を決めるか,前景/背景を指定する線を基に初期値を決めるかを

cv::GC_INIT_WITH_RECT 矩形領域内の情報を基に初期化
cv::GC_INIT_WITH_MASK mask(第二引数)内の前景/背景を指定する線(後述)を基に初期化
第5引数なし 初期化せずにマスクを更新する


第二引数のmaskは8UC1(8bitで1Colorのcv::Mat)であり、その各画素の値は、

GC_BGD 0 背景である
GC_FGD 1 前景である
GC_PR_BGD 2 背景らしい
GC_PR_FGD 3 前景らしい

に分類されます。GrabCut関数は、GC_PR_BGDとGC_PR_FGD のピクセルを更新し、GC_BGDとGC_FGDの部分は更新されません。

つまり、画像の前景だけを表示したい場合は、maskの画素値がGC_FGDとGC_PR_FGD の部分だけを表示します。

この記事の下のプログラムは、GC_BGDとGC_FGDはユーザーがマウスで指定し、そのユーザー入力をもとにGrabCutが前景らしい部分と背景らしい部分(GC_PR_BGDとGC_PR_FGD )を計算するものです。


プログラム

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


using namespace cv;
using namespace std;

bool LbtnPushed;
bool RbtnPushed;
bool need_update;
bool use_mask;

cv::Mat src;
cv::Mat mask, result_img;
cv::Mat bgModel, fgModel; // the models (internally used)


const string winName = "image";
cv::Rect myrect;


static void getBinMask(const Mat& comMask, Mat& binMask)
{
  if (comMask.empty() || comMask.type() != CV_8UC1)
    CV_Error(Error::StsBadArg, "comMask is empty or has incorrect type (not CV_8UC1)");
  if (binMask.empty() || binMask.rows != comMask.rows || binMask.cols != comMask.cols)
    binMask.create(comMask.size(), CV_8UC1);
  binMask = comMask & 1;
}


static void on_mouse(int event, int x, int y, int flags, void* param){
  const int radius = 2;

  switch (event){
  case EVENT_LBUTTONDOWN: 
    LbtnPushed = true;
    break;

  case EVENT_RBUTTONDOWN:
    RbtnPushed = true;
    break;
  
  case EVENT_LBUTTONUP:
    LbtnPushed = false;
    break;

  case EVENT_RBUTTONUP:
    RbtnPushed = false;
    break;

  case EVENT_MOUSEMOVE:
    if (LbtnPushed){
      circle(mask, cv::Point(x, y), radius, GC_FGD, -1);// 前景ラベル(GC_FGD)をセットする
      std::cout << "new foredround point = (" << x << ", " << y << ")\n";

    }else if (RbtnPushed) {
      circle(mask, cv::Point(x, y), radius, GC_BGD, -1); // 背景ラベル(GC_BGD)をセットする
      std::cout << "new background point = (" << x << ", " << y << ")\n";
    }
    break;
  }
}


bool loop() {
  if (need_update) {
    if (use_mask) {
      //セグメンテーションの更新
      cv::grabCut(src, // 入力画像
        mask,   // セグメンテーションされたマスク
        myrect,// 入力画像の前景の位置を示した矩形
        bgModel, fgModel, // GMMモデル
        1); // 繰り返しの回数

      std::cout << "mask updated using mask!!\n";
    }else {
      //セグメンテーションの初期化
      cv::grabCut(src, // 入力画像
        mask,   // セグメンテーションされたマスク
        myrect,// 入力画像の前景の位置を示した矩形
        bgModel, fgModel, // GMMモデル
        1, // 繰り返しの回数
        cv::GC_INIT_WITH_RECT); // use myrectを使ってmaskを初期化する
      use_mask = true;
      std::cout << "mask updated using rect!!\n";
    }

    Mat binMask;
    binMask = mask & 1; //binMaskは、maskがGC_FGD(=1)とGC_PR_FGD(=3)のピクセルが1になり、それ以外は0になる
    result_img = cv::Mat(src.size(), CV_8UC3, cv::Scalar(0, 255, 0)); //全面緑の画像作成
    src.copyTo(result_img, binMask); //binMaskが1の部分だけをresult_imgにコピーする
    
    need_update = false;

  }
  cv::imshow(winName, result_img); //画像表示

  int key = cv::waitKey(1);
  if (key == 110) need_update = true; 
  return !(key == 27);
}


int main(){
  namedWindow(winName, WINDOW_AUTOSIZE);
  setMouseCallback(winName, on_mouse, 0);
  need_update = true;
  use_mask = false;

  src = cv::imread("test.jpg");
  mask.create(src.size(), CV_8UC1);
  mask.setTo(Scalar::all(GC_PR_FGD));
  myrect = cv::Rect(1, 1, src.cols - 2, src.rows - 2); //前景がすべて治まるように設定します。
  (mask(myrect)).setTo(Scalar(GC_PR_FGD));

  while(loop()){}

  return 0;
}

実行結果

コンパイルで作成された実行ファイルと同じフォルダにtest.jpgを入れて実行します。
(test.jpgの例)
f:id:pythonjacascript:20200830131125j:plain

初期状態は↓画像の様にセグメンテーションがあいまいな状態で表示されます。
f:id:pythonjacascript:20200830133613j:plain

そこで、そのWindowの中で、マウスを左クリックしながら動かすと、その部分が前景となります。逆に右クリックしながら動かすと、その部分が背景になります。また、キーボードのnキーを押すと、GrabCutがもう一度実行されて、より正確なセグメンテーション結果になるはずです。

そのような地道な作業を繰り返していくことで、下の画像のようになります。
f:id:pythonjacascript:20200830131140j:plain
 


(参考)ロトブラシ(動画に対する前景抽出)の実装方法

AEのロトブラシ(動画に対して前景抽出を行う)アルゴリズムは下のページ内にあるPDFで解説されています。
Video SnapCut

このアルゴリズムを実装してくれているのがこのレポジトリの様です。
github.com

簡単にいえば、動画のあるフレームに対し前景抽出を行い、前景の物体の輪郭に沿ってWindowを作成します。そして、輪郭のフレーム間の動きを推定することで、それぞれのWindowを輪郭に沿って動かし、そのWindow内でセグメンテーションを行う、という手法の様です。輪郭の動きに沿ってWindowを移動させる際、誤ってWindow同士の間に隙間ができないよう、敢えて大きめ(かぶりが生じるよう)にWindowのサイズを調整します。

【OpenCV/C++】OpenCVでトーンカーブっぽいものを実装する

タイトルの通りです。今回作ってみるのはこれです。
f:id:pythonjacascript:20200828125840g:plain
右側の画面の2つの黄色の円を動かすことで、画像の色調補正を行うことができるプログラムです。
右側の画面の白色の曲線が補正の値を表していて、横軸が入力値(RGBそれぞれ)、縦軸が出力値となります。


で、プログラムです。

プログラム

以下がプログラム全文です。

# include <opencv2/opencv.hpp>
# include <opencv2/highgui.hpp>
# include <opencv2/imgproc/imgproc_c.h> //CV_AA用
#include <iostream>

#define ESCAPE_KEY 27
#define SQUARE(x) ((x)*(x))

float mX1 = 0.25f;
float mX2 = 0.75f;
float mY1 = 0.25f;
float mY2 = 0.75f;

float Bezier_fA(float aA1, float aA2) { return 1.0f - 3.0f * aA2 + 3.0f * aA1; }
float Bezier_fB(float aA1, float aA2) { return 3.0f * aA2 - 6.0 * aA1; }
float Bezier_fC(float aA1) { return 3.0 * aA1; }

float CalcBezier(float aT, float aA1, float aA2) { return  ((Bezier_fA(aA1, aA2)*aT + Bezier_fB(aA1, aA2))*aT + Bezier_fC(aA1))*aT; }
float GetSlope(float aT, float aA1, float aA2) { return 3.0 * Bezier_fA(aA1, aA2)*aT*aT + 2.0 * Bezier_fB(aA1, aA2) * aT + Bezier_fC(aA1); }

float getForX(float aX) {
  float currentSlope;
  float aGuessT = aX;
  for (__int8 i = 0; i < 4; i++) {
    currentSlope = GetSlope(aGuessT, mX1, mX2);
    if (currentSlope == 0.0) return aGuessT;
    float currentX = CalcBezier(aGuessT, mX1, mX2) - aX;
    aGuessT -= currentX / currentSlope;
  }
  return aGuessT;
}


float getBezier(float aX) {
  if (mX1 == mY1 && mX2 == mY2) return aX; // linear
  return CalcBezier(getForX(aX), mY1, mY2);
}

#define WINDOW_SIZE 500.0f
#define WINDOW_MARGIN 25

cv::Mat img = cv::Mat::zeros(WINDOW_SIZE + WINDOW_MARGIN * 2, WINDOW_SIZE + WINDOW_MARGIN * 2, CV_64FC3);
bool isBtnDown = false;

void mouse_callback(int event, int x, int y, int flags, void *userdata) {
  if (event == cv::EVENT_LBUTTONDOWN) {
    isBtnDown = true;
    return;
  }
  else if (event == cv::EVENT_LBUTTONUP) {
    isBtnDown = false;
    return;
  }

  float posX = float(x - WINDOW_MARGIN) / WINDOW_SIZE;
  float posY = 1.0f - float(y - WINDOW_MARGIN) / WINDOW_SIZE;

  float dis_1 = SQUARE(posX - mX1) + SQUARE(posY - mY1);
  float dis_2 = SQUARE(posX - mX2) + SQUARE(posY - mY2);

  if (event == cv::EVENT_MOUSEMOVE && isBtnDown == true) {
    if (dis_1 < dis_2 && dis_1 < 10) {
      mX1 = posX;
      mY1 = posY;
    }
    if (dis_1 > dis_2 && dis_2 < 10) {
      mX2 = posX;
      mY2 = posY;
    }
    return;
  }
}

void main() {
  cv::Mat lookUpTable(1, 256, CV_8U);
  uchar* p = lookUpTable.data;
  cv::Mat sample_img = cv::imread("test.jpg");
  cv::Mat lut_applied_img = sample_img.clone();

  cv::namedWindow("test", cv::WINDOW_FREERATIO);
  cv::setMouseCallback("test", mouse_callback);

  while (1) {
    cv::rectangle(img, cv::Point(0, 0), cv::Point(WINDOW_SIZE + WINDOW_MARGIN * 2, WINDOW_SIZE + WINDOW_MARGIN * 2), cv::Scalar(0, 0, 0), -1, 8, 0);
    cv::rectangle(img, cv::Point(WINDOW_MARGIN, WINDOW_MARGIN), cv::Point(WINDOW_SIZE + WINDOW_MARGIN, WINDOW_SIZE + WINDOW_MARGIN), cv::Scalar(225, 225, 225), 1, 8, 0);

    cv::Point point_1 = cv::Point(WINDOW_SIZE * mX1 + WINDOW_MARGIN, WINDOW_SIZE * (1.0f - mY1) + WINDOW_MARGIN);
    cv::Point point_2 = cv::Point(WINDOW_SIZE * mX2+ WINDOW_MARGIN, WINDOW_SIZE *  (1.0f - mY2) + WINDOW_MARGIN);

    // ベジエ曲線の描画
    int posX_old = 0;
    int posY_old = 500.0f * (1.0f - getBezier(0));

    for (int i = 0; i < 256; i++) {
      float value = getBezier(float(i) / 256.0f);
      int posX = WINDOW_SIZE * float(i) / 256.0f;
      int posY = 500.0f * (1.0f - value);
      cv::line(img, cv::Point(posX_old + WINDOW_MARGIN, posY_old + WINDOW_MARGIN), cv::Point(posX + WINDOW_MARGIN, posY + WINDOW_MARGIN), cv::Scalar(225, 225, 225), 2, CV_AA);
      posX_old = posX; posY_old = posY;

      p[i] = (uchar)(value * 256.0f); //LUT作成
    }

    //トーンカーブエディターを描画
    cv::line(img, cv::Point(WINDOW_MARGIN, WINDOW_SIZE + WINDOW_MARGIN), point_1, cv::Scalar(200, 150, 0), 2, CV_AA);
    cv::line(img, cv::Point(WINDOW_SIZE + WINDOW_MARGIN, WINDOW_MARGIN), point_2, cv::Scalar(200, 150, 0), 2, CV_AA);
    cv::circle(img, point_1, 7, cv::Scalar(0, 200, 200), 3, 4);
    cv::circle(img, point_2, 7, cv::Scalar(0, 200, 200), 3, 4);

    std::string text = "(" + std::to_string(mX1) + ", " + std::to_string(mY1) + "), (" + std::to_string(mX2) + ", " + std::to_string(mY2) + ")";
    cv::putText(img, text, cv::Point(0, 100), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(100, 100, 100));
    cv::imshow("test", img);

    cv::LUT(sample_img, lookUpTable, lut_applied_img); //1DLUTとしてトーンカーブを当てる
    cv::imshow("output", lut_applied_img);

    int key = cv::waitKey(1);
    std::cout << "(" << mX1 << ", " << mY1 << "), (" << mX2 << ", " << mY2 << ")\n";

    if (key == ESCAPE_KEY) break; // esc or enterキーで終了
  }
  std::cout << "program finished";
}

  

説明

上のプログラムの構成は、➀トーンカーブの曲線(ベジエ曲線)作成、②トーンカーブをもとに1DLUT(cv::LUTを作成)、③LUTをあてた画像を表示、という3部分に分かれています。

ベジエ曲線について

以下のサイトを参考にさせていただきましたm(__)m。
greweb.me

import matplotlib.pyplot as plt

mX1 = 0.25
mY1 = 0.1
mX2 = 0.25
mY2 = 1.0

def A(aA1, aA2):
    return 1.0 - 3.0 * aA2 + 3.0 * aA1

def B(aA1, aA2):
    return 3.0 * aA2 - 6.0 * aA1

def C(aA1):
    return 3.0 * aA1

def CalcBezier(aT, aA1, aA2):
    return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT

def GetSlope(aT, aA1, aA2):
    return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1)

def GetTForX(aX):
    # Newton raphson iteration
    aGuessT = aX
    for i in range(4):
        currentSlope = GetSlope(aGuessT, mX1, mX2)
    if currentSlope == 0.0:
        return aGuessT
    currentX = CalcBezier(aGuessT, mX1, mX2) - aX
    aGuessT -= currentX / currentSlope
    return aGuessT

def get(aX):
    if mX1 == mY1 and mX2 == mY2:
        return aX  # linear
    return CalcBezier(GetTForX(aX), mY1, mY2)

x_list = []
y_list = []
for i in range(100):
    x_list.append(float(i)/100.0)
    y_list.append(get(float(i)/100.0))

print(y_list)
plt.scatter(x=x_list, y=y_list)
plt.show()

上のプログラムを実行すると、したのような画像が表示されるはずです。
f:id:pythonjacascript:20200828123806j:plain

上のプログラムは、mX1, mY1, mX2, mY2の値をもとに、ベジエ曲線を作成しています。なので、自由な形状のトーンカーブが作れるように、OpenCVのmouseCallbackを使ってこれらの値操作できるようにします。


LUT(LookUpTable)

OpenCVには3DLUTの機能がないので、独自に実装する必要があります:
shizenkarasuzon.hatenablog.com
  

しかし、1DのLUTはすでに機能があるのでそれを使わない手はありません。
試しに、明るさを0.5倍にするようなLUTを当てるプログラムを書いてみます

# include <opencv2/opencv.hpp>
# include <opencv2/highgui.hpp>
# include <opencv2/imgproc/imgproc_c.h>

int main(){
    cv::Mat lookUpTable(1, 256, CV_8U);
    uchar* p = lookUpTable.data;
    for( int i = 0; i < 256; ++i){
        p[i] = i*0.5;
    }
    cv::Mat img = cv::imread("test.jpg");
    cv::LUT(img, lookUpTable, img);
    cv::imshow("test", img);
    cv::waitKey(0);
    return 0;
}


実行すると、明るさが半減したtest.jpgが表示されます。
f:id:pythonjacascript:20200828125048j:plain
  

これら2つを応用したのがこの記事の一番上のプログラムです。