相机与模型

相机与图像

一、相机模型

1. 针孔相机模型

1)内参

来源于视觉slam十四讲

O − x − y − z O − x − y − z Oxyz 为相机坐标系,习惯上我们让 z z z轴指向相机前方, x x x 向右, y y y 向下。 O O O 为摄像机的光心,也是针孔模型中的针孔。现实世界的空间点 P P P,经过小孔 O O O 投影之后,落在物理成像平面 O ′ − x ′ − y ′ O^{\prime}− x^{\prime} − y^{\prime} Oxy 上,成像点为 P ′ P^{\prime} P。设 P P P 的坐标为 [ X , Y , Z ] T [X, Y, Z]^T [X,Y,Z]T , P ′ P^{\prime} P [ X ′ , Y ′ , Z ′ ] T [X^{\prime} , Y^{\prime} , Z^{\prime} ]^T [X,Y,Z]T ,并且设物理成像平面到小孔的距离为 f f f(焦距)。那么,根据三角形相似关系,有:
Z f = X X ′ = Y Y ′ \frac{Z}{f} = \frac{X}{X^{\prime}} = \frac{Y}{Y^{\prime}} fZ=XX=YY
那么 ,整理后
X ′ = f X Z Y ′ = f Y Z X^{\prime} = f\frac{X}{Z} \\ Y^{\prime} = f\frac{Y}{Z} \\ X=fZXY=fZY
我们得到了相机坐标系物理成像坐标系之间的关系,但为了描述像素与相机之间的关系,还需要在在物理成像平面上固定一个像素平面 o − u − v o − u − v ouv。我们在像素平面得到了 P ′ P^{\prime} P 的像素坐标: [ u , v ] T [u, v]^T [u,v]T

像素坐标系通常的定义方式是:原点 o ′ o^{\prime} o 位于图像的左上角, u u u 轴向右与 x x x 轴平行, v v v轴向下与 y y y轴平行。像素坐标系与成像平面之间,相差了一个缩放和一个原点的平移。我们设像素坐标在 u u u轴上缩放了 α 倍,在 v v v上缩放了 β 倍。同时,原点平移了 [ c x , c y ] T [c_ x , c_ y ]^ T [cx,cy]T 。那么, P ′ P^{\prime} P 的坐标与像素坐标$ [u, v]^ T$的关系为:
u = α X ′ + c x v = β Y ′ + c y u= \alpha X^{\prime} + c_x \\ v = \beta Y^{\prime} + c_y u=αX+cxv=βY+cy
α f \alpha f αf 合并成 f x f_x fx β f \beta f βf合并成 f y f_y fy ,并将 X ′ , Y ′ X^{\prime},Y^{\prime} X,Y带入上式,可得:
u = f x X Z + c x v = f y Y Z + c y u= f_x \frac{X}{Z} + c_x \\ v = f_y \frac{Y}{Z}+ c_y u=fxZX+cxv=fyZY+cy
其中, f f f 的单位为米, α , β α, β α,β 的单位为像素每米,所以 f x , f y f_ x , f_ y fx,fy 的单位为像素。把该式写成矩阵形式,会更加简洁,不过左侧需要用到齐次坐标:
[ u v 1 ] = 1 Z [ f x 0 c x 0 f y c x 0 0 1 ] [ X Y Z ] = 1 Z K P c \left[\begin{array}{ccc}u\\v\\1 \end{array}\right] =\frac{1}{Z} \left[\begin{array}{ccc}f_x & 0 & c_x\\0 & f_y & c_x\\0 & 0 & 1 \end{array}\right] \left[\begin{array}{ccc}X\\Y\\Z \end{array}\right] = \frac{1}{Z}KP_c uv1 =Z1 fx000fy0cxcx1 XYZ =Z1KPc
该式中,我们把中间的量组成的矩阵称为相机的内参数矩阵(Camera Intrinsics)K

通常,相机的内参在出厂之后是固定的,不会在使用过程中发生变化,而有时需要自己去标定内参,比如说张正友标定法标定相机内参。

2)外参

我们使用的是 P P P 在相机坐标系下的坐标。由于相机在运动,所以 P P P 的相机坐标应该是它的世界坐标(记为 P w P_ w Pw ),
根据相机的当前位姿,变换到相机坐标系下的结果。相机的位姿有它的旋转矩阵和平移向量来描述
Z P u v = Z [ u v 1 ] = K ( R P w + t ) = K T P w ZP_{u v} = Z \left[\begin{array}{ccc}u\\v\\1 \end{array}\right] = K(RP_w + t) = KTP_w ZPuv=Z uv1 =K(RPw+t)=KTPw
相机的位姿 R, t 又称为相机的外参数(Camera Extrinsics)。相比于不变的内参,外参会随着相机运动发生改变,同时也是 SLAM中待估计的目标,代表着机器人的轨迹。

最后,我们小结一下单目相机的成像过程:

  1. 首先,世界坐标系下有一个固定的点 P P P ,世界坐标为 P w P w Pw ;
  2. 由于相机在运动,它的运动由 R , t R, t R,t或变换矩阵 T ∈ S E ( 3 ) T ∈ SE(3) TSE(3) 描述。P 的相机坐标为: P ~ c = R P w + t P̃ _c = RP _w + t P~c=RPw+t
  3. 这时的 P ~ c P̃_ c P~c仍有 X , Y , Z X, Y, Z X,Y,Z 三个量,把它们投影到归一化平面 Z = 1 Z = 1 Z=1 上,得到 P P P的归
    一化相机坐标 : P c = [ X / Z , Y / Z , 1 ] T :P_ c = [X/Z, Y /Z, 1]^ T :Pc=[X/Z,Y/Z,1]T
  4. 最后,P 的归一化坐标经过内参后,对应到它的像素坐标: P u v = K P c P _{uv} = KP _c Puv=KPc

综上所述,我们一共谈到了四种坐标:世界、相机、归一化相机和像素坐标。四种坐标系的变换反映了整个成像的过程。

2. 双目相机模型

双目相机的原理是通过同步采集左右相机的图像,计算图像间视差,来估计每一个像素的深度。双目相机一般由左眼和右眼两个水平放置的相机组成。我们可以把两个相机都看作针孔相机。它们是水平放置的,意味两个相机的光圈中心都位于 x 轴上。它们的距离称为双目相机的基线(Baseline, 记作 b),是双目相机的重要参数。

图片来源于视觉slam十四讲

O L , O R O _L , O _R OL,OR 为左右光圈中心,蓝色框为成像平面, f f f 为焦距。 u L u_ L uL u R u _R uR 为成像平面的坐标。请注意按照图中坐标定义, u R u _R uR 应该是负数,所以图中标出的距离为 − u R −u_ R uR

现在,考虑一个空间点 P P P ,它在左眼和右眼各成一像,记作 P L , P R P _L , P _R PL,PR 。由于相机基线的存在,这两个成像位置是不同的。理想情况下,由于左右相机只有在 x 轴上有位移,因此 P P P 的像也只在 x x x 轴(对应图像的 u u u 轴)上有差异。我们记它在左侧的坐标为 u L u _L uL ,右侧坐标为 u R u _R uR 。那么,它们的几何关系如上图所示。根据三角形 P − P L − P R 和 P − O L − O R P − P_ L − P_ R 和 P − O _L − O _R PPLPRPOLOR的相似关系,有
z − f z = b − u L + u R b \frac{z-f}{z}= \frac{b-u_L+u_R}{b} \\ zzf=bbuL+uR
z = f b d ,    d = u L − u R z = \frac{fb}{d} , \space \space d= u_L-u_R z=dfb,  d=uLuR

3. 图像操作

我们知道,一张图像是有像素组成的,在一张灰度图中,每个像素位置 ( x , y ) (x, y) (x,y)
应到一个灰度值 I , I, I,所以一张宽度为 w,高度为 h 的图像,数学形式可以记成一个矩阵:
I ( x , y ) ∈ R w × h . I(x, y) ∈ R ^{w×h }. I(x,y)Rw×h.我们通过OpenCV库来熟悉图像的读取,存储以及遍历操作

1)图像基本操作
#include <iostream>
#include "opencv2/core.hpp"
#include "opencv2/highgui.hpp"

int main(int argc, char** argv) 
{
    cv::Mat grayim(600, 800, CV_8UC1);
    cv::Mat colorim(600, 800, CV_8UC3);

    // 遍历所有像素,并设置像素值
    for (int i = 0; i < grayim.rows; i++) {   
        // 获取第i行首像素指针
        uchar *p = grayim.ptr<uchar>(i);
        for (int j = 0; j < grayim.cols; j++) {   
            // 对第i行的每个像素操作
            p[j] = (i+j)%255;
        }  
    }
    
    //遍历所有像素,并设置像素值
    //for( int i = 0; i < grayim.rows; ++i)
    //	for( int j = 0; j < grayim.cols; ++j )
    //		grayim.at<uchar>(i,j) = (i+j)%255;
    cv::imshow("gray",grayim);
    cv::waitKey(1000);

    // 遍历所有像素,并设置像素值
    for (int i = 0; i < colorim.rows; i++) {
        for (int j = 0; j < colorim.cols; j++) {
            cv::Vec3b pixel;
            pixel[0] = i%255;
            pixel[1] = j%255;
            pixel[3] = 0;
            colorim.at<cv::Vec3b>(i,j) = pixel;
        }
    }
    cv::imshow("colorim",colorim);
    cv::waitKey(1000);

    // 读取图片
    cv::Mat image;
    image = cv::imread(argv[1]);
    cv::imshow("image",image);
    cv::waitKey(0);
    return 0;
}
2) 双目图像生成点云图

通过双目视觉的左右图像,计算图像对应的视差图,然后计算各像素在相机坐标系下的坐标,他们将构成点云。

Pangolin显示点云

#include <iostream>
#include <string>
#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"
#include <Eigen/Core>

#include <pangolin/pangolin.h>
#include <unistd.h>

std::string left_file = "/home/tgc/练习/opencv/save/left.png";
std::string right_file = "/home/tgc/练习/opencv/save/right.png";

// 在pangolin中画图,已写好,无需调整
void showPointCloud(
    const std::vector<Eigen::Vector4d, Eigen::aligned_allocator<Eigen::Vector4d> > &pointcloud);

int main(int argc, char** argv)
{   


    // 内参
    double fx = 718.856, fy = 718.856, cx = 607.1928, cy = 185.2157;
    // 基线
    double b = 0.573;
     //焦距
    double f = 0.04;    


      // 读取图像
    cv::Mat left = cv::imread(left_file, 0);
    cv::Mat right = cv::imread(right_file, 0);
    cv::Ptr<cv::StereoSGBM> sgbm = cv::StereoSGBM::create(
        0, 96, 9, 8 * 9 * 9, 32 * 9 * 9, 1, 63, 10, 100, 32);    // 神奇的参数
    cv::Mat disparity_sgbm, disparity;
    sgbm->compute(left, right, disparity_sgbm);
    disparity_sgbm.convertTo(disparity, CV_32F, 1.0 / 16.0f);

    // 生成点云
    std::vector<Eigen::Vector4d, Eigen::aligned_allocator<Eigen::Vector4d>> pointcloud;

    // 如果你的机器慢,请把后面的v++和u++改成v+=2, u+=2
    for (int v = 0; v < left.rows; v++)
        for (int u = 0; u < left.cols; u++) {
            if (disparity.at<float>(v, u) <= 0.0 || disparity.at<float>(v, u) >= 96.0) continue;

            Eigen::Vector4d point(0, 0, 0, left.at<uchar>(v, u) / 255.0); // 前三维为xyz,第四维为颜色

            // 根据双目模型计算 point 的位置
            double x = (u - cx) / fx;
            double y = (v - cy) / fy;
            // double depth = fx * b / (disparity.at<uchar>(v, u));
            double depth = f * b * 1000 /(disparity.at<uchar>(v, u));

            point[0] = x * depth;
            point[1] = y * depth;
            point[2] = depth;

            pointcloud.push_back(point);
        }

    // cv::imshow("disparity", disparity / 96.0);
    // cv::waitKey(0);
    // 画出点云
    showPointCloud(pointcloud);

    return 0;
}


void showPointCloud(const std::vector<Eigen::Vector4d, Eigen::aligned_allocator<Eigen::Vector4d>> &pointcloud) {

    if (pointcloud.empty()) {
        std::cerr << "Point cloud is empty!" << std::endl;
        return;
    }

    pangolin::CreateWindowAndBind("Point Cloud Viewer", 1024, 768);
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    pangolin::OpenGlRenderState s_cam(
        pangolin::ProjectionMatrix(1024, 768, 500, 500, 512, 389, 0.1, 1000),
        pangolin::ModelViewLookAt(0, -0.1, -1.8, 0, 0, 0, 0.0, -1.0, 0.0)
    );

    pangolin::View &d_cam = pangolin::CreateDisplay()
        .SetBounds(0.0, 1.0, pangolin::Attach::Pix(175), 1.0, -1024.0f / 768.0f)
        .SetHandler(new pangolin::Handler3D(s_cam));

    while (pangolin::ShouldQuit() == false) {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        d_cam.Activate(s_cam);
        glClearColor(1.0f, 1.0f, 1.0f, 1.0f);

        glPointSize(2);
        glBegin(GL_POINTS);
        for (auto &p: pointcloud) {
            glColor3f(p[3], p[3], p[3]);
            glVertex3d(p[0], p[1], p[2]);
        }
        glEnd();
        pangolin::FinishFrame();
        usleep(5000);   // sleep 5 ms
    }
    return;
}

PCL显示点云

#include <iostream>
#include <string>
#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"
#include <Eigen/Core>
#include <pcl-1.9/pcl/io/pcd_io.h>
#include <pcl-1.9/pcl/point_types.h>
#include <pcl-1.9/pcl/point_cloud.h>   
  
std::string left_file = "/home/tgc/练习/opencv/save/left.png";
std::string right_file = "/home/tgc/练习/opencv/save/right.png";  


int main(int argc, char** argv)
{   


    // 内参
    double fx = 718.856, fy = 718.856, cx = 607.1928, cy = 185.2157;
    // 基线
    double b = 0.573;
     //焦距
    double f = 0.04;    

  	typedef pcl::PointXYZRGB PointT;
    typedef pcl::PointCloud<PointT> PointCloud;
    // 新建一个点云
    PointCloud::Ptr pointCloud( new PointCloud );

    cv::Mat left = cv::imread(left_file, 0);
    cv::Mat right = cv::imread(right_file, 0);
    cv::Ptr<cv::StereoSGBM> sgbm = cv::StereoSGBM::create(
        0, 96, 9, 8 * 9 * 9, 32 * 9 * 9, 1, 63, 10, 100, 32);
    cv::Mat disparity_sgbm, disparity;
    sgbm->compute(left, right, disparity_sgbm);
    disparity_sgbm.convertTo(disparity, CV_32F, 1.0 / 16.0f);

    for (int v = 0; v < left.rows; v++) {
        for (int u = 0; u < left.cols; u++) {
            if (disparity.at<float>(v, u) <= 0.0 || disparity.at<float>(v, u) >= 96.0) continue;

            Eigen::Vector4d point(0, 0, 0, left.at<uchar>(v, u) / 255.0); // 前三维为xyz,第四维为颜色
            // Eigen::Vector3d point; // 前三维为xyz,第四维为颜色

            // 根据双目模型计算 point 的位置
            double x = (u - cx) / fx;
            double y = (v - cy) / fy;
            // double depth = fx * b / (disparity.at<uchar>(v, u));
            double depth = f * b * 1000 /(disparity.at<uchar>(v, u));

            point[0] = x * depth;
            point[1] = y * depth;
            point[2] = depth;

            PointT p;
            p.x = point[0];
            p.y = point[1];
            p.z = point[2];
            p.b = left.data[ v*left.step+u*left.channels() ];
            p.g = left.data[ v*left.step+u*left.channels() +1 ];
            p.r = left.data[ v*left.step+u*left.channels() +2 ];

            pointCloud->points.push_back(p);
            
        }
    }

    pointCloud->is_dense = false;
    std::cout << "point number is :" << pointCloud->size() << std::endl;
    pcl::io::savePCDFileBinary("map.pcd", *pointCloud);
    return 0;
}

参考连接 : 视觉slam十四讲

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值