Python – Extracting Pitch, Yaw, and Roll EXIF Information from JPEG Using Python PIL

exiffme-formpythonpython-imaging-library

I have a series of JPEGS captured during an aerial patrol. These JPEGS have metadata stored within JPEG, this metadata includes the GPSInfo and other data.

I am trying to extract this information into a format I can use (csv etc.). I am also going to try and implement this in a Python script that will run on a shared virtual machine, possibly in FME.

I am able to run the Python PIL library and extract out much of the information I need. See below:

import PIL.Image
import PIL.ExifTags
from PIL import Image, ExifTags
from PIL.ExifTags import GPSTAGS
from PIL.ExifTags import TAGS
from os import walk
import os
import pandas as pd
import os
from datetime import datetime

def get_exif(filepath):

    image = Image.open(filepath)
    image.verify()

    return image._getexif()

The following falls under a Main() method

    for filename in os.listdir(direct):
    if filename.endswith(".jpg"):
        print (i)
        filepath = os.path.join(direct, filename)#

        attribs = []
        exif = get_exif(filepath)
        fields = ['GPSInfo', 'Model', 'ExposureTime', 'FocalLength', 'Software', 'DateTime', 'DateTimeDigitised']
        exifData = {}
        for f in fields:
            for tag, value in exif.items():
                decodedTag = ExifTags.TAGS.get(tag, tag)
                if decodedTag == f:
                    exifData["File"] = filename
                    if decodedTag == 'GPSInfo':
                        exifData["Y"] = value[2]
                        exifData["X"] = value[4]
                        exifData["Z"] = value[6]
                        continue
                    else:
                        exifData[decodedTag] = value
                        continue
        df = df.append(exifData, ignore_index = True)
        i +=1

I had initially extracted all tags from the Exif, and then created a list to filter only the tags I wanted.

On seeing the output, a colleague asked for the Pitch, Roll and Yaw to be extracted as well, but these were not in the original list of tags I extracted.

An image was run through the commandline ExifTools.exe, and the following data formed part of the output.

GPS Roll : -100.509252
GPS Pitch : -66.017852017852
GPS Yaw : 84.001227001227

Is there a way to have the PIL library return these values for Yaw, Pitch and Roll?

I cannot use the commandline on the machine I want to deploy this and I cannot add any new transformers to my FME Installation on this machine.

Best Answer

I think it is a matter of the python implementation. If you have a look at the ExifTags.py the tags are not listed. May that's because the tags are not standardized. If you have a look in the exiv2 GPSInfo Group, the tags are also not listed.

I had a look into the Image::ExifTool source code, there are about 100 data formats addressed or handled:

PhotoMechanic Exif GeoTiff CanonRaw KyoceraRaw Lytro MinoltaRaw PanasonicRaw
SigmaRaw JPEG GIMP Jpeg2000 GIF BMP BMP::OS2 BMP::Extra BPG BPG::Extensions
PICT PNG MNG FLIF DjVu DPX OpenEXR MIFF PGF PSP PhotoCD Radiance PDF
PostScript Photoshop::Header Photoshop::Layers Photoshop::ImageData
FujiFilm::RAF FujiFilm::IFD Samsung::Trailer Sony::SRF2 Sony::SR2SubIFD
Sony::PMP ITC ID3 FLAC Ogg Vorbis APE APE::NewHeader APE::OldHeader Audible
MPC MPEG::Audio MPEG::Video MPEG::Xing M2TS QuickTime QuickTime::ImageFile
QuickTime::Stream Matroska MOI MXF DV Flash Flash::FLV Real::Media
Real::Audio Real::Metafile Red RIFF AIFF ASF DICOM MIE JSON HTML XMP::SVG
Palm Palm::MOBI Palm::EXTH Torrent EXE EXE::PEVersion EXE::PEString
EXE::MachO EXE::PEF EXE::ELF EXE::AR EXE::CHM LNK Font VCard
VCard::VCalendar RSRC Rawzor ZIP ZIP::GZIP ZIP::RAR RTF OOXML iWork ISO
FLIR::AFF FLIR::FPF MacOS::MDItem MacOS::XAttr

When I filter the modules for the GPSRoll tag, I find advices like:

/usr/share/perl5/Image/ExifTool$ grep GPSRoll *.pm
Geotag.pm:            # Note: GPSPitch and GPSRoll are non-standard, and must be user-defined
Geotag.pm:            @r = $et->SetNewValue(GPSRoll => $$tFix{roll}, %opts);
Geotag.pm:                    GPSImgDirection GPSImgDirectionRef GPSPitch GPSRoll))
Geotag.pm:user-defined tags, GPSPitch and GPSRoll, must be active.

So it seems that the attitude data tagging is made on a "Thin Ice" standard and you have to enable some user defined tag capabilities. When I made a test against image with Roll, Pitch and Yaw tags from a PhaseOne Camera image (which I often use), the library Image::ExifTool can read the tags with complete different names (GPSIMURoll ...). At the moment I don't know how it works.

According to the FME constraints, is it possible to use the either exiftool or the Image::ExifTool library either by a shell-out and parse the resulting code like:

# Pseudo code
with Popen(["exiftool", jpgFileName ], stdout=PIPE) as proc:
    parseExifTool(proc.stdout.read())

or tailor a IPC specific call in Perl to get the info's.

Test dataset

The test dataset is from a PhaseOne iXM-RS150F aerial camara. The IIQ format is a special image format based on 2 page TIF with a overview image TIF coded and the large RAW dataset block. Metadata are embedded Tif specific present in the IFD.

Python Pillow (PIL fork)

Here is my Python test code (I'm not a crack in python coding), which is hopefully similar to the Perl code below. Unfortunatly PIL cannot read or decode the exif directory of my IIQ image. May it works for you with the JPG.

While it in Perl it works (see the test below). May be as mentioned, that's because the library supports a large number of also strange or vendor related (raw) image formats (..see table of images supported).

Python script

#!/usr/bin/env python3
import sys
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
DATA = 'data/example.IIQ';

def printf(format, \*args):
    sys.stdout.write(format % args)

image = Image.open(DATA)
info  = image.info

if info is None:
    print("ERROR: Image info is empty!")
    exit(1)

printf("INFO.TAGS in file %s ARE:\n", DATA);
for k, v in info.items():
    printf(" %16s: %s\n",k, v);

exifInfo = info.get('exif')

if exifInfo is None:
    print("ERROR: Image exif info is empty!")
    exit(1)

printf("EXIF.DATA file %s ARE:\n", DATA);
for k, v in exifInfo.items():
    printf(" %16s %s\n", k, v);

#EOF

Resulting output from the Python script

./Pillow-Exifs.py
INFO.TAGS in file data/example.IIQ ARE:
      compression: raw
              dpi: (292.57142857142856, 292.57142857142856)
ERROR: Image exif info is empty!

Perl Family

Exif Tool

Extract GPS tags with the exiftool (Perl5 related code base). The exiftool inserts some spaces to make the matter better human readable. In the machine prepared dictionary these space disappear.

$ exiftool data/example.IIQ | grep GPS
GPS Date/Time                   : 10:17:41.405459
GPS Latitude                    : 52 deg 8' 57.22" N
GPS Longitude                   : 8 deg 16' 52.81" W
GPS Altitude Ref                : Above Sea Level
GPS Event ID                    : 24
GPSIMU Pitch                    : -0.29
GPSIMU Roll                     : -0.18
GPSIMU Yaw                      : 358.31
GPSIMU Yaw Ref                  : T
GPS Altitude                    : 551.9 m Above Sea Level
GPS Latitude Ref                : North
GPS Longitude Ref               : West
GPS Position                    : 52 deg 8' 57.22" N, 8 deg 16' 52.81" W

Perl script -- library Image::ExifTool

Extract the GPS tags with the perl library Image::ExifTool where the ExifTool belongs to.

#!/usr/bin/env perl
use strict;
use warnings;
use Image::ExifTool qw(:Public);

# The test dataset from a PhaseOne iXM-RS150F.
my $DATA = 'data/example.IIQ';

# Get hash of meta information tag
# The hash is returned as a reference.
my $info = ImageInfo($DATA);

# Filter the GPS Tags and print them
print "TAGS if file $DATA ARE:\n";
for my $tag (sort keys %$info) {
    print "\t$tag\n" if $tag =~ /^GPS/;
}
print "\n";


# Set the filter for tags of interest
my @tagsUse = qw(GPSAltitude GPSLatitude GPSLongitude
                 GPSIMUPitch GPSIMURoll GPSIMUYaw);

# Filter the GPS Tags, get the values and print them.
print "TAGS & VALUES of INTEREST ARE:\n";
for my $tag (@tagsUse) {
    my $value = $info->{$tag};
    $value = 'NA' if not defined $value; 
    printf  "%16s: %s\n", $tag, $value;
}

print "\nREADY\n";

Resulting output from the Perl script

TAGS if file data/example.IIQ ARE:
    GPSAltitude
    GPSAltitude (1)
    GPSAltitudeRef
    GPSDateTime
    GPSEventID
    GPSIMUPitch
    GPSIMURoll
    GPSIMUYaw
    GPSIMUYawRef
    GPSLatitude
    GPSLatitudeRef
    GPSLongitude
    GPSLongitudeRef
    GPSPosition

TAGS & VALUES of INTEREST ARE:
     GPSAltitude: 551.9 m Above Sea Level
     GPSLatitude: 52 deg 8' 57.22" N
    GPSLongitude: 8 deg 16' 52.81" W
     GPSIMUPitch: -0.29
      GPSIMURoll: -0.18
       GPSIMUYaw: 358.31

READY
Related Question