以下是简单的程序分析:
这个程序是在simple.c的基础上进行的,所以这里只是分析不同的部分,对simple不理解的清参考前几篇文章;
1:在这个程序中自定义了一个结构体:
typedef struct {
char *patt_name;
int patt_id;
int model_id;
int visible;
double width;
double center[2];
double trans[3][4];
} OBJECT_T;
这个机构体几乎涵盖了做基于标识的AR的一些常规参数;
然后定义了两个变量,初始化了目标的一些值;
OBJECT_T object[2] = {
{OBJ1_PATT_NAME, -1, OBJ1_MODEL_ID, 0, OBJ1_SIZE, {0.0,0.0}},
{OBJ2_PATT_NAME, -1, OBJ2_MODEL_ID, 0, OBJ2_SIZE, {0.0,0.0}}
};
2:在mainloop函数中增加了以下代码,可以求出多个标识相对于相机的转换矩阵:
/* check for object visibility *///先检查第一个标识的匹配,计算旋转矩阵,然后计算第二个标识的转换矩阵;若识别了标识,则定义这个标识为可视的。
for( i = 0; i < 2; i++ ) {
k = -1;
for( j = 0; j < marker_num; j++ ) {
if( object[i].patt_id == marker_info[j].id ) {
if( k == -1 ) k = j;
else if( marker_info[k].cf < marker_info[j].cf ) k = j;
}
}
object[i].visible = k;
if( k >= 0 ) {
arGetTransMat(&marker_info[k],
object[i].center, object[i].width,
object[i].trans);
draw( object[i].model_id, object[i].trans );
}
}
3:在这个程序中扩展增加了解释通过可视化的两个标识相对于相机的转换矩阵,如何求出这两个标识之间的转换矩阵,在程序演示中可以看出,在后台打印出这个转换矩阵,随着两个标识的移动,计算出的转换矩阵是变化的。
if( object[0].visible >= 0
&& object[1].visible >= 0 ) {
double wmat1[3][4], wmat2[3][4];
arUtilMatInv(object[0].trans, wmat1);
arUtilMatMul(wmat1, object[1].trans, wmat2);
for( j = 0; j < 3; j++ ) {
for( i = 0; i < 4; i++ ) printf("%8.4f ", wmat2[j][i]);
printf("\n");
}
printf("\n\n");
}
4:在绘制函数中,针对不同的标识ID绘制不同的目标:
switch( object ) {
case 0:
glTranslatef( 0.0, 0.0, 25.0 );
glutSolidCube(50.0);
break;
case 1:
glTranslatef( 0.0, 0.0, 40.0 );
glutSolidSphere(40.0, 24, 24);
break;
case 2:
glutSolidCone(25.0, 100.0, 20, 24);
break;
default:
glTranslatef( 0.0, 0.0, 10.0 );
glutSolidTorus(10.0, 40.0, 24, 24);
break;
}
在ARToolKit中,坐标系统是很重要的,需要详细了解下,才不会在错误的地方放置目标和渲染目标。
1:
使用arGetTransMat可以得到标识在相机坐标系中的位置,如果想知道相机在标识坐标系中的位置,要用arMatrixInverse()把上面得到的转换矩阵进行反转。
2:当使用OpenGL对ARToolKit进行渲染时,注意OpenGL是一个右手坐标系,z轴指向你,相机面对的方向是-Z,
ARToolKit使用的是校正过参数的相机透视,所以会造成OpenGL的投影矩阵的离轴。gluPerspective不能创造一个投影矩阵,要有额外的参数glFrustum;在这里不使用分解ARToolKit投影的参数传递给glFrustum,直接导入OpenGL的投影矩阵,glMatrixMode(GL_PROJECTION_MATRIX); 然后调用 glLoadMatrix。
当使用gsub_lite进行渲染时,调用arglCameraFrustum可以消除离轴影响。
使用OpenCV进行摄像机标定,在OpenCV例程中有程序可以实现,
/samples/cpp/tutorial_code/calib3d/camera_calibration里的程序就可以进行单目相机标定。
在http://download.csdn.net/detail/chuhang_zhqr/9293665有我上传的,内含测试图片和文件,想直接用的就去下载吧。
接下来分析下摄像机标定的过程:
1:这个程序中把初始化参数放在了一个文件(in_VID5.xml)中,在程序的开始首先读入文件中的参数:
const string inputSettingsFile = argc > 1 ? argv[1] : "in_VID5.xml";
FileStorage fs(inputSettingsFile, FileStorage::READ); // Read the settings
if (!fs.isOpened())
{
cout << "Could not open the configuration file: \"" << inputSettingsFile << "\"" << endl;
return -1;
}
fs["Settings"] >> s;
fs.release(); // close Settings file
if (!s.goodInput)
{
cout << "Invalid input detected. Application stopping. " << endl;
return -1;
}
1
AR--摄像机标定AR--摄像机标定
这个in_VID5.xml可以设置图像大小,棋盘格大小,还可以设置图像源输入:相机输入(“0”),视频输入,图像序列输入(“/home/zhu/program_c/opencv_study/test_down/camera_calibration/VID5.xml”),相机输入时,在运行程序时,按g开始采集图像并进行计算标定系数,期间可以移动棋盘格或相机,获得不同方位的棋盘格图像,使标定系数尽可能精确。
2:读入相机初始参数后,判断这些数据是否符合,就是验证数据的正确性。
void interprate()
{
goodInput = true;
if (boardSize.width <= 0 || boardSize.height <= 0)
{
cerr << "Invalid Board size: " << boardSize.width << " " << boardSize.height << endl;
goodInput = false;
}
if (squareSize <= 10e-6)
{
cerr << "Invalid square size " << squareSize << endl;
goodInput = false;
}
if (nrFrames <= 0)
{
cerr << "Invalid number of frames " << nrFrames << endl;
goodInput = false;
}
if (input.empty()) // Check for valid input
inputType = INVALID;
else
{
if (input[0] >= '0' && input[0] <= '9')
{
stringstream ss(input);
ss >> cameraID;
inputType = CAMERA;
}
else
{
if (readStringList(input, imageList))
{
inputType = IMAGE_LIST;
nrFrames = (nrFrames < (int)imageList.size()) ? nrFrames : (int)imageList.size();
}
else
inputType = VIDEO_FILE;
}
if (inputType == CAMERA)
inputCapture.open(cameraID);
if (inputType == VIDEO_FILE)
inputCapture.open(input);
if (inputType != IMAGE_LIST && !inputCapture.isOpened())
inputType = INVALID;
}
if (inputType == INVALID)
{
cerr << " Inexistent input: " << input;
goodInput = false;
}
flag = 0;
if(calibFixPrincipalPoint) flag |= CV_CALIB_FIX_PRINCIPAL_POINT;
if(calibZeroTangentDist) flag |= CV_CALIB_ZERO_TANGENT_DIST;
if(aspectRatio) flag |= CV_CALIB_FIX_ASPECT_RATIO;
calibrationPattern = NOT_EXISTING;
if (!patternToUse.compare("CHESSBOARD")) calibrationPattern = CHESSBOARD;
if (!patternToUse.compare("CIRCLES_GRID")) calibrationPattern = CIRCLES_GRID;
if (!patternToUse.compare("ASYMMETRIC_CIRCLES_GRID")) calibrationPattern = ASYMMETRIC_CIRCLES_GRID;
if (calibrationPattern == NOT_EXISTING)
{
cerr << " Inexistent camera calibration mode: " << patternToUse << endl;
goodInput = false;
}
atImageList = 0;
}
3:读入一帧图像,开始寻找特征点,就是棋盘的黑白格角点,
found = findChessboardCorners( view, s.boardSize, pointBuf,
CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_FAST_CHECK | CV_CALIB_CB_NORMALIZE_IMAGE);
寻找棋盘图的内角点位置
int cvFindChessboardCorners(const void* image,CvSize pattern_size,
CvPoint2D32f* coeners,int* corner_count=NULL,
int flags=CV_CALIB_CB_ADAPTIVE_THRESH);
image 输入的棋盘图,必须是8位的灰度或者彩色图像。
pattern_size 棋盘图中每行和每列角点的个数。
corners 检测到的角点
corner_count 输出,角点的个数。如果不是NULL,函数将检测到的角点的个数存储于此变量。
flags 各种操作标志,可以是0或者下面值的组合:
CV_CALIB_CB_ADAPTIVE_THRESH - 使用自适应阈值(通过平均图像亮度计算得到)将图像转换为黑白图,而不是一 个固定的阈值。
CV_CALIB_CB_NORMALIZE_IMAGE - 在利用固定阈值或者自适应的阈值进行二值化之前,先使用cvNormalizeHist来 均衡化图像亮度。
CV_CALIB_CB_FILTERR_QUADS - 使用其他的准测(如轮廓面积,周长,方形形状)来去除在轮廓检测阶段检测到的 错误方块。
函数cvFindChessboardCorners试图确定输入图像是否是棋盘模式,并确定角点的位置。
如果所有角点都被检测到且它们都被以一定顺序排布(一行一行地,每行从左到右),
函数返回非零值,否则在函数不能发现所有角点或者记录他们的情况下,函数返回0.
例如一个正常地棋盘图有8x8个方块和7x7个内角点,内角点是黑色方块相互连通的位置。
这个函数检测到地坐标只有一个大约地值,如果要精确地确定它们的位置,可以使用函数cvFindCornerSubPix。
接下来确定角点的精确位置:
cvtColor(view, viewGray, COLOR_BGR2GRAY);
cornerSubPix( viewGray, pointBuf, Size(11,11),
Size(-1,-1), TermCriteria( CV_TERMCRIT_EPS+CV_TERMCRIT_ITER, 30, 0.1 ));//这里是迭代次数,达到精度或者达到迭代次数就可以结束了。
最后在图像上显示检测到的角点位置,用彩色点和线清楚显示:
drawChessboardCorners( view, s.boardSize, Mat(pointBuf), found );
void cvDrawChessboardCorners(CvArr* image,CvSize pattern_size,
CvPoint2D32f* corners,int count,
int pattern_was_found);
image 结果图像,必须是八位彩色图像。
pattern_size 每行和每列地内角点数目。
corners 检测到地角点数组。
count 角点数目。
pattern_was_found 指示完整地棋盘被发现(!=0)还是没有发现(=0)。
可以传输cvFindChessboardCorner函数的返回值。
当棋盘没有完全检测出时,函数cvDrawChessboardCorners以红色圆圈绘制检测到的棋盘角点。
如果整个棋盘都检测到,则用直线连接所有的角点。
4:当所有图片都检测完了,或者图片足够多了,得到每帧图像中所有角点的精确位置,就开始计算校正系数:
runCalibrationAndSave(s, imageSize, cameraMatrix, distCoeffs, imagePoints)
1
1
要计算校正系数,必须先求这些棋盘点在真实世界坐标系的三维坐标。
for( int i = 0; i < boardSize.height; ++i )
for( int j = 0; j < boardSize.width; ++j )
corners.push_back(Point3f(float( j*squareSize ), float( i*squareSize ), 0));
break;
根据求得的真实世界坐标系的三维坐标和之前在相机坐标系找到的二维的角点位置的转换关系,就可以求得相机内参数,失真系数,相机外参数等:
calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix,distCoeffs, rvecs, tvecs, s.flag|CV_CALIB_FIX_K4|CV_CALIB_FIX_K5);
利用定标来计算摄像机的内参数和外参数
void cvCalibrateCamera2(const CvMat* object_points,const CvMat* image_points,
const CvMat* point_counts,CvSize image_size,
CvMat* intrinsic_matrix,CvMat* distortion_coeffs,
CvMat* rotation_vectors=NULL,
CvMat* translation_vectors=NULL,
int flags=0);
object_points 定标点的世界坐标,为3xN或者Nx3的矩阵,这里N是所有视图中点的总数。
image_Points 定标点的图像坐标,为2xN或者Nx2的矩阵,这里N是所有视图中点的总数。
point_counts 向量,指定不同视图里点的数目,1xM或者Mx1向量,M是视图数目。
image_size 图像大小,只用在初始化参数时。
intrinsic_matrix 输出参数矩阵(A),如果指定CV_CALIB_USE_INTRINSIC_GUESS和(或)CV_CALIB_FIX_ASPECT_RATI ON,fx,fy,cx和cy部分或者全部必须被初始化。
distortion_coeffs 输出大小为4x1或者1x4的向量,里面为形变参数(旋转矩阵的紧凑表达方式,具体参考函数cvRodr igues2)
translation_vectors 输出大小为3xM或Mx3的矩阵,里面为平移向量。
flags 不同的标志,可以是0,或者下面值的组合:
CV_CALIB_USE_INTRINSIC_GUESS - 内参数矩阵包含fx,fy,cx和cy的初始值。否则,(cx,cy)被初始化到图像中心 (这儿用到图像大小),焦距用最小平方差公式计算得到。注意,如果内部参数已知,没有必要使用这个参数,使用cvF indExtrinsicCameraParams2则可。
CV_CALIB_FIX_PRINCIPAL_POINT - 主点在全局优化过程中不变,一直在中心位置或者在其他指定的位置(当CV_CALIB_ USE_INTRINSIC_GUESS设置的时候)。
CV_CALIB_FIX_ASPECT_PATIO - 优化过程中认为fx和fy中只有一个独立变量,保持比例fx/fy不变,fx/fy的值跟内参数矩阵初始化时的值一样。在这种情况下,(fx,fy)的实际初始值或者从输入内存矩阵中读取(当CV_CALIB_USE_INTRINSIC_GUESS被指定时),或者采用估计值(后者情况中fx和fy可能被设置为任意值,只有比值被使用)。
CV_CALIB_ZERO_TANGENT_DIST - 切向形变参数(p1,p2)被设置为0,其值在优化过程中保持为0.
函数cvCalibrateCamera2从每个视图中估计相机的内参数和外参数。3维物体的点和它们对应的在每个视图的2维投影必须被指定。
这些可以通过使用一个已知几何形状且具有容易检测的特征点的物体来实现。这样的一个物体被称作定标设备或者定标模式。
opencv有内建的把棋盘当作定标设备方法(参考cvFindChessboardCorners)。
目前,传入初始化的内参数(当CV_CALIB_USE_INTRINSIC_GUESS不被设置时)只支持平面定标设备(物体点的z坐标必须全0或者全1)。
不过3维定标设备依然可以用在提供初始化内参数矩阵情况。在内参数和外参数矩阵的初始值都计算出之后,它们会被优化用来减小反投影误差(图像上的实际坐标跟cvProjectPoints2计算出的图像坐标的差的平方和)。
5:通过计算真实坐标系中的点与标定系数的关系,重新把真实坐标系的点投射到屏幕上,与拍摄到的图片中角点对比,求误差:
static double computeReprojectionErrors( const vector<vector<Point3f> >& objectPoints,
const vector<vector<Point2f> >& imagePoints,
const vector<Mat>& rvecs, const vector<Mat>& tvecs,
const Mat& cameraMatrix , const Mat& distCoeffs,
vector<float>& perViewErrors)
{
vector<Point2f> imagePoints2;
int i, totalPoints = 0;
double totalErr = 0, err;
perViewErrors.resize(objectPoints.size());
for( i = 0; i < (int)objectPoints.size(); ++i )
{
projectPoints( Mat(objectPoints[i]), rvecs[i], tvecs[i], cameraMatrix,
distCoeffs, imagePoints2);
err = norm(Mat(imagePoints[i]), Mat(imagePoints2), CV_L2);
int n = (int)objectPoints[i].size();
perViewErrors[i] = (float) std::sqrt(err*err/n);
totalErr += err*err;
totalPoints += n;
}
return std::sqrt(totalErr/totalPoints);
}
6:把想保存的数据保存到一个输出.yml中,以便在AR中导入相机参数时使用。
FileStorage fs( s.outputFileName, FileStorage::WRITE );
1
1
7:之后便可以在AR中进行调用了,关于xml和yml文件的读写请自行百度。
下篇文章分析下AR如何计算虚拟目标在相机坐标系中的转换矩阵。