当前位置:   article > 正文

人工智能框架数据集转换C++实现(二):Lmdb数据集_lmdb数据库 c++

lmdb数据库 c++

最近在研究将各种数据集转换为不同AI框架的自有数据,这些框架包括Caffe,MXNet,Tensorflow等.C++这样一个通用而又强大的语言,却让使用C++的同鞋在AI时代有一个痛点,那就是目前的AI框架基本都是底层用C/C++实现,但提供的接口却大部分都是python的接口,而且Python的接口封装的都特别好,MXNet还好,提供im2rec.cc这样的C/C++源码,而Caffe,尤其是Tensorflow这样的框架,想用C++来转换数据就需要花点功夫了.前一章讲解了TFRecord格式的转换C++实现,本文讲解Caffe的Lmdb格式的转换实现,虽然Caffe中caffe/examples/imagenet含有create_imagenet.sh文件,该文件会调用convert_imageset.cpp代码,但仍然存在的问题是Caffe代码众多,如果只是学习的话有的同鞋可能会看的一头雾水,这些转换代码到底和哪些文件相关呢?如果我把转换代码单独拿出来,这部分代码能有多少呢?检测任务的代码在单独的ssh-caffe中,能否将分类代码和检测的统一呢?这就是我们这篇文章的目的.

1.不同框架的数据分别是怎样的?

MXNet的自有数据集:rec格式

Caffe的自有据集:Lmdb格式

Tensorflow的自有数据集:TFRecord格式

2.什么是Lmdb格式?

lmdb 是Lightning Memory-Mapped Database的缩写.闪电般的内存映射数据库。它文件结构简单,一个文件夹,里面一个数据文件,一个锁文件。数据随意复制,随意传输。它的访问简单,不需要运行单独的数据库管理进程,只要在访问数据的代码里引用LMDB库,访问时给文件路径即可。 它包含一个数据文件和一个锁文件.

caffe中的数据集借助了protobuf的表现形式,但是一个标准的caffe.proto特别大(数千行),哪些是我们需要的呢?别急,咱们只需要下面这么点数据(不到60行代码):

syntax = "proto2";

package caffe;

message Datum {
  optional int32 channels = 1;
  optional int32 height = 2;
  optional int32 width = 3;
  // the actual image data, in bytes
  optional bytes data = 4;
  optional int32 label = 5;
  // Optionally, the datum could also hold float data.
  repeated float float_data = 6;
  // If true data contains an encoded image that need to be decoded
  optional bool encoded = 7 [default = false];
}

// The label (display) name and label id.
message LabelMapItem {
  // Both name and label are required.
  optional string name = 1;
  optional int32 label = 2;
  // display_name is optional.
  optional string display_name = 3;
}

message LabelMap {
  repeated LabelMapItem item = 1;
}

// The normalized bounding box [0, 1] w.r.t. the input image size.
message NormalizedBBox {
  optional float xmin = 1;
  optional float ymin = 2;
  optional float xmax = 3;
  optional float ymax = 4;
  optional int32 label = 5;
  optional bool difficult = 6;
  optional float score = 7;
  optional float size = 8;
}

// Annotation for each object instance.
message Annotation {
  optional NormalizedBBox bbox = 2;
}

// Group of annotations for a particular label.
message AnnotationGroup {
  optional int32 group_label = 1;
  repeated Annotation annotation = 2;
}

// An extension of Datum which contains "rich" annotations.
message AnnotatedDatum {
  optional Datum datum = 1;
  // Each group contains annotation for a particular class.
  repeated AnnotationGroup annotation_group = 3;
}

这是一个protobuf2的格式定义,需要使用以下命令通过该文件生成头文件caffe.pb.h和cc文件caffe.pb.cc:

protoc -I=. --cpp_out=./ caffe.proto  

3.自有数据集该准备成什么样?

此处以VOC2007数据集为检测任务的例子讲解,LFW数据集为分类任务讲解.

对于分类任务,数据集统一构建一个这样的列表,该表的构建使用过caffe的同鞋应该很熟悉(略有不同的是,文件名和标签中间不是空格,而是\t,这是因为防止标签或者图像文件中包含空格的情况):

/output/oldFile/1000015_10/wKgB5Fr6WwWAJb7iAAABKohu5Nw109.png   0  
/output/oldFile/1000015_10/wKgB5Fr6WwWAEbg6AAABC_mxdD8880.png   0  
/output/oldFile/1000015_10/wKgB5Fr6WwWAUGTdAAAA8wVERrQ677.png   0  
/output/oldFile/1000015_10/wKgB5Fr6WwWAPJ-lAAABPYAoeuY242.png   0  
/output/oldFile/1000015_10/wKgB5Fr6WwWARVIWAAABCK2alGs331.png   0  
/output/oldFile/1000015_10/wKgB5Fr6WwWAV3R5AAAA5573dko147.png   0  
/output/oldFile/1000015_10/wKgB5Fr6WwaAUjQRAAABIkYxqoY008.png   0  
...  
/output/oldFile/1000015_10/wKgB5Vr6YF-AALG-AAAA-qStI_Q208.png   1  
/output/oldFile/1000015_10/wKgB5Vr6YGCAe1VYAAABN5fz53Y240.png   1  
/output/oldFile/1000015_10/wKgB5Vr6YGCAQo7fAAABVFasXJ4223.png   1  
/output/oldFile/1000015_10/wKgB5Vr6YGCAL00yAAABJdrU4U0508.png   1  
/output/oldFile/1000015_10/wKgB5Vr6YGCAFjTyAAABJVgoCrU242.png   1  
/output/oldFile/1000015_10/wKgB5Vr6YGCAKmMMAAABMd1_pJg240.png   1  
/output/oldFile/1000015_10/wKgB5Vr6YGCAR2FqAAABFCQ7LRY651.png   1  

对于VOC2007数据集,构建的列表如下(略有不同的是,文件名和标签中间不是空格,而是\t,这是因为防止标签或者图像文件中包含空格的情况):

  1. /home/test/data/VOC2007/JPEGImages/004379.jpg /home/test/data/VOC2007/Annotations/004379.xml
  2. /home/test/data/VOC2007/JPEGImages/001488.jpg /home/test/data/VOC2007/Annotations/001488.xml
  3. /home/test/data/VOC2007/JPEGImages/004105.jpg /home/test/data/VOC2007/Annotations/004105.xml
  4. /home/test/data/VOC2007/JPEGImages/006146.jpg /home/test/data/VOC2007/Annotations/006146.xml
  5. /home/test/data/VOC2007/JPEGImages/004295.jpg /home/test/data/VOC2007/Annotations/004295.xml
  6. /home/test/data/VOC2007/JPEGImages/001360.jpg /home/test/data/VOC2007/Annotations/001360.xml
  7. /home/test/data/VOC2007/JPEGImages/003468.jpg /home/test/data/VOC2007/Annotations/003468.xml
  8. ...

4.数据集转换的流程是怎样的?

数据列表准备好之后,就可以开始分析数据集转换的流程,大体上来说就是对于分类任务,首先初始化一个LMDB和Transaction,然后处理列表中的数据,每一行对应一个Datum,每行包含图片路径和相应的标签,使用OPENCV读取图片为Mat后,将其转换为Datum的格式,并获取图片的宽高通道数,标签等信息,也都保存到Datum中,最后将每行的Datum序列化SerializeToString为string,调用Transaction写入.分类任务智慧使用到caffe.proto中的Datum,对于检测任务区别则在于增加了对xml文件的解析,并保存bbox信息等,同时也会使用到caffe.proto中的AnnotatedDatum和NormalizedBBox.

首先需要构建几个类,其中定义相关的虚函数,这些类定义在文件db.hpp和文件db.cpp中,代码如下,该代码对caffe中的db.cpp和db.hpp有所删减:

  1. #ifndef CAFFE_UTIL_DB_HPP
  2. #define CAFFE_UTIL_DB_HPP
  3. #include <string>
  4. #include "caffe.pb.h"
  5. namespace caffe {
  6. namespace db {
  7. using namespace std;
  8. enum Mode { READ, WRITE, NEW };
  9. class Cursor {
  10. public:
  11. Cursor() {}
  12. virtual ~Cursor() {}
  13. virtual void SeekToFirst() = 0;
  14. virtual void Next() = 0;
  15. virtual string key() = 0;
  16. virtual string value() = 0;
  17. virtual bool valid() = 0;
  18. };
  19. class Transaction {
  20. public:
  21. Transaction() {}
  22. virtual ~Transaction() {}
  23. virtual void Put(const string& key, const string& value) = 0;
  24. virtual void Commit() = 0;
  25. };
  26. class DB {
  27. public:
  28. DB() {}
  29. virtual ~DB() {}
  30. virtual void Open(const string& source, Mode mode) = 0;
  31. virtual void Close() = 0;
  32. virtual Cursor* NewCursor() = 0;
  33. virtual Transaction* NewTransaction() = 0;
  34. };
  35. DB* GetDB();
  36. } // namespace db
  37. } // namespace caffe
  38. #endif // CAFFE_UTIL_DB_HPP

db.cpp代码如下,该代码对caffe中的db.cpp和db.hpp有所删减:

  1. #include "db.hpp"
  2. #include "db_lmdb.hpp"
  3. #include "glog/logging.h"
  4. #include <string>
  5. namespace caffe {
  6. namespace db {
  7. DB* GetDB() { return new LMDB(); }
  8. } // namespace db
  9. } // namespace caffe

基于以上几个类,集合lmdb可以继承出3个主要的类,分别是LMDB,LMDBTransaction以及LMDBCursor,这几个类既可以构成caffe中操作lmdb的主要类,代码保存在db_lmdb.hpp和db_lmdb.cpp中,其中db_lmdb.hpp代码如下,db_lmdb.cpp代码可自行去caffe中查找,这部分代码并未删减:

  1. #ifdef USE_LMDB
  2. #ifndef CAFFE_UTIL_DB_LMDB_HPP
  3. #define CAFFE_UTIL_DB_LMDB_HPP
  4. #include <string>
  5. #include <vector>
  6. #include "lmdb.h"
  7. #include "db.hpp"
  8. #include "glog/logging.h"
  9. namespace caffe {
  10. namespace db {
  11. inline void MDB_CHECK(int mdb_status) {
  12. CHECK_EQ(mdb_status, MDB_SUCCESS) << mdb_strerror(mdb_status);
  13. }
  14. class LMDBCursor : public Cursor {
  15. public:
  16. explicit LMDBCursor(MDB_txn* mdb_txn, MDB_cursor* mdb_cursor)
  17. : mdb_txn_(mdb_txn), mdb_cursor_(mdb_cursor), valid_(false) {
  18. SeekToFirst();
  19. }
  20. virtual ~LMDBCursor() {
  21. mdb_cursor_close(mdb_cursor_);
  22. mdb_txn_abort(mdb_txn_);
  23. }
  24. virtual void SeekToFirst() { Seek(MDB_FIRST); }
  25. virtual void Next() { Seek(MDB_NEXT); }
  26. virtual string key() {
  27. return string(static_cast<const char*>(mdb_key_.mv_data), mdb_key_.mv_size);
  28. }
  29. virtual string value() {
  30. return string(static_cast<const char*>(mdb_value_.mv_data),
  31. mdb_value_.mv_size);
  32. }
  33. virtual bool valid() { return valid_; }
  34. private:
  35. void Seek(MDB_cursor_op op) {
  36. int mdb_status = mdb_cursor_get(mdb_cursor_, &mdb_key_, &mdb_value_, op);
  37. if (mdb_status == MDB_NOTFOUND) {
  38. valid_ = false;
  39. } else {
  40. MDB_CHECK(mdb_status);
  41. valid_ = true;
  42. }
  43. }
  44. MDB_txn* mdb_txn_;
  45. MDB_cursor* mdb_cursor_;
  46. MDB_val mdb_key_, mdb_value_;
  47. bool valid_;
  48. };
  49. class LMDBTransaction : public Transaction {
  50. public:
  51. explicit LMDBTransaction(MDB_env* mdb_env) : mdb_env_(mdb_env) {}
  52. virtual void Put(const string& key, const string& value);
  53. virtual void Commit();
  54. private:
  55. MDB_env* mdb_env_;
  56. vector<string> keys, values;
  57. void DoubleMapSize();
  58. // DISABLE_COPY_AND_ASSIGN(LMDBTransaction);
  59. };
  60. class LMDB : public DB {
  61. public:
  62. LMDB() : mdb_env_(NULL) {}
  63. virtual ~LMDB() { Close(); }
  64. virtual void Open(const string& source, Mode mode);
  65. virtual void Close() {
  66. if (mdb_env_ != NULL) {
  67. mdb_dbi_close(mdb_env_, mdb_dbi_);
  68. mdb_env_close(mdb_env_);
  69. mdb_env_ = NULL;
  70. }
  71. }
  72. virtual LMDBCursor* NewCursor();
  73. virtual LMDBTransaction* NewTransaction();
  74. private:
  75. MDB_env* mdb_env_;
  76. MDB_dbi mdb_dbi_;
  77. };
  78. } // namespace db
  79. } // namespace caffe
  80. #endif // CAFFE_UTIL_DB_LMDB_HPP
  81. #endif // USE_LMDB

但是,有了类还不行,还需要有一些操作的入口函数,这些路口函数全部定义在io.hpp和io.cpp文件中,其中分类任务主要调用的是ReadImageToDatum函数,检测任务主要调用的是ReadProtoFromTextFile,MapNameToLabel,ReadRichImageToAnnotatedDatum函数.下面对这几个主要的函数进行讲解:

ReadImageToDatum函数将图像和标签均写入Datum中,此处标签假定已经由name转换为了0,1,2,3,4等的int形式.

  1. bool ReadImageToDatum(const string& filename, const int label, const int height,
  2. const int width, const int min_dim, const int max_dim,
  3. const bool is_color, const std::string& encoding,
  4. Datum* datum) {
  5. cv::Mat cv_img =
  6. ReadImageToCVMat(filename, height, width, min_dim, max_dim, is_color);
  7. if (cv_img.data) {
  8. if (encoding.size()) {
  9. if ((cv_img.channels() == 3) == is_color && !height && !width &&
  10. !min_dim && !max_dim && matchExt(filename, encoding)) {
  11. datum->set_channels(cv_img.channels());
  12. datum->set_height(cv_img.rows);
  13. datum->set_width(cv_img.cols);
  14. return ReadFileToDatum(filename, label, datum);
  15. }
  16. EncodeCVMatToDatum(cv_img, encoding, datum);
  17. datum->set_label(label);
  18. return true;
  19. }
  20. CVMatToDatum(cv_img, datum);
  21. datum->set_label(label);
  22. return true;
  23. } else {
  24. return false;
  25. }
  26. }

ReadProtoFromTextFile函数是检测任务读取标签map文件的函数,该map文件格式如下:

item {
  name: "aeroplane"
  label: 0
  display_name: "aeroplane"
}
item {
  name: "bicycle"
  label: 1
  display_name: "bicycle"
}
item {
  name: "bird"
  label: 2
  display_name: "bird"
}
item {
  name: "boat"
  label: 3
  display_name: "boat"
}

该函数定义为:

  1. bool ReadProtoFromTextFile(const char* filename, Message* proto) {
  2. int fd = open(filename, O_RDONLY);
  3. CHECK_NE(fd, -1) << "File not found: " << filename;
  4. FileInputStream* input = new FileInputStream(fd);
  5. bool success = google::protobuf::TextFormat::Parse(input, proto);
  6. delete input;
  7. close(fd);
  8. return success;
  9. }

MapNameToLabel函数将NaReadProtoFromTextFile读取出来的LabelMap转换为一个std::map<string, int>* name_to_label的形式,便于后面的操作:

  1. bool MapNameToLabel(const LabelMap& map, const bool strict_check,
  2. std::map<string, int>* name_to_label) {
  3. // cleanup
  4. name_to_label->clear();
  5. for (int i = 0; i < map.item_size(); ++i) {
  6. const string& name = map.item(i).name();
  7. const int label = map.item(i).label();
  8. if (strict_check) {
  9. if (!name_to_label->insert(std::make_pair(name, label)).second) {
  10. LOG(FATAL) << "There are many duplicates of name: " << name;
  11. return false;
  12. }
  13. } else {
  14. (*name_to_label)[name] = label;
  15. }
  16. }
  17. return true;
  18. }

ReadRichImageToAnnotatedDatum是检测任务的主要函数,该函数首先调用了ReadImageToDatum函数,之后对不同形式的标注文件进行解析,由于大部分都是xml形式,所以只需要调用ReadXMLToAnnotatedDatum,简化后的函数如下:

  1. bool ReadRichImageToAnnotatedDatum(
  2. const string& filename, const string& labelfile, const int height,
  3. const int width, const int min_dim, const int max_dim, const bool is_color,
  4. const string& encoding, const AnnotatedDatum_AnnotationType type,
  5. const string& labeltype, const std::map<string, int>& name_to_label,
  6. AnnotatedDatum* anno_datum) {
  7. // Read image to datum.
  8. bool status =
  9. ReadImageToDatum(filename, -1, height, width, min_dim, max_dim, is_color,
  10. encoding, anno_datum->mutable_datum());
  11. if (status == false) {
  12. return status;
  13. }
  14. anno_datum->clear_annotation_group();
  15. switch (type) {
  16. case AnnotatedDatum_AnnotationType_BBOX:
  17. int ori_height, ori_width;
  18. GetImageSize(filename, &ori_height, &ori_width);
  19. if (labeltype == "xml") {
  20. return ReadXMLToAnnotatedDatum(labelfile, ori_height, ori_width,
  21. name_to_label, anno_datum);
  22. } else {
  23. LOG(FATAL) << "Unknown label file type.";
  24. return false;
  25. }
  26. break;
  27. default:
  28. LOG(FATAL) << "Unknown annotation type.";
  29. return false;
  30. }
  31. }

除了以上文件以外,转换代码还会涉及到format.hpp文件,该文件比较单一:

  1. #ifndef CAFFE_UTIL_FORMAT_H_
  2. #define CAFFE_UTIL_FORMAT_H_
  3. #include <iomanip> // NOLINT(readability/streams)
  4. #include <sstream> // NOLINT(readability/streams)
  5. #include <string>
  6. namespace caffe {
  7. inline std::string format_int(int n, int numberOfLeadingZeros = 0) {
  8. std::ostringstream s;
  9. s << std::setw(numberOfLeadingZeros) << std::setfill('0') << n;
  10. return s.str();
  11. }
  12. }
  13. #endif // CAFFE_UTIL_FORMAT_H_

如果还需要考虑对文件列表进行打乱,则还需要增加rng.hpp文件,而该文件又涉及到common.cpp和common.hpp,当然你也可以将convert_imageset.cpp中的Shuffle代码注释,那么你只需要这么几个文件就可以编译一个独立的lmdb转换代码:caffe.proto,convert_imageset.cpp,db.cpp  db.hpp  db_lmdb.cpp  db_lmdb.hpp  format.hpp  io.cpp  io.hpp 

对于分类任务,convert_imageset.cpp文件中的头可以修改为:

  1. #include <algorithm>
  2. #include <fstream> // NOLINT(readability/streams)
  3. #include <string>
  4. #include <utility>
  5. #include <vector>
  6. #include "boost/scoped_ptr.hpp"
  7. #include "gflags/gflags.h"
  8. #include "glog/logging.h"
  9. #include "caffe.pb.h"
  10. #include "db.hpp"
  11. #include "format.hpp"
  12. #include "io.hpp"
  13. using namespace caffe; // NOLINT(build/namespaces)
  14. using std::pair;
  15. using boost::scoped_ptr;

对于检测任务,convert_annoset.cpp文件中的头可以修改为:

  1. #include <algorithm>
  2. #include <fstream> // NOLINT(readability/streams)
  3. #include <map>
  4. #include <string>
  5. #include <utility>
  6. #include <vector>
  7. #include "boost/scoped_ptr.hpp"
  8. #include "boost/variant.hpp"
  9. #include "gflags/gflags.h"
  10. #include "glog/logging.h"
  11. #include <opencv2/highgui/highgui_c.h>
  12. #include <opencv2/core/core.hpp>
  13. #include <opencv2/highgui/highgui.hpp>
  14. #include <opencv2/imgproc/imgproc.hpp>
  15. #include "caffe.pb.h"
  16. #include "db.hpp"
  17. #include "format.hpp"
  18. #include "io.hpp"
  19. using namespace caffe; // NOLINT(build/namespaces)
  20. using std::pair;
  21. using boost::scoped_ptr;

Makefile文件如下,运行make,一个独立的lmdb转换程序就产生了:

all:
	rm -rf caffe.pb*
	/usr/local/protobuf/bin/protoc -I=. --cpp_out=./ caffe.proto
	g++ -g --std=c++11 -DUSE_LMDB -DUSE_OPENCV -o convert_annoset convert_annoset.cpp caffe.pb.cc db.cpp db_lmdb.cpp io.cpp format.hpp -I/usr/local/opencv2/include -L/usr/local/opencv2/lib -L. -lopencv_core -lopencv_highgui -lopencv_imgproc -I. -I/usr/local/protobuf/include/ -L/usr/local/protobuf/lib -lprotobuf -lglog -lgflags -lpthread -lboost_system -lboost_filesystem -llmdb
	g++ -g --std=c++11 -DUSE_LMDB -DUSE_OPENCV -o convert_imageset convert_imageset.cpp caffe.pb.cc db.cpp db_lmdb.cpp io.cpp format.hpp -I/usr/local/opencv2/include -L/usr/local/opencv2/lib -L. -lopencv_core -lopencv_highgui -lopencv_imgproc -I. -I/usr/local/protobuf/include/ -L/usr/local/protobuf/lib -lprotobuf -lglog -lgflags -lpthread -lboost_system -lboost_filesystem -llmdb


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/289245
推荐阅读
相关标签
  

闽ICP备14008679号