资讯动态

Python读写JPG的EXIF GPS坐标:两套方案与实战避坑

发布时间:2026/9/16 11:33:46 来源:尧图企业网站定制
简介航拍影像JPG中的GPS坐标读取与写入是无人机测绘、地理信息处理及图像元数据开发中的常见需求。该资源面向Python开发者提供基于piexif与gpsphoto两种主流库的完整实现分别以读写脚本演示从图片中提取经度、纬度以及将新坐标批量写入JPG的方法。包内共9个文件包含4个Python脚本、2个CSV坐标数据文件及3张带GPS信息的样例JPG图片整个压缩包仅225KB轻量易用。已有791人学习下载。通过该资源包读者既能获得可直接运行的脚本也能理解Exif标准中GPS信息在JPG中的存储结构结合CSV文件还能快速搭建图片与坐标的批量读写流程适合用于自动化处理航拍影像的工程实践。 光靠“打开图片看”判断不了拍摄地航拍影像的 GPS 坐标能不能被二次使用取决于 EXIF 里那段标签结构有没有被正确读写。很多人把 JPG 重新保存一次或调一下方向再读坐标时返回一片空不是文件坏了而是读写路径压根没走对。Python 里做这件事有两套成熟方案一套是 exifread 配合 piexif 的轻量组合另一套是 pyexiv2 的 C 绑定库。前者纯 Python 实现部署简单适合单张和中小批次处理后者依赖原生 exiv2 库处理大批量航拍影像时性能和标签兼容性更好。这篇文章把 JPG 里 GPS 坐标的存储结构、两种实现方式、参数细节和常见的坑完整过一遍。2. 读写 JPG 的 GPS 坐标先弄清 EXIF 的 GPSInfo 结构与坐标系2.1 一张航拍 JPG 的 GPS 坐标藏在 EXIF 的哪个位置JPG 文件格式本质上是若干段的拼接开头是SOI文件头后面跟着APP0、APP1等标记段。EXIF 数据存放在APP1段里内部又是一个完整的 TIFF 结构TIFF 头 IFD0主图像信息 IFD1缩略图 若干个扩展 IFD。GPS 坐标记录在 GPSInfo 这个子 IFD 中它不是一个独立的数据块而是 IFD0 里的一个指针指向文件其他位置的子 IFD。无人机飞控在写入照片时会同时写 IFD0 里的拍摄参数和 GPS IFD 里的定位信息。不熟悉结构的人直接用字符串搜索文件内容找经纬度能找到但很容易改错原因就是 GPS 数据不是明文存储而是以特定标签和类型排列的。用 exifread 读取时标签名会按“组名 标签名”展开例如GPS GPSLatitude、GPS GPSLongitude。用 piexif 读取时GPS 部分在exif_dict[GPS]这个字典里。识别一张航拍图是否具备定位信息只需判断这两个入口是否为空。多数测绘后期软件如 ArcGIS、QGIS 也是读取同一段数据这意味着当你用 Python 改对了 GPS IFDGIS 软件里就能直接显示位置。2.2 度分秒是三个分数不是小数点EXIF 规范里经纬度不存浮点数而是存三组分数Rational顺序是度、分、秒。每组分数由分子和分母两个 32 位整数构成。例如北纬 39°56′12.34″在 EXIF 里存为(39, 1)、(56, 1)、(1234, 100)。exifread 读取出来的是Ratio对象列表pyexiv2 返回的是字符串39/1 56/1 1234/100。转换为十进制数时要逐项做除法d 39 / 1 m 56 / 1 s 1234 / 100 decimal d m / 60 s / 3600这里有一个常见的精度误区秒的分母决定了存储精度。分母是 100 时秒的最小单位是 0.01 秒约等于 0.31 米的地面距离对航拍影像的概略定位完全够用。如果做像控点级处理分母取 1000 更稳妥0.001 秒对应约 0.03 米超过普通航测 POS 数据本身精度。写入时把秒四舍五入到分母对应的位数不要直接无损保留浮点否则生成的分数可能异常。坐标引用系也要在一开始就区分。航拍影像的 EXIF 坐标默认采用 WGS84 椭球体这是 GPS 定位直接输出的坐标系。部分国产航线规划软件会输出 GCJ-02 加密坐标如果这个值被直接写入 EXIF后期在 ArcGIS 或 QGIS 里加载会偏移几百米。判断方式很简单把读出的坐标和 POS 文件里的经纬度对比差值大于 300 米且带规律偏移基本就是坐标系没对齐。2.3 exifread/piexif 与 pyexiv2两种 python 读写的选型边界方案选型取决于环境约束和操作规模。下表从依赖、能力、性能三个维度对比。方案依赖写入能力缩略图处理批量性能适用场景exifread piexif纯 Python标准 EXIF 标签写入时可能丢失中等单张处理、离线环境pyexiv2libexiv2 原生库EXIF、XMP、IPTC保留原缩略图较高批量归档、POS 匹配exifread 只负责读取写入要靠 piexif。piexif 的优点是 pip 安装即用不依赖系统动态库适合内网隔离的 Python 环境缺点是在调用piexif.insert时会用新的 EXIF 段整体替换旧段原缩略图IFD1偶尔会丢失。如果下游流程不需要原图缩略图影响不大但若交付对象是测绘院或需要进行几何校正缩略图缺失可能导致部分软件读取异常。pyexiv2 是对 C 库 exiv2 的封装读取和修改都走原生实现支持的标签范围远超 piexif包括 XMP 里的自定义字段。Pix4D、大疆智图等软件的工程文件里常见 XMP 坐标标签纯 Python 方案无法处理。代价是安装包体积较大且在部分 Linux 服务器上需要额外确认libexiv2动态库存在。两者不是互斥关系实际项目中常见做法是先使用 exifread 做快速遍历筛出缺失坐标的影像再用 pyexiv2 批量重写。3. 方式一exifread piexif 实现 JPG 图片 GPS 坐标读写3.1 用 exifread 读取航拍 JPG 里的 GPS 坐标并转十进制exifread 按标签处理整个 EXIF 段不需要额外安装底层库。读取时以二进制方式打开文件用process_file解析。下面代码把 GPS 标签解析成十进制经纬度import exifread def gps_to_decimal(ratio_list): d ratio_list[0][0] / ratio_list[0][1] m ratio_list[1][0] / ratio_list[1][1] s ratio_list[2][0] / ratio_list[2][1] return d m / 60 s / 3600 def read_jpg_gps(path): with open(path, rb) as f: tags exifread.process_file(f, detailsTrue) if GPS GPSLatitude not in tags or GPS GPSLongitude not in tags: return None lat gps_to_decimal(tags[GPS GPSLatitude].values) lon gps_to_decimal(tags[GPS GPSLongitude].values) if GPS GPSLatitudeRef in tags and tags[GPS GPSLatitudeRef].values S: lat -lat if GPS GPSLongitudeRef in tags and tags[GPS GPSLongitudeRef].values W: lon -lon return lat, lon这段代码有三个细节值得说。第一process_file的detailsTrue参数让返回值里保留Ratio对象而不是转换为浮点避免精度截断。第二tags[GPS GPSLatitude]是标签对象.values才是列表列表中每个元素是(分子, 分母)的元组。第三南北纬和东西经的判断不是看数值正负而是看Ref标签S表示南纬W表示西经必须在数值上取负否则南半球和西半球影像会落到错误位置。3.2 用 piexif 把经纬度写入 GPS IFD 并回写文件写入时要把十进制浮点先反向转换成度分秒分数再填充到 GPS IFD 中。piexif 的GPSIFD枚举定义了各个标签键名与 EXIF 标准一致。import piexif def decimal_to_dms(dec): is_positive dec 0 dec abs(dec) d int(dec) m_float (dec - d) * 60 m int(m_float) s_float (m_float - m) * 60 s round(s_float, 2) return is_positive, ((d, 1), (m, 1), (int(s * 100), 100)) lng 116.391 lat 39.907 lng_positive, lng_dms decimal_to_dms(lng) lat_positive, lat_dms decimal_to_dms(lat) gps_ifd { piexif.GPSIFD.GPSVersionID: (2, 3, 0, 0), piexif.GPSIFD.GPSLatitudeRef: bN if lat_positive else bS, piexif.GPSIFD.GPSLatitude: lat_dms, piexif.GPSIFD.GPSLongitudeRef: bE if lng_positive else bW, piexif.GPSIFD.GPSLongitude: lng_dms, piexif.GPSIFD.GPSAltitudeRef: 0, piexif.GPSIFD.GPSAltitude: (1234, 1), } exif_dict piexif.load(path) exif_dict[GPS] gps_ifd piexif.insert(piexif.dump(exif_dict), path)decimal_to_dms里秒值做了四舍五入并乘 100 作为分子分母固定 100可以保证秒的两位小数精度。GPS IFD 中每个标签的类型不能写错GPSLatitudeRef是 ASCII 类型必须传字节串bN而不是字符串GPSLatitude是三个 Rational 组成的元组每个 Rational 又是(分子, 分母)元组。piexif.load会把原文件里的全部 EXIF 内容读进来修改后再整体写回所以之前已有的镜头参数、拍摄时间等标签不会丢。3.3 给没有 GPS 信息的 JPG 补写坐标位置无人机没有连接 RTK或者文件从压缩软件中转出时丢失了定位段这类照片就需要手工补写。判断照片是否已有 GPS 信息直接看exif_dict[GPS]是否为空字典即可exif_dict piexif.load(path) if not exif_dict.get(GPS): exif_dict[GPS] gps_ifd piexif.insert(piexif.dump(exif_dict), path) else: print(已存在 GPS 标签跳过或覆盖)实际交付时补写坐标的数据源通常是 POS 文件或航线记录而不是全图统一值。这种情况下逐张构造gps_ifd的成本会变得很高每张图都要做一次度分秒转换和写入。还有一个容易被忽略的问题piexif.insert重写整个文件会在原目录生成临时副本再替换磁盘剩余空间不足时会写了一半失败导致原始文件损坏。批量补写前先估算单张文件大小乘以数量确认磁盘有 1.2 倍以上余量再执行。提示写入后如果照片在 Windows 照片查看器里无法打开多半是GPSVersionID没写。这个字节序列必须存在部分解码器把它当作 GPS IFD 的版本校验。4. 方式二pyexiv2 批量处理航拍影像 GPS 坐标的读写4.1 为什么大批量处理时切换 pyexiv2当照片数量达到数百张逐张piexif.load和dump的耗时差异会积累到不可忽略的程度。pyexiv2 的读写操作直接由原生库完成省去 Python 层对每段 EXIF 字节的逐个解析。但这并不是切换到 pyexiv2 的唯一理由。更实际的原因是缩略图保留。piexif 在insert时虽然保留了 IFD0 主标签但对缩略图段的处理并不稳定pyexiv2 通过 exiv2 内核维护缩略图与主 EXIF 的关联修改坐标后缩略图仍然可显示。另外航测后期会遇到带 XMP 坐标的照片这类附加信息属于另一个命名空间piexif 完全不支持pyexiv2 可以同时读写 EXIF 和 XMP 标签。对于需要把 POS 坐标批量同步进影像的交付流程pyexiv2 更加省事。4.2 pyexiv2 读写单张 JPG 坐标的最小代码pyexiv2 的标签名是完整路径格式GPS 相关标签以Exif.GPSInfo.开头。读取示例import pyexiv2 def read_gps_pyexiv2(path): with pyexiv2.Image(path) as img: exif img.read_exif() lat exif.get(Exif.GPSInfo.GPSLatitude, ) lat_ref exif.get(Exif.GPSInfo.GPSLatitudeRef, N) lon exif.get(Exif.GPSInfo.GPSLongitude, ) lon_ref exif.get(Exif.GPSInfo.GPSLongitudeRef, E) if not lat or not lon: return None def parse_fraction(s): parts s.split( ) return sum(int(p.split(/)[0]) / int(p.split(/)[1]) for p in parts) lat_decimal parse_fraction(lat) lon_decimal parse_fraction(lon) if lat_ref S: lat_decimal -lat_decimal if lon_ref W: lon_decimal -lon_decimal return lat_decimal, lon_decimal写入时转换函数需要生成 pyexiv2 规定的字符串格式。把十进制度转成度/1 分/1 秒/100的字符串填入对应标签def decimal_to_fraction_str(dec): dec abs(dec) d int(dec) m_float (dec - d) * 60 m int(m_float) s_float (m_float - m) * 60 s round(s_float, 2) return f{d}/1 {m}/1 {int(s * 100)}/100 def write_gps_pyexiv2(path, lat, lng, alt0.0): lat_str decimal_to_fraction_str(lat) lng_str decimal_to_fraction_str(lng) with pyexiv2.Image(path) as img: img.modify_exif({ Exif.GPSInfo.GPSLatitudeRef: N if lat 0 else S, Exif.GPSInfo.GPSLatitude: lat_str, Exif.GPSInfo.GPSLongitudeRef: E if lng 0 else W, Exif.GPSInfo.GPSLongitude: lng_str, Exif.GPSInfo.GPSAltitude: f{alt:.1f}/10, })modify_exif接收的是字典键是 EXIF 标签全名值是字符串。pyexiv2 内部会处理类型转换比 piexif 省去手动区分字节串和 Rational 元组的步骤。注意GPSAltitude这里传的是123.4/10形式分母为 10内容输出后是 12.34 米。with语句保证图片对象正常关闭避免文件句柄泄露。4.3 批量重写把 POS 文件里的坐标逐张写进 JPG实际航拍任务中GPS 坐标通常来自 POS 设备并保存在单独的文本或 CSV 文件里需要和 JPG 文件名匹配后写入。批量处理的核心代码框架import glob import os import pyexiv2 # pos_map 键为文件名值为 (lat, lon, alt) pos_map { DJI_00123.JPG: (39.907, 116.391, 120.5), DJI_00124.JPG: (39.908, 116.392, 121.0), } missed [] for jpg_path in glob.glob(rE:\drone\flight_a\*.JPG): name os.path.basename(jpg_path) if name not in pos_map: missed.append(name) continue lat, lng, alt pos_map[name] try: with pyexiv2.Image(jpg_path) as img: img.modify_exif({ Exif.GPSInfo.GPSLatitudeRef: N if lat 0 else S, Exif.GPSInfo.GPSLatitude: decimal_to_fraction_str(lat), Exif.GPSInfo.GPSLongitudeRef: E if lng 0 else W, Exif.GPSInfo.GPSLongitude: decimal_to_fraction_str(lng), Exif.GPSInfo.GPSAltitude: f{alt:.1f}/10, }) except Exception as e: print(f{jpg_path} 写入失败: {e}) print(f未匹配到POS的照片: {missed})这段代码里所有写入操作都在try内原因是写入中途崩溃会直接损坏 JPG 文件。pos_map是手工构造的示例实际使用时建议先用脚本解析 POS 文件按文件名建立映射关系。还存在一种常见情况POS 文件名与 JPG 文件名后缀大小写不一致例如 POS 里是.jpg而磁盘上是.JPG匹配时统一转为小写再做判断。写入失败的照片单独收集到missed列表方便二次排查。5. 进阶GPS 坐标换算精度、验证方法与 4 个常见坑5.1 度分秒与十进制互转的精度控制EXIF 中经纬度的有效位数直接决定落点精度。以纬度为例1 度约等于 111.32 千米0.00001 度约为 1.11 米0.0000001 度约为 1.1 厘米。写入时可对照下表选择秒的分母。秒的分母秒最小单位对应地面距离1000.01 秒约 0.31 米10000.001 秒约 0.03 米100000.0001 秒约 0.003 米分母超过 10000 没有实际意义因为普通 GPS 接收机的原始误差在 1 到 3 米量级RTK 数据才达到厘米级。转换时建议对最后一位做四舍五入而不是直接截断截断会让写入后的坐标与原始 POS 值产生系统性偏差。5.2 三个方法验证写入结果写入后不能只看程序退出码要通过独立路径回读确认。最简单的方式是用 pyexiv2 重新读取打印python -c import pyexiv2; dpyexiv2.Image(DJI_00123.JPG).read_exif(); print(d[Exif.GPSInfo.GPSLatitude], d[Exif.GPSInfo.GPSLongitude])如果机器上有 exiftool用它校验更直观。-n参数直接输出十进制坐标exiftool -GPSLatitude -GPSLongitude -GPSAltitude -n DJI_00123.JPG最终验证是把照片放进 GIS 软件在 ArcGIS 的目录视图中预览或拖入 QGIS 查看点是否落到预期区域。三个方法分别验证字节层、标签层和应用层任何一层不对下游生产流程都会出问题。5.3 最容易踩的四个坑坑表现规避方式漏写 GPSVersionID部分软件读不出坐标写入时固定(2,3,0,0)piexif.insert 丢失缩略图交付文件预览空白改用 pyexiv2坐标参考系写混GIS 中偏移数百米与 POS 原始数据对比写入中断损坏文件JPG 无法打开先复制到临时目录批量处理再单独提醒一个时间戳问题Exif.GPSInfo.GPSDateStamp和GPSTimeStamp标签不写也能定位但 Google 相册和部分影像管理平台会据此排序没写时间的航拍图会被归类到 1970 年。写入坐标时顺手把拍摄时间一起补上格式严格为YYYY:MM:DD HH:MM:SS不要把日期和时间用横线拼接。最后还有一个操作习惯值得养成批量改坐标前把原文件整体复制一份校验通过后再删除备份这比任何容错代码都可靠。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价