Python 中常用图像数据结构

最近更新于 2024-05-05 12:30

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() # 关闭所有窗口

file

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()

file

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()

file

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 数组来操作,更好理解其结构。

这里创建一个 2×2 分辨率的图片,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()

file
形状是 2×2 分辨率,3 通道(RGB)
file

对这种图像数据结构的切片操作格式如下

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()

file

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()

file

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()

file

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()

file

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'])

file

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()

file

3.5.1 透明度通道

前面的操作都是前三个基色的通道,没有涉及第 4 个通道透明度,下面这张胡子图片就是具有 4 通道的图片,可以右键另存为用于测试。
file

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()

file

结合透明度信息后,就不会完全照搬把贴的图片拿上去挡住,不透明的部分就显示原图的内容。这里使用 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()

这里我也不能保证我的思路是对的,只是使用胡子图片验证没问题。

3.6 读图默认数据类型

3.6.1 OpenCV

import cv2

img = cv2.imread('test.png')
print(img.dtype)

file

OpenCV 默认的数据类型为 uint8,即每个像素点的取值都是 0-255

3.6.2 Matplotlib

import matplotlib.pyplot as plt

img = plt.imread('test.png')
print(img.dtype)

file

Matplotlib 默认的数据类型为 float32,即每个像素点的取值是 0-1 的小数

3.6.3 Pillow

from PIL import Image
import numpy as np

img = Image.open('test.png')
print(np.array(img).dtype)

file
Pillow 不是使用的 NumPy 数组存储图像数据,转化为 NumPy 时,NumPy 会根据数据进行推断,可以看到 Pillow 默认存储的图像数据类型是契合 uint8 的

Python 中常用图像数据结构
Scroll to top