抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

高斯模糊是被广泛使用的图形算法之一,在实现高斯模糊之前,先要了解正态分布

正态分布

一维的正态分布为

f(x)=12πσe(xμ)22σ2f(x)=\frac{1}{\sqrt{2\pi}\sigma}e^{-\frac{(x-\mu)^2}{2\sigma^2}}

直接让f(x)f(x)f(y)f(y)相乘,就得到了二维的正态分布

f(x,y)=12πσ2e(x2+y2)22σ2f(x,y)=\frac{1}{2\pi \sigma^2}e^{-\frac{(x^2+y^2)^2}{2\sigma^2}}

此处直接令μ=0μ=0,将会在下面解释.

权值矩阵

设有一个(2n+1)(2n+1)阶矩阵MM,且有

Mi,j=1\sum M_{i,j}=1

,我们称这个矩阵为权值矩阵,称Mi,jM_{i,j}(i,j)(i,j)点处的权.其中nn是高斯模糊的半径.

离散卷积

离散卷积是卷积对离散量的特殊形式,假设现有原图矩阵AA,权值矩阵BB,则点(x,y)(x,y)处的离散卷积为

G(x,y)=i[x±n]j[y±n]Ai,jBi,jG(x,y)=\sum\limits_{ \substack{i\in [x\pm n] \\[3pt]j\in [y\pm n]} }A_{i,j}\cdot B_{i,j}

在更严格的定义中,A(i,j)A(i,j)应该与B(ui,vj)B(u-i,v-j)相乘,但是针对本文的高斯模糊而言,其效果是一样的,且上面给出的公式更为简洁.

现在举个例子,有一张尺寸为3*3的图片SS,将其颜色转化成矩阵AA,为

[123456789]\begin{bmatrix} 1 & 2 & 3 \cr 4 & 5 & 6 \cr 7 & 8 & 9 \cr \end{bmatrix}

有权值矩阵BB

[010151010]\begin{bmatrix} 0 & -1 & 0 \cr -1 & 5 & -1 \cr 0 & -1 & 0 \cr \end{bmatrix}

A(i,j)A(i,j)B(i,j)B(i,j)相乘,将结果相加

(1)×2+(1)×4+(1)×6+(1)×8+5×5=5(-1) \times 2 + (-1) \times 4 + (-1) \times 6 + (-1) \times 8 + 5 \times 5 = 5

则以上两个矩阵的离散卷积结果为55,这就是矩阵AA经过处理后得到的新矩阵M(2,2)M(2,2)的值.

在高斯模糊中,设模糊半径为nn,则定义一个维数为2n+12n+1的权值矩阵GG,且G(i,j)=f(in1,jn1)G(i,j)=f(i-n-1,j-n-1),类似于将一个直角坐标系放在了GG的中点处,这就是μ=0μ=0的原因.此处的f是二维正态分布函数.然后求和Gi,j\sum G_{i,j},将矩阵的每个数除以这个和.求和的步骤是防止图片过亮或过暗.

将得到的矩阵G代替B计算,其结果就是高斯模糊的结果

优化

上述方法的效率较低,在介绍正态分布时,二维的正态分布函数是两个一维函数相乘得到的,这两个一维函数分别是f(x)f(x)f(y)f(y),f(x)f(x)代表水平方向,f(y)f(y)代表垂直方向.对于一个n维权值矩阵,用它来处理a×ba\times b尺寸的图片,如果用二维正态分布函数来计算,总共需要计算abnn=abn2a\cdot b\cdot n\cdot n=abn^2次,及其繁琐.这时我们可以使用一维的正态分布函数,得出一个“权值列向量”,这个向量的作用类似权值矩阵,用这个列向量把图片横向处理,相当于f(x)f(x),再用它把图片纵向处理,相当于f(y)f(y),此时图片经过两次处理,相当于f(x)f(y)f(x)\cdot f(y),也可以达到二维正态分布的效果,而计算量仅仅是abn+abn=2abna\cdot b\cdot n+a\cdot b\cdot n=2abn,下降了一个数量级.该方法不详细介绍,将在代码中展示.

代码实现

GaussianBlur类,算法的核心部分

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
public final class GaussianBlur {
private static final int precision = 10000; // 精度,由于返回的是int数组,精度较低,因此需要将所有值同时扩大数倍,可以理解为把小数点向右移动
private static final double E = 2.718281828459045;//自然常数e
private static final double PI = 3.141592653589793;//圆周率

/**
* 快速高斯模糊
* @param picture 三维数组,picture[a][b][c],a表示颜色,012分别为R,G,B;b和c代表尺寸,宽度为b,高度为c
* @param radius 半径
* @return 格式如同picture的数组
*/
public static int[][][] GaussianBlur(int[][][] picture,int radius){
int i, j, x, R, G, B, proportion, subscript;
int[] matrix = LinearNormalDistribution(radius,1.5);
int width = picture[0].length, height = picture[0][0].length;
int[][][] color_1 = new int[3][width][height]; // 用来存高斯模糊后的数据
int[][][] color_2 = new int[3][width][height]; // 临时存储纵向滤波之后的数据
//纵向滤波
for (i = 0; i < width; i++) {
for (j = 0; j < height; j++) {
R = G = B = 0;
for (x = j - radius; x <= j + radius; x++) {
proportion = matrix[x + radius - j];
subscript = (x >= 0 && x < height) ? x : 2 * j - x; // 如果坐标越界了,则计算对称点来代替
R += picture[0][i][subscript] * proportion;
G += picture[1][i][subscript] * proportion;
B += picture[2][i][subscript] * proportion;
}
color_2[0][i][j] = R / precision;
color_2[1][i][j] = G / precision;
color_2[2][i][j] = B / precision;
}
}
//横向滤波
for (i = 0; i < height; i++) {
for (j = 0; j < width; j++) {
R = G = B = 0;
for (x = j - radius; x <= j + radius; x++) {
proportion = matrix[x + radius - j];
subscript = (x >= 0 && x < width) ? x : 2 * j - x;
R += color_2[0][subscript][i] * proportion;
G += color_2[1][subscript][i] * proportion;
B += color_2[2][subscript][i] * proportion;
}
//注意for语句中i代表高度,j代表宽度,所以下面三个语句的i和j并没有写错位置
color_1[0][j][i] = R / precision;
color_1[1][j][i] = G / precision;
color_1[2][j][i] = B / precision;
}
}
return color_1;
}

/**
* 慢速高斯模糊,采用二维正态分布的方法来处理图像
* @param picture 三维数组,picture[a][b][c],a表示颜色,012分别为R,G,B;b和c代表尺寸,宽度为b,高度为c
* @param radius 半径
* @return 格式如同picture的数组
*/
public static int[][][] SlowGaussianBlur(int[][][] picture,int radius){
//flag为真时计算加权,为假时直接代入矩阵
int[][] matrix = NormalDistribution(radius,1.5);
int i, j, x, y, R, G, B, proportion, left, right, width = picture[0].length, height = picture[0][0].length;
int[][][] color = new int[3][width][height];
//选取每个点
for (i = 0; i < width; i++) {
for (j = 0; j < height; j++) {
//选取半径为radius的矩阵
R = G = B = 0;
for (x = i - radius; x <= i + radius; x++) {
for (y = j - radius; y <= j + radius; y++) {
//求出颜色
proportion = matrix[x + radius - i][y + radius - j];
left = (x >= 0 && x < width) ? x : 2 * i - x;
right = (y >= 0 && y < height) ? y : 2 * j - y;
R += picture[0][left][right] * proportion;
G += picture[1][left][right] * proportion;
B += picture[2][left][right] * proportion;
}
}
color[0][i][j] = R / precision;
color[1][i][j] = G / precision;
color[2][i][j] = B / precision;
}
}
return color;
}

/**
* 用一维正态分布函数来计算“权值列向量”,效率较高
* @param radius 模糊半径
* @param SIGMA 正态分布参数,如果自己没把握,就填1.5
* @return “权值列向量”
*/
private static int[] LinearNormalDistribution(int radius,double SIGMA){
int[] matrix = new int[2 * radius + 1]; // 定义一个列向量
int sum, i;
//计算各个点的正态分布值
sum = matrix[radius] = (int) (precision / (2 * PI * SIGMA * SIGMA)); // sum的初值为向量中心点的值,例如向量(1,2,3,2,1),则初值为3
for (i = 1; i <= radius; i++) {
//根据对称性,可以减少一倍的运算量,i=0的情况已经在sum初值那一步考虑
matrix[radius-i] = matrix[radius+i] = (int) ((Math.pow(E, -i * i / (2 * SIGMA * SIGMA)) / (2 * PI * SIGMA * SIGMA)) * precision);
sum += matrix[radius+i] * 2; // 计算向量所有值之和
}
for (i = 0; i < 2 * radius + 1; i++) {
matrix[i] = matrix[i] * precision / sum; // 所有值都除以sum,确保它们的和为“1”,由于扩大了10000倍,所以这个“1”实际上应该是10000
}
return matrix;
}

/**
* 用二维正态分布函数来计算权值矩阵,效率较低
* @param radius 模糊半径
* @param SIGMA 正态分布参数,如果自己没把握,就填1.5
* @return 权值矩阵
*/
private static int[][] NormalDistribution(int radius, double SIGMA) {
int sum = 0, i, j;
int[][] matrix = new int[2 * radius + 1][2 * radius + 1]; // 定义一个矩阵
//计算各个点的正态分布值
for (i = 0; i <= radius; i++) {
for (j = 0; j <= radius; j++) {
//写入矩阵并累加,根据矩阵的对称性可以减少3/4的运算量
matrix[radius-i][radius-j]
= matrix[radius-i][radius+j]
= matrix[radius+i][radius-j]
= matrix[radius+i][radius+j]
= (int) (Math.pow(E, -(i * i + j * j) / (2 * SIGMA * SIGMA)) / (2 * PI * SIGMA * SIGMA) * precision);
sum += 4 * matrix[radius+i][radius+j];
}
}
//计算权值
for (i = 0; i <= 2 * radius; i++) {
for (j = 0; j <= 2 * radius; j++) {
matrix[i][j] = matrix[i][j] * precision / sum; // 所有值都除以sum,确保它们的和为“1”,由于扩大了10000倍,所以这个“1”实际上应该是10000
}
}
return matrix;
}
}

Filter类,通过调用GaussianBlur类来处理图像

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
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

public final class Filter {
public static BufferedImage GaussianBlur(String path){
int pixel;
try {
BufferedImage image = ImageIO.read(new File(path));
int width = image.getWidth(),height = image.getHeight();
int[][][] picture = new int[3][width][height];
for(int i=image.getMinX();i<width;i++){
for(int j=image.getMinY();j<height;j++){
pixel = image.getRGB(i,j);
//获取每个点的RGB值
picture[0][i][j] = (pixel & 0xff0000) >> 16;
picture[1][i][j] = (pixel & 0xff00) >> 8;
picture[2][i][j] = (pixel & 0xff);
}
}
picture = GaussianBlur.GaussianBlur(picture,100); // 快速高斯模糊
for(int i=image.getMinX();i<width;i++){
for(int j=image.getMinY();j<height;j++){
pixel = ((picture[0][i][j] & 0xff) << 16) + ((picture[1][i][j] & 0xff) << 8) + (picture[2][i][j] & 0xff);
image.setRGB(i,j,pixel);
}
}
return image;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}

Main,打开1.jpg并高斯模糊,保存为2.jpg

1
2
3
4
5
6
7
8
9
10
11
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;

public class Main {

public static void main(String[] args) throws Exception{
BufferedImage image = Filter.GaussianBlur("D://1.jpg");
ImageIO.write(image,"jpg",new File("D://2.jpg"));
}
}

效果

原图

DearXuan

高斯模糊之后的图

DearXuan
(半径20,SIGMA1.5)

评论