讓我們從定義開始,三維重建 — 3d reconstruction — 是在長程數(shù)據(jù)處理的基礎(chǔ)上開發(fā)對象的 3D 模型。可以使用多種原理進(jìn)行三維重建:立體測量、立體光度測量、體積去除或運(yùn)動數(shù)據(jù)。
本教程可以作為一個指南,解釋了如何開發(fā)一個簡單的應(yīng)用程序來使用 GPU 重建對象的幾何形狀。
在上述原則中,我們選擇了由Brian Cureless和Mark Levoy在其題為“ A Volumetric Method for Building Complex Models from Range Images ”的文章中提出的體積去除算法。
下圖闡明了算法的基本原理。從一組圖像中重建的3D對象顯示在左側(cè)。在處理圖像時,該算法會移除位于對象前面的 3D 點(diǎn)(作者將結(jié)構(gòu)光技術(shù)應(yīng)用于深度映射)。第一次照片處理的結(jié)果顯示在中心。使用從第二臺相機(jī)獲得的數(shù)據(jù),程序刪除額外的 3D 點(diǎn)。使用的角度越多,移除的額外 3D 點(diǎn)就越多;最后,只剩下屬于該對象的點(diǎn)。
在應(yīng)用程序中,我們實(shí)現(xiàn)了算法的簡化版本,該算法僅刪除圖像中位于對象輪廓之外的點(diǎn)。繼原文之后,我們將整個空間劃分為一組立方元素(體素)。
為了確定體素是否屬于 3D 對象,我們應(yīng)用 GPU 渲染并將獲得的投影與對象輪廓相匹配。
要獲得投影,使用以下函數(shù):
inline void HComparator::render(HModel *model, const long long *voxel, HFrame *frame){
pixelBuffer->makeCurrent();
glClear(GL_COLOR_BUFFER_BIT);
frame->setGL();
float coords[3];
model->position(voxel, coords);
glTranslatef(coords[0], coords[1], coords[2]);
double s = model->delta();
glScalef(s, s, s);
glCallList(voxelList);
glFlush();
}
更詳細(xì)地解釋是,
pixelBuffer->makeCurrent () — 將繪圖內(nèi)容切換到屏幕外 QGLPixelBuffer緩沖區(qū)。
初始化輸出緩沖區(qū)時,裁剪、深度測試和混合被禁用,因?yàn)槲ㄒ坏哪繕?biāo)是確定相對于對象的體素空間位置。
void HComparator::initPixelBuffer(){
pixelBuffer->makeCurrent();
glDisable(GL_CULL_FACE);
glDisable(GL_DEPTH_TEST);
glDisable(GL_LIGHTING);
glDisable(GL_BLEND);
glEnable(GL_VERTEX_ARRAY);
glClearColor(1, 1, 1, 1);
glColor3f(0, 0, 0);
createVoxelList();
}
切換HComparator::render中的內(nèi)容后,清空輸出緩沖區(qū)并設(shè)置投影參數(shù)。
glClear(GL_COLOR_BUFFER_BIT);
frame->setGL();
float coords[3];
model->position(voxel, coords);
glTranslatef(coords[0], coords[1], coords[2]);
double s = model->delta();
glScalef(s, s, s);
為了渲染體素,調(diào)用glCallList(voxelList)函數(shù)來執(zhí)行預(yù)先形成的命令列表。初始化函數(shù)為:
void HComparator::createVoxelList(){
float eps = 1e-4;
static GLdouble vertices[]={
0, 0, 0, 1, 0, 0,
1, 1, 0, 0, 1, 0,
0, 0, 1, 1, 0, 1,
1, 1, 1, 0, 1, 1
};
for (int i = 0; i < 24; i++)
{
if (vertices[i] == 0)
{
vertices[i] -= eps;
}
if (vertices[i] == 1)
{
vertices[i] += eps;
}
}
static GLubyte indices[]={
0, 3, 2, 1, 2, 3, 7, 6,
0, 4, 7, 3, 1, 2, 6, 5,
4, 5, 6, 7, 0, 1, 5, 4
};
voxelList=glGenLists(1);
glVertexPointer(3, GL_DOUBLE, 0, vertices);
glMatrixMode(GL_MODELVIEW);
glNewList(voxelList, GL_COMPILE);
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_BYTE, indices);
glEndList();
}
繪制后,使用HComparator::compareData函數(shù)確定相對于對象的體素空間位置。
char HComparator::compareData(HModel *model, const long long *voxel, HFrame* frame){
int min_x, min_y, max_x, max_y;
long int width, _width, height;
auto image=frame->data;
getBounds(min_x, min_y, max_x, max_y, frame->width, frame->height);
width=max_x-min_x;
height=max_y-min_y;
if ((width == 0) || (height == 0))
{
return 4;
}
_width=width;
if(_width%4)
_width+=4-_width%4;
glReadPixels(min_x, min_y, width, height, GL_RED, GL_UNSIGNED_BYTE, currentData);
char result=4;
for(int j=0; j<height; j++){
auto data_ptr = ¤tData[j*_width];
auto image_ptr = &image[(min_y + j)*frame->width + min_x];
for(auto i=0; i<width; i++){
if(data_ptr[i] == 0){
if(image_ptr[i] != 0){
if(result==1)
return 2;
result=0;
}
else{
if(result==0)
return 2;
result=1;
}
}
}
}
return result;
}
compareData函數(shù)復(fù)制緩沖區(qū)內(nèi)容并根據(jù)三個可能的選項(xiàng)將其與對象輪廓進(jìn)行比較(見下圖):
a) 體素完全位于對象內(nèi)(代碼 1);
b) 體素屬于邊界(代碼2);
c) 體素完全位于對象之外(代碼 0)。
用于開發(fā) 3D 模型的角度集由HReconstruction::process函數(shù)順序處理。我們從每個體素都屬于對象的假設(shè)開始。如果確定體素位置超出對象的某個角度,則其處理停止并從模型中移除。執(zhí)行整個處理,直到考慮所有角度。最后,只剩下屬于對象模型的體素。
void HReconstruction::process() {
int idle_counter = 0;
_voxel[0] = _voxel[0]%_model->N();
_voxel[1] = _voxel[1]%_model->N();
_voxel[2] = _voxel[2]%_model->N();
for (; _voxel[0] < _model->N(); _voxel[0]++) {
for (; _voxel[1] < _model->N(); _voxel[1]++) {
for (; _voxel[2] < _model->N(); _voxel[2]++) {
char voxel_type = 1;
for (auto &frame: _model->frames()) {
char t=_comparator->compare(_model, _voxel, frame);
if ((t == 0) || (t == 4)) {
voxel_type = 0;
break;
}
}
_model->setStatus(_voxel, voxel_type);
if(idle_counter++>idleValue)
return;
}
_voxel[2] = 0;
}
_voxel[1] = 0;
}
workFlag=0;
}
為了匹配體素和對象輪廓,應(yīng)該知道投影參數(shù)。它們由GL_PROJECTION和GL_MODELVIEW矩陣定義(參見setGL函數(shù))。
void HFrame::setGL(){
glMatrixMode(GL_PROJECTION);
glViewport(0, 0, width, height);
glLoadMatrixd((double*)intrisicParameters.data);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glLoadMatrixd((double*)modelviewMatrix.data);
}
GL_PROJECTION矩陣由相機(jī)參數(shù)定義,特別是焦距和圖像大小(
HFrame::loadIntrisicParameters函數(shù))。
void HFrame::loadIntrisicParameters(const Mat &img, double focal_length)
{
// http://kgeorge.github.io/2014/03/08/calculating-opengl-perspective-matrix-from-opencv-intrinsic-matrix/
Mat_<double> persp(4,4); persp.setTo(0);
double f = focalLengthToPixels(focal_length, img.rows);
double fx = f;
double fy = f;
double cx = img.cols/(double)2;
double cy = img.rows/(double)2;
persp(0,0) = fx/cx;
persp(1,1) = fy/cy;
double near = 0.01;
double far = 1000;
persp(2,2) = -(far+near)/(far-near);
persp(2,3) = -2.0*far*near / (far-near);
persp(3,2) = -1.0;
persp = persp.t(); //to col-major for OpenGL
intrisicParameters = persp.clone();
}
可以使用增強(qiáng)現(xiàn)實(shí)標(biāo)記來確定相機(jī)的 3D 位置,我們從 aruco 庫中獲取它。標(biāo)記是要打印在一張紙上的特殊圖像(見下圖)。
拍攝物體時,標(biāo)記必須保持不動,并在每張物體照片中進(jìn)入相機(jī)視野。
該庫檢測標(biāo)記控制點(diǎn),然后使用相機(jī)焦距計(jì)算標(biāo)記 3D 位置(rvec和tvec)。
auto arucoDict = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);
auto grid = cv::aruco::GridBoard::create(6, 8, 0.04, 0.02, arucoDict);
Mat gray;
vector<Mat> board_corners;
Mat board_ids;
cvtColor(img_bgr, gray, COLOR_BGR2GRAY);
cv::aruco::detectMarkers(gray, arucoDict, board_corners, board_ids);
if (board_corners.empty())
{
return false;
}
double pixelsFocalLength = focalLengthToPixels(focalLength, img_bgra.rows);
cameraMatrix = (Mat_<double>(3, 3)<< pixelsFocalLength, 0, img_bgra.cols*0.5, 0, pixelsFocalLength, img_bgra.rows*0.5, 0, 0, 1);
cv::aruco::estimatePoseBoard(board_corners, board_ids, grid, cameraMatrix, Mat(), rvec, tvec);
loadExtrisicParameters(rvec, tvec, modelviewMatrix);
rvec和tvec參數(shù)確定GL_MODELVIEW矩陣(參見HFrame ::loadExtrisicParameters函數(shù))。
void HFrame::loadExtrisicParameters(const Mat &Rvec, const Mat &Tvec, Mat &modelview_matrix)
{
// http://kgeorge.github.io/2014/03/08/calculating-opengl-perspective-matrix-from-opencv-intrinsic-matrix/
CV_Assert(Rvec.rows == 3);
CV_Assert(Tvec.rows == 3);
Mat Rot(3,3,CV_32FC1);
Rodrigues(Rvec, Rot);
// [R | t] matrix
Mat_<double> para = Mat_<double>::eye(4,4);
Rot.convertTo(para(Rect(0,0,3,3)),CV_64F);
Tvec.copyTo(para(Rect(3,0,1,3)));
Mat cvToGl = Mat::zeros(4, 4, CV_64F);
cvToGl.at<double>(0, 0) = 1.0f;
cvToGl.at<double>(1, 1) = -1.0f; // Invert the y axis
cvToGl.at<double>(2, 2) = -1.0f; // invert the z axis
cvToGl.at<double>(3, 3) = 1.0f;
para = cvToGl * para;
Mat(para.t()).copyTo(modelview_matrix); // transpose to col-major for OpenGL
}
至此,我們學(xué)會了如何在圖像平面上投影體素,確定體素相對于物體圖像的位置,計(jì)算投影參數(shù),并通過處理來自多個攝像機(jī)的數(shù)據(jù)來確定體素是否屬于物體體;這是一種用于重建 3D 對象的簡化但完整的技術(shù),源代碼可以在這里下載。
原文鏈接:
http://www.bimant.com/blog/simple-3d-reconstruction-implementation/