一、介绍
字符画是一种纯由字符组成,在文本编辑器中行列排开的二维字符矩阵中,整体展示出可识别的图案。特点是无色,画面的最小单元是字符而非像素。可由基本的文本编辑器模拟图像。
字符画的展示:
蜡笔小心表情转换为字符画后的效果
二、原理
图片文件的本质是像素矩阵,每个点储存的有该点的色彩信息。计算机在处理图片时将每个点解析为不同的颜色,并显示在屏幕上构成了视觉上的图片。
单个像素点的颜色由三原色RGB三个变量决定。若RGB皆为0,即十六进制下的#000000,没有颜色填充的点,该点颜色为纯黑。RGB皆为255,即十六进制下的#FFFFFF,填充满三原色,混合为白色。
灰度是表明像素点明暗程度的数值,也即黑白图像中点的颜色深度,范围在0到255,纯白为255 ,纯黑为0。灰度值指的是单个像素点的亮度。灰度值越大表示越亮。
字符画的实现是将源图片的RGB矩阵转换为灰度矩阵,再根据灰度的明暗程度选择不同密度的字符替换掉像素点,并打印该字符矩阵,即可得到字符画。 越亮的点,其占位字符密度越低,越暗的点,其占位字符密度高,即可在宏观程度上体现整幅图片的明暗变化。
像素点由明到暗,相对于的字符集由稀疏到密集,定义拥有该特性的字符集。
这里提供一种有效的字符集:
char[] ss = " `.^,:~\"<!ct+{i7?u30pw4A8DX%#HWM".toCharArray();
该字符集从0下标开始,随着下标的递增,字符像素的密度也逐渐增大,模拟出了明暗的渐变效果,因此可作为灰度的映射。
三、实现
Java中用于处理图片的类是Image类,Image是一个抽象类,其实现类BufferedImage可以将图片加载到内存当中,其所表示的图片在内存中拥有一个缓冲区,因此可以很方便的操作图片。并且该实现类也提供了绘图相关的api。
本程序只需要获取图片的宽高属性,以及各像素点属性。
我们需要将字符通过IO流输出到指定文本文件内,之后打开该文件即可查看。因此采用字符缓冲流进行文件操作。
BufferedWriter bw = new BufferedWriter(new FileWriter(target));
其中 target 为目标文本文件的路径,可填绝对路径,或相对路径(当前工作区开始)
定义BufferedImage并加载指定图片。
BufferedImage bi = ImageIO.read(Files.newInputStream(Paths.get(ImageSource)));
int width = bi.getWidth();
int height = bi.getHeight();
其中 ImageSource 为图片的文件路径。如:“D:\\picture\\1.jpg”。 加载完后调用BufferedImage对象的成员获取图片的宽高属性width 和 height。
对于单像素点的操作有
int pixel = bi.getRGB(i, j);
int[] rgb = new int[3]; //分别表示红绿蓝RGB。
rgb[0] = pixel >> 16 & 0xff;
rgb[1] = pixel >> 8 & 0xff;
rgb[2] = pixel & 0xff;
调用对象的 getRGB(int x,int y) 方法可以得到一个像素点色值的十六进制数返回值。若要分离RGB3个变量,可以通过位运算的方式,每次取其中的两位。
获取到一个像素点的RGB属性后,计算该点的灰度值。灰度值的计算一般有5种方法,如下:
1.浮点法:Gray=R*0.3+G*0.59+B*0.11
2.整数法:Gray=(R*30+G*59+B*11)/100
3.移位法:Gray =(R*77+G*151+B*28)>>8;
4.平均值法:Gray=(R+G+B)/3;
5.仅取绿色:Gray=G;
上述5种方法均有一定程度上的误差,在本程序中为了追求运算速度,采用移位计算以加快处理。即:
int Gray = (rgb[0] * 28 + rgb[1] * 151 + rgb[2] * 77) >> 8; //计算像素点的灰度
int x = Gray * ss.length / 255 % ss.length; //通过灰度的百分比 计算出相应密度的字符 在字符集中的位置。
通过以上计算,即可得该像素点会被替换为字符 ss[x] ,将替换后的字符打印,即实现了画出一个像素的的功能。
便利图片的所有像素点,在矩阵中打印转换后的字符。
for (int j = 0; j < height; j += 2) {
for (int i = 0; i < width; i += 2) {
int pixel = bi.getRGB(i, j);
int[] rgb = new int[3];
rgb[0] = pixel >> 16 & 0xff;
rgb[1] = pixel >> 8 & 0xff;
rgb[2] = pixel & 0xff;
int Gray = (rgb[0] * 28 + rgb[1] * 151 + rgb[2] * 77) >> 8; //通过三原色值计算像素点的灰度
int x = Gray * ss.length / 255 % ss.length; //灰度值的百分比 计算出相应密度的字符表
bw.write(ss[x]); //输出该字符
bw.write(ss[x]);
}
bw.newLine();
}
bw.flush();
通过改变循环的控制变量的自增数值,可起到控制打印后文本文件长度也即“模拟像素”的大小的作用。
由于字符集中的字符为半角字符,在显示时高度总是为宽度的二倍,因此每个像素打印两倍的字符即可平衡宽高比。
测试代码,成功输出文本文件,可正常显示字符画。
四、扩展
由图片转换为字符画可联想到,是否可由动画转换为字符动画。
为了便于操作,这里采用的动画载体是gif文件。
导入jar包或引入坐标:
<dependency>
<groupId>com.madgag</groupId>
<artifactId>animated-gif-lib</artifactId>
<version>1.4</version>
</dependency>
在源代码中导入包, 定义 GifDecoder 对象,并加载指定的gif文件。
import com.madgag.gif.fmsware.GifDecoder;
GifDecoder gd = new GifDecoder();
gd.read(Files.newInputStream(new File(gifSource).toPath()));
参数 gifSource 即gif资源文件的地址。 将之前描述的图片转字符画的步骤封装为方法 imageToChar 方便调用。
通过GifDecoder对象的getFrameCount()方法获取帧数,getFrame(int x) 方法定位到第x帧图片。对每一帧图片进行转换处理。
for (int i = 0; i < gd.getFrameCount(); i++) { //逐帧转换为字符集。
BufferedImage frame = gd.getFrame(i);
imageToChar(frame,"out.txt"); //参数提供缓冲图片对象,以及目标文本输出地址。
Thread.sleep(50);
}
测试运行。
记事本打开的文本文件无法实时更新修改,查看运行效果建议使用NotePad++,或vscode。图片的大小建议在360p以内。若图片太大可通过修改像素坐标循环变量的自增值,来控制转换后的“像素”。
以下为动漫一拳超人中 主角琦玉的招式——认真一拳,所转换的字符动画展示。
本程序的代码实现思路较为简单明了,网上关于字符画的教程很多,笔者以较为擅长的java语言进行了一次修缮和总结。看到这读者可以点个赞支持下呀。