Firefly开源社区

标题: 基于MobileNet-SSD的目标检测Demo(二) [打印本页]

作者: 暴走的阿Sai    时间: 2018-9-28 10:42
标题: 基于MobileNet-SSD的目标检测Demo(二)
本帖最后由 暴走的阿Sai 于 2018-9-28 10:42 编辑

上一篇文章《基于MobileNet-SSD的目标检测Demo(一)》介绍了如何在VOC数据集的基础上削减分类训练出自己的分类器,并且尝试着进一步把SSD改为SSDLite。但作为一个Demo,在RK3399上MobileNet-SSD每秒钟只能检测6-7帧,如果每次检测后再把视频内容展现出来,那么展示的视频也只有6-7帧,这样的展示效果似乎不太好。在本篇文章中,我们将尝试把视频的获取与展示和检测任务分离开来,分别放在两个不同的线程上工作,同时将不同的线程绑定到不同的cpu核上,使得两者的工作不会冲突。

进程和线程

进程和线程是操作系统中的两个重要概念。

背景

考虑在51单片机或是STM32上开发程序,通常这些程序都是串行结构。打个比方,

  1. while(1){
  2.     segmentShow(1217);
  3. }
复制代码


2. 用一个超声波模块进行测距,并且用数码管显示结果

  1. while(1){
  2.     ultrasonicTrig();
  3.     while(!ultrasonicDataValid());
  4.     segmentShow(ultrasonicGetDatum());
  5. }
复制代码


3. 超声波数量少还没关系,数码管还是能正常驱动,如果多来几个呢?(简化一下,数码管只显示求和的结果)
  1. while(1){
  2.     for(i=0; i<100; i++){
  3.         ultrasonicTrig(i);
  4.         while(!ultrasonicDataValid(i));
  5.     }
  6.     segmentShow(sum(ultrasonicgetDatum()));
  7. }
复制代码

CPU大部分时间都用去等超声波信号了呀,数码管根本就不能驱动起来,那换种方式——
  1. i=0;
  2. segment=0;
  3. sum=0;
  4. ultrasonicTrig(i);
  5. while(1){
  6.     if(ultrasonicDataValid(i)){
  7.         sum+=ultrasonicGetData(i);
  8.         ultrasonicTrig(++i);
  9.     }
  10.     if(i>=99){
  11.         segment=sum;
  12.         i=sum=0;
  13.     }
  14.     segmentShow(segment);
  15. }
复制代码

这样一来,如果超声波数据无效就继续驱动数码管,不会让CPU空等。


4. 那如果不是简单的求和运算呢?
  1. i=0;
  2. segment=0;
  3. ultras[100];
  4. ultrasonicTrig(i);
  5. while(1){
  6.     if(ultrasonicDataValid(i)){
  7.         ultras[i]=ultrasonicGetData(i);
  8.         ultrasonicTrig(++i);
  9.     }
  10.     if(i>=99){
  11.         segment=process(ultras);  // 变成了其他复杂的运算
  12.         i=0;
  13.     }
  14.     segmentShow(segment);
  15. }
复制代码


万一process函数运算过程很复杂,占用了很多CPU的时间,导致数码管不能及时刷新,那数码管依旧驱动不起来。当然,你可以去推算process运算的复杂程度,人为地去拆解运算,变成process[0]process[1]、……、process[n]最后再由combine把中间结果整合起来(注意这里要保证每个操作都足够小,不会占用太多运算时间)。


  1. i=0;
  2. i_process=0;
  3. segment=0;
  4. ultras[100];
  5. tmp[10];
  6. ultrasonicTrig(i);
  7. while(1){
  8.     if(ultrasonicDataValid(i)){
  9.         ultras[i]=ultrasonicGetData(i);
  10.         ultrasonicTrig(++i);
  11.     }
  12.     if(i>=99){
  13.         tmp[i_process]=process[i_process](ultras);
  14.         if(i_process==9){
  15.             segment=combine(tmp);
  16.             i_process=0;
  17.         }else{
  18.             i_process++;
  19.         }
  20.         i=0;
  21.     }
  22.     segmentShow(segment);
  23. }
复制代码

看起来确实可行,但是手工拆解运算,这也太恶心了吧,而且这一点都不优雅,万一我程序开发着开发着,这个运算过程发生改变了怎么办?重新拆解运算?那不得炸毛?!


5. 那换个思路,我们让两个任务分时进行吧,每个任务轮流运算10ms,超时就带上你的中间结果滚蛋
  1. // 设置定时器,每10ms中产生一次中断
  2. timer_handler(){  // 中断处理
  3.     save_metadata();    // 触发中断后保存数据
  4.     change_task();      // 切换到另一个任务
  5.     load_metadata();    // 取出新换入任务的中间数据
  6.     task_run();         // 继续任务
  7. }
  8. // ……省略任务定义
复制代码

这样虽然增加了额外的开销(任务的调度),但形成了一个通用化的任务调度功能,无论具体任务怎么改变都能够适用,减少CPU闲置的机会,可以更好的压榨CPU


实际应用当中,我们经常都能碰见这种多任务的情况,人为地分配任务给处理器需要大量的推算和分解,费时费力还不易调整。在这里,我们形成了一个简单的通用的任务调度功能,其实这也是现代操作系统的基本功能之一,对操作系统而言,这些任务就是一个个的“进程”,中间数据被称为“进程上下文”,而操作系统有一个专门的模块负责“进程调度”的工作,这里举例的固定时间片是一种最简单的调度方式。

进程






这是一张进程状态转换图,


进程上下文保存在一个特殊的称为进程控制块(PCB)的结构里,其中包含


常见的用户进程创建有两种,



  1. #include <stdio.h>
  2. int main (){
  3.      int pid, ppid;
  4.      pid = fork ();      // fork函数将派生出一个相同的进程,返回新进程的id(对于原始进程返回0)
  5.      printf ("%d first output from both processes\n", pid);
  6.      sleep (20);
  7.      if (pid > 0){
  8.          printf ("%d %s", pid, "This is the child's pid,  output by the parent process\n");
  9.      }else if (pid ==0){
  10.          printf ("%d %s", pid, "is printed inside the child process if the fork succeeds \n");
  11.          pid = getpid();    // getpid函数可以获取当前进程的id
  12.          printf ("%d %s", pid, "is the child pid printed by the child, obtained by getpid()\n");
  13.      }else
  14.          printf ("fork failed\n");
  15.      return (0);
  16. }
复制代码

进程间的通信通常由信号量、中断和共享空间实现,简单地说,

  • 信号量,事实上就是一个整型变量。操作系统负责维护一个信号量池,进程可以在该池注册带有名称的信号量,多个进程间可以对同一个信号量进行加法或减法操作(通常称为PV操作,该操作由操作系统管理,不会读写冲突),但减法的结果不能小于0,否则进程就会阻塞挂起,等待有足够的信号量可以减去。信号量有点像资源的指示标志,进程占用资源的时候作减法,释放资源的时候做加法。
  • 中断,是Linux提供的一套机制,事实上kill指令就是对目标进程发送一个特定的中断信号,进程接收到中断之后会跳转到指定的中断处理函数进行处理(当然进程也可以设置忽略某一些信号)。在Linux中可以通过指令kill -l查看可用信号,如下:



不同的信号有不同的含义,其中10号的SIGUSR1和12号的SIGUSR2是两个可以用户自定义的中断信号。



具体的实现不展开讲,因为我们接下来要用到的是线程而不是进程。
如果你对进程间通信感兴趣,也可以参考我本科期间的一个课程作业,一个 简单的本地聊天程序 (具体使用方法参见chat.c里的注释)。

线程

在单一程序中,进程的粒度似乎还是太大了,如果把一个程序的大任务细分多个小任务,明明大家都是同一个目标,却要使用独立的上下文,上下文频繁地保存和加载,这样似乎不太方便。于是就产生了粒度更小的线程,一个进程可以拥有多个线程,这些线程共享一个上下文环境。

相比于进程,


线程的通信方式比进程简单,这跟进程的“信号量+共享空间”的组合有些类似,


简单的实现方式——
  1. #include <pthread.h>

  2. pthread_mutex_t mutex;          // 线程锁(用于保护shared_variable变量)
  3. int shared_variable = 0;        // 线程间共享的全局变量

  4. void *thread_write(void *){
  5.     pthread_mutex_lock(&mutex);     // 写入前上锁(如果mutex锁住,则阻塞等待)
  6.     shared_variable++;
  7.     pthread_mutex_unlock(&mutex);   // 写入后解锁
  8. }

  9. void *thread_read(void *){
  10.     pthread_mutex_lock(&mutex);     // 读取前上锁(如果mutex锁住,则阻塞等待)
  11.     printf("%d\n", shared_variable++);
  12.     pthread_mutex_unlock(&mutex);   // 读取后解锁
  13. }

  14. int main(){
  15.     pthread_t id1, id2;

  16.     pthread_mutex_init(&mutex, NULL);   // 初始化线程锁

  17.     pthread_create(&id1, NULL, thread_write, NULL);  // 创建写线程
  18.     pthread_create(&id2, NULL, thread_read, NULL);   // 创建读线程

  19.     pthread_join(id1, NULL);    // 阻塞主线程,等待线程id1执行完毕
  20.     pthread_join(id2, NULL);    // 阻塞主线程,等待线程id2执行完毕

  21.     pthread_mutex_destroy(&mutex);    // 销毁线程锁

  22.     return 0;
  23. }
复制代码

pthread_create、pthread_join、pthread_mutex_init还可以传递其他参数,其他复杂的用法可以自行查阅资料。

处理器调度
在背景中我们提到了一种规定时间的调度方法,接下来我们也简单介绍其他一些处理器的调度策略。
假设有A-E五个进程集合,他们的启动时间和CPU占用总时间如下表所示——

进程
启动时间
CPU占用总时间
A
t=0
3
B
t=2
6
C
t=4
4
D
t=6
5
E
t=8
2

先来先服务策略(First Come First Served, FCFS)

非抢占策略,进程依次进入等待队列,先进入队列的先使用CPU,直到进程结束再从队列取出下一个进程。


轮转策略(Round Robin, RR)

一种基于时钟的抢占策略,又称为时间片策略,设定一个时间片q,每次从进程等待队列中取出一个进程执行一个时间片,如果没执行完就放回等待队列的队尾,然后从队首取出下一个进程出来执行一个时间片。假设q=1,则


注意:这里假设如果同时发生“中断”和“新服务入队”,则先将新服务入队,再交换进程。



最短进程优先(Shortest Process Next, SPN)

一种基于预计处理时间的非抢占策略,每次从等待队列中取出预计处理时间最短的进程出来执行,直到进程结束再从进程取出下一个预计处理时间最短的进程。


最短剩余时间(Shortest Remaining Time, SRT)

一种基于预计剩余处理时间的抢占策略,在SPN的基础上,当有新进程加入队列时总会估算各个进程的剩余时间,然后选择预计剩余处理时间最短的进程出来执行。


最高响应比优先(Highest Response Ratio Next, HRRN)

非抢占策略,定义一个响应比参数,<span class="MathJax" id="MathJax-Element-1-Frame" tabindex="0" data-mathml="响应比=等待时间+预计处理时间预计处理时间" role="presentation" style="box-sizing: border-box; display: inline; line-height: normal; word-spacing: normal; word-wrap: normal; white-space: nowrap; float: none; direction: ltr; max-width: none; max-height: none; min-width: 0px; min-height: 0px; border: 0px; position: relative;">响应比=等待时间+预计处理时间预计处理时间响应比=等待时间+预计处理时间预计处理时间,每次从等待队列中挑选响应比最高的进程出来执行,直到进程执行结束再从队列中取出下一个响应比最高的进程。



反馈调度

可以注意到FCFS、RR策略相对简单,不太能很好的利用CPU,而SPN、SRT、HRRN策略虽然不错,但依赖于处理时间和剩余处理时间的估计,而现实应用中这种时间估计往往是难以实现的。因此产生了反馈调度策略,对进程进行分级(而不是简单的一个等待队列),进程运行时间长调度的优先级越低,每被抢占一次就下降一级。


反馈调度往往会和前述的简单调度(比如FCFS或RR)结合使用,同时长进程周转时间会出现惊人的增加的现象(长进程多次被抢占,优先级不断下降,而长期得不到调度),所以会有一些相应的补偿措施(比如设置允许被抢占的次数,每当超过这个次数才会对进程进行降级操作)。


实时调度

嵌入式开发中还常见一些实时调度策略,与前述策略不同的时间,这些任务有最后期限的限制,这通常分为两种——一种是“硬实时”,要求必须满足最后期限的限制,否则将给系统带来不可接受的破坏或致命的错误(任务超时完成是无意义的);另一种是“软实时”,希望能够满足最后期限的限制,但并非强制(即使超时完成任务也有意义)。

拆解目标检测demo
前边介绍了进程、线程以及处理器调度的概念和简单的使用,接下来我们考虑如何将我们的目标检测demo拆解成两个线程以提高展示的流畅性。
我们的目标是将demo拆解成 目标检测 和 视频流获取和展示 两个线程,其中需要共享的数据包括 图像数据 和 检测结果两部分(为了响应退出按钮,后续的示例程序还会额外增加一个作为退出标志的共享数据),每部分数据需要配备一把线程锁进行读写保护。

最终程序如下——

注意:这里不仅划分了线程,还针对不同线程的任务分配了cpu,比如RK3399上CPU0-3是四个小核,我们用来做视频流的获取、检测结果的标注和视频流的展示;CPU4-5是大核,我们用来做核心的目标检测任务。两个不同的线程使用不同的CPU核,互不冲突,合理地分配CPU对应用程序也会有一定的提升。
  1. #include <unistd.h>
  2. #include <iostream>
  3. #include <iomanip>
  4. #include <string>
  5. #include <vector>
  6. #include "opencv2/imgproc/imgproc.hpp"
  7. #include "opencv2/highgui/highgui.hpp"
  8. #include "tengine_c_api.h"
  9. #include <sys/time.h>
  10. #include <stdio.h>
  11. #include "common.hpp"
  12. #include <pthread.h>    // 包含线程控制相关的库

  13. #define DEF_PROTO "models/MobileNetSSD_deploy.prototxt"
  14. #define DEF_MODEL "models/MobileNetSSD_deploy.caffemodel"

  15. struct Box
  16. {
  17.     float x0;
  18.     float y0;
  19.     float x1;
  20.     float y1;
  21.     int class_idx;
  22.     float score;
  23. };

  24. void get_input_data_ssd(cv::Mat img, float* input_data, int img_h,  int img_w){
  25.     if (img.empty()){
  26.         std::cerr << "Failed to read image from camera.\n";
  27.         return;
  28.     }

  29.     cv::resize(img, img, cv::Size(img_h, img_w));
  30.     img.convertTo(img, CV_32FC3);
  31.     float *img_data = (float *)img.data;
  32.     int hw = img_h * img_w;

  33.     float mean[3]={127.5,127.5,127.5};
  34.     for (int h = 0; h < img_h; h++){
  35.         for (int w = 0; w < img_w; w++){
  36.             for (int c = 0; c < 3; c++){
  37.                 input_data[c * hw + h * img_w + w] = 0.007843* (*img_data - mean[c]);
  38.                 img_data++;
  39.             }
  40.         }
  41.     }
  42. }

  43. void post_process_ssd(cv::Mat img, float threshold,float* outdata,int num)
  44. {
  45.     const char* class_names[] = {"background",
  46.                             "aeroplane", "bicycle", "bird", "boat",
  47.                             "bottle", "bus", "car", "cat", "chair",
  48.                             "cow", "diningtable", "dog", "horse",
  49.                             "motorbike", "person", "pottedplant",
  50.                             "sheep", "sofa", "train", "tvmonitor"};
  51.     int raw_h = img.size().height;
  52.     int raw_w = img.size().width;
  53.     std::vector<Box> boxes;
  54.     int line_width=raw_w*0.002;
  55.     // printf("detect ruesult num: %d \n",num);
  56.     for (int i=0;i<num;i++){
  57.         if(outdata[1]>=threshold){
  58.             Box box;
  59.             box.class_idx=outdata[0];
  60.             box.score=outdata[1];
  61.             box.x0=outdata[2]*raw_w;
  62.             box.y0=outdata[3]*raw_h;
  63.             box.x1=outdata[4]*raw_w;
  64.             box.y1=outdata[5]*raw_h;
  65.             boxes.push_back(box);
  66.             // printf("%s\t:%.0f%%\n", class_names[box.class_idx], box.score * 100);
  67.             // printf("BOX:( %g , %g ),( %g , %g )\n",box.x0,box.y0,box.x1,box.y1);
  68.         }
  69.         outdata+=6;
  70.     }
  71.     for(int i=0;i<(int)boxes.size();i++){
  72.         Box box=boxes[i];
  73.         cv::rectangle(img, cv::Rect(box.x0, box.y0,(box.x1-box.x0),(box.y1-box.y0)),cv::Scalar(255, 255, 0),line_width);
  74.         std::ostringstream score_str;
  75.         score_str<<box.score;
  76.         std::string label = std::string(class_names[box.class_idx]) + ": " + score_str.str();
  77.         int baseLine = 0;
  78.         cv::Size label_size = cv::getTextSize(label, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
  79.         cv::rectangle(img, cv::Rect(cv::Point(box.x0,box.y0- label_size.height),
  80.                                   cv::Size(label_size.width, label_size.height + baseLine)),
  81.                       cv::Scalar(255, 255, 0), CV_FILLED);
  82.         cv::putText(img, label, cv::Point(box.x0, box.y0),
  83.                     cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0));
  84.     }
  85. }

  86. float outdata[15*6];    // 线程间共享变量——检测结果
  87. cv::Mat frame;      // 线程间共享变量——图像数据
  88. int detect_num;     // 线程间共享变量——检测结果
  89. bool quit_flag = false; // 程序间共享变量——退出标志
  90. graph_t graph;

  91. // 与共享变量对应的线程锁
  92. pthread_mutex_t m_frame, m_outdata, m_quit;

  93. void *th_vedio(void *){
  94.     // 将线程绑定到cpu0-3上
  95.     cpu_set_t mask;
  96.     CPU_ZERO(&mask);
  97.     CPU_SET(0, &mask);
  98.     CPU_SET(1, &mask);
  99.     CPU_SET(2, &mask);
  100.     CPU_SET(3, &mask);
  101.     if (sched_setaffinity(0, sizeof(cpu_set_t), &mask) < 0) {
  102.         printf("Error: setaffinity()\n");
  103.         exit(0);
  104.     }

  105.     cv::VideoCapture capture(0);
  106.     capture.set(CV_CAP_PROP_FRAME_WIDTH, 960);
  107.     capture.set(CV_CAP_PROP_FRAME_HEIGHT, 540);
  108.     cv::namedWindow("MSSD", CV_WINDOW_NORMAL);
  109.     cvResizeWindow("MSSD", 1280, 720);

  110.     while(1){
  111.         pthread_mutex_lock(&m_frame);
  112.         capture >> frame;
  113.         pthread_mutex_unlock(&m_frame);


  114.         float show_threshold=0.25;
  115.         pthread_mutex_lock(&m_outdata);  pthread_mutex_lock(&m_frame);      // 上锁
  116.         post_process_ssd(frame, show_threshold, outdata, detect_num);
  117.         pthread_mutex_unlock(&m_outdata);   // 解锁
  118.         cv::imshow("MSSD", frame);
  119.         pthread_mutex_unlock(&m_frame);     // 解锁
  120.         if( cv::waitKey(10) == 'q' ){
  121.             pthread_mutex_lock(&m_quit);    // 上锁
  122.             quit_flag = true;
  123.             pthread_mutex_unlock(&m_quit);  // 解锁
  124.             break;
  125.         }
  126.         usleep(10000);  // 注意必须sleep(不然太过频繁地取帧会影响检测线程的调度)
  127.     }
  128. }

  129. void *th_detect(void*){
  130.     // 将该线程绑定到cpu4-5上
  131.     cpu_set_t mask;
  132.     CPU_ZERO(&mask);
  133.     CPU_SET(4, &mask);
  134.     CPU_SET(5, &mask);
  135.     if (sched_setaffinity(0, sizeof(cpu_set_t), &mask) < 0) {
  136.         printf("Error: setaffinity()\n");
  137.         exit(0);
  138.     }

  139.     // input
  140.     int img_h = 300;
  141.     int img_w = 300;
  142.     int img_size = img_h * img_w * 3;
  143.     float *input_data = (float *)malloc(sizeof(float) * img_size);

  144.     int node_idx=0;
  145.     int tensor_idx=0;
  146.     tensor_t input_tensor = get_graph_input_tensor(graph, node_idx, tensor_idx);
  147.     if(!check_tensor_valid(input_tensor)){
  148.         printf("Get input node failed : node_idx: %d, tensor_idx: %d\n",node_idx,tensor_idx);
  149.         return NULL;
  150.     }

  151.     int dims[] = {1, 3, img_h, img_w};
  152.     set_tensor_shape(input_tensor, dims, 4);
  153.     prerun_graph(graph);

  154.     int repeat_count = 1;
  155.     const char *repeat = std::getenv("REPEAT_COUNT");

  156.     if (repeat)
  157.         repeat_count = std::strtoul(repeat, NULL, 10);

  158.     int out_dim[4];
  159.     tensor_t out_tensor;
  160.     while(1){
  161.         pthread_mutex_lock(&m_quit);    // 上锁
  162.         if(quit_flag)  break;
  163.         pthread_mutex_unlock(&m_quit);  // 解锁

  164.         struct timeval t0, t1;
  165.         float total_time = 0.f;

  166.         for (int i = 0; i < repeat_count; i++){
  167.             pthread_mutex_lock(&m_frame);       // 上锁
  168.             get_input_data_ssd(frame, input_data, img_h,  img_w);
  169.             pthread_mutex_unlock(&m_frame);     // 解锁

  170.             gettimeofday(&t0, NULL);
  171.             set_tensor_buffer(input_tensor, input_data, img_size * 4);
  172.             run_graph(graph, 1);

  173.             gettimeofday(&t1, NULL);
  174.             float mytime = (float)((t1.tv_sec * 1000000 + t1.tv_usec) - (t0.tv_sec * 1000000 + t0.tv_usec)) / 1000;
  175.             total_time += mytime;
  176.         }
  177.         std::cout << "--------------------------------------\n";
  178.         std::cout << "repeat " << repeat_count << " times, avg time per run is " << total_time / repeat_count << " ms\n";

  179.         out_tensor = get_graph_output_tensor(graph, 0,0);
  180.         get_tensor_shape( out_tensor, out_dim, 4);
  181.         pthread_mutex_lock(&m_outdata);     // 上锁
  182.         detect_num = out_dim[1] <= 15 ? out_dim[1] : 15;
  183.         memcpy(outdata, get_tensor_buffer(out_tensor), sizeof(float)*detect_num*6);
  184.         pthread_mutex_unlock(&m_outdata);   // 解锁
  185.     }

  186.     free(input_data);
  187. }

  188. int main(int argc, char *argv[])
  189. {
  190.     const std::string root_path = get_root_path();
  191.     std::string proto_file;
  192.     std::string model_file;

  193.     int res;
  194.     while( ( res=getopt(argc,argv,"p:m:h"))!= -1){
  195.         switch(res){
  196.             case 'p':
  197.                 proto_file=optarg;
  198.                 break;
  199.             case 'm':
  200.                 model_file=optarg;
  201.                 break;
  202.             case 'h':
  203.                 std::cout << "[Usage]: " << argv[0] << " [-h]\n"
  204.                           << "   [-p proto_file] [-m model_file]\n";
  205.                 return 0;
  206.             default:
  207.                 break;
  208.         }
  209.     }

  210.     const char *model_name = "mssd_300";
  211.     if(proto_file.empty()){
  212.         proto_file = DEF_PROTO;
  213.         std::cout<< "proto file not specified,using "<< proto_file << " by default\n";

  214.     }
  215.     if(model_file.empty()){
  216.         model_file = DEF_MODEL;
  217.         std::cout<< "model file not specified,using "<< model_file << " by default\n";
  218.     }

  219.     // init tengine
  220.     init_tengine_library();
  221.     if (request_tengine_version("0.1") < 0)
  222.         return 1;
  223.     if (load_model(model_name, "caffe", proto_file.c_str(), model_file.c_str()) < 0)
  224.         return 1;
  225.     std::cout << "load model done!\n";

  226.     // create graph
  227.     graph = create_runtime_graph("graph", model_name, NULL);
  228.     if (!check_graph_valid(graph)){
  229.         std::cout << "create graph0 failed\n";
  230.         return 1;
  231.     }

  232.     // 初始化线程锁
  233.     pthread_mutex_init(&m_frame, NULL);
  234.     pthread_mutex_init(&m_outdata, NULL);
  235.     pthread_mutex_init(&m_quit, NULL);

  236.     // 创建线程
  237.     pthread_t id1, id2;
  238.     pthread_create(&id1, NULL, th_vedio, NULL);
  239.     pthread_create(&id2, NULL, th_detect, NULL);

  240.     // 等待线程
  241.     pthread_join(id1, NULL);
  242.     pthread_join(id2, NULL);

  243.     // 销毁线程锁
  244.     pthread_mutex_destroy(&m_frame);
  245.     pthread_mutex_destroy(&m_outdata);
  246.     pthread_mutex_destroy(&m_quit);

  247.     postrun_graph(graph);
  248.     destroy_runtime_graph(graph);
  249.     remove_model(model_name);

  250.     return 0;
  251. }
复制代码


在本文中,我们将尝试把视频的获取与展示和检测任务分离开来,分别放在两个不同的线程上工作,同时将不同的线程绑定到不同的cpu核上,使得两者的工作不会冲突。但在实际使用中你会发现,模型对于小目标的检测能力还是有所欠缺,下一篇文章我们将探究如何改善检测模型的小目标检测能力。








作者: wx_甜菜_JEKP3    时间: 2018-10-31 16:36
非常感谢,不过感觉用tensorflow更好
作者: wx_甜菜_JEKP3    时间: 2018-10-31 16:39
感觉嵌入式设备上还是只做推理比较好
作者: 15505095562    时间: 2018-11-5 22:34
你好,想问下你有没有用到这块板的gpu加速。我想用拿他跑图像分割,用cpu的话特别慢。




欢迎光临 Firefly开源社区 (https://dev.t-firefly.com/) Powered by Discuz! X3.1