161 votes

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

Je crois savoir que vous pouvez obtenir la taille de l'image en utilisant l'IAP 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 devant charger l'image en mémoire. Est-ce possible ? Je ne fais que des statistiques sur la taille des images et je ne me soucie pas de leur contenu. Je veux juste rendre mon traitement plus rapide.

122voto

Paulo Scardine Points 17518

Si vous ne vous préoccupez pas du contenu de l'image, l'IAP est probablement une surcharge.

Je suggère d'analyser la sortie du module python magic :

>>> 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'une enveloppe autour de libmagic qui lit aussi peu d'octets que possible afin d'identifier la signature d'un type de fichier.

Version pertinente de script :

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

[mise à jour]

Hmmm, malheureusement, lorsqu'on l'applique aux jpegs, cela donne "'Données d'image JPEG, norme EXIF 2.21'". Pas de taille d'image ! - Alex Flint

On dirait que les jpegs résistent à la magie :-)

Je peux comprendre pourquoi : pour obtenir les dimensions de l'image pour les fichiers JPEG, vous devez lire plus d'octets que ce que libmagic aime lire.

J'ai retroussé mes manches et je suis venu avec ce snippet très peu testé (obtenu sur GitHub) qui ne nécessite aucun module tiers.

Look, Ma! No deps!

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

84voto

Hooked Points 16345

Comme les commentaires l'indiquent, l'IAP ne charge pas l'image en mémoire lorsqu'il appelle .open . En regardant les docs de PIL 1.1.7 la docstring pour .open dit :

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 cela ne constitue pas une lecture complète du fichier. En effet .open renvoie simplement un objet fichier et le nom du fichier en cas de succès. En outre, la fonction docs dites :

open(fichier, mode="r")

Ouvre et identifie le fichier image donné.

Il s'agit d'une opération paresseuse ; cette fonction identifie le fichier, mais les données d'image réelles ne sont pas lues à partir du fichier jusqu'à ce que vous essayiez de traiter les données (ou appelez la fonction charge méthode).

En creusant davantage, nous constatons que .open appelle _open qui est une surcharge spécifique au format de l'image. Chacune des implémentations de _open peuvent être trouvés dans un nouveau fichier, par exemple, les fichiers .jpeg sont en JpegImagePlugin.py . Examinons-la en profondeur.

C'est là que les choses semblent devenir un peu délicates, car il y a une boucle infinie qui est interrompue 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 être pourrait lire le fichier entier s'il était malformé. En revanche, s'il lit le marqueur d'information correctement, il devrait s'échapper rapidement. La fonction handler en fin de compte self.size qui sont les dimensions de l'image.

72voto

Jonathan Points 644

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

Installez :

pip install imagesize

Utilisation :

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/

14voto

DreamFlasher Points 601

Le PO était intéressé par une solution "plus rapide", j'étais curieux de savoir quelle était la solution la plus rapide et j'essaie d'y répondre avec un benchmark du monde réel.

Je compare :

J'exécute le code suivant sur 202897 fichiers JPG pour la plupart.

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

Résultats :

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

Ainsi, l'opsdroid, le shibukawa et le kobaltcore fonctionnent à la même vitesse. Un autre point intéressant pour moi serait maintenant de mieux comprendre laquelle des bibliothèques a le meilleur support de format.

[EDIT] J'ai donc pris les devants et testé si les bibliothèques rapides donnent des résultats différents :

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

Et ils ne le font pas.

11voto

user2923419 Points 386

Je vais souvent chercher la taille des images 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 à envoyer des morceaux à un conteneur d'images et à tester s'il peut analyser l'image à chaque fois. J'arrête 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)

Sortie :

(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 vous aidera.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X