前言
导入所需库。
import numpy as np
from PIL import Image
通常我们把一张灰度图看作是一个二维数组。
灰度图:二维数组
数组中的每一个元素用来表示像素点的亮度值
对彩色的图片我们可以使用三维数组来表示。
彩色图:三个三维数组
数组中的第三维分别存储着像素点的红 绿 蓝分量
读取图片
使用Image.open来读取图片文件。
im=Image.open('example1.jpg')
# im.show()
将图片转换成数组
既然灰度图片是一个二维数组,彩色图片是三组三维数组,那么可以通过np.array将图片转换成为一个numpy数组。
im=np.array(im)
使用shape和dtype可以看到:
print(im.shape,im.dtype)
-
这张图片一共有1520行720列;
-
3表示它是一个彩色图片,有红 绿 蓝三个颜色分量;
-
unit8表示表示数据类型是8位整数,即一个字节。
(1520, 720, 3) uint8
取出20 100位置的像素点的值。
print(im[20,100])
-
103 101 112 分别表示分别表示红 绿 蓝三种颜色分量的数值。
[103 101 112]
Image.fromarray的作用:实现array到image的转换。1
im=np.array(im)
im=Image.fromarray(im)
灰度图
使用convert可以转换图像模式,L代表像素从 rgb 的三色3字节模式转换为单一灰度模式,即灰度图片。
im_01=im.convert("L")
im_01.save("img_01.jpg")
im_01.show()
其中灰度色彩的取值为0-255,表示灰度色彩的变化。
再重新查看20 100处的数值,现在只有一个数值,即103灰度值。
im=np.array(im_01)
print(im[20,100])
103
一张彩图通过如下公式将 rgb 三色的数值转换成灰度值。
Gray=R*0.3+G*0.59+B*0.11
我们可以通过下标来访问某个像素点的颜色。
-
这个例子中输出像素点100 100的值为 rgb 三色的数值为 103 101 112。
im=np.array(Image.open("example1.jpg")) print(im[100,100])
[103 101 112]
-
接下来使用切片命令单独取出r分量的值,现在就只输出r分量的值为103
im=im[:,:,0] print(im[100,100])
103
由于经历除法运算会得到浮点数的结果,然而将数组转为图像的过程中,数组中的数据是浮点型因而无法转为图像而报错:cannot write mode F as JPEG,因此需要改变image的类型。2
若还想用fromarray将numpy的二维数组转换成图像形式保存,那就需要astype的方式将图像的颜色分量转成在0-255的整数,这样以便于输出或者保存。
pil=Image.fromarray(im.astype('uint8'))
pil.show()
当然,我们也可发现,在由rgb数值转化为灰度值的过程中,使用灰度值计算公式会出现浮点数,然而最后输出的数值为整数103,这是因为灰度图默认是uint8整型,因而无需变换。
也可以使用np.uint()转换成整数。其中:
- iml=255-im0是将图反转,也就是我们平时所看到的底片效果;
- im2做了区间变化;
- im3求了每个像素点的平方,数字变大了很多,图像变暗。
im0=np.array(Image.open("fcity.jpg").convert('L')) im1=255-im0 im2=(100/255)*im0+150 im3=255*(im1/255)**2 pil_im=Image.fromarray(np.uint(iml)) # pi1_im=Image.fromarray(np.uint(im2)) # pil_im=Image.fromarray(np.uint(im3)) pil_im.show()
降采样
如果想要实现对图片降采样,可以使用这样的带跨度的切片方式,取出部分像素点来实现,5表示每隔5个取一次值,图片会小很多。
im=np.array(Image.open("example1.jpg"))
im=im[::5,::5,:]
pil=Image.fromarray(im.astype('uint8'))
pil.show()
裁剪图片
如果想要裁剪图片,可以使用以下切片参数设置:
-
第一维40-1000表示行,也就是图片的高;
-
第二维300-900是列像素,也就是图片的宽。
im=np.array(Image.open("girl.jpg")) im=im[40:1000,300:900] pil=Image.fromarray(im.astype('uint8')) pil.show()
图片翻转
如果想要实现图片翻转,也可以使用如下的参数设定:
-
第一维参数的最后一个值设置为-1为上下翻转;
-
第二维参数的最后一个值设为-1为左右翻转;
-
第三维参数的最后一个值设置为-1为颜色翻转,这里指的是rgb分量值逆序排列,可以通过print(im[20,100])与上述的值对比而发现。
im=np.array(Image.open("example1.jpg")) im=im[:,::-1,:]#左右翻转 # im=im[::-1,:,:]#上下翻转 # im=im[:,:,::-1]#颜色翻转 # print(im[20,100]) pil=Image.fromarray(im.astype('uint8')) pil.show()
重叠图片
首先,用刚才翻转图片的方式翻转一朵玫瑰花的图片,
im=np.array(Image.open("rose.jpg"))
im=im[:,::-1,:]#左右翻转
#im=im[::-1,:]#上下翻转
pil=Image.fromarray(im.astype('uint8'))
pil.show()
pil.save("rose_turn.jpg")
然后使用这样的公式,按比例把两张图片混合在一起,得到一张混合的玫瑰花。
imBlend=iml*0.5+im2*0.5
-
这里一定不要忘记使用astype来转换成整数,因为按公式计算后得到的数组是一个浮点数数组。
iml=np.array(Image.open("rose.jpg")) im2=np.array(Image.open("rose_turn.jpg")) imBlend=iml*0.5+im2*0.5 imBlend=Image.fromarray(imBlend.astype('uint8')) imBlend.show()
手绘图
下面我们来完成一个实现手绘效果的代码。
手绘风格,即黑白轮廓的描绘,首先需要读取原图图像的明暗变化,即灰度值。从直观视觉的感受上定义,图像灰度值显著变化的地方就是梯度,它描绘了图像灰度变化的强度。
手绘图像的基本思想就是利用像素之间的梯度值重构每一个像素值。
梯度:连续值之间的变化率,即斜率。
提取图像轮廓
通常可以使用梯度计算来提取图像轮廓。
我们使用以下公式来完成计算梯度:
一维数组:
存在俩侧值 :
- 斜率=(右侧值-左侧值)/2
只存在一侧值 :
- 斜率=(本身-左侧值) 或者 (右侧值-本身)
好消息是,numpy中提供了直接获取灰度图像梯度的函数gradient(),传入图像数组可返回代表x和y各自方向上梯度变换的二维元组。
np.gradient()函数计算图像梯度值作为新色彩计算的基础。
其中,a的值是玫瑰花的灰度值,
a=np.array(Image.open("rose.jpg").convert('L'))
计算梯度后得到的grad将其分成x轴和y轴两个方向的矩阵。
grad=np.gradient(a) # 取图像灰度的梯度值
grad_x,grad_y=grad # 分别取横纵图像梯度值
深度权值depth
现在我们来看一下depth的作用。
给x和y方向的梯度值赋值权值depth,这种坐标空间变化相当于给物体加了一个虚拟的光源。
根据灰度值大小,模拟各部分相对于人视角的远近程度,使画面显得更有“深度”,而这个深度就是depth。
因为在利用梯度重构图像时,0-255之间的不同的灰度值对应于不同的梯度取值,depth的作用在于调解这个对应关系。
本例中的depth设置为10,也可以尝试设置为0-100之间的其他值。
depth=10. # (0-100)
depth较小时,背景区域接近白色,画面显示轮廓描绘;
depth较大时,整体画面灰度值较深,近似于浮雕效果。
以下两句执行后的效果:grad_x和grad_y矩阵数值缩小了十倍,shape仍然是500*700。
grad_x=grad_x*depth/100
grad_y=grad_y*depth/100
光源的位置
为了体现光照的效果,我们设计一个光源,下面是光源的位置。
一个是俯视角度vec_el,一个是方位角度vec_az。
vec_el=np.pi/2.2 # 光源的俯视角度,弧度值
vec_az=np.pi/4 # 光源的方位角度,弧度值
两个角度的设定和单位向量构成了基础的柱坐标系,它体现了物体相对于虚拟光源的位置。
我们需要建立光源对各个梯度值变化影响的函数dx dy,进而运算出新的像素值,它可以体现边界变化点灰度的变化,从而形成手绘效果。
dx=np.cos(vec_el)*np.cos(vec_az) # 光源对x轴的影响
dy=np.cos(vec_el)*np.sin(vec_az) # 光源对y轴的影响
具体来说,为了更好的体现立体感,需要增加一个z方向的梯度值。
dz=np.sin(vec_el) # 光源对z轴的影响
为了更直观的进行计算,可以把角度对应的柱坐标转换成为 x y z立体坐标系,而dx dy dz则是像素点在施加了模拟光源后在x y z方向上明暗变化的加权向量。
A是梯度幅值,也是梯度大小,
A=np.sqrt(grad_x**2+grad_y**2+1.)
各个方向上总梯度除以幅值,得到每个像素单元的梯度值,利用每个像素单元的梯度值和方向加权向量合成了灰度值,
uni_x=grad_x/A
uni_y=grad_y/A
uni_z=1./A
a2=255*(dx*uni_x+dy*uni_y+dz*uni_z) # 光源归一化
clip函数用于防止溢出并归一化到0-255区间,
a2=a2.clip(0,255)
最后两行从数组中恢复图像并保存。
im2=Image.fromarray(a2.astype('uint8')) # 重构图像
im2.save('roseHandDrew.jpg')
效果对比
- 原图
- 手绘图