使用Python实现图像的手绘风格效果
- 一、引言
- 二、代码详细解释与示例
- 三、完整框架流程
- 四、运行
- 五、结论
- 附:完整代码
一、引言
在数字图像处理领域,模拟手绘风格是一项有趣且具有挑战性的任务。手绘风格图像通常具有独特的纹理和深浅变化,给人一种亲切和艺术感。本文旨在通过计算图像的梯度并模拟光照效果,实现一种简单而有效的手绘风格图像生成方法。
二、代码详细解释与示例
-
导入必要的库:
from PIL import Image import numpy as np import os
PIL
(Pillow):用于图像的打开、转换和保存。NumPy
:用于数组和矩阵操作,以及数学计算。os
:用于文件和目录操作。
-
计算图像的阴影效果:
def calculate_shading(img, depth, vec_el, vec_az):# 计算图像在x和y方向上的梯度grad = np.gradient(img)grad_x, grad_y = grad # grad_x是x方向的梯度,grad_y是y方向的梯度# 根据深度因子调整梯度大小grad_x *= depth / 100.grad_y *= depth / 100.# 计算梯度的模(大小),并添加一个小常数以避免除以零A = np.sqrt(grad_x ** 2 + grad_y ** 2 + 1e-7) # 1e-7是为了数值稳定性# 计算单位梯度向量(在x, y, z方向上的分量)# 这里我们假设z方向上的分量为1/A,因为它代表图像表面的“法线”方向uni_x = grad_x / Auni_y = grad_y / Auni_z = 1. / A# 计算光源方向向量(在x, y, z方向上的分量)dx = np.cos(vec_el) * np.cos(vec_az) # 光源在x方向的影响dy = np.cos(vec_el) * np.sin(vec_az) # 光源在y方向的影响dz = np.sin(vec_el) # 光源在z方向(垂直于图像平面)的影响# 计算光源方向与单位梯度向量的点积,模拟光照效果# 点积越大,表示该点越亮;点积越小(越接近-1),表示该点越暗(但由于我们取绝对值并映射到0-255,所以实际上表现为更深的阴影)# 但由于我们是模拟手绘风格,所以直接乘以255并裁剪到0-255范围内b = 255 * (dx * uni_x + dy * uni_y + dz * uni_z)# 裁剪结果到0-255范围内,确保图像数据的有效性b = b.clip(0, 255)return b
- 函数
calculate_shading
接收一个灰度图像数组img
、深淡程度depth
、光源的俯视角度vec_el
和方位角度vec_az
作为参数。 - 使用
np.gradient
计算图像在x和y方向上的梯度,得到grad_x
和grad_y
。 - 根据
depth
因子调整梯度大小,然后计算梯度的模A
,并添加一个小常数以避免除以零的错误。 - 计算单位梯度向量
uni_x
、uni_y
和uni_z
(假设z方向上的分量为1/A)。 - 计算光源方向向量
dx
、dy
和dz
。 - 通过计算光源方向与单位梯度向量的点积,模拟光照效果,得到带有阴影效果的图像数组
b
。 - 使用
clip
函数将结果裁剪到0-255范围内,确保图像数据的有效性。
示例:
假设我们有一个灰度图像img_array
,我们可以调用这个函数来计算阴影效果:shaded_img = calculate_shading(img_array, depth=50.0, vec_el=np.pi / 2.2, vec_az=np.pi / 4.0)
- 函数
-
将输入图片处理成手绘风格并保存:
# 将输入图片处理成手绘风格并保存为新文件def img_handpainted(input_img_path, depth=20.0, vec_el=np.pi / 2.2, vec_az=np.pi / 4.0):"""将指定路径的灰度图像转换成手绘风格的图像并保存。参数:input_img_path (str): 输入图片的路径。depth (float): 深淡程度控制因子,默认为20.0。vec_el (float): 光源的俯视角度(俯仰角),默认为np.pi/2.2。vec_az (float): 光源的方位角度(方位角),默认为np.pi/4.0。"""# 检查输入文件是否存在if not os.path.exists(input_img_path):raise FileNotFoundError(f"The file {input_img_path} does not exist.")# 打开并转换图像为灰度图,然后转换为NumPy数组img = np.array(Image.open(input_img_path).convert('L')).astype('float')# 从文件路径中提取图像名称(不包括扩展名)img_name = os.path.basename(input_img_path)[:-4]# 计算阴影效果shaded_img = calculate_shading(img, depth, vec_el, vec_az)# 将结果转换回图像格式并保存img_handwrite = Image.fromarray(shaded_img.astype('uint8'))outname = './' + img_name + '_handpainted.jpg'img_handwrite.save(outname)print(f"Saved image as {outname}")
- 函数
img_handpainted
接收输入图片的路径input_img_path
、深淡程度depth
、光源的俯视角度vec_el
和方位角度vec_az
作为参数。 - 检查输入文件是否存在,如果不存在则抛出
FileNotFoundError
异常。 - 打开并转换图像为灰度图,然后转换为NumPy数组。
- 从文件路径中提取图像名称(不包括扩展名)。
- 调用
calculate_shading
函数计算阴影效果。 - 将结果转换回图像格式,并保存为新的JPEG文件。
- 打印保存的文件名。
示例:
假设我们有一个灰度图像文件example.jpg
,我们可以调用这个函数来生成手绘风格的图像:img_handpainted('./example.jpg', depth=30.0)
- 函数
三、完整框架流程
-
准备阶段:
- 安装必要的Python库:Pillow(PIL)和NumPy。
- 准备一张灰度图像作为输入。
-
计算阴影效果:
- 读取输入图像并转换为灰度图。
- 将灰度图转换为NumPy数组,以便进行数学计算。
- 调用
calculate_shading
函数,根据光源方向和深淡程度计算阴影效果。
-
生成手绘风格图像:
- 将计算得到的阴影效果应用到原始灰度图像上。
- 将结果转换回图像格式。
- 保存为新的JPEG文件。
-
运行脚本:
- 执行脚本,查看生成的手绘风格图像。
四、运行
原图片:
执行代码后生成的图片:
五、结论
通过本文介绍的方法,我们可以轻松地将普通灰度图像转换成具有手绘风格效果的图像。这种方法不仅简单易行,而且生成的图像具有独特的手绘质感,非常适合用于艺术创作和图像处理领域。希望本文能够帮助读者理解和实现这一有趣的技术。
附:完整代码
# -*- coding: utf-8 -*-
# @Time : 2021/7/4 22:30
# @Author : Leuanghing Chen
# @Blog : https://blog.csdn.net/weixin_46153372?spm=1010.2135.3001.5421
# @File : 手绘效果图.py
# @Software : PyCharmfrom PIL import Image
import numpy as np
import os# 计算图像的阴影效果,模拟手绘风格的深浅变化
def calculate_shading(img, depth, vec_el, vec_az):"""计算并返回带有阴影效果的灰度图像。参数:img (numpy.ndarray): 输入的灰度图像,应该是一个二维数组。depth (float): 深淡程度控制因子,范围0-100。值越大,阴影效果越明显。vec_el (float): 光源的俯视角度(俯仰角),以弧度为单位。控制光源相对于图像平面的垂直角度。vec_az (float): 光源的方位角度(方位角),以弧度为单位。控制光源在图像平面上的水平位置。返回:numpy.ndarray: 带有阴影效果的灰度图像,与输入图像尺寸相同。"""# 计算图像在x和y方向上的梯度grad = np.gradient(img)grad_x, grad_y = grad # grad_x是x方向的梯度,grad_y是y方向的梯度# 根据深度因子调整梯度大小grad_x *= depth / 100.grad_y *= depth / 100.# 计算梯度的模(大小),并添加一个小常数以避免除以零A = np.sqrt(grad_x ** 2 + grad_y ** 2 + 1e-7) # 1e-7是为了数值稳定性# 计算单位梯度向量(在x, y, z方向上的分量)# 这里我们假设z方向上的分量为1/A,因为它代表图像表面的“法线”方向uni_x = grad_x / Auni_y = grad_y / Auni_z = 1. / A# 计算光源方向向量(在x, y, z方向上的分量)dx = np.cos(vec_el) * np.cos(vec_az) # 光源在x方向的影响dy = np.cos(vec_el) * np.sin(vec_az) # 光源在y方向的影响dz = np.sin(vec_el) # 光源在z方向(垂直于图像平面)的影响# 计算光源方向与单位梯度向量的点积,模拟光照效果# 点积越大,表示该点越亮;点积越小(越接近-1),表示该点越暗(但由于我们取绝对值并映射到0-255,所以实际上表现为更深的阴影)# 但由于我们是模拟手绘风格,所以直接乘以255并裁剪到0-255范围内b = 255 * (dx * uni_x + dy * uni_y + dz * uni_z)# 裁剪结果到0-255范围内,确保图像数据的有效性b = b.clip(0, 255)return b# 将输入图片处理成手绘风格并保存为新文件
def img_handpainted(input_img_path, depth=20.0, vec_el=np.pi / 2.2, vec_az=np.pi / 4.0):"""将指定路径的灰度图像转换成手绘风格的图像并保存。参数:input_img_path (str): 输入图片的路径。depth (float): 深淡程度控制因子,默认为20.0。vec_el (float): 光源的俯视角度(俯仰角),默认为np.pi/2.2。vec_az (float): 光源的方位角度(方位角),默认为np.pi/4.0。"""# 检查输入文件是否存在if not os.path.exists(input_img_path):raise FileNotFoundError(f"The file {input_img_path} does not exist.")# 打开并转换图像为灰度图,然后转换为NumPy数组img = np.array(Image.open(input_img_path).convert('L')).astype('float')# 从文件路径中提取图像名称(不包括扩展名)img_name = os.path.basename(input_img_path)[:-4]# 计算阴影效果shaded_img = calculate_shading(img, depth, vec_el, vec_az)# 将结果转换回图像格式并保存img_handwrite = Image.fromarray(shaded_img.astype('uint8'))outname = './' + img_name + '_handpainted.jpg'img_handwrite.save(outname)print(f"Saved image as {outname}")# 当脚本作为主程序运行时,调用img_handpainted函数
if __name__ == '__main__':img_handpainted('./1.jpg')