# OpenCV+QT

# 文件项目配置

属性(相对路径)

配置调试选择 x64

常规 —— 输出目录 —— ..\..\bin

调试 —— 工作目录 —— ..\..\bin

C/C++—— 附加包含目录 —— ..\include

链接器 —— 常规 —— 附加库目录 —— ..\..\lib

链接器 —— 输入 —— 附加依赖项 —— 在前面加 opencv_world331d.dll

# 开发遇到的坑

# ui 文件打不开

需要手动右键 ui文件 设置打开方式,选择 qtdesigner.exe ,并设置为默认打开方式

# 源文件打不开

要考虑是否创建项目的时候漏了一些库没有选择,可以参照 pro 文件和 cmakelist 文件的配置看少配置了什么

# mat

Mat 是 OpenCV 中的基本图像容器。

1
2
3
4
5
6
7
8
//地址遍历不一定连续的Mat
for (int row = 0; row < mat.rows; row++) {
for (int col = 0; col < mat.cols; col++) {
(&mat.data[row * mat.step])[col * es] = 80;//B
(&mat.data[row * mat.step])[col * es+1] = 120;
(&mat.data[row * mat.step])[col * es+2] = 200;
}
}

mat.data 是指向图像数据的指针,类型是 uchar* (无符号字符指针),表示图像的原始像素数据。

mat.step 是每行像素数据在内存中的字节数(步长),它可以比 mat.cols * es 大,特别是在行有对齐要求时。

es 是每个像素的字节数(对于 CV_8UC3 类型, es 是 3,因为每个像素有 3 个通道:B、G 和 R)。

row * mat.step 计算出当前行在图像数据中的起始位置。

col * es 计算出当前列在当前行中的偏移量。

&mat.data[row * mat.step] 得到当前行的起始地址,再加上 col * es 得到当前像素的起始地址。

# ROI 感兴趣区域裁剪图像

# cv::Rect rect(100,100,300,300)

100: 横向位置 100

100:从上往下 100

然后在这个点取一个矩形 300*300

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(int argc, char* argv[])
{
Mat src = imread("1.png");
// 定义一个矩形区域
Rect rect(100, 100, 100, 100);
Mat roi = src(rect);// 提取该矩形区域作为ROI
MatSize s = roi.size;// 获取ROI的大小

//创建一个自动调整大小的窗口,其大小与显示的图像相匹配。
namedWindow("roi",WINDOW_AUTOSIZE);
imshow("roi", roi);
namedWindow("src");
imshow("src", src);
waitKey(0);
return 0;
}

# 像素格式和灰度图

# RGB

显卡输出的数据是 RGB 形式的,三个字节一个像素

# YUV

亮度,色度,饱和度,电视,黑白电视,Y 信号就行

存储空间会比 RGB 小,两个字节一个像素

压缩算法基于 YUV,更利于做压缩算法,都是转化为 YUV 然后用压缩算法

图像处理,最终显示的时候要把 YUV 转换为 RGB

# RGAY

灰度图,一个字节 0~25

一个字节一个像素 ,高速摄影

# cvtColor(src,img,COLOR_BGR2GRAY)

#include<opencv2/imgproc.hpp>

格式转换,BGR 转换为灰度图

# 手动实现转换灰度图

1
2
3
4
5
6
7
8
9
10
11
12
13
//手动实现转换灰度图
//des没有加引用会造成错误
void RGBToGray(Mat src, Mat &des) {
//GRay = (R*30+G*59+B*11+50)/100
des.create(src.rows, src.cols, CV_8UC1);
for (int r = 0; r < src.rows; r++) {
for (int c = 0; c < src.rows; c++) {
Vec3b& m = src.at<Vec3b>(r, c);
int gray = (m[2] * 30 + m[1] * 59 + m[0] * 11 + 50) / 100;
des.at<uchar>(r, c) = gray;
}
}
}

# 二值化和阈值

# 二值化

图片的一种存储方式

一个黑一个白

有大概五种算法

# 1、 THRESH_BINARY 二进制阈值化

# 2、 THRESH_BINARY_INV 反二进制阈值化

根据参数,如果满足条件就变成白色,如果不满足就变成黑色,反向处理

1
2
3
4
5
6
//二进制阈值化
//(原图,目标图,阈值,最大值(这里255是白色的意思),方法)
threshold(gray, bin, 100, 255,THRESH_BINARY);

//反二进制阈值化
threshold(gray, ibin, 100, 255, THRESH_BINARY_INV);

# 改变图片的对比度和亮度

# g(i,j) = a*f(i,j) + b

目标像素 = a (对比度) * 原始颜色 + b (亮度)

a 1.0~3.0 (对比)

b 0~100 (亮度)

1
2
3
4
5
6
7
8
9
10
11
void ChangeGain(Mat& src, Mat& des, float a, int b) {
//`g(i,j) = a*f(i,j) + b`
des.create(src.rows, src.cols, src.type());
for (int r = 0; r < src.rows; r++) {
for (int c = 0; c < src.cols; c++) {
for (int i = 0; i < 3; i++) {
des.at<Vec3b>(r, c)[i] = saturate_cast<uchar>(a * src.at<Vec3b>(r, c)[i] + b);
}
}
}
}

# saturate_cast<uchar> 防止溢出

可能会溢出,超过 255,就不是全白,反射,变成了全黑,不是我们想要的,所以要防止溢出,设置超过 255 它就是 255,全白,R 这个通道超过 255 就是全红,小于 0 就设为 0

# opencv 函数 convertTo()

1
2
3
4
5
6
7
8
9
10
11
12
//main()
//调整对比度和亮度
Mat src = imread("1.png");
Mat des;
PrintMs("");
ChangeGain(src, des, 2.0, 50);
PrintMs("ChangeGain");
Mat des2;
//使用opencv的函数,测试体现convertTo更快
//-1表示与原图src一致
src.convertTo(des2, -1, 2.0, 50);
PrintMs("convertTo");

# 图像尺寸调整

# INTER_NEAREST 邻近算法

小变大 —— 拷贝周围的像素,会生成马赛克

# 自定义

1
2
3
4
5
6
7
8
9
10
int sx,sy = 0;//原图对应的坐标
float fy = float(src.rows)/out.rows;
float fx = float(src.cols)/out.cols;
for(int y = 0;y < out.rows;y++ ){
sy = fy * y + 0.5;//四舍五入
for(int x = 0;x < out.cols;x++){
sx = fx * x +0.5;
out.at<Vec3b>(x,y) = src.at<Vec3b>(sy,sx);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//自定义缩放代码
void xresize(Mat& src, Mat& des, Size size) {
//type()指的是这个类型是RGB还是灰度图
des.create(size, src.type());
int sx, sy = 0;//原图对应的坐标
float fy = float(src.rows) / des.rows;
float fx = float(src.cols) / des.cols;
for (int y = 0; y < des.rows; y++) {
sy = fy * y + 0.5;//四舍五入
for (int x = 0; x < des.cols; x++) {
sx = fx * x + 0.5;
des.at<Vec3b>(x, y) = src.at<Vec3b>(sy, sx);
}
}
}

1
2
3
4
5
//main()
Mat src = imread("1.png");
Mat img128;
//原图,目标图,放缩尺寸
xresize(src, img128, Size(128, 128));

# OpenCV 的 resize()

1
2
//opencv自带的函数,有多线程处理、优化
resize(src, img256, Size(256, 256),0,0,INTER_NEAREST);

第 4、5 个参数表示 fx,fy, 当 Size 为空时,它们作为一个比例来乘以原图的大小

第 6 个参数,算法类型,默认是双线性插值,这里是邻近算法

# CV_INTER_LINEAR 双线性插值(缺省使用)

# 滤波

image-20240806120841036

解决放大图像边界会出现马赛克、模糊的情况

# 双线性内插值

使放大的图像边界更加平滑

image-20240806121855326

image-20240806122849235

使用:直接把方法参数处改为 INTER_LINEAR

# 图像金字塔

在图像放大缩小、拼接、扭曲都可以用到这种算法

1
2
3
4
5
//高斯金字塔,原图,目标图
pyrDown(src, gsrc);

//拉普拉斯金字塔
pyrUp(src, lsrc);

image-20240806123833136

# 高斯金字塔(向下采样缩小)

用来向下采样

把整个分辨率降低

image-20240806123851349

比如图像是 8*8 的,卷积后将所有偶数行和列去除,就缩小成了 4*4 的图像

G (i+1) 表示上一层

# 高斯内核

提供好的固定的矩阵

opencv 提供的高斯金字塔只支持这种 5*5 的矩阵

image-20240806205111854

# 拉普拉斯金字塔

用来从金字塔低层图像重建上层未采样图像

image-20240806205436330

# 两幅图像混合(blending)

# 公式

dst = src1*a + src2*(1-a) + gamma

最终叠化成果的像素集 = 原图 1 * 透明度 + 原图 2 * 透明度

(1-透明度) 是为了融合度高,一个高一个低

image-20240806223204829

# OpenCV- addWeighted()

第五个参数(0.0):对图像的增益,比如颜色更深,白色更亮

第六个参数(dst):最终生成的目标

# 图像旋转和镜像

1
2
3
4
5
6
7
//旋转rotate
Mat rot;
cv::rotate(src, rot, ROTATE_90_CLOCKWISE);

//镜像flip
Mat fl;
cv::flip(src, fl, 0);

# 旋转

image-20240806224542424

# 镜像

image-20240806224619312

0 (x)—— 上下做镜像

1 (y)—— 左右做镜像

-1—— 两个都做

# 通过 ROI 图像合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//roi图像合并
//使图像高度一致
int height = src.rows;
int width1 = src.cols;
int width2 = src2.cols;
//将高图像等比缩放
if (src.rows > src2.rows) {
height = src2.rows;
width1 = src.cols * ((float)src2.rows / (float)src.rows);
resize(src, src, Size(width1, height));
}
else if (src.rows < src2.rows) {
width2 = src2.cols * ((float)src.rows / (float)src2.rows);
resize(src2, src2, Size(width2, height));
}
//创建目标Mat
Mat des;
des.create(height, width1 + width2, src.type());
Mat r1 = des(Rect(0, 0, width1, height));
Mat r2 = des(Rect(width1, 0, width2, height));
src.copyTo(r1);
src2.copyTo(r2);

# ffmpeg 工具抽取剪切音频合并视频

# 抽取音频

ffmpeg.exe -i 1.avi -vn 1.mp3

-i 表示源 1.avi 输入文件 -vn 表示不转换视频 1.MP3 输出文件

# 剪切音频

ffmpeg -ss 0:0:30 -t 0:0:20 -i input.mp3 -c copy output.mp3

-ss 表示开始时间 -t 表示剪切时间

# 音视频合并

ffmpeg.exe -i 1.mp3 -i 1.mp4 -c copy out.mp4

# OpenCV VideoCapture 读取视频

# VideoCapture 类

这是一个读取视频的类,视频源可以是文件、摄像头、RTSP 流都可以

# 打开摄像头方式

# bool open(int index)

这个参数 index 索引对应你的所有摄像机列表

# open(int cameraNum.int apiPreference)

可以手动选择 api 接口,默认是 0 ,自动监测

# VideoCapture cap(index)

这是一个构造函数

# 打开视频流文件

# bool open(const String &filename)

# VideoCapture cap(const String& file)

# bool open(const String &filename,int apiPreference)

# 关闭和空间释放

# ~VideoCapture()

析构函数,智能指针,在复制之后,引用计数会加一,直到没有引用,才会释放,通过智能指针,空间的释放不用做太多的管理

如果不想要转码的时候调用全部 gpu,可以修改源码

# release()

主动释放,如果 VideoCapture 被两次调用,一个引用 - 1,另一个空间也不会被释放掉,只是把你这次的引用 - 1

# 读取一帧视频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
VideoCapture video;
video.open("1.mp4");
if (!video.isOpened()) {
cout << "fail";
return -1;
}
cout << "success";
Mat frame;//用于存储每一帧视频图像
for (;;) {

//从视频中读取一帧并存储在 frame 中。如果读取失败,退出循环。
/*if (!video.read(frame)) {
break;
}*/


//读帧,解码
if (!video.grab()) {
break;
}
//转换颜色格式
if (!video.retrieve(frame)) {
break;
}

if (frame.empty()) break;
//显示当前帧。
imshow("video", frame);
//等待5毫秒,与播放出来的速度有关
waitKey(5);
}

# read(OutputArray image)

它先是解压缩(解码),然后对图片做了色彩转换

h264 用一帧画面存储整个,也是用 jpg 来压缩,后面 50 帧画面只存储与这一帧的变化,这样的压缩率非常高,这样的话就存在一个问题,每一帧必须都要解码,比如说第十帧是针对第九帧的变化,那么取出一整个画面就是不对的,所以我们可以先把前面的解码,先不做图像转换,不显示,以提高效率

# bool grab()

读取并解码

# virtual bool retrieve(OutputArray image,int flag = 0 )

图像色彩转换

# vc>>mat

# 获取视频、相机属性

其他属性的获取可以看 OpenCV 的 api 文档 ——OpenCV Flags for video I/O

1
2
3
int fps = video.get(CAP_PROP_FPS);//帧率,使用get(),里面放以下的参数
int s = 30;
if (fps != 0) s = 1000 / fps;//帧数

# CAP_PROP_FPS 帧率

一秒钟的帧数

# CAP_PROP_FRAME_COUNT 总帧数

计算视频时长:总帧数除以帧率(s)

# CAP_PROP_POS_FRAMES 播放帧的位置

跳帧,下一帧

# CAP_PROP_FRAME_WIDTH HEIGHT

可以获取到视频帧的宽度高度,虽然一解码的时候可以获取到这个信息,但是有些处理,还没有读帧的时候需要用到这个高度,就需要用到,比如窗口自适应

# 设置视频播放的进度

1
2
3
4
5
int cur = video.get(CAP_PROP_POS_FRAMES);
//视频经过3s跳到开头
if (cur > 90) {
video.set(CAP_PROP_POS_FRAMES, 0);
}

# CAP_PROP_POS_MSEC 毫秒位置

毫秒 -> 帧 ->ffmpeg 时间,要转两次,浮点数值来回转换,会出现数据的丢失,所以建议使用帧位置

# CAP_PROP_POS_FRAMES 帧位置

# VideoWriter

image-20240807123150031

要主动释放 release ()

# void write(const Mat&)

# CvVideoWriter_FFMPEG::writeFrame

# 录制摄像头视频并存储代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
int main(int argc, char* argv[])
{
//参数 0 指定使用第一个连接的摄像头
VideoCapture cam(0);

//检查摄像头是否成功打开
if (!cam.isOpened()) {
cout << "fail";
return -1;
}
cout << "success";

//用于存储图像帧
Mat img;
//用于写入视频文件
VideoWriter vw;

//获取摄像头的帧率(fps)
int fps = cam.get(CAP_PROP_FPS);
//如果获取失败或帧率小于等于0,则默认设置帧率为25。
if (fps <= 0) fps = 25;

//打开视频写入器 vw 并准备写入文件 "out.avi"。
vw.open("out.avi",
VideoWriter::fourcc('X', '2', '6', '4'),//指定视频编码格式为X264。
fps,//每秒帧数
Size(cam.get(CAP_PROP_FRAME_WIDTH),//指定视频的宽度和高度
cam.get(CAP_PROP_FRAME_HEIGHT))
//两个函数返回摄像头当前设置的帧宽度和高度,单位是像素
);

//检查视频写入器是否成功打开
if (!vw.isOpened()) {
cout << "falied" << endl;
getchar();
return -1;
}
cout << "vw success";

namedWindow("cam");

for (;;) {
//读取摄像头的一帧图像到 img
cam.read(img);
if (img.empty()) break;
imshow("cam", img);
//将图像 img 写入视频文件
vw.write(img);
//q退出
if (waitKey(5) == 'q') break;
}

return 0;
}

# 视频编辑器

# bug 修复

1、信号没有发送

线程没有开启,要在线程中加入 start()

2、在头文件中的函数要在 cpp 中有

# 流程

# 前期

  • 设置页面背景色
  • 取消顶部菜单栏 setWindowFlags(Qt::FramelessWindowHint);
  • 添加关闭按钮,设置信号与槽

# 视频显示

  • ui 添加 glWidget,并提升为新建立的类
  • 在主.cpp 中添加上传文件按钮,并设置 open 函数,连接信号槽

1
2
3
4
5
6
7
8
9
10
11
12
13
void VideoUI::open()
{
//返回的是一个路径
QString name = QFileDialog::getOpenFileName(this,QString::fromLocal8Bit( "请选择要编辑的视频/图片"));
if (name.isEmpty()) return;
//主要是防止中文文件报错
string file = name.toLocal8Bit().data();
//QMessageBox::information(this, QString::fromLocal8Bit("获取视频名消息"), name);
//在线程中创建对象,打开文件
if (!videoThread::Get()->open(file)) {
QMessageBox::information(this, "",name+"fail");
}
}

  • 创建线程,开启线程,在线程中打开文件,在 VideoUI 的 open 函数中调用 videoThread 类中的 get 创建对象,执行 open 函数

1
2
3
4
5
6
7
8
9
10
11
//用于打开视频文件或摄像头,并从中读取视频帧
static VideoCapture cap1;

bool videoThread::open(const std::string file)
{
mutex.lock();
bool re = cap1.open(file);
mutex.unlock();
cout << re << endl;
return re;
}

  • 线程启动后会自动调用 run (), 读取每帧视频存到 mat 对象中
  • 传递信号至 ui 中的显示容器中,用 setImage1 () 处理图像,paintEvent () 刷新图像

# 视频进度条

# 设置进度条随视频变化:

在 VideoUI 中设置定时器,定时器获取到当前视频的播放状态然后再改变滑动条的位置

videoThread 返回播放进度或者返回成员也能算出进度

# 拖动进度条变化视频:
  • 拖动进度条发送信号,带有参数,传进槽函数中处理
  • 在槽函数中将参数传入线程中调用 seek 函数,seek 函数获取当前视频的总帧数,计算出当前的位置
  • 传到另一个 seek 中设置画面到算出的这个位置

# 视频亮度和对比度调整

videoUI->Filter->imagePro

# 添加 videoFilter 类

相当于一个任务列表,执行多个任务,把任务依次给 imagePro 来执行,所以要先搞 imagePro

# 接收界面设置的参数
# 输出处理后的视频

# 后续每添加一个功能

先做函数,再加过滤器(任务),调用对应处理的函数,然后就是 ui 部分

# videoThread.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#pragma once
#include<QThread>

class videoThread:public QThread
{
public:
//这是一个静态成员函数声明。
//static 意味着这个函数属于整个类,而不是属于某个对象(实例)。
//videoThread* 是返回类型,表示这个函数返回的是一个 videoThread 类型的指针。
//可以在没有创建对象的情况下调用它。
static videoThread* Get()
{
static videoThread vt;
return &vt;
}

//打开一号视频源文件
//const 表示这个输入字符串在函数内不能被修改。
bool open(const std::string file);

//这是一个清理对象时自动调用的函数。
~videoThread();

//线程入口函数
void run();

//将以下内容隐藏起来,只允许类本身或其子类访问。
protected:
//构造函数在创建类的对象时自动调用。
videoThread();
};

# videoThread.hpp

# mutex 互斥锁

用来防止多个线程同时访问 cap1 对象,避免数据竞争,在执行 “任务” 的时候上锁,防止别的线程插队,等执行完后再解锁,这个上锁解锁的原则就是晚锁早解,提高效率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include "videoThread.h"
#include<opencv2/imgcodecs.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<iostream>
#include<qdebug.h>
using namespace cv;
using namespace std;

//用于打开视频文件或摄像头,并从中读取视频帧
static VideoCapture cap1;
//检查线程是否退出
static bool isexit = false;

videoThread::videoThread()
{
//要开启线程,否则其他都不会执行
start();
}

bool videoThread::open(const std::string file)
{
mutex.lock();
//打开视频文件
bool re = cap1.open(file);
mutex.unlock();
cout << re << endl;
return re;
}

void videoThread::run()
{
Mat m;
for (;;) {
mutex.lock();
//如果isexit被设置为true(表示线程需要退出),则退出循环,并终止线程
if (isexit) {
mutex.unlock();
break;
}
//判断视频是否打开
if (!cap1.isOpened())
{
mutex.unlock();
msleep(5);
continue;
}

//读取视频
if (!cap1.read(m) || m.empty())
{
mutex.unlock();
msleep(5);
continue;
}

//显示图像,发送信号
emit viewImage1(m);
msleep(40);
mutex.unlock();
}

}

videoThread::~videoThread()
{
mutex.lock();
isexit = true;
mutex.unlock();
}

# videoWidget.hpp

# void videoWidget::paintEvent(QPaintEvent* e)

这是一个覆盖(override)了 QOpenGLWidget 中的 paintEvent 函数。 paintEvent 在 Qt 中是一个虚函数,当窗口部件需要重绘时(例如窗口大小改变,或者通过 update() 函数请求重绘时),Qt 会自动调用这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include "videoWidget.h"
#include<QPainter>

//初始化操作在QOpenGLWidget(p)进行
videoWidget::videoWidget(QWidget* p) :QOpenGLWidget(p)
{

}

void videoWidget::paintEvent(QPaintEvent* e)
{
//用于执行绘图操作
QPainter p;

//begin()函数开始在当前小部件(this)上进行绘图操作
p.begin(this);

//在小部件的原点(QPoint(0, 0))处绘制图像img
//img包含了之前设置的图像数据
p.drawImage(QPoint(0, 0), img);

//结束绘图操作,释放相关资源
p.end();
}

videoWidget::~videoWidget()
{

}

void videoWidget::setImage(cv::Mat mat)
{
if (img.isNull())
{
//图像处理都要用uchar,否则会造成数据丢失
//宽高像素
//创建了一个大小为width() * height() * 3的缓冲区buf,用来存储图像数据
//width()和height()是视频小部件的宽度和高度
//乘以3是因为每个像素在Format_RGB888格式下占用3个字节(分别表示红、绿、蓝三个颜色通道)
uchar* buf = new uchar[width() * height() * 3];
//创建一个新的QImage对象,并将buf作为其内部数据存储
img = QImage(buf, width(), height(), QImage::Format_RGB888);
}
//将OpenCV图像mat的数据复制到QImage的缓冲区中。
//img.bits()返回QImage数据的指针,mat.data是cv::Mat图像数据的指针。
//mat.cols * mat.rows * mat.elemSize()计算了需要复制的数据大小
memcpy(img.bits(), mat.data, mat.cols * mat.rows * mat.elemSize());

//刷新会调用到paintevent
//设置了新的图像数据后,小部件会立即刷新显示
update();
}