高斯模糊是被广泛使用的图形算法之一,在实现高斯模糊之前,先要了解正态分布
正态分布
一维的正态分布为
直接让f(x)和f(y)相乘,就得到了二维的正态分布
此处直接令μ=0,将会在下面解释。
权值矩阵
设有一个(2n+1)阶矩阵M,且有,我们称这个矩阵为权值矩阵,称为(i,j)点处的权。其中n是高斯模糊的半径。
离散卷积
离散卷积是卷积对离散量的特殊形式,假设现有原图矩阵A,权值矩阵B,则点(x,y)处的离散卷积为
在更严格的定义中,A(i,j)应该与B(u-i,v-j)相乘,但是针对本文的高斯模糊而言,其效果是一样的,且上面给出的公式更为简洁。
现在举个例子,有一张尺寸为3*3的图片S,将其颜色转化成矩阵A,为
有权值矩阵B为
将A(i,j)与B(i,j)相乘,将结果相加
(-1) * 2 + (-1) * 4 + (-1) * 6 + (-1) * 8 + 5 * 5 = 5,则以上两个矩阵的离散卷积结果为5,这就是矩阵A经过处理后得到的新矩阵M(2,2)的值。
在高斯模糊中,设模糊半径为n,则定义一个维数为2n+1的权值矩阵G,且G(i,j)=f(i-n-1,j-n-1),类似于将一个直角坐标系放在了G的中点处,这就是μ=0的原因。此处的f是二维正态分布函数。然后求和,将矩阵的每个数除以这个和。求和的步骤是防止图片过亮或过暗。
将得到的矩阵G代替B计算,其结果就是高斯模糊的结果
优化
上述方法的效率较低,在介绍正态分布时,二维的正态分布函数是两个一维函数相乘得到的,这两个一维函数分别是f(x)和f(y),f(x)代表水平方向,f(y)代表垂直方向。对于一个n维权值矩阵,用它来处理a*b尺寸的图片,如果用二维正态分布函数来计算,总共需要计算a*b*n*n=abn²次,及其繁琐。这时我们可以使用一维的正态分布函数,得出一个“权值列向量”,这个向量的作用类似权值矩阵,用这个列向量把图片横向处理,相当于f(x),再用它把图片纵向处理,相当于f(y),此时图片经过两次处理,相当于f(x)*f(y),也可以达到二维正态分布的效果,而计算量仅仅是a*b*n+a*b*n=2abn,下降了一个数量级。该方法不详细介绍,将在代码中展示。
代码实现
GaussianBlur类,算法的核心部分
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类来处理图像
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
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"));
}
}
效果
原图
高斯模糊之后的图(半径20,SIGMA1.5)