OpenCV3 C++ による基本的な画像変換
[History] [Last Modified] (2020/02/02 13:53:07)
ここは
趣味のプログラミングを楽しむための情報共有サービス。記事の一部は有料設定にして公開できます。 詳しくはこちらをクリック📝
Recent posts
Popular pages

概要

OpenCV3 C++ を用いて基本的な画像変換を行います。

サイズの変更 (resize)

Uploaded Image

#include <opencv2/opencv.hpp>

int main() {
    cv::Mat img = cv::imread("aaa.png", -1);
    if(img.empty()) {
        return -1;
    }

    cv::Mat img2, img3;
    cv::resize(img, img2, img.size() / 2);
    cv::resize(img, img3, img.size() * 2);

    cv::imshow("original", img);
    cv::imshow("original / 2", img2);
    cv::imshow("original x 2", img3);
    cv::waitKey(0);
    return 0;
}

関連事項にダウンサンプリング cv::pyrDown があります。

アフィン変換

Uploaded Image

平面内の線形変換は 2x2 行列で表現できます。これに平行移動を加えたアフィン変換は 2x3 行列で表現できます。これら 6 変数からなるアフィン変換の行列 $T$ は、3 つの点について変換前後の座標が分かれば一意に定まるということになります。

$$T = \begin{bmatrix} A & t \\ \end{bmatrix} \ = \begin{bmatrix} a & b & t_x \\ c & d & t_y \\ \end{bmatrix} $$

画像ではなく点を変換したい場合は cv::transform を利用します。関連ページとして、同次変換があります。

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

int main(){
    cv::Mat src = cv::imread("aaa.png", -1);
    if(src.empty()) {
        return -1;
    }

    // アフィン変換を、三点の移動を指定して決めます。
    cv::Point2f srcTri[3];
    cv::Point2f dstTri[3];

    srcTri[0] = cv::Point2f(0, 0);
    srcTri[1] = cv::Point2f(1, 0);
    srcTri[2] = cv::Point2f(0, 1);

    dstTri[0] = cv::Point2f(0, 0);
    dstTri[1] = cv::Point2f(2, 0);
    dstTri[2] = cv::Point2f(0, 1);

    cv::Mat T1 = cv::getAffineTransform(srcTri, dstTri);

    // アフィン変換をより直感的に指定することもできます。
    cv::Point center = cv::Point(src.cols/2, src.rows/2);
    double angle = 45;
    double scale = 1.0;
    cv::Mat T2 = cv::getRotationMatrix2D(center, angle, scale);

    // 画像の各点をアフィン変換してみます。
    cv::Mat dst1 = cv::Mat::zeros(src.rows, src.cols, src.type());
    cv::Mat dst2 = cv::Mat::zeros(src.rows, src.cols, src.type());
    warpAffine(src, dst1, T1, dst1.size());
    warpAffine(src, dst2, T2, dst2.size());

    // 単体の点をアフィン変換してみます。
    std::vector<cv::Point2f> v1 = {cv::Point2f(0, 0), cv::Point2f(100, 100)}, v2;
    cv::transform(v1, v2, T2);
    std::cout << T2 << std::endl;
    std::cout << v1 << std::endl; // [0, 0; 100, 100]
    std::cout << v2 << std::endl; // [-41.421356, 100; 100, 100]

    cv::imshow("src", src);
    cv::imshow("dst1", dst1);
    cv::imshow("dst2", dst2);
    cv::waitKey(0);
    return 0;
}

透視変換

アフィン変換よりも自由度が 2 だけ高い変換として透視変換 (ホモグラフィ) があります。アフィン変換は透視変換の特殊な場合です。平面内の透視変換は 3x3 行列 $H$ で表現できます。線形変換ではなく、変換の最後に除算が必要になります。この除算のために 3x3 行列ですが、変数の個数は 8 になります。4 点について変換前後の座標が分かれば変換行列が一意に定まるということになります。

$$s \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} \ = H \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} $$

$$dst(x, y) = src\Bigl(\ \frac{H_{00} x + H_{01} y + H_{02}}{H_{20} x + H_{21} y + H_{22}}, \ \frac{H_{10} x + H_{11} y + H_{12}}{H_{20} x + H_{21} y + H_{22}} \ \Bigr) $$

cv::transform では除算が表現できないため、画像ではなく個別の点を変換したい場合は cv::perspectiveTransform を利用します。

Uploaded Image

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

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

    // 透視変換を、4点の移動を指定して決めます。
    std::vector<cv::Point2f> pts_src;
    std::vector<cv::Point2f> pts_dst;

    pts_src.push_back(cv::Point2f(0, 0));
    pts_src.push_back(cv::Point2f(200, 0));
    pts_src.push_back(cv::Point2f(0, 200));
    pts_src.push_back(cv::Point2f(200, 200));

    pts_dst.push_back(cv::Point2f(50, 0));
    pts_dst.push_back(cv::Point2f(150, 0));
    pts_dst.push_back(cv::Point2f(0, 200));
    pts_dst.push_back(cv::Point2f(200, 200));

    cv::Mat h = cv::getPerspectiveTransform(pts_src, pts_dst);
    // cv::Mat h = cv::findHomography(pts_src, pts_dst);

    // 画像の各点を透視変換してみます。
    cv::Mat dst = cv::Mat::zeros(src.rows, src.cols, src.type());
    cv::warpPerspective(src, dst, h, dst.size());

    // 単体の点を透視変換してみます。
    std::vector<cv::Point2f> v1 = {cv::Point2f(0, 0), cv::Point2f(100, 100)}, v2;
    cv::perspectiveTransform(v1, v2, h);
    std::cout << h << std::endl;
    std::cout << v1 << std::endl; // [0, 0; 100, 100]
    std::cout << v2 << std::endl; // [50, 3.5527137e-15;  100, 66.666664]

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

4点よりも多い変換前後の座標が分かっている場合は cv::getPerspectiveTransform ではなく cv::findHomography を利用すると、変換前後の座標についてノイズが含まれている場合にも対応できます。

インペイント処理 (画像修復)

修復対象が太すぎず、周囲に修復のために十分な情報が残っている場合に機能します。

Uploaded Image

文字を入力してみます

import numpy as np
import cv2 as cv

mask = np.zeros((200, 200, 3), dtype=np.uint8)
cv.putText(mask, 'Hello World!', (10, 100), cv.FONT_HERSHEY_SIMPLEX, 1.5, (255,255,255), 1)
cv.imwrite('mask.png', mask)

img = cv.imread('aaa.png', cv.IMREAD_COLOR)
cv.putText(img, 'Hello World!', (10, 100), cv.FONT_HERSHEY_SIMPLEX, 1.5, (255,255,255), 1)
cv.imwrite('bbb.png', img)

Uploaded Image

Uploaded Image

以下のように修復できます。

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

int main() {
    cv::Mat src = cv::imread("bbb.png", cv::IMREAD_COLOR);
    cv::Mat mask = cv::imread("mask.png", cv::IMREAD_GRAYSCALE);
    cv::Mat dst = cv::Mat::zeros(src.rows, src.cols, src.type());

    cv::inpaint(src, mask, dst, 3, cv::INPAINT_TELEA);
    // cv::inpaint(src, mask, dst, 3, cv::INPAINT_NS);

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

Uploaded Image

コントラストの変更処理

グレースケール画像の全ピクセルについて画素値 (例えば 0-255) をヒストグラムにすると、ある一定の領域に集中している場合があります。cv::equalizeHist を用いると、これを平坦化してコントラストを調整することができます。

Uploaded Image

画像によっては、cv::createCLAHE を用いて、例えば 8x8 の領域毎に分けて同様の処理を行った方が良い場合もあります。

Uploaded Image

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

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

    cv::cvtColor(src, src, cv::COLOR_BGR2GRAY);
    cv::equalizeHist(src, dst);

    cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(2.0, cv::Size(8,8));
    clahe->apply(src, dst2);

    cv::imshow("src", src);
    cv::imshow("dst", dst);
    cv::imshow("dst2", dst2);
    cv::waitKey(0);
    return 0;
}

画像の傾き補正 (deskew)

手書き数字の分類などで、事前処理として画像の傾きを補正する必要がある場合があります。

Uploaded Image

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

int SZ = 43;

cv::Mat deskew(cv::Mat& img) {
    cv::Moments m = cv::moments(img);
    if(abs(m.mu02) < 1e-2) {
        return img.clone();
    }
    float skew = m.mu11 / m.mu02;
    cv::Mat warpMat = (cv::Mat_<float>(2,3) << 1, skew, -0.5 * SZ * skew, 0, 1, 0);
    cv::Mat imgOut = cv::Mat::zeros(img.rows, img.cols, img.type());
    cv::warpAffine(img, imgOut, warpMat, imgOut.size(), cv::WARP_INVERSE_MAP | cv::INTER_LINEAR);
    return imgOut;
}

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

    std::cout << src.size() << std::endl; //=> [43 x 43]

    cv::Mat dst = deskew(src);

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

画像のモーメント $m_{p,q}$ を cv::moments で計算して傾き具合い skew を計算します。

$$m_{p,q} = \sum^{N}_{i=1} I(x_i, y_i) x^p y^q $$

skew に応じて、画像内の各点が x 軸方向に平行移動するようにアフィン変換行列を指定します。

Uploaded Image

Sobel 微分による輪郭の検出

cv::Canny と同様に、最初に平滑化を行ってから微分してエッジ検出を行います。

Uploaded Image

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

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

    // 微分を行った際にノイズが発生しないようにするために、平滑化を行います。
    cv::GaussianBlur(img, img, cv::Size(3, 3), 0, 0, cv::BORDER_DEFAULT);

    // グレースケールに変換します。
    cv::Mat gray;
    cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);

    // x軸方向、y軸方向に一次微分を行います。
    // 結果は CV_16S 符号付きの16ビット整数です。
    // -32768 〜 32767
    cv::Mat grad_x, grad_y;
    cv::Sobel(gray, grad_x, CV_16S, 1, 0, -1);
    cv::Sobel(gray, grad_y, CV_16S, 0, 1, -1);

    // CV_8U 符号なしの8ビット整数 0 〜 256 に変換します。
    cv::Mat abs_grad_x, abs_grad_y;
    cv::convertScaleAbs(grad_x, abs_grad_x);
    cv::convertScaleAbs(grad_y, abs_grad_y);

    // x軸方向、y軸方向の微分の絶対値について、同じ重みを付けて足し合わせます。
    cv::Mat grad;
    cv::addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad);

    imshow("Sobel Simple Edge Detector", grad);
    cv::waitKey(0);
    return 0;
}

同様に、二次微分によって輪郭を検出することもできます

モルフォロジー変換

モルフォロジー変換は畳み込み処理の一つです。周囲のピクセルの画素値の Min を取るように畳み込む収縮 Erosion と、Max を取るように畳み込む膨張 Dilation の二つが基本となります。それら二つを組み合わせる処理として、例えばモルフォロジー勾配があります。膨張した画像と収縮した画像の差分を取ることで、物体の境界線が得られます。オープニングやクロージングによって斑点ノイズを除去することもできます。

Uploaded Image

#include <opencv2/opencv.hpp>

int main() {
    cv::Mat src = cv::imread("aaa.png", cv::IMREAD_COLOR);
    cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));

    cv::Mat erosion_dst;
    cv::erode(src, erosion_dst, kernel);

    cv::Mat dilation_dst;
    cv::dilate(src, dilation_dst, kernel);

    cv::Mat gradient_dst;
    cv::morphologyEx(src, gradient_dst, cv::MORPH_GRADIENT, kernel);

    cv::imshow("src", src);
    cv::imshow("erosion", erosion_dst);
    cv::imshow("dilation", dilation_dst);
    cv::imshow("gradient", gradient_dst);
    cv::waitKey(0);
    return 0;
}

カーネルのサイズを変更すると、膨張および収縮の度合いを調整できます。

Related pages
    概要 サポートベクタマシン (SVM; Support Vector Machine) は分類アルゴリズムの一つです。二つのクラスに分類されたデータをもとに分類器を構成します。その分類器を用いると、未知のデータを二つのクラスに分類できます。OpenCV3 C++ に実装されている SVM アルゴリズムを利用して、手書き数字を 0-9 のいずれかに分類してみます。
    概要 OpenCV3 C++ を用いて画像から特定の色の領域を取り出す方法のうち、HSV 色空間における色相を指定する方法と、バックプロジェクション (逆投影法) を利用する方法の二つを記載します。 HSV 色空間における色相を指定する方法 色を表現する空間には RGB の他に HSV (Hue 色相、Saturation 彩度、Value 明度)
    概要 cv::Canny などで検出したエッジをもとに cv::findContours で輪郭を計算できます。輪郭に関連した処理の例を記載します。 輪郭の描画 #include <opencv2/opencv.hpp> #include <iostream> int main() { cv::Mat src = cv::imread("aaa.png", cv::IMREAD_
    概要 カメラキャリブレーション (Camera Calibration, Camera Resectioning) を行うと、レンズの歪みを表現するパラメータや、カメラのワールド座標系での位置姿勢を推定できます。 チェスボードのようなキャリブレーション専用のボードが利用されます。 キャリブレーションで得られたパラメータを用いると、例えば歪みを補正することができます。
    概要 ワールド座標に固定された単一カメラが存在するとします。RGB-D カメラではなく RGB カメラです。この単一カメラからはカラーまたはグレースケール画像が取得できます。カメラキャリブレーションの考え方を利用すると、カメラで取得した画像に写っている既知の物体のワールド座標における位置姿勢を推定できます。 カメラキャリブレーションによる内部パラメータ推定結果の保存
    概要 OpenCV を用いて、複数の画像から一枚のパノラマ画像を作成します。内部パラメータが分かっているカメラを位置を変えずに回転させて画像を取得していき、各画像を取得した時点でのカメラの向きをもとに画像を重ね合わせる方法と、各画像における特徴点が一致するように画像を重ね合わせる方法の二つについて記載します。 キャリブレーションされたカメラを定位置で回転させる方法
    特徴点の検出 Feature Detection 特徴点として利用できるものの一つに、物体の角があります。角を検出するアルゴリズムの一つに Harris Corner Detection があります。分かりやすさのため、モルフォロジー変換で「角」を膨張させています。 #!/usr/bin/python # -*- coding: utf-8 -*- import numpy as np i
    概要 特徴点が分かっている既知の物体については、単一カメラを用いて位置姿勢が推定できます。物体や全体のシーンが未知である場合は、例えばステレオ形式のカメラ二つを用いることで同様の結果を得ます。具体的には、距離情報を濃淡として保存した距離画像 (Depth Map; 奥行きマップ) を作成します。 エピポーラ幾何について