一、应用场景
在地理信息系统(GIS)、摄影测量、环境监测、考古研究等领域,常常需要从大量图片中提取 GPS 经纬度信息并进行分析。例如:
- 地理标记照片管理:摄影师需要整理带有位置信息的照片
- 环境监测:通过无人机拍摄的照片定位污染点或植被变化区域
- 考古研究:记录文物发现位置的照片
- 旅行日志:根据旅行照片生成行程路线图
二、界面设计
设计一个简洁易用的图形界面,包含以下元素:
- 文件选择区域:支持拖放或点击选择图片文件夹
- 处理进度显示:显示已处理图片数量和总数量
- 结果预览表格:实时显示提取的 GPS 信息
- 导出功能:将结果保存为 CSV 或 Excel 表格
- 日志区域:显示处理过程中的错误或提示信息
下面是一个实现该功能的完整代码:
import os
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
import pandas as pd
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
import threading
import queue
from datetime import datetime
class GPSExtractorApp:
def __init__(self, root):
self.root = root
self.root.title("图片GPS信息提取工具")
self.root.geometry("900x600")
self.root.minsize(800, 500)
# 设置中文字体
self.style = ttk.Style()
self.style.configure("TLabel", font=("SimHei", 10))
self.style.configure("TButton", font=("SimHei", 10))
self.style.configure("Treeview", font=("SimHei", 10))
# 创建队列用于线程间通信
self.queue = queue.Queue()
# 创建界面
self.create_widgets()
def create_widgets(self):
# 主框架
main_frame = ttk.Frame(self.root, padding=10)
main_frame.pack(fill=tk.BOTH, expand=True)
# 文件选择区域
file_frame = ttk.LabelFrame(main_frame, text="选择图片文件夹", padding=10)
file_frame.pack(fill=tk.X, pady=(0, 10))
self.folder_path = tk.StringVar()
folder_entry = ttk.Entry(file_frame, textvariable=self.folder_path, width=50)
folder_entry.pack(side=tk.LEFT, padx=(0, 10), fill=tk.X, expand=True)
browse_btn = ttk.Button(file_frame, text="浏览...", command=self.browse_folder)
browse_btn.pack(side=tk.LEFT)
process_btn = ttk.Button(file_frame, text="开始处理", command=self.start_processing)
process_btn.pack(side=tk.RIGHT)
# 进度条区域
progress_frame = ttk.Frame(main_frame, padding=(0, 5))
progress_frame.pack(fill=tk.X)
self.progress_var = tk.DoubleVar()
self.progress_label = ttk.Label(progress_frame, text="准备就绪")
self.progress_label.pack(side=tk.LEFT)
self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100)
self.progress_bar.pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=(10, 0))
# 结果表格区域
result_frame = ttk.LabelFrame(main_frame, text="提取结果", padding=10)
result_frame.pack(fill=tk.BOTH, expand=True, pady=(10, 0))
# 创建表格
columns = ("filename", "latitude", "longitude", "altitude", "datetime")
self.tree = ttk.Treeview(result_frame, columns=columns, show="headings")
# 设置列标题
self.tree.heading("filename", text="文件名")
self.tree.heading("latitude", text="纬度")
self.tree.heading("longitude", text="经度")
self.tree.heading("altitude", text="海拔")
self.tree.heading("datetime", text="拍摄时间")
# 设置列宽
self.tree.column("filename", width=200)
self.tree.column("latitude", width=120)
self.tree.column("longitude", width=120)
self.tree.column("altitude", width=80)
self.tree.column("datetime", width=150)
self.tree.pack(fill=tk.BOTH, expand=True)
# 导出按钮
export_frame = ttk.Frame(main_frame, padding=(0, 10))
export_frame.pack(fill=tk.X)
export_btn = ttk.Button(export_frame, text="导出到CSV", command=self.export_to_csv)
export_btn.pack(side=tk.RIGHT)
# 日志区域
log_frame = ttk.LabelFrame(main_frame, text="处理日志", padding=10)
log_frame.pack(fill=tk.BOTH, expand=True, pady=(10, 0))
self.log_text = tk.Text(log_frame, height=5, state=tk.DISABLED)
self.log_text.pack(fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(self.log_text, command=self.log_text.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.log_text.config(yscrollcommand=scrollbar.set)
def browse_folder(self):
folder_selected = filedialog.askdirectory()
if folder_selected:
self.folder_path.set(folder_selected)
self.log("已选择文件夹: " + folder_selected)
def log(self, message):
self.log_text.config(state=tk.NORMAL)
self.log_text.insert(tk.END, message + "\n")
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
def start_processing(self):
folder = self.folder_path.get()
if not folder or not os.path.isdir(folder):
messagebox.showerror("错误", "请选择有效的文件夹")
return
# 清空表格和日志
for item in self.tree.get_children():
self.tree.delete(item)
self.log_text.config(state=tk.NORMAL)
self.log_text.delete(1.0, tk.END)
self.log_text.config(state=tk.DISABLED)
# 启动处理线程
self.processing_thread = threading.Thread(target=self.process_images, args=(folder,))
self.processing_thread.daemon = True
self.processing_thread.start()
# 开始轮询队列
self.root.after(100, self.poll_queue)
def process_images(self, folder):
# 获取所有图片文件
image_extensions = ['.jpg', '.jpeg', '.png', '.tiff']
image_files = []
for root, dirs, files in os.walk(folder):
for file in files:
if os.path.splitext(file)[1].lower() in image_extensions:
image_files.append(os.path.join(root, file))
total = len(image_files)
if total == 0:
self.queue.put(("log", "未找到图片文件"))
self.queue.put(("progress", 0, "准备就绪"))
return
self.queue.put(("log", f"找到 {total} 个图片文件"))
self.queue.put(("progress", 0, f"开始处理 {total} 个文件"))
results = []
# 处理每个图片文件
for i, image_path in enumerate(image_files):
try:
gps_info = self.get_gps_info(image_path)
if gps_info:
filename = os.path.basename(image_path)
results.append({
"filename": filename,
"latitude": gps_info["latitude"],
"longitude": gps_info["longitude"],
"altitude": gps_info.get("altitude", "N/A"),
"datetime": gps_info.get("datetime", "N/A")
})
self.queue.put(("add_item", filename, gps_info["latitude"],
gps_info["longitude"], gps_info.get("altitude", "N/A"),
gps_info.get("datetime", "N/A")))
else:
self.queue.put(("log", f"警告: {os.path.basename(image_path)} 不包含GPS信息"))
except Exception as e:
self.queue.put(("log", f"错误: 处理 {os.path.basename(image_path)} 时出错 - {str(e)}"))
progress = (i + 1) / total * 100
self.queue.put(("progress", progress, f"已处理 {i+1}/{total}"))
# 保存结果到实例变量
self.results = results
self.queue.put(("log", f"处理完成,共提取 {len(results)} 条GPS信息"))
self.queue.put(("progress", 100, "处理完成"))
def get_gps_info(self, image_path):
try:
with Image.open(image_path) as img:
exif_data = img._getexif()
if not exif_data:
return None
# 解析EXIF数据
exif = {}
for tag, value in exif_data.items():
tag_name = TAGS.get(tag, tag)
exif[tag_name] = value
# 检查是否有GPS信息
if 'GPSInfo' not in exif:
return None
# 解析GPS信息
gps_info = {}
for gps_tag, value in exif['GPSInfo'].items():
gps_tag_name = GPSTAGS.get(gps_tag, gps_tag)
gps_info[gps_tag_name] = value
# 提取经纬度
if ('GPSLatitude' in gps_info and 'GPSLatitudeRef' in gps_info and
'GPSLongitude' in gps_info and 'GPSLongitudeRef' in gps_info):
# 转换经纬度格式
lat = self.convert_to_degrees(gps_info['GPSLatitude'])
if gps_info['GPSLatitudeRef'] == 'S':
lat = -lat
lon = self.convert_to_degrees(gps_info['GPSLongitude'])
if gps_info['GPSLongitudeRef'] == 'W':
lon = -lon
result = {
"latitude": lat,
"longitude": lon
}
# 提取海拔
if 'GPSAltitude' in gps_info:
result["altitude"] = gps_info['GPSAltitude']
# 提取拍摄时间
if 'DateTimeOriginal' in exif:
result["datetime"] = exif['DateTimeOriginal']
return result
return None
except Exception as e:
raise Exception(f"无法获取GPS信息: {str(e)}")
def convert_to_degrees(self, value):
"""将GPS坐标从度分秒格式转换为十进制度数"""
d = float(value[0][0]) / float(value[0][1])
m = float(value[1][0]) / float(value[1][1])
s = float(value[2][0]) / float(value[2][1])
return d + (m / 60.0) + (s / 3600.0)
def poll_queue(self):
while not self.queue.empty():
try:
msg = self.queue.get(0)
if msg[0] == "log":
self.log(msg[1])
elif msg[0] == "progress":
self.progress_var.set(msg[1])
self.progress_label.config(text=msg[2])
elif msg[0] == "add_item":
self.tree.insert("", tk.END, values=msg[1:])
except queue.Empty:
pass
# 继续轮询队列,直到处理完成
if hasattr(self, 'processing_thread') and self.processing_thread.is_alive():
self.root.after(100, self.poll_queue)
def export_to_csv(self):
if not hasattr(self, 'results') or not self.results:
messagebox.showinfo("提示", "没有可导出的数据")
return
file_path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")]
)
if file_path:
try:
df = pd.DataFrame(self.results)
df.to_csv(file_path, index=False, encoding="utf-8-sig")
messagebox.showinfo("成功", f"数据已成功导出到 {file_path}")
self.log(f"数据已导出到 {file_path}")
except Exception as e:
messagebox.showerror("错误", f"导出失败: {str(e)}")
self.log(f"导出失败: {str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = GPSExtractorApp(root)
root.mainloop()
三、详细代码步骤
导入必要的库:
- 使用
os
和tkinter
构建图形界面 pandas
处理表格数据PIL
库读取图片 EXIF 信息threading
和queue
实现多线程处理
- 使用
创建主应用类:
- 初始化界面组件,包括文件选择、进度条、结果表格和日志区域
- 设置中文字体确保界面正常显示
实现图片处理功能:
- 遍历指定文件夹中的所有图片文件
- 使用 PIL 库读取图片 EXIF 信息
- 解析 GPS 信息,包括经纬度、海拔和拍摄时间
- 将度分秒格式的经纬度转换为十进制格式
多线程处理:
- 使用单独的线程处理图片,避免界面卡顿
- 通过队列在主线程和处理线程之间通信
- 实时更新进度条和日志信息
结果展示与导出:
- 在表格中显示提取的 GPS 信息
- 支持将结果导出为 CSV 格式文件
这个工具可以满足基本的图片 GPS 信息提取需求,通过上述优化可以使其更加健壮和功能丰富。