赞
踩
最近在研究将各种数据集转换为不同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,这是因为防止标签或者图像文件中包含空格的情况):
- /home/test/data/VOC2007/JPEGImages/004379.jpg /home/test/data/VOC2007/Annotations/004379.xml
- /home/test/data/VOC2007/JPEGImages/001488.jpg /home/test/data/VOC2007/Annotations/001488.xml
- /home/test/data/VOC2007/JPEGImages/004105.jpg /home/test/data/VOC2007/Annotations/004105.xml
- /home/test/data/VOC2007/JPEGImages/006146.jpg /home/test/data/VOC2007/Annotations/006146.xml
- /home/test/data/VOC2007/JPEGImages/004295.jpg /home/test/data/VOC2007/Annotations/004295.xml
- /home/test/data/VOC2007/JPEGImages/001360.jpg /home/test/data/VOC2007/Annotations/001360.xml
- /home/test/data/VOC2007/JPEGImages/003468.jpg /home/test/data/VOC2007/Annotations/003468.xml
- ...
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有所删减:
- #ifndef CAFFE_UTIL_DB_HPP
- #define CAFFE_UTIL_DB_HPP
-
- #include <string>
-
- #include "caffe.pb.h"
-
- namespace caffe {
- namespace db {
- using namespace std;
- enum Mode { READ, WRITE, NEW };
-
- class Cursor {
- public:
- Cursor() {}
- virtual ~Cursor() {}
- virtual void SeekToFirst() = 0;
- virtual void Next() = 0;
- virtual string key() = 0;
- virtual string value() = 0;
- virtual bool valid() = 0;
- };
-
- class Transaction {
- public:
- Transaction() {}
- virtual ~Transaction() {}
- virtual void Put(const string& key, const string& value) = 0;
- virtual void Commit() = 0;
- };
-
- class DB {
- public:
- DB() {}
- virtual ~DB() {}
- virtual void Open(const string& source, Mode mode) = 0;
- virtual void Close() = 0;
- virtual Cursor* NewCursor() = 0;
- virtual Transaction* NewTransaction() = 0;
- };
-
- DB* GetDB();
-
- } // namespace db
- } // namespace caffe
-
- #endif // CAFFE_UTIL_DB_HPP
db.cpp代码如下,该代码对caffe中的db.cpp和db.hpp有所删减:
- #include "db.hpp"
- #include "db_lmdb.hpp"
- #include "glog/logging.h"
-
- #include <string>
-
- namespace caffe {
- namespace db {
-
- DB* GetDB() { return new LMDB(); }
-
- } // namespace db
- } // namespace caffe
基于以上几个类,集合lmdb可以继承出3个主要的类,分别是LMDB,LMDBTransaction以及LMDBCursor,这几个类既可以构成caffe中操作lmdb的主要类,代码保存在db_lmdb.hpp和db_lmdb.cpp中,其中db_lmdb.hpp代码如下,db_lmdb.cpp代码可自行去caffe中查找,这部分代码并未删减:
- #ifdef USE_LMDB
- #ifndef CAFFE_UTIL_DB_LMDB_HPP
- #define CAFFE_UTIL_DB_LMDB_HPP
-
- #include <string>
- #include <vector>
-
- #include "lmdb.h"
-
- #include "db.hpp"
-
- #include "glog/logging.h"
- namespace caffe {
- namespace db {
-
- inline void MDB_CHECK(int mdb_status) {
- CHECK_EQ(mdb_status, MDB_SUCCESS) << mdb_strerror(mdb_status);
- }
-
- class LMDBCursor : public Cursor {
- public:
- explicit LMDBCursor(MDB_txn* mdb_txn, MDB_cursor* mdb_cursor)
- : mdb_txn_(mdb_txn), mdb_cursor_(mdb_cursor), valid_(false) {
- SeekToFirst();
- }
- virtual ~LMDBCursor() {
- mdb_cursor_close(mdb_cursor_);
- mdb_txn_abort(mdb_txn_);
- }
- virtual void SeekToFirst() { Seek(MDB_FIRST); }
- virtual void Next() { Seek(MDB_NEXT); }
- virtual string key() {
- return string(static_cast<const char*>(mdb_key_.mv_data), mdb_key_.mv_size);
- }
- virtual string value() {
- return string(static_cast<const char*>(mdb_value_.mv_data),
- mdb_value_.mv_size);
- }
- virtual bool valid() { return valid_; }
-
- private:
- void Seek(MDB_cursor_op op) {
- int mdb_status = mdb_cursor_get(mdb_cursor_, &mdb_key_, &mdb_value_, op);
- if (mdb_status == MDB_NOTFOUND) {
- valid_ = false;
- } else {
- MDB_CHECK(mdb_status);
- valid_ = true;
- }
- }
-
- MDB_txn* mdb_txn_;
- MDB_cursor* mdb_cursor_;
- MDB_val mdb_key_, mdb_value_;
- bool valid_;
- };
-
- class LMDBTransaction : public Transaction {
- public:
- explicit LMDBTransaction(MDB_env* mdb_env) : mdb_env_(mdb_env) {}
- virtual void Put(const string& key, const string& value);
- virtual void Commit();
-
- private:
- MDB_env* mdb_env_;
- vector<string> keys, values;
-
- void DoubleMapSize();
-
- // DISABLE_COPY_AND_ASSIGN(LMDBTransaction);
- };
-
- class LMDB : public DB {
- public:
- LMDB() : mdb_env_(NULL) {}
- virtual ~LMDB() { Close(); }
- virtual void Open(const string& source, Mode mode);
- virtual void Close() {
- if (mdb_env_ != NULL) {
- mdb_dbi_close(mdb_env_, mdb_dbi_);
- mdb_env_close(mdb_env_);
- mdb_env_ = NULL;
- }
- }
- virtual LMDBCursor* NewCursor();
- virtual LMDBTransaction* NewTransaction();
-
- private:
- MDB_env* mdb_env_;
- MDB_dbi mdb_dbi_;
- };
-
- } // namespace db
- } // namespace caffe
-
- #endif // CAFFE_UTIL_DB_LMDB_HPP
- #endif // USE_LMDB
但是,有了类还不行,还需要有一些操作的入口函数,这些路口函数全部定义在io.hpp和io.cpp文件中,其中分类任务主要调用的是ReadImageToDatum函数,检测任务主要调用的是ReadProtoFromTextFile,MapNameToLabel,ReadRichImageToAnnotatedDatum函数.下面对这几个主要的函数进行讲解:
ReadImageToDatum函数将图像和标签均写入Datum中,此处标签假定已经由name转换为了0,1,2,3,4等的int形式.
- bool ReadImageToDatum(const string& filename, const int label, const int height,
- const int width, const int min_dim, const int max_dim,
- const bool is_color, const std::string& encoding,
- Datum* datum) {
- cv::Mat cv_img =
- ReadImageToCVMat(filename, height, width, min_dim, max_dim, is_color);
- if (cv_img.data) {
- if (encoding.size()) {
- if ((cv_img.channels() == 3) == is_color && !height && !width &&
- !min_dim && !max_dim && matchExt(filename, encoding)) {
- datum->set_channels(cv_img.channels());
- datum->set_height(cv_img.rows);
- datum->set_width(cv_img.cols);
- return ReadFileToDatum(filename, label, datum);
- }
- EncodeCVMatToDatum(cv_img, encoding, datum);
- datum->set_label(label);
- return true;
- }
- CVMatToDatum(cv_img, datum);
- datum->set_label(label);
- return true;
- } else {
- return false;
- }
- }
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"
}
该函数定义为:
- bool ReadProtoFromTextFile(const char* filename, Message* proto) {
- int fd = open(filename, O_RDONLY);
- CHECK_NE(fd, -1) << "File not found: " << filename;
- FileInputStream* input = new FileInputStream(fd);
- bool success = google::protobuf::TextFormat::Parse(input, proto);
- delete input;
- close(fd);
- return success;
- }
MapNameToLabel函数将NaReadProtoFromTextFile读取出来的LabelMap转换为一个std::map<string, int>* name_to_label的形式,便于后面的操作:
- bool MapNameToLabel(const LabelMap& map, const bool strict_check,
- std::map<string, int>* name_to_label) {
- // cleanup
- name_to_label->clear();
-
- for (int i = 0; i < map.item_size(); ++i) {
- const string& name = map.item(i).name();
- const int label = map.item(i).label();
- if (strict_check) {
- if (!name_to_label->insert(std::make_pair(name, label)).second) {
- LOG(FATAL) << "There are many duplicates of name: " << name;
- return false;
- }
- } else {
- (*name_to_label)[name] = label;
- }
- }
- return true;
- }
ReadRichImageToAnnotatedDatum是检测任务的主要函数,该函数首先调用了ReadImageToDatum函数,之后对不同形式的标注文件进行解析,由于大部分都是xml形式,所以只需要调用ReadXMLToAnnotatedDatum,简化后的函数如下:
- bool ReadRichImageToAnnotatedDatum(
- const string& filename, const string& labelfile, const int height,
- const int width, const int min_dim, const int max_dim, const bool is_color,
- const string& encoding, const AnnotatedDatum_AnnotationType type,
- const string& labeltype, const std::map<string, int>& name_to_label,
- AnnotatedDatum* anno_datum) {
- // Read image to datum.
- bool status =
- ReadImageToDatum(filename, -1, height, width, min_dim, max_dim, is_color,
- encoding, anno_datum->mutable_datum());
- if (status == false) {
- return status;
- }
- anno_datum->clear_annotation_group();
- switch (type) {
- case AnnotatedDatum_AnnotationType_BBOX:
- int ori_height, ori_width;
- GetImageSize(filename, &ori_height, &ori_width);
- if (labeltype == "xml") {
- return ReadXMLToAnnotatedDatum(labelfile, ori_height, ori_width,
- name_to_label, anno_datum);
-
- } else {
- LOG(FATAL) << "Unknown label file type.";
- return false;
- }
- break;
- default:
- LOG(FATAL) << "Unknown annotation type.";
- return false;
- }
- }
除了以上文件以外,转换代码还会涉及到format.hpp文件,该文件比较单一:
- #ifndef CAFFE_UTIL_FORMAT_H_
- #define CAFFE_UTIL_FORMAT_H_
-
- #include <iomanip> // NOLINT(readability/streams)
- #include <sstream> // NOLINT(readability/streams)
- #include <string>
-
- namespace caffe {
-
- inline std::string format_int(int n, int numberOfLeadingZeros = 0) {
- std::ostringstream s;
- s << std::setw(numberOfLeadingZeros) << std::setfill('0') << n;
- return s.str();
- }
- }
-
- #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文件中的头可以修改为:
- #include <algorithm>
- #include <fstream> // NOLINT(readability/streams)
- #include <string>
- #include <utility>
- #include <vector>
-
- #include "boost/scoped_ptr.hpp"
- #include "gflags/gflags.h"
- #include "glog/logging.h"
-
- #include "caffe.pb.h"
- #include "db.hpp"
- #include "format.hpp"
- #include "io.hpp"
-
- using namespace caffe; // NOLINT(build/namespaces)
- using std::pair;
- using boost::scoped_ptr;
对于检测任务,convert_annoset.cpp文件中的头可以修改为:
- #include <algorithm>
- #include <fstream> // NOLINT(readability/streams)
- #include <map>
- #include <string>
- #include <utility>
- #include <vector>
-
- #include "boost/scoped_ptr.hpp"
- #include "boost/variant.hpp"
- #include "gflags/gflags.h"
- #include "glog/logging.h"
-
- #include <opencv2/highgui/highgui_c.h>
- #include <opencv2/core/core.hpp>
- #include <opencv2/highgui/highgui.hpp>
- #include <opencv2/imgproc/imgproc.hpp>
- #include "caffe.pb.h"
- #include "db.hpp"
- #include "format.hpp"
- #include "io.hpp"
- using namespace caffe; // NOLINT(build/namespaces)
- using std::pair;
- 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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。