基于OpenCV的视频编辑应用开发

  1. 项目内容展示
  2. 项目开发日志

项目效果展示

视频编辑器的界面和基础功能介绍
界面

界面由原视频(左上方)和输出视频(右上方),以及下方的编辑界面组成。
在这里插入图片描述

基础功能

打开视频文件,视频的播放和暂停,处理后视频的导出,以及拖动进度条对视频播放进度和裁剪开始和结束位置进行设置。

视频图像裁剪功能

指定输出视频某一区域内容
在这里插入图片描述

灰度图转换

在这里插入图片描述

添加水印

可以选择水印图片并调整位置和透明度
在这里插入图片描述

设置亮度

在这里插入图片描述

设置对比度

在这里插入图片描述

旋转

可旋转90, 180, 270度
在这里插入图片描述

镜像

可以设置水平和竖直镜像
在这里插入图片描述

重新调整视频尺寸

在这里插入图片描述

高斯滤波和拉普拉斯滤波

高斯滤波可以压缩图像尺寸
拉普拉斯滤波可以使图像更加清晰
在这里插入图片描述

项目开发日志

项目目标

基于VS、Qt平台开发一款视频编辑器,其功能包括视频播放、截图、剪切、合并、融合、添加水印、亮度和对比度调整、高斯滤波和拉普拉斯滤波、图像旋转和镜像、图像颜色调整功能。

项目类

![[Pasted image 20240325222847.png]]
XVideoUI
XVideoThread
XVideoFilter
XVideoWidget
XImagePro

创建项目目录

——bin
——![[Pasted image 20240325225629.png]]

——lib
——![[Pasted image 20240325225653.png]]

——src
——![[Pasted image 20240325225720.png]]

创建项目

选择Qt Widget Application模板
![[Pasted image 20240327034628.png]]

配置项目名称和路径
![[Pasted image 20240327034846.png]]

配置Qt选项
![[Pasted image 20240327034932.png]]

![[Pasted image 20240327035101.png]]

设置VS中项目属性

在这里插入图片描述

设置Qt基础界面属性

调整界面宽高和背景颜色
在这里插入图片描述

添加视频播放窗口

添加窗口和按钮
![[Pasted image 20240327041103.png]]

提升窗口类并添加类

提升窗口类
![[Pasted image 20240327041710.png]]

添加类
![[Pasted image 20240327041908.png]]

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)
{

}
设置按钮

在界面编辑器设置信号和函数声明
![[Pasted image 20240327042342.png]]

在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中添加
在这里插入图片描述

![[Pasted image 20240327143012.png]]

进度条显示视频播放进度

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);
}
设置亮度和对比度

添加界面组件
![[Pasted image 20240327173103.png]]

![[Pasted image 20240327173759.png]]

添加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);

}
最终效果

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值