はじめに
自分はカメラ (写真)が趣味なので撮影、編集や写真整理、写真のメタデータに興味があります。
Pythonで写真のメタデータを取得しようと思って調べると、pillowを使う記事が多いですが、pillowはRAW画像や動画に対応しないので、カメラで撮ったすべてのファイルに対応できません。
なのでPythonで写真や動画のメタデータを取得するライブラリを作りました!
インストール
pip install photo-metadata
メタデータを取得
幅広い形式に対応するため、バックエンドにexiftool
を使いました。
なのでexiftoolをインストールする必要があります。
exiftool
pythonで写真のメタデータを取得する際はexiftoolが一番だと思っています。
Metadataクラス
Metadata
クラスは、メタデータ操作の中心となるクラスです。
from photo_metadata import Metadata
初期化
metadata = Metadata(file_path="path/to/your/image.jpg")
-
file_path
(str | Path): 画像ファイルのパス
__init__メソッドの内部
command_exiftool_text = f'{str(_exiftool_path)} -G -json "{file_path}"'
if sys.platform == "linux":
result = subprocess.run(command_exiftool_text, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
else:
result = subprocess.run(command_exiftool_text, stderr=subprocess.PIPE, stdout=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW)
encoding = chardet.detect(result.stdout)["encoding"] or chardet.detect(result.stderr)["encoding"] or "utf-8"
stdout_text = result.stdout.decode(encoding, errors="replace")
stderr_text = result.stderr.decode(encoding, errors="replace")
if result.returncode != 0:
raise RuntimeError(f"Failed to get metadata error: {stdout_text}\n{stderr_text}")
metadata: dict = json.loads(stdout_text)[0]
subprocessでコマンド実行しています。
出力をjson形式にして、json.loads()
でdict型にできるように、-json
オプションを付けています。このオプションを付けると、exiftoolの出力がjson形式の文字列になります。
文字化けを防ぐためにchardetでエンコーディングを判定しています。
メタデータの取得
メタデータは、辞書のようにアクセスできます。
英語のタグでアクセス
date_time = metadata["EXIF:DateTimeOriginal"]
print(date_time)
日本語のタグでアクセス
date_time = metadata[photo_metadata.key_ja_to_en("EXIF:撮影日時")]
print(date_time)
メタデータの変更
メタデータは、辞書のように変更できます。
metadata["EXIF:DateTimeOriginal"] = "2024:02:17 12:34:56"
変更をファイルに書き込む
metadata.write_metadata_to_file()
write_metadata_to_fileメソッドの内部
# メタデータをJSONファイルに一時的に保存
temp_json = tempfile.NamedTemporaryFile(delete=False, suffix='.json')
temp_json.close()
print(temp_json.name)
with open(temp_json.name, 'w', encoding='utf-8') as f:
json.dump(write_metadata, f, ensure_ascii=False, indent=4)
try:
# exiftoolを使用してメタデータを書き込む
command = f'{str(_exiftool_path)} -json="{temp_json.name}" -overwrite_original "{file_path}"'
if sys.platform == "linux":
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
else:
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW)
encoding = chardet.detect(result.stdout)["encoding"] or chardet.detect(result.stderr)["encoding"] or "utf-8"
text_stdout = result.stdout.decode(encoding, errors="replace")
text_stderr = result.stderr.decode(encoding, errors="replace")
print(f"exiftool standard output: {text_stdout}")
print(f"exiftool standard error: {text_stderr}")
if result.returncode != 0:
raise RuntimeError(f"Failed to write metadata. Error: {text_stdout}\n{text_stderr}")
finally:
# 一時ファイルを削除
os.unlink(temp_json.name)
コマンドで-json=
でパスを渡すと、jsonファイルに書いてあるメタデータをファイルに書き込んでくれます。
-overwrite_original
オプションを使用すると、元のファイルのメタデータを上書きします。
メタデータの削除
メタデータは、del
ステートメントで削除できます。
del metadata["EXIF:DateTimeOriginal"]
比較
==
と!=
演算子を使用して、2つのMetadata
オブジェクトを比較できます。
metadata1 = Metadata("image1.jpg")
metadata2 = Metadata("image2.jpg")
if metadata1 == metadata2:
print("メタデータは同じです")
else:
print("メタデータは異なります")
別のファイルで比較するとだいたいFalseになります。
メタデータには詳細な情報がたくさん入っているのがわかります。
その他の関数やメソッド
-
get_key_map()
: 日本語キー変換用の辞書を取得できます -
set_exiftool_path(exiftool_path: str | Path) -> None:
: exiftoolのパスを設定できます -
get_exiftool_path() -> Path
: 設定されたexiftoolのパスを取得できます -
set_jp_tags_json_path(jp_tags_json_path: str | Path) -> None:
: 日本語タグのJSONファイルのパスを設定できます -
get_jp_tags_json_path() -> Path
: 設定された日本語タグのJSONファイルのパスを取得できます` -
key_en_to_ja(key_en: str) -> str:
: 英語のキーを日本語に変換します -
key_ja_to_en(key_ja: str) -> str:
: 日本語のキーを英語に変換します -
display_japanese(self, return_type: Literal["str", "print", "dict"] = "print") -> str:
: メタデータを日本語のキーで表示できます -
get_date(self, format: str = '%Y:%m:%d %H:%M:%S')
: 撮影日時を取得 (日付フォーマットを指定できます) -
get_model_name(self)
: カメラの機種名を取得 -
get_lens_name(self)
: レンズ名を取得 -
get_focal_length(self)
: 焦点距離を取得 -
get_image_dimensions(self)
: 画像の寸法を取得 -
get_file_size(self)
: ファイルサイズを取得 -
get_gps_coordinates(self)
: GPS座標を取得 -
export_gps_to_google_maps(self)
: GPS情報をGoogleマップのURLに変換 -
write_metadata_to_file(self, file_path: str = None)
: メタデータをファイルに書き込む -
export_metadata(self, output_path: str = None, format: Literal["json", "csv"] = 'json', lang_ja_metadata: bool = False):
: メタデータをファイルにエクスポート -
show(self)
: ファイルを表示します -
@classmethod def load_all_metadata(cls, file_path_list: list[str], progress_func: Callable[[int], None] | None = None, max_workers: int = 40) -> dict[str, "Metadata":
: 複数のファイルのメタデータを並列処理で高速に取得します。
exiftool_pathのデフォルトは"exiftool"です
get_dateメソッド
撮影日時を取得します。
def get_date(self, format: str = '%Y:%m:%d %H:%M:%S', default_time_zone: str = '+09:00'):
if "EXIF:DateTimeOriginal" in self.metadata:
date = self.metadata["EXIF:DateTimeOriginal"]
date = datetime.datetime.strptime(date, '%Y:%m:%d %H:%M:%S').strftime(format)
elif "QuickTime:CreateDate" in self.metadata:
if self.metadata["QuickTime:CreateDate"] == "0000:00:00 00:00:00":
return self.error_string
if "QuickTime:TimeZone" in self.metadata:
dt = datetime.datetime.strptime(self.metadata["QuickTime:CreateDate"], '%Y:%m:%d %H:%M:%S')
tz = datetime.datetime.strptime(self.metadata["QuickTime:TimeZone"].replace("+", ""), "%H:%M")
tz = datetime.timedelta(hours=int(tz.strftime("%H")), minutes=int(tz.strftime("%M")))
date = dt + tz
date = date.strftime(format)
else:
dt = datetime.datetime.strptime(self.metadata["QuickTime:CreateDate"], '%Y:%m:%d %H:%M:%S')
tz = datetime.datetime.strptime(default_time_zone.replace("+", ""), "%H:%M")
tz = datetime.timedelta(hours=int(tz.strftime("%H")), minutes=int(tz.strftime("%M")))
date = dt + tz
date = date.strftime(format)
else:
date = self.error_string
return date
datetime.strptime、.strftimeを使って日付のフォーマットを指定できるようにしました。
動画にも対応できるように、"QuickTime:CreateDate"で撮影日時を取得します。ただ、"QuickTime:CreateDate"はUTCの時間なので、"QuickTime:TimeZone"の時間をプラスしています。
"QuickTime:CreateDate"があって、"QuickTime:TimeZone"が無い場合にデフォルトのタイムゾーンを指定できます。
file_path = "ファイルパス"
md = Metadata(file_path)
print(md.get_date('%Y年%m月%d日-%H.%M.%S'))
get_metadata_obj_dictメソッド
複数のファイルのメタデータを高速に取得します。
@classmethod
def load_all_metadata(
cls,
file_path_list: list[str],
progress_func: Callable[[int], None] | None = None,
max_workers: int = 40
) -> dict[str, "Metadata"]:
"""
Load metadata from multiple file paths in parallel.
Args:
file_path_list (list[str]): List of file paths to extract metadata from.
progress_func (Callable[[int], None] | None): Optional function to receive progress updates (0–100).
max_workers (int): Number of threads to use for parallel processing.
Returns:
dict[str, Metadata]: A dictionary mapping each file path to its corresponding Metadata object.
Example:
>>> file_list = ["image1.jpg", "image2.png"]
>>> metadata_dict = Metadata.load_all_metadata(file_list)
>>> metadata_dict["image1.jpg"]["DateTimeOriginal"]
'2023:01:01 10:00:00'
"""
def load_one(file_path: str):
return file_path, cls(file_path)
total = len(file_path_list)
result_dict: dict[str, Metadata] = {}
progress = 0
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [executor.submit(load_one, path) for path in file_path_list]
for future in tqdm(concurrent.futures.as_completed(futures), total=total, dynamic_ncols=True):
file_path, metadata = future.result()
result_dict[file_path] = metadata
progress += 1
if progress_func is not None:
progress_func((progress * 100) // total)
if progress_func is not None:
progress_func(100)
return result_dict
tqdmでプログレスバーを表示します。
concurrent.futures.as_completedはFutureオブジェクトのリストで終わった順にイテレーターを返すので、このコードでは終わった順にforが回ります。
おわりに
写真や動画のメタデータを取得に関するpythonライブラリを作ってみました。
自分では比較的簡単に、便利なライブラリが作れたと思っています。
exiftoolやsubprocessの使い方や、メタデータについて知れました。
コードはGitHubで見れます。
pypi
Pythonで写真や動画のメタデータを取得したい方の参考になれば幸いです。