41

机器视觉:TensorFlow C++提取特征

 6 years ago
source link: https://www.tuicool.com/articles/UVBjuuR
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

深情似海,问相逢初度,是何年纪?依约而今还记取,不是前生夙世。放学花前,题诗石上,春水园亭里。逢君一笑,人间无此欢喜。

无奈苍狗看云,红羊数劫,惘惘休提起。客气渐多真气少,汩没心灵何已。千古声名,百年担负,事事违初意。心头阁住,儿时那种情味。

在实际应用中,将TensorFlow模型推理C++工程化的好处,不仅提升执行效率,保证服务的高可用性,还便于嵌入各类应用框架中。本文主要记录TensorFlow推理过程C++工程化,包含两部分的内容,分别为:

  • TensorFlow C++提取特征。通过TensorFlow C++接口,我们可以搭建高效、稳定的特征抽取接口服务、类别预测服务等。
  • TensorFlow 嵌入到QT中。将TensorFlow引入QT后,我们可以快捷地开发一些基于深度学习的桌面类的应用。

举个例子,如果要开发一个像INstance Search图像检索桌面应用,其中最重要的一步,就是将提取特征过程进行C++化,为QT界面程序调用提供特征抽取接口。

TensorFlow 编译过程可以参考 Ubuntu18.04下C++编译tensorflow并在QT中使用 ,或者参考有道笔记 TensorFlow c++ 编译 。注意,在MAC OS下,最好是将 libtensorflow_cc.solibtensorflow_framework.so/usr/local/lib 拷贝一份,避免出现编译成功但链接失败的错误。如果要在Xcode中使用TensorFlow,在 Header Search Paths 中添加:

/xxx/libs/tf_1.9/include
/xxx/libs/tf_1.9/include/tensorflow/
/xxx/libs/tf_1.9/include/third-party/
/xxx/libs/tf_1.9/include/bazel-genfiles/
/xxx/tensorflow/contrib/makefile/gen/protobuf/include
/xxx/libs/eigen3
/xxx/libs/opencv/3.4.2/include

Library Search Paths 中添加:

/xxx/tf/lib
/xxx/opencv/3.4.2/lib

然后在XCode中把相关的动态添加库添加进来,就可以开心地在Xcode里面调用TensorFlow C++的各种API了。

TF C++ 特征提取

我们定义一个 CnnFeature 类,对外暴露单张图片特征提取接口 computeFeat 和N张图片批量特征提取接口 computeFeatsBatch ,除此之外,其他所有的细节,调用方都不用关心,具体的 CnnFeature 定义为:

class CnnFeature{
public:
    CnnFeature(const int batchSize_, const std::string modelBin_) {
        netInputSize = 640;
        inPutName = "input:0";
        outPutName = "head/out_emb:0";
        batchSize = batchSize_;
        initModel(modelBin_);
    }
    
    ~CnnFeature() {
    }
    int computeFeat(const cv::Mat& img, std::vector<float> &ft);
    int computeFeatsBatch(const std::vector<cv::Mat> &img, std::vector<std::vector<float>> &fts);
    
private:
    int nomalizeVector(std::vector<float> &v, const int feature_dim);
    int initModel(const std::string& modelBin);
    
protected:
    int netInputSize;
    int batchSize;
    tensorflow::GraphDef graph_def;
    unique_ptr<tensorflow::Session> session;
    tensorflow::SessionOptions sess_opt;
    
    tensorflow::Tensor inputTensor;
    
    std::string inPutName = "input:0";
    std::string outPutName = "head/out_emb:0";
};

各变量和函数的意义分别为:

  • netInputSize:为模型接收的输入的尺寸,如果输入图片尺寸不满足,内部会将其resize到640的尺寸;
  • inPutName:PB模型的输入节点;
  • outPutName:PB模型的输出节点;
  • batchSize:这个没啥好解释的;
  • initModel函数:实例初始化时,完成PB模型的载入以及网络初始化相关的工作;
  • nomalizeVector:对网络输出的特征,进行L2归一化;

下面针对重点待实现的3个函数进行展开,分别为 initModelcomputeFeatcomputeFeatsBatch

模型初始化

通过 ReadBinaryProto 读入PB模型,然后创建Session。对于PB模型,TF有一个限制,就是PB模型的大小不能超过2G,如果超过2G, ReadBinaryProto 读取PB模型会失败,这时候需要将PB模型转为txt模型,txt模型的读取,TF没有模型大小的限制。

int CnnFeature::initModel(const std::string& modelBin)
{
    // 读取模型文件
    if (!ReadBinaryProto(tensorflow::Env::Default(), modelBin, &graph_def).ok())
    {
        std::cout << "Read model .pb failed" << std::endl;
        return -1;
    }

    //sess_opt.config.mutable_gpu_options()->set_allow_growth(true);
    (&session)->reset(NewSession(sess_opt));
    if (!session->Create(graph_def).ok())
    {
        cout << "Create graph failed" << endl;
        return -1;
    }
    
    return 1;
}

单图特征提取接口

图片的读取以及预处理,小白菜比较喜欢用OpenCV,另外2个需要注意的地方是:

tensorflow::Tensor
tensorflow::Tensor

下面是完整的 computeFeat 接口实现:

int CnnFeature::computeFeat(const cv::Mat& img, std::vector<float> &ft)
{
    int tmpBatchSize = 1;
    
    if (img.empty()) return 0;
    
    cv::Mat imgResized(netInputSize, netInputSize, CV_8UC3, cv::Scalar(0, 0, 0));
    cv::resize(img, imgResized, cv::Size(netInputSize, netInputSize));
    
    inputTensor = tensorflow::Tensor(tensorflow::DT_FLOAT,
                                     tensorflow::TensorShape({tmpBatchSize, netInputSize, netInputSize, 3}));
    auto inputTensorMapped = inputTensor.tensor<float, 4>();
    
    for (int y = 0; y < imgResized.rows; ++y)
    {
        for (int x = 0; x < imgResized.cols; ++x)
        {
            cv::Vec3b color = imgResized.at<cv::Vec3b>(cv::Point(x, y));
            inputTensorMapped(0, y, x, 0) = (float)color[2];
            inputTensorMapped(0, y, x, 1) = (float)color[1];
            inputTensorMapped(0, y, x, 2) = (float)color[0];
        }
    }
    
    std::vector<tensorflow::Tensor> outputs;
    std::pair<std::string, tensorflow::Tensor> imgPair(inPutName, inputTensor);
    
    tensorflow::Status status = session->Run({imgPair}, {outPutName}, {}, &outputs); //Run, 得到运行结果,存到outputs中
    if (!status.ok())
    {
        cout << "Running model failed"<<endl;
        cout << status.ToString() << endl;
        return -1;
    }
    
    // 得到模型运行结果
    tensorflow::Tensor t = outputs[0];
    auto tmap = t.tensor<float, 2>();
    int output_dim = (int)t.shape().dim_size(1);
    
    // 保存特征
    ft.clear();
    for (int n = 0; n < output_dim; n++)
    {
        ft.push_back(tmap(0, n));
    }
    
    // 特征归一化
    nomalizeVector(ft, (int)ft.size());
    
    return 1;
}

上面OpenCV的Mat在传递到 tensorflow::Tensor 并没有对每个像素做中心化(小白菜的模型在训练的时候,就没做这样的处理),如果需要中心化,改为 ((float)color[i] - 127) / 128.0 即可。得到特征后,调用 nomalizeVector 函数对特征完成L2归一化。  

批量特征提取接口

对于传进来的N张图片,处理逻辑也比较直观:将N张图片拆分成nBatchs个batch和residNum个剩余的图片,然后将这nBatchs个batch分别传入网络,得到nBatchs个batch提取的特征,最后处理剩余的构不成1个batch的图片,即residNum个剩余的图片。下面是批量特征提取接口实现:

int CnnFeature::computeFeatsBatch(const std::vector<cv::Mat> &imgs, std::vector<std::vector<float>> &fts)
{
    if (imgs.empty()) return 0;
    
    fts.clear();
    
    int nBatchs = (int)imgs.size() / batchSize;
    int residNum = (int)imgs.size() % batchSize;
    
    inputTensor = tensorflow::Tensor(tensorflow::DT_FLOAT,
                                     tensorflow::TensorShape({batchSize, netInputSize, netInputSize, 3}));
    auto inputTensorMapped = inputTensor.tensor<float, 4>();
    std::vector<float> ft;

    
    // n 个batch
    for (int i = 0; i < nBatchs; i++)
    {
        cv::Mat imgResized;
        for (int j = 0; j < batchSize; j++)
        {
            if (imgs.at(i*batchSize+j).empty())
            {
                std::cout << "image is empty" << std::endl;
                imgResized = cv::Mat::zeros(netInputSize, netInputSize, CV_8UC3);
            } else {
                cv::resize(imgs.at(i*batchSize+j), imgResized, cv::Size(netInputSize, netInputSize));
            }
            
            // 填充数据到tensor里面
            for (int y = 0; y < imgResized.rows; ++y)
            {
                for (int x = 0; x < imgResized.cols; ++x)
                {
                    cv::Vec3b color = imgResized.at<cv::Vec3b>(cv::Point(x, y));
                    inputTensorMapped(j, y, x, 0) = (float)color[2];
                    inputTensorMapped(j, y, x, 1) = (float)color[1];
                    inputTensorMapped(j, y, x, 2) = (float)color[0];
                }
            }
        }

        
        std::vector<tensorflow::Tensor> outputs;
        std::pair<std::string, tensorflow::Tensor> imgPair(inPutName, inputTensor);
        
        tensorflow::Status status = session->Run({imgPair}, {outPutName}, {}, &outputs); //Run, 得到运行结果,存到outputs中
        if (!status.ok())
        {
            cout << "Running model failed: " << status.ToString() <<endl;
            return -1;
        }
        
        // 得到模型运行结果
        tensorflow::Tensor t = outputs[0];
        auto tmap = t.tensor<float, 2>();
        int output_dim = (int)t.shape().dim_size(1);
        
        // 保存特征
        std::vector<float> feat;
        for (int k = 0; k < batchSize; k++)
        {
            ft.clear();
            for (int n = 0; n < output_dim; n++)
            {
                ft.push_back(tmap(k, n));
            }
            
            // 特征归一化
            nomalizeVector(ft, (int)ft.size());
            
            fts.push_back(ft);
        }
    }
    
    // 剩余的图片
    for (int i = 0; i < residNum; i++)
    {
        ft.clear();
        cv::Mat tmpImg = imgs.at(nBatchs*batchSize + i);
        if (tmpImg.empty())
        {
            std::cout << "image is empty" << std::endl;
            tmpImg = cv::Mat::zeros(netInputSize, netInputSize, CV_8UC3);
        }
        if (computeFeat(tmpImg, ft))
        {
            fts.push_back(ft);
        }
    }
    
    return 1;
}

当图片出现异常时,上面代码处理的方式是将当前这张有异常的图片,用一张纯黑色的图片替代,得到的特征,是这张纯黑图片的特征。

完成 computeFeatcomputeFeatsBatch 两个接口的时候,我们便可以在此基础上,做一些更加有意思的事,比如通过gRPC提供server,或者使用QT开发跨平台桌面应用。

QT引入TF

TF编译完成后,要想在QT中引入TF,是一件比较容易的事,直接在 .pro 文件中,写入头文件和库文件的路径,比如:

# OpenCV include
INCLUDEPATH += /usr/local/include/
INCLUDEPATH += /usr/local/include/opencv
INCLUDEPATH += /usr/local/include/opencv2

INCLUDEPATH += /xxx/libs/tf_1.9/include
INCLUDEPATH += /xxx/libs/tf_1.9/include/bazel-genfiles
INCLUDEPATH += /xxx/libs/tf_1.9/include/tensorflow
INCLUDEPATH += /xxx/libs/tf_1.9/include/third-party
INCLUDEPATH += /xxx/tensorflow/contrib/makefile/gen/protobuf/include

INCLUDEPATH += /usr/local/include/eigen3


LIBS += -L/usr/local/lib \
 -lopencv_calib3d \
 -lopencv_core \
 -lopencv_features2d \
 -lopencv_flann \
 -lopencv_highgui \
 -lopencv_imgcodecs \
 -lopencv_imgproc \
 -lopencv_ml \
 -lopencv_objdetect \
 -lopencv_photo \
 -lopencv_dnn \
 -ltensorflow_cc \
 -ltensorflow_framework

完成TF的 INCLUDEPATHLIBS 添加后,即可在QT里面开心的使用TF C++的各种接口了。

完整实现

完整的TF C++通过PB模型提取特征的接口实现,可以在 tf_extract_feat 找到, demo.cpp 里的pb模型,这里就不给出了,可以在自己转的PB模型上验证一下,看看跟自己的Python得到的特征,是否是一致的。

总结

本篇博客主要记录了TensorFlow C++通过PB模型特征提取的实现(单张图片、批量提取),以及在QT中引入QT中的方法。实际上,可以在此基础上稍微修改下,可以将其拓展到TF C++目标检测、分类等上。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK