服务端上传视频处理(二)
背景
在实现好服务端的Python图像分析服务后,我们之前提到的遗留问题应该被解决:即dlib精确度不够的问题,下面我们将通过darknet完成人脸的检测。也就是说,在之前完成的工作的基础之上,将darknet和我们的服务进行集成。
darknet进一步修饰
之前我们实现的darknet中不能很好地处理一张图片内有多个目标的情况,接下来我们需要完成对darknet的Python接口的处理,使其能够一张图片处理中返回多个结果
处理流程改变
过去,我们将所有的识别信息全部存储在darknet本地的数组中,现在看来这样是不可取的,原因有以下两点:
- 我们无法在darknet模块初始化阶段确定图像的数量,更不能确定结果的数量
- 在提取结果的过程中,我们很难分辨那个结果属于那张图片
基于上述问题,我们重新设计了darknet的结果存储机制,使其更加灵活:
- 采用和图片输入一样的链状缓冲区,存储结果,每一个节点中存储一张图片的结果数量,结果数据
- darknet模块的使用流程变成:
- 输入一批图片
- 多次提取结果,每次可以取出一张图片的,顺序符合输入顺序
具体改动和调试
-
链表的实现和线程安全性
过去的图像缓冲区中存在问题,就是没考虑到线程安全性,虽然在当前的使用方式下,发生线程安全问题的可能性不高,但是为了保证正确性,我们还是重新加入了相关措施,并且屏蔽了外界过程直接便利链表的功能,直接向外界提供了函数结构来完成链状队列的存取,具体内容如下
- pydarknet.h
#include "darknet.h" #include <numpy/ndarrayobject.h> #include <string.h> #include <stdio.h> #ifndef __PYDARKNET_ #define __PYDARKNET_ #define NET_SIZE 416 #define IN_W 640 #define IN_H 480 #define NEW_H 312 #define HALF_H 180 //6线程用 #define NEW_W NET_SIZE #define SCALE ((float)(IN_W - 1) / (NET_SIZE -1)) /* * 正经的从ndarray.data里取值的方法 * ptr : double* * w : int * h : int * c : int * 类型错了谁也救不了 */ //#define GET_PIXEL(ptr, w, h, c) ((float) (ptr)[(h)*IN_W*3 + (w)*3 + 2 - (c)]/255.) network * net; char** names; int batch_size; int** do_detection(char*, float); void __finalize(void); void __init(void); #ifdef radical__ int total_size; //总共的图片数量 int* g_res_store; //在识别结束之前所有的结果暂时先放在内存里 typedef struct __linked_buffer { struct __linked_buffer* next; //下一个batch image* batch_pic; //本batch的转换结果(图片统一free) float* batch_pic_data; //为了方便资源回收&引用 float thresh; //每个batch的thresh,虽然不认为会变化 int batch_size; //下一个batch的大小 }linked_buffer_t; /** * @brief list_head * 链表头指针,我不打算用什么空头 */ linked_buffer_t* buffer_head; void buffer_init(); void buffer_finalize(); linked_buffer_t* create_buffer(int buffer_size, float thresh); void free_buffer(linked_buffer_t* rubbish); #endif #ifdef app__ typedef struct __linked_buffer { struct __linked_buffer* next; //下一个batch image* batch_pic; //本batch的转换结果(图片统一free) float* batch_pic_data; //为了方便资源回收&引用 float thresh; //每个batch的thresh,虽然不认为会变化 int batch_size; //下一个batch的大小 }linked_buffer_t; /** * @brief list_head * 链表头指针,我不打算用什么空头 */ void buffer_init(); void buffer_finalize(); linked_buffer_t* create_buffer(int buffer_size, float thresh); linked_buffer_t* retrieve_buffer(); void free_buffer(linked_buffer_t* rubbish); typedef struct __linked_res { struct __linked_res* next; //下一张图片的指针 int dets; //当前图片检测出的人脸数量 int* data; //存储的数据,长度是dets×4 }linked_res_t; void res_init(); //初始化信号量 void res_finalize(); //销毁信号量 linked_res_t* create_res(int dets, int* data); //创建一个新的节点,加入列表 linked_res_t* retrieve_res(); //节点中的数据已经送出,释放节点的空间,但是不释放数组 void destroy_res(linked_res_t* rubbish); //由于某种原因当前节点作废,释放掉节点和内部数据域的空间 #endif #endif
- linked_buffer.c
#include "pydarknet.h" #include <semaphore.h> #define MAX_BUFFER_SIZE 3 static sem_t size_sig; static sem_t res_sig; static pthread_mutex_t buffer_mutex; static pthread_mutex_t res_mutex; linked_buffer_t* buffer_head; linked_buffer_t* buffer_rear; /** * @brief create_buffer * 创建一个缓冲区,顺便把空间也给他申请了吧 * @param buffer_size 当前的batch_size * @param buffered_data 需要转换的图片,在这里就申请好空间,并在这里释放 * @return */ linked_buffer_t* create_buffer(int buffer_size, float thresh) { sem_wait(&size_sig); //为image结构申请空间 image* sized_pics = calloc(buffer_size, sizeof(image)); float* cont_data_field = calloc(batch_size*NET_SIZE*NET_SIZE*3, sizeof(float)); for(int i = 0; i < buffer_size; i++) { sized_pics[i].data = cont_data_field + i*3*NET_SIZE*NET_SIZE; sized_pics[i].w = NET_SIZE; sized_pics[i].h = NET_SIZE; sized_pics[i].c = 3; } //申请新节点并赋值 linked_buffer_t* ret = malloc(sizeof(linked_buffer_t)); ret->next = NULL; ret->batch_size = buffer_size; ret->thresh = thresh; ret->batch_pic = sized_pics; ret->batch_pic_data = cont_data_field; pthread_mutex_lock(&buffer_mutex); //sync if(buffer_head == NULL && buffer_rear == NULL) buffer_head = buffer_rear = ret; else { buffer_head->next = ret; buffer_head = ret; } pthread_mutex_unlock(&buffer_mutex); //sync fin printf("allocated new buffer on %x\n", ret); return ret; } linked_buffer_t* retrieve_buffer() { pthread_mutex_lock(&buffer_mutex); //sync linked_buffer_t* retval = buffer_rear; if(retval == NULL) { pthread_mutex_unlock(&buffer_mutex); return NULL; } buffer_rear = buffer_rear->next; if(buffer_rear == NULL) buffer_head = NULL; sem_post(&size_sig); pthread_mutex_unlock(&buffer_mutex); //sync fin return retval; } /** * @brief free_buffer * 按照上面释放掉申请的所有资源 * @param rubbish */ void free_buffer(linked_buffer_t* rubbish) { free(rubbish->batch_pic_data); free(rubbish->batch_pic); free(rubbish); } void buffer_init() { buffer_head = buffer_rear = NULL; sem_init(&size_sig, 0, MAX_BUFFER_SIZE); pthread_mutex_init(&buffer_mutex, NULL); } void buffer_finalize() { pthread_mutex_lock(&buffer_mutex); buffer_head = buffer_rear = NULL; sem_destroy(&size_sig); pthread_mutex_unlock(&buffer_mutex); pthread_mutex_destroy(&buffer_mutex); } linked_res_t* res_head; linked_res_t* res_rear; void res_init() { res_head = res_rear = NULL; sem_init(&res_sig, 0, 0); pthread_mutex_init(&res_mutex, NULL); } void res_finalize() { pthread_mutex_lock(&res_mutex); res_head = res_rear = NULL; sem_destroy(&res_sig); pthread_mutex_unlock(&res_mutex); pthread_mutex_destroy(&res_mutex); } linked_res_t* create_res(int size, int* res) { linked_res_t* n = malloc(sizeof(linked_res_t)); n->data = res; n->dets = size; n->next = NULL; pthread_mutex_lock(&res_mutex); if(res_head == NULL && res_rear == NULL) res_head = res_rear = n; else{ res_head->next = n; res_head = n; } sem_post(&res_sig); pthread_mutex_unlock(&res_mutex); return n; } linked_res_t* retrieve_res() { sem_wait(&res_sig); pthread_mutex_lock(&res_mutex); linked_res_t* retval = res_rear; assert(retval != NULL); res_rear = res_rear->next; if(res_rear == NULL) res_head = NULL; pthread_mutex_unlock(&res_mutex); return retval; } /** * @brief free * @return res节点释放是否成功 */ void destroy_res(linked_res_t *rubbish) { free(rubbish->data); free(rubbish); }
现在的两种缓冲区,在实现上存在如下区别:
- 信号量的作用:图像缓冲区的信号量是用来控制缓冲区大小的,防止连续输入导致内存溢出;而结果缓冲区的信号量就是单纯为了实现同步的读者写者功能
- 作废节点的回收:图像缓冲的节点在图像转换完成之后,就完全失去了作用,需要释放包括其指针成员指向的空间在内的所有空间;而结果缓冲区中,数据缓冲区是要作为Python新对象的数据域的,不能释放,节点回收只需使用free释放掉节点本身的空间就可以了
-
识别结果处理的重新定义
darknet框架本身是支持多个结果的,所以我们只需要讲返回值调整成多个结果即可,但是问题是如何返回。因为调用函数的时候并不知道返回的结果有多少个
//这个玩意是我自己加的,让我们把识别结果转化成int[]就结束 int list_detection_one_class(image im, detection *dets, int num, float thresh, int** ptr_buffer) { int i, cnt = 0; for(i = 0; i < num; i++) { if(dets[i].prob[0] >= thresh) cnt++; } if(cnt == 0) return 0; printf("cnt = %d\n",cnt); *ptr_buffer = calloc(sizeof(int), cnt*4); for(cnt = 0,i = 0; i < num; ++i){ if(dets[i].prob[0] < thresh) continue; box b = dets[i].bbox; //printf("%f %f %f %f\n", b.x, b.y, b.w, b.h); /* //可选方案 int w__ = im.w*b.w; int h__ = im.h*b.h; int avg = (w__+h__) /2; int left = b.x*im.w-avg/2; int right = b.x*im.w+avg/2; int top = b.y*im.h-avg/2; int bot = b.y*im.h+avg/2; */ int left = (b.x-b.w/2.)*im.w; int right = (b.x+b.w/2.)*im.w; int top = (b.y-b.h/2.)*im.h; int bot = (b.y+b.h/2.)*im.h; if(left < 0) left = 0; if(right > im.w-1) right = im.w-1; if(top < 0) top = 0; if(bot > im.h-1) bot = im.h-1; (*ptr_buffer)[cnt*4] = left; (*ptr_buffer)[cnt*4+1] = right; (*ptr_buffer)[cnt*4+2] = top; (*ptr_buffer)[cnt*4+3] = bot; cnt++; } return cnt; }
我们采用了返回值+输出参数的方法。代码中可选方案代表画出框的形状调整
-
外部逻辑和接口的相关适配
在上述改动的基础上,其他的代码也作出了相应的改动,具体如下:
-
主逻辑变动
-
我们不能再通过已经处理的图片数量判断是否结束,而是在显示调用终止函数的时候,才能判断结束
int** do_detection(char* data, float thresh) { linked_buffer_t* new_batch = create_buffer(batch_size, thresh);//可能会阻塞!! if(detect_num == -1) {//还没开始工作,current_job域还是空的,由于这个分支只会执行一次 //det_worker.current_job = retrieve_buffer(); sem_post(&det_sig); detect_num = 0; } detect_num += batch_size; feed_pipeline(data, new_batch); //----------------------------- //printf("353:stuck here?\n"); //----------------------------- sem_wait(&conv_idle); //策略改变——进来的时候断言转换线程空闲,等到转换完成后退出 //----------------------------- printf("pic %d wait success!...\n", detect_num); //----------------------------- return NULL; } //...... void __finalize( void ) { int i; for(i = 0; i < CONV_THS; i++) { pic_workers[i].src = -1; sem_post(&pic_workers[i].next_batch); } create_buffer(1,0); //通知识别线程可以结束 sem_wait(&det_sig); //等待识别线程结束 pthread_join(&det_worker, NULL); //printf("threads finalized\n"); for(i = 0; i < CONV_THS; i++) sem_destroy(&pic_workers[i].next_batch); //printf("sems finalized\n"); sem_destroy(&conv_idle); sem_destroy(&det_sig); buffer_finalize(); }
-
识别线程的处理方法,现在每处理一个张图片,都要对结果进行回收
static void* thread_detect(void* args) { det_t* task = (det_t*) args; linked_buffer_t* job; float thresh; image* batch_pics; int b_size; int* det_res_ptr; //检测结果的指针 sem_wait(&det_sig); //等主线程准备好数据(串好链表) while(1) { int batch_index; while((job = retrieve_buffer()) == NULL) pthread_yield();//task->current_job; thresh = job->thresh; if(thresh == 0) { sem_post(&det_sig); return NULL; } batch_pics = job->batch_pic; b_size = job->batch_size; for(batch_index = 0; batch_index < b_size; batch_index++) { sem_wait(&task->pic_avail); //-------------------------- printf("det_worker : Got one pic to detect...\n"); //-------------------------- int size = test_detector(batch_pics + batch_index, thresh, &det_res_ptr); create_res(size, det_res_ptr); //-------------------------- printf("det_worker : res created at %d...\n", what_time_is_it_now() - gtimer); //-------------------------- } free_buffer(job); } return NULL; }
-
-
技术验证
既然是Python模块,就要使用Python代码来进行验证,正好使用opencv的webcam功能完成一个实时的测试
#!/usr/bin/python
import cv2
import pydarknet as pd
import numpy as np
import time
DataFile = "models/face.data"
CfgFile = "models/yolov3-tiny.cfg"
WeightFile = "models/yolov3-tiny.backup6"
imageSize = (360, 640, 3)
if __name__ == '__main__':
cap = cv2.VideoCapture(0)
pd.init_detector(DataFile, CfgFile, WeightFile)
buf = np.zeros((1, 480, 640, 3))
while 1:
ret, frame = cap.read()
buf[0] = frame
print 'till now, nothing happens'
pd.feed_detector(0.5, buf)
print 'i fed it a shit!'
res = pd.pull_result()
#time.sleep(3)
for i in range(res.shape[0]):
cv2.rectangle(frame, (res[i][0],res[i][2]), (res[i][1],res[i][3]), (0,255,255), 2)
cv2.imshow("window",frame)
if cv2.waitKey(30) == 27:
break
cap.release()
小结
至此,我的主要工作基本完成,下面就需要专注于整个系统的集成