以前写过一篇关于字符化的文章,当然用的算法比较老,虽然性能很好(就纯Python实现而言),但确实效果不算特别好,简单说一下思路,就翻篇吧。

老方法

当我们考虑一个图片,用字符将每个像素块替换为一个字符,那么首先会想到的,是根据每个字符产生的平均的亮度效应,与待替换像素块的平均亮度作映射。通过生成缩略图,比如我们需要一个50×50字符数量的图,那我们就将原图缩略成50×50像素,然后将每个字符缩略成1个像素,接着我们根据亮度,映射一下就好。因为只是一个常量的映射,因此性能相对来说挺不错的。

问题

性能好当然是有代价的。最大的问题在于细节的缺失,不管原来的像素块是有一个什么样的模式在里面,缩略之后什么都没有了。

新思路

那既然只考虑平均的亮度效应不行,那我们就逐个像素比较,找出最相近的字符,那不就好了嘛。

方法论

首先,字符化之后,字符显然是黑色的,或者准确地说,是前景色。因此就有个前景色选择的预处理问题,图片中哪种颜色是前景色,这就必须要人工介入选择。当然通常情况下把黑色作为前景色处理,是不会有错的。
那么接着需要考虑的是,逐个像素比较出最相近的字符要怎么比较,或者规范的说,如何定义像素块之间的距离。相对来说,比较直观的想法就是把整个包含N个像素点的像素块拉成一个N维向量,然后比较向量间的距离,比如余弦距离等等。
最后,从方法论上讲比较简单,不过就是渲染出单个字符,然后逐个比较嘛,但工程上的实现,确实有些十分讨厌的地方,下面会接着谈。

实现

我个人的一个实现已经放在Github上了。
链接在此👉https://github.com/jsjyhzy/charalize👈
代码是有一点乱,但是因为比较短,所以也就不多做解释,应该能无文档看懂代码吧。(希望)

距离函数

一般来讲向量间的距离用闵科夫斯基距离就可以了,所谓闵科夫斯基距离就是两个向量各维上的标量差的绝对值的p次方之和的p次方根,当p取1时退化为曼哈顿距离,p取2时退化为欧几里得距离,在我的实现中取了p=2,也就是欧几里得距离。关于欧几里得距离与曼哈顿距离的效果如下图所示:

另外值得注意的是,向量间的距离,通常还去考虑余弦距离度量,但在这个向量化方法中(彩色图先转化为灰度图,然后flatten二维数组为N维向量)这么做会有一个feature(绝对不是bug噢😎)。就是通过余弦距离度量生成的字符,会非常有效地捕捉到原画的线条,或者叫像素变化剧烈的部分,而色块则会非常星际地忽视。如下图所示:

这个区别就比较大(余弦距离 左 欧几里得距离 右)

究其原因,在于色块的像素块在我这个向量化方法中,会变成全是x的N维向量。对于余弦距离而言,全是x的N维向量和全是y的N维向量,其实是一回事。但对于线条而言,哪怕只有几个像素的变化,其距离变化相较闵科夫斯基之流会更加敏感,比如对辫子处的处理上:

左 余弦距离 中 原画 右 欧几里得距离

当然可以考虑综合余弦距离和闵科夫斯基距离做成一个集成学习,但就两个和而不同的算法可能还是有点….稀薄🤣。等以后能想到其他算法了,应该就可以搞一搞集成学习。

字符与字体

目前这个新思路,虽说新,但和原来的算法一样,也只支持等宽字体,也就是说一定是横纵分割的块。因此,首先要用等宽字体。但是就算用了等宽字体,也要视所使用的字符。哪怕字体告诉你“我是等宽字体哟😃”,实际上也有可能不等宽。这就涉及到字符的半角与全角之分。如果只看ASCII字符的话,那毫无疑问没有什么半角全角,但如果纳入,比如说日文平假名,那通常可以看到2个ASCII字符才跟一个平假名等宽的景象。
因此我们需要将ASCII字符变成全角的,全角Latin字符在U+FF00到U+FF65,所以只要把半角Latin字符序号加上65248就可以了,全角空格可以直接用U+3000,或者Python3里用char(12288)来表示

加入对话

1条评论

留下评论