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

from PIL import Imageim = 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.

10

Best Answer


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 pythonimport osimport structclass UnknownImageFormat(Exception):passdef get_image_size(file_path):"""Return (width, height) for a given img file content - no externaldependencies except the os and struct modules from core"""size = os.path.getsize(file_path)with open(file_path) as input:height = -1width = -1data = input.read(25)if (size >= 10) and data[:6] in ('GIF87a', 'GIF89a'):# GIFsw, 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')):# PNGsw, 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'):# JPEGmsg = " 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))breakelse: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

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, descriptionif handler is not None:handler(self, i)if i == 0xFFDA: # start of scanrawmode = self.modeif self.mode == "CMYK":rawmode = "CMYK;I" # assume adobe conventionsself.tile = [("jpeg", (0,0) + self.size, 0, (rawmode, ""))]# self.__offset = self.fp.tell()breaks = self.fp.read(1)elif i == 0 or i == 65535:# padded marker or junk; move ons = "\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.

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 imagesizewidth, height = imagesize.get("test.png")print(width, height)

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

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

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:

  • cv2.imread: https://www.kite.com/python/docs/cv2.imread
  • PIL.open: https://pillow.readthedocs.io/en/stable/reference/Image.html
  • opsdroid/image_size: https://github.com/opsdroid/image_size (packaged version of https://raw.githubusercontent.com/scardine/image_size/master/get_image_size.py)
  • shibukawa/imagesize_py: https://github.com/shibukawa/imagesize_py
  • kobaltcore/pymage_size: https://github.com/kobaltcore/pymage_size

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

"""pip install opsdroid-get-image-size --userpip install pymage_sizepip install imagesize"""import concurrent.futuresfrom pathlib import Pathimport cv2import numpy as npimport pandas as pdfrom tqdm import tqdmfrom PIL import Imageimport get_image_sizeimport imagesizeimport pymage_sizefiles = [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, -1return (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 resultsdef 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.

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 ImageFileImPar=ImageFile.Parser()with open(r"D:\testpic\test.jpg", "rb") as f:ImPar=ImageFile.Parser()chunk = f.read(2048)count=2048while chunk != "":ImPar.feed(chunk)if ImPar.image:breakchunk = f.read(2048)count+=2048print(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.

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, reimage_size = list(map(int, re.findall('(\d+)x(\d+)', subprocess.getoutput("file " + filename))[-1]))

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, magicdef 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:returnif imghdr.what(fname) == 'png':check = struct.unpack('>i', head[4:8])[0]if check != 0x0d0a1a0a:returnwidth, 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 nextsize = 2ftype = 0while 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 blockfhandle.seek(1, 1) # Skip `precision' byte.height, width = struct.unpack('>HH', fhandle.read(4))except Exception: #IGNORE:W0703returnelif 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:returnreturn width, height

Beside this is a very old question, I tried several of these approaches but nothing works for my large 3D Tif-File. So here is a very easy and fast solution, using the "tifffile" package "memmap" function:

 import tifffilememmap_image = tifffile.memmap(fp)memmap_image.shape

Runtime for my 450 GB 32bit Tif-Image: 10 milliseconds

I tried:

  1. magic library
  2. PIL (Python Image Library)
  3. imagesize library

What I discovered:

  1. Using regex to parse the string output from magic is unreliable because the string output is different for all file types. If your system accepts several or many different image types, you'd have to make sure you're parsing the string output correctly. See my test's output below to for what the string output looks like for each file type.
  2. imagesize library is not very robust. Out of the box currently, it analyzes JPEG/JPEG 2000/PNG/GIF/TIFF/SVG/Netpbm/WebP. When it tries to find the image size of a file it's not equipped to handle, it gives you a size of (-1, -1). You can see this in my .BMP example below.

My Conclusion - I would choose PIL. It's not as fast as imagesize library, but it is more robust to handle more file types. I also think it's "fast enough" for most use-cases. Using re to parse the magic output is not reliable, and it's much slower compared to PIL.

My Test

I took a saved image on my hard drive (636 x 636) and saved it into 6 different file formats (.png, .jpg, .jpeg, .tif, .tiff, .bmp). The images and the script were all saved in the same directory. The file sizes of each file type in my test are commented out below next to the file names.

The script:

import osimport reimport timeitimport magicimport imagesizeimport timefrom PIL import Image"""Notes:- all images are the same image saved as different formats- file extensions tested are: .png, .jpg, .jpeg, .tif, .tiff, .bmp- all images in this test are size 636 x 636- all images are in the same directory as this scriptIf you want to setup this similar experiment, take a single image,save it as: png_image.png, jpg_image.jpg, jpeg_image.jpeg, tif_image.tif,tiff_image.tiff, and bmp_image.bmp (others if you'd like),in the same directory as this script, and run this script.Or name the images whatever and modify the script below. You do you."""NUMBER = 10000REPEAT = 5def regex(filename):name,ext = os.path.splitext(filename)if ext.lower() in ['.tif', '.tiff']:return '^(?=.*width=(\d+))(?=.*height=(\d+))'elif ext.lower() in ['.jpg', '.jpeg', '.png']:return '(\d+)\s?x\s?(\d+)'elif ext.lower() in ['.bmp']:return '(\d+)\s?x\s?(\d+)\s?x\s?\d+'else:raise Exception('Extension %s is not accounted for.' % ext.lower())PNG_FILE = 'png_image.png' # 559 KBJPG_FILE = 'jpg_image.jpg' # 94 KBJPEG_FILE = 'jpeg_image.jpeg' # 94 KBTIF_FILE = 'tif_image.tif' # 768 KBTIFF_FILE = 'tiff_image.tiff' # 768 KBBMP_FILE = 'bmp_image.bmp' # 1,581 KBFILENAMES = [PNG_FILE, JPG_FILE, JPEG_FILE, TIF_FILE, TIFF_FILE, BMP_FILE]now = time.time()for filename in FILENAMES:print('#' * 36)print((" Testing %s" % filename).center(36, "#"))print('#' * 36)print('# ' + 'magic library'.center(32) + ' #')print(' ', 'output:', magic.from_file(filename))print(' ', "Size:", re.findall(regex(filename), magic.from_file(filename))[-1])print(' ', "Regex used:", regex(filename))print('# ' + 'PIL library'.center(32) + ' #')image = Image.open(filename)print(' ', image)print(' ', "Size:", image.size)print(' ', "Regex used:", 'None')print('# ' + 'imagesize library'.center(32) + ' #')image = imagesize.get(filename)print(' ', "Size:", image)print(' ', "Regex used:", 'None')print('-' * 30 + '\n')print("#################################end#######################################\n")start = time.time()for filename in FILENAMES:print((" Testing %s " % filename).center(36, "#"))# magic librarymagic_timer = timeit.Timer(stmt="width, height = re.findall(pattern, magic.from_file(filename))[-1]",setup="import magic; import re; filename='" + filename + "'; pattern=r'" + regex(filename) + "';",)magic_timeit = magic_timer.timeit(number=NUMBER)magic_repeat = magic_timer.repeat(repeat=REPEAT, number=NUMBER)print('magic'.ljust(12) + ":", "%.15f," % magic_timeit, "%s repeat avg. : %.15f" % (REPEAT, sum(magic_repeat) / REPEAT))# PIL librarypillow_timer = timeit.Timer(stmt="width, height = Image.open(filename).size;",setup="from PIL import Image; filename='" + filename + "';",)pillow_timeit = pillow_timer.timeit(number=NUMBER)pillow_repeat = pillow_timer.repeat(repeat=REPEAT, number=NUMBER)print('PIL'.ljust(12) + ":", "%.15f," % pillow_timeit, "%s repeat avg. : %.15f" % (REPEAT, sum(pillow_repeat) / REPEAT))# imagesize libraryimagesize_timer = timeit.Timer(stmt="width, height = imagesize.get(filename);",setup="import imagesize; filename='" + filename + "';",)imagesize_timeit = imagesize_timer.timeit(number=NUMBER)imagesize_repeat = imagesize_timer.repeat(repeat=REPEAT, number=NUMBER)print('imagesize'.ljust(12) + ":", "%.15f," % imagesize_timeit, "%s repeat avg. : %.15f" % (REPEAT, sum(imagesize_repeat) / REPEAT))stop = time.time()mins, secs = divmod(stop - start, 60)print('\nTest time: %d minutes %d seconds' % (mins, secs)) print("\n#################################end#######################################\n")

The output:

########################################### Testing png_image.png############################################ magic library #output: PNG image data, 636 x 636, 8-bit/color RGB, non-interlacedSize: ('636', '636')Regex used: (\d+)\s?x\s?(\d+)# PIL library #<PIL.PngImagePlugin.PngImageFile image mode=RGB size=636x636 at 0x1EBDE962710>Size: (636, 636)Regex used: None# imagesize library #Size: (636, 636)Regex used: None------------------------------########################################### Testing jpg_image.jpg############################################ magic library #output: JPEG image data, JFIF standard 1.01, resolution (DPI), density 96x96, segment length 16, baseline, precision 8, 636x636, frames 3Size: ('636', '636')Regex used: (\d+)\s?x\s?(\d+)# PIL library #<PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=636x636 at 0x1EBDF3E1810>Size: (636, 636)Regex used: None# imagesize library #Size: (636, 636)Regex used: None------------------------------########################################## Testing jpeg_image.jpeg########################################### magic library #output: JPEG image data, JFIF standard 1.01, resolution (DPI), density 96x96, segment length 16, baseline, precision 8, 636x636, frames 3Size: ('636', '636')Regex used: (\d+)\s?x\s?(\d+)# PIL library #<PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=636x636 at 0x1EBDF3E3010>Size: (636, 636)Regex used: None# imagesize library #Size: (636, 636)Regex used: None------------------------------########################################### Testing tif_image.tif############################################ magic library #output: TIFF image data, little-endian, direntries=16, height=636, bps=63732, compression=LZW, PhotometricIntepretation=RGB, width=636Size: ('636', '636')Regex used: ^(?=.*width=(\d+))(?=.*height=(\d+))# PIL library #<PIL.TiffImagePlugin.TiffImageFile image mode=RGBA size=636x636 at 0x1EBDF3E1810>Size: (636, 636)Regex used: None# imagesize library #Size: (636, 636)Regex used: None------------------------------########################################## Testing tiff_image.tiff########################################### magic library #output: TIFF image data, little-endian, direntries=16, height=636, bps=63732, compression=LZW, PhotometricIntepretation=RGB, width=636Size: ('636', '636')Regex used: ^(?=.*width=(\d+))(?=.*height=(\d+))# PIL library #<PIL.TiffImagePlugin.TiffImageFile image mode=RGBA size=636x636 at 0x1EBDF3E3160>Size: (636, 636)Regex used: None# imagesize library #Size: (636, 636)Regex used: None------------------------------########################################### Testing bmp_image.bmp############################################ magic library #output: PC bitmap, Windows 3.x format, 636 x 636 x 32Size: ('636', '636')Regex used: (\d+)\s?x\s?(\d+)\s?x\s?\d+# PIL library #<PIL.BmpImagePlugin.BmpImageFile image mode=RGB size=636x636 at 0x1EBDF3E31F0>Size: (636, 636)Regex used: None# imagesize library #Size: (-1, -1)Regex used: None------------------------------#################################end#######################################

The timing comparisons of each library / method.I set timeit to 10,000 times, and repeat of 5. For reference, it took 7 minutes 46 seconds to run.

###### Testing png_image.png #######magic : 9.280310999951325 , 5 repeat avg. : 8.674063340038993PIL : 1.069168900023215 , 5 repeat avg. : 1.100983139988966imagesize : 0.676764299976639 , 5 repeat avg. : 0.658798480057158###### Testing jpg_image.jpg #######magic : 7.006248699966818 , 5 repeat avg. : 6.803474060003646PIL : 1.295019199955277 , 5 repeat avg. : 1.230920840008184imagesize : 0.709322200040333 , 5 repeat avg. : 0.706342480005696##### Testing jpeg_image.jpeg ######magic : 6.531979499966837 , 5 repeat avg. : 6.501230620010756PIL : 1.263985900091939 , 5 repeat avg. : 1.263613799982704imagesize : 0.666680400026962 , 5 repeat avg. : 0.701455319998786###### Testing tif_image.tif #######magic : 11.265482199960388, 5 repeat avg. : 11.423775779991411PIL : 3.702962300041690 , 5 repeat avg. : 3.857250300026499imagesize : 0.764358000014909 , 5 repeat avg. : 0.750753180007450##### Testing tiff_image.tiff ######magic : 11.288321400061250, 5 repeat avg. : 11.339019200019539PIL : 4.116472600027919 , 5 repeat avg. : 3.834464759984985imagesize : 0.753993199905381 , 5 repeat avg. : 0.758465819992125###### Testing bmp_image.bmp #######magic : 16.124460300081410, 5 repeat avg. : 16.291060140007176PIL : 0.919579099980183 , 5 repeat avg. : 0.928753740014508imagesize : 0.649574000039138 , 5 repeat avg. : 0.654250180022791Test time: 7 minutes 46 seconds#################################end#######################################

Note: I'm no timing expert, so if my timing approach seems invalid, please point it out.

Skip straight to the EXIF data and store an integer.

from PIL import Imageimg_x = Image.open(image_filename)._getexif()[40962]img_y = Image.open(image_filename)._getexif()[40963]

See this page for EXIF tags.