1. 前言

前段时间正好看到一位大神的博客 http://blog.csdn.net/onezeros/article/details/6110838
利用双目视觉来构建一个书写系统, 涉及到相机的标定, 图像处理, 重构等相关内容, 自己对这方面也比较感兴趣, 于是就将他的代码git clone 下来做了研究, 这里给出一些研究的相关记录
同时, 我将自己根据源代码进行改写的opencv2.0 版本的工程代码也放到了自己的github 中:
https://github.com/zhyh2010/3dreconstructionTools

2. 工程分析

2.1 ChessboardImageGenerator

2.1.1 目标

这个工程的主要目的是为了得到张正友标定方法中所涉及的棋盘格图像

2.1.2 核心思路

这个工程中最核心的绘制思想是, 为每一个棋盘格区域中的每一个像素点进行绘制, 本质上四层 for 循环就可搞定

2.1.3 绘制效果

2.1.4 核心代码

由于原始代码是通过opencv 1.0 写的, 我们这里改为 opencv 2.0 版本

void drawBlocks(Mat src, bool isLight, int offsetx, int offsety, int blockLen){for (int i = 0; i < blockLen; i++){for (int j = 0; j < blockLen; j++){src.at<double>(offsetx + i, offsety + j) = (isLight ? 255 : 0);}}
}void drawChessBox(Mat & src, int h, int w, int len){int height = h*len;int width = w*len;src = Mat(width, height, CV_64FC1);for (int i = 0; i < h; i++){for (int j = 0; j < w; j++){drawBlocks(src, (i + j) % 2, i * len, j * len, len);}}
}

2.2 ImageSampler

2.2.1 目标

将相机运行起来, 按住enter 键实现存图功能, 这部分比较简单, 不再多说了

2.3 ChessboardCorner

2.3.1 目标

这个工程的目标主要在于找出所拍摄的棋盘格图像中的角点信息, 需要注意的是, 这个棋盘格必须是要完整的位于相机的视场中, 否则 findChessboardCorners 会调用出错, 并且输入一定是灰度图

2.3.2 核心思路

先通过 findChessboardCorners 找到粗略的角点, 然后通过cornerSubPix 找出亚像素级的角点信息

2.3.3 检测效果


2.3.4 核心代码

bool FindCorners(Mat src, int nw, int nh, Mat & corners, Mat & rgb){corners = Mat(nw, nh, CV_32FC2);bool patternfound = findChessboardCorners(src, Size(nw, nh), corners, CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_NORMALIZE_IMAGE);if (patternfound){cornerSubPix(src, corners, Size(11, 11), Size(-1, -1),TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 30, 0.1));}cvtColor(src, rgb, CV_GRAY2BGR);drawChessboardCorners(rgb, Size(nw, nh), corners, patternfound);return patternfound;
}

2.4 CalibrateFromPoints

2.4.1 目标

根据之前提取的棋盘格点信息, 标定相机的内外参数

2.4.2 基本思路

本质上, 这个方法就是调用张正友的标定方法 , 在opencv 中通过 calibrateCamera 函数 进行实现

2.4.3 核心代码

void Calibration(vector<vector<Point3f>> & points3d, vector<vector<Point2f>> & points2d, Size imgSize, Mat & intriniscMat, Mat & distortionMat, vector<Mat> & rotateMat, vector<Mat> & transMat){calibrateCamera(points3d, points2d, imgSize, intriniscMat, distortionMat, rotateMat, transMat, 0);
}

2.5 VideoRecorder

2.5.1 目标

录制一段带有待检测目标的视频, 这部分类似 imageSampler 不再多说了

2.6 GetFingerPos

2.6.1 目标

检测视频中的目标物体的信息

2.6.2 基本思想

通过图像处理手段检测图像中的手的信息, 取检测到的手的最上方部分作为监测点

2.6.3 检测效果

2.6.4 核心代码

这里dealwithImg 其实已经找到了手的区域, 所以其实没有必要在 FindTarget 中使用 BFS 进行遍历, 这部分可以后续做优化

void dealWithImg(Mat & src){Mat src_YCrCb;cvtColor(src, src_YCrCb, CV_RGB2YCrCb);                     // 转化到 YCRCB 空间处理vector<Mat> channels;split(src_YCrCb, channels);Mat target = channels[2];threshold(target, target, 0, 255, CV_THRESH_OTSU);              // 二值化处理Mat element = getStructuringElement(MORPH_RECT, Size(5, 5));   // 开运算去除噪点morphologyEx(target, target, MORPH_OPEN, element);vector<vector<Point>> contours;                                 // 利用最大范围查找手臂findContours(target, contours, CV_RETR_EXTERNAL, CHAIN_APPROX_NONE);double mymax = 0;vector<Point> max_contours;for (int i = 0; i < contours.size(); i++){double area = contourArea(contours[i]);if (area > mymax){mymax = area;max_contours = contours[i];}}vector<vector<Point>> final_cont;final_cont.push_back(max_contours);Mat target1 = Mat::zeros(src.rows, src.cols, CV_8U);drawContours(target1, final_cont, -1, Scalar(255), CV_FILLED);src = target1;
}/*!* \fn 通过BFS 遍历二值化图像, 找到他们的连通域, 选出面积大于指定阈值的区域块, 并以最大的作为这个图像的区域块*          本质上是查找 图像中的最大图形的最小外包轮廓*   \brief *   \param *   \return */
void FindTarget(Mat img, const int area_threshold, Target & targets){Target tar;for (int h = 0; h < img.rows; h++){for (int w = 0; w < img.cols; w++){if (img.at<uchar>(h, w) == 255){Target target;target.top = h;target.bottom = h;target.left = w;target.right = w;queue<Point> points;points.push(Point(w, h));img.at<uchar>(h, w) = 0;//find target with breadth iteration  BFSwhile (!points.empty()){target.area++;Point p = points.front();points.pop();if (p.x > 0 && img.at<uchar>(p.y, p.x - 1) == 255){//leftimg.at<uchar>(p.y, p.x - 1) = 0;points.push(Point(p.x - 1, p.y));if (target.left > p.x - 1){target.left = p.x - 1;}}if (p.y + 1 < img.rows && img.at<uchar>(p.y + 1, p.x) == 255){//bottomimg.at<uchar>(p.y + 1, p.x) = 0;points.push(Point(p.x, p.y + 1));if (target.bottom < p.y + 1){target.bottom = p.y + 1;}}if (p.x + 1 < img.cols && img.at<uchar>(p.y, p.x + 1) == 255){//rightimg.at<uchar>(p.y, p.x + 1) = 0;points.push(Point(p.x + 1, p.y));if (target.right < p.x + 1){target.right = p.x + 1;}}if (p.y > 0 && img.cols && img.at<uchar>(p.y - 1, p.x) == 255){//topimg.at<uchar>(p.y - 1, p.x) = 0;points.push(Point(p.x, p.y - 1));if (target.top > p.y - 1){target.top = p.y - 1;}}}if (target.area > area_threshold){if (target.area > tar.area){tar = target;}}}}}targets = tar;
}void playVideo(int frameCount, vector<VideoCapture> & caps, vector<vector<Point2d>> & pointsMat){pointsMat = vector<vector<Point2d>>(2);int frameCounter = 0;bool isRun = true;vector<Mat> imgs(2);while (frameCounter < frameCount){if (isRun && getCameraFrame(caps[0], imgs[0]) && getCameraFrame(caps[1], imgs[1])){imshow("camera1", imgs[0]);imshow("camera2", imgs[1]);for (int i = 0; i < 2; i++){dealWithImg(imgs[i]);Target target;// find target, 会清空原始数据FindTarget(imgs[i].clone(), 4000, target);Point2d point(-1.0, -1.0);if (target.width() >= 0){int left = -1;int right = -1;// 查找在 target.bottom 处 左右短线的中值for (int w = target.left; w <= target.right; w++){if (imgs[i].at<uchar>(target.bottom, w) == 255){if (left < 0)left = w;elseright = w;}}point.y = target.bottom;if (right > 0){point.x = (right + left) >> 1;}                   }   pointsMat[i].push_back(point);}frameCounter++;}imshow("binary1", imgs[0]);imshow("binary2", imgs[1]);int key = cvWaitKey(3);if (key == ' '){isRun = !isRun;}else if (key == 27){break;}}
}

2.7 Show3dPoints

2.7.1 目标

根据已经标定出来的相机的内外参数, 重构手指的三维点坐标, 并显示

2.7.2 核心思想

使用线性三角形法进行重构

2.7.3 效果

由于opengl 绘图不太熟悉, 我们直接将数据导出到matlab 进行绘图

虽然尺度跟我们的预期差别有些大, 但是手指的运动轨迹基本是正确的

2.7.4 核心代码

需要注意的是, 这里采用的坐标系是建立在第一幅图下面的, (仅使用了 rotate0, translate0)
实际中, 为了追求精度, 我们应该对几个工位得到他们之间的 rtx 关系, 然后优化得到他们在左相机坐标系中的物点坐标, 这样的结果才会精确些, 但是由于我们的图像处理部分的精度本来就不高, 所以这部分也没有这个必要了

void reconstruct3Dpoint(Mat matl, Mat matr, Point2d left, Point2d right, Point3d & point){Mat A(4, 4, CV_64F);Mat pl1 = matl.row(0);Mat pl2 = matl.row(1);Mat pl3 = matl.row(2);Mat pr1 = matr.row(0);Mat pr2 = matr.row(1);Mat pr3 = matr.row(2);double xl = left.x, yl = left.y;double xr = right.x, yr = right.y;A.row(0) = xl * pl3 - pl1;A.row(1) = yl * pl3 - pl2;A.row(2) = xr * pr3 - pr1;A.row(3) = yr * pr3 - pr2;Mat res;SVD::solveZ(A, res);point.x = res.at<double>(0, 0) / res.at<double>(3, 0);point.y = res.at<double>(1, 0) / res.at<double>(3, 0);point.z = res.at<double>(2, 0) / res.at<double>(3, 0);
}void getPerpectiveProjectionMat(vector<Mat> & mats){mats = vector<Mat>(2);vector<string> names{ "../data/images/coorfile0.ifl-parameters.yml", "../data/images/coorfile1.ifl-parameters.yml" };for (int i = 0; i < 2; i++){FileStorage fs(names[i], CV_STORAGE_READ);if (!fs.isOpened()){throw exception("无法打开相应的棋盘格点文件");}Mat r, t, intri, rr;fs["rotate0"] >> r;fs["translate0"] >> t;fs["intrinsic"] >> intri;Rodrigues(r, rr);Mat m;hconcat(rr, t, m);m = intri * m;mats[i] = m;fs.release();}
}bool saveToText(vector<Point3d> & data, string fileName){ofstream output(fileName);for (auto item : data){output << setw(20) << fixed << setprecision(15) << item.x << "\t" <<setw(20) << fixed << setprecision(15) << item.y << "\t" <<setw(20) << fixed << setprecision(15) << item.z << endl;}return true;
}void get3dPoints(vector<Point3d> & points, string & yml1, string & yml2){vector<Mat> mats;getPerpectiveProjectionMat(mats);//read two 2d points sequencesvector<vector<Point2d>> fingers(2);vector<string> fsname{ yml1, yml2 };for (int i = 0; i < 2; i++){FileStorage fs(fsname[i], CV_STORAGE_READ);fs["fingertip"] >> fingers[i];fs.release();}points.clear();//reconstructvector<Point2d> lefts, rights;for (int i = 0; i < fingers[0].size(); i++){Point2d left = fingers[0][i];Point2d right = fingers[1][i];Point3d point;if (left.x > 0 && left.y > 0 && right.x > 0 && right.y > 0){reconstruct3Dpoint(mats[0], mats[1], left, right, point);points.push_back(point);lefts.push_back(left);rights.push_back(right);}}auto res = ComputeDis(points, mats[0], mats[1], lefts, rights);saveToText(points, "3dpoints.txt");
}

2.8 重构误差

我们通过将计算得到的物点数据进行反投得到反投误差

可以发现, 这个反投误差还是比较大的, 初步分析造成这个现象的原因有:
1. 两个相机之间的基线距离太近
2. 图像处理精度太低
3. 随机的找了第一幅图作为我们的坐标系, 尺度的物理意义不明确
4. etc
这些需要后续进行进一步的研究

3. 涉及的一些参考资料

  1. 基于双目视觉和三维重构的三维书写系统 http://blog.csdn.net/onezeros/article/details/6110838
  2. error LNK2026: 模块对于 SAFESEH 映像是不安全的 http://blog.csdn.net/x356982611/article/details/48825813
  3. 关于VideoWriter保存视频的格式问题 http://www.opencv.org.cn/forum.php?mod=viewthread&tid=32589
  4. 在OpenCV中用cvCalibrateCamera2进行相机标定 (强烈推荐)http://blog.csdn.net/zhubo22/article/details/8874217
  5. Camera Calibration 相机标定:Opencv应用方法 http://blog.csdn.net/yhl_leo/article/details/49427383
  6. opencv中标定函数calibrateCamera http://blog.csdn.net/ychl87/article/details/11473593

[复现笔记]基于双目视觉和三维重构的三维书写系统相关推荐

  1. 基于Android的红外测温设计,基于Android的红外三维重构移动APP设计与实现

    摘要: 物体的冷热程度主要依据温度来衡量,在科学进步的过程中也研制出很多测量物体温度的设备,非制冷型红外热成像仪就是应用最为广泛的一种.非制冷型红外热像仪是一种快速测量,非接触式的测温方法,而且成本非 ...

  2. 双目立体视觉源代码 双目立体视觉匹配程序 双目视觉3d成像(三维重构图像处理) 基于双目视觉的深度计算和三维重建 opencv写的双目视觉摄像机标定和三维重建代码

    双目视觉/双目标定源码/图片集标定匹配三维重建坐标计算OpenCV 1.双目立体视觉源代码(包括标定,匹配,三维重建) 2.双目视觉实验图片集(双目立体视觉中使用的标准实验图,适合初学者进 行实验使用 ...

  3. 双目视觉三维重构(一)————简介

    CSDN中关于立三维重构的介绍层出不穷,CNKI中也有各类综述对三维重构进行总结,撰写这篇博客仅作为本人对该类博客.论文的总结学习,加深自身学习的印象.作为学习的笔记.如有错误的地方,欢迎指正. 1. ...

  4. 双目三维重建_【光电视界】简单介绍双目视觉三维重构

    今日光电        有人说,20世纪是电的世纪,21世纪是光的世纪:知光解电,再小的个体都可以被赋能.欢迎来到今日光电! ----与智者为伍 为创新赋能---- 1.三维重构 1.1.三维重构到底 ...

  5. 包裹点云位姿估计_基于点云位姿平均的非合作目标三维重构

    基于点云位姿平均的非合作目标三维重构 李宜鹏 ; 解永春 [期刊名称] <空间控制技术与应用> [年 ( 卷 ), 期] 2020(046)001 [摘要] 针对在轨非合作目标 , 提出一 ...

  6. 3D视觉笔记(1) - 双目视觉三维测量原理

    中心投影模型(针孔相机模型) 在之前的笔记中,有讨论过针孔相机的模型和世界坐标系统的点如何投影到图像坐标系中.参考如下两篇笔记: 几何角度理解相机成像过程_亦枫Leonlew的博客-CSDN博客本笔记 ...

  7. 三维重构学习笔记(3):坚实的后盾 OpenCV3

    三维重构学习笔记(3):坚实的后盾+OpenCV3 前面两篇笔记分别记录了关于三维重构中,有关相机标定.SFM流程的问题.除了公式的推倒和理解,仿真时始终仰仗OpenCV3大法,为了以后学习使用方便, ...

  8. 【基于侧扫声呐和SFS方法的地形三维重构】(一)增益补偿和斜距校正

    本文主要参考文献如下 [1]王杰英. 侧扫声呐图像的三维重构[D].浙江大学,2018. \qquad这里先向学长表示感谢! 本文主要讲述了侧扫声呐原始图像需要进行的一些信号处理与图像处理步骤(增益补 ...

  9. 三维重构学习笔记(4):坚实的后盾OpenCV(ORB)

    ORB特征提取.匹配 ORB(Oriented FAST and Rotated BRIEF)是一种局部不变特征,从名字可以看出ORB具有FAST和旋转不变的特性.相比于经典的SIFT.SURF,OR ...

最新文章

  1. 堪称神级的 Java 技术栈手册火了!
  2. java架构师,必须掌握的几点技术?
  3. python numpy筛选 总数
  4. 作为JavaScript开发人员,这些必备的VS Code插件你都用过吗?
  5. 生产者/消费者模式(阻塞队列)
  6. 25.C++:最通俗的讲解,什么是面向过程?什么是面向对象?
  7. 安全数据分析理念的变化
  8. 监控路由器虚拟服务器,远程监控路由器虚拟服务器设置
  9. SpringBoot 配置文件 application.properties(二)
  10. BCC异或校验 Linux C
  11. [Leetcode][第557题][JAVA][反转字符串中的单词 III][遍历][String函数]
  12. usbserialcontroller驱动安装不了_win10-有NVIDIA独显提示未安装控制面板的离线安装方式...
  13. pl sql代码提示手动提示设置
  14. [转]80后偷偷“老了”的八大表现
  15. php自动安装myqsl,php – 在自制的小牛上安装MySQL麻烦
  16. 她不讲武德,北航博士竟然把60年来的文本分类综述都整理了!!!
  17. SD卡无法格式化怎么办的解决方法
  18. 刑法中关于计算机犯罪的定义,界定计算机犯罪概念的原则
  19. speedoffice(Excel)表格的外框线怎么设置?
  20. 磁盘/分区克隆:如何将硬盘数据快速迁移至新的硬盘?

热门文章

  1. win7nbsp;下玩魔兽争霸的解决办法
  2. 数据库添加外键报1061错误
  3. JavaScript 面试知识点总结
  4. 项目打包打的是什么包_人是靠什么说话的|早安打工人是什么梗?打工人表情包分享...
  5. 修改UIImage大小的正确姿势
  6. C语言中a++和++a的区别
  7. 《动物庄园》读后感210407
  8. 如何让计算机断开网络连接网络设置,拨号网络怎么设置连接和断开
  9. 时间块青春版android版,时间块青春版与标准版
  10. [增强学习][Reinforcement Learning]学习笔记与回顾-2-马尔可夫决策过程MDP