首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

Caffe C++ API 完整調用指南:從模型加載到特徵提取

     編輯:關於C++

Caffe C++ 接口架構概覽

Caffe 的 C++ 接口採用 Net 作為核心類,封裝了網絡結構、參數、前向/反向傳播。典型工作流程:

  1. 構建 `Net` 對象(加載 `deploy.prototxt`)
  2. 載入訓練權重(`caffemodel`)
  3. 設置輸入數據(預處理、均值減去)
  4. 調用 `Forward()` 執行前向推理
  5. 從輸出 Blob 讀取結果

完整分類器封裝類

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace caffe;
using std::string;

class Classifier {
public:
    Classifier(const string& model_file, const string& trained_file,
               const string& mean_file, const string& label_file);
    
    std::vector> Classify(const cv::Mat& img, int N = 5);

private:
    void SetMean(const string& mean_file);
    std::vector Predict(const cv::Mat& img);
    void WrapInputLayer(std::vector* input_channels);
    void Preprocess(const cv::Mat& img, std::vector* input_channels);

    std::shared_ptr> net_;
    cv::Size input_geometry_;
    int num_channels_;
    cv::Mat mean_;
    std::vector labels_;
};

構造函數:模型加載與初始化

Classifier::Classifier(const string& model_file, const string& trained_file,
                       const string& mean_file, const string& label_file) {
    // 設置計算模式
#ifdef CPU_ONLY
    Caffe::set_mode(Caffe::CPU);
#else
    Caffe::set_mode(Caffe::GPU);
#endif

    // 載入網絡結構
    net_.reset(new Net(model_file, TEST));
    net_->CopyTrainedLayersFrom(trained_file);

    // 校驗輸入輸出
    CHECK_EQ(net_->num_inputs(), 1) << "Network must have exactly one input.";
    CHECK_EQ(net_->num_outputs(), 1) << "Network must have exactly one output.";

    Blob* input_layer = net_->input_blobs()[0];
    num_channels_ = input_layer->channels();
    CHECK(num_channels_ == 3 || num_channels_ == 1) 
        << "Input layer must have 1 or 3 channels.";
    input_geometry_ = cv::Size(input_layer->width(), input_layer->height());

    // 載入均值文件
    SetMean(mean_file);

    // 載入標籤
    std::ifstream labels(label_file);
    CHECK(labels) << "Unable to open labels file " << label_file;
    string line;
    while (std::getline(labels, line)) labels_.push_back(line);

    Blob* output_layer = net_->output_blobs()[0];
    CHECK_EQ(labels_.size(), output_layer->channels())
        << "Label count differs from output layer dimension.";
}

均值文件處理 (BinaryProto 格式)

void Classifier::SetMean(const string& mean_file) {
    BlobProto blob_proto;
    ReadProtoFromBinaryFileOrDie(mean_file.c_str(), &blob_proto);
    
    Blob mean_blob;
    mean_blob.FromProto(blob_proto);
    CHECK_EQ(mean_blob.channels(), num_channels_)
        << "Mean file channels mismatch.";

    // 將 Blob 拆分為通道
    std::vector channels;
    float* data = mean_blob.mutable_cpu_data();
    for (int i = 0; i < num_channels_; ++i) {
        cv::Mat channel(mean_blob.height(), mean_blob.width(), CV_32FC1, data);
        channels.push_back(channel);
        data += mean_blob.height() * mean_blob.width();
    }

    // 合併通道並計算全局均值像素
    cv::Mat mean;
    cv::merge(channels, mean);
    cv::Scalar channel_mean = cv::mean(mean);
    mean_ = cv::Mat(input_geometry_, mean.type(), channel_mean);
}

預處理管線

void Classifier::Preprocess(const cv::Mat& img, std::vector* input_channels) {
    cv::Mat sample;
    // 顏色空間轉換
    if (img.channels() == 3 && num_channels_ == 1)
        cv::cvtColor(img, sample, cv::COLOR_BGR2GRAY);
    else if (img.channels() == 4 && num_channels_ == 1)
        cv::cvtColor(img, sample, cv::COLOR_BGRA2GRAY);
    else if (img.channels() == 4 && num_channels_ == 3)
        cv::cvtColor(img, sample, cv::COLOR_BGRA2BGR);
    else if (img.channels() == 1 && num_channels_ == 3)
        cv::cvtColor(img, sample, cv::COLOR_GRAY2BGR);
    else
        sample = img;

    // 尺寸調整
    cv::Mat sample_resized;
    if (sample.size() != input_geometry_)
        cv::resize(sample, sample_resized, input_geometry_);
    else
        sample_resized = sample;

    // 歸一化至 float 並減去均值
    cv::Mat sample_float;
    if (num_channels_ == 3)
        sample_resized.convertTo(sample_float, CV_32FC3);
    else
        sample_resized.convertTo(sample_float, CV_32FC1);
    
    cv::Mat sample_normalized;
    cv::subtract(sample_float, mean_, sample_normalized);

    // 直接寫入輸入層(零拷貝)
    cv::split(sample_normalized, *input_channels);
    
    CHECK(reinterpret_cast(input_channels->at(0).data) 
          == net_->input_blobs()[0]->cpu_data())
        << "Input channels not wrapping input layer.";
}

前向推理與結果解析

std::vector Classifier::Predict(const cv::Mat& img) {
    Blob* input_layer = net_->input_blobs()[0];
    input_layer->Reshape(1, num_channels_, input_geometry_.height, input_geometry_.width);
    net_->Reshape();

    std::vector input_channels;
    WrapInputLayer(&input_channels);
    Preprocess(img, &input_channels);

    net_->ForwardPrefilled();

    Blob* output_layer = net_->output_blobs()[0];
    const float* begin = output_layer->cpu_data();
    const float* end = begin + output_layer->channels();
    return std::vector(begin, end);
}

std::vector> Classifier::Classify(const cv::Mat& img, int N) {
    std::vector output = Predict(img);
    
    // Argmax 取 Top-N
    std::vector> pairs;
    for (size_t i = 0; i < output.size(); ++i)
        pairs.emplace_back(output[i], i);
    std::partial_sort(pairs.begin(), pairs.begin() + N, pairs.end(),
        [](const auto& a, const auto& b) { return a.first > b.first; });

    std::vector> predictions;
    for (int i = 0; i < N; ++i) {
        int idx = pairs[i].second;
        predictions.emplace_back(labels_[idx], output[idx]);
    }
    return predictions;
}

低級 API 直接操作:更精細的控制

網絡初始化與模型加載

#include "caffe/caffe.hpp"
#include 

using namespace caffe;

int main() {
    const char* proto = "deploy.prototxt";
    const char* model = "bvlc_reference_caffenet.caffemodel";
    
    Caffe::set_mode(Caffe::CPU); // 或 GPU
    boost::shared_ptr> net(new Net(proto, TEST));
    net->CopyTrainedLayersFrom(model);
}

遍歷層結構參數

NetParameter param;
ReadNetParamsFromBinaryFileOrDie(model, ¶m);
for (int i = 0; i < param.layer_size(); ++i) {
    LOG(ERROR) << "Layer " << i << ": " << param.layer(i).name() 
               << "\t" << param.layer(i).type();
    if (param.layer(i).type() == "Convolution") {
        ConvolutionParameter conv_param = param.layer(i).convolution_param();
        LOG(ERROR) << "\tkernel: " << conv_param.kernel_size()
                   << ", pad: " << conv_param.pad()
                   << ", stride: " << conv_param.stride();
    }
}

前向傳播與特徵提取

> blob = net->blobs()[blob_id];
const float* feat_ptr = blob->cpu_data(); // NCHW 格式

權重讀取與修改

> layer = net->layers()[layer_id];
std::vector>> blobs = layer->blobs();
const float* weight = blobs[0]->cpu_data();
const float* bias = blobs.size() > 1 ? blobs[1]->cpu_data() : nullptr;

// 修改權重(如微調、量化)
float* weight_ptr = nullptr;
switch (Caffe::mode()) {
    case Caffe::CPU: weight_ptr = blobs[0]->mutable_cpu_data(); break;
    case Caffe::GPU: weight_ptr = blobs[0]->mutable_gpu_data(); break;
}
caffe_copy(blobs[0]->count(), new_weight_data, weight_ptr);

// 保存新模型
NetParameter net_param;
net->ToProto(&net_param, false);
WriteProtoToBinaryFile(net_param, "new_model.caffemodel");

關鍵最佳實踐總結

場景推薦做法
分類任務使用 `Classifier` 封裝類,自動處理預處理、Top-N 排序
特徵提取直接操作 `Net`,通過 `blob_names()` 定位中間層
模型手術讀取 `NetParameter`,修改層參數,重新序列化
批量推理設置 `input_layer->Reshape(batch_size, ...)`,一次 Forward 多圖
GPU/CPU 切換`Caffe::set_mode()` + 對應的 `mutable_cpu/gpu_data()`

常見坑點避雷

  1. 均值文件格式:必須是 BinaryProto,非 NPY;通道順序為 BGR
  2. 輸入尺寸:必須與 `deploy.prototxt` 定義的 `input_dim` 完全一致
  3. 通道數匹配:模型輸入通道數與圖像讀取通道數必須一致(灰度圖需擴展)
  4. 標籤行數:`labels.txt` 行數必須等於輸出層通道數
  5. 內存佈局:Caffe Blob 為 NCHW,OpenCV Mat 為 HWC,`cv::split` 實現零拷貝轉換

總結

Caffe C++ API 提供了從高層封裝到低層直接操作的完整譜系。`Classifier` 類適合快速集成分類任務;直接操作 `Net`、`Blob`、`Layer` 適合特徵提取、模型分析、權重手術等進階場景。掌握「模型加載 → 預處理管線 → 前向傳播 → 結果解析」這條主線,並理解 NCHW 內存佈局與零拷貝技巧,是高效使用 Caffe C++ 接口的關鍵。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved