> 文章列表 > runtime中加入多线程和应用开发

runtime中加入多线程和应用开发

runtime中加入多线程和应用开发

1. 单线程的runtime

  1. nvinfer1::runtime: 创建推理运行时runtime

  2. runtime->(load(engine)): 反序列化mengine

  3. mengine->context: 创建执行上下文context

  4. buffer(mengine): 创建输入输出的缓冲区, 确定cap输入文件是视频文件还是RTSP流, 并且获取数据, 是否推流, 推流的话实例化推流器并把获取的数据配置给推流器

  5. 申请足够的cuda内存然后进行预处理,检查应用开发的配置文件, 做完了才能够执行推理

  6. context->executeV2(buffer)

  7. 推理完从buffer把数据搬回到CPU, NMS恢复成框的时候如果在红框里面就显示, 绘制聚集点的问题

2. 单线程注释版(包括应用开发)

2.1 构建runtime

runtime: 运行tensorRT模型的环境, 这里创建一个runtime对象, 用于指定深度学习模型的前向推理, 包括预处理,后处理,然后根据计算设备进行优化

// ========= 1. 创建推理运行时runtime =========auto runtime = std::unique_ptr<nvinfer1::IRuntime>(nvinfer1::createInferRuntime(sample::gLogger.getTRTLogger()));if (!runtime){std::cout << "runtime create failed" << std::endl;return -1;}

2.2 反序列化生成engine: runtime->mengine

build.cu中通过一系列操作加载config, network配置好了engine, 这里反序列化为TensorRT的engine对象, 可以使用这个对象进行模型的推理。 engine会多次引用所以用shared_ptr

// ======== 2. 反序列化生成engine =========// 加载模型文件auto plan = load_engine_file(engine_file);// 反序列化生成engineauto mEngine = std::shared_ptr<nvinfer1::ICudaEngine>(runtime->deserializeCudaEngine(plan.data(), plan.size()));if (!mEngine)       

engine是个二进制的文件, 这里就是用普通的二进制读取方法去读取, 要注意的点就是engine比较大所以传入的参数是引用, 但是为了保护engine这里还用了const去修饰, 让整个engine变成只读的。

在读取二进制的时候常用到reinterpret_cast<类型>转换, 适用于指针的类型转换, static_cast<>更适合于基本的类型转换, int, float

最后返回的engine的类型是std::vector, 包含了文件字节数据以及长度信息

std::vector<unsigned char> load_engine_file(const std::string &file_name)
{std::vector<unsigned char> engine_data;std::ifstream engine_file(file_name, std::ios::binary);assert(engine_file.is_open() && "Unable to load engine file.");engine_file.seekg(0, engine_file.end);int length = engine_file.tellg();engine_data.resize(length);engine_file.seekg(0, engine_file.beg);engine_file.read(reinterpret_cast<char *>(engine_data.data()), length);return engine_data;
}

2.3 应用前的函数准备, 恢复坐标和做点

从文件中读取多边形的定点,这些定点是相对于图像宽度和高度的比例。因此需要将这些相对坐标乘以图像宽度和高度,恢复为原始坐标。这里是闯入应用

// 从文件中恢复多边形定点
void readPoints(std::string filename, Polygon &g_ploygon, int width, int height)
{std::ifstream file(filename);std::string str;while (std::getline(file, str)){std::stringstream ss(str);std::string x, y;std::getline(ss, x, ',');std::getline(ss, y, ',');// recover to original sizex = std::to_string(std::stof(x) * width);y = std::to_string(std::stof(y) * height);g_ploygon.push_back({std::stoi(x), std::stoi(y)});}
}

这段代码实现的是在图像中心绘制一个圆形区域,并将这个区域与原图像进行叠加,实现图像的融合效果。这里是聚集应用

void blender_overlay(int x, int y, int radius, cv::Mat &image, float alpha, int height, int width)
{// initialint rect_l = x - radius;int rect_t = y - radius;int rect_w = radius * 2;int rect_h = radius * 2;int point_x = radius;int point_y = radius;// check if out of rangeif (x + radius > width){rect_w = radius + (width - x);}if (y + radius > height){rect_h = radius + (height - y);}if (x - radius < 0){rect_l = 0;rect_w = radius + x;point_x = x;}if (y - radius < 0){rect_t = 0;rect_h = radius + y;point_y = y;}// get roicv::Mat roi = image(cv::Rect(rect_l, rect_t, rect_w, rect_h));cv::Mat color;roi.copyTo(color);// draw circlecv::circle(color, cv::Point(point_x, point_y), radius, cv::Scalar(255, 0, 255), -1);// blendcv::addWeighted(color, alpha, roi, 1.0 - alpha, 0.0, roi);
}

2.4 创建执行上下文: mengine->context

通过context(nvinfer1::IExecutionContext)可以将数据输入TensorRT的推理引擎中, 但是需要先反序列加载engine和输入输出数据的内存空间(缓冲区)

auto context = std::unique_ptr<nvinfer1::IExecutionContext>(mEngine->createExecutionContext());if (!context){std::cout << "context create failed" << std::endl;return -1;}

2.5 创建缓冲区

在这里,首先创建了一个 BufferManager 对象 buffers,它负责为模型的输入和输出数据分配和管理缓冲区。 BufferManager 类是由 TensorRT 示例代码提供的一个实用类.

每一步操作都从buffer拿数据搞完了再放回去

buffer是与TensorRT engine相关联的。在TensorRT中,输入和输出数据需要通过BufferManager来分配和管理,其中BufferManager会创建和存储一系列的缓冲区来存储输入和输出Tensor。在运行时,我们需要将输入数据放入输入Tensor的缓冲区中,然后通过执行上下文执行推理,推理结果也会被存储在输出Tensor的缓冲区中。因此,BufferManager是一个用于在运行时分配和管理输入和输出Tensor缓冲区的实用工具。

samplesCommon::BufferManager buffers(mEngine);

2.6 推流, 获取信息并且配置推流器

如果rtsp, 就使用opencv中的FFmpeg读取帧, 否则就通过指定路径从本地读取文件帧

RTSP(Real Time Streaming Protocol)是一种实时传输协议,通常用于实时视频流的传输。RTSP在实时性和可靠性方面做得很好,但其传输协议并不是很成熟,需要与其他协议一起使用,如RTP、RTCP等。

RTMP(Real Time Messaging Protocol)是Adobe公司开发的实时传输协议,主要用于视频直播和点播。RTMP支持音频、视频和数据的实时传输,通过流媒体服务器将数据流发送到客户端播放器。相对于RTSP,RTMP的稳定性更好,支持更高质量的音视频传输,同时也更容易进行流媒体推送和转码。但是,RTMP在跨平台方面支持不够完善,只能在Adobe Flash播放器上播放,而且需要使用RTMP协议的专用服务器。

Bitrate(比特率)是指每秒传输的数据量,通常用单位bps(bits per second)表示。不同分辨率和不同帧率的视频需要不同的比特率才能保证画面质量和流畅度。

cv::VideoCapture cap;if (input_video_path == "rtsp"){// auto rtsp = "rtsp://192.168.1.241:8556/live1.sdp";auto rtsp = "rtsp://10.20.33.52:8556/live1.sdp";std::cout << "当前使用的是RTSP流" << std::endl;cap = cv::VideoCapture(rtsp, cv::CAP_FFMPEG);}else{std::cout << "当前使用的是视频文件" << std::endl;cap = cv::VideoCapture(input_video_path);}

cv::Size(frame_width, frame_height) 获取宽高, 再用fps, bitrate, rtmp协议的推流地址来对推流器进行初始化

是的,不同分辨率和不同帧率的视频需要不同的比特率才能保证画面质量和流畅度。比特率(bitrate)是指视频编码后每秒钟所占用的数据位数,通常用Mbps(兆位每秒)表示。

// 获取画面尺寸cv::Size frameSize(cap.get(cv::CAP_PROP_FRAME_WIDTH), cap.get(cv::CAP_PROP_FRAME_HEIGHT));// 获取帧率double video_fps = cap.get(cv::CAP_PROP_FPS);std::cout << "width: " << frameSize.width << " height: " << frameSize.height << " fps: " << video_fps << std::endl;// 实例化推流器streamer::Streamer streamer;if (do_stream){streamer::StreamerConfig streamer_config(frameSize.width, frameSize.height,frameSize.width, frameSize.height,video_fps, bitrate, "main", "rtmp://localhost/live/mystream");streamer.init(streamer_config);}

定义frame, 每帧, 申请cuda内存, 申请CUDA内存是为了将视频帧转移到GPU上进行处理

cv::Mat frame;int img_size = frameSize.width * frameSize.height;cuda_preprocess_init(img_size); // 申请cuda内存Polygon g_ploygon;

检测文件是否存在,存在就用之前写好的函数恢复绝对坐标,因为之前是用的相对坐标

// 检查多边形定点配置文件是否存在std::ifstream infile("./config/polygon.txt");if (infile){std::cout << "检测到多边形顶点配置文件,从文件中恢复多边形定点" << std::endl;readPoints("./config/polygon.txt", g_ploygon, frameSize.width, frameSize.height);}else{std::cout << "未检测到多边形顶点配置文件" << std::endl;return -1;}

2.7 图像预处理:

这里开始进入while循环,这里把每一帧图像传给frame

while (cap.isOpened()){// step1 startauto start_1 = std::chrono::high_resolution_clock::now();cap >> frame;if (frame.empty()){std::cout << "文件处理完毕" << std::endl;break;

使用GPU做letterbox, 归一化, BGR2RGBM, NHWC 2 NCHW, 注意这里的数据都是从buffer上面拿的

process_input_gpu(frame, (float *)buffers.getDeviceBuffer(kInputTensorName));
// 使用cuda预处理所有步骤
void process_input_gpu(cv::Mat &src, float *input_device_buffer)
{cuda_preprocess(src.ptr(), src.cols, src.rows, input_device_buffer, kInputW, kInputH);
}
void cuda_preprocess(uint8_t *src, int src_width, int src_height,float *dst, int dst_width, int dst_height)

2.8 执行推理 context->executeV2(buffers.getDeviceBindings().data());

context->executeV2是TensorRT API中的一个函数,用于执行推理操作。该函数的参数是buffers,即输入和输出数据的缓冲区,其中buffers.getDeviceBindings().data()表示将缓冲区中的数据以指针数组的形式传入该函数,使得该函数可以获取输入数据并输出推理结果。在执行该函数之前,需要先构建好TensorRT的推理引擎和上下文,将模型加载到推理引擎中,为模型的输入和输出分配缓冲区等操作。

// ========== 5. 执行推理 =========// step3 startauto start_3 = std::chrono::high_resolution_clock::now();context->executeV2(buffers.getDeviceBindings().data());

2.9 NMS过滤

 // 拷贝回hostbuffers.copyOutputToHost();// 从buffer manager中获取模型输出int32_t *num_det = (int32_t *)buffers.getHostBuffer(kOutNumDet); // 检测到的目标个数int32_t *cls = (int32_t *)buffers.getHostBuffer(kOutDetCls);     // 检测到的目标类别float *conf = (float *)buffers.getHostBuffer(kOutDetScores);     // 检测到的目标置信度float *bbox = (float *)buffers.getHostBuffer(kOutDetBBoxes);     // 检测到的目标框// step3 endauto end_3 = std::chrono::high_resolution_clock::now();auto elapsed_3 = std::chrono::duration_cast<std::chrono::microseconds>(end_3 - start_3).count() / 1000.f;// 执行nms(非极大值抑制),得到最后的检测框// step4 startauto start_4 = std::chrono::high_resolution_clock::now();std::vector<Detection> bboxs;yolo_nms(bboxs, num_det, cls, conf, bbox, kConfThresh, kNmsThresh);
void yolo_nms(std::vector<Detection>& res, int32_t* num_det, int32_t* cls, float* conf, float* bbox, float conf_thresh, float nms_thresh) {res.clear();std::map<int32_t, std::vector<Detection>> m;for (int i = 0; i < num_det[0]; i++) {if (conf[i] <= conf_thresh) continue;Detection det;det.bbox[0] = bbox[i * 4 + 0];det.bbox[1] = bbox[i * 4 + 1];det.bbox[2] = bbox[i * 4 + 2];det.bbox[3] = bbox[i * 4 + 3];det.conf = conf[i];det.class_id = cls[i];if (m.count(det.class_id) == 0) m.emplace(det.class_id, std::vector<Detection>());m[det.class_id].push_back(det);}for (auto it = m.begin(); it != m.end(); it++) {auto& dets = it->second;std::sort(dets.begin(), dets.end(), cmp);for (size_t i = 0; i < dets.size(); ++i) {auto& item = dets[i];res.push_back(item);for (size_t j = i + 1; j < dets.size(); ++j) {if (iou(item.bbox, dets[j].bbox) > nms_thresh) {dets.erase(dets.begin() + j);--j;}}}}
}

2.10 开发应用

做完NMS就可以开发应用了, 也是在每一帧里面做的

// 记录所有的检测框中心点std::vector<Point> all_points;// 遍历检测结果for (size_t j = 0; j < bboxs.size(); j++){cv::Rect r = get_rect(frame, bboxs[j].bbox);// 获取检测框中心点Point p_center = {r.x + int(r.width / 2), r.y + int(r.height / 2)};// 筛选labelid为0的检测框if (bboxs[j].class_id == 0){all_points.push_back(p_center);}// 检测框中心点是否在多边形内,在则画红框,不在则画绿框if (isInside(g_ploygon, p_center)){cv::rectangle(frame, r, cv::Scalar(0x00, 0x00, 0xFF), 2);}else{cv::rectangle(frame, r, cv::Scalar(0x27, 0xC1, 0x36), 2);}// 绘制labelid// cv::putText(frame, std::to_string((int)bboxs[j].class_id), cv::Point(r.x, r.y - 10), cv::FONT_HERSHEY_PLAIN, 1.2, cv::Scalar(0x27, 0xC1, 0x36), 2);}// 获取聚集点auto gather_points = gather_rule(all_points, dist_threshold);for (size_t i = 0; i < gather_points.size(); i++){if (gather_points[i].size() < 3)continue;for (size_t j = 0; j < gather_points[i].size(); j++){// std::cout << "聚集点:" << gather_points[i][j].x << "," << gather_points[i][j].y << std::endl;// 绘制聚集点blender_overlay(gather_points[i][j].x, gather_points[i][j].y, 80, frame, 0.3, frameSize.height, frameSize.width);// cv::circle(frame, cv::Point(gather_points[i][j].x, gather_points[i][j].y), 10, cv::Scalar(0, 0, 255), -1);}}// 绘制多边形std::vector<cv::Point> polygon;for (size_t i = 0; i < g_ploygon.size(); i++){polygon.push_back(cv::Point(g_ploygon[i].x, g_ploygon[i].y));}cv::polylines(frame, polygon, true, cv::Scalar(0, 0, 255), 2);// step5 endauto end_5 = std::chrono::high_resolution_clock::now();auto elapsed_5 = std::chrono::duration_cast<std::chrono::microseconds>(end_5 - start_5).count() / 1000.f;

3. 单线程总结

  1. 最耗时的还是读取文件和推流, 后处理CPU跟GPU相比差别不大, 应用开发在后处理完成后做可以节约资源。

4. 多线程runtime 伪代码

可以看成每个线程生产东西,

stage1_vector
stage2void readFrame()
{while (cap.isOpen()){cap >> frame;// 线程1生产frame// 使用stage1互斥锁std::unique_lock<std::mutex> lock(stage_1_mutex);stage1.condition wait();    // 满了就堵塞线程1stage1_vector.push(frame);  // 把frame放到共享资缓存区stage1.condition notify();  // 唤醒线程1消费}
}
void inference()
{// 消费frame// 使用stage1互斥锁std::unique_lock<std::mutex> lock(stage_1_mutex);// 如果消费完了frame就堵塞线程1消费 再唤醒线程1生产stage1_not_empty();预处理推理后处理// 线程2 生产十个框, 数据类型是 std::vector<bufferItem>然后叫消费者去消费这十个框    }
void postProcess()
{线程2空了就堵塞消费 唤醒线程2生产应用开发线程3生产 std::vector<Mat>}
void streamer()
{推流线程3消费
}

读取视频文件有显著提升但是RTSP几乎没变,因为这个是跟相机的帧率的