图像变换是指改变图像的几何形状或空间位置的操作。常见的几何变换包括平移、旋转、缩放、剪切(shear)以及更复杂的仿射变换和透视变换。这些变换在图像配准、图像校正、创建特效等场景中非常有用。
6.1仿射变换(Affine Transformation)
仿射变换是一种线性变换,它可以表示为矩阵乘法和平移的组合。在二维图像中,一个仿射变换可以由一个2x3的矩阵表示:
其中 ( (x, y) ) 是原始图像中的点坐标,( (x’, y’) ) 是变换后图像中的点坐标。这个矩阵可以实现平移、旋转、缩放、剪切的任意组合。
在Pillow中,可以使用 img.transform(size, method, data, filter) 方法进行仿射变换。其中 method 是 Image.AFFINE,data 是一个包含六个浮点数的元组 ( (a_{11}, a_{12}, b_1, a_{21}, a_{22}, b_2) )。
其中 ( (x, y) ) 是原始图像中的点坐标,( (x’, y’) ) 是变换后图像中的点坐标。这个矩阵可以实现平移、旋转、缩放、剪切的任意组合。
在Pillow中,可以使用 img.transform(size, method, data, filter) 方法进行仿射变换。其中 method 是 Image.AFFINE,data 是一个包含六个浮点数的元组 ( (a_{11}, a_{12}, b_1, a_{21}, a_{22}, b_2) )。
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
# 假设我们有一个示例图像 (例如,上面使用的 example.jpg 或模拟图像)
try:
img = Image.open('example.jpg').convert('RGB') # 确保是RGB模式
print("成功加载图像使用 Pillow Image.open")
except FileNotFoundError:
print("示例图像 example.jpg 未找到。")
# 创建一个模拟图像
img = Image.new('RGB', (400, 300), color = 'lightblue')
from PIL import ImageDraw
draw = ImageDraw.Draw(img)
draw.text((50, 50), "Hello, Affine!", fill='black', font_size=30)
print("已创建模拟图像。")
# 定义仿射变换矩阵的六个参数 (a11, a12, b1, a21, a22, b2)
# 示例 1: 平移 (向右平移 50 像素,向下平移 30 像素)
# 变换矩阵参数: (1, 0, tx, 0, 1, ty)
tx = 50
ty = 30
affine_params_translate = (1, 0, tx, 0, 1, ty)
# 应用仿射变换
# size: 输出图像的尺寸 (width, height)
# method: Image.AFFINE
# data: 仿射变换参数元组
# filter: 重采样滤波器
translated_img = img.transform(img.size, Image.AFFINE, affine_params_translate, Image.Resampling.BILINEAR)
# 示例 2: 旋转 (逆时针旋转 30 度)
# 旋转矩阵参数: (cos(theta), -sin(theta), 0, sin(theta), cos(theta), 0)
theta_deg = 30
theta_rad = np.deg2rad(theta_deg) # 将角度转换为弧度
cos_theta = np.cos(theta_rad)
sin_theta = np.sin(theta_rad)
# 如果绕原点旋转,b1和b2为0
affine_params_rotate_origin = (cos_theta, -sin_theta, 0, sin_theta, cos_theta, 0)
# 绕图像中心旋转需要额外的平移步骤,或者调整变换矩阵
# 假设图像中心是 (cx, cy)
# 变换步骤:平移中心到原点 -> 旋转 -> 平移回中心
# 对应的仿射矩阵参数可以通过矩阵乘法计算得到
# 更简单的实现是先计算好中心点,然后构建变换参数
# 平移到原点: (1, 0, -cx, 0, 1, -cy)
# 旋转: (cos, -sin, 0, sin, cos, 0)
# 平移回中心: (1, 0, cx, 0, 1, cy)
# 复合变换矩阵 = [平移回中心] * [旋转] * [平移到原点]
# 计算过程比较繁琐,Pillow的 rotate() 方法更常用,但这里演示 affine 的灵活性
# 应用绕原点旋转的仿射变换
rotated_img_affine = img.transform(img.size, Image.AFFINE, affine_params_rotate_origin, Image.Resampling.BILINEAR)
# 注意:绕原点旋转可能会导致图像部分移出画布,需要调整输出尺寸
# 示例 3: 缩放 (x 方向缩放 1.2 倍,y 方向缩放 0.8 倍)
# 变换矩阵参数: (sx, 0, 0, 0, sy, 0)
sx = 1.2
sy = 0.8
affine_params_scale = (sx, 0, 0, 0, sy, 0)
# 应用缩放仿射变换
scaled_img_affine = img.transform(img.size, Image.AFFINE, affine_params_scale, Image.Resampling.BILINEAR)
# 示例 4: 剪切 (x 方向剪切,y 不变)
# 变换矩阵参数: (1, shx, 0, shy, 1, 0)
shx = 0.5 # x 方向剪切因子
shy = 0 # y 方向剪切因子 (这里 y 不变)
affine_params_shear = (1, shx, 0, shy, 1, 0)
# 应用剪切仿射变换
sheared_img_affine = img.transform(img.size, Image.AFFINE, affine_params_shear, Image.Resampling.BILINEAR)
# 显示结果
plt.figure(figsize=(15, 10))
plt.subplot(2, 3, 1)
plt.imshow(img)
plt.title('原始图像')
plt.axis('off')
plt.subplot(2, 3, 2)
plt.imshow(translated_img)
plt.title(f'平移 ({tx}, {ty})')
plt.axis('off')
plt.subplot(2, 3, 3)
plt.imshow(rotated_img_affine) # 注意:绕原点旋转,可能部分移出
plt.title(f'仿射旋转 ({theta_deg} 度, 绕原点)')
plt.axis('off')
plt.subplot(2, 3, 4)
plt.imshow(scaled_img_affine)
plt.title(f'缩放 ({sx}x, {sy}y)')
plt.axis('off')
plt.subplot(2, 3, 5)
plt.imshow(sheared_img_affine)
plt.title(f'剪切 (shx={shx})')
plt.axis('off')
plt.tight_layout()
plt.show()
# 保存结果
translated_img.save('output_affine_translated.png')
rotated_img_affine.save('output_affine_rotated_origin.png')
scaled_img_affine.save('output_affine_scaled.png')
sheared_img_affine.save('output_affine_sheared.png')
print("仿射变换示例已完成并保存结果。")
代码解释:
- img = Image.open('example.jpg').convert('RGB'):加载图像并确保它是RGB模式,因为一些变换可能对模式敏感。
- img.transform(size, method, data, filter):这是Pillow中执行各种变换的通用方法。
- size:输出图像的尺寸元组(width, height)。可以保持原尺寸,也可以根据需要放大以容纳整个变换后的图像。
- method=Image.AFFINE:指定变换方法为仿射变换。
- data:一个包含6个浮点数的元组(a11, a12, b1, a21, a22, b2),对应仿射变换矩阵的前两行。
- filter=Image.Resampling.BILINEAR:指定重采样滤波器。在图像变换后,新的像素位置可能落在原始像素之间,需要通过重采样(插值)来确定新像素的值。BILINEAR是双线性插值,提供了较好的质量和速度平衡。其他选项如NEAREST (最近邻插值,速度最快,质量最低)和LANCZOS (高质量滤波器,速度较慢)适用于不同场景。
- affine_params_translate = (1, 0, tx, 0, 1, ty):平移变换的仿射参数。a11=1, a22=1保持缩放不变,a12=0, a21=0保持剪切不变,b1=tx, b2=ty实现平移。
- theta_rad = np.deg2rad(theta_deg):将角度从度转换为弧度,因为三角函数通常使用弧度。
- cos_theta = np.cos(theta_rad)和sin_theta = np.sin(theta_rad):计算旋转所需的正弦和余弦值。
- affine_params_rotate_origin = (cos_theta, -sin_theta, 0, sin_theta, cos_theta, 0):绕原点逆时针旋转的仿射参数。
- affine_params_scale = (sx, 0, 0, 0, sy, 0):缩放变换的仿射参数。a11=sx, a22=sy实现x和y方向的缩放,其他参数为0保持不变形和不平移。
- affine_params_shear = (1, shx, 0, shy, 1, 0):剪切变换的仿射参数。shx控制x方向的剪切,shy控制y方向的剪切。
- 使用Matplotlib显示原始图像和各种仿射变换后的图像。
通过调整这六个参数,我们可以组合实现复杂的仿射变换。
在OpenCV中,仿射变换通常通过cv2.getAffineTransform()计算由三个对应点对确定的仿射变换矩阵,然后使用cv2.warpAffine()应用变换。
import numpy as np
import cv2 # 导入OpenCV库
import matplotlib.pyplot as plt
# 假设我们有一个示例图像 (加载为OpenCV格式)
# OpenCV 默认使用 BGR 模式加载彩色图像
try:
img_cv = cv2.imread('example.jpg')
if img_cv is None:
raise FileNotFoundError
print("成功加载图像使用 cv2.imread")
except FileNotFoundError:
print("示例图像 example.jpg 未找到。")
# 创建一个模拟图像 (灰度)
img_cv = np.full((300, 400), 150, dtype=np.uint8) # 灰度背景 150
# cv2.putText(img_cv, "Hello, Affine!", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2) # 黑色文字
print("已创建模拟灰度图像。")
# OpenCV 图像通常是 NumPy 数组
rows, cols = img_cv.shape[:2] # 获取图像高度和宽度
# 示例 1: 平移 (向右平移 50 像素,向下平移 30 像素)
tx = 50
ty = 30
# 平移矩阵 M: [[1, 0, tx], [0, 1, ty]]
M_translate = np.float32([[1, 0, tx],
[0, 1, ty]]) # 必须是 float32 类型
# 应用仿射变换 (cv2.warpAffine)
# src: 输入图像
# M: 2x3 变换矩阵 (float32 类型)
# dsize: 输出图像尺寸 (width, height)
# flags: 插值方法 (cv2.INTER_LINEAR, cv2.INTER_NEAREST, etc.)
# borderMode: 边界处理方式 (cv2.BORDER_CONSTANT, cv2.BORDER_REPLICATE, etc.)
# borderValue: 边界填充值 (borderMode=cv2.BORDER_CONSTANT 时使用)
translated_img_cv = cv2.warpAffine(img_cv, M_translate, (cols, rows)) # 输出尺寸通常与原图相同,部分会移出
# 如果需要扩展输出尺寸以包含整个平移后的图像
# new_width = cols + abs(tx)
# new_height = rows + abs(ty)
# translated_img_cv_expanded = cv2.warpAffine(img_cv, M_translate, (new_width, new_height))
# 示例 2: 旋转 (逆时针旋转 30 度,绕中心点)
angle_deg = 30
# 获取旋转矩阵
# cv2.getRotationMatrix2D(center, angle, scale)
# center: 旋转中心点 (x, y)
# angle: 旋转角度 (以度为单位,正值表示逆时针旋转)
# scale: 缩放因子 (1.0 表示不缩放)
center = (cols // 2, rows // 2)
M_rotate = cv2.getRotationMatrix2D(center, angle_deg, 1.0)
# 应用旋转仿射变换
rotated_img_cv = cv2.warpAffine(img_cv, M_rotate, (cols, rows)) # 保持原尺寸,部分会裁剪
# 如果需要扩展尺寸以包含整个旋转后的图像
# 可以通过计算旋转后图像的四个角点的新坐标,然后确定新的边界框来获取新的尺寸
# cv2.warpAffine 提供了额外的输出尺寸计算功能,但手动计算更灵活
# 例如,计算新尺寸以便完整包含旋转后的图像:
# (x, y) 是原始图像的四个角点坐标
# (x', y') = M * [x, y, 1].T
# 找到 (x', y') 的 min/max x 和 y 坐标,确定新的边界框和尺寸
cos = np.abs(M_rotate[0, 0]) # cos(angle)
sin = np.abs(M_rotate[0, 1]) # sin(angle)
new_width = int(rows * sin + cols * cos)
new_height = int(rows * cos + cols * sin)
# 调整平移分量以将整个图像移到新图像的中心
M_rotate[0, 2] += (new_width / 2) - center[0]
M_rotate[1, 2] += (new_height / 2) - center[1]
rotated_img_cv_expanded = cv2.warpAffine(img_cv, M_rotate, (new_width, new_height))
# 示例 3: 缩放 (x 和 y 方向都缩放 1.5 倍)
M_scale = np.float32([[1.5, 0, 0],
[0, 1.5, 0]])
# 应用缩放仿射变换
# 注意:这里需要指定新的输出尺寸
scaled_img_cv = cv2.warpAffine(img_cv, M_scale, (int(cols * 1.5), int(rows * 1.5)))
# 示例 4: 使用三对对应点进行仿射变换
# 定义原始图像的三个点和它们在目标图像中的对应位置
# points_original: [[x1, y1], [x2, y2], [x3, y3]]
# points_target: [[x1', y1'], [x2', y2'], [x3', y3']]
pts1 = np.float32([[50, 50], [200, 50], [50, 200]]) # 原始图像的三个点
pts2 = np.float32([[10, 100], [200, 50], [100, 250]]) # 目标图像中对应的三个点
# 计算仿射变换矩阵 M
M_points = cv2.getAffineTransform(pts1, pts2)
# 应用基于点的仿射变换
transformed_img_cv = cv2.warpAffine(img_cv, M_points, (cols, rows)) # 输出尺寸可以调整
# 显示结果
plt.figure(figsize=(15, 10))
plt.subplot(2, 3, 1)
plt.imshow(cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)) # OpenCV是BGR,用Matplotlib显示需要转RGB
plt.title('原始图像')
plt.axis('off')
plt.subplot(2, 3, 2)
plt.imshow(cv2.cvtColor(translated_img_cv, cv2.COLOR_BGR2RGB))
plt.title('OpenCV 平移')
plt.axis('off')
plt.subplot(2, 3, 3)
plt.imshow(cv2.cvtColor(rotated_img_cv_expanded, cv2.COLOR_BGR2RGB)) # 显示扩展尺寸的旋转结果
plt.title('OpenCV 旋转 (绕中心)')
plt.axis('off')
plt.subplot(2, 3, 4)
plt.imshow(cv2.cvtColor(scaled_img_cv, cv2.COLOR_BGR2RGB))
plt.title('OpenCV 缩放')
plt.axis('off')
plt.subplot(2, 3, 5)
plt.imshow(cv2.cvtColor(transformed_img_cv, cv2.COLOR_BGR2RGB))
plt.title('OpenCV 基于点变换')
plt.axis('off')
plt.tight_layout()
plt.show()
# 保存结果 (OpenCV保存图像)
cv2.imwrite('output_cv_affine_translated.jpg', translated_img_cv)
cv2.imwrite('output_cv_affine_rotated_expanded.jpg', rotated_img_cv_expanded)
cv2.imwrite('output_cv_affine_scaled.jpg', scaled_img_cv)
cv2.imwrite('output_cv_affine_transformed.jpg', transformed_img_cv)
print("OpenCV 仿射变换示例已完成并保存结果。")
代码解释:
- import cv2:导入OpenCV库。
- img_cv = cv2.imread('example.jpg'):使用cv2.imread()加载图像。OpenCV默认加载彩色图像为BGR格式。
- M_translate = np.float32([[1, 0, tx], [0, 1, ty]]):创建平移的2x3仿射变换矩阵。必须是float32类型。
- cv2.warpAffine(img_cv, M_translate, (cols, rows)):应用仿射变换。
- 第一个参数是输入图像。
- 第二个参数是2x3的变换矩阵。
- 第三个参数是输出图像的尺寸元组(width, height)。
- cv2.getRotationMatrix2D(center, angle_deg, 1.0):获取绕指定中心旋转指定角度(度)和缩放因子为1.0的2x3仿射变换矩阵。
- 计算旋转后扩展尺寸:通过计算旋转后图像四个角点的新位置,可以确定包含整个旋转图像所需的最小矩形边界框的尺寸。
- M_scale = np.float32([[1.5, 0, 0], [0, 1.5, 0]]):创建缩放的2x3仿射变换矩阵。对角线元素[0, 0]和[1, 1]分别控制x和y方向的缩放因子。
- cv2.getAffineTransform(pts1, pts2):根据原始图像中的三对对应点pts1和目标图像中的对应点pts2,计算确定这个仿射变换的2x3矩阵。仿射变换由三对非共线点唯一确定。
- cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB):在使用Matplotlib显示OpenCV加载的BGR格式图像时,需要将其转换为RGB格式,否则颜色会不正确。
- cv2.imwrite('output_cv_affine_translated.jpg', translated_img_cv):使用cv2.imwrite()保存图像。OpenCV会自动根据文件扩展名选择编码格式。
- OpenCV在处理图像变换方面功能强大且通常效率更高,尤其是在需要计算变换矩阵(如基于点对应)或进行复杂变换时。
第六章:图像变换(续)
6.2透视变换(Perspective Transformation)
-
透视变换,也称为投影变换,比仿射变换更复杂,它可以改变图像的“视角”。仿射变换保持平行线的平行性,而透视变换则不保留平行性,但保留直线的直线性。透视变换可以将图像中的一个平面投影到另一个平面上,这对于校正由相机倾斜引起的图像畸变(如扫描文档的校正)、创建虚拟现实场景或进行图像拼接非常重要。
-
一个二维图像的透视变换可以由一个3x3的矩阵表示:
-
-
变换后的齐次坐标 ( (x’, y’, w’) ) 与二维笛卡尔坐标 ( (x_{new}, y_{new}) ) 的关系是:
-
[ x_{new} = \frac{x’}{w’} = \frac{a_{11}x + a_{12}y + b_1}{c_1x + c_2y + d}
y_{new} = \frac{y’}{w’} = \frac{a_{21}x + a_{22}y + b_2}{c_1x + c_2y + d} ] -
这个矩阵有8个自由度(因为矩阵乘法结果可以整体乘以一个非零常数而不改变 ( x_{new} ) 和 ( y_{new} ),通常令 ( d=1 )),因此需要至少4对非共线的对应点来确定变换矩阵。
-
在OpenCV中,可以使用 cv2.getPerspectiveTransform() 计算由四对对应点确定的透视变换矩阵,然后使用 cv2.warpPerspective() 应用变换。
import numpy as np
import cv2 # 导入OpenCV库
import matplotlib.pyplot as plt
# 假设我们有一个示例图像 (加载为OpenCV格式)
# OpenCV 默认使用 BGR 模式加载彩色图像
try:
img_cv = cv2.imread('example.jpg')
if img_cv is None:
raise FileNotFoundError
print("成功加载图像使用 cv2.imread")
except FileNotFoundError:
print("示例图像 example.jpg 未找到。")
# 创建一个模拟图像 (彩色),并在上面绘制一些形状以方便观察透视变换效果
img_cv = np.full((400, 600, 3), 200, dtype=np.uint8) # 浅灰色背景
# 绘制一个矩形
cv2.rectangle(img_cv, (100, 100), (500, 300), (255, 0, 0), 5) # 蓝色矩形
# 绘制一些文字
cv2.putText(img_cv, "Perspective Transform", (120, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) # 红色文字
# 绘制一些点
cv2.circle(img_cv, (100, 100), 10, (0, 255, 0), -1) # 绿色圆点 (左上角)
cv2.circle(img_cv, (500, 100), 10, (0, 255, 0), -1) # 绿色圆点 (右上角)
cv2.circle(img_cv, (500, 300), 10, (0, 255, 0), -1) # 绿色圆点 (右下角)
cv2.circle(img_cv, (100, 300), 10, (0, 255, 0), -1) # 绿色圆点 (左下角)
print("已创建模拟彩色图像。")
# 定义原始图像的四个角点坐标 (必须是 float32 类型)
# 这些点通常是需要进行透视校正的平面上的四个角点
rows, cols = img_cv.shape[:2] # 获取图像高度和宽度
# 例如,原始图像的四个角点
pts1 = np.float32([[100, 100], [500, 100], [500, 300], [100, 300]]) # 对应上面绘制的矩形的四个角点
# 定义目标图像中这四个点对应的位置 (必须是 float32 类型)
# 例如,将这四个点变换到一个新的矩形区域,实现“拉直”效果
# 假设目标矩形的角点
pts2 = np.float32([[50, 50], [550, 50], [550, 350], [50, 350]]) # 变换到一个更大的矩形区域,保持矩形形状
# 检查点数量是否正确 (需要四对点)
if pts1.shape != (4, 2) or pts2.shape != (4, 2):
print("错误: 需要提供四对对应点进行透视变换。")
else:
# 1. 计算透视变换矩阵 M
# cv2.getPerspectiveTransform(src, dst)
# src: 原始图像中的四对点 (float32)
# dst: 目标图像中对应的四对点 (float32)
M_perspective = cv2.getPerspectiveTransform(pts1, pts2)
print("计算得到的透视变换矩阵 M:\n", M_perspective)
# 2. 应用透视变换 (cv2.warpPerspective)
# src: 输入图像
# M: 3x3 变换矩阵 (float32 类型)
# dsize: 输出图像尺寸 (width, height)
# flags: 插值方法 (cv2.INTER_LINEAR, cv2.INTER_NEAREST, etc.)
# borderMode: 边界处理方式
# borderValue: 边界填充值
# 输出尺寸可以根据目标点的位置自行决定,这里为了演示方便,先使用一个固定的较大尺寸
output_width = 600
output_height = 400
transformed_img_cv = cv2.warpPerspective(img_cv, M_perspective, (output_width, output_height))
# 显示原始图像和透视变换后的图像
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
# OpenCV是BGR格式,Matplotlib显示需要转RGB
plt.imshow(cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB))
plt.title('原始图像')
# 在原始图像上标记用于变换的点
for pt in pts1:
plt.plot(pt[0], pt[1], 'ro') # 用红色圆点标记原始点
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(cv2.cvtColor(transformed_img_cv, cv2.COLOR_BGR2RGB))
plt.title('透视变换后图像')
# 在变换后图像上标记目标点位置 (理论上变换后的点应该在这些位置)
for pt in pts2:
plt.plot(pt[0], pt[1], 'ro') # 用红色圆点标记目标点
plt.axis('off')
plt.tight_layout()
plt.show()
# 保存结果 (OpenCV保存图像)
cv2.imwrite('output_cv_perspective_transformed.jpg', transformed_img_cv)
print("OpenCV 透视变换示例已完成并保存为: output_cv_perspective_transformed.jpg")
# 真实案例模拟:文档扫描校正
# 假设我们扫描了一份倾斜的文档,需要将文档区域“拉直”成一个矩形
# 加载一个包含倾斜矩形区域的模拟图像
doc_img = np.full((500, 700, 3), 230, dtype=np.uint8) # 浅灰色背景
# 模拟文档区域 (倾斜的四边形)
doc_pts_src = np.float32([[150, 120], [550, 100], [600, 400], [100, 420]]) # 倾斜的四边形角点
# 模拟在文档区域绘制一些内容
cv2.putText(doc_img, "Scanned Document", (200, 250), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 150), 2) # 蓝色文字
cv2.line(doc_img, tuple(doc_pts_src[0].astype(int)), tuple(doc_pts_src[1].astype(int)), (0, 100, 0), 2) # 绿色线
cv2.line(doc_img, tuple(doc_pts_src[1].astype(int)), tuple(doc_pts_src[2].astype(int)), (0, 100, 0), 2)
cv2.line(doc_img, tuple(doc_pts_src[2].astype(int)), tuple(doc_pts_src[3].astype(int)), (0, 100, 0), 2)
cv2.line(doc_img, tuple(doc_pts_src[3].astype(int)), tuple(doc_pts_src[0].astype(int)), (0, 100, 0), 2)
# 定义目标矩形区域的尺寸和位置
# 假设目标是将文档区域校正为一个 400x600 像素的矩形 (例如,宽度600,高度400)
doc_output_width = 600
doc_output_height = 400
# 目标矩形的四个角点 (通常是规则的矩形)
doc_pts_dst = np.float32([[0, 0], [doc_output_width - 1, 0], [doc_output_width - 1, doc_output_height - 1], [0, doc_output_height - 1]])
# 计算透视变换矩阵
M_doc_correct = cv2.getPerspectiveTransform(doc_pts_src, doc_pts_dst)
# 应用透视变换进行校正
corrected_doc_img = cv2.warpPerspective(doc_img, M_doc_correct, (doc_output_width, doc_output_height))
# 显示原始倾斜文档图像和校正后的图像
plt.figure(figsize=(14, 7))
plt.subplot(1, 2, 1)
plt.imshow(cv2.cvtColor(doc_img, cv2.COLOR_BGR2RGB))
plt.title('原始倾斜文档图像')
# 标记原始文档角点
for pt in doc_pts_src:
plt.plot(pt[0], pt[1], 'ro')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(cv2.cvtColor(corrected_doc_img, cv2.COLOR_BGR2RGB))
plt.title('透视校正后文档图像')
# 标记目标文档角点
for pt in doc_pts_dst:
plt.plot(pt[0], pt[1], 'ro')
plt.axis('off')
plt.tight_layout()
plt.show()
# 保存校正后的文档图像
cv2.imwrite('output_cv_document_corrected.jpg', corrected_doc_img)
print("文档透视校正示例已完成并保存为: output_cv_document_corrected.jpg")
代码解释:
- import numpy as np, import cv2, import matplotlib.pyplot as plt:导入所需的库。
- 加载或创建模拟图像:为了示例,如果找不到‘example.jpg’,则创建一个包含矩形、文字和角点的模拟图像,以便观察透视变换如何改变形状。
- pts1 = np.float32([[100, 100], [500, 100], [500, 300], [100, 300]]):定义原始图像中用于变换的四个点。这些点通常是图像中一个已知平面(如书本封面、文档页面、地面标志等)的角点。**注意:**这些点必须是float32类型,这是OpenCV函数要求的。
- pts2 = np.float32([[50, 50], [550, 50], [550, 350], [50, 350]]):定义目标图像中与pts1中的点对应的位置。这里将原始矩形变换到一个更大的矩形,但保持矩形形状,这相当于在应用透视变换的同时进行了缩放和平移。在文档校正等应用中,pts2通常定义一个规则的矩形区域。**注意:**同样必须是float32类型。
- M_perspective = cv2.getPerspectiveTransform(pts1, pts2):使用cv2.getPerspectiveTransform()函数计算从pts1到pts2的透视变换矩阵。它需要四对对应点来唯一确定3x3的变换矩阵(最后一个元素通常固定为1)。
- transformed_img_cv = cv2.warpPerspective(img_cv, M_perspective, (output_width, output_height)):使用cv2.warpPerspective()函数将计算出的透视变换应用到原始图像上。
- 第一个参数是输入图像。
- 第二个参数是计算得到的3x3透视变换矩阵。
- 第三个参数是输出图像的尺寸元组(width, height)。这个尺寸需要足够大以包含变换后的感兴趣区域。
- Matplotlib显示:将OpenCV的BGR图像转换为RGB格式后,使用Matplotlib显示原始图像和变换后的图像,并在图像上用红点标记原始点和目标点,以便直观比较。
- cv2.imwrite(...):保存结果图像。
文档扫描校正案例:
- doc_img = np.full(...):创建一个模拟的文档扫描图像,其中包含一个倾斜的矩形区域。
- doc_pts_src = np.float32([[150, 120], [550, 100], [600, 400], [100, 420]]):定义模拟文档区域(一个四边形)的四个角点。在实际应用中,这些点可以通过图像处理方法(如边缘检测、轮廓查找、角点检测)或用户手动标记获得。
- doc_pts_dst = np.float32([[0, 0], [doc_output_width - 1, 0], [doc_output_width - 1, doc_output_height - 1], [0, doc_output_height - 1]]):定义目标图像中,文档区域应该变换到的一个规则矩形的角点。这里设置为从(0, 0)到(width-1, height-1)的标准矩形,这将把倾斜的四边形“拉直”为这个矩形。
- M_doc_correct = cv2.getPerspectiveTransform(doc_pts_src, doc_pts_dst):计算从倾斜四边形到目标矩形的透视变换矩阵。
- corrected_doc_img = cv2.warpPerspective(doc_img, M_doc_correct, (doc_output_width, doc_output_height)):将变换矩阵应用到原始文档图像,生成校正后的图像。
透视变换是图像处理中用于处理平面投影畸变的重要工具,常用于文档校正、图像拼接、相机校准等领域。
6.3图像插值(Image Interpolation)
在进行图像缩放、旋转、透视变换等几何变换时,输出图像的像素位置可能不会精确地对应到输入图像的整数像素坐标上。这时就需要通过插值算法,利用输入图像中周围已知像素的信息来估计新像素位置的值。插值算法的选择会影响变换后图像的质量和计算速度。
常见的图像插值算法包括:
- 最近邻插值(Nearest Neighbor Interpolation):取离新像素位置最近的输入像素的值作为新像素的值。
- 优点:计算速度最快。
- 缺点:会产生块状效应(马赛克),引入锯齿,图像质量最低。
- 双线性插值(Bilinear Interpolation):考虑新像素位置周围的2x2个输入像素,通过对它们的值进行加权平均(线性插值)来计算新像素的值。权重与距离成反比。
- 优点:图像质量比最近邻插值好,过渡比较平滑。
- 缺点:会引入一定的模糊。
- 双三次插值(Bicubic Interpolation):考虑新像素位置周围的4x4个输入像素,通过对它们的值进行更复杂的加权平均(使用三次多项式插值)来计算新像素的值。
- 优点:图像质量通常比双线性插值好,细节保留更多,边缘更锐利。
- 缺点:计算复杂度最高,速度相对较慢。
- Lanczos插值(Lanczos Interpolation):一种高质量的插值方法,使用Lanczos函数作为滤波器。
- 优点:在缩小图像时效果很好,能有效抑制锯齿和振铃效应,保留较多细节。
- 缺点:计算复杂度较高。
Pillow和OpenCV的图像变换函数通常都提供了选择插值方法的参数。
import numpy as np
from PIL import Image
import cv2
import matplotlib.pyplot as plt
# 假设我们有一个示例图像 (为了更好演示插值,使用一张有细节的图片)
# 如果没有,可以创建一个包含文字和线条的模拟图像
try:
# 尝试加载一个真实图像
img_orig = Image.open('example.jpg').convert('RGB')
print("成功加载图像 example.jpg 用于插值示例。")
except FileNotFoundError:
print("示例图像 example.jpg 未找到,创建模拟图像用于插值示例。")
img_orig = Image.new('RGB', (400, 300), color = 'white')
from PIL import ImageDraw, ImageFont
draw = ImageDraw.Draw(img_orig)
try:
# 尝试加载一个字体文件
font = ImageFont.truetype("arial.ttf", 40) # 使用Arial字体,大小40
except IOError:
# 如果找不到字体文件,使用默认字体
font = ImageFont.load_default()
print("未找到 arial.ttf,使用默认字体。")
draw.text((20, 20), "Interpolation", fill='black', font=font)
draw.line([(10, 150), (390, 150)], fill='red', width=3) # 红线
draw.line([(200, 10), (200, 290)], fill='blue', width=3) # 蓝线
for i in range(0, 300, 20): # 绘制一些斜线
draw.line([(0, i), (i + 100, 0)], fill='green')
print("已创建模拟图像用于插值示例。")
# 将图像缩小到原尺寸的一半,并比较不同插值方法的效果
original_width, original_height = img_orig.size
new_width = original_width // 2
new_height = original_height // 2
new_size = (new_width, new_height)
# Pillow 示例
# 最近邻插值
img_nearest_pil = img_orig.resize(new_size, Image.Resampling.NEAREST)
# 双线性插值
img_bilinear_pil = img_orig.resize(new_size, Image.Resampling.BILINEAR)
# 双三次插值
img_bicubic_pil = img_orig.resize(new_size, Image.Resampling.BICUBIC)
# Lanczos 插值
img_lanczos_pil = img_orig.resize(new_size, Image.Resampling.LANCZOS)
# OpenCV 示例 (需要将 Pillow Image 转换为 OpenCV 格式 - NumPy 数组)
img_orig_cv = cv2.cvtColor(np.array(img_orig), cv2.COLOR_RGB2BGR) # Pillow 是RGB,OpenCV是BGR
# 最近邻插值
img_nearest_cv = cv2.resize(img_orig_cv, new_size, interpolation=cv2.INTER_NEAREST)
# 双线性插值
img_bilinear_cv = cv2.resize(img_orig_cv, new_size, interpolation=cv2.INTER_LINEAR)
# 双三次插值
img_bicubic_cv = cv2.resize(img_orig_cv, new_size, interpolation=cv2.INTER_CUBIC)
# Lanczos 插值
img_lanczos_cv = cv2.resize(img_orig_cv, new_size, interpolation=cv2.INTER_LANCZOS4) # OpenCV提供多种 Lanczos 核大小
# 显示原始图像和不同插值方法缩小的结果
plt.figure(figsize=(15, 10))
plt.subplot(2, 3, 1)
plt.imshow(img_orig)
plt.title('原始图像')
plt.axis('off')
plt.subplot(2, 3, 2)
plt.imshow(img_nearest_pil)
plt.title('Pillow 最近邻插值')
plt.axis('off')
plt.subplot(2, 3, 3)
plt.imshow(img_bilinear_pil)
plt.title('Pillow 双线性插值')
plt.axis('off')
plt.subplot(2, 3, 4)
plt.imshow(img_bicubic_pil)
plt.title('Pillow 双三次插值')
plt.axis('off')
plt.subplot(2, 3, 5)
plt.imshow(img_lanczos_pil)
plt.title('Pillow Lanczos 插值')
plt.axis('off')
# OpenCV 显示的结果需要转回RGB
plt.subplot(2, 3, 6)
# 为了比较方便,这里只显示一种OpenCV的结果,例如双线性
plt.imshow(cv2.cvtColor(img_bilinear_cv, cv2.COLOR_BGR2RGB))
plt.title('OpenCV 双线性插值')
plt.axis('off')
plt.tight_layout()
plt.show()
# 保存结果 (Pillow 和 OpenCV 各保存一个示例)
img_bilinear_pil.save('output_interpolation_bilinear_pil.jpg')
cv2.imwrite('output_interpolation_bilinear_cv.jpg', img_bilinear_cv)
print("不同插值方法的缩放示例已完成并保存结果。")
代码解释:
- 加载或创建模拟图像:为了清晰展示不同插值方法的区别,创建一个包含文字、直线和斜线的图像作为示例。这些元素更容易显示插值引入的锯齿或模糊。
- original_width, original_height = img_orig.size:获取原始图像的尺寸。
- new_width = original_width // 2, new_height = original_height // 2, new_size = (new_width, new_height):计算缩小到一半后的目标尺寸。
- img_orig.resize(new_size, Image.Resampling.NEAREST)等:使用Pillow的resize()方法进行缩放,并通过Image.Resampling常量指定不同的插值方法。Image.Resampling是Pillow 9.1.0+的推荐用法,早期版本使用Image.NEAREST等。
- img_orig_cv = cv2.cvtColor(np.array(img_orig), cv2.COLOR_RGB2BGR):将Pillow Image对象(RGB模式)转换为OpenCV兼容的NumPy数组(BGR模式),以便使用OpenCV的函数。
- cv2.resize(img_orig_cv, new_size, interpolation=cv2.INTER_NEAREST)等:使用OpenCV的cv2.resize()函数进行缩放,并通过interpolation参数指定不同的插值方法,例如cv2.INTER_NEAREST, cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_LANCZOS4。OpenCV提供了更多的插值选项,包括INTER_AREA(在缩小图像时效果较好)和INTER_LINEAR_EXACT。
- Matplotlib显示:显示原始图像以及使用不同库和不同插值方法缩小的结果,以便直观比较图像质量的差异。最近邻插值通常看起来最差(锯齿明显),双线性较平滑,双三次和Lanczos通常提供更好的视觉效果,尤其是在保留边缘和细节方面。
在实际应用中,选择哪种插值方法取决于对速度和图像质量的要求。对于实时应用或对速度要求极高的场景,最近邻插值可能是首选。对于大多数一般的图像处理任务,双线性插值是一个不错的折衷。对于需要最高图像质量的场景(如图像打印、医学影像),双三次或Lanczos插值更合适。