前几天遇到一个风格化图片转换的需求,效果像这样:
像这样,需要用纯色圆形填充图像,形成风格化的图片样式。
实现原理
整体原理还是比较简单的,有点类似与马赛克的处理方式。
假设图片宽 w 像素,高 h 像素,需要使用半径为 r 的圆填充好,那么只需要:
- 先把图片划分成边长为 2*r 的栅格;
- 选取每个栅格中心点的像素颜色,作为这个栅格要填充的颜色;
- 新建画布,对每个栅格绘制实心圆形,颜色是上一步计算的结果。
代码分解
读取图片
import cv2 # pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple pip install opencv-python==4.5.5.62
img = cv2.imread(target_file)
print(f'Read image, got width={img.shape[1]}, height={img.shape[0]}')
新建画布
dest = numpy.zeros((img.shape[0], img.shape[1], 3), dtype=numpy.uint8)
这个比较简单,就是创建一个三维数组,最低维有三个元素代表颜色(BGR),第二维是行,最高维是列。
划分并遍历栅格
for i in range(0, len(img), circle_radius * 2):
# 数组每一行代表图形中的一行,从左上角开始(i相当于y值)
for j in range(0, len(img[i]), circle_radius * 2):
# 数组每一列代表图形中的一列,从左上角开始(j相当于x值)
center = (j + circle_radius, i + circle_radius) # x, y
这里有个比较坑的点,在使用数组遍历所有像素的时候,第一层循环从从上到下遍历图片的每一行,第二层循环从左到右遍历该行的像素。
但是啊但是,CV 中的点坐标以左上角为原点,右方向为 x 轴正方向,下方向为 y 轴正方向。注意看这个 center,它的点表示要和数组访问恰好相反。
获取颜色并在新画布绘制圆形
point_color_arr = img[min(center[1], len(img)) - 1][min(center[0], len(img[i])) - 1]
point_color = (int(point_color_arr[0]), int(point_color_arr[1]), int(point_color_arr[2])) # BGR
cv2.circle(dest, center, circle_radius, point_color, -1)
取中点处的颜色,即是我们要的颜色。注意这里访问的时候是 img[center[1]][center[0]]
,就是因为前面说的,点坐标与数组下标恰好相反。
优化:取栅格中全部颜色的平均值
我们还可以顺带实现一下类似于马赛克的那种,每个格子颜色取平均值,这样能使格子之间的颜色过渡更自然。
n_points = 0
point_color = [0, 0, 0]
for ii in range(i, min(i + circle_radius * 2, len(img)), 1):
for jj in range(j, min(j + circle_radius * 2, len(img[i])), 1):
n_points += 1
point_color += img[ii][jj]
point_color = tuple([int(x / n_points) for x in point_color])
这里唯一要注意的点就是,直接从 img 读出来的是数组,我们需要转换成 int tuple 才能用。
写入文件或显示到屏幕
显示到屏幕可以这样:
cv2.namedWindow("image")
cv2.imshow('image', dest)
cv2.waitKey(100000) # 显示 10000 ms 即 10s 后消失
cv2.destroyAllWindows()
要把图片保存到文件可以这样:
dest_file = 'dest-test-29-t.png'
cv2.imwrite(dest_file, dest)
完整源代码
circle_radius = 29 # in px
target_file = 'test.png'
dest_file = 'dest-test-29-t.png'
use_average = True
save_file = True
import cv2 # opencv默认读取格式是BGR
import numpy
import tqdm
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
img = cv2.imread(target_file)
print(f'Read image, got width={img.shape[1]}, height={img.shape[0]}')
dest = numpy.zeros((img.shape[0], img.shape[1], 3), dtype=numpy.uint8)
for i in tqdm.tqdm(range(0, len(img), circle_radius * 2), "Processing: "):
# 数组每一行代表图形中的一行,从左上角开始(i相当于y值)
for j in range(0, len(img[i]), circle_radius * 2):
# 数组每一列代表图形中的一列,从左上角开始(j相当于x值)
center = (j + circle_radius, i + circle_radius) # x, y
# calculate color for this area, get average
if use_average:
n_points = 0
point_color = [0, 0, 0]
for ii in range(i, min(i + circle_radius * 2, len(img)), 1):
for jj in range(j, min(j + circle_radius * 2, len(img[i])), 1):
n_points += 1
point_color += img[ii][jj]
point_color = tuple([int(x / n_points) for x in point_color])
else:
point_color_arr = img[min(center[1], len(img)) - 1][min(center[0], len(img[i])) - 1]
point_color = (int(point_color_arr[0]), int(point_color_arr[1]), int(point_color_arr[2])) # BGR
cv2.circle(dest, center, circle_radius, point_color, -1)
if save_file:
cv2.imwrite(dest_file, dest)
else:
cv2.namedWindow("image")
cv2.imshow('image', dest)
cv2.waitKey(100000) # 显示 10000 ms 即 10s 后消失
cv2.destroyAllWindows()
程序效果
原图:
使用中心点作为颜色来源:
使用颜色平均值: