Python/PIL仿射变换

18

这是一个有关PIL的基本变换问题。在过去的几年中,我至少尝试了几次来正确地实现它,但似乎我对于PIL中Image.transform的使用还不太熟悉。我想要实现一种相似变换(或仿射变换),其中我可以明确指定图像的边界限制。为了确保我的方法可行,我在Matlab中进行了实现。

Matlab实现如下:

im = imread('test.jpg');
y = size(im,1);
x = size(im,2);
angle = 45*3.14/180.0;
xextremes = [rot_x(angle,0,0),rot_x(angle,0,y-1),rot_x(angle,x-1,0),rot_x(angle,x-1,y-1)];
yextremes = [rot_y(angle,0,0),rot_y(angle,0,y-1),rot_y(angle,x-1,0),rot_y(angle,x-1,y-1)];
m = [cos(angle) sin(angle) -min(xextremes); -sin(angle) cos(angle) -min(yextremes); 0 0 1];
tform = maketform('affine',m')
round( [max(xextremes)-min(xextremes), max(yextremes)-min(yextremes)])
im = imtransform(im,tform,'bilinear','Size',round([max(xextremes)-min(xextremes), max(yextremes)-min(yextremes)]));
imwrite(im,'output.jpg');

function y = rot_x(angle,ptx,pty),
    y = cos(angle)*ptx + sin(angle)*pty

function y = rot_y(angle,ptx,pty),
    y = -sin(angle)*ptx + cos(angle)*pty

这个代码可以正常工作。以下是输入内容:

enter image description here

以下是输出内容:

enter image description here

以下是实现相同转换的Python/PIL代码:

import Image
import math

def rot_x(angle,ptx,pty):
    return math.cos(angle)*ptx + math.sin(angle)*pty

def rot_y(angle,ptx,pty):
    return -math.sin(angle)*ptx + math.cos(angle)*pty

angle = math.radians(45)
im = Image.open('test.jpg')
(x,y) = im.size
xextremes = [rot_x(angle,0,0),rot_x(angle,0,y-1),rot_x(angle,x-1,0),rot_x(angle,x-1,y-1)]
yextremes = [rot_y(angle,0,0),rot_y(angle,0,y-1),rot_y(angle,x-1,0),rot_y(angle,x-1,y-1)]
mnx = min(xextremes)
mxx = max(xextremes)
mny = min(yextremes)
mxy = max(yextremes)
im = im.transform((int(round(mxx-mnx)),int(round((mxy-mny)))),Image.AFFINE,(math.cos(angle),math.sin(angle),-mnx,-math.sin(angle),math.cos(angle),-mny),resample=Image.BILINEAR)
im.save('outputpython.jpg')

这是Python的输出:

图片描述

我在多个操作系统上使用了数个版本的Python和PIL尝试过这个问题,结果大多都相同。

这是最简单的情况,用来说明问题。如果我想要旋转图像,可以使用im.rotate调用进行旋转,但我也希望能够扭曲和缩放图像,这只是一个例子,以说明问题。我希望能够对所有仿射变换得到相同的输出结果,我希望能够正确地完成它。

编辑:

如果我将transform行更改为以下内容:

im = im.transform((int(round(mxx-mnx)),int(round((mxy-mny)))),Image.AFFINE,(math.cos(angle),math.sin(angle),0,-math.sin(angle),math.cos(angle),0),resample=Image.BILINEAR)

这是我得到的输出:

输入图像描述

编辑 #2

我将其旋转了-45度,并将偏移量更改为-0.5 * mnx和-0.5 * mny,得到了以下结果:

输入图像描述


1
在Python和Matlab中,图像的(0,0)空间位置是否可能定义不同?对于Matlab而言,(0,0)是图像的左上角。而对于Python而言,它是否可能是图像的中心?如果在Python中省略转换的翻译部分(即没有“-mnx”和“-mny”),会发生什么情况? - user2469775
另外,您可能需要使用“-angle”而不是“angle”进行工作。 - Shai
@Shai:我尝试了你建议的方法,并编辑了问题,附上了我得到的结果。 - carlosdc
我猜我的猜测和你的一样好。我相信此时尝试和错误将会给你正确的结果。一旦你到达那里,我相信逆向工程矩阵以理解PIL的行为将更容易。 - Shai
@Shai:谢谢!这在我的工作中时不时会出现。我总是能够解决它,但从来没有一个我想要的原则性解决方案(或者像Matlab中得到的那样)。 - carlosdc
显示剩余2条评论
4个回答

21

好的!所以我整个周末都在努力理解这个问题,现在我认为我有了一个令我满意的答案。感谢大家的评论和建议!

我从以下链接开始研究:

Python PIL中的仿射变换

虽然我看到作者可以进行任意相似变换,但他并没有解释我的代码为什么不起作用,也没有解释我们需要转换的图像的空间布局,也没有提供线性代数解决方案。

但是,我从他的代码中看到他将矩阵的旋转部分(a、b、d和e)分成了比例,这让我感到奇怪。我回去阅读了PIL文档,其中我引用了以下内容:

"im.transform(size, AFFINE, data, filter) => image

对图像应用仿射变换,并将结果放置在给定大小的新图像中。

Data是一个6元组(a,b,c,d,e,f),其中包含来自仿射变换矩阵的前两行。对于输出图像中的每个像素(x,y),新值取自输入图像中位置(a x + b y + c, d x + e y + f),四舍五入到最近的像素。

此函数可用于缩放、平移、旋转和剪切原始图像。"

因此,参数(a,b,c,d,e,f)是变换矩阵,但它将目标图像中的坐标(x,y)映射到源图像中的坐标(a x + b y + c, d x + e y + f)。但不是您要应用的变换矩阵的参数,而是其逆矩阵。也就是说:

  • 奇怪的
  • 与Matlab中的不同
  • 但现在,幸运的是,我完全理解了

我附上我的代码:

import Image
import math
from numpy import matrix
from numpy import linalg

def rot_x(angle,ptx,pty):
    return math.cos(angle)*ptx + math.sin(angle)*pty

def rot_y(angle,ptx,pty):
    return -math.sin(angle)*ptx + math.cos(angle)*pty

angle = math.radians(45)
im = Image.open('test.jpg')
(x,y) = im.size
xextremes = [rot_x(angle,0,0),rot_x(angle,0,y-1),rot_x(angle,x-1,0),rot_x(angle,x-1,y-1)]
yextremes = [rot_y(angle,0,0),rot_y(angle,0,y-1),rot_y(angle,x-1,0),rot_y(angle,x-1,y-1)]
mnx = min(xextremes)
mxx = max(xextremes)
mny = min(yextremes)
mxy = max(yextremes)
print mnx,mny
T = matrix([[math.cos(angle),math.sin(angle),-mnx],[-math.sin(angle),math.cos(angle),-mny],[0,0,1]])
Tinv = linalg.inv(T);
print Tinv
Tinvtuple = (Tinv[0,0],Tinv[0,1], Tinv[0,2], Tinv[1,0],Tinv[1,1],Tinv[1,2])
print Tinvtuple
im = im.transform((int(round(mxx-mnx)),int(round((mxy-mny)))),Image.AFFINE,Tinvtuple,resample=Image.BILINEAR)
im.save('outputpython2.jpg')

并且Python的输出如下:

enter image description here

让我再次总结这个问题的答案:

PIL需要应用相反的仿射变换。


1
我真希望能给你更多的赞!谢谢你! - lesolorzanov
这个笔记本提供了清晰的解释:https://data.kaizhao.net/notebooks/batch-affine.html - Kai ZHAO

10

我希望能够进一步扩展carlosdcRuediger Jungbeck的答案,提供一个更实用的Python代码解决方案,并进行一些解释。

首先,正如carlosdc的回答所述,PIL使用逆仿射变换。但是,没有必要使用线性代数来计算原始变换的逆变换——相反,它可以直接轻松地表达出来。我将使用缩放和旋转图像以使其围绕中心旋转的示例,就像Ruediger Jungbeck的答案中链接的code一样,但是很容易将其扩展到执行例如剪切等操作。

在接近如何表达缩放和旋转的逆仿射变换之前,请考虑如何找到原始变换。如Ruediger Jungbeck的回答中所提示的那样,缩放和旋转的组合操作的变换是通过关于原点缩放图像关于原点旋转图像的基本运算符的组合来找到的。

然而,由于我们想要以图像自身的中心进行缩放和旋转,而原点(0,0)被PIL定义为图像的左上角,因此我们首先需要将图像平移到其中心与原点重合。在应用缩放和旋转后,我们还需要以某种方式将图像平移回来,使得图像的新中心(在缩放和旋转后可能不同于旧中心)最终位于图像画布的中心。

因此,我们要实现的原始“标准”仿射变换将是以下基本运算符的组合:

  1. 找到图像的当前中心(c_x, c_y),并将图像平移(-c_x, -c_y),使图像的中心位于原点(0, 0)
  2. 以原点为中心缩放图像,缩放因子为(s_x, s_y)
  3. 以原点为中心旋转图像,旋转角度为\theta
  4. 找到图像的新中心(t_x, t_y),并将图像平移(t_x, t_y),使新中心最终位于图像画布的中心。

为了找到我们需要的变换,首先需要知道基本操作的变换矩阵,它们如下所示:

  • {{翻译者(x, y)}}的翻译:{{\begin{bmatrix}1 & 0 & x\\0 & 1 & y\\0 & 0 & 1\end{bmatrix}}}
  • 缩放{{(s_x, s_y)}}:{{\begin{bmatrix}s_x & 0 & 0\\0 & s_y & 0\\0 & 0 & 1\end{bmatrix}}}
  • 旋转{{\theta}}:{{\begin{bmatrix}\cos(\theta) & \sin(\theta) & 0\\-\sin(\theta) & \cos(\theta) & 0\\0 & 0 & 1\end{bmatrix}}}

然后,我们的复合变换可以表示为:

相当于

或者

在哪里

.

现在,要找到此组合仿射变换的逆变换,我们只需要按相反的顺序计算每个基本运算符的逆运算符的组合。也就是说,我们想要:
1. 通过(-t_x, -t_y)平移图像 2. 将图像绕原点旋转-\theta 3. 将图像以原点为中心缩放(1/s_x, 1/s_y) 4. 通过(c_x, c_y)平移图像
这将得到一个转换矩阵。

在哪里

.

这与Ruediger Jungbeck的答案中链接到的代码中使用的变换完全相同。可以通过重用carlosdc在其帖子中用于计算图像(t_x, t_y)的方法,使其更加方便,并将图像平移(t_x, t_y) ——将旋转应用于图像的所有四个角落,然后计算最小和最大X和Y值之间的距离。但是,由于图像围绕其自身的中心旋转,因此无需旋转所有四个角落,因为每对相对面的角落都会“对称”旋转。

这是carlosdc代码的重写版本,已经修改为直接使用逆仿射变换,并添加了缩放:

from PIL import Image
import math


def scale_and_rotate_image(im, sx, sy, deg_ccw):
    im_orig = im
    im = Image.new('RGBA', im_orig.size, (255, 255, 255, 255))
    im.paste(im_orig)

    w, h = im.size
    angle = math.radians(-deg_ccw)

    cos_theta = math.cos(angle)
    sin_theta = math.sin(angle)

    scaled_w, scaled_h = w * sx, h * sy

    new_w = int(math.ceil(math.fabs(cos_theta * scaled_w) + math.fabs(sin_theta * scaled_h)))
    new_h = int(math.ceil(math.fabs(sin_theta * scaled_w) + math.fabs(cos_theta * scaled_h)))

    cx = w / 2.
    cy = h / 2.
    tx = new_w / 2.
    ty = new_h / 2.

    a = cos_theta / sx
    b = sin_theta / sx
    c = cx - tx * a - ty * b
    d = -sin_theta / sy
    e = cos_theta / sy
    f = cy - tx * d - ty * e

    return im.transform(
        (new_w, new_h),
        Image.AFFINE,
        (a, b, c, d, e, f),
        resample=Image.BILINEAR
    )


im = Image.open('test.jpg')
im = scale_and_rotate_image(im, 0.8, 1.2, 10)
im.save('outputpython.png')

这是结果的样子(通过缩放(sx, sy) = (0.8, 1.2),并逆时针旋转10度):

Scaled and rotated


1
@Colbi 抱歉,但是你的计算是错误的 ;) 你不能简单地将 -tx-ty 项与 abde 一起包含在矩阵中。此外,你正在以错误的顺序乘以矩阵。请参见完整简化计算。 - Erlend Graff

1

我认为这个链接可以回答你的问题。

如果不能,你可以考虑将仿射变换连接成另一个变换。

因此,您可以将所需的操作拆分为:

  1. 将原点移动到图像中心

  2. 旋转

  3. 将原点移回

  4. 调整大小

然后,您可以计算出单个变换。


0

图像围绕中心点旋转。PIL图像坐标系的中心点(0, 0)位于左上角。

如果您使用矩阵乘积来构建仿射变换,建议添加临时居中/去中心转换。

我们从以下基本块构建仿射变换:

import numpy as np

def translation(x, y):
    mat = np.eye(3)
    mat[0, 2] = x
    mat[1, 2] = y
    return mat

def scaling(s):
    mat = np.eye(3)
    mat[0, 0] = s
    mat[1, 1] = s
    return mat

def rotation(degree):
    mat = np.eye(3)
    rad = np.deg2rad(degree)
    mat[0, 0] = np.cos(rad)
    mat[0, 1] = -np.sin(rad)
    mat[1, 0] = np.sin(rad)
    mat[1, 1] = np.cos(rad)
    return mat

def tmp_center(w, h):
    mat = np.eye(3)
    mat[0, 2] = -w/2
    mat[1, 2] = -h/2
    return mat

然后加载图像,并定义转换。 与其他库不同,确保使用反向转换,就像其他人指出的那样。
from PIL import Image
img = Image.from_array(...)
w, h = img.size
T = translation(20, 23) @ tmp_center(-w, -h) @ rotation(5) @ scaling(0.69) @ tmp_center(w, h)
coeff = np.linalg.inv(T).flatten()[:6]

out = img.transform(img.size, Image.AFFINE, coeff, resample.Image.BILINEAR)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接