17

这是 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

这按预期工作。这是输入:

在此处输入图像描述

这是输出:

在此处输入图像描述

这是实现相同转换的 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 调用进行旋转,但我也想剪切和缩放,这只是一个示例来说明问题。我想为所有仿射变换获得相同的输出。我希望能够做到这一点。

编辑:

如果我将转换线更改为:

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 并获得了这个:

在此处输入图像描述

4

4 回答 4

19

好的!所以我整个周末都在努力理解这个问题,我想我有一个让我满意的答案。谢谢大家的意见和建议!

我先看看这个:

PIL python中的仿射变换

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

但我确实从他的代码中看到,我确实看到他将矩阵的旋转部分(a、b、d 和 e)划分为令我感到奇怪的比例。我回去阅读我引用的 PIL 文档:

“im.transform(大小,仿射,数据,过滤器)=>图像

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

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

此功能可用于对原始图像进行缩放、平移、旋转和剪切。”

所以参数 (a,b,c,d,e,f) 是一个变换矩阵,但是将目标图像中的 (x,y) 映射到 (ax + by + c, dx + ey + 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的输出:

在此处输入图像描述

让我在最后的总结中再次说明这个问题的答案:

PIL 需要您要应用的仿射变换的逆。

于 2013-06-17T07:00:27.500 回答
10

我想对carlosdcRuediger Jungbeck的答案进行一些扩展,以提供更实用的 Python 代码解决方案并进行一些解释。

首先,如carlosdc 的回答中所述,PIL 使用反仿射变换是绝对正确的。但是,不需要使用线性代数来计算原始变换的逆变换——相反,它可以很容易地直接表示。我将在示例中使用围绕其中心缩放和旋转图像,如在Ruediger Jungbeck 的答案中链接到的代码中,但扩展它以执行例如剪切也相当简单。

在讨论如何表示缩放和旋转的反仿射变换之前,请考虑我们如何找到原始变换。正如Ruediger Jungbeck 的回答中所暗示的那样,缩放和旋转组合操作的转换被发现是基本运算符的组合,用于围绕原点缩放图像并围绕原点旋转图像

但是,由于我们要围绕图像自身的中心缩放和旋转图像,并且PIL 将原点 (0, 0) 定义为图像的左上角,因此我们首先需要平移图像,使其中心重合与原产地。应用缩放和旋转后,我们还需要将图像平移回来,以使图像的新中心(在缩放和旋转后可能与旧中心不同)最终位于图像的中心帆布。

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

  1. 找到图像的当前中心(c_x, c_y),并将图像平移(-c_x, -c_y),因此图像的中心在原点(0, 0)

  2. 通过一些比例因子缩放关于原点的图像(s_x, s_y)

  3. 将图像围绕原点旋转某个角度\θ

  4. 找到图像的新中心(t_x, t_y),并将图像平移,(t_x, t_y)使新中心最终位于图像画布的中心。

为了找到我们所追求的变换,我们首先需要知道基本算子的变换矩阵,如下所示:

  • 翻译(x, y)
  • 缩放比例(s_x, s_y)
  • 旋转\θ

那么,我们的复合变换可以表示为:

这等于

或者

在哪里

.

现在,要找到这个复合仿射变换的逆,我们只需要逆序计算每个基本算子的逆的组合。也就是说,我们想要

  1. 将图像翻译为(-t_x, -t_y)

  2. 将图像绕原点旋转-\θ

  3. 将图像关于原点缩放(1/s_x, 1/s_y).

  4. 翻译图像(c_x, c_y)

这导致了一个变换矩阵

在哪里

.

这与Ruediger Jungbeck 的答案链接的代码中使用的转换完全相同。可以通过重用 carlosdc 在他们的帖子中用于计算图像的相同技术来更方便,并通过对图像的所有四个角应用旋转来转换图像,然后计算最小值和最大值之间的距离X 和 Y 值。但是,由于图像是围绕自己的中心旋转的,因此无需旋转所有四个角,因为每对相对的角都是“对称”旋转的。(t_x, t_y)(t_x, t_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 度):

缩放和旋转

于 2018-03-24T18:21:33.167 回答
1

我认为应该回答你的问题。

如果不是,您应该考虑仿射变换可以连接到另一个变换中。

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

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

  2. 旋转

  3. 将原点移回

  4. 调整大小

你可以计算出一个单一的转换。

于 2013-06-14T06:37:31.117 回答
0

图像围绕中心点旋转。PIL Image 坐标系的中心 (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)
于 2020-08-19T17:03:46.563 回答