これは、数学とコードの2つの部分からなる答えです。
算数
関係する投影法は興味深いので、私はこの問題が好きですが、数学はそれほど困難なく手作業で解決することができます。まず、画像がそのように歪む理由を正確に理解することが重要です。凹型の円柱が前にあるフラットな画像があるとします。
最初のステップは、画像を曲面に移動する正投影を行うことです。
次に、それらの点は遠近法で画像平面に投影されます。この場合、円柱のすべての部分のz座標が画像平面よりも大きいため、画像全体が縮小することに注意してください。あなたの場合、円柱は左端と右端で画像平面に接触しているため、収縮は発生しません。ポイントが後方に投影されると、イメージプレーン上でフラットラインを形成しなくなったことに注意してください。円柱のz座標がxとともに変化するため、カーブがあります。
最初のトリックは、実際にこのプロセスを逆方向に表現したいということです。最初に、元の画像のすべてのピクセルを取得して、それを新しい画像に移動したいと思うかもしれません。古い画像に表示されていた新しい画像のすべてのピクセルをチェックし、その色を設定すると、実際にははるかにうまく機能します。つまり、3つのことを行う必要があります。
- シリンダーパラメータを設定する
- カメラから光線を投影し、新しい画像の各ポイントを通過して、円柱上のx、y、z座標を見つけます
- 正投影を使用して、その光線を画像平面に戻します(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の円柱に線を投影します。これが用語の簡単な図です。
投影している線は原点から始まり、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の出力例です。