之前在研究c++ opencv,通过掉opencv接口,实现三维坐标转到二维坐标,但是一直没有时间去搞。刚好最近项目需要,用到c#来实现透视投影,然后就细致研究了下。总体而言还是离不开小孔成像模型:
关键就是三个步骤:
- 世界坐标转相机局部坐标
- 相机坐标转为归一化坐标
得到的模型是
- 归一化坐标转像素坐标
下边得图片是我对整个过程得总结:
待会会给出源码,现在来看看每个步骤得关键步骤和代码。
世界坐标转相机局部坐标
public void Trans_World2Eye(_3Dpoint w, _3Dpoint e)
{
/* 将物体转换到相机下,得到相对坐标 */
w.x -= camera.from.x;
w.y -= camera.from.y;
w.z -= camera.from.z;
/* Convert to eye coordinates using basis vectors */
//用三个在初始化过程计算出来的基向量来表达物体坐标,
//这时候e 为物体在相机坐标系下得坐标
e.x = w.x * basisa.x + w.y * basisa.y + w.z * basisa.z;
e.y = w.x * basisb.x + w.y * basisb.y + w.z * basisb.z;
e.z = w.x * basisc.x + w.y * basisc.y + w.z * basisc.z;
}
代码中,basisa、basisb、basisc是相机坐标系得三个基向量,是这样求解得:
//view direction vector
basisb.x = camera.to.x - camera.from.x;
basisb.y = camera.to.y - camera.from.y;
basisb.z = camera.to.z - camera.from.z;
//单位化3
Normalise(basisb);
///向量叉乘,得到第三个垂直向量
basisa = CrossProduct(camera.up, basisb);
Normalise(basisa);
/* Are the up vector and view direction colinear */
if (EqualVertex(basisa, origin))
{
return (false);
}
basisc = CrossProduct(basisb, basisa);
得到三个基向量。
相机坐标转归一化坐标
public void Trans_Eye2Norm(_3Dpoint e, _3Dpoint n)
{
double d;
if (camera.projection == 0)
{
d = camera.zoom / e.y;
n.x = d * e.x / tanthetah;
n.y = e.y;
n.z = d * e.z / tanthetav;
}
else
{
n.x = camera.zoom * e.x / tanthetah;
n.y = e.y;
n.z = camera.zoom * e.z / tanthetav;
}
}
这里输入了一些相机参数,个人得理解是类似下图:
参数化对于计算得到三维向量得两个方向进行了归一化,这里略微有点不同,是因为后边转像素坐标时候,y轴忽略掉了,不参与计算。
归一化坐标转像素坐标
像素坐标坐标原点默认是左上角,因此转换了之后需要进行平移操作。
public void Trans_Norm2Screen(_3Dpoint norm, _2Dpoint projected)
{
//MessageBox.Show("the value of are");
projected.h = Convert.ToInt32(screen.center.h - screen.size.h * norm.x / 2);
projected.v = Convert.ToInt32(screen.center.v - screen.size.v * norm.z / 2);
}
核心代码
关键思路和核心代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Graphics3Dto2D
{
class Projection
{
private _3Dpoint origin;
private _3Dpoint e1, e2, n1, n2;
private Camera camera;
private Screen screen;
private double tanthetah, tanthetav;
private _3Dpoint basisa, basisb, basisc;
private double EPSILON;
private double DTOR;// 0.01745329252
public _2Dpoint p1;
public _2Dpoint p2;
public Projection()
{
EPSILON = 0.001;
DTOR = 0.01745329252;
camera = new Camera();
screen = new Screen();
origin = new _3Dpoint();
basisa = new _3Dpoint();
basisb = new _3Dpoint();
basisc = new _3Dpoint();
p1 = new _2Dpoint();
p2 = new _2Dpoint();
e1 = new _3Dpoint();
e2 = new _3Dpoint();
n1 = new _3Dpoint();
n2 = new _3Dpoint();
if (Trans_Initialise() != true)
{
MessageBox.Show("Error in initializing variable");
}
}
public bool Trans_Initialise()
{
#region 确保up from to 都有意义
/* Is the camera position and view vector coincident ?
摄像机位置和视图向量是否一致(判断to和from是否重叠,重叠就不给予接下去得计算,没有意义)
*/
if (EqualVertex(camera.to, camera.from))
{
return (false);
}
/* 是否设置了up向量 */
if (EqualVertex(camera.up, origin))
{
return (false);
}
#endregion
//view direction vector
basisb.x = camera.to.x - camera.from.x;
basisb.y = camera.to.y - camera.from.y;
basisb.z = camera.to.z - camera.from.z;
//单位化3
Normalise(basisb);
///向量叉乘,得到第三个垂直向量
basisa = CrossProduct(camera.up, basisb);
Normalise(basisa);
/* Are the up vector and view direction colinear */
if (EqualVertex(basisa, origin))
{
return (false);
}
basisc = CrossProduct(basisb, basisa);
/*成像平面合法 ? */
if (camera.angleh < EPSILON || camera.anglev < EPSILON)
{
return (false);
}
/* 计算相机光圈静校正,注意:角度为度数 */
tanthetah = Math.Tan(camera.angleh * DTOR / 2);
tanthetav = Math.Tan(camera.anglev * DTOR / 2);
/* 合法的镜头缩放 ? */
if (camera.zoom < EPSILON)
{
return (false);
}
/* 裁剪平面合法 ? */
if (camera.front < 0 || camera.back < 0 || camera.back <= camera.front)
{
return (false);
}
return true;
}
public void Trans_World2Eye(_3Dpoint w, _3Dpoint e)
{
/* 将物体转换到相机下,得到相对坐标 */
w.x -= camera.from.x;
w.y -= camera.from.y;
w.z -= camera.from.z;
/* Convert to eye coordinates using basis vectors */
//用三个在初始化过程计算出来的基向量来表达物体坐标,
//这时候e 为物体在相机坐标系下得坐标
e.x = w.x * basisa.x + w.y * basisa.y + w.z * basisa.z;
e.y = w.x * basisb.x + w.y * basisb.y + w.z * basisb.z;
e.z = w.x * basisc.x + w.y * basisc.y + w.z * basisc.z;
}
public bool Trans_ClipEye(_3Dpoint e1, _3Dpoint e2)
{
double mu;
//判断是否在切平面前后,从而判断是否能看观测到改物体
/* Is the vector totally in front of the front cutting plane ? */
if (e1.y <= camera.front && e2.y <= camera.front)
{
return (false);
}
/* Is the vector totally behind the back cutting plane ? */
if (e1.y >= camera.back && e2.y >= camera.back)
{
return (false);
}
/* Is the vector partly in front of the front cutting plane ? */
if ((e1.y < camera.front && e2.y > camera.front) ||
(e1.y > camera.front && e2.y < camera.front))
{
mu = (camera.front - e1.y) / (e2.y - e1.y);
if (e1.y < camera.front)
{
e1.x = e1.x + mu * (e2.x - e1.x);
e1.z = e1.z + mu * (e2.z - e1.z);
e1.y = camera.front;
}
else
{
e2.x = e1.x + mu * (e2.x - e1.x);
e2.z = e1.z + mu * (e2.z - e1.z);
e2.y = camera.front;
}
}
/* Is the vector partly behind the back cutting plane ? */
if ((e1.y < camera.back && e2.y > camera.back) ||
(e1.y > camera.back && e2.y < camera.back))
{
mu = (camera.back - e1.y) / (e2.y - e1.y);
if (e1.y < camera.back)
{
e2.x = e1.x + mu * (e2.x - e1.x);
e2.z = e1.z + mu * (e2.z - e1.z);
e2.y = camera.back;
}
else
{
e1.x = e1.x + mu * (e2.x - e1.x);
e1.z = e1.z + mu * (e2.z - e1.z);
e1.y = camera.back;
}
}
return (true);
}
public void Trans_Eye2Norm(_3Dpoint e, _3Dpoint n)
{
double d;
if (camera.projection == 0)
{
d = camera.zoom / e.y;
n.x = d * e.x / tanthetah;
n.y = e.y;
n.z = d * e.z / tanthetav;
}
else
{
n.x = camera.zoom * e.x / tanthetah;
n.y = e.y;
n.z = camera.zoom * e.z / tanthetav;
}
}
public bool Trans_ClipNorm(_3Dpoint n1, _3Dpoint n2)
{
double mu;
/* Is the line segment totally right of x = 1 ? */
if (n1.x >= 1 && n2.x >= 1)
return (false);
/* Is the line segment totally left of x = -1 ? */
if (n1.x <= -1 && n2.x <= -1)
return (false);
/* Does the vector cross x = 1 ? */
if ((n1.x > 1 && n2.x < 1) || (n1.x < 1 && n2.x > 1))
{
mu = (1 - n1.x) / (n2.x - n1.x);
if (n1.x < 1)
{
n2.z = n1.z + mu * (n2.z - n1.z);
n2.x = 1;
}
else
{
n1.z = n1.z + mu * (n2.z - n1.z);
n1.x = 1;
}
}
/* Does the vector cross x = -1 ? */
if ((n1.x < -1 && n2.x > -1) || (n1.x > -1 && n2.x < -1))
{
mu = (-1 - n1.x) / (n2.x - n1.x);
if (n1.x > -1)
{
n2.z = n1.z + mu * (n2.z - n1.z);
n2.x = -1;
}
else
{
n1.z = n1.z + mu * (n2.z - n1.z);
n1.x = -1;
}
}
/* Is the line segment totally above z = 1 ? */
if (n1.z >= 1 && n2.z >= 1)
return (false);
/* Is the line segment totally below z = -1 ? */
if (n1.z <= -1 && n2.z <= -1)
return (false);
/* Does the vector cross z = 1 ? */
if ((n1.z > 1 && n2.z < 1) || (n1.z < 1 && n2.z > 1))
{
mu = (1 - n1.z) / (n2.z - n1.z);
if (n1.z < 1)
{
n2.x = n1.x + mu * (n2.x - n1.x);
n2.z = 1;
}
else
{
n1.x = n1.x + mu * (n2.x - n1.x);
n1.z = 1;
}
}
/* Does the vector cross z = -1 ? */
if ((n1.z < -1 && n2.z > -1) || (n1.z > -1 && n2.z < -1))
{
mu = (-1 - n1.z) / (n2.z - n1.z);
if (n1.z > -1)
{
n2.x = n1.x + mu * (n2.x - n1.x);
n2.z = -1;
}
else
{
n1.x = n1.x + mu * (n2.x - n1.x);
n1.z = -1;
}
}
return (true);
}
public void Trans_Norm2Screen(_3Dpoint norm, _2Dpoint projected)
{
//MessageBox.Show("the value of are");
projected.h = Convert.ToInt32(screen.center.h - screen.size.h * norm.x / 2);
projected.v = Convert.ToInt32(screen.center.v - screen.size.v * norm.z / 2);
}
/// <summary>
/// 投影点
/// </summary>
/// <returns></returns>
public bool Trans_Point(_3Dpoint w1)
{
//转换到相机坐标系下
Trans_World2Eye(w1, e1);
//判断相机是否能够看到物体
if (Trans_ClipEye(e1, e2))
{
//相机坐标系转归一化平面
Trans_Eye2Norm(e1, n1);
//归一化平面转屏幕坐标
Trans_Norm2Screen(n1, p1);
}
return true;
}
/// <summary>
/// 投影线
/// </summary>
/// <param name="w1"></param>
/// <param name="w2"></param>
/// <returns></returns>
public bool Trans_Line(_3Dpoint w1, _3Dpoint w2)
{
//转换到相机坐标系下
Trans_World2Eye(w1, e1);
Trans_World2Eye(w2, e2);
//判断相机是否能够看到物体
if (Trans_ClipEye(e1, e2))
{
//相机坐标系转归一化平面
Trans_Eye2Norm(e1, n1);
Trans_Eye2Norm(e2, n2);
if (Trans_ClipNorm(n1, n2))
{
//归一化平面转屏幕坐标
Trans_Norm2Screen(n1, p1);
Trans_Norm2Screen(n2, p2);
return (true);
}
}
return (true);
}
public void Normalise(_3Dpoint v)
{
double length;
length = Math.Sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
v.x /= length;
v.y /= length;
v.z /= length;
}
public _3Dpoint CrossProduct(_3Dpoint p1, _3Dpoint p2)
{
_3Dpoint p3;
p3 = new _3Dpoint(0, 0, 0);
p3.x = p1.y * p2.z - p1.z * p2.y;
p3.y = p1.z * p2.x - p1.x * p2.z;
p3.z = p1.x * p2.y - p1.y * p2.x;
return p3;
}
public bool EqualVertex(_3Dpoint p1, _3Dpoint p2)
{
if (Math.Abs(p1.x - p2.x) > EPSILON)
return (false);
if (Math.Abs(p1.y - p2.y) > EPSILON)
return (false);
if (Math.Abs(p1.z - p2.z) > EPSILON)
return (false);
return (true);
}
}
}
总结
十分感谢国外作者如此优秀得实现,才能让我看到这整个过程,并且实现在自己得程序当中,目前还有问题,但是看懂了原理也能够分析存在问题得原因。因此再次分享给大家。后续会上传源程序,并且增加了中文得解释,大家相互学习。