MENU

溶けかけてるうさぎ HP GALLERY BLOG TOP RECENT ARTICLES POPULAR ARTICLES ABOUT THIS BLOG

CATEGORY

大学 (143) 仕事 (25) 航空宇宙 (110) 写真 (79) 旅行 (33) 飯・酒 (20) コンピュータ (123) その他 (47)

TAG

ARCHIVE

RECENT

必要範囲で切り抜くことで衛星データを高速にダウンロード ふと 博士号について 自宅を部分的に交換するととても便利! 【駅メモ!】6000 駅達成! 【Archive】『衛星データ利用の方々にとって近いようで触れる機会のなさそうな小話』 @日本衛星データコミニティ勉強会

必要範囲で切り抜くことで衛星データを高速にダウンロード

事象発生日:2025-12-23

記事公開日:2025-12-23

アクセス数:19

衛星データのダウンロードって大変ですよね.特に時系列解析をしたいときなどに,数年分の衛星データをダウンロードするのにものすごい時間がかかる,ということもしばしば.

ということで,必要なバンドのみ,かつ,領域のみ切り取ってダウンロードする手法を備忘録的に残します.

はじめに

これは FOSS4G Advent Calendar 2025 の 12 月 23 日の記事です.

FOSS4Gとは,Free and Open Source Software for Geospatial の略で,地理空間情報に関わるフリー&オープンソースソフトウェアの総称です.

私が FOSS4G を扱うようになったのは 2 年前(その経緯や最近取り組んでいることは,『人工衛星の作り方ではなく使い方と向き合った2年間を参照)で,去年はこんな記事を書かせていただきました.

 

 

今年は,時系列解析などのために大量の衛星データを効率的にダウンロードする Tips を紹介しようと思います.

衛星データ解析とデータ取得

衛星データ解析入門については,ぴっかりん氏の Zenn Books 『光学衛星画像解析の1歩目を踏み出してみよう!― QGIS編 ―を参照するのが良いと思います.

その「Chapter 05 Webサイトで衛星画像を探してみよう、見てみよう」で述べられているように,無料で使える衛星データはたくさんあり,例えば光学衛星 Sentinel-2 やレーダー衛星 (SAR 衛星) Sentinel-1 などは,Copernicus Browser から GUI 経由で入手できます.

 

GUI でのデータダウンロードは直感的操作で便利ですが,大量のデータの取得には向いていなかったり,また,1 シーン(大きな衛星画像を配信用に区切られた画像データ)ごとにまとめられる zip ファイルには,多くのバンドの画像(Sentinel-1 では VV 偏波と VH 偏波の 2 種類,Sentinel-2 では,B1 ~ B12 の 13 バンド)や校正情報,メタデータなど,様々なデータが含まれているうえに,1 シーンの範囲は下図のようにそれなりに広大なため,1 ダウンロードのファイルサイズも大きいです.

Sentinel-1 の 1 シーン (Copernicus Browser)

1 シーン全域のすべてのデータを,時系列解析する全期間ダウンロードするのは大変骨の折れる作業です.

さらに,これらのオープン衛星データは北米やヨーロッパのサーバーに保存されていることが多く,ただでさえサイズの大きなデータを海底ケーブル経由の国際通信でダウンロードしなければならず,想像を超える時間がかかってしまうのです.

 

一方で,データ解析をするときの実際は,ほしいデータ(バンド)や領域はごく一部であったりすることがしばしばです.

バンドと領域を絞って効率的にダウンロード

ということなので,バンドと領域を絞って効率的にダウンロードしていきましょう.

 

そのためのスクリプトは以下です.

#!/usr/bin/env python
import logging
import os
import sys
from pathlib import Path
import planetary_computer
import pystac_client
import rasterio
import rasterio.mask
import requests

OUTPUT_DIR = "./output/cog/"
COLLECTIONS = "sentinel-1-rtc"
# https://github.com/stac-api-extensions/query
QUERY = {"sat:orbit_state": {"eq": "descending"}}
SEARCH_AOI = {
    "coordinates": [
        [
            [138.7249, 35.3591],
            [138.7249, 35.3687],
            [138.7365, 35.3687],
            [138.7365, 35.3591],
            [138.7249, 35.3591]
        ]
    ],
    "type": "Polygon"
}  # 富士山山頂近辺
START_DATE = "2025-12-01"
END_DATE = "2025-12-20"
# CROP_AOI = None
CROP_AOI = [820023, 3907273, 857351, 3939096]  # [xmin, ymin, xmax, ymax] in UTM coordinates

def download_cropped_img(item, pol, crop_aoi):
    asset = item.assets[pol]
    signed_href = planetary_computer.sign(asset.href)  # COG の URL に署名
    filename = item.id + f"_{pol}.tif"  # 出力ファイル名
    file_path = Path(OUTPUT_DIR) / filename

    if os.path.exists(file_path):
        logging.info(f"Skip {filename} (already exists)")
        return

    logging.info(f"Download: {filename} ...")

    try:
        if crop_aoi is None:
            # そのまま保存
            response = requests.get(signed_href, timeout=6000.0)
            response.raise_for_status()
            with open(file_path, "wb") as f:
                f.write(response.content)

        else:
            with rasterio.open(signed_href) as src:
                # crop_aoi [minx, miny, maxx, maxy] を polygon geometry へ変換
                minx, miny, maxx, maxy = crop_aoi
                bbox_polygon = {
                    "type": "Polygon",
                    "coordinates": [
                        [
                            [minx, miny],
                            [maxx, miny],
                            [maxx, maxy],
                            [minx, maxy],
                            [minx, miny],
                        ]
                    ],
                }
                # mask を適用
                out_image, out_transform = rasterio.mask.mask(src, [bbox_polygon], crop=True)
                out_meta = src.meta.copy()
                out_meta.update(
                    {
                        "height": out_image.shape[1],
                        "width": out_image.shape[2],
                        "transform": out_transform,
                    }
                )

                with rasterio.open(file_path, "w", **out_meta) as dest:
                    dest.write(out_image)

        logging.info(f"Saved: {filename}")
    except Exception as e:
        logging.info(f"Error processing {item.id}: {e}")


def download_stac_metadata(item):
    stac_metadata_ural_base = "https://planetarycomputer.microsoft.com/api/stac/v1/collections/" + COLLECTIONS + "/items/"
    url = stac_metadata_ural_base + item.id
    filename = item.id + ".json"
    file_path = Path(OUTPUT_DIR) / filename

    if os.path.exists(file_path):
        logging.info(f"Skip {filename} (already exists)")
        return
    logging.info(f"Save meta data: {filename} ...")

    try:
        response = requests.get(url, timeout=600.0)
    except Exception as e:
        logging.info(f"Error processing {item.id}: {e}")
        return

    if response.status_code == 200:
        with open(file_path, "wb") as f:
            f.write(response.content)
        logging.info(f"Saved: {filename}")
    else:
        logging.info(f"Error processing {item.id}: {response.status_code}")


def main():
    logging.basicConfig(
        level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
    )
    os.makedirs(OUTPUT_DIR, exist_ok=True)

    api_key = os.getenv("PC_SDK_SUBSCRIPTION_KEY")
    planetary_computer.settings.set_subscription_key(api_key)

    catalog = pystac_client.Client.open(
        "https://planetarycomputer.microsoft.com/api/stac/v1",
        modifier=planetary_computer.sign_inplace,
    )
    search = catalog.search(
        collections=COLLECTIONS,
        intersects=SEARCH_AOI,
        datetime=f"{START_DATE}/{END_DATE}",
        query=QUERY,
    )

    items = search.item_collection()
    logging.info(f"Items: {len(items)} items")

    for item in items:
        download_stac_metadata(item)
        for pol in ["vv", "vh"]:
            if pol not in item.assets:
                continue
            download_cropped_img(item, pol, CROP_AOI)


if __name__ == "__main__":
    main()
    sys.exit(0)
バンドと領域を絞った Sentinel-1 ダウンロードのサンプルコード

大事な部分を見ていきます.

STAC による API 経由での画像取得

時空間データ(主に衛星データ)のカタログを表現・検索するためのオープン標準として STAC (SpatioTemporal Asset Catalog) というものがあります.

現在,多くのデータ提供者がこの標準に基づいてデータを公開しており,STAC API を利用することで,API 経由で衛星データを検索し,実データを取得することが可能です.

ここでは,私が良く利用している Microsoft Planetary Computer を例としてサンプルコードを掲載しています.

 

STAC によるデータ検索などについては,STAC 公式チュートリアルや,Microsoft Planetary Computer の Sentinel-1 データのサンプルコードをご覧いただくとして,サンプルコードでは具体例として Sentinel 1 Radiometrically Terrain Corrected (RTC) データを対象に,撮影日 が 2025/12/01 から 2025/12/20 の期間における富士山山頂付近のデータを検索し,取得しようとしています.

COLLECTIONS = "sentinel-1-rtc"
# https://github.com/stac-api-extensions/query
QUERY = {"sat:orbit_state": {"eq": "descending"}}
SEARCH_AOI = {
    "coordinates": [
        [
            [138.7249, 35.3591],
            [138.7249, 35.3687],
            [138.7365, 35.3687],
            [138.7365, 35.3591],
            [138.7249, 35.3591]
        ]
    ],
    "type": "Polygon"
}  # 富士山山頂近辺
START_DATE = "2025-12-01"
END_DATE = "2025-12-20"
    catalog = pystac_client.Client.open(
        "https://planetarycomputer.microsoft.com/api/stac/v1",
        modifier=planetary_computer.sign_inplace,
    )
    search = catalog.search(
        collections=COLLECTIONS,
        intersects=SEARCH_AOI,
        datetime=f"{START_DATE}/{END_DATE}",
        query=QUERY,
    )

    items = search.item_collection()
    logging.info(f"Items: {len(items)} items")

バンドを選択して取得

STAC では,実際のデータファイルは asset と呼ばれる単位で管理されます.

Sentinel などの衛星データでは,各バンド(SAR では偏波)ごとの画像ファイルが個別の asset として提供されるのが一般的です.

Sentinel-1 では偏波は VV と VH の 2 種類が利用可能あり,サンプルコードのように必要な偏波(バンド)のみを選択して画像データを取得することができます.

    for item in items:
        download_stac_metadata(item)
        for pol in ["vv", "vh"]:
            if pol not in item.assets:
                continue
            download_cropped_img(item, pol, CROP_AOI)
def download_cropped_img(item, pol, crop_aoi):
    asset = item.assets[pol]

シーンから必要な領域のみを切り取って取得

STAC を通じて配信される衛星データは, COG (Cloud Optimized GeoTIFF) と呼ばれる形式で配信されることが一般的です.

COG については空畑の解説記事などを参照いただくとして,Cloud Optimized と名乗っているように,HTTP の Range Request を用いることで,大きな画像ファイルのうちの必要な領域のデータのみに選択的にアクセスできるよう最適化されています.

 

このように必要な部分のみへ選択的にアクセスできるという特性は, rasterio などのライブラリで画像ファイルを部分的に読み込んで扱えることを意味しており,サンプルコードでは rasterio.mask によって指定した領域のみを切り抜いてダウンロードしています.

 

ここでの注意点は主に 2 つあります.

1 つめは rasterio.mask.maskcrop パラメタ(デフォルト値は False)で,これを True にしないと選択範囲外がマスクされるだけで,画像のサイズは変わりません(範囲外は NoData で埋められるだけ).

2 つめは座標系であり,切り抜き処理は asset として取得されるファイルの座標系に基づいて行う必要があります.

Sentinel-1 は UTM 座標系で配信されており, CROP_AOI で指定する切り抜き範囲についても UTM 座標系で定義してます.

CROP_AOI = [820023, 3907273, 857351, 3939096]  # [xmin, ymin, xmax, ymax] in UTM coordinates
            with rasterio.open(signed_href) as src:
                # crop_aoi [minx, miny, maxx, maxy] を polygon geometry へ変換
                minx, miny, maxx, maxy = crop_aoi
                bbox_polygon = {
                    "type": "Polygon",
                    "coordinates": [
                        [
                            [minx, miny],
                            [maxx, miny],
                            [maxx, maxy],
                            [minx, maxy],
                            [minx, miny],
                        ]
                    ],
                }
                # mask を適用
                out_image, out_transform = rasterio.mask.mask(src, [bbox_polygon], crop=True)
                out_meta = src.meta.copy()
                out_meta.update(
                    {
                        "height": out_image.shape[1],
                        "width": out_image.shape[2],
                        "transform": out_transform,
                    }
                )

                with rasterio.open(file_path, "w", **out_meta) as dest:
                    dest.write(out_image)

実際にダウンロード

サンプルコードでは,以下の行でダウンロード領域を切り抜くかどうかを指定できるようになっています.

# CROP_AOI = None
CROP_AOI = [820023, 3907273, 857351, 3939096]  # [xmin, ymin, xmax, ymax] in UTM coordinates

実際にダウンロードした結果は次のようになり,意図通りの動作になっていることがわかります.

ダウンロード領域を指定しない場合の取得した画像(背景地図は OSM)
ダウンロード領域を指定した場合の取得した画像(背景地図は OSM)

実行時間(ダウンロード時間)について確認するために,サンプルコードの実行ログを掲載します(STAC Search の結果, 3 シーンがヒットしており,それぞれについてメタデータおよび VV,VH のデータをダウンロードしています.).

通常のように富士山が含まれる 1 シーンをまるごとダウンロードしようとすると,1 画像につき 40 分ほどかかっていますが,富士山周辺のみを切り抜いた場合,約 2 分でダウンロードできています.

なお,2 つめのシーン (S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc) の VV,VH について,前者は接続エラーでダウンロードが途中で失敗し,後者は後述する理由で失敗していますね.

$ python ./download_sentinel-1.py
2025-12-21 05:50:00,973 - INFO - Items: 3 items
2025-12-21 05:50:00,973 - INFO - Save meta data: S1A_IW_GRDH_1SDV_20251214T205139_20251214T205204_062316_07CDC4_rtc.json ...
2025-12-21 05:50:01,811 - INFO - Saved: S1A_IW_GRDH_1SDV_20251214T205139_20251214T205204_062316_07CDC4_rtc.json
2025-12-21 05:50:01,811 - INFO - Download: S1A_IW_GRDH_1SDV_20251214T205139_20251214T205204_062316_07CDC4_rtc_vv.tif ...
2025-12-21 06:29:54,696 - INFO - Saved: S1A_IW_GRDH_1SDV_20251214T205139_20251214T205204_062316_07CDC4_rtc_vv.tif
2025-12-21 06:29:54,928 - INFO - Download: S1A_IW_GRDH_1SDV_20251214T205139_20251214T205204_062316_07CDC4_rtc_vh.tif ...
2025-12-21 07:09:06,643 - INFO - Saved: S1A_IW_GRDH_1SDV_20251214T205139_20251214T205204_062316_07CDC4_rtc_vh.tif
2025-12-21 07:09:06,875 - INFO - Save meta data: S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc.json ...
2025-12-21 07:09:07,781 - INFO - Saved: S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc.json
2025-12-21 07:09:07,781 - INFO - Download: S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc_vv.tif ...
2025-12-21 07:35:56,339 - INFO - Error processing S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc: ('Connection broken: IncompleteRead([1280720896](tel:1280720896) bytes read, 577416664 more expected)', IncompleteRead([1280720896](tel:1280720896) bytes read, 577416664 more expected))
2025-12-21 07:35:56,340 - INFO - Download: S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc_vh.tif ...
2025-12-21 09:26:52,149 - INFO - Error processing S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc: HTTPSConnectionPool(host='[sentinel1euwestrtc.blob.core.windows.net](http://sentinel1euwestrtc.blob.core.windows.net)', port=443): Read timed out.
2025-12-21 09:26:52,150 - INFO - Save meta data: S1A_IW_GRDH_1SDV_20251202T205140_20251202T205205_062141_07C6EC_rtc.json ...
2025-12-21 09:26:53,025 - INFO - Saved: S1A_IW_GRDH_1SDV_20251202T205140_20251202T205205_062141_07C6EC_rtc.json
2025-12-21 09:26:53,025 - INFO - Download: S1A_IW_GRDH_1SDV_20251202T205140_20251202T205205_062141_07C6EC_rtc_vv.tif ...
2025-12-21 10:04:21,592 - INFO - Saved: S1A_IW_GRDH_1SDV_20251202T205140_20251202T205205_062141_07C6EC_rtc_vv.tif
2025-12-21 10:04:21,818 - INFO - Download: S1A_IW_GRDH_1SDV_20251202T205140_20251202T205205_062141_07C6EC_rtc_vh.tif ...
2025-12-21 10:41:02,698 - INFO - Saved: S1A_IW_GRDH_1SDV_20251202T205140_20251202T205205_062141_07C6EC_rtc_vh.tif
ダウンロード領域を指定しない場合の実行結果
$ python ./download_sentinel-1.py
2025-12-21 22:04:11,423 - INFO - Items: 3 items
2025-12-21 22:04:11,424 - INFO - Save meta data: S1A_IW_GRDH_1SDV_20251214T205139_20251214T205204_062316_07CDC4_rtc.json ...
2025-12-21 22:04:12,270 - INFO - Saved: S1A_IW_GRDH_1SDV_20251214T205139_20251214T205204_062316_07CDC4_rtc.json
2025-12-21 22:04:12,270 - INFO - Download: S1A_IW_GRDH_1SDV_20251214T205139_20251214T205204_062316_07CDC4_rtc_vv.tif ...
2025-12-21 22:05:37,142 - INFO - Saved: S1A_IW_GRDH_1SDV_20251214T205139_20251214T205204_062316_07CDC4_rtc_vv.tif
2025-12-21 22:05:37,143 - INFO - Download: S1A_IW_GRDH_1SDV_20251214T205139_20251214T205204_062316_07CDC4_rtc_vh.tif ...
2025-12-21 22:07:21,609 - INFO - Saved: S1A_IW_GRDH_1SDV_20251214T205139_20251214T205204_062316_07CDC4_rtc_vh.tif
2025-12-21 22:07:21,610 - INFO - Save meta data: S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc.json ...
2025-12-21 22:07:22,720 - INFO - Saved: S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc.json
2025-12-21 22:07:22,721 - INFO - Download: S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc_vv.tif ...
2025-12-21 22:07:23,440 - INFO - Error processing S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc: Input shapes do not overlap raster.
2025-12-21 22:07:23,441 - INFO - Download: S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc_vh.tif ...
2025-12-21 22:07:23,916 - INFO - Error processing S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc: Input shapes do not overlap raster.
2025-12-21 22:07:23,917 - INFO - Save meta data: S1A_IW_GRDH_1SDV_20251202T205140_20251202T205205_062141_07C6EC_rtc.json ...
2025-12-21 22:07:24,820 - INFO - Saved: S1A_IW_GRDH_1SDV_20251202T205140_20251202T205205_062141_07C6EC_rtc.json
2025-12-21 22:07:24,821 - INFO - Download: S1A_IW_GRDH_1SDV_20251202T205140_20251202T205205_062141_07C6EC_rtc_vv.tif ...
2025-12-21 22:09:53,307 - INFO - Saved: S1A_IW_GRDH_1SDV_20251202T205140_20251202T205205_062141_07C6EC_rtc_vv.tif
2025-12-21 22:09:53,307 - INFO - Download: S1A_IW_GRDH_1SDV_20251202T205140_20251202T205205_062141_07C6EC_rtc_vh.tif ...
2025-12-21 22:13:13,348 - INFO - Saved: S1A_IW_GRDH_1SDV_20251202T205140_20251202T205205_062141_07C6EC_rtc_vh.tif
ダウンロード領域を指定した場合の実行結果

後者で Input shapes do not overlap raster. というエラーとともに画像の切り抜きに失敗している理由について,軽く触れておきます.

切り抜きが成功しているシーン (S1A_IW_GRDH_1SDV_20251214T205139_20251214T205204_062316_07CDC4_rtcS1A_IW_GRDH_1SDV_20251202T205140_20251202T205205_062141_07C6EC_rtc) と失敗しているシーン (S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc) それぞれについて,シーンの領域は下図の領域でした.

切り抜きに成功したシーン
切り抜きに失敗したシーン

どちらのシーンも SEARCH_AOI で指定した富士山山頂周辺が含まれており意図した動作です.

 

地理空間情報に馴染みのある勘の鋭い方々はすでにお気づきだと思いますが,富士山周辺は UTM 座標系の Zone 53 と 54 の境目に位置しています(UTM 座標系では,地球表面を平面に投影するときの歪みを低減するために,子午線で区切られた Zone ごとに異なる座標系を定義している).

 

つまり,S1A_IW_GRDH_1SDV_20251214T205139_20251214T205204_062316_07CDC4_rtcS1A_IW_GRDH_1SDV_20251202T205140_20251202T205205_062141_07C6EC_rtc は UTM zone 53N の座標系で配信されている画像ファイルであり,S1A_IW_GRDH_1SDV_20251209T204343_20251209T204408_062243_07CAEF_rtc は UTM Zone 54N の座標系で配信されている画像ファイルです.

このため,CROP_AOI を UTM zone 53N で指定していた本サンプルコードの場合,後者のシーンでは CROP_AOI の領域が意図した領域を示せず,切り抜きに失敗したわけです(座標系めんどくいさい~~~).

おわりに

というわけで,衛星データをしゅっとダウンロードする Tips の紹介でした.

 

とはいえ,例えば機械学習のモデルを作りたいときなど,膨大な領域のデータを取得したい,なんてこともよくあります.

そういうときには,例えば衛星データ配信サーバーに地理的に近いサーバー(eg, AWS リージョンなど)に仮想環境をたててしまって,そこで処理を行う,なんてこともあります.

 

衛星データをはじめとする地理空間情報の扱いは,データサイズとの戦いであったりもするので,皆さんの Tips などがありましたら,ぜひ教えて下さいね!

出典・参考

Qiita Advent Calendar 2025. FOSS4G Advent Calendar 2025. Retrieved December 22, 2025, from https://qiita.com/advent-calendar/2025/foss4g
ArkEdge Space Blog. 人工衛星の作り方ではなく使い方と向き合った2年間. Retrieved December 22, 2025, from https://blog.arkedge.space/entry/2025/12/22/113000
Zenn. 光学衛星画像解析の1歩目を踏み出してみよう!― QGIS編 ―. Retrieved December 22, 2025, from https://zenn.dev/ra0kley/books/4688c64971b61a
Copernicus Data Space Ecosystem. Sentinel-1 - Documentation. Retrieved December 22, 2025, from https://documentation.dataspace.copernicus.eu/Data/SentinelMissions/Sentinel1.html
Copernicus Data Space Ecosystem. Sentinel-2 - Documentation. Retrieved December 22, 2025, from https://documentation.dataspace.copernicus.eu/Data/SentinelMissions/Sentinel2.html
STAC. STAC Tutorials. Retrieved December 22, 2025, from https://stacspec.org/en/tutorials/
Microsoft Planetary Computer. Sentinel 1 Radiometrically Terrain Corrected (RTC). Retrieved December 22, 2025, from https://planetarycomputer.microsoft.com/dataset/sentinel-1-rtc
空畑. COG(Cloud Optimized Geotiff)とは?~メリット、適用データ、使い方~ Tellus v3.0から適用される新しいデータ形式に迫る!. Retrieved December 22, 2025, from https://sorabatake.jp/25634/

関連記事

コメントを投稿

名前

Email (※公開されることはありません)

コメント