💨

『Python で学ぶ衛星データ解析基礎』を R でやってみる (3) 4章その1

に公開

https://gihyo.jp/book/2022/978-4-297-13232-3

前回

https://zenn.dev/yutannihilation/articles/6f32eeed669592

画像のダウンロード

本と同じ条件で検索するとこんな感じです。

# ここでは直接 bbox を返すだけでいいが、本と合わせてポリゴンを返すようにする
sel_square <- function(lon, lat, delta_lon, delta_lat) {
  poly <- sf::st_polygon(list(
    rbind(
      c(lon + delta_lon, lat + delta_lat),
      c(lon + delta_lon, lat - delta_lat),
      c(lon - delta_lon, lat - delta_lat),
      c(lon - delta_lon, lat + delta_lat),
      c(lon + delta_lon, lat + delta_lat)
    )
  ))

  sf::st_sfc(poly, crs = 4326)
}

stac_source <- rstac::stac("https://earth-search.aws.element84.com/v1")

aoi <- sel_square(130.53, 31.60, 0.06, 0.02)

items <- stac_source |>
  rstac::stac_search(
    collections = "sentinel-2-l2a",
    datetime = "2019-01-01T00:00:00Z/2020-12-31T23:59:59Z",
    bbox = sf::st_bbox(aoi),
    limit = 100
  ) |> 
  rstac::get_request()

ここで、本では eo:cloud_cover < 10 というのを検索条件に入れているのですが、やってみても効きませんでした。rstac がおかしいのかな?と思ったのですが、curl や rustac などほかのツールで試してもだめだったので、 諦めました。/queryable というエンドポイントで CQL2 中で使える属性が確認できるのですが、これ見てもいける感じするんだけどなあ...

/queryables の結果
httr2::request("https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/queryables") |> 
  httr2::req_perform() |> 
  httr2::resp_body_json()
#> $`$schema`
#> [1] "https://json-schema.org/draft/2020-12/schema"
#> 
#> $`$id`
#> [1] "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/queryables"
#> 
#> $type
#> [1] "object"
#> 
#> $title
#> [1] "Queryables for Collection sentinel-2-l2a"
#> 
#> $properties
#> $properties$`eo:cloud_cover`
#> $properties$`eo:cloud_cover`$`$ref`
#> [1] "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/definitions/fields/properties/eo:cloud_cover"
#> 
#> 
#> 
#> $additionalProperties
#> [1] TRUE

上の条件だと広すぎて 400 件以上がヒットするのですが、 rstac::stac_search()limit を上げるとエラーになってしまいます。仕方ないので、ダウンロードできた 100 件の中から条件がいいものを選ぶことにします。

# eo:cloud_cover の昇順に並び替えて、いちばん雲がないものを選ぶ
items |> 
  rstac::items_as_tibble() |> 
  arrange(`eo:cloud_cover`) |> 
  glimpse()

# URL を調べる
urls <- items |> 
  rstac::items_filter(filter_fn = \(x) x$properties$`s2:product_uri` == "S2B_MSIL2A_20201112T015919_N0500_R060_T52SFA_20230414T013628.SAFE") |> 
  rstac::assets_url(c("swir16", "nir", "red", "green", "blue"))

dir.create("data", showWarnings = FALSE)        # 保存ディレクトリ
destfiles <- file.path("data", basename(urls))  # ファイル名
curl::multi_download(urls, destfiles)           # ダウンロード

NDVI、MNDWI の計算

ダウンロードしたファイルを読み込みます。ちなみに、前回まで気付いてなかったんですが、rast() には複数のファイルを渡せます。このうち、バンド11は解像度が異なるので後回しにします。

library(terra)

tiffs <- rast(c(
  "data/B08.tif", 
  "data/B04.tif",
  "data/B03.tif",
  "data/B02.tif"
))

tiffs
#> class       : SpatRaster 
#> dimensions  : 10980, 10980, 4  (nrow, ncol, nlyr)
#> resolution  : 10, 10  (x, y)
#> extent      : 6e+05, 709800, 3490200, 3600000  (xmin, xmax, ymin, ymax)
#> coord. ref. : WGS 84 / UTM zone 52N (EPSG:32652) 
#> sources     : B08.tif  
#>               B04.tif  
#>               B03.tif  
#>               B02.tif  
#> names       : B08, B04, B03, B02 

トゥルーカラーでプロットするとこんな感じ。ちなみに、説明してませんでしたが、値を 0~255 にスケールし直すのは、stretch() を使わなくても geom_spatraster_rgb()stretch という引数があります。ただ、minqmaxq にあたる引数はないので、細かく調整したい場合はやはり自分で stretch() で指定するのがいいでしょう。

library(tidyterra)
library(ggplot2)

ggplot() +
  geom_spatraster_rgb(data = tiffs[[2:4]], stretch = "lin")


出典:Element 84 提供の Sentinel-2 の衛星画像データを使って描画

これを鹿児島市周辺で切り抜きます。行政区画のデータを国土数値情報からダウンロードしてきます。

kagoshima <- sf::st_read("N03-20240101_46_GML/N03-20240101_46.geojson")

N03_004 に市町村名が入っているので、鹿児島市のみに絞り込みます。それを st_bbox() で bbox に変換、さらに bbox を st_as_sfc() で地物に変換します。また、切り抜くためには CRS を合わせないといけないので st_transform() で投影変換します。

aoi <- kagoshima |> 
  dplyr::filter(N03_004 == "鹿児島市") |> 
  sf::st_bbox() |> 
  sf::st_as_sfc() |> 
  sf::st_transform(crs(tiffs))

まとめて切り抜いておきます。

tiffs <- tiffs |> 
  crop(aoi)

NDVI の計算はこうです。1つにまとめたラスタから特定のバンドだけを取り出すには [[ ]] でアクセスできます。

ndvi <- (tiffs[[1]] - tiffs[[2]]) / (tiffs[[1]] + tiffs[[2]])

ggplot() +
  geom_spatraster(data = ndvi) +
  scale_fill_whitebox_c(palette = "purple")


出典:Element 84 提供の Sentinel-2 の衛星画像データを使って描画

MNDWI の計算をするために、バンド11を読み込みます。上に書いたように、バンド11は解像度が他と異なっています。このままでは計算できないので、リサンプリングが必要です。リサンプリングには resample() を使います。 method にリサンプリング方法がいろいろ指定できますが、ここでは本に合わせて average を使っておきます。

swir <- rast("data/B11.tif") |> 
  crop(aoi)

swir_resampled <- resample(swir, tiffs, method = "average")

以下が計算結果です。

mndwi <- (tiffs[[3]] - swir_resampled) / (tiffs[[3]] + swir_resampled)

ggplot() +
  geom_spatraster(data = mndwi) +
  scale_fill_whitebox_c(palette = "purple")


出典:Element 84 提供の Sentinel-2 の衛星画像データを使って描画

Discussion