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

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  BFS
                while (!points.empty()){
                    target.area++;
                    Point p = points.front();
                    points.pop();

                    if (p.x > 0 && img.at<uchar>(p.y, p.x - 1) == 255){//left
                        img.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){//bottom
                        img.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){//right
                        img.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){//top
                        img.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;
                            else
                                right = 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 sequences
    vector<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();

    //reconstruct
    vector<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
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值