-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
05-geometry-operations-ja.Rmd
974 lines (798 loc) · 60.7 KB
/
05-geometry-operations-ja.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
# ジオメトリ演算 {#geometry-operations}
```{r, include=FALSE}
source("code/before_script.R")
```
## 必須パッケージ {- #prerequisites-05}
- この章では、Chapter \@ref(spatial-operations) と同じパッケージを使用するが、Chapter \@ref(spatial-class) でインストールされた **spDataLarge** を追加している。
```{r 05-geometry-operations-1, message=FALSE}
library(sf)
library(terra)
library(dplyr)
library(spData)
library(spDataLarge)
```
## イントロダクション {#introduction-05}
これまで本書では、地理データセットの構造 (Chapter \@ref(spatial-class))、地理以外の属性 (Chapter \@ref(attr)) と空間関係 (Chapter \@ref(spatial-operations)) に基づく操作方法について説明してきた。
この章では、バッファ作成、ベクタの簡略化と変換、ラスタデータの集計や最サンプルなど、空間オブジェクトの地理的要素の操作に重点を置いている。
この本を読めば (そして最後にある演習を試した後)、`sf` オブジェクトのジオメトリ列と、他の地理的オブジェクトとの関係でラスタに表されたピクセルの範囲と地理的位置を理解し、コントロールできるようになるはずである。
Section \@ref(geo-vec) は、「単項」と「二項」演算によるベクタジオメトリの変換を扱う。
単項演算 (unary operation) とは、単体のジオメトリに対して、線分やポリゴンの単純化、バッファや中心点の作成、「アフィン変換」による単体のジオメトリの移動・拡大・縮小・回転などを行う (Section \@ref(simplification) ~ Section \@ref(affine-transformations) を参照)。
一方、二項変換 (binary transformations) とは、あるジオメトリを別のジオメトリの形状に基づいて変更するもので、Section \@ref(clipping) で切り取り (clip) を、Section \@ref(geometry-unions) で結合\index{べくた@ベクタ!けつごう@結合} (union) を説明する。
ジオメトリ型の変換 (例えば、ポリゴンからラインへの変換) は、Section \@ref(type-trans) で実際に行う。
Section \@ref(geo-ras) は、ラスタオブジェクトの幾何学変換を扱っている。
これは、基本となる画素のサイズと数を変更し、新しい値を割り当てるというものである。
ラスタの解像度 (ラスタ集計・分解ともいう)、範囲、原点を変更する方法を教える。
これらの操作は、異なるソースのラスタデータセットの位置合わせを行う場合に特に有効である。
整列されたラスタオブジェクトは、ピクセル間の一対一の対応を共有し、Section \@ref(map-algebra) で説明されているマップ代数演算を使用して処理することができる。
Section \@ref(raster-vector) では、ベクタオブジェクトとラスタオブジェクトの間の操作をカバーする。
ラスタ値をベクタジオメトリで「マスク」し、「抽出」する方法を紹介する。
重要なのは、ラスタデータを「ポリゴン化」し、ベクタデータを「ラスタ化」する方法を示し、この2つのデータモデルをより互換性のあるものにすることである。
## ベクタデータに対するジオメトリ操作 {#geo-vec}
ここでは、ベクタ (`sf`) オブジェクトのジオメトリを何らかの方法で変更する操作について説明する。
前の章 (Section \@ref(spatial-vec)) で紹介した空間データ操作よりも高度なもので、ここではジオメトリを掘り下げていくことがある。
このセクションで説明する関数は、クラス `sf` のオブジェクトに加えて、クラス `sfc` のオブジェクトにも作用する。
### 簡略化 {#simplification}
\index{べくた@ベクタ!かんりゃくか@簡略化}
簡略化とは、通常、縮尺の小さい地図で使用するために、ベクタオブジェクト (線やポリゴン) を一般化する処理のことである。
オブジェクトを単純化するもう一つの理由は、それらが消費するメモリ、ハードディスク容量、ネットワーク帯域幅の量を減らすためである。
インタラクティブ地図として公開する前に、複雑な形状を簡略化することが賢明だろう。
**sf** パッケージは `st_simplify()` を提供する。これは Douglas-Peucker アルゴリズムの実装を使用して、頂点数を削減するものである。
`st_simplify()` は、`dTolerance` を使用することで、一般化のレベルを地図で使われている単位で制御することができる [詳細は @douglas_algorithms_1973]。
Figure \@ref(fig:seine-simp) は、セーヌ川とその支流を表すジオメトリ `LINESTRING` を簡略化したものである。
以下のコマンドで簡略化したジオメトリを作成してみよう。
```{r 05-geometry-operations-2}
seine_simp = st_simplify(seine, dTolerance = 2000) # 2000 m
```
```{r seine-simp, echo=FALSE, fig.cap="seine のオリジナルと簡略化した形状の比較。", warning=FALSE, fig.scap="Simplification in action.", message=FALSE, fig.asp=0.5, dev="ragg_png"}
library(tmap)
p_simp1 = tm_shape(seine) + tm_lines() +
tm_title("オリジナルデータ") + tm_layout(fontfamily = "HiraginoSans-W3")
p_simp2 = tm_shape(seine_simp) + tm_lines() +
tm_title("st_simplify")
tmap_arrange(p_simp1, p_simp2, ncol = 2)
```
ここでできた `seine_simp` オブジェクトは、元の `seine` のコピーであるが、頂点の数は少なくなっている。
これは明らかで、以下の検証のように、結果は視覚的にシンプルになり (Figure \@ref(fig:seine-simp)、右)、元のオブジェクトよりもメモリ消費が少ない。
```{r 05-geometry-operations-3}
object.size(seine)
object.size(seine_simp)
```
\index{べくた@ベクタ!かんりゃくか@簡略化}
簡略化はポリゴンにも適用できる。
下の例は、米国本土 `us_states` を表している。
```{r 05-geometry-operations-5}
us_states_simp1 = st_simplify(us_states, dTolerance = 100000) # 100 km
```
`st_simplify()` の制限として、ジオメトリ単位でオブジェクトを簡略化することが挙げられる。
このため、「トポロジー」が失われ、Figure \@ref(fig:us-simp) (パネル右上) に示すような、重なり合った「穴のあいた」面単位になってしまうのである。
**rmapshaper** の `ms_simplify()` が代替となる。
デフォルトでは、Douglas-Peucker アルゴリズムのいくつかの制限を克服した Visvalingam アルゴリズムが使用される [@visvalingam_line_1993]。
<!-- https://bost.ocks.org/mike/simplify/ -->
次のコードチャンクは、この関数を使用して、`us_states` を簡略化している。
結果は入力 (引数 `keep` で設定) の 1% の頂点しか持たないが、`keep_shapes = TRUE` を設定したため、オブジェクトの数はそのままである。^[
マルチポリゴンオブジェクトの簡略化では、`keep_shapes` 引数が TRUE に設定されていても、内部の小さなポリゴンが削除されることがある。これを防ぐには、`explode = TRUE` を設定する必要がある。このオプションは、単純化する前に、すべてのマルチポリゴンを個別のポリゴンに変換する。
]
```{r 05-geometry-operations-6, warning=FALSE, message=FALSE}
# 保持するポイントの割合 (0~1、デフォルト 0.05)
us_states_simp2 = rmapshaper::ms_simplify(us_states, keep = 0.01,
keep_shapes = TRUE)
```
\index{べくた@ベクタ!かんりゃくか@簡略化}
簡略化の代わりに、ポリゴンや線のジオメトリの境界を平滑化するという方法もあり、**smoothr** パッケージ\index{smoothr (package)}で実装されている。
平滑化はジオメトリのエッジを補間するため、必ずしも頂点の数が少なくなるわけではないが、ラスタを空間的にベクトル化したジオメトリを扱うときに特に有用である (このトピックは Chapter \@ref(raster-vector) で説明する)。
**Smoothr** は、Gaussian kernel 回帰、Chaikin's corner cutting アルゴリズム、スプライン補間の 3 つの平滑化手法を実装しており、パッケージ vignette と[web](https://strimas.com/smoothr/)で説明されている。
`st_simplify()` と同様に、平滑化アルゴリズムは「トポロジー」を保存しないことに注意。
**smoothr** の主要関数は `smooth()` であり、 `method` 引数は使用する平滑化手法を指定する。
以下は、Gaussian kernel 回帰を使用して、`method=ksmooth` を使用して米国の州の境界線を滑らかにする。
引数 `smoothness` は、形状を滑らかにするために使用するガウスの帯域幅を制御する。デフォルト値は 1。
```{r 05-geometry-operations-6b, warning=FALSE}
us_states_simp3 = smoothr::smooth(us_states, method = "ksmooth", smoothness = 6)
```
最後に、元のデータセットと 2 つの簡易版を視覚的に比較してみよう。
Figure \@ref(fig:us-simp) で、Douglas-Peucker (`st_simplify`)、Visvalingam (`ms_simplify`)、Gaussian kernel 回帰 (`smooth(method=ksmooth`) アルゴリズムの出力に違いがあることがわかる。
```{r us-simp, echo=FALSE, fig.cap="ポリゴンの簡略化。sf (右上)、rmapshaper (左下)、smoothr (右下) の各パッケージの関数で生成された簡略版と元のアメリカ合衆国のジオメトリ形状を比較。", warning=FALSE, fig.scap="Polygon simplification in action.", dev="ragg_png"}
library(tmap)
p_ussimp1 = tm_shape(us_states) + tm_polygons() + tm_title("オリジナルデータ") + tm_layout(fontfamily = "HiraginoSans-W3")
p_ussimp2 = tm_shape(us_states_simp1) + tm_polygons() + tm_title("st_simplify")
p_ussimp3 = tm_shape(us_states_simp2) + tm_polygons() + tm_title("ms_simplify")
p_ussimp4 = tm_shape(us_states_simp3) + tm_polygons() + tm_title('smooth(method = "ksmooth")')
tmap_arrange(p_ussimp1, p_ussimp2, p_ussimp3, p_ussimp4, ncol = 2, nrow = 2)
```
### 重心 {#centroids}
\index{べくた@ベクタ!じゅうしん@重心}
重心 (centroid) 演算は、地理的な物体の中心を特定するものである。
統計的な中心傾向の測定 (「平均」の平均値や中央値の定義を含む) と同様に、物体の地理的な中心を定義する方法はたくさんある。
いずれも、より複雑なベクタオブジェクトの一点表現を作成する。
重心の操作で最もよく使われるのは、<u>地理的重心</u>である。
このタイプの重心操作 (「セントロイド」とも呼ばれる) は、空間オブジェクトにおける質量の中心を表す (指の上で皿のバランスをとることを想像してほしい)。
地理的重心は、複雑な形状をシンプルな点で表現したり、ポリゴン間の距離を推定したりと、さまざまな用途に利用されている。
これらは、以下のコードで示すように、**sf** 関数 (`st_centroid()`) で計算することができる。では、New Zealand の地域とセーヌ川の支流の地理的重心 (Figure \@ref(fig:centr) の黒い点で示される) を生成してみよう。
```{r 05-geometry-operations-7, warning=FALSE}
nz_centroid = st_centroid(nz)
seine_centroid = st_centroid(seine)
```
地理的重心は、親オブジェクトの境界の外にあることもある (ドーナツを想像すればよい)。
このような場合、親オブジェクトに点があることを保証するために、*point on surface* オペレーションを使用することができ (例: 島国のような不規則なマルチポリゴンオブジェクトのラベル付け)、Figure \@ref(fig:centr) の赤い点に示されている。
これらの赤い点は、常に親オブジェクトの上にあることに確認しておこう。
`st_point_on_surface()` を使い、以下のように作成する。^[
`st_point_on_surface()` の動作の説明は、https://gis.stackexchange.com/a/76563/20955 に記載されている。
]
```{r 05-geometry-operations-8, warning=FALSE}
nz_pos = st_point_on_surface(nz)
seine_pos = st_point_on_surface(seine)
```
```{r centr, warning=FALSE, echo=FALSE, fig.cap="New Zealand の地域 (左) とセーヌ川 (右) のデータセットの重心 (黒い点) と「サーフェス上の点」(赤い点)。", fig.scap="Centroid vs point on surface operations.", dev="ragg_png"}
p_centr1 = tm_shape(nz) + tm_polygons(col = "gray80", fill = "gray90") +
tm_shape(nz_centroid) + tm_symbols(shape = 1, col = "black", size = 0.5) +
tm_shape(nz_pos) + tm_symbols(shape = 1, col = "red", size = 0.5) +
tm_layout(scale = 1.6)
p_centr2 = tm_shape(seine) + tm_lines(col = "gray80") +
tm_shape(seine_centroid) + tm_symbols(shape = 1, col = "black", size = 0.5) +
tm_shape(seine_pos) + tm_symbols(shape = 1, col = "red", size = 0.5) +
tm_add_legend(type = "symbols", shape = 1, col = c("black", "red"),
labels = c("重心", "サーフェス上の点")) +
tm_layout(scale = 1.6)
tmap_arrange(p_centr1, p_centr2, ncol = 2)
```
重心には、他にも<u>チェビシェフ中心</u>や<u>ビジュアル中心</u>などが存在する。
ここでは深入りしないが、Chapter \@ref(algorithms) で見るように、R を使って計算することが可能である。
### バッファ {#buffers}
\index{べくた@ベクタ!ばっふぁ@バッファ}
バッファとは、フィーチャから一定距離内の領域を表すポリゴンのことである。
入力が点、線、ポリゴンのいずれであっても、出力はバッファとなる。
簡略化 (可視化やファイルサイズの縮小によく使われる) とは異なり、バッファ作成は地理的なデータ解析に使われる傾向がある。
この線から所定の距離内にある点はいくつあるか?
この新店舗から移動可能な距離にあるのは、どのような層なのだろうか?
このような疑問には、関心のある地理的要素の周囲にバッファを作成することで回答し、可視化することができる。
Figure \@ref(fig:buffs) は、セーヌ川と支流を囲む様々な大きさの緩衝地帯 (5 km と 50 km)を示している。
これらのバッファは以下のコマンドで作成できる。コマンド `st_buffer()` は少なくとも 2 つの引数を必要とする。入力ジオメトリと、CRS の単位 (この場合はメートル) で指定された距離である。
```{r 05-geometry-operations-9}
seine_buff_5km = st_buffer(seine, dist = 5000)
seine_buff_50km = st_buffer(seine, dist = 50000)
```
```{r buffs, echo=FALSE, fig.cap="Seine データセット周辺の 5 km (左) と 50 km (右) のバッファ。ジオメトリフィーチャごとに 1 つのバッファが作成されることを反映した色に注目。", fig.show='hold', out.width="100%", fig.scap="Buffers around the seine dataset.", dev="ragg_png"}
p_buffs1 = tm_shape(seine_buff_5km) + tm_polygons(fill = "name") +
tm_shape(seine) + tm_lines() +
tm_title("5 km バッファ") +
tm_layout(legend.show = FALSE, fontfamily = "HiraginoSans-W3")
p_buffs2 = tm_shape(seine_buff_50km) + tm_polygons(fill = "name") +
tm_shape(seine) + tm_lines() +
tm_title("50 km バッファ") +
tm_layout(legend.show = FALSE, fontfamily = "HiraginoSans-W3")
tmap_arrange(p_buffs1, p_buffs2, ncol = 2)
```
```{block2 05-geometry-operations-10, type = "rmdnote"}
`st_buffer()` には、追加の引数がある。
最も重要な引数は、以下の通り。
- `nQuadSegs` (GEOS\index{GEOS} エンジン使用時): これは「1 象限あたりの分割数」を意味し、デフォルトでは 30 に設定されている (バッファで作られる円は $4 \times 30 = 120$ ラインで構成されることを意味する)。
この引数が有用な例外的なケースとしては、バッファ操作の出力によって消費されるメモリが大きな懸念材料である場合 (この場合は減らす)、または非常に高い精度が必要な場合 (この場合は増やす)。
- `max_cells` (S2\index{S2} エンジン使用時): 値が大きいほどバッファが滑らかになるが、時間がかかる
- `endCapStyle` と `joinStyle` (GEOS エンジン使用時): バッファの縁の見た目を制御する
- `singleSide` (GEOS エンジン使用時): バッファを入力ジオメトリの片側につくるか両側につくるかを制御する
```
```{r nQuadSegs, eval=FALSE, echo=FALSE}
# nQuadSegs の例
seine_buff_simple = st_buffer(seine, dist = 50000, nQuadSegs = 3)
plot(seine_buff_simple, key.pos = NULL, main = "50 km buffer")
plot(seine, key.pos = NULL, lwd = 3, pal = rainbow, add = TRUE)
seine_points = st_cast(seine[1, ], "POINT")
buff_single = st_buffer(seine_points[1, ], 50000, 2)
buff_points = st_cast(buff_single, "POINT")
plot(st_geometry(buff_single), add = TRUE)
```
```{r buffargs, eval=FALSE, echo=FALSE}
seine_wgs = st_transform(seine, "EPSG:4326")
plot(st_buffer(seine, 5000))
plot(st_buffer(seine_wgs, 5000))
plot(st_buffer(seine, 5000, nQuadSegs = 1))
plot(st_buffer(seine_wgs, 5000, nQuadSegs = 1)) # no effect
plot(st_buffer(seine, 5000, max_cells = 10)) # no effect
plot(st_buffer(seine_wgs, 5000, max_cells = 100))
plot(st_buffer(seine, 5000, endCapStyle = "FLAT", joinStyle = "MITRE"))
plot(st_buffer(seine_wgs, 5000, endCapStyle = "FLAT", joinStyle = "MITRE")) # no effect
plot(st_buffer(seine, 5000, singleSide = TRUE))
plot(st_buffer(seine_wgs, 5000, singleSide = TRUE)) # no effect
```
### アフィン変換 {#affine-transformations}
\index{べくた@ベクタ!あふぃんへんかん@アフィン変換}
アフィン変換とは、直線と平行を保存する変換のことである。
ただし、角度や長さは必ずしも保存されるとは限らない。
アフィン変換には、特に、平行移動、拡大縮小、回転が含まれる。
さらに、これらを任意に組み合わせて使用することも可能である。
アフィン変換はジオコンピュテーションに不可欠な要素である。
例えば、ラベルの配置には平行移動が必要であり、非連続領域のカルトグラムでは拡大縮小が使用され (Section \@ref(other-mapping-packages) 参照)、歪んだ地図や間違った投影に基づいて作成されたジオメトリを再投影または改善する際には多くのアフィン変換が適用される。
**sf** パッケージは `sfg` と `sfc` のクラスのオブジェクトに対してアフィン変換を実装している。
```{r 05-geometry-operations-11}
nz_sfc = st_geometry(nz)
```
平行移動は、すべての点を地図単位で同じ距離だけ移動させる。
ベクタオブジェクトに数値ベクタを追加することで、実現できるだろう。
例えば、以下のコードでは、すべての y 座標を北に 10 万メートル移動させ、x 座標はそのままにしている (Figure \@ref(fig:affine-trans) 左図)。
```{r 05-geometry-operations-12}
nz_shift = nz_sfc + c(0, 100000)
```
拡大縮小は、オブジェクトを係数倍ずつ拡大または縮小する機能である。
グローバルにもローカルにも適用可能である。
グローバル拡大縮小は、すべてのジオメトリの位相関係を維持したまま、原点座標を基準としてすべての座標値を増減させる。
`sfg`、`sfc` オブジェクトの減算または乗算によって行うことができる。
```{r 05-geometry-operations-13, echo=FALSE,eval=FALSE}
nz_scale0 = nz_sfc * 0.5
```
ローカル拡大縮小はジオメトリを独立して扱い、ジオメトリが拡大縮小されるポイント (重心など) を必要とする。
以下の例では、各ジオメトリは重心を中心に2倍に縮小されている (Figure \@ref(fig:affine-trans) 中央)。
そのために、まず各オブジェクトは、その中心が `0, 0` (`(nz_sfc - nz_centroid_sfc)`) の座標となるように移動される。
次に、ジオメトリのサイズを半分に縮小する (`* 0.5`)。
最後に、各オブジェクトの重心を入力データの座標に戻す (`+ nz_centroid_sfc`)。
```{r 05-geometry-operations-14}
nz_centroid_sfc = st_centroid(nz_sfc)
nz_scale = (nz_sfc - nz_centroid_sfc) * 0.5 + nz_centroid_sfc
```
2 次元座標の回転には、回転行列が必要である。
$$
R =
\begin{bmatrix}
\cos \theta & -\sin \theta \\
\sin \theta & \cos \theta \\
\end{bmatrix}
$$
時計回りにポイントを回転させる。
回転行列は R で次のように実装することができる。
```{r 05-geometry-operations-15}
rotation = function(a){
r = a * pi / 180 # 度をラジアンに変換
matrix(c(cos(r), sin(r), -sin(r), cos(r)), nrow = 2, ncol = 2)
}
```
`rotation` 関数は、1 つの引数 `a` (回転角度, angle) を度単位で受け取る。
重心のような選択された点を中心に回転させることができる (Figure \@ref(fig:affine-trans) 右図)。
その他の例については、`vignette("sf3")` を参照 (訳注: [日本語版](https://www.uclmail.net/users/babayoshihiko/R/index.html))。
```{r 05-geometry-operations-16}
nz_rotate = (nz_sfc - nz_centroid_sfc) * rotation(30) + nz_centroid_sfc
```
```{r affine-trans, echo=FALSE, fig.cap="アフィン変換 (平行移動、拡大縮小、回転)。", warning=FALSE, eval=TRUE, fig.scap="Affine transformations.", dev="ragg_png"}
st_crs(nz_shift) = st_crs(nz_sfc)
st_crs(nz_scale) = st_crs(nz_sfc)
st_crs(nz_rotate) = st_crs(nz_sfc)
p_at1 = tm_shape(nz_sfc) + tm_polygons() +
tm_shape(nz_shift) + tm_polygons(col = "red") +
tm_title("シフト") + tm_layout(fontfamily = "HiraginoSans-W3")
p_at2 = tm_shape(nz_sfc) + tm_polygons() +
tm_shape(nz_scale) + tm_polygons(col = "red") +
tm_title("拡大縮小") + tm_layout(fontfamily = "HiraginoSans-W3")
p_at3 = tm_shape(nz_sfc) + tm_polygons() +
tm_shape(nz_rotate) + tm_polygons(col = "red") +
tm_title("回転") + tm_layout(fontfamily = "HiraginoSans-W3")
tmap_arrange(p_at1, p_at2, p_at3, ncol = 3)
```
```{r 05-geometry-operations-17, echo=FALSE,eval=FALSE}
nz_scale_rotate = (nz_sfc - nz_centroid_sfc) * 0.25 * rotation(90) + nz_centroid_sfc
```
```{r 05-geometry-operations-18, echo=FALSE,eval=FALSE}
shearing = function(hx, hy){
matrix(c(1, hy, hx, 1), nrow = 2, ncol = 2)
}
nz_shear = (nz_sfc - nz_centroid_sfc) * shearing(1.1, 0) + nz_centroid_sfc
```
```{r 05-geometry-operations-19, echo=FALSE,eval=FALSE}
plot(nz_sfc)
plot(nz_shear, add = TRUE, col = "red")
```
最後に、新しく作成されたジオメトリは、`st_set_geometry()` 関数を使用して古いジオメトリを置き換えることができる。
```{r 05-geometry-operations-20, eval=F}
nz_scale_sf = st_set_geometry(nz, nz_scale)
```
### 切り取り (clip) {#clipping}
\index{べくた@ベクタ!きりとり@切り取り}
\index{くうかん@空間!ぶぶんしゅうごう@部分集合}
空間切り取り (clip) は、空間部分集合の作成の一種で、影響を受けるフィーチャの少なくとも一部の `geometry` 列を変更するものである。
切り取りは、
点よりも複雑なフィーチャである線、ポリゴン、およびそれらの複合にしか適用できない。
コンセプトを説明するために、まずは簡単な例で説明する。
中心点が互いに 1 単位離れていて、半径が 1 である 2 つの重なり合った円を用意しよう (Figure \@ref(fig:points))。
```{r points, fig.cap="重なり合った円。", fig.asp=0.4, crop = TRUE}
op = par(mar = rep(0, 4))
b = st_sfc(st_point(c(0, 1)), st_point(c(1, 1))) # 点を二つ作成
b = st_buffer(b, dist = 1) # 点を円に変換
plot(b, border = "gray")
text(x = c(-0.5, 1.5), y = 1, labels = c("x", "y"), cex = 3) # テキストを追加
```
```{r, echo=FALSE}
par(op)
```
どちらかの円を選択するのではなく、`x` <u>と</u> `y` の両方で覆われた空間を選択したい。
これは、関数 `st_intersection()`\index{べくた@ベクタ!こうさ@交差}、左手と右手の円を表す `x` と `y` というオブジェクトを使って説明することができる (Figure \@ref(fig:circle-intersection))。
```{r circle-intersection, fig.cap="重なり合った円はグレーで表示され、円同士が交差していることを示す。", fig.asp=0.4, fig.scap="Overlapping circles showing intersection types.", crop = TRUE}
op = par(mar = rep(0, 4))
x = b[1]
y = b[2]
x_and_y = st_intersection(x, y)
plot(b, border = "gray")
plot(x_and_y, col = "lightgray", border = "gray", add = TRUE) # 範囲を intersect
```
```{r, echo=FALSE}
par(op)
```
次のコードチャンクは、`x` と `y` を表すベン図のすべての組み合わせに対して、これがどのように機能するかを示している。これは、書籍 *R for Data Science* の [Figure 5.1](https://r4ds.had.co.nz/transform.html#logical-operators) から着想を得ている [@grolemund_r_2016]。
```{r venn-clip, echo=FALSE, fig.cap="論理演算子の空間的等価性。", warning=FALSE}
source("code/05-venn-clip.R")
```
### 部分集合と切り取り (clip)
\index{べくた@ベクタ!きりとり@切り取り}
\index{くうかん@空間!ぶぶんしゅうごう@部分集合}
オブジェクトの切り取りは、その形状を変更することができるが、オブジェクトを部分集合を作成することもでき、切り取り/部分集合オブジェクトと交差する (または部分的に交差する) フィーチャのみを返す。
この点を説明するために、を Figure \@ref(fig:venn-clip) の円 `x` と `y` の境界ボックスをカバーする点の部分集合を作成しよう。
ある点は1つの円の中に入り、ある点は両方の円の中に入り、ある点はどちらの円の中にも入らない。
`st_sample()` は、円 `x` と `y` の範囲内にある点を <u>単純なランダム化</u>をして生成し、Figure \@ref(fig:venn-subset) に示すような出力を得るために、以下のように使用する。ここで一つ問題がある。円 `x` と `y` 両方と交差する点の部分集合はどのように得られるだろうか?
```{r venn-subset, fig.cap="円x、yを囲むバウンディングボックス内にランダムに分布する点。オブジェクトx、yの両方と交差する点がハイライトされる。", fig.height=6, fig.width=9, fig.scap="Randomly distributed points within the bounding box. Note that only one point intersects with both x and y, highlighted with a red circle.", echo=FALSE}
op = par(mar = rep(0, 4))
bb = st_bbox(st_union(x, y))
box = st_as_sfc(bb)
set.seed(2024)
p = st_sample(x = box, size = 10)
p_xy1 = p[x_and_y]
plot(box, border = "gray", lty = 2)
plot(x, add = TRUE, border = "gray")
plot(y, add = TRUE, border = "gray")
plot(p, add = TRUE, cex = 3.5)
plot(p_xy1, cex = 5, col = "red", add = TRUE)
text(x = c(-0.5, 1.5), y = 1, labels = c("x", "y"), cex = 3)
```
```{r, echo=FALSE}
par(op)
```
```{r venn-subset-to-show, eval=FALSE}
bb = st_bbox(st_union(x, y))
box = st_as_sfc(bb)
set.seed(2024)
p = st_sample(x = box, size = 10)
x_and_y = st_intersection(x, y)
```
以下のコードは、同じ結果を得るための3つの方法を示している。
以下のコードチャンクの最初の行に示すように、`x` と `y` の交差\index{べくた@ベクタ!こうさ@交差} (前のコードチャンクでは `x_and_y` で表される) を直接部分集合オブジェクトとして使用することができる。
また、以下のコードチャンクの 2 行目で示すように、`p` で表される入力点と部分集合/クリッピングオブジェクト `x_and_y` との<u>交点</u>を求めることもできる。
この第二のアプローチは、`x_and_y` と部分的に交差するフィーチャを返すが、部分集合・オブジェクトの境界を越える空間的に広範囲なフィーチャについては、形状を修正したものを返すことになる。
3 つ目のアプローチは、前の章で紹介した二項の空間述語 `st_intersects()` を使って部分集合オブジェクトを作成することである。
結果は (属性名の表面的な違いを除いて) 同じだが、実装は大きく異なる。
```{r 05-geometry-operations-21}
# way #1
p_xy1 = p[x_and_y]
# way #2
p_xy2 = st_intersection(p, x_and_y)
# way #3
sel_p_xy = st_intersects(p, x, sparse = FALSE)[, 1] &
st_intersects(p, y, sparse = FALSE)[, 1]
p_xy3 = p[sel_p_xy]
```
```{r 05-geometry-operations-22, echo=FALSE, eval=FALSE}
# オブジェクトが同一かどうかをテスト
identical(p_xy1, p_xy2)
identical(p_xy2, p_xy3)
identical(p_xy1, p_xy3)
waldo::compare(p_xy1, p_xy2) # 属性名以外は同じ
waldo::compare(p_xy2, p_xy3) # 属性名以外は同じ
# bb からサンプルする別の方法
bb = st_bbox(st_union(x, y))
pmulti = st_multipoint(pmat)
box = st_convex_hull(pmulti)
```
上記の例は、応用というより教育的な目的で作成されたものであり、R で地理的ベクタオブジェクトを扱うための理解を深めるために結果を再現することを勧める。しかし、これは、どの実装を使うべきかという重要な問題を提起している。
一般に、上記の最初のアプローチのような簡潔な実装が好まれる。
Chapter \@ref(algorithms) では、同じ技術やアルゴリズムの異なる実装を選択する問題に戻る。
### ジオメトリ結合 {#geometry-unions}
\index{べくた@ベクタ!けつごう@結合}
\index{しゅうやく@集約!くうかん@空間}
Section \@ref(vector-attribute-aggregation) で見たように、空間的な集約は、同グループの接しているポリゴンのジオメトリを静かにディゾルブ (dissolve) させることができる。
以下のコードでは、Base と **dplyr**\index{dplyr (package)} 関数を使って 48 の米国の州と District of Columbia (`us_states`) を 4 つの地域に集約している (結果は Figure \@ref(fig:us-regions) を参照)。
```{r 05-geometry-operations-23}
regions = aggregate(x = us_states[, "total_pop_15"], by = list(us_states$REGION),
FUN = sum, na.rm = TRUE)
regions2 = us_states |>
group_by(REGION) |>
summarize(pop = sum(total_pop_15, na.rm = TRUE))
```
```{r 05-geometry-operations-24, echo=FALSE}
# st_join(buff, africa[, "pop"]) |>
# summarize(pop = sum(pop, na.rm = TRUE))
# summarize(africa[buff, "pop"], pop = sum(pop, na.rm = TRUE))
```
```{r us-regions, fig.cap="連続したポリゴンに対する空間的な集計。アメリカの州の人口を地域に集計し、人口を色で表した。この操作により、州間の境界が自動的に解消されることに注意。", echo=FALSE, warning=FALSE, out.width="100%", fig.scap="Spatial aggregation on contiguous polygons."}
source("code/05-us-regions.R", print.eval = TRUE)
```
ジオメトリ的にはどうなっているのだろうか?
裏側では、`aggregate()` と `summarize()` の両方がジオメトリを結合し、`st_union()` を使ってジオメトリ間の境界を解消している。
これは、アメリカ西部の連合体を作成する以下のコードチャンクで実証されている。
```{r 05-geometry-operations-25}
us_west = us_states[us_states$REGION == "West", ]
us_west_union = st_union(us_west)
```
この関数は 2 つの形状を受け取り、結合することができる。以下のコードでは、Texas を組み込んだ西側のブロックを結合している (課題: 結果を再現してプロットしなさい)。
```{r 05-geometry-operations-26, message=FALSE}
texas = us_states[us_states$NAME == "Texas", ]
texas_union = st_union(us_west_union, texas)
```
```{r 05-geometry-operations-27, echo=FALSE, eval=FALSE}
plot(texas_union)
# aim: experiment with st_union
us_south2 = st_union(us_west[1, ], us_west[6, ])
plot(us_southhwest)
```
### ジオメトリ型の変換 {#type-trans}
\index{べくた@ベクタ!じおめとりがたきゃすと@ジオメトリ型キャスト}
ジオメトリキャストは、ジオメトリ型の変換を可能にする強力な操作である。
これは、**sf** パッケージの `st_cast()` 関数で実装されている。
重要なことは、`st_cast()` は、単一のシンプルフィーチャ (`sfg`) オブジェクト、シンプルフィーチャ列 (`sfc`)、およびシンプルフィーチャオブジェクトで異なる動作をすることである。
ここでは、複合点を作成して、シンプルフィーチャ (`sfg`) オブジェクトに対するジオメトリ型のキャストの動作を説明する。
```{r 05-geometry-operations-28}
multipoint = st_multipoint(matrix(c(1, 3, 5, 1, 3, 1), ncol = 2))
```
この場合、`st_cast()` は新しいオブジェクトを線やポリゴン (Figure \@ref(fig:single-cast)) に変換するのに便利である。
```{r 05-geometry-operations-29}
linestring = st_cast(multipoint, "LINESTRING")
polyg = st_cast(multipoint, "POLYGON")
```
```{r single-cast, echo = FALSE, fig.cap="多点ジオメトリからキャストされた線とポリゴンの例。", warning=FALSE, fig.asp=0.3, fig.scap="Examples of casting operations.", dev="ragg_png"}
p_sc1 = tm_shape(st_sfc(multipoint, crs = "+proj=merc")) + tm_symbols(shape = 1,
col = "black",
size = 0.5) +
tm_title("複合点") +
tm_layout(inner.margins = c(0.15, 0.05, 0.15, 0.05))
p_sc2 = tm_shape(st_sfc(linestring, crs = "+proj=merc")) + tm_lines() +
tm_title("複合線") +
tm_layout(inner.margins = c(0.15, 0.05, 0.15, 0.05))
p_sc3 = tm_shape(st_sfc(polyg, crs = "+proj=merc")) + tm_polygons(col = "black") +
tm_title("ポリゴン") +
tm_layout(inner.margins = c(0.15, 0.05, 0.15, 0.05))
tmap_arrange(p_sc1, p_sc2, p_sc3, ncol = 3)
```
複合点から線への変換は、GPS 測定やジオタグ付きメディアなど、順序付けられたポイント観測から線オブジェクトを作成する一般的な操作である。
これにより、移動した経路の長さを計算するなどの空間演算を行うことができる。
複合点や線からポリゴンへの変換は、例えば、湖の周囲で取得した GPS 測定値のセットや建物の敷地の角から面積を計算するためによく使われる。
また、`st_cast()` では、逆の変換をすることも可能である。
```{r 05-geometry-operations-30}
multipoint_2 = st_cast(linestring, "MULTIPOINT")
multipoint_3 = st_cast(polyg, "MULTIPOINT")
all.equal(multipoint, multipoint_2)
all.equal(multipoint, multipoint_3)
```
```{block2 05-geometry-operations-31, type='rmdnote'}
単純なジオメトリ (`sfg`) に対して、`st_cast()` は、非複合タイプから複合タイプ (例えば、`POINT` から `MULTIPOINT`) や複合タイプから非複合タイプへのジオメトリキャストも提供する。
しかし、複合タイプから非複合タイプにキャストする場合、古いオブジェクトの最初の要素だけが出力オブジェクトに残る。
```
```{r 05-geometry-operations-32, include=FALSE}
cast_all = function(xg) {
lapply(c("MULTIPOLYGON", "MULTILINESTRING", "MULTIPOINT", "POLYGON", "LINESTRING", "POINT"),
function(x) st_cast(xg, x))
}
t = cast_all(multipoint)
t2 = cast_all(polyg)
```
シンプルフィーチャのジオメトリ列 (`sfc`) とシンプルフィーチャオブジェクトのジオメトリキャストは、ほとんどの場合、`sfg` の場合と同じように動作する。
重要な違いの 1 つは、複合タイプから非複合タイプへの変換である。
この処理の結果、`sfc` または `sf` の複合オブジェクトは、多数の非複合オブジェクトに分割される。
以下のような `sf` オブジェクトを持っている。
- `POI` - 点 (定義上、一つの点)
- `MPOI` - 4 点からなる複合点
- `LIN` - 5 点からなる線
- `MLIN` - 二つの線の複合線 (5 つの点と 2 つの点)
- `POL` - ポリゴン (5 つの点)
- `MPOL` - 二つのポリゴンからなる複合ポリゴン (どちらも 5 つの点)
- `GC` - GEOMETRYCOLLECTION で複合点 (4 つの点) と線 (5 つの点)
Table \@ref(tab:sfs-st-cast) は、シンプルフィーチャに対して可能なジオメトリタイプの変換を示したものである。
単一のシンプルフィーチャ (表の最初の列で表される) は、Table \@ref(tab:sfs-st-cast) の列で表される複数のジオメトリタイプに変換することができる。
例えば、1 つの点から複数行の文字列やポリゴンに変換することはできない。`[1, 4:5]` のセルに NA が含まれている理由を説明する。
一部の変換では、単一のフィーチャ入力を複数のサブフィーチャに分割し、`sf` オブジェクトを「拡張」(重複する属性値を持つ新しい行を追加) している。
例えば、5 組の座標からなる多点ジオメトリを「POINT」ジオメトリに変換すると、出力には 5 つのフィーチャが含まれる。
```{r sfs-st-cast, echo=FALSE}
sfs_st_cast = read.csv("extdata/sfs-st-cast.csv")
abbreviate_geomtypes = function(geomtypes) {
geomtypes_new = gsub(pattern = "POINT", replacement = "POI", x = geomtypes)
geomtypes_new = gsub(pattern = "POLYGON", replacement = "POL", x = geomtypes_new)
geomtypes_new = gsub(pattern = "LINESTRING", replacement = "LIN", x = geomtypes_new)
geomtypes_new = gsub(pattern = "MULTI", replacement = "M", x = geomtypes_new)
geomtypes_new = gsub(pattern = "GEOMETRYCOLLECTION", replacement = "GC", x = geomtypes_new)
geomtypes_new
}
sfs_st_cast$input_geom = abbreviate_geomtypes(sfs_st_cast$input_geom)
names(sfs_st_cast) = abbreviate_geomtypes(names(sfs_st_cast))
names(sfs_st_cast)[1] = ""
knitr::kable(sfs_st_cast,
caption = paste("シンプルフィーチャジオメトリ",
" (Section 2.1 参照) に対する",
"入力型は行、出力型は列で指定。"),
caption.short = "Geometry casting on simple feature geometries.",
booktabs = TRUE) |>
kableExtra::add_footnote("注: 括弧内の値はフィーチャ数を表し、NA は操作が不可能なことを表す。", notation = "none")
```
例として、新しいオブジェクトである `multilinestring_sf` にジオメトリタイプの変換を適用してみよう (Figure \@ref(fig:line-cast) の左側)。
```{r 05-geometry-operations-33}
multilinestring_list = list(matrix(c(1, 4, 5, 3), ncol = 2),
matrix(c(4, 4, 4, 1), ncol = 2),
matrix(c(2, 4, 2, 2), ncol = 2))
multilinestring = st_multilinestring((multilinestring_list))
multilinestring_sf = st_sf(geom = st_sfc(multilinestring))
multilinestring_sf
```
道路や河川のネットワークをイメージしていただきたい。
新しいオブジェクトは、すべての線を定義する 1 つの行だけを持っている。
このため、各線分に名前を付けたり、1 本の線の長さを計算したりすることができないなど、実行できる操作に制限がある。
このような場合、1 つの複合線を 3 つの線に分離する `st_cast()` 関数を使用することができる。
```{r 05-geometry-operations-34}
linestring_sf2 = st_cast(multilinestring_sf, "LINESTRING")
linestring_sf2
```
```{r line-cast, echo=FALSE, fig.cap="複合線 (左) と線 (右) 間の型キャストの例。", warning=FALSE, fig.scap="Examples of type casting.", message=FALSE, dev="ragg_png"}
p_lc1 = tm_shape(multilinestring_sf) + tm_lines(lwd = 3) +
tm_title("複合線") + tm_layout(fontfamily = "HiraginoSans-W3")
linestring_sf2$name = c("Riddle Rd", "Marshall Ave", "Foulke St")
p_lc2 = tm_shape(linestring_sf2) + tm_lines(lwd = 3, col = "name", palette = "Set2") +
tm_title("線") +
tm_layout(legend.show = FALSE, fontfamily = "HiraginoSans-W3")
tmap_arrange(p_lc1, p_lc2, ncol = 2)
```
新しく作成されたオブジェクトでは、属性の作成 (詳しくは Section \@ref(vec-attr-creation) を参照) や長さの測定が可能である。
```{r 05-geometry-operations-35}
linestring_sf2$name = c("Riddle Rd", "Marshall Ave", "Foulke St")
linestring_sf2$length = st_length(linestring_sf2)
linestring_sf2
```
## ラスタデータに対するジオメトリ操作 {#geo-ras}
\index{らすた@ラスタ!そうさ@操作}
幾何学的なラスタ操作には、画像の平行移動 (shift)、反転 (flip)、ミラー (mirror)、拡大縮小 (scale)、回転 (rotate)、幾何補正 (warp) などがある。
これらの操作は、ジオリファレンスなど様々な用途で必要とされ、既知の CRS を持つ正確な地図に画像を重ね合わせるために使用される [@liu_essential_2009]。
ジオリファレンスには、さまざまな手法が存在する。
- 既知の[地上基準点](https://www.qgistutorials.com/en/docs/3/georeferencing_basics.html)に基づく地理補正 (georectification)
- 局所的な地形も考慮したオルソ補正 (orthorectification)
- 画像[位置合わせ](https://en.wikipedia.org/wiki/Image_registration) (image registration) は、異なるセンサーから撮影された同じものの画像を、ある画像と別の画像に (座標系や解像度の点で) 位置合わせして結合するために使用する。
最初の 2 つについては、手作業が必要になることが多いため、R はむしろ不向きで、通常は専用の GIS ソフトウェアの助けを借りて行われる (Chapter \@ref(gis) も参照)。
一方、R では複数の画像の位置合わせが可能であり、本節ではその方法を中心に紹介する。
これには、画像の範囲、解像度、原点を変更することがよく含まれる。
もちろん、投影法の一致も必要であるが、それはすでに Section \@ref(reproj-ras) で説明した。
いずれにせよ、1 枚のラスタ画像に対してジオメトリ演算を行う理由は他にもある。
例えば、Chapter \@ref(location) で、ドイツの大都市圏を人口 50 万人以上の 20 km^2^ のピクセルと定義している。
しかし、元の住民ラスタの解像度は 1 km^2^ であるため、解像度を 20 分の 1 に下げる (集約する) (Section \@ref(define-metropolitan-areas) を参照)。
ラスタを集約するもう一つの理由は、単純に実行時間の短縮やディスクスペースの節約である。
もちろん、この方法はラスタデータの粗い解像度が可能なタスクの場合にのみ推奨される。
### ジオメトリ交差 {#geometric-intersections}
\index{らすた@ラスタ!こうさ@交差}
Section \@ref(spatial-raster-subsetting) では、他の空間オブジェクトが重なったラスタから値を抽出する方法を示した。
空間出力を取り出すには、ほぼ同じ部分集合構文を使うことができる。
唯一の違いは、`drop` の引数を `FALSE` にすることで、行列構造を維持したいことを明確にしなければならないことである。
これは、中点が `clip` と重なるセルを含むラスタオブジェクトを返すものである。
```{r 05-geometry-operations-36, eval=FALSE}
elev = rast(system.file("raster/elev.tif", package = "spData"))
clip = rast(xmin = 0.9, xmax = 1.8, ymin = -0.45, ymax = 0.45,
resolution = 0.3, vals = rep(1, 9))
elev[clip, drop = FALSE]
```
同じ操作で、`intersect()` と `crop()` のコマンドも使用できる。
### 範囲と原点 {#extent-and-origin}
\index{らすた@ラスタ!けつごう@結合}
ラスタをマージしたり、マップ代数演算を実行する場合、解像度、投影、原点、範囲が一致する必要がある。そうでない場合、分解能が 0.2 度のラスタの値を、分解能が 1 度の 2 番目のラスタにどのように追加すればよいのだろうか。
また、投影方法や解像度の異なるセンサーの衛星画像を合成したい場合も、同じような問題が発生する。
このような不一致は、ラスタの位置合わせをすることで対処することができる。
最も単純なケースとして、2 つの画像はその範囲 (extent) だけが異なる。
以下のコードでは、ラスタの各辺に 1 行と 2 列を追加し、新しい値をすべて `NA` (Figure \@ref(fig:extend-example)) に設定する。
```{r extend-example0}
elev = rast(system.file("raster/elev.tif", package = "spData"))
elev_2 = extend(elev, c(1, 2))
```
```{r extend-example, fig.cap = "元のラスタ (左) と同じラスタ (右) を上下 1 行ずつ、左右 2 列ずつに拡張したもの。", fig.scap="Extending rasters.", echo=FALSE, fig.asp=0.5}
source("code/05-extend-example.R", print.eval = TRUE)
```
R の **terra** パッケージで、範囲が異なる2つのオブジェクトに対して代数演算を実行するとエラーが発生する。
```{r 05-geometry-operations-37, error=TRUE}
elev_3 = elev + elev_2
```
しかし、2 つのラスタの範囲を `extend()` で揃えることができる。
追加する行や列の数を (以前のように) 関数に指示するのではなく、別のラスタオブジェクトを使って計算させるようにしている。
ここでは、`elev` のオブジェクトを、`elev_2` の範囲に拡張している。
新しく追加された行と列は、`NA` を受け取る。
```{r 05-geometry-operations-38}
elev_4 = extend(elev, elev_2)
```
\index{らすた@ラスタ!げんてん@原点}
ラスタの原点 (origin) は、座標 (0, 0) に最も近いセルコーナーである。
`origin()` 関数は、原点の座標を返す。
以下の例では、座標 (0, 0) のセルコーナーが存在するが、必ずしもそうとは限らない。
```{r 05-geometry-operations-39}
origin(elev_4)
```
2 つのラスタが異なる原点を持つ場合、それらのセルは完全に重ならず、マップ代数は不可能となる。
原点を変更する場合 `origin()` を使用する。^[
2 つのラスタデータセットの原点がわずかに離れている場合、`terra::terraOptions()` の引数 `tolerance` を増やすだけで十分な場合がある。
]
Figure \@ref(fig:origin-example) は、このように原点を変更した場合の効果を明らかにしたものである。
```{r}
# 原点を変更
origin(elev_4) = c(0.25, 0.25)
```
```{r origin-example, fig.cap="同じ値を持つが、原点が異なるラスタ。", echo=FALSE}
elev_poly = st_as_sf(as.polygons(elev, dissolve = FALSE))
elev4_poly = st_as_sf(as.polygons(elev_4, dissolve = FALSE))
tm_shape(elev4_poly) +
tm_grid() +
tm_polygons(fill = "elev", lwd = 0.5) +
tm_shape(elev_poly) +
tm_polygons(fill = "elev") +
tm_layout(frame = FALSE, legend.show = FALSE,
inner.margins = c(0.1, 0.12, 0, 0))
```
なお、解像度を変更すると (次節)、原点も頻繁に変更される。
### 低解像度化と高解像度化 {#aggregation-and-disaggregation}
\index{らすた@ラスタ!しゅうやく@集約}
\index{らすた@ラスタ!ぶんかい@分解}
ラスタデータセットは、その解像度に関しても異なる場合がある。
解像度を合わせるには、1 つのラスタの解像度を下げる (低解像度化、`aggregate()`) か上げる (高解像度化、`disagg()`) かのどちらかを選択する。^[
ここでは、空間分解能を指す。
リモートセンシングでは、スペクトル (スペクトルバンド)、時間分解能 (同じエリアを時間をかけて観測)、ラジオメトリック (色深度) 分解能も重要である。
ドキュメントにある `tapp()` の例で、時間的ラスタ集計の方法を確認してみよう。
]
例として、`dem` (**spDataLarge** パッケージにある) の空間解像度を 5 倍に変更する (Figure \@ref(fig:aggregate-example))。
さらに、出力セルの値は入力セルの平均に対応する (`median()`、`sum()` など、他の関数も使用可能)。
```{r 05-geometry-operations-40}
dem = rast(system.file("raster/dem.tif", package = "spDataLarge"))
dem_agg = aggregate(dem, fact = 5, fun = mean)
```
```{r aggregate-example, fig.cap = "オリジナル (左) と 集計後 (右)。", echo=FALSE, dev="ragg_png"}
p_ar1 = tm_shape(dem) +
tm_raster(col.scale = tm_scale_continuous()) +
tm_title("A. オリジナル") +
tm_layout(frame = FALSE, legend.show = FALSE, fontfamily = "HiraginoSans-W3")
p_ar2 = tm_shape(dem_agg) +
tm_raster(col.scale = tm_scale_continuous()) +
tm_title("B. Aggregate 後") +
tm_layout(frame = FALSE, legend.show = FALSE, fontfamily = "HiraginoSans-W3")
tmap_arrange(p_ar1, p_ar2, ncol = 2)
```
Table \@ref(tab:aggdf) は、オリジナルと集計後の比較である。
`aggregate()` で解像度を「下げた」ことにより、$(30.85, 30.85)$ から $(154.25, 154.25)$ に上がった。
列数 (`nrow`) と行数 (`ncol`) を減らすことで行った (Section \@ref(raster-data) 参照)。
範囲は、新しいグリッドに合うように微調整された。
```{r aggdf}
#| echo: false
agg_df = data.frame(
Object = c("dem", "dem_agg"),
Resolution = c("(30.85, 30.85)", "(154.25, 154.25)"),
Dimensions = c("117 * 117", "24 * 24"),
Extent = c("794599.1, 798208.6, 8931775, 8935384", "794599.1, 798301.1, 8931682, 8935384")
)
knitr::kable(agg_df, caption = "Properties of the original and aggregated raster.", booktabs = TRUE)
```
\index{らすた@ラスタ!ぶんかい@分解}
`disagg()` 関数は、ラスタオブジェクトの解像度を上げ、新しく作成されたセルに値を割り当てるための 2 つの方法を提供する。
新たに生成されたセルの値を計算する方法は 2 つある。デフォルトの方法 (`method = "near"`) は、すべての出力セルに入力セルの値を与えるだけなので、値が重複し、「ブロック状の」出力となる。
`bilinear` 法は、入力画像の 4 つの最近接画素中心 (Figure \@ref(fig:bilinear) のオレンジ色の点) を用いて、距離で重み付けした平均値を計算する (Figure \@ref(fig:bilinear) の矢印)。
出力セルの値は、左上の四角で表現される (Figure \@ref(fig:bilinear))。
```{r 05-geometry-operations-41}
dem_disagg = disagg(dem_agg, fact = 5, method = "bilinear")
identical(dem, dem_disagg)
```
```{r bilinear, echo = FALSE, fig.width=8, fig.height=10, fig.cap="双一次補間法で分解する場合は、最も近い 4 つの入力セルの距離加重平均で出力を決定。", fig.scap="Bilinear disaggregation in action.", warning=FALSE}
source("code/05-bilinear.R", print.eval = TRUE)
# knitr::include_graphics("https://user-images.githubusercontent.com/1825120/146619205-3c0c2e3f-9e8b-4fda-b014-9c342a4befbb.png")
```
```{r}
#| echo: false
#| eval: false
identical(dem, dem_disagg)
compareGeom(dem, dem_disagg)
all.equal(dem, dem_disagg)
```
`dem` と `dem_disagg` の値を比較すると、両者が同一ではないことがわかる (`compareGeom()` や `all.equal()` を使うこともできる)。
分解は単純な補間技術であるため、これは予想外の結果である。
分解することで解像度が上がるが、対応する値は解像度の低いソースと同程度の精度しかないことを念頭に置くことが重要である。
### リサンプリング {#resampling}
\index{らすた@ラスタ!りさんぷりんぐ@リサンプリング}
上記の低解像度化・高解像度化の方法は、低解像度化・高解像度化係数によってラスタの解像度を変えたい場合にのみ適している。
しかし、解像度や原点の異なるラスタが 2 つ以上ある場合はどうしたらよいのだろうか。
これがリサンプリングの役割で、新しい画素の位置に対して値を計算する処理である。
つまり、この処理では、元のラスタの値を受け取り、カスタム解像度と原点を持つターゲットラスタの値を新たに計算し直す (Figure \@ref(fig:resampl0))。
```{r resampl0, echo=FALSE, fig.cap="オリジナルからカスタムの解像度と原点のターゲットラスタへのリサンプリング", fig.asp = 0.4}
target_rast = rast(xmin = 794650, xmax = 798250,
ymin = 8931750, ymax = 8935350,
resolution = 150, crs = "EPSG:32717")
target_rast_p = st_as_sf(as.polygons(target_rast))
dem_resampl1 = resample(dem, target_rast, method = "near")
tm1 = tm_shape(dem) +
tm_raster(col.scale = tm_scale(breaks = seq(200, 1100, by = 150))) +
tm_title("Original raster") +
tm_layout(frame = FALSE, legend.show = FALSE)
tm2 = tm_shape(dem) +
tm_raster(col.scale = tm_scale(breaks = seq(200, 1100, by = 150))) +
tm_title("Target raster") +
tm_layout(frame = FALSE, legend.show = FALSE) +
tm_shape(target_rast_p, is.main = TRUE) +
tm_borders()
tm3 = tm_shape(dem) +
tm_raster(col_alpha = 0) +
tm_shape(dem_resampl1) +
tm_raster(col.scale = tm_scale(breaks = seq(200, 1100, by = 150))) +
tm_title("Resampled raster") +
tm_layout(frame = FALSE, legend.show = FALSE)
tmap_arrange(tm1, tm2, tm3, nrow = 1)
```
\index{らすた@ラスタ!りさんぷりんぐ@リサンプリング}
Figure \@ref(fig:resampl) に示すように、解像度/原点の異なるラスタの値を推定する方法はいくつかある。
主なリサンプリング方法には、以下のようなものがある。
- 最近傍 (Nearest neighbor): 元のラスタの最も近いセルの値を、ターゲットのセルに割り当てる。これは高速でシンプルな手法で、通常、カテゴリラスタのリサンプリングに適している。
- 双一次補間 (Bilinear interpolation): 元のラスタから 4 つの最近接セルを加重平均して、ターゲットのセルに割り当てる (Figure \@ref(fig:bilinear))。これは、連続したラスタに適した最も高速な方法である。
- 三次補間 (Cubic interpolation): 3 次多項式関数を適用し、出力セルの値を決定するために、元のラスタの16個の最近接セルの値を使用する。連続したラスタに使用され、双一次補間の結果より滑らかなサーフェスになるが、より計算量が多くなる。
- 三次スプライン補間 (Cubic spline interpolation): 出力セルを決定するために元のラスタの 16 個の最近接セルの値を使用するが、結果を導き出すために三次スプライン補間 (区分的 3 次多項式関数) を適用する。連続したラスタに使用される。
- ランチョスリサンプリング (Lanczos windowed sinc resampling): 出力セルの値を決定するために、元のラスタの 36 個の最近接セルの値を使用する。連続的なラスタに使用される。^[
この方法の詳細な説明は、https://gis.stackexchange.com/a/14361/20955。
]
上記の説明では、<u>最近傍</u>リサンプリングのみがカテゴリラスタに適しており、連続ラスタにはすべてのメソッドが (異なるアウトカムで) 使用できることが強調されている。
さらに、連続した方式を採用するごとに、処理時間が長くなる。
さらに、リサンプリングは関連するセルの統計値 (最小値や最頻値など) を用いることもできる、
リサンプリングを適用するために、**terra** パッケージは、`resample()` 関数を提供する。
入力ラスタ (`x`)、目的の空間特性を持つラスタ (`y`)、リサンプリング方法 (`method`) を受け付ける。
`resample()` 関数の動作を確認するために、対象となる空間特性を持つラスタが必要である。
この例では、`target_rast` を作成するが、すでに存在するラスタオブジェクトを使用することが多いだろう。
```{r 05-geometry-operations-42}
target_rast = rast(xmin = 794650, xmax = 798250,
ymin = 8931750, ymax = 8935350,
resolution = 300, crs = "EPSG:32717")
```
次に、2 つのラスタオブジェクトを最初の 2 つの引数として与え、上で説明したリサンプリングメソッドのうちの 1 つを提供する必要がある。
```{r 05-geometry-operations-42b}
dem_resampl = resample(dem, y = target_rast, method = "bilinear")
```
Figure \@ref(fig:resampl) は、`dem` オブジェクトについて、異なるリサンプリング手法を比較したものである。
```{r resampl, echo=FALSE, fig.cap="ラスタのオリジナルと 5 種類のリサンプリング手法の視覚的比較。", dev="ragg_png"}
dem_resampl1 = resample(dem, target_rast, method = "near")
dem_resampl2 = resample(dem, target_rast, method = "bilinear")
dem_resampl3 = resample(dem, target_rast, method = "cubic")
dem_resampl4 = resample(dem, target_rast, method = "cubicspline")
dem_resampl5 = resample(dem, target_rast, method = "lanczos")
library(tmap)
tm1 = tm_shape(dem) +
tm_raster(col.scale = tm_scale(breaks = seq(200, 1100, by = 150))) +
tm_title("オリジナル") +
tm_layout(frame = FALSE, legend.show = FALSE, fontfamily = "HiraginoSans-W3")
tm2 = tm_shape(dem_resampl1) +
tm_raster(col.scale = tm_scale(breaks = seq(200, 1100, by = 150))) +
tm_title("最近傍 (near)") +
tm_layout(frame = FALSE, legend.show = FALSE, fontfamily = "HiraginoSans-W3")
tm3 = tm_shape(dem_resampl2) +
tm_raster(col.scale = tm_scale(breaks = seq(200, 1100, by = 150))) +
tm_title("双一次補間 (bilinear)") +
tm_layout(frame = FALSE, legend.show = FALSE, fontfamily = "HiraginoSans-W3")
tm4 = tm_shape(dem_resampl3) +
tm_raster(col.scale = tm_scale(breaks = seq(200, 1100, by = 150))) +
tm_title("三次補間 (cubic)") +
tm_layout(frame = FALSE, legend.show = FALSE, fontfamily = "HiraginoSans-W3")
tm5 = tm_shape(dem_resampl4) +
tm_raster(col.scale = tm_scale(breaks = seq(200, 1100, by = 150))) +
tm_title("三次スプライン補間 (cubicspline)") +
tm_layout(frame = FALSE, legend.show = FALSE, fontfamily = "HiraginoSans-W3")
tm6 = tm_shape(dem_resampl5) +
tm_raster(col.scale = tm_scale(breaks = seq(200, 1100, by = 150))) +
tm_title("lanczos") +
tm_layout(frame = FALSE, legend.show = FALSE)
tmap_arrange(tm1, tm2, tm3, tm4, tm5, tm6)
```
`resample()` 関数には、さらに、`sum`、`min`、`q1`、`med`、`q3`、`max`、`average`、`mode`、`rms` などのリサンプリング手法も用意されている。
これらはすべて、NA でないすべてのグリッドセルの値に基づいて所定の統計量を計算するものである。
例えば、`sum` は、各ラスタセルが空間的に広範な変数 (例えば人数) を表す場合に有効である。
`sum` を使用した効果として、リサンプルされたラスタは元のラスタと同じサンプル総数を持つはずである。
\index{らすた@ラスタ!りさんぷりんぐ@リサンプリング}
Section \@ref(reproj-ras) で見るように、ラスタの再投影はリサンプリングの特殊なケースで、対象ラスタが元のラスタと異なる CRS を持つ場合に行われる。
\index{GDAL}
```{block2 type='rmdnote'}
**terra** のジオメトリ操作はユーザーフレンドリーで、かなり高速であり、大きなラスタオブジェクトでも動作する。
しかし、広範なラスタや多くのラスタファイルに対して、**terra** が最も効率的でない場合もあり、代替手段を検討する必要がある。
最も確立された選択肢は、GDAL ライブラリである。
これは、以下のようないくつかのユーティリティ関数を含んでる。
- `gdalinfo` - ラスタファイルの解像度、CRS、バウンディングボックスなど、さまざまな情報を一覧表示
- `gdal_translate` - ラスタデータを異なるファイル形式間で変換
- `gdal_rasterize` - ベクタデータからラスタファイルへの変換
- `gdalwarp` - ラスタのモザイク処理、リサンプリング、切り取り、再投影が可能
上記のすべての関数は C++ で書かれているが、**gdalUtilities** パッケージを使用して R で呼び出すことができる (Section \@ref(gdal) 参照)。
GDAL の関数はすべてラスタファイルのパスを入力として受け取り、その出力をラスタファイルとして返す (例えば、 `gdalUtilities::gdal_translate("my_file.tif", "new_file.tif", t_srs = "EPSG:4326")`)。
これは、入力として `SpatRaster` オブジェクトを想定している **terra** のアプローチとは大きく異なる。
```
## 演習
```{r, echo=FALSE, results='asis'}
res = knitr::knit_child('_05-ex-ja.Rmd', quiet = TRUE, options = list(include = FALSE, eval = FALSE))
cat(res, sep = '\n')
```