# 数字视频技术及应用实验一:关键帧提取与分割
# 实验内容
阅读实验二指导书,提取视频关键帧(镜头的第一帧为关键帧),然后分别利用分水岭算法和阈值分割算法对关键帧进行图像分割,其中。至少任选两种不同的阈值分割算法进行实验。
# 实验步骤
在 vs 中新建工程。
2. 配置 OpenCV。
3. 读取视频。
4. 计算前后两帧灰度值之差,若大于某阈值,则将该帧作为关键帧。
5. 对关键帧采用阈值分割算法或分水岭算法(OpenCV 中有封装好的分割函数,但建议按照原理自己实现),生成分割图像。
6. 显示原视频、关键帧和分割后结果。
# 实验方法
# 1. 关键帧提取算法
- 提取视频关键帧(镜头的第一帧为关键帧)
实现代码
Mat frame_key; | |
cap >> frame_key; | |
if (frame_key.empty()) | |
cout << "frame_key is empty!" << endl; | |
waitKey(10); | |
Mat frame; | |
Mat previousImage, currentImage, resultImage; | |
while (1) | |
{ | |
currentFrame++; | |
Mat frame; | |
cap >> frame; | |
if (frame.empty()) | |
{ | |
cout << "frame is empty!" << endl; | |
break; | |
} | |
imshow("当前视频", frame); | |
waitKey(10); | |
Mat srcImage_base; | |
Mat srcImage_test1; | |
srcImage_base = frame_key; | |
srcImage_test1 = frame; | |
// 将图像从 BGR 色彩空间转换到 HSV 色彩空间 | |
cvtColor(srcImage_base, previousImage, CV_BGR2GRAY); | |
cvtColor(srcImage_test1, currentImage, CV_BGR2GRAY); | |
absdiff(currentImage, previousImage, resultImage); // 帧差法,相减 | |
threshold(resultImage, resultImage, 10, 255.0, CV_THRESH_BINARY); // 二值化,像素值相差大于 20 则置为 255,其余为 0 | |
float counter = 0; | |
float num = 0; | |
// 统计两帧相减后图像素 | |
for (int i = 0; i < resultImage.rows; i++) | |
{ | |
uchar* data = resultImage.ptr<uchar>(i); // 获取每一行的指针 | |
for (int j = 0; j < resultImage.cols; j++) | |
{ | |
num = num + 1; | |
if (data[j] == 255) // 访问到像素值 | |
{ | |
counter = counter + 1; | |
} | |
} | |
} | |
p = counter / num; | |
if (p > 0.6) // 输出关键帧 | |
{ | |
frame_key = frame; | |
imshow("关键帧视频", frame_key); | |
waitKey(10); | |
} | |
} |
# 2. 阈值分割算法
OTUS 大津法阈值分割
自适应阈值分割
实现方法:
调用 opencv 内部函数:
threshold(previousImage, previousImage, 80, 250, THRESH_BINARY); | |
imshow("OTUS大津法阈值分割", previousImage); | |
adaptiveThreshold(currentImage, resultImage, 255, 0, 0, 7, 9); | |
imshow("自适应阈值分割", previousImage); |
# 3. 分水岭算法
# 算法步骤
- 图像灰度化。
- 根据 Sobel 算子计算图像梯度(梯度范围为 0-255),边缘像素的梯度记为其邻域像素的梯度。
- 对各像素点的梯度值从小到大排序,梯度值相同的像素为同一层。
- 处理第一层所有的像素点,如果其邻域已经被标识属于某一个区域,则将这个像素加入队列。
- 队列非空时,弹出第一个元素。扫描该像素的邻域像素,如果其邻域像素的梯度值相等,则根据邻域像素的标识来更新该像素的标识。一直循环到队列为空。
- 再次扫描当前梯度值层级的像素,如果还有像素未被标识,说明它是一个新的极小区域,则当前区域的值(当前区域的值从 0 开始计数)加 1 后赋值给该标识的像素。然后从该像素出发继续执行步骤 5)遍历该梯度值层级的所有像素,直至没有新的极小区域。
- 返回步骤 4),处理下一个梯度值层级的像素,直至所有层级的像素都被处理。
# 实现代码 (主要)
Vec3b RandomColor(int value) | |
{ | |
value = value % 255; // 生成 0~255 的随机数 | |
RNG rng; | |
int aa = rng.uniform(0, value); | |
int bb = rng.uniform(0, value); | |
int cc = rng.uniform(0, value); | |
return Vec3b(aa, bb, cc); | |
} | |
void watershed(Mat& the_image) | |
{ | |
Mat image = the_image; // 载入 RGB 彩色图像 | |
// imshow("Source Image", image); | |
// 灰度化,滤波,Canny 边缘检测 | |
Mat imageGray; | |
cvtColor(image, imageGray, CV_RGB2GRAY);// 灰度转换 | |
GaussianBlur(imageGray, imageGray, Size(5, 5), 2); // 高斯滤波 | |
// imshow("Gray Image", imageGray); | |
Canny(imageGray, imageGray, 80, 150); | |
// imshow("Canny Image", imageGray); | |
// 查找轮廓 | |
vector<vector<Point>> contours; | |
vector<Vec4i> hierarchy; | |
findContours(imageGray, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point()); | |
Mat imageContours = Mat::zeros(image.size(), CV_8UC1); // 轮廓 | |
Mat marks(image.size(), CV_32S); //Opencv 分水岭第二个矩阵参数 | |
marks = Scalar::all(0); | |
int index = 0; | |
int compCount = 0; | |
for (; index >= 0; index = hierarchy[index][0], compCount++) | |
{ | |
// 对 marks 进行标记,对不同区域的轮廓进行编号,相当于设置注水点,有多少轮廓,就有多少注水点 | |
drawContours(marks, contours, index, Scalar::all(compCount + 1), 1, 8, hierarchy); | |
drawContours(imageContours, contours, index, Scalar(255), 1, 8, hierarchy); | |
} | |
// 传入的矩阵 marks 里 | |
Mat marksShows; | |
convertScaleAbs(marks, marksShows); | |
// imshow("marksShow", marksShows); | |
// imshow ("轮廓", imageContours); | |
watershed(image, marks); | |
// 分水岭算法之后的矩阵 marks | |
Mat afterWatershed; | |
convertScaleAbs(marks, afterWatershed); | |
imshow("After Watershed", afterWatershed); | |
// 对每一个区域进行颜色填充 | |
Mat PerspectiveImage = Mat::zeros(image.size(), CV_8UC3); | |
for (int i = 0; i < marks.rows; i++) | |
{ | |
for (int j = 0; j < marks.cols; j++) | |
{ | |
int index = marks.at<int>(i, j); | |
if (marks.at<int>(i, j) == -1) | |
{ | |
PerspectiveImage.at<Vec3b>(i, j) = Vec3b(255, 255, 255); | |
} | |
else | |
{ | |
PerspectiveImage.at<Vec3b>(i, j) = RandomColor(index); | |
} | |
} | |
} | |
imshow("After ColorFill", PerspectiveImage); | |
// 分割并填充颜色的结果跟原始图像融合 | |
Mat wshed; | |
addWeighted(image, 0.4, PerspectiveImage, 0.6, 0, wshed); | |
imshow("AddWeighted Image", wshed); | |
waitKey(10); | |
} |
# 全部代码
#include <stdio.h> | |
#include <stdlib.h> | |
#include <iostream> | |
#include <fstream> | |
#include <opencv2/core/core.hpp> | |
#include <opencv2/highgui/highgui.hpp> | |
#include <opencv2/imgproc/imgproc.hpp> | |
#include <opencv2/objdetect/objdetect.hpp> | |
#include <opencv2/ml/ml.hpp> | |
#include <string.h> | |
using namespace std; | |
using namespace cv; | |
using namespace std; | |
Vec3b RandomColor(int value) | |
{ | |
value = value % 255; // 生成 0~255 的随机数 | |
RNG rng; | |
int aa = rng.uniform(0, value); | |
int bb = rng.uniform(0, value); | |
int cc = rng.uniform(0, value); | |
return Vec3b(aa, bb, cc); | |
} | |
void watershed(Mat& the_image) | |
{ | |
Mat image = the_image; // 载入 RGB 彩色图像 | |
// imshow("Source Image", image); | |
// 灰度化,滤波,Canny 边缘检测 | |
Mat imageGray; | |
cvtColor(image, imageGray, CV_RGB2GRAY);// 灰度转换 | |
GaussianBlur(imageGray, imageGray, Size(5, 5), 2); // 高斯滤波 | |
// imshow("Gray Image", imageGray); | |
Canny(imageGray, imageGray, 80, 150); | |
// imshow("Canny Image", imageGray); | |
// 查找轮廓 | |
vector<vector<Point>> contours; | |
vector<Vec4i> hierarchy; | |
findContours(imageGray, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point()); | |
Mat imageContours = Mat::zeros(image.size(), CV_8UC1); // 轮廓 | |
Mat marks(image.size(), CV_32S); //Opencv 分水岭第二个矩阵参数 | |
marks = Scalar::all(0); | |
int index = 0; | |
int compCount = 0; | |
for (; index >= 0; index = hierarchy[index][0], compCount++) | |
{ | |
// 对 marks 进行标记,对不同区域的轮廓进行编号,相当于设置注水点,有多少轮廓,就有多少注水点 | |
drawContours(marks, contours, index, Scalar::all(compCount + 1), 1, 8, hierarchy); | |
drawContours(imageContours, contours, index, Scalar(255), 1, 8, hierarchy); | |
} | |
// 传入的矩阵 marks 里 | |
Mat marksShows; | |
convertScaleAbs(marks, marksShows); | |
// imshow("marksShow", marksShows); | |
// imshow ("轮廓", imageContours); | |
watershed(image, marks); | |
// 分水岭算法之后的矩阵 marks | |
Mat afterWatershed; | |
convertScaleAbs(marks, afterWatershed); | |
imshow("After Watershed", afterWatershed); | |
// 对每一个区域进行颜色填充 | |
Mat PerspectiveImage = Mat::zeros(image.size(), CV_8UC3); | |
for (int i = 0; i < marks.rows; i++) | |
{ | |
for (int j = 0; j < marks.cols; j++) | |
{ | |
int index = marks.at<int>(i, j); | |
if (marks.at<int>(i, j) == -1) | |
{ | |
PerspectiveImage.at<Vec3b>(i, j) = Vec3b(255, 255, 255); | |
} | |
else | |
{ | |
PerspectiveImage.at<Vec3b>(i, j) = RandomColor(index); | |
} | |
} | |
} | |
imshow("After ColorFill", PerspectiveImage); | |
// 分割并填充颜色的结果跟原始图像融合 | |
Mat wshed; | |
addWeighted(image, 0.4, PerspectiveImage, 0.6, 0, wshed); | |
imshow("AddWeighted Image", wshed); | |
waitKey(10); | |
} | |
int main() | |
{ | |
int loop; | |
cout << "请输入你的选择,0阈值分割,1分水岭算法:" << endl; | |
cin >> loop; | |
if (loop == 0) | |
{ | |
long currentFrame = 1; | |
float p; | |
VideoCapture cap; | |
// 这里放置需要提取关键字的视频 | |
cap.open("D:\\C++\\OpenCV\\exp1.avi"); | |
if (!cap.isOpened())// 如果视频不能正常打开则返回 | |
{ | |
cout << "cannot open video!" << endl; | |
return 0; | |
} | |
Mat frame_key; | |
cap >> frame_key; | |
if (frame_key.empty()) | |
cout << "frame_key is empty!" << endl; | |
//imshow("fram_1", frame_key); | |
waitKey(10); | |
Mat frame; | |
Mat previousImage, currentImage, resultImage; | |
while (1) | |
{ | |
currentFrame++; | |
Mat frame; | |
cap >> frame; | |
if (frame.empty()) | |
{ | |
cout << "frame is empty!" << endl; | |
break; | |
} | |
imshow("当前视频", frame); | |
waitKey(10); | |
Mat srcImage_base; | |
Mat srcImage_test1; | |
srcImage_base = frame_key; | |
srcImage_test1 = frame; | |
// 将图像从 BGR 色彩空间转换到 HSV 色彩空间 | |
cvtColor(srcImage_base, previousImage, CV_BGR2GRAY); | |
cvtColor(srcImage_test1, currentImage, CV_BGR2GRAY); | |
absdiff(currentImage, previousImage, resultImage); // 帧差法,相减 | |
threshold(resultImage, resultImage, 10, 255.0, CV_THRESH_BINARY); // 二值化,像素值相差大于 20 则置为 255,其余为 0 | |
float counter = 0; | |
float num = 0; | |
// 统计两帧相减后图像素 | |
for (int i = 0; i < resultImage.rows; i++) | |
{ | |
uchar* data = resultImage.ptr<uchar>(i); // 获取每一行的指针 | |
for (int j = 0; j < resultImage.cols; j++) | |
{ | |
num = num + 1; | |
if (data[j] == 255) // 访问到像素值 | |
{ | |
counter = counter + 1; | |
} | |
} | |
} | |
p = counter / num; | |
if (p > 0.6) // 输出关键帧 | |
{ | |
frame_key = frame; | |
imshow("关键帧视频", frame_key); | |
waitKey(10); | |
cvtColor(frame_key, previousImage, CV_BGR2GRAY); | |
threshold(previousImage, previousImage, 80, 250, THRESH_BINARY); | |
imshow("OTUS大津法阈值分割", previousImage); | |
adaptiveThreshold(currentImage, resultImage, 255, 0, 0, 7, 9); | |
imshow("自适应阈值分割", previousImage); | |
waitKey(10); | |
} | |
} | |
} | |
else if (loop == 1) | |
{ | |
long currentFrame = 1; | |
float p; | |
VideoCapture cap; | |
// 这里放置需要提取关键字的视频 | |
cap.open("D:\\C++\\OpenCV\\exp1.avi"); | |
if (!cap.isOpened())// 如果视频不能正常打开则返回 | |
{ | |
cout << "cannot open video!" << endl; | |
return 0; | |
} | |
Mat frame_key; | |
cap >> frame_key; | |
if (frame_key.empty()) | |
cout << "frame_key is empty!" << endl; | |
//imshow("fram_1", frame_key); | |
waitKey(10); | |
watershed(frame_key); | |
Mat frame; | |
Mat previousImage, currentImage, resultImage; | |
while (1) | |
{ | |
currentFrame++; | |
Mat frame; | |
cap >> frame; | |
if (frame.empty()) | |
{ | |
cout << "frame is empty!" << endl; | |
break; | |
} | |
imshow("当前视频", frame); | |
waitKey(10); | |
Mat srcImage_base; | |
Mat srcImage_test1; | |
srcImage_base = frame_key; | |
srcImage_test1 = frame; | |
// 将图像从 BGR 色彩空间转换到 HSV 色彩空间 | |
cvtColor(srcImage_base, previousImage, CV_BGR2GRAY); | |
cvtColor(srcImage_test1, currentImage, CV_BGR2GRAY); | |
absdiff(currentImage, previousImage, resultImage); // 帧差法,相减 | |
threshold(resultImage, resultImage, 10, 255.0, CV_THRESH_BINARY); // 二值化,像素值相差大于 20 则置为 255,其余为 0 | |
float counter = 0; | |
float num = 0; | |
// 统计两帧相减后图像素 | |
for (int i = 0; i < resultImage.rows; i++) | |
{ | |
uchar* data = resultImage.ptr<uchar>(i); // 获取每一行的指针 | |
for (int j = 0; j < resultImage.cols; j++) | |
{ | |
num = num + 1; | |
if (data[j] == 255) // 访问到像素值 | |
{ | |
counter = counter + 1; | |
} | |
} | |
} | |
p = counter / num; | |
if (p > 0.6) // 输出关键帧 | |
{ | |
frame_key = frame; | |
// imshow ("关键帧视频", frame_key); | |
waitKey(10); | |
watershed(frame_key); | |
// imshow ("关键帧视频", frame_key); | |
waitKey(10); | |
} | |
} | |
} | |
} |
# 实验结果
- 1、关键帧提取和阈值分割
- 2、分水岭算法
# 实验总结
1、这里视频关键帧提取采取的是检测镜头的方法,b 把镜头的第一帧认为是关键帧,判断镜头的方法是帧差法,通过判断帧之间的差异,达到一定的阈值被认为是一个新的镜头。
2、阈值分割,没有自己重新写 OTSU 等算法,直接采用 opencv 库函数中的算法。
3、分水岭算法,由图像的分水岭算法推及到视频,每一帧进行处理,对于图像而言,使用了很多图像处理的算法,例如灰度化、sobel 算子进行边缘检测;使用队列存储被标识的像素点。