主要为了介绍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数据完成