🎨

【実践】SAR衛星画像カラー表示のPython実装

に公開


Contains modified Copernicus Sentinel data 2025

はじめに

本記事はSAR衛星画像のカラー表示の内容についてです。

衛星画像を目的を持って可視化することで解析する際のヒントになるので、研究者データサイエンティストは知っておくと役に立つでしょう。

何かの現象に対して、具体的に色の違いから物事を説明できるため資料作成にも有効です。

また、衛星画像がめっちゃ綺麗なのでカラー画像化することでより一層、衛星画像の輝きを感じられるのではないかと思ってます!

おさらい

偏波


© JAXA https://www.satnavi.jaxa.jp/ja/news/2024/07/31/9609/index.html より

SAR(合成開口レーダー)においての偏波とは、観測する電波の位相の向きの事です。しかし、そう言われてもピンとこないのでだいぶ丸め込んで説明すると、電波の反射回数や反射物の形状を表しています。

https://www.eorc.jaxa.jp/ALOS/img_up/jpal_polarization.htm

バンドのようにチャンネル方向に別のSAR画像を取得できるイメージです。
この異なる偏波でどのような意味を表すかは、カラー化の方法の見せ所になります。

カラー画像化

では、実装を見ていきましょう。
ここでは、処理の説明とコアとなる実装の部分だけを掲載しております。

読み込みから可視化、保存までの処理については以下の Github をご覧いただけると幸いです。

https://github.com/syu-tan/sar-color-python-article

また、データについては以下に配置しております。皆さんのお手元でも動かして色合いを調整してみることができます。

https://github.com/syu-tan/sar-color-python-article/tree/main/data

RGB Ratio


Contains modified Copernicus Sentinel data 2025

RGB比率(RGB Ratio)は、VV偏波とVH偏波の割合を色で表現したカラーリングになります。
赤チャンネルにはVV偏光、緑チャンネルにはVH偏光、青チャンネルにはVH/VV比を使用しています。

チャネル チャンネルの意味 物理的に強調されやすい散乱 値が高い時の解釈 値が低い時の解釈 実装上の注意
R(赤) R \leftarrow VV 表面(鏡面~表面粗さ由来の)散乱、構造物の面反射 平坦な裸地の粗い面、屋根・道路、都市部の強い面反射で赤成分↑ 水面・湿地(滑らか):赤↓ dB入力なら線形へ変換。ダイナミックレンジ圧縮(例:ハイライトコンプ)推奨
G(緑) G \leftarrow VH 体積散乱(植生)、多重散乱、都市の複雑な散乱 植生や都市の複雑散乱で緑成分↑ 水面・平滑地表:緑↓ 熱雑音や影響が出やすいので下限クリップを少し入れると安定
B(青) B \leftarrow \dfrac{VH}{VV+\varepsilon} 「交差偏波の相対優位」=体積散乱の優位性 植生(VHが相対的に強い)で青成分↑ → 青緑傾向 VV優位(都市の面反射・乾いた裸地)で青成分↓ 0割回避の\varepsilon必須。スケーリング(例:×定数→圧縮)で見栄えを調整

よって、土壌被覆で見ていくと、水域は濃い赤(黒)、市街地は黄色、植生は青緑色、裸地は濃い紫色で表示されます。

土地被覆 VV(dB表示) VH(dB表示) 比率(VH/VV) 期待色(ご指定) 色がそう見える理由(R=VV, G=VH, B=VH/VV)
水域 −25 ~ −15 −35 ~ −25 ≲ 0.05〜0.1 濃い赤(黒) VV・VHとも非常に低く全体に暗くなり黒寄り。可視化のスケール設定によっては、VVがわずかに残って暗赤に見えることがあります。
市街地 −12 ~ −3 −17 ~ −7 0.2 ~ 0.6 黄色 VV高(R↑)、VHも比較的高い(G↑)一方、比(VH/VV)は中程度で青は抑え気味→R+G優勢=黄。散乱がさらに強く比も高いと白寄りにも変化します。
植生 −15 ~ −7 −20 ~ −10 0.3 ~ 0.8 青緑色 体積散乱でVHが相対的に強く(G↑)、比も高い(B↑)。VVは中程度(R中)→ G+B優勢=青緑。葉水分や作物高で比が上がると青味が強まります。
裸地 −18 ~ −8 −25 ~ −15 0.1 ~ 0.3 濃い紫色 乾いた粗い地表でVVが相対的に高く(R↑)、VHは低め(G↓)。表面粗さや礫・岩混在で比が中程度に上がるとR優勢+B成分が少し乗って紫寄りに見える場合があります(平滑だと赤~橙寄り)。

HighlightCompress

RGB比率の可視化において、明るすぎる色を調整する HighlightCompress という処理があります。これはSARはインパルス応答の原理の特性上で大きな値はとても大きくなってしまうことを防ぐ処理をしています。単に閾値でクリップする方法やログスケールもありますが、ここでは線形スケールの傾斜を調整するようにしています。

def highlight_compress(x, minv=0.0, maxv=0.8):
    """
    Sentinel HubのHighlightCompressVisualizer相当の簡易実装(per-channel)。
    ・minv → 0,  maxv → 約0.9259 に写像
    ・その先は緩やかに 1.0 に近づける(2*maxv - minv で 1.0)
    """
    a, b = float(minv), float(maxv)
    x = np.asarray(x, dtype=np.float32)

    # しきい値
    th1 = a + 0.92 * (b - a)
    th2 = 2.0 * b - a

    y = np.zeros_like(x, dtype=np.float32)

    # 区間1: a < x <= th1  → 0..0.92 の線形
    mask1 = (x > a) & (x <= th1)
    y[mask1] = (x[mask1] - a) / (b - a)

    # 区間2: th1 < x < th2 → 0.92..1.0 の緩勾配線形
    mask2 = (x > th1) & (x < th2)
    # 傾き 0.08 / (th2 - th1) = 0.08 / (1.08*(b-a)) ≒ 0.074074/(b-a)
    slope2 = 0.08 / (th2 - th1)
    y[mask2] = 0.92 + slope2 * (x[mask2] - th1)

    # 区間3: x >= th2 → 1.0
    y[x >= th2] = 1.0

    return np.clip(y, 0.0, 1.0)

関数定義だけだと、わかりにくいと思うので可視化してみます。

hc_data = highlight_compress(np.linspace(0, 2.5, 512), 0.0, 1)

# 可視化
plt.figure(figsize=(12, 5), dpi=160, facecolor='white', edgecolor='black')
plt.plot(np.linspace(0, 2.5, 512), hc_data, label='Highlight Compress', color='blue')
plt.title('Highlight Compress Function')
plt.xlabel('Input Value')
plt.ylabel('Output Value')
plt.grid(True)
plt.axvline(x=0.92, color='red', linestyle='--', label='0.92')
plt.legend()
plt.savefig(os.path.join(PATH_OUTPUT, 'highlight_compress_function.png'), bbox_inches='tight', pad_inches=0.1)
plt.show();

0.92 までは線形に上昇して、それ以上に大きな値は傾斜が緩やかになっていますね。

RGB Ratio 実装

では、本題の RGB比率の実装です。偏波ごとに同じくらいの色合いになるように係数が割り当てられています。

gain:float = 0.8

r_raw = (gain * vv_lin) / 0.28
g_raw = (gain * vh_lin) / 0.06
b_raw = (gain * (vh_lin / vv_lin)) / 0.49

# チャンネル毎にハイライト圧縮
r = highlight_compress(r_raw, 0.0, 0.8)
g = highlight_compress(g_raw, 0.0, 0.8)
b = highlight_compress(b_raw, 0.0, 0.8)

rgb = np.stack([r, g, b], axis=-1).astype(np.float32)

こちらも簡易的にプロットすることで確認していきましょう。

plt.figure(figsize=(16, 8), dpi=100, facecolor='white', edgecolor='black')
plt.imshow(rgb_ratio_u8)
plt.title('Sentinel-1 RGB-RATIO (VV, VH)')
plt.savefig(os.path.join(PATH_OUTPUT, 'sentinel1_rgb_ratio.png'), bbox_inches='tight', pad_inches=0.1)
plt.show() 


Contains modified Copernicus Sentinel data 2025

PNG画像として保存する方法は以下です。

plt.imsave(os.path.join(PATH_OUTPUT, 'sentinel1_rgb_ratio_vv_vh.png'), rgb_ratio_u8)

SAR画像はとても綺麗ですね!詳細も見てみましょう。お台場周辺の船舶にサイドローブが赤色に輝いています。


Contains modified Copernicus Sentinel data 2025

False Color


Contains modified Copernicus Sentinel data 2025

False Color (擬色)は、Annamaria Luongo が提案した色合いになります。海上監視(氷監視、船舶監視など)、陸上監視(農業、森林伐採など)幅広く活用できるようです。

実装としては、9つの定数(Constant の c かな?)によって定義される色合いです。

# 2) 定数(Evalscriptの値のまま)
c1, c2, c3 = 1e-3, 0.01, 0.02
c4, c5, c6 = 0.03, 0.045, 0.05
c7, c8 = 0.9, 0.25

# 3) Non-enhanced / Enhanced の分岐(log の定義域は eps で保護)
if not enhanced:
    termR = c1 - np.log(c6 / (c3 + 2.0 * vv + eps) + eps)
    R = c4 + np.log(np.maximum(termR, eps))

    G = c6 + np.exp(
        c8 * (np.log(c2 + 2.0 * vv + eps) + np.log(c3 + 5.0 * vh + eps))
    )

    B = 1.0 - np.log(c6 / (c5 - c7 * vv + eps) + eps)
else:
    termR = (
        c1
        - np.log(c6 / (c3 + 2.5 * vv + eps) + eps)
        + np.log(c6 / (c3 + 1.5 * vh + eps) + eps)
    )
    R = c4 + np.log(np.maximum(termR, eps))

    G = c6 + np.exp(
        c8 * (np.log(c2 + 2.0 * vv + eps) + np.log(c3 + 7.0 * vh + eps))
    )

    B = 0.8 - np.log(c6 / (c5 - c7 * vv + eps) + eps)

rgb = np.stack([R, G, B], axis=-1).astype(np.float32)

こちらも可視化して画像を見ていきましょう。海は青色で、大地は黄色、植生は緑色ぽく表現されるんでしょうかね〜

plt.figure(figsize=(16, 8), dpi=100, facecolor='white', edgecolor='black')
plt.imshow(false_color_u8)
plt.title('Sentinel-1 SAR False Color Visualization (Enhanced)')
plt.savefig(os.path.join(PATH_OUTPUT, 'sentinel1_sar_false_color.png'), bbox_inches='tight', pad_inches=0.1)
plt.show();


Contains modified Copernicus Sentinel data 2025

plt.imsave(os.path.join(PATH_OUTPUT, 'sentinel1_sar_false_color_vv_vh.png'), false_color_u8)

Urban


Contains modified Copernicus Sentinel data 2025

Urban(都市)は、Monja B. Šebelaによって考案されて、都市部や個々の建物の変化がわかる色合いになっています。VH偏波とVV偏波を用いて、土壌や建物の形状情報を紫と緑で表示してくれます。

色ごとの典型例としては以下のような感じです。

色の見え方 凡例条件 想定される土地被覆 メモ
黒〜暗色 VV 低、VH 低(5.5*VH > 0.5 が偽) 水域・滑面・レーダーシャドウ 全チャネル低で暗色に収束
VH 中、VV 低(赤は偽、青=VH×8 が優勢) 岩裸地・山肌の粗い面、弱い植生 都市以外でも VH が立つと青み
VV 高、VH 低(赤は偽、緑=VV が優勢) 都市(VV優勢な建造物) 面反射・方位で VV が強い構造物
VH 高(赤=真で1)、VV 低〜中、青も高 都市(VH優勢な建造物) VH が高く赤と青が混ざり紫に
VV 高、VH 高(赤=真、緑高、青高) 都市(両偏波の反射が高い建造物・複雑な構造) 両偏波とも強く加色合成で白寄り
青緑(シアン) VH 中、VV 中(赤は偽、緑+青) 弱い植生・粗い裸地 都市以外の散乱が混在する色調
明るい色 VH または VV が異常に高い 雪氷・急斜面(誤検出) 、ダブルバウンス 高標高域での判別は要注意

土壌被覆ごとの色としては以下のような感じです。

土地被覆 VV(緑) VH(青 & 赤のトリガ) 期待色 補足
水域・滑面 低(赤=偽) 黒〜暗色 鏡面で両偏波とも弱い
都市(VV優勢) 低〜中(赤=偽のこと多い) 面向き・材質で VV が卓越
都市(VH優勢) 低〜中 高(赤=真、青も高) 複雑散乱で VH が強く出る
都市(両方強い) 高(赤=真) 構造物が多様で両偏波強い
裸地・岩場・山肌 低〜中(多くは赤=偽) 青〜青緑 粗さ・含水で変動、都市と混同注意
植生(疎〜中) 中(多くは赤=偽) 青緑 体積散乱寄与で青+緑が増える
雪氷・急斜面 変動 変動(赤が真になる例あり) 明るく強調 高標高域では分離が難しい

実装はシンプルで、赤色と青色のスケールと赤色の閾値の3種類のパラメーターによって再現できます。

r_scale = 5.5 #  R: 5.5*VH > 0.5
r_thresh = 0.5
b_scale = 8.0 # B: VH*8

# Urban Areas Script の再現
R = (r_scale * vh > r_thresh).astype(np.float32)  # 二値
G = vv
B = b_scale * vh

rgb = np.stack([R, G, B], axis=-1)

こちらも可視化していきましょう。

plt.figure(figsize=(16, 8), dpi=100, facecolor='white', edgecolor='black')
plt.imshow(urban_color_u8)
plt.title('Sentinel-1 Urban Areas Visualization')
plt.savefig(os.path.join(PATH_OUTPUT, 'sentinel1_urban_areas.png'), bbox_inches='tight', pad_inches=0.1)
plt.show();


Contains modified Copernicus Sentinel data 2025

保存方法は以下です。

plt.imsave(os.path.join(PATH_OUTPUT, 'sentinel1_urban_areas_vv_vh.png'), urban_color_u8)

きっかけ

Sentinel-Hub の画像が 綺麗だな〜 と思ったのがきっかけです。
再現したいなと思っていたら evalscript で実装があったので自分で理解のために Python 実装をして遊んでみました。

他の衛星にも使用できるので かっこいい画像を作りたい 時はやってみてください!

さいごに

最後まで読んで頂きありがとうございます!もし皆さんのお勉強の足しにでもなれば幸いです。
衛星画像は手軽に使えるサンプルが少ないですが、Github にてご用意しているのでお手元で手軽に確認できるのではないかと思います。

https://github.com/syu-tan/sar-color-python-article/blob/main/src/sar_color_example.ipynb

謝辞

ESA Sentinel-1 の再配布可能なクレジットでの提供に感謝致します。Sentinelシリーズ は宇宙産業を支えてる柱の1つですね。

サンプルデータの Copernicus Hub のリンクは以下です。

https://browser.dataspace.copernicus.eu/?zoom=12&lat=35.66274&lng=139.83195&themeId=DEFAULT-THEME&visualizationUrl=U2FsdGVkX1%2BZ%2Bk8POcsVbnDoyxTE5NigWFM72MxBrBcXRGZQ3TW0ae5C2UvEWsIh0RwVYTWlUorReJ7MC1%2B%2BvdkpXRx2IxFPUP3lFBDXm0VUNTcfuutPbAqEw2M4Iw6o&datasetId=S1_CDAS_IW_VVVH&fromTime=2025-09-16T00%3A00%3A00.000Z&toTime=2025-09-16T23%3A59%3A59.999Z&layerId=8_RGB-RATIO&demSource3D="MAPZEN"&cloudCoverage=30&dateMode=SINGLE

宣伝

日頃はこのようなZennの個人記事を書いています。

https://zenn.dev/syu_tan

そして、宙畑にもよく寄稿しています。

https://sorabatake.jp/32461/

年内には、SAR衛星解析入門の書籍 を出版します。
また、来年には QGISの衛星画像解析の書籍 を出版いたします。

自己紹介

普段は宇宙領域でテックリードをしております。X(旧Twitter)アカウントでは、宇宙領域や機械学習などの科学やコンペなどについて発言することが多いです。

SAR解析をよくやっていますが、画像系AI、地理空間や衛星データ、点群データ、3Dデータに関心があります。勉強している人は好きなので楽しく絡んでくれると嬉しいです。

SAR解析者への道シリーズ もよろしくお願いします!

衛星データ解析として、宙畑のライターもしています。
https://sorabatake.jp/?s=秀輔

お仕事はとても忙しいのでご相談やご提案くらいでしたら可能です。

Discussion