【Python】Python处理图像

Python图像处理深度剖析:从基础到精通的实践指南

第一章:数字图像基础

在深入学习图像处理技术之前,我们必须对数字图像本身有一个清晰的认识。

1.1 什么是数字图像?

一幅数字图像可以被看作是一个二维的离散信号。在最常见的表示形式中,数字图像是由一个矩形的像素(Pixel)网格组成的。每个像素是图像中的一个最小单元,它携带了该位置的颜色或灰度信息。

想象一个图像是一个巨大的表格,表格的每一个小格子就是一个像素。表格的行数和列数决定了图像的尺寸或分辨率。例如,一个1920x1080的图像意味着它有1920列像素和1080行像素,总共有1920 * 1080 = 2,073,600个像素。

1.2 像素与分辨率

  • 像素 (Pixel): Picture Element的缩写,是数字图像最基本的构成单元。每个像素通常由一个或多个数值表示其颜色或亮度。
  • 分辨率 (Resolution): 通常指图像的宽度和高度的像素数量,例如 1920x1080。更高的分辨率意味着图像包含更多的像素,细节更丰富,但也占用更大的存储空间和需要更多的处理能力。另一个相关的概念是DPI (Dots Per Inch) 或 PPI (Pixels Per Inch),这通常用于描述图像在物理介质(如打印)上的密度,影响打印质量。

1.3 颜色深度与颜色空间

  • 颜色深度 (Color Depth): 指用于表示每个像素颜色的二进制位数。颜色深度决定了图像可以显示的颜色数量。

    • 1位图像: 每个像素1位,只能表示黑白两种颜色。
    • 8位灰度图像: 每个像素8位,可以表示2^8 = 256种灰度级别(从纯黑到纯白)。
    • 24位彩色图像: 最常见的彩色图像类型,通常使用RGB颜色模型,每个颜色通道(红、绿、蓝)使用8位,总共24位。这可以表示 2^24 ≈ 1670万种颜色。
    • 32位彩色图像: 在24位RGB的基础上增加一个8位的Alpha通道,用于表示像素的透明度。
  • 颜色空间 (Color Space): 是一种用于量化和描述颜色的数学模型。不同的颜色空间适用于不同的应用场景。

    • RGB (Red, Green, Blue): 这是最常用的颜色空间,基于红、绿、蓝三原色的加色混合原理。计算机显示器、电视等设备通常使用RGB。每个像素由三个通道的值表示,例如 (R, G, B)。

    • 灰度 (Grayscale): 图像中每个像素只有一个亮度值,没有颜色信息。通常由R、G、B三个通道的加权平均得到,例如 亮度 = 0.2989 * R + 0.5870 * G + 0.1140 * B (ITU-R BT.601标准)。

    • HSV (Hue, Saturation, Value): 色调、饱和度、明度。这个颜色空间更接近人类感知颜色的方式。

      • 色调 (Hue): 描述颜色的种类(如红色、黄色、蓝色),通常用角度表示(0-360度)。
      • 饱和度 (Saturation): 描述颜色的纯度或鲜艳程度,值越高颜色越鲜艳。
      • 明度 (Value): 描述颜色的亮度或暗度,值越高颜色越亮。
        HSV空间常用于颜色分割、基于颜色的目标跟踪等应用。
    • HSL (Hue, Saturation, Lightness): 类似于HSV,L代表亮度(Lightness)。

    • Lab: Lab*颜色空间是一种感知均匀的颜色空间,旨在使颜色之间的数值差异与人类感知的颜色差异相对应。

      • L: 知觉亮度(Lightness)。
      • a: 从绿色到红色的颜色通道。
      • b: 从蓝色到黄色的颜色通道。
        Lab空间常用于颜色测量、颜色比较和图像增强。
    • CMYK (Cyan, Magenta, Yellow, Key/Black): 青色、品红色、黄色、黑色。这是用于印刷的减色混合颜色空间。

1.4 图像文件格式

不同的图像文件格式使用不同的方式存储像素数据,包括压缩方式、是否支持透明度、是否支持动画等。

  • JPEG (Joint Photographic Experts Group): 有损压缩格式,适用于照片等连续色调图像。通过牺牲部分图像细节来达到较高的压缩率。不支持透明度。
  • PNG (Portable Network Graphics): 无损压缩格式,支持透明度(Alpha通道)。适用于需要保留图像细节、图形、Logo等,或者需要透明背景的场景。
  • BMP (Bitmap): 位图格式,通常不压缩或使用简单的无损压缩。文件体积较大,但保留了图像的原始像素数据。
  • TIFF (Tagged Image File Format): 支持多种压缩算法(包括无损和有损),支持多页、透明度等特性。常用于印刷、扫描、医学影像等专业领域。
  • GIF (Graphics Interchange Format): 支持256色调色板的无损格式,支持动画。适用于简单的图形和动画。

理解这些基础概念是进行有效图像处理的前提。接下来,我们将学习如何在Python中加载、显示和操作这些图像数据。

第二章:基本图像操作与Pillow库

Pillow是Python Imaging Library (PIL) 的一个活跃分支,它是Python中进行图像处理的基础库。Pillow提供了强大的图像文件格式支持,以及丰富的图像处理功能。

2.1 安装Pillow

在使用Pillow之前,需要先安装它。

pip install Pillow

2.2 图像加载、显示与保存

使用Pillow进行最基本的图像操作非常直观。

# 导入Image模块,它是Pillow库的核心
from PIL import Image
import os # 导入os模块用于文件路径操作

# 定义一个图像文件的路径
image_path = 'example.jpg' # 假设当前目录下有一个example.jpg文件
output_path = 'output.png' # 定义输出文件的路径

# 检查文件是否存在,如果不存在则创建一个模拟图像文件用于示例
if not os.path.exists(image_path):
    # 创建一个新的RGB图像,尺寸为200x100像素,背景色为红色
    dummy_img = Image.new('RGB', (200, 100), color = 'red')
    # 将模拟图像保存为example.jpg
    dummy_img.save(image_path)
    print(f"创建了模拟图像文件: {
     image_path}")


# 1. 加载图像
try:
    # 使用Image.open()函数加载图像文件
    img = Image.open(image_path)
    print(f"成功加载图像: {
     image_path}")

    # 2. 显示图像信息
    # img.format: 图像文件格式 (e.g., 'JPEG')
    print(f"图像格式: {
     img.format}")
    # img.size: 图像尺寸 (width, height)
    print(f"图像尺寸 (宽度x高度): {
     img.size}")
    # img.mode: 图像模式 (e.g., 'RGB', 'L' for grayscale, 'RGBA')
    print(f"图像模式: {
     img.mode}")

    # 3. 显示图像 (这通常会在操作系统中打开一个图像查看器)
    # img.show()
    # print("已调用show()方法显示图像 (可能需要手动关闭查看器)")

    # 4. 保存图像到不同的格式
    # 将加载的图像保存为PNG格式
    img.save(output_path, 'PNG')
    print(f"图像已保存为: {
     output_path}")

    # 尝试将图像转换为灰度并保存
    # img.convert('L'): 将图像转换为8位灰度图像
    gray_img = img.convert('L')
    # 保存灰度图像
    gray_img.save('output_grayscale.jpg', 'JPEG')
    print("图像已转换为灰度并保存为: output_grayscale.jpg")

    # 尝试将图像转换为RGBA并保存 (如果原图不是RGBA)
    # img.convert('RGBA'): 将图像转换为32位RGBA图像 (支持透明度)
    # 如果原图没有Alpha通道,会创建一个全不透明的Alpha通道
    rgba_img = img.convert('RGBA')
    # 保存RGBA图像为PNG格式 (PNG支持透明度)
    rgba_img.save('output_rgba.png', 'PNG')
    print("图像已转换为RGBA并保存为: output_rgba.png")


except FileNotFoundError:
    # 如果文件不存在,打印错误信息
    print(f"错误: 文件 {
     image_path} 未找到.")
except Exception as e:
    # 捕获其他可能的异常
    print(f"处理图像时发生错误: {
     e}")

代码解释:

  • from PIL import Image: 导入Pillow库中的Image模块,这是进行图像操作的主要入口。
  • import os: 导入os模块,用于检查文件是否存在,以便在示例中创建一个模拟文件。
  • image_path = 'example.jpg': 定义一个字符串变量,存储输入图像的文件名。
  • output_path = 'output.png': 定义一个字符串变量,存储输出图像的文件名。
  • if not os.path.exists(image_path):: 检查 image_path 指向的文件是否存在。
  • dummy_img = Image.new('RGB', (200, 100), color = 'red'): 如果文件不存在,使用 Image.new() 创建一个新图像。'RGB' 指定图像模式为真彩色,(200, 100) 指定图像尺寸为宽200像素,高100像素,color='red' 设置背景色为红色。
  • dummy_img.save(image_path): 将新创建的模拟图像保存到 image_path 指定的文件。
  • Image.open(image_path): 使用 Image.open() 函数加载指定路径的图像文件。它返回一个 Image 对象。
  • print(f"图像格式: {img.format}"): 访问图像对象的 format 属性,打印图像的文件格式(如’JPEG’, ‘PNG’)。
  • print(f"图像尺寸 (宽度x高度): {img.size}"): 访问图像对象的 size 属性,它是一个元组 (width, height),打印图像的宽度和高度。
  • print(f"图像模式: {img.mode}"): 访问图像对象的 mode 属性,打印图像的颜色模式(如’RGB’, ‘L’, ‘RGBA’)。
  • # img.show(): 调用图像对象的 show() 方法,尝试使用系统默认的图像查看器打开并显示图像。这行代码被注释掉了,因为在某些环境中它可能不是预期的行为,或者会中断脚本执行直到查看器关闭。
  • img.save(output_path, 'PNG'): 调用图像对象的 save() 方法,将图像保存到 output_path 指定的文件。第二个参数 'PNG' 指定了保存的文件格式。Pillow会根据文件扩展名自动猜测格式,但明确指定更保险。
  • gray_img = img.convert('L'): 调用图像对象的 convert() 方法,将图像转换为指定的模式。'L' 表示转换为8位灰度图像。转换会返回一个新的 Image 对象。
  • gray_img.save('output_grayscale.jpg', 'JPEG'): 将转换后的灰度图像保存为JPEG格式。
  • rgba_img = img.convert('RGBA'): 调用 convert('RGBA') 将图像转换为32位RGBA模式,这增加了透明度通道。
  • rgba_img.save('output_rgba.png', 'PNG'): 将RGBA图像保存为PNG格式,因为PNG支持Alpha通道。
  • try...except...: 使用try-except块来捕获可能发生的错误,如文件未找到 (FileNotFoundError) 或其他处理图像时可能出现的异常。

2.3 图像尺寸调整 (Resizing)

改变图像的尺寸是常见的操作。Pillow提供了 resize() 方法。

from PIL import Image

# 假设我们有之前加载的img对象

# 定义目标尺寸 (宽度, 高度)
new_size = (300, 200)

# 调整图像尺寸
# resize() 方法返回一个新的Image对象,原始图像不会被修改
# 第二个参数可以指定重采样滤波器,影响调整大小后的图像质量
# Image.Resampling.LANCZOS (或 PIL.Image.LANCZOS): 高质量滤波器
# Image.Resampling.BILINEAR: 双线性插值,速度较快
# Image.Resampling.NEAREST: 最近邻插值,速度最快,质量最低,用于像素艺术等
# Image.Resampling.BICUBIC: 双三次插值
resized_img = img.resize(new_size, Image.Resampling.LANCZOS)

# 显示或保存调整尺寸后的图像
# resized_img.show()
resized_img.save('output_resized.jpg')
print(f"图像尺寸已调整到 {
     new_size} 并保存为: output_resized.jpg")

# 保持纵横比进行尺寸调整
# 如果想让图像的长边变为指定长度,同时保持比例
max_side = 400
# 获取原始图像的宽度和高度
original_width, original_height = img.size

# 计算缩放比例
if original_width > original_height:
    # 如果宽度更大,以宽度为基准计算比例
    scale_ratio = max_side / original_width
else:
    # 如果高度更大或相等,以高度为基准计算比例
    scale_ratio = max_side / original_height

# 计算新的尺寸
new_width = int(original_width * scale_ratio)
new_height = int(original_height * scale_ratio)
size_proportional = (new_width, new_height)

# 调整尺寸,保持纵横比
resized_proportional_img = img.resize(size_proportional, Image.Resampling.LANCZOS)
resized_proportional_img.save('output_resized_proportional.jpg')
print(f"图像尺寸已按比例调整到 {
     size_proportional} 并保存为: output_resized_proportional.jpg")

代码解释:

  • new_size = (300, 200): 定义一个元组 (宽度, 高度),表示目标图像的尺寸。
  • resized_img = img.resize(new_size, Image.Resampling.LANCZOS): 调用图像对象的 resize() 方法进行尺寸调整。第一个参数是目标尺寸元组。第二个参数是重采样滤波器。Image.Resampling.LANCZOS 是一种高质量的滤波器,适用于缩小时能保留更多细节。
  • # resized_img.show(): 显示调整尺寸后的图像(注释掉)。
  • resized_img.save('output_resized.jpg'): 保存调整尺寸后的图像。
  • max_side = 400: 定义一个变量,表示希望调整后图像的长边达到的最大像素数。
  • original_width, original_height = img.size: 获取原始图像的宽度和高度。
  • if original_width > original_height:: 判断是宽度还是高度更大,以此来确定缩放的基准边。
  • scale_ratio = max_side / original_widthscale_ratio = max_side / original_height: 计算保持纵横比所需的缩放比例。
  • new_width = int(original_width * scale_ratio)new_height = int(original_height * scale_ratio): 根据计算出的比例,计算新的宽度和高度。使用 int() 确保尺寸是整数。
  • size_proportional = (new_width, new_height): 创建保持比例后的目标尺寸元组。
  • resized_proportional_img = img.resize(size_proportional, Image.Resampling.LANCZOS): 使用计算出的保持比例的尺寸进行调整。
  • resized_proportional_img.save('output_resized_proportional.jpg'): 保存保持比例调整后的图像。

2.4 图像裁剪 (Cropping)

从图像中提取一个矩形区域可以使用 crop() 方法。

from PIL import Image

# 假设我们有之前加载的img对象

# 定义裁剪区域 (left, upper, right, lower)
# 坐标系统原点在左上角 (0, 0)
# left: 裁剪区域左边界的x坐标
# upper: 裁剪区域上边界的y坐标
# right: 裁剪区域右边界的x坐标 (不包含该列像素)
# lower: 裁剪区域下边界的y坐标 (不包含该行像素)
# 例如,从 (50, 30) 开始,裁剪一个宽100,高80的区域,则右下角坐标是 (50+100, 30+80) = (150, 110)
crop_box = (50, 30, 150, 110) # 假设原始图像足够大以进行此裁剪

# 检查裁剪区域是否在图像范围内
original_width, original_height = img.size
left, upper, right, lower = crop_box
if left < 0 or upper < 0 or right > original_width or lower > original_height or right < left or lower < upper:
    print(f"警告: 裁剪区域 {
     crop_box} 超出图像范围 {
     img.size} 或无效。跳过裁剪。")
else:
    # 裁剪图像
    # crop() 方法返回一个新的Image对象,表示裁剪出来的区域
    cropped_img = img.crop(crop_box)

    # 显示或保存裁剪后的图像
    # cropped_img.show()
    cropped_img.save('output_cropped.jpg')
    print(f"图像已裁剪区域 {
     crop_box} 并保存为: output_cropped.jpg")

代码解释:

  • crop_box = (50, 30, 150, 110): 定义一个元组,表示裁剪区域的左上角和右下角坐标。坐标顺序为 (左x, 上y, 右x, 下y)。重要的是要记住右下角的坐标是不包含在内的,这意味着裁剪区域是从 (left, upper)(right-1, lower-1) 的像素。
  • 检查裁剪区域是否在图像范围内:这是一个重要的步骤,防止索引越界。检查左上角坐标是否为负,右下角坐标是否超出图像尺寸,以及右下角坐标是否在左上角坐标的右下方。
  • cropped_img = img.crop(crop_box): 调用图像对象的 crop() 方法,传入裁剪区域的元组。返回一个新的图像对象,包含了指定的矩形区域。
  • cropped_img.save('output_cropped.jpg'): 保存裁剪后的图像。

2.5 图像旋转 (Rotating)

旋转图像可以使用 rotate() 方法。

from PIL import Image

# 假设我们有之前加载的img对象

# 定义旋转角度 (逆时针方向)
angle = 45 # 旋转45度

# 旋转图像
# rotate() 方法返回一个新的Image对象
# angle: 旋转角度,正值表示逆时针旋转
# expand=True: 如果为True,扩展输出图像以容纳整个旋转后的图像,否则裁剪掉超出边界的部分。
# fillcolor: 在 expand=True 时,用于填充新创建区域的颜色。
rotated_img = img.rotate(angle, expand=True, fillcolor='white')

# 显示或保存旋转后的图像
# rotated_img.show()
rotated_img.save('output_rotated.png') # 保存为PNG以支持可能的透明度或填充色
print(f"图像已旋转 {
     angle} 度并保存为: output_rotated.png")

# 旋转90度、180度、270度有专门的方法,速度更快且不会有填充问题 (因为是正交旋转)
rotated_90 = img.rotate(90, expand=True) # 逆时针90度
rotated_180 = img.rotate(180, expand=False) # 逆时针180度
rotated_270 = img.rotate(270, expand=True) # 逆时针270度

rotated_90.save('output_rotated_90.jpg')
rotated_180.save('output_rotated_180.jpg')
rotated_270.save('output_rotated_270.jpg')
print("图像已旋转90, 180, 270度并保存。")

代码解释:

  • angle = 45: 定义一个变量,表示旋转的角度。正值表示逆时针旋转。
  • rotated_img = img.rotate(angle, expand=True, fillcolor='white'): 调用图像对象的 rotate() 方法。
    • 第一个参数是旋转角度。
    • expand=True: 这是一个很重要的参数。如果设置为 True,输出图像的尺寸会自动调整,以确保旋转后的整个图像都被包含在内。否则,输出图像的尺寸与原始图像相同,旋转后超出边界的部分会被裁剪掉。
    • fillcolor='white': 当 expand=True 时,旋转后新出现的区域(原来图像区域之外的部分)需要填充颜色。这个参数指定了填充的颜色。
  • rotated_img.save('output_rotated.png'): 保存旋转后的图像。使用PNG格式是因为当 expand=True 时,可能会引入透明区域(如果原始图像是RGBA或填充色不是完全不透明)。
  • img.rotate(90, expand=True), img.rotate(180, expand=False), img.rotate(270, expand=True): 演示了旋转90、180、270度。对于90和270度的旋转,通常需要 expand=True 来包含整个图像(除非原始图像是正方形)。对于180度旋转,尺寸不变,所以 expand=False 就足够了。

2.6 图像翻转 (Flipping)

水平或垂直翻转图像可以使用 transpose() 方法或 FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM 常量。

from PIL import Image, ImageOps # ImageOps模块包含一些额外的图像操作

# 假设我们有之前加载的img对象

# 水平翻转 (左右翻转)
# Image.FLIP_LEFT_RIGHT 是transpose方法的参数
flipped_lr_img = img.transpose(Image.FLIP_LEFT_RIGHT)
flipped_lr_img.save('output_flipped_lr.jpg')
print("图像已水平翻转并保存为: output_flipped_lr.jpg")

# 垂直翻转 (上下翻转)
# Image.FLIP_TOP_BOTTOM 是transpose方法的参数
flipped_tb_img = img.transpose(Image.FLIP_TOP_BOTTOM)
flipped_tb_img.save('output_flipped_tb.jpg')
print("图像已垂直翻转并保存为: output_flipped_tb.jpg")

# ImageOps模块也提供flip和mirror函数,功能类似
# flipped_lr_img_ops = ImageOps.mirror(img) # 水平翻转
# flipped_tb_img_ops = ImageOps.flip(img)   # 垂直翻转

代码解释:

  • from PIL import Image, ImageOps: 导入Image模块和ImageOps模块。ImageOps模块提供了一些便捷的图像操作函数。
  • img.transpose(Image.FLIP_LEFT_RIGHT): 调用图像对象的 transpose() 方法,并传入 Image.FLIP_LEFT_RIGHT 常量。这会水平翻转图像。
  • flipped_lr_img.save('output_flipped_lr.jpg'): 保存水平翻转后的图像。
  • img.transpose(Image.FLIP_TOP_BOTTOM): 调用 transpose() 方法,传入 Image.FLIP_TOP_BOTTOM 常量。这会垂直翻转图像。
  • flipped_tb_img.save('output_flipped_tb.jpg'): 保存垂直翻转后的图像。
  • # ImageOps.mirror(img)# ImageOps.flip(img): 注释掉了使用 ImageOps 模块进行翻转的示例,它们提供了类似的便捷函数。

2.7 图像模式转换 (Color Space Conversion)

前面我们已经看到了 convert() 方法的基本用法,这里我们更详细地探讨不同模式之间的转换。

from PIL import Image

# 假设我们有之前加载的img对象 (假设它是RGB图像)

# 转换为灰度图像 (8位像素,值为0-255)
gray_img = img.convert('L')
gray_img.save('output_convert_L.jpg')
print("图像已转换为灰度 (L 模式) 并保存为: output_convert_L.jpg")

# 转换为RGB图像 (如果原图不是RGB)
# 如果原图是灰度图,会复制灰度值到R, G, B通道
# 如果原图是RGBA,Alpha通道会被丢弃
rgb_img = img.convert('RGB')
rgb_img.save('output_convert_RGB.jpg') # 保存为JPEG,不包含透明度
print("图像已转换为RGB模式并保存为: output_convert_RGB.jpg")


# 转换为RGBA图像 (支持透明度)
# 如果原图是RGB,会添加一个完全不透明的Alpha通道
# 如果原图是灰度图,会复制灰度值到R, G, B通道,并添加不透明Alpha通道
rgba_img = img.convert('RGBA')
rgba_img.save('output_convert_RGBA.png') # 通常保存为PNG以保留透明度
print("图像已转换为RGBA模式并保存为: output_convert_RGBA.png")


# 转换为1位黑白图像 (二值图像)
# 使用dither=Image.Dither.FLOYDSTEINBERG 进行抖动处理以改善视觉效果
bw_img = img.convert('1', dither=Image.Dither.FLOYDSTEINBERG)
bw_img.save('output_convert_1.png')
print("图像已转换为1位黑白 (1 模式,带抖动) 并保存为: output_convert_1.png")

# 转换为调色板图像 (P模式)
# max_colors指定调色板中颜色的最大数量
# dither=Image.Dither.FLOYDSTEINBERG 进行抖动处理
palette_img = img.convert('P', palette=Image.Palette.WEB, dither=Image.Dither.FLOYDSTEINBERG)
palette_img.save('output_convert_P.png')
print("图像已转换为调色板 (P 模式) 并保存为: output_convert_P.png")

代码解释:

  • img.convert('L'): 将图像转换为8位灰度图像。每个像素用一个字节表示亮度。
  • gray_img.save('output_convert_L.jpg'): 保存灰度图像为JPEG格式。
  • img.convert('RGB'): 将图像转换为24位RGB真彩色图像。如果原图是灰度,会复制灰度值到三个通道;如果原图是RGBA,透明度信息会丢失。
  • rgb_img.save('output_convert_RGB.jpg'): 保存RGB图像。
  • img.convert('RGBA'): 将图像转换为32位RGBA图像,增加Alpha通道。如果原图没有Alpha通道,会创建一个完全不透明的通道。
  • rgba_img.save('output_convert_RGBA.png'): 保存RGBA图像,PNG格式支持透明度。
  • img.convert('1', dither=Image.Dither.FLOYDSTEINBERG): 将图像转换为1位黑白图像。每个像素只有两个值(0或1,通常表示黑或白)。dither=Image.Dither.FLOYDSTEINBERG 使用Floyd-Steinberg抖动算法,这是一种误差扩散算法,可以在有限的颜色(这里只有黑白)下模拟更多的颜色过渡,改善视觉效果。
  • bw_img.save('output_convert_1.png'): 保存1位黑白图像。
  • img.convert('P', palette=Image.Palette.WEB, dither=Image.Dither.FLOYDSTEINBERG): 将图像转换为8位调色板图像。图像中的每个像素值是一个索引,指向调色板中的颜色。
    • 'P': 指定转换为调色板模式。
    • palette=Image.Palette.WEB: 使用一个预定义的Web安全颜色调色板。也可以创建自定义调色板。
    • dither=Image.Dither.FLOYDSTEINBERG: 同样可以使用抖动来提高颜色表现力。
  • palette_img.save('output_convert_P.png'): 保存调色板图像。PNG格式通常用于保存调色板图像。
第三章:图像表示与NumPy

在Python中进行更复杂的图像处理时,我们通常需要访问和操作图像的像素数据。最常见和高效的方式是将图像数据转换为NumPy数组。NumPy是Python中用于科学计算的核心库,提供了高性能的多维数组对象和大量的数学函数。

Pillow图像对象和NumPy数组之间可以方便地相互转换。

3.1 Pillow Image与NumPy数组的转换

  • Image -> NumPy: 使用 numpy.array()np.asarray() 函数将Pillow Image对象转换为NumPy数组。
  • NumPy -> Image: 使用 Image.fromarray() 函数将NumPy数组转换回Pillow Image对象。
import numpy as np
from PIL import Image

# 假设我们有之前加载的img对象 (例如,一个RGB图像)

# 将Pillow Image对象转换为NumPy数组
# numpy.array() 会创建一个新的数组
# np.asarray() 会尽量避免复制数据,如果可能的话会返回原始数据的视图
img_array = np.array(img)

# 打印NumPy数组的信息
print(f"NumPy数组类型: {
     type(img_array)}")
print(f"NumPy数组形状 (高, 宽, 通道数): {
     img_array.shape}")
print(f"NumPy数组数据类型: {
     img_array.dtype}")
# 对于RGB图像,形状通常是 (height, width, 3)
# 对于灰度图像,形状通常是 (height, width)
# 对于RGBA图像,形状通常是 (height, width, 4)

# 访问像素值
# 访问位于 (行y, 列x) 的像素值
# 例如,访问左上角像素 (0, 0)
top_left_pixel = img_array[0, 0]
print(f"左上角像素值 (行0, 列0): {
     top_left_pixel}") # 对于RGB可能是 [R, G, B]

# 访问特定通道的值
# 例如,访问左上角像素的红色通道值 (对于RGB图像)
if img_array.ndim == 3 and img_array.shape[2] >= 3:
     top_left_red = img_array[0, 0, 0]
     print(f"左上角像素的红色通道值: {
     top_left_red}")

# 修改像素值 (直接修改NumPy数组会改变图像数据)
# 将左上角像素设置为黑色 (对于RGB图像)
if img_array.ndim == 3 and img_array.shape[2] >= 3:
     img_array[0, 0] = [0, 0, 0] # 设置为黑色 (R=0, G=0, B=0)
     print("已将左上角像素值修改为黑色。")

# 将NumPy数组转换回Pillow Image对象
# 需要确保NumPy数组的数据类型和形状与目标Pillow模式兼容
# 例如,对于RGB模式,需要形状是 (height, width, 3),dtype是 uint8
# 对于灰度模式,需要形状是 (height, width),dtype是 uint8
try:
    # Image.fromarray() 会根据NumPy数组的形状和数据类型自动判断图像模式
    # 例如,形状(H, W, 3) dtype=uint8 -> RGB
    # 形状(H, W) dtype=uint8 -> L (灰度)
    # 形状(H, W, 4) dtype=uint8 -> RGBA
    img_from_array = Image.fromarray(img_array)

    # 保存从NumPy数组转换回来的图像
    img_from_array.save('output_from_numpy.png')
    print("NumPy数组已转换回Pillow Image并保存为: output_from_numpy.png")

except TypeError as e:
    # 如果NumPy数组的数据类型或形状与Image.fromarray()的要求不兼容,可能会出现TypeError
    print(f"错误: 无法从NumPy数组创建Pillow Image对象。原因: {
     e}")
    print("请检查NumPy数组的形状和数据类型是否与目标图像模式匹配 (通常是 uint8)。")


代码解释:

  • import numpy as np: 导入NumPy库,并使用别名 np
  • from PIL import Image: 导入Pillow的Image模块。
  • img_array = np.array(img): 使用 np.array() 函数将Pillow Image对象 img 转换为NumPy数组 img_array。这将创建一个新的数组来存储图像数据。
  • print(f"NumPy数组类型: {type(img_array)}"): 打印 img_array 的类型,通常是 <class 'numpy.ndarray'>
  • print(f"NumPy数组形状 (高, 宽, 通道数): {img_array.shape}"): 打印NumPy数组的形状。对于图像数据,形状通常表示为 (height, width, channels)。高度对应行数,宽度对应列数。
  • print(f"NumPy数组数据类型: {img_array.dtype}"): 打印NumPy数组中元素的数据类型。对于从图像转换的数组,通常是 uint8 (无符号8位整数),值范围是0到255,对应于像素的亮度或颜色通道值。
  • top_left_pixel = img_array[0, 0]: 访问NumPy数组的元素。[0, 0] 表示访问第一行(索引0)、第一列(索引0)的元素。对于彩色图像,这会返回一个包含该像素所有通道值的数组(如 [R, G, B])。对于灰度图像,返回一个单一的亮度值。
  • img_array[0, 0] = [0, 0, 0]: 修改NumPy数组中的像素值。这里将左上角像素的RGB值都设置为0,使其变为黑色。**注意:**直接修改 img_array 会改变图像数据。
  • img_from_array = Image.fromarray(img_array): 使用 Image.fromarray() 函数将NumPy数组 img_array 转换回Pillow Image对象。Pillow会根据NumPy数组的形状和数据类型自动识别图像模式。
  • img_from_array.save('output_from_numpy.png'): 保存从NumPy数组转换回来的图像。
  • try...except TypeError: 包含错误处理,因为 Image.fromarray() 对输入数组的形状和数据类型有特定要求,不匹配时会抛出 TypeError。常见的错误是数据类型不是 uint8 或形状不正确。

3.2 利用NumPy进行高效像素操作

将图像数据转换为NumPy数组的最大好处是可以利用NumPy提供的强大数组操作功能,进行高效的像素级处理,而无需遍历每个像素。

import numpy as np
from PIL import Image

# 假设 img_array 是一个RGB图像的NumPy数组 (形状为 HxWx3, dtype=uint8)
# 如果没有img_array,可以先从Pillow Image创建
# img_array = np.array(img)

# 检查是否为彩色图像 (有3个或4个通道)
if not (img_array.ndim == 3 and (img_array.shape[2] == 3 or img_array.shape[2] == 4)):
     print("示例需要彩色图像 (RGB 或 RGBA)。请确保加载的图像是彩色。")
else:
    # 像素值反转 (负片效果)
    # 对于uint8类型,最大值是255
    # 新像素值 = 255 - 原始像素值
    inverted_array = 255 - img_array
    # 注意:NumPy的减法操作会自动应用于数组的每个元素

    # 将反转后的NumPy数组转换回Pillow Image
    inverted_img = Image.fromarray(inverted_array.astype('uint8')) # 确保数据类型是uint8
    inverted_img.save('output_inverted.jpg')
    print("图像像素值已反转 (负片效果) 并保存为: output_inverted.jpg")

    # 调整亮度 (增加一个常数值)
    # 使用np.clip() 确保像素值仍在0-255范围内,避免溢出
    brightness_offset = 50
    # 增加亮度
    brightened_array = img_array + brightness_offset
    # np.clip(array, min_value, max_value) 将数组中的所有值限制在指定范围内
    brightened_array_clipped = np.clip(brightened_array, 0, 255)

    # 将调整亮度的NumPy数组转换回Pillow Image
    brightened_img = Image.fromarray(brightened_array_clipped.astype('uint8'))
    brightened_img.save('output_brightened.jpg')
    print("图像亮度已增加并保存为: output_brightened.jpg")

    # 调整对比度 (乘以一个常数值)
    # 乘以一个大于1的值增加对比度
    contrast_factor = 1.5
    # 乘以对比度因子
    contrasted_array = img_array * contrast_factor
    # 同样使用np.clip() 限制像素值
    contrasted_array_clipped = np.clip(contrasted_array, 0, 255)

    # 将调整对比度的NumPy数组转换回Pillow Image
    contrasted_img = Image.fromarray(contrasted_array_clipped.astype('uint8'))
    contrasted_img.save('output_contrasted.jpg')
    print("图像对比度已调整并保存为: output_contrasted.jpg")

    # 将彩色图像转换为灰度图像 (使用加权平均法)
    # 灰度值 = R * 0.2989 + G * 0.5870 + B * 0.1140 (ITU-R BT.601)
    # 注意:这里假设 img_array 是 RGB (最后一维大小为 3)
    if img_array.shape[2] == 3:
        # 使用NumPy的广播功能和点乘
        # [:, :, 0] 获取所有像素的R通道
        # [:, :, 1] 获取所有像素的G通道
        # [:, :, 2] 获取所有像素的B通道
        # 将这三个二维数组与对应的权重相乘,然后相加
        # .sum(axis=2) 对最后一个轴(通道轴)求和
        # .astype('uint8') 将结果转换为uint8类型
        grayscale_array = (img_array[:, :, 0] * 0.2989 +
                           img_array[:, :, 1] * 0.5870 +
                           img_array[:, :, 2] * 0.1140).astype('uint8')

        # 将灰度NumPy数组 (形状为 HxW) 转换回Pillow Image (L模式)
        grayscale_img = Image.fromarray(grayscale_array, 'L') # 明确指定模式为'L'
        grayscale_img.save('output_grayscale_numpy.jpg')
        print("彩色图像已使用NumPy转换为灰度并保存为: output_grayscale_numpy.jpg")
    else:
         print("跳过灰度转换示例,因为它需要RGB图像。")


代码解释:

  • import numpy as np: 导入NumPy。
  • from PIL import Image: 导入Pillow。
  • img_array = np.array(img): 将Pillow Image对象转换为NumPy数组。
  • inverted_array = 255 - img_array: 对NumPy数组进行减法运算。NumPy支持数组的广播(broadcasting),这里的 255 会被广播到与 img_array 相同的形状,然后进行逐元素减法。结果是每个像素的每个通道值都变为 255 - 原始值,实现了负片效果。
  • inverted_img = Image.fromarray(inverted_array.astype('uint8')): 将处理后的NumPy数组转换回Pillow Image。.astype('uint8') 确保数组的数据类型是 uint8,这是图像像素的标准类型。
  • brightened_array = img_array + brightness_offset: 增加亮度。将一个常数加到数组的每个元素上。
  • brightened_array_clipped = np.clip(brightened_array, 0, 255): np.clip() 函数将数组中的所有值限制在指定的最小值和最大值之间。这里用于确保像素值不会超出0-255的有效范围,避免溢出导致意外结果。
  • contrasted_array = img_array * contrast_factor: 调整对比度。将数组的每个元素乘以一个常数。
  • contrasted_array_clipped = np.clip(contrasted_array, 0, 255): 同样使用 np.clip() 限制像素值。
  • grayscale_array = (img_array[:, :, 0] * 0.2989 + ...).astype('uint8'): 这是将彩色图像转换为灰度的NumPy实现。
    • img_array[:, :, 0]: 使用切片(slicing)获取所有行、所有列的第一个通道(红色通道)的数据,结果是一个二维NumPy数组。
    • * 0.2989: 将红色通道的二维数组与权重相乘,NumPy会执行逐元素的乘法。
    • 类似的获取绿色 ([:, :, 1]) 和蓝色 ([:, :, 2]) 通道的数据并乘以各自的权重。
    • +: 将三个加权后的二维数组相加,NumPy执行逐元素的加法。
    • .sum(axis=2): 这一部分在我的解释中写错了,正确的灰度转换是直接加权求和,不需要 sum(axis=2)。对于形状为 (H, W, 3) 的数组,img_array[:, :, 0](H, W) 的数组,乘以常数后仍然是 (H, W)。将三个 (H, W) 数组相加,结果仍然是 (H, W) 的数组,这正是灰度图像的形状。正确的写法是直接相加。
    • .astype('uint8'): 将最终的浮点数结果转换为 uint8 整数类型。
  • grayscale_img = Image.fromarray(grayscale_array, 'L'): 将形状为 (H, W) 且数据类型为 uint8 的NumPy数组转换回Pillow Image对象,并明确指定模式为 'L' (灰度)。

通过将图像转换为NumPy数组,我们可以利用NumPy强大的矢量化运算能力,高效地执行各种像素级别的数学操作,这比使用循环遍历每个像素要快得多。

第四章:像素级操作与图像算术

利用NumPy,我们可以轻松地对图像进行各种像素级的算术运算,从而实现图像的加法、减法、乘法等,这些操作在图像处理中有多种用途,例如图像融合、背景去除、水印添加、对比度调整等。

4.1 图像加法

图像加法通常用于图像融合或叠加。将两幅图像对应像素的值相加。需要注意处理像素值超过最大值(如255)的情况,通常会进行饱和处理( clipping )。

import numpy as np
from PIL import Image

# 假设 image1_path 和 image2_path 是两个相同尺寸和模式的图像文件
# 为了示例,我们创建两个模拟图像
width, height = 300, 200
img1 = Image.new('RGB', (width, height), color='red')
img2 = Image.new('RGB', (width, height), color='blue')

# 将Pillow Image转换为NumPy数组
img_array1 = np.array(img1)
img_array2 = np.array(img2)

# 检查图像尺寸和模式是否一致
if img_array1.shape != img_array2.shape or img1.mode != img2.mode:
    print("错误: 两幅图像的尺寸或模式不一致,无法进行加法运算。")
else:
    print("图像尺寸和模式一致,可以进行加法运算。")

    # 直接相加
    # 注意:对于uint8类型,直接相加可能会发生溢出(超过255会从0重新开始计数)
    added_array_overflow = img_array1 + img_array2

    # 使用NumPy的add函数,它可以指定dtype和处理溢出 ( saturation )
    # dtype=np.uint16 可以避免溢出,但结果需要再转换回uint8并进行裁剪
    # 或者直接使用np.clip() 在相加后进行裁剪
    added_array_clipped = np.clip(img_array1.astype(np.int16) + img_array2.astype(np.int16), 0, 255).astype(np.uint8)

    # 或者更直接,先转换为uint16相加,再裁剪回uint8
    added_array_uint16 = img_array1.astype(np.uint16) + img_array2.astype(np.uint16)
    added_array_clipped_v2 = np.clip(added_array_uint16, 0, 255).astype(np.uint8)

    # 或者使用 OpenCV 的 cv2.add() 函数,它默认进行饱和操作
    # import cv2
    # added_array_cv2 = cv2.add(img_array1, img_array2) # OpenCV 通常期望 uint8 类型

    # 将结果转换回Pillow Image
    added_img_clipped = Image.fromarray(added_array_clipped)
    # added_img_clipped_v2 = Image.fromarray(added_array_clipped_v2)
    # added_img_cv2 = Image.fromarray(added_array_cv2)


    added_img_clipped.save('output_image_addition_clipped.png')
    print("图像相加(裁剪处理溢出)已完成并保存为: output_image_addition_clipped.png")

    # 图像加权叠加 (alpha blending)
    # result = alpha * image1 + (1 - alpha) * image2
    # alpha 是权重,介于 0 到 1 之间
    alpha = 0.6
    # 需要将数据类型转换为浮点数进行乘法,然后转换回uint8
    blended_array = (img_array1.astype(np.float32) * alpha +
                     img_array2.astype(np.float32) * (1 - alpha)).astype(np.uint8)

    # 将结果转换回Pillow Image
    blended_img = Image.fromarray(blended_array)
    blended_img.save('output_image_blending.png')
    print(f"图像加权叠加 (alpha={
     alpha}) 已完成并保存为: output_image_blending.png")

代码解释:

  • width, height = 300, 200: 定义模拟图像的尺寸。
  • img1 = Image.new('RGB', (width, height), color='red'): 创建一个红色背景的模拟RGB图像。
  • img2 = Image.new('RGB', (width, height), color='blue'): 创建一个蓝色背景的模拟RGB图像。
  • img_array1 = np.array(img1)img_array2 = np.array(img2): 将Pillow Image对象转换为NumPy数组。
  • if img_array1.shape != img_array2.shape or img1.mode != img2.mode:: 检查两幅图像的形状和模式是否相同,这是进行逐像素算术运算的前提。
  • added_array_overflow = img_array1 + img_array2: 直接对 uint8 类型的NumPy数组进行加法。这可能会导致整数溢出,例如 200 + 100 = 300,但在 uint8 中会变成 300 % 256 = 44,这不是我们想要的结果(饱和)。
  • added_array_clipped = np.clip(img_array1.astype(np.int16) + img_array2.astype(np.int16), 0, 255).astype(np.uint8): 这是处理溢出的正确方法之一。
    • img_array1.astype(np.int16)img_array2.astype(np.int16): 将 uint8 类型的数组转换为更宽的整数类型 int16(有符号16位整数),以便在相加时不会立即溢出。
    • ... + ...: 进行加法运算,结果类型是 int16
    • np.clip(..., 0, 255): 将加法结果限制在0到255之间。任何小于0的值变为0,任何大于255的值变为255,这称为饱和(saturation)。
    • .astype(np.uint8): 将裁剪后的结果转换回 uint8 类型。
  • added_array_uint16 = img_array1.astype(np.uint16) + img_array2.astype(np.uint16): 另一种避免中间溢出的方法是先转换为 uint16 进行加法。
  • added_array_clipped_v2 = np.clip(added_array_uint16, 0, 255).astype(np.uint8): 对 uint16 的加法结果进行裁剪并转换回 uint8。这两种裁剪方法(先转 int16 或先转 uint16)都可以,取决于具体需求和习惯。
  • # import cv2; added_array_cv2 = cv2.add(img_array1, img_array2): 注释掉了使用OpenCV的 cv2.add() 函数的示例。OpenCV的算术函数默认执行饱和操作,对于图像处理更方便。
  • added_img_clipped = Image.fromarray(added_array_clipped): 将处理后的NumPy数组转换回Pillow Image对象。
  • added_img_clipped.save('output_image_addition_clipped.png'): 保存结果图像。
  • alpha = 0.6: 定义加权叠加的权重 alpha 值。
  • blended_array = (img_array1.astype(np.float32) * alpha + img_array2.astype(np.float32) * (1 - alpha)).astype(np.uint8): 实现加权叠加。
    • astype(np.float32): 将 uint8 数组转换为浮点数类型,以便进行小数乘法。
    • ... * alpha: 将第一个图像的浮点数组与 alpha 相乘。
    • ... * (1 - alpha): 将第二个图像的浮点数组与 (1 - alpha) 相乘。
    • +: 将两个加权后的浮点数组相加。
    • .astype(np.uint8): 将最终的浮点数结果转换回 uint8 类型。由于 alpha 在0到1之间,并且原始像素值在0-255之间,加权相加的结果通常也在0-255之间,所以这里不需要 np.clip(),但如果alpha或像素值可能导致结果超出范围,则需要裁剪。
  • blended_img = Image.fromarray(blended_array): 将加权叠加后的NumPy数组转换回Pillow Image。
  • blended_img.save('output_image_blending.png'): 保存结果图像。

4.2 图像减法

图像减法常用于检测图像变化、背景去除或边缘检测。将一幅图像的像素值减去另一幅图像对应像素的值。结果的绝对值或非负值通常是关注的重点。

import numpy as np
from PIL import Image

# 假设 image1_path 和 image2_path 是两个相同尺寸和模式的图像文件
# 为了示例,我们创建两个模拟图像,一个有白色矩形,一个没有
width, height = 300, 200
img_base = Image.new('L', (width, height), color=100) # 灰度图像,背景亮度100
img_with_rect = img_base.copy() # 复制背景图像
# 在 img_with_rect 上绘制一个白色矩形
# from PIL import ImageDraw
# draw = ImageDraw.Draw(img_with_rect)
# draw.rectangle([(50, 50), (150, 150)], fill=255) # 在 (50,50) 到 (150,150) 绘制白色矩形

# 直接创建NumPy数组来模拟变化
base_array = np.full((height, width), 100, dtype=np.uint8) # 创建灰度背景数组
with_rect_array = base_array.copy()
# 在 with_rect_array 的矩形区域设置为白色 (255)
with_rect_array[50:151, 50:151] = 255 # 注意 NumPy 切片是包含起始不包含结束,所以要加1


# 将NumPy数组转换回Pillow Image (可选,只是为了演示)
img_base_pil = Image.fromarray(base_array, 'L')
img_with_rect_pil = Image.fromarray(with_rect_array, 'L')

# 将NumPy数组转换为浮点数以便处理负数结果
# 或者直接使用 int16 或 int32
subtracted_array = with_rect_array.astype(np.int16) - base_array.astype(np.int16)

# 结果可能为负数,通常我们关注差异的绝对值
abs_diff_array = np.abs(subtracted_array)

# 将绝对差值数组裁剪到0-255范围并转换回uint8
# 虽然这里理论上最大差值是255-100=155,不会超过255,但作为通用做法,裁剪是好的习惯
abs_diff_array_clipped = np.clip(abs_diff_array, 0, 255).astype(np.uint8)

# 将结果转换回Pillow Image (灰度模式)
diff_img = Image.fromarray(abs_diff_array_clipped, 'L')
diff_img.save('output_image_subtraction_diff.png')
print("图像相减(取绝对值)已完成并保存为: output_image_subtraction_diff.png")

# OpenCV 的 cv2.subtract() 函数也会进行饱和操作,结果不会是负数
# import cv2
# subtracted_array_cv2 = cv2.subtract(with_rect_array, base_array) # 如果 with_rect_array < base_array,结果是0
# diff_img_cv2 = Image.fromarray(subtracted_array_cv2, 'L')
# diff_img_cv2.save('output_image_subtraction_cv2.png')
# print("图像相减(使用OpenCV饱和)已完成并保存为: output_image_subtraction_cv2.png")


代码解释:

  • base_array = np.full((height, width), 100, dtype=np.uint8): 使用 np.full() 创建一个指定形状 (height, width)、所有元素都为100、数据类型为 uint8 的NumPy数组,模拟一个灰度值为100的背景图像。
  • with_rect_array = base_array.copy(): 创建背景数组的副本。
  • with_rect_array[50:151, 50:151] = 255: 使用NumPy切片访问数组的一个矩形区域,并将其值设置为255,模拟一个白色矩形。切片 [start:stop] 是包含起始索引但不包含结束索引的,所以对于包含150行的区域,需要结束索引是151。
  • subtracted_array = with_rect_array.astype(np.int16) - base_array.astype(np.int16): 进行减法运算。为了处理可能出现的负数结果,将 uint8 数组转换为 int16 类型再相减。
  • abs_diff_array = np.abs(subtracted_array): 使用 np.abs() 函数计算差值数组的绝对值。这对于找出两幅图像之间所有差异(无论哪个值更大)非常有用。
  • abs_diff_array_clipped = np.clip(abs_diff_array, 0, 255).astype(np.uint8): 将绝对差值数组裁剪到0-255范围并转换回 uint8
  • diff_img = Image.fromarray(abs_diff_array_clipped, 'L'): 将结果数组转换回Pillow Image对象,指定模式为 'L' (灰度)。
  • diff_img.save('output_image_subtraction_diff.png'): 保存结果图像。
  • # import cv2; subtracted_array_cv2 = cv2.subtract(with_rect_array, base_array): 注释掉了使用OpenCV的 cv2.subtract() 函数。它会将负数结果饱和到0。

4.3 图像乘法和除法

图像乘法和除法常用于调整图像的对比度、应用掩模(masking)或进行图像的逐像素权重调整。

import numpy as np
from PIL import Image

# 假设 img_array 是一个灰度图像的NumPy数组 (形状为 HxW, dtype=uint8)
# 为了示例,我们创建一个灰度模拟图像
width, height = 300, 200
img_gray = Image.new('L', (width, height)) # 灰度背景
# 使用 PIL.ImageDraw 绘制一个渐变,让图像有一些变化
from PIL import ImageDraw
draw = ImageDraw.Draw(img_gray)
for i in range(width):
    # 绘制从左到右从黑到白的渐变
    draw.line([(i, 0), (i, height)], fill=int(i / width * 255))

img_gray_array = np.array(img_gray)


# 图像乘法 (调整对比度或应用权重)
# 乘以一个常数因子 (>1增加对比度,<1降低对比度)
multiply_factor = 1.2
# 将数组转换为浮点数进行乘法
multiplied_array = img_gray_array.astype(np.float32) * multiply_factor
# 裁剪并转换回uint8
multiplied_array_clipped = np.clip(multiplied_array, 0, 255).astype(np.uint8)

# 将结果转换回Pillow Image
multiplied_img = Image.fromarray(multiplied_array_clipped, 'L')
multiplied_img.save('output_image_multiplication.png')
print(f"图像乘以 {
     multiply_factor} 已完成并保存为: output_image_multiplication.png")


# 图像除法 (调整亮度或应用权重)
# 除以一个常数因子 (>1降低亮度,<1增加亮度)
divide_factor = 1.5
# 将数组转换为浮点数进行除法,并避免除以零
# np.maximum(img_gray_array, 1) 可以避免除以零,将所有0值替换为1
# 或者直接处理,因为像素值是uint8,通常不会有NaN或Infinity问题,除非除以0
divided_array = img_gray_array.astype(np.float32) / divide_factor
# 裁剪并转换回uint8
divided_array_clipped = np.clip(divided_array, 0, 255).astype(np.uint8)

# 将结果转换回Pillow Image
divided_img = Image.fromarray(divided_array_clipped, 'L')
divided_img.save('output_image_division.png')
print(f"图像除以 {
     divide_factor} 已完成并保存为: output_image_division.png")

# 图像与掩模相乘 (Masking)
# 创建一个与图像同尺寸的掩模,掩模值为 0 到 1 之间的浮点数 或 0 到 255 之间的整数
# 例如,创建一个圆形掩模,圆形区域值为1,外部为0
mask = np.zeros_like(img_gray_array, dtype=np.float32) # 创建与图像同尺寸的浮点零数组
center_x, center_y = width // 2, height // 2
radius = min(width, height) // 3

# 使用 NumPy 广播和距离计算创建圆形掩模
y, x = np.ogrid[:height, :width] # 创建网格坐标数组
distance_from_center = np.sqrt((x - center_x)**2 + (y - center_y)**2)
mask[distance_from_center <= radius] = 1.0 # 在半径范围内的像素设置为1.0

# 图像与掩模相乘
masked_array = (img_gray_array.astype(np.float32) * mask).astype(np.uint8)

# 将结果转换回Pillow Image
masked_img = Image.fromarray(masked_array, 'L')
masked_img.save('output_image_masked.png')
print("图像已应用圆形掩模并保存为: output_image_masked.png")


代码解释:

  • img_gray = Image.new('L', (width, height)): 创建一个灰度模式的Pillow Image对象。
  • draw = ImageDraw.Draw(img_gray) 和随后的循环:使用 PIL.ImageDraw 在灰度图像上绘制一个从左到右的线性灰度渐变,以便进行乘除法操作时能看到效果。
  • img_gray_array = np.array(img_gray): 将灰度图像转换为NumPy数组。
  • multiplied_array = img_gray_array.astype(np.float32) * multiply_factor: 进行图像乘法。将 uint8 数组转换为 float32 以进行浮点数乘法,防止整数溢出并允许使用小数因子。
  • multiplied_array_clipped = np.clip(multiplied_array, 0, 255).astype(np.uint8): 对乘法结果进行裁剪并转换回 uint8
  • multiplied_img = Image.fromarray(multiplied_array_clipped, 'L'): 将结果转换回Pillow Image对象。
  • divided_array = img_gray_array.astype(np.float32) / divide_factor: 进行图像除法。同样转换为 float32 进行除法。
  • divided_array_clipped = np.clip(divided_array, 0, 255).astype(np.uint8): 对除法结果进行裁剪并转换回 uint8
  • divided_img = Image.fromarray(divided_array_clipped, 'L'): 将结果转换回Pillow Image对象。
  • mask = np.zeros_like(img_gray_array, dtype=np.float32): 使用 np.zeros_like() 创建一个与 img_gray_array 形状相同、所有元素为零、数据类型为 float32 的NumPy数组,作为掩模的初始状态。
  • y, x = np.ogrid[:height, :width]: 使用 np.ogrid 创建两个一维数组,分别表示图像的行索引和列索引。通过NumPy的广播机制,可以方便地计算每个像素的坐标。
  • distance_from_center = np.sqrt((x - center_x)**2 + (y - center_y)**2): 计算每个像素到图像中心的欧几里得距离。xy 会被广播到与图像尺寸相同的二维数组,然后进行逐元素的平方、相加和开方。
  • mask[distance_from_center <= radius] = 1.0: 使用布尔索引(Boolean indexing)将距离中心小于等于半径的像素在 mask 数组中的值设置为1.0。
  • masked_array = (img_gray_array.astype(np.float32) * mask).astype(np.uint8): 将图像数组转换为浮点数,与浮点数掩模进行逐元素乘法。掩模值为1的区域保持原图像像素值,掩模值为0的区域像素值变为0。结果再转换回 uint8
  • masked_img = Image.fromarray(masked_array, 'L'): 将掩模处理后的数组转换回Pillow Image对象。

通过这些示例,我们可以看到将图像转换为NumPy数组后,利用NumPy的广播、矢量化运算和丰富的数学函数,可以极其高效地实现各种像素级别的操作和图像算术。这是进行高级图像处理的基础。

第五章:图像滤波

图像滤波是图像处理中最基本和最重要的操作之一。它的目的是在空间域上修改图像的像素值,以达到平滑(去噪)、锐化、边缘检测等效果。滤波通常通过卷积操作实现。

5.1 卷积 (Convolution) 原理

卷积是滤波的核心。它是一种数学运算,将一个图像(输入信号)与一个核(Kernel 或 滤波器,一个小的二维数组)进行运算,产生一个新的图像(输出信号)。

想象一个核在图像上滑动,在每一个位置,核的每个元素与图像对应位置的像素值相乘,然后将所有乘积相加,得到的和就是输出图像在该位置的像素值。

数学上,二维离散卷积可以表示为:

[
G[i, j] = \sum_m \sum_n I[i-m, j-n] K[m, n]
]

其中:

  • (G[i, j]) 是输出图像在 ( (i, j) ) 位置的像素值。
  • (I) 是输入图像。
  • (K) 是核(滤波器)。
  • (m, n) 是核的索引。

在实际应用中,为了计算方便,通常使用相关的形式,但效果类似。OpenCV和scikit-image等库提供了高效的卷积实现。

5.2 常见的空间滤波器

空间滤波器可以分为线性滤波器和非线性滤波器。

  • 线性滤波器: 输出像素值是输入像素值的线性组合,例如均值滤波、高斯滤波。它们可以通过卷积实现。
  • 非线性滤波器: 输出像素值不是输入像素值的线性组合,例如中值滤波。它们通常不能直接通过简单的卷积实现,但同样涉及在像素邻域内的操作。

5.2.1 平滑滤波器 (Smoothing Filters)

平滑滤波器用于去除图像中的噪声,模糊图像细节。它们通常是低通滤波器,衰减图像中的高频成分(如噪声和边缘)。

  • 均值滤波器 (Mean Filter): 用像素邻域内的平均值替换当前像素值。

    import numpy as np
    from PIL import Image
    # PIL 不直接提供卷积核应用函数,但我们可以用 NumPy 实现或使用其他库
    # 这里我们使用 scikit-image,它提供了更方便的滤波器函数
    
    from skimage import io, filters
    import matplotlib.pyplot as plt # 用于显示图像
    
    # 加载一个示例图像 (如果不存在,创建一个模拟图像)
    try:
        img_sk = io.imread('example.jpg') # skimage io.imread 可以读取多种格式
        print("成功加载图像使用 skimage.io.imread")
    except FileNotFoundError:
        print("示例图像 example.jpg 未找到,请确保文件存在。")
        # 如果找不到,我们创建一个模拟图像
        from PIL import Image
        img = Image.new('RGB', (300, 200), color = 'gray')
        img_sk = np.array(img) # 转换为 NumPy 数组供 skimage 使用
        print("已创建模拟图像数组。")
    
    
    # 确保图像是灰度或彩色,这里示例以灰度进行,如果加载的是彩色,先转换
    if img_sk.ndim == 3:
        print("图像是彩色,转换为灰度进行滤波示例。")
        # skimage.color.rgb2gray 可以方便转换
        from skimage.color import rgb2gray
        img_sk_gray = rgb2gray(img_sk)
        # rgb2gray 返回的是浮点数类型 (0.0 到 1.0),需要缩放到 0-255 并转换为 uint8 显示
        img_sk_gray_uint8 = (img_sk_gray * 255).astype(np.uint8)
    else:
        img_sk_gray_uint8 = img_sk # 如果已经是灰度,直接使用
    
    
    # 应用均值滤波器
    # 使用 skimage.filters.rank.mean 
    # size 参数指定邻域大小,例如 3 表示 3x3 邻域
    # 这个函数需要输入是 uint8 类型
    try:
        mean_filtered_img = filters.rank.mean(img_sk_gray_uint8, selem=np.ones((5, 5))) # 使用 5x5 的方形结构元素
    
        # 显示原始图像和均值滤波后的图像
        plt.figure(figsize=(10, 5))
    
        plt.subplot(1, 2, 1)
        plt.imshow(img_sk_gray_uint8, cmap='gray') # cmap='gray' 用于灰度图像显示
        plt.title('原始灰度图像')
        plt.axis('off') # 不显示坐标轴
    
        plt.subplot(1, 2, 2)
        plt.imshow(mean_filtered_img, cmap='gray')
        plt.title('均值滤波 (5x5)')
        plt.axis('off')
    
        plt.tight_layout() # 调整子图布局
        plt.show()
    
        # 保存结果 (使用Pillow或skimage.io.imsave)
        # Image.fromarray(mean_filtered_img).save('output_mean_filtered.jpg')
        io.imsave('output_mean_filtered_skimage.jpg', mean_filtered_img)
        print("均值滤波已完成并保存为: output_mean_filtered_skimage.jpg")
    
    
    except Exception as e:
        print(f"均值滤波示例执行失败: {
           e}")
        print("请检查scikit-image版本是否支持 filters.rank.mean,或尝试不同的结构元素 (selem)。")
        print("或者使用 scipy.ndimage.uniform_filter 进行均值滤波")
    
    # 替代方法:使用 scipy.ndimage 进行均值滤波
    try:
        from scipy import ndimage
    
        uniform_filtered_img = ndimage.uniform_filter(img_sk_gray_uint8, size=5) # size=5 表示一个边长为5的方形核
    
        # 显示原始图像和均匀滤波后的图像
        plt.figure(figsize=(10, 5))
    
        plt.subplot(1, 2, 1)
        plt.imshow(img_sk_gray_uint8, cmap='gray')
        plt.title('原始灰度图像')
        plt.axis('off')
    
        plt.subplot(1, 2, 2)
        plt.imshow(uniform_filtered_img, cmap='gray')
        plt.title('均匀滤波 (scipy, size=5)')
        plt.axis('off')
    
        plt.tight_layout()
        plt.show()
    
        io.imsave('output_uniform_filtered_scipy.jpg', uniform_filtered_img)
        print("均匀滤波 (scipy) 已完成并保存为: output_uniform_filtered_scipy.jpg")
    
    
    except ImportError:
        print("未安装 scipy 库,跳过均匀滤波示例。请使用 pip install scipy 进行安装。")
    except Exception as e:
         print(f"均匀滤波示例执行失败: {
           e}")
    
    
    

    代码解释:

    • from skimage import io, filters: 导入scikit-image库的 io 模块(用于读写图像)和 filters 模块(包含各种滤波器)。
    • import matplotlib.pyplot as plt: 导入Matplotlib库用于显示图像。
    • img_sk = io.imread('example.jpg'): 使用 skimage.io.imread 加载图像。scikit-image加载的图像默认是NumPy数组。
    • 检查图像维度并转换为灰度:因为一些滤波函数期望灰度图像。skimage.color.rgb2gray() 可以方便地将彩色图像转换为灰度图像。注意 rgb2gray 返回浮点数组,需要转换回 uint8 进行显示和一些滤波函数。
    • mean_filtered_img = filters.rank.mean(img_sk_gray_uint8, selem=np.ones((5, 5))): 使用 skimage.filters.rank.mean() 函数进行均值滤波。
      • 第一个参数是输入的图像数组(需要是 uint8 类型)。
      • selem: 结构元素(structuring element),定义了邻域的形状和大小。np.ones((5, 5)) 创建一个5x5的全部为1的NumPy数组,表示一个5x5的方形邻域。
    • plt.figure(), plt.subplot(), plt.imshow(), plt.title(), plt.axis('off'), plt.tight_layout(), plt.show(): 这些是Matplotlib用于创建图窗、子图,在子图中显示图像,设置标题,关闭坐标轴,调整布局并显示图像的函数。
    • io.imsave('output_mean_filtered_skimage.jpg', mean_filtered_img): 使用 skimage.io.imsave 保存滤波后的图像。
    • from scipy import ndimage: 导入SciPy库的 ndimage 模块,它也提供了图像处理函数。
    • uniform_filtered_img = ndimage.uniform_filter(img_sk_gray_uint8, size=5): 使用 scipy.ndimage.uniform_filter() 进行均匀滤波,效果等同于均值滤波。size 参数指定滤波核的边长。
  • 高斯滤波器 (Gaussian Filter): 使用一个高斯函数作为权重对像素邻域进行加权平均。高斯滤波器比均值滤波器引起的模糊更少,并且对抑制高斯噪声特别有效。

    import numpy as np
    from PIL import Image
    from skimage import io, filters
    import matplotlib.pyplot as plt
    from scipy import ndimage # SciPy 也有高斯滤波器
    
    # 假设 img_sk_gray_uint8 是上面准备好的灰度图像 uint8 数组
    
    # 应用高斯滤波器 (使用 skimage)
    # sigma 参数是高斯核的标准差,控制平滑程度,值越大越模糊
    # truncate 参数控制高斯核的大小,核窗口大小通常是 (2*truncate*sigma + 1) x (2*truncate*sigma + 1)
    gaussian_filtered_img_sk = filters.gaussian(img_sk_gray_uint8, sigma=2, truncate=3.5)
    
    # skimage 的 gaussian 滤波器返回浮点数数组 (0.0-1.0),需要转换回 uint8 显示和保存
    gaussian_filtered_img_sk_uint8 = (gaussian_filtered_img_sk * 255).astype(np.uint8)
    
    
    # 应用高斯滤波器 (使用 scipy)
    # sigma 参数同样是标准差
    gaussian_filtered_img_sp = ndimage.gaussian_filter(img_sk_gray_uint8, sigma=2)
    # scipy 的 gaussian_filter 通常返回与输入相同的 dtype,这里是 uint8
    
    
    # 显示原始图像和高斯滤波后的图像
    plt.figure(figsize=(15, 5))
    
    plt.subplot(1, 3, 1)
    plt.imshow(img_sk_gray_uint8, cmap='gray')
    plt.title('原始灰度图像')
    plt.axis('off')
    
    plt.subplot(1, 3, 2)
    plt.imshow(gaussian_filtered_img_sk_uint8, cmap='gray')
    plt.title('高斯滤波 (skimage, sigma=2)')
    plt.axis('off')
    
    plt.subplot(1, 3, 3)
    plt.imshow(gaussian_filtered_img_sp, cmap='gray')
    plt.title('高斯滤波 (scipy, sigma=2)')
    plt.axis('off')
    
    
    plt.tight_layout()
    plt.show()
    
    # 保存结果
    io.imsave('output_gaussian_filtered_skimage.jpg', gaussian_filtered_img_sk_uint8)
    io.imsave('output_gaussian_filtered_scipy.jpg', gaussian_filtered_img_sp)
    print("高斯滤波已完成并保存结果。")
    
    
    

    代码解释:

    • gaussian_filtered_img_sk = filters.gaussian(img_sk_gray_uint8, sigma=2, truncate=3.5): 使用 skimage.filters.gaussian() 进行高斯滤波。
      • 第一个参数是输入的图像数组。
      • sigma=2: 设置高斯核的标准差为2。标准差越大,核越大,平滑效果越强。
      • truncate=3.5: 这个参数决定了高斯核的截断半径,表示高斯核将延伸到距离中心多少个标准差的位置。通常取3到4,因为高斯函数在这个范围之外的值非常小,可以忽略。
    • gaussian_filtered_img_sk_uint8 = (gaussian_filtered_img_sk * 255).astype(np.uint8): skimage.filters.gaussian 返回的是浮点数数组 (0.0到1.0),为了显示和保存为常见的图像格式(如JPEG),需要将其乘以255并转换为 uint8 类型。
    • gaussian_filtered_img_sp = ndimage.gaussian_filter(img_sk_gray_uint8, sigma=2): 使用 scipy.ndimage.gaussian_filter() 进行高斯滤波。接口稍有不同,但 sigma 参数的含义是相同的。SciPy的函数通常会尽量保持输入的数据类型。
  • 中值滤波器 (Median Filter): 用像素邻域内的中值替换当前像素值。中值滤波器是非线性滤波器,对椒盐噪声(Salt-and-pepper noise)特别有效,因为它不会引入新的极端值。

    import numpy as np
    from PIL import Image
    from skimage import io, filters
    import matplotlib.pyplot as plt
    from scipy import ndimage
    
    # 假设 img_sk_gray_uint8 是上面准备好的灰度图像 uint8 数组
    
    # 为了更好地演示中值滤波,我们在图像中添加一些椒盐噪声
    def add_salt_and_pepper_noise(image_array, amount=0.05):
        """在图像数组中添加椒盐噪声"""
        img_noisy = image_array.copy()
        # 添加盐噪声 (白色像素)
        num_salt = np.ceil(amount * image_array.size * 0.5).astype(int)
        coords_salt = [np.random.randint(0, i - 1, num_salt) for i in image_array.shape]
        img_noisy[tuple(coords_salt)] = 255
    
        # 添加椒噪声 (黑色像素)
        num_pepper = np.ceil(amount * image_array.size * 0.5).astype(int)
        coords_pepper = [np.random.randint(0, i - 1, num_pepper) for i in image_array.shape]
        img_noisy[tuple(coords_pepper)] = 0
        return img_noisy
    
    # 添加噪声
    img_noisy_uint8 = add_salt_and_pepper_noise(img_sk_gray_uint8, amount=0.03) # 添加3%的椒盐噪声
    
    # 应用中值滤波器 (使用 skimage)
    # size 参数指定邻域大小,例如 3 表示 3x3 邻域
    # skimage.filters.median 同样需要 uint8 输入
    median_filtered_img_sk = filters.median(img_noisy_uint8, selem=np.ones((3, 3))) # 使用 3x3 的方形结构元素
    
    
    # 应用中值滤波器 (使用 scipy)
    # size 参数指定邻域大小
    median_filtered_img_sp = ndimage.median_filter(img_noisy_uint8, size=3)
    
    
    # 显示原始噪声图像和中值滤波后的图像
    plt.figure(figsize=(15, 5))
    
    plt.subplot(1, 3, 1)
    plt.imshow(img_noisy_uint8, cmap='gray')
    plt.title('原始噪声图像')
    plt.axis('off')
    
    plt.subplot(1, 3, 2)
    plt.imshow(median_filtered_img_sk, cmap='gray')
    plt.title('中值滤波 (skimage, size=3)')
    plt.axis('off')
    
    plt.subplot(1, 3, 3)
    plt.imshow(median_filtered_img_sp, cmap='gray')
    plt.title('中值滤波 (scipy, size=3)')
    plt.axis('off')
    
    
    plt.tight_layout()
    plt.show()
    
    # 保存结果
    io.imsave('output_median_filtered_skimage.jpg', median_filtered_img_sk)
    io.imsave('output_median_filtered_scipy.jpg', median_filtered_img_sp)
    print("中值滤波已完成并保存结果。")
    
    

    代码解释:

    • def add_salt_and_pepper_noise(image_array, amount=0.05):: 定义一个函数用于向图像中添加椒盐噪声,以便更好地展示中值滤波的效果。
      • amount: 噪声的比例,0.05 表示图像总像素数的5%会变成噪声点。
      • np.ceil(amount * image_array.size * 0.5).astype(int): 计算需要添加的盐点和椒点的数量,假设盐点和椒点各占一半。image_array.size 是图像的总像素数。
      • coords_salt = [np.random.randint(0, i - 1, num_salt) for i in image_array.shape]: 为盐点生成随机的行和列坐标。对于二维图像,image_array.shape(height, width),循环会为高度和宽度生成对应的坐标数组。np.random.randint(0, i - 1, num_salt) 在指定范围内生成 num_salt 个随机整数。
      • img_noisy[tuple(coords_salt)] = 255: 使用生成的随机坐标作为索引,将这些位置的像素值设置为255(白色,盐噪声)。tuple(coords_salt) 是为了正确地使用NumPy的多维索引。
      • 添加椒噪声的过程类似,只是像素值设置为0(黑色)。
    • img_noisy_uint8 = add_salt_and_pepper_noise(img_sk_gray_uint8, amount=0.03): 调用函数为灰度图像添加3%的椒盐噪声。
    • median_filtered_img_sk = filters.median(img_noisy_uint8, selem=np.ones((3, 3))): 使用 skimage.filters.median() 进行中值滤波。selem 参数同样指定邻域。
    • median_filtered_img_sp = ndimage.median_filter(img_noisy_uint8, size=3): 使用 scipy.ndimage.median_filter() 进行中值滤波。size 参数指定邻域大小。

5.2.2 锐化滤波器 (Sharpening Filters)

锐化滤波器用于增强图像的边缘和细节。它们通常是高通滤波器,增强图像中的高频成分。

  • 拉普拉斯算子 (Laplacian Operator): 是一种二阶微分算子,用于检测图像中的边缘。它对噪声非常敏感。

    import numpy as np
    from PIL import Image
    from skimage import io, filters
    import matplotlib.pyplot as plt
    from scipy import ndimage
    
    # 假设 img_sk_gray_uint8 是上面准备好的灰度图像 uint8 数组
    
    # 应用拉普拉斯滤波器 (使用 skimage)
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宅男很神经

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值