直方图均衡,顾名思义,是处理影像灰度在统计直方图上分布问题,是一种图像增强算法。以图像某灰度值分布概率以及低于该值得所有值得分布概率之和p与该图像所有灰度级别L的乘积作为该灰度值均衡后的值。
直方图均衡公式:
以8位灰度图像的直方图均衡为例,第一步是统计图像所有灰度值在0~255分布概率;第二步通过从低到高累加各个灰度级别概率p(i),直方均衡公式p(i)*255计算出对应值,生成查找表;第三部通过原图对应查找表生成直方均衡后图形。
统计概率分布和直方均衡计算:
private Map<Integer, Double> getPDF(BufferedImage image) {
Map<Integer, Double> map = new HashMap<>();
int width = image.getWidth();
int height = image.getHeight();
double totalPixel = 3 * width * height;
for (int i = 0; i < 256; i++) {
map.put(i, 0.0);// 通过循环,往集合里面填充0~255个位置,初始值都为0
}
//分别统计图像上0~255上分布总数
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;
map.put(r, map.get(r) + 1);
map.put(g, map.get(g) + 1);
map.put(b, map.get(b) + 1);
}
}
直方均衡计算,生成查找表:
private void getTable(Map<Integer, Double> map) {
double param = 0.0;
for(int i = 0;i < 256;i++) {
param += map.get(i);
table[i] = (int)Math.round(param * 255);
}
}
测试图像:
分布概率直方图:
直方均衡后:
直方均衡后概率分布:
从图像和概率直方图分布看,原图的灰度峰值在50左右,且大部分都分布在较小灰度值上,经过直方图均衡后,图像变亮,且灰度值在0~255上分布比较均匀。这是传统直方图均衡实现方式,但其中存在缺陷,由于中间部分灰度因为占据较小概率分布,有可能被其附近较大概率的灰度淹没。例如,灰度值为100的统计概率为50%,那么均衡后值为128,而110处概率分布为50.1%,那么均衡计算后值仍为128,原图中100~110之间灰度也就被淹没。从测试图像细节中也可以发现这个问题,如下所示:
原图部分细节:
从图像上可以看到三叉路口存在较明显的亮度差异形成的边界线。那么再看直方均衡后图像细节。
由于直方均衡的作用,部分灰度级被淹没,三叉路口路面边界线已经丢失,整个路面也完全变为白色,丢失了大量细节。目前直方图均衡算法的改进也有很多,一种是基于HSI色彩空间。由于RGB色彩空间灰度值为离散整数值,在计算过程中容易丢失精度。将图像从RGB色彩空间转换为HSI色彩空间,分离出来的亮度I值为连续变换值,在直方均衡过程中更容易保留更多细节。那么首先要将RGB转换为HSI。
RGB转HSI代码:
public Double[] rgb2HSI(int[] rgb) {
double sum = rgb[0] + rgb[1] + rgb[2];
double r = rgb[0] / sum;
double g = rgb[1] / sum;
double b = rgb[2] / sum;
double s1 = 0.5 * ((r - g) + (r - b));
double s2 = Math.pow((r - g), 2) + (r - b) * (g - b);
double s3 = s1 / Math.sqrt(s2);
double h = 0.0;
if (b <= g) {
h = Math.acos(s3);
} else if (b > g) {
h = 2 * Math.PI - Math.acos(s3);
}
double s = 1 - 3 * Math.min(r, Math.min(g, b));
double H = ((h * 180.0) / Math.PI);
double S = s * 100.0;
double I = sum / 3;
return new Double[] { H, S, I };
}
HSI转换RGB代码:
public Integer[] hsi2RGB(Double[] hsi) {
double h = (hsi[0] * Math.PI) / 180.0;
double s = hsi[1] / 100.0;
double i = hsi[2] / 255.0;
double x = i * (1 - s);
double y = i * (1 + (s * Math.cos(h)) / Math.cos(Math.PI / 3.0 - h));
double z = 3 * i - (x + y);
double r = 0, g = 0, b = 0;
if (h < ((2 * Math.PI) / 3)) {
b = x;
r = y;
g = z;
} else if (h >= ((2 * Math.PI) / 3) && h < ((4 * Math.PI) / 3)) {
h = h - ((2 * Math.PI) / 3.0);
x = i * (1 - s);
y = i * (1 + (s * Math.cos(h)) / Math.cos(Math.PI / 3.0 - h));
z = 3 * i - (x + y);
r = x;
g = y;
b = z;
} else if (h >= ((4 * Math.PI) / 3) && h < ((2 * Math.PI))) {
h = h - ((4 * Math.PI) / 3.0);
x = i * (1 - s);
y = i * (1 + (s * Math.cos(h)) / Math.cos(Math.PI / 3.0 - h));
z = 3 * i - (x + y);
g = x;
b = y;
r = z;
}
int red = (int) Math.round((r * 255));
int green = (int) Math.round((g * 255));
int blue = (int) Math.round((b * 255));
return new Integer[] { red, green, blue};
}
尽管HSI色彩空间可以解决直方均衡中灰度级丢失问题,但HSI与RGB色彩空间的转换需要耗费大量计算,导致图像处理过程比较缓慢。下面就介绍一下博主自己想到的一种解决办法。
虽然使用传统直方均衡容易丢失部分灰度级,但仍然可以采用传统方式进行直方均衡计算,将直方均衡计算后图像与原图进行均值融合,便可以恢复部分丢失的灰度值。尽管该方法不能完全恢复丢失的灰度级别,但相对于传统直方均衡,已经有了很大改善。处理流程为:对原图分别进行直方均衡和自动色阶,将计算后的图像进行均值融合,得到最终图像。
效果图:
相对于传统直方均衡,该方法计算后图像稍暗,但可以保留更多细节。再看细节部分。
从放大后的细节上看,三叉路口处的边界线仍然清晰可辨。路面也没有完全变为白色。
实际上,编程实现还可以对算法进一步优化,由于自动色阶算法同样需要概率分布计算,那么直方图均衡和自动色阶的概率分布计算可以一起做!然后根据概率分布分别计算直方图均衡和自动色阶查找表,之后取出原图每个像素,利用查找表计算直方图均衡和自动色阶对应的值,对两个值求均值即可。
实现代码:
public BufferedImage histogramEqualization(BufferedImage image) {
BufferedImage resultImage = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
Map<Integer, Double> map = getPDF(image);
getTable(map); //初始化直方图均衡查找表
// 自动色阶前裁切掉最暗和最亮部分占比较少的值
int min = getMin(map, 0.0001);
int max = getMax(map, 0.0001);
int[] arr = new int[] { min, max };
getContrastByMaxMin(arr); // 初始化自动色阶查找表
int width = image.getWidth();
int height = image.getHeight();
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
int rgb = image.getRGB(i, j);
int R = (rgb >> 16) & 0xff;
int G = (rgb >> 8) & 0xff;
int B = rgb & 0xff;
double hisR = histogramtTable[R];
double hisG = histogramtTable[G];
double hisB = histogramtTable[B];
double contrastR = autoContrastTable[R];
double contrastG = autoContrastTable[G];
double contrastB = autoContrastTable[B];
R = (int) Math.round((hisR + contrastR) / 2);
G = (int) Math.round((hisG + contrastG) / 2);
B = (int) Math.round((hisB + contrastB) / 2);
rgb = (255 & 0xff) << 24 | (R & 0xff) << 16 | (G & 0xff) << 8 | (B & 0xff);
resultImage.setRGB(i, j, rgb);
}
}
return resultImage;
}