(原文:https://blog.iyatt.com/?p=13222 )
1 测试环境
Python 3.12.1
numpy 1.26.3
opencv-python 4.9.0.80
pillow 10.2.0
matplotlib 3.8.2
注:
- 基于 2022.1.16 和 2022.4.9 的三篇博文再次验证并重写,原文已删除
- 测试使用的图片文件为 AI 绘制
2 图像数据结构
2.1 OpenCV 打开图片并显示
Python 版 OpenCV 中图像数据是用的 NumPy 数组存储,通道顺序为 BGRA(蓝 绿 红 透明度),三通道则为 BGR。
import cv2
image_path = 'demo.png' # 图片路径
img = cv2.imread(image_path) # 打开图片文件
cv2.imshow('my image', # 窗口标题
img) # 图像数据
cv2.waitKey(0) # 阻塞窗口,按任意键继续
cv2.destroyAllWindows() # 关闭所有窗口
2.2 Matplotlib 打开图片并显示
Matplotlib 和 OpenCV 一样都是采用的 NumPy 数组存储图像数据,只是通道顺序为 RGB。
import matplotlib.pyplot as plt
image_path = 'demo.png'
image = plt.imread(image_path)
plt.axis('off') # 不显示坐标轴
plt.imshow(image)
plt.show()
2.3 Pillow 打开图片用 OpenCV 显示
Pillow 是 Python 中较为常用的图像库。
from PIL import Image
import cv2
import numpy as np
image_path = 'demo.png'
pillow_image = Image.open(image_path)
opencv_image = cv2.cvtColor(
np.array(pillow_image), # Pillow 图像数据结构转 NumPy
cv2.COLOR_RGB2BGR # 通道顺序由 RGB 转为 BGR
)
cv2.imshow('Pillow Image To OpenCV Image', opencv_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
2.4 OpenCV 打开图片用 Tkinter 显示(OpenCV 转 Pillow)
Tkinter 是 Python 的官方 GUI 库,Pillow 的图像数据支持直接在 Tkinter 中显示,因此这里把 OpenCV 图像转为 Pillow 再到 Tkinter 中显示。
import cv2
import tkinter as tk
from PIL import Image, ImageTk
image_path = 'demo.png'
class Application(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.master = master
def interface(self):
global pillow_image # 注意 Tkinter 显示的图片要使用全局变量
opencv_image = cv2.imread(image_path)
pillow_image = ImageTk.PhotoImage(
Image.fromarray(
cv2.cvtColor(
opencv_image,
cv2.COLOR_BGR2RGB
)
)
)
tk.Label(self.master, image=pillow_image).pack()
if __name__ == '__main__':
root = tk.Tk()
root.title('OpenCV 打开图片并在 Tkinter 中显示') # 窗口标题
app = Application(root)
app.interface()
root.mainloop()
2.5 Pillow 打开图片并使用 Tkinter 显示
import tkinter as tk
from PIL import Image, ImageTk
image_path = 'demo.png'
class Application(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.master = master
def interface(self):
global pillow_image # 注意 Tkinter 显示的图片要使用全局变量
pillow_image = ImageTk.PhotoImage(
Image.open(image_path)
)
tk.Label(self.master, image=pillow_image).pack()
if __name__ == '__main__':
root = tk.Tk()
root.title('Pillow 打开图片并在 Tkinter 中显示')
app = Application(root)
app.interface()
root.mainloop()
2.6 Matplotlib 打开图片用 OpenCV 显示
Matplotlib 和 OpenCV 都是使用 NumPy 数组保存图像数据,两者转换只需要修改通道顺序即可,非常方便。
import matplotlib.pyplot as plt
import cv2
image_path = 'demo.png'
matplotlib_image = plt.imread(image_path)
opencv_image = cv2.cvtColor(
matplotlib_image,
cv2.COLOR_RGB2BGR
)
cv2.imshow(
'Matplotlib To OpenCV',
opencv_image
)
cv2.waitKey(0)
cv2.destroyAllWindows()
2.7 OpenCV 打开图片用 Matplotlib 显示
import matplotlib.pyplot as plt
import cv2
image_path = 'demo.png'
opencv_image = cv2.imread(image_path)
matplotlib_image = cv2.cvtColor(
opencv_image,
cv2.COLOR_RGB2BGR
)
plt.imshow(matplotlib_image)
plt.axis('off')
plt.show()
2.8 Matplotlib 打开图片用 Tkinter 显示
import matplotlib.pyplot as plt
import tkinter as tk
from PIL import Image, ImageTk
image_path = 'demo.png'
class Application(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.master = master
def interface(self):
global pillow_image # 注意 Tkinter 显示的图片要使用全局变量
matplotlib_image = plt.imread(image_path)
pillow_image = ImageTk.PhotoImage(
Image.fromarray(
(matplotlib_image * 255).astype('uint8') # 把 float32 转为 uint8
)
)
tk.Label(self.master, image=pillow_image).pack()
if __name__ == '__main__':
root = tk.Tk()
root.title('Matplotlib 打开图片并在 Tkinter 中显示') # 窗口标题
app = Application(root)
app.interface()
root.mainloop()
3 基于 NumPy 数组的图像数据结构操作
OpenCV 和 Matplotlib 中图像数据都是使用 NumPy,这里试着创建一个 NumPy 数组来操作,更好理解其结构。
这里创建一个 2x2 分辨率的图片,4 个点分别定义为黑色RGB(0,0,0),白色RGB(255,255,255),红色RGB(255,0,0),紫色RGB(255,0,255),先用 Matplotlib 示例,通道顺序就是 RGB
import matplotlib.pyplot as plt
import numpy as np
data = np.array([
[[0, 0, 0], [255, 255, 25]],
[[255, 0, 0], [255, 0, 255]]
])
print('形状:', data.shape)
plt.imshow(data)
plt.show()
形状是 2x2 分辨率,3 通道(RGB)
对这种图像数据结构的切片操作格式如下
image[y1:y2:ys, x1:x2:xs, c1:c2:cs]
逗号分隔的三部分分别是操作 y 轴、x 轴、颜色通道,每部分冒号分隔的 1 和 2 对应起始和结束,s 对应步长,可以省略。
3.1 颜色通道顺序转换
前面 Matplotlib 和 OpenCV 图像数据互相转换是使用的 OpenCV 的 cvtColor 函数,这里可以尝试基于 NumPy 数组操作,将 cs 设为 -1,则会逆向通道顺序。
Matplotlib 显示图像会自动调整比例,但是 OpenCV 会原比例显示,所以这里需要放大图像再显示。
import cv2
import numpy as np
data = np.array([
[[0, 0, 0], [255, 255, 25]],
[[255, 0, 0], [255, 0, 255]]
], dtype=np.uint8)
new_data = data[:,:,::-1] # 通道顺序逆向
new_data = cv2.resize(
new_data,
(255, 255), # 放大后的分辨率
interpolation=cv2.INTER_NEAREST # 最近邻插值法,直接复制原图像像素,不计算衔接边缘
)
cv2.imshow('my data', new_data)
cv2.waitKey(0)
cv2.destroyAllWindows()
3.2 图像部分截取
import matplotlib.pyplot as plt
image_path = 'demo.png'
img = plt.imread(image_path)
plt.axis('off')
roi = img[14:556, 219:633] # 截取 y 取值 14~556,x 取值到 219~633 的部分
plt.imshow(roi)
plt.show()
3.3 颜色通道分离
3.3.1 OpenCV
下面的示例中从图片文件读取,然后将图像数据的三色通道分离,另外创建一个等大小的空数据通道,然后再尝试用空数据填充 G、B 通道和分离出来的 R 通道合并生成一个新的彩色图片,新生成的图片中缺失了绿色和蓝色通道则变为了“黑红”图片。
import cv2
import numpy as np
image_path = 'demo.png'
image = cv2.imread(image_path)
B = image[:, :, 0:1] # 截取 0 通道[0,1),前开后闭,即蓝色
G = image[:, :, 1:2] # 截取 1 通道,绿色
R = image[:, :, 2:3] # 截取 2 通道,红色
ZERO = np.zeros(B.shape, dtype=B.dtype) # 创建一个空数据的通道
R_image = cv2.merge([ZERO, ZERO, R])
cv2.imshow('R image', R_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
3.3.2 Matplotlib
import matplotlib.pyplot as plt
import numpy as np
image_path = 'demo.png'
image = plt.imread(image_path)
R = image[:, :, 0] # 直接截取单个通道,或者 0:1 也行
G = image[:, :, 1]
B = image[:, :, 2]
ZERO = np.zeros(B.shape, dtype=R.dtype) # 创建一个空数据的通道
B_image = np.dstack((ZERO, ZERO, B))
plt.imshow(B_image)
plt.axis('off')
plt.show()
3.4 深拷贝和浅拷贝
图像数据的深拷贝和浅拷贝,在基于 NumPy 数组的前提下,也就是 NumPy 数组的深拷贝和浅拷贝。直接用等号赋值实际得到的是为原数组起的一个别名,通过原来的数组名和新起的名字操作的都是同一块地址,实际就是浅拷贝。使用 copy 方法拷贝则是深拷贝,深拷贝不是创建一个别名,而是新开辟空间,并复制原来数组的数据到新空间,新旧数组是独立的空间。
import numpy as np
array = np.array([
[[0, 0, 255]]
])
array1 = array
array2 = array.copy()
print('array 地址/是否只读:', array.__array_interface__['data'])
print('array1 地址/是否只读:', array1.__array_interface__['data'])
print('array2 地址/是否只读:', array2.__array_interface__['data'])
3.5 贴图
import matplotlib.pyplot as plt
image_path = 'demo.png'
src = plt.imread(image_path)
copy_image = src.copy() # 深拷贝
copy_image[628:810, 194:548] = [255, 255, 255] # x:194~548。y:628~810 填充为白色RGB(255,255,255)
copy_image[713:1017, 239:478] = src[104:408, 289:528] # 截取原图人脸部分 x:289~528,y:104~408,贴到拷贝图像的 x:239~478,y:713~1017
plt.axis('off')
plt.imshow(copy_image)
plt.show()
3.6.1 透明度通道
前面的操作都是前三个基色的通道,没有涉及第 4 个通道透明度,下面这张胡子图片就是具有 4 通道的图片,可以右键另存为用于测试。
import matplotlib.pyplot as plt
import numpy as np
image_path = 'demo.png'
beard_path = 'demo1.png' # 胡子图片文件
src = plt.imread(image_path)
beard = plt.imread(beard_path)
# 为原图添加 alpha 通道(透明度)
image_with_alpha = np.dstack([
src,
np.ones((src.shape[0], src.shape[1]), dtype=src.dtype)
])
beard_h, beard_w = beard.shape[:2] # 获取胡子图片的尺寸
mask_boolean = beard[:, :, 3] == 1 # alpha 值为 1 的像素点即为完全不透明的值
image_with_alpha[523:523+beard_h, 94:94+beard_w][mask_boolean] = beard[mask_boolean] # 将胡子不透明的部分像素值嵌入图像中
plt.axis('off')
plt.imshow(image_with_alpha)
plt.show()
结合透明度信息后,就不会完全照搬把贴的图片拿上去挡住,不透明的部分就显示原图的内容。这里使用 Matplotlib 读取的图片数据类型为 float32(OpenCV 是 uint8,为 0-255 的整数),每个通道的像素点数据为 0-1 的小数,alpha 通道为 1 就是完全呈现 RGB 的值,alpha 为 0 就是完全不呈现 RGB 值,中间就是过渡。
上面写的例子其实很有局限性,用的胡子图片比较特殊,透明度的值是极化的,要么完全透明,要么完全不透明,所以可以采用上面的方法判断不透明的就直接复制替换原图的部分,但是如果透明度是 0-1 之间的不完全透明,也不是完全不透明,就不能用这种方法处理。不完全透明的情况下,贴上去的图不能完全遮挡原图,也就是原图和贴上去的图的像素值信息都要显示出来。
要解决上面提到的问题就得从透明度本身的性质着手,先只考虑一个像素点的情况,假如原图的像素点值为[1, 0, 0, 1],就是完全显示红色的点,然后我要将一个 [0, 0, 1, 0.6] 的点贴上去,这个点本身是纯蓝色,但是透明度为 0.6,即只呈现蓝色的 60%,那么剩下的 40% 就显示背景(即原图),那么最终显示的就应该是$$[1 \times 0.4 + 0 \times 0.6, 0 \times 0.4 + 0 \times 0.6, 0 \times 0.4 + 1 \times 0.6, 1 \times 0.4 + 0.6 \times 0.6]$$
,就有下面的代码:
import matplotlib.pyplot as plt
import numpy as np
image_path = 'demo.png'
beard_path = 'demo1.png' # 胡子图片文件
src = plt.imread(image_path)
beard = plt.imread(beard_path)
# 为原图添加 alpha 通道(透明度)
image_with_alpha = np.dstack([
src,
np.ones((src.shape[0], src.shape[1]), dtype=src.dtype)
])
beard_h, beard_w = beard.shape[:2] # 获取胡子图片的尺寸
beard_alpha1 = beard[:, :, 3] # 取出胡子图片的透明度
beard_alpha2 = 1 - beard_alpha1 # 计算出原图被贴图位置应该具有的透明度
for c in range(4):
image_with_alpha[523:523+beard_h, 94:94+beard_w, c] = beard_alpha2 * image_with_alpha[523:523+beard_h, 94:94+beard_w, c] + beard_alpha1 * beard[:, :, c]
plt.axis('off')
plt.imshow(image_with_alpha)
plt.show()
这里我也不能保证我的思路是对的,只是使用胡子图片验证没问题。