PIL 缩略图正在旋转我的图像?

Posted

技术标签:

【中文标题】PIL 缩略图正在旋转我的图像?【英文标题】:PIL thumbnail is rotating my image? 【发布时间】:2011-05-12 19:51:51 【问题描述】:

我正在尝试(从数码相机拍摄)大(巨大)图像,并将它们转换成可以在网络上显示的东西。这看起来很简单,而且可能应该如此。但是,当我尝试使用 PIL 创建缩略图版本时,如果我的源图像高于它的宽度,则生成的图像会旋转 90 度,这样源图像的顶部就位于生成图像的左侧。如果源图像宽于高,则生成的图像是正确的(原始)方向。这可能与我发送的 2 元组大小有关吗?我正在使用缩略图,因为它似乎是为了保留纵横比。还是我只是完全失明,做着愚蠢的事情?元组的大小为 1000,1000,因为我希望将最长边缩小到 1000 像素,同时保留 AR。

代码看起来很简单

img = Image.open(filename)
img.thumbnail((1000,1000), Image.ANTIALIAS)
img.save(output_fname, "JPEG")

提前感谢您的帮助。

【问题讨论】:

为其他人添加注释:我认为.thumbnail() 不要轮换——我使用img.show() 验证了自己。实际上是.save() 方法写入文件。我尝试的是: - 而不是写入磁盘文件尝试写入内存文件from io import BytesIO; buffer = BytesIO; img.save(buffer, "JPEG"); Image.open(buffer).show() 【参考方案1】:

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


当图片的高度大于宽度时,表示相机已旋转。一些相机可以检测到这一点并将该信息写入图片的 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 数据并手动应用转换。 感谢两位的回答。我正在尝试剥离所有 EXIF 数据,但如果必须旋转数据,则将其添加回来。这比我最初想象的更像是一个 PITA。现在只需制定脚本即可。再次感谢! 由于您正在调整大小,您可能并不在意,但请不要忘记,即使是简单的旋转有时也会对 jpeg 进行有损操作。 Storm_to 的回答更好。方向有多个值,需要以不同方式处理。 我赞成处理所有 8 个方向的版本。另外,这里有一组很棒的测试图片github.com/recurser/exif-orientation-examples,来自 Dave Perrett。【参考方案2】:

我同意“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()

【讨论】:

如果用于非 JPEG 文件或 EXIF 数据不存在,请注意这会导致 stracktrace。 这段代码的缩进有问题吗? “fororientation...break”逻辑块的目的是什么? @Robert 它从 ExifTags 集合中抓取“方向”标签,然后用它来测试方向值 为什么要遍历所有标签,它不总是相同的键吗?【参考方案3】:

Hoopes 的答案很棒,但使用转置方法比旋转更有效。 Rotate 对每个像素进行实际的过滤计算,实际上是整个图像的复杂调整大小。此外,当前的 PIL 库似乎存在一个错误,即在旋转图像的边缘添加了一条黑线。 Transpose 速度快了很多,而且没有这个错误。我只是调整了 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()

【讨论】:

如果 exif[orientation] in [3,6,8],您可以将该条件粉碎为 :image = image.transpose(Image.ROTATE_180) 【参考方案4】:

xilvar 的回答非常好,但有两个小缺点我想在被拒绝的编辑中修复,所以我将其作为答案发布。

首先,如果文件不是 JPEG 或不存在 exif 数据,xilvar 的解决方案将失败。另一方面,它总是旋转 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 可能无效的日志。我并不是说它可能会给每个人带来错误,但它发生在我身上并且可能非常罕见。 我会使用orientation = next(k for k, v in ExifTags.TAGS.items() if v == 'Orientation'),因为这个脚本依赖于这个标签并且PIL的ExifTags.py似乎有它。【参考方案5】:

这是适用于所有 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

【讨论】:

很好的解决方案。像魅力一样工作。 这行得通,但我想知道这里是否可以做更多的 OOP 风格,比如从 PIL 添加到 Image 类。另外,我认为在 try 块中包含这么多代码不是一个好主意。你有几件事可能会失败,很高兴知道哪个恕我直言。 使用插件系统,您可以将JPEG插件的保存功能替换为保存时自动旋转的功能。也许并不总是想要,因为这往往会摆脱 EXIF 数据,但可以方便且少写一行。 gist.github.com/Tatsh/9f713edc102df99fc612486a2c571a6e【参考方案6】:

您好,我正在尝试实现图像的旋转,感谢本文中先前的答案,我做到了。但我升级了解决方案并想分享它。我希望有人会发现这很有用。

def get_rotation_code(img):
    """
    Returns rotation code which say how much photo is rotated.
    Returns None if photo does not have exif tag information. 
    Raises Exception if cannot get Orientation number from python 
    image library.
    """
    if not hasattr(img, '_getexif') or img._getexif() is None:
        return None

    for code, name in ExifTags.TAGS.iteritems():
        if name == 'Orientation':
            orientation_code = code
            break
    else:
        raise Exception('Cannot get orientation code from library.')

    return img._getexif().get(orientation_code, None)


class IncorrectRotationCode(Exception):
    pass


def rotate_image(img, rotation_code):
    """
    Returns rotated image file.

    img: PIL.Image file.
    rotation_code: is rotation code retrieved from get_rotation_code.
    """
    if rotation_code == 1:
        return img
    if rotation_code == 3:
        img = img.transpose(Image.ROTATE_180)
    elif rotation_code == 6:
        img = img.transpose(Image.ROTATE_270)
    elif rotation_code == 8:
        img = img.transpose(Image.ROTATE_90)
    else:
        raise IncorrectRotationCode(' is unrecognized '
                                    'rotation code.'
                                    .format(rotation_code))
    return img

用途:

>>> img = Image.open('/path/to/image.jpeg')
>>> rotation_code = get_rotation_code(img)
>>> if rotation_code is not None:
...     img = rotate_image(img, rotation_code)
...     img.save('/path/to/image.jpeg')
...

【讨论】:

【参考方案7】:

我是编程、Python 和 PIL 的菜鸟,所以前面答案中的代码示例对我来说似乎很复杂。我没有遍历标签,而是直接找到标签的键。在python shell中,可以看到orientation的key是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 数据。【参考方案8】:

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

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

【讨论】:

只有 import functools 丢失,仍然应该是 Python 2.7 中开箱即用的公认答案 向你致敬。这是最完整的答案,使用最合适的工具。其他答案要么使用rotate 而不是transpose,要么缺少所有八种可能的状态。 JPEG 退出方向用于转置,而不是用于旋转。 jpegclub.org/exif_orientation.html 确实可以直接插入。但是它可靠地工作吗?请确认,因为我现在得到的结果好坏参半。这是我测试过的图像(方向为0):imgur.com/a/053MR 我刚刚修复了代码中方向为:未知 (0) 的错误。这导致索引为 -1,这意味着 python 返回数组中的最后一项,即 ROTATE_90,这确实使用户非常交叉。 @Felix 好吧,规范不允许方向为 0。但情况仍需处理。我通过在列表中添加一个条目而不是额外的 if 来修复您的修复,并在我在它的时候添加了一些 cmets。【参考方案9】:

这里有一些很好的答案,我只是想发布一个清理后的版本...... 该函数假定您已经在某处完成了 Image.open(),并将在其他地方执行 image.save(),并且只需要一个可以插入的函数来修复旋转。

def _fix_image_rotation(image):
 orientation_to_rotation_map = 
     3: Image.ROTATE_180,
     6: Image.ROTATE_270,
     8: Image.ROTATE_90,
 
 try:
     exif = _get_exif_from_image(image)
     orientation = _get_orientation_from_exif(exif)
     rotation = orientation_to_rotation_map.get(orientation)
     if rotation:
         image = image.transpose(rotation)

 except Exception as e:
     # Would like to catch specific exceptions, but PIL library is poorly documented on Exceptions thrown
     # Log error here

 finally:
     return image


def _get_exif_from_image(image):
 exif = 

 if hasattr(image, '_getexif'):  # only jpegs have _getexif
     exif_or_none = image._getexif()
     if exif_or_none is not None:
         exif = exif_or_none

 return exif


def _get_orientation_from_exif(exif):
 ORIENTATION_TAG = 'Orientation'
 orientation_iterator = (
     exif.get(tag_key) for tag_key, tag_value in ExifTags.TAGS.items()
     if tag_value == ORIENTATION_TAG
 )
 orientation = next(orientation_iterator, None)
 return orientation

【讨论】:

【参考方案10】:

我需要一个能够兼顾所有方向的解决方案,而不仅仅是368

我尝试了 Roman Odaisky 的 solution - 它看起来全面而干净。但是,使用具有各种方向值的实际图像对其进行测试有时会导致错误的结果(例如,this one 方向设置为 0)。

另一个viable solution 可能是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 答案。 不错。最完整的答案。 PIL==6.2.2 中的一件小事,我没有 _getexif() 但有 getexif()【参考方案11】:

除了其他答案之外,我遇到了问题,因为我会在运行函数之前使用 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)

【讨论】:

【参考方案12】:

有一个更简单的方法来解决这一切:

    from PIL import image
    im1 = Image.open(path_to_image)
    im1.thumbnail(size1, Image.ANTIALIAS)
    y, z = im1.size
    d = z * 1.5
    if y > d:
         im1.rotate(90, expand=1)

希望对你有帮助:)

【讨论】:

【参考方案13】:

Pillow 有 an API 自动处理 EXIF 方向标签:

from PIL import Image, ImageOps

original_image = Image.open(filename)

fixed_image = ImageOps.exif_transpose(original_image)

【讨论】:

在此之前我测试了一些其他的解决方案。这是最简单的,我的问题也解决了,因为它也检查输入格式。只接受有效的 PIL Image 输入。就我而言,我之前使用以下行破坏了 exif 信息: image = numpy.array(image) 这是唯一对我有用的解决方案。但在我的情况下,我使用 ImageReader 来读取图像而不是 Image。所以我必须将文件保存到内存中,然后用 Image() 打开 exif_t​​ranspose,然后使用 ImageReader()。 谢谢,这是最优雅的解决方案,无需切换到其他库

以上是关于PIL 缩略图正在旋转我的图像?的主要内容,如果未能解决你的问题,请参考以下文章

批量生成缩略图

Django / PIL - 上传图像时保存缩略图版本

PIL:缩略图并以方形图像结尾

如何使用 PIL 生成圆形缩略图?

在swift中创建网页缩略图

图片上传有效,缩略图歪斜?