web-dev-qa-db-ja.com

円柱投影で表示されるワープ画像

円柱からの投影のように見えるように、フラットな画像をワープしたいと思います。

私はこのようなフラットな画像を持っています:

I have this flat image

そして私はそれをこのようなものとして2D画像で見せたいです:

output image should be something like this

幾何学的な投影法については少し段階的に廃止しました。私は this のような他のいくつかの質問を訪問しましたが、これらの円筒座標(シータとロー)をデカルト(x、y)平面のx、y座標にどのように表すことができるかわかりません。詳細な例を挙げてくれませんか? iPhone用にコーディングしていますが、OpenCVなどのサードパーティライブラリは使用していません。

本当にありがとう。

28
Usman.3D

これは、2つの部分、数学とコードの答えです。

数学

関係する投影法は興味深いのでこの問題が好きですが、数学はそれほど難しくなく手作業で解くことができます。まず、正確に画像が歪む理由を理解することが重要です。平らな画像があり、その前に凹型の円柱が置かれているとします。

Demo Image 1

最初のステップは、画像を曲面上に移動する正投影です。

Demo Image 2

次に、それらの点が遠近法で画像平面に投影されます。この場合、円柱のすべての部分のZ座標が画像平面よりも大きいため、画像全体が縮小することに注意してください。あなたの場合、円柱は左端と右端でイメージプレーンに接触しているため、収縮は発生しません。ポイントが逆投影されると、それらがイメージプレーン上でフラットラインを形成しなくなることに注意してください。円柱のz座標がxとともに変化するため、曲線が存在します。

Demo Image 3

最初のトリックは、実際にこのプロセスを逆に表現したいということです。最初に、元の画像のすべてのピクセルを取り、それを新しい画像に移動したいと考えるかもしれません。古い画像に現れた新しい画像のすべてのピクセルをチェックして色を設定すると、実際にははるかにうまく機能します。つまり、3つのことを行う必要があります。

  1. シリンダーパラメーターを設定する
  2. カメラからの光線を投影し、新しい画像の各点を通過して、円柱上のx、y、z座標を見つけます。
  3. 正射投影を使用してその光線をイメージプレーンに戻します(つまり、zコンポーネントをドロップするだけです)。

すべてを追跡するのは少し難しいので、一貫した用語を使用するようにします。まず、シリンダーが画像の端に触れることを保証したいと思います。これが当てはまる場合、選択できる2つの自由パラメーターは、円柱の半径と焦点距離です。

Zx平面の円の方程式は、

x^2+(z-z0)^2 = r^2

円の中心がz軸上にあると仮定します。円柱のエッジが幅wと焦点距離fを持つイメージ平面のエッジに接触する場合

omega^2+(f-z0)^2 = r^2 //define omega = width/2, it cleans it up a bit
z0 = f-sqrt(r^2-omega^2)

これで、手順2に進むシリンダーのすべてのパラメーターがわかりました。カメラからラインを投影し、ximのイメージプレーンを介してxcのシリンダーに投影します。以下は、用語の簡単な図です。

Demo4

投影している線は原点から始まり、ximでイメージプレーンと交差しています。その方程式は次のように書くことができます

x = xim*z/f

円柱を通過するときのx座標が必要なので、方程式を組み合わせます

xim^2*z^2/f^2 + z^2 - 2*z*z0 +z0^2 - r^2 = 0

二次方程式を使用してzを解いてから、線方程式にプラグインしてxを取得できます。 2つのソリューションは、線が円に接する2つの場所に対応します。これは、イメージプレーンの後に発生するものにのみ関心があり、x座標が常に大きいため、-b + sqrt(...)を使用するためです。 。その後

xc = xim*z/f;
yc = yim*z/f;

正投影を削除する最後の手順は簡単で、zコンポーネントをドロップするだけで完了です。

コード

OpenCVを使用していないとのことですが、デモではイメージコンテナーとして使用します。すべての操作はピクセル単位で行われるため、これを変換して、使用している任意のイメージコンテナーで機能するようにすることは難しくありません。まず、最終的な画像の画像座標から元の画像の座標に変換する関数を作成しました。 OpenCVは画像の原点を左上に配置するため、w/2とh/2を減算することから始め、それらを追加して終了します。

cv::Point2f convert_pt(cv::Point2f point,int w,int h)
{
    //center the point at 0,0
    cv::Point2f pc(point.x-w/2,point.y-h/2);

    //these are your free parameters
    float f = w;
    float r = w;

    float omega = w/2;
    float z0 = f - sqrt(r*r-omega*omega);

    float zc = (2*z0+sqrt(4*z0*z0-4*(pc.x*pc.x/(f*f)+1)*(z0*z0-r*r)))/(2* (pc.x*pc.x/(f*f)+1)); 
    cv::Point2f final_point(pc.x*zc/f,pc.y*zc/f);
    final_point.x += w/2;
    final_point.y += h/2;
    return final_point;
}

あとは、新しい画像のすべてのポイントを古い画像でサンプリングするだけです。これを行う方法はたくさんありますが、私が知っている最も簡単な方法であるバイリニア補間を行います。また、これはグレースケールでのみ機能するように設定されているため、カラーで機能するようにするには、3つのチャネルすべてにプロセスを適用するだけです。私はそれがこの方法で少し明確になると思っただけです。


for(int y = 0; y < height; y++)
{
    for(int x = 0; x < width; x++)
    {
        cv::Point2f current_pos(x,y);
        current_pos = convert_pt(current_pos, width, height);

        cv::Point2i top_left((int)current_pos.x,(int)current_pos.y); //top left because of integer rounding

        //make sure the point is actually inside the original image
        if(top_left.x < 0 ||
           top_left.x > width-2 ||
           top_left.y < 0 ||
           top_left.y > height-2)
        {
            continue;
        }

        //bilinear interpolation
        float dx = current_pos.x-top_left.x;
        float dy = current_pos.y-top_left.y;

        float weight_tl = (1.0 - dx) * (1.0 - dy);
        float weight_tr = (dx)       * (1.0 - dy);
        float weight_bl = (1.0 - dx) * (dy);
        float weight_br = (dx)       * (dy);

        uchar value =   weight_tl * image.at<uchar>(top_left) +
        weight_tr * image.at<uchar>(top_left.y,top_left.x+1) +
        weight_bl * image.at<uchar>(top_left.y+1,top_left.x) +
        weight_br * image.at<uchar>(top_left.y+1,top_left.x+1);

        dest_im.at<uchar>(y,x) = value;
    }
}

以下は、f = w/2およびr = wのサンプル出力です。

enter image description here

63
Hammer