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

主要为了介绍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数据完成
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容