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 年 3 月 10 日星期一,分类于 GIS

Wyvern's Open Satellite Feed

上个月,位于加拿大埃德蒙顿的 Wyvern 公司(一家拥有 36 名员工,融资 1600 万美元的初创公司)针对其 VNIR(可见光和近红外)的 23 到 31 波段高光谱卫星影像,启动了一项开放数据计划。

这些影像由他们三颗 Dragonette 6U CubeSat 卫星中的一颗所拍摄。这些卫星由 AAC Clyde Space 公司制造,该公司在英国、瑞典和其他一些国家设有办事处。它们在地球表面上方 517 - 550 公里的轨道上运行,星下点空间分辨率 (GSD) 为 5.3 米。

SpaceX 从加利福尼亚州的 Vandenberg Space Force Base 发射了他们的全部三颗卫星。它们的发射时间分别为 2023 年 4 月 15 日、6 月 12 日和 11 月 11 日。

Wyvern

他们位于蒙特利尔的 AWS S3 存储桶中托管了大约 130 GB 的 GeoTIFF 文件。发布的 25 张图像拍摄于 6 月至两周前。

在这篇文章中,我将研究 Wyvern 的开放数据源。

我的工作站

我使用的是 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。

安装先决条件

我将使用 GDAL 3.9.3、Python 3.12.3 和其他一些工具来帮助分析本文中的数据。

$sudo add-apt-repository ppa:deadsnakes/ppa
$sudo add-apt-repository ppa:ubuntugis/ubuntugis-unstable
$sudo apt update
$sudo apt install \
gdal-bin \
jq \
libimage-exiftool-perl \
libtiff-tools \
python3-pip \
python3.12-venv

我将设置一个 Python Virtual Environment 并安装一些依赖项。

$python3 -m venv ~/.wyvern
$source ~/.wyvern/bin/activate
$python3 -m pip install \
astropy \
geocoder \
pystac \
rich \
shapely \
sgp4

我将在此帖中使用我的 GeoTIFFs 分析实用程序

$git clone https://github.com/marklit/geotiffs \
~/geotiffs
$python3 -m pip install \
-r ~/geotiffs/requirements.txt

我将在此帖中使用 DuckDB,以及它的 H3, JSON, Lindel, ParquetSpatial 扩展。

$cd ~
$wget -c https://github.com/duckdb/duckdb/releases/download/v1.1.3/duckdb_cli-linux-amd64.zip
$unzip -j duckdb_cli-linux-amd64.zip
$chmod +x duckdb
$~/duckdb
INSTALL h3 FROM community;
INSTALL lindel FROM community;
INSTALL json;
INSTALL parquet;
INSTALL spatial;

我将设置 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+ plugin,通过 Bing 的 Virtual Earth Basemap 以及 CARTO 添加了地理空间上下文到地图。深色的非卫星影像地图主要由来自 Natural Earth 和 Overture 的矢量数据组成。

Dragonette 卫星

以下是 PulseOrbital 针对 2025 年 3 月 8 日和 9 日 Wyvern 的 Dragonette 星座对塔林上空的估计飞越列表。

Wyvern

下面我将尝试估计他们的三颗卫星各自的当前位置。我在 n2yo 上找到了 Two-line elements (TLE) 详细信息。

我在 2025 年 3 月 10 日运行了以下代码。它生成了一个 CSV 文件,其中包含他们三颗卫星的名称和估计位置。

$python3
from  datetime      import datetime, UTC
import json
from  astropy       import units as u
from  astropy.time    import Time
from  astropy.coordinates import ITRS, \
                 TEME, \
                 CartesianDifferential, \
                 CartesianRepresentation
from  sgp4.api      import Satrec
from  sgp4.api      import SGP4_ERRORS

tles = '''AAC-HSI-SAT1
     1 56225U 23054AZ 25068.93354999 .00004447 00000-0 28073-3 0 9998
     2 56225 97.4339 315.2974 0008749 291.1678 68.8616 15.09548670105253
     AAC-HSI-SAT2
     1 56995U 23084BX 25069.13857882 .00006243 00000-0 33207-3 0 9998
     2 56995 97.7590 200.5235 0013312 352.0602  8.0417 15.15565025 95939
     AAC-HSI-SAT3
     1 58848U 23174DH 25068.89805865 .00005229 00000-0 30564-3 0 9999
     2 58848 97.4007 137.8776 0011809 171.1785 188.9655 15.12246107 73641
     '''.strip().splitlines()
with open('locations.csv', 'w') as f:
  while tles:
    name = tles.pop(0).strip()
    line1 = tles.pop(0).strip()
    line2 = tles.pop(0).strip()
    satellite = Satrec.twoline2rv(line1, line2)
    t = Time(datetime.now(UTC).isoformat().split('+')[0],
         format='isot',
         scale='utc')
    error_code, teme_p, teme_v = satellite.sgp4(t.jd1, t.jd2) # in km and km/s
    if error_code != 0:
      raise RuntimeError(SGP4_ERRORS[error_code])
    teme_p = CartesianRepresentation(teme_p * u.km)
    teme_v = CartesianDifferential(teme_v * u.km / u.s)
    teme = TEME(teme_p.with_differentials(teme_v), obstime=t)
    itrs_geo = teme.transform_to(ITRS(obstime=t))
    location = itrs_geo.earth_location
    loc = location.geodetic
    f.write('"%s", "POINT (%f %f)"\n' % (name,
                       loc.lon.deg,
                       loc.lat.deg))

以下是在 QGIS 中渲染的上述 CSV 数据。

Wyvern

开放数据源

Wyvern 有一个 STAC 目录,其中列出了影像位置和拍摄元数据。我将下载此元数据,并使用 Mapbox 的反向地理编码服务获取每个图像的地址详细信息。Mapbox 的免费套餐每月提供 10 万次地理编码搜索。

$python3
import json
import geocoder
from  pystac      import Catalog
from  rich.progress  import track
from  shapely.geometry import shape

mapbox_key = '...' # WIP: Replace with your key
root = Catalog.from_file(href='https://wyvern-prod-public-open-data-program.s3.ca-central-1.amazonaws.com/catalog.json')
seen = set()
with open('enriched.json', 'w') as f:
  for item in track(list(root.get_items(recursive=True))):
    if item.assets['Cloud optimized GeoTiff'].href in seen:
      continue
    seen.add(item.assets['Cloud optimized GeoTiff'].href)
    centroid_ = shape(item.geometry).centroid
    resp = geocoder.mapbox([centroid_.y, centroid_.x],
                key=mapbox_key,
                method='reverse')
    assert resp.ok is True, resp.status
    f.write(json.dumps(
      {'properties':  item.properties,
       'geom':     shape(item.geometry).wkt,
       'id':      item.id,
       'bbox':     item.bbox,
       'assets':    {k.lower().replace(' ', '_'): v.href
                for k, v in item.assets.items()},
       'mapbox':    resp.current_result.__dict__,
       'collection_id': item.collection_id},
      sort_keys=True) + '\n')

上面生成了一个 33 行的 JSONL 文件。以下是一个示例记录。

$head -n 1 enriched.json | jq -S .
{
"assets":{
"cloud_optimized_geotiff":"https://wyvern-prod-public-open-data-program.s3.ca-central-1.amazonaws.com/wyvern_dragonette-001_20240703T171837_4c406dd3/wyvern_dragonette-001_20240703T171837_4c406dd3.tiff",
"data_mask":"https://wyvern-prod-public-open-data-program.s3.ca-central-1.amazonaws.com/wyvern_dragonette-001_20240703T171837_4c406dd3/wyvern_dragonette-001_20240703T171837_4c406dd3_data_mask.tiff",
"overview_image":"https://wyvern-prod-public-open-data-program.s3.ca-central-1.amazonaws.com/wyvern_dragonette-001_20240703T171837_4c406dd3/wyvern_dragonette-001_20240703T171837_4c406dd3_preview.png",
"pixel_quality_mask":"https://wyvern-prod-public-open-data-program.s3.ca-central-1.amazonaws.com/wyvern_dragonette-001_20240703T171837_4c406dd3/wyvern_dragonette-001_20240703T171837_4c406dd3_pixel_quality_mask.tiff",
"thumbnail_image":"https://wyvern-prod-public-open-data-program.s3.ca-central-1.amazonaws.com/wyvern_dragonette-001_20240703T171837_4c406dd3/wyvern_dragonette-001_20240703T171837_4c406dd3_thumbnail.png"
},
"bbox":[
-117.14246656709959,
48.26920917388883,
-116.64844506075083,
48.6222705607206
],
"collection_id":"2024",
"geom":"POLYGON ((-117.09090885825756 48.570741224333794, -117.09090885825756 48.57083115336763, -117.09070614118478 48.570876117884545, -117.08948983874814 48.57110094046913, -117.08847625338427 48.5712807985368, -117.07421848593255 48.57370888245031, -117.04800041118719 48.57816036962509, -117.03975658356107 48.57955426964951, -117.02671511854598 48.58175753097844, -117.01522781775549 48.58369100520587, -117.00319993810427 48.58571440846713, -116.98792858528867 48.5882773859314, -116.96988676581184 48.59129000856483, -116.9558317154329 48.593628163444514, -116.9458310065094 48.59529185057044, -116.93900619839269 48.596415963493364, -116.89765191554693 48.60320560554782, -116.89461115945532 48.60370021523391, -116.8650144668304 48.60851141854402, -116.85089184409387 48.61080460890678, -116.84812137743262 48.61125425407595, -116.84589148963212 48.61161397021129, -116.81298375148523 48.616919783207486, -116.81237560026692 48.616919783207486, -116.81217288319415 48.6164251735214, -116.81183502140618 48.61552588318306, -116.81122687018787 48.61390716057405, -116.80784825230832 48.60486929267375, -116.78994157754666 48.556667330538794, -116.77967057919281 48.52901415263488, -116.77095374506355 48.50554267480424, -116.76811570604472 48.49789870692836, -116.70905746551009 48.33845452994091, -116.70371924926037 48.324020920010575, -116.7027732362541 48.32145794254631, -116.70250294682374 48.32069354575872, -116.70250294682374 48.32042375865722, -116.70257051918134 48.320378794140304, -116.7035841045452 48.320198936072636, -116.70412468340592 48.3201090070388, -116.97833331051073 48.275054561088034, -116.98002261945051 48.27478477398653, -116.9801577641657 48.27478477398653, -116.98022533652329 48.27482973850345, -116.98036048123846 48.275054561088034, -116.98049562595365 48.27541427722337, -116.98069834302642 48.27595385142637, -116.99819958364253 48.32267198450307, -117.03381021609304 48.417906831333134, -117.0373239786878 48.42730441536877, -117.0754347883692 48.529238975219464, -117.07881340624874 48.53827684311977, -117.08861139809946 48.56449115648234, -117.09003041760887 48.56831314042028, -117.09084128589997 48.57051640174921, -117.09090885825756 48.570741224333794))",
"id":"wyvern_dragonette-001_20240703T171837_4c406dd3",
"mapbox":{
"_geometry":{
"coordinates":[
-116.89724,
48.44575
],
"type":"Point"
},
"fieldnames":[
"accuracy",
"address",
"bbox",
"city",
"confidence",
"country",
"housenumber",
"lat",
"lng",
"ok",
"postal",
"quality",
"raw",
"state",
"status",
"street"
],
"json":{
"address":"38 Miller Gulch, Priest River, Idaho 83821, United States",
"city":"Priest River",
"country":"United States",
"housenumber":"38",
"lat":48.44575,
"lng":-116.89724,
"ok":true,
"postal":"83821",
"quality":1,
"raw":{
"address":"38",
"center":[
-116.89724,
48.44575
],
"context":[
{
"id":"postcode.8128753314501880",
"text":"83821"
},
{
"id":"place.268052716",
"mapbox_id":"dXJuOm1ieHBsYzpEL29vN0E",
"text":"Priest River",
"wikidata":"Q1517705"
},
{
"id":"district.1885932",
"mapbox_id":"dXJuOm1ieHBsYzpITWJz",
"text":"Bonner County",
"wikidata":"Q483932"
},
{
"id":"region.58604",
"mapbox_id":"dXJuOm1ieHBsYzo1T3c",
"short_code":"US-ID",
"text":"Idaho",
"wikidata":"Q1221"
},
{
"id":"country.8940",
"mapbox_id":"dXJuOm1ieHBsYzpJdXc",
"short_code":"us",
"text":"United States",
"wikidata":"Q30"
}
],
"country":"United States",
"district":"Bonner County",
"geometry":{
"coordinates":[
-116.89724,
48.44575
],
"type":"Point"
},
"id":"address.8128753314501880",
"place":"Priest River",
"place_name":"38 Miller Gulch, Priest River, Idaho 83821, United States",
"place_type":[
"address"
],
"postcode":"83821",
"properties":{
"accuracy":"point",
"mapbox_id":"dXJuOm1ieGFkcjplYWRmMWVhNi03MDg3LTQzNjUtOWNkNi05NGFjMWNlZTA5NDg"
},
"region":"Idaho",
"relevance":1,
"text":"Miller Gulch",
"type":"Feature"
},
"state":"Idaho",
"status":"OK"
},
"northeast":[],
"northwest":[],
"raw":{
"address":"38",
"center":[
-116.89724,
48.44575
],
"context":[
{
"id":"postcode.8128753314501880",
"text":"83821"
},
{
"id":"place.268052716",
"mapbox_id":"dXJuOm1ieHBsYzpEL29vN0E",
"text":"Priest River",
"wikidata":"Q1517705"
},
{
"id":"district.1885932",
"mapbox_id":"dXJuOm1ieHBsYzpITWJz",
"text":"Bonner County",
"wikidata":"Q483932"
},
{
"id":"region.58604",
"mapbox_id":"dXJuOm1ieHBsYzo1T3c",
"short_code":"US-ID",
"text":"Idaho",
"wikidata":"Q1221"
},
{
"id":"country.8940",
"mapbox_id":"dXJuOm1ieHBsYzpJdXc",
"short_code":"us",
"text":"United States",
"wikidata":"Q30"
}
],
"country":"United States",
"district":"Bonner County",
"geometry":{
"coordinates":[
-116.89724,
48.44575
],
"type":"Point"
},
"id":"address.8128753314501880",
"place":"Priest River",
"place_name":"38 Miller Gulch, Priest River, Idaho 83821, United States",
"place_type":[
"address"
],
"postcode":"83821",
"properties":{
"accuracy":"point",
"mapbox_id":"dXJuOm1ieGFkcjplYWRmMWVhNi03MDg3LTQzNjUtOWNkNi05NGFjMWNlZTA5NDg"
},
"region":"Idaho",
"relevance":1,
"text":"Miller Gulch",
"type":"Feature"
},
"southeast":[],
"southwest":[]
},
"properties":{
"constellation":"Dragonette",
"created":"2024-11-01T23:03:41Z",
"datetime":"2024-07-03T17:18:40.010328Z",
"end_datetime":"2024-07-03T17:18:42.438451Z",
"eo:cloud_cover":5.63,
"gsd":5.26,
"instruments":[
"VNIR Hyperspectral Imaging Sensor"
],
"license":"other",
"platform":"Dragonette-001",
"processing:facility":"Wyvern",
"processing:level":"L1B",
"processing:version":"1.3",
"product_type":"hyperspectral",
"proj:code":"EPSG:4326",
"proj:shape":[
7311,
7852
],
"providers":[
{
"name":"Wyvern Inc.",
"roles":[
"licensor",
"producer",
"processor"
],
"url":"https://www.wyvern.space/"
}
],
"sat:platform_international_designator":"2023-054AZ",
"sensor_mode":"strip",
"sensor_type":"optical",
"start_datetime":"2024-07-03T17:18:37.582204Z",
"updated":"2024-11-01T23:03:41Z",
"view:azimuth":65.36472836367665,
"view:incidence_angle":2.106011989129783,
"view:off_nadir":2.6882806109241932,
"view:sun_azimuth":116.34019361959561,
"view:sun_elevation":50.52691114712514,
"wyvern:radiometric_resolution":"12"
}
}

数据分析

我将他们的图像元数据加载到 DuckDB 中