美文网首页
关于openMVG源码(三)SfM描述文件/相机内参分析

关于openMVG源码(三)SfM描述文件/相机内参分析

作者: 神奇的考拉 | 来源:发表于2024-01-16 14:42 被阅读0次

    主要为了介绍openMVG执行sfm计算的开始步骤: openMVG_main_SfMInit_ImageListing, 来完成对输入图片数据集的描述,进行内参分析,输出SfM_Data文件,包括视图信息和相机内参数
    一、输入参数

    • i:输入image集路径
    • d:主要提供多种镜头的配置参数,可以直接使用openMVG提供的
    • o:输出的sfm_data文件路径
      需注意:相机内参数是必须提供的,内参矩阵K和焦距是需要提供其中之一的(两者不能同时存在的);
      1、相机内参数
      当存在相机内参数矩阵时通过checkIntrinsicStringValidity(...)检查是否包含9个参数,并从这9个参数中获得焦距、主点坐标;
      当存在焦距时则从图像的文件头中读取图像的尺寸,由此计算相机的主点坐标(长、宽的一半)
      若是不存在相机内参数也不存在焦距,会通过尝试exif工具从图像中读取焦距信息,若无法获取,则直接退出程序;
      2、相机模型
      主要是根据输入相机模型,选择不同的相机对象,不同的对象对应的相机内参数不同,
      其中包括:针孔相机(不包含畸变/单个径向畸变参数/三个镜像畸变/三个径向畸变+两个枕型畸变),鱼眼相机和广角相机
      3、GPS先验
      当使用exif工具来从图像中读取gps信息,并在checkGPS(...)中进行坐标值转换,将gps的经纬度信息转换为地球坐标系下的(x,y,z),默认提供的坐标系:
    • ECEF(Earth-Centered, Earth-Fixed, 地心地固坐标系)
    • UTM(Universal Transverse Mercator Grid System,通用横墨卡托格网系统)
      默认情况下GPS各个轴的权重=1
      4、在读完所有参数配置以后,再通过GroupSharedIntrinsics(...)将同源的相机图像进行组合,即同一个相机内参数矩阵的图像放在一起,加快特征匹配和运动估计的速度。
      5、将上述的所有信息一同导出到json文件

    二、验证脚本
    -d ../openMVG/src/openMVG/exif/sensor_width_database/sensor_width_camera_database.txt

    -i ../openMVG_Build/image_datasets/ImageDataset_SceauxCastle/images

    -o ../openMVG_Build/image_datasets/ImageDataset_SceauxCastle/matches

    三、源码深入
    1、参数详情

    • -i : 提供的重建三维的images
    • -d:提供不同相机型号与其关联的传感器宽度集合对; 相机型号;传感器宽度
    • -o:sfm_data.json文件输出的目录
    • -f: 焦距(像素)与后面的k,两者只能提供其一
    • -k:相机内参矩阵与前面的f,两者只能提供其一;格式类似f;0;ppx;0;f;ppy;0;0;1
    • -c:用户使用的相机模型
    • -g: 是否将提供的imags中相同特性的图片组合在一起处理;默认是true合并一起处理(同一个相机内参数矩阵的图像放在一起,加快特征匹配和运动估计的速度)
    • -P: 使用位姿
    • -W: 预验权重
    • -m:将gps坐标进行转换方式
      1、当提供相机内参矩阵K:使用如下的方法来验证数据有效性
      checkIntrinsicStringValidity(sKmatrix, #相机内参矩阵K
      focal, # 焦点
      ppx, # 图片中心x
      ppy) # 图片中心y
      要求相机内参矩阵K必须是9个内容,并用";"分割; 另外要验证这个9个内容的有效性(不能使用0.0,尤其是焦点f,中心点x,y的值)
      2、确保焦距和相机内参矩阵不能同时提供
      sKmatrix.size() > 0 && focal_pixels != -1.0
      3、读取相机传感器宽度与类型对集:
      循环读取sensor_width_camera_database.txt内容,输出std::vector<Datasheet>: <model, sensor_size>
      parseDatabase( sfileDatabase, # 默认的sensor_width_camera_database.txt
      vec_database ) # std::vector<Datasheet>: <model, sensor_size>
      4、是否使用GPS位姿:由于需要对三维重建的图像进行俩俩匹配,存在计算量大的问题,则可以基于GPS信息搜索其邻域图像,来完成构建图像对,整个过程会节省大量的计算资源
      checkPriorWeightsString(
      sPriorWeights # 指定每个坐标轴的权重,默认都是1.0
      )

    通过使用";"将x;y;z各个坐标轴的权重提取出来,构成std::pair<bool, Vec3>对: (true, 权重值)
    5、构建一个带Views和相机配置的空场景

    • SfM结构
      struct SfM_Data{
      Views views; /// 储存影像物理性质、索引号等基本信息
      Poses poses; /// 储存影像外参数,包括旋转矩阵、平移矩阵
      Intrinsics intrinsics; /// 储存影像内参数,支持多组不同内参数
      Landmarks structure; /// 物方点信息,物方点坐标及其tracks
      //。。。省略其他代码
      }

    • Views : 存储View类型
      Hash_Map<IndexT, # view索引
      std::shared_ptr<View>> # 关联的View
      关于View定义:View视图通过一个字符串和视图、相机和位姿的唯一索引来定义图像
      struct View
      {
      // 当前视图关联的原始图片
      std::string s_Img_path;

      // 当前视图的id
      IndexT id_view;

      // 相机内参索引和位姿索引
      IndexT id_intrinsic, id_pose;

      // 当前vbiew关联图片的宽高
      IndexT ui_width, ui_height;
      // 。。。 省略代码。。。
      }
      关于Poses位姿
      using Poses = Hash_Map<IndexT, geometry::Pose3>; //定义姿势集合(按View::id_Pose索引)

    方向矩阵和旋转中心 Mat3 rotation_; Vec3 center_; Eigen类型
    关于Intrinsics内参矩阵
    using Intrinsics =
    Hash_Map<IndexT, std::shared_ptr<cameras::IntrinsicBase>> #相机的内参属性
    关于Landmarks
    定义由TrackId索引的地标集合,Landmark包含两个成员:
    Vec3 X; #3d点
    Observations obs; #3d点所对应于图像上的坐标的hash表,
    因为一个世界中的坐标可以被多张相机所观测到,
    Landmarks点位又分为三角测量获得的点(用于BA)和地面控制点(用于GCP)

    • 填充创建的空场景
      // # 循环输入images
      for ( std::vector<std::string>::const_iterator iter_image = vec_image.begin();
      iter_image != vec_image.end();
      ++iter_image, ++my_progress_bar )
      {
      // 得到每个image的宽、高、焦点、中心点x,y
      width = height = ppx = ppy = focal = -1.0;

      // 获取image的名字带后缀的
      const std::string sImageFilename = stlplus::create_filespec( sImageDir, *iter_image );
      const std::string sImFilenamePart = stlplus::filename_part(sImageFilename);

      // ======== 验证是否为可用的image ======== //
      // 1、验证输入的image是否是支持的格式
      if (openMVG::image::GetFormat(sImageFilename.c_str()) == openMVG::image::Unknown)
      {
      error_report_stream
      << sImFilenamePart << ": Unkown image file format." << "\n";
      continue; // image cannot be opened
      }

      // 2、是否为遮罩图像
      if (sImFilenamePart.find("mask.png") != std::string::npos
      || sImFilenamePart.find("_mask.png") != std::string::npos)
      {
      error_report_stream
      << sImFilenamePart << " is a mask image" << "\n";
      continue;
      }

      // 获取image header信息
      ImageHeader imgHeader;
      // 若是当前处理的image没有获取到header 则直接跳过,继续后续的image的处理
      if (!openMVG::image::ReadImageHeader(sImageFilename.c_str(), &imgHeader))
      continue; // image cannot be read

      // 从当前image header中获取当前图片的宽、高; 继而计算图片中心点
      width = imgHeader.width;
      height = imgHeader.height;
      ppx = width / 2.0;
      ppy = height / 2.0;

    // ======= 用户提供了焦点 ====== //

    // 1、已知校准K矩阵
    if (sKmatrix.size() > 0) // Known user calibration K matrix
    {
    //校验提供K矩阵参数内容是否有效
    if (!checkIntrinsicStringValidity(sKmatrix, focal, ppx, ppy))
    focal = -1.0; // 不满足条件时焦点=-1.0
    }
    else // 2、焦距大小
    if (focal_pixels != -1 )
    focal = focal_pixels;

    // 3、当用户提供的校准K矩阵和焦距都是无效
    // 则会读取当前图片的exif信息
    if (focal == -1)
    {
    std::unique_ptr<Exif_IO> exifReader(new Exif_IO_EasyExif);
    exifReader->open( sImageFilename );

    // 当前image exif是有效的
    const bool bHaveValidExifMetadata =
      exifReader->doesHaveExifInfo()
      && !exifReader->getModel().empty()
      && !exifReader->getBrand().empty();
    
    // 当前image的exif中提供了有效的焦距
    if (bHaveValidExifMetadata) // If image contains meta data
    {
      // 可能会存在焦距=0的情况,需要剔除
      if (exifReader->getFocal() == 0.0f)
      {
        error_report_stream
          << stlplus::basename_part(sImageFilename) << ": Focal length is missing." << "\n";
        focal = -1.0;
      }
      else
      // Create the image entry in the list file
      {
        const std::string sCamModel = exifReader->getBrand() + " " + exifReader->getModel();
    
        Datasheet datasheet;
        // 根据当前的相机型号从sensor_width_carema_database.txt数据集中获取其对应的焦距
        // 当image拍摄的相机型号及传感器宽度记录不在sensor_width_carema_database.txt,需要手动添加
        if ( getInfo( sCamModel, vec_database, datasheet ))
        {
          // The camera model was found in the database so we can compute it's approximated focal length
          // 根据当前图片的宽、高以及设备传感器sensor_size来计算焦距
          const double ccdw = datasheet.sensorSize_;
          focal = std::max ( width, height ) * exifReader->getFocal() / ccdw;
        }
        else
        {
          error_report_stream
            << stlplus::basename_part(sImageFilename)
            << "\" model \"" << sCamModel << "\" doesn't exist in the database" << "\n"
            << "Please consider add your camera model and sensor width in the database." << "\n";
        }
      }
    }
    

    }

    // 接下来就是构建View视图关联的相机内参
    // Build intrinsic parameter related to the view
    std::shared_ptr<IntrinsicBase> intrinsic;

    if (focal > 0 && ppx > 0 && ppy > 0 && width > 0 && height > 0)
    {
    // Create the desired camera type
    // 根据提供用户相机类型:不同相机型号 对应的相机内参是不同的格式
    switch (e_User_camera_model)
    {
    case PINHOLE_CAMERA: // 无畸变
    intrinsic = std::make_shared<Pinhole_Intrinsic>
    (width, height, focal, ppx, ppy);
    break;
    case PINHOLE_CAMERA_RADIAL1: // 径向畸变K1
    intrinsic = std::make_shared<Pinhole_Intrinsic_Radial_K1>
    (width, height, focal, ppx, ppy, 0.0); // setup no distortion as initial guess
    break;
    case PINHOLE_CAMERA_RADIAL3: // 径向畸变K1,K2,K3
    intrinsic = std::make_shared<Pinhole_Intrinsic_Radial_K3>
    (width, height, focal, ppx, ppy, 0.0, 0.0, 0.0); // setup no distortion as initial guess
    break;
    case PINHOLE_CAMERA_BROWN: // 径向畸变K1,K2,K3,切向畸变T1, T2
    intrinsic = std::make_shared<Pinhole_Intrinsic_Brown_T2>
    (width, height, focal, ppx, ppy, 0.0, 0.0, 0.0, 0.0, 0.0); // setup no distortion as initial guess
    break;
    case PINHOLE_CAMERA_FISHEYE: // 具有4个畸变系数的简单鱼眼畸变模型
    intrinsic = std::make_shared<Pinhole_Intrinsic_Fisheye>
    (width, height, focal, ppx, ppy, 0.0, 0.0, 0.0, 0.0); // setup no distortion as initial guess
    break;
    case CAMERA_SPHERICAL: // 球面相机
    intrinsic = std::make_shared<Intrinsic_Spherical>
    (width, height);
    break;
    default:
    OPENMVG_LOG_ERROR << "Error: unknown camera model: " << (int) e_User_camera_model;
    return EXIT_FAILURE;
    }
    }

    // Build the view corresponding to the image
    // 接着就是构建与图像对应的视图
    Vec3 pose_center;
    // 当有GPS权重就需要定义为优先旋转,并将gps转为地球坐标,其中涉及坐标系转换
    // 从当前图片exif信息中提取经度、纬度、海拔;再根据指定的gps转为其他坐标系的类型进行对应的转换
    // - ECEF(Earth-Centered, Earth-Fixed, 地心地固坐标系
    // - UTM(Universal Transverse Mercator Grid System,通用横墨卡托格网系统
    if (getGPS(sImageFilename, i_GPS_XYZ_method, pose_center) && b_Use_pose_prior)
    {
    // Views的子类,可以选择是否优先旋转,或者优先调整位置
    ViewPriors v(*iter_image, views.size(), views.size(), views.size(), width, height);

    // Add intrinsic related to the image (if any)
    // 根据前面的相机型号,得到对应的相机内参数,将其添加到sfm_data中intrinsics块中
    if (!intrinsic) // 若是没有相机内参数,则用无效字段填充
    {
      //Since the view have invalid intrinsic data
      // (export the view, with an invalid intrinsic field value)
      v.id_intrinsic = UndefinedIndexT;
    }
    else // 若是有,则直接添加
    {
      // Add the defined intrinsic to the sfm_container
      intrinsics[v.id_intrinsic] = intrinsic;
    }
    
    v.b_use_pose_center_ = true;
    v.pose_center_ = pose_center;
    
    // 使用坐标轴的权重
    // prior weights
    if (prior_w_info.first == true)
    {
      v.center_weight_ = prior_w_info.second;
    }
    
    // 将当前image的视图view添加到sfm
    // Add the view to the sfm_container
    views[v.id_view] = std::make_shared<ViewPriors>(v);
    

    }
    else // 也会存在没有GPS信息:比如在室内GPS较差,出现漂移的情况,在后期重建带来模型偏差大的问题;则会关闭GPS
    {
    View v(*iter_image, views.size(), views.size(), views.size(), width, height);

    // Add intrinsic related to the image (if any)
    if (!intrinsic)
    {
      //Since the view have invalid intrinsic data
      // (export the view, with an invalid intrinsic field value)
      v.id_intrinsic = UndefinedIndexT;
    }
    else
    {
      // Add the defined intrinsic to the sfm_container
      intrinsics[v.id_intrinsic] = intrinsic;
    }
    
    // Add the view to the sfm_container
    views[v.id_view] = std::make_shared<View>(v);
    

    }
    }
    以上即为将images集所有的图片进行出来,最终将满足要求的image的视图View、相机内参intrinsic、位姿Poses、物方点Landmarks等信息添加到SfM中,如下图
    {
    "sfm_data_version": "0.3",
    "root_path": "/Users/dalan/tests/openmvg_openmvs/video_images",
    "views": [ // 视图
    {
    "key": 59,
    "value": {
    "polymorphic_id": 1073741824,
    "ptr_wrapper": {
    "id": 2147483649,
    "data": {
    "local_path": "",
    "filename": "IMG_20240112_225353_TIMEBURST9.jpg",
    "width": 2248,
    "height": 4000,
    "id_view": 59,
    "id_intrinsic": 0,
    "id_pose": 59
    }
    }
    }
    },
    // 省略
    ],
    "intrinsics": [ // 相机内参
    {
    "key": 0,
    "value": {
    "polymorphic_id": 2147483649,
    "polymorphic_name": "pinhole_radial_k3",
    "ptr_wrapper": {
    "id": 2147483709,
    "data": {
    "width": 2248,
    "height": 4000,
    "focal_length": 2966.929157887857,
    "principal_point": [
    1124.0,
    2000.0
    ],
    "disto_k3": [
    0.0,
    0.0,
    0.0
    ]
    }
    }
    }
    }
    ],
    "extrinsics": [], // 相机外参
    "structure": [], // 结构:三维点及其二维观测
    "control_points": [] // 控制点:地标
    }

    • 将同一个相机内参数矩阵的图像放在一起,加快特征匹配和运动估计的速度:b_Group_camera_model开启时
      GroupSharedIntrinsics(SfM_Data & sfm_data)代码如下:
      // sfm中的视图集
      Views & views = sfm_data.views;
      // sfm中的相机内参集
      Intrinsics & intrinsics = sfm_data.intrinsics;

    // 主要是为了相机内参; 由于存在同源相机特性的图片可以合并处理
    // Build hash & build a set of the hash in order to maintain unique Ids
    std::set<size_t> hash_index;
    std::vector<size_t> hash_value;

    for (const auto & intrinsic_it : intrinsics)
    {
    const cameras::IntrinsicBase * intrinsicData = intrinsic_it.second.get();
    const size_t hashVal = intrinsicData->hashValue();
    hash_index.insert(hashVal);
    hash_value.push_back(hashVal);
    }

    // 更新相机内参集中的intrinsic
    // From hash_value(s) compute the new index (old to new indexing)
    Hash_Map<IndexT, IndexT> old_new_reindex;
    size_t i = 0;
    for (const auto & intrinsic_it : intrinsics)
    {
    old_new_reindex[intrinsic_it.first] = std::distance(hash_index.cbegin(), hash_index.find(hash_value[i]));
    ++i;
    }
    //--> Save only the required Intrinsics (do not need to keep all the copy)
    Intrinsics intrinsic_updated;
    for (const auto & intrinsic_it : intrinsics)
    {
    intrinsic_updated[old_new_reindex[intrinsic_it.first]] = intrinsics[intrinsic_it.first];
    }
    // Update intrinsics (keep only the necessary ones) -> swapping
    intrinsics.swap(intrinsic_updated);

    // 有必要的话更新View中关于intrinsic的索引信息
    // Update views intrinsic IDs (since some intrinsic position have changed in the map)
    for (auto & view_it: views)
    {
    View * v = view_it.second.get();
    // Update the Id only if a corresponding index exists
    if (old_new_reindex.count(v->id_intrinsic))
    v->id_intrinsic = old_new_reindex[v->id_intrinsic];
    }

    • 存储sfm数据:
      Save(
      sfm_data, # sfm元数据
      stlplus::create_filespec( sOutputDir, "sfm_data.json" ).c_str(), # 输出的文件
      ESfM_Data(VIEWS|INTRINSICS)) # 视图和相机内参;输出到sfm_data.json中具体内容视图和相机内参,也可输出其他的
      // 输出的sfm数据格式
      const std::string ext = stlplus::extension_part(filename);
      if (ext == "json")
      return Save_Cereal<cereal::JSONOutputArchive>(sfm_data, filename, flags_part);
      else if (ext == "bin")
      return Save_Cereal<cereal::PortableBinaryOutputArchive>(sfm_data, filename, flags_part);
      else if (ext == "xml")
      return Save_Cereal<cereal::XMLOutputArchive>(sfm_data, filename, flags_part);
      else if (ext == "ply")
      return Save_PLY(sfm_data, filename, flags_part);
      else if (ext == "baf") // Bundle Adjustment file
      return Save_BAF(sfm_data, filename, flags_part);
      else
      {
      OPENMVG_LOG_ERROR << "Unknown sfm_data export format: " << filename;
      }
      return false;
      至此构建SfM数据完成

    相关文章

      网友评论

          本文标题:关于openMVG源码(三)SfM描述文件/相机内参分析

          本文链接:https://www.haomeiwen.com/subject/fppkodtx.html