基于FFT的图片模糊检测算法

Background

我们希望对输入图片进行检测,判断图片是否清晰。传统方法中,我们可以通过使用Laplacian算子,求输入图片的二阶导图,得到图像的边缘信息。然后对二阶导图求方差,根据方差值的大小可以判断出图像模糊的模糊程度1。方差值越小,图像越模糊。

可是该方法存在的问题是,很难确定一个阀值来区分清晰和模糊图片。在不同的场景中,清晰与模糊之间的阀值会发生变化。

The downside is that the Laplacian method required significant manual tuning to define the “threshold” at which an image was considered blurry or not. If you could control your lighting conditions, environment, and image capturing process, it worked quite well — but if not, you would obtain mixed results, to say the least.2

detecting_blur_result_006 detecting_blur_result_004

我们参考了34中提出的方法,实现了一个基于傅立叶变换(FFT)的图片模糊检测算法。算法中,首先会将输入图片转变为频谱图。频谱图中,中心代表的是低频,往四面八方扩展后逐渐变为高频。通过屏蔽掉频谱图的中心区域,对图像实现高通滤波,保留图像中边缘等高频的信息。然后对频谱图求均值,求图片中平均高频幅值,通过该平均幅值来判断图像是否模糊。平均幅值越小,图像越模糊。

Code

  • 对于全黑图片,我们将Blur Value设置为100,并且认为是模糊图片。
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
196
197
cv::Mat ColorMat(const cv::Mat &mat_float, bool draw_colorbar = false, const bool is_white_background = false, double min_val = 1, double max_val = 0, const cv::Mat &user_color = cv::Mat(), int colorbar_width = 50, int colorbar_gap = 5) {
if (min_val > max_val) {
cv::minMaxLoc(mat_float, &min_val, &max_val);
}

cv::Mat mat;
mat_float.convertTo(mat, CV_8UC1, 255 / (max_val - min_val), -255 * min_val / (max_val - min_val));

cv::Mat mat_show;

if (user_color.empty()) {
cv::applyColorMap(mat, mat_show, cv::COLORMAP_JET);
} else {
cv::applyColorMap(mat, mat_show, user_color);
}

if (is_white_background) {
cv::Mat mask;
cv::threshold(mat, mask, 0, 255, cv::THRESH_BINARY_INV);
cv::Mat img_white(mat.size(), CV_8UC3, cv::Scalar(255, 255, 255));
img_white.copyTo(mat_show, mask);
}

if (draw_colorbar) {
cv::Mat color_bar_value(cv::Size(colorbar_width, mat_show.rows), CV_8UC1);
cv::Mat color_bar;

for (int i = 0; i < mat_show.rows; i++) {
uchar value = 255 - 255 * float(i) / float(mat_show.rows);
for (int j = 0; j < colorbar_width; j++) {
color_bar_value.at<uchar>(i, j) = value;
}
}

if (user_color.empty()) {
cv::applyColorMap(color_bar_value, color_bar, cv::COLORMAP_JET);
} else {
cv::applyColorMap(color_bar_value, color_bar, user_color);
}

cv::Mat mat_colorbar_show(cv::Size(mat_show.cols + colorbar_width + colorbar_gap, mat_show.rows), CV_8UC3, cv::Scalar(255, 255, 255));

mat_show.copyTo(mat_colorbar_show(cv::Rect(0, 0, mat_show.cols, mat_show.rows)));
color_bar.copyTo(mat_colorbar_show(cv::Rect(mat_show.cols + colorbar_gap, 0, color_bar.cols, color_bar.rows)));

cv::putText(mat_colorbar_show, ToStr(max_val), cv::Point(mat_show.cols + colorbar_gap, 20), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(255, 255, 255), 1);
cv::putText(mat_colorbar_show, ToStr(min_val), cv::Point(mat_show.cols + colorbar_gap, mat_show.rows - 10), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(255, 255, 255), 1);

mat_show = mat_colorbar_show;
}

return mat_show;
};

cv::Mat ShowColorMat(const std::string &name, const cv::Mat &mat_float, bool draw_colorbar = false, const float scale = 1, const bool is_white_background = false, double min_val = 1, double max_val = 0, const cv::Mat &user_color = cv::Mat(), int colorbar_width = 50, int colorbar_gap = 5) {
cv::Mat mat_resize;
cv::resize(mat_float, mat_resize, cv::Size(), scale, scale, cv::INTER_NEAREST);
cv::Mat img = ColorMat(mat_resize, draw_colorbar, is_white_background, min_val, max_val, user_color, colorbar_width, colorbar_gap);
cv::imshow(name, img);
return img;
}

cv::Mat FilterMask(const float radius, const cv::Size &mask_size) {
cv::Mat mask = cv::Mat::ones(mask_size, CV_8UC1);
cv::circle(mask, cv::Point(mask_size.width / 2, mask_size.height / 2), radius, cv::Scalar(0), -1);

// https://datahacker.rs/opencv-discrete-fourier-transform-part2/
cv::Mat mask_float, filter_mask;
mask.convertTo(mask_float, CV_32F);
std::vector<cv::Mat> mask_merge = {mask_float, mask_float};
cv::merge(mask_merge, filter_mask);
return filter_mask;
}

bool BlurDetection(const cv::Mat &img,
const cv::Mat &filter_mask,
float &blur_value,
const float thresh = 10,
const bool debug_show = false) {
// https://pyimagesearch.com/2020/06/15/opencv-fast-fourier-transform-fft-for-blur-detection-in-images-and-video-streams/
// https://github.com/Qengineering/Blur-detection-with-FFT-in-C/blob/master/main.cpp
// https://docs.opencv.org/3.4/d8/d01/tutorial_discrete_fourier_transform.html

cv::Mat img_scale, img_gray, img_fft;
cv::resize(img, img_scale, filter_mask.size());

if (img_scale.channels() == 3) {
cv::cvtColor(img_scale, img_gray, cv::COLOR_BGR2GRAY);
} else {
img_gray = img_scale;
}
img_gray.convertTo(img_fft, CV_32F);

// If DFT_SCALE is set, the scaling is done after the transformation.
// When DFT_COMPLEX_OUTPUT is set, the output is a complex matrix of the same size as input.
cv::dft(img_fft, img_fft, cv::DFT_COMPLEX_OUTPUT); // cv::DFT_SCALE |

// # zero-out the center of the FFT shift (i.e., remove low
// # frequencies), apply the inverse shift such that the DC
// # component once again becomes the top-left, and then apply
// # the inverse FFT

// rearrange the quadrants of the result, so that the origin (zero, zero) corresponds with the image center.
int cx = img_gray.cols / 2;
int cy = img_gray.rows / 2;

// center low frequencies in the middle
// by shuffling the quadrants.
cv::Mat q0(img_fft, cv::Rect(0, 0, cx, cy)); // Top-Left - Create a ROI per quadrant
cv::Mat q1(img_fft, cv::Rect(cx, 0, cx, cy)); // Top-Right
cv::Mat q2(img_fft, cv::Rect(0, cy, cx, cy)); // Bottom-Left
cv::Mat q3(img_fft, cv::Rect(cx, cy, cx, cy)); // Bottom-Right

cv::Mat tmp; // swap quadrants (Top-Left with Bottom-Right)
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);

q1.copyTo(tmp); // swap quadrant (Top-Right with Bottom-Left)
q2.copyTo(q1);
tmp.copyTo(q2);

if (debug_show) {
std::vector<cv::Mat> planes;
cv::Mat fft_mag;
cv::split(img_fft, planes); // planes[0] = Re(DFT(I), planes[1] = Im(DFT(I))
cv::magnitude(planes[0], planes[1], fft_mag);
fft_mag += cv::Scalar::all(1); // switch to logarithmic scale
cv::log(fft_mag, fft_mag);
ShowColorMat("fft", fft_mag, true, 1, false, 0, 20);
}

// Block the low frequencies
cv::mulSpectrums(img_fft, filter_mask, img_fft, 0); // multiply 2 spectrums

if (debug_show) {
std::vector<cv::Mat> planes;
cv::Mat fft_mag;
cv::split(img_fft, planes); // planes[0] = Re(DFT(I), planes[1] = Im(DFT(I))
cv::magnitude(planes[0], planes[1], fft_mag);
fft_mag += cv::Scalar::all(1); // switch to logarithmic scale
cv::log(fft_mag, fft_mag);
ShowColorMat("fft block low frequencies", fft_mag, true, 1, false, 0, 20);
}

// shuffle the quadrants to their original position
cv::Mat p0(img_fft, cv::Rect(0, 0, cx, cy)); // Top-Left - Create a ROI per quadrant
cv::Mat p1(img_fft, cv::Rect(cx, 0, cx, cy)); // Top-Right
cv::Mat p2(img_fft, cv::Rect(0, cy, cx, cy)); // Bottom-Left
cv::Mat p3(img_fft, cv::Rect(cx, cy, cx, cy)); // Bottom-Right

p0.copyTo(tmp);
p3.copyTo(p0);
tmp.copyTo(p3);

p1.copyTo(tmp); // swap quadrant (Top-Right with Bottom-Left)
p2.copyTo(p1);
tmp.copyTo(p2);

cv::dft(img_fft, img_fft, cv::DFT_SCALE | cv::DFT_INVERSE);

std::vector<cv::Mat> complex_number;
cv::Mat img_blur;
cv::split(img_fft, complex_number); // planes[0] = Re(DFT(I), planes[1] = Im(DFT(I))
magnitude(complex_number[0], complex_number[1], img_blur); // abs of complex number


double min_val, max_val;
cv::minMaxLoc(img_blur, &min_val, &max_val);

if (max_val <= 0.f) {
blur_value = 100;
// black image
return true;
}

cv::log(img_blur, img_blur);
blur_value = cv::mean(img_blur)[0] * 20.f;

return blur_value < thresh;
}

int main(int argc, char *argv[]) {
float blur_thresh = 14;
float filter_mask_radius = 50;
cv::Size mask_size = cv::Size(640, 360);

cv::Mat img = cv::Mat::zeros(cv::Size(1280, 720), CV_8UC1);
cv::Mat filter_mask = FilterMask(filter_mask_radius, mask_size);
float blur_value;
BlurDetection(img, filter_mask, blur_value, blur_thresh, true);
cv::imshow("input", img);
std::cout<<blur_value<<std::endl;
cv::waitKey(0);

return 1;
}

Validation

radius of filter_mask = 50 size of filter_mask = (640, 360) blur_thresh = 16

Input FFT FFT without low frq. Blur Value Blur?
mesuem_18_1034 fft_mesuem_18_1034 fft_block_mesuem_18_1034 16.24 False
mesuem_18_1012 fft_mesuem_18_1012 fft_block_mesuem_18_1012 11.67 True
mesuem_18_18711 fft_mesuem_18_18711 fft_block_mesuem_18_18711 16.86 False
mesuem_18_18727 fft_mesuem_18_18727 fft_block_mesuem_18_18727 12.91 True

  1. https://pyimagesearch.com/2015/09/07/blur-detection-with-opencv/↩︎

  2. https://pyimagesearch.com/2020/06/15/opencv-fast-fourier-transform-fft-for-blur-detection-in-images-and-video-streams/↩︎

  3. https://pyimagesearch.com/2020/06/15/opencv-fast-fourier-transform-fft-for-blur-detection-in-images-and-video-streams/↩︎

  4. https://github.com/Qengineering/Blur-detection-with-FFT-in-C/blob/master/main.cpp↩︎