【PR】この記事には広告が含まれています。

はるか3万6千kmの上空から、静かに地球を見つめる「気象衛星ひまわり」。
その目に映る宇宙の静けさと地球のうつろいを、自宅の小さなディスプレイに映し出せたら…そんな夢のような装置をRaspberry Piで作ってみました。

この記事では、ひまわりの最新画像を自動で取得して、ディスプレイ表示するシステムの作り方を紹介します。比較的安価な小型ディスプレイを使っているので、ぜひお試しください。

記事の後半では、ダイソーのミラーデジタル時計に装置一式を収納する応用方法も紹介しています。
用意するもの
使用したラズパイは、Raspberry Pi Zero 2 Wにピンヘッダーを取り付けたものです。サイズ的にもスペック的にも今回のプロジェクトに適任です。まだ持っていない場合は、あらかじめピンヘッダーが実装されている「WH」モデルを選ぶと、配線作業がスムーズに進みます。
Zero 2 Wの電源アダプターは、2.5A以上が推奨されています。コネクタはmicroUSBです。
本記事のコードは、以下のモデルでも動作確認しています。
表示に使用したのは、SPI接続のILI9341ドライバーを搭載した2.2インチのディスプレイです。解像度は240×320ピクセルで、気象衛星ひまわりの画像も鮮明に表示できます。
Raspberry PiとLCDを接続するためのジャンパーワイヤーが必要です。ジャンパーワイヤーはGPIOピンとディスプレイの端子に差し込んで接続するための細い電線です。今回はメス-メスのジャンパーワイヤーを9本使用します。
ラズパイとディスプレイの接続

ディスプレイは、SPI通信を使ってRaspberry Piと接続します。配線には、データ転送に必要なSCKやMOSIのほか、制御用のCS(チップセレクト)、DC(データ/コマンド切り替え)、RST(リセット)ピン、さらにバックライト用のBLピンを使用します。
以下の図のように、ジャンパーワイヤーを使ってRaspberry PiのGPIOピンとディスプレイの各ピンを接続します。

ソフトウェアの準備
Raspberry Pi OSはBookworm 64-bitのデスクトップ版を使用しました。

Raspberry PiのOSをインストールする方法は、以下の記事で詳しく解説しています。
≫【2025年最新版】OSインストールから初期設定まで|セットアップ手順のすべて
ラズパイのSPIを有効にする
LCDを使うには、SPIインターフェースを有効にする必要があります。
デスクトップ左上の「ラズベリーマーク(メニュー)」をクリックし、設定」→「Raspberry Piの設定」を選びます。

「インターフェイス」タブを開き、「SPI」の項目を「有効」に切り替えます。

再起動を求められた場合は「はい」を選んで再起動します。
ライブラリのインストール
Pythonで使う「ライブラリ」とは、便利な機能が詰まった道具箱のようなものです。ライブラリ使うと、本来は長くなってしまうプログラムを短いコードで書けるようになります。
以下の手順に沿って、必要なライブラリをインストールします。インストール作業はターミナルを使って行います。

ターミナルとは、コマンドを入力してRaspberry Piを操作するための画面です。

Pythonライブラリをインストールするため仮想環境myenvを作成します。myenvは仮想環境の名前です。好きな名前に変更できます。
チェックポイント
仮想環境とは、Pythonの実行環境をプロジェクトごとに独立して管理できる仕組みです。これにより、ライブラリ同士のバージョンによる競合を防ぎ、予期せぬエラーを回避できます。

Bookwormでは仮想環境を使用せずにpipを実行するとエラーが発生し、ライブラリをインストールできない仕様になっています。
下記のコマンドをターミナルに入力してEnterキーを押してください。
python3 -m venv ~/myenv --system-site-packages
作成した仮想環境myenvを有効にします。
source myenv/bin/activate
pipを最新バージョンにアップグレードします。pipはPython専用のインストールツールです。
pip install --upgrade pip
Adafruitが公開しているRGBディスプレイを制御するためのadafruit-circuitpython-rgb-displayライブラリをインストールします。
pip install adafruit-circuitpython-rgb-display

adafruit-circuitpython-rgb-displayは、ILI9341などの汎用的なディスプレイコントローラーにも対応しており、Adafruit製以外のディスプレイでも利用できます。
ディスプレイの動作確認をする
Raspberry PiでPythonのプログラムを動かして、ディスプレイが正常に動作するかを確認します。
Thonnyを使ってPythonプログラムを実行する
Raspberry Piでは「Thonny(ソニー)」という初心者向けのPython開発環境が標準でインストールされています。以下の手順でプログラムを実行できます。
デスクトップ画面左上の「ラズベリーメニュー」から「プログラミング」→「Thonny」を開く。

Thonnyでは画面上部の大きな白いスペースにプログラムを入力し、画面上の「▶(再生マーク)」をクリックします。すると、下の部分(シェル)に実行結果が表示されます。

ディスプレイのプログラムを実行する前に、仮想環境に切り替える操作をします。

仮想環境に切り替えることにより、先ほどインストールしたライブラリが使用可能になります。
仮想環境への切り替え
Thonnyでそのままプログラムを実行すると、先ほどインストールしたライブラリが見つからずにエラーが出ます。これを避けるため、以下の手順で仮想環境に切り替えます。
Thonnyの画面右下部分をクリックします。

「Configure interpreter…」をクリック。

「…」のボタンをクリック。

「ホーム」をクリックする。

先ほど作成した仮想環境のmyenvフォルダを開き、binフォルダ内のpythonを選択します。

「OK」をクリックします。

プログラムの実行環境が仮想環境に切り替わりました。

テスト用コードの実行
以下は、ディスプレイに赤い背景と「Hello, ILI9341!」という文字を表示するシンプルな動作確認用コードです。Thonny画面上部のスペースにコピーペーストし、画面上の「▶(再生マーク)」をクリックします。
from board import SCK, MOSI, MISO, D5, D18, D23, D24
from busio import SPI
from digitalio import DigitalInOut, Direction
from adafruit_rgb_display.ili9341 import ILI9341
from PIL import Image, ImageDraw, ImageFont
# Setup GPIO pins for display
CS = DigitalInOut(D5)
DC = DigitalInOut(D24)
RST = DigitalInOut(D23)
BL = DigitalInOut(D18)
BL.direction = Direction.OUTPUT
BL.value = True # Turn on backlight
# Initialize SPI and display
spi = SPI(clock=SCK, MOSI=MOSI, MISO=MISO)
display = ILI9341(spi, cs=CS, dc=DC, rst=RST, width=240, height=320)
# Create a red image
image = Image.new("RGB", (240, 320), (255, 0, 0))
draw = ImageDraw.Draw(image)
# Load default font
font = ImageFont.load_default()
# Draw text
draw.text((20, 150), "Hello, ILI9341!", font=font, fill=(255, 255, 255))
# Display the image
display.image(image)
このコードでは、最初にGPIOピンを設定します。ディスプレイのCS(チップセレクト)、DC(データ/コマンド)、RST(リセット)には、それぞれD5、D24、D23のピンを割り当てています。また、D18のピンを使ってバックライト(BL)を制御し、出力として設定した上で点灯させます。
次に、SPI通信を初期化します。Raspberry PiのSCK(クロック)、MOSI(マスターアウトスレーブイン)、MISO(マスターインスレーブアウト)のピンを使い、ILI9341ディスプレイと通信する準備を整えます。
ディスプレイを使うために、画面の幅を240ピクセル、高さを320ピクセルとして初期設定を行います。次に、Pillowライブラリを使って赤い背景の画像を作り、その上に白い文字で「Hello, ILI9341!」というメッセージを描きます。
最後にその画像を .image()
メソッドを使ってディスプレイに表示します。これにより、ディスプレイの接続やSPI通信が正常に行われているかを確認できます。

ひまわり画像の取得先と画像タイプの違い

「ひまわり」は日本の気象庁が運用する静止気象衛星で、赤道上空約3万6千kmの宇宙空間に位置し、地球の同じ場所を常に観測し続けています。現在は「ひまわり8号」と「9号」が運用されており、アジア太平洋地域全体をリアルタイムに監視しています。
本プロジェクトでは、国立情報学研究所(NII)が提供するデジタル台風(Digital Typhoon)という公開データベースからひまわりの画像を取得します。
気象衛星画像には「赤外画像」と「可視画像」の2種類があります。
種類 | 特徴 | メリット | デメリット |
---|---|---|---|
赤外画像![]() | 赤外線(熱)をとらえて表示 | 昼夜問わず常に全体を表示可能 | 色味がやや不自然 |
可視画像![]() | 太陽光を反射した様子を表示 | 昼間は自然な見た目で美しい | 夜間はほとんど見えない |
赤外画像は、地球や雲が放つ赤外線(熱)をもとに作られており、昼夜を問わず常に地球全体が表示されます。夜でも雲の動きが見えるのが大きな利点です。ただし、色味が人工的で、可視画像のような自然な見た目ではありません。
可視画像は太陽の光を反射した様子をとらえたもので、雲や地表を自然な美しい色合いで表示します。昼間は鮮明に表示されますが、夜間は太陽光が当たらないため地球の一部が暗くなり、月の満ち欠けのように少しずつ見える範囲が変化します。そのため、夜間はほとんど何も見えず、表示としては物足りない場合があります。
本記事ではこれらの特徴を活かした2つの表示方法を紹介します。ひとつは最新の赤外画像をシンプルに表示する方法。もうひとつは直近24時間分の可視画像を集めて、連続再生するアニメーション表示です。
ひまわりの赤外画像を表示

以下のコードはRaspberry Piで気象衛星ひまわりの赤外画像を10分ごとに取得し、ILI9341ディスプレイに表示するものです。
import time
import requests
from PIL import Image, ImageDraw
from io import BytesIO
from board import SCK, MOSI, MISO, D5, D18, D23, D24
from busio import SPI
from digitalio import DigitalInOut, Direction
from adafruit_rgb_display.ili9341 import ILI9341
# --- Display setup ---
CS = DigitalInOut(D5)
DC = DigitalInOut(D24)
RST = DigitalInOut(D23)
BL = DigitalInOut(D18)
BL.direction = Direction.OUTPUT
BL.value = True
spi = SPI(clock=SCK, MOSI=MOSI, MISO=MISO)
display = ILI9341(spi, cs=CS, dc=DC, rst=RST, width=240, height=320)
# ひまわりの最新画像を取得する関数
def fetch_image():
url = "http://agora.ex.nii.ac.jp/digital-typhoon/latest/globe/512x512/ir.jpg"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
img = Image.open(BytesIO(response.content)).convert("RGB")
return img
except Exception as e:
print("Fetch failed:", e)
return None
# 表示用に画像を整形する関数
def prepare_image(img):
img = img.rotate(90, expand=True) # 表示向きに90度回転
img = img.resize((240, 240)) # ディスプレイ用にサイズ変更
# 円形マスクを作成して地球部分だけを切り抜き
mask = Image.new("L", (240, 240), 0)
draw = ImageDraw.Draw(mask)
draw.ellipse((0, 0, 240, 240), fill=255)
black_bg = Image.new("RGB", (240, 240), (0, 0, 0)) # 黒背景画像
earth_only = Image.composite(img, black_bg, mask) # 円形で切り抜いた画像と合成
canvas = Image.new("RGB", (240, 320), (0, 0, 0))
canvas.paste(earth_only, (0, 40))
return canvas
# --- メインループ ---
last_canvas = None
while True:
img = fetch_image()
if img:
last_canvas = prepare_image(img)
print("New image fetched and prepared.")
else:
print("Using previous image.")
if last_canvas:
display.image(last_canvas)
time.sleep(600) # 10 minutes
まず、fetch_image()
関数では、「デジタル台風」のサイトから最新の赤外画像をダウンロードし、PILライブラリを使って画像として読み込みます。使用している画像のURLは http://agora.ex.nii.ac.jp/digital-typhoon/latest/globe/512x512/ir.jpg
で、このURLは1時間ごとに新しい画像が公開されます。
画像を表示する前に、prepare_image()
関数でディスプレイに合わせた加工を行います。画像は90度回転させ、ディスプレイの横向き表示に対応させています。続いて画像サイズを240×240に縮小し、円形のマスクをかけることで、日付や時刻などの文字を隠し、地球だけが目立つように調整。

最後に、ディスプレイと同じサイズである240×320ピクセルのキャンバスの中央に、地球の画像を配置します。

メイン処理では、まず画像を取得して加工し、ディスプレイに表示します。取得に失敗した場合でも、最後に表示した画像をそのまま再表示することで、画面が真っ黒になってしまうのを防ぎます。この処理は10分ごとに繰り返されるため、新しい画像があれば自動的に更新されます。
赤外画像は昼夜を問わず観測できるため、常に地球全体を表示できます。
ひまわりの可視画像をアニメーション表示する

赤外画像は色合いが人工的で、可視画像のような自然な見た目にはなりません。一方、可視画像は昼間は美しく表示されますが、夜は真っ暗になります。そこで、直近24時間分の可視画像を集めて連続再生することで、常に表示があり、動きのある地球を楽しめるようにしました。
ひまわりの可視画像を保存するプログラム

本プロジェクトではプログラムが複雑になりすぎないように、画像の取得用プログラムと、保存された画像をアニメーションのように表示するプログラムを分けて作成し、それぞれを同時に実行する構成としました。
ここでは、まず画像を保存する側のプログラムを紹介します。
import os
import requests
from datetime import datetime, timedelta, timezone
from PIL import Image
from io import BytesIO
import time
# Constants
IMAGE_DIR = "/home/pi/image"
MAX_IMAGES = 144
INTERVAL = 8 * 60 # 8 minutes
# JST timezone
JST = timezone(timedelta(hours=9))
# Ensure image directory exists
os.makedirs(IMAGE_DIR, exist_ok=True)
# 最新の保存済み画像の時刻を取得する
def get_latest_saved_time():
files = sorted(f for f in os.listdir(IMAGE_DIR) if f.endswith(".bmp"))
if not files:
return None
latest_file = files[-1]
try:
dt = datetime.strptime(latest_file[:13], "%Y%m%d_%H%M") # ?: "20250612_1530"
return dt
except Exception:
return None
# 指定時刻から取得候補となる「分」のリストを返す(10分刻み)
def generate_valid_minute_slots(base_time_utc, full=False):
if full:
return ["50", "40", "30", "20", "10", "00"]
current_minute = int(base_time_utc.strftime('%M'))
return [f"{m:02d}" for m in [50, 40, 30, 20, 10, 0] if m <= current_minute]
# 指定された画像を保存する(JSTのタイムスタンプをファイル名に付ける)
def save_image(canvas, jst_dt):
filename = jst_dt.strftime("%Y%m%d_%H%M") + ".bmp"
filepath = os.path.join(IMAGE_DIR, filename)
canvas.save(filepath, format="BMP")
print(f"Saved: {filepath}\n")
# 古い画像を削除して、最大枚数を超えないようにする
def cleanup_old_images():
files = sorted(f for f in os.listdir(IMAGE_DIR) if f.endswith(".bmp"))
print(len(files))
while len(files) > MAX_IMAGES:
os.remove(os.path.join(IMAGE_DIR, files[0]))
print("Deleted:", files[0])
files.pop(0)
# 指定したUTC時刻をもとに画像を取得し、保存・整理を行う
def try_fetch_and_save(base_time_utc, full_range=False):
date_str = base_time_utc.strftime('%Y/%m/%d')
base_date_str = base_time_utc.strftime('%Y%m%d')
hour_str = base_time_utc.strftime('%H')
minute_slots = generate_valid_minute_slots(base_time_utc, full=full_range)
for mnt in minute_slots:
time_str = f"{base_date_str}{hour_str}{mnt}"
url = f"http://agora.ex.nii.ac.jp/digital-typhoon/iiif/{date_str}/{time_str}00.tif/0,0,11000,11000/688,/0/default.jpg"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
img = Image.open(BytesIO(response.content)).rotate(90, expand=True)
img = img.resize((240, 240))
canvas = Image.new("RGB", (240, 320), (0, 0, 0))
canvas.paste(img, (0, 40))
jst_dt = datetime.strptime(time_str + "00", "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc).astimezone(JST)
save_image(canvas, jst_dt)
cleanup_old_images()
return True
except Exception:
jst_dt = datetime.strptime(time_str + "00", "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc).astimezone(JST)
print("No data time (JST):", jst_dt.strftime("%Y/%m/%d %H:%M:%S"))
time.sleep(2)
return False
# Main loop
while True:
wait_after_backfill = False
latest_jst = get_latest_saved_time()
if latest_jst:
target_jst = latest_jst - timedelta(minutes=10)
filename = target_jst.strftime("%Y%m%d_%H%M") + ".bmp"
filepath = os.path.join(IMAGE_DIR, filename)
if not os.path.exists(filepath):
print("Trying backfill for:", filename)
target_utc = target_jst.astimezone(timezone.utc)
if try_fetch_and_save(target_utc):
wait_after_backfill = True
if wait_after_backfill:
time.sleep(5)
now_utc = datetime.now(timezone.utc)
if not try_fetch_and_save(now_utc):
print("Retrying with 1 hour earlier...")
try_fetch_and_save(now_utc - timedelta(hours=1), full_range=True)
time.sleep(INTERVAL)
上記は「デジタル台風」から最新の気象衛星画像を定期的にダウンロードし、BMP形式で保存するプログラムです。保存された画像は指定フォルダに最大144枚まで保管され、古いものから自動で削除されます。ひまわりの可視画像は10分おきに提供されるため、144枚で24時間分のデータに相当します。
画像の取得には以下のようなURLが使われます:
url = f"http://agora.ex.nii.ac.jp/digital-typhoon/iiif/{date_str}/{time_str}00.tif/0,0,11000,11000/688,/0/default.jpg"
このURLは指定した日時(date_str
とtime_str
)に対応する可視画像を取得するためのもので、必要な時間の画像を個別にダウンロードできます。

URLで指定する時刻は、UTC(協定世界時)であり、日本時間(JST)より9時間遅れています。
プログラムは8分ごとに最新のひまわり画像を取得して保存します。まず、現在の「分」情報から取得できそうな時刻の候補(たとえば35分なら、30・20・10・00)を新しい順に試していきます。最初に見つかった画像を保存し、取得できない場合は1時間前のデータまでさかのぼって取得を試みます。取得した画像は240×320に加工され、「20250612_1530.bmp」のように日本時間(JST)のタイムスタンプをファイル名として保存します。
/home/pi/に「image」という名称のフォルダが作成され、そこに画像が自動で保存されていきます。


保存する画像の枚数が多くなるので、専用のフォルダを作ることにしました。
画像の取りこぼしを防ぐための工夫として、保存済み画像の中で最新の時刻を確認し、その10分前の画像が存在しない場合には、そちらを優先して取得するようにしました。これは、ひまわり画像の提供時刻が必ずしも一定間隔とは限らず、8分ごとに取得しても一部の時刻が抜け落ちてしまうことがあるためです。
プログラムをhimawari_image_save.pyという名称で/home/piフォルダに保存します。

Saveボタンをクリックして、名前に「himawari_image_save.py」を入力し、OKを押します。

ターミナルから保存用プログラムを実行
画像保存用のプログラム himawari_image_save.py
は、Thonnyではなくターミナルから実行します。Thonnyで起動すると、次に紹介するアニメーション表示用のプログラムが同時に動かせなくなるためです。
ターミナルで以下のコマンドを入力して実行します。
python3 himawari_image_save.py

/home/pi/imageに画像が保存されていきます。

ダブルクリックしてみると、保存された画像を確認できます。

保存された画像をアニメーション表示するプログラム

himawari_image_save.pyが画像の保存をしている間に、アニメーション表示するプログラムを用意します。
import glob
import time
from PIL import Image
from board import SCK, MOSI, MISO, D5, D18, D23, D24
from busio import SPI
from digitalio import DigitalInOut, Direction
from adafruit_rgb_display.ili9341 import ILI9341
# Setup GPIO pins for display
CS = DigitalInOut(D5)
DC = DigitalInOut(D24)
RST = DigitalInOut(D23)
BL = DigitalInOut(D18)
BL.direction = Direction.OUTPUT
BL.value = True # Turn on backlight
# Initialize SPI and display
spi = SPI(clock=SCK, MOSI=MOSI, MISO=MISO)
display = ILI9341(spi, cs=CS, dc=DC, rst=RST, width=240, height=320)
while True:
# 画像フォルダからBMPファイルをすべて取得し、名前順に並べる
image_files = sorted(glob.glob("/home/pi/image/*.bmp"))
# 画像を1枚ずつ表示
for filepath in image_files:
img = Image.open(filepath)
display.image(img)
time.sleep(0.01)
time.sleep(5) # 5秒待ってから画像リストを再取得(新しい画像が増えている可能性があるため)
このコードは、Raspberry PiとILI9341ディスプレイを使って、「/home/pi/image」フォルダ内にあるBMP画像を順番に1枚ずつ表示するものです。画像を高速で切り替えて表示することで、アニメーションのような視覚効果を得ることができます。
フォルダ内の画像ファイルは、「20250612_1530.bmp」のように日時が名前に含まれた形式で保存されています。そのため、ファイル名を文字列順に並べると古い順(撮影された順)になります。この順番で画像を1枚ずつ読み込み、180度回転させてディスプレイに表示します。すべての画像を表示し終わると、5秒待機してから同じ処理を繰り返します。

この処理を実行している間も、画像取得用のプログラム(himawari_image_save.py
)が最新の画像を継続的に保存しているため、アニメーションで表示される画像も自動で更新されていきます。
ダイソーのミラーデジタル時計に収納する

表示の仕組みが完成したら、ケースに入れたくなるものです。ダイソーのミラーデジタル時計を加工して、ラズパイ本体とディスプレイを収納することにしました。むき出しのディスプレイや配線のごちゃごちゃした印象をすっきり隠せるので、スマートな見た目に仕上がります。

前面は鏡のような仕上がりで、周囲の風景が映り込みます。

さっそく容赦なく分解していきます。

鏡の部分は薄いシート状になっており、時計の数字が表示される箇所だけが光を通すようになっています。それ以外の部分も、裏側のコーティングを削れば光を通すようにできます。

ディスプレイを取り付ける部分のコーティングを、マイナスドライバーで少しずつ削っていきます。逆に透けさせたくない部分は、油性マジックで塗りつぶして光が漏れないようにしました。
ケース本体側も大幅に加工が必要です。ラズパイ本体やディスプレイ、配線が干渉する部分をニッパーで切り取り、必要なスペースを確保します。

ディスプレイの土台部分は3Dプリンターで作成しました。

電源ケーブルの取り回しを考慮し、ラズパイは以下の位置に配置することにしました。

ケースに収納したラズパイを外部から操作できるようにするため、あらかじめVNCの設定をしておきます。VNCとは「Virtual Network Computing(バーチャル・ネットワーク・コンピューティング)」の略で、離れた場所からラズパイの画面を操作できる仕組みです。
VNCの設定方法は、以下の記事で詳しく解説しています。
≫【ラズベリーパイを遠隔操作】VNCでPCからリモート接続する方法
ディスプレイは水色の土台部分に両面テープで固定しています。

ケースの背面には、ラズパイの電源コネクタに直接差し込めるように穴を追加しました。

雑な加工ですが、ケーブルを差し込めばほとんど目立たなくなります。

完成。ミラーの奥に、ぼんやりと地球が浮かび上がります。ミラー越しだと少しぼやけて見えるため、赤外画像のほうが輪郭がはっきりして見やすく感じます。
