スマホやデジカメで撮影された画像データには、撮影日時に加え、位置情報(緯度・経度)、絞り、シャッター速度、カメラの機種名、メーカー名など、さまざまな情報が Exif(Exchangeable image file format) と呼ばれるタグ情報となって埋め込まれている。対応している画像フォーマットは JPEG、TIFF、PNGだ。
今回は、Python と外部ライブラリ Pillow と piexif を使い、JPEG画像ファイルから Exif情報を取り出したり削除するプログラムを作ってみることにする。
目次
サンプル・プログラム
getExif.py | Exif情報を表示するサンプル・プログラム本体。 |
deleteExif.py | Exif情報を削除するサンプル・プログラム本体。 |
Exif情報を表示するサンプル・プログラムの使い方
Exif情報の表示を行うには、画像処理を行う外部ライブラリ Pillow(PIL)をインストールする。まだインストールしていない方は、pipコマンドを使ってインストールしてほしい。
pip install PIL
インストールできたら、プログラム "getExif.py" を実行してみてほしい。前回つくったものと同じようなファイルダイアログを表示するので、Exif情報を表示させたい画像ファイルを1つ選択する。Exif情報があればコンソールに表示する。
解説:Exif情報を表示するメイン・プログラム
getExif.py
# メイン・プログラム =======================================================
try:
# 画像ファイルを選択する
imageFname = getFileName()
print(f"ファイル名: {imageFname}")
# Exif情報を取得して表示
exifData = getExif(imageFname)
if exifData:
for tag, data in exifData.items():
print(f"{data[0]}: {data[1]}")
else:
print("この画像にはExif情報がありません")
except Exception as e:
print(str(e))
プログラムの流れが分かるよう、メイン・プログラムから説明する。
まず、Exif情報を表示させたい画像ファイルを選択する。
次に、画像ファイルからExif情報を取り出し、Exifタグ名をキーにする辞書を exifData に格納する。
もし exifData の内容が空でなければ、for文を使って、「1行=1つのExifタグ情報」の形でコンソールに表示する。空ならば、その旨のメッセージを表示する。
最後に例外処理を行う。
まず、Exif情報を表示させたい画像ファイルを選択する。
次に、画像ファイルからExif情報を取り出し、Exifタグ名をキーにする辞書を exifData に格納する。
もし exifData の内容が空でなければ、for文を使って、「1行=1つのExifタグ情報」の形でコンソールに表示する。空ならば、その旨のメッセージを表示する。
最後に例外処理を行う。
解説:画像ファイルからExif情報を取得する
getExif.py
def getExif(imageFname):
"""画像ファイルからExif情報を取得する
Args:
data: 画像ファイル名(フルパス)
Returns:
list: (ラベル, 値)のリスト
None: Exifデータがない
Raises:
FileNotFoundError: ファイルが存在しない,読み込めない
"""
# タグデータを読みやすいように変換するテーブル
convTAGS = {
# タグ名 : 関数名
"GPSInfo" : convGPSInfo,
"ExifImageWidth" : convImageWidth,
"ExifImageHeight" : convImageHeight,
"Make" : convMaker,
"Model" : convModel,
"BodySerialNumber" : convSerial,
"XResolution" : convXResolution,
"YResolution" : convYResolution,
"DateTime" : convDateTime,
"OffsetTime" : convOffsetTime,
"Copyright" : convCopyright,
"ExposureProgram" : convExposureProgram,
"ISOSpeedRatings" : convISOSpeedRatings,
"ShutterSpeedValue" : convShutterSpeedValue,
"FNumber" : convFNumber,
"FocalLength" : convFocalLength,
"Flash" : convFlash,
"MeteringMode" : convMeteringMode,
"LensSpecification" : convLensSpecification,
"LensModel" : convLensModel,
}
# タグデータ保存するテーブル
storeTAGdata = {
# タグ名 : 関数名
"ResolutionUnit" : storeResolutionUnit,
}
# 画像ファイルを開く
try:
image = Image.open(imageFname)
# 失敗したら例外を返す.
except Exception:
raise FileNotFoundError(f"{imageFname} は画像データではありません")
# Exif情報を取得
exifData = image._getexif() # getexif()は使いづらい(;^^)‥‥
# Exif情報がある場合
if exifData:
exif = {}
# ExifタグIDを取り出す
for tagID, value in exifData.items():
tagName = TAGS.get(tagID, tagID)
# Exifデータを読みやすい形式に変換する
if tagName in convTAGS:
data = convTAGS[tagName](value)
if data is not None:
exif[tagName] = data
if tagName in storeTAGdata:
storeTAGdata[tagName](value)
return exif
else:
return None
ユーザー定義関数 getExif は、画像ファイル名を引数とし、Exifタグ名をキーにする辞書を返す。
Exif情報は、画像ファイルの中にバイナリデータの形で埋め込まれている。外部ライブラリ PIL を使うことで取り出しやすくはなるのだが、タグ名は英語のままであるし、データもバイナリのままのものが多い。そこで今回は、表示するタグを絞り、日本語で読みやすい形に変換することにした。
ただ、Exif情報の内容によって変換方法が異なるので、Exifタグ毎に変換関数を用意する。
タグ名が見つかったら対応する関数を呼び出すのに、if文 や match文を使うと分岐の数が膨大になるし、後々、表示したいタグを追加したり削除するのに手間がかかる。
そこで、Pythonの辞書の性質を利用し、Exifタグ名をキーとし、変換する関数を要素とする辞書 convTAGS を用意した。また、変換はしないが、タグデータを保存するための辞書 storeTAGdata も用意した。構造は両者とも同じである。
まず、PILの open関数を使って画像ファイルを開く。
次に、_getexif関数を使って Exif情報を辞書 exifData に格納する。ここで _getexif関数はアンダーバーで始まっていることから分かる通り、内部関数である。公開関数として用意されている getexif関数を使うのが本筋なのだが、現在のバージョンでは getexif関数で拾うことができない Exif情報が複数あることを確認した。そこで、ここでは _getexif関数を使うことをご容赦願いたい。
さて、Exif情報があれば辞書 exifData は空ではないので、if exifData のブロックを実行する。読みやすい形に変換した Exif情報は辞書 exif に格納する。
for文を使って、辞書 exifDataからキー tagID、要素 value を1つずつ取り出す。ここで取り出されるキー tagID は、Exifタグ名ではなく、仕様書に定義されている ExifタグID(2バイト符号無し整数)である。 PILの TAGS.get関数を使い、ExifタグIDを Exifタグ名に変換し、変数 tagName に格納する。
tagName の内容が、冒頭で定義した辞書 convTAGS にあれば、if tagName in convTAGS のブロックを実行する。すなわち、辞書 convTAGS で対応する関数 convTAGS[tagName] を実行する。このとき、呼び出す関数に引数として、Exifタグの中身 value を渡す。
同様に、tagName の内容が、辞書 storeTAGdata にあれば、対応する関数を実行する。
このようにすることで、長大な if文 や match文を書くことなく、目的の関数を呼び出すことができる。このような書き方を関数型プログラミングと呼ぶ。
Exif情報は、画像ファイルの中にバイナリデータの形で埋め込まれている。外部ライブラリ PIL を使うことで取り出しやすくはなるのだが、タグ名は英語のままであるし、データもバイナリのままのものが多い。そこで今回は、表示するタグを絞り、日本語で読みやすい形に変換することにした。
ただ、Exif情報の内容によって変換方法が異なるので、Exifタグ毎に変換関数を用意する。
タグ名が見つかったら対応する関数を呼び出すのに、if文 や match文を使うと分岐の数が膨大になるし、後々、表示したいタグを追加したり削除するのに手間がかかる。
そこで、Pythonの辞書の性質を利用し、Exifタグ名をキーとし、変換する関数を要素とする辞書 convTAGS を用意した。また、変換はしないが、タグデータを保存するための辞書 storeTAGdata も用意した。構造は両者とも同じである。
まず、PILの open関数を使って画像ファイルを開く。
次に、_getexif関数を使って Exif情報を辞書 exifData に格納する。ここで _getexif関数はアンダーバーで始まっていることから分かる通り、内部関数である。公開関数として用意されている getexif関数を使うのが本筋なのだが、現在のバージョンでは getexif関数で拾うことができない Exif情報が複数あることを確認した。そこで、ここでは _getexif関数を使うことをご容赦願いたい。
さて、Exif情報があれば辞書 exifData は空ではないので、if exifData のブロックを実行する。読みやすい形に変換した Exif情報は辞書 exif に格納する。
for文を使って、辞書 exifDataからキー tagID、要素 value を1つずつ取り出す。ここで取り出されるキー tagID は、Exifタグ名ではなく、仕様書に定義されている ExifタグID(2バイト符号無し整数)である。 PILの TAGS.get関数を使い、ExifタグIDを Exifタグ名に変換し、変数 tagName に格納する。
tagName の内容が、冒頭で定義した辞書 convTAGS にあれば、if tagName in convTAGS のブロックを実行する。すなわち、辞書 convTAGS で対応する関数 convTAGS[tagName] を実行する。このとき、呼び出す関数に引数として、Exifタグの中身 value を渡す。
同様に、tagName の内容が、辞書 storeTAGdata にあれば、対応する関数を実行する。
このようにすることで、長大な if文 や match文を書くことなく、目的の関数を呼び出すことができる。このような書き方を関数型プログラミングと呼ぶ。
解説:GPS情報を読みやすい形に変換する
getExif.py
def convGPSInfo(data):
"""GPS情報を読みやすい形に変換する
Args:
data: Exifデータ
Returns:
str,str: ラベル, 位置情報;
None: データが無いまたは不明
"""
res = ""
if len(data) > 4:
# 緯度
if data[1] == "N":
label = "北緯"
else:
label = "南緯"
lat = 0
deg = 1
for x in data[2]:
lat += x / deg
deg *= 60
latitude = f"{label} {lat:g}度"
# 経度
if data[3] == "E":
label = "東経"
else:
label = "西経"
lng = 0
deg = 1
for x in data[4]:
lng += x / deg
deg *= 60
longitude = f"{label} {lat:g}度"
# 緯度, 経度
res = f"{latitude}, {longitude}"
if len(data) > 5:
# 高度
match int.from_bytes(data[5]):
case 0:
altitude = f"海抜 {float(data[6]):g}メートル"
case 1:
altitude = f"海面下 {float(data[6]):g}メートル"
case _:
altitude = f"高度不明"
res += f", {altitude}"
if (res != ""):
return "位置情報", res
else:
return None
それぞれの Exif情報を日本語で読みやすい形式に変換する関数を、タグの数だけ用意する。前述の通り、これらの関数は引数が1つだけで、その中に Exifタグデータがセットされて呼ばれる。戻り値は、読みやすい形式のExifデータ(文字列)である。
全部説明するのは大変なので、ここでは、GPS情報を読みやすい形に変換するユーザー関数 convGPSInfo を例として説明する。
_getExif関数で取り出されるGPS情報は、リストには次の形で
次に、引数 data の要素が5個を超えているならば、高度の基準、高度が格納されているので、これらをフォーマット文字列を使って日本語で読みやすい形に変換する。
撮影時のUTC時間、UTC日付は読み飛ばす。
全部説明するのは大変なので、ここでは、GPS情報を読みやすい形に変換するユーザー関数 convGPSInfo を例として説明する。
_getExif関数で取り出されるGPS情報は、リストには次の形で
- GPSLatitudeRef‥‥緯度の基準。N(北)またはS(南)が入っている。
- GPSLatitude‥‥緯度。リストに、度、分、秒の順に入っている。
- GPSLongitudeRef‥‥緯度の基準。E(東)またはW(西)が入っている。
- GPSLongitude ‥‥経度。リストに、度、分、秒の順に入っている。
- GPSAltitudeRef‥‥高度の基準。0: 海抜、1: 海面下。
- GPSAltitude‥‥高度。単位はメートル。
- GPSTimeStamp‥‥撮影時のUTC時間。時、分、秒が空白で区切られた文字列。
- GPSDateStamp‥‥撮影時のUTC日付。年、月、日が空白で区切られた文字列。
次に、引数 data の要素が5個を超えているならば、高度の基準、高度が格納されているので、これらをフォーマット文字列を使って日本語で読みやすい形に変換する。
撮影時のUTC時間、UTC日付は読み飛ばす。
Exif情報を削除するサンプル・プログラムの使い方
PIL で取り出した Exif情報はリードオンリーで、変更することができない。そこで、Exif情報の削除を行うために、Exif情報を操作できる外部ライブラリ piexifをインストールする。まだインストールしていない方は、pipコマンドを使ってインストールしてほしい。
pip install piexif
インストールできたら、プログラム "deleteExif.py" を実行してみてほしい。"getExif.py" と同じようなファイルダイアログを表示するので、Exif情報を削除したい画像ファイルを1つ選択する。
次に、削除したい Exif情報のチェックボックスにチェックを入れ、[削除]ボタンをクリックする。すると、指定した Exif情報を削除したものが、元のファイルと同じフォルダに、ファイル名に "_2" を追加した画像ファイルとして保存する。もし同名ファイルがあれば上書きしない(保存しない)。チェックボックスで「すべて」を選択すると、すべての Exif情報を削除する
解説:Exif情報を削除するメイン・プログラム
deleteExif.py
# メイン・プログラム =======================================================
try:
# 画像ファイルを選択する
inFname = getFileName()
# 出力ファイル名を作成する
outFname = makeDestFname(inFname, "_2")
except Exception as e:
messagebox.showerror("エラー", str(e))
exit()
try:
# 削除したいExif情報の選択ダイアログを作成する
root = tk.Tk()
selectDeleteTAGS(root, inFname, outFname)
# ダイアログを表示する
root.mainloop()
except Exception as e:
messagebox.showerror("エラー", str(e))
exit()
# バージョンアップ履歴 ======================================================
プログラムの流れが分かるよう、メイン・プログラムから説明する。
まず、Exif情報を削除したい画像ファイルを選択する。次に、出力ファイル名を作成する。これらは、前回と同じ処理なので説明は省略する。
次に、Tkinterダイアログを使って、削除したいExif情報の選択ダイアログを作成する。Tkinterダイアログ の表示に mainloop を使うのは前回と同じだ。
最後に例外処理を行う。
まず、Exif情報を削除したい画像ファイルを選択する。次に、出力ファイル名を作成する。これらは、前回と同じ処理なので説明は省略する。
次に、Tkinterダイアログを使って、削除したいExif情報の選択ダイアログを作成する。Tkinterダイアログ の表示に mainloop を使うのは前回と同じだ。
最後に例外処理を行う。
解説:削除したいExifタグを選択する
deleteExif.py
def selectDeleteTAGS(root, inFname, outFname):
"""削除したいExifタグを選択する
Args:
root(obj): tkinterダイアログ
inFname: 入力画像ファイル名(フルパス)
outFname: 出力画像ファイル名(フルパス)
"""
root.title("選択")
# 選択チェックボックスと削除するExifタグのリスト
selectTAGS = {
"位置情報" : {
"var" : tk.IntVar(),
"tags": ["GPSLatitude", "GPSLongitude", "GPSAltitude"]
},
"製造番号" : {
"var" : tk.IntVar(),
"tags": ["BodySerialNumber", "LensSerialNumber"]
},
"撮影日時" : {
"var" : tk.IntVar(),
"tags": ["DateTime", "DateTimeOriginal", "DateTimeDigitized"]
},
"すべて" : {
"var" : tk.IntVar(),
"tags": [ALL_EXIF_TAGS]
},
}
# ラベルの表示
message = "削除したいExif情報を選択してください."
label = tk.Label(root, text=message)
label.grid(row=0, column=0, columnspan=2, pady=10)
# チェックボックスの作成と配置
row = 1
col = 0
for key, item in selectTAGS.items():
checkbox = tk.Checkbutton(root, text=key, variable=item["var"])
checkbox.grid(row=row, column=col, padx=10, pady=10, sticky="w")
col += 1
if col == COLUMNS_CHECKBOX:
col = 0
row += 1
# ボタン
if (col > 0):
row += 1
submitButton = tk.Button(root, text="削除", command=lambda:submit(selectTAGS, inFname, outFname))
submitButton.grid(row=row, column=0, columnspan=COLUMNS_CHECKBOX, pady=10)
ユーザー関数 selectDeleteTAGS は、Tkinterダイアログ上に削除したいExifタグを選択するチェックボックスと、実行ボタンを配置する関数である。
"getExif.py" と同じ考え方で、選択チェックボックスに関する情報を、あらかじめ辞書 selectTAGS に用意しておく。辞書 selectTAGS は二重構造になっている。外側のキー値は、ダイアログに表示するラベル名である。その内側にキーが2つあって、varにはチェックボックスの値(0:チェック無し、1:チェック有り)を格納し、tagsには削除対象となる Exifタグ名をリストで格納しておく。たとえば「位置情報」を選んだときには、GPS情報の緯度、経度、高度の3つすべてを削除したいので、このようなリスト構造にしてある。
Tkinterダイアログへの部品の配置は前回説明した通りだ。このプログラムを改造して、削除チェックボックスを増減しやすいように、for文を使ってチェックボックスを並べるようにした。チェックボックスを生成するのは tk.Checkbutton である。横方向の列の数は、グローバル変数 COLUMNS_CHECKBOX に代入しておく。
[削除]ボタンがクリックされたときに実行するユーザー関数は、後述する submit だが、今回は何を選択したかを渡したいので、Pythonの無名関数という仕組みを用い、lambda:submit(selectTAGS, inFname, outFname) という形で呼び出す。
"getExif.py" と同じ考え方で、選択チェックボックスに関する情報を、あらかじめ辞書 selectTAGS に用意しておく。辞書 selectTAGS は二重構造になっている。外側のキー値は、ダイアログに表示するラベル名である。その内側にキーが2つあって、varにはチェックボックスの値(0:チェック無し、1:チェック有り)を格納し、tagsには削除対象となる Exifタグ名をリストで格納しておく。たとえば「位置情報」を選んだときには、GPS情報の緯度、経度、高度の3つすべてを削除したいので、このようなリスト構造にしてある。
Tkinterダイアログへの部品の配置は前回説明した通りだ。このプログラムを改造して、削除チェックボックスを増減しやすいように、for文を使ってチェックボックスを並べるようにした。チェックボックスを生成するのは tk.Checkbutton である。横方向の列の数は、グローバル変数 COLUMNS_CHECKBOX に代入しておく。
[削除]ボタンがクリックされたときに実行するユーザー関数は、後述する submit だが、今回は何を選択したかを渡したいので、Pythonの無名関数という仕組みを用い、lambda:submit(selectTAGS, inFname, outFname) という形で呼び出す。
解説:指定したExif情報を削除する
deleteExif.py
def deleteExif(inFname, outFname, deleteTAGList):
"""画像ファイルから指定したExif情報を削除して別ファイルに保存する.
Args:
inFname: 入力画像ファイル名(フルパス)
outFname: 出力画像ファイル名(フルパス)
deleteTAGList: 削除したいExifタグのリスト
Raises:
FileNotFoundError: 入力ファイルが存在しない,または読み込めない
FileExistsFoundError: 出力ファイルが存在しており書き込めない
ValueError: 入力ファイルにExifデータがない,または削除タグがない
"""
print(f"deleteTAGList = {deleteTAGList}") # for debug
# 入力ファイルを出力ファイルにコピーする
try:
shutil.copy(inFname, outFname)
permissions = os.stat(outFname).st_mode
# 書き込み禁止なら書き込み可能に属性変更する
if not (permissions & stat.S_IWUSR):
newPermissions = permissions | stat.S_IWUSR
os.chmod(outFname, newPermissions)
# 失敗したら例外を返す.
except Exception as e:
os.remove(outFname)
raise Exception(f"{outFname} を作成できません")
# Exif情報を取得する
exifData = piexif.load(outFname)
# piexifの3つのキー
piexifKeys = ["0th", "Exif", "GPS"]
# Exif情報がある場合
if exifData["0th"]:
deleteFlag = False
# 指定したExifタグを削除する
for piexifKey in piexifKeys:
for tagID, item in piexif.TAGS[piexifKey].items():
if item["name"] in deleteTAGList:
del exifData[piexifKey][tagID]
deleteFlag = True
print(f"delete {item["name"]}") # for debug
# 指定したExifタグが存在しない
if not deleteFlag:
raise ValueError(f"{inFname} には指定した削除タグがありません")
# Exif情報を更新する
try:
exifBytes = piexif.dump(exifData)
piexif.insert(exifBytes, outFname)
print(f"save {outFname}") # for debug
except Exception as e:
raise Exception(f"その他のエラー -- {str(e)}")
# Exif情報がない場合
else:
os.remove(outFname)
raise ValueError(f"{inFname} にはExif情報がありません")
ユーザー関数 deleteAllExif は、上述の ユーザー関数 selectDeleteTAGS で指定した削除したいExif情報のリストを引数として受け取り、入力画像ファイルから当該Exif情報を削除し、出力画像ファイルを作成する。
外部ライブラリ piexif には Exif情報を書き込み更新する piexif.insert関数があるのだが、これはすでに存在している画像ファイルに対して作用する。そこで、入力画像ファイル inFname を出力ファイル outFname にコピーする。これには、標準ライブラリ shutil の shutil.copy関数を使う。もし入力画像ファイル inFname が書き込み禁止属性だと、Exif情報を書き込むことができないので、標準ライブラリ os の os.stat関数を使って書き込み可能属性に変更する。
外部ライブラリ PIL や OpenCV を併用すれば、わざわざ画像ファイルをコピーしなくても、更新後の Exif情報 を出力ファイル outFname に新規保存できるのだが、このとき、画像のリサンプリングが発生し、画像サイズや圧縮品質が変わってしまう。そこで今回は、このように画像をコピーし、コピー先の画像の Exif情報 を削除するという方法にした。
画像ファイルのコピーができたら、出力画像ファイル outFname の Exif情報を取得する。ここで使うのは piexif.load 関数で、辞書形式で Exif情報 を返す。ここで気をつけなければならないのは、取得できる辞書は2重構造になっていて、たとえば撮影日時を示すExifタグ DateTime が格納されているのは exifData["0th"]["DateTime"] だが、GPS情報の経度が格納されているのは exifData["GPS"]["GPSLongitude"] である。このように構造がやや複雑なので、"getExif.py" の方では piexif を用いなかった。
画像ファイルに Exif情報が含まれていれば、かならずキー "0th" に値がセットされるので、この内容が空かどうかで Exif情報の有無を判定する。
1番目のキーである3つを ["0th", "Exif", "GPS"] に格納しておき、削除したかどうかの結果を格納する変数 deleteFlag に False を代入する。for文を使って削除キーリスト deleteTAGList にマッチする者を探していく。なお、1番目のキー "1st" はサムネイルなので、本関数では削除しない。後述する全てを削除する関数以外で削除することができます。
Exif情報の削除は delete を使って対象の要素を削除する。削除したら、変数 deleteFlag に True を代入し、コンソールに削除した Exifタグ名を表示する。for文 が終了した時点で deleteFlag が False のままだったら、指定した Exif情報 が存在していなかったという例外を発生する。
削除した後の辞書 exifBytes に対して piexif.dump関数を実行することで、ファイルに書き込むExifデータを作成する。そして、piexif.insert関数を使って出力画像ファイル outFname にExifデータを書き込む。
外部ライブラリ piexif には Exif情報を書き込み更新する piexif.insert関数があるのだが、これはすでに存在している画像ファイルに対して作用する。そこで、入力画像ファイル inFname を出力ファイル outFname にコピーする。これには、標準ライブラリ shutil の shutil.copy関数を使う。もし入力画像ファイル inFname が書き込み禁止属性だと、Exif情報を書き込むことができないので、標準ライブラリ os の os.stat関数を使って書き込み可能属性に変更する。
外部ライブラリ PIL や OpenCV を併用すれば、わざわざ画像ファイルをコピーしなくても、更新後の Exif情報 を出力ファイル outFname に新規保存できるのだが、このとき、画像のリサンプリングが発生し、画像サイズや圧縮品質が変わってしまう。そこで今回は、このように画像をコピーし、コピー先の画像の Exif情報 を削除するという方法にした。
画像ファイルのコピーができたら、出力画像ファイル outFname の Exif情報を取得する。ここで使うのは piexif.load 関数で、辞書形式で Exif情報 を返す。ここで気をつけなければならないのは、取得できる辞書は2重構造になっていて、たとえば撮影日時を示すExifタグ DateTime が格納されているのは exifData["0th"]["DateTime"] だが、GPS情報の経度が格納されているのは exifData["GPS"]["GPSLongitude"] である。このように構造がやや複雑なので、"getExif.py" の方では piexif を用いなかった。
画像ファイルに Exif情報が含まれていれば、かならずキー "0th" に値がセットされるので、この内容が空かどうかで Exif情報の有無を判定する。
1番目のキーである3つを ["0th", "Exif", "GPS"] に格納しておき、削除したかどうかの結果を格納する変数 deleteFlag に False を代入する。for文を使って削除キーリスト deleteTAGList にマッチする者を探していく。なお、1番目のキー "1st" はサムネイルなので、本関数では削除しない。後述する全てを削除する関数以外で削除することができます。
Exif情報の削除は delete を使って対象の要素を削除する。削除したら、変数 deleteFlag に True を代入し、コンソールに削除した Exifタグ名を表示する。for文 が終了した時点で deleteFlag が False のままだったら、指定した Exif情報 が存在していなかったという例外を発生する。
削除した後の辞書 exifBytes に対して piexif.dump関数を実行することで、ファイルに書き込むExifデータを作成する。そして、piexif.insert関数を使って出力画像ファイル outFname にExifデータを書き込む。
解説:すべてのExif情報を削除する
deleteExif.py
def deleteAllExif(inFname, outFname):
"""画像ファイルからすべてのExif情報を削除して別ファイルに保存する.
Args:
inFname: 入力画像ファイル名(フルパス)
outFname: 出力画像ファイル名(フルパス)
Raises:
FileNotFoundError: 入力ファイルが存在しない,または読み込めない
FileExistsFoundError: 出力ファイルが存在しており書き込めない
ValueError: 入力ファイルにExifデータがない
"""
# 入力ファイルを出力ファイルにコピーする
try:
shutil.copy(inFname, outFname)
permissions = os.stat(outFname).st_mode
# 書き込み禁止なら書き込み可能に属性変更する
if not (permissions & stat.S_IWUSR):
newPermissions = permissions | stat.S_IWUSR
os.chmod(outFname, newPermissions)
# 失敗したら例外を返す.
except Exception as e:
os.remove(outFname)
raise Exception(f"{outFname} を作成できません")
# Exif情報を取得する
exifData = piexif.load(outFname)
# Exif情報がある場合
if exifData["0th"]:
# 空のExif情報を書き込む
piexif.remove(outFname)
print(f"delete All Exif Data") # for debug
print(f"save {outFname}") # for debug
# Exif情報がない場合
else:
os.remove(outFname)
raise ValueError(f"{inFname} にはExif情報がありません")
ユーザー関数 deleteAllExif は、上述のユーザー関数 selectDeleteTAGS で「すべて」が選択されたときに実行するもので、引数で指定された入力画像ファイルの全ての Exif情報を削除し、出力画像ファイルに出力する。
処理の前半――入力画像ファイル inFname を出力画像ファイル outFname にコピーし、Exif情報 があるかどうかを判定するところまでは、前述の deleteExif関数 と同じである。
そして、出力画像ファイル outFname のすべての Exif情報を削除する。削除には piexif.remove関数を使う。
処理の前半――入力画像ファイル inFname を出力画像ファイル outFname にコピーし、Exif情報 があるかどうかを判定するところまでは、前述の deleteExif関数 と同じである。
そして、出力画像ファイル outFname のすべての Exif情報を削除する。削除には piexif.remove関数を使う。
解説:piexifで扱うExifタグの構造
piexif で扱う Exifタグの構造を一覧に整理した。
タグ情報の削除だけでなく追加もできるので、サンプル・プログラムを自由に改造してみていただきたい。
タグ情報の削除だけでなく追加もできるので、サンプル・プログラムを自由に改造してみていただきたい。
キー#1 | キー#2 | タグID | データ型 |
---|---|---|---|
0th | 画像基本情報 | ||
ProcessingSoftware | 0x000B | ascii | |
NewSubfileType | 0x00FE | long | |
SubfileType | 0x00FF | short | |
ImageWidth | 0x0100 | long | |
ImageLength | 0x0101 | long | |
BitsPerSample | 0x0102 | short | |
Compression | 0x0103 | short | |
PhotometricInterpretation | 0x0106 | short | |
Threshholding | 0x0107 | short | |
CellWidth | 0x0108 | short | |
CellLength | 0x0109 | short | |
FillOrder | 0x010A | short | |
DocumentName | 0x010D | ascii | |
ImageDescription | 0x010E | ascii | |
Make | 0x010F | ascii | |
Model | 0x0110 | ascii | |
StripOffsets | 0x0111 | long | |
Orientation | 0x0112 | short | |
SamplesPerPixel | 0x0115 | short | |
RowsPerStrip | 0x0116 | long | |
StripByteCounts | 0x0117 | long | |
XResolution | 0x011A | ratinal | |
YResolution | 0x011B | ratinal | |
PlanarConfiguration | 0x011C | short | |
GrayResponseUnit | 0x0122 | short | |
GrayResponseCurve | 0x0123 | short | |
T4Options | 0x0124 | long | |
T6Options | 0x0125 | long | |
ResolutionUnit | 0x0128 | short | |
TransferFunction | 0x012D | short | |
Software | 0x0131 | ascii | |
DateTime | 0x0132 | ascii | |
Artist | 0x013B | ascii | |
HostComputer | 0x013C | ascii | |
Predictor | 0x013D | short | |
WhitePoint | 0x013E | ratinal | |
PrimaryChromaticities | 0x013F | ratinal | |
ColorMap | 0x0140 | short | |
HalftoneHints | 0x0141 | short | |
TileWidth | 0x0142 | short | |
TileLength | 0x0143 | short | |
TileOffsets | 0x0144 | short | |
TileByteCounts | 0x0145 | short | |
SubIFDs | 0x014A | long | |
InkSet | 0x014C | short | |
InkNames | 0x014D | ascii | |
NumberOfInks | 0x014E | short | |
DotRange | 0x0150 | byte | |
TargetPrinter | 0x0151 | ascii | |
ExtraSamples | 0x0152 | short | |
SampleFormat | 0x0153 | short | |
SMinSampleValue | 0x0154 | short | |
SMaxSampleValue | 0x0155 | short | |
TransferRange | 0x0156 | short | |
ClipPath | 0x0157 | byte | |
XClipPathUnits | 0x0158 | long | |
YClipPathUnits | 0x0159 | long | |
Indexed | 0x015A | short | |
JPEGTables | 0x015B | undefined | |
OPIProxy | 0x015F | short | |
JPEGProc | 0x0200 | long | |
JPEGInterchangeFormat | 0x0201 | long | |
JPEGInterchangeFormatLength | 0x0202 | long | |
JPEGRestartInterval | 0x0203 | short | |
JPEGLosslessPredictors | 0x0205 | short | |
JPEGPointTransforms | 0x0206 | short | |
JPEGQTables | 0x0207 | long | |
JPEGDCTables | 0x0208 | long | |
JPEGACTables | 0x0209 | long | |
YCbCrCoefficients | 0x0211 | ratinal | |
YCbCrSubSampling | 0x0212 | short | |
YCbCrPositioning | 0x0213 | short | |
ReferenceBlackWhite | 0x0214 | ratinal | |
XMLPacket | 0x02BC | byte | |
Rating | 0x4746 | short | |
RatingPercent | 0x4749 | short | |
ImageID | 0x800D | ascii | |
CFARepeatPatternDim | 0x828D | short | |
CFAPattern | 0x828E | byte | |
BatteryLevel | 0x828F | ratinal | |
Copyright | 0x8298 | ascii | |
ExposureTime | 0x829A | ratinal | |
ImageResources | 0x8649 | byte | |
ExifTag | 0x8769 | long | |
InterColorProfile | 0x8773 | undefined | |
GPSTag | 0x8825 | long | |
Interlace | 0x8829 | short | |
TimeZoneOffset | 0x882A | long | |
SelfTimerMode | 0x882B | short | |
FlashEnergy | 0x920B | ratinal | |
SpatialFrequencyResponse | 0x920C | undefined | |
Noise | 0x920D | undefined | |
FocalPlaneXResolution | 0x920E | ratinal | |
FocalPlaneYResolution | 0x920F | ratinal | |
FocalPlaneResolutionUnit | 0x9210 | short | |
ImageNumber | 0x9211 | long | |
SecurityClassification | 0x9212 | ascii | |
ImageHistory | 0x9213 | ascii | |
ExposureIndex | 0x9215 | ratinal | |
TIFFEPStandardID | 0x9216 | byte | |
SensingMethod | 0x9217 | short | |
XPTitle | 0x9C9B | byte | |
XPComment | 0x9C9C | byte | |
XPAuthor | 0x9C9D | byte | |
XPKeywords | 0x9C9E | byte | |
XPSubject | 0x9C9F | byte | |
PrintImageMatching | 0xC4A5 | undefined | |
DNGVersion | 0xC612 | byte | |
DNGBackwardVersion | 0xC613 | byte | |
UniqueCameraModel | 0xC614 | ascii | |
LocalizedCameraModel | 0xC615 | byte | |
CFAPlaneColor | 0xC616 | byte | |
CFALayout | 0xC617 | short | |
LinearizationTable | 0xC618 | short | |
BlackLevelRepeatDim | 0xC619 | short | |
BlackLevel | 0xC61A | ratinal | |
BlackLevelDeltaH | 0xC61B | srational | |
BlackLevelDeltaV | 0xC61C | srational | |
WhiteLevel | 0xC61D | short | |
DefaultScale | 0xC61E | ratinal | |
DefaultCropOrigin | 0xC61F | short | |
DefaultCropSize | 0xC620 | short | |
ColorMatrix1 | 0xC621 | srational | |
ColorMatrix2 | 0xC622 | srational | |
CameraCalibration1 | 0xC623 | srational | |
CameraCalibration2 | 0xC624 | srational | |
ReductionMatrix1 | 0xC625 | srational | |
ReductionMatrix2 | 0xC626 | srational | |
AnalogBalance | 0xC627 | ratinal | |
AsShotNeutral | 0xC628 | short | |
AsShotWhiteXY | 0xC629 | ratinal | |
BaselineExposure | 0xC62A | srational | |
BaselineNoise | 0xC62B | ratinal | |
BaselineSharpness | 0xC62C | ratinal | |
BayerGreenSplit | 0xC62D | long | |
LinearResponseLimit | 0xC62E | ratinal | |
CameraSerialNumber | 0xC62F | ascii | |
LensInfo | 0xC630 | ratinal | |
ChromaBlurRadius | 0xC631 | ratinal | |
AntiAliasStrength | 0xC632 | ratinal | |
ShadowScale | 0xC633 | srational | |
DNGPrivateData | 0xC634 | byte | |
MakerNoteSafety | 0xC635 | short | |
CalibrationIlluminant1 | 0xC65A | short | |
CalibrationIlluminant2 | 0xC65B | short | |
BestQualityScale | 0xC65C | ratinal | |
RawDataUniqueID | 0xC65D | byte | |
OriginalRawFileName | 0xC68B | byte | |
OriginalRawFileData | 0xC68C | undefined | |
ActiveArea | 0xC68D | short | |
MaskedAreas | 0xC68E | short | |
AsShotICCProfile | 0xC68F | undefined | |
AsShotPreProfileMatrix | 0xC690 | srational | |
CurrentICCProfile | 0xC691 | undefined | |
CurrentPreProfileMatrix | 0xC692 | srational | |
ColorimetricReference | 0xC6BF | short | |
CameraCalibrationSignature | 0xC6F3 | byte | |
ProfileCalibrationSignature | 0xC6F4 | byte | |
AsShotProfileName | 0xC6F6 | byte | |
NoiseReductionApplied | 0xC6F7 | ratinal | |
ProfileName | 0xC6F8 | byte | |
ProfileHueSatMapDims | 0xC6F9 | long | |
ProfileHueSatMapData1 | 0xC6FA | float | |
ProfileHueSatMapData2 | 0xC6FB | float | |
ProfileToneCurve | 0xC6FC | float | |
ProfileEmbedPolicy | 0xC6FD | long | |
ProfileCopyright | 0xC6FE | byte | |
ForwardMatrix1 | 0xC714 | srational | |
ForwardMatrix2 | 0xC715 | srational | |
PreviewApplicationName | 0xC716 | byte | |
PreviewApplicationVersion | 0xC717 | byte | |
PreviewSettingsName | 0xC718 | byte | |
PreviewSettingsDigest | 0xC719 | byte | |
PreviewColorSpace | 0xC71A | long | |
PreviewDateTime | 0xC71B | ascii | |
RawImageDigest | 0xC71C | undefined | |
OriginalRawFileDigest | 0xC71D | undefined | |
SubTileBlockSize | 0xC71E | long | |
RowInterleaveFactor | 0xC71F | long | |
ProfileLookTableDims | 0xC725 | long | |
ProfileLookTableData | 0xC726 | float | |
OpcodeList1 | 0xC740 | undefined | |
OpcodeList2 | 0xC741 | undefined | |
OpcodeList3 | 0xC74E | undefined | |
ZZZTestSlong1 | 0xECBE | slong | |
ZZZTestSlong2 | 0xECBF | slong | |
ZZZTestSByte | 0xECC0 | sbyte | |
ZZZTestSShort | 0xECC1 | sshort | |
ZZZTestDFloat | 0xECC2 | double | |
Exif | 撮影条件などの詳細情報 | ||
ExposureTime | 0x829A | ratinal | |
FNumber | 0x829D | ratinal | |
ExposureProgram | 0x8822 | short | |
SpectralSensitivity | 0x8824 | ascii | |
ISOSpeedRatings | 0x8827 | short | |
OECF | 0x8828 | undefined | |
SensitivityType | 0x8830 | short | |
StandardOutputSensitivity | 0x8831 | long | |
RecommendedExposureIndex | 0x8832 | long | |
ISOSpeed | 0x8833 | long | |
ISOSpeedLatitudeyyy | 0x8834 | long | |
ISOSpeedLatitudezzz | 0x8835 | long | |
ExifVersion | 0x9000 | undefined | |
DateTimeOriginal | 0x9003 | ascii | |
DateTimeDigitized | 0x9004 | ascii | |
OffsetTime | 0x9010 | ascii | |
OffsetTimeOriginal | 0x9011 | ascii | |
OffsetTimeDigitized | 0x9012 | ascii | |
ComponentsConfiguration | 0x9101 | undefined | |
CompressedBitsPerPixel | 0x9102 | ratinal | |
ShutterSpeedValue | 0x9201 | srational | |
ApertureValue | 0x9202 | ratinal | |
BrightnessValue | 0x9203 | srational | |
ExposureBiasValue | 0x9204 | srational | |
MaxApertureValue | 0x9205 | ratinal | |
SubjectDistance | 0x9206 | ratinal | |
MeteringMode | 0x9207 | short | |
LightSource | 0x9208 | short | |
Flash | 0x9209 | short | |
FocalLength | 0x920A | ratinal | |
SubjectArea | 0x9214 | short | |
MakerNote | 0x927C | undefined | |
UserComment | 0x9286 | undefined | |
SubSecTime | 0x9290 | ascii | |
SubSecTimeOriginal | 0x9291 | ascii | |
SubSecTimeDigitized | 0x9292 | ascii | |
Temperature | 0x9400 | srational | |
Humidity | 0x9401 | ratinal | |
Pressure | 0x9402 | ratinal | |
WaterDepth | 0x9403 | srational | |
Acceleration | 0x9404 | ratinal | |
CameraElevationAngle | 0x9405 | srational | |
FlashpixVersion | 0xA000 | undefined | |
ColorSpace | 0xA001 | short | |
PixelXDimension | 0xA002 | long | |
PixelYDimension | 0xA003 | long | |
RelatedSoundFile | 0xA004 | ascii | |
InteroperabilityTag | 0xA005 | long | |
FlashEnergy | 0xA20B | ratinal | |
SpatialFrequencyResponse | 0xA20C | undefined | |
FocalPlaneXResolution | 0xA20E | ratinal | |
FocalPlaneYResolution | 0xA20F | ratinal | |
FocalPlaneResolutionUnit | 0xA210 | short | |
SubjectLocation | 0xA214 | short | |
ExposureIndex | 0xA215 | ratinal | |
SensingMethod | 0xA217 | short | |
FileSource | 0xA300 | undefined | |
SceneType | 0xA301 | undefined | |
CFAPattern | 0xA302 | undefined | |
CustomRendered | 0xA401 | short | |
ExposureMode | 0xA402 | short | |
WhiteBalance | 0xA403 | short | |
DigitalZoomRatio | 0xA404 | ratinal | |
FocalLengthIn35mmFilm | 0xA405 | short | |
SceneCaptureType | 0xA406 | short | |
GainControl | 0xA407 | short | |
Contrast | 0xA408 | short | |
Saturation | 0xA409 | short | |
Sharpness | 0xA40A | short | |
DeviceSettingDescription | 0xA40B | undefined | |
SubjectDistanceRange | 0xA40C | short | |
ImageUniqueID | 0xA420 | ascii | |
CameraOwnerName | 0xA430 | ascii | |
BodySerialNumber | 0xA431 | ascii | |
LensSpecification | 0xA432 | ratinal | |
LensMake | 0xA433 | ascii | |
LensModel | 0xA434 | ascii | |
LensSerialNumber | 0xA435 | ascii | |
Gamma | 0xA500 | ratinal | |
GPS | 位置情報 | ||
GPSVersionID | 0x0000 | byte | |
GPSLatitudeRef | 0x0001 | ascii | |
GPSLatitude | 0x0002 | ratinal | |
GPSLongitudeRef | 0x0003 | ascii | |
GPSLongitude | 0x0004 | ratinal | |
GPSAltitudeRef | 0x0005 | byte | |
GPSAltitude | 0x0006 | ratinal | |
GPSTimeStamp | 0x0007 | ratinal | |
GPSSatellites | 0x0008 | ascii | |
GPSStatus | 0x0009 | ascii | |
GPSMeasureMode | 0x000A | ascii | |
GPSDOP | 0x000B | ratinal | |
GPSSpeedRef | 0x000C | ascii | |
GPSSpeed | 0x000D | ratinal | |
GPSTrackRef | 0x000E | ascii | |
GPSTrack | 0x000F | ratinal | |
GPSImgDirectionRef | 0x0010 | ascii | |
GPSImgDirection | 0x0011 | ratinal | |
GPSMapDatum | 0x0012 | ascii | |
GPSDestLatitudeRef | 0x0013 | ascii | |
GPSDestLatitude | 0x0014 | ratinal | |
GPSDestLongitudeRef | 0x0015 | ascii | |
GPSDestLongitude | 0x0016 | ratinal | |
GPSDestBearingRef | 0x0017 | ascii | |
GPSDestBearing | 0x0018 | ratinal | |
GPSDestDistanceRef | 0x0019 | ascii | |
GPSDestDistance | 0x001A | ratinal | |
GPSProcessingMethod | 0x001B | undefined | |
GPSAreaInformation | 0x001C | undefined | |
GPSDateStamp | 0x001D | ascii | |
GPSDifferential | 0x001E | short | |
GPSHPositioningError | 0x001F | ratinal | |
1st | サムネイル画像情報 |
練習問題
次回予告
次のコラムで、画像のExif情報からプライバシーが漏れる懸念があることを紹介する。だが、スナップ写真からプライバシーが漏れる最大の懸念は、顔だろう。
Python の外部ライブラリ OpenCV には顔検出機能がある。次回は、これを使って、写真の顔を隠したり顔にスタンプを貼るプログラムを作ってみる。
Python の外部ライブラリ OpenCV には顔検出機能がある。次回は、これを使って、写真の顔を隠したり顔にスタンプを貼るプログラムを作ってみる。
コラム:富士フイルムのデジカメ
1996年(平成8年)7月に富士フイルムのデジカメ「DS-7」を購入した。本編で紹介した Exif だが、開発元は富士フイルムで、この DS-7 で撮影したJPEG画像にも Exif が記録される。ただし、レンズは単焦点だし、絞りは2段しかないし、位置情報も取得しようがない時代の代物である。Exif のバージョンも1.0だった。
Exif は JEITA(電子情報技術産業協会)で規格化され、最新バージョンは2019年(平成31年)5月に改訂された2.32である。CIPA(一般社団法人カメラ映像機器工業会)のサイトでドラフト版を読むことができる。本編で紹介したプログラムは、このドラフト版を参考に作成している。
位置情報は Exif バージョン2.0(1997年11月)で追加になった。
本文で紹介した DateTimeタグは、長らくローカル時間しか記録できなかったが、バージョン2.31(2016年7月)になり、ようやく、UTCとの時差を記録する OffsetTimeタグが追加になった。今回プログラムに実装したところ、2023年(令和5年)5月に購入したミラーレス一眼レフ「EOS R10」では OffsetTimeタグが記録されていることを確認できた。
枯れた技術である Exif だが、地道に改良が続けられているようだ。
位置情報は Exif バージョン2.0(1997年11月)で追加になった。
本文で紹介した DateTimeタグは、長らくローカル時間しか記録できなかったが、バージョン2.31(2016年7月)になり、ようやく、UTCとの時差を記録する OffsetTimeタグが追加になった。今回プログラムに実装したところ、2023年(令和5年)5月に購入したミラーレス一眼レフ「EOS R10」では OffsetTimeタグが記録されていることを確認できた。
枯れた技術である Exif だが、地道に改良が続けられているようだ。
コラム:Exif情報からプライバシーが漏れる
「7.1 セキュリティ対策の基本」で学んだ「セキュリティ情報=自分の(組織の)資産」とは意味合いが異なる場合があるのだが、自分のプライバシーも守りたいことがある。
本編で述べたように、Exif情報には撮影日時、位置情報、カメラの製造番号などが含まれているので、そのままネットにアップすると、「誰が」までは特定できないが、「いつ」「どこで」「どのカメラを使って」撮影した写真か分かってしまう。少なくとも投稿アカウントの行動履歴を把握することができる。
本編で述べたように、Exif情報には撮影日時、位置情報、カメラの製造番号などが含まれているので、そのままネットにアップすると、「誰が」までは特定できないが、「いつ」「どこで」「どのカメラを使って」撮影した写真か分かってしまう。少なくとも投稿アカウントの行動履歴を把握することができる。
そこで、Twitter(現・X)やInstagramなどのメジャーなSNSでは、投稿した画像から位置情報などを削除して公開する仕様になっている。
それ以外の媒体へ写真を投稿するときには、本編で作ったプログラムや、Exif情報を削除するアプリを使った方がいいだろう。
また、Exif情報を削除したからと言って安心はできない。「SNSにアップした写真から住所が漏れる」で紹介しているように、Googleレンズなどを使い、写真に写っている特徴的な事物から撮影場所を特定される場合がある。時刻を含めて特定されると、自宅を留守にしている時間帯を推測され、最悪、空き巣被害に繋がる。こうなるとセキュリティ対策として考えなければならない。
写真1つをとっても、ネットに投稿するときには十分留意したいものだ。
それ以外の媒体へ写真を投稿するときには、本編で作ったプログラムや、Exif情報を削除するアプリを使った方がいいだろう。
また、Exif情報を削除したからと言って安心はできない。「SNSにアップした写真から住所が漏れる」で紹介しているように、Googleレンズなどを使い、写真に写っている特徴的な事物から撮影場所を特定される場合がある。時刻を含めて特定されると、自宅を留守にしている時間帯を推測され、最悪、空き巣被害に繋がる。こうなるとセキュリティ対策として考えなければならない。
写真1つをとっても、ネットに投稿するときには十分留意したいものだ。
(この項おわり)