Get Image size WITHOUT loading image into memory

PythonImageImage Processing

Python Problem Overview


I understand that you can get the image size using PIL in the following fashion

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

However, I would like to get the image width and height without having to load the image in memory. Is that possible? I am only doing statistics on image sizes and dont care for the image contents. I just want to make my processing faster.

Python Solutions


Solution 1 - Python

If you don't care about the image contents, PIL is probably an overkill.

I suggest parsing the output of the python magic module:

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

This is a wrapper around libmagic which read as few bytes as possible in order to identify a file type signature.

Relevant version of script:

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

[update]

> Hmmm, unfortunately, when applied to jpegs, the above gives "'JPEG image data, EXIF standard 2.21'". No image size! – Alex Flint

Seems like jpegs are magic-resistant. :-)

I can see why: in order to get the image dimensions for JPEG files, you may have to read more bytes than libmagic likes to read.

Rolled up my sleeves and came with this very untested snippet (get it from GitHub) that requires no third-party modules.

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

[update 2019]

Check out a Rust implementation: https://github.com/scardine/imsz

Solution 2 - Python

As the comments allude, PIL does not load the image into memory when calling .open. Looking at the docs of PIL 1.1.7, the docstring for .open says:

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

There are a few file operations in the source like:

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

but these hardly constitute reading the whole file. In fact .open simply returns a file object and the filename on success. In addition the docs say:

> open(file, mode=”r”) > > Opens and identifies the given image file.
> > This is a lazy operation; this function identifies the file, but the actual image data is not read from the file until you try to process the data (or call the load method).

Digging deeper, we see that .open calls _open which is a image-format specific overload. Each of the implementations to _open can be found in a new file, eg. .jpeg files are in JpegImagePlugin.py. Let's look at that one in depth.

Here things seem to get a bit tricky, in it there is an infinite loop that gets broken out of when the jpeg marker is found:

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

Which looks like it could read the whole file if it was malformed. If it reads the info marker OK however, it should break out early. The function handler ultimately sets self.size which are the dimensions of the image.

Solution 3 - Python

There is a package on pypi called imagesize that currently works for me, although it doesn't look like it is very active.

Install:

pip install imagesize

Usage:

import imagesize

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

Homepage: https://github.com/shibukawa/imagesize_py

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

Solution 4 - Python

I often fetch image sizes on the Internet. Of course, you can't download the image and then load it to parse the information. It's too time consuming. My method is to feed chunks to an image container and test whether it can parse the image every time. Stop the loop when I get the information I want.

I extracted the core of my code and modified it to parse local files.

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)

Output:

(2240, 1488)
38912

The actual file size is 1,543,580 bytes and you only read 38,912 bytes to get the image size. Hope this will help.

Solution 5 - Python

The OP was interested in a "faster" solution, I was curious about the fastest solution and I am trying to answer that with a real-world benchmark.

I am comparing:

I am running the following code on 202897 mostly JPG files.

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

Results:

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

So the opsdroid, shibukawa and kobaltcore perform at the same speed. Another interesting point for me would now be to better understand which of the libraries has the best format support.

[EDIT] So I went ahead and tested if the fast libraries provide different results:

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

And they don't.

Solution 6 - Python

Another short way of doing it on Unix systems. It depends on the output of file which I am not sure is standardized on all systems. This should probably not be used in production code. Moreover most JPEGs don't report the image size.

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

Solution 7 - Python

This answer has an another good resolution, but missing the pgm format. This answer has resolved the pgm. And I add the bmp.

Codes is below

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSami A. HaijaView Question on Stackoverflow
Solution 1 - PythonPaulo ScardineView Answer on Stackoverflow
Solution 2 - PythonHookedView Answer on Stackoverflow
Solution 3 - PythonJonathanView Answer on Stackoverflow
Solution 4 - Pythonlovetl2002View Answer on Stackoverflow
Solution 5 - PythonDreamFlasherView Answer on Stackoverflow
Solution 6 - PythonLenar HoytView Answer on Stackoverflow
Solution 7 - PythonYantao XieView Answer on Stackoverflow