@TOC
背景
用Java实现字符拼成一个图片,先看一下效果:
- 左边是原图,右边是用字符拼成的效果,即写好代码,读入一张图片,然后用指定的字符把这个图片的轮廓拼出来 放大之后是这个样子
- 用love拼成了哆啦A梦的轮廓
源码
public class AscPic {
public static void main(String[] args) throws IOException {
String path = "d:/qqq.png";//导入的图片
String base = "love";//将会用这个字符串里的字符填充图片
BufferedImage image = ImageIO.read(new File(path));//读入图片,并用图片缓冲区对象来接收
//双层for循环,遍历图片
for (int y = 0; y < image.getHeight(); y++) {//先竖向遍历,再横向遍历,即一行一行的找,后面也会一行一行的打印
for (int x = 0; x < image.getWidth(); x++) {
int color = image.getRGB(x, y);//图片缓冲区自带的方法,可以得到当前点的颜色值,返回值是int类型
int r=(color>>16)&0xff;
int g=(color>>8)&0xff;
int b=color&0xff;
float gray = 0.299f * r + 0.578f * g + 0.114f * b;//灰度值计算公式,固定比例,无需理解
int index = Math.round(gray * (base.length()) / 255);
if(index>=base.length()) {
System.out.print(" ");//白色的地方打空格,相当于白色背景,这样图片轮廓比较明显
}else {
System.out.print(base.charAt(index));//有颜色的地方打字符
}
}
System.out.println();//一行打完,换行
}
}
}
代码思路
整体思路为导入想好处理的图片,遍历,得到每个像素点的颜色,然后将其转换成灰度值(也就是把彩色转换成黑白),根据得到的灰度值计算字符串索引,达到效果就是不同颜色可以对应不同的下标,以此来匹配字符串中的字符,最后按照原有的坐标点把这些字符打印出来即可
- 定义好想要填充的字符串
- 导入想要处理的图片,需要用到BufferedImage(图片缓冲区)这个对象
- 遍历整张图片,这里需要注意,外层循环遍历y轴,内层遍历x轴,因为打印的时候需要一行一行打印,打完一行要换行
- 根据getRGB(x,y)方法,传入当前的坐标点,得到当前点的颜色
- 从得到的颜色中单独拆分出r,g,b的值
- 根据得到的rgb计算对应的灰度值
- 根据灰度值计算索引
- 打印
难点讲解
如何得到rgb
int color = image.getRGB(x, y);//图片缓冲区自带的方法,可以得到当前点的颜色值,返回值是int类型
int r=(color>>16)&0xff;
int g=(color>>8)&0xff;
int b=color&0xff;
首先明确一点,所谓的rgb就是三原色,red,green,blue,无论是ps还是程序,都会通过给rgb赋值来拼成一个新的颜色。通过etRGB(x,y)得到的颜色是一个int类型,我们用color来表示。这个值包含四部分内容,分别为a,r,g,b,即透明度,red,green,blue。每一部分恰好占一个字节。所以我们要做的就是从这个int中去单独得到从左数第二,第三,第四字节的数值。 怎么做呢?先来回顾两个位运算的基本知识:
- "&",与,都是0则结果为0,都是1结果为1,一个1一个0结果为0.从数学上理解,&操作符其实是在取交集。 7&5=? 首先换算成二进制,7的二进制是0111,5的二进制是0101 0111&0101=0101,还是5。 通过上面的计算有没有发现一个规律,如果我想要让一个数和另一个数&完结果还是这个数本身怎么办?比如我想让0101,0111,0011和一个数&完结果还是他们本身,那么这个数应该是多少。
- ">>",右移. 8>>2,表示8向右平移两位,结果为2. 8的二进制1000,右移两位,0010.
现在想想,怎么样通过上面两个符号从一个int中得到某一个字节的数值,比如得到第二字节的值,也就是r的值。
假装你们想了20分钟。
我们用二进制来看,假设我们得到的color换算成二进制是: 01111100 01011010 10001101 00111110 我们要得到从左数第二个字节的值,怎么办? 首先,把这个数向右平移两个字节,也就是16位,那么就是color>>16,结果为 00000000 00000000 01111100 01011010 此时,再和11111111与一下,是不是就得到这个数本身了。 00000000 00000000 01111100 01011010 & 00000000 00000000 00000000 11111111
= 00000000 00000000 00000000 01011010 现在得到的就是r的值。写法:(color>>16)& 0xff. 0x表示16进制,16进制的ff表示的就是二进制的8个1。 以此类推,得到g就是(color>>8)& 0xff,得到b就是color & 0xff。 注:这里注意,方法不唯一,也可以先与后右移。这种方法是用位运算符来做的,当然也能转换成二进制数组然后拆分。那么为什么要用位运算符来做呢,第一是方便,第二就是很有逼格啊老铁。
如何让不同颜色匹配不同字符
int index = Math.round(gray * (base.length()) / 255);
gray是我们求出的灰度值,它的最大值也是255。不同颜色得到的gray不一样,同理,gray/255得到的就是不同颜色对应的一个比例,用这个比例乘以字符串长度就完成了不同颜色或者说不同颜色段匹配了不同的字符下标。因此得到gray * (base.length()) / 255,然后通过Math.round()方法四舍五入取整。 这里需要注意一下, 这样打印出来的字符图片色调会比较深,为了让颜色区分更明显,我们可以让一些接近于白色的浅色都打印成空格,那么我们就写成
int index = Math.round(gray * (base.length()+1) / 255);
把字符串长度加一,然后再做乘除操作,这样会让一部分颜色对应的索引大于字符串长度。 我们打印的时候是这样判断的,如果index大于等于字符串长度就打印空格,如果不是才打印字符。
为什么我的图片只能打一半
eclipse的控制台输出的行数有限,超过规定行数就覆盖了。这时可以把y++,改成y+=2,相当于每隔两个点取一下颜色并打印,也就是把原图纵向上缩小了一半。为了保持原图的比例,同理也可以把x做相应调整。
OutOfMemoryError错误
内存溢出,图片太大了,换个小一点的图片
为什么用汉字来打印图案会扭曲
字母或字符占得大小比汉字要小,汉字所占的空间很大,如果按照原有的点来打印汉字,因为汉字的宽度几乎比字母大一倍,所以会被挤出去,造成了扭曲。解决方法:改变y和x的自增数,也就是把x++改成+2,+3这样的,横向上每隔两个点打印一下,给汉字预留出空间
装逼时刻
找你女神的照片来打印,字符可以用“love”或者“我爱你” 放大 互联网+时代,表白当然也要用技术手段表白,程序员不是不解风情,我们的浪漫,与众不同。