GeoDeep 在 Maxar 卫星图像上的 AI 检测应用
Mark Litwintschik
我拥有 15 年的咨询和实践经验,服务于英国、美国、瑞典、爱尔兰和德国的客户。之前的客户包括 Bank of America Merrill Lynch, Blackberry, Bloomberg, British Telecom, Ford, Google, ITV, LeoVegas, News UK, Pizza Hut, Royal Mail, T-Mobile, Williams Formula 1, Wise & UBS。 我拥有加拿大和英国护照。 我的简历, Twitter & LinkedIn. 首页 | 基准测试 | 分类 | Atom Feed 发布于 2025 年 4 月 10 日星期四,归类于 人工智能
GeoDeep 在 Maxar 卫星图像上的 AI 检测应用
GeoDeep 是一个用于检测卫星图像中物体的 Python 包。它由 1,026 行 Python 代码组成,并大量使用了 ONNX Runtime 和 Rasterio。
GeoDeep 由 Piero Toffanin 编写,他位于佛罗里达,是 OpenDroneMap 的联合创始人。他还编写了 LibreTranslate,我之前在一篇文章中介绍过。
Maxar 是一家卫星制造商和星座运营商。他们运营一个 开放数据项目,并且经常发布自然灾害发生前后区域的图像。以下是他们迄今为止发布的位置。
3 月 28 日,缅甸发生地震,远至泰国曼谷都有震感。Spyridon Staridas 是一位位于希腊的制图师,他制作了这张世界各地地震历史地图。泰国被地震多发国家包围,但与亚洲其他地区相比,泰国发生地震的情况相对较少。
地震发生后不久,Maxar 发布了受影响地区的历史卫星图像,后来又加入了地震后拍摄的图像。截至本文撰写时,他们已经发布了近 10 GB 的 GeoTIFF 文件。
以下是缅甸中部的影像覆盖范围。影像时间跨度从 2 月 2 日到 4 月初。
以下是曼谷的卫星影像覆盖范围。
在这篇文章中,我将在 Maxar 提供的缅甸和泰国曼谷的卫星图像上运行 GeoDeep 的一些内置 AI 模型。
我的工作站
我使用的是 5.7 GHz 的 AMD Ryzen 9 9950X CPU。它有 16 个核心和 32 个线程,以及 1.2 MB 的 L1 缓存、16 MB 的 L2 缓存和 64 MB 的 L3 缓存。它连接了一个液体冷却器,并安装在一个宽敞的全尺寸 Cooler Master HAF 700 电脑机箱中。
该系统具有 96 GB 的 DDR5 RAM,时钟频率为 4,800 MT/s,以及一个第五代 Crucial T700 4 TB NVMe M.2 SSD,其读取速度高达 12,400 MB/s。SSD 上有一个散热器,以帮助降低其温度。这是我系统的 C 盘。
该系统由一个 1,200 瓦的全模块化 Corsair 电源供电,并安装在 ASRock X870E Nova 90 主板上。
我通过 Microsoft 的 Ubuntu for Windows 在 Windows 11 Pro 上运行 Ubuntu 24 LTS。如果您想知道为什么我不使用基于 Linux 的桌面作为我的主要工作环境,那是因为我仍然在使用 Nvidia GTX 1080 GPU,它在 Windows 上具有更好的驱动程序支持,而且我时不时会使用 ArcGIS Pro,它只原生支持 Windows。
安装先决条件
我将使用 Python 3.12.3 和 jq 来帮助分析这篇文章中的数据。
$sudoadd-apt-repositoryppa:deadsnakes/ppa
$sudoaptupdate
$sudoaptinstall\
jq\
python3-pip\
python3.12-venv
我将设置一个 Python 虚拟环境并安装最新的 GeoDeep 版本。
$python3-mvenv~/.geodeep
$source~/.geodeep/bin/activate
$python3-mpipinstall\
geodeep
我将在此文章中使用 DuckDB,以及它的 H3, JSON, Lindel, Parquet 和 Spatial 扩展。
$cd~
$wget-chttps://github.com/duckdb/duckdb/releases/download/v1.1.3/duckdb_cli-linux-amd64.zip
$unzip-jduckdb_cli-linux-amd64.zip
$chmod+xduckdb
$~/duckdb
INSTALLh3FROMcommunity;
INSTALLlindelFROMcommunity;
INSTALLjson;
INSTALLparquet;
INSTALLspatial;
我将设置 DuckDB,使其每次启动时都加载每个已安装的扩展。
$vi~/.duckdbrc
.timer on
.width 180
LOAD h3;
LOAD lindel;
LOAD json;
LOAD parquet;
LOAD spatial;
这篇文章中的地图是用 QGIS 3.42 版本渲染的。QGIS 是一个桌面应用程序,可在 Windows、macOS 和 Linux 上运行。该应用程序近年来越来越受欢迎,每月有来自世界各地用户的约 1500 万次应用程序启动。
我使用 QGIS 的 Tile+ 插件 添加了地理空间上下文,其中使用了 OpenStreetMap (OSM) 的底图瓦片以及 CARTO 的地图。
上面 Maxar 图像位置的深色非卫星地图主要由来自 Natural Earth 和 Overture 的矢量数据组成。
我使用了 Kaggle 上的这个 GeoJSON 文件 来勾勒出曼谷的各个区。
我在这篇文章中使用了 EPSG:32647 作为地图投影。以下是 QGIS 对此投影的概述。
Maxar 的曼谷卫星图像
我将下载 Maxar 的一张曼谷图像及其元数据。
$wget-Oard_47_122022102202_2025-02-14_10400100A39C6A00-visual.tif\
https://maxar-opendata.s3.amazonaws.com/events/Earthquake-Myanmar-March-2025/ard/47/122022102202/2025-02-14/10400100A39C6A00-visual.tif
$wgethttps://raw.githubusercontent.com/opengeos/maxar-open-data/refs/heads/master/datasets/Earthquake-Myanmar-March-2025/10400100A39C6A00_union.geojson
该图像是一个 17408 x 17408 像素、62 MB 的 GeoTIFF 文件。它的覆盖范围为 5.3 公里 x 3.7 公里,捕捉了曼谷 Bang Sue 区西北部的一个区域。以下是该图像与曼谷其他地区的关系。
以下是该图像的放大版。
以下是该图像的元数据。它是在当地时间 2 月 14 日上午 11:02 拍摄的。该图像没有任何云层覆盖。
$jq-S.features[0].properties\
10400100A39C6A00_union.geojson
{
"ard_metadata_version":"0.0.1",
"catalog_id":"10400100A39C6A00",
"data-mask":"https://maxar-opendata.s3.amazonaws.com/events/Earthquake-Myanmar-March-2025/ard/47/122022102202/2025-02-14/10400100A39C6A00-data-mask.gpkg",
"datetime":"2025-02-14T04:02:00Z",
"grid:code":"MXRA-Z47-122022102202",
"gsd":0.35,
"ms_analytic":"https://maxar-opendata.s3.amazonaws.com/events/Earthquake-Myanmar-March-2025/ard/47/122022102202/2025-02-14/10400100A39C6A00-ms.tif",
"pan_analytic":"https://maxar-opendata.s3.amazonaws.com/events/Earthquake-Myanmar-March-2025/ard/47/122022102202/2025-02-14/10400100A39C6A00-pan.tif",
"platform":"WV03",
"proj:bbox":"659843.75,1529843.75,665156.25,1533587.620632182",
"proj:code":"EPSG:32647",
"proj:geometry":{
"coordinates":[
[
[
665156.25,
1529843.75
],
[
659843.75,
1529843.75
],
[
659843.75,
1533553.3890408671
],
[
665156.25,
1533587.6206321821
],
[
665156.25,
1529843.75
]
]
],
"type":"Polygon"
},
"quadkey":"122022102202",
"tile:clouds_area":0.0,
"tile:clouds_percent":0,
"tile:data_area":19.7,
"utm_zone":47,
"view:azimuth":262.1,
"view:incidence_angle":64.7,
"view:off_nadir":22.9,
"view:sun_azimuth":139.1,
"view:sun_elevation":55.2,
"visual":"https://maxar-opendata.s3.amazonaws.com/events/Earthquake-Myanmar-March-2025/ard/47/122022102202/2025-02-14/10400100A39C6A00-visual.tif"
}
检测模型
以下是 GeoDeep 打包的模型。这些模型列在 geodeep/models.py
中。为了清晰起见,我对该列表进行了排序。
MODELS = {
'aerovision': 'aerovision16-yolo8.onnx',
'birds': 'bird_detection_retinanet_deepforest.onnx',
'buildings': 'buildings_ramp_XUnet_256.onnx',
'cars': 'car_aerial_detection_yolo7_ITCVD_deepness.onnx',
'planes': 'model_yolov7_tiny_planes_256.onnx',
'roads': 'road_segmentation_model_with_metadata_26_10_22.onnx',
'trees': 'tree_crown_detection_retinanet_deepforest.onnx',
'trees_yolov9': 'yolov9_trees.onnx',
'utilities': 'utilities-811-yolo8.onnx',
}
首次使用这些模型时,它们将被下载到 ~/.cache/geodeep
文件夹中。
$ls-lhS~/.cache/geodeep
194M .. yolov9_trees.onnx
94M .. road_segmentation_model_with_metadata_26_10_22.onnx
79M .. buildings_ramp_XUnet_256.onnx
32M .. tree_crown_detection_retinanet_deepforest.onnx
24M .. car_aerial_detection_yolo7_ITCVD_deepness.onnx
23M .. model_yolov7_tiny_planes_256.onnx
11M .. aerovision16-yolo8.onnx
可以使用以下命令检查模型。
$geodeep-inspectcars
det_type: YOLO_v5_or_v7_default
det_conf: 0.3
det_iou_thresh: 0.8
classes: []
seg_thresh: 0.5
seg_small_segment: 11
resolution: 10.0
class_names: {'0': 'car'}
model_type: Detector
tiles_overlap: 10.0
tiles_size: 640
input_shape: [1, 3, 640, 640]
input_name: images
以下命令将显示所有模型的概述。
$python3
import json
from geodeep.inference import create_session
from geodeep import models
from geodeep.models import get_model_file
with open('models.json', 'w') as f:
for model in models.list_models():
_, config = create_session(get_model_file(model))
config['model_name'] = model
f.write(json.dumps(config, sort_keys=True) + '\n')
$~/duckdb
SELECT*EXCLUDE(class_names),
class_names::TEXTASclass_names
FROMmodels.json
ORDERBYmodel_name;
┌─────────┬──────────┬────────────────┬──────────────────────┬────────────┬────────────────────┬──────────────┬────────────┬────────────┬───────────────────┬────────────┬───────────────┬────────────┬ class_names │
│ classes │ det_conf │ det_iou_thresh │ det_type │ input_name │ input_shape │ model_name │ model_type │ resolution │ seg_small_segment │ seg_thresh │ tiles_overlap │ tiles_size │ varchar │
│ json[] │ double │ double │ varchar │ varchar │ int64[] │ varchar │ varchar │ double │ int64 │ double │ double │ int64 │ varchar │
├─────────┼──────────┼────────────────┼──────────────────────┼────────────┼────────────────────┼──────────────┼────────────┼────────────┼───────────────────┼────────────┼───────────────┼────────────┼----------------------------------------------------┤
│ [] │ 0.3 │ 0.3 │ YOLO_v8 │ images │ [1, 3, 640, 640] │ aerovision │ Detector │ 30.0 │ 11 │ 0.5 │ 25.0 │ 640 │ {'0': small-vehicle, '1': large-vehicle, '10': b… │
│ [] │ 0.4 │ 0.4 │ retinanet │ images │ [1, 3, 1000, 1000] │ birds │ Detector │ 2.0 │ 11 │ 0.5 │ 5.0 │ 1000 │ {'0': bird, '1': NULL, '10': NULL, '11': NULL, '… │
│ [] │ 0.3 │ 0.8 │ YOLO_v5_or_v7_defa… │ input │ [1, 3, 256, 256] │ buildings │ Segmentor │ 50.0 │ 11 │ 0.5 │ 5.0 │ 256 │ {'0': Background, '1': Building, '10': NULL, '11… │
│ [] │ 0.3 │ 0.8 │ YOLO_v5_or_v7_defa… │ images │ [1, 3, 640, 640] │ cars │ Detector │ 10.0 │ 11 │ 0.5 │ 10.0 │ 640 │ {'0': car, '1': NULL, '10': NULL, '11': NULL, '1… │
│ [] │ 0.3 │ 0.3 │ YOLO_v5_or_v7_defa… │ images │ [1, 3, 256, 256] │ planes │ Detector │ 70.0 │ 11 │ 0.5 │ 5.0 │ 256 │ {'0': plane, '1': NULL, '10': NULL, '11': NULL, … │
│ [] │ 0.3 │ 0.8 │ YOLO_v5_or_v7_defa… │ input │ [1, 3, 512, 512] │ roads │ Segmentor │ 21.0 │ 11 │ 0.5 │ 15.0 │ 512 │ {'0': not_road, '1': road, '10': NULL, '11': NUL… │
│ [] │ 0.3 │ 0.4 │ retinanet │ images │ [1, 3, 400, 400] │ trees │ Detector │ 10.0 │ 11 │ 0.5 │ 5.0 │ 400 │ {'0': tree, '1': NULL, '10': NULL, '11': NULL, '… │
│ [] │ 0.5 │ 0.4 │ YOLO_v9 │ images │ [1, 3, 640, 640] │ trees_yolov9 │ Detector │ 10.0 │ 11 │ 0.5 │ 25.0 │ 640 │ {'0': Tree, '1': NULL, '10': NULL, '11': NULL, '… │
│ [] │ 0.3 │ 0.3 │ YOLO_v8 │ images │ [1, 3, 640, 640] │ utilities │ Detector │ 3.0 │ 11 │ 0.5 │ 10.0 │ 640 │ {'0': Gas, '1': Manhole, '10': NULL, '11': NULL… │
└─────────┴──────────┴────────────────┴──────────────────────┴────────────┴────────────────────┴──────────────┴────────────┴────────────┴───────────────────┴────────────┴───────────────┴────────────┴----------------------------------------------------┘
GeoDeep 在 GitHub 上的 README 文件列出了 一些细节,其中包含有关预构建模型的信息,以及有关如何使用 YOLO 和至少 1,000 张图像创建自己的模型的信息。
检测汽车
以下汽车检测模型仅花费了几分钟时间就在 Maxar 的图像上运行。它检测到 304 辆汽车。
$geodeep\
ard_47_122022102202_2025-02-14_10400100A39C6A00-visual.tif\
cars\
--outputcars.geojson
$jq-S'.features|length'cars.geojson# 304
以下是检测位置的概述。
有很多汽车没有被该模型检测到。
在 Chao Phraya 河附近也有许多误报。
结果中只出现了一个检测类别“car”。以下是置信度分数的分布。
$jq'.features|.[]'\
cars.geojson\
>cars.unrolled.json
$~/duckdb
SELECTROUND(properties.score*10)::int*10ASpercent,
COUNT(*)num_detections
FROMREAD_JSON('cars.unrolled.json')
GROUPBY1
ORDERBY1;
┌─────────┬────────────────┐
│ percent │ num_detections │
│ int32 │ int64 │
├─────────┼────────────────┤
│ 30 │ 86 │
│ 40 │ 97 │
│ 50 │ 50 │
│ 60 │ 34 │
│ 70 │ 25 │
│ 80 │ 10 │
│ 90 │ 2 │
└─────────┴────────────────┘
检测树木
树木检测模型在 Maxar 的图像中发现了 14,136 棵树。
$geodeep\
ard_47_122022102202_2025-02-14_10400100A39C6A00-visual.tif\
trees\
--outputtrees.geojson
$jq-S'.features|length'trees.geojson# 14136
该模型运行了几分钟,因为它完全在我的 CPU 上运行,而不是在我的 GPU 上运行。
我在 GeoDeep 的代码库中找不到将推理设备更改为我的 Nvidia GPU 的标志。我已经提出了 一个问题,以了解是否实际上可以支持 GPU 推理。
以下是树木检测的概述。
误报很少,但有很多树木未被检测到。
很少有树木检测的置信度值高于 50%,即使它们通常都很准确。
结果中只出现了一个检测类别“tree”。以下是置信度分数的分布。
$jq'.features|.[]'\
trees.geojson\
>trees.unrolled.json
$~/duckdb
SELECTROUND(properties.score*10)::int*10ASpercent,
COUNT(*)num_detections
FROMREAD_JSON('trees.unrolled.json')
GROUPBY1
ORDERBY1;
┌─────────┬────────────────┐
│ percent │ num_detections │
│ int32 │ int64 │
├─────────┼────────────────┤
│ 30 │ 4412 │
│ 40 │ 5656 │
│ 50 │ 2678 │
│ 60 │ 1073 │
│ 70 │ 287 │
│ 80 │ 29 │
│ 90 │ 1 │
└─────────┴────────────────┘
使用 YOLOv9 检测树木
以下模型的运行速度比以前的模型快得多。它只花了一分钟左右的时间运行。但即便如此,仅检测到 402 棵树,比以前的模型少了两个数量级。
$geodeep\
ard_47_122022102202_2025-02-14_10400100A39C6A00-visual.tif\
trees_yolov9\
--outputtrees_yolov9.geojson
$jq-S'.features|length'trees_yolov9.geojson# 402
结果中只出现了一个检测类别“Tree”。以下是置信度分数的分布。
$jq'.features|.[]'\
trees_yolov9.geojson\
>trees_yolov9.unrolled.json
$~/duckdb
SELECTROUND(properties.score*10)::int*10ASpercent,
COUNT(*)num_detections
FROMREAD_JSON('trees_yolov9.unrolled.json')
GROUPBY1
ORDERBY1;
┌─────────┬────────────────┐
│ percent │ num_detections │
│ int32 │ int64 │
├─────────┼────────────────┤
│ 50 │ 106 │
│ 60 │ 187 │
│ 70 │ 92 │
│ 80 │ 15 │
│ 90 │ 2 │
└─────────┴────────────────┘
QGIS 抱怨说 GeoJSON 文件无效,因此我将其转换为 GPKG 文件。
$~/duckdb
COPY(
SELECTST_GEOMFROMGEOJSON(geometry)geom,
ROUND(properties.score*10)::int*10ASpercent
FROMREAD_JSON('trees_yolov9.unrolled.json',
maximum_object_size=100000000)
)TO'trees_yolov9.gpkg'
WITH(FORMATGDAL,
DRIVER'GPKG',
LAYER_CREATION_OPTIONS'WRITE_BBOX=YES');
这是检测的概述。
我没有看到很多误报,但很多树木没有被检测到。
检测建筑物
以下模型在 Maxar 的图像中检测到 23,561 座建筑物。
$geodeep\
ard_47_122022102202_2025-02-14_10400100A39C6A00-visual.tif\
buildings\
--outputbuildings.geojson
$jq-S'.features|length'buildings.geojson# 23561
此模型不报告置信度值。
$jq-S'.features[0]|del(.geometry)'\
buildings.geojson
{
"properties":{
"class":"Building"
},
"type":"Feature"
}
生成的 GeoJSON 文件为 437 MB。当我尝试将其拖到我的 QGIS 项目中时,QGIS 抱怨说它不是有效的数据源。下面我将结果转换为 49 MB 的 GeoPackage (GPKG) 文件。
有 15 条记录被归类为“Background”,我将其从 GPKG 文件中排除。
$jq'.features|.[]'\
buildings.geojson\
>buildings.unrolled.json
$~/duckdb
COPY(
SELECTST_GEOMFROMGEOJSON(geometry)geom
FROMREAD_JSON('buildings.unrolled.json',
maximum_object_size=100000000