繁体   English   中英

Python/PIL 仿射变换

[英]Python/PIL affine transformation

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

在此处输入图片说明

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

我首先看这个:

PIL python 中的仿射变换

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

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

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

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

数据是一个 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 需要您要应用的仿射变换的逆。

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

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

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

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

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

  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. 旋转关于原点的图像-\\theta .

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

  4. 翻译图像(c_x, c_y) .

这导致转换矩阵

在哪里

.

与链接到Ruediger Jungbeck's answer代码中使用的转换完全相同 通过重用 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 度):

缩放和旋转

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

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

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

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

  2. 旋转

  3. 原点移回

  4. 调整大小

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

图像围绕中心点旋转。 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)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM