一:Mat 图像容器
Mat是OpenCV 里装图像的 "盒子",将图像的像素数组 + 宽度 + 高度 + 格式信息全部打包在一起。
1.Mat 里存了什么?
一张图片 = 一堆像素点Mat 就是存这些像素:
- cols:宽度
- rows:高度
- channels:通道数(1 = 灰度,3 = 彩色,4 = 带透明)
- data:真正的像素数据(uchar*)
- type:图像格式(CV_8UC1 / CV_8UC3...)
2.常用的Mat格式
1.CV_8UC1
8 位无符号,1 通道 → 灰度图
2.CV_8UC3
8 位无符号,3 通道 → 彩色图 注意:OpenCV 默认是 BGR 顺序
3.CV_8UC4
8 位无符号,4 通道 → BGRA 带透明
3.mat最常用的操作
1.imread 读取图像
cpp
Mat img = imread("test.jpg"); // 读取图片
Mat frame; // 空图
2.判断空图
cpp
if (img.empty()) {
// 图片加载失败
}
3.获取宽,高,通道数
cpp
int w = img.cols;
int h = img.rows;
int ch = img.channels();
4.复制图像
cpp
Mat a = img.clone(); // 深拷贝(安全)
Mat b = img; // 浅拷贝(共用数据)
5.imshow 显示图像
cpp
Mat img = imread("test.jpg");
imshow("窗口标题", img); // 显示
waitKey(0); // 等待按键(必须写,不然一闪而过)
注意:只能在主线程中调用,子线程中不会生效。
6.imwrite 保存图像
cpp
Mat frame; // 你的图像
// 保存到当前目录
imwrite("保存.jpg", frame);
// 保存到指定路径
imwrite("/home/liu/图片/保存.png", frame);
二:图像颜色格式
0.转换函数
cpp
// 源图像,目标图像,转换码
cvtColor(src, dst, 码表);
1.BGR(OpenCV默认)
- 顺序:蓝 绿 红
- 来源:OpenCV 天生就是 BGR
- 用途:读图、摄像头、YOLO 推理
cpp
Mat img = imread("a.jpg"); // 默认 BGR
2.BGRA(带透明通道)
- 4 通道
- BGR + 透明(Alpha)
- 透明图片用(PNG)
- 用途:贴图、水印、带透明的纹理
cpp
cvtColor(mat, mat, COLOR_BGR2BGRA);
3.RGB(Qt/OpenGL/ 屏幕显示专用)
- 3 通道
- 顺序:红 绿 蓝
- Qt、图片浏览器、屏幕 都用 RGB
- 你必须转成 RGB 才能正常显示
cpp
cvtColor(mat, mat, COLOR_BGR2RGB);
4.GRAY(灰度图)
- 1 通道
- 只有亮度,没有颜色
- 黑→灰→白
- 用途:去色、识别、压缩
cpp
cvtColor(mat, mat, COLOR_BGR2GRAY);
5.HSV(颜色识别专用)
- 3 通道:色调、饱和度、亮度
- 用途:根据颜色找物体(红 / 黄 / 蓝物体跟踪)
- 比 RGB/BGR 更适合识别颜色
cpp
cvtColor(mat, mat, COLOR_BGR2HSV);
6./YUV(摄像头原始格式)
- 摄像头输出最常见格式
- Y = 亮度,UV = 颜色
- 常见:YUYV、UYVY、NV12、NV21
- 必须转 BGR/RGB 才能显示
cpp
cvtColor(mat, mat, COLOR_YUV2BGR_YUYV);
7.综合对比表
| 格式 | 通道 | 特点 | 谁在用 |
|---|---|---|---|
| BGR | 3 | OpenCV 默认 | OpenCV、YOLO |
| BGRA | 4 | 带透明 | PNG、纹理 |
| RGB | 3 | 正常颜色 | Qt、OpenGL、屏幕 |
| GRAY | 1 | 黑白 | 去色、识别 |
| HSV | 3 | 颜色识别 | 颜色跟踪 |
| YUV | 2/3 | 摄像头原始 | 摄像头、RTSP |
三:Mat转换为QImage
Qt中界面显示一般都采用QImage容器,不能直接显示Mat容器,
Mat是OpenCV的图像格式,QImage是Qt的图像格式
Mat的颜色顺序是BGR,QImage的颜色顺序的RGB
cpp
/**
* @brief 万能 Mat 转 QImage(支持所有常见格式)
* @param mat 输入 OpenCV Mat(支持 CV_8UC1 CV_8UC3 CV_8UC4)
* @return 输出 Qt QImage(可直接显示/上传OpenGL)
*/
QImage matToQImage(const cv::Mat &mat)
{
// 1. 空图直接返回空
if (mat.empty())
return QImage();
// 2. 获取宽高
int width = mat.cols;
int height = mat.rows;
// 3. 根据 Mat 类型转 QImage
switch (mat.type()) {
// ==========================
// 灰度图 CV_8UC1
// ==========================
case CV_8UC1: {
QImage img(mat.data, width, height, mat.step, QImage::Format_Grayscale8);
return img.copy(); // 深拷贝,安全
}
// ==========================
// 彩色图 BGR → RGB(最重要)
// ==========================
case CV_8UC3: {
cv::Mat rgb;
cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB); // 必须转!
QImage img(rgb.data, width, height, rgb.step, QImage::Format_RGB888);
return img.copy();
}
// ==========================
// 带透明通道 BGRA → RGBA
// ==========================
case CV_8UC4: {
QImage img(mat.data, width, height, mat.step, QImage::Format_RGBA8888);
return img.copy();
}
// 不支持的格式
default:
return QImage();
}
}
四:VideoCapture 视频捕获
VideoCapture = 视频 / 摄像头的 "读取器",支持:
1.打开摄像头
cpp
cap.open(0); // 0=默认摄像头
cap.open(-1); //自动搜索
cap.open(0, cv::CAP_V4L2); //Linux下强制 V4L2 防止阻塞
2.本地视频
cpp
cap.open("test.mp4"); // 视频
3.网络视频流
cpp
cap.open("rtsp://..");// 网络流
4. 判断是否打开成功
cpp
if (!cap.isOpened()) {
qDebug() << "打开失败!";
}
5. 读一帧图像
cpp
Mat frame;
cap.read(frame);
6.关闭摄像头
cpp
// 关闭摄像头,释放硬件
cap.release();
7.读取宽,高,帧率
cpp
// 宽度
int w = cap.get(CAP_PROP_FRAME_WIDTH);
// 高度
int h = cap.get(CAP_PROP_FRAME_HEIGHT);
// 帧率
double fps = cap.get(CAP_PROP_FPS);
8.摄像头线程标准写法
cpp
void CameraThread::run()
{
VideoCapture cap;
cap.open(0); // 打开摄像头
if (!cap.isOpened()) {
qDebug() << "摄像头打开失败!";
return;
}
Mat frame;
while (m_running)
{
cap.read(frame);
if (frame.empty()) continue;
// 发给界面显示
emit frameReady(frame);
msleep(30);//精准 30fps
}
cap.release();
}
为什么不采用定时器捕获?
① 定时器精度差:QTimer 最小只能到 15ms
② 定时器依附主线程事件循环:界面一滑动、绘图、加载东西,定时器直接被卡住,延迟
③ 无法做到真正实时:摄像头是连续不断输出数据定时器是间隔性去拿→ 必然丢帧
五:简易视频播放器(QLabel版本+OpenGL版本)
1.效果图
显示图片

显示本地视频

打开本地摄像头 
打开网络视频流
以下是几个可用测试地址(可能有延迟,打开后需要等几秒钟):
rtmp://mobliestream.c3tv.com:554/live/goodtv.sdp
rtmp://ns8.indexforce.com/home/mystream
rtmp://ns8.indexforce.com/home/mystream

2.整体思路
1.现在设计师界面拖入一个QLabel和一个QOpenGLWidget,在下方按钮添加几个想要的功能。
QOpenGLWdiget需要提升至一个类种用于实现画面渲染。


2.捕获视频或网络流在子线程中进行,然后确定多少帧率取一次视频帧,将这个视频帧通过信号槽传递给主线程中进行画面的更新,打开图片可直接在主线程中传递到对应控件上显示即可,主线程专门复制渲染或者绘制(事件循环)。
3.OpenCV模块工程文件配置:
cpp
# 自动找 OpenCV
find_package(OpenCV REQUIRED)
# 包含头文件
include_directories(${OpenCV_INCLUDE_DIRS})
#链接库
target_link_libraries(项目名 PRIVATE ${OpenCV_LIBS})
3.核心部分代码
1.捕获线程:
cpp
#ifndef CAPTURETHREAD_H
#define CAPTURETHREAD_H
#include <QThread>
#include <QImage>
#include <QTimer>
//捕获线程支持的数据源类型
enum class CapType
{
camera,//相机
localvideo,//本地视频
urlVideo//网络视频流
};
class CaptureThread : public QThread
{
Q_OBJECT
public:
CaptureThread(CapType type);
void run()override;
public slots:
void ExitCap();
void SetSource(QString source);//图片,视频,url
signals:
void SendImg(QImage img);
private:
CapType m_capType;
bool m_running;
QString m_source;
};
#endif // CAPTURETHREAD_H
cpp
#include "CaptureThread.h"
#include <opencv4/opencv2/opencv.hpp>
#include <QDebug>
using namespace cv;
/**
* @brief 万能 Mat 转 QImage(支持所有常见格式)
* @param mat 输入 OpenCV Mat(支持 CV_8UC1 CV_8UC3 CV_8UC4)
* @return 输出 Qt QImage(可直接显示/上传OpenGL)
*/
QImage matToQImage(const cv::Mat &mat)
{
// 1. 空图直接返回空
if (mat.empty())
{
return QImage();
}
// 2. 获取宽高
int width = mat.cols;
int height = mat.rows;
// 3. 根据 Mat 类型转 QImage
switch (mat.type()) {
case CV_8UC1: {
QImage img(mat.data, width, height, mat.step, QImage::Format_Grayscale8);
return img.copy(); // 深拷贝,安全
}
case CV_8UC3: {
cv::Mat rgb;
cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB); // 必须转!
QImage img(rgb.data, width, height, rgb.step, QImage::Format_RGB888);
return img.copy();
}
case CV_8UC4: {
QImage img(mat.data, width, height, mat.step, QImage::Format_RGBA8888);
return img.copy();
}
default:
return QImage();
}
}
CaptureThread::CaptureThread(CapType type):
m_running(true),
m_capType(type)
{
}
void CaptureThread::SetSource(QString source)
{
m_source=source;
}
void CaptureThread::ExitCap()
{
m_running=false;
}
void CaptureThread::run()
{
VideoCapture cap;
if(m_capType==CapType::camera)
{
cap.open(0);
}
else
{
cap.open(m_source.toStdString());
}
if(!cap.isOpened())
{
qDebug()<<"打开失败";
return;
}
Mat frame;
while(m_running)
{
cap.read(frame);
if(frame.empty())//没有数据了,退出
{
break;
}
emit SendImg(matToQImage(frame));
msleep(30);//30fps
}
cap.release();
}
2.GLWidget渲染部分:
cpp
#ifndef VIDEOWIDGET_H
#define VIDEOWIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
#include <QOpenGLBuffer>
#include <QOpenGLVertexArrayObject>
class VideoWidget : public QOpenGLWidget,private QOpenGLFunctions_3_3_Core
{
Q_OBJECT
public:
VideoWidget(QWidget *parent=nullptr);
~VideoWidget();
void initializeGL();
void resizeGL(int w,int h);
void paintGL();
public slots:
void updateFrame(QImage img);
private:
void initShader();
QOpenGLShaderProgram *m_shader;
void initTexture();
QOpenGLTexture* m_texture;
void initContainer();
QOpenGLBuffer *m_vbo;
QOpenGLVertexArrayObject *m_vao;
};
#endif // VIDEOWIDGET_H
cpp
#include "VideoWidget.h"
#include <QImage>
VideoWidget::VideoWidget(QWidget *parent):QOpenGLWidget(parent) {}
VideoWidget::~VideoWidget()
{}
void VideoWidget::initializeGL()
{
initializeOpenGLFunctions();
initShader();
initTexture();
initContainer();
}
void VideoWidget::resizeGL(int w,int h)
{
}
void VideoWidget::paintGL()
{
glClearColor(1.0,1.0,1.0,1.0);
glClear(GL_COLOR_BUFFER_BIT);
m_shader->bind();
m_vao->bind();
m_texture->bind();
glDrawArrays(GL_TRIANGLE_FAN,0,4);
}
void VideoWidget::initShader()
{
const char* vertexShader=R"(
#version 330 core
layout(location=0) in vec2 aPos;
layout(location=1) in vec2 aTexCoord;
out vec2 texCoord;
void main()
{
gl_Position=vec4(aPos,0.0,1.0);
texCoord=aTexCoord;
}
)";
const char* fragmentShader=R"(
#version 330 core
in vec2 texCoord;
out vec4 fragColor;
uniform sampler2D tex;
void main()
{
fragColor=texture(tex,texCoord);
}
)";
m_shader=new QOpenGLShaderProgram(this);
m_shader->addShaderFromSourceCode(QOpenGLShader::Vertex,vertexShader);
m_shader->addShaderFromSourceCode(QOpenGLShader::Fragment,fragmentShader);
m_shader->link();
}
void VideoWidget::initTexture()
{
m_texture=new QOpenGLTexture(QOpenGLTexture::Target2D);
m_texture->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture->setWrapMode(QOpenGLTexture::ClampToEdge);
}
void VideoWidget::updateFrame(QImage img)
{
//如果图像尺寸变了,必须销毁旧纹理
if (m_texture && (m_texture->width() != img.width() || m_texture->height() != img.height()))
{
qDebug() << "调整尺寸";
m_texture->destroy();
delete m_texture;
m_texture = nullptr;
}
// 纹理不存在则重新创建
if (!m_texture)
{
m_texture = new QOpenGLTexture(QOpenGLTexture::Target2D);
m_texture->create();
m_texture->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture->setWrapMode(QOpenGLTexture::ClampToEdge);
}
// 直接更新数据,不再设置格式/大小
m_texture->setData(img.mirrored(false, true));
update();
}
void VideoWidget::initContainer()
{
float vertex[]={
-1.0,1.0, 0.0,1.0,
1.0,1.0, 1.0,1.0,
1.0,-1.0, 1.0,0.0,
-1.0,-1.0, 0.0,0.0
};
m_vao=new QOpenGLVertexArrayObject(this);
m_vao->create();
m_vao->bind();
m_vbo=new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);
m_vbo->create();
m_vbo->bind();
m_vbo->allocate(vertex,sizeof(vertex));
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(float)*4,(void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1,2,GL_FLOAT,GL_FALSE,sizeof(float)*4,(void*)(sizeof(float)*2));
glEnableVertexAttribArray(1);
m_vbo->release();
m_vao->release();
}
3.更新图片帧槽函数:
cpp
void Widget::showFrame(QImage img)
{
if (img.isNull()) return;
ui->lblVideo->setPixmap(QPixmap::fromImage(img).scaled(ui->lblVideo->size(), Qt::KeepAspectRatio));//QLabel显示
ui->openGLWidget->updateFrame(img);//OpenGL 显示
}