静态图片转字符画

前言

不知道大家是否看到过这样的视频,说是有人用女朋友的名字或者1314画出女朋友的照片。

哈哈,不知道后续怎么样?但是他们绘画技艺真的很高啊。那么我们如果用python能不能实现呢?尽管不能像手绘一样那么灵动,也可以实现大致的效果,这就是静态图片转字符画。

思路梳理

本质上,我们所要做的任务就是将原图片的像素点信息用字符来替换。如何替换可以达到表示图像信息的效果呢?在图片灰度化的过程中,亮度低的区域用0表示,亮度高的区域用1表示。受此启发,我们可以在亮度低的地方多放置字符,亮度高的地方少放置字符,从而获得类似于图片灰度的效果。

那么我们如何确定哪些地方的像素是一类的呢?一种办法是直方图,把图像看作是一张拼图,按照像素值的高低划分为不同的区域,再将不同区域的拼图块放入瓶子中。但是这种方法太固化了,因为事先我们是不知道原图像素值分布区域的,所以划分区域的范围是无从把握的,如果我们是等分的话,那么一张偏亮图像可能就全部划分成一类了。

既然是分类问题,那么就可以考虑k-means算法了。

这里是k-means算法的介绍:

K-means算法-CSDN博客

因此,我们的处理办法就是先将一张图片灰度化,然后进行k-means分类。将分类结果的质心按照明暗程度排序,然后对于原图像像素都归依为其所在簇的质心,然后按照事先设置的明暗区域放置字符,最后输出结果。

实现过程

1.转numpy数组

    if type(frame) != np.ndarray:
        frame = np.array(frame)
    height, width, *_ = frame.shape  
    frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)#转为灰度图
    frame_array = np.float32(frame_gray.reshape(-1))#将多维数组转为一维

将图像转为numpy数组的形式,然后化为灰度图,并将多维数组转为一维。

2.初始化k-means参数

criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)

定义K-Means算法的终止条件。

第一个参数cv2.TERM_CRITERIA_EPS+cv2.TERM_CRITERIA_MAX_ITER:表示终止条件由两部分组成,当满足以下任一条件时,聚类算法将停止:
1.    cv2.TERM_CRITERIA_EPS:算法在数据集中点的移动小于一个很小的值(这里设置的为1.0),表示聚类结果已经足够稳定。
2.    cv2.TERM_CRITERIA_MAX_ITER:算法达到预设的最大迭代次数,这里是10次。

 flags = cv2.KMEANS_RANDOM_CENTERS

定义K-Means算法初始质心(聚类中心)的选择方式,这里是随机选择的方式。

3.执行k-means聚类

compactness, labels, centroids = cv2.kmeans(frame_array, K, None, criteria, 10, flags)

执行K-Means聚类操作K表示要分成的聚类数目。

None这个参数是bestLabels的初始标签数组,设置为None表示让函数自动生成初始聚类中心。

10这是attempts参数,表示在flags指定的初始化方式下,算法尝试找到最佳聚类中心的次数。

执行后返回三个值:

compactness:表示所有点到其最近质心的距离之和。反映聚类效果的好坏,该值越低表示聚类效果越好。

labels:包含了每个数据点所属的聚类标签的数组。标签从0开始,表示数据点属于哪个质心。

centroids:包含了每个聚类的质心坐标的数组。质心是其对应聚类中所有点的中心点,用于代表该聚类。

接下来对centroids进行处理。

因为经过k-means聚类后中心点的坐标是浮点类型的,所以首先转为整形:

centroids = np.uint8(centroids)

然后一维化

centroids = centroids.flatten()

而k-means聚类后求得的中心是随机的,将其排序后存起来:

centroids_sorted = sorted(centroids)

然后获得不同centroids 的明暗程度,0 为最暗

centroids_index = np.array([centroids_sorted.index(value) for value in centroids])

这是一个列表推导式,用于找到centroids每个质心在centroids_sorted中的索引,centroids_index表示centroids中每个质心排序的位置。

举个例子来了解上面的三个步骤:

假如质心是这样的:

那么排序后是:

最后是求得索引:

4.设置明暗区间

    bright = [abs((3 * i - 2 * K) / (3 * K)) for i in range(1, 1 + K)]
    bright_bound = bright.index(np.min(bright))
    shadow = [abs((3 * i - K) / (3 * K)) for i in range(1, 1 + K)]
    shadow_bound = shadow.index(np.min(shadow))

这样能够较为均衡的将K个类别分为三个区域0~shadow_bound为暗部区域,shadow_bound~bright_bound为过渡区域,bright_bound~K为明亮区域。而在图像中,亮度变化大的地方一般就是轮廓。所以在暗部我们可以放置数字字符,过渡区域可以放置'-'用来表示轮廓,明亮区域就不用放置字符了。

5.处理标签信息

首先一维化

labels = labels.flatten()

将 labels 转变为实际的明暗程度列表

labels = centroids_index[labels]

解析列表

labels_picked = [labels[rows * width:(rows + 1) * width:2] for rows in range(0, height, 2)]

本质上是隔一行,同时在一行中也隔一列,这是因为我们放置的字符比一个像素要大。

6.进行绘制

    canvas.fill(255)
    y = 8
    for rows in labels_picked:
        x = 0
        for cols in rows:
            if cols <= shadow_bound:
                cv2.putText(canvas, str(random.randint(2, 9)),
                            (x, y), cv2.FONT_HERSHEY_PLAIN, 0.45, 1)
            elif cols <= bright_bound:
                cv2.putText(canvas, "-", (x, y),
                            cv2.FONT_HERSHEY_PLAIN, 0.4, 0, 1)
            x += 6
        y += 6

扩大了3倍,且之前隔一行隔一列,所以x,y每次移动6个像素。

完整代码:

import cv2
import random
import numpy as np

def img2strimg(frame, K=5):   
    if type(frame) != np.ndarray:
        frame = np.array(frame)
    height, width, *_ = frame.shape  
    frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    frame_array = np.float32(frame_gray.reshape(-1))
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
    flags = cv2.KMEANS_RANDOM_CENTERS
    # 得到 labels(类别)、centroids(矩心)
    compactness, labels, centroids = cv2.kmeans(frame_array, K, None, criteria, 10, flags)
    centroids = np.uint8(centroids)
    # labels 的数个矩心以随机顺序排列,所以需要简单处理矩心
    centroids = centroids.flatten()
    centroids_sorted = sorted(centroids)
    # 获得不同 centroids 的明暗程度,0 为最暗
    centroids_index = np.array([centroids_sorted.index(value) for value in centroids])
    bright = [abs((3 * i - 2 * K) / (3 * K)) for i in range(1, 1 + K)]
    bright_bound = bright.index(np.min(bright))
    shadow = [abs((3 * i - K) / (3 * K)) for i in range(1, 1 + K)]
    shadow_bound = shadow.index(np.min(shadow))
    labels = labels.flatten()
    # 将 labels 转变为实际的明暗程度列表
    labels = centroids_index[labels]
    # 解析列表
    labels_picked = [labels[rows * width:(rows + 1) * width:2] for rows in range(0, height, 2)]
    canvas = np.zeros((3 * height, 3 * width, 3), np.uint8)
	# 创建长宽为原图三倍的白色画布
    canvas.fill(255)
    y = 8
    for rows in labels_picked:
        x = 0
        for cols in rows:
            if cols <= shadow_bound:
                cv2.putText(canvas, str(random.randint(2, 9)),
                            (x, y), cv2.FONT_HERSHEY_PLAIN, 0.45, 1)
            elif cols <= bright_bound:
                cv2.putText(canvas, "-", (x, y),
                            cv2.FONT_HERSHEY_PLAIN, 0.4, 0, 1)
            x += 6
        y += 6
    return canvas

if __name__ == '__main__':
    fp = r"static/static3.jpg"
    img = cv2.imread(fp)
    str_img = img2strimg(img)
    cv2.imwrite("static/static3_ascii.jpg", str_img)

运行展示:

 放大后可以观察到轮廓为‘-’,内容部分就是数字

  • 16
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值