专栏名称: 新机器视觉
最前沿的机器视觉与计算机视觉技术
51好读  ›  专栏  ›  新机器视觉

学习笔记 | 日志、参数文件读写

新机器视觉  · 公众号  · 科技媒体  · 2024-10-03 21:30

主要观点总结

文章主要介绍了在机器人调试过程中,日志系统和参数文件的重要性。提供了简单模板,实现日志写入、参数加载。包括一个日志写入模板,以及一个参数读取模板,使用C++11和OpenCV实现。还提供了文件存储、遍历和删除文件的方法,用于删除一段时间之前的日志文件和新建当前日志。参数文件采用YAML格式,支持多种类型参数的读写,包括整型、浮点型、字符型、矩阵类型等。

关键观点总结

关键观点1: 日志系统和参数文件的重要性

在机器人调试过程中,日志系统和参数文件对于调试和故障排查至关重要。

关键观点2: 日志写入和参数加载模板

提供了日志写入和参数加载的模板,便于在程序中进行日志记录和参数加载。

关键观点3: 文件存储、遍历和删除文件

介绍了如何存储、遍历和删除文件,以管理日志文件。

关键观点4: 参数文件使用YAML格式

参数文件采用YAML格式,支持多种类型参数的读写。

关键观点5: 日志和参数文件的应用

在机器人调试中,日志和参数文件的应用对于系统调试和性能优化具有重要作用。


正文

在机器人调试过程中,日志系统和参数文件非常重要。这里提供一个简单魔板,实现日志写入、参数加载。包含:


1、一个可以通过程序把符号、数字等日志信息写入程序的模板。仅依赖C++11(或更高版本)。每次运行新建一个日志文件,拥有规律命名格式。可以自动删除一段时间之前所产生的文件。


2、一个可以通过程序读取、加载、修改不同类型的参数的模板,包括整型、浮点型、字符型、矩阵类型的参数。依赖于opencv。


日志相关


命名规则


命名规则通过 "log_ + 日期" 的格式 所以首先需要获取时间:

// 获取以s为单位的时间点,所对应的字符串std::string get_current_time_str(){    // 获取当前时间    std::chrono::system_clock::time_point now = std::chrono::system_clock::now();    // 将当前时间转换为 std::time_t 类型    std::time_t now_time = std::chrono::system_clock::to_time_t(now);    // 将 std::time_t 类型转换为可读的字符串    char buffer[100];    strftime(buffer, sizeof(buffer), "%Y-%m-%d-%H-%M-%S", std::localtime(&now_time));    return std::string(buffer);}
// 获取以ms为单位的时间点,所对应的字符串std::string get_current_time_str_ms(){    // 获取当前时间    std::chrono::system_clock::time_point now = std::chrono::system_clock::now();    // 将当前时间转换为 std::time_t 类型    std::time_t now_time = std::chrono::system_clock::to_time_t(now);    // 获取毫秒部分    std::chrono::milliseconds ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;    // 将 std::time_t 类型转换为可读的字符串    char buffer[100];    strftime(buffer, sizeof(buffer), "%Y-%m-%d-%H-%M-%S", std::localtime(&now_time));    std::stringstream ss;    ss << buffer << "." << std::setw(3) << std::setfill('0') << ms.count();    return ss.str();}


提取时间(用做日期比较)


在删除以前的文件时,我们需要从文件的命名规则里提取出时间,所以这里写一个函数用于将"%Y-%m-%d-%H-%M-%S"格式的字符串转为时间格式。

// 该函数的作用,是将"%Y-%m-%d-%H-%M-%S"格式的字符串转为时间格式std::tm parse_filename_date(const std::string& filename) {    std::tm tm = {};    std::istringstream ss(filename);    ss >> std::get_time(&tm, "%Y-%m-%d-%H-%M-%S");    return tm;}


遍历并删除文件(根据日期比较)


在删除以前的文件时,我们需要遍历文件夹内所有的文件名,并根据文件名后缀的日期比较,判断是否删除文件。这段代码里,通过std::chrono接口获取时间点,通过1entry = readdir(dir)) != NULL遍历文件夹,通过parse_filename_date获取文件后缀所对应的日期,通过std::remove删除文件。需要提前创建文件夹,否则会进入if (dir == NULL)判断。输入参数date_ago可以被设置为std::chrono::hours(24 * 10);表示10天, n小时:std::chrono::hours( n )、n分钟:std::chrono::minutes( n )、类似还有seconds,microseconds,nanoseconds

  void Traverse_delete_files(const std::string& filedir, std::chrono::system_clock::duration date_ago) {
       std::chrono::system_clock::time_point now = std::chrono::system_clock::now();        std::chrono::system_clock::time_point some_days_ago = now - date_ago;
       struct dirent* entry;        DIR* dir = opendir(filedir.c_str());        if (dir == NULL) {            std::cerr << "Error opening directory: " << filedir << "\n";            exit(0);        }
       // 遍历filedir文件夹        while ((entry = readdir(dir)) != NULL) {            // 得到每个文件名            std::string fname = entry->d_name;            // 剔除对 "."、".."、文件名字符数少于5的文件 的操作            if (fname == "." || fname == ".." || fname.size()<5) continue;            // 剔除对 文件名前四个字符不是"log_"的文件            std::string first_five_chars = fname.substr(0, 4);// .substr(0, 4)表示从第1个字符开始取4个字符            if(first_five_chars != "log_")  continue;
           std::cout << "find log file name: "  << fname << std::endl;
           // 取出 log_2023-06-06-21-03-00 的日期部分            std ::string date_str = fname.substr(4);// .substr(4)表示取从第5个字符开始的所以字符
           // 将日期部分的字符,转为时间格式            std::tm file_date = parse_filename_date(date_str);            std::chrono::system_clock::time_point file_timepoint = std::chrono::system_clock::from_time_t(std::mktime(&file_date));
           // 如果这个时间在some_days_ago之前,则删除这个文件            if (file_timepoint < some_days_ago) {                // 注意fname只是文件名,还需要加上路径                std::string rmpath = filedir + fname;                std::cout << "remove file: " << rmpath << std::endl;
               // 删除文件                std::remove(rmpath.c_str());            }        }    }


新建当前日志


把新建当前程序的日志文件的操作,放到对一段时间以前的日志文件删除操作之后。这里将log_ofs、mlog_mutex设置为函数外的变量,是希望可以在其它在程序外或者其它线程,进行日志写操作 首先进行了一个覆盖写操作,若文件存在则全部覆盖掉(清除),若文件不存在会新建一个。然后log_ofs被设置为追加写操作。需要提前创建文件夹,否则不会创建日志文件,会进入if (!log_ofs1.is_open())判断。

    std::ofstream log_ofs;    std::mutex mlog_mutex;    void set_newlog(const std::string& filedir) {        std::string current_time_str = get_current_time_str();        std::string filename = filedir + "log_" + current_time_str;
       // 创建一个用于写入的 ofstream 对象,若文件不存在,会新建文件,std::ios::out表示覆盖写        std::ofstream log_ofs1(filename, std::ios::out);        // log_file1 << "System started" << std::endl;        log_ofs1.close();
       // 重新打开文件,std::ios::app表示追加写        log_ofs.open(filename, std::ios::app);        std::cout << "Set log to: " << filename << std::endl;    }


日志内容写入


将日志信息写入log文件。通过流操作,可以像cout一样方便地写入,这里提供一个写入模板:

    // 一个写入模板,mlogFile_mutex与log_file可以被当做全局变量,在不同的地方对文件进行写入    // mlogFile_mutex保护文件在多线程使用的时候不被同时写入    float testnum = 128.128;    std::unique_lock<std::mutex> lock(mlog_mutex);    log_ofs << "testnum: " << testnum << " time: "<< get_current_time_str_ms() << std::endl;    lock.unlock();


总体代码


根据以上的设计,总结代码如下:

// 日志文件写入、删除// g++ logtest.cpp -o test -std=c++11#include #include #include #include #include #include #include class MyLog{public:    MyLog(const std::string& filedir, std::chrono::system_clock::duration date_ago){        mfiledir = filedir;        // n小时:std::chrono::hours( n ),n分钟:std::chrono::minutes( n ),类似还有seconds,microseconds,nanoseconds        mdate_ago = date_ago;    };        ~MyLog(){        std::unique_lock<std::mutex> lock(mlog_mutex);        mlog_ofs << "system end, log_ofs closed  " << get_current_time_str_ms() << std::endl;        lock.unlock();        mlog_ofs.close();    };    // 获取以s为单位的时间点,所对应的字符串    std::string get_current_time_str(){        // 获取当前时间        std::chrono::system_clock::time_point now = std::chrono::system_clock::now();        // 将当前时间转换为 std::time_t 类型        std::time_t now_time = std::chrono::system_clock::to_time_t(now);        // 将 std::time_t 类型转换为可读的字符串        char buffer[100];        strftime(buffer, sizeof(buffer), "%Y-%m-%d-%H-%M-%S", std::localtime(&now_time));        return std::string(buffer);    }    // 获取以ms为单位的时间点,所对应的字符串    std::string get_current_time_str_ms(){        // 获取当前时间        std::chrono::system_clock::time_point now = std::chrono::system_clock::now();        // 将当前时间转换为 std::time_t 类型        std::time_t now_time = std::chrono::system_clock::to_time_t(now);        // 获取毫秒部分        std::chrono::milliseconds ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;        // 将 std::time_t 类型转换为可读的字符串        char buffer[100




    
];        strftime(buffer, sizeof(buffer), "%Y-%m-%d-%H-%M-%S", std::localtime(&now_time));        std::stringstream ss;        ss << buffer << "." << std::setw(3) << std::setfill('0') << ms.count();        return ss.str();    }    // 该函数的作用,是将"%Y-%m-%d-%H-%M-%S"格式的字符串转为时间格式    std::tm parse_filename_date(const std::string& filename) {        std::tm tm = {};        std::istringstream ss(filename);        ss >> std::get_time(&tm, "%Y-%m-%d-%H-%M-%S");        return tm;    }    // 该函数的作用,遍历文件夹,并删除一段时间之前的日志文件    void Traverse_delete_files(const std::string& filedir, std::chrono::system_clock::duration date_ago) {        std::chrono::system_clock::time_point now = std::chrono::system_clock::now();        std::chrono::system_clock::time_point some_days_ago = now - date_ago;        struct dirent* entry;        DIR* dir = opendir(filedir.c_str());        if (dir == NULL) {            std::cerr << "Error opening directory: " << filedir << "\n";            exit(0);        }        // 遍历filedir文件夹        while ((entry = readdir(dir)) != NULL) {            // 得到每个文件名            std::string fname = entry->d_name;            // 剔除对 "."、".."、文件名字符数少于5的文件 的操作            if (fname == "." || fname == ".." || fname.size()<5) continue;            // 剔除对 文件名前四个字符不是"log_"的文件             std::string first_five_chars = fname.substr(0, 4);// .substr(0, 4)表示从第1个字符开始取4个字符            if(first_five_chars != "log_")  continue;            std::cout << "find log file name: "  << fname << std::endl;            // 取出 log_2023-06-06-21-03-00 的日期部分            std::string date_str = fname.substr(4);// .substr(4)表示取从第5个字符开始的所以字符            // 将日期部分的字符,转为时间格式            std::tm file_date = parse_filename_date(date_str);            std::chrono::system_clock::time_point file_timepoint = std::chrono::system_clock::from_time_t(std::mktime(&file_date));            // 如果这个时间在some_days_ago之前,则删除这个文件            if (file_timepoint < some_days_ago) {                // 注意fname只是文件名,还需要加上路径                std::string rmpath = filedir + fname;                std::cout << "remove file: " << rmpath << std::endl;                // 删除文件                std::remove(rmpath.c_str());            }        }    }    void Traverse_delete_files() {        Traverse_delete_files(mfiledir, mdate_ago);    }    // 该函数的作用,新建当前程序日志文件    void set_newlog(const std::string& filedir) {        std::string current_time_str = get_current_time_str();        std::string filename = filedir + "log_" + current_time_str;        // 创建一个用于写入的 ofstream 对象,若文件不存在,会新建文件,std::ios::out表示覆盖写        std::ofstream log_ofs1(filename, std::ios::out);        if (!log_ofs1.is_open()) {            std::cerr << "log_ofs1: Failed to open the file." << std::endl;            exit(0);        }        log_ofs1 << "System started" << std::endl;        log_ofs1.close();        // 重新打开文件,std::ios::app表示追加写        mlog_ofs.open(filename, std::ios::app);        std::cout << "Set log to: " << filename << std::endl;    }    void set_newlog() {        set_newlog(mfiledir);    }    std::ofstream mlog_ofs;    std::mutex mlog_mutex;private:    std::string mfiledir;    std::chrono::system_clock::duration mdate_ago;};int main(){    std::string path_log = "/home/john/Desktop/ctest/log/";    MyLog mylog(path_log, std::chrono::hours(24 * 10));    mylog.Traverse_delete_files();    mylog.set_newlog();    float testnum = 128.128;    std::unique_lock<std::mutex> lock(mylog.mlog_mutex);    mylog.mlog_ofs << "testnum: " << testnum << " time: " << mylog.get_current_time_str_ms() << std::endl;    lock.unlock();        return 0;}


运行结果为:























参数读取相关


参数读取,这里是读取yaml文件格式的参数,这里使用opencv的接口实现。也可以使用yaml-cpp工具库实现,但opencv的接口实现有一个好处是,可以读写矩阵格式。


参数文件创建与FileStorage初始化


先参数文件创建,我们可以直接新建一个yaml文件就好了。或者或者我们可以使用写操作,在没有这个参数文件的时候,程序会新建一个参数文件,前提是它的文件夹存在,记得fs.release();。注意opencv里,参数文件的前两行需要有:

%YAML:1.0---


FileStorage初始化的操作如下:

    string configPath  = "/home/john/Desktop/ctest/test.yaml";    /* 初始化 */    FileStorage fs;    // FileStorage fs(configPath, FileStorage::READ);
   /********************** 覆盖写操作 **************************/    fs.open(configPath, FileStorage::WRITE);// 覆盖写,此步会清空 yaml 文件    if(!fs.isOpened()){        cerr << "fs.isOpened() return false at FileStorage::WRITE! FAIL" << endl;          return 1;      }    /*********************** 追加写操作 **************************/    fs.open(configPath, FileStorage::APPEND);// 追加写,此步是在 yaml 文件末尾追加,不会清空文件    if(!fs.isOpened()){        cerr << "fs.isOpened() return false at FileStorage::APPEND! FAIL" << endl;          return 1;      }    /*********************** 读取操作 **************************/    fs.open(configPath, FileStorage::READ);    if(!fs.isOpened()){        cerr << "fs.isOpened() return false at FileStorage::READ! FAIL" << endl;          return 1;      }..................    fs.release();


参数读写格式


FileStorage使用上与流操作类似。FileStorage支持int、float、bool等普通变量类型的读写,还支持vector、map、matrix格式的读写。


对于普通变量的读写

  // 写    fs << "a_int_value" << 10;    fs << "b_float_value" << 11.0;    fs << "c_string_value" << "  Valar Morghulis  \r\n  Valar Dohaeris";    ......    ......    ......  // 读    int a;    float b;    string c;    fs["a_int_value"]       >> a;    fs["b_float_value"]     >> b;    fs["c_string_value"]    >> c;      cout << "\na_int_value: "        << a << endl;    cout << "\nb_float_value: "      << b << endl;    cout << "\nc_string_value: \n"   << c << endl;


写入内容在yaml文件里显示为



读出内容在终端显示为



vector的读写

  // 写fs << "e_vector_value" <<"[" << "One" << "Two" << "Three" << "]";     ......    ......    ......  // 读    vector<string> e;    FileNode n = fs["e_vector_value"];// 从序列中读取vector    if (n.type() != FileNode::SEQ){        cerr << "e_vector_value is not a sequence! FAIL" << endl;          return 1;      }    n >> e;      cout << "\ne_vector_value (size: " << e.size() << "): " << endl;    for (vector<string>::iterator it = e.begin(); it != e.end(); ++it){        cout << "  " << *it << endl;    }


写入内容在yaml文件里显示为



读出内容在终端显示为



map的读写

  // 写    fs << "f_map_value" << "{" << "One" << 1 << "Two" << 2 << "}";    ......    ......    ......  // 读    FileNode n = fs["e_vector_value"];// 从序列中读取vector    if (n.type() != FileNode::SEQ){        cerr << "e_vector_value is not a sequence! FAIL" << endl;          return 1;      }    n >> e;      cout << "\nf_map_value (size: " << f.size() << "): " << endl;  for(std::map<string, int>::iterator it = f.begin();it != f.end();it++){    std::cout << "  " << it->first << ": " << it->second << endl;  }


写入内容在yaml文件里显示为



读出内容在终端显示为



matrix的读写


一般我们矩阵操作使用eigen,opencv的矩阵格式是mat,opencv提供了cv2eigen()函数,把mat类型转为eigen类型。若报错:fatal error: unsupported/Eigen/CXX11/Tensor: No such file or directory 在cmake里添加:add_definitions(-DOPENCV_DISABLE_EIGEN_TENSOR_SUPPORT)

  // 写    fs << "d_matrix_value" << (cv::Mat_<int>(3, 3) <<   12, 22, 90,                                                         12






请到「今天看啥」查看全文