使用python进行图像处理—像素级操作与图像算术(4)

利用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):计算每个像素到图像中心的欧几里得距离。x和y会被广播到与图像尺寸相同的二维数组,然后进行逐元素的平方、相加和开方。
  • 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的广播、矢量化运算和丰富的数学函数,可以极其高效地实现各种像素级别的操作和图像算术。这是进行高级图像处理的基础。

相关推荐
RockyRich6 分钟前
突然无法调用scikit-learn、xgboost
python·机器学习·scikit-learn
真的很上进6 分钟前
2025最全TS手写题之partial/Omit/Pick/Exclude/Readonly/Required
java·前端·vue.js·python·算法·react·html5
随意02314 分钟前
STL 1 容器
开发语言·c++
敲键盘的小夜猫1 小时前
大模型智能体核心技术:CoT与ReAct深度解析
人工智能·python
华科云商xiao徐1 小时前
Python利用Scrapy框架部署分布式爬虫
python·scrapy
小前端大牛马1 小时前
java教程笔记(十四)-线程池
java·笔记·python
南瓜胖胖1 小时前
【R语言编程——数据调用】
开发语言·r语言
老歌老听老掉牙1 小时前
旋量理论:刚体运动的几何描述与机器人应用
python·算法·机器学习·机器人·旋量
henreash1 小时前
C# dll版本冲突解决方案
开发语言·c#
黎䪽圓2 小时前
【Java多线程从青铜到王者】单例设计模式(八)
java·开发语言·设计模式