使用 Pillow 将矩形映射到四边形

Mapping a rectangle to a quad with Pillow

我正在尝试编写一个 Python 程序来获取输入图像(例如 JPEG)并生成“地球仪组件”输出图像,类似于 le Paper Globe。本质上,如果输出图像经过打印、剪切、折叠和粘合,应该得到投影到粗糙球体上的原始图像。

该程序会将输入图像分成 32 个(水平 8 个,垂直 4 个)矩形,然后将每个矩形映射到一些精心挑选的梯形或更一般的四边形上。我找到了 Pillow/PIL 方法 that maps a quad onto a square,但找不到将矩形映射到四边形的方法。

有谁知道如何在 Python 中将输入图像的矩形映射到输出图像的四边形上? 我更喜欢 Pillow/PIL, 但任何可以打开和保存 JPEG 的库都可以。

基本上,您需要进行一些透视变换才能完成此操作。 Pillow Image.transform for that. You'd need to calculate all necessary parameters beforehand, i.e. the homographic transform, cf. this Q&A. I personally would use OpenCV's warpPerspective, and get the transformation matrix by using getPerspectiveTransform, such that you only need to provide four points in the source image, and four points in the destination image. 在这方面有一个很好的快速入门。

在我们进入细节之前,我只是想确定以下是您想要实现的目标:

所以,完整的算法是:

  1. 使用 Pillow 加载源图像和具有一些四边形的专用输出图像。我假设是白色背景上的黑色四边形。
  2. 将图像转换为 NumPy 数组以便能够与 OpenCV 一起使用。
  3. 设置源点。这些只是您感兴趣区域 (ROI) 的角落。
  4. 找到——或知道——目的地。这些是四边形的角。自动查找这些可能会变得非常困难,因为顺序必须与为 ROI 点设置的顺序相同。
  5. 获取变换矩阵,并应用实际的透视变换。
  6. 将变形图像的所需部分复制到初始输出图像的四边形。
  7. 转换回一些枕头图像并保存。

而且,这是完整的代码,包括一些可视化:

import cv2
import numpy as np
from PIL import Image, ImageDraw

# Input image to get rectangle (region of interest, roi) from
image = Image.open('path/to/your/image.png')
roi = ((100, 30), (300, 200))

# Dummy output image with some quad to paste to
output = Image.new('RGB', (600, 800), (255, 255, 255))
draw = ImageDraw.Draw(output)
draw.polygon(((100, 20), (40, 740), (540, 350), (430, 70)), outline=(0, 0, 0))

# Convert images to NumPy arrays for processing in OpenCV
image_cv2 = np.array(image)
output_cv2 = np.array(output)

# Source points, i.e. roi in input image
tl = (roi[0][0], roi[0][1])
tr = (roi[1][0], roi[0][1])
br = (roi[1][0], roi[1][1])
bl = (roi[0][0], roi[1][1])
pts = np.array([bl, br, tr, tl])

# Find (or know) target points in output image w.r.t. the quad
# Attention: The order must be the same as defined by the roi points!
tl_dst = (100, 20)
tr_dst = (430, 70)
br_dst = (540, 350)
bl_dst = (40, 740)
dst_pts = np.array([bl_dst, br_dst, tr_dst, tl_dst])

# Get transformation matrix, and warp image
pts = np.float32(pts.tolist())
dst_pts = np.float32(dst_pts.tolist())
M = cv2.getPerspectiveTransform(pts, dst_pts)
image_size = (output_cv2.shape[1], output_cv2.shape[0])
warped = cv2.warpPerspective(image_cv2, M, dsize=image_size)

# Get mask from quad in output image, and copy content from warped image
gray = cv2.cvtColor(output_cv2, cv2.COLOR_BGR2GRAY)
gray = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY_INV)[1]
cnts = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
mask = np.zeros_like(output_cv2)
mask = cv2.drawContours(mask, cnts, 0, (255, 255, 255), cv2.FILLED)
mask = mask.all(axis=2)
output_cv2[mask, :] = warped[mask, :]

# Transform back to PIL images
output_new = Image.fromarray(output_cv2)
output_new.save('final_output.jpg')

# Just for visualization
import matplotlib.pyplot as plt
draw = ImageDraw.Draw(image)
draw.rectangle(roi, outline=(255, 0, 0), width=3)
plt.figure(0, figsize=(18, 9))
plt.subplot(1, 3, 1), plt.imshow(image), plt.title('Input with ROI')
plt.subplot(1, 3, 2), plt.imshow(output), plt.title('Output with quad')
plt.subplot(1, 3, 3), plt.imshow(output_new), plt.title('Final output')
plt.tight_layout(), plt.show()

在第 4 步,自动寻找目标点,你可以这样做:

# Find target points in output image w.r.t. the quad
gray = cv2.cvtColor(output_cv2, cv2.COLOR_BGR2GRAY)
gray = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY_INV)[1]
cnts = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
approx = cv2.approxPolyDP(cnts[0], 0.03 * cv2.arcLength(cnts[0], True), True)

这基本上是在图像中找到轮廓,并逼近角点。您仍然需要找到结果点的正确顺序...

----------------------------------------
System information
----------------------------------------
Platform:      Windows-10-10.0.16299-SP0
Python:        3.8.5
Matplotlib:    3.3.3
NumPy:         1.19.5
OpenCV:        4.5.1
Pillow:        8.1.0
----------------------------------------