EXIF - 隐藏在图片中的重要信息

Feei <feei#feei> 05/2018

EXIF指可交换图片文件格式(Exchangeable Image file Format),可以在图片中记录数码相机的相关数据,比如拍摄的设备、拍摄的参数以及地理位置等。

0x01 技术原理

最新的EXIF v2.3由CIPA(相机影像产品工业协会)和JEITA(日本电子信息技术产业协会)制定,

使用16进制编辑器(Hex Fiend)打开一张由iPhone拍摄的图片:

EXIF

右侧可以看到Apple iPhone X以及一些时间信息,翻阅EXIF标准可以看到,0xFFD8为图片压缩数据的开始、0xFFE1为EXIF属性信息、0xFFE2为EXIF扩展信息、0xFFD9为压缩数据的结尾。

EXIF Structure

根据上面结构图可以看到主要内容分为几块:

EXIF

根据标准中每项信息标记的点可以读取图片背后对应的信息。

0x02 技术实现

下面是一张iPhone拍摄的图片全部EXIF元数据信息:

# https://github.com/FeeiCN/EXIF
"""
''  : 为空则未想好翻译内容
None: 为None则不予显示
"""
TRANSLATE_KEY = {
    'Image Make': '机器制造',  # Apple
    'Image Model': '机器模型',  # iPhone X
    'Image XResolution': '图片分辨率(X)',  # 72
    'Image YResolution': '图片分辨率(Y)',  # 72
    'Image ResolutionUnit': '分辨率单位',  # Pixels/Inch
    'Image Software': '软件版本',  # 11.3
    'Image DateTime': '拍摄时间',  # 2018:05:04 18:45:32
    'Image TileWidth': '图片平铺宽度',  # 512
    'Image TileLength': '图片平铺长度',  # 512
    'Image ExifOffset': None,  # 204
    'Image Orientation': '图片方向',  # Horizontal (normal)
    'GPS GPSLatitudeRef': None,  # N
    'GPS GPSLatitude': 'GPS纬度(N)',  # [39, 16, 2259/50]
    'GPS GPSLongitudeRef': None,  # E
    'GPS GPSLongitude': 'GPS经度(E)',  # [99, 48, 897/25]
    'GPS GPSAltitudeRef': None,  # 0
    'GPS GPSAltitude': 'GPS高度',  # 44355/32
    'GPS GPSTimeStamp': 'GPS时间',  # [10, 45, 31]
    'GPS GPSSpeedRef': None,  # K
    'GPS GPSSpeed': 'GPS速度',  # 0(公里每小时)
    'GPS GPSImgDirectionRef': None,  # T
    'GPS GPSImgDirection': 'GPS方向',  # 7067/1037
    'GPS GPSDestBearingRef': None,  # T
    'GPS GPSDestBearing': 'GPS方位',  # 7067/1037
    'GPS GPSDate': 'GPS日期',  # 2018:05:04
    'GPS Tag 0x001F': 'GPS Tag',  # 10
    'Image GPSInfo': 'GPS信息',  # 1754
    'EXIF ExposureTime': '曝光时间',  # 1/15
    'EXIF FNumber': 'F数',  # 9/5
    'EXIF ExposureProgram': '曝光程序',  # Program Normal
    'EXIF ISOSpeedRatings': 'ISO感光等级',  # 40
    'EXIF ExifVersion': 'EXIF版本',  # 0221
    'EXIF DateTimeOriginal': '原始时间',  # 2018:05:04 18:45:32
    'EXIF DateTimeDigitized': '数字时间',  # 2018:05:04 18:45:32
    'EXIF ComponentsConfiguration': '组件peizhi ',  # YCbCr
    'EXIF ShutterSpeedValue': '快门速度',  # 3303/845
    'EXIF ApertureValue': '光圈',  # 2159/1273
    'EXIF BrightnessValue': '亮度',  # 3694/1557
    'EXIF ExposureBiasValue': '曝光补偿',  # 0
    'EXIF MeteringMode': '测光模式',  # Pattern
    'EXIF Flash': '闪光灯',  # Flash did not fire, auto mode
    'EXIF FocalLength': '焦距',  # 4
    'EXIF SubjectArea': '主体区域',  # [2015, 1511, 2217, 1330]
    'EXIF SubSecTimeOriginal': '子原始时间',  # 357
    'EXIF SubSecTimeDigitized': '子数字时间',  # 357
    'EXIF FlashPixVersion': 'FlashPix版本',  # 0100
    'EXIF ExifImageWidth': '图片宽度',  # 3662
    'EXIF ExifImageLength': '图片高度',  # 2744
    'EXIF SensingMethod': '传感方式',  # One-chip color area
    'EXIF SceneType': '场景类型',  # Directly Photographed
    'EXIF ExposureMode': '曝光模式',  # Auto Exposure
    'EXIF WhiteBalance': '白平衡',  # Auto
    'EXIF FocalLengthIn35mmFilm': '35mm胶片',  # 28
    'EXIF SceneCaptureType': '场景模式',  # Standard
    'EXIF LensSpecification': '镜头规格',  # [4, 6, 9/5, 12/5]
    'EXIF LensMake': '镜头制造',  # Apple
    'EXIF LensModel': '镜头模型',  # iPhone X back dual camera 4mm f/1.8
}

exif-py是一个基础的EXIF解析Python模块,使用它可以方便的读取图片中的EXIF元数据。

def read_exif(path):
    data = {
        'image': [],
        'other': {}
    }

    # 基础信息
    data['image'].append('文件类型: {type}'.format(type=os.path.splitext(path)[1].upper()))
    data['image'].append('文件创建时间: {time}'.format(time=creation_date(path)))
    data['image'].append('文件修改时间: {time}'.format(time=modification_date(path)))
    data['image'].append('文件大小: {size}'.format(size=file_size(path)))

    f = open(path, 'rb')
    tags = exifread.process_file(f)
    for key, value in tags.items():
        key = key.strip()
        if key not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename', 'EXIF MakerNote'):
            t_tag = key
            if key in TRANSLATE_KEY and TRANSLATE_KEY[key] != '':
                t_tag = TRANSLATE_KEY[key]
                if t_tag is None:
                    print('SKIP TAG', key)
                    continue
            t_value = value
            if str(value) in TRANSLATE_VALUE and TRANSLATE_VALUE[str(value)] != '':
                t_value = TRANSLATE_VALUE[str(value)]
            print(t_tag, '-', t_value)

            # Special Cover Value
            if key in ['GPS GPSLatitude', 'GPS GPSLongitude']:
                tmp_v = str(t_value).replace('[', '').replace(']', '').split(', ')
                for i in range(len(tmp_v)):
                    if '/' in tmp_v[i]:
                        tmp_v[i] = float(int(tmp_v[i].split('/')[0]) / int(tmp_v[i].split('/')[1]))
                    else:
                        tmp_v[i] = float(int(tmp_v[i]))
                t_value = cover_gps(tmp_v[0], tmp_v[1], tmp_v[2])
                data['other'][key.split(' ')[1]] = t_value
            if key in ['EXIF DateTimeOriginal', 'EXIF DateTimeDigitized', 'Image DateTime', 'GPS GPSDate']:
                if ' ' in str(t_value):
                    t_value = '{date} {time}'.format(date=str(t_value).split(' ')[0].replace(':', '-'), time=str(t_value).split(' ')[1])
                else:
                    t_value = str(t_value).replace(':', '-')

            if 'Image ' in key:
                tag = 'image'
            elif 'GPS ' in key:
                tag = 'gps'
            elif 'EXIF ' in key:
                tag = 'exif'
            else:
                tag = key
            v = '{t_tag}: {v}'.format(t_tag=t_tag, v=str(t_value))
            if tag in data:
                print(v)
                data[tag].append(v)
            else:
                data[tag] = [v]
        else:
            print('NOT IN', key, value)
    return data

GPS坐标转换

可以看到获取到的GPS经纬度坐标是纬度(N)[39, 16, 2259/50]/经度(E)[99, 48, 897/25]格式的,格式化为度分秒则是N39° 16′ 45.18″/E99° 48′ 35.88″

根据度分秒格式的GPS经纬度坐标可以在在线经纬度地图地图中找到所在位置,但比如在高德中则需要十进制的格式。

度分秒转换为十进制互相转换

根据GPS坐标转换公式: 度 = 度 + 分/60 + 秒/3600

39 + 16/60 + 45.18/3600 = 39.279216667/(E)99 + 48/60 + 35.88/3600 = 99.809966667

def cover_gps(n1, n2, n3):
    # [39, 16, 2259/50]
    # [99, 48, 897/25]
    # 39 + 16/60 + (2259/50)/3600 = 39.279216667
    # 99 + 48/60 + (897/25)/3600 = 99.809966667
    return float(n1) + (float(n2) / 60) + (float(n3) / 3600)

十进制坐标到实际地址位置互相转换

文件创建和修改时间

主要需要考虑的是跨平台调用,每个平台上对于文件创建时间的处理不太一致。

文件创建时间

文件修改时间

import os
import datetime
import platform

def creation_date(path):
    if platform.system() == 'Windows':
        t = os.path.getctime(path)
    else:
        stat = os.stat(path)
        try:
            t = stat.st_birthtime
        except AttributeError:
            t = stat.st_mtime
    return datetime.datetime.fromtimestamp(t).strftime('%Y-%m-%d %H:%M:%S')


def modification_date(path):
    return datetime.datetime.fromtimestamp(os.path.getmtime(path)).strftime('%Y-%m-%d %H:%M:%S')

0x03 使用场景

通过一张图片可以获取到的信息:

使用场景

日常场景

0x04 开源

整个项目已开源至GitHub(https://github.com/FeeiCN/EXIF)。

EXIF Web

参考引用