基于线程池的配电房图像检测

目录

[第1章 基础知识](#第1章 基础知识)

[1.1 帧率](#1.1 帧率)

[1.2 分辨率](#1.2 分辨率)

[第2章 图像检测](#第2章 图像检测)

[2.1 图像检测流程](#2.1 图像检测流程)

[2.2 框架选择](#2.2 框架选择)

[2.3 图像处理脚手架](#2.3 图像处理脚手架)

[第3章 线程池处理一路视频流](#第3章 线程池处理一路视频流)

[3.1 基本思路](#3.1 基本思路)

[3.2 如何保证输出顺序与输入视频帧顺序一致](#3.2 如何保证输出顺序与输入视频帧顺序一致)

[3.2 工程应用](#3.2 工程应用)

[3.2.1 图像检测任务](#3.2.1 图像检测任务)

[3.2.2 线程池](#3.2.2 线程池)

[3.2.3 主线程](#3.2.3 主线程)

[第4章 线程池处理多路视频流](#第4章 线程池处理多路视频流)

[4.1 工程应用一(不推荐)](#4.1 工程应用一(不推荐))

[4.1.1 为每路 RTSP 流独立管理一套上下文](#4.1.1 为每路 RTSP 流独立管理一套上下文)

[4.2 工程应用二(推荐)](#4.2 工程应用二(推荐))


第1章 基础知识

1.1 帧率

帧率(FPS, Frames Per Second)是指每秒显示的画面帧数。帧率越高,画面越流畅,动态效果越好。常见的帧率有24fps、30fps、60fps等。帧率:动态画面的流畅度。

  1. 24fps:电影行业的标准帧率,足以捕捉并展现大部分动态场景。
  2. 30fps:网络视频和电视节目的常见帧率,提供更为流畅的观看体验。
  3. 60fps及以上:高帧率视频在快速移动的场景中表现出色,但并非所有设备都支持高帧率播放。

1.2 分辨率

分辨率是指视频画面在一定区域内包含的像素点的数量。像素是构成图像的最小单位,每个像素都有特定的颜色和亮度。分辨率通常以"宽×高"的形式表示,如1920×1080,这代表视频的水平方向有1920个像素,垂直方向有1080个像素。

美国电影电视工程师协会(‌SMPTE)‌制定了一系列的高清数字电视格式标准,其中包含了常用的分辨率标准:

  1. 720P: ‌其有效显示格式为1280×720,‌采用逐行扫描(‌Progressive scanning)‌方式
  2. 1080P:‌其有效显示格式为1920×1080,‌也是采用逐行扫描方式。
  3. 1080i: 与1080p相对,‌1080i采用隔行扫描(‌Interlace scanning)‌,‌也是1920×1080的分辨率。‌这种格式在传输过程中可能提供更高的帧率,‌但图像质量相对于1080p可能会有所降低。
  4. 2K:水平分辨率达到约2000像素,最标准的2K分辨率为2048×1024,常见的2K分辨率为2560×1440(16:9)。(UHDTV1)4K:UHDTV1由SMPTE ST 2036-1标准定义的VHD超高清系统,其分辨率为3840×2160(16:9)。(UHDTV2)
  5. 8K:UHDTV2同样由SMPTE ST 2036-1标准定义的VHD超高清系统,分辨率达到7680×4320。

第2章 图像检测

2.1 图像检测流程

2.2 框架选择

目标检测:yolo系列

推理框架:OpenCV C++

2.3 图像处理脚手架

MFC动态创建类

第3章 线程池处理一路视频流

为什么要做线程池来做视频流检测?

答:配电房的RTSP帧率是25FPS。模型检测一帧图像需要18ms,想要做到实时推理检测是不可能,所以需要使用多线程。然而线程的创建和销毁很消耗CPU资源,所以我们为了减轻CPU的资源消耗提前创建好一个线程池。

3.1 基本思路

(1)创建一个线程池(线程池中包含4个线程对象,线程对象本质是一个图像处理对象);

(2)创建一个生产者线程,生产者线程获取视频流中每一帧图像,并且递交到线程池中的任务队列中。注意提交的时候需要将帧ID也提交过去。即任务队列是一个存放未处理帧ID和帧的容器。

cpp 复制代码
std::queue<std::pair<int, cv::Mat>> tasks;

(3)创建一个消费者线程,消费者线程从线程池中的结果容器中获取检测好的结果。注意消费者线程在获取检测结果帧的同时需要同时带着帧ID去获取。即存放检测结果的容器是存放具体帧和具体帧对应的结果的容器。

cpp 复制代码
std::map<int, cv::Mat> img_results;

3.2 如何保证输出顺序与输入视频帧顺序一致

帧ID 线程ID 处理时间 完成时间
0 0 20 ms 20 ms
1 1 40 ms 40 ms
2 2 10 ms 10 ms
3 3 18ms 18 ms
4 2 5 ms 5 ms

工程jiegou是 生产者 + 消费者 + 线程池(多个线程) 的结构:

  • 生产者线程从 RTSP 获取视频帧,依次编号(例如 id = 0, 1, 2, 3, ...),然后调用:

cpp 复制代码
threadPool.submitTask(frame, id);

队列中的结构:

{(第0帧,cv::Mat), (第1帧,cv::Mat), (第2帧,cv::Mat), (第3帧,cv::Mat), (第4帧,cv::Mat), ..........}

注意:在queue容器中未检测的视频帧,是按照帧顺序来存放的。

  • 多个**工作线程(线程池中的图像处理对象)**并发执行:
cpp 复制代码
imgProcessObj->run(task.second, resultImg);

所以帧的处理是并行异步完成的。

  • 处理完成的结果保存到map中
cpp 复制代码
{
      std::lock_guard<std::mutex> lock(mtx2);
      img_results.insert({task.first, resultImg});
      cv_result.notify_one();
}

map中的结构(仅仅针对表中的4帧数据)

{(第2帧,cv::Mat), (第4帧,cv::Mat), (第3帧,cv::Mat), (第0帧,cv::Mat), (第1帧,cv::Mat), ..........}

注意:在map容器中检测完的视频帧,并不是按照帧顺序来存放的,而是谁先检查完毕,先存放谁。

  • 消费者线程,从map中来获取检测结果时,就需要一个自增的ID来map中获取帧。

3.2 工程应用

3.2.1 图像检测任务

cpp 复制代码
#ifndef IMGPROCESS_H
#define IMGPROCESS_H

#include <opencv2/opencv.hpp>

class CImgProcess {
public:
    explicit CImgProcess();
    ~CImgProcess();

public:
    void run(cv::Mat &srcImg, cv::Mat &resultImg);
    
};

#endif /**IMGPROCESS_H */

cpp 复制代码
#include "imgprocess.h"


CImgProcess::CImgProcess()
{

}

CImgProcess::~CImgProcess()
{

}

void CImgProcess::run(cv::Mat &srcImg, cv::Mat &resultImg)
{
    // 确保源图像不是空的
    if(srcImg.empty()) {
        std::cerr << "src NULL" << std::endl;
        return;
    }

    // 使用OpenCV的cvtColor函数将图像转换为灰度图像
    cv::cvtColor(srcImg, resultImg, cv::COLOR_BGR2GRAY);
}

3.2.2 线程池

cpp 复制代码
#ifndef THREADPOOL_H
#define THREADPOOL_H

#include <opencv2/opencv.hpp>
#include "imgprocess.h"


class CThreadPool
{
public:
    explicit CThreadPool();
    ~CThreadPool();

public:
    /**设置线程池 */
    void setUp(int threadCount);

    /**提交要处理的任务 */
    int submitTask(const cv::Mat &img, int id);

    /**获取任务处理结果 */
    int getTargetResult(cv::Mat &resultImg, int id);

    /**停止所有线程 */
    void stopAll();

private:
    /**线程池的工作场 */
    void worker(int id);

private:
    /**线程池当前状态,默认是没有停止运行的。也就是stop = false*/
    bool stop;  

    /**处理任务的对象 */
    std::vector<std::shared_ptr<CImgProcess>> taskProcessorObj;

    /**线程对象 */
    std::vector<std::thread> threads;

    /**待处理的任务 */
    std::queue<std::pair<int, cv::Mat>> tasks;

    /**处理任务的对象去获取任务时需要一把锁 */
    std::mutex mtx1;

    /**处理任务的对象去获取任务时需要一把锁, 这把锁的条件变量 */
    std::condition_variable cv_task;

    /**处理对象,处理完成的结果 */
    std::map<int, cv::Mat> img_results;

    /**处理任务的对象,处理完成的结果需要放置到Map中,所以需要一把锁 */
    std::mutex mtx2;

    /**处理任务的对象,处理完成的结果需要放置到Map中,所以需要一把锁,这把锁自然要一个条件变量 */
    std::condition_variable cv_result;
};

#endif /**THREADPOOL_H */

cpp 复制代码
#include "threadpool.h"
#include <iostream>
#include <chrono>

// 构造函数
CThreadPool::CThreadPool()
    : stop(false)
{
}

// 析构函数
CThreadPool::~CThreadPool()
{
    stopAll();
}

// 设置线程池大小和初始化
void CThreadPool::setUp(int threadCount)
{
    // 创建处理任务的对象
    for (int i = 0; i < threadCount; ++i) {
        std::shared_ptr<CImgProcess> taskProcessorObjItem = std::make_shared<CImgProcess>();
        taskProcessorObj.push_back(taskProcessorObjItem);
    }

    // 创建线程
    for (int i = 0; i < threadCount; ++i) {
        threads.emplace_back(&CThreadPool::worker, this, i); // 注意这里传递了 'this'
    }
}

// 工作线程执行的任务
void CThreadPool::worker(int id)
{
    while (!stop) {
        std::pair<int, cv::Mat> task;
        std::shared_ptr<CImgProcess> imgProcessObj = taskProcessorObj[id];

        {
            std::unique_lock<std::mutex> lock(mtx1);
            cv_task.wait(lock, [this] { return !tasks.empty() || stop; });

            if (stop) {
                return;
            }

            task = tasks.front();
            tasks.pop();
        }

        cv::Mat resultImg;
        imgProcessObj->run(task.second, resultImg);

        {
            std::lock_guard<std::mutex> lock(mtx2);
            img_results.insert({task.first, resultImg});
            cv_result.notify_one();
        }
    }
}

// 提交任务到线程池
int CThreadPool::submitTask(const cv::Mat &img, int id)
{
    {
        std::lock_guard<std::mutex> lock(mtx1);
        tasks.push({id, img});
    }
    cv_task.notify_one();

    return 0;
}

// 获取任务结果
int CThreadPool::getTargetResult(cv::Mat &resultImg, int id)
{
    int loop_cnt = 0;
    while (img_results.find(id) == img_results.end()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
        loop_cnt++;

        if (loop_cnt > 1000) {
            std::cout << "getTargetImgResult timeout" << std::endl;
            return -1;
        }
    }

    std::lock_guard<std::mutex> lock(mtx2);
    resultImg = img_results[id];
    img_results.erase(id);

    return 0;
}

// 停止所有工作线程
void CThreadPool::stopAll()
{
    stop = true;
    cv_task.notify_all();
    for (auto& thread : threads) {
        if (thread.joinable()) {
            thread.join();
        }
    }
}

3.2.3 主线程

cpp 复制代码
#include <iostream>
#include <opencv2/opencv.hpp>
#include <memory>
#include "imgprocess.h"
#include "threadpool.h"

#if 0
const std::string ImgPath = "E:/Mytest/test20250430/apple.jpg";

int main(int argc, char **argv){
    cv::Mat srcImg = cv::imread(ImgPath, cv::IMREAD_COLOR);
    if(srcImg.empty()){
        std::cerr << "Failed to load image: " << ImgPath << std::endl;
        return -1;
    }

    cv::Mat resultImg;
    std::shared_ptr<CImgProcess> imgProcess = std::make_shared<CImgProcess>();
    imgProcess->run(srcImg, resultImg);

    cv::namedWindow("abc");
    cv::imshow("abc", resultImg);
    
    cv::waitKey(0);
    cv::destroyAllWindows();
    return 0;
}

#endif

// 设置 RTSP 流地址
std::string rtsp_url1 = "rtsp://admin:bpg123456@10.10.12.228:554/h264/2/main/av_stream";

static int g_frame_start_id = 0; // 读取视频帧的索引
static int g_frame_end_id = 0;   // 模型处理完的索引

// 创建线程池
static CThreadPool *yolov5_thread_pool = nullptr;
bool end = false;

/**获取视频流(生产者) */
void read_stream(std::string rtsp_addr);

/**获取处理结果(消费者) */
void get_results(bool record = false);

int main(int argc, char **argv){

    // 实例化线程池
    yolov5_thread_pool = new CThreadPool();
    yolov5_thread_pool->setUp(4);

    // 读取视频
    std::thread read_stream_thread(read_stream, rtsp_url1);

    // 启动结果线程
    std::thread result_thread(get_results, true);

    // 等待线程结束
    read_stream_thread.join();
    result_thread.join();
    return 0;
}

void read_stream(std::string rtsp_addr)
{
    // 读取视频
    cv::VideoCapture cap(rtsp_addr);
    if (!cap.isOpened())
    {
        std::cout << "Failed to open video file: " << rtsp_addr << std::endl;
    }

    // 获取视频尺寸、帧率
    int width = cap.get(cv::CAP_PROP_FRAME_WIDTH);
    int height = cap.get(cv::CAP_PROP_FRAME_HEIGHT);
    int fps = cap.get(cv::CAP_PROP_FPS);
    printf("Video size: %d x %d, fps: %d", width, height, fps);

    // 画面
    cv::Mat img;

    while (true)
    {

        // 读取视频帧
        cap >> img;
        if (img.empty())
        {
            printf("Video end.");
            // 等待一下没有处理结束的画面
            std::this_thread::sleep_for(std::chrono::milliseconds(5000));
            end = true;
            break;
        }

        // 提交任务,这里使用clone,因为不这样数据在内存中可能不连续,导致绘制错误
        yolov5_thread_pool->submitTask(img.clone(), g_frame_start_id++);
    }
    // 释放资源
    cap.release();
}

void get_results(bool record)
{
    // 记录开始时间
    auto start_all = std::chrono::high_resolution_clock::now();
    int frame_count = 0;
    std::string fps_str;
    std::string duration_str;

    cv::VideoWriter writer;
    if(record)
    {
        writer = cv::VideoWriter("thread_pool_demo.mp4", cv::VideoWriter::fourcc('a', 'v', 'c', '1'), 30, cv::Size(1280, 720));
    }
    // 开始计时
    auto start_1 = std::chrono::high_resolution_clock::now();

    while (true)
    {
        // 结果
        cv::Mat img;
        auto ret = yolov5_thread_pool->getTargetResult(img, g_frame_end_id++);
        // 如果读取完毕,且模型处理完毕,结束
        if (end && ret != 0)
        {
            break;
        }
        frame_count++;
        // all end
        auto end_all = std::chrono::high_resolution_clock::now();
        auto elapsed_all_2 = std::chrono::duration_cast<std::chrono::microseconds>(end_all - start_all).count() / 1000.f;
        // 每隔1秒打印一次
        if (elapsed_all_2 > 1000)
        {
            printf("Method2 Time:%fms, FPS:%f, Frame Count:%d", elapsed_all_2, frame_count / (elapsed_all_2 / 1000.0f), frame_count);
            fps_str = std::to_string(frame_count) + "fps";
            frame_count = 0;
            start_all = std::chrono::high_resolution_clock::now();
        }
        
        if(record)
        {
            auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end_all - start_1).count() / 1000.f;
            duration_str = std::to_string(duration) + "ms";
            cv::putText(img, fps_str , cv::Point(10, 30), cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(0, 0, 255), 2);
            cv::putText(img, duration_str, cv::Point(10, 50), cv::FONT_HERSHEY_PLAIN, 1.2, cv::Scalar(0xFF, 0xFF, 0xFF), 2);
            // 写入视频帧
            writer << img;
        }

    }
    // 结束所有线程
    yolov5_thread_pool->stopAll();
    if (writer.isOpened())
    {
        writer.release();
    }
    printf("Get results end.");
}

第4章 线程池处理多路视频流

4.1 工程应用一(不推荐)

4.1.1 为每路 RTSP 流独立管理一套上下文

cpp 复制代码
struct StreamContext
{
    std::string rtsp_url;
    std::atomic<int> frame_start_id{0};
    std::atomic<int> frame_end_id{0};
    std::atomic<bool> end{false};
    CThreadPool *thread_pool = nullptr;
    std::thread read_thread;
    std::thread result_thread;
};


int main(int argc, char **argv)
{
    std::vector<std::string> rtsp_urls = {
        "rtsp://admin:pwd@10.10.12.228:554/h264/2/main/av_stream",
        "rtsp://admin:pwd@10.10.12.229:554/h264/2/main/av_stream",
        "rtsp://admin:pwd@10.10.12.230:554/h264/2/main/av_stream",
        // ... 共10路
    };

    std::vector<std::unique_ptr<StreamContext>> streams;

    for (auto &url : rtsp_urls)
    {
        auto ctx = std::make_unique<StreamContext>();
        ctx->rtsp_url = url;
        ctx->thread_pool = new CThreadPool();
        ctx->thread_pool->setUp(4); // 每路4线程(或共享全局线程池)

        ctx->read_thread = std::thread(read_stream, std::ref(*ctx));
        ctx->result_thread = std::thread(get_results, std::ref(*ctx), true);

        streams.push_back(std::move(ctx));
    }

    // 等待所有线程结束
    for (auto &ctx : streams)
    {
        ctx->read_thread.join();
        ctx->result_thread.join();
        ctx->thread_pool->stopAll();
        delete ctx->thread_pool;
    }

    return 0;
}

**1路视频流创建一个线程池(4个处理线程) + 一个生产者线程 + 一个消费者线程 = 6个线程。**假设现场有40路视频流,就会创建240个线程。

4.2 工程应用二(推荐)

把每路 StreamContext 的线程池改成 共享全局线程池 ,只保留每路的 read_thread 和 result_thread

相关推荐
机器之心5 小时前
谷歌最强AI,被港科大开源超了?让海外创作者喊出「King Bomb」的P图大杀器来了
人工智能·openai
DKunYu5 小时前
PyTorch入门
人工智能·pytorch·python·深度学习
机器之心5 小时前
谷歌重大突破!量子计算首次可验证,登《Nature》封面
人工智能·openai
gustt5 小时前
每天一杯科技资讯咖啡,用 n8n 喂给 AI
人工智能·node.js
技术闲聊DD5 小时前
深度学习(8)- PyTorch 数据处理与加载
人工智能·pytorch·深度学习
Juchecar5 小时前
从LLM训练和推理角度,区分 “知识”与“智慧”
人工智能
掘金一周5 小时前
一个前端工程师的年度作品:从零开发媲美商业级应用的后台管理系统 | 掘金一周 10.23
前端·人工智能·后端
哈基鑫5 小时前
YOLOv3 核心笔记
笔记·yolo·目标跟踪
Axis tech5 小时前
TESOLLO:使用MANUS & Franka机械臂提高机器人灵活性
人工智能·机器人