73

The High Efficiency Image File (HEIF) format is the default when airdropping an image from an iPhone to a OSX device. I want to edit and modify these .HEIC files with Python.

I could modify phone settings to save as JPG by default but that doesn't really solve the problem of being able to work with the filetype from others. I still want to be able to process HEIC files for doing file conversion, extracting metadata, etc. (Example Use Case -- Geocoding)

Pillow

Here is the result of working with Python 3.7 and Pillow when trying to read a file of this type.

$ ipython
Python 3.7.0 (default, Oct  2 2018, 09:20:07)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.2.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from PIL import Image

In [2]: img = Image.open('IMG_2292.HEIC')
---------------------------------------------------------------------------
OSError                                   Traceback (most recent call last)
<ipython-input-2-fe47106ce80b> in <module>
----> 1 img = Image.open('IMG_2292.HEIC')

~/.env/py3/lib/python3.7/site-packages/PIL/Image.py in open(fp, mode)
   2685         warnings.warn(message)
   2686     raise IOError("cannot identify image file %r"
-> 2687                   % (filename if filename else fp))
   2688
   2689 #

OSError: cannot identify image file 'IMG_2292.HEIC'

It looks like support in python-pillow was requested (#2806) but there are licensing / patent issues preventing it there.

ImageMagick + Wand

It appears that ImageMagick may be an option. After doing a brew install imagemagick and pip install wand however I was unsuccessful.

$ ipython
Python 3.7.0 (default, Oct  2 2018, 09:20:07)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.2.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from wand.image import Image

In [2]: with Image(filename='img.jpg') as img:
   ...:     print(img.size)
   ...:
(4032, 3024)

In [3]: with Image(filename='img.HEIC') as img:
   ...:     print(img.size)
   ...:
---------------------------------------------------------------------------
MissingDelegateError                      Traceback (most recent call last)
<ipython-input-3-9d6f58c40f95> in <module>
----> 1 with Image(filename='ces2.HEIC') as img:
      2     print(img.size)
      3

~/.env/py3/lib/python3.7/site-packages/wand/image.py in __init__(self, image, blob, file, filename, format, width, height, depth, background, resolution, pseudo)
   4603                     self.read(blob=blob, resolution=resolution)
   4604                 elif filename is not None:
-> 4605                     self.read(filename=filename, resolution=resolution)
   4606                 # clear the wand format, otherwise any subsequent call to
   4607                 # MagickGetImageBlob will silently change the image to this

~/.env/py3/lib/python3.7/site-packages/wand/image.py in read(self, file, filename, blob, resolution)
   4894             r = library.MagickReadImage(self.wand, filename)
   4895         if not r:
-> 4896             self.raise_exception()
   4897
   4898     def save(self, file=None, filename=None):

~/.env/py3/lib/python3.7/site-packages/wand/resource.py in raise_exception(self, stacklevel)
    220             warnings.warn(e, stacklevel=stacklevel + 1)
    221         elif isinstance(e, Exception):
--> 222             raise e
    223
    224     def __enter__(self):

MissingDelegateError: no decode delegate for this image format `HEIC' @ error/constitute.c/ReadImage/556

Any other alternatives available to do a conversion programmatically?

j12y
  • 2,112
  • 3
  • 17
  • 22
  • 1
    Similarly Sindre Sorhus has an excellent HEIC Converter to generate JPEG or PNG images but not the flexibility I'm looking for. https://sindresorhus.com/heic-converter – j12y Jan 28 '19 at 05:31
  • 1
    [ExifTool](https://www.sno.phy.queensu.ca/~phil/exiftool/) provides a CLI for working with image metadata and supports HEIF. Should be easy to wrap in Python. – buzjwa May 10 '19 at 09:20
  • This may help... https://stackoverflow.com/a/54558699/2836621 – Mark Setchell Feb 01 '20 at 14:42
  • 1
    Just to note: Today was first release of [pillow-heif](https://pypi.org/project/pillow-heif/) that support 64 bit windows. Now it support almost all platforms, except windows arm and 32 bit systems. In this topic two people showed it's basic usage. – Alexander Piskun Feb 10 '22 at 14:02

14 Answers14

48

Consider using PIL in conjunction with pillow-heif:

pip3 install pillow-heif
from PIL import Image
from pillow_heif import register_heif_opener

register_heif_opener()

image = Image.open('image.heic')

That said, I'm not aware of any licensing/patent issues that would prevent HEIF support in Pillow (see this or this). libheif is widely adopted and free to use, provided you do not bundle the HEIF decoder with a device and fulfill the requirements of the LGPLv3 license.

mara004
  • 1,435
  • 11
  • 24
27

You guys should check out this library, it's a Python 3 wrapper to the libheif library, it should serve your purpose of file conversion, extracting metadata:

https://github.com/david-poirier-csn/pyheif

https://pypi.org/project/pyheif/

Example usage:

 import io

 import whatimage
 import pyheif
 from PIL import Image


 def decodeImage(bytesIo):

    fmt = whatimage.identify_image(bytesIo)
    if fmt in ['heic', 'avif']:
         i = pyheif.read_heif(bytesIo)

         # Extract metadata etc
         for metadata in i.metadata or []:
             if metadata['type']=='Exif':
                 # do whatever
        
         # Convert to other file format like jpeg
         s = io.BytesIO()
         pi = Image.frombytes(
                mode=i.mode, size=i.size, data=i.data)

         pi.save(s, format="jpeg")

  ...
dmmfll
  • 2,666
  • 2
  • 35
  • 41
danial
  • 607
  • 2
  • 12
  • 20
  • 1
    My experience with `pyheif` is that it successfully reads HEIC files, but I don't understand how or why `Image.frombytes()` is supposed to work in the code above. Wouldn't that require PIL to understand HEIF? In any event, what I get is a badly corrupted JPG file when I run it. – Norm Oct 19 '19 at 14:38
  • 1
    `read_heif` required actual data, so the line should actually be: `pyheif.read_heif(bytesIo.read())` – Cigogne Eveillée Oct 22 '19 at 19:43
  • 3
    can you give some examples of the `do whatever`? `metadata['data']` here appears to be of type `bytes`. But when I attempt to: `metadata['data'].decode('utf-8'))`, I see: `UnicodeDecodeError: 'utf-8' codec can't decode byte 0x86 in position 27: invalid start byte` – user9074332 Jan 11 '20 at 01:24
  • 4
    fwiw, I tried to install `pyheif` on windows and ran into [this](https://github.com/david-poirier-csn/pyheif/issues/2) issue. Turns out `pyheif` is not compatible with Windows. – Zhang18 May 25 '20 at 19:59
21

I was quite successful with Wand package : Install Wand: https://docs.wand-py.org/en/0.6.4/ Code for conversion:

   from wand.image import Image
   import os

   SourceFolder="K:/HeicFolder"
   TargetFolder="K:/JpgFolder"

   for file in os.listdir(SourceFolder):
      SourceFile=SourceFolder + "/" + file
      TargetFile=TargetFolder + "/" + file.replace(".HEIC",".JPG")
    
      img=Image(filename=SourceFile)
      img.format='jpg'
      img.save(filename=TargetFile)
      img.close()
  • 2
    It seems ImageMagick (low level lib used by Wand) does not support heic delegate in some distro's package manager out of the box (eg: Centos 8). – Rodriguez Feb 27 '21 at 00:36
  • Thanks!, this worked for me as I couldn't get pyheif to work in windows 10. – AlvaroFG Sep 04 '21 at 17:58
11

Here is another solution to convert heic to jpg while keeping the metadata intact. It is based on mara004s solution above, however I was not able to extract the images timestamp in that way, so had to add some code. Put the heic files in dir_of_interest before applying the function:

import os
from PIL import Image, ExifTags
from pillow_heif import register_heif_opener
from datetime import datetime
import piexif
import re
register_heif_opener()

def convert_heic_to_jpeg(dir_of_interest):
        filenames = os.listdir(dir_of_interest)
        filenames_matched = [re.search("\.HEIC$|\.heic$", filename) for filename in filenames]

        # Extract files of interest
        HEIC_files = []
        for index, filename in enumerate(filenames_matched):
                if filename:
                        HEIC_files.append(filenames[index])

        # Convert files to jpg while keeping the timestamp
        for filename in HEIC_files:
                image = Image.open(dir_of_interest + "/" + filename)
                image_exif = image.getexif()
                if image_exif:
                        # Make a map with tag names and grab the datetime
                        exif = { ExifTags.TAGS[k]: v for k, v in image_exif.items() if k in ExifTags.TAGS and type(v) is not bytes }
                        date = datetime.strptime(exif['DateTime'], '%Y:%m:%d %H:%M:%S')

                        # Load exif data via piexif
                        exif_dict = piexif.load(image.info["exif"])

                        # Update exif data with orientation and datetime
                        exif_dict["0th"][piexif.ImageIFD.DateTime] = date.strftime("%Y:%m:%d %H:%M:%S")
                        exif_dict["0th"][piexif.ImageIFD.Orientation] = 1
                        exif_bytes = piexif.dump(exif_dict)

                        # Save image as jpeg
                        image.save(dir_of_interest + "/" + os.path.splitext(filename)[0] + ".jpg", "jpeg", exif= exif_bytes)
                else:
                        print(f"Unable to get exif data for {filename}")
aleksandereiken
  • 430
  • 5
  • 9
  • Great effort! I tried it, it saved me, converted all the pictures successfully while keeping all the meta data intact. Thank you so much. – Mohammad ElNesr Sep 27 '22 at 22:36
  • In my case it is not working. The Created, Modification and Accessed time gets saved as the time of that picture being saved (created). All three the same value. – M.K Nov 09 '22 at 23:26
  • this is the most complete example as of the current date IMO – DaReal Nov 11 '22 at 19:42
6

Adding to the answer by danial, i just had to modify the byte array slighly to get a valid datastream for further work. The first 6 bytes are 'Exif\x00\x00' .. dropping these will give you a raw format that you can pipe into any image processing tool.

import pyheif
import PIL
import exifread

def read_heic(path: str):
    with open(path, 'rb') as file:
        image = pyheif.read_heif(file)
        for metadata in image.metadata or []:
            if metadata['type'] == 'Exif':
                fstream = io.BytesIO(metadata['data'][6:])

    # now just convert to jpeg
    pi = PIL.Image.open(fstream)
    pi.save("file.jpg", "JPEG")

    # or do EXIF processing with exifread
    tags = exifread.process_file(fstream)

At least this worked for me.

Peter Kunszt
  • 101
  • 2
  • 5
  • 2
    Using your code, when I pass an HEIC file path I get `PIL.UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x109aefef0>`. – Josh Clark Dec 26 '20 at 05:55
  • Had the same problem as Josh, PIL is unable to identify the HEIC image with this code – pedroprates Aug 22 '21 at 13:35
6

You can use the pillow_heif library to read HEIF images in a way compatible with PIL.

The example below will import a HEIF picture and save it in png format.

from PIL import Image
import pillow_heif

heif_file = pillow_heif.read_heif("HEIC_file.HEIC")
image = Image.frombytes(
    heif_file.mode,
    heif_file.size,
    heif_file.data,
    "raw",
)

image.save("./picture_name.png", format="png")
    
Score_Under
  • 1,189
  • 10
  • 20
adel
  • 354
  • 3
  • 4
5

This will do go get the exif data from the heic file

import pyheif
import exifread
import io

heif_file = pyheif.read_heif("file.heic")

for metadata in heif_file.metadata:

    if metadata['type'] == 'Exif':
        fstream = io.BytesIO(metadata['data'][6:])

    exifdata = exifread.process_file(fstream,details=False)

    # example to get device model from heic file
    model = str(exifdata.get("Image Model"))
    print(model)
PidePython
  • 61
  • 1
  • 5
4

Starting from version 0.10.0, it becomes much simpler.

Saving 8/10/12 bit HEIF files to 8/16 bit PNG using OpenCV:

import numpy as np
import cv2
from pillow_heif import open_heif

heif_file = open_heif("images/rgb12.heif", convert_hdr_to_8bit=False, bgr_mode=True)
np_array = np.asarray(heif_file)
cv2.imwrite("image.png", np_array)

For versions < 0.10.0

Example for working with HDR(10/12) bit HEIF files using OpenCV and pillow-heif:

import numpy as np
import cv2
import pillow_heif

heif_file = pillow_heif.open_heif("images/rgb12.heif", convert_hdr_to_8bit=False)
heif_file.convert_to("BGRA;16" if heif_file.has_alpha else "BGR;16")
np_array = np.asarray(heif_file)
cv2.imwrite("rgb16.png", np_array)

Input file for this example can be 10 or 12 bit file.

1

Works perfectly... (Even on Windows)

import glob
from PIL import Image
from pillow_heif import register_heif_opener

register_heif_opener()

for heic_pic_name in glob.glob("*.heic"):   #searching .heic images in existing folder
    my_pic = Image.open(heic_pic_name)      #opening .heic images
    jpg_pic_name = heic_pic_name.split('.')[0]+'.jpg'   #creating new names for .jpg images
    my_pic.save(jpg_pic_name, format="JPEG", optimize = True, quality = 100)    #saving
0

I am facing the exact same problem as you, wanting a CLI solution. Doing some further research, it seems ImageMagick requires the libheif delegate library. The libheif library itself seems to have some dependencies as well.

I have not had success in getting any of those to work as well, but will continue trying. I suggest you check if those dependencies are available to your configuration.

Tropicalrambler
  • 619
  • 1
  • 9
  • 15
0

Simple solution after going over multiple responses from people.
Please install whatimage, pyheif and PIL libraries before running this code.


[NOTE] : I used this command for install.

python3 -m pip install Pillow

Also using linux was lot easier to install all these libraries. I recommend WSL for windows.


  • code
import whatimage
import pyheif
from PIL import Image
import os

def decodeImage(bytesIo, index):
    with open(bytesIo, 'rb') as f:
    data = f.read()
    fmt = whatimage.identify_image(data)
    if fmt in ['heic', 'avif']:
    i = pyheif.read_heif(data)
    pi = Image.frombytes(mode=i.mode, size=i.size, data=i.data)
    pi.save("new" + str(index) + ".jpg", format="jpeg")

# For my use I had my python file inside the same folder as the heic files
source = "./"

for index,file in enumerate(os.listdir(source)):
    decodeImage(file, index)
myeongkil kim
  • 2,465
  • 4
  • 16
  • 22
d1p3
  • 39
  • 1
  • Please explain what your code does and how it does it. – M-Chen-3 Dec 27 '20 at 03:44
  • 1
    This is a simple solution using multiple libraries. It basically opens each .heic file inside the same folder as the ".py" file and converts to jpg. Did you mean code explanation? – d1p3 Dec 28 '20 at 20:18
  • Yes, every answer should explain how the code works. – M-Chen-3 Dec 28 '20 at 20:19
  • Your code has invalid indentation. Please correct your example so that it is actually testable. – mara004 Jan 30 '22 at 12:36
0

It looked like that there is a solution called heic-to-jpg, but I might be not very sure about how this would work in colab.

Qiyu Zhong
  • 74
  • 6
0

the first answer works, but since its just calling save with a BytesIO object as the argument, it doesnt actually save the new jpeg file, but if you create a new File object with open and pass that, it saves to that file ie:

import whatimage
import pyheif
from PIL import Image


def decodeImage(bytesIo):

    fmt = whatimage.identify_image(bytesIo)
    if fmt in ['heic', 'avif']:
         i = pyheif.read_heif(bytesIo)
        
         # Convert to other file format like jpeg
         s = open('my-new-image.jpg', mode='w')
         pi = Image.frombytes(
                mode=i.mode, size=i.size, data=i.data)

         pi.save(s, format="jpeg")

Kyle Roux
  • 736
  • 5
  • 11
0

I use the pillow_heif library. For example, I use this script when I have a folder of heif files I want to convert to png.

from PIL import Image
import pillow_heif
import os 
from tqdm import tqdm 
import argparse

def get_images(heic_folder):

    # Get all the heic images in the folder
    imgs = [os.path.join(heic_folder, f) for f in os.listdir(heic_folder) if f.endswith('.HEIC')]

    # Name of the folder where the png files will be stored
    png_folder = heic_folder + "_png"

    # If it doesn't exist, create the folder
    if not os.path.exists(png_folder):
        os.mkdir(png_folder)
    
    for img in tqdm(imgs):
        heif_file = pillow_heif.read_heif(img)
        image = Image.frombytes(
            heif_file.mode,
            heif_file.size,
            heif_file.data,
            "raw",

        )

        image.save(os.path.join(png_folder,os.path.basename(img).split('.')[0])+'.png', format("png"))


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Convert heic images to png')
    parser.add_argument('heic_folder', type=str, help='Folder with heic images')
    args = parser.parse_args()

    get_images(args.heic_folder)
DL876576
  • 1
  • 1