参考:
引文
Photoshop 的图像黑白调整功能,是通过对红、黄、绿、青、蓝和洋红等6种颜色的比例调节来完成的。这个是一直都知道,还经常使用的。但是不清楚它是怎么计算的。
后来,从博文
中找到Photoshop 图像黑白调整功能的计算公式:
gray= (max - mid) * ratio_max + (mid - min) * ratio_max_mid + min
但是,用不了啊用不了,不是java版的,而我最终要用到Android中。当时为了赶时间,直接用了平均值算法,那效果实在不给力,于是又换了系统自带的过滤器,效果也一般。现在闲暇了,于是,将这个算法用java实现一下。
正文
黑白调节计算公式解说
ps通过对红、黄、绿、青、蓝和洋红等6种颜色的比例来黑白化图片:
gray= (max - mid) * ratio_max + (mid - min) * ratio_max_mid + min
gray :像素的灰度值,不是真的灰色哈。
max : 像素点R、G、B三色中的最大者
mid:像素点R、G、B三色中的中间者
min:像素点R、G、B三色中的最小者
ratio_max:最大的颜色所占比率
ratio_max_mid:最大的颜色和中间颜色所占的比率
这六种颜色在ps中默认比例为:
redRadio = 40%;
yellowRadio = 60%;
greenRadio = 40%;
cyanRadio = 60%;
blueRadio = 20%;
magentaRadio =80%;
为了验证这一点,我打开ps:
可以看到,的确如此
并且这个数值是可以调整的,在ps中可以通过调节这几个数值,达到理想的去色效果。
java中的实现效果
奋战了许久,我用java来实现了这个算法,其效果如下。
原图:
java黑白化后:
Photoshop黑白化后:
很像,简直一模一样了,连大小都一样了。不过,大小自然也可能不一样,因为ps直接保存会存储一些图片元数据,同时底层的处理可能也有所不同。
实现代码
废话不多说,以下是初次的实现代码:
封装工具类
package date1114.图片;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.imageio.ImageIO;
public class ImageUtil {
//这个方法就拿来保存,测试效果一下
public static void save(BufferedImage image,String path) {
try {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYYMMddHHmmssSSS");
String imageFormat;
int type = image.getType();
switch (type) {
case BufferedImage.TYPE_4BYTE_ABGR:
case BufferedImage.TYPE_4BYTE_ABGR_PRE:
imageFormat = "png";
break;
case BufferedImage.TYPE_INT_ARGB:
case BufferedImage.TYPE_INT_ARGB_PRE:
imageFormat = "bmp";
break;
default:
imageFormat = "jpg";
break;
}
String date = simpleDateFormat.format(new Date());
ImageIO.write(image,imageFormat , new File(path+"_"+date+"."+imageFormat));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Photoshop 黑白算法,默认效果
* @param image
* @return 新的黑白化图片
*/
public static BufferedImage createBlackWhiteImage(BufferedImage image) {
return createBlackWhiteImage(image, null);
}
/**
* Photoshop 黑白算法,默认效果
* @param image
* @radios 颜色通道配置,依次为红、黄、 绿、 青、 蓝、紫六个通道
* @return 新的黑白化图片
*/
public static BufferedImage createBlackWhiteImage(BufferedImage image,float[] radios) {
int width = image.getWidth(); //获取位图的宽
int height = image.getHeight(); //获取位图的高
BufferedImage result = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
int alpha = 0xff000000;
int r = 0;
int g = 0;
int b = 0;
int max = 0;
int min = 0;
int mid = 0;
int gray = 0;
float radioMax = 0;
float radioMaxMid = 0;
if (radios == null) {
// 红 黄 绿 青 蓝 紫
radios = new float[]{0.4f,0.6f,0.4f,0.6f,0.2f,0.8f};
}
for (int i = 0; i < width; i++) {//一列列扫描
for (int j = 0; j < height; j++) {
gray = image.getRGB(i, j);
alpha = gray >>> 24;
r = (gray>>16) & 0x000000ff;
g = (gray >> 8) & 0x000000ff;
b = gray & 0x000000ff;
if (r >= g && r>=b) {
max = r;
radioMax = radios[0];
}
if (g>= r && g>=b) {
max = g;
radioMax = radios[2];
}
if (b >= r && b>=g) {
max = b;
radioMax = radios[4];
}
if (r<=g && r<=b) { // g+ b = cyan 青色
min = r;
radioMaxMid = radios[3];
}
if (b <= r && b<=g) {//r+g = yellow 黄色
min = b;
radioMaxMid = radios[1];
}
if (g <= r && g<=b) {//r+b = m 洋红
min = g;
radioMaxMid = radios[5];
}
mid = r + g + b-max -min;
// 公式:gray= (max - mid) * ratio_max + (mid - min) * ratio_max_mid + min
gray = (int) ((max - mid) * radioMax + (mid - min) * radioMaxMid + min);
gray = (alpha << 24) | (gray << 16) | (gray << 8) | gray;
result.setRGB(i, j, gray);
}
}
return result;
}
}
调用和测试:
package date1114.图片;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import javax.imageio.ImageIO;
public class Test {
public static void main(String[] args) {
String url = "https://s2.51cto.com/images/blog/202310/08194705_652296b9380d9701.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=";
try {
BufferedImage colorImage = ImageIO.read(new URL(url));
ImageUtil.save(colorImage, "color");
BufferedImage grayImage = ImageUtil.createBlackWhiteImage(colorImage);
ImageUtil.save(grayImage, "gray");
} catch (IOException e) {
e.printStackTrace();
}
}
}
进行处理,这个http地址其实就是本文彩虹色环的url地址。
性能优化
隔了两天,发现上述的代码似乎有优化的空间。
我在本地磁盘选取了一张4096*4096的大图进行黑白化,多次进行测试,处理时间在2800
毫秒上下浮动。
于是考虑是否因为代码在循环体中使用bufferImage进行了颜色的修改从而导致性能降低,也就是怀疑循环体中的这段代码:
for(){
for(){
//...
result.setRGB(i, j, gray);//BufferImage设置像素点颜色
//...
那么,转换为像素数组来进行计算,效率会不会更高一些?
从流中读取所有像素点:
int[] pixels = image.getRGB(0, 0, width,height, null, 0, width);
在这时候还是挺忐忑的,因为看这个读取像素的方法,其实现可能用了循环(印象中操作流时,总是循环的),搞不好性能又低下了。
得到像素数组后,逐行或逐列扫描将每一个像素点变灰。于是将result.setRGB(i, j, gray);//BufferImage
替换为:
//result.setRGB(i, j, gray);
pixels[j*width+i] = gray;
最后,循环结束,将像素数组转换为BufferImage流:
result.setRGB(0, 0, width, height, pixels, 0, width);
运行,咦,耗时降低了,在2600毫秒上下。提升不明显啊,或许从流中读取像素点也耗了一些时间?(为什么这样想?在多次循环中,优化性能可以从不做复杂的操作,避免创建多个对象,声明多个引用等方面来考虑,于是对象有了克隆,循环体中的变量可以在循环外部声明等。而这里,多次从流这个复杂对象读取东西,感觉不妙,不知道它底层是怎么读的,或许就有循环,耗时操作。况且,都拿到像素数组,直接从数组中读取色彩就o了)
改:
// gray = image.getRGB(i, j);
gray = pixels[j*width+i];
一运行,咦,这回居然只要1600毫秒,比未优化前少了整整1.2秒钟。
几乎算大功告成了吧。感觉是的。
以下是优化后的所有代码:
package date1114.Photoshop图片黑白化算法;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.imageio.ImageIO;
public class ImageUtil {
//这个方法就拿来保存,测试效果一下
public static void save(BufferedImage image,String path) {
try {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYYMMddHHmmssSSS");
String imageFormat;
int type = image.getType();
switch (type) {
case BufferedImage.TYPE_4BYTE_ABGR:
case BufferedImage.TYPE_4BYTE_ABGR_PRE:
imageFormat = "png";
break;
case BufferedImage.TYPE_INT_ARGB:
case BufferedImage.TYPE_INT_ARGB_PRE:
imageFormat = "bmp";
break;
default:
imageFormat = "jpg";
break;
}
String date = simpleDateFormat.format(new Date());
ImageIO.write(image,imageFormat , new File(path+"_"+date+"."+imageFormat));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Photoshop 黑白算法,默认效果
* @param image
* @return 新的黑白化图片
*/
public static BufferedImage createBlackWhiteImage(BufferedImage image) {
return createBlackWhiteImage(image, null);
}
/**
* Photoshop 黑白算法,默认效果
* @param image
* @radios 颜色通道配置,依次为红、黄、 绿、 青、 蓝、紫六个通道
* @return 新的黑白化图片
*/
public static BufferedImage createBlackWhiteImage(BufferedImage image,float[] radios) {
int width = image.getWidth(); //获取位图的宽
int height = image.getHeight(); //获取位图的高
int alpha = 0;
int r = 0;
int g = 0;
int b = 0;
int max = 0;
int min = 0;
int mid = 0;
int gray = 0;
float radioMax = 0;
float radioMaxMid = 0;
if (radios == null) {
// 红 黄 绿 青 蓝 紫
radios = new float[]{0.4f,0.6f,0.4f,0.6f,0.2f,0.8f};
}
//int[] pixels = new int[width*height];
int[] pixels = image.getRGB(0, 0, width,height, null, 0, width);
// BufferedImage result = new BufferedImage(width, height, image.getType());
for (int i = 0; i < width; i++) {//一列列扫描
for (int j = 0; j < height; j++) {
// gray = image.getRGB(i, j);
gray = pixels[j*width+i];
alpha = gray >>> 24;
r = (gray>>16) & 0x000000ff;
g = (gray >> 8) & 0x000000ff;
b = gray & 0x000000ff;
if (r >= g && r>=b) {
max = r;
radioMax = radios[0];
}
if (g>= r && g>=b) {
max = g;
radioMax = radios[2];
}
if (b >= r && b>=g) {
max = b;
radioMax = radios[4];
}
if (r<=g && r<=b) { // g+ b = cyan 青色
min = r;
radioMaxMid = radios[3];
}
if (b <= r && b<=g) {//r+g = yellow 黄色
min = b;
radioMaxMid = radios[1];
}
if (g <= r && g<=b) {//r+b = m 洋红
min = g;
radioMaxMid = radios[5];
}
mid = r + g + b-max -min;
// 公式:gray= (max - mid) * ratio_max + (mid - min) * ratio_max_mid + min
gray = (int) ((max - mid) * radioMax + (mid - min) * radioMaxMid + min);
gray = (alpha << 24) | (gray << 16) | (gray << 8) | gray;
// result.setRGB(i, j, gray);
pixels[j*width+i] = gray;
}
}
BufferedImage result = new BufferedImage(width, height, image.getType());
result.setRGB(0, 0, width, height, pixels, 0, width);
return result;
}
}
测试代码:
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public class Test {
public static void main(String[] args) {
String url = "D:/abc/a.jpg";
try {
BufferedImage colorImage = ImageIO.read(new File(url));
ImageUtil.save(colorImage, "color");
long time = System.currentTimeMillis();
BufferedImage grayImage = ImageUtil.createBlackWhiteImage(colorImage);
System.out.println(System.currentTimeMillis()-time);
ImageUtil.save(grayImage, "gray");
} catch (IOException e) {
e.printStackTrace();
}
}
}
ps:
虽然优化完成,但是鉴于安卓bitmap.getPixel() 与 bitmap.setPixel() 导致特别耗时的现象,我有些些怀疑bufferedImage.getRGB()与bufferedImage.getRGB() 也可能存在一定程度上的过度耗时现象,或许不如直接使用像素数组来的快,这个就留待以后研究了。
安卓和pc端比较,安卓完败。同样的图片,同样的4096*4096像素,类似的实现过程,安卓却用了20秒!喷血了——这是在未优化之前的结果。
——end