最近购入了一台二手的索尼A7R2相机,拍照还是挺不错的,因为我没有啥视频需求。但是这是2015年发布的相机,不能连接creater's app。拍出来的照片没有GPS信息。

于是我想到了使用手表记录运动轨迹然后导入EXIF信息,我使用的小米手表S4,选择的运动模式是健走,导出类型选导出数据文件,选GPX文件,可保存到本地或者通过微信发送至电脑。

Screenshot_2025-11-10-19-51-58-526_com.mi.health

Screenshot_2025-11-10-19-54-58-064_com.android.fileexplorer

GPX文件相当于一个xml文件,部分格式如下,主要就是记录了时间(每秒)与经纬度信息。

<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<gpx version="1.0" xmlns="http://www.topografix.com/GPX/1/1"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
    <name>20251108健走</name>
    <desc>Export from Mi Fitness</desc>
    <trk>
        <extensions>
            <totalDistance>4013</totalDistance>
            <cumulativeClimb>18.042004</cumulativeClimb>
            <cumulativeDecrease>16.06397</cumulativeDecrease>
        </extensions>
        <trkseg>
            <trkpt lat="24.936992645263672" lon="118.58102416992188">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:06.000Z</time>
            </trkpt>
            <trkpt lat="24.93698501586914" lon="118.5810317993164">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:07.000Z</time>
            </trkpt>
            <trkpt lat="24.93696403503418" lon="118.5810317993164">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:08.000Z</time>
            </trkpt>
            <trkpt lat="24.936946868896484" lon="118.5810317993164">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:09.000Z</time>
            </trkpt>
            <trkpt lat="24.93693733215332" lon="118.58106231689453">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:10.000Z</time>
            </trkpt>
            <trkpt lat="24.936922073364258" lon="118.58106231689453">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:11.000Z</time>
            </trkpt>
            <trkpt lat="24.936908721923828" lon="118.5810546875">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:12.000Z</time>
            </trkpt>
            <trkpt lat="24.936899185180664" lon="118.5810546875">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:13.000Z</time>
            </trkpt>
            <trkpt lat="24.9368839263916" lon="118.5810546875">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:14.000Z</time>
            </trkpt>
            <trkpt lat="24.936864852905273" lon="118.58104705810547">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:15.000Z</time>
            </trkpt>
            <trkpt lat="24.936853408813477" lon="118.58103942871094">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:16.000Z</time>
            </trkpt>
            <trkpt lat="24.936843872070312" lon="118.58103942871094">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:17.000Z</time>
            </trkpt>
            <trkpt lat="24.936832427978516" lon="118.58103942871094">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:18.000Z</time>
            </trkpt>
            <trkpt lat="24.936826705932617" lon="118.58103942871094">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:19.000Z</time>
            </trkpt>
            <trkpt lat="24.93682098388672" lon="118.58104705810547">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:20.000Z</time>
            </trkpt>
            <trkpt lat="24.936813354492188" lon="118.5810546875">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:21.000Z</time>
            </trkpt>
            <trkpt lat="24.936800003051758" lon="118.58106231689453">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:22.000Z</time>
            </trkpt>
            <trkpt lat="24.936784744262695" lon="118.5810546875">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:23.000Z</time>
            </trkpt>
            <trkpt lat="24.936771392822266" lon="118.5810546875">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:24.000Z</time>
            </trkpt>
            <trkpt lat="24.936758041381836" lon="118.5810546875">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:25.000Z</time>
            </trkpt>
            <trkpt lat="24.936742782592773" lon="118.5810546875">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:26.000Z</time>
            </trkpt>
            <trkpt lat="24.936731338500977" lon="118.5810546875">
                <ele>30.0</ele>
                <time>2025-11-08T14:19:27.000Z</time>
            </trkpt>
            ......
        </trkseg>
    </trk>
</gpx>

照片的EXIF信息中有拍摄时间信息,于是可以匹配到对应时间的GPS信息。

主要流程就是:读取并解析GPX信息→读取照片EXIF时间信息→匹配最接近的时间信息对应的GPS信息→写入GPS信息到照片。

python脚本如下,首先需要安装exiftool工具

import xml.etree.ElementTree as ET
from datetime import datetime, timezone, timedelta
import sys
import os
import subprocess
from pathlib import Path

def parse_gpx_file(gpx_file_path):
    """
    解析GPX文件,提取时间(转为北京时间)和经纬度信息
    """
    try:
        # 解析GPX文件
        tree = ET.parse(gpx_file_path)
        root = tree.getroot()
        
        # GPX命名空间
        namespaces = {
            'gpx': 'http://www.topografix.com/GPX/1/1'
        }
        
        # 查找所有的trkpt(轨迹点)
        track_points = root.findall('.//gpx:trkpt', namespaces)
        
        if not track_points:
            # 如果没有找到trkpt,尝试查找rtept(路线点)或wpt(航点)
            track_points = root.findall('.//gpx:rtept', namespaces)
            if not track_points:
                track_points = root.findall('.//gpx:wpt', namespaces)
        
        # 存储轨迹点数据
        gpx_data = []
        
        for point in track_points:
            # 获取经纬度
            lat = point.get('lat')
            lon = point.get('lon')
            
            # 获取时间
            time_element = point.find('gpx:time', namespaces)
            time_str = time_element.text if time_element is not None else None
            
            # 处理时间
            local_time = None
            if time_str:
                try:
                    # 根据您的说明,GPX文件中的时间已经是中国当地时间
                    # 所以我们只需要解析时间字符串,不需要进行时区转换
                    if time_str.endswith('Z'):
                        # 移除Z并解析时间
                        temp_time = datetime.fromisoformat(time_str[:-1])
                        local_time = temp_time
                    else:
                        # 直接解析时间
                        local_time = datetime.fromisoformat(time_str)
                except ValueError as e:
                    print(f"无法解析时间格式: {time_str}, 错误: {e}")
                    continue
            
            if local_time and lat and lon:
                # 保持原始精度,不进行浮点数转换
                gpx_data.append({
                    'time': local_time,
                    'lat': lat,  # 保持字符串格式以维持精度
                    'lon': lon   # 保持字符串格式以维持精度
                })
        
        print(f"GPX文件解析完成,共找到 {len(gpx_data)} 个轨迹点")
        return gpx_data
            
    except ET.ParseError as e:
        print(f"解析GPX文件出错: {e}")
    except FileNotFoundError:
        print(f"文件未找到: {gpx_file_path}")
    except Exception as e:
        print(f"处理文件时出错: {e}")
    
    return []

def get_photo_capture_time(photo_path):
    """
    使用exiftool获取照片的拍摄时间
    """
    try:
        # 使用exiftool获取拍摄时间
        result = subprocess.run([
            'exiftool', 
            '-DateTimeOriginal', 
            '-s', 
            '-s', 
            '-s', 
            photo_path
        ], capture_output=True, text=True, check=True)
        
        time_str = result.stdout.strip()
        if time_str and time_str != '-':
            # 解析时间字符串 (格式: 2025:11:08 14:19:06)
            capture_time = datetime.strptime(time_str, '%Y:%m:%d %H:%M:%S')
            return capture_time
    except subprocess.CalledProcessError:
        pass
    except ValueError:
        pass
    
    # 如果DateTimeOriginal不可用,尝试CreateDate
    try:
        result = subprocess.run([
            'exiftool', 
            '-CreateDate', 
            '-s', 
            '-s', 
            '-s', 
            photo_path
        ], capture_output=True, text=True, check=True)
        
        time_str = result.stdout.strip()
        if time_str and time_str != '-':
            capture_time = datetime.strptime(time_str, '%Y:%m:%d %H:%M:%S')
            return capture_time
    except (subprocess.CalledProcessError, ValueError):
        pass
    
    return None

def find_closest_gpx_point(photo_time, gpx_data, max_time_diff=10):
    """
    在GPX数据中找到与照片拍摄时间最接近的点
    """
    if not gpx_data or not photo_time:
        return None
    
    closest_point = None
    min_time_diff = None
    
    for point in gpx_data:
        time_diff = abs(point['time'] - photo_time)
        if max_time_diff is not None and time_diff > timedelta(seconds=max_time_diff):
            continue
            
        if min_time_diff is None or time_diff < min_time_diff:
            min_time_diff = time_diff
            closest_point = point
    
    return closest_point

def find_best_matching_gpx_point(photo_time, gpx_data):
    """
    查找最佳匹配的GPX点,使用更智能的匹配策略
    """
    if not gpx_data or not photo_time:
        return None
    
    # 首先尝试严格的10秒匹配
    closest_point = find_closest_gpx_point(photo_time, gpx_data, 10)
    if closest_point:
        return closest_point
    
    # 如果没有找到,尝试更宽松的匹配(30秒内)
    closest_point = find_closest_gpx_point(photo_time, gpx_data, 30)
    if closest_point:
        time_diff = abs(closest_point['time'] - photo_time)
        print(f"  注意: 时间差较大 ({time_diff.total_seconds():.1f}秒),但仍使用最接近的点")
        return closest_point
    
    # 如果时间差超过30秒,不进行匹配
    # 查找全局最接近的点,检查时间差是否超过30秒
    closest_point = find_closest_gpx_point(photo_time, gpx_data, None)
    if closest_point:
        time_diff = abs(closest_point['time'] - photo_time)
        if time_diff > timedelta(seconds=30):
            print(f"  时间差过大 ({time_diff.total_seconds():.1f}秒 > 30秒),跳过GPS信息写入")
            return None
        else:
            print(f"  警告: 时间差较大 ({time_diff.total_seconds():.1f}秒),使用最接近的点")
            return closest_point
    
    return None

def write_gps_to_photo(photo_path, lat, lon):
    """
    使用exiftool将GPS信息写入照片,保持原始精度
    """
    try:
        # 转换经纬度为EXIF格式
        lat_ref = 'N' if float(lat) >= 0 else 'S'
        lon_ref = 'E' if float(lon) >= 0 else 'W'
        
        # 使用原始精度的字符串格式
        result = subprocess.run([
            'exiftool',
            f'-GPSLatitude={lat}',      # 保持原始精度
            f'-GPSLongitude={lon}',     # 保持原始精度
            f'-GPSLatitudeRef={lat_ref}',
            f'-GPSLongitudeRef={lon_ref}',
            '-overwrite_original',
            photo_path
        ], check=True, capture_output=True, text=True)
        
        print(f"  已写入GPS信息: lat={lat}, lon={lon}")
        return True
    except subprocess.CalledProcessError as e:
        print(f"写入GPS信息失败: {e}")
        return False

def process_photos(path, gpx_data):
    """
    处理照片文件或目录中的所有照片
    """
    path_obj = Path(path)
    
    # 支持的照片格式
    photo_extensions = {'.jpg', '.jpeg', '.png', '.tiff', '.tif'}
    
    photo_files = []
    
    # 如果是文件
    if path_obj.is_file():
        if path_obj.suffix.lower() in photo_extensions:
            photo_files = [path_obj]
    # 如果是目录
    elif path_obj.is_dir():
        for ext in photo_extensions:
            photo_files.extend(path_obj.glob(f'*{ext}'))
            photo_files.extend(path_obj.glob(f'*{ext.upper()}'))
    
    if not photo_files:
        print(f"未找到照片文件: {path}")
        return
    
    print(f"找到 {len(photo_files)} 个照片文件")
    
    success_count = 0
    
    for photo_path in photo_files:
        print(f"\n处理照片: {photo_path.name}")
        
        # 获取照片拍摄时间
        photo_time = get_photo_capture_time(str(photo_path))
        if not photo_time:
            print(f"  无法获取拍摄时间")
            continue
        
        print(f"  拍摄时间: {photo_time.strftime('%Y-%m-%d %H:%M:%S')}")
        
        # 查找最接近的GPX点,使用改进的匹配策略
        closest_point = find_best_matching_gpx_point(photo_time, gpx_data)
        if not closest_point:
            print(f"  未找到时间匹配的GPS数据 (时间差超过30秒或无匹配数据)")
            continue
        
        time_diff = abs(closest_point['time'] - photo_time)
        print(f"  匹配GPX点: {closest_point['time'].strftime('%Y-%m-%d %H:%M:%S')} (误差: {time_diff.total_seconds():.1f}秒)")
        print(f"  GPS坐标: {closest_point['lat']}, {closest_point['lon']}")
        
        # 写入GPS信息
        if write_gps_to_photo(str(photo_path), closest_point['lat'], closest_point['lon']):
            print(f"  GPS信息写入成功")
            success_count += 1
        else:
            print(f"  GPS信息写入失败")
    
    print(f"\n处理完成: {success_count}/{len(photo_files)} 张照片成功写入GPS信息")

def main():
    """
    主函数
    """
    if len(sys.argv) < 3:
        print("使用方法: python gpx2exif.py <gpx文件路径> <照片文件或目录路径>")
        print("示例: python gpx2exif.py track.gpx photos/")
        print("示例: python gpx2exif.py track.gpx photo.jpg")
        return
    
    gpx_file_path = sys.argv[1]
    photo_path = sys.argv[2]
    
    # 解析GPX文件
    gpx_data = parse_gpx_file(gpx_file_path)
    if not gpx_data:
        print("GPX数据解析失败")
        return
    
    # 处理照片
    process_photos(photo_path, gpx_data)

if __name__ == "__main__":
    main()

使用方法:

# 处理目录中的所有照片
python gpx2exif.py track.gpx photos/

# 处理单张照片
python gpx2exif.py track.gpx photo.jpg

注意事项:

  1. 需要系统中已安装exiftool工具
  2. 照片的拍摄时间需要与GPX记录时间在同一时区
  3. 时间匹配精度为±10秒,可以根据需要调整
  4. 写入操作会直接修改原文件(使用-overwrite\_original参数)
最后修改:2025 年 11 月 10 日