事の発端#
多くのメディアが報じているように、ソーシャルメディアでチャットをしているときに、画像を送信する際に「元画像」オプションを選択すると、
画像に含まれる GPS 情報に基づいて個人の位置情報が露呈する可能性があります。
では、自分で地理位置情報をクエリすることを考えたことがありますか?
今日はちょうどその記事を見かけたので、再現してみます。
キーの申請#
まず、高德オープンプラットフォームに登録してログインします。
アプリケーションを作成し、右側の+
ボタンをクリックしてkey
を申請します:
key
に名前を付けてwebサービス
を選択して提出します:
逆ジオコーディングのAPI
を取得しました:
GPS から取得した経度、緯度と高德地図の座標には一定の誤差があり、高德公式は座標変換の方法を提供しています。
使用するkey
はこのWEB
サービスに属しているため、再度申請する必要はありません。このkey
は 2 回使用されます。
画像処理#
exifread
ライブラリを使用して画像のexif
情報を読み取ります。公式のGitHubには一般的なパラメータの取得方法があります。
まず、pip
でインストールします:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple exifread
基本的な使用方法は以下の通りです:
with open("test.jpg", 'rb') as f:
image_exif = exifread.process_file(f)
for tag, value in image_exif.items():
print(tag, value)
これはすべての情報を印刷するためにこのように使用していますが、実際にはexif
はdict
として読み取られ、直接キー値方式で読み取ることができるので、for
ループは必要ありません。
(** ここで説明しますが、テストしたい場合は、自分の携帯電話で写真を撮ってテストしてください。WeChat や QQ でアップロードした画像は無効です **
携帯電話のカメラで撮影して、ネットワークドライブやファイル転送などの方法で転送できます。例えば、牛快伝を使用できます。)
ただし、ここにはパラメータが多すぎて、大部分は必要ありません。以下の情報を取得するだけで大丈夫です:
Image Make:携帯電話ブランド
Image Model:携帯電話モデル
EXIF LensModel:カメラ情報
GPS GPSLongitudeRef:経度マーク
GPS GPSLongitude:経度
GPS GPSLatitudeRef:緯度マーク
GPS GPSLatitude:緯度
ここで
プログラムを書くときは、経度と緯度はN方向を正の値とします。
GPS を高德に変換#
画像から読み取った GPS 情報はまだ適切ではなく、高德api
は変換機能を提供しています。
同じapi
を使用して関数を構築し、リクエストを送信するだけです:
クエリ#
高德座標を取得した後、同じapi
を使用して新しいurl
を構築し、gps
に逆ジオコーディング変換をリクエストできます。
gps
座標を街道住所に変換します。
全コードは以下の通りです:
# -*- coding: utf-8 -*-
'''
@author: soapffz
@function: 高德地図APIを使用して元画像情報と地理位置をクエリする(単一またはフォルダバッチ)
@time: 2019-09-03
'''
from os import path, listdir
import exifread
import requests
import json
"""
ライブラリがインストールされていない場合は、次を参照してください:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple exifread
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple json
高德オープンプラットフォーム:https://lbs.amap.com/dev/key/app
記事のチュートリアル:https://soapffz.com/383.html
バッチクエリを行う場合は、すべてを1つのフォルダに置き、初期化時にフォルダパスを入力してください。
"""
class Location_query(object):
def __init__(self, key, img_path):
# 受け取ったパラメータ、高德APIと画像パス(フォルダパスの可能性もあり)
self.key = key
if len(self.key) != 32:
print("あなたのkeyはどうやら間違っているようです...")
exit(0)
self.img_path = img_path
self.judge_dir()
def judge_dir(self):
# フォルダかどうかを判断
if path.isdir(self.img_path):
print("あなたが選択したのはフォルダです")
self.pic_l = []
for item in listdir(self.img_path):
if path.splitext(item)[1].lower() in [".jpg", ".png", ".jpeg"]:
self.pic_l.append(path.join(self.img_path, item))
if len(self.pic_l) == 0:
print("あなたが選んだフォルダには画像が1枚もありません、あなたの大叔父を調べてください")
exit(0)
elif len(self.pic_l) == 1:
self.singal_image_info(self.pic_l[0])
else:
print("合計で{}枚の画像を取得しました\n".format(len(self.pic_l)))
self.multi_pics_query()
else:
self.singal_image_info(self.img_path)
def singal_image_info(self, img):
# 単一画像のクエリ
if type(img) != "list":
image_l = []
image_l.append(img)
else:
image_l = img
singal_pic_info_d = self.get_image_info(image_l)
for key, value in singal_pic_info_d.items():
print(key)
for info in value:
print(info)
def multi_pics_query(self):
# 複数の画像を処理します。逆ジオコーディングは一度に20対の座標しか処理できないため、すべての座標を20個ごとに分割してリストにします。
cut_pic_l = [self.pic_l[i:i + 20]
for i in range(0, len(self.pic_l), 20)]
info_l = {} # すべてのクエリ情報を保存
for cut in cut_pic_l:
batch_info = self.get_image_info(cut)
info_l = {**info_l, **batch_info}
for key, value in info_l.items():
print(key)
for info in value:
print(info)
print("\n")
def parse_gps_express(self, gps_express):
"""
GPS座標値を数値に変換
:param gps_express: GPS座標表現 [1,2,3/4]
:return: GPS座標数値 1.035542
"""
try:
express = str(gps_express).replace(
" ", "").replace("[", "").replace("]", "")
parts = express.split(",")
subpart = parts[2].split("/")
degress = float(parts[0])
minutes = float(parts[1])
seconds = float(subpart[0]) / float(subpart[1])
return degress + minutes / 60 + seconds / 3600
except:
raise Exception("画像情報エラー")
def get_image_info(self, images):
"""
写真撮影地のGPS座標
:param photo_path: 複数の写真のディスクパス(逆ジオコーディングAPIは一度に20対しかクエリできないため、ここでも最大20枚の画像に設定)
:return: 複数の写真の詳細情報(携帯電話ブランド、携帯電話モデル、カメラ情報、地理位置)
"""
multi_image_info = {} # 画像の情報を保存、キーは画像、値は画像の情報
gps_coordinates = {} # 複数のGPS情報をバッチクエリするために保存し、クエリ回数の無駄を避ける、キーは画像、値はGPS値
for image in images:
singal_image_info = []
image_name = image.split("\\")[-1]
with open(image, 'rb') as f:
image_exif = exifread.process_file(f)
if "Image Make" in image_exif.keys():
# 携帯電話情報が存在する場合、画像の携帯電話情報を取得
singal_image_info.append(
"携帯電話ブランドは:{}".format(str(image_exif["Image Make"])))
singal_image_info.append("携帯電話モデルは:{}".format(
str(image_exif["Image Model"])))
singal_image_info.append("カメラ情報は:{}".format(
str(image_exif["EXIF LensModel"])))
else:
singal_image_info.append("携帯電話情報を取得できませんでした")
if "GPS GPSLongitudeRef" in image_exif.keys():
# GPS情報が存在する場合、画像のGPS情報を取得
#longitude_ref = str(image_exif["GPS GPSLongitudeRef"]).strip()
longitude = self.parse_gps_express(
str(image_exif["GPS GPSLongitude"]))
#latitude_ref = str(image_exif["GPS GPSLatitudeRef"]).strip()
latitude = self.parse_gps_express(
str(image_exif["GPS GPSLatitude"]))
#lng = longitude if "E" == longitude_ref else 0 - longitude
#lat = latitude if "E" == latitude_ref else 0 - latitude
gps_coordinates[image] = str(longitude) + "," + str(latitude)
# ["116.487585177952,39.991754014757","116.487585177952,39.991653917101"]
else:
singal_image_info.append("GPS情報を取得できませんでした")
multi_image_info[image] = singal_image_info
for image, location in zip(gps_coordinates.keys(), self.convert_gps(gps_coordinates.values())):
multi_image_info[image].append("位置は:{}".format(location))
return multi_image_info
def convert_gps(self, gps_coordinates):
"""
座標変換:GPSを高德に変換
:param gps_coordinates: 複数の位置"GPS経度,GPS緯度"の集合(タプルまたは配列でも可)
:return: 複数の位置"高德経度,高德緯度"の集合リスト、最大で一度に40対の座標を受け取る
"""
try:
coordinates = "|".join(gps_coordinates)
url = "https://restapi.amap.com/v3/assistant/coordinate/convert?key={0}&locations={1}&coordsys=gps&output=json".format(
self.key, coordinates)
response = requests.get(url)
result = json.loads(response.text)
if "1" == result["status"]:
gd_coordinates = result["locations"].split(";")
return self.get_map_address((gd_coordinates))
else:
print("エラー:", result["infocode"], result["info"])
except Exception as e:
raise e
def get_map_address(self, gd_coordinates):
"""
逆ジオコーディング(高德座標を住所に変換)
:param gd_coordinates: 複数の位置"高德経度,高德緯度"の集合(タプルまたは配列でも可)
:return: 複数の位置住所情報のリスト、最大で一度に20個の経緯度点をクエリ
"""
try:
coordinates = "|".join(gd_coordinates)
batch = "true" if len(gd_coordinates) > 1 else "false"
url = "https://restapi.amap.com/v3/geocode/regeo?key={0}&location={1}&batch={2}&radius=500&extensions=base&output=json".format(
self.key, coordinates, batch)
response = requests.get(url)
result = json.loads(response.text)
if "1" == result["status"]:
address = []
if batch == "true":
address.extend([add["formatted_address"]
for add in list(result["regeocodes"])])
else:
fmt_add = result["regeocode"]["formatted_address"]
address.append(fmt_add)
return address
else:
print(result)
except Exception as e:
raise e
if __name__ == "__main__":
key = "" # 申請した高德オープンプラットフォームAPIを記入
img_path = "" # 画像またはフォルダのアドレスを記入
Location_query(key, img_path)
単一画像のクエリ結果は以下の通りです:
フォルダクエリの結果は以下の通りです:
TODO
その中で一つわからない点があるのは、画像から読み取った GPS 情報:longitude
とlatitude
の関係です。
参考記事では、方向がN
の場合は負数に変換する必要があると見ましたが、
負数は高德地図の GPS 規範に合わないため、負号を付けない方がクエリできるので、この 2 行はコメントアウトしましたが、まだ理解できていません。
EXIF 表示ソフト#
資料を調べているときに見つけたもので、まだ作成していませんが、zdfan
で最新のクラック版を見つけました。実際に使用できることが確認できました。
私たちのクエリ結果が正しいことも確認できます:
この記事は終わりです。
参考記事: