不一样的方法(不用wordcloud库),使用python自己实现词云

我们经常在网上看到很炫酷的词云,有没有想过自己来实现生成一个词云呢?

   

看到词云时的所想所思?

当第一眼看到词云时,基于程序员的条件反射,一个大大的「How」字浮现在眼前:它是如何实现的?

通过仔细的观察,又发现一些现象:词语无重叠,而且词语之间的空隙被更小的词语所填满,并且以有趣的姿势交错在一起。

考虑了一会儿,我知道我又一次进入了未知领域,以我所知的方法,似乎没有一个能够轻易解决这个问题。

问题的关键所在?

首先,在图片上绘制文字对于Python来说肯定不是难事,

同样的,设置文字为不同颜色或不同大小也应该不是难事。

文字的位置、颜色、大小的随机也很简单,random 库就能实现。

那就只有一个关键的问题了,要如何做到词语之间紧密排布但又没有重叠呢?

 

初级试探?

假设先在一张白色的图片 A 上放一个黑色的词语 A,又在另一张同样大小的白色的图片 B 上放一个词语 B,把这两张图片进行对比,如果重叠,就把词语 B 换一个地方,重新对比;不重叠时,记下这个位置,词语 B 放到到图片 A 上……

图片是由一个一个像素组成的,比如一张 800 × 600 的图片,说明它是由 800 × 600 = 48万 个像素组成。每个像素中保存了一个颜色值,通过对比图片 A 和 B 的每一个像素,如果相同位置上,A 和 B 的像素都为黑色,那就意味这两个词语有重叠。

虽然还有一些不清楚的地方,不过这确实是一个可行的方案。

 

开始?

我们知道 python 有一个 wordcloud 库可以生成词云。

那要开始介绍 wordcloud 库吗?


是也不是。我更关心的是 wordcloud 库如何解决词语之间紧密排布但又没有重叠这一关键问题?

 

 

Wordcloud 探秘

从 Github 上可以轻易的下载到该库的代码。从入口函数开始,粗略的追踪了几个函数的调用,有几个地方引起了我的注意:

  1. 它使用了 Pillow 库,搜索发现这是一个 python 图像处理库。
  2. 它在一个 for word, freq in frequencies 循环中,将单词逐个放到图片上。
  3. 这个循环中,除了对字体进行设置外,还调用了一个 sample_position 方法来寻找放置的位置(没错,这就是问题的关键所在)。
  4. sample_position 很简单,它的实现在 query_integral_image.pyx 文件中,仅有 30 多行,它从图片左上角第一个像素开始,遍历所有像素,检查各个位置能否放置。integral_image指的是积分图像。

 

图像处理一角?

积分图?听起来似乎很复杂,但理解起来却特别简单。首先,积分图不是一张真实的图片,接下来……

我们还是来看图吧,像素值即该像素点有图则为1,否则为0:

如果点击最左边的图中的某个像素位置,就可以绘制图形。

积分图中,每个单元的值,等于原图此位置左上角所有像素值之和(橙框的值 = 蓝框中所有值之和)。

这个性质,能够帮助我们快速判断一个区域内有没有内容。为什么这么说呢?

 

快速重叠检测?

如果一个矩形区域内没有内容,说明这个区域内所有像素值之和为零。

根据积分图的特征,下图中,我们可以进行如下计算:

用大矩形(绿色)所有像素值之和,减去该大矩形中上方(青色)和左侧(紫色)两个矩形像素值之和(同时包括左上角的橙色的小矩形),所以再加上左上角小矩形(橙色)像素值之和,就得到了所求蓝色区域内像素值之和。

大矩形(绿色): 6
左侧矩形(紫色): 0
上方矩形(青色): 1
左上矩形(橙色): 0
目标矩形(蓝色): 6 - 0 - 1 + 0 = 5

 

这样,只需要进行 四次取值一次运算 就能够判断某区域是否为空,比逐个像素检测快很多。

由于每个词语都能够被框在一个矩形中(宽度为 w,高度为 h),我们只需要对图片每个位置 (x,y) 进行计算,如果 (x,y) 到 (x + w - 1, y + h - 1) 这个矩形区域内没有内容,就能够放置这个词语。

至此,我们已经掌握了词云的第一个关键点:重叠检测方法。

 

 

词云布局方法?

我们已经能够判断新放置的词语是否和其它词语重叠,接下来的问题就是选择一种策略来放置新词语。

wordcloud 库使用了一个非常简单的随机布局方法:

  1. 从左上角第一个像素开始,判断在这个位置放置新词语会不会与已放置的内容重叠;
  2. 如果不重叠,将这个位置添加一个列表 list 中;
  3. 遍历图片所有像素,将所有可放置的位置都加到 list 中;
  4. 从 list 中随机选择一个位置放置这个词语。

这样的方法推翻了我们之前的观测,wordcloud 并没有做任何工作来保证词语之间紧密排布。但这并不困难,只要我们换一种方法:

  1. 选择一个初始位置 P;
  2. 对每一个单词,从位置 P 开始,稍微移动一点点距离,看能不能放下,如果不能,再稍微移动一点距离,直到能够放下。

不错,就是简单的贪心策略。

 

试着这样实现?

import random 
import math

import numpy as np
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont


def find_position(img,size_x,size_y):
    #返回给定轴上的累积和numpy.cumsum(a,axis = None,dtype = None,out = None )
    #0为纵轴,1为横轴
    #assarray()创建ndarray数组对象
    integral = np.cumsum(np.cumsum(np.asarray(img),axis=1),axis=0)
    for x in range(1,800-size_x):
        for y in range(1,600-size_y):
            #左上角小矩形+大矩形(矩阵行对应y,列对应x)
            area = integral[y-1,x-1] + integral[y+size_y-1,x+size_x -1]
            #减去左侧矩形和上方矩形
            area -= integral[y-1,x+size_x-1] + integral[y+size_y-1,x-1]
            #area为0,返回(x,y)
            if not area:
                return x,y
    #返回为空
    return None,None

#循环将词语绘制到图像上
def main():
    #词语素材
    words = ['作者','高度','浓缩','中国','西北','农村','历史',\
        '变迁','过程','作品','思想','艺术','高度','统一','主人公','面对','困境','艰苦','奋斗','精神']
    #新建画布对象,Image.new(mode,size,color=None)
    img = Image.new('L',(800,600))
    #新建画布绘画对象
    draw = ImageDraw.Draw(img)
    for word in words:
        #字体大小随机
        font_size = random.randint(50,150)
        #字体为黑体
        font = ImageFont.truetype('C:\Windows\Fonts\SIMHEI.TTF', font_size)

        #计算文字矩形框的大小(宽x,高y)
        box_size = draw.textsize(word,font=font)

        #图像上找出文字放置的位置
        x,y = find_position(img,box_size[0],box_size[1])

        #存在x,则将词语放置在(x,y)处
        if x:
            draw.text((x,y),word,fill="white",font=font)
    img.show()

if __name__ == '__main__':
    main()

至此,我们已经理解 wordcloud 生成词云的基本原理。

 

明显的缺点?

当词语数量增多时,运行代码,久久未能得出结果。我意识到生成词云是一个需要极大计算量的工作。

wordcloud 使用了很多使代码变得难以理解的优化策略,并且将 find_position 函数转用 Cython 实现(转换为 C 语言)也在很大程度上说明了这个问题。

速度问题暂且不提,毕竟生成词云不是我的日常工作。但是通过实现上面的算法,我还发现另一个问题:矩形检测的积分图算法似乎不能很好的支持文字的旋转

这件事很好理解,如果将一个单词旋转一定角度,那么它的外接矩形面积必然会比不旋转时候大,这就需要更大的矩形区域来放置这个词语,导致很多实际可以放置的位置却不能放置。

那么,有没有更好的策略呢?

 

更好的策略——四叉树

在 Wordcloud 算法中,每放置完一个词语,就需要重新计算一次积分图,下一个词语需要与整张图片进行重叠检测。

那有没有可能将每个词视为单独的实体,在放置新词时,检测它与其它每一个词语有没有重叠?

显然应该有。

有一种叫做 层次边界框(Hierarchical bounding boxes)的方法来快速实现两个词语间的重叠检测。

层次边界框这个词太拗口,我们换用「四叉树」来代指这种结构,它本质上也是一棵记录空间信息的四叉树。

四叉树的构建并不困难,将图片横纵各切一刀,平均分割为「左上、左下、右上、右下」四个区域,如果某个区域中有内容(此时可以用积分图算法判断),那么继续将这个区域分割为四个部分,直到区域的大小小于某个值。

四叉树每深一层,对形状的描述就越精确,每一次分隔,都能排除一些空白矩形区域,剩下的有像素的区域,都记录到了树中。

    

    

四叉树每深一层,对形状的描述就越精确,每一次分隔,都能排除一些空白矩形区域,剩下的有像素的区域,都记录到了树中。

后续内容等待更新

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛定谔的猫96

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值