Skip to content

Commit 4874c01

Browse files
committed
Add a simple C++ classification example
Closes #2487 Example usage: ./build/examples/cpp_classification/classification.bin \ models/bvlc_reference_caffenet/deploy.prototxt \ models/bvlc_reference_caffenet/bvlc_reference_caffenet.caffemodel \ data/ilsvrc12/imagenet_mean.binaryproto \ data/ilsvrc12/synset_words.txt \ examples/images/cat.jpg
1 parent 8df472a commit 4874c01

2 files changed

Lines changed: 332 additions & 0 deletions

File tree

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#include <caffe/caffe.hpp>
2+
#include <opencv2/core/core.hpp>
3+
#include <opencv2/highgui/highgui.hpp>
4+
#include <opencv2/imgproc/imgproc.hpp>
5+
#include <iosfwd>
6+
#include <memory>
7+
#include <string>
8+
#include <utility>
9+
#include <vector>
10+
11+
using namespace caffe; // NOLINT(build/namespaces)
12+
using std::string;
13+
14+
/* Pair (label, confidence) representing a prediction. */
15+
typedef std::pair<string, float> Prediction;
16+
17+
class Classifier {
18+
public:
19+
Classifier(const string& model_file,
20+
const string& trained_file,
21+
const string& mean_file,
22+
const string& label_file);
23+
24+
std::vector<Prediction> Classify(const cv::Mat& img, int N = 5);
25+
26+
private:
27+
void SetMean(const string& mean_file);
28+
29+
std::vector<float> Predict(const cv::Mat& img);
30+
31+
void WrapInputLayer(std::vector<cv::Mat>* input_channels);
32+
33+
void Preprocess(const cv::Mat& img,
34+
std::vector<cv::Mat>* input_channels);
35+
36+
private:
37+
shared_ptr<Net<float> > net_;
38+
cv::Size input_geometry_;
39+
int num_channels_;
40+
cv::Mat mean_;
41+
std::vector<string> labels_;
42+
};
43+
44+
Classifier::Classifier(const string& model_file,
45+
const string& trained_file,
46+
const string& mean_file,
47+
const string& label_file) {
48+
#ifdef CPU_ONLY
49+
Caffe::set_mode(Caffe::CPU);
50+
#else
51+
Caffe::set_mode(Caffe::GPU);
52+
#endif
53+
54+
/* Load the network. */
55+
net_.reset(new Net<float>(model_file, TEST));
56+
net_->CopyTrainedLayersFrom(trained_file);
57+
58+
CHECK_EQ(net_->num_inputs(), 1) << "Network should have exactly one input.";
59+
CHECK_EQ(net_->num_outputs(), 1) << "Network should have exactly one output.";
60+
61+
Blob<float>* input_layer = net_->input_blobs()[0];
62+
num_channels_ = input_layer->channels();
63+
CHECK(num_channels_ == 3 || num_channels_ == 1)
64+
<< "Input layer should have 1 or 3 channels.";
65+
input_geometry_ = cv::Size(input_layer->width(), input_layer->height());
66+
67+
/* Load the binaryproto mean file. */
68+
SetMean(mean_file);
69+
70+
/* Load labels. */
71+
std::ifstream labels(label_file.c_str());
72+
CHECK(labels) << "Unable to open labels file " << label_file;
73+
string line;
74+
while (std::getline(labels, line))
75+
labels_.push_back(string(line));
76+
77+
Blob<float>* output_layer = net_->output_blobs()[0];
78+
CHECK_EQ(labels_.size(), output_layer->channels())
79+
<< "Number of labels is different from the output layer dimension.";
80+
}
81+
82+
static bool PairCompare(const std::pair<float, int>& lhs,
83+
const std::pair<float, int>& rhs) {
84+
return lhs.first > rhs.first;
85+
}
86+
87+
/* Return the indices of the top N values of vector v. */
88+
static std::vector<int> Argmax(const std::vector<float>& v, int N) {
89+
std::vector<std::pair<float, int> > pairs;
90+
for (size_t i = 0; i < v.size(); ++i)
91+
pairs.push_back(std::make_pair(v[i], i));
92+
std::partial_sort(pairs.begin(), pairs.begin() + N, pairs.end(), PairCompare);
93+
94+
std::vector<int> result;
95+
for (int i = 0; i < N; ++i)
96+
result.push_back(pairs[i].second);
97+
return result;
98+
}
99+
100+
/* Return the top N predictions. */
101+
std::vector<Prediction> Classifier::Classify(const cv::Mat& img, int N) {
102+
std::vector<float> output = Predict(img);
103+
104+
std::vector<int> maxN = Argmax(output, N);
105+
std::vector<Prediction> predictions;
106+
for (int i = 0; i < N; ++i) {
107+
int idx = maxN[i];
108+
predictions.push_back(std::make_pair(labels_[idx], output[idx]));
109+
}
110+
111+
return predictions;
112+
}
113+
114+
/* Load the mean file in binaryproto format. */
115+
void Classifier::SetMean(const string& mean_file) {
116+
BlobProto blob_proto;
117+
ReadProtoFromBinaryFileOrDie(mean_file.c_str(), &blob_proto);
118+
119+
/* Convert from BlobProto to Blob<float> */
120+
Blob<float> mean_blob;
121+
mean_blob.FromProto(blob_proto);
122+
CHECK_EQ(mean_blob.channels(), num_channels_)
123+
<< "Number of channels of mean file doesn't match input layer.";
124+
125+
/* The format of the mean file is planar 32-bit float BGR or grayscale. */
126+
std::vector<cv::Mat> channels;
127+
float* data = mean_blob.mutable_cpu_data();
128+
for (int i = 0; i < num_channels_; ++i) {
129+
/* Extract an individual channel. */
130+
cv::Mat channel(mean_blob.height(), mean_blob.width(), CV_32FC1, data);
131+
channels.push_back(channel);
132+
data += mean_blob.height() * mean_blob.width();
133+
}
134+
135+
/* Merge the separate channels into a single image. */
136+
cv::Mat mean;
137+
cv::merge(channels, mean);
138+
139+
/* Compute the global mean pixel value and create a mean image
140+
* filled with this value. */
141+
cv::Scalar channel_mean = cv::mean(mean);
142+
mean_ = cv::Mat(input_geometry_, mean.type(), channel_mean);
143+
}
144+
145+
std::vector<float> Classifier::Predict(const cv::Mat& img) {
146+
Blob<float>* input_layer = net_->input_blobs()[0];
147+
input_layer->Reshape(1, num_channels_,
148+
input_geometry_.height, input_geometry_.width);
149+
/* Forward dimension change to all layers. */
150+
net_->Reshape();
151+
152+
std::vector<cv::Mat> input_channels;
153+
WrapInputLayer(&input_channels);
154+
155+
Preprocess(img, &input_channels);
156+
157+
net_->ForwardPrefilled();
158+
159+
/* Copy the output layer to a std::vector */
160+
Blob<float>* output_layer = net_->output_blobs()[0];
161+
const float* begin = output_layer->cpu_data();
162+
const float* end = begin + output_layer->channels();
163+
return std::vector<float>(begin, end);
164+
}
165+
166+
/* Wrap the input layer of the network in separate cv::Mat objects
167+
* (one per channel). This way we save one memcpy operation and we
168+
* don't need to rely on cudaMemcpy2D. The last preprocessing
169+
* operation will write the separate channels directly to the input
170+
* layer. */
171+
void Classifier::WrapInputLayer(std::vector<cv::Mat>* input_channels) {
172+
Blob<float>* input_layer = net_->input_blobs()[0];
173+
174+
int width = input_layer->width();
175+
int height = input_layer->height();
176+
float* input_data = input_layer->mutable_cpu_data();
177+
for (int i = 0; i < input_layer->channels(); ++i) {
178+
cv::Mat channel(height, width, CV_32FC1, input_data);
179+
input_channels->push_back(channel);
180+
input_data += width * height;
181+
}
182+
}
183+
184+
void Classifier::Preprocess(const cv::Mat& img,
185+
std::vector<cv::Mat>* input_channels) {
186+
/* Convert the input image to the input image format of the network. */
187+
cv::Mat sample;
188+
if (img.channels() == 3 && num_channels_ == 1)
189+
cv::cvtColor(img, sample, CV_BGR2GRAY);
190+
else if (img.channels() == 4 && num_channels_ == 1)
191+
cv::cvtColor(img, sample, CV_BGRA2GRAY);
192+
else if (img.channels() == 4 && num_channels_ == 3)
193+
cv::cvtColor(img, sample, CV_BGRA2BGR);
194+
else if (img.channels() == 1 && num_channels_ == 3)
195+
cv::cvtColor(img, sample, CV_GRAY2BGR);
196+
else
197+
sample = img;
198+
199+
cv::Mat sample_resized;
200+
if (sample.size() != input_geometry_)
201+
cv::resize(sample, sample_resized, input_geometry_);
202+
else
203+
sample_resized = sample;
204+
205+
cv::Mat sample_float;
206+
if (num_channels_ == 3)
207+
sample_resized.convertTo(sample_float, CV_32FC3);
208+
else
209+
sample_resized.convertTo(sample_float, CV_32FC1);
210+
211+
cv::Mat sample_normalized;
212+
cv::subtract(sample_float, mean_, sample_normalized);
213+
214+
/* This operation will write the separate BGR planes directly to the
215+
* input layer of the network because it is wrapped by the cv::Mat
216+
* objects in input_channels. */
217+
cv::split(sample_normalized, *input_channels);
218+
219+
CHECK(reinterpret_cast<float*>(input_channels->at(0).data)
220+
== net_->input_blobs()[0]->cpu_data())
221+
<< "Input channels are not wrapping the input layer of the network.";
222+
}
223+
224+
int main(int argc, char** argv) {
225+
if (argc != 6) {
226+
std::cerr << "Usage: " << argv[0]
227+
<< " deploy.prototxt network.caffemodel"
228+
<< " mean.binaryproto labels.txt img.jpg" << std::endl;
229+
return 1;
230+
}
231+
232+
::google::InitGoogleLogging(argv[0]);
233+
234+
string model_file = argv[1];
235+
string trained_file = argv[2];
236+
string mean_file = argv[3];
237+
string label_file = argv[4];
238+
Classifier classifier(model_file, trained_file, mean_file, label_file);
239+
240+
string file = argv[5];
241+
242+
std::cout << "---------- Prediction for "
243+
<< file << " ----------" << std::endl;
244+
245+
cv::Mat img = cv::imread(file, -1);
246+
CHECK(!img.empty()) << "Unable to decode image " << file;
247+
std::vector<Prediction> predictions = classifier.Classify(img);
248+
249+
/* Print the top N predictions. */
250+
for (size_t i = 0; i < predictions.size(); ++i) {
251+
Prediction p = predictions[i];
252+
std::cout << std::fixed << std::setprecision(4) << p.second << " - \""
253+
<< p.first << "\"" << std::endl;
254+
}
255+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
title: CaffeNet C++ Classification example
3+
description: A simple example performing image classification using the low-level C++ API.
4+
category: example
5+
include_in_docs: true
6+
priority: 10
7+
---
8+
9+
# Classifying ImageNet: using the C++ API
10+
11+
Caffe, at its core, is written in C++. It is possible to use the C++
12+
API of Caffe to implement an image classification application similar
13+
to the Python code presented in one of the Notebook example. To look
14+
at a more general-purpose example of the Caffe C++ API, you should
15+
study the source code of the command line tool `caffe` in `tools/caffe.cpp`.
16+
17+
## Presentation
18+
19+
A simple C++ code is proposed in
20+
`examples/cpp_classification/classification.cpp`. For the sake of
21+
simplicity, this example does not support oversampling of a single
22+
sample nor batching of multiple independant samples. This example is
23+
not trying to reach the maximum possible classification throughput on
24+
a system, but special care was given to avoid unnecessary
25+
pessimization while keeping the code readable.
26+
27+
## Compiling
28+
29+
The C++ example is built automatically when compiling Caffe. To
30+
compile Caffe you should follow the documented instructions. The
31+
classification example will be built as `examples/classification.bin`
32+
in your build directory.
33+
34+
## Usage
35+
36+
To use the pre-trained CaffeNet model with the classification example,
37+
you need to download it from the "Model Zoo" using the following
38+
script:
39+
```
40+
./scripts/download_model_binary.py models/bvlc_reference_caffenet
41+
```
42+
The ImageNet labels file (also called the *synset file*) is also
43+
required in order to map a prediction to the name of the class:
44+
```
45+
./data/ilsvrc12/get_ilsvrc_aux.sh.
46+
```
47+
Using the files that were downloaded, we can classify the provided cat
48+
image (`examples/images/cat.jpg`) using this command:
49+
```
50+
./build/examples/cpp_classification/classification.bin \
51+
models/bvlc_reference_caffenet/deploy.prototxt \
52+
models/bvlc_reference_caffenet/bvlc_reference_caffenet.caffemodel \
53+
data/ilsvrc12/imagenet_mean.binaryproto \
54+
data/ilsvrc12/synset_words.txt \
55+
examples/images/cat.jpg
56+
```
57+
The output should look like this:
58+
```
59+
---------- Prediction for examples/images/cat.jpg ----------
60+
0.3134 - "n02123045 tabby, tabby cat"
61+
0.2380 - "n02123159 tiger cat"
62+
0.1235 - "n02124075 Egyptian cat"
63+
0.1003 - "n02119022 red fox, Vulpes vulpes"
64+
0.0715 - "n02127052 lynx, catamount"
65+
```
66+
67+
## Improving Performance
68+
69+
To further improve performance, you will need to leverage the GPU
70+
more, here are some guidelines:
71+
72+
* Move the data on the GPU early and perform all preprocessing
73+
operations there.
74+
* If you have many images to classify simultaneously, you should use
75+
batching (independent images are classified in a single forward pass).
76+
* Use multiple classification threads to ensure the GPU is always fully
77+
utilized and not waiting for an I/O blocked CPU thread.

0 commit comments

Comments
 (0)