前言
专门列出一章文件读写,主要是因为之前的知识较为零散,想汇总起来以供参考。本文在初稿撰写时只加入了较为简单的C++标准下文件读写,后续会加入较为复杂的目录寻找等Python标准下的写作规范。
提示:以下是本篇文章正文内容,下面案例可供参考
一、C++文件读写(基于文件指针)
1.基本概念与主要使用函数
提到文件读写,较为简单并且基本够用的做法是使用一下四个函数:
(1)FILE *fp = fopen(filename.c_str(),_mode);
(2)int succeed = fscanf(FILE *fp,const string format,T *value);
(3)int succeed = fprintf(FILE *fp,const string format,T *value);
(4)fclose();
——————————
这个里面最重要的是中间的fscanf和fprintf。最初的fopen函数是打开了一个文件流。fscanf函数则是读入数据的主力,它搜索文件流中符合format形式的数据,赋值给value对应的指针,该函数遇到空格和换行就停止。fprintf函数是将value指针中存储的数据,按照format的形式,加入到输出文件流中,需要手动的添加换行符。因此,我们配合指针、数组,即可完成数据的读写。顺便说一下,笔者刚开始十分疑惑为什么一个文件读写的函数,会有一个int类型的返回值。其实这是有编程哲学的,这个函数返回的int值统计了数据被正确赋值到变量的个数,这对我们查找程序中的问题很有帮助。此外,很多函数写作的时候往往是bool类型,甚至直接就被写成void类型,因为这些函数的功能往往是对数据进行操作(一定要引用传递哦,否则会报“段错误:核心已转储”,经历过的人都懂),很难给出一个具体的返回值。对于这些函数来说,返回值的意义已经不大了,主要是为了查找问题,例如设定一个bool的函数,检查是否所有的数据都处理完毕,如果有问题的话,就返回False。
2.光束法平差文件读取使用案例
下面来看一个案例,完成对光束法平差文件的读写。该文件的基本数据结构如下:
光束法平差记录了相机、物方坐标点和投影的关系。这种文件一般有一个文件头,第一行是三个统计量,分别是相机总数、点号总数以及总参数量。接下来是每个物方点的经过某个相机投影后的像点坐标,刻画了投影关系,一行4个数据,总共nPoints行。而后是相机参数,一列9个为一组,记录了3个旋转角、3个平移量、1个焦距和两个2个变参数,共有nCameras组。最后是所有物方点的信息,一列3个为一组,分别为XYZ点,共有nPoints组。
——————————————
我们的目标是,读取这个文件,并且将相机参数、点以及参数存储进列表。
为了能够让我们的程序适应不同的BA问题(对应不同的BA数据文件),我们定义了一个BALProblem类,使用其成员变量来存储数据(这是一个非常常见的编程技巧),并在构造函数中读取文件、初始化成员变量。在这个类的最外面,设置一个检测机制的模板函数FscanOrDie(),通过其返回值判断文件读取是否有异常出现:代码如下(示例):
template<typename T>
void FscanOrDie(FILE *fptr,const char* format,T*value){
int n_read = fscanf(fptr,format,value);
if(n_read!=1){
std::cerr << "Invalid UW data file. ";
}
}
class BALProblem{
public:
BALProblem(const string filename){
FILE *fp = fopen(filename.c_str(),'r');
FscanfOrDie(fp,"%d",&m_nCameras);
FscanfOrDie(fp,"%d",&m_nPoints);
FscanfOrDie(fp,"%d",&m_nObservations);
//注意,这里一定要是指针
std::cout << "Header: " << m_nCameras
<< " " << m_nPoints
<< " " << m_nObservations;
//根据观测值数量记录相机编号和点编号列表
camera_index_ = new int[m_nObservations];
point_index_ = new int[m_nObservations];
observations_ = new double[2 * m_nObservations];
//读取数据
for(int i=0;i<m_nObservations;++i){
FscanfOrDie(fp,"%d",&camera_index_[i]);//或camera_index_+i
FscanfOrDie(fp,"%d",&point_index_[i]);
for(int j=0;j<2;++j){
FscanfOrDie(fp,"%lf",observation_+2*i+j);
}
}
m_nParameters = 9 * m_nCameras + 3 * m_nPoints;
parameters_ = new double[m_nParameters];
for(int p=0;p<m_nParameters;++p){
FscanfOrDie(fp,"%lf",parameters_+p);
//统一管理所有的参数
}
}
private:
int m_nCameras;
int m_nPoints;
int m_nObservations;
int m_nParameters;
int *camera_index_;
int *point_index_;
double *observations_;
double *parameters_;
};
注意fscanf赋值的必须是指针。下面给出写文件的代码:
void BALProblem::WriteToFile(const std::string &filename) const {
FILE *fptr = fopen(filename.c_str(), "w");
if (fptr == NULL) {
std::cerr << "Error: unable to open file " << filename;
return;
}
fprintf(fptr, "%d %d %d %d\n", m_nCameras, m_nCameras, m_nPoints, m_nObservations);
for (int i = 0; i < m_nObservations; ++i) {
fprintf(fptr, "%d %d", camera_index_[i], point_index_[i]);
for (int j = 0; j < 2; ++j) {
fprintf(fptr, " %g", observations_[2 * i + j]);
}
fprintf(fptr, "\n");
}
for (int i = 0; i < num_cameras(); ++i) {
double angleaxis[9];
if (use_quaternions_) {
//OutPut in angle-axis format.
QuaternionToAngleAxis(parameters_ + 10 * i, angleaxis);
memcpy(angleaxis + 3, parameters_ + 10 * i + 4, 6 * sizeof(double));
} else {
memcpy(angleaxis, parameters_ + 9 * i, 9 * sizeof(double));
}
for (int j = 0; j < 9; ++j) {
fprintf(fptr, "%.16g\n", angleaxis[j]);
}
}
const double *points = parameters_ + camera_block_size() * num_cameras_;
for (int i = 0; i < num_points(); ++i) {
const double *point = points + i * point_block_size();
for (int j = 0; j < point_block_size(); ++j) {
fprintf(fptr, "%.16g\n", point[j]);
}
}
fclose(fptr);
}
这个类中还给出了对外定义的get数据接口,这里就不再赘述了。结合fscanf函数和fprintf函数,以及最基本的数组创建,可以有效实现文件的读取。下面介绍另外一种常见的文件读取方式:基于流的。
二、C++文件读写(基于文件流)
基于文件流的方式更加普遍、高效与清晰。需要使用到的是iostream和fstream库。直接给出一个读取位姿的案例。这个案例的数据存储在estimated.txt文件中,每一行7个数据,分别是时间、xyz位置、以四元数格式记载的姿态。这种具有统一表格格式的的数据特别适合使用流数据来读取:
typedef vector<Sophus::SE3d, Eigen::aligned_allocator<Sophus::SE3d>> TrajectoryType;
TrajectoryType ReadTrajectory(const string &path) {
ifstream fin(path);
TrajectoryType trajectory;
if (!fin) {
cerr << "trajectory " << path << " not found." << endl;
return trajectory;
}
while (!fin.eof()) {
double time, tx, ty, tz, qx, qy, qz, qw;
fin >> time >> tx >> ty >> tz >> qx >> qy >> qz >> qw;
Sophus::SE3d p1(Eigen::Quaterniond(qx, qy, qz, qw), Eigen::Vector3d(tx, ty, tz));
trajectory.push_back(p1);
}
return trajectory;
}
在读取流的过程中,行是fin指针中段循环的单位。如果要转换数据格式到Eigen等库时,每个小循环中就可以进行类型转换。
总结
本文总结了2种C++语言下的文件读取方式。可以看出,当文件较为简单时,采用统一的文件流形式可以简化代码,而文件较为复杂时,特别是数据结构各异时,编写fscanf函数组合的方式,具有更好的通用性。本文会持续更新,加入更多优秀的文件读写代码。