【译】用Java生成字符画

来自:Ascii art generator in Java
github源码地址

ASCII码字符画艺术是一种利用ASCII码标准中的可打印字符来产生视觉艺术效果的技术,它的存在在历史上是有意义的,当时的打印机还无法打印图片,而且当时在邮件中嵌入图像还无法实现,所以它也用于邮件中。在本文中,我将为你呈现一个用Java实现的、可以配置字体和对比度的ASCII码字符画生成器程序。因为这个程序是我在周末用几个小时搞定的,还不完美,但这是一个有意思的实验,在下面你可以看到实现代码,我将解释它的工作原理。

算法

算法的思路很简单。首先,我们将程序中要用到的每一个字符转化成一张图片,并缓存它。然后,我们遍历原始图像,对于每个字符大小的图片块,找出最佳匹配的字符。为了实现这一点,我们首先对原始图像做一些预处理:我们先将图像转化为灰度图,然后让其通过一个阈值滤波器,这样我们就得到一个黑白色的图像,我们可以将其与每个字符对比并计算差值。接着,对每个图片块选取最相似的字符,一直进行下去,直到整个图像都转换完成。此外,我们还可以根据需要调整阈值大小来调整对比度,增强最终的效果。
为了实现这一点,一个非常简单的方法是将红、绿、蓝的值都设置成三种颜色的平均值:
红=绿=蓝=(红+绿+蓝)/3
如果这个值低于阈值,我们就将它设置成白色,否则我们将其设置成黑色。最后,我们以像素为单位将图像与每个字符进行比较并计算出平均误差。如下面的图片和代码片段所示。

eiffel

int r1 = (charPixel >> 16) & 0xFF;
int g1 = (charPixel >> 8) & 0xFF;
int b1 = charPixel & 0xFF;

int r2 = (sourcePixel >> 16) & 0xFF;
int g2 = (sourcePixel >> 8) & 0xFF;
int b2 = sourcePixel & 0xFF;

int thresholded = (r2 + g2 + b2) / 3 < THRESHOLD ? 0 : 255;

error = Math.sqrt((r1 - thresholded) * (r1 - thresholded) + 
    (g1 - thresholded) * (g1 - thresholded) + (b1 - thresholded) * (b1 - thresholded));

因为颜色是存储在单个整数中,所以我们首先提取单个颜色成分并执行上面我解释过的计算,另一个挑战是准确地测量字符尺寸,并以它们为中心作图。在试验了多种方法之后,我最终发现这个比较好的方法:

Rectangle rect = new TextLayout(Character.toString((char) i), fm.getFont(), 
    fm.getFontRenderContext()).getOutline(null).getBounds();

g.drawString(character, 0, (int) (rect.getHeight() - rect.getMaxY()));

你可以在Github上下载完整的源代码。
下面是一些使用不同字体尺寸和阈值的例子:

part1_9pic


Part 2

由于上一篇博客谈到的ASCII码字符画生成器(在Github上查看源码)收到了很多反馈,我决定继续这个项目,如果大家很喜欢的话我再增加几个feature。我重新设计了程序的主要部分,让它更具扩展性,易于采用不同的算法,生成不同的输出等等。在这一部分,我会展示这个项目的新的架构,让你可以更容易地集成到自己的代码中,按照你的需要扩展它。

架构:

architecture

AsciiImgCache

在渲染ASCII码字符之前,实例化这个类是必要的。它将字体和字符数组作为参数,为每个字母生成图片,如果你不想麻烦,代码中有默认的字符数组。
假如你好奇:

private static final char[] defaultCharacters = 
    "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. "

例子:

// use only '/' '\' and ' '
AsciiImgCache mediumBlackAndWhiteCache = AsciiImgCache.
    create(new Font("Courier", Font.BOLD, 10), new char[] {'\\', ' ', '/'});

// use default list
AsciiImgCache largeFontCache = AsciiImgCache.
    create(new Font("Courier",Font.PLAIN, 16));

BestCharacterFitStrategy

这个类是用来确定原图片与每个字符有多接近的算法的抽象。它有一个方法:

float calculateError(final GrayscaleMatrix character, final GrayscaleMatrix tile);

这个方法比较两张图片,返回一个浮点型的误差。每个字母都将与图片比较,最小误差的那个将被选中,返回。目前这个类中有两个可用的方法:ColorSquareErrorFitStrategy和 StructuralSimilarityFitStrategy。

ColorSquareErrorFitStrategy

这个很容易理解,它比较每一个像素点,计算每个灰度的均方差,用数学的语言来说就是:
$MSE=\frac{1}{n} \sum\limits_{1}\limitsn(C_i-T_i)2$
n是像素点的个数,C和T分别是字符和分割的图片。

StructuralSimilarityFitStrategy

图像相似性指标(The structural similarity (SSIM) index algorithm)要求重现人类的感知,它的目标是提高类似与MSE的传统算法。我不会给出关于它机理的更多细节,如果你有兴趣,你可以在Wikipedia上面了解更多。我实验了一下,貌似实现了一个更优的版本。

AsciiConverter

这是算法的核心,它包括了所有分割原图片、匹配最佳字符的逻辑的实现。但是,它不包含输出ASCII字符画--它需要子类的实现。目前有两种实现:AsciiToImageConverter 和 AsciiToStringConverter,你可能猜到了,图片是用字符串输出产生的。

使用示例:

既然talk is cheap,我就展示一下产生ASCII码图片的大致流程:

// initialize cache
AsciiImgCache cache = AsciiImgCache.create(new Font("Courier",Font.BOLD, 6));

// load image
BufferedImage portraitImage = ImageIO.read(new File("image.png"));

// initialize converters
AsciiToImageConverter imageConverter = 
    new AsciiToImageConverter(cache, new ColorSquareErrorFitStrategy());
AsciiToStringConverter stringConverter = 
    new AsciiToStringConverter(cache, new StructuralSimilarityFitStrategy());

// image output
ImageIO.write(imageConverter.convertImage(portraitImage), "png", 
    new File("ascii_art.png"));
// string converter, output to console
System.out.println(stringConverter.convertImage(portraitImage));

这有一些根据不同参数产生的实例图像:
原始图像

原始图像

16磅字体,MSE

16磅字体,MSE

16磅字体,SSIM

16磅字体,SSIM

3字符10磅字体,MSE

3字符16磅字体,MSE

3字符10磅字体,SSIM

3字符16磅字体,SSIM

6磅字体,MSE

6磅字体,MSE

6磅字体,SSIM

6磅字体,SSIM

进一步的工作

现在脑袋里有一些想法:

  • 搜索并尝试更多的图像比较的算法
  • 预处理图像,获得更好的结果(提高对比度,检测边缘等等)
  • 并行地进行图像处理,提高性能,尝试一下看看是否需要
  • 增加更多的转换结果(如html文件的输出)
  • 增加多种颜色字符的输出
  • 增加测试单元

如果你想改善代码,或者发现了代码的bug,在博客里评论或者到Github来贡献代码吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,259评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,977评论 4 60
  • 小姨乳腺癌,经过几年的抗争,据院长说,可能要画上句号了。 表弟十岁那年,小姨独自前往深圳,追寻心中...
    秦岭小山风阅读 382评论 0 0
  • 昨天来到芜湖,陪老婆。 随着雨季的结束,正式宣告炎热夏季的来临。今天一早就醒来,也许是天气的原因,抑或是生物钟随着...
    plutoese阅读 466评论 0 51
  • 近来易怒!可能夜班上的太勤,又或是人尽皆知的生活琐事磨破了我的耐心!能给我做调味剂的就是和身边的人聊天,聊聊他们的...
    管好你的嘴小鬼阅读 137评论 0 0