经典去雾算法-何凯明09年提出暗通道先验去雾(Single Image Haze Removal Using Dark Channel Prior)
暗通道去雾公式:I(x) = f(x)*t(x) + (1 – t(x))*A
I(x)为待去雾图像,f(x)为去雾后图像,t(x)为透射率(0,1),A为大气光成分。
根据公式,去雾算法可解释为:有雾时,相机获取到的图像为两部分组成,一部分为被拍摄物体发射光线穿过雾霾后的光线,另一部分为大气光被雾霾反射后的光线。被拍摄物体发射光线f(x)通过雾霾,雾霾透射率为t(x),那么被摄物体光线到达相机后值为 f(x)*t(x)。原始大气光值为A,大气光的方向可看做与被拍摄物体光线完全相反,大气光一部分穿过雾霾,一部分被雾霾反射,被反射后的光线值即为A - A*t(x))。
整个去雾流程如下图:
有了算法模型,接下来对各个部分进行细化实现,首先计算透射率。暗通道先验法指出,根据大量图像统计,无雾图像RGB通道总有一个通道值趋近于0,也就是说在该像素上透射率t(x)趋近于1,f(x)与I(x)几乎相等。而有雾图像,由于雾霾反射的大气光干扰,RGB通道上的最低值,也就是被反射后的大气光值,通过这个值即可计算出该像素上的透射率,当然实际情况下需要考虑其他因素造成的干扰,一般情况下需要对原始图像通过RGB最低值计算的灰度图进行最低值滤波,根据某个像素相邻范围的最低值计算透射率。
整个计算透射率过程分为三个步骤:第一步为原始图像通过RGB最低值转灰度图;第二步对灰度图进行最低通道滤波,滤波器半径可选,取滤波器内最低值作为中心像素值;第三部对低通滤波后的灰度图进行高斯低通滤波,主要作用为平滑图像,让边缘过度平缓。
透射率计算流程如下:
计算大气光成分值则比较简单,大气光成分A可以看做一个独立的RGB像素,包含三个值。一般是对原始图像RGB通道统计0~255概率分布,分别从各个通道最大值开始取占比大于万分之一的值作为A中对应值。
完成透射率和大气光成分计算后,接下来就是根据公式计算去雾图像。实际上还需要计算透射率图像,透射率图像通过高斯低通滤波后图像进行反转得到,在实际应用开发上,这一步可以省略,直接通过滤波后灰度图实际上可以看做雾霾反射率图T(x),那么透射率即t(x)=1-T(x)。去雾处理后的图像一般会比较暗,可以通过自动对比度或自动色阶进行优化。
基于暗通道的去雾对于浓度不均匀的雾霾去雾效果比较好,但计算透射率过程由于最低值滤波和高斯低通滤波随着卷积核增大,消耗计算量也越大,处理单张影像还好,如果用于批量处理影像,显然不太实用。当然在实际应用中,可以考虑减少滤波核半径,牺牲一些效果,提升处理速度。把最低值通滤波和高斯低通滤波核半径都设置为1进行测试。
测试图像:
去雾效果图:
附上代码,java版的实现:
package tools;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
public class AutoHazeRemoval {
//自动去雾
public BufferedImage hazeRemoval(BufferedImage image) {
BufferedImage tempImage = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
BufferedImage grayImage = new ImageGray().transferGrayImageByLowest(image);
grayImage = new LowestFilter().lowestFilter(grayImage, 1);
grayImage = new GaussianBlur().gaussBlur(grayImage, 1, 1);
int[] A = extractA(image);
for (int i = 0; i < image.getWidth(); i++) {
for (int j = 0; j < image.getHeight(); j++) {
int rgb = image.getRGB(i, j);
double R = (rgb >> 16) & 0xff;
double G = (rgb >> 8) & 0xff;
double B = rgb & 0xff;
//计算透射率
int transmissionValue = grayImage.getRGB(i, j) & 0xff;
double transmission = 0.95 * (double)transmissionValue / 255;
R = (R - A[0] * transmission) / (1 - transmission);
G = (G - A[1] * transmission) / (1 - transmission);
B = (B - A[2] * transmission) / (1 - transmission);
rgb = (255 & 0xff) << 24 | (clamp((int)R) & 0xff) << 16
| (clamp((int)G) & 0xff) << 8 | (clamp((int)B) & 0xff);
tempImage.setRGB(i, j, rgb);
}
}
return tempImage;
}
/**
* 通过RGB在0~255频率分布计算全局大气光成分
*
* @param image
* @return
*/
public int[] extractA(BufferedImage image) {
List<Integer[]> list = generateBinary(image);
int[] result = new int[3];
result[0] = getValue(image,list.get(0));
result[1] = getValue(image,list.get(1));
result[2] = getValue(image,list.get(2));
return result;
}
private int getValue(BufferedImage image,Integer[] list) {
double temp = 0.0;
int result = 0;
for(int i = 255; i > 0; i--) {
double num = list[i] / (double)(image.getWidth()*image.getHeight());
temp += num;
if (temp >= 0.0001) {
result = i;
break;
}
}
return result;
}
/**
* 图像二值化 计算图像RGB值从0~255之间分布
*
* @return
*/
public List<Integer[]> generateBinary(BufferedImage image) {
List<Integer[]> list = new ArrayList<>();
Integer[] rlist = new Integer[256];
Integer[] glist = new Integer[256];
Integer[] blist = new Integer[256];
// 通过循环,往集合里面填充0~255个位置,初始值都为0
for (int i = 0; i < 256; i++) {
rlist[i] = 0;
glist[i] = 0;
blist[i] = 0;
}
for (int i = 0; i < image.getWidth(); i++) {
for (int j = 0; j < image.getHeight(); j++) {
int rgb = image.getRGB(i, j);
int r = (rgb >> 16) & 0xff;
int g = (rgb >> 8) & 0xff;
int b = rgb & 0xff;
rlist[r] = rlist[r] + 1;
glist[g] = rlist[g] + 1;
blist[b] = rlist[b] + 1;
}
}
list.add(rlist);
list.add(glist);
list.add(blist);
return list;
}
// 判断a,r,g,b值,大于256返回256,小于0则返回0,0到256之间则直接返回原始值
private int clamp(int rgb) {
if (rgb > 255)
return 255;
if (rgb < 0)
return 0;
return rgb;
}
}