Google Mapsを用いた衛星画像および色情報取得と分析

その1 データ読み込みから色空間取得まで

宇都宮 譲 Yuzuru Utsunomiya, Ph. D.

2021-04-14 09:34:33


yuzuruweb TOP

概論

画像に含まれる情報から、さまざまな社会経済指標を推定しようという試み。古典的には夜間画像からGDPや人口を推定する研究が多かった。近年はカラー画像を用意に入手できるようになったから、そろそろカラー化してもいいのではないかとおもう。夜間画像では昼間しか活動できない産業を表現しきれないし。

することはとっても簡単。タイ都県毎に格子状に設けた点から無作為に50点を決め、各点を中心とする衛星画像を取得する。衛星画像をラスタ化し、RGB色情報を取得する。それだけ。50点は十分なサンプルサイズが集まるならば減らしてもよいし、増やす必要があるなら増やしてもよい。

しかし、おもに大きすぎるデータサイズのせいで、いろいろ工夫が必要になるこの頃。カラー画像を利用することはだれもがおもいついたことだろう。けれど、実行しようとするとなかなか工夫が必要。

データを読み込む

ライブラリ読み込みは省略。おもなライブラリは、以下に示す通り。

  • tidyverse():おなじみパッケージ。dplyr()tidyr()purrr()など、おなじみパッケージが今回も大活躍。
  • sp():地理情報を扱うパッケージ。
  • ggmap():Google mapsから地図を取得するライブラリ。Google APIが必要。
  • raster():地理的属性を与えた画像をラスタ化するライブラリ。

ggmap()ライブラリが使うGoogle APIはを読み込む。読み込めたかどうか、ggmap::has_google_key()で確かめる。読み込めていれば、trueと値が返える。

Google APIは別ファイルに書き込む。漏洩して不正利用されると課金されるから注意。不正利用されなくても、あまりにたくさん使うとやはり課金される。とはいえ、今回使う程度ではまったく課金されないから、安心して何度でも試すとよい。

sfデータを次に読み込む。GADMはsfデータを配布する。シェープファイルを使う手もあるが、sfファイルのほうが圧倒的に軽量である。

変数設定

衛星画像を取得する座標と取得する座標数を指定する。われわれがサンプリングする点は、格子の交点である。交点が多いほうがNは大きくなるが、あまり多いとデータ処理が煩雑になる。

  • cellsize:座標を取得するための仮想的な格子サイズを決定する。1は緯度経度1\(\circ\)を指す。0.5ならその半分。今回は0.025($度)を指定した。ある程度細かな地域差を反映させるため。バンコク近傍で、点と点の距離は200メートル弱。
  • sample_size:サンプルサイズ。想定されるモデルが含む変数は5つなので、とりあえず50を指定。あまり多いと、格子を小さくしない限りサンプルサイズを確保できなくなる。

画像処理につかうaggregate_funfact_funについては、後ほど解説する。

サンプリングする点を決定する

格子を作成し交点を求める。

もともとの都県地図データは、GADMからダウンロードする。シェープファイルよりもsfファイルのほうがファイルサイズは小さいしRにとって親和的で使いやすい。インターネット経由で都度ダウンロードする方法もあるけれど、手元にデータがあったほうが安心でしょう。アップデートはときどき確かめに出かければよろしい。

格子はsf::st_make_grid()関数で作成する。あるポリゴンからはみ出ないよう(内側に)格子を作成する関数である。sf::st_intersection()は、あるポリゴン(今回は都県)内にある格子交点を求める関数である。これら関数を用いて、ある都県内に等間隔にならぶ点を生成し座標を取得する。

交点座標は緯度経度で表現する。緯度経度は10進法で表現する。次で使うGoogle Mapsが緯度経度を10進法にて受け取るから。60進法(DMS)でも受け取るが、記号を付すなど面倒くさい。

以上作業には、purrr::map2()を用いる。都県毎に並列処理する。都県数は2021年3月時点で77。現時点でRで繰り返し処理するなら、purrr::map()決定版。forループを回すよりもかなり速いうえにコードを簡単にできる。チートシートにならって、層別してネストする。求める交点は、都県別に同じ数だけ得られるとありがたい。なので、都県毎にネストする。都県名はNAME_1という変数に格納されている。

並列処理は、マルチスレッド化するfuture()パッケージやfurrr()パッケージを併用すると速くなるらしい。ただし、Windows上ではマルチコアは使えない。わたしの環境で使おうとすると、メモリ読み込みエラーが発生することがある。速度もさほど変わらない気がする。計算専用機を準備しろ、ということかもしれない。いいけどねぇ。まぁ、おまじないがわりに。Markdownを生成しhtmlをコンパイルするときは素晴らしい速度になる。うれぴー。

purrr::map2()内には、いわゆるラムダ式で関数を書き込むと楽である。別に関数を用意してもいいし、functionを埋め込んでもよい。前者は見失う可能性があり、後者はいかにも芸がない。

ここでつかったdplyr::sample_n()は、とっても便利。指定した点から、sizeで指定した数だけランダムサンプリングしてくれる。ここでは、都県毎に得た点をランダムに50つずつ抽出した。昔からあったsample関数よりもtidyなデータを取り扱うことに向いている。計算速度はあまりかわらない気がする。

サンプリングした各点について、都県毎にシリアルナンバーを振ってバインドして保存したらここはおしまい。都県内で場所による影響があるかもしれないので、あとから検討しやすいように1から50までの値をシリアルナンバーとして割り当てる。Chiang mai県の28番、みたいに識別できればよい。

衛星画像処理はとかく計算時間がかかる。全体をいくつかに分割、結果を保管すると再利用時に役立つ。保存後はgc()を忘れずに。

Rコードはこんなかんじ。

## fix sampling points / location from sf data
th_sf_sample <-
  th_sf %>%
  group_by(NAME_1) %>%
  nest() %>%
  # Make grids on the map by province
  dplyr::mutate(
    province_grid_centers = furrr::future_map(
      data,
      ~
        # Make grids
        sf::st_make_grid(
          x = .,
          cellsize = cellsize,
          what = "centers"
        ) %>%
        # Obtain crossing point of the grids
        sf::st_intersection(.) %>%
        # Bind the coordinates of crossing points.
        base::do.call(rbind, .) %>%
        dplyr::as_tibble() %>%
        stats::setNames(c("lon","lat")
        )
    )
  ) %>%
  # Sample the coordinates of crossing points by province
  dplyr::mutate(
    sample_grid = furrr::future_map2(
      .x = province_grid_centers,
      .y = sample_size,
      .f =
        ~
        dplyr::sample_n(.x,
                        size = .y,
                        replace = FALSE
        )
    )
  ) %>%
  tidyr::unnest(sample_grid) %>%
  dplyr::bind_cols(
    individual = factor(
      rep(
        c(
          1:sample_size
        ),
        times = length(th_sf$NAME_1)
      )
    )
  )
# ## save all the results
# Note
# Comment out when not in use. The procedure below takes long computation period.
# # This process needs extremely long periods.
# saveRDS(th_sf_sample, "th_sf_sample.rds")
# gc()
# gc()
# gc()
#

# Obtain satellite imagery from Google maps
#

衛星画像を取得する

Google Maps APIを利用して衛星画像を取得する。都県毎に繰り返す処理があるから、purrr::map2()を使う。昔ならapply系関数を使っていた。衛星画像取得に必要な設定は以下に示すとおり。

  • lon:経度。画像中心になる画像を示す。
  • lat:緯度。画像中心になる画像を示す。
  • maptype:地図種類を指定する。今回はGoogle earthが提供する衛星画像を選択する。衛星画像以外にも、Google mapsが提供する各種地図を使える。調査地点を紹介する地図を作成するときはとても便利。
  • format:画像種類。pngがファイルサイズが小さく扱いやすいだろう。
  • zoom:おそらくこれが重要。取得する画像倍率を調整する。Google独自基準でズームされるので、適宜設定するよりない。いろいろ試して、より詳細かつ格子幅を超えない範囲を撮影する画像を取得する。

どういうわけかラムダ式が使えないから、関数を埋め込む。設定は直打ちで十分。

取得した衛星画像を取得したリストの隣に、解像度を調整するための変数を加える。変数名冒頭を共通にしておけば、あとから縦型データに変換するときに処理が楽。同じ理屈で色をつくるための変数も加えてしまう。詳細な使い方は次節。

今回は都県毎に50枚ずつ衛星画像を取得した。何度も取得すると課金されかねないので、生成物ごと.rds形式にて保存してしまう。これであとから衛星画像を再利用できる。必要ならば何度でも再取得すればよい。課金についてはおもったほどかからないから、あまり心配する必要もないようにおもわれる。

生成された変数内に格納された画像はファイルサイズが大きい。保存するファイルも当然大きなサイズになる。保存には時間がかかるから、注意。だいたい3.5GBくらい。必要がない変数は、dplyr::select()で削除してしまうほうがよい。以降の処理も同様。

Rコードはこんなかんじ。

th_sf_sample <- readRDS("th_sf_sample.rds")
#
th_sf_rgb <-
  # th_sf_sample$sample_grid %>%
  # # bind the grids' coordinates together
  # base::do.call(dplyr::bind_rows,.) %>%
  # obtain the satellite imagery
  th_sf_sample %>%
  dplyr::mutate(
    ggmap_image_satellite = purrr::map2(
      lon,
      lat,
      function(lon, lat)
      {
        ggmap::get_map(
          location = c(
            lon = lon,
            lat = lat
          ),
          maptype = "satellite",
          size = c(320, 320),
          scale = 1,
          format = "png",
          zoom = 18 # <- BEWARE!!
        )
      }
    )
  ) %>%
  # Note on factor number and aggregate function.
  # The factor numbers depend on our previous experiments.
  # 2, 4, and 8 need too long computation period 
  dplyr::mutate(
    fact_number_10 = c(10), # dense
    fact_number_20 = c(20),  # modest
    fact_number_20 = c(40)  # rough
  ) %>%
  tidyr::gather(key = "fn",  value = "fact_number", starts_with("fact_number_")) %>%
  # Note on the aggregate function
  # The function, "mean", depends on our previous experiments.
  mutate(
    aggregate_fun_mean = c("mean")
  ) %>%
  tidyr::gather(
    key = "af",  
    value = "aggregate_fun", 
    starts_with("aggregate_fun_")
  ) %>%
  # omit unnecessary variables
  dplyr::select(
    -data, 
    -province_grid_centers, 
    - fn, 
    -af
  ) %>%
  # change position of the values
  dplyr::relocate(
    ggmap_image_satellite, 
    .after = last_col()
  )
#
# ## save all the results
# Note
# Comment out when not in use. The procedure below takes long computation period.
# # This process needs extremely long periods.
saveRDS(th_sf_rgb, "th_sf_rgb.rds")
gc()
gc()
gc()
#

# Rasterise the obtained satellite imagery

ラスタ化する

ラスタ化とは、画像をタイルのように分割することを指す。そのままでは巨大なファイルサイズとなりがちな画像を、小さなファイルサイズにて取り扱い可能となる。

ラスタ化は、小さいタイルをくっつけて大きなタイルを作成する作業である。どの程度大きくするか、タイルついている色を小さなタイルにすでについている複数の色から大きなタイルの色をどう決めるかが鍵になる。

ラスタライズするには、bounadry box属性があるほうがよい。取得した衛星画像には属性がついていないから、base::attr()関数で付加する。このあたりのコードはStackoverflowで見かけたコードそのまんま。

ラスタ化自体は、Rはraster::aggregate()という関数を提供する。設定する変数は以下に示すとおり。

  • x:地理的属性を付した画像データを与える。
  • aggregate_fun:タイル色を決める変数。大きなタイルを構成する小さいタイルの色の平均(RGB値の平均)を採用する関数がデフォルト。最大と最小と最頻値もある。最頻値なんて、ここでしか見たことがない。
  • fact_number:タイルサイズを決める変数。タイルサイズは与えた数値倍になる。すなわち、生成されたラスタに含まれるタイル数は、与えた数値の逆数(\(\frac{1}{与えた数値}\)\(\times\) 与えた数値の逆数(\(\frac{1}{与えた数値}\))になる。デフォルトは1。ただタイルにするだけ。

変数が多いので、並列処理はpurrr::pmap()関数を使う。この関数は少々クセがあるからあまり使いたくないけれど、便利は便利。それでも計算時間はかかる。1時間くらい。なにかしに出かける前に計算させておくといいとおもう。

タイル色決定にあたってどの関数をつかえばいいか、タイルサイズをどの程度にすればよいか、明示的な基準はないようである。どの関数を使えばよいやら。タイルサイズが小さければもともとの画像がもつ情報はさほど縮約されないがデータサイズは大きくなる。逆もまた然り。「もともとの画像にみられる構造物が識別できるか」「明暗がさほどかわらないか」を目視で確認するために、

  • aggregate_fun:mean, min, max, modis
  • fact_number:2, 4, 8

を設定してラスタライズ、各パターンを比較した。結果、タイル色はデフォルト通り、タイルサイズは4がいいだろうということになった。しかしこれが後で大変なことになる。いや、解析に時間がかかりすぎるんですよ。ベイズ推定なんてしようとすると、いつまでたっても終わらない。さて、どうしたものか。

Rコードはこんなかんじ。

th_sf_rgb <- readRDS("th_sf_rgb.rds")
#
th_sf_raster <-
  th_sf_rgb %>%
  dplyr::mutate(
    image_raster = purrr::map(
      ggmap_image_satellite,
      function(map){
        map_bbox <-
          # Add "boundary box" to the ggmap objects
          base::attr(
            map,
            "bb"
          )
        # change range of the boundary box of ggmap object
        .extent <-
          raster::extent(
            as.numeric(map_bbox[c(2,4,1,3)])
          )
        # rasterise the ggmap object
        my_map <-
          raster::raster(
            x = .extent,
            nrow= nrow(map),
            ncol = ncol(map)
          )
        # obtain color information by RGB format
        rgb_cols <- stats::setNames(as.data.frame(t(grDevices::col2rgb(map))), c('red','green','blue'))
        red <- my_map
        raster::values(red) <- rgb_cols[["red"]]
        green <- my_map
        values(green) <- rgb_cols[["green"]]
        blue <- my_map
        values(blue) <- rgb_cols[["blue"]]
        stack(red,green,blue)
      }
    )
  ) %>%
  # aggregate the imagery using raster:;aggregate() function
  dplyr::mutate(
    aggregate_image_raster = purrr::pmap(
      list(
        image_raster = image_raster,
        aggregate_fun=aggregate_fun,
        fact_number=fact_number
      ),
      function(image_raster, aggregate_fun, fact_number){
        raster::aggregate(x = image_raster,
                          fun = aggregate_fun,
                          fact = fact_number
        )
      }
    )
  )
# ## save all the results
# Note
# Comment out when not in use. The procedure below takes long computation period.
# # This process needs extremely long periods.
saveRDS(th_sf_raster, "th_sf_raster.rds")
gc()
gc()
gc()
#

## make a dataset

ラスタ化した画像から色情報を取得する

いよいよ前半の山場。RGB色空間抽出。

ラスタ化した画像を構成するピクセル(画素。画像を構成するタイルのようなもの)は、ひとつの色を保持する。画像はこのピクセルが構成する集合である。ピクセルが保持する色分布がわかれば、画像の特徴を色という観点から表現できよう。

こうした発想自体は、珍しくはない。リモートセンシングで頻用されるNDVI(正規化植生指数)は、衛星が獲得した色情報を用いて作成した指標である。指標は基本的に単変量であるから扱いやすい。しかし、指標作成に用いる各変量が保持する情報が死んでしまう。色に関する情報があるならば、情報をフル活用したいではないですかぁ~。

RGB色空間を表現するために、Google Mapsから取得した衛星画像は、RGBにて色情報を保持する。ならば、この色情報を取り出せば、上記目標を達成できると考えられる。

そんな好事家のために、Rは関数を用意してくれている。raster::rasterToPoints()という関数が色を拾ってくれる。

今回書いたコード中、コンパイル時間がもっとも長い箇所でもある。ちょっと大変。だいたい30分くらいかかる。必要なデータは、RGBに関する情報と位置、都県名のみである。それ以外の情報は必要ないのですべて除去する。もったいつけてラスタ化した画像など保存したならば、ファイルサイズがは100GBを超える(128GBまではやった)し、処理時間が何日でもかかることになる。ラスタ画像が必要ならば、前段でやった手順で生成すればよい。そのほうがはるかに時間を節約できる。

Rコードはこんなかんじ。

th_sf_raster <- readRDS("th_sf_raster.rds")
gc()
gc()
gc()
#
# # Magic spelling for multisession computation
# future::plan(multisession, workers = 4)
# plan()
#
# Transform the rasterized imagery into point with RGB numbers data
th_sf_data <-
  th_sf_raster %>%
  dplyr::select(
    NAME_1,
    individual,
    fact_number,
    aggregate_fun,
    aggregate_image_raster
  ) %>%
  mutate(
    aggregate_image_rasterpoint = furrr::future_map(
      aggregate_image_raster,
      ~
        as_tibble(
          rasterToPoints(.)
        )
    )
  ) %>%
  # omit unnecessary valuables, nest, and groups
  tidyr::unnest(aggregate_image_rasterpoint) %>%
  dplyr::select(-aggregate_image_raster) %>%
  ungroup(.)
#
# ## save all the results
# Note
# Comment out when not in use. The procedure below takes long computation period.
# This process needs extremely long periods.
saveRDS(th_sf_data, "th_sf_data.rds")
# garbage collection
gc()
gc()
gc()
#

# ## make rasterplots from the data above for confirmation

画像を描画・確認する

衛星画像を再現できれば、ラスタ化は成功。できなければ、どこかに問題がある。よくあるのは、一部のピクセルしか描画されないことであろうか。

この処理もけっこう時間がかかる。色がついたタイルを\((解像度)^{2}\)だけ描くから、当然に時間がかかるとみてよい。しかもすべて描くとなると、かなり時間がかかる。しかし実は、すべて描く必要はない。確認用に100枚程度描画できれば十分でしょう。

補遺 丸め誤差を処理して発散した観測値を除去して

上記を一通りやって、データをつくって記述統計量を算出しヒストグラムを描画した。なんだか様子がおかしい。だいたい解決したので、メモを残す。

取得したRGB値で表現する色空間を用いると、さまざまな指標を算出できる。とはいえ、よくつかうNDVIは算出できない。ここでは、かわりに種々提案されるRGBのみから算出可能な指標を計算する。ドローン(UAV)発達とともに、RGBだけで計算できる指標は様々提案されまくりである。一例として、VARIを挙げる。植生量を評価するための指標で、タイやベトナムのように田畑や山林が多く残存しかつ住宅や工場と近接する国にはもってこいである。たいていの衛星写真に田畑や山林が含まれることは、こうした判断を補強するでしょう。

VARIは、以下にて定義される。

\[ VARI = \frac{Green - Red}{Green + Red -Blue} \]

VARIを計算することは造作もない。しかし、ワナがひそんでいる。記述統計量を計算すればInfやーInfやNanが算出される。Infと-Infは分母に対して著しく分子の絶対値が大きくて発散している。NaNは分母がゼロになっている。VARIの定義からして、ありえることである。いつ発散するかは、画像を眺めても予想できない。

発散するひとつの原因は丸め誤差である。RかGかBがごく大きい(ないし小さい)値になれば、それだけで発散する危険がある。竹澤先生によれば、コンピュータにつきものである数値演算誤差がRにもあるらしい。なので、VARI分子と分母を各々丸めてあげる。全体を丸めても意味がない。いずれか片方で数値誤差が発生すれば、VARIが発散する。たったこれだけの処理で、ずいぶんと発散する頻度は低下する。いままで発散しそうな値や値を組み合わせてつくる発散しやすい指標を取り扱うことがなかったから、いい勉強になった。

丸めてあげても発散することは考えられる。実際、発散する。こうなってはVARIが発散した観測値を取り除くしかない。is.inf()関数やis.nan()関数を用いて、VARIが発散したかどうか評価しつつ発散したVARIを含む観測値を除去する。ここまでやれば、発散した値を含まなくなる。

結局、VARI範囲は\(-Inf<VARI<Inf\)から\(-70<VARI<70\)にまで落ち着いた。それでもかなり広い気がするが、定義上仕方がないことなんでしょ。他指標も試してみようかな。RGBでつくれる指標はまだまだあるみたいだし。

Rコードはこんなかんじ。発散したことに気が付かずおかしなデータをなんとか使えるようにするために、RGBを正規化した痕跡が残っている。

# read original data
th_sf_data <- readRDS("th_sf_data.rds") 
# compute the index
# NOTE
# So many kinds of index are there!!
th_sf_image_index <- 
  th_sf_data %>%
  # limit cases just for trial
  # Comment out when not in use.
  # dplyr::filter(fact_number == 40) %>% 
  # replace variables' name
  data.table::setnames(
    c(
      "province",
      "individual",
      "fact_number",
      "aggregate_fun",
      "lon",
      "lat",
      "red",
      "green",
      "blue"
      )
    ) %>% 
  # # to avoid dividion by zero, add 1 to teh RGB element
  # dplyr::mutate(
  #   red = red + 1,
  #   green = green + 1,
  #   blue = blue + 1,
  # ) %>% 
  group_by(province, individual) %>% 
  nest() %>% 
  # limit cases just for trial
  # Comment out when not in use.
  # dplyr::filter(individual == "1") %>% 
  dplyr::mutate(
    scale.red = purrr::map(
      data,
      ~
        scale(.$red)
    ),
    scale.green = purrr::map(
      data,
      ~
        scale(.$green)
    ),  
    scale.blue = purrr::map(
      data,
      ~
        scale(.$blue)
    )
  ) %>% 
  ungroup() %>% 
  unnest(
    cols = c(
      "data",
      "scale.red",
      "scale.green",
      "scale.blue"
      )
    ) %>% 
  data.table::setnames(
    c(
      "province",
      "individual",
      "fact_number",
      "aggregate_fun",
      "lon",
      "lat",
      "red",
      "green",
      "blue",
      "scale.red",
      "scale.green",
      "scale.blue"
    )
  ) %>% 
  dplyr::mutate(
    red = round(as.numeric(red)),
    green = round(as.numeric(green)),
    blue = round(as.numeric(blue)),
    scale.red = as.numeric(scale.red),
    scale.green = as.numeric(scale.green),
    scale.blue = as.numeric(scale.blue)
  ) %>% 
  # Warning!!
  # To avoid numerical calculation error, we should round the numerator / denominator both
  # In detail, refer to the following page.
  # http://cse.naro.affrc.go.jp/takezawa/r-tips/r/37.html
  dplyr::mutate(
    vari = round((green - red)) / round((green + red - blue)),
    scale.vari = round((scale.green - scale.red) / (scale.green + scale.red - scale.blue)) 
  ) %>% 
  # No matter how we make effort to avoid divergence, it often emerges.
  # To omit observation including that, we should detect that and omit. 
  dplyr::mutate(
    fugainf = is.finite(vari),
    fuganan = is.nan(vari)
  ) %>% 
  dplyr::filter(fugainf == "TRUE" & fuganan == "FALSE") %>% 
  dplyr::select(-fugainf, -fuganan)
# # ## save all the results
# # Note
# # Comment out when not in use. The procedure below takes long computation period.
# # # This process needs extremely long periods.
# saveRDS(th_sf_image_index, "th_sf_image_index.rds")
# gc()
# gc()
# gc()
# 
# #
# ##
# ### --- END --- ###

さて、ようやく作図にかかれるかな。


yuzuruweb TOP