海底写真を撮る場合、次のようなシステムとなります。
フライトコントローラーからGPS, コンパスデータをもらいます。
またカメラのシャッター指示もフライトコントローラーからなされます。
ビデオ
オンラインでみたいので、LTE接続が好ましいということになります。USBエクステンダーの可能性もあります。
カメラ
フライトコントローラーのGPIOをチェックしてコンパニオンコンピュータからGoProにシャッター指令を出します。
ウィンチ
海底に近づけるためにウィンチでカメラの位置を変更します。
ポストプロセス
陸上に引き上げたら、Goproから写真。コンパニオンコンピューターから写真ファイルの名称、時間、緯度経度、方向を記録したファイルを引き上げ、データを並べて見てみます。写真の撮影範囲の大きさは海底からの距離とカメラの性能によるので調整が必要です。
これを読んでおられる方は「こいつはGoogleMapとかGoogleEarthとか便利なサービスを知らないのか?」と考えているかもしれません。いやいや、とっくに試しました。しかし画面に1メートル単位の地図を表示し、直接写真を地図上に貼るAPIなんてないんです。
さらにライセンスの問題があります。Googleサービスは一見無料ですが、多く使うと課金されるようになっています。
Googleなしでブラウザー上で地図にイメージを貼り付けるにはLeafletというパッケージを使います。写真を方向に応じた回転をさせるためにはプラグインパッケージのLeaflet.DistorableImageを使います。
以下、制作したJavascriptの主要な部分をあげます。これを利用していろいろカスタマイズしてのぞみの地図が作れるはずです。タイルは国土地理院のものを使います。
地図の表示
表示するページのhtml内に次のdivを用意します。地図表示のエリアです。
<div id="maparea"></div>
他にLeafletを使うためにファイルを読み込むリンクをhead内に設定します。
<head> <meta charset="UTF-8" /> <meta http-equiv="Pragma" content="no-cache"> <meta http-equiv="Cache-Control" content="no-cache"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" /> <met name="description" content="map under the sea." /> <title>海底写真地図 V.0.1</title> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <link rel="stylesheet" href="オレオレ.css" /> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://cdn.jsdelivr.net/npm/leaflet-imageoverlay-rotated"></script> </head>
最後の一行をよくみてください。imageoverlay-rotetedはLeafletのプラグインで回転した写真をそのまま貼ることができます。
これの存在に気づくまでかなりかかりました。
まず、マップのタイル表示は以下でできます。まずは地図が出ないとどうしようもないので、以下を動かしてみてください。
渡すパラメーターのlatはlatitude(緯度)、lonはlongitude(経度)です。ぱっと思いつかない方はlon=38.40481 lat=141.40944でも入れてみてください。石巻市蛤浜の緯度経度です。
// mapの描画
function map_init(lat, lon){
map = L.map('maparea',{zoomControl: false})
//地図の中心とズームレベルを指定
map.setView([lat, lon], 18)
//表示するタイルレイヤのURLとAttributionコントロールの記述を設定して、地図に追加する
var gsi = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
{zoomSnap: 0.1, zoom: 18, maxNativeZoom:18,maxZoom:25,attribution: "地理院標準タイル",})
var gsiphoto = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg',
{zoomSnap: 0.1, zoom: 18, maxNativeZoom:18,maxZoom:25,attribution: "地理院写真タイル"});
var gsipale = L.tileLayer('http://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png',
{zoomSnap: 0.1, zoom: 18, maxNativeZoom:18,maxZoom:25,attribution: "地理院淡色タイル"});
//オープンストリートマップのタイル
var osm = L.tileLayer('http://tile.openstreetmap.jp/{z}/{x}/{y}.png',
{zoomSnap: 0.1, zoom: 18, maxNativeZoom:18,maxZoom:25,attribution: "OpenStreetMap" });
//baseMapsオブジェクトのプロパティに3つのタイルを設定
var baseMaps = {
'地理院地図' : gsi,
'地理院写真' : gsiphoto,
'地理院淡色' : gsipale,
'OSM' : osm,
};
// layers コントロールにbaseMapsおbジェクトを設定して地図に追加
// コントロール内にプロパティがでる
L.control.layers(baseMaps).addTo(map);
// とりあえずデフォルト
gsi.addTo(map);
//スケールコントロールを最大幅200px,右下、単位mで地図に追加
L.control.scale({ maxWidth: 200, position: 'bottomright', imperial: false }).addTo(map);
//ズームコントロールを左下に
L.control.zoom({ position: 'bottomleft'}).addTo(map);
}
表示できたでしょうか。出ない場合はブラウザーのデベロッパーコンソールからエラーを解析してください。
さらにここに一枚、写真を貼ります。
function paste1(img_url, centerLat, centerLon, direction){
const lat1m = 0.000008983148616; // 緯度1メートル
const lon1m = 0.000010966382364; // 経度 1メートル
const topleft = [centerLat+lat1m, centerLon-lon1m]
const bottomright = [centerLat-lat1m, centerLOn+lon1m]
bounds = L.LatLngBounds(topleft, bottomright)
L.imageOverlay(img_url, bounds).addTo(map)
}
渡すパラメーターの説明です。
img_urlはなんでもいいのですが、迷ったら
img_url = “https://blog.umineco.company/wp-content/uploads/2023/10/underthesea.jpg”
でもお使いください。
cetnerlatは写真の中央の緯度経度を意味します。directionはこの関数では使っていないので0で。
コードで推測できると思いますが、貼り付ける写真の左上と右下を指定します。このふたつの緯度経度をまとめた配列をboundsとleafletでは呼びます。
これを実行すると地図に写真が貼られるはず。これをイメージオーバーレイといいます。
次に写真を撮影された方位にあわせて回転します。座標を求める関数がこれ。AIと相談しながら作りました。
出力は回転した写真の左上、右上、右下の3点です。rotatedプラグインが必要とします。
widthとheightはグローバル変数として定義してください。単位はメートルです。
function calculateRotatedImageBounds(centerLat, centerLon, direction, width, height) {
// 方向をラジアンに変換
const angle = (direction * Math.PI) / 180;
// 画像の半分のサイズを計算
const halfWidth = width / 2;
const halfHeight = height / 2;
// 回転行列を適用する関数
function rotatePoint(x, y) {
const rotatedX = x * Math.cos(angle) - y * Math.sin(angle);
const rotatedY = x * Math.sin(angle) + y * Math.cos(angle);
return [rotatedX, rotatedY];
}
// 3つの角の座標を計算
const topLeft = rotatePoint(-halfWidth, halfHeight);
const topRight = rotatePoint(halfWidth, halfHeight);
const bottomLeft = rotatePoint(-halfWidth, -halfHeight);
// 緯度経度に変換(簡易的な平面近似)
const latPerMeter = 1 / 111111; // 1度あたりのメートル数の逆数(概算)
const lonPerMeter = 1 / (111111 * Math.cos(centerLat * Math.PI / 180));
function toLatLng(point) {
const lat = centerLat + point[1] * latPerMeter;
const lon = centerLon + point[0] * lonPerMeter;
return L.latLng(lat, lon);
}
// 3点の緯度経度を返す
return {
topLeft: toLatLng(topLeft),
topRight: toLatLng(topRight),
bottomLeft: toLatLng(bottomLeft)
};
}
これにあわせてpaste1関数を書き直します。
function paste1(img_url, centerLat, centerLon, direction){
bounds = calculateRotatedImageBounds(centerLat, centerLon, direction, img_width, img_height);
const rotatedOverlay = L.imageOverlay.rotated(
img_url,
bounds.topLeft,
bounds.topRight,
bounds.bottomLeft,
{
opacity: 0.8,
interactive: false
}
).addTo(map);
}
これらが写真イメージ表示に必要なプログラムです。
私の場合は、写真撮影のログから緯度、経度、方向、ファイル名を取り出してpaste1関数を動しています。写真のEXIF情報からpaste1関数を動かすなどもできると思います。
こんな感じができあがりました。