前言
多年前刚刚接触Opencv那会还没有AI,第一次处理视频的时候,仅仅通过usb摄像头显示都还可以,但是通过rtsp等网络方式的方法接入,在显示图像的过程再处理点什么,那简直是卡,通过网上搜索,建议使用多线程处理,然后一堆代码,终于从里面理清了,最近也有同事遇到同样的问题,我说让看代码,他说里面掺杂了太多业务,不太好看明白,所以才有了这篇文章:从最简单的方法实现多线程处理视频流,不参与任何业务。一个负责获取视频帧,一个负责处理,使用最简单的实践达到目的。同理的思想用于在一个系统之上,我开发了一个QT快速开发系统,也是使用最简单,不掺杂任何业务的实现,避免其他人为了吃顿饭,还要买个锅碗瓢盆
一、最简单的摄像头显示程序
让我们从最基础的版本开始:一个单线程程序,直接从摄像头读取并显示画面。
基础版本代码
#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;
}
基础版本的特点
- 优点:简单直接,易于理解
- 缺点:所有操作都在一个线程中执行,如果添加复杂的图像处理,会导致画面卡顿
二、尝试使用线程
初学者可能会尝试将摄像头读取放入单独的线程
#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;
}
三、再进一步使用双线程实现
#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. 线程同步机制
mutex mtx; // 互斥锁,防止数据竞争
atomic<bool> running; // 原子变量,控制线程结束
- 互斥锁:确保同一时刻只有一个线程访问共享数据
- 原子变量:安全地在多线程间传递状态信息
2. 线程分工
3. 关键代码说明
// 保护共享数据的访问
{
lock_guard<mutex> lock(mtx); // 自动加锁解锁
frame.copyTo(sharedFrame); // 安全的拷贝
}
五、进阶:添加更多图像处理效果
你可以在显示线程中添加各种OpenCV特效:
// 在显示线程的处理部分添加
// 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 双线程对比
多线程编程要点
- 正确使用互斥锁保护共享数据
- 避免死锁:注意加锁顺序
- 使用原子变量控制线程状态
- 确保主线程等待子线程结束
- OpenCV的显示操作必须在主线程
改进点
有朋友说我这个会导致使用的那个线程空转,然后上面那个如果挂了,底下一直阻塞。的确是有这个问题,我的出发点是最简单的实现,既然这么说了,那就优化一下
#include <iostream>
#include <opencv2/opencv.hpp>
#include <thread>
using namespace std;
cv::Mat shareFrame;
atomic<bool> running(true);
mutex mtx;
condition_variable condin_v;
bool frameReady = false;
void captureThread()
{
cv::VideoCapture cap(0);
if (!cap.isOpened())
{
std::cerr << "Error: Could not open camera!" << std::endl;
running = false;
condin_v.notify_all();
return;
}
cv::Mat frame;
cap.set(cv::CAP_PROP_FPS, 30);
while (running) {
cap >> frame;
if (frame.empty())
{
std::cerr << "Error: Empty frame!" << std::endl;
break;
}
{
lock_guard<mutex> lock(mtx);
frame.copyTo(shareFrame);
frameReady = true;
}
condin_v.notify_one();
this_thread::sleep_for(chrono::microseconds(33));
}
cap.release();
condin_v.notify_all();
}
void displayAndProcessThread()
{
cv::namedWindow("Camera", cv::WINDOW_AUTOSIZE);
cv::Mat frame;
while (running)
{
{
unique_lock<mutex> lock(mtx);
condin_v.wait(lock, [] {return frameReady || !running; });
if (!running) break;
if (!shareFrame.empty())
{
shareFrame.copyTo(frame);
frameReady = false;
}
}
if (!frame.empty())
{
string text = "Hello Opencv!";
cv::putText(frame, text, cv::Point(50, 50), cv::FONT_HERSHEY_DUPLEX, 1.0, cv::Scalar(0, 255, 0), 2);
cv::imshow("Camera", frame);
}
char key = cv::waitKey(30);
if (key == 'Q' || key == 'q')
{
running = false;
condin_v.notify_all();
break;
}
}
cv::destroyAllWindows();
}
int main() {
thread captureVideo(captureThread);
thread display(displayAndProcessThread);
captureVideo.join();
display.join();
return 0;
}
时间 →
生产者线程 | 消费者线程
--------------------|--------------------
获取锁 |
生产帧 | (可能正在等待)
frameReady = true |
释放锁 |
notify_one() ------→ 被唤醒
| 尝试获取锁
| 获取锁成功
| 检查 frameReady = true
| 消费帧
| frameReady = false
| 释放锁
| 处理并显示帧
文章转载自: Tlink