【问题标题】:Get Image size WITHOUT loading image into memory获取图像大小而不将图像加载到内存中
【发布时间】:2013-03-25 21:46:38
【问题描述】:

我了解您可以通过以下方式使用 PIL 获取图像大小

from PIL import Image
im = Image.open(image_filename)
width, height = im.size

但是,我想获得图像的宽度和高度,而不必将图像加载到内存中。那可能吗?我只对图像大小进行统计,并不关心图像内容。我只是想让我的处理速度更快。

【问题讨论】:

  • 我不是 100% 确定,但我不相信 .open() 将整个文件读入内存......(这就是 .load())所做的 - 据我所知 -这和使用PIL 一样好
  • 即使你认为你有一个只读取图像头信息的函数,文件系统预读代码仍然可能会加载整个图像。除非您的应用程序需要,否则担心性能是徒劳的。
  • 我对您的回答深信不疑。谢谢@JonClements 和斯塔克
  • 使用pmap 监控进程使用的内存的快速内存测试表明PIL 确实不会将整个图像加载到内存中。

标签: python image image-processing


【解决方案1】:

如果你不关心图像内容,PIL 可能是矫枉过正。

我建议解析python魔术模块的输出:

>>> t = magic.from_file('teste.png')
>>> t
'PNG image data, 782 x 602, 8-bit/color RGBA, non-interlaced'
>>> re.search('(\d+) x (\d+)', t).groups()
('782', '602')

这是一个 libmagic 的包装器,它读取尽可能少的字节以识别文件类型签名。

脚本的相关版本:

https://raw.githubusercontent.com/scardine/image_size/master/get_image_size.py

[更新]

嗯,不幸的是,当应用于 jpeg 时,上面给出了“'JPEG 图像数据,EXIF 标准 2.21'”。没有图片大小! ——亚历克斯·弗林特

似乎 jpeg 具有抗魔力。 :-)

我明白为什么:为了获得 JPEG 文件的图像尺寸,您可能需要读取比 libmagic 喜欢读取的更多的字节数。

卷起袖子,带来了不需要第三方模块的this very untested snippet (get it from GitHub)

#-------------------------------------------------------------------------------
# Name:        get_image_size
# Purpose:     extract image dimensions given a file path using just
#              core modules
#
# Author:      Paulo Scardine (based on code from Emmanuel VAÏSSE)
#
# Created:     26/09/2013
# Copyright:   (c) Paulo Scardine 2013
# Licence:     MIT
#-------------------------------------------------------------------------------
#!/usr/bin/env python
import os
import struct

class UnknownImageFormat(Exception):
    pass

def get_image_size(file_path):
    """
    Return (width, height) for a given img file content - no external
    dependencies except the os and struct modules from core
    """
    size = os.path.getsize(file_path)

    with open(file_path) as input:
        height = -1
        width = -1
        data = input.read(25)

        if (size >= 10) and data[:6] in ('GIF87a', 'GIF89a'):
            # GIFs
            w, h = struct.unpack("<HH", data[6:10])
            width = int(w)
            height = int(h)
        elif ((size >= 24) and data.startswith('\211PNG\r\n\032\n')
              and (data[12:16] == 'IHDR')):
            # PNGs
            w, h = struct.unpack(">LL", data[16:24])
            width = int(w)
            height = int(h)
        elif (size >= 16) and data.startswith('\211PNG\r\n\032\n'):
            # older PNGs?
            w, h = struct.unpack(">LL", data[8:16])
            width = int(w)
            height = int(h)
        elif (size >= 2) and data.startswith('\377\330'):
            # JPEG
            msg = " raised while trying to decode as JPEG."
            input.seek(0)
            input.read(2)
            b = input.read(1)
            try:
                while (b and ord(b) != 0xDA):
                    while (ord(b) != 0xFF): b = input.read(1)
                    while (ord(b) == 0xFF): b = input.read(1)
                    if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
                        input.read(3)
                        h, w = struct.unpack(">HH", input.read(4))
                        break
                    else:
                        input.read(int(struct.unpack(">H", input.read(2))[0])-2)
                    b = input.read(1)
                width = int(w)
                height = int(h)
            except struct.error:
                raise UnknownImageFormat("StructError" + msg)
            except ValueError:
                raise UnknownImageFormat("ValueError" + msg)
            except Exception as e:
                raise UnknownImageFormat(e.__class__.__name__ + msg)
        else:
            raise UnknownImageFormat(
                "Sorry, don't know how to get information from this file."
            )

    return width, height

[2019 年更新]

查看 Rust 实现:https://github.com/scardine/imsz

【讨论】:

  • 我还在the version @EJEHardenberg provides above之后的评论中添加了检索通道数(不要与位深度混淆)的功能。
  • 好东西。我在 GitHub 项目中添加了对位图的支持。谢谢!
  • 注意:当前版本不适合我。 @PauloScardine 在 github.com/scardine/image_size 上有更新的工作版本
  • 在 MacOS 上获取UnicodeDecodeError: 'utf-8' codec can't decode byte 0x89 in position 0: invalid start byte,在data = input.read(25) 上获取python3,在图像上获取file 提供PNG image data, 720 x 857, 8-bit/color RGB, non-interlaced
【解决方案2】:

正如 cmets 所暗示的,PIL 在调用 .open 时不会将图像加载到内存中。查看PIL 1.1.7 的文档,.open 的文档字符串说:

def open(fp, mode="r"):
    "Open an image file, without loading the raster data"

源码中有一些文件操作如:

 ...
 prefix = fp.read(16)
 ...
 fp.seek(0)
 ...

但这些几乎不构成读取整个文件。事实上.open 只是返回一个文件对象和成功时的文件名。另外docs 说:

打开(文件,模式=“r”)

打开并识别给定的图像文件。

这是一个惰性操作;此函数识别文件,但在您尝试处理数据(或调用 load 方法)之前,不会从文件中读取实际的图像数据。

深入挖掘,我们看到.open 调用_open,这是一个特定于图像格式的重载。 _open 的每个实现都可以在一个新文件中找到,例如。 .jpeg 文件位于JpegImagePlugin.py。让我们深入了解一下。

这里的事情似乎有点棘手,其中有一个无限循环,当找到 jpeg 标记时会中断:

    while True:

        s = s + self.fp.read(1)
        i = i16(s)

        if i in MARKER:
            name, description, handler = MARKER[i]
            # print hex(i), name, description
            if handler is not None:
                handler(self, i)
            if i == 0xFFDA: # start of scan
                rawmode = self.mode
                if self.mode == "CMYK":
                    rawmode = "CMYK;I" # assume adobe conventions
                self.tile = [("jpeg", (0,0) + self.size, 0, (rawmode, ""))]
                # self.__offset = self.fp.tell()
                break
            s = self.fp.read(1)
        elif i == 0 or i == 65535:
            # padded marker or junk; move on
            s = "\xff"
        else:
            raise SyntaxError("no marker found")

如果文件格式错误,它看起来可以读取整个文件。但是,如果它读取信息标记 OK,它应该会提前爆发。函数handler 最终设置self.size,它们是图像的尺寸。

【讨论】:

  • 确实如此,但是open 是否获得了图像的 size 或者这也是一个惰性操作?如果它是惰性的,它是否会同时读取图像数据?
  • 文档链接指向 Pillow a fork from PIL。但是,我在网上找不到官方文档链接。如果有人将其作为评论发布,我会更新答案。引用可以在文件Docs/PIL.Image.html中找到。
  • @MarkRansom 我已经尝试回答您的问题,但是要 100% 确定看起来我们必须深入研究每个特定于图像的实现。只要找到标头,.jpeg 格式看起来就可以。
  • @Hooked:非常感谢您对此进行调查。我接受你是正确的,尽管我非常喜欢 Paulo 下面的最小解决方案(尽管公平地说,OP 没有提到要避免 PIL 依赖)
  • @AlexFlint 没问题,浏览代码总是很有趣。我会说保罗赢得了他的赏金,那是他在那里为你写的一个很好的 sn-p。
【解决方案3】:

pypi 上有一个名为 imagesize 的包,目前对我有用,虽然它看起来不是很活跃。

安装:

pip install imagesize

用法:

import imagesize

width, height = imagesize.get("test.png")
print(width, height)

首页:https://github.com/shibukawa/imagesize_py

PyPi:https://pypi.org/project/imagesize/

【讨论】:

  • 我对比了imagesize.get、magic.from_file和PIL图像的速度,通过timeit得到了实际的图像大小。结果表明,imagesize.get (0.019s) > PIL(0.104s) > magic with regex (0.1699s) 的速度。
  • 我可以确认,imagesize 效果很好,对于简单地获取图像的尺寸非常有效
【解决方案4】:

我经常在 Internet 上获取图片尺寸。当然,你不能下载图像然后加载它来解析信息。太费时间了。我的方法是将块提供给图像容器并测试它是否每次都可以解析图像。当我得到我想要的信息时停止循环。

我提取了代码的核心并对其进行了修改以解析本地文件。

from PIL import ImageFile

ImPar=ImageFile.Parser()
with open(r"D:\testpic\test.jpg", "rb") as f:
    ImPar=ImageFile.Parser()
    chunk = f.read(2048)
    count=2048
    while chunk != "":
        ImPar.feed(chunk)
        if ImPar.image:
            break
        chunk = f.read(2048)
        count+=2048
    print(ImPar.image.size)
    print(count)

输出:

(2240, 1488)
38912

实际文件大小为 1,543,580 字节,而您仅读取 38,912 字节即可获得图像大小。希望这会有所帮助。

【讨论】:

  • 每次count+=2048都不是很聪明。线性...
【解决方案5】:

OP 对“更快”的解决方案感兴趣,我对最快的解决方案很好奇,我正在尝试用真实世界的基准来回答这个问题。

我在比较:

我在 202897 主要是 JPG 文件上运行以下代码。

"""
pip install opsdroid-get-image-size --user
pip install pymage_size
pip install imagesize
"""

import concurrent.futures
from pathlib import Path

import cv2
import numpy as np
import pandas as pd
from tqdm import tqdm
from PIL import Image
import get_image_size
import imagesize
import pymage_size

files = [str(p.resolve())
         for p in Path("/data/").glob("**/*")
         if p.suffix in {".jpg", ".jpeg", ".JPEG", ".JPG", ".png", ".PNG"}]

def get_shape_cv2(fname):
    img = cv2.imread(fname)
    return (img.shape[0], img.shape[1])

with concurrent.futures.ProcessPoolExecutor(8) as executor:
    results = list(tqdm(executor.map(get_shape_cv2, files), total=len(files)))

def get_shape_pil(fname):
    img=Image.open(fname)
    return (img.size[0], img.size[1])

with concurrent.futures.ProcessPoolExecutor(8) as executor:
    results = list(tqdm(executor.map(get_shape_pil, files), total=len(files)))

def get_shape_scardine_size(fname):
    try:
        width, height = get_image_size.get_image_size(fname)
    except get_image_size.UnknownImageFormat:
        width, height = -1, -1
    return (width, height)

with concurrent.futures.ProcessPoolExecutor(8) as executor:
    results = list(tqdm(executor.map(get_shape_scardine_size, files), total=len(files)))

def get_shape_shibukawa(fname):
    width, height = imagesize.get(fname)
    return (width, height)

with concurrent.futures.ProcessPoolExecutor(8) as executor:
    results = list(tqdm(executor.map(get_shape_shibukawa, files), total=len(files)))

def get_shape_pymage_size(fname):
    img_format = pymage_size.get_image_size(fname)
    width, height = img_format.get_dimensions()
    return (width, height)

with concurrent.futures.ProcessPoolExecutor(8) as executor:
    results = list(tqdm(executor.map(get_shape_pymage_size, files), total=len(files)))

结果:

  • cv2.imread: 8m23s
  • PIL.open: 2m00s
  • opsdroid/image_size: 29 秒
  • shibukawa/imagesize_py: 29 秒
  • kobaltcore/pymage_size: 29 秒

所以 opsdroid、shibukawa 和 kobaltcore 以相同的速度运行。现在对我来说另一个有趣的点是更好地了解哪些库具有最好的格式支持。

[编辑] 所以我继续测试快速库是否提供不同的结果:

# test if the libs provide the same results
def show_size_differences(fname):
    w1, h1 = get_shape_scardine_size(fname)
    w2, h2 = get_shape_pymage_size(fname)
    w3, h3 = get_shape_shibukawa(fname)
    if w1 != w2 or w2 != w3 or h1 != h2 or h2 != h3:
        print(f"scardine: {w1}x{h1}, pymage: {w2}x{h2}, shibukawa: {w3}x{h3}")

with concurrent.futures.ProcessPoolExecutor(8) as executor:
    results = list(tqdm(executor.map(show_size_differences, files), total=len(files)))

他们没有。

【讨论】:

  • 但是为什么它们很慢?如果它们只是流行库中的简单错误怎么办?
  • 请也从scardine添加!
  • scardine 和 opsdroid 是一样的,见 Scardine 的自述文件
【解决方案6】:

在 Unix 系统上执行此操作的另一种简短方法。这取决于file 的输出,我不确定它是否在所有系统上都是标准化的。这可能不应该在生产代码中使用。此外,大多数 JPEG 不报告图像大小。

import subprocess, re
image_size = list(map(int, re.findall('(\d+)x(\d+)', subprocess.getoutput("file " + filename))[-1]))

【讨论】:

  • IndexError: list index out of range
  • 当我直接运行 python 文件时,这对我有用。但是当我尝试对我的代码进行 docker 容器化时,会抛出以下错误 IndexError: list index out of range 似乎以下命令无法在 docker 容器中找到我的文件 subprocess.getoutput("file " + filename)
【解决方案7】:

这个answer 有另一个很好的分辨率,但缺少pgm 格式。这个answer 已经解决了pgm。我添加了 bmp

代码如下

import struct, imghdr, re, magic

def get_image_size(fname):
    '''Determine the image type of fhandle and return its size.
    from draco'''
    with open(fname, 'rb') as fhandle:
        head = fhandle.read(32)
        if len(head) != 32:
            return
        if imghdr.what(fname) == 'png':
            check = struct.unpack('>i', head[4:8])[0]
            if check != 0x0d0a1a0a:
                return
            width, height = struct.unpack('>ii', head[16:24])
        elif imghdr.what(fname) == 'gif':
            width, height = struct.unpack('<HH', head[6:10])
        elif imghdr.what(fname) == 'jpeg':
            try:
                fhandle.seek(0) # Read 0xff next
                size = 2
                ftype = 0
                while not 0xc0 <= ftype <= 0xcf:
                    fhandle.seek(size, 1)
                    byte = fhandle.read(1)
                    while ord(byte) == 0xff:
                        byte = fhandle.read(1)
                    ftype = ord(byte)
                    size = struct.unpack('>H', fhandle.read(2))[0] - 2
                # We are at a SOFn block
                fhandle.seek(1, 1)  # Skip `precision' byte.
                height, width = struct.unpack('>HH', fhandle.read(4))
            except Exception: #IGNORE:W0703
                return
        elif imghdr.what(fname) == 'pgm':
            header, width, height, maxval = re.search(
                b"(^P5\s(?:\s*#.*[\r\n])*"
                b"(\d+)\s(?:\s*#.*[\r\n])*"
                b"(\d+)\s(?:\s*#.*[\r\n])*"
                b"(\d+)\s(?:\s*#.*[\r\n]\s)*)", head).groups()
            width = int(width)
            height = int(height)
        elif imghdr.what(fname) == 'bmp':
            _, width, height, depth = re.search(
                b"((\d+)\sx\s"
                b"(\d+)\sx\s"
                b"(\d+))", str).groups()
            width = int(width)
            height = int(height)
        else:
            return
        return width, height

【讨论】:

  • imghdr 但是处理某些 jpeg 很差。
猜你喜欢
  • 1970-01-01
  • 2020-05-01
  • 2010-12-05
  • 2012-06-04
  • 2023-04-05
  • 1970-01-01
  • 1970-01-01
  • 2023-03-24
  • 2023-03-10
相关资源
最近更新 更多