
前々回記事
vol.44 【損傷個所を診るためのビューア"てんかく忍者QGISプラグイン"のプロトタイプを紹介します】で
損傷一覧を得るまでの①~④の仕込みがあるという話をしました。
またこれらを実現するためのQGISプラグインの機能を
vol.45 【損傷個所を診るためのビューア"てんかく忍者QGISプラグイン"のプロトタイプの機能】
でご紹介しました。
今回は、下記の"仕込み"と呼んだ部分で実際に何をしているのかを深堀りしていきます。
映像に写っているものを抽出し、意味を与えるキモの工程と言えると思います。
QGISで"おいしい結果"を味わってもらうため、腕の振るい甲斐があります。
今回は、具体的な処理方法を説明するため、若干のプログラマートークとなりますが、「中でこんなことをしているんだなー」「ここを修正すれば当社にも適用できそうだなー」というような理解でOKです。
GNSS-BeatBoxを用いた撮影でアウトプットされる*.gpxと*.mp4の同期処理とYOLOによる損傷検出を行う工程です。
| input: | *.mp4、*.gpx |
| output: |
*_yolo.mp4、*_yolo.txt、*.parquet (*minimodem*.csv、*.wav、*.log) |
(カッコ内)は途中生成ファイル。
弊社内では、ultralytics社のYOLO11をPythonから起動して、動画の物体検出を実行するプログラムを作り、使用しています。
decode_from_mp4.pyというプログラムが、mp4から同期音声を抽出してwavファイルを生成し、mp4はYOLO物体検出のvideo_out.pyへ、wavファイルはminimodemへ送り、GPXに記録されたstartが音声トラック(=映像トラック)の何フレーム目から開始されているかを検出します。
"""
decode_from_mp4.py
MP4動画から音声信号を抽出・復号し、minimodemログ・ビート情報・YOLO検出結果・GPXデータを統合して
ParquetおよびSRT形式で出力する統合処理スクリプト。
主な処理ステップ:
1. 音声抽出・拡張・フィルタリング
2. ビート検出とStartフレーム推定
3. minimodemによる信号復号とログ処理
4. YOLOによる物体検出(未出力時のみ)
5. GPXとの時刻同期と空間統合
6. ParquetおよびSRT出力
7. ダッシュボードへの進捗通知
使用例:
>>> python decode_from_mp4.py input.mp4 --job_id pothole_yolo8m/0001
"""
"""
video_out.py
YOLOを用いて動画フレームの物体検出を行い、アノテーション付き動画と検出結果ログを出力するモジュール。
主な機能:
- 動画ファイルの読み込みとフレーム単位の処理
- YOLOモデルによる物体検出とアノテーション描画
- 検出結果のテキスト出力と動画保存
- 処理状況のログ出力およびジョブステータスの更新
使用例:
>>> python video_out.py --input sample.mp4 --model pothole_yolo8m.pt --show
"""
動画の各フレームと緯度経度を対応させるのに先立ち、1秒ごとに取得されたGPXデータを動画のフレーム数に応じて案分します。
衛星時刻の差分と緯度経度を平面座標に変換した上で速度勾配を使って案分距離を補正します。
・動画のFPSに応じた案分処理:shred_self
・特徴的な機能"同期音声の検出":detect_first_beat、analyze_beat
・特徴的な機能"ドロップフレーム処理":get_fps_from_video、is_drop_frame、time_to_frame
| ポットホール検出の例 |
![]() |
YOLOv8向けにネットで提供されているポットホール検出モデルを、庄内川堤防道路の動画で検出している例。
video_out.pyは、WindowsのPowerShellde動作し、複数起動が可能です。
| 複数同時実行(6タスク) |
![]() |
6つ動画を同時に処理している間のGPU,CPU,メモリ、SSDなどのシステムリソースの利用状況です。
| 動画物体検出中の負荷(GPU) | 物体検出中の負荷(SSD) |
![]() |
![]() |
(GPU対応のプログラムとして構成されているにも関わらず)GPUはほとんど使われておらず、CPUと動画の書き出し先になっているSSDの負荷がMAXになっています。
アノテーション付き動画と結果テキストを生成
![]() |
video_outを実行するとアノテーション付きの動画と、動画フレームごとのYOLO物体検出結果のテキストが出力されます。
| GISデータ、静止画、オフラインビューアを出力するgenerate_maps |
![]() |
| input: | *.parquet、*_yolo.mp4、*_yolo.txt |
| output: | WEBマップ html,js,json,geojson、images/*.jpg GISデータ gpkg、shp,geojson、geoparquet、kml |
"""
generate_maps モジュール
YOLO検出結果をもとに、地図と画像を自動生成するコマンドラインツール。
Parquet形式の検出ログを入力として、GeoJSONやHTMLマップ、画像フレームを出力する。
使用例:
python generate_maps.py --archive_root "W:/WatchFolder/Archived" --mode density --min_velocity 0.1
"""
generate_mapsの動作
# 1. .fused.parquet を探す
# 2. Parquet読み込みとbbox正規化
# 3.フィルタ処理を通す(補完・整形・フィルタ済みのdfを得る)
# 4. YOLO検出結果配列のJSON化
# 5. フィールドの加工(YOLO結果、日本時間)
# 6. GeoDataFrame生成(加工+geometry生成、属性選択はなし=全フィールドをそのまま出力)
# 7.コンテンツ中心を決めておく
# 8. 初回用テンプレート生成(上書きしない)
# 9. Webマップ用GeoJSON + 表示名辞書を出力
# 10.geoJSONを書き出す
# 11. GIS形式の出力(ユーザ指定に応じて)
# 12. HTMLマップ生成
# 13. frame_index.json 生成
映像のフレーム単位で出力されたYOLOの検出結果(*yolo.txt)をparquetデータベースと突合し、フィルタリングや新たな列値の生成を施した結果を、各種GISフォーマットへと変換出力します。(generate_maps)
この時に、速度ゼロ(停止中)のフレームの除去や、空間統合のインデックスデータ付与などを実行し、データの間引きを行うことで、これ以降の処理が必要なフレームのみに最適化されます。
YOLO検出結果の生データから、主要な検出クラスや検出数、検出数のMAXや平均などの統計情報も予め付与したり、GNSSの受信精度を評価してランク値を付与しています。
※その後の処理でGNSS受信精度が信頼できない値のものを削除する・・などの調整が可能なようにこのような属性を付与しています。
| generate_mapsが出力するGISフォーマット |
![]() |
KPマスタを作成する最も簡単な方法は、既存のKPを持つポイントデータの差分を補完処理すること、もしくは国土交通省が公開している、線路中心線データや道路ネットワークデータを入手し、この上にポイントを生成する方法です。
QGISでは線分を距離や分割数によって案分する"QChainage"などのプラグインもあります。
使い方等はここでは説明を割愛しますが、機械的にKPマスタを自動生成する方法は色々ありますので、既に手元にKPの情報をお持ちのお客様は、一度弊社にご相談下さい。100m単位のKPマスタから1mKPに案分したり、目標物で位置を合わせるなどの修正も可能です。
この作業の最後に、新たにKPを付与したgpkgを*_kp.gpkgなどの名称で別名保存してください。
※QGISの揮発レイヤとしてメモリ上に生成したものは、明示的にファイル保存しないと保存されません。
| KP(キロ程)マスタ |
![]() |
ここまででQGISで読み込むための空間データベース*.gpkgが出力されます。
次にKPを割り当てるのは、QGISプラグイン上での作業になります。
| input: | *.gpkg、*yolo.mp4 |
| output: | *.gpkg |
QGISでinputファイルを呼び出して、プラグインの"開幕"を実行するとGridにgpkgの属性情報が表示されます。
次に、QGISのレイヤメニューから"レイヤ追加"-"ベクタレイヤを追加"を選択して、予め準備したKPマスタを選択します。
KPマスタは空間オブジェクト(ポイントデータ)の他には、kp列(浮動小数点)があればOKです。
最後に、QGISプラグインのTenkaku操作座敷メニューから"補足情報"を選択すると、下記のダイアログが現れます。
*.gpkgで追加したい列名としてkp、kp参照元の列名(例:kiroteiなど)を入力し、実行ボタンを押します。
この操作だけで、データグリッド上のすべてのレコードの緯度経度と、KPマスタの最近接点を計算し、すべてのレコードにKPの値が代入されます。
KPマスタが更新されれば、新たに値を振りなおすこともできますので、仮のKPをあてておいて、後に修正済の正確なKP等で割り当て直すことも可能です。
更にKPだけではなく、任意の地理オブジェクトの属性情報を、列値として代入する機能を追加することも可能です。
例えば、施設管理オブジェクトとして橋梁や橋脚などの番号、遊間番号、枕木グループ番号、踏切名称や何らかの系列、系統名など部ジェクトがあれば、そこに"含まれる"や"最も近い"などの空間検索の結果を列値に含められます。
線状ではなく、面上で決まった管理IDが無い場合は、ボロノイ図などで空間的なインデックスを作成しておいて、そのIDを割り当てておくもの有効だと思います。(架電柱などの管理でボロノイ図による空間タイルを作成したことがあります)
| YOLOで検出した遊間(レール継ぎ目)位置の可視化 |
![]() |
QGIS上に読み込んだ空間データベースの内容を加工して、YOLO物体検出の結果から"損傷スコア"を割り出し、"損傷レベル"を付与します。
| input: | なし |
| output: |
graphs/*.png aggregated_damage_scores.csv |
②までで、すべてのレコードに案分された正確な位置情報とキロ程が付与されたレコードがグリッド上に準備されます。
次にデータグリッドに意味を注入する作業"損傷スコアと損傷レベルの代入"をします。
操作としてはシンプルです。
グリッドで全件を表示し、KP昇順に並んでいる状態で、"まとめ・分析"ボタンを押します。
この処理はデータ量に拠りますが300秒位掛かります。
少々回り道になりますが、データ間引き処理について補足します。
| 損傷スコアと損傷レベルのグリッド表示 |
![]() |
<tr">データ間引きの秘密
![]() |
②までで作成したKPを付与したgpkgを再度QGISプラグインで呼び出すとちょっと面白いことが起こります。
KPが列に含まれている場合は、KPの昇順でデフォルトがソートされ、データ件数が少なくなっています。
これは不具合ではなく、レコード全件をキロ程の1m単位で代表選出した選ばれしレコードだけが表示されているからです。
1mKPで複数の画像が存在する場合に、ただ一枚を選択することをQGISプラグインがデータを準備して、グリッドに表示する前に実行しています。代表選抜の根拠としているのは、YOLOの物体検出数が多いフレームを優先です。
間引きの効果は絶大で、6-7万件あったデータが1.7万件などに減ります。
動画のフレームに写っている対象が1m以下の狭いエリアを撮影していれば別ですが、通常の撮影条件で1m単位で写真が表示されていれば、多くの場合はフレーム欠落なしで余計なフレームを省いて、見るべきところだけ見ている状態になっているのではないでしょうか?
| 損傷スコアと損傷レベル |
![]() |
「さて、いよいよ本題です。」
「YOLOが検出したクラス毎のアノテーションやコンフィデンスのデータを、どのように損傷スコアとして値に出来るのか?」
また
「損傷スコアとして算出された数値から、どのようにして損傷レベルを評価するか?」
これは実は一筋縄でいく問題ではなく、機械学習やデータマイニングの出番となるところです。
今回の事例しているYOLO損傷検出(物体検出)モデルは、保線管理のための枕木検出モデルです。
PC枕木、合成枕木、木枕木、木枕木の損傷、交換対象枕木のマークなど枕木損傷を知るためのクラスと、遊間、踏切、トンネルなどの位置を知るためのランドマークをクラス化しています。
このクラス分類は、明らかにどの種類の枕木がどこに敷設されているか?その状態は良いのか?悪いのか?を知るためのクラス設計になっていますね。踏切、遊間、トンネル、橋梁などは位置を確認するために入っているランドマークと見做せます。
PC枕木化されている部分は安心材料で、交換対象枕木や痛みのある枕木は不安材料。PCと木枕木が混在している部分の経過も知りたいはずだと推測します。
PC枕木→-2
合成枕木→-1
木枕木損傷→+1
交換対象枕木→+2
木枕木正常→0
その他→0
係数は、ひとまずこれでやってみます。
「何がどうだから安心、不安」ということを上記の情報からどのように導くか?
補正係数や多項式などで実際には人間の感覚にフィットするようなマッチングが必要であることは理解していますが、「エイや!」で仮に考えてみましょう。
以前とあるお客様と実施した、一般道路面損傷の劣化度評価の経験から、「物体検出のアノテーション数や面積は、人間が評価する際のレーーティングと80%近い相関を見せる」という事例があります。
この事例を流用すれば、「たくさん出ているクラスと人の感覚は相関している&出現数が少なくても大きく影響する区分が存在する」・・という点を考慮できていれば、良い結果を導き出せるはずです。
もちろん、ここでやっている係数当てはめが正しいという確証はありません。
ですので、プログラム内で定数ではなく、外部から供給できる変数として定義しておきます。
{
"03_Magi_PC": -2,
"04_Magi_Gosei": -1,
"06_Magi_Seijou": 0,
"05_Magi_Yukan":0,
"13_Fumikiri": 0,
"11_Tunnel": 0,
"12_Bridge": 0,
"02_Magi_KakeTou": 1,
"01_Magi_DmgPt": 2
}
損傷スコア=検出クラス毎の検出数*出現頻度の正規化係数*確からしさ*係数
枕木の本数を数える時、それぞれの出現頻度を補正してやらないと、滅多に検出されない"レアクラス"では1本の重みが異なるので、検出数が多いものほど影響が出ないように検出総数を母数として正規化しておきます。
これで交換対象枕木のように1か所の意味合いが大きいものの声がより通るようになるはずです。
YOLOの検出結果の信頼度は、そのスコアを確かさを雄弁に語る重要なパラメータになりますので、これも掛けておきます。
仮で設定した損傷スコアによって算出された結果を集計して確認します。
損傷スコア0が突出して多いのは、ランドマークなどの検出の係数を0と設定しているからで、"0に何を掛けてもゼロになるので、0が突出していることは正しい"。
注目すべきは、仮で設定した算定式で導いた損傷スコアの分布が正規分布に近い形を示していることです。
正規分布が常に正しいという保証はないものの、世の中の統計の大部分が正規分布に近い形で出ることからすると、何か安心できる結果になっている気がします。
| 損傷スコアの頻度分布 |
![]() |
すべてのレコードにKPを割り当てているので、KPごとに損傷スコアを集計してグラフ化してみましょう。
特定のKP範囲で損傷スコアがマイナス側(グラフの青)にふれているところと、プラス側(グラフの赤)にふれているところが分布しているように見えてきます。
赤い部分は木枕木(損傷枕木)が連続しているところで、青いところはPC枕木や合成枕木が連続していることであることが写真で確認できました。
例外的な部分がどこにあるのかについては別途確認がに必要ですが、マクロ視点でどこがどうなっているを見るにはなかなか良い指標になっている気がします。
| 損傷スコアのKPごと分布 |
![]() |
グラフだけではなく地理的な状況と見比べるのにはGISが便利です。
特に今回は、保線におけるレールや枕木などの支持力の維持が目的ですので、「カーブや勾配などの列車のスピード変化や荷重移動が大きな部分に傷みが集中するのではないか?」といった仮説を持っています。
gpkgレイヤを複製して、ヒートマップレイヤを作成する。
damage_score列の値を使ってヒートマップを生成してみると、先にKPで表現していた赤いところ、青いところが、地形的にどういったところなのかを背景図と合わせて表現することが出来る。
| 損傷スコアのGIS上ヒートマップ表示 |
![]() |
先の損傷スコアの頻度分布グラフを眺めていると、下記のような区分ができるように見えてきます。
損傷スコア<-1 : 優良
-1<損傷スコア<0 :安定
0<損傷スコア<0.5 :観察
0.5<損傷スコア<1 :注意
1<損傷スコア :損傷
合っている確証はありませんが、何となく等区分で良い気がします。
言葉の当てはめが適切かどうかも考慮すべき点ですが、まずこれで結果と照らし合わせてみましょう。
損傷レベルと損傷スコアの当てはめはプログラマーが定数として決め打ちすべきものではなく、顧客との議論や点検時の感覚の取り込みが重要なので、変数として外部から与えられるようにしておきます。
{
"優良": {
"range": [-999, -1],
"weight": 0,
"color": "#2E8B57",
"label": "優良",
"comment":"状態は非常に良好です。"
},
"安定": {
"range": [-1, 0],
"weight": 1,
"color": "#4682B4",
"label": "安定",
"comment":"状態は良好です。"
},
"観察": {
"range": [0, 0.5],
"weight": 2,
"color": "#FFCE54",
"label": "観察",
"comment":"軽微な損傷が見られますが、全体的には安定しています。"
},
"注意": {
"range": [0.5, 1.0],
"weight": 3,
"color": "#FC6F51",
"label": "注意",
"comment":"損傷の疑いがあります。目視確認を推奨します。"
},
"損傷": {
"range": [1.0, 999],
"weight": 3,
"color": "#DA4453",
"label": "損傷",
"comment":"明確な損傷が認められます。対応が必要です。"
}
}
| GIS上での損傷レベル可視化 |
![]() |
先にQGISで損傷スコアをヒートマップ化しましたが、今度は損傷レベルの値に応じて、ポイントの大きさや色を設定して可視化して、識別しやすくします。
損傷スコアの値を区分していますので、損傷スコアのヒートマップで赤いところは、赤い大きな丸が出てくるのは当然ですが、より"注意してみるべきところ"と"今は問題にしなくても良いところ"が明確に可視化できています。
この、一目で分かるように、視覚的に表現する・・というのがグラフやGISの良いところです。
いよいよ最後の加工です。
予め見るべき箇所として選別して、必要な情報のみを抽出するのは、フィルタの役割です。
初期状態では下記の列項目を選択できるように設定しています。
| フィルタによる絞り込み |
![]() |
KP範囲ドロップダウン指定
任意列ドロップダウン指定
fix:rtk-fix,rtk-float,dgps-fix、gps-fix
yolo_status:detected、silent
check_flag:True/False
damage_level:設定による
dominant_class:YOLOクラス
任意列複数指定
任意列は例えば、damage_levelで絞り込んで、更にdominant_classを追加指定するなどの複数指定が可能です。
| パンくずボタン |
![]() |
設定したフィルタ条件は、ボタンとしてグリッド上部に現れます。これはWEBサイトのナビゲーションにあるパンくずリストと同様に、検索条件の絞り込みを緩める場合に使います。条件を再設定することを繰り返す場合に便利です。
|
Excel出力 |
![]() |
output:*_grid_export.xslx
グリッドに表示されるレコードを絞り込んだ状態で"Excel出力"のボタンを押すと、表示されているレコードのExcelが出力されます。絞り込みの結果が数百件程度であれば1秒もかからずにExcelが生成されます。
17,000件の場合でも53秒でした。
| 画像一括出力 |
![]() |
output:images/****/*.jpg
レイヤで*gpkgのレイヤで選択されていることを確認して、"画像一括出力"ボタンをクリックすると、グリッドで選択されているレコードに該当する静止画がimages/中間フォルダ/*.jpgとして書き出されます。
およそ1.2-1.3FPS程度で書き出します。
17,000件の静止画出力で10,615秒かかります。(約3時間です)
| ヒートマップ出力 |
![]() |
output:graph/heatmap*.png、yolo_detections_export.csv、class_cooccurrence_matrix_weighted.png、class_cooccurrence_matrix_note.txt
"ヒートマップ出力"ボタンでグリッドに表示されているレコードの、YOLO検出クラス毎の検出数をKP単位で集計したヒートマップが出力されます。
全件の傾向を偏りなく見るため、通常はフィルタを全件に戻して行います。
KP1Kmで1枚のヒートマップを出力するので、KP範囲で17Kmあれば17枚のグラフを出力します。
YOLO検出結果を検出クラス毎に集計し、フレーム番号とKPを付与したデータを、他のデータマイニングのためにcsvとして出力します。
17,000件のヒートマップ出力で131秒かかります。(約2分です)
| 損傷スコアグラフ出力 |
![]() |
| 共起グラフ出力 |
![]() |
output:graphs/damage_score_*.png、aggregated_damage_scores.csv
"分析・まとめ"ボタンを押すと、グリッドに表示されているレコードで分析・まとめを実行します。
全件の傾向を偏りなく見るため、通常はフィルタを全件に戻して行います。
報告書で挿入される損傷スコアグラフや他データマイニングツールのためのcsvデータを出力します。
損傷スコアグラフは、全体とKP1Km単位の複数のグラフを出力
17,000件の分析まとめで280秒かかります。(約5分です)
| 報告書出力 |
![]() |
output:*_report.xlsx
"報告書出力"ボタンを押すと、グリッドに表示されているレコードの報告書を生成します。
全件の傾向を偏りなく見るため、通常はフィルタを全件に戻して行います。
17,000件の報告書出力で168秒秒かかります。(約3分です)
▼この記事を書いたひと

R&Dセンター 松井 良行
R&Dセンター 技術戦略担当部長。コンピュータと共に35年。そしてこれからも!
●富士見事務所 TEL : 052-228-8744(交通部営業課) FAX : 052-323-3337(交通部共通)
〒460-0014 愛知県名古屋市中区富士見町13−22 ファミール富士見711 地図
PoCのお問い合わせ:交通部営業課
技術的なお問い合わせ:R&Dセンター


