PIL的缩略图为什么会旋转我的图片?

86
我正在尝试将大型(巨大的)图像(来自数码相机)转换为我可以在web上显示的内容。这似乎很简单,也应该是的。但是,当我尝试使用PIL创建缩略图版本时,如果我的源图像比它的宽度更高,则生成的图像会旋转90度,使得源图像的顶部位于结果图像的左侧。如果源图像比其高度更宽,则生成的图像是正确(原始)的方向。这可能与我发送为大小的2元组有关吗?我使用缩略图,因为它似乎旨在保留长宽比。或者我只是完全瞎了,做了一些愚蠢的事情?大小元组为1000,1000,因为我想要将最长的一边缩小到1000像素,同时保持宽高比不变。
代码看起来很简单。
img = Image.open(filename)
img.thumbnail((1000,1000), Image.ANTIALIAS)
img.save(output_fname, "JPEG")

提前感谢任何帮助。


1
为其他人添加一条注释:我认为.thumbnail()不会旋转 - 我使用img.show()进行了验证。实际上是.save()方法将其写入文件。我的尝试是:- 尝试将其写入内存文件而不是磁盘文件 from io import BytesIO; buffer = BytesIO; img.save(buffer, "JPEG"); Image.open(buffer).show() - Grijesh Chauhan
14个回答

84

我同意“unutbu”和Ignacio Vazquez-Abrams的几乎所有回答,然而...

EXIF方向标志可以具有1到8之间的值,具体取决于相机的拍摄方式。

竖直照片可以在左侧或右侧将相机顶部朝上拍摄,水平照片可以倒置拍摄。

这是一段考虑到这一点的代码(已经通过了DSLR Nikon D80的测试)。

    import Image, ExifTags

    try :
        image=Image.open(os.path.join(path, fileName))
        for orientation in ExifTags.TAGS.keys() : 
            if ExifTags.TAGS[orientation]=='Orientation' : break 
        exif=dict(image._getexif().items())

        if   exif[orientation] == 3 : 
            image=image.rotate(180, expand=True)
        elif exif[orientation] == 6 : 
            image=image.rotate(270, expand=True)
        elif exif[orientation] == 8 : 
            image=image.rotate(90, expand=True)

        image.thumbnail((THUMB_WIDTH , THUMB_HIGHT), Image.ANTIALIAS)
        image.save(os.path.join(path,fileName))

    except:
        traceback.print_exc()

1
这段代码的缩进有问题吗? - Bibhas Debnath
2
“for orientation... break”逻辑块的目的是什么? - Jim
@Robert 它从 ExifTags 集合中获取“方向”标签,然后将其用于测试方向值。 - storm_to
3
为什么要遍历所有标签,难道不是总是同一个键吗? - dangel
1
根据 https://www.media.mit.edu/pia/Research/deepview/exif.html 的说明,方向的标签为274(0x0112),一旦调用image._getexif(),因此您可以使用274作为该字典的键来定位方向字段的值。 - yangsiyu
显示剩余2条评论

73

只需使用Pillow中的PIL.ImageOps.exif_transpose

与本问题中提出的每个函数(包括我的原始函数)不同,它会注意从EXIF中删除方向字段(因为图像不再以奇怪的方式定位),并确保返回值是全新的图像对象,因此对其所做的更改不会影响原始对象。


3
仅缺少 import functools,但仍然应该被接受作为Python 2.7中可直接使用的答案。 - Kapitein Witbaard
6
谢谢你。这是最完整的回答,使用了最适当的工具。其他答案要么使用rotate而不是transpose,要么缺少所有八个可能的状态。JPEG的退出方向是用于转置而不是旋转。http://jpegclub.org/exif_orientation.html - OdraEncoded
1
确实可以插入。但它是否可靠地工作?请确认一下,因为我目前得到了混合的结果。这是我测试过的一个图像(方向为0):https://imgur.com/a/053MR - Hassan Baig
1
我刚刚修复了一个代码中的错误,其中方向为:Unknown(0)。这导致索引为-1,这意味着Python返回数组中的最后一项,即ROTATE_90,这会让用户非常生气。 - Chris Sattinger
喜欢函数式编程风格 :) - declension
显示剩余7条评论

38

xilvar的答案很好,但有两个小缺陷需要修正,在一次被拒绝的编辑中我已经解决了这些问题,所以我将其发表为答案。

首先,xilvar的解决方案在文件不是JPEG格式或没有exif数据时会失败。另外,它总是旋转180度而非适当的角度。

import Image, ExifTags

try:
    image=Image.open(os.path.join(path, fileName))
    if hasattr(image, '_getexif'): # only present in JPEGs
        for orientation in ExifTags.TAGS.keys(): 
            if ExifTags.TAGS[orientation]=='Orientation':
                break 
        e = image._getexif()       # returns None if no EXIF data
        if e is not None:
            exif=dict(e.items())
            orientation = exif[orientation] 

            if orientation == 3:   image = image.transpose(Image.ROTATE_180)
            elif orientation == 6: image = image.transpose(Image.ROTATE_270)
            elif orientation == 8: image = image.transpose(Image.ROTATE_90)

    image.thumbnail((THUMB_WIDTH , THUMB_HIGHT), Image.ANTIALIAS)
    image.save(os.path.join(path,fileName))

except:
    traceback.print_exc()

我会使用orientation = exif.get(orientation, None)然后if orientation is None: return,并添加一些日志,说明图像的exif可能无效。我不是说它会对每个人都造成错误,但它曾经发生过在我身上,而且可能非常罕见。 - Bartosz Dabrowski
1
我会使用 orientation = next(k for k, v in ExifTags.TAGS.items() if v == 'Orientation'),因为该脚本依赖于此标签,而 PIL 的 ExifTags.py 库似乎已经包含了它。 - kirpit

35

Pillow有一个API可以自动处理EXIF方向标签:(参见)

from PIL import Image, ImageOps

original_image = Image.open(filename)

fixed_image = ImageOps.exif_transpose(original_image)

在使用这个方法之前,我尝试了其他一些解决方案。但是这个方法是最简单的,并且解决了我的问题,因为它还检查了输入格式。只有有效的PIL图像输入才会被接受。 在我的情况下,我使用以下代码破坏了exif信息:image = numpy.array(image)。 - Antti A
这是唯一对我有效的解决方案。但在我的情况下,我使用ImageReader来读取图像而不是Image。因此,我必须将文件保存到内存中并使用Image()打开,然后进行exif_transpose,最后使用ImageReader()。 - MassDefect_
谢谢,这是最优雅的解决方案,而不需要切换到其他库。 - cheng yang
这是最有效的解决方案。完美地运作!谢谢! - Data_sniffer

21

请注意,下面有更好的答案。


当一张图片的高度大于宽度时,这意味着相机被旋转了。一些相机可以检测到这种情况,并在图片的EXIF元数据中写入该信息。一些查看器会注意到这些元数据并适当地显示图像。

PIL可以读取图片的元数据,但在保存图像时不会写入/复制元数据。因此,您的智能图像查看器将无法像以前那样旋转图像。

继@Ignacio Vazquez-Abrams的评论后,您可以使用PIL这种方式读取元数据,然后根据需要旋转:

import ExifTags
import Image

img = Image.open(filename)
print(img._getexif().items())
exif=dict((ExifTags.TAGS[k], v) for k, v in img._getexif().items() if k in ExifTags.TAGS)
if not exif['Orientation']:
    img=img.rotate(90, expand=True)
img.thumbnail((1000,1000), Image.ANTIALIAS)
img.save(output_fname, "JPEG")

请注意,上述代码可能不适用于所有相机。

最简单的解决方案可能是使用其他程序来制作缩略图。

phatch 是一个用Python编写的批量照片编辑器,可以处理/保留EXIF元数据。你可以使用这个程序来制作缩略图,或者查看其源代码以了解如何在Python中实现。我相信它使用pyexiv2 来处理EXIF元数据。pyexiv2 可能比PIL 的ExifTags模块更好地处理EXIF。

imagemagick 是另一种制作批量缩略图的可能性。


感谢你们两位的回答。我正在尝试剥离所有的EXIF数据,但如果需要旋转,则重新添加数据。这比我最初预想的要麻烦得多。现在只是一个解决脚本的问题了。再次感谢! - Hoopes
由于您正在调整大小,所以您可能不会在意,但请记住,即使是简单的旋转操作在JPEG图像上有时也会导致损失。 - Paul McMillan
请查看下面的答案,已进行了几项改进。我建议使用社区维基。 - Maik Hoepfel
2
我点赞了处理所有8个方向的版本。此外,这里有一组很棒的测试图片,来自Dave Perrett的https://github.com/recurser/exif-orientation-examples。 - Scott Lawton
你救了我的一天 <3 - Dinh Phong
显示剩余3条评论

21

这里有一个适用于所有8个方向的版本:

def flip_horizontal(im): return im.transpose(Image.FLIP_LEFT_RIGHT)
def flip_vertical(im): return im.transpose(Image.FLIP_TOP_BOTTOM)
def rotate_180(im): return im.transpose(Image.ROTATE_180)
def rotate_90(im): return im.transpose(Image.ROTATE_90)
def rotate_270(im): return im.transpose(Image.ROTATE_270)
def transpose(im): return rotate_90(flip_horizontal(im))
def transverse(im): return rotate_90(flip_vertical(im))
orientation_funcs = [None,
                 lambda x: x,
                 flip_horizontal,
                 rotate_180,
                 flip_vertical,
                 transpose,
                 rotate_270,
                 transverse,
                 rotate_90
                ]
def apply_orientation(im):
    """
    Extract the oritentation EXIF tag from the image, which should be a PIL Image instance,
    and if there is an orientation tag that would rotate the image, apply that rotation to
    the Image instance given to do an in-place rotation.

    :param Image im: Image instance to inspect
    :return: A possibly transposed image instance
    """

    try:
        kOrientationEXIFTag = 0x0112
        if hasattr(im, '_getexif'): # only present in JPEGs
            e = im._getexif()       # returns None if no EXIF data
            if e is not None:
                #log.info('EXIF data found: %r', e)
                orientation = e[kOrientationEXIFTag]
                f = orientation_funcs[orientation]
                return f(im)
    except:
        # We'd be here with an invalid orientation value or some random error?
        pass # log.exception("Error applying EXIF Orientation tag")
    return im

很棒的解决方案。运行得非常好。 - John Pang
这个可以工作,但我想知道是否有更多面向对象的风格可以在这里实现,比如从PIL中添加到Image类。此外,我认为在try块中有那么多代码并不是一个好主意。你有几件事情可能会失败,知道哪些失败了会很好,我个人认为。 - Tatsh
通过使用插件系统,您可以将JPEG插件的保存功能替换为自动旋转保存的功能。这可能并不总是需要的,因为它往往会丢失EXIF数据,但是这样做会更方便,而且可以少写一行代码。https://gist.github.com/Tatsh/9f713edc102df99fc612486a2c571a6e - Tatsh

10

我需要一个能处理所有方向而不仅仅是 3, 68 的解决方案。

我尝试了 Roman Odaisky 的解决方案 - 它看起来全面而干净。但是,用实际图像测试它的多种方向值有时会导致错误结果(例如这个设置为方向0的)。

另一个可行的解决方案可能是Dobes Vandermeer提供的。但我没有尝试过,因为我觉得可以更简单地编写逻辑(这是我喜欢的)。

所以,不再拖延,以下是一个更简单、更易维护(在我看来)的版本:

from PIL import Image

def reorient_image(im):
    try:
        image_exif = im._getexif()
        image_orientation = image_exif[274]
        if image_orientation in (2,'2'):
            return im.transpose(Image.FLIP_LEFT_RIGHT)
        elif image_orientation in (3,'3'):
            return im.transpose(Image.ROTATE_180)
        elif image_orientation in (4,'4'):
            return im.transpose(Image.FLIP_TOP_BOTTOM)
        elif image_orientation in (5,'5'):
            return im.transpose(Image.ROTATE_90).transpose(Image.FLIP_TOP_BOTTOM)
        elif image_orientation in (6,'6'):
            return im.transpose(Image.ROTATE_270)
        elif image_orientation in (7,'7'):
            return im.transpose(Image.ROTATE_270).transpose(Image.FLIP_TOP_BOTTOM)
        elif image_orientation in (8,'8'):
            return im.transpose(Image.ROTATE_90)
        else:
            return im
    except (KeyError, AttributeError, TypeError, IndexError):
        return im

已经测试并发现在所有提到的Exif方向的图像上都可以正常工作。但是,请您也进行自己的测试。


这是干净、完整、现代的Python3答案。 - WhiteHotLoveTiger
不错。非常完整的答案。在PIL==6.2.2中有一个小问题,im没有_getexif(),但有getexif() - jkhadka

8

我是一个编程新手,对Python和PIL都不熟悉,前面答案中的代码示例对我来说很复杂。所以,我直接找到了标签的键值,而没有遍历标签。在Python shell中,您可以看到方向的键值是274。

>>>from PIL import ExifTags
>>>ExifTags.TAGS

我使用 image._getexif() 函数获取图像中的 ExifTags。如果方向标签不存在,则会引发错误,因此我使用 try/except。
Pillow 的文档说旋转和转置之间在性能或结果方面没有区别。我通过计时两个函数来确认这一点。我使用旋转,因为它更简洁。 rotate(90) 逆时针旋转,该函数似乎接受负度数。
from PIL import Image, ExifTags

# Open file with Pillow
image = Image.open('IMG_0002.jpg')

#If no ExifTags, no rotating needed.
try:
# Grab orientation value.
    image_exif = image._getexif()
    image_orientation = image_exif[274]

# Rotate depending on orientation.
    if image_orientation == 3:
        rotated = image.rotate(180)
    if image_orientation == 6:
        rotated = image.rotate(-90)
    if image_orientation == 8:
        rotated = image.rotate(90)

# Save rotated image.
    rotated.save('rotated.jpg')
except:
    pass

这对我来说可以旋转图像,但是长宽比也被反转了。我遇到的另一个问题是,当我保存在原始文件上时,完全丢失了EXIF数据。 - Richard

6

Hoopes的回答很好,但使用转置方法比旋转更有效率。旋转对每个像素进行实际的过滤计算,实际上是整个图像的复杂调整大小。另外,当前的PIL库似乎存在一个bug,旋转图像的边缘会添加一条黑线。转置要快得多,而且没有那个bug。我只是稍微修改了hoopes的答案,改用了转置。

import Image, ExifTags

try :
    image=Image.open(os.path.join(path, fileName))
    for orientation in ExifTags.TAGS.keys() : 
        if ExifTags.TAGS[orientation]=='Orientation' : break 
    exif=dict(image._getexif().items())

    if   exif[orientation] == 3 : 
        image=image.transpose(Image.ROTATE_180)
    elif exif[orientation] == 6 : 
        image=image.rotate(Image.ROTATE_180)
    elif exif[orientation] == 8 : 
        image=image.rotate(Image.ROTATE_180)

    image.thumbnail((THUMB_WIDTH , THUMB_HIGHT), Image.ANTIALIAS)
    image.save(os.path.join(path,fileName))

except:
    traceback.print_exc()

3
你可以将这个条件语句简化为: <code> 如果 exif[orientation] 在 [3,6,8] 中: 图像 = 图像.transpose(Image.ROTATE_180) </code> - Trey Stout

3

除了其他答案之外,我遇到了问题,因为在运行函数之前我会使用im.copy() -- 这会剥离必要的exif数据。确保在运行im.copy()之前保存exif数据:

try:
    exif = im._getexif()
except Exception:
    exif = None

# ...
# im = im.copy() somewhere
# ...

if exif:
    im = transpose_im(im, exif)

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