【问题标题】:Python real time image classification problems with Neural Networks神经网络的 Python 实时图像分类问题
【发布时间】:2017-01-24 03:55:42
【问题描述】:

我正在尝试使用 caffe 和 python 进行实时图像分类。我在一个进程中使用 OpenCV 从我的网络摄像头流式传输,在一个单独的进程中,使用 caffe 对从网络摄像头拉取的帧执行图像分类。然后我将分类结果传回主线程,为网络摄像头流添加字幕。

问题是,即使我有一个 NVIDIA GPU 并且正在 GPU 上执行 caffe 预测,主线程也会变慢。通常不做任何预测,我的网络摄像头流以 30 fps 运行;但是,根据预测,我的网络摄像头流最多可以达到 15 fps。

我已验证 caffe 在执行预测时确实使用了 GPU,并且我的 GPU 或 GPU 内存没有达到最大值。我还验证了我的 CPU 内核在程序期间的任何时候都没有达到最大值。我想知道我是否做错了什么,或者是否没有办法让这两个过程真正分开。任何建议表示赞赏。这是我的参考代码

class Consumer(multiprocessing.Process):

    def __init__(self, task_queue, result_queue):
        multiprocessing.Process.__init__(self)
        self.task_queue = task_queue
        self.result_queue = result_queue
        #other initialization stuff

    def run(self):
        caffe.set_mode_gpu()
        caffe.set_device(0)
        #Load caffe net -- code omitted 
        while True:
            image = self.task_queue.get()
            #crop image -- code omitted
            text = net.predict(image)
            self.result_queue.put(text)

        return

import cv2
import caffe
import multiprocessing
import Queue 

tasks = multiprocessing.Queue()
results = multiprocessing.Queue()
consumer = Consumer(tasks,results)
consumer.start()

#Creating window and starting video capturer from camera
cv2.namedWindow("preview")
vc = cv2.VideoCapture(0)
#Try to get the first frame
if vc.isOpened():
    rval, frame = vc.read()
else:
    rval = False
frame_copy[:] = frame
task_empty = True
while rval:
    if task_empty:
       tasks.put(frame_copy)
       task_empty = False
    if not results.empty():
       text = results.get()
       #Add text to frame
       cv2.putText(frame,text)
       task_empty = True

    #Showing the frame with all the applied modifications
    cv2.imshow("preview", frame)

    #Getting next frame from camera
    rval, frame = vc.read()
    frame_copy[:] = frame
    #Getting keyboard input 
    key = cv2.waitKey(1)
    #exit on ESC
    if key == 27:
        break

我很确定这是 caffe 预测减慢了一切,因为当我注释掉预测并在进程之间来回传递虚拟文本时,我再次获得 30 fps。

class Consumer(multiprocessing.Process):

    def __init__(self, task_queue, result_queue):
        multiprocessing.Process.__init__(self)
        self.task_queue = task_queue
        self.result_queue = result_queue
        #other initialization stuff

    def run(self):
        caffe.set_mode_gpu()
        caffe.set_device(0)
        #Load caffe net -- code omitted
        while True:
            image = self.task_queue.get()
            #crop image -- code omitted
            #text = net.predict(image)
            text = "dummy text"
            self.result_queue.put(text)

        return

import cv2
import caffe
import multiprocessing
import Queue 

tasks = multiprocessing.Queue()
results = multiprocessing.Queue()
consumer = Consumer(tasks,results)
consumer.start()

#Creating window and starting video capturer from camera
cv2.namedWindow("preview")
vc = cv2.VideoCapture(0)
#Try to get the first frame
if vc.isOpened():
    rval, frame = vc.read()
else:
    rval = False
frame_copy[:] = frame
task_empty = True
while rval:
    if task_empty:
       tasks.put(frame_copy)
       task_empty = False
    if not results.empty():
       text = results.get()
       #Add text to frame
       cv2.putText(frame,text)
       task_empty = True

    #Showing the frame with all the applied modifications
    cv2.imshow("preview", frame)

    #Getting next frame from camera
    rval, frame = vc.read()
    frame_copy[:] = frame
    #Getting keyboard input 
    key = cv2.waitKey(1)
    #exit on ESC
    if key == 27:
        break

【问题讨论】:

  • 您是否为代码的各个块计时? CPU 和 GPU 之间的数据传输可能会导致大量开销。
  • 我怎么知道传输是否是导致它变慢的原因?这里没有从 GPU 传输到 CPU 的显式代码
  • 您是否尝试将net.predict(image) 替换为使用大量CPU 的代码与预测时间大致相同?例如,for i in range(10000000): pass 在我的机器上大约需要 0.22 秒。对于我的机器和网络摄像头,您的代码以这种方式以 30 fps 的速度运行。
  • 但是预测应该发生在 GPU 上吧?那么为什么在这种情况下增加 CPU 使用率会有所帮助呢?有点迷茫
  • 我使用 cuda-convnet 进行 非实时 视频分析,并且 CPU 和 GPU 负载不错。不过,我还没有分析 CPU 使用情况是什么部分是我,什么是 cuda-convnet。不过,我使用了批处理,直观地讲,单帧可能会导致更多的 CPU 开销。但我的直觉可能是错误的。 :)

标签: python multiprocessing deep-learning caffe gpgpu


【解决方案1】:

一些解释和一些反思:

  1. 我在带有Intel Core i5-6300HQ @2.3GHz cpu、8 GB RAMNVIDIA GeForce GTX 960M gpu(2GB 内存)的笔记本电脑上运行了下面的代码,结果是:

    无论我是否在运行 caffe 的情况下运行代码(通过注释掉 net_output = this->net_->Forward(net_input)void Consumer::entry() 中的一些必要内容),我总能在主线程中获得大约 30 fps。

    在具有Intel Core i5-4440 cpu、8 GB RAMNVIDIA GeForce GT 630 gpu(1GB 内存)的 PC 上得到了类似的结果。

  2. 我在同一台笔记本电脑上运行了问题中@user3543300的代码,结果是:

    无论 caffe 是否正在运行(在 gpu 上),我也可以达到 30 fps 左右。

  3. 根据@user3543300 的反馈,使用上述两个版本的代码,@user3543300 在运行 caffe 时(在Nvidia GeForce 940MX GPU and Intel® Core™ i7-6500U CPU @ 2.50GHz × 4 笔记本电脑上)只能获得 15 fps 左右。 并且当 caffe 作为独立程序在 gpu 上运行时,网络摄像头的帧率也会有所下降。

所以我仍然认为问题很可能在于硬件 I/O 限制,例如 DMA 带宽(关于 DMA 的这个线程可能会暗示。)或 RAM 带宽。 希望@user3543300可以检查一下或者找出我没有意识到的真正问题。

如果问题确实是我上面所想的,那么一个明智的想法是减少 CNN 网络引入的内存 I/O 开销。事实上,为了解决硬件资源有限的嵌入式系统上的类似问题,已经有一些关于这个主题的研究,例如QautizationStructurally Sparse Deep Neural NetworksSqueezeNetDeep-Compression。因此,希望通过应用这些技巧也有助于提高问题中网络摄像头的帧率。


原答案:

试试这个 c++ 解决方案。它在您的任务中使用I/O overhead 的线程,我使用bvlc_alexnet.caffemodeldeploy.prototxt 对其进行了图像分类测试,并且在caffe 运行时(在GPU 上)没有看到主线程(网络摄像头流)明显减慢:

#include <stdio.h>
#include <iostream>
#include <string>
#include <boost/thread.hpp>
#include <boost/shared_ptr.hpp>
#include "caffe/caffe.hpp"
#include "caffe/util/blocking_queue.hpp"
#include "caffe/data_transformer.hpp"
#include "opencv2/opencv.hpp"

using namespace cv;

//Queue pair for sharing image/results between webcam and caffe threads
template<typename T>
class QueuePair {
  public:
    explicit QueuePair(int size);
    ~QueuePair();

    caffe::BlockingQueue<T*> free_;
    caffe::BlockingQueue<T*> full_;

  DISABLE_COPY_AND_ASSIGN(QueuePair);
};
template<typename T>
QueuePair<T>::QueuePair(int size) {
  // Initialize the free queue
  for (int i = 0; i < size; ++i) {
    free_.push(new T);
  }
}
template<typename T>
QueuePair<T>::~QueuePair(){
  T *data;
  while (free_.try_pop(&data)){
    delete data;
  }
  while (full_.try_pop(&data)){
    delete data;
  }
}
template class QueuePair<Mat>;
template class QueuePair<std::string>;

//Do image classification(caffe predict) using a subthread
class Consumer{
  public:
    Consumer(boost::shared_ptr<QueuePair<Mat>> task
           , boost::shared_ptr<QueuePair<std::string>> result);
    ~Consumer();
    void Run();
    void Stop();
    void entry(boost::shared_ptr<QueuePair<Mat>> task
             , boost::shared_ptr<QueuePair<std::string>> result);

  private:
    bool must_stop();

    boost::shared_ptr<QueuePair<Mat> > task_q_;
    boost::shared_ptr<QueuePair<std::string> > result_q_;

    //caffe::Blob<float> *net_input_blob_;
    boost::shared_ptr<caffe::DataTransformer<float> > data_transformer_;
    boost::shared_ptr<caffe::Net<float> > net_;
    std::vector<std::string> synset_words_;
    boost::shared_ptr<boost::thread> thread_;
};
Consumer::Consumer(boost::shared_ptr<QueuePair<Mat>> task
                 , boost::shared_ptr<QueuePair<std::string>> result) :
 task_q_(task), result_q_(result), thread_(){

  //for data preprocess
  caffe::TransformationParameter trans_para;
  //set mean
  trans_para.set_mean_file("/path/to/imagenet_mean.binaryproto");
  //set crop size, here is cropping 227x227 from 256x256
  trans_para.set_crop_size(227);
  //instantiate a DataTransformer using trans_para for image preprocess
  data_transformer_.reset(new caffe::DataTransformer<float>(trans_para
                        , caffe::TEST));

  //initialize a caffe net
  net_.reset(new caffe::Net<float>(std::string("/path/to/deploy.prototxt")
           , caffe::TEST));
  //net parameter
  net_->CopyTrainedLayersFrom(std::string("/path/to/bvlc_alexnet.caffemodel"));

  std::fstream synset_word("path/to/caffe/data/ilsvrc12/synset_words.txt");
  std::string line;
  if (!synset_word.good()){
    std::cerr << "synset words open failed!" << std::endl;
  }
  while (std::getline(synset_word, line)){
    synset_words_.push_back(line.substr(line.find_first_of(' '), line.length()));
  }
  //a container for net input, holds data converted from cv::Mat
  //net_input_blob_ = new caffe::Blob<float>(1, 3, 227, 227);
}
Consumer::~Consumer(){
  Stop();
  //delete net_input_blob_;
}
void Consumer::entry(boost::shared_ptr<QueuePair<Mat>> task
    , boost::shared_ptr<QueuePair<std::string>> result){

  caffe::Caffe::set_mode(caffe::Caffe::GPU);
  caffe::Caffe::SetDevice(0);

  cv::Mat *frame;
  cv::Mat resized_image(256, 256, CV_8UC3);
  cv::Size re_size(resized_image.cols, resized_image.rows);

  //for caffe input and output
  const std::vector<caffe::Blob<float> *> net_input = this->net_->input_blobs();
  std::vector<caffe::Blob<float> *> net_output;

  //net_input.push_back(net_input_blob_);
  std::string *res;

  int pre_num = 1;
  while (!must_stop()){
    std::stringstream result_strm;
    frame = task->full_.pop();
    cv::resize(*frame, resized_image, re_size, 0, 0, CV_INTER_LINEAR);
    this->data_transformer_->Transform(resized_image, *net_input[0]);
    net_output = this->net_->Forward();
    task->free_.push(frame);

    res = result->free_.pop();
    //Process results here
    for (int i = 0; i < pre_num; ++i){
      result_strm << synset_words_[net_output[0]->cpu_data()[i]] << " " 
                  << net_output[0]->cpu_data()[i + pre_num] << "\n";
    }
    *res = result_strm.str();
    result->full_.push(res);
  }
}

void Consumer::Run(){
  if (!thread_){
    try{
      thread_.reset(new boost::thread(&Consumer::entry, this, task_q_, result_q_));
    }
    catch (std::exception& e) {
      std::cerr << "Thread exception: " << e.what() << std::endl;
    }
  }
  else
    std::cout << "Consumer thread may have been running!" << std::endl;
};
void Consumer::Stop(){
  if (thread_ && thread_->joinable()){
    thread_->interrupt();
    try {
      thread_->join();
    }
    catch (boost::thread_interrupted&) {
    }
    catch (std::exception& e) {
      std::cerr << "Thread exception: " << e.what() << std::endl;
    }
  }
}
bool Consumer::must_stop(){
  return thread_ && thread_->interruption_requested();
}


int main(void)
{
  int max_queue_size = 1000;
  boost::shared_ptr<QueuePair<Mat>> tasks(new QueuePair<Mat>(max_queue_size));
  boost::shared_ptr<QueuePair<std::string>> results(new QueuePair<std::string>(max_queue_size));

  char str[100], info_str[100] = " results: ";
  VideoCapture vc(0);
  if (!vc.isOpened())
    return -1;

  Consumer consumer(tasks, results);
  consumer.Run();

  Mat frame, *frame_copy;
  namedWindow("preview");
  double t, fps;

  while (true){
    t = (double)getTickCount();
    vc.read(frame);

    if (waitKey(1) >= 0){
      consuer.Stop();
      break;
    }

    if (tasks->free_.try_peek(&frame_copy)){
      frame_copy = tasks->free_.pop();
      *frame_copy = frame.clone();
      tasks->full_.push(frame_copy);
    }
    std::string *res;
    std::string frame_info("");
    if (results->full_.try_peek(&res)){
      res = results->full_.pop();
      frame_info = frame_info + info_str;
      frame_info = frame_info + *res;
      results->free_.push(res);
    }    

    t = ((double)getTickCount() - t) / getTickFrequency();
    fps = 1.0 / t;

    sprintf(str, " fps: %.2f", fps);
    frame_info = frame_info + str;

    putText(frame, frame_info, Point(5, 20)
         , FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0));
    imshow("preview", frame);
  }
}

src/caffe/util/blocking_queue.cpp,在下面做一些小改动并重建caffe:

...//Other stuff
template class BlockingQueue<Batch<float>*>;
template class BlockingQueue<Batch<double>*>;
template class BlockingQueue<Datum*>;
template class BlockingQueue<shared_ptr<DataReader::QueuePair> >;
template class BlockingQueue<P2PSync<float>*>;
template class BlockingQueue<P2PSync<double>*>;
//add these 2 lines below
template class BlockingQueue<cv::Mat*>;
template class BlockingQueue<std::string*>;

【讨论】:

  • 这看起来很有趣。我会尝试并报告回来。只有一个问题,我如何将cv::Mat 作为输入传递给 C++ 中的 caffe 网络。另外,当我调用预训练网络时,raw_scalechannel_swap 是否有任何参数,就像在 python 中一样?我以前从未使用过 C++ caffe。
  • @user3543300 data_transformer.cpp 中的接口DataTransformer&lt;Dtype&gt;::Transform(const cv::Mat&amp; cv_img, Blob&lt;Dtype&gt;* transformed_blob) 会将cv::Mat 转换为caffe::Blob 对象,该对象将通过调用Net::Forward( const vector&lt;Blob&lt;Dtype&gt;*&gt; &amp; bottom, Dtype* loss) 作为caffe 网络的输入。 DataTransformer::Transform() 会自动在其中执行channel_swap predure,但是如果要将图像数据从[0,255] 标准化到[0,1],您应该使用caffe::DataTransformer 中的成员函数set_scale(float value) 显式设置比例。跨度>
  • 我有点困惑,但是在 python 中我是这样做的:net = caffe.Classifier(net_model_file,net_pretrained, mean=mean, channel_swap=(2,1,0), raw_scale=255, image_dims=(256, 256)) 你是说这一切都是自动完成的吗?
  • 我运行了代码,我的 fps 再次降低到 15 左右。不知道发生了什么。我有一个 Nvidia GeForce 940MX GPU 和 Intel® Core™ i7-6500U CPU @ 2.50GHz × 4
  • @user3543300 重要的是 GPU 内存带宽吗?
【解决方案2】:

似乎 caffe 的 python 包装器阻止了Global Interpreter Lock (GIL)。因此调用任何 caffe python 命令会阻塞 ALL python 线程。

一种解决方法(风险自负)是为特定的 caffe 功能禁用 GIL。例如,如果您希望能够在没有锁定的情况下运行forward,您可以编辑$CAFFE_ROOT/python/caffe/_caffe.cpp。添加此功能:

void Net_Forward(Net<Dtype>& net, int start, int end) {
  Py_BEGIN_ALLOW_THREADS;   // <-- disable GIL
  net.ForwardFromTo(start, end);
  Py_END_ALLOW_THREADS;     // <-- restore GIL
}

并将.def("_forward", &amp;Net&lt;Dtype&gt;::ForwardFromTo) 替换为:

.def("_forward", &Net_Forward)

更改后别忘了make pycaffe

更多详情请见this

【讨论】:

  • GIL 是否适用于多处理。因为我在这个示例程序中使用的是多处理而不是多线程。
  • @user3543300 我真的不知道。我使用多线程而不是多处理。我也观察到多处理的类似行为,但没有在多处理条件下检查此解决方案。
【解决方案3】:

您的代码中可能会发生一种想法,即它在第一次调用时在 gpu 模式下工作,而在以后的调用中,它会在 cpu 模式下计算分类,因为它是默认模式。在旧版本的 caffe 上设置 gpu 模式一次就足够了,现在新版本需要每次设置模式。您可以尝试以下更改:

def run(self):

        #Load caffe net -- code omitted 
        while True:
            caffe.set_mode_gpu()
            caffe.set_device(0)
            image = self.task_queue.get()
            #crop image -- code omitted
            text = net.predict(image)
            self.result_queue.put(text)

        return

另外,请查看消费者线程运行时的 gpu 计时。您可以对 nvidia 使用以下命令:

nvidia-smi

上面的命令会告诉你运行时的 gpu 利用率。

如果没有解决另一种解决方法是,在一个线程下制作opencv帧提取代码。由于它与 I/O 和设备访问有关,因此您可能会在与 GUI 线程/主线程不同的线程上运行它。该线程将在队列中推送帧,当前消费者线程将进行预测。在这种情况下,请小心处理带有关键块的队列。

【讨论】:

  • 我尝试了您的两个建议,但没有看到任何改进。在每次显式调用set_mode_gpu 后,我使用 nvidia x 服务器设置(在 ubuntu 上)查看 gpu 利用率,并看到 gpu 利用率跃升至 99%。但是,我按照您的建议让我的帧提取一个进程和 GUI 显示另一个进程(这些都不是主程序),并且没有看到任何性能提升。事实上,我认为我的 cpu 使用率可能略有上升。
  • gpu中单帧分类需要多少时间?
  • 大约 0.15 秒
  • 每次预测需要 0.15 秒,因此每秒处理的帧数不能超过 6 帧。尽管您使用线程进行预测,但如果您接近每秒处理 30 帧,它将有持续的延迟。我不确定你是否使用 cudnn。如果没有,您可以使用它。它比 GPU 模式更快。
  • 另一种方法可以使其更快,您可以批量处理。假设您在故意延迟 0.5 秒后开始显示视频。您可以在一秒钟内拆分 3 个批处理操作,每个批处理可以处理 10 帧。这可能比单帧花费更多时间,但肯定会比单 * n 倍快。如果您在 0.5 秒后开始延迟显示,如果处理一个批次需要 300 毫秒,那么您将在开始显示帧时处理 10 帧...
【解决方案4】:

尝试多线程方法而不是多处理。生成进程比生成线程慢。一旦他们运行,没有太大的区别。在您的情况下,我认为线程方法将受益,因为涉及的帧数据如此之多。

【讨论】:

猜你喜欢
  • 2017-12-21
  • 2019-03-28
  • 2021-12-25
  • 2015-04-18
  • 2015-06-02
  • 2020-11-08
  • 2012-07-15
  • 2011-02-22
  • 2012-08-05
相关资源
最近更新 更多