前言
不知道大家是否看到过这样的视频,说是有人用女朋友的名字或者1314画出女朋友的照片。
哈哈,不知道后续怎么样?但是他们绘画技艺真的很高啊。那么我们如果用python能不能实现呢?尽管不能像手绘一样那么灵动,也可以实现大致的效果,这就是静态图片转字符画。
思路梳理
本质上,我们所要做的任务就是将原图片的像素点信息用字符来替换。如何替换可以达到表示图像信息的效果呢?在图片灰度化的过程中,亮度低的区域用0表示,亮度高的区域用1表示。受此启发,我们可以在亮度低的地方多放置字符,亮度高的地方少放置字符,从而获得类似于图片灰度的效果。
那么我们如何确定哪些地方的像素是一类的呢?一种办法是直方图,把图像看作是一张拼图,按照像素值的高低划分为不同的区域,再将不同区域的拼图块放入瓶子中。但是这种方法太固化了,因为事先我们是不知道原图像素值分布区域的,所以划分区域的范围是无从把握的,如果我们是等分的话,那么一张偏亮图像可能就全部划分成一类了。
既然是分类问题,那么就可以考虑k-means算法了。
这里是k-means算法的介绍:
因此,我们的处理办法就是先将一张图片灰度化,然后进行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)
运行展示:
放大后可以观察到轮廓为‘-’,内容部分就是数字