日曜技術者のメモ

趣味でやった事のメモ書きです。

VivadoHLSで射影変換を実装してみた-1-

台形補正のアルゴリズムを調べてたら射影変換でやるみたいなのでFPGAに実装してみた。

OpenCV

まずはOpenCVでさくっとテスト

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
int main(void)
{
    //cv::Mat src_img;
    //src_img = cv::imread("test.jpg", 1);
    IplImage *pImg;
    pImg = cvLoadImage("test.jpg");
    //IplImage -> Mat
    cv::Mat mat(pImg);
    cv::Mat src_img;
    src_img = pImg;

    cv::Mat dst_img = src_img.clone();

    if (src_img.empty()){
        return -1;
    }

    const cv::Point2f src_pt[]={
        cv::Point2f(256, 128),
        cv::Point2f(768, 128),
        cv::Point2f(768, 640),
        cv::Point2f(256, 640) };

    const cv::Point2f dst_pt[]={
        cv::Point2f(356, 228),
        cv::Point2f(768, 228),
        cv::Point2f(668, 640),
        cv::Point2f(256, 540)};

        //パラメータ計算
    cv::Mat homography_matrix = cv::getPerspectiveTransform(src_pt,dst_pt);
        //射影変換
    cv::warpPerspective( src_img, dst_img, homography_matrix,src_img.size());

    cv::namedWindow("Image", CV_WINDOW_AUTOSIZE | CV_WINDOW_FREERATIO);
    cv::imshow("Image", dst_img);
    cv::waitKey(0);
}

f:id:ginnyu-tei:20170705220240j:plain f:id:ginnyu-tei:20170705220131j:plain こんな風に変換前の4点座標と変換後の4点座標を指定することで
座標に合うように画像を変形してくれます。

C++で実装

C++での実装は↓のページを丸パクリ参考にしました。
mf-atelier.sakura.ne.jp

C++で実装する際、ついでに16ビットシフトして整数演算化しました。
多分小数点そこまでなくても問題ないと思う

void homography(
    int x,
    int y,
    int a,
    int b,
    int c,
    int d,
    int e,
    int f,
    int g,
    int h,
    int *u,
    int *v)
{
    *u = (x*a + y*b + c) / (x*g + y*h + 65536);
    *v = (x*d + y*e + f) / (x*g + y*h + 65536);
}


int main(void)
{

    double a, b, c, d, e, f, g, h;
    int i_a, i_b, i_c, i_d, i_e, i_f, i_g, i_h;
    int u, v;

    int    naOrig[4][2] = { { 356, 228 }, { 768, 228 }, { 668, 640 }, { 256, 540 } };
    int    naTran[4][2] = { { 256, 128 }, { 768, 128 }, { 768, 640 }, { 256, 640 } };

    //cv::Mat src_img;
    //src_img = cv::imread("test.jpg", 1);

    IplImage *pImg;
    pImg = cvLoadImage("test.jpg");
    //IplImage -> Mat
    cv::Mat mat(pImg);
    cv::Mat src_img;
    src_img = pImg;

    cv::Mat dst_img = src_img.clone();

    if (src_img.empty()){
        return -1;
    }

    homography_param
        (
        naOrig,
        naTran,
        &a,
        &b,
        &c,
        &d,
        &e,
        &f,
        &g,
        &h
        );

    std::cout <<
        "a=" << a << std::endl <<
        "b=" << b << std::endl <<
        "c=" << c << std::endl <<
        "d=" << d << std::endl <<
        "e=" << e << std::endl <<
        "f=" << f << std::endl <<
        "g=" << g << std::endl <<
        "h=" << h <<
        std::endl;

    i_a = (int)(a * 65536);
    i_b = (int)(b * 65536);
    i_c = (int)(c * 65536);
    i_d = (int)(d * 65536);
    i_e = (int)(e * 65536);
    i_f = (int)(f * 65536);
    i_g = (int)(g * 65536);
    i_h = (int)(h * 65536);

    std::cout <<
        "i_a=" << i_a << std::endl <<
        "i_b=" << i_b << std::endl <<
        "i_c=" << i_c << std::endl <<
        "i_d=" << i_d << std::endl <<
        "i_e=" << i_e << std::endl <<
        "i_f=" << i_f << std::endl <<
        "i_g=" << i_g << std::endl <<
        "i_h=" << i_h <<
        std::endl;

    cv::Vec3b *src = src_img.ptr<cv::Vec3b>(0);
    cv::Vec3b *dst = dst_img.ptr<cv::Vec3b>(0);

    for (int y = 0; y < 768; y++){
        for (int x = 0; x < 1024; x++){
            homography(x, y, i_a, i_b, i_c, i_d, i_e, i_f, i_g, i_h, &u, &v);
            if ((u >= 0) & (u < 1024) &
                (v >= 0) & (v < 768)){
                dst[y * 1024 + x] = src[v * 1024 + u];
            }
            else {
                dst[y * 1024 + x] = cv::Vec3b(0, 0, 0);
            }
        }
    }
    cv::namedWindow("Image", CV_WINDOW_AUTOSIZE | CV_WINDOW_FREERATIO);
    cv::imshow("Image", dst_img);
    cv::waitKey(0);
}

パラメータ変換式は参考元のコードをほぼそのまま使用した。 (関数名をhomography_paramに変更)
逆行列計算はOpenCVを使って↓の用に実装

 // 逆行列
    //matinv(8, dATA, dATA_I);
    cv::Mat matA = (cv::Mat_<double>(8, 8));

    for (int j = 0; j < 8; j++){
        for (int i = 0; i < 8; i++){
            matA.at<double>(i, j) = dATA[i][j];
        }
    }

    matA = matA.inv();

    for (int j = 0; j < 8; j++){
        for (int i = 0; i < 8; i++){
            dATA_I[i][j] = matA.at<double>(i, j);
        }
    }

パラメータをどうやって計算するか知りたかったので数式を見たら
8次元連立一次方程式で自分で解くのは諦めました・・・

それと大事な事なんですが、ここでは変換後座標から変換前座標を計算したいので
変換前座標と変換後座標を入れ替えて計算しています。

こうする事で変換後画像内に変換前画像が当てはまらず
ピクセルが抜けてしまう事が防げます。
その為、ピクセルを補完をしなくても良くなります。
(正確には一番近い画素を変換後座標にコピーしているので
ニアレストネイバーを使ってる言える。)

余談 射影変換の逆計算方法が初め分からず逆行列の計算を
するのかとげんなりしていましたが以下ページを見て解決しました。

detail.chiebukuro.yahoo.co.jp

長くなったのでVivado HLSのコードは次回