ExifTags Module

Note

The source images loaded from the demos in this page is in the real-world, but I can’t redistribute those. I downloaded those to local by google searching with words “Images for jpeg contained gpsinfo sample”, so most of all of those are not free liscenced.

Keeping exif data when saving

doc

https://pillow.readthedocs.io/en/latest/handbook/image-file-formats.html#jpeg, http://effbot.org/imagingbook/format-jpeg.htm

Before you use ExifTags, note that save method doesn’t preserve the exif in case of default.

>>> from io import BytesIO
>>> from PIL import Image
>>>
>>> src = Image.open("01.jpg")
>>> src.info.get('exif', b'')[:20]
'Exif\x00\x00II*\x00\x08\x00\x00\x00\x0c\x00\x0f\x01\x02\x00'
>>>
>>> #
>>> tmp = BytesIO()
>>> src.save(tmp, "JPEG")  # drop exif data
>>> dst = Image.open(tmp)
>>> dst.info.get('exif', b'')[:20]
''
>>>
>>> #
>>> tmp = BytesIO()
>>> src.save(tmp, "JPEG", exif=b"")  # override exif data
>>> dst = Image.open(tmp)
>>> dst.info.get('exif', b'')[:20]
''
>>>
>>> #
>>> tmp = BytesIO()
>>> src.save(tmp, "JPEG", exif=src.info["exif"])  # keep exif data
>>> dst = Image.open(tmp)
>>> dst.info.get('exif', b'')[:20]
'Exif\x00\x00II*\x00\x08\x00\x00\x00\x0c\x00\x0f\x01\x02\x00'
>>>

Exploring Exif Items As Human Readable

Python 3.5.1 (v3.5.1:37a07cee5969, Dec  6 2015, 01:54:25) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import PIL
>>> PIL.VERSION, PIL.PILLOW_VERSION
('1.1.7', '4.1.1')
>>>
>>> from PIL import Image
>>> from PIL.ExifTags import TAGS, GPSTAGS
>>>
>>> # build reverse dicts
>>> _TAGS_r = dict(((v, k) for k, v in TAGS.items()))
>>> _GPSTAGS_r = dict(((v, k) for k, v in GPSTAGS.items()))
>>>
>>> #
>>> img = Image.open("01.jpg")
>>> img.info.keys()
dict_keys(['dpi', 'exif'])
>>> len(img.info['exif'])
13798
>>> img.info['exif'][:30]  # raw exif data
b'Exif\x00\x00II*\x00\x08\x00\x00\x00\x0c\x00\x0f\x01\x02\x00\x06\x00\x00\x00\x9e\x00\x00\x00\x10\x01'
>>> #
>>> exifd = img._getexif()  # this merges gpsinfo as data rather than an offset pointer
>>> type(exifd)
<class 'dict'>
>>> exifd.keys()  # numeric keys
dict_keys([36864, 37377, 37378, 36867, 36868, 40965, 37510, 37383, 37385, 37386, 40962, 41486, 271, 272, 37521, 274, 531, 33432, 37380, 282, 283, 33434, 42032, 34850, 40961, 34853, 34855, 296, 41987, 37121, 34866, 33437, 34864, 42033, 306, 42036, 42037, 42034, 315, 41985, 40960, 41990, 41487, 40963, 37520, 41986, 34665, 37522, 41488, 37500])
>>> #
>>> # 1. ignore "MakerNote" and "UserComment" because these can be too long
>>> # 2. ignore unknwon tag
>>> keys = list(exifd.keys())
>>> keys.remove(_TAGS_r["MakerNote"])
>>> keys.remove(_TAGS_r["UserComment"])
>>> keys = [k for k in keys if k in TAGS]
>>> # symbolic name of keys
>>> print("\n".join([TAGS[k] for k in keys]))
ExifVersion
ShutterSpeedValue
--- snip ---
ColorSpace
GPSInfo
ISOSpeedRatings
--- snip ---
FocalPlaneResolutionUnit
>>> # each values
>>> print("\n".join([str((TAGS[k], exifd[k])) for k in keys]))
('ExifVersion', b'0230')
('ShutterSpeedValue', (417792, 65536))
--- snip ---
('ColorSpace', 1)
('GPSInfo', {0: b'\x02\x03\x00\x00', 1: 'S', 2: ((20, 1), (150146, 10000), (0, 1)), 3: 'E', 4: ((44, 1), (251558, 10000), (0, 1)), 5: b'\x00', 6: (235, 10), 7: ((14, 1), (31, 1), (59000, 1000)), 8: '12', 9: 'A', 10: '3', 11: (14, 10), 29: '2012:08:13'})
('ISOSpeedRatings', 100)
--- snip ---
>>> gpsinfo = exifd[_TAGS_r["GPSInfo"]]
>>> gpsinfo
{0: b'\x02\x03\x00\x00', 1: 'S', 2: ((20, 1), (150146, 10000), (0, 1)), 3: 'E', 4: ((44, 1), (251558, 10000), (0, 1)), 5: b'\x00', 6: (235, 10), 7: ((14, 1), (31, 1), (59000, 1000)), 8: '12', 9: 'A', 10: '3', 11: (14, 10), 29: '2012:08:13'}
>>> print("\n".join([GPSTAGS[k] for k in gpsinfo.keys()]))
GPSVersionID
GPSLatitudeRef
GPSLatitude
GPSLongitudeRef
GPSLongitude
GPSAltitudeRef
GPSAltitude
GPSTimeStamp
GPSSatellites
GPSStatus
GPSMeasureMode
GPSDOP
GPSDateStamp
>>> print("\n".join([str((GPSTAGS[k], gpsinfo[k])) for k in gpsinfo.keys()]))
('GPSVersionID', b'\x02\x03\x00\x00')
('GPSLatitudeRef', 'S')
('GPSLatitude', ((20, 1), (150146, 10000), (0, 1)))
('GPSLongitudeRef', 'E')
('GPSLongitude', ((44, 1), (251558, 10000), (0, 1)))
('GPSAltitudeRef', b'\x00')
('GPSAltitude', (235, 10))
('GPSTimeStamp', ((14, 1), (31, 1), (59000, 1000)))
('GPSSatellites', '12')
('GPSStatus', 'A')
('GPSMeasureMode', '3')
('GPSDOP', (14, 10))
('GPSDateStamp', '2012:08:13')
Python 3.9.2 (tags/v3.9.2:1a79785, Feb 19 2021, 13:44:55) [MSC v.1928 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import PIL
>>> PIL.__version__
'8.2.0'
>>> from PIL import Image
>>> from PIL.ExifTags import TAGS, GPSTAGS
>>>
>>> # build reverse dicts
>>> _TAGS_r = dict(((v, k) for k, v in TAGS.items()))
>>> _GPSTAGS_r = dict(((v, k) for k, v in GPSTAGS.items()))
>>>
>>> #
>>> img = Image.open("02.jpg")
>>> img.info.keys()
dict_keys(['jfif', 'jfif_version', 'dpi', 'jfif_unit', 'jfif_density', 'exif', 'photoshop', 'adobe', 'adobe_transform', 'progressive', 'progression', 'icc_profile'])
>>> len(img.info['exif'])
4997
>>> img.info['exif'][:30]  # raw exif data
b'Exif\x00\x00II*\x00\x08\x00\x00\x00\x11\x00\x00\x01\x03\x00\x01\x00\x00\x00 \x0f\x00\x00\x01\x01'
>>> #
>>> exifd = img.getexif()._get_merged_dict()  # this merges gpsinfo as data rather than an offset pointer
>>> type(exifd)
<class 'dict'>
>>> exifd.keys()  # numeric keys
dict_keys([256, 257, 258, 259, 34853, 262, 296, 34665, 271, 272, 305, 274, 306, 277, 282, 283, 284, 36864, 37377, 37378, 36867, 36868, 37380, 37381, 37383, 37384, 37385, 37386, 40961, 40962, 41992, 41993, 41994, 40963, 41996, 41495, 41728, 33434, 33437, 41729, 42016, 34850, 34855, 41986, 41987])
>>> keys = list(exifd.keys())
>>> keys = [k for k in keys if k in TAGS]
>>> # symbolic name of keys
>>> print("\n".join([TAGS[k] for k in keys]))
ImageWidth
ImageLength
BitsPerSample
Compression
GPSInfo
PhotometricInterpretation
--- snip ---
WhiteBalance
>>> # each values
>>> print("\n".join([str((TAGS[k], exifd[k])) for k in keys]))
('ImageWidth', 3872)
('ImageLength', 2592)
('BitsPerSample', (8, 8, 8))
('Compression', 1)
('GPSInfo', {0: b'\x00\x00\x02\x02', 1: 'N', 2: (53.0, 20.0, 58.86), 3: 'W', 4: (6.0, 15.0, 36.29), 5: b'\x00', 6: 0.0})
('PhotometricInterpretation', 2)
--- snip ---
('WhiteBalance', 1)
>>> gpsinfo = exifd[_TAGS_r["GPSInfo"]]
>>> gpsinfo
{0: b'\x00\x00\x02\x02', 1: 'N', 2: (53.0, 20.0, 58.86), 3: 'W', 4: (6.0, 15.0, 36.29), 5: b'\x00', 6: 0.0}
>>> print("\n".join([GPSTAGS[k] for k in gpsinfo.keys()]))
GPSVersionID
GPSLatitudeRef
GPSLatitude
GPSLongitudeRef
GPSLongitude
GPSAltitudeRef
GPSAltitude
>>> print("\n".join([str((GPSTAGS[k], gpsinfo[k])) for k in gpsinfo.keys()]))
('GPSVersionID', b'\x00\x00\x02\x02')
('GPSLatitudeRef', 'N')
('GPSLatitude', (53.0, 20.0, 58.86))
('GPSLongitudeRef', 'W')
('GPSLongitude', (6.0, 15.0, 36.29))
('GPSAltitudeRef', b'\x00')
('GPSAltitude', 0.0)

Building the exif tags by hand, and saving it

It is too heavy to build exif data by hand with only pillow, because exif data is a binary (of course!), its specification might be larger than you think, and pillow’s supports for it are not rich enough.

Actually, the Exif tag structure is borrowed from TIFF files, so you might think TiffImagePlugin.ImageFileDirectory* are useful, but those are insufficient for Exif (in JPEG). Several reasons:

  1. TiffImagePlugin.ImageFileDirectory* doesn’t know about value type information for non-TIFF tags.

  2. TiffImagePlugin.ImageFileDirectory* doesn’t know about a whole Exif structure in JPEG.

  3. TiffImagePlugin.ImageFileDirectory* doesn’t know about substructure needed by GPSInfo.

If your needs are very simple, indeed you can:

>>> from io import BytesIO
>>> from PIL import Image, TiffImagePlugin, TiffTags
>>> from PIL.ExifTags import TAGS
>>> from PIL.TiffImagePlugin import ImageFileDirectory_v2
>>>
>>> _TAGS_r = dict(((v, k) for k, v in TAGS.items()))
>>>
>>> #
>>> jpgimg1 = Image.new("RGB", (64, 64))
>>>
>>> # Image File Directory
>>> ifd = ImageFileDirectory_v2()
>>>
>>> # TiffTags knows "Artist" (0x013b)
>>> TiffTags.lookup(_TAGS_r["Artist"])
TagInfo(value=315, name='Artist', type=2, length=1, enum={})
>>> ifd[_TAGS_r["Artist"]] = u'somebody'
>>> #ifd.tagtype[_TAGS_r['Artist']] = 2  # string, but you don't have to set explicitly.
>>>
>>> # TiffTags doesn't know "LightSource" (0x9208)
>>> TiffTags.lookup(_TAGS_r["LightSource"])
TagInfo(value=37384, name='LightSource', type=None, length=0, enum={})
>>> ifd[_TAGS_r['LightSource']] = 1  # DayLight
>>> ifd.tagtype[_TAGS_r['LightSource']] = 3  # short, you must set.
>>>
>>> ##
>>> out = BytesIO()
>>> ifd.save(out)
48
>>> ## you must add magic number of exif structure
>>> exif = b"Exif\x00\x00" + out.getvalue()
>>>
>>> jpgimg1.save("out.jpg", exif=exif)
>>> jpgimg2 = Image.open("out.jpg")
>>> #
>>> jpgimg2._getexif()
{37384: 1, 315: 'somebody'}

But your needs should be more complex, and your code will lose maintainability soon.

You can use other third party library, for example piexif:

>>> from PIL import Image
>>> import piexif
>>> zeroth_ifd = {
...     piexif.ImageIFD.Artist: u"someone",
...     piexif.ImageIFD.XResolution: (96, 1),
...     piexif.ImageIFD.YResolution: (96, 1),
...     piexif.ImageIFD.Software: u"piexif"
...     }
>>> exif_ifd = {
...     piexif.ExifIFD.DateTimeOriginal: u"2099:09:29 10:10:10",
...     piexif.ExifIFD.LensMake: u"LensMake",
...     piexif.ExifIFD.Sharpness: 65535,
...     piexif.ExifIFD.LensSpecification: ((1, 1), (1, 1), (1, 1), (1, 1)),
...     }
>>> exif_dict = {"0th": zeroth_ifd, "Exif": exif_ifd}
>>> exif_bytes = piexif.dump(exif_dict)
>>> jpgimg1 = Image.new("RGB", (64, 64))
>>> jpgimg1.save("out.jpg", exif=exif_bytes)
>>> jpgimg2 = Image.open("out.jpg")
>>> #
>>> jpgimg2._getexif()
{36867: '2099:09:29 10:10:10', 315: 'someone', 34665: 105, 41994: 65535, 305: 'piexif', 42034: ((1, 1), (1, 1), (1, 1), (1, 1)), 42035: 'LensMake', 282: (96, 1), 283: (96, 1)}

Accessing to GeoTIFF tags by Pillow

A typical library that can handle GeoTIFF is related to GDAL, but GDAL tends to be difficult to introduce due to its many dependencies, and some people say “I don’t want to think about using it if possible”. I’m not the only one thinking about this, but noGDAL is probably the forefront.

Depending on how much GeoTIFF functionality you use for image processing, Pillow alone is quite possible if you just want to decipher GeoTIFF-specific tags:

getgeotifftags.py
# -*- coding: utf-8 -*-
import struct
from PIL import Image


_tags = {
    # https://www.awaresystems.be/imaging/tiff/tifftags/modelpixelscaletag.html
    33550: "ModelPixelScale",  # (ScaleX, ScaleY, ScaleZ)

    # https://www.awaresystems.be/imaging/tiff/tifftags/modeltiepointtag.html
    33922: "ModelTiepoint",  # (...,I,J,K, X,Y,Z...)

    # https://www.awaresystems.be/imaging/tiff/tifftags/modeltransformationtag.html
    34264: "ModelTransformation",  # double*16

    # https://www.awaresystems.be/imaging/tiff/tifftags/geokeydirectorytag.html
    34735: "GeoKeyDirectory",

    # https://www.awaresystems.be/imaging/tiff/tifftags/geodoubleparamstag.html
    34736: "GeoDoubleParams",

    # https://www.awaresystems.be/imaging/tiff/tifftags/geoasciiparamstag.html
    34737: "GeoAsciiParams",
}


_keys = {
    1024: 'GTModelTypeGeoKey',
    1025: 'GTRasterTypeGeoKey',
    1026: 'GTCitationGeoKey',
    2048: 'GeographicTypeGeoKey',
    2049: 'GeogCitationGeoKey',
    2050: 'GeogGeodeticDatumGeoKey',
    2051: 'GeogPrimeMeridianGeoKey',
    2052: 'GeogLinearUnitsGeoKey',
    2053: 'GeogLinearUnitSizeGeoKey',
    2054: 'GeogAngularUnitsGeoKey',
    2055: 'GeogAngularUnitsSizeGeoKey',
    2056: 'GeogEllipsoidGeoKey',
    2057: 'GeogSemiMajorAxisGeoKey',
    2058: 'GeogSemiMinorAxisGeoKey',
    2059: 'GeogInvFlatteningGeoKey',
    2060: 'GeogAzimuthUnitsGeoKey',
    2061: 'GeogPrimeMeridianLongGeoKey',
    2062: 'GeogTOWGS84GeoKey',
    3059: 'ProjLinearUnitsInterpCorrectGeoKey',  # GDAL
    3072: 'ProjectedCSTypeGeoKey',
    3073: 'PCSCitationGeoKey',
    3074: 'ProjectionGeoKey',
    3075: 'ProjCoordTransGeoKey',
    3076: 'ProjLinearUnitsGeoKey',
    3077: 'ProjLinearUnitSizeGeoKey',
    3078: 'ProjStdParallel1GeoKey',
    3079: 'ProjStdParallel2GeoKey',
    3080: 'ProjNatOriginLongGeoKey',
    3081: 'ProjNatOriginLatGeoKey',
    3082: 'ProjFalseEastingGeoKey',
    3083: 'ProjFalseNorthingGeoKey',
    3084: 'ProjFalseOriginLongGeoKey',
    3085: 'ProjFalseOriginLatGeoKey',
    3086: 'ProjFalseOriginEastingGeoKey',
    3087: 'ProjFalseOriginNorthingGeoKey',
    3088: 'ProjCenterLongGeoKey',
    3089: 'ProjCenterLatGeoKey',
    3090: 'ProjCenterEastingGeoKey',
    3091: 'ProjFalseOriginNorthingGeoKey',
    3092: 'ProjScaleAtNatOriginGeoKey',
    3093: 'ProjScaleAtCenterGeoKey',
    3094: 'ProjAzimuthAngleGeoKey',
    3095: 'ProjStraightVertPoleLongGeoKey',
    3096: 'ProjRectifiedGridAngleGeoKey',
    4096: 'VerticalCSTypeGeoKey',
    4097: 'VerticalCitationGeoKey',
    4098: 'VerticalDatumGeoKey',
    4099: 'VerticalUnitsGeoKey',
}


def getgeotiffdata(img):  # FIXME: this can't handle correctly if its endian is big-endian.
    result = {}

    if not hasattr(img, "tag"):
        img = Image.open(img)
    tagdata = img.tag.tagdata

    # ModelPixelScale
    if 33550 in tagdata:
        result[_tags[33550]] = struct.unpack(
            "<3d", tagdata[33550])

    # ModelTiepoint
    if 33922 in tagdata:
        result[_tags[33922]] = struct.unpack(
            "<{}d".format(len(tagdata[33922]) // 8), tagdata[33922])

    # ModelTransformation
    if 34264 in tagdata:
        result[_tags[34264]] = struct.unpack(
            "<{}d".format(len(tagdata[34264]) // 8), tagdata[34264])

    # GeoKeyDirectory
    #   GeoDoubleParams
    #   GeoAsciiParams
    if 34735 in tagdata:
        inner = result[_tags[34735]] = [{}, {}]
        #
        gkd = struct.unpack("<{}H".format(len(tagdata[34735]) // 2), tagdata[34735])
        gkd = [gkd[i:i + 4] for i in range(0, len(gkd), 4)]
        KeyDirectoryVersion, KeyRevision, KeyRevisionMinor = gkd.pop(0)[:-1]
        inner[0]["KeyDirectoryVersion"] = KeyDirectoryVersion
        inner[0]["KeyRevision"] = KeyRevision
        inner[0]["KeyRevisionMinor"] = KeyRevisionMinor
        #
        for keyid, tagid, count, offset in gkd:
            if tagid == 0:
                value = offset
            else:
                if tagid == 34736:
                    value = tagdata[tagid]
                    value = struct.unpack("<{}d".format(len(value) // 8), value)
                elif tagid == 34737:
                    value = tagdata[tagid][offset:offset + count]
                    value = value.decode()
                    if value[-1] == "|":
                        value = value[:-1]
                else:
                     raise NotImplementedError("sorry")
            inner[1][_keys.get(keyid, keyid)] = value
    return result, img.size


if __name__ == '__main__':
    import sys
    import json
    geotiffdata, (width, height) = getgeotiffdata(sys.argv[1])
    print(json.dumps(geotiffdata, indent=2))
[me@host: ~]$ python getgeotifftags.py data/manhattan.tif
{
  "ModelTiepoint": [
    0.0,
    0.0,
    0.0,
    583057.357,
    4516255.36,
    0.0
  ],
  "ModelPixelScale": [
    0.999948245999997,
    0.999948245999997,
    0.0
  ],
  "GeoKeyDirectory": [
    {
      "KeyRevision": 1,
      "KeyDirectoryVersion": 1,
      "KeyRevisionMinor": 0
    },
    {
      "PCSCitationGeoKey": "IMAGINE GeoTIFF Support\nCopyright 1991 - 2005 by Leica Geosystems Geospatial Imaging, LLC. All Rights Reserved\n@(#)$RCSfile: egtf.c $ IMAGINE 9.0 $Revision: 10.0 $ $Date: 2005/07/26 15:10:00 EST $\nUTM Zone 18N\nEllipsoid = GRS 1980\nDatum = NAD83",
      "ProjLinearUnitsGeoKey": 9001,
      "ProjectedCSTypeGeoKey": 26918,
      "GTModelTypeGeoKey": 1,
      "GTRasterTypeGeoKey": 1,
      "GTCitationGeoKey": "IMAGINE GeoTIFF Support\nCopyright 1991 - 2005 by Leica Geosystems Geospatial Imaging, LLC. All Rights Reserved\n@(#)$RCSfile: egtf.c $ IMAGINE 9.0 $Revision: 10.0 $ $Date: 2005/07/26 15:10:00 EST $\nProjection Name = UTM\nUnits = meters\nGeoTIFF Units = meters"
    }
  ]
}
[me@host: ~]$ python getgeotifftags.py data/landsat.tif
{
  "ModelTiepoint": [
    0.0,
    0.0,
    0.0,
    -13051837.419021819,
    6193028.747207512,
    0.0
  ],
  "ModelPixelScale": [
    45.19374321600039,
    45.14032535950664,
    0.0
  ],
  "GeoKeyDirectory": [
    {
      "KeyRevision": 1,
      "KeyDirectoryVersion": 1,
      "KeyRevisionMinor": 0
    },
    {
      "ProjLinearUnitsGeoKey": 9001,
      "ProjectedCSTypeGeoKey": 3857,
      "GTModelTypeGeoKey": 1,
      "GeogAngularUnitsGeoKey": 9102,
      "GeogCitationGeoKey": "WGS 84",
      "GTRasterTypeGeoKey": 1,
      "GTCitationGeoKey": "WGS 84 / Pseudo-Mercator"
    }
  ]
}
[me@host: ~]$ python getgeotifftags.py NE1_LR_LC/NE1_LR_LC.tif
{
  "ModelTiepoint": [
    0.0,
    0.0,
    0.0,
    -180.0,
    90.0,
    0.0
  ],
  "ModelPixelScale": [
    0.02222222222222,
    0.02222222222222,
    0.0
  ],
  "GeoKeyDirectory": [
    {
      "KeyRevision": 1,
      "KeyDirectoryVersion": 1,
      "KeyRevisionMinor": 0
    },
    {
      "GTModelTypeGeoKey": 2,
      "GeogAngularUnitsGeoKey": 9102,
      "GeogInvFlatteningGeoKey": [
        298.257223563,
        6378137.0
      ],
      "GeogCitationGeoKey": "WGS 84",
      "GTRasterTypeGeoKey": 1,
      "GeographicTypeGeoKey": 4326,
      "GeogSemiMajorAxisGeoKey": [
        298.257223563,
        6378137.0
      ]
    }
  ]
}
[me@host: ~]$ python getgeotifftags.py FAA_UTM18N_NAD83.tif
{
  "ModelTiepoint": [
    0.5,
    0.5,
    0.0,
    223598.2000551123,
    4217895.013917857,
    0.0
  ],
  "ModelPixelScale": [
    23.927070933333344,
    23.927070933333344,
    0.0
  ],
  "GeoKeyDirectory": [
    {
      "KeyRevision": 1,
      "KeyDirectoryVersion": 1,
      "KeyRevisionMinor": 0
    },
    {
      "ProjLinearUnitsGeoKey": 9001,
      "GTCitationGeoKey": "#MAP_PROJECTION\n\"NAD83 / UTM zone 18N\"\nNAD83,6378137,0.0818191910428158,0\n\"Transverse Mercator\",0,-75,0.9996,500000,0\n#UNITS_LENGTH\nm,1\n#MAP_DATUM_TRANSFORM\n\"NAD83 to WGS 84 (1)\",0,0,0,0,0,0,0\n",
      "ProjectedCSTypeGeoKey": 26918,
      "GTRasterTypeGeoKey": 1,
      "GTModelTypeGeoKey": 1
    }
  ]
}

You may also want to consider tifffile. If you use it, you can like this:

getgeotifftags2.py
# -*- coding: utf-8 -*-
import tifffile


def getgeotiffdata(img):
    return tifffile.TiffFile(img).pages[0].geotiff_tags


if __name__ == '__main__':
    import sys
    import json
    print(json.dumps(getgeotiffdata(sys.argv[1]), indent=2))
[me@host: ~]$ python getgeotifftags2.py NE1_LR_LC/NE1_LR_LC.tif
 {
  "KeyDirectoryVersion": 1,
  "KeyRevision": 1,
  "KeyRevisionMinor": 0,
  "GTModelTypeGeoKey": 2,
  "GTRasterTypeGeoKey": 1,
  "GeographicTypeGeoKey": 4326,
  "GeogCitationGeoKey": "WGS 84",
  "GeogAngularUnitsGeoKey": 9102,
  "GeogSemiMajorAxisGeoKey": 6378137.0,
  "GeogInvFlatteningGeoKey": 298.257223563,
  "ModelPixelScale": [
    0.02222222222222,
    0.02222222222222,
    0.0
  ],
  "ModelTiepoint": [
    0.0,
    0.0,
    0.0,
    -180.0,
    90.0,
    0.0
  ]
}

You can get “modeltransformation” from “modelpixelscale”, and “modeltiepoint”:

getgeotifftags.py
# -*- coding: utf-8 -*-
#
# ...(snip)...
#

def gettransforms(geotiffdata):
    """
    build modeltransformation from modelpixelscale, and modeltiepoint
    """
    transforms = []
    if "ModelPixelScale" in geotiffdata and "ModelTiepoint" in geotiffdata:
        # see "B.6. GeoTIFF Tags for Coordinate Transformations"
        # in https://earthdata.nasa.gov/files/19-008r4.pdf.
        sx, sy, sz = geotiffdata["ModelPixelScale"]
        tiepoints = geotiffdata["ModelTiepoint"]
        for tp in range(0, len(tiepoints), 6):
            i, j, k, x, y, z = tiepoints[tp:tp + 6]
            transforms.append([
                [sx, 0.0, 0.0, x - i * sx],
                [0.0, -sy, 0.0, y + j * sy],
                [0.0, 0.0, sz, z - k * sz],
                [0.0, 0.0, 0.0, 1.0]])
    return transforms


if __name__ == '__main__':
    import sys
    import json
    import numpy as np
    geotiffdata, (width, height) = getgeotiffdata(sys.argv[1])
    print(json.dumps(geotiffdata, indent=2))
    transforms = gettransforms(geotiffdata)
    print(np.dot(transforms, [0, 0, 0, 1])[0,:2])
    print(np.dot(transforms, [width, height, 0, 1])[0,:2])
[me@host: ~]$ python getgeotifftags.py gm-jpn-ve_u_1_1/jpn/ve.tif
{
  "ModelTiepoint": [
    0.0,
    0.0,
    0.0,
    120.00833129882812,
    49.99166809767485,
    0.0
  ],
  "ModelPixelScale": [
    0.008333333767950535,
    0.008333333767950535,
    0.0
  ],
  "GeoKeyDirectory": [
    {
      "KeyRevision": 1,
      "KeyDirectoryVersion": 1,
      "KeyRevisionMinor": 0
    },
    {
      "GTRasterTypeGeoKey": 2
    }
  ]
}
[120.0083313   49.9916681]
[155.00833312   19.99166653]