スポンサーリンク

ISSが飛行中の地域を画像化・地図上にマッピングする

ほぼ1年前になるんだけど、以前、ISS(国際宇宙ステーション)が飛行中に通過する地名をリアルタイムで出力するPythonスクリプトを作ったことを記事にした(というか、ほぼAIが作ってくれたんだけどね)。

ISSが飛行中の地名を出力するスクリプトを作った(ほぼAIが)
久しぶりに時間ができたので、ポンス・ブルックス彗星の写真でも撮りに行こうと思ったんだが、どうも曇りになりそうなのでやめた。代わりにISSが飛行している現在の地上の地名を出力するPythonスクリプトを書いて気分を紛らわすことにした。マシンパ...
ISSが飛行している地上の風景を描いた(ぜんぶAIが)
地球の表面積のうち7割が海なんだそう。そして地球の半分は夜なんだ。ISSが約90分かけて地球を1周しているうち、明るい陸地の上空にいる確率は15%ってことになるのかな。そういうわけだから、天文デジタル時計に映し出される風景は、たいてい海のこ...

 

個人的には、世界旅行をしているような気分になれて面白いこともあり、今回は気が向いたので、緯度経度から取得した地名をキーにDalle3で画像を生成するところまでを自動化する新しいスクリプトを作成した。だいたいやりたいことが実現して、いまは Raspberry Pi 5 上で動作しているよ。文末に生成したコードを備忘としてはっておく。

まずは出力例をみてちょうだい(地図と画像は手で並べて、スクショをトリミング。画像が1024×1024の正方形なのは課金を意識したから)。

カザフスタンの首都アスタナの東を飛行中

アルゼンチン上空

ロシア・オレンブルク州上空

モロッコ上空 地名は「ⵟⴰⵏⵊ-ⵟⵉⵜⴰⵡⵉⵏ-ⵍⵃⵓⵙⵉⵎⴰ طنجة تطوان الحسيمة, Maroc ⵍⵎⵖⵔⵉⴱ المغرب」だが、文字化け防げず ><

 

どんな機能を追加したのか

  • 生成した画像に地名をオーバーレイで表示
  • 飛行中の緯度経度をもとに、飛行位置を地図にマッピング
  • 海上を飛行しても画像を生成(ISSのイラストが描かれるので、これはこれで美しい)

やっぱり生成AIの力を借りる

今回のコーディングも、ほぼ 生成AI にお願いした。プログラミングの知識がなくてもコードが作れるのは本当にありがたいよね。自分が書いたら確実に対処しなかったエラートラップまでしっかりカバーしてくれる。とはいえ、「そうじゃないんだよ!」 という修正をすることも多々あって、時には自分で書いたほうが速いんじゃないかと思う場面もあるはある。自分の実力だけでは書けなかったのは確か。

OpenAI APIを使って画像生成

このスクリプトは OpenAIのAPI を使って画像を生成している。そのため、

  • 1回の画像生成に $0.04(約6円) かかる
  • 一定時間内のリクエスト数に制限がある(多すぎると生成できない)
  • 地理的・政治的な理由で生成拒否される空域がある

例えば、ダルフール上空で画像を生成しようとしたら、

「ダルフール地方は過去に紛争の影響を受けた地域のため、不適切な表現や誤解を招く可能性のある内容が制限されることがあります。」

と言われて拒否されたことがあった。そういうフィルタがあるのも理解はできるけど、殺し合いの場面を出してほしいというプロンプトではない。

プロンプトはシンプルにこんなの

Create a realistic image of {address}. The image should represent the culture and nature.

,

ターミナルで実行中、GUI作るか悩む

現状、スクリプトは ターミナルから起動 する形になってる。シンプルでいいけど、もうちょっと手軽に操作できたら便利かなぁと思い、GUIを作るかどうか悩み中。とりあえず満足したので今回はおしまい。下記は、今回作ったPythonスクリプト。もし使う場合は、tabを調整してください。

import os
import requests
from datetime import datetime
from skyfield.api import load
from geopy.geocoders import Nominatim
from geopy.exc import GeopyError
import openai
import folium # 地図描画用
from PIL import Image, ImageDraw, ImageFont # 画像編集用


# OpenAI APIキー設定
OPENAI_API_KEY = "APIキーをここに記述"
openai.api_key = OPENAI_API_KEY


# 画像保存用ディレクトリ
IMG_DIR = "img"
os.makedirs(IMG_DIR, exist_ok=True) # ディレクトリが存在しない場合は作成


# Linux 専用の全世界対応フォント(文字化け防止)
if os.path.exists("/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf"):
FONT_PATH = "/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf" # 全言語対応
elif os.path.exists("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"):
FONT_PATH = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc" # 日本語・中国語・韓国語
else:
FONT_PATH = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" # 代替フォント


def get_location(latitude, longitude):
"""緯度・経度から住所を取得"""
geolocator = Nominatim(user_agent="myGeocoder")
try:
location = geolocator.reverse((latitude, longitude), exactly_one=True)
if location and location.address:
return location.address
else:
return "The ISS flying over the ocean"
except GeopyError as e:
print(f"An error occurred: {e}")
return "ジオコーディングエラー"
except AttributeError:
return "The ISS flying over the ocean"


def wrap_text(draw, text, font, max_width):
"""テキストを指定の幅内で自動改行"""
lines = []
words = text.split()
current_line = ""


for word in words:
test_line = current_line + " " + word if current_line else word
bbox = draw.textbbox((0, 0), test_line, font=font)
text_width = bbox[2] - bbox[0]


if text_width <= max_width:
current_line = test_line
else:
lines.append(current_line)
current_line = word


if current_line:
lines.append(current_line)


return lines


def add_text_overlay(image_path, output_path, text):
"""全世界対応フォントで地名をオーバーレイ表示し、改行対応"""
try:
img = Image.open(image_path)
draw = ImageDraw.Draw(img)


# フォント設定(適切なサイズ)
font_size = max(20, img.width // 30)
try:
font = ImageFont.truetype(FONT_PATH, font_size)
except IOError:
print("❌ フォントが見つかりません!デフォルトフォントを使用します")
font = ImageFont.load_default()


# 最大横幅を画像の90%に制限
max_text_width = int(img.width * 0.9)
lines = wrap_text(draw, text, font, max_text_width)


# ✅ 修正: `getsize()` → `textbbox()` で高さを取得
bbox = draw.textbbox((0, 0), "A", font=font)
line_height = bbox[3] - bbox[1]
text_height = line_height * len(lines)


# テキストの配置 (中央下部)
x = (img.width - max_text_width) // 2
y = img.height - text_height - 20 # 下部から少し上


# テキストの背景を半透明に(黒)
padding = 10
background_box = [x - padding, y - padding, x + max_text_width + padding, y + text_height + padding]
draw.rectangle(background_box, fill=(0, 0, 0, 180))


# 改行対応でテキスト描画
for i, line in enumerate(lines):
draw.text((x, y + i * line_height), line, font=font, fill="white")


img.save(output_path)
print(f"✅ 地名をオーバーレイした画像を保存しました: {output_path}")
except Exception as e:
print(f"❌ 画像へのオーバーレイ処理エラー: {e}")



def generate_image_with_dalle(prompt, file_name_png, file_name_jpg, address):
"""DALL·E API を使用して画像を生成し、ローカルに保存"""
try:
response = openai.images.generate(
model="dall-e-3",
prompt = f"Create an realistic image of {address}. The image should represent the culture and nature.",
n=1,
size="1024x1024"
)


# 画像URLを取得
image_url = response.data[0].url


# 画像をダウンロード
image_response = requests.get(image_url, stream=True)
if image_response.status_code == 200:
with open(file_name_png, "wb") as img_file:
for chunk in image_response.iter_content(1024):
img_file.write(chunk)
print(f"✅ PNG画像を保存しました: {file_name_png}")


# PNGをJPEGに変換
with Image.open(file_name_png) as img:
img = img.convert("RGB") # JPEGはRGB形式のみ対応
img.save(file_name_jpg, "JPEG")
print(f"✅ JPEG画像を保存しました: {file_name_jpg}")


# 地名をオーバーレイ
add_text_overlay(file_name_jpg, file_name_jpg, address)


return file_name_jpg
else:
print("❌ 画像のダウンロードに失敗しました")
return None
except openai.OpenAIError as e:
print(f"❌ 画像の生成エラー: {e}")
return None # エラー時は None を返す


def generate_map(latitude, longitude, address):
"""ISSの現在地を地図に表示し、HTMLファイルとしてカレントディレクトリに保存"""
file_name_map = "iss_map.html"
try:
print("🗺️ 地図を生成中...")
map_ = folium.Map(location=[latitude, longitude], zoom_start=5)
folium.Marker(
location=[latitude, longitude],
popup=address,
tooltip="ISSの現在地",
icon=folium.Icon(color="red", icon="cloud"),
).add_to(map_)


map_.save(file_name_map)
print(f"✅ 地図を保存しました: {file_name_map}")
except Exception as e:
print(f"❌ 地図の生成エラー: {e}")


# ISS の現在位置を取得
stations_url = 'http://celestrak.com/NORAD/elements/stations.txt'
try:
satellites = load.tle_file(stations_url)
by_name = {sat.name: sat for sat in satellites}
iss = by_name['ISS (ZARYA)']


# タイムスケールオブジェクトをロード
ts = load.timescale()
t = ts.now()


# ISS の現在位置を取得
geocentric = iss.at(t)


# 緯度経度を取得
subpoint = geocentric.subpoint()
latitude = subpoint.latitude.degrees
longitude = subpoint.longitude.degrees


print(f'Latitude: {latitude}, Longitude: {longitude}')


address = get_location(latitude, longitude)
print(f"Address: {address}")


# 地図を生成(カレントディレクトリに保存)
generate_map(latitude, longitude, address)


# 画像の保存用ファイル名を生成
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
file_name_png = os.path.join(IMG_DIR, f"iss_image_{timestamp}.png")
file_name_jpg = os.path.join(IMG_DIR, f"iss_image_{timestamp}.jpg")


# 画像生成
saved_file = generate_image_with_dalle(f"Create an image of {address}", file_name_png, file_name_jpg, address)


if saved_file:
print(f"✅ 画像が正常に保存されました: {saved_file}")


except Exception as e:
print(f"❌ ISSのデータ取得中にエラーが発生しました: {e}")

この記事へのコメント

  1. あら、コメントできるようになった。
    昨日見たとき、コメント記入欄が出てこなかったよ。
    おいら側の問題だったのかな?^^

    • スパムコメントをメンテする時間がなかったので、コメント欄を閉鎖していたんですよ。投稿の間があくときは、また閉鎖するかもです。

      • なるほど。^^納得

  2. ISSの車窓からの眺めは、雪雲で天体がダメなこの時節に持ってこいです。
    前に教えてもらったNASAの公式は最近何だか帆みたいなのが出っ張ってて少しジャマなのが難点ですけど。
    民間?のを見つけて見てるんですが、こちらのカメラ映像は共産圏上空でも画像が途切れることもなく快適ですね。

    • カメラが調整中になることがあったのは、共産圏上空だったという理由もあったんですね。最近は、下の方に地図が出ている民間のチャンネル使ってます〜

タイトルとURLをコピーしました