0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで写真や動画のメタデータを取得しよう!!

Last updated at Posted at 2025-02-23

はじめに

自分はカメラ (写真)が趣味なので撮影、編集や写真整理、写真のメタデータに興味があります。

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で写真や動画のメタデータを取得したい方の参考になれば幸いです。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?