☰
Python+Google Earth+YOLOv9:卫星影像烟囱目标检测与坐标定位实战
2026/10/29 6:54:36 网站建设 项目流程

做环保、工业摸排和城市影像调查的人,应该都遇到过这种场景:手里拿到一个区域的经纬度范围,要尽快搞清楚里面有多少根烟囱、大概分布在哪。以前最土的办法就是打开地图软件,一块一块切图,再用眼睛挨个找,一天下来眼睛酸得不行,漏检率还挺高。后来我把两条技术线拼到了一起——用Python自动从Google Earth抓取指定区域的卫星图像,再接上YOLOv9做个烟囱目标检测模型,整条链路跑通之后,效率提升了明显一个档次。这篇文章把我的完整思路、代码细节、训练过程和踩过的坑都整理出来,希望能帮到做遥感检测、环保数据或者刚刚开始玩YOLO的朋友。

这套系统解决的核心问题很直接:输入一块区域的经纬度范围,输出一份带坐标、带置信度的烟囱检测清单。适合环保督查、大气污染溯源、碳排清单校验的工程师,也适合想做卫星影像目标检测落地的开发者。你只需要会基本的Python,顺着这篇内容就能把“地图取图—标注—训练—推理—坐标回写”整条链路搭起来。

1. 项目整体设计思路

1.1 核心需求拆解

我习惯在动手写代码之前,先把需求掰碎了看。这个项目表面上是“识别烟囱”,但往深里拆,其实是三件事。

第一,必须解决“图从哪里来”的问题。烟囱检测需要稳定、清晰的卫星影像,而且得能按经纬度批量拉取。你不能每次都打开地图软件手动截图,那样系统的自动化和可批量复制能力就是零。所以图像获取模块是整个系统的地基,这个模块不顺,后面全是空谈。

第二,需要一个能识别烟囱的目标检测模型。烟囱在卫星图上并不算特别复杂的视觉目标,但它有个特点:目标小、形态单一、容易和冷却塔、水塔这类东西混淆。所以模型的选择、训练策略、正负样本设计,都要围绕这个业务场景来调。

第三,检测结果必须落到地理坐标上。模型输出的是像素坐标的矩形框,但业务人员要的是“某根烟囱在这个区域的哪个经纬度”。这一步不处理好,系统就只能停留在跑demo的阶段,没法真正给业务用。

1.2 为什么是Google Earth加YOLOv9这个组合

图像源的选型,我当时对比过几类方案。一是直接买商业卫星影像,质量高,但按平方公里计费,项目前期探索成本太高;二是用天地图、高德这类公共服务,国内访问稳定,但部分区域影像更新频次和分辨率参差不齐;三是用Google Earth,整体影像分辨率高、色彩还原度稳定、覆盖广,个人学习研究和非商用项目完全够用,配合桌面客户端或者网页端可以做批量抓取的自动化。

模型选型的逻辑也类似。YOLO系列一直是目标检测里工程落地最顺的框架,社区活跃、权重文件小、推理速度快。YOLOv9相比之前的版本,在梯度信息保留机制上做了改进,小目标的检测能力有实打实的提升,而烟囱在卫星影像里恰恰就是占比很小的目标。再加上Ultralytics封装得很友好,训练和推理都几行代码搞定,不需要从零去搭一个检测网络。

1.3 系统整体工作流程

整个系统跑起来的流程,用一句话概括就是“坐标驱动闭环”。第一步,准备一个经纬度网格清单,比如你要查一块2公里乘2公里的区域,把它切成若干个小的网格块,每个网格块的中心点就是一个采样点。第二步,图像获取模块按这个清单批量抓取卫星图,图片保留含经纬度信息的命名。第三步,对图片做预处理,统一尺寸、增强对比度、去掉水印干扰。第四步,YOLOv9模型逐张检测,输出带置信度的烟囱框。第五步,把像素坐标通过换算关系转成经纬度,生成CSV、Excel或者KML格式的结果报告,方便直接在地图软件里打开核对。

这个流程看起来不复杂,但每一步都有不少细节,下面我按模块展开讲。

2. 图像获取:从地图上自动批量拿图的两条可行路线

2.1 路线对比

要做自动化拿图,网上常见的玩法大致分成两派。一派是控制Google Earth Pro桌面客户端,通过模拟鼠标键盘完成“跳转坐标—保存图像”的动作;另一派是打开网页版地图,用Selenium控浏览器截图。两条路线我都实测过,各有长短。

对比维度Google Earth Pro半自动路线Selenium网页地图路线
图像清晰度高,且可调整分辨率中等,受限于浏览器窗口和瓦片加载
自动化稳定性需要调试屏幕坐标,容易受弹窗影响相对稳定,只要页面加载逻辑固定
批量效率偏慢,单张3到5秒中等,单张2到4秒
使用门槛需要安装桌面客户端需要安装浏览器驱动
合规友好度适合研究学习,商业用途需自查条款同样需要注意地图服务使用条款

我个人建议:如果只是跑几百张小图快速验证模型,走Selenium路线,脚本写起来省心;如果要做正式的研究区域普查、对图质要求高,那就用Google Earth Pro桌面端,效果更可控。下面分别讲清具体怎么做。

2.2 方案A:Google Earth Pro半自动截图

这个方案的核心思路,是先把你需要采样的网格点做成一个KML文件,导入Google Earth Pro,然后用Python脚本模拟操作,循环完成“定位到采样点—设置视野高度—保存当前视图”的动作。

第一步,用Python生成采样网格和KML文件。假设你要查的区域经纬度范围是min_lat、min_lng到max_lat、max_lng,我一般按0.002度作为网格步长,大概是两百米左右,这样每张图覆盖范围不大不小,既不会让烟囱小到看不清,也不会因为单张图覆盖太大导致目标过密。

如果你不确定步长怎么定,可以用一个简单的换算关系:纬度方向上1度约等于111公里,0.002度就是222米。经度方向要根据纬度修正,但作为网格精度来说,直接按0.002度来切足够用。

生成网格的代码长这样:

import numpy as np def create_grid(min_lat, min_lng, max_lat, max_lng, step=0.002): lats = np.arange(min_lat, max_lat, step) lngs = np.arange(min_lng, max_lng, step) grid = [(round(float(lat), 6), round(float(lng), 6)) for lat in lats for lng in lngs] return grid def generate_kml(grid, kml_path): with open(kml_path, 'w', encoding='utf-8') as f: f.write('<?xml version="1.0" encoding="UTF-8"?>\n') f.write('<kml xmlns="http://www.opengis.net/kml/2.2">\n') f.write('<Document>\n') for idx, (lat, lng) in enumerate(grid): f.write(f''' <Placemark> <name>{idx}</name> <Point> <coordinates>{lng},{lat},0</coordinates> </Point> </Placemark>''') f.write('\n</Document>\n</kml>\n')

第二步,用pyautogui写控制脚本。核心动作只有几个:双击列表里的地标、等待地图跳转和瓦片渲染、打开保存图像对话框、填写文件名、保存。实际操作中,最稳妥的办法是不要硬编码屏幕坐标,而是用pyautogui的图片匹配功能,截图里找按钮图标,这样不同分辨率下也能用。

第三步是设置视野。保存图像前要把“视图高度”固定在一个值,比如300到400米,这样每张图的覆盖范围和空间分辨率大致一致,后续训练和坐标换算才不会乱套。图像尺寸建议调到1200乘1200以上,分辨率太低的话,烟囱在图上就只有几个像素,模型很难学。

这一步的自动化脚本,我很坦诚地说,稳定性大概七成,主要取决于Google Earth Pro客户端版本和系统环境,首次调试需要花一两个小时去对齐各种弹窗。

2.3 方案B:Selenium加网页地图截图

如果你不想折腾桌面软件,Selenium控网页是更轻量的选择。网页版地图支持带经纬度和缩放级别的URL,比如直接访问形如https://www.google.com/maps/@纬度,经度,18z的链接,浏览器就会定位到对应位置。你只需要用Selenium打开这个链接,等待地图加载完成,然后调用driver.save_screenshot()把整个页面截下来即可。

这里有个关键参数需要解释:URL结尾的缩放级别(z值)决定了图像空间分辨率。18级大概对应每像素0.5米左右,能看到比较清晰的建筑轮廓和烟囱阴影;19级更精细但抓图数量会翻倍,耗时也长。建议先在一小块区域测试,找到视觉最舒服的级别。

Selenuim截图有个细节容易忽略:要确保地图上不弹气泡框、不显示当前定位按钮这些干扰元素。我的做法是在截图前执行几段JavaScript,把地图上的浮动元素隐藏掉,再把浏览器窗口固定到一个统一尺寸,比如1280乘1280,这样所有截图的有效内容区域才一致。

from selenium import webdriver from selenium.webdriver.chrome.options import Options def fetch_map_image(lat, lng, zoom=18, save_path='map.png'): opts = Options() opts.add_argument('--headless') opts.add_argument('--window-size=1280,1280') opts.add_argument('--hide-scrollbars') driver = webdriver.Chrome(options=opts) url = f'https://www.google.com/maps/@{lat},{lng},{zoom}z' driver.get(url) driver.implicitly_wait(10) # 隐藏干扰元素 driver.execute_script(''' document.querySelectorAll('div[role="dialog"], .widget-overlay').forEach(el => el.style.display='none'); ''') driver.save_screenshot(save_path) driver.quit()

一个非常重要的提示:批量抓取地图影像,无论用哪种方式,都要提前确认该地图服务的使用条款,仅将结果用于个人学习、科学研究和非商业项目。如果项目要商业化落地,建议采购具有正规授权的影像数据源。

2.4 图像预处理:让模型吃得舒服

从地图直接抓回来的图是没法立刻喂给模型的,主要有三个问题要处理。

第一是水印和Logo。地图图的角落一般会带地图服务商的标识,这块区域如果直接进模型,会让模型误学一些无意义的特征。简单粗暴的办法是把底部区域固定裁剪掉。我通常会把整张图的下方8%裁掉,再统一缩放回模型输入尺寸。

第二是尺寸不统一。UI自动化的截图受窗口影响,不同机器上尺寸可能不一样。我的习惯是先把所有图统一缩放成1280乘1280,再按滑窗切分为640乘640的瓦片,如果目标在切片边缘被切断,再加一点overlap,比如每次滑动512像素,这样能有效减少烟囱被切歪导致漏检的概率。

第三是图像增强。光照、对比度、云影遮挡在不同图之间差异很大,适当做一点自适应直方图均衡化会有帮助。但要注意别太猛,过度增强会让图像失真,反而降低模型的泛化表现。

3. YOLOv9烟囱检测模型训练实战

3.1 数据集构建:标注规范和正负样本策略

很多人在训练检测模型时,急着堆数据量,忽略了数据质量。烟囱检测尤其如此,因为烟囱在卫星影像里的视觉特征非常明确,质量上来了,几百个框就能把模型训得不错。

数据来源就是我前面讲的图像获取模块,把不同区域、不同季节、不同建筑风格的图都抓一点回来。标注工具我推荐用LabelImg或者CVAT。我更喜欢CVAT,因为它支持多人协作和远程标注,团队项目里效率高很多。

标注的时候要定好标准,不然一个组员把整个冷却塔框进去,另一个组员只框烟囱的烟体,模型就学乱了。我的标准是:烟囱主体从基座顶部到烟囱口,矩形框必须完整包住烟囱柱体,可以带上一点阴影但不要把周围厂房建筑框进来。对于独立烟囱和一个厂区里并肩排着的多个烟囱,每个都单独标注。

负样本是这个项目里特别关键的一环。水塔、冷却塔、教堂尖顶、塔吊,甚至一些工业设施的阴影,在卫星影像上都可能跟烟囱撞脸。我建议专门抽几百张完全不含烟囱、但包含这些干扰物的图,放进数据集里作为负样本。YOLO本身支持“没有目标就不标注”的图片,它们在训练中能有效降低误检率。

标注导出格式要选YOLO的txt格式,每一行是“类别id 中心点x 中心点y 宽度 高度”,x、y、w、h都是相对图像尺寸归一化到0到1之间的小数。自己写脚本转化坐标时一定要小心,很容易在归一化时算错,标注文件错一丁点,模型就训练不出来。

3.2 训练环境与模型选择

硬件上,我自己用的是RTX 3090,如果你有24G显存,跑YOLOv9c模型、1280分辨率、batch 8到16没问题。显存不够也不用慌,可以把输入尺寸降到640,或者用更轻量的yolov9s权重,牺牲一点精度换训练速度。

软件环境很简单,核心依赖就几个:Python3.8以上、PyTorch、Ultralytics。安装命令:

pip install ultralytics torch torchvision

数据目录结构我建议保持Ultralytics的默认约定:

dataset/ ├── data.yaml ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/

data.yaml内容:

path: ./dataset train: images/train val: images/val names: 0: chimney

模型权重从YOLOv9的预训练权重开始,比从头训练收敛快得多。对于烟囱这类场景,预训练模型已经学会了通用的边缘、纹理特征,我们只是在这个基础上微调它的域适应能力。

3.3 训练命令与关键参数调整

Ultralytics封装好了完整的训练流程,命令式训练很简洁:

yolo detect train data=dataset/data.yaml model=yolov9c.pt epochs=200 imgsz=1280 batch=8 name=chimney_exp

几个关键参数我想单独解释一下,因为它们对最终效果影响很大。

imgsz是重中之重。烟囱在卫星图里属于小目标,如果你用640的图像尺寸,一个烟囱可能只有10乘50像素,特征太少,模型很容易漏检。我建议至少1280起步,有条件直接1536。代价是显存占用和训练时间都成倍增加,这是一个需要根据硬件条件来权衡的取舍。

epochs不用迷信越大越好。我用200轮左右,结合早停机制,一般50到100轮就差不多收敛了。Ultralytics默认开启了早停,patience设为50就行。

batch受显存限制,但不要为了放大batch去削减分辨率。batch 8加上梯度累积,效果通常比batch 2跑很多轮要稳。

训练的时候还有几个实战技巧:

  • 开启AMP混合精度训练,显存能省不少,速度也更快,对精度影响很小。
  • 数据增强默认开启了mosaic、翻转、HSV扰动这些,烟囱这种旋转不变性很强的目标,mosaic效果很好,不用关。
  • 建议把close_mosaic=10设一下,最后10轮关掉mosaic,让模型在接近真实分布的数据上做最后的收敛微调。

3.4 模型评估:不能只看mAP

训练完先看两个数字:mAP50和mAP50-95。前者是目标检测里最常用的粗指标,后者更严格,要求预测框和真实框重叠度很高才算对。烟囱检测场景,mAP50达到0.8以上可以用,0.9以上相当好了。

但业务上我更关心召回率,也就是“真实烟囱里我找到了多少”。环保这种场景,漏检一个烟囱的代价远超误报一个。误报了,后续人工复核能很快排除;漏检了,这个烟囱就彻底从清单里消失了。所以我调阈值时,倾向把confidence阈值压低,比如0.2左右,宁可多返回些候选框,让业务侧去二次筛查。

评估时还建议把PR曲线和混淆矩阵打出来看看。如果大量误检集中在“冷却塔”“圆柱形水塔”上,解决方案不是盲目加训练轮数,而是去补充对应的负样本重训。这里有个小经验:把模型推理出错的负样本图片收集起来,每一类误检收集几十张,重新放进训练集里迭代,比单纯增加正样本数据量提升更明显。

4. 检测集成与坐标回标

4.1 模型封装和批量推理

训练好的权重在runs/detect/chimney_exp/weights/best.pt。接下来把它封装成一个批量推理脚本,读取所有待检测图片,输出检测框信息。Ultralytics的Python接口写起来非常顺手:

from ultralytics import YOLO model = YOLO('runs/detect/chimney_exp/weights/best.pt') def detect_image(img_path, conf_thres=0.2): results = model.predict( img_path, conf=conf_thres, iou=0.45, imgsz=1280, verbose=False ) boxes = results[0].boxes detections = [] if boxes is not None: for box in boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() confidence = float(box.conf[0]) detections.append((x1, y1, x2, y2, confidence)) return detections

批量检测就套一个文件遍历:

import os, csv def batch_detect(img_dir, output_csv): rows = [] for img_name in os.listdir(img_dir): if not img_name.lower().endswith(('.png', '.jpg', '.jpeg')): continue img_path = os.path.join(img_dir, img_name) for x1, y1, x2, y2, conf in detect_image(img_path): rows.append([img_name, round(x1,2), round(y1,2), round(x2,2), round(y2,2), round(conf,4)]) with open(output_csv, 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['image', 'x1', 'y1', 'x2', 'y2', 'confidence']) writer.writerows(rows)

4.2 像素坐标到经纬度的换算

这是整个系统里技术含量不高、但最容易翻车的一步。原理其实就是一个比例换算:如果每张图覆盖的经纬度跨度是固定的,那像素坐标和经纬度就是线性关系。

假设你的图统一是1280乘1280,每张图覆盖0.002度乘0.002度的范围,图中心点的经纬度(lat, lng)就写在文件名里。那么目标框中心点的像素坐标,换算成经纬度可以用下面的公式:

def pixel_to_geo(center_px_x, center_px_y, img_size, center_lat, center_lng, span=0.002): top_lat = center_lat + span / 2 left_lng = center_lng - span / 2 lat = top_lat - (center_px_y / img_size) * span lng = left_lng + (center_px_x / img_size) * span return round(lat, 6), round(lng, 6)

这里有个前提条件必须提前保证:所有输入模型的图,要么直接来自同一个视野高度设置,要么做了严格的裁剪和缩放映射。如果图像获取阶段视野高度不一致,覆盖范围就不一致,那上面的线性模型就不成立,坐标换算就会差出几十米,这在业务上是不能接受的。

所以我在整个项目里反复强调:图像获取阶段的标准化比你想象的重要得多,它影响的不仅是训练效果,还直接影响最终坐标精度。

4.3 输出报告可视化

最后产出的报告,我一般做两份。一份是CSV或Excel,字段包含“烟囱编号、图像名、像素坐标、置信度、纬度、经度”。另一份是带空间参考的地图文件,推荐KML格式,直接在Google Earth或其他GIS软件里打开就能看到每个烟囱的位置标签。

生成KML的方法,我直接写入一个简单的XML文件:

def generate_kml_report(results, output_path): with open(output_path, 'w', encoding='utf-8') as f: f.write('<?xml version="1.0" encoding="UTF-8"?>\n') f.write('<kml xmlns="http://www.opengis.net/kml/2.2">\n') f.write('<Document>\n') for idx, row in enumerate(results): lat, lng = row['lat'], row['lng'] f.write(f''' <Placemark> <name>Chimney-{idx:04d}</name> <description>conf:{row['confidence']}</description> <Point> <coordinates>{lng},{lat},0</coordinates> </Point> </Placemark>''') f.write('\n</Document>\n</kml>\n')

这一步做完了,整个从图像获取到结果展示的闭环就打通了。业务人员拿到KML,直接导入地图软件,就可以对照影像做人工复核。

5. 实战中踩过的坑与排查记录

5.1 图像获取阶段的问题速查

图像获取阶段看似简单,实际坑最多。我整理了一个速查表:

现象原因解决方案
图片模糊、细节不够视野高度设得太高降低视图高度到300到400米,或调高URL缩放级别
图片色调差异大不同时段瓦片光照不一致在预处理阶段做对比度归一化和白平衡
每张图覆盖范围不一致视野高度设置被改动脚本里固定视野高度,图像命名时记录坐标和高度
截图带大量多余界面元素UI自动化时窗口布局变化改用图片匹配定位,或切换到Selenium无头模式
某几张图瓦片没加载完就截图网络延迟造成空白区域增加显式等待,检测地图瓦片容器加载数量再做判断

我记得调试UI自动化的时候,出现过最离谱的一个问题:在脚本里循环到某个坐标点时,Google Earth Pro突然弹了一个“版本升级”的提示框,整个脚本就卡死在那里,一晚上白跑。后来我养成了两个习惯,一是启动前先手动关掉所有弹窗设置,二是脚本里每隔几步就截一张屏,出问题时能快速定位卡在哪一步。

5.2 训练和推理阶段的问题速查

现象原因解决方案
mAP不错但实际漏检多训练时图像尺寸小,目标太小提高imgsz到1280以上,或改用滑窗切片推理
大量把冷却塔识别成烟囱负样本不足专门收集冷却塔图片加入训练
阴影误检严重烟囱阴影特征和烟囱本体混淆增加带长阴影目标作为负样本,或降低confidence阈值后人工筛
显存不够batch过大或分辨率过高降batch、开AMP、用模型EMA省显存
训练输出NaN学习率过高或数据里有空标注按错误信息调低学习率,检查labels目录是否有空文件
推理大图很慢图像尺寸过大,模型一次处理时间长切块推理,用多线程并发处理

关于滑窗切片,我再多说一句。一张1280乘1280的图直接送进模型,如果显卡一般,推理速度会比较尴尬。我建议在推理阶段把大图按640乘640的窗口切成四块,每块做一个部分重叠,再合并结果。切块推理不仅解决了显存问题,还能让模型看得更清楚,对提高小目标召回有正向帮助。

5.3 给后来者的实用建议

如果让我对一个想复刻这个系统的新手说几句实在话,我会提四点。

第一,先花半天时间跑通最小的闭环。不要一上来就想全自动搞定几百平方公里的大区域。先拿五张图,手动下载、手动标注二十个烟囱、训一个小模型、手动推理,把这个链条里所有环节都摸一遍,再谈规模化。

第二,数据的标准化程度决定了系统上限。图像获取阶段宁可慢一点,也要保证视野高度统一、覆盖范围一致、命名规则清晰。我见过很多项目死在“数据不标准”上,模型调参调得再猛也没用。

第三,模型训练不需要追求“一步到位”。先用一个小数据集把代码流程跑通,再逐步扩充数据量。不要第一次训练就想搞一个几千张图的大工程,一旦代码或标注格式出问题,返工成本非常高。

第四,业务端一定要保留人工复核环节。自动检测的目标是帮人从几百张图里筛出候选点,而不是替代人做最终决策。影像检测必然有误差,设计系统时就该把人工复核纳入工作流。

另外要特别强调一句合规问题:批量获取地图影像务必要遵守对应地图服务的使用条款。个人学习、科研用途通常没问题,但任何商业用途都要提前确认授权范围,最好直接采购正规的商业影像服务,这个钱不能省。

最后还是想聊一点个人体会。刚开始做这个项目的时候,我一度纠结于模型结构怎么改、超参数怎么调,总觉得识别精度不够是模型的问题。但真正跑完整个系统以后我才意识到,对烟囱检测这类业务场景,数据链路和工程规范带来的提升,远远大于模型本身的小改动。把取图、标注、训练、换算、可视化整条链路理顺,让每一步都稳定复用,这才是这类系统能真正落地的关键。如果你后续想扩展,可以把这套框架套到其他小目标识别上,比如光伏板、违规建筑、裸土堆场,核心方法论是通用的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询