はじめに
本連載記事は、地理空間情報をのAI-ReadyなDataLoaderである『SpatialArrow』を設計し、実装し、検証した内容を3部作として執筆したものです。今回は、その第2回目の記事となります。
第1回【SpatialArrow概念編】GeoAI学習で「画像分割によって地物が途切れる問題」をどう解決するか?(固定タイル方式の課題とSpatialArrowの設計思想)
第2回【SpatialArrow実装編】GeoAI学習で地物を切り刻まずに学習へ渡す(ROI単位で画像とBoundingBoxを生成する、AI-ReadyなDataLoaderの実装)
第3回【SpatialArrow検証編】GeoAI学習で地物を分割せずに学習させたら精度は上がるか?(屋根上PV検出の比較検証で見逃しが16.5%減少)
前回の説明記事では、地理空間データの正本と、モデルへ渡す一時的なビューを分けるSpatialArrowの考え方を紹介しました。今回は、分割前の矩形ポリゴンと位置情報付き画像タイルから、物体検出モデルへ渡す画像と教師データを生成する実装を説明します。
対象は屋根上ソーラーパネル(以下PV)のBounding Box(以下BB)検出で、モデルにはRF-DETR Mediumを使用します。本記事では、プログラムの構成、GeoArrowの役割、環境構築、DataLoaderの動作確認までを扱います。データセットの整備と検出精度の比較は、次回の記事で紹介します。
1. 今回実装するDataLoaderの入出力
入力は、分割前のBBを保存したpv_bb.fgbと、PNG・PGW・PRJで構成された背景画像です。PNGは画素、PGWは画素と地理座標の対応、PRJは座標参照系(CRS)を保持します。
モデルへ渡すのは、1024×1024ピクセルの画像テンソルと、その画像内の座標で表したBBです。地理座標や地物IDをそのままニューラルネットワークへ入力するのではなく、元の情報を保持したうえでモデル用の表現を作ります。
本記事でいう「ビュー」はモデル入力用に構成する画像、「ROI」はその読み出し範囲です。保存タイルの境界と、学習用ROIの境界を別に扱うことが実装の中心になります。
| 段階 | 入出力 |
|---|---|
| 入力 | 分割前の矩形ポリゴン、位置情報付き画像タイル |
| 準備 | 地物のGeoParquet、タイルカタログ、ROIのプラン、COCO互換索引 |
| 実行時 | 必要なタイルから画像を接合し、元の地物から画像座標のBBを生成 |
| モデル入力 | 画像テンソルと、BB・ラベルなどを格納したtarget |
ここで比較対象となる固定タイル方式は、保存する画像の範囲でBBをクリップし、COCO形式の教師データを事前に作る方法です。今回の実装は、地物全体を含むROIを計画し、画像を実行時に構成します。
図1:共通の元データから学習入力を構成する流れ(模式図)。SpatialArrow側ではROIを事前に計画し、必要な画像部分を実行時に接合する。
2. プログラムの構成
今回の実装は、spatialarrow_pvというPythonモジュールにまとめています。独立した公式配布パッケージのインストール手順ではなく、本実験用ソースの構成です。
| ファイル | 主な役割 |
|---|---|
core.py |
タイル走査、地物読込み、ROI探索、画像接合、座標変換 |
geoarrow_store.py |
GeoArrowのネイティブジオメトリ配列とWKBの相互変換 |
rf_adapter.py |
RF-DETRのDatasetとの接続、テンソル化、BBの正規化 |
cli.py |
diagnose、prepare、preview、probe、trainの入口 |
__main__.py |
python -m spatialarrow_pvからCLIを呼び出す |
requirements_geo.txt |
地理空間処理に必要な依存パッケージ |
処理は、準備段階と学習時に分かれます。ROI探索を毎回の__getitem__()で実行するのではなく、探索結果をプランとして保存し、学習時はそのプランを読み出します。
厳密には、画像と教師を1サンプルずつ作る中心部分がDatasetで、バッチ化やワーカーによる読込みを担うのがPyTorchのDataLoaderです。本記事では、この接続全体をDataLoaderの実装として説明します。
3. 環境構築
使用した環境
以下は本実験の環境で確認したバージョンです。すべての環境での動作を保証する組合せではありません。
| 項目 | バージョン・設定 |
|---|---|
| Python | 3.11.16 |
| RF-DETR | 1.11.0 |
| PyTorch / torchvision | 2.12.1+cu130 / 0.27.1+cu130 |
| NumPy | 2.3.5 |
| PyArrow / Pyogrio | 25.0.1 / 0.13.0 |
| Shapely / PyProj | 2.1.2 / 3.7.2 |
| GeoArrow PyArrow | 0.3.0 |
| Pillow | 12.3.0 |
| GPU | NVIDIA RTX PRO 6000 Blackwell Max-Q |
| 混合精度 | bfloat16 |
動作済みのRF-DETR環境へ追加する
ここでは、GPUでRF-DETR Mediumが動作するconda環境rfdetr_bwがあることを前提にします。CUDA・PyTorch自体の新規導入は扱いません。既存環境を複製し、地理空間処理用ライブラリを追加します。
conda create -n spatialarrow_rfdetr --clone rfdetr_bw
conda activate spatialarrow_rfdetr
cd /path/to/spatialarrow_rfdetr_v01
python -m pip install \
"numpy==2.3.5" "pyarrow==25.0.1" "pyogrio==0.13.0" \
"shapely==2.1.2" "pyproj==3.7.2" \
"geoarrow-pyarrow==0.3.0" "Pillow==12.3.0"
python -m pip check
python -m spatialarrow_pv diagnose
/path/to/spatialarrow_rfdetr_v01はソースを展開した場所に置き換えます。モジュールの親ディレクトリから実行してください。
diagnoseではパッケージのバージョン、CUDAの利用可否、GPU名、RF-DETRのCocoDetectionの引数を表示します。RF-DETRは内部APIとの接続があるため、本記事の対象バージョンを確認してから進めます。
NumPyのバージョン制約が別ファイルやPIP_CONSTRAINTで指定されている場合は、その内容と導入するパッケージの要件を確認してください。本実験ではNumPy 2.3.5でpip checkが通る環境を使用しました。依存関係の確認後、環境情報を保存しておきます。
python -m pip freeze > environment-pip.txt
conda env export > environment-conda.yml
4. GeoArrowで分割前の地物を管理する
load_objects()はpyogrio.read_arrow()でFlatGeobufを読み出します。ジオメトリはCRSを付けたGeoArrow配列へ変換し、PolygonとMultiPolygonをネイティブなMultiPolygon型へそろえます。
次はgeoarrow_store.pyの中心部分です。
import geoarrow.pyarrow as ga
from pyproj import CRS
def native_geometry(values, crs):
wkb = ga.with_crs(
ga.as_wkb(values),
CRS.from_user_input(crs).to_json_dict(),
)
native = ga.as_geoarrow(wkb, promote_multi=True)
if native.type.extension_name != 'geoarrow.multipolygon':
raise ValueError(f'Expected native GeoArrow MultiPolygon, got {native.type}')
return native
永続化にはGeoParquetを使用します。objects.geoparquetには、元のCRSのジオメトリ、object_id、class_idなどを保存します。保存時のジオメトリ表現はWKBで、実行時にネイティブGeoArrow配列へ戻して利用します。
地物IDは、指定したID属性または利用可能なfid・idから作り、該当する属性がない場合はジオメトリのハッシュを使います。形状を編集するとハッシュも変わるため、継続して地物を追跡する運用では、一意で欠損のないID属性を用意して--id-fieldで指定します。
空間検索やCRS変換にはShapelyとPyProjも使用します。GeoArrowから必要な地物を選び、WKBを介して変換処理へ渡すため、処理全体をゼロコピーとする実装ではありません。
5. 保存タイルの位置とROIを計画する
タイルを共通の画素グリッドへ配置する
scan_tiles()はtrainとvalidのPNGを走査し、対応するPGW・PRJを読みます。今回の実装では、北向きで回転のないタイル、同一CRS、同一画素サイズ、共通グリッドへの整列を前提にします。混在するCRSや解像度を持つタイルの自動再投影は行いません。
PGWの左上座標は画素の中心を示すため、画像の境界を基準とする座標へ半画素分を変換します。これを基準に各タイルの画素位置を登録します。
地物全体を含むROIを選ぶ
Plannerは地物とタイルの空間索引を作り、各地物を起点に切り出し位置とサイズを探索します。今回の設定では1024~2048ピクセル四方の候補を使用します。
採用条件は、ROIと面積を持って交差する既知のBBがすべて完全に収まること、タイルの領域でROI全体を覆えること、trainとvalidの画像領域を混在させないことです。別のBBに内包されているという理由で地物を削除する処理はありません。
候補判定の中心は次の処理です。
region = box(x, y, x + size, y + size)
object_indexes = intersecting(self.object_tree, self.objects, region)
if any(not region.covers(self.objects[i]) for i in object_indexes):
return None, 'partial_object'
候補探索は有限で、全地物を採用できる保証はありません。未採用地物と理由の集計を出力し、学習対象となる範囲を確認できるようにしています。部分地物を採用しないのは本実験の方針であり、SpatialArrow全体がクリップを禁止するという意味ではありません。
6. 実行時に画像とBBを生成する
必要な画像部分をメモリ上で接合する
RuntimeReader.read(image_id)はプランを読み、ROIと交差するタイルだけを取り出します。各タイルの必要な部分をROI内の対応位置へ転記し、画像全体を構成します。
重複領域の画素値が食い違う場合や、透明画素・未充填部分が残る場合はエラーにします。透明でない黒画素を未整備領域として自動判定するものではないため、黒塗りマスクを含む入力は別途確認が必要です。
構成した画像の一辺がモデルの入力サイズと異なる場合は、1024×1024へリサイズします。タイルはLRUキャッシュで再利用し、--cache-tilesで保持枚数を指定します。
元のジオメトリから画像座標のBBを作る
RuntimeReader.boxes(image_id)は、ビューに対応する地物IDでGeoArrow配列を選択し、必要に応じて背景画像のCRSへ変換します。その外接矩形を共通画素座標へ変換し、ROIの原点を引いて、リサイズ倍率を適用します。
共通画素座標のBBを$(x_1,y_1,x_2,y_2)$、ROIの原点を$(u_0,v_0)$、一辺を$S$、モデル入力の一辺を$R$とすると、COCOの$(x,y,w,h)$は次のようになります。
s=\frac{R}{S},\qquad
(x,y,w,h)=\bigl(s(x_1-u_0),\ s(y_1-v_0),\ s(x_2-x_1),\ s(y_2-y_1)\bigr)
元の地物のジオメトリは更新せず、ビューごとにモデル入力用のBBを生成します。
Datasetでモデル用の形式へ変換する
SpatialCocoDetectionはRF-DETRのCocoDetectionを継承します。__getitem__()の流れを簡略化すると、次のようになります。
# 処理順を示す擬似コード。実際の検査処理などは省略しています。
def __getitem__(self, index):
image_id = self.ids[index]
image = self.reader.read(image_id)
boxes = self.reader.boxes(image_id)
annotations = load_coco_annotations(image_id)
annotations = replace_and_check_boxes(annotations, boxes)
image, target = self.prepare(image, annotations)
image, target = transform_for_model(image, target)
return image, target
実装では、実行時に生成したBBとCOCO索引のBBが一致することを確認してから、RF-DETR側の変換処理へ渡します。最後に画像をテンソル化・正規化し、BBを0~1の中心座標・幅・高さへ変換します。
学習時の幾何変換は、画像とBBを同時に左右反転する処理に限定しています。ランダムクロップやscale jitterによって、せっかく全体を含めた地物が再び切断されることを防ぐためです。
地理空間の文脈を取り出す
get_view_context(index)から、地物ID、参照タイル、ROI、CRS、画像座標から地理座標への変換情報を取り出せます。ただし、この情報は別メソッドで取得する構成であり、標準の学習targetへすべて自動挿入するものではありません。
また、この変換は左右反転前のビューを基準とします。拡張後の学習画像へ重ねる場合は反転も考慮する必要があります。検証データでは左右反転を行いません。
7. RF-DETRとの接続とCOCO互換索引
RF-DETRは既存のCOCO用処理でカテゴリや評価用アノテーションを管理するため、今回も_annotations.coco.jsonを生成します。ただし、そこに記録されたruntime_XXXXXXXX.pngは論理的な画像名であり、学習画像を一式保存しているわけではありません。
画像の読込みは独自Datasetが担当し、COCO互換索引は既存の評価器と接続するために使用します。「地理空間データを直接扱う」ことと、「モデル側のインターフェースに合わせた索引を作る」ことを両立させています。
install_bridge()は、実行中のPythonプロセス内でRF-DETRのDataset生成入口を独自実装へ差し替えます。インストール済みパッケージのファイルは編集しません。ただし、内部APIに依存するため、RF-DETRを更新した場合はdiagnoseとprobeによる再確認が必要です。
未対応の入口や入力解像度の不一致を検出した場合は停止します。今回のアダプタは物体検出用で、セグメンテーションには対応していません。独立したtestデータセットも扱わないため、学習処理ではrun_test=Falseとしています。
8. プラン作成から動作確認まで
入力を配置する
画像データセットにはtrainとvalidを用意し、それぞれのPNGにPGW・PRJを対応させます。分割前のBBはdata/pv_bb.fgbへ配置します。
以下の/path/to/georeferenced_tilesは実際のデータセットに置き換えてください。出力先は、新しい空のディレクトリを指定します。
python -u -m spatialarrow_pv prepare \
--source /path/to/georeferenced_tiles \
--objects data/pv_bb.fgb \
--output work/plan \
--split-policy preserve \
--resolution 1024 \
--base-size 1024 \
--max-size 2048
既存のtrain/valid分割を維持するためpreserveを指定しています。正本のBBを保存タイルごとに切断する処理はありません。
生成物を確認する
| 生成物 | 内容 |
|---|---|
objects.geoparquet |
元の地物ジオメトリとID |
catalog.json |
元画像へのパス、配置、CRS、更新確認用情報 |
views.json |
ROIの位置・サイズ、地物ID、参照タイル |
roi_object_index.parquet |
ROIと地物の対応 |
train/_annotations.coco.json、valid/_annotations.coco.json
|
RF-DETR・評価用の索引 |
manifest.json |
入力解像度、設定、入力FGBのハッシュなど |
report.json、uncovered_objects.csv
|
採用範囲、未採用地物、候補却下の集計 |
プラン作成後に画像や位置情報、地物を変更した場合は、新しい出力先にプランを作り直します。画像側はファイルサイズ・更新時刻による検査がありますが、あらゆる変更を完全に追跡する仕組みではありません。
プレビューとDataLoader接続を確認する
python -m spatialarrow_pv preview \
--plan work/plan --output work/preview --count 12
python -m spatialarrow_pv probe \
--plan work/plan --workers 4
previewは実際の画像接合処理で画像を生成し、BBを重ねた確認用画像を出力します。--count 12ではtrainとvalidをそれぞれ最大12ビュー確認します。屋根上のPVとBBが対応していることを目視で確認します。
probeでは、両splitのサンプルについて画像・BBの値、カテゴリ変換、RF-DETRのcollate関数によるバッチ化、Dataset生成入口との接続を確認します。GPU上での学習や全ビューの正しさまで保証する検査ではありません。
全ビューの画像読込みを確認する場合は、次を実行します。
python -m spatialarrow_pv preview \
--plan work/plan --output work/preview_all --all
--allは全ビューを読み込みますが、確認用画像の保存数は制限されます。全画像を目視レビューしたこととは区別します。
9. 学習を起動する
DataLoaderの接続確認後、tmux内で学習を起動します。ここでは最大50 epoch、batch size 8、勾配累積2とします。
tmux new -s spatialarrow_train
conda activate spatialarrow_rfdetr
cd /path/to/spatialarrow_rfdetr_v01
mkdir -p logs
set -o pipefail
python -u -m spatialarrow_pv train \
--plan work/plan \
--epochs 50 --batch-size 8 --grad-accum-steps 2 --workers 4 \
2>&1 | tee "logs/train_$(date +%Y%m%d_%H%M%S).log"
未カバーの地物がある場合、このコマンドは学習前に停止します。report.jsonとuncovered_objects.csvを確認し、採用された部分集合で実験する場合だけ--allow-uncoveredを追加します。これは未採用地物を学習へ戻すオプションではありません。
学習コマンドは、解像度をプランから読み出し、アダプタを設置してからRFDETRMediumを作成します。multi-scale・scale jitterを無効にし、学習設定とプラン情報をspatialarrow_run.jsonへ保存します。
tmuxから離れる場合はCtrl+Bの後にDを押し、再接続には次を使用します。
tmux attach -t spatialarrow_train
おわりに
本記事では、GeoArrowで元の地物を保持し、事前に計画したROIから画像とBBを実行時に生成して、RF-DETRへ渡す実装を説明しました。保存タイル、地物の正本、モデル入力を分けることで、既存の画像タイルを利用しながら、地物全体を含む学習ビューを構成できます。
次回は、この実装を実際の屋根上PVデータへ適用します。アノテーションの作成方針、採用できた地物と未採用地物、固定タイル方式との比較条件を整理し、共通の検証データで検出精度がどのように変わったかを確認します。
第1回【SpatialArrow概念編】GeoAI学習で「画像分割によって地物が途切れる問題」をどう解決するか?(固定タイル方式の課題とSpatialArrowの設計思想)
第2回【SpatialArrow実装編】GeoAI学習で地物を切り刻まずに学習へ渡す(ROI単位で画像とBoundingBoxを生成する、AI-ReadyなDataLoaderの実装)
第3回【SpatialArrow検証編】GeoAI学習で地物を分割せずに学習させたら精度は上がるか?(屋根上PV検出の比較検証で見逃しが16.5%減少)
