Obtenir la taille de l'image SANS charger l'image en mémoire

113

Je comprends que vous pouvez obtenir la taille de l'image à l'aide de PIL de la manière suivante

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

Cependant, je voudrais obtenir la largeur et la hauteur de l'image sans avoir à charger l'image en mémoire. Est-ce possible? Je ne fais que des statistiques sur les tailles d'image et ne me soucie pas du contenu de l'image. Je veux juste accélérer mon traitement.

Sami A. Haija
la source
8
Je ne suis pas sûr à 100%, mais je ne crois pas que cela .open()lit le fichier entier en mémoire ... (c'est ce que .load()) fait - donc pour autant que je sache - c'est aussi bon que possiblePIL
Jon Clements
5
Même si vous pensez avoir une fonction qui ne lit que les informations d'en-tête de l'image, le code de lecture anticipée du système de fichiers peut toujours charger l'image entière. Se soucier des performances est improductif à moins que votre application ne l'exige.
Stark
1
J'ai été convaincu de vos réponses. Merci @JonClements et stark
Sami A. Haija
9
Un test de mémoire rapide utilisant pmappour surveiller la mémoire utilisée par un processus me montre qu'en effet PILne charge pas toute l'image en mémoire.
Vincent Nivoliers

Réponses:

63

Comme l'indiquent les commentaires, PIL ne charge pas l'image en mémoire lors de l'appel .open. En regardant la documentation de PIL 1.1.7, la docstring for .opendit:

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

Il y a quelques opérations sur les fichiers dans la source comme:

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

mais ceux-ci ne constituent guère la lecture de l'ensemble du dossier. En fait, .openrenvoie simplement un objet fichier et le nom du fichier en cas de succès. De plus, les documents disent:

open (fichier, mode = "r")

Ouvre et identifie le fichier image donné.

C'est une opération paresseuse; cette fonction identifie le fichier, mais les données d'image réelles ne sont pas lues à partir du fichier tant que vous n'essayez pas de traiter les données (ou d'appeler la méthode de chargement ).

En creusant plus profondément, nous voyons que les .openappels _opensont une surcharge spécifique au format d'image. Chacune des implémentations à _openpeut être trouvée dans un nouveau fichier, par exemple. Les fichiers .jpeg sont au format JpegImagePlugin.py. Examinons celui-ci en profondeur.

Ici, les choses semblent devenir un peu délicates, il y a une boucle infinie qui se rompt lorsque le marqueur jpeg est trouvé:

    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")

Ce qui semble pouvoir lire tout le fichier s'il était mal formé. S'il lit le marqueur d'information OK, cependant, il devrait sortir tôt. La fonction handlerdéfinit finalement self.sizequelles sont les dimensions de l'image.

Accroché
la source
1
C'est vrai, mais openobtient-il la taille de l'image ou est-ce aussi une opération paresseuse? Et s'il est paresseux, lit-il les données d'image en même temps?
Mark Ransom
Le lien doc pointe vers Pillow a fork de PIL. Je ne trouve cependant pas de lien officiel de doc sur le web. Si quelqu'un le publie sous forme de commentaire, je mettrai à jour la réponse. Le devis se trouve dans le fichier Docs/PIL.Image.html.
Accroché le
@MarkRansom J'ai essayé de répondre à votre question, mais pour être sûr à 100%, il semble que nous devions nous plonger dans chaque implémentation spécifique à l'image. Le .jpegformat semble correct tant que l'en-tête est trouvé.
Accroché le
@Hooked: Merci beaucoup d'avoir examiné cela. J'accepte que vous ayez raison bien que j'aime assez la solution plutôt minimale de Paulo ci-dessous (même si pour être honnête, l'OP n'a pas mentionné vouloir éviter la dépendance PIL)
Alex Flint
@AlexFlint Pas de problème, c'est toujours amusant de fouiller dans le code. Je dirais que Paulo a gagné sa prime cependant, c'est un bel extrait qu'il a écrit pour vous là-bas.
Accroché le
88

Si vous ne vous souciez pas du contenu de l'image, PIL est probablement exagéré.

Je suggère d'analyser la sortie du module magique 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')

Il s'agit d'un wrapper autour de libmagic qui lit le moins d'octets possible afin d'identifier une signature de type de fichier.

Version pertinente du script:

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

[mettre à jour]

Hmmm, malheureusement, lorsqu'il est appliqué aux jpegs, ce qui précède donne "'Données d'image JPEG, norme EXIF ​​2.21'". Aucune taille d'image! - Alex Flint

On dirait que les jpegs sont résistants à la magie. :-)

Je peux comprendre pourquoi: pour obtenir les dimensions de l'image pour les fichiers JPEG, vous devrez peut-être lire plus d'octets que libmagic aime lire.

J'ai retroussé mes manches et je suis venu avec cet extrait de code non testé (obtenez-le sur GitHub) qui ne nécessite aucun module tiers.

Regarde, maman!  Pas de profondeur!

#-------------------------------------------------------------------------------
# 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

[mise à jour 2019]

Découvrez une implémentation de Rust: https://github.com/scardine/imsz

Paulo Scardine
la source
3
J'ai également ajouté la possibilité de récupérer le nombre de canaux (à ne pas confondre avec la profondeur de bits) dans le commentaire après la version fournie par @EJEHardenberg ci-dessus .
Greg Kramida
2
Bonne chose. J'ai ajouté la prise en charge des bitmaps dans le projet GitHub. Merci!
Mallard
2
REMARQUE: la version actuelle ne fonctionne pas pour moi. @PauloScardine a une version de travail mise à jour sur github.com/scardine/image_size
DankMasterDan
2
Obtenez UnicodeDecodeError: 'utf-8' codec can't decode byte 0x89 in position 0: invalid start bytesur MacOS, python3 sur data = input.read(25), filesur l'image donnePNG image data, 720 x 857, 8-bit/color RGB, non-interlaced
mrgloom
3
Semble que le code de raw.githubusercontent.com/scardine/image_size/master/... fonctionne.
mrgloom
24

Il existe un package sur pypi appelé imagesizequi fonctionne actuellement pour moi, bien qu'il ne semble pas être très actif.

Installer:

pip install imagesize

Usage:

import imagesize

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

Page d'accueil: https://github.com/shibukawa/imagesize_py

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

Jonathan
la source
3
J'ai comparé la vitesse imagesize.get, magic.from_file et PIL pour obtenir la taille réelle de l'image par timeit. Les résultats ont montré que la vitesse imagesize.get (0.019s)> PIL (0.104s)> magic avec regex (0.1699s).
RyanLiu
9

Je récupère souvent des tailles d'image sur Internet. Bien sûr, vous ne pouvez pas télécharger l'image, puis la charger pour analyser les informations. Cela prend trop de temps. Ma méthode consiste à alimenter en morceaux un conteneur d'image et à tester s'il peut analyser l'image à chaque fois. Arrêtez la boucle lorsque j'obtiens les informations que je veux.

J'ai extrait le noyau de mon code et l'ai modifié pour analyser les fichiers locaux.

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)

Production:

(2240, 1488)
38912

La taille réelle du fichier est de 1 543 580 octets et vous ne lisez que 38 912 octets pour obtenir la taille de l'image. J'espère que cela aidera.

lovetl2002
la source
1

Une autre façon rapide de le faire sur les systèmes Unix. Cela dépend de la sortie filedont je ne suis pas sûr qu'elle soit normalisée sur tous les systèmes. Cela ne devrait probablement pas être utilisé dans le code de production. De plus, la plupart des JPEG ne signalent pas la taille de l'image.

import subprocess, re
image_size = list(map(int, re.findall('(\d+)x(\d+)', subprocess.getoutput("file " + filename))[-1]))
Lenar Hoyt
la source
GivesIndexError: list index out of range
mrgloom
0

Cette réponse a une autre bonne résolution, mais il manque le format pgm . Cette réponse a résolu le pgm . Et j'ajoute le bmp .

Les codes sont ci-dessous

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
Yantao Xie
la source
imghdrcependant gère assez mal certains jpeg.
martixy