モーダルを閉じる工作HardwareHub ロゴ画像

工作HardwareHubは、ロボット工作や電子工作に関する情報やモノが行き交うコミュニティサイトです。さらに詳しく

利用規約プライバシーポリシー に同意したうえでログインしてください。

OpenCV3 C++ による基本的な画像変換

モーダルを閉じる

ステッカーを選択してください

お支払い手続きへ
モーダルを閉じる

お支払い内容をご確認ください

購入商品
」ステッカーの表示権
メッセージ
料金
(税込)
決済方法
GooglePayマーク
決済プラットフォーム
確認事項

利用規約をご確認のうえお支払いください

※カード情報はGoogleアカウント内に保存されます。本サイトやStripeには保存されません

※記事の執筆者は購入者のユーザー名を知ることができます

※購入後のキャンセルはできません

作成日作成日
2020/01/15
最終更新最終更新
2024/07/17
記事区分記事区分
一般公開

目次

    C/C++やアルゴリズムに注目し、実践的な知識を発信しています!

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

    サイズの変更 (resize)

    #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 があります。

    アフィン変換

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

    T=[At] =[abtxcdty]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 行列 HH で表現できます。線形変換ではなく、変換の最後に除算が必要になります。この除算のために 3x3 行列ですが、変数の個数は 8 になります。4 点について変換前後の座標が分かれば変換行列が一意に定まるということになります。

    s[xy1] =H[xy1]s \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} \ = H \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}
    dst(x,y)=src( H00x+H01y+H02H20x+H21y+H22, H10x+H11y+H12H20x+H21y+H22 )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 を利用します。

    #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 を利用すると、変換前後の座標についてノイズが含まれている場合にも対応できます。

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

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

    文字を入力してみます。

    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)
    

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

    #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;
    }
    

    コントラストの変更処理

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

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

    #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)

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

    #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;
    }
    

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

    mp,q=i=1NI(xi,yi)xpyqm_{p,q} = \sum^{N}_{i=1} I(x_i, y_i) x^p y^q

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

    Sobel 微分による輪郭の検出

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

    #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 の二つが基本となります。それら二つを組み合わせる処理として、例えばモルフォロジー勾配があります。膨張した画像と収縮した画像の差分を取ることで、物体の境界線が得られます。オープニングやクロージングによって斑点ノイズを除去することもできます。

    #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;
    }
    

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

    Likeボタン(off)0
    詳細設定を開く/閉じる
    アカウント プロフィール画像

    C/C++やアルゴリズムに注目し、実践的な知識を発信しています!

    記事の執筆者にステッカーを贈る

    有益な情報に対するお礼として、またはコメント欄における質問への返答に対するお礼として、 記事の読者は、執筆者に有料のステッカーを贈ることができます。

    >>さらに詳しくステッカーを贈る
    ステッカーを贈る コンセプト画像

    Feedbacks

    Feedbacks コンセプト画像

      ログインするとコメントを投稿できます。

      ログインする

      関連記事