圖像變換是指改變圖像的幾何形狀或空間位置的操作。常見的幾何變換包括平移、旋轉、縮放、剪切(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 ImageDrawdraw = 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 FileNotFoundErrorprint("成功加載圖像使用 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 FileNotFoundErrorprint("成功加載圖像使用 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 = 600output_height = 400transformed_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顯示需要轉RGBplt.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 = 600doc_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, ImageFontdraw = ImageDraw.Draw(img_orig)try:# 嘗試加載一個字體文件font = ImageFont.truetype("arial.ttf", 40) # 使用Arial字體,大小40except 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插值更合適。