前言
多年前刚刚接触Opencv,还没有AI,那个时候第一次处理视频的时候,仅仅通过usb摄像头显示都还可以,但是通过rtsp等网络方式的方法接入,在显示图像的过程再处理点什么,那简直是卡,
通过网上搜索,建议使用多线程处理,然后一堆代码,终于从里面理清了,最近也有同事遇到同样的问题,我说让看代码,他说里面掺杂了太多业务,不太好看明白,所以才有了这篇文章:从最简单的方法实现多线程处理视频流,不参与任何业务。
一个负责获取视频帧,一个负责处理,使用最简单的实践达到目的。同理的思想用于在一个系统之上,我开发了一个QT快速开发系统,也是使用最简单,不掺杂任何业务的实现,避免其他人为了吃顿饭,还要买个锅碗瓢盆
QT快速开发框架
一、最简单的摄像头显示程序
让我们从最基础的版本开始:一个单线程程序,直接从摄像头读取并显示画面。
基础版本代码
cpp
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
int main() {
// 打开摄像头(默认摄像头编号0)
cv::VideoCapture cap(0);
if (!cap.isOpened()) {
cerr << "Error: Could not open camera!" << endl;
return -1;
}
cv::Mat frame;
while (true) {
cap >> frame; // 读取一帧
if (frame.empty()) {
cerr << "Error: Empty frame!" << endl;
break;
}
cv::imshow("Camera", frame); // 显示画面
char key = cv::waitKey(1);
if (key == 'q' || key == 'Q') {
break; // 按q键退出
}
}
cap.release();
cv::destroyAllWindows();
return 0;
}
基础版本的特点
- 优点:简单直接,易于理解
- 缺点:所有操作都在一个线程中执行,如果添加复杂的图像处理,会导致画面卡顿
二、尝试使用线程
初学者可能会尝试将摄像头读取放入单独的线程
cpp
#include <iostream>
#include <opencv2/opencv.hpp>
#include <thread>
using namespace std;
void captureThread() {
cv::VideoCapture cap(0);
if (!cap.isOpened()) {
std::cerr << "Error: Could not open camera!" << std::endl;
return;
}
cv::Mat frame;
while (true) {
cap >> frame;
if (frame.empty()) break;
cv::imshow("Camera", frame); // 错误:在子线程中显示
char key = cv::waitKey(1);
if (key == 'q' || key == 'Q') break;
}
cap.release();
cv::destroyAllWindows();
}
int main() {
thread captureVideo(captureThread);
captureVideo.join();
return 0;
}
三、再进一步使用双线程实现
cpp
#include <iostream>
#include <opencv2/opencv.hpp>
#include <thread>
#include <mutex>
#include <atomic>
using namespace std;
// 共享数据
cv::Mat sharedFrame;
mutex mtx;
atomic<bool> running(true);
// 线程1:负责捕获视频帧
void captureThread() {
cv::VideoCapture cap(0);
if (!cap.isOpened()) {
cerr << "Error: Could not open camera!" << endl;
running = false;
return;
}
cv::Mat frame;
while (running) {
cap >> frame;
if (frame.empty()) {
cerr << "Error: Empty frame!" << endl;
break;
}
// 使用互斥锁保护共享数据
lock_guard<mutex> lock(mtx);
frame.copyTo(sharedFrame);
}
cap.release();
}
// 线程2:负责处理和显示
void displayAndProcessThread() {
cv::namedWindow("Camera", cv::WINDOW_AUTOSIZE);
while (running) {
cv::Mat frame;
// 获取最新的帧
{
lock_guard<mutex> lock(mtx);
if (!sharedFrame.empty()) {
sharedFrame.copyTo(frame);
}
}
if (!frame.empty()) {
// ===== 在这里添加你的图像处理代码 =====
// 示例1:添加文字
string text = "Hello OpenCV!";
cv::putText(frame, text, cv::Point(50, 50),
cv::FONT_HERSHEY_SIMPLEX, 1.0,
cv::Scalar(0, 255, 0), 2);
// 示例2:添加时间戳信息
cv::putText(frame, "Press 'q' to quit",
cv::Point(10, frame.rows - 10),
cv::FONT_HERSHEY_SIMPLEX, 0.5,
cv::Scalar(0, 0, 255), 1);
// 显示处理后的画面
cv::imshow("Camera", frame);
}
// 检查退出条件
char key = cv::waitKey(30);
if (key == 'q' || key == 'Q') {
running = false;
break;
}
}
cv::destroyAllWindows();
}
int main() {
cout << "Program started. Press 'q' to quit." << endl;
// 创建两个线程
thread capture(captureThread);
thread display(displayAndProcessThread);
// 等待线程结束
display.join();
capture.join();
cout << "Program terminated." << endl;
return 0;
}
四、代码解析
1. 线程同步机制
cpp
mutex mtx; // 互斥锁,防止数据竞争
atomic<bool> running; // 原子变量,控制线程结束
- 互斥锁:确保同一时刻只有一个线程访问共享数据
- 原子变量:安全地在多线程间传递状态信息
2. 线程分工
| 线程 | 职责 | 说明 |
|---|---|---|
| captureThread | 捕获视频帧 | 持续从摄像头读取,存入共享变量 |
| displayAndProcessThread | 处理和显示 | 获取帧,添加特效,显示画面 |
3. 关键代码说明
cpp
// 保护共享数据的访问
{
lock_guard<mutex> lock(mtx); // 自动加锁解锁
frame.copyTo(sharedFrame); // 安全的拷贝
}
五、进阶:添加更多图像处理效果
你可以在显示线程中添加各种OpenCV特效:
cpp
// 在显示线程的处理部分添加
// 1. 转为灰度图
cv::Mat gray;
cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
// 2. 边缘检测
cv::Mat edges;
cv::Canny(gray, edges, 50, 150);
// 3. 人脸检测(需要haar cascade文件)
// cv::CascadeClassifier faceCascade;
// faceCascade.load("haarcascade_frontalface_default.xml");
// vector<cv::Rect> faces;
// faceCascade.detectMultiScale(gray, faces);
// 4. 添加帧率显示
static int frameCount = 0;
static auto startTime = chrono::steady_clock::now();
frameCount++;
auto currentTime = chrono::steady_clock::now();
auto elapsed = chrono::duration_cast<chrono::seconds>(currentTime - startTime).count();
if (elapsed >= 1) {
double fps = frameCount / elapsed;
cout << "FPS: " << fps << endl;
frameCount = 0;
startTime = currentTime;
}
六、总结
单线程 vs 双线程对比
| 特性 | 单线程 | 双线程 |
|---|---|---|
| 实现复杂度 | 简单 | 中等 |
| 响应性 | 好 | 极好 |
| 处理复杂任务 | 会卡顿 | 流畅 |
| CPU利用率 | 一般 | 更好 |
| 代码可维护性 | 简单 | 良好 |
多线程编程要点
- 正确使用互斥锁保护共享数据
- 避免死锁:注意加锁顺序
- 使用原子变量控制线程状态
- 确保主线程等待子线程结束
- OpenCV的显示操作必须在主线程
需要一份无污染的代码,不包含任何业务
QT快速开发框架