灰度投影法视频稳像

前言

做毕设时处理的视频有较大的抖动,影响数据分析,于是查了一下视频消抖的相关资料,准备简单实现一下。

原理

视频发生抖动时的最显著特征就是帧与帧之间会发生整体的位移,检测出位移之后再通过进一步的逻辑判定视频是否产生抖动,因此基本上视频的抖动都是围绕着如何检测出这个位移进行的。

检测方案

  1. 光流法:光流是空间运动物体在观察成像平面上的像素运动的瞬时速度。光流法是利用图像序列中像素在时间域上的变化以及相邻帧之间的相关性来找到上一帧跟当前帧之间存在的对应关系,从而计算出相邻帧之间物体的运动信息的一种方法。
    由于变化的光线会被错误地识别为光流, 因此该方法对光线敏感, 从而会影响到识别效果。光流法比较耗费时间、计算复杂性大,并且抗噪声的能力很差, 在对实时性要求苛刻的情况下并不适用。
  2. 块匹配法: 基于块的运动估计,基本思想是将图像序列的每一帧分成许多互不重叠的块,并认为块内所有像素的位移量都相同,然后对每个宏块到参考帧某一给定特定搜索范围内根据一定的块匹配准则找出与当前块最相似的块,即匹配块,匹配块与当前块的相对位移即为运动矢量。块匹配法的性能取决于:块尺寸的大小,匹配准则以及搜索策略。
  3. 特征点匹配法:提取图像特征点,计算特征点图像描述子(图像灰度/变化梯度等),通过描述子来匹配特征点,确定特征点的匹配关系,利用三角/对极几何/PnP等算法估算相机运动。计算精度高,但是耗时。光流法算得上是特征点匹配法的一种。
  4. 灰度投影法:使用比较多,计算量也相对较小,实际效果也较好的方法,本文着重对灰度投影法做介绍。

灰度投影法原理

灰度投影法通过图像整体行、列灰度投影曲线的相关性来估计相邻帧之间的全局运动向量,即检测视频序列帧间的平移向量。灰度投影法仅对视频相邻帧行、列各自的投影曲线进行一次互相关运算处理,就能计算出视频序列帧之间全局的运动偏移向量。此算法计算量小且具有较高的精度,所以应用广
泛,是比较常用的一种全局运动估计算法。
算法分为图像灰度映射平移检测两部分。

图像灰度映射

以二维图像的像素行和列为单位,将图像特征转化为沿行、列坐标的曲线,从而更容易对图像分布特征进行计算。
$$\begin{cases}
G_k(j)=\displaystyle\sum_{i=0}^mG_k(i,j)\
G_k(i)=\displaystyle\sum_{j=0}^nG_k(i,j)\
\end{cases}
$$
上式中,$G_k(j)、G_k(i)$分别是第 k 帧图像中第 j 列和第 i 行的灰度投影值,图像总共有 m 行 n 列,$G_k(i,j)$为 (i, j) 位置处的像素灰度值。

平移检测

对视频序列当前帧和参考帧的行、列投影曲线进行一次互相关运算即可实现平移检测。相关运算公式:
$$ K(\omega)=\displaystyle\sum_{j=1}^N[C_k(j+\omega-1)-C_r(m+j)]^2,1 \leq \omega \leq 2m+1 $$
式中$C_k(j)、C_r(j)$分别为第 k 帧和第 r 帧图像第 j 列的灰度投影值,m 为运动估计的最大抖动范围。
为了节省计算量,本项目中直接把参考帧的灰度投影和当前帧的灰度投影对应相减得到差值求和,然后把当前帧进行平移,直到得到最小的差值和,此时平移的距离就是要修正的距离。

OpenCV代码实现

在 OpenCV 中实现灰度投影,代码如下。

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#include "headfile.h"
#include "antishake.h"

using namespace cv;
using namespace std;
/*灰度投影法*/
int shift_h, shift_w; // h、w偏移量

typedef struct GrayVector
{
vector<int> col; // 列灰度 w
vector<int> row; // 行灰度 h
};

GrayVector begin_gray; // 参考灰度向量

// 获取行.列灰度值信息
// 返回 GrayVector
GrayVector get_gray_data(Mat frame)
{
Mat channels[3];
int nCols = frame.cols;
int nRows = frame.rows;
cv::split(frame, channels);
GrayVector temp;
temp.col.resize(nCols, 0);
temp.row.resize(nRows, 0);
while (1)
{
for (int i = 0; i < nRows; i++)
{
for (int j = 0; j < nCols; j++)
{
int data = channels[1].at<uchar>(i, j);
temp.col[j] = temp.col[j] + data;
temp.row[i] = temp.row[i] + data;
}
temp.row[i] = temp.row[i] / nRows;
}
break;
}
for (int j = 0; j < nCols; j++)
{
temp.col[j] = temp.col[j] / nCols;
}
return temp;
}

void antishake_main()
{
Mat frame;
LadleVideo.set(CAP_PROP_POS_FRAMES, 10);
GrayVector master_data,slave_data;
LadleVideo >> frame;
master_data = get_gray_data(frame);
LadleVideo.set(CAP_PROP_POS_FRAMES, 30);
LadleVideo >> frame;
slave_data = get_gray_data(frame);
GrayVector result;
result = cross_correlation(master_data, slave_data);
cout << "size " << result.col.size() << endl;
cout << "number " << result.col[0] << endl;
cout << "size " << result.row.size() << endl;
cout << "number " << result.row[0] << endl;
}

typedef struct MinValueDistance
{
int vaule;
int distance;
}MinValueDistance;

// 返回值: 偏移量
GrayVector cross_correlation(GrayVector master, GrayVector slave)
{
int length_col, length_row;
length_col = master.col.size();
length_row = master.row.size();
GrayVector ReturnValue; // 返回值
int search_distance = 200; // 单侧搜索距离长度 以master为参考 左-右+
MinValueDistance minvalue_col = { 999999,999999 };
MinValueDistance minvalue_row = { 999999,999999 };
// 左搜索
int temp_data = 0;
for (int i = 0; i < search_distance; i++)
{
for (int num = 0; num < length_col - i; num++)
{
int temp = master.col[num] - slave.col[num + i];
temp = temp > 0 ? temp : -temp;
temp_data += temp;
}
if (temp_data < minvalue_col.vaule)
{
minvalue_col.vaule = temp_data;
minvalue_col.distance = -i;
}
temp_data = 0;
}
// 右
for (int i = 0; i < search_distance; i++)
{
for (int num = 0; num < length_col - i; num++)
{
int temp = master.col[num + i] - slave.col[num];
temp = temp > 0 ? temp : -temp;
temp_data += temp;
}
if (temp_data < minvalue_col.vaule)
{
minvalue_col.vaule = temp_data;
minvalue_col.distance = i;
}
temp_data = 0;
}

// 左搜索
for (int i = 0; i < search_distance; i++)
{
for (int num = 0; num < length_row - i; num++)
{
int temp = master.row[num] - slave.row[num + i];
temp = temp > 0 ? temp : -temp;
temp_data += temp;
}
if (temp_data < minvalue_row.vaule)
{
minvalue_row.vaule = temp_data;
minvalue_row.distance = -i;
}
temp_data = 0;
}
// 右
for (int i = 0; i < search_distance; i++)
{
for (int num = 0; num < length_row - i; num++)
{
int temp = master.row[num + i] - slave.row[num];
temp = temp > 0 ? temp : -temp;
temp_data += temp;
}
if (temp_data < minvalue_row.vaule)
{
minvalue_row.vaule = temp_data;
minvalue_row.distance = i;
}
temp_data = 0;
}
ReturnValue.col.push_back(minvalue_col.distance);
ReturnValue.row.push_back(minvalue_row.distance);
return ReturnValue;
}

void show_anti_video()
{
Mat frame;
Mat save_img;
VideoWriter vw;
int fps = LadleVideo.get(CAP_PROP_FPS);
vw.open("./zzzout.avi",
VideoWriter::fourcc('X', '2', '6', '4'),
fps,
Size(LadleVideo.get(CAP_PROP_FRAME_WIDTH),
LadleVideo.get(CAP_PROP_FRAME_HEIGHT))
);
GrayVector master_data, slave_data;
LadleVideo >> frame;
master_data = get_gray_data(frame);
#define ANTISHAKE_SHOW_SAVE 0 /*1-显示 0-保存*/
while (1)
{
LadleVideo >> frame;
if (frame.empty()) {
break;
}
slave_data = get_gray_data(frame);
GrayVector result;
result = cross_correlation(master_data, slave_data);

Mat warp_matrix = (cv::Mat_<float>(2, 3) <<
cos(0), -sin(0), result.col[0], // w
sin(0), cos(0), result.row[0]); // h
warpAffine(frame, save_img, warp_matrix, cv::Size(frame.cols, frame.rows), cv::INTER_LINEAR);
#if 0 == ANTISHAKE_SHOW_SAVE
vw.write(save_img);
std::cout << "第" << LadleVideo.get(CAP_PROP_POS_FRAMES) << std::endl;
#elif 1 == ANTISHAKE_SHOW_SAVE
namedWindow("srcImg", WINDOW_GUI_EXPANDED);
cv::imshow("srcImg", save_img);
cv::waitKey(33);
#endif
}
LadleVideo.release();
vw.release();
}

效果展示

未消抖:

消抖:

未消抖:

消抖:

参考文献

链接:https://pan.baidu.com/s/1cwtCCv0_RORIfJVSif1cuA?pwd=ci1m
提取码:ci1m