宙畑 Sorabatake

解析ノートブック

2020年の抱負を叫ぶため、衛星データでやまびこができるポイントを探す!(前編)

やまびこができるポイントを探して叫びたい! ということで、衛星データでやまびこができるポイントを探せるのか、実践してみました。

2022年8月31日以降、Tellus OSでのデータの閲覧方法など使い方が一部変更になっております。新しいTellus OSの基本操作は以下のリンクをご参照ください。
https://www.tellusxdp.com/ja/howtouse/tellus_os/start_tellus_os.html

2020年が始まりましたが、皆様はどのような抱負を掲げましたか? また、誰かにその抱負を話しましたか?

宙畑では今年も「宇宙産業を日本の基幹産業に」をスローガンに皆様に様々な記事をお届けしてまいります!

しかし、多忙な日々の中でついつい新年の抱負を忘れてしまいがち。流されてしまわないように、この想いを叫びたい、思いっきり叫びたい、やまびこが聞こえる場所で。

ということで、今回は衛星データを活用して東京都内でやまびこができる場所を探してみたい! まずはそもそも衛星データでやまびこができるスポットを探せるのか、日本一のやまびこスポットと言われている「ヤッホーポイント」を衛星で確認してみました。

(1)やまびこの仕組み

まずはやまびこがきれいに聞こえる条件について、考えてみましょう。

やまびこがきれいに聞こえるには声をぶつける対象物までの距離が大切だと考えられます。

「ヤッホー」という定番のやまびこの叫び声の長さがだいたい1秒弱程度。それに対して、音速が秒速340メートルです。つまり、ヤッホーは1秒で340メートル進むということになります。

「ヤッホー」と叫んだ後、1秒後にやまびこが返ってくるとちょうど綺麗に聞こえるとすると、叫んだ地点から反射する対象までの距離が340メートル程度だと良いということになります。

(2)Tellusでヤッホーポイントを見てみた

実際にやまびこが綺麗に聞こえると言われている場所が、本当に上記条件を満たしているのか、衛星データプラットフォームTellusで見てみました。

探してみると、和歌山県日高川町に日本一と呼ばれる場所があるようです。

以下、日本一の「ヤッホー」ポイントでやまびこをしている動画がありました。

 

綺麗にやまびこが聞こえていますね。

まずは、ヤッホーポイントから、跳ね返りがある場所までの距離を計ってみましょう。

Tellusでは線の描画をすることで距離を算出できる機能がついており、2点の距離が確認できます。

椿山ダム湖・第一展望台(33.965178, 135.392709)からの距離を測る

ヤッホーポイントは周囲を川に囲まれた場所にあります。

正確な跳ね返り点は分かりませんが、動画の様子から、川の向こう岸すぐだと推定しました。

測定してみると上記の通り、340メートルで、条件に合致しました。

(3)ヤッホーポイントをさらに詳しく標高データで見てみる

先ほどは真上から見た衛星写真を使っていたため、正確な跳ね返り点が分かりませんでした。

もう少し詳しく跳ね返り点を調査するため、衛星で取得した標高データで確認することにしました。

今回は「Tellusで標高の高い山の森林限界を可視化してみた」でご紹介した標高データを見るコードと同様のものを用います。

import requests, json
from skimage import io
from io import BytesIO
import numpy as np
import math
import matplotlib.pyplot as plt
%matplotlib inline

TOKEN = '自分のトークン'

def get_astergdem2_dsm(zoom, xtile, ytile):
    """
    ASTER GDEM2 の標高タイルを取得

    Parameters
    ----------
    zoom : str 
        ズーム率 
    xtile : str 
        座標X 
    ytile : number 
        座標Y
    Returns
    -------
    img_array : ndarray
        標高タイル(PNG)
    """
    url = " https://gisapi.tellusxdp.com/astergdem2/dsm/{}/{}/{}.png".format(zoom, xtile, ytile)
    headers = {
        "Authorization": "Bearer " + TOKEN
    }
    
    r = requests.get(url, headers=headers)
    return io.imread(BytesIO(r.content))

# ヤッホーポイントが存在しているタイル座標を指定
z = 12
x = 3588
y = 1636
gdem_img = get_astergdem2_dsm(z, x, y)

def get_landsat_tile_path_from_scene_id(id):
    """
    Landsat-8のシーンIDから、バンド別合成画像APIで使用するパスを取得する

    Parameters
    ----------
    id : str
        シーンID
    Returns
    -------
    path : str
        APIパス文字列
    """
    url = "https://gisapi.tellusxdp.com/api/v1/landsat8/get_scene/{}".format(id)
    headers = {
        "content-type": "application/json",
        "Authorization": "Bearer " + TOKEN
    }
    
    r = requests.get(url, headers = headers)
    data = json.loads(r.content.decode('utf-8'))
    return data["tile_path"]

def get_landsat_image(scene_id, zoom, xtile, ytile, opacity=1, r=4, g=3, b=2, preset=None):
    """
    Landsat-8のバンド別合成画像を取得する

    Parameters
    ----------
    scene_id : str
        シーンID
    zoom : str 
        ズーム率 
    xtile : str 
        座標X 
    ytile : number 
        座標Y
    opacity : number
        透過率
    r : number
        Redに割り当てるバンド
    g : number
        Greenに割り当てるバンド
    b : number
        Blueに割り当てるバンド
    preset : str
        使用するプリセット
    Returns
    -------
    img_array : ndarray
        バンド別合成画像タイル(PNG)
    """
    host = "https://gisapi.tellusxdp.com"
    path = "/blend/" + get_landsat_tile_path_from_scene_id(scene_id).format(z=zoom, x=xtile, y=ytile)
    queries = "?opacity={}&r={}&g={}&b={}".format(opacity, r, g, b)
    if preset is not None:
        queries += "&preset=" + preset
    url = host + path + queries
    
    headers = {
        "content-type": "application/json",
        "Authorization": "Bearer " + TOKEN
    }
    
    r = requests.get(url, headers=headers)

    return io.imread(BytesIO(r.content))

scene_id = "LC81100362018103LGN00"
natural_img = get_landsat_image(scene_id, z, x, y, preset="true")

io.imshow(natural_img)

衛星データを表示。ヤッホーポイントを含む黄線に沿って標高の断面図を切り取ります

ヤッホーポイントは図中の黄色い点なので、X軸方向(図中黄線)に標高データを出してみると以下のようになりました。

def get_colors(tile, index, direction, z, x, y, xtile_size=1, ytile_size=1, u=1):
    """
    タイル画像から植生に応じたRGB値を取得する

    Parameters
    ----------
    tile : ndarray 
        植生タイル
    index : number 
        走査するピクセルのインデックス 
    direction : number 
        走査方向(0で上から下、1で左から右)
    zoom : number 
        ズーム率 
    xtile : number 
        座標X 
    ytile : number 
        座標Y
    xtile_size : number 
        X方向のタイル数 
    ytile_size : number  
        Y方向のタイル数
    u : number 
        標高分解能[m](デフォルト 1.0[m])
    """
    if direction == 0: 
        line = tile[:,index]
    else:
        line = tile[index,:]

    colors = []
    colors_err = []
    for k in range(len(line)):
        R = line[k,0]
        G = line[k,1]
        B = line[k,2]
        try:
            colors.append([R, G, B, u])
        except ValueError as e:
            colors.append([0,0,0,0])
            colors_err.append((i,j))
    return colors

def get_tile_bbox(zoom, topleft_x, topleft_y, size_x=1, size_y=1):
    """
    タイル座標からバウンダリボックスを取得
    https://tools.ietf.org/html/rfc7946#section-5
    """
    def num2deg(xtile, ytile, zoom):
        # https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Python
        n = 2.0 ** zoom
        lon_deg = xtile / n * 360.0 - 180.0
        lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
        lat_deg = math.degrees(lat_rad)
        return (lon_deg, lat_deg)
    
    right_top = num2deg(topleft_x + size_x , topleft_y, zoom)
    left_bottom = num2deg(topleft_x, topleft_y + size_y, zoom)
    return (left_bottom[0], left_bottom[1], right_top[0], right_top[1])

def calc_height_chiriin_style(R, G, B, u=100):
    """
    標高タイルのRGB値から標高を計算する
    """
    hyoko = int(R*256*256 + G * 256 + B)
    if hyoko == 8388608:
        raise ValueError('N/A')
    if hyoko > 8388608:
        hyoko = (hyoko - 16777216)/u
    if hyoko < 8388608:
        hyoko = hyoko/u
    return hyoko


def plot_height(tile_gdem, tile_natural, index, direction, z, x, y, xtile_size=1, ytile_size=1, u=1):
    """
    高度のグラフをプロットする

    Parameters
    ----------
    tile_gdem : ndarray 
        標高タイル
    tile_natural : ndarray 
        natural画像タイル
    bbox : object 
        標高タイルのバウンディングボックス 
    index : number 
        走査するピクセルのインデックス 
    direction : number 
        走査方向(0で上から下、1で左から右)
    zoom : number 
        ズーム率 
    xtile : number 
        座標X 
    ytile : number 
        座標Y
    xtile_size : number 
        X方向のタイル数 
    ytile_size : number  
        Y方向のタイル数
    u : number 
        標高分解能[m](デフォルト 1.0[m])
    """

    bbox = get_tile_bbox(z, x, y, xtile_size, ytile_size) 
    width = abs(bbox[2] - bbox[0])
    height = abs(bbox[3] - bbox[1])
    if direction == 0: 
        line = tile_gdem[:,index]
        per_deg = -1 * height/tile_gdem.shape[0]
        fixed_deg = width/tile_gdem.shape[1] * index + bbox[0]
        deg = bbox[3]
        title = 'height at {} degrees longitude'.format(fixed_deg)
    else:
        line = tile_gdem[index,:]
        per_deg = width/tile_gdem.shape[1]
        fixed_deg = -1 * height/tile_gdem.shape[0] * index + bbox[3]
        deg = bbox[0]
        title = 'height at {} degrees latitude'.format(fixed_deg)
        
    heights = []
    heights_err = []
    degrees = []
    for k in range(len(line)):
        R = line[k,0]
        G = line[k,1]
        B = line[k,2]
        try:
            heights.append(calc_height_chiriin_style(R, G, B, u))
        except ValueError as e:
            heights.append(0)
            heights_err.append((i,j))
        degrees.append(deg)
        deg += per_deg

    fig, ax= plt.subplots(figsize=(8, 4))

    colors = np.array(get_colors(tile_natural, index, direction, z, x, y)) / np.array([255, 255, 255, 1])
    plt.title(title)
    plt.scatter(degrees, heights, s=20,c=colors)
    ax.text(0.01, 0.95, 'Errors: {}'.format(len(heights_err)), transform=ax.transAxes)
    plt.show()

plot_height(gdem_img, natural_img, 181, 1, z, x, y)

周囲を山に囲まれていることがよくわかります。

今回のポイントをよく見るために拡大すると以下のようになります。

def get_colors(tile, index, direction, z, x, y, xtile_size=1, ytile_size=1, u=1):
    """
    タイル画像から植生に応じたRGB値を取得する

    Parameters
    ----------
    tile : ndarray 
        植生タイル
    index : number 
        走査するピクセルのインデックス 
    direction : number 
        走査方向(0で上から下、1で左から右)
    zoom : number 
        ズーム率 
    xtile : number 
        座標X 
    ytile : number 
        座標Y
    xtile_size : number 
        X方向のタイル数 
    ytile_size : number  
        Y方向のタイル数
    u : number 
        標高分解能[m](デフォルト 1.0[m])
    """
    if direction == 0: 
        line = tile[:,index]
    else:
        line = tile[index,:]

    colors = []
    colors_err = []
    for k in range(len(line)):
        R = line[k,0]
        G = line[k,1]
        B = line[k,2]
        try:
            colors.append([R, G, B, u])
        except ValueError as e:
            colors.append([0,0,0,0])
            colors_err.append((i,j))
    return colors

def get_tile_bbox(zoom, topleft_x, topleft_y, size_x=1, size_y=1):
    """
    タイル座標からバウンダリボックスを取得
    https://tools.ietf.org/html/rfc7946#section-5
    """
    def num2deg(xtile, ytile, zoom):
        # https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Python
        n = 2.0 ** zoom
        lon_deg = xtile / n * 360.0 - 180.0
        lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
        lat_deg = math.degrees(lat_rad)
        return (lon_deg, lat_deg)
    
    right_top = num2deg(topleft_x + size_x , topleft_y, zoom)
    left_bottom = num2deg(topleft_x, topleft_y + size_y, zoom)
    return (left_bottom[0], left_bottom[1], right_top[0], right_top[1])

def calc_height_chiriin_style(R, G, B, u=100):
    """
    標高タイルのRGB値から標高を計算する
    """
    hyoko = int(R*256*256 + G * 256 + B)
    if hyoko == 8388608:
        raise ValueError('N/A')
    if hyoko > 8388608:
        hyoko = (hyoko - 16777216)/u
    if hyoko < 8388608:
        hyoko = hyoko/u
    return hyoko


def plot_height(tile_gdem, tile_natural, index, direction, z, x, y, xtile_size=1, ytile_size=1, u=1):
    """
    高度のグラフをプロットする

    Parameters
    ----------
    tile_gdem : ndarray 
        標高タイル
    tile_natural : ndarray 
        natural画像タイル
    bbox : object 
        標高タイルのバウンディングボックス 
    index : number 
        走査するピクセルのインデックス 
    direction : number 
        走査方向(0で上から下、1で左から右)
    zoom : number 
        ズーム率 
    xtile : number 
        座標X 
    ytile : number 
        座標Y
    xtile_size : number 
        X方向のタイル数 
    ytile_size : number  
        Y方向のタイル数
    u : number 
        標高分解能[m](デフォルト 1.0[m])
    """

    bbox = get_tile_bbox(z, x, y, xtile_size, ytile_size) 
    width = abs(bbox[2] - bbox[0])
    height = abs(bbox[3] - bbox[1])
    if direction == 0: 
        line = tile_gdem[:,index]
        per_deg = -1 * height/tile_gdem.shape[0]
        fixed_deg = width/tile_gdem.shape[1] * index + bbox[0]
        deg = bbox[3]
        title = 'height at {} degrees longitude'.format(fixed_deg)
    else:
        line = tile_gdem[index,:]
        per_deg = width/tile_gdem.shape[1]
        fixed_deg = -1 * height/tile_gdem.shape[0] * index + bbox[3]
        deg = bbox[0]
        title = 'height at {} degrees latitude'.format(fixed_deg)
        
    heights = []
    heights_err = []
    degrees = []
    for k in range(len(line)):
        R = line[k,0]
        G = line[k,1]
        B = line[k,2]
        try:
            heights.append(calc_height_chiriin_style(R, G, B, u))
        except ValueError as e:
            heights.append(0)
            heights_err.append((i,j))
        degrees.append(deg)
        deg += per_deg

    fig, ax= plt.subplots(figsize=(8, 4))

    colors = np.array(get_colors(tile_natural, index, direction, z, x, y)) / np.array([255, 255, 255, 1])
    plt.title(title)
    plt.scatter(degrees, heights, s=20,c=colors)
    ax.text(0.01, 0.95, 'Errors: {}'.format(len(heights_err)), transform=ax.transAxes)
    plt.xlim(135.38,135.40)
    plt.show()
plot_height(gdem_img, natural_img, 181, 1, z, x, y)

赤点で示したヤッホーポイントから、西側へ0.0025°行った場所がちょうど同じ標高くらいです。

これを距離に換算すると250m程度になります。先ほど仮定した条件に遠からず、といったところでしょうか。

(4)衛星データでやまびこポイントを見つける方法

やまびこができる場所の条件が分かったところで、やまびこポイントを見つける方法を考えて見ます。

ステップは以下の通りです。

(1)近所で山あり谷ありな場所の当てをつける

(2)Google Mapで該当する場所の中で人が立ち入れて叫べそうなポイントを見つける

(3)衛星から取得する標高データで、(2)の地点と標高が同じで2点の距離が300メートルのポイントを見つける

次回は、この手順でやまびこポイントを見つけてみたいと思います。

(5)まとめ

今回は和歌山県のやまびこができるとすでに分かっているポイントについて衛星データで確認してみました。

日本は、国土の約7割を山地が占める国。きっと読者の皆さんの近所の山々にもやまびこが綺麗に返ってくるポイントが見つかるはずです。ぜひ試してみてください。

※危険な場所である可能性があるため、実際に訪れる場合は、人が安全に、そして誰でも入れる場所があるのかは必ず確かめてからお願いいたします。

では、これから高尾山のやまびこができるポイントを探しだし、2020年の抱負を叫んできます。