边缘提取---图片转线稿
一种提取图片边缘的算法
# 起源
突发奇想利用边缘提取做了一个图片转线稿的小程序
先来看一下效果:
# 环境
Python 3.8 、PIL、numpy
直接使用 pip install 安装即可
pip install pillow | |
pip install numpy |
# 思路
众所周知,我们所看到的图片是一副二维图片,由一堆 RGB 色值各不相同的像素点构成。
那么我们可以将各个像素点的 RGB 色值转变为图片的第三维度,这样图片就由二维平面图变为了三维立体图。
此时我们将这张图片看作一个实实在在的三维物体,三维物体表面某点高度由对应二维平面的像素点的 RGB 色值决定,RGB 色值有大有小,自然三维物体表面就会有高低起伏。
在某个高度,某个方向上添加一个点光源照射这个物体,在光的照射下,物体凹凸不平的表面自然而然就会出现阴影(RGB 色值变化越大的地方表面越陡,阴影越深),再将这些阴影投影回二维平面,这样就得到了图片的边缘。
# 实现
-
# 将原图转为灰度图
为了简化矩阵,提高运算速度,我们对图像进行灰度图转换
-
# 使用高斯滤波进行降噪
这个算法对噪点很敏感(你想想平地上突然隆起一个笔直的擎天柱是不是显得特别扎眼)去除噪点影响,方法为使用高斯滤波,使图像模糊,然后使用领域降噪对图像进行降噪处理
-
# 计算图像梯度
numpy 自带矩阵梯度计算 (np.gradient),该梯度用来作为图像升维参考,梯度值越大,说明图像颜色在该点变化率越大,对应的虚拟深度应该也越大。
-
# 赋予图像虚拟深度
因为图像中的 RGB 值有大有小,直接作为三维高度并不合适,需要经过一些权衡计算来将其转变为三维高度,我们这里结合图像梯度和颜色向量来计算,并预设一个放大深度值,用来放大深度效果。
-
# 光源设置
这个需要进行调参了,光源的位置是一个很玄学的问题,位置不同,最后的效果也完全不一样。
个人觉得光源的最佳位置是与平面俯视角(即 z 轴与平面的夹角)呈 α =(pi / 2.2 )°,平面方向角(即 x 与 y 的夹角呈 β =(pi / 4 )°,这个角度下提取到的线稿最为清晰。
-
# 三维梯度转二维灰度
最后我们需要的是二维矩阵而不是三维矩阵,因此分别求平面各点在 xy 平面、xz 平面、yz 平面的投影
将三个方向的投影的值相加的和与原色相乘,也就是(灰度化)
-
# 极值处理
将处理后的图像进行极值处理,RGB 值靠近黑色(0)的归为黑色,靠近白色(255)的归为白色,突出边缘。
-
# 锐化处理
为了让我们最后的线稿更加清晰,可以对处理后的图片进行一定程度的锐化操作
# 代码
# 领域降噪 | |
def calculate_noise_count(img_obj, w, h, width, height): | |
count = 0 | |
for _w_ in [w - 1, w, w + 1]: | |
for _h_ in [h - 1, h, h + 1]: | |
if _w_ > width - 1: | |
continue | |
if _h_ > height - 1: | |
continue | |
if _w_ == w and _h_ == h: | |
continue | |
if img_obj[_w_, _h_] < 230: # 这里因为是灰度图像,设置小于 230 为非白色 | |
count += 1 | |
return count | |
# 高斯滤波 + 锐化 + 领域降噪的线稿提取方案 | |
def img_ege_get(byte: bytes): | |
time = datetime.now() | |
byte_stream = io.BytesIO(byte) | |
im1 = Image.open(byte_stream).convert('L') # 灰度图 | |
im = im1.filter(ImageFilter.GaussianBlur(radius=0.75)) # 高斯模糊 75% | |
a = np.asarray(im).astype('float') | |
depth = 10. # 设定虚拟深度 | |
grad = np.gradient(a) | |
grad_x, grad_y = grad | |
grad_x = grad_x * depth / 100. | |
grad_y = grad_y * depth / 100. | |
# 梯度向量计算 | |
A = np.sqrt(grad_x ** 2 + grad_y ** 2 + 1.) | |
uni_x = grad_x / A | |
uni_y = grad_y / A | |
uni_z = 1. / A | |
vec_el = np.pi / 2.2 | |
vec_az = np.pi / 4. | |
dx = np.cos(vec_el) * np.cos(vec_az) | |
dy = np.cos(vec_el) * np.sin(vec_az) | |
dz = np.sin(vec_el) | |
b = 255 * (dx * uni_x + dy * uni_y + dz * uni_z) | |
b = b.clip(0, 255) # 二值化处理,要么为 0,(黑色边缘)要么为 255(白色背景) | |
im2 = Image.fromarray(b.astype('uint8')) | |
im2 = im2.filter(ImageFilter.SHARPEN) | |
weight, height = im2.size | |
# 降噪 | |
pim = im2.load() | |
map(row_noise, [(pim, height, weight, w,) for w in range(weight)]) | |
imgByteArray = io.BytesIO() | |
im2.save(imgByteArray, format='png') | |
imgByteArray = imgByteArray.getvalue() | |
print(datetime.now() - time) | |
return imgByteArray | |
# 领域降噪 | |
def row_noise(pim, height, weight, w): | |
for h in range(height): | |
if calculate_noise_count(pim, w, h, weight, height) < 4: | |
pim[w, h] = 255 |