美文网首页
【译】用Java生成字符画

【译】用Java生成字符画

作者: knarfeh | 来源:发表于2016-04-16 21:17 被阅读1457次

    来自: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来贡献代码吧。

    相关文章

      网友评论

          本文标题:【译】用Java生成字符画

          本文链接:https://www.haomeiwen.com/subject/oskolttx.html