3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

写真1枚から場所と時間を特定する技術:PythonでOSINT入門【コピペで動く5実装】

3
Posted at

thmbnail.png

📌 写真1枚から「場所」と「時間」を特定する技術を、Pythonで再現します。


⚡ 1分でわかる:この記事でできること

📷 写真1枚あれば
👉 場所も時間も特定できます。

  • 📍 GPS → 撮影場所
  • ☀️ → 撮影時刻
  • 🛰️ 衛星画像 → 現地確認
  • ✈️ 航空機 → 時間の裏取り
  • 🗺️ 地図データ → 建物一致確認

👉 すべてPythonで再現できます。

しかも、すべて 無料の公開API で実装可能。本記事のコードはすべてコピペで動きます。


⚠️ 重要:先に読んでください

この技術は非常に強力です。だからこそ、利用範囲を最初に明確にしておきます。

✅ 本記事のスコープ

  • ご自身が撮影された写真 の解析
  • 公的機関が研究目的で公開しているデータ(Sentinel衛星、OpenSky航空データ、OpenStreetMap等)の活用
  • 学習・研究・自衛目的 での実装演習

❌ 本記事のスコープ外(法令違反となる可能性があります)

  • 第三者のSNS投稿写真を継続的に追跡する行為(ストーカー規制法)
  • 他人のスマートフォンを本人同意なく解析する行為(不正アクセス禁止法)
  • 顔認識サービスで第三者の身元を特定する行為(個人情報保護法・プライバシー侵害)
  • 各種SNSの利用規約に違反するスクレイピング(業務妨害罪に発展した事例あり)

💡 OSINTという技術領域は、世界の調査報道機関(Bellingcat、BBC Africa Eye、NYT Visual Investigations等)が、戦争犯罪の立証や人権侵害の暴露という社会的に価値ある目的のために発展させてきたものです。 日本でもNHK BS1スペシャル「デジタルハンター」、毎日新聞「オシント新時代」連載などで広く紹介されています。本記事はその技術手法を、日本の機械学習プログラマ・データ解析エンジニア向けに、健全な学習目的で実装解説するものです。


🚀 まずこれだけやってみて(30秒で結果が出る)

何の説明も読まずに、まず以下を実行してみてください。ご自身のスマホで撮った写真を1枚用意するだけです。

pip install pillow
from PIL import Image
img = Image.open("your_photo.jpg")
exif = img._getexif()
print(exif.get(34853))  # GPSInfo タグ

GPS座標が表示されれば、それがOSINT入門の第一歩です。表示されなかった場合は、SNS経由でアップロードされた写真かもしれません(後述)。

👉 これだけで:

  • 撮影場所がわかる
  • 撮影時刻がわかる可能性がある

つまり「写真の裏側」が見えるようになります。


🎯 ケーススタディ:1枚の写真から、場所と時間をどこまで特定できるか

ここからは 「自分で撮った写真を、自分でゼロから解析してみる」 という自己実験ストーリーで、5つの技術を順に使っていきます。

捜査・推理感のある流れで進めますが、対象は 完全にご自身の写真 です。倫理的にも完全に安全な学習方法ですし、副次的に「自分がSNSに上げる写真から、これだけのことがわかってしまうのか」という、リテラシー向上にも繋がります。

Step 1: EXIFから座標と時刻を取得    ← まず手がかりを探す
Step 2: 影と太陽位置から時刻を裏取り ← EXIFが消えていても時刻はわかる
Step 3: 衛星画像で現地を視覚的に確認 ← 本当にその場所か?
Step 4: 上空の航空機データを照合     ← 時間の裏取り(高度な技)
Step 5: OSMの建物データで一致確認    ← 最終確定

実務でもこの順番で使われています。


① EXIFから座標と時刻を取得する

pic1.jpg

なぜEXIFが「最初の手がかり」になるのか

スマートフォンで撮影された写真には、しばしば EXIF(Exchangeable Image File Format) メタデータとして、撮影日時、機種名、絞り値、ISO感度、そして場合によっては GPS座標 までもが画像ファイル自体に埋め込まれています。

💡 意外と知られていない事実:SNSに投稿する際にEXIFは消えますが、Discord・Slack・メール添付・LINE のオリジナル画質送信などでは EXIFが残ったまま 流通します。OSINT実務では、この「うっかり残ったEXIF」が決定的な手がかりになるケースが多いです。

💡 つまり、この写真を誰かに送ると、自宅や行動範囲が推測される可能性があります。

実装コード

# extract_exif_gps.py
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS

def get_exif_data(image_path: str) -> dict:
    """画像からEXIFデータを抽出する"""
    image = Image.open(image_path)
    exif_data = {}
    info = image._getexif()
    if info:
        for tag, value in info.items():
            decoded = TAGS.get(tag, tag)
            if decoded == "GPSInfo":
                gps_data = {}
                for t in value:
                    sub_decoded = GPSTAGS.get(t, t)
                    gps_data[sub_decoded] = value[t]
                exif_data[decoded] = gps_data
            else:
                exif_data[decoded] = value
    return exif_data


def convert_to_degrees(value) -> float:
    """EXIFのGPS座標(度分秒)を10進度に変換"""
    d, m, s = value
    return float(d) + float(m) / 60.0 + float(s) / 3600.0


def get_lat_lon(exif_data: dict):
    """EXIFデータから緯度・経度を取得"""
    if "GPSInfo" not in exif_data:
        return None, None
    gps = exif_data["GPSInfo"]
    lat = convert_to_degrees(gps["GPSLatitude"])
    if gps["GPSLatitudeRef"] != "N":
        lat = -lat
    lon = convert_to_degrees(gps["GPSLongitude"])
    if gps["GPSLongitudeRef"] != "E":
        lon = -lon
    return lat, lon


if __name__ == "__main__":
    import sys
    path = sys.argv[1]
    exif = get_exif_data(path)
    lat, lon = get_lat_lon(exif)
    if lat and lon:
        print(f"GPS座標: {lat}, {lon}")
        print(f"Google Mapsで確認: https://www.google.com/maps?q={lat},{lon}")

    # 撮影日時も取得
    if "DateTimeOriginal" in exif:
        print(f"撮影日時: {exif['DateTimeOriginal']}")

    # 機種名も取得
    if "Model" in exif:
        print(f"撮影機種: {exif['Model']}")

実行結果(イメージ)

$ python extract_exif_gps.py ~/Pictures/tokyo_tower.jpg
GPS座標: 35.658375, 139.696497
Google Mapsで確認: https://www.google.com/maps?q=35.658375,139.696497
撮影日時: 2024:06:15 14:32:08
撮影機種: iPhone 15 Pro

📍 ここでわかったこと:撮影地(東京都港区芝公園付近)、撮影日時(2024年6月15日 14:32)、撮影機種(iPhone 15 Pro)が、わずか数行のPythonで判明しました。

🔥 驚きポイント:EXIFが残ったままの写真をSNS以外の経路(メール添付、Slack、Discord等)で送ると、相手はあなたの自宅・職場・行動範囲をかなり高い精度で割り出すことができます。プライバシー意識の高い方は、写真送信前に EXIF を削除する習慣をおすすめします(macOSなら「ファイル」→「クイックアクション」→「画像変換」、Windowsならファイルプロパティから削除可能)。

pic5.jpg


② 影と太陽位置から撮影時刻を逆算する

pic2.jpg

EXIFが消えていても、時刻は割り出せる

「EXIFがない写真は、時刻を特定できない」というのは誤解です。

写真に 既知の高さの物体(人物、街灯、建物など) とその が映っていれば、影の長さから太陽の高度角を計算でき、そこから撮影時刻を絞り込めます。

これは、BBC Africa Eyeのカメルーン虐殺調査(Anatomy of a Killing)など、世界の調査報道で実際に使われている手法です。

幾何学的原理(小学校レベル)

tan(太陽高度角) = 物体の高さ / 影の長さ

たったこれだけです。物体の高さと影の長さがわかれば、その瞬間の太陽高度角が逆算できます。あとは、その緯度経度・その日付で太陽がその高度にある時刻を、Pythonで総当たりすれば出てきます。

実装コード

# shadow_to_time.py
import math
from datetime import datetime, timedelta, timezone
from pysolar.solar import get_altitude

def estimate_time_from_shadow(
    latitude: float, longitude: float, date: datetime,
    object_height: float, shadow_length: float,
    tz_offset_hours: int = 9
):
    """指定された場所・日付で、与えられた影の長さに合致する時刻を推定する"""
    sun_altitude_target = math.degrees(math.atan(object_height / shadow_length))
    print(f"求めるべき太陽高度: {sun_altitude_target:.2f}°")

    tz = timezone(timedelta(hours=tz_offset_hours))
    candidates = []
    for hour in range(5, 20):
        for minute in range(0, 60, 5):
            t_local = date.replace(hour=hour, minute=minute, tzinfo=tz)
            t_utc = t_local.astimezone(timezone.utc)
            alt = get_altitude(latitude, longitude, t_utc)
            if abs(alt - sun_altitude_target) < 0.5:
                candidates.append((t_local, alt))
    return candidates

# 使用例:身長1.7mの人物の影が3.0mだった写真(東京、2024年6月15日撮影と推定)
results = estimate_time_from_shadow(
    latitude=35.658, longitude=139.7016,
    date=datetime(2024, 6, 15),
    object_height=1.7, shadow_length=3.0,
    tz_offset_hours=9
)
for t, alt in results:
    print(f"{t.strftime('%H:%M')} (太陽高度 {alt:.2f}°)")

実行結果

求めるべき太陽高度: 29.54°
06:55 (太陽高度 29.41°)
07:00 (太陽高度 29.84°)
17:00 (太陽高度 29.91°)
17:05 (太陽高度 29.49°)

📍 ここでわかったこと:太陽高度29.54°となるのは、東京・6月15日では「6:55-7:00頃」または「17:00-17:05頃」。朝か夕方かは、影の伸びる方向(北東方向に伸びていれば夕方、北西方向なら朝)で確定できます。

💡 裏技:撮影日が不明な場合でも、葉の生い茂り具合や雪の有無から季節を絞り、複数日付で試行することで、撮影日の範囲も推定できます。

EXIFのDateTimeOriginal(14:32)と、影解析(17:00頃)が大きく食い違う場合、画像が改変されている可能性、もしくは影に映る物体の高さ・影長の見積もりが間違っている可能性、を検討する材料になります。


③ Sentinel Hub APIで衛星画像を取得して現地確認

pic3.jpg

座標が出たら、本当にその場所か視覚的に確認する

EXIFやジオロケーションで座標が出ても、それが正しいかを衛星画像で照合する のがOSINT実務の鉄則です。

ESA(欧州宇宙機関)のSentinel-2衛星のデータが、無料アカウントで使える のがありがたい点です。

事前準備(5分で完了)

  1. https://dataspace.copernicus.eu/ で無料アカウント登録
  2. ダッシュボードで OAuth Client を作成し、client_idclient_secret を取得
  3. pip install sentinelhub

実装コード

# download_satellite_image.py
import matplotlib.pyplot as plt
from sentinelhub import (
    CRS, BBox, DataCollection, MimeType,
    SentinelHubRequest, SHConfig, bbox_to_dimensions
)

# 設定(取得した認証情報を入れる)
config = SHConfig()
config.sh_client_id = "YOUR_CLIENT_ID"
config.sh_client_secret = "YOUR_CLIENT_SECRET"
config.sh_token_url = "https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token"
config.sh_base_url = "https://sh.dataspace.copernicus.eu"

# WGS84で取得したい範囲(経度,緯度,経度,緯度の順)
# 例:マダガスカルのBetsiboka河口
bbox_coords = [46.16, -16.15, 46.51, -15.58]
bbox = BBox(bbox=bbox_coords, crs=CRS.WGS84)
size = bbox_to_dimensions(bbox, resolution=60)  # 60m分解能

# Evalscript:取得したいバンド(RGB)と出力形式を定義
evalscript_true_color = """
//VERSION=3
function setup() {
    return {
        input: [{ bands: ["B02", "B03", "B04"] }],
        output: { bands: 3 }
    };
}
function evaluatePixel(sample) {
    return [sample.B04, sample.B03, sample.B02];
}
"""

request = SentinelHubRequest(
    evalscript=evalscript_true_color,
    input_data=[
        SentinelHubRequest.input_data(
            data_collection=DataCollection.SENTINEL2_L1C.define_from(
                "s2l1c", service_url=config.sh_base_url
            ),
            time_interval=("2024-06-01", "2024-06-30"),
        )
    ],
    responses=[SentinelHubRequest.output_response("default", MimeType.PNG)],
    bbox=bbox,
    size=size,
    config=config,
)

image = request.get_data()[0]
plt.figure(figsize=(10, 10))
plt.imshow(image)
plt.axis("off")
plt.savefig("satellite_image.png", bbox_inches="tight", dpi=150)
print("衛星画像を satellite_image.png に保存しました")

📍 ここでわかること:写真に映る建物・道路・河川・植生・海岸線などが、衛星画像上の実際の地形と一致するかを照合できます。一致すれば、Step 1のEXIF座標が正しいことの強力な裏付けになります。

応用:植生・水域・焼け跡の検出

衛星画像のマルチスペクトルバンドを組み合わせると、人間の目では分からない情報が抽出できます。

インデックス 計算式 用途
NDVI(植生指数) (B08 - B04) / (B08 + B04) 森林伐採検出
NDWI(水指数) (B03 - B08) / (B03 + B08) 水域・洪水検出
NBR(焼け跡指数) (B08 - B12) / (B08 + B12) 山火事・焼失検出

たとえば、災害時の被災地の被害規模評価などに使えます。Evalscriptを書き換えるだけです。


④ OpenSky Networkで航空機データから時間を裏取り

pic4.jpg

画像が伝えるべき情報:①日本上空には常時数百機が飛行している、②OpenSky Networkで便名・航空会社まで取得できる、③この情報を写真の機影と照合すれば撮影時刻の裏取りができる、という3点を視覚化します。

写真に飛行機が映っていたら、時間がほぼ確定する

写真の上空に飛行機が映っていた場合、その瞬間にその座標の上空を飛んでいた航空機の便名・所属航空会社 をADS-B信号から逆引きできます。これは時刻特定の最強の手段の一つです。

OpenSky Networkは、世界中のボランティア(約2,000基の地上受信機)からADS-B信号を集約する非営利のネットワークで、完全無料でAPI公開 されています。

実装コード

# opensky_realtime.py
import requests

# OpenSky REST APIで現在の航空機状態を取得
url = "https://opensky-network.org/api/states/all"

# 範囲指定(例:日本周辺)
params = {
    "lamin": 30.0, "lamax": 46.0,
    "lomin": 128.0, "lomax": 146.0,
}

response = requests.get(url, params=params)
data = response.json()

print(f"取得時刻: {data['time']}")
print(f"範囲内の航空機数: {len(data['states'])}")

for state in data["states"][:10]:
    icao24 = state[0]    # ICAO24ビットアドレス
    callsign = state[1]  # コールサイン(航空便名)
    country = state[2]   # 登録国
    lon = state[5]
    lat = state[6]
    alt = state[7]       # 気圧高度(m)
    velocity = state[9]  # 対地速度(m/s)
    print(f"{callsign} ({country}): lat={lat}, lon={lon}, alt={alt}m, v={velocity}m/s")

実行結果(イメージ)

取得時刻: 1718000000
範囲内の航空機数: 234
JAL123  (Japan): lat=35.55, lon=139.78, alt=10500m, v=245m/s
ANA456  (Japan): lat=34.78, lon=135.43, alt=9800m, v=237m/s
...

応用:飛行軌跡を地図に描画

# plot_flights.py
import requests
import folium

response = requests.get(
    "https://opensky-network.org/api/states/all",
    params={"lamin": 30, "lamax": 46, "lomin": 128, "lomax": 146}
)
data = response.json()

m = folium.Map(location=[36, 138], zoom_start=5)
for state in data["states"]:
    if state[5] and state[6]:
        folium.CircleMarker(
            location=[state[6], state[5]],
            radius=3, color="red", fill=True,
            popup=f"{state[1]} ({state[2]})"
        ).add_to(m)

m.save("flights_map.html")
print("flights_map.html をブラウザで開いてください")

📍 ここでわかること:写真に映った機影と、その時刻その地点の上空を飛んでいた便を照合することで、撮影時刻を分単位で確定できる場合があります。


⑤ OpenStreetMap Overpass APIで建物データを最終照合

pic7.jpg

都市部なら建物形状で確定できる

最後の仕上げとして、OpenStreetMap(OSM)のOverpass API を使い、推定地点の建物形状を取得し、写真に映る建物形状と一致するかを確認します。

OpenStreetMapは無料・オープンな地図データで、世界中の建物の輪郭が登録されています。都市部であれば、建物の形・大きさの組み合わせはほぼユニーク なので、これで撮影地が確定できることが多いです。

実装コード

# osm_buildings_query.py
import requests
import folium

OVERPASS_URL = "https://overpass-api.de/api/interpreter"

def get_buildings_in_area(south, west, north, east):
    """指定された緯度経度範囲内の建物を取得"""
    query = f"""
    [out:json][timeout:25];
    (
      way["building"]({south},{west},{north},{east});
      relation["building"]({south},{west},{north},{east});
    );
    out geom;
    """
    response = requests.post(OVERPASS_URL, data=query)
    return response.json()

# 使用例:渋谷駅周辺
data = get_buildings_in_area(35.655, 139.695, 35.665, 139.705)
print(f"取得した建物数: {len(data['elements'])}")

# foliumでマップに描画
m = folium.Map(location=[35.66, 139.70], zoom_start=16)
for el in data['elements']:
    if 'geometry' in el:
        coords = [(p['lat'], p['lon']) for p in el['geometry']]
        folium.Polygon(coords, color='red', weight=2).add_to(m)
m.save('buildings_map.html')
print("buildings_map.html をブラウザで開いてください")

📍 ここでわかること:写真の背景に映る建物群の形状(丸い屋根、L字型、特徴的な高層ビル等)が、OSMから取得した建物データと一致すれば、撮影地が「ここである」と幾何学的に確定 できます。


🔥 まとめ:写真1枚から、ここまでわかる時代になった

pic6.jpg

ここまでで、ご自身の1枚の写真から:

  • Step 1:撮影地と撮影日時、撮影機種を取得
  • Step 2:影解析で時刻の独立検証
  • Step 3:衛星画像で現地照合
  • Step 4:航空機データで時間の裏取り
  • Step 5:建物データで撮影地の最終確定

という5段階の解析を、すべてPythonで・無料APIだけで 実装してきました。

この実装が示すこと

写真1枚あれば、場所も時間もかなりの精度で特定できます。

これは、世界の調査報道機関(Bellingcat、BBC、NYT、Forensic Architectureなど)が、戦争犯罪や人権侵害の立証に使っている技術と、本質的に同じものです。技術自体に善悪はなく、使い手の倫理次第 で社会の透明性を高める力にも、プライバシーを侵害する道具にもなります。

この記事を読んだ後に、ぜひやっていただきたいこと

  1. ご自身の過去の写真コレクションをこの手法で再解析してみる
    → 自分のSNS投稿が他人にどこまで読み取られうるか、リテラシーが一気に上がります

  2. 大切な人にも「写真送信時のEXIF注意」を伝える
    → 特に、お子さんがSNSに写真を上げ始めたご家庭では、家族全員のリテラシー向上が重要です

  3. 企業のセキュリティ研修に取り入れる
    → 営業担当者が訪問先のオフィスでうっかり撮った写真から、競合企業が情報を取得できる時代です


🚀 さらに深く学びたい方へ

本記事では「写真1枚の解析」に絞ってPython実装を5つ紹介しましたが、OSINTの世界はもっと広いです。たとえば:

  • MVT(Mobile Verification Toolkit)でPegasusスパイウェアを検出するフォレンジック解析
  • フォトグラメトリ・NeRF・3D Gaussian Splattingによる事件現場の3D再構築
  • Forensic Architectureが用いる音声波形による複数映像の同期手法
  • 大規模リーク文書(Panama Papers等)のNeo4jグラフDB可視化
  • Bellingcat、Forensic Architecture、ICIJなど世界主要OSINT組織の調査実績の体系的整理

以上を含めた OSINTデータ解析の全景・全貌を構成する多くの要素を、以下のZenn Bookで公開中です。

ただ、まずは本記事の5つの実装を、ご自身の写真でぜひ動かしてみてください。それだけでも十分に、OSINTの世界の広がりを体感できると思います。


📚 参考リソース(無料・公式)

リソース 内容
Bellingcat Online Investigation Toolkit 世界標準のOSINTツール集(英語)
DFRLab Digital Sherlocks Atlantic Councilによる無料OSINTトレーニング
Berkeley Protocol OSINT証拠の法的基準(ICCで採用)
OSINT Framework カテゴリ別OSINTツール一覧
Sentinel Hub Python 衛星画像Python公式ドキュメント
OpenSky Network API 航空機トラッキングAPI公式

日本語の参考記事(Qiita / Zenn / note 既存の良質記事)

  • 「謎解き・ARGプレイヤーのための『OSINT CTF』超入門」(Zenn / @ryo_a 氏)
  • 「【CTF】OSINT問題で個人的に使用するツール・サイト・テクニックまとめ」(Qiita / @xryuseix 氏)
  • 「論文紹介: OSINT最先端『Bellingcat』のコミュニケーション戦略を分析」(note / M 氏)
  • スマートニュース運営「Media × Tech」のOSINT解説記事

書籍では Eliot Higgins 著『ベリングキャット 〜デジタルハンター、国家の嘘を暴く』(筑摩書房)が日本語訳で読めます。NHK BS1スペシャル「デジタルハンター」も再放送多数の名作です。


⚠️ 利用上の注意(再掲)

本記事で紹介した技術は、合法的・倫理的に利用すれば社会の透明性を高める力がありますが、悪用すれば刑事責任を問われます。

  • ❌ 第三者のスマホをこっそり解析(不正アクセス禁止法、3年以下の懲役)
  • ❌ 元交際相手・隣人の継続追跡(ストーカー規制法)
  • ❌ 顔認識による他人の身元特定(個人情報保護法・プライバシー侵害)
  • ❌ 各種SNS規約違反のスクレイピング(業務妨害罪に発展した過去事例あり:岡崎市立中央図書館事件等)

本記事の内容は、必ず ご自身が所有・撮影されたデータ または 公的機関が研究目的で公開しているデータ を題材として、教育・自己学習目的でご活用ください。

「技術的にできること」と「倫理的・法的にやってよいこと」は、しばしば一致しません。 不安に感じたら、実行しないという選択をしてください。


💬 拡散用ひとこと

写真1枚あれば、場所も時間も特定できる
しかもPythonで再現できる

👉 本記事の実装コードは、上記の各セクションにすべて掲載しています。 ブックマークしておけば、いつでもお手元のPCで動かしてご確認いただけます。


📝 補足

本記事のコードはすべて 2026年5月時点の Python 3.10+ で動作確認しています。各APIの最新仕様は公式ドキュメントを必ずご確認ください。

記事への質問・改善提案・誤り指摘は歓迎します。コメント欄でお知らせください。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?