在图像处理软件Photoshop中有一个自动对比度功能,可以一键调节图像亮度、对比度。比如,像下面这张由于曝光不足形成的非常暗的图像,只有图像中间比较亮一些的路面才能看到,其他地面上的物体几乎无法看见。
那么再来看一下使用Photoshop自动对比度之后效果。
一键操作后,图像上的大部分物体都体现出来了,调整后的亮度、对比度、饱和度等几乎不会给人失真的感觉,效果比手动调节亮度、对比度、或者曲线调整要好很多。实际上,这个功能的牛逼之处不止如此,再看另一张图。
这是一张受到雾霾干扰的图像,整体感觉就是非常朦胧,看不清地面物体。用自动对比度看看效果:
去雾效果杠杠的,完全就像是没有雾霾的天气拍出来的。 那么如此牛逼的功能是怎么实现的呢?其实原理还是比较简单,一般图像由RGB三个通道合成,每个通道由0~255数值来体现,如果是比较暗的图像,其RGB通道数值可能在比较低的范围内,比如第一张非常暗的图像,其RGB三个通道数值范围为(0~60),即最亮的地方才60的亮度值,离最大亮度255还差的远。
有了图像的各个通道数据,就可以在此基础上做些文章了,比如,可以直接将(0~60)映射到(0~255)。使用java编写代码测试一下,用ImageIO读取和写入图像,用bufferedImage对象获取图像RGB信息,然后对各个通道值进行处理。
代码如下:
// 自动对比度
public void autoContrast(BufferedImage image) {
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);
double r = (rgb >> 16) & 0xff;
double g = (rgb >> 8) & 0xff;
double b = rgb & 0xff;
//进行转换
r = getContrastByMaxMin(r);
g = getContrastByMaxMin(g);
b = getContrastByMaxMin(b);
rgb = (255 & 0xff) << 24 | (clamp((int)r) & 0xff) << 16 | (clamp((int)g) & 0xff) << 8 | (clamp((int)b) & 0xff);
image.setRGB(i, j, rgb);
}
}
}
//根据亮度最大最小值拉伸对比度
private double getContrastByMaxMin(double a) {
if (a >= arr[0] && a <= arr[1]) {
result = (a - arr[0]) * 255 / (arr[1] - arr[0]);
}
return result;
}
运行效果如下:
图像确实变亮了!但是仔细看看,效果似乎比Photoshop差一些,这是怎么回事?
其实,虽然获取到了各个通道的动态范围(0~60),但这只是最大最小值,而大部分数值范围可能在(10~50)之间,那么在进行拉伸的时候,很多值并没有拉伸到合适的数值上。那么Photoshop是怎么处理的呢?很简单!就是对上下范围进行裁切,Photoshop自动对比度是裁切掉上下范围各0.001%的像素,再得到需要进行映射的范围,对范围以外的数值直接映射到0或255。当然,要获得准确的裁切值,需要对各个通道值进行统计,即统计图像各个通道在(0~255)上分布概率。
代码如下:
// 自动对比度
public void autoContrast(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
//计算图像强度分布频率
Map<Integer,Double> map = getPDF(image);
//获取最大最小强度
int min = getMin(map);
int max = getMax(map);
int[] arr = new int[] {min,max};
//进行对比度拉伸
for(int i = 0; i < width; i++) {
for(int j = 0; j < height; j++) {
int rgb = image.getRGB(i, j);
double r = (rgb >> 16) & 0xff;
double g = (rgb >> 8) & 0xff;
double b = rgb & 0xff;
//进行转换
r = getContrastByMaxMin(r, arr);
g = getContrastByMaxMin(g, arr);
b = getContrastByMaxMin(b, arr);
rgb = (255 & 0xff) << 24 | (clamp((int)r) & 0xff) << 16 | (clamp((int)g) & 0xff) << 8 | (clamp((int)b) & 0xff);
image.setRGB(i, j, rgb);
}
}
}
//取出最小强度
private int getMin(Map<Integer, Double> map){
double temp = 0.0;
int result = 0;
for(int i = 0; i < 256; i++) {
double num = map.get(i);
temp += num;
if (temp >= 0.001) {
result = i;
break;
}
}
return result;
}
//取出最大强度
private int getMax(Map<Integer, Double> map){
double temp = 0.0;
int result = 0;
for(int i = 255; i >= 0; i--) {
double num = map.get(i);
temp += num;
if (temp >= 0.001) {
result = i;
break;
}
}
return result;
}
//根据亮度最大最小值拉伸对比度
private double getContrastByMaxMin(double a, int[] arr) {
double result = 0.0;
if (arr[0] == arr[1]) {
result = arr[0];
}
if (a > arr[1]) {
result = 255;
}
if (a < arr[0]) {
result = 0;
}
if (a >= arr[0] && a <= arr[1]) {
result = (a - arr[0]) * 255 / (arr[1] - arr[0]);
}
return result;
}
/**
* 统计图像从0~255之间分布
*
* @return
*/
private Map<Integer, Double> getPDF(BufferedImage image) {
Map<Integer, Double> map = new HashMap<>();
int width = image.getWidth();
int height = image.getHeight();
double totalPixel = width * height;
for (int i = 0; i < 256; i++) {
map.put(i, 0.0);// 通过循环,往集合里面填充0~255个位置,初始值都为0
}
//分别统计图像上0~255上分布总数
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;
map.put(r, map.get(r) + 1);
map.put(g, map.get(g) + 1);
map.put(b, map.get(b) + 1);
}
}
//计算分布概率
for(int i = 0; i < 256; i++) {
double value = map.get(i);
value = value / (3 * totalPixel);
map.put(i, value);
}
return map;
}
// 判断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;
}
再看效果:
是不是跟Photoshop一毛一样!