Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heic support and make tag #165

Merged
merged 6 commits into from
Sep 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions picframe/config/configuration_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ model:
"EXIF FocalLength",
"EXIF DateTimeOriginal",
"Image Model",
"Image Make",
"IPTC Caption/Abstract",
"IPTC Object Name",
"IPTC Keywords"]
Expand Down
52 changes: 45 additions & 7 deletions picframe/get_image_meta.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import exifread
import logging
import os
from PIL import Image

class GetImageMeta:
Expand Down Expand Up @@ -102,7 +103,23 @@ def get_orientation(self):

def get_exif(self, key):
try:
val = self.__get_if_exist(key)
iso_keys = ['EXIF ISOSpeedRatings', 'EXIF PhotographicSensitivity', 'EXIF ISO'] # ISO prior 2.2, ISOSpeedRatings 2.2, PhotographicSensitivity 2.3
if key in iso_keys:
for iso in iso_keys:
val = self.__get_if_exist(iso)
if val:
break
else:
val = self.__get_if_exist(key)

if val is None:
Copy link
Collaborator

@jgodfrey jgodfrey Sep 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it should work to cascade between the two groups, so maybe this is all we need for now. It might be nice (for future) to have something like get_exif_fuzzy (or similar) that used a dictionary keyed by a generic tag name with the value being a list of prioritized group/tag names to search for a match. So, something like this, for example:

{'make': ['Image Make', 'EXIF Make'], 'model': ['Image Model', 'EXIF Model']}

grp, tag = key.split(" ", 1)
if grp == "EXIF":
newkey = "Image" + " " + tag
val = self.__get_if_exist(newkey)
elif grp == "Image":
newkey = "EXIF" + " " + tag
val = self.__get_if_exist(newkey)
if val is not None:
if key == 'EXIF FNumber':
val = round(val.values[0].num / val.values[0].den, 1)
Expand All @@ -117,11 +134,32 @@ def get_exif(self, key):

def get_size(self):
try: # corrupt image file might crash app
width = self.get_exif('EXIF ExifImageWidth')
height = self.get_exif('EXIF ExifImageLength')
if width and height:
return int(width), int(height)
return Image.open(self.__filename).size
return GetImageMeta.get_image_object(self.__filename).size
except Exception as e:
self.__logger.warning("get_size failed on %s -> %s", self.__filename, e)
return (0, 0)
return (0, 0)

@staticmethod
def get_image_object(fname):
ext = os.path.splitext(fname)[1].lower()
if ext in ('.heif','.heic'):
try:
import pyheif

heif_file = pyheif.read(fname)
image = Image.frombytes(heif_file.mode, heif_file.size, heif_file.data,
"raw", heif_file.mode, heif_file.stride)
if image.mode not in ("RGB", "RGBA"):
image = image.convert("RGB")
return image
except:
logger = logging.getLogger("get_image_meta.GetImageMeta")
logger.warning("Failed attempt to convert %s \n** Have you installed pyheif? **", fname)
else:
try:
image = Image.open(fname)
if image.mode not in ("RGB", "RGBA"): # mat system needs RGB or more
image = image.convert("RGB")
except: # for whatever reason
image = None
return image
7 changes: 4 additions & 3 deletions picframe/image_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class ImageCache:

EXTENSIONS = ['.png','.jpg','.jpeg','.heif','.heic']
EXIF_TO_FIELD = {'EXIF FNumber': 'f_number',
'EXIF Make': 'make',
'Image Make': 'make',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure which one is correct here (haven't done any research). I will say that my db currently DOES contain some records where the current make field is defined - so, the EXIF Make property must be defined in some cases...

Copy link
Owner Author

@helgeerbe helgeerbe Sep 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked for Apple, Canon, Sony. I only find "Image Make"

DEBUG:exifread:IFD 0 (Image) at offset 8:
DEBUG:exifread: Make: (0x010F) ASCII=SONY @ 146
DEBUG:exifread: Model: (0x0110) ASCII=ILCE-7RM3 @ 152

Never "Make" in EXIF:

DEBUG:exifread:Exif SubIFD at offset 286:
DEBUG:exifread: ExposureTime: (0x829A) Ratio=3/10 @ 664
DEBUG:exifread: FNumber: (0x829D) Ratio=8 @ 672

Do you have an example image for me?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See exifread: Note that the dictionary keys are the IFD name followed by the tag name. For example:

'EXIF DateTimeOriginal', 'Image Orientation', 'MakerNote FocusMode'

If you check exiftool tags description you will find the "Group" column:

  • IFD0 = Image
  • ExifIFD = EXIF

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have an example image for me?

Here's a few examples. Looks like about 70% of my (frame) images have EXIF Make instead of Image Make. However, those images also have Exif Model instead of Image Model. So, with the current (mismatched) code, these have the db make defined but not the model.

Ultimately, we might have to be a bit more clever with some of the info and cascade through a number of tags looking for a definition...

2018-10-20_145624

2017-05-17_08-29-09

Copy link
Owner Author

@helgeerbe helgeerbe Sep 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the example. Checking the standard I would say, that "Make" belongs to 0th IFD (Image) group. And is not defined for Exif IFD (Exif). So in my understanding your sample images are not correct. You are using Picasa, this Software is no longer maintained, isn't it?

What we can easily do is look first in the correct group and as fallback search in the other. Assuming we are focusing only on "Exif" and "Image" group.

exifread is returning everything anyway:

GetImageMeta__tags:{'Image Software': (0x0131) ASCII=Picasa @ 38, 'Image ExifOffset': (0x8769) Long=46 @ 30, 'Thumbnail Compression': (0x0103) Short=JPEG ...yle) @ 558, 'Thumbnail XResolution': (0x011A) Ratio=72 @ 626, 'Thumbnail YResolution': (0x011B) Ratio=72 @ 634, 'Thumbnail ResolutionUnit': (0x0128) Short=Pixel...Inch @ 594, 'Thumbnail JPEGInterc...angeFormat': (0x0201) Long=642 @ 606, 'Thumbnail JPEGInterc...rmatLength': (0x0202) Long=5620 @ 618, 'EXIF ImageWidth': (0x0100) Short=1911 @ 56, 'EXIF ImageLength': (0x0101) Long=2875 @ 68, 'EXIF BitsPerSample': (0x0102) Signed Short=16 @ 80, 'EXIF Make': (0x010F) ASCII=NIKON...TION @ 328, 'EXIF Model': (0x0110) ASCII=NIKON D70 @ 346, 'EXIF Orientation': (0x0112) Short=Horiz...mal) @ 116, ...}

But I wouldn't do this for other Groups like "Thumbnail"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are using Picasa, this Software is no longer maintained, isn't it?

I'm definitely not a Picasa user (and, yeah, I think it's been gone for a long time), though I did see it noted in the Image Software tag in one of the samples. I assume Picasa (or some part of it) must still be in use somewhere in the Google image pipeline. I must have downloaded that particular image instance from my Google Photos account instead of locating the original copy locally - that's my guess anyway...

What we can easily do is look first in the correct group and as fallback search in the other. Assuming we are focusing only on "Exif" and "Image" group.

Agreed. That's exactly what I meant with my earlier statement...

Ultimately, we might have to be a bit more clever with some of the info and cascade through a number of tags looking for a definition...

But I wouldn't do this for other Groups like "Thumbnail"

Agreed.

Copy link
Collaborator

@jgodfrey jgodfrey Sep 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in my understanding your sample images are not correct.

Yeah, I think I agree with you (although, the EXIF spec doc is not a fun read...)

I'll try to get to the bottom of what's creating my questionable EXIF data, although I suspect it's coming from "ON1 Photo Raw", which is my primary RAW converter and image editor. Although, that's surprising if so, as it's a widely used package with regular updates...

'Image Model': 'model',
'EXIF ExposureTime': 'exposure_time',
'EXIF ISOSpeedRatings': 'iso',
Expand Down Expand Up @@ -454,7 +454,8 @@ def __get_exif_info(self, file_path_name):
e['orientation'] = exifs.get_orientation()

width, height = exifs.get_size()
if e['orientation'] in (5, 6, 7, 8):
ext = os.path.splitext(file_path_name)[1].lower()
if ext not in ('.heif','.heic') and e['orientation'] in (5, 6, 7, 8):
width, height = height, width # swap values
e['width'] = width
e['height'] = height
Expand All @@ -466,7 +467,7 @@ def __get_exif_info(self, file_path_name):
e['exposure_time'] = exifs.get_exif('EXIF ExposureTime')
e['iso'] = exifs.get_exif('EXIF ISOSpeedRatings')
e['focal_length'] = exifs.get_exif('EXIF FocalLength')
e['rating'] = exifs.get_exif('EXIF Rating')
e['rating'] = exifs.get_exif('Image Rating')
e['lens'] = exifs.get_exif('EXIF LensModel')
e['exif_datetime'] = None
val = exifs.get_exif('EXIF DateTimeOriginal')
Expand Down
44 changes: 14 additions & 30 deletions picframe/viewer_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import os
import numpy as np
from PIL import Image, ImageFilter, ImageFile
from picframe import mat_image
from picframe import mat_image, get_image_meta
from datetime import datetime

# supported display modes for display switch
Expand Down Expand Up @@ -173,29 +173,6 @@ def clock_is_on(self):
def clock_is_on(self, val):
self.__show_clock = val

def __check_heif_then_open(self, fname):
ext = os.path.splitext(fname)[1].lower()
if ext in ('.heif','.heic'):
try:
import pyheif

heif_file = pyheif.read(fname)
image = Image.frombytes(heif_file.mode, heif_file.size, heif_file.data,
"raw", heif_file.mode, heif_file.stride)
if image.mode not in ("RGB", "RGBA"):
image = image.convert("RGB")
return image
except:
self.__logger.warning("Failed attempt to convert %s \n** Have you installed pyheif? **", fname)
else:
try:
image = Image.open(fname)
if image.mode not in ("RGB", "RGBA"): # mat system needs RGB or more
image = image.convert("RGB")
except: # for whatever reason
image = None
return image

# Concatenate the specified images horizontally. Clip the taller
# image to the height of the shorter image.
def __create_image_pair(self, im1, im2):
Expand All @@ -210,7 +187,11 @@ def __create_image_pair(self, im1, im2):
dst.paste(im2, (im1.width + sep, 0))
return dst

def __orientate_image(self, im, orientation):
def __orientate_image(self, im, pic):
ext = os.path.splitext(pic.fname)[1].lower()
if ext in ('.heif','.heic'): # heif and heic images are converted to PIL.Image obects and are alway in correct orienation
return im
orientation = pic.orientation
if orientation == 2:
im = im.transpose(Image.FLIP_LEFT_RIGHT)
elif orientation == 3:
Expand Down Expand Up @@ -272,15 +253,18 @@ def __tex_load(self, pics, size=None):

# Load the image(s) and correct their orientation as necessary
if pics[0]:
im = self.__check_heif_then_open(pics[0].fname)
if pics[0].orientation != 1:
im = self.__orientate_image(im, pics[0].orientation)
im = get_image_meta.GetImageMeta.get_image_object(pics[0].fname)
if im is None:
return None
if pics[0].orientation != 1:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we wanted to shift the "heic orientation" issue to load/display time, it'd need to happen right here somewhere. Either we wouldn't call __orientate_image() for HEIC images or we'd need to change that method to return the original image unchanged if it were passed a HEIC image (though, it'd need to know the image type, which it doesn't know today).

im = self.__orientate_image(im, pics[0])

if pics[1]:
im2 = self.__check_heif_then_open(pics[1].fname)
im2 = get_image_meta.GetImageMeta.get_image_object(pics[1].fname)
if im2 is None:
return None
if pics[1].orientation != 1:
im2 = self.__orientate_image(im2, pics[1].orientation)
im2 = self.__orientate_image(im2, pics[1])

screen_aspect, image_aspect, diff_aspect = self.__get_aspect_diff(size, im.size)

Expand Down
10 changes: 6 additions & 4 deletions test/test_get_image_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ def test_exifs_jpg():
assert width == 1920
assert height == 1200
val = exifs.get_exif('Image Make')
assert val == "ILCE-7RM3"
assert val == "SONY"
val = exifs.get_exif('EXIF Make') # This should work as well
assert val == "SONY"
except:
pytest.fail("Unexpected exception")

Expand All @@ -87,7 +89,7 @@ def test_get_orientation():

exifs = GetImageMeta("test/images/test3.HEIC")
orientation = exifs.get_orientation()
assert orientation == 6
assert orientation == 6
except:
pytest.fail("Unexpected exception")

Expand All @@ -98,8 +100,8 @@ def test_exifs_heic():
assert orientation == 6

width, height = exifs.get_size()
assert width == 4032
assert height == 3024
assert height == 4032
assert width == 3024


f_number = exifs.get_exif('EXIF FNumber')
Expand Down