- 项目内容展示
- 项目开发日志
项目效果展示
视频编辑器的界面和基础功能介绍
界面
界面由原视频(左上方)和输出视频(右上方),以及下方的编辑界面组成。
基础功能
打开视频文件,视频的播放和暂停,处理后视频的导出,以及拖动进度条对视频播放进度和裁剪开始和结束位置进行设置。
视频图像裁剪功能
指定输出视频某一区域内容
灰度图转换
添加水印
可以选择水印图片并调整位置和透明度
设置亮度
设置对比度
旋转
可旋转90, 180, 270度
镜像
可以设置水平和竖直镜像
重新调整视频尺寸
高斯滤波和拉普拉斯滤波
高斯滤波可以压缩图像尺寸
拉普拉斯滤波可以使图像更加清晰
项目开发日志
项目目标
基于VS、Qt平台开发一款视频编辑器,其功能包括视频播放、截图、剪切、合并、融合、添加水印、亮度和对比度调整、高斯滤波和拉普拉斯滤波、图像旋转和镜像、图像颜色调整功能。
项目类
![[Pasted image 20240325222847.png]]
XVideoUI
XVideoThread
XVideoFilter
XVideoWidget
XImagePro
创建项目目录
——bin
——
——lib
——
——src
——
创建项目
选择Qt Widget Application模板
配置项目名称和路径
配置Qt选项
设置VS中项目属性
设置Qt基础界面属性
调整界面宽高和背景颜色
添加视频播放窗口
添加窗口和按钮
提升窗口类并添加类
提升窗口类
添加类
XVideoWidget.h
#pragma once
#include<QOpenGLWidget>
class XVideoWidget :public QOpenGLWidget
{
Q_OBJECT
public:
XVideoWidget(QWidget* p);
virtual ~XVideoWidget();
void paintEvent(QPaintEvent* e);
};
XVideoWidget.cpp
#include "XVideoWidget.h"
XVideoWidget::XVideoWidget(QWidget* p) :QOpenGLWidget(p)
{
}
XVideoWidget::~XVideoWidget()
{
}
void XVideoWidget::paintEvent(QPaintEvent* e)
{
}
设置按钮
在界面编辑器设置信号和函数声明
在XVideoUI.h添加代码
XVideoUI.cpp
#include "XVideoUI.h"
#include<QFileDialog>
#include<QMessageBox>
#include<string>
using namespace std;
XVideoUI::XVideoUI(QWidget* parent)
: QWidget(parent)
{
ui.setupUi(this);
}
XVideoUI::~XVideoUI()
{
}
void XVideoUI::Open()
{
//选择文件并获取文件名,获取失败则返回,之后将QString转换成string格式方便后续处理
QString name = QFileDialog::getOpenFileName(this, QString::fromLocal8Bit("选择视频文件"));
if (name.isEmpty()) return;
string file = name.toLocal8Bit().data();
//调试代码
QMessageBox::information(0, "", name);
}
添加线程类VideoThread
XVideoThread.h
#pragma once
#include<QThread>
#include<opencv2/core.hpp>
#include <QtCore/qmutex.h>
class XVideoThread :public QThread
{
Q_OBJECT
public:
//单例模式的设计,确保在程序的生命周期内只有一个 VideoThread 实例。
static XVideoThread* Get()
{
static XVideoThread vt;
return &vt;
}
~XVideoThread();
//打开src1视频文件
bool _Open(const std::string file);
//线程入口函数
void run();
signals:
//显示src1视频图像
void XViewImage1(cv::Mat mat);
protected:
QMutex mutex;
XVideoThread();
};
VideoThread.cpp
#include "XVideoThread.h"
#include<opencv2/imgproc.hpp>
#include<opencv2/imgcodecs.hpp>
#include<opencv2/highgui.hpp>
#include<iostream>
using namespace std;
using namespace cv;
static VideoCapture cap1;
static bool isexit = false;
XVideoThread::XVideoThread()
{
//从对象创建就运行run()
start();
}
XVideoThread::~XVideoThread()
{
mutex.lock();
isexit = true;
mutex.unlock();
}
bool XVideoThread::_Open(const std::string file)
{
mutex.lock();
bool re = cap1.open(file);
mutex.unlock();
return re;
}
void XVideoThread::run()
{
Mat mat1;
while (true)
{
mutex.lock();
//判断是否退出
if (isexit)
{
mutex.unlock();
break;
}
//判断视频是否打开
if (!cap1.isOpened())
{
mutex.unlock();
msleep(5);
continue;
}
//读取一帧
if (!cap1.read(mat1) || mat1.empty())
{
mutex.unlock();
msleep(5);
continue;
}
//显示图像
XViewImage1(mat1);
msleep(40);
mutex.unlock();
}
}
VideoWidget中添加槽函数
VideoWidget.h
#pragma once
#include<QOpenGLWidget>
#include<QPainter>
#include<opencv2/core.hpp>
#include<opencv2/imgproc.hpp>
using namespace cv;
class XVideoWidget :public QOpenGLWidget
{
Q_OBJECT
public:
XVideoWidget(QWidget* p);
virtual ~XVideoWidget();
void paintEvent(QPaintEvent* e);
public slots:
//XViewImage1对应的槽函数
void SetImage(cv::Mat mat);
protected:
QImage img;
};
VideoWidget.cpp
#include "XVideoWidget.h"
#include <iostream>
using namespace std;
XVideoWidget::XVideoWidget(QWidget* p) :QOpenGLWidget(p)
{
}
XVideoWidget::~XVideoWidget()
{
}
void XVideoWidget::paintEvent(QPaintEvent* e)
{
QPainter p;
p.begin(this);
p.drawImage(QPoint(0, 0), img);
p.end();
}
void XVideoWidget::SetImage(cv::Mat mat)
{
Mat des;
//判断是否第一次设置图像,是则创建一个新的 QImage 对象
if (img.isNull())
{
uchar* buf = new uchar[width() * height() * 3];
img = QImage(buf, width(), height(), QImage::Format_RGB888);
}
//设置图像大小
cv::resize(mat, des, Size(img.size().width(), img.size().height()));
//设置图像颜色格式
cv::cvtColor(des, des, COLOR_BGR2RGB);
//复制内存空间
memcpy(img.bits(), des.data, des.cols * des.rows * des.elemSize());
update();//每次更新会调用QPaintEvent()
}
VideoUI中建立信号与槽函数的连接
VideoUI.cpp
#include "XVideoUI.h"
#include"XVideoThread.h"
#include<QFileDialog>
#include<QMessageBox>
#include<string>
using namespace std;
XVideoUI::XVideoUI(QWidget* parent)
: QWidget(parent)
{
ui.setupUi(this);
//注册信号类型
qRegisterMetaType<cv::Mat>("cv::Mat");
//信号与槽函数建立连接
QObject::connect(XVideoThread::Get(),
SIGNAL(XViewImage1(cv::Mat)),
ui.src1,
SLOT(SetImage(cv::Mat)));
}
XVideoUI::~XVideoUI()
{
}
void XVideoUI::Open()
{
//选择文件并获取文件名,获取失败则返回,之后将QString转换成string格式方便后续处理
QString name = QFileDialog::getOpenFileName(this, QString::fromLocal8Bit("选择视频文件"));
if (name.isEmpty()) return;
string file = name.toLocal8Bit().data();
//调试代码
QMessageBox::information(0, "", name);
if (!XVideoThread::Get()->_Open(file))
{
QMessageBox::information(this, "error", name + " open failed!");
}
}
根据视频参数调整帧率
VideoThread.h中添加
VideoThread.cpp中添加
进度条显示视频播放进度
ui界面添加进度条
进度条随播放进度移动
添加GetPos()函数
VideoThread.cpp中添加
double XVideoThread::GetPos()
{
double pos = 0;
mutex.lock();
if (!cap1.isOpened())
{
mutex.unlock();
return pos;
}
double count = cap1.get(CAP_PROP_FRAME_COUNT);
double cur = cap1.get(CAP_PROP_POS_FRAMES);
if (count > 0.001)
pos = cur / count;
mutex.unlock();
return pos;
}
VideoUI.h中添加
VideoUI.cpp中添加
void XVideoUI::timerEvent(QTimerEvent* e)
{
if (pressSlider) return;
double pos = XVideoThread::Get()->GetPos();
ui.playSlider->setValue(pos * 1000000);
}
进度条拖动调整播放进度
VideoThread.h 中添加跳转函数
VideoThread.cpp
bool VideoThread::Seek(int frame)
{
mutex.lock();
if (!cap1.isOpened())
{
mutex.unlock();
return false;
}
int re = cap1.set(CAP_PROP_POS_FRAMES, frame);
mutex.unlock();
return re;
}
bool VideoThread::Seek(double pos)
{
double count = cap1.get(CAP_PROP_FRAME_COUNT);
int frame = pos * count;
return Seek(frame);
}
界面中添加信号和函数
VideoUI.h中添加
VideoUI.cpp
void VideoUI::SliderPress()
{
pressSlider = true;
}
void VideoUI::SliderRelease()
{
pressSlider = false;
}
void VideoUI::SetPos(int pos)
{
VideoThread::Get()->Seek((double)pos / 1000.0);
}
设置亮度和对比度
添加界面组件
添加ImagePro类
ImagePro.h
#pragma once
#include<opencv2/core.hpp>
using namespace cv;
class XImagePro
{
public:
XImagePro();
~XImagePro();
//设置原图,清理处理结果
void Set(Mat mat1, Mat mat2);
//获取处理后结果
Mat Get();
//设置亮度和对比度
void Gain(double bright, double constrast);
private:
//原图
Mat src1, src2;
//处理图
Mat des;
};
ImagePro.cpp
#include "XImagePro.h"
XImagePro::XImagePro()
{
}
XImagePro::~XImagePro()
{
}
void XImagePro::Set(Mat mat1, Mat mat2)
{
if (mat1.empty()) return;
this->src1 = mat1;
this->src2 = mat2;
this->src1.copyTo(des);
}
Mat XImagePro::Get()
{
return des;
}
void XImagePro::Gain(double bright, double constrast)
{
if (des.empty()) return;
des.convertTo(des, -1, constrast, bright);
}
添加Filter类
Filter.h
#pragma once
#include<opencv2/core.hpp>
#include<vector>
#include<QThread>
#include <QtCore/qmutex.h>
#include"XImagePro.h"
using namespace std;
using namespace cv;
enum XTaskType
{
XTASK_NONE,
XTASK_GAIN, //亮度对比度调整
};
struct XTask
{
XTaskType type;
vector<double> para;
};
class XFilter
{
public:
static XFilter* Get();
virtual ~XFilter();
virtual Mat Filter(Mat mat1, Mat mat2) = 0;
virtual void Add(XTask task) = 0;
virtual void Clear() = 0;
protected:
XFilter();
};
Filter.cpp
#include "XFilter.h"
class CXFilter :public XFilter
{
public:
vector<XTask> tasks;
QMutex mutex;
Mat Filter(Mat mat1, Mat mat2)
{
mutex.lock();
XImagePro p;
p.Set(mat1, mat2);
for (int i = 0; i < tasks.size(); i++)
{
switch (tasks[i].type)
{
case XTASK_GAIN:
p.Gain(tasks[i].para[0], tasks[i].para[1]);
break;
default:
break;
}
}
Mat re = p.Get();
mutex.unlock();
return re;
}
void Add(XTask task)
{
mutex.lock();
tasks.push_back(task);
mutex.unlock();
}
void Clear()
{
mutex.lock();
tasks.clear();
mutex.unlock();
}
};
XFilter* XFilter::Get()
{
static CXFilter cx;
return &cx;
}
XFilter::XFilter()
{
}
XFilter::~XFilter()
{
}
设置信号
VideoUI.cpp
void VideoUI::Set()
{
Filter::Get()->Clear();
if (ui.light->value() > 0 || ui.contrast->value() > 1)
{
Filter::Get()->Add(XTask{ XTASK_GAIN,
{(double)ui.light->value(), ui.contrast->value() }
});
}
}
视频导出
bool VideoThread::StartSave(string filename, int width, int height)
{
cout << "开始导出" << endl;
Seek(0);
mutex.lock();
if (!cap1.isOpened())
{
mutex.unlock();
return false;
}
if (width <= 0)
width = cap1.get(CAP_PROP_FRAME_WIDTH);
if (height <= 0)
height = cap1.get(CAP_PROP_FRAME_HEIGHT);
vw.open(filename,
VideoWriter::fourcc('X', '2', '6', '4'),
this->fps,
Size(width, height));
if (!vw.isOpened())
{
mutex.unlock();
cout << "fail to startsave" << endl;
return false;
}
this->isWrite = true;
mutex.unlock();
return true;
}
//停止保存视频
void VideoThread::StopSave()
{
cout << "停止导出" << endl;
mutex.lock();
vw.release();
isWrite = false;
mutex.unlock();
}
VideoUI.cpp中添加
void XVideoUI::ExportEnd()
{
isExport = false;
ui.exportButton->setText("Start Export");
}
void XVideoUI::Export()
{
if (isExport)
{
XVideoThread::Get()->StopSave();
isExport = false;
ui.exportButton->setText("Start Export");
return;
}
QString name = QFileDialog::getSaveFileName(this, "save", "out1.avi");
if (name.isEmpty()) return;
string filename = name.toLocal8Bit().data();
if (XVideoThread::Get()->StartSave(filename))
{
isExport = true;
ui.exportButton->setText("Stop Save");
}
}
//导出视频结束
QObject::connect(XVideoThread::Get(),
SIGNAL(SaveEnd()),
this,
SLOT(ExportEnd())
播放与暂停
XVideoUI.cpp
void XVideoUI::PlayPause()
{
if (IsPlay == true)
{
XVideoThread::Get()->Pause();
IsPlay = false;
ui.playButton_3->setText(QString::fromLocal8Bit("暂停"));
}
else
{
XVideoThread::Get()->Play();
IsPlay = true;
ui.playButton_3->setText(QString::fromLocal8Bit("播放"));
}
cout << endl;
}
XVideoThread.cpp
void XVideoThread::Play()
{
mutex.lock();
isPlay = true;
mutex.unlock();
}
void XVideoThread::Pause()
{
mutex.lock();
isPlay = false;
mutex.unlock();
}
旋转
XVideoUI.cpp中添加
//旋转
if (ui.comboBox->currentIndex() == 1)
{
XFilter::Get()->Add(XTask{ XTASK_ROTATE90 });
}
else if (ui.comboBox->currentIndex() == 2)
{
XFilter::Get()->Add(XTask{ XTASK_ROTATE180 });
}
else if (ui.comboBox->currentIndex() == 3)
{
XFilter::Get()->Add(XTask{ XTASK_ROTATE270 });
}
XImagePro
void XImagePro::Rotate90()
{
if (des.empty())
return;
rotate(des, des, ROTATE_90_CLOCKWISE);
}
void XImagePro::Rotate180()
{
if (des.empty())
return;
rotate(des, des, ROTATE_180);
}
void XImagePro::Rotate270()
{
if (des.empty())
return;
rotate(des, des, ROTATE_90_COUNTERCLOCKWISE);
}
镜像
XVideoUI.cpp的Set()函数中添加
//镜像
if (ui.comboBox_2->currentIndex() == 1)
{
XFilter::Get()->Add(XTask{ XTASK_FLIPX });
}
else if (ui.comboBox_2->currentIndex() == 2)
{
XFilter::Get()->Add(XTask{ XTASK_FLIPY });
}
else if (ui.comboBox_2->currentIndex() == 3)
{
XFilter::Get()->Add(XTask{ XTASK_FLIPXY });
}
XImagePro
void XImagePro::FlipX()
{
if (des.empty())
return;
flip(des, des, 0);
}
void XImagePro::FlipY()
{
if (des.empty())
return;
flip(des, des, 1);
}
void XImagePro::FlipXY()
{
if (des.empty())
return;
flip(des, des, -1);
}
视频尺寸调整
void XImagePro::Resize(int width, int height)
{
if (des.empty())
return;
resize(des, des, Size(width, height));
}
ROI剪切
void XImagePro::Clip(int x, int y, int w, int h)
{
if (des.empty())
return;
if (x < 0 || y < 0 || w <= 0 || h <= 0)
return;
if (x > des.cols || y > des.rows)
return;
des = des(Rect(x, y, w, h));
}
case XTASK_CLIP:
p.Clip(tasks[i].para[0], tasks[i].para[1], tasks[i].para[2], tasks[i].para[3]);
break;
灰度图
XImagePro
void XImagePro::Gray()
{
if (des.empty())
return;
cvtColor(des, des, COLOR_BGR2GRAY);
}
添加水印
XVideoUI
void XVideoUI::Mark()
{
QString name = QFileDialog::getOpenFileName(this, QString::fromLocal8Bit("选择水印文件"));
if (name.isEmpty()) return;
string file = name.toLocal8Bit().data();
Mat mark = imread(file);
if (mark.empty()) return;
XVideoThread::Get()->SetMark(mark);
isMark = true;
}
XImagePro
void XImagePro::Mark(int x, int y, double a)
{
if (des.empty())
return;
if (src2.empty())
return;
Mat ro1 = des(Rect(x, y, src2.cols, src2.rows));
addWeighted(src2, a, ro1, 1 - a, 0, ro1);
}