商品写真の位置情報を消したら、写真が横倒しになった。合成した1枚で試した

magick src/sample.jpg -strip strip/sample.jpg。この1行で、GPSタグは消えました。同時に、写真の向きも消えました。
本来は横長に見えるはずの写真が、90度倒れて出てきます。座標を消す作業が、写真を壊したわけです。
きっかけは単純でした。デジタル商品の素材に写真を使うときも、画像入力のできるAIに商品写真を渡すときも、私は渡す前にEXIFを開く習慣がありません。スマホは設定によって、撮影位置を写真に書き込みます。書き込まれていたら、そのまま誰かの手元に届きます。
今回は手元の実写真を使わず、プログラムで描いた合成画像だけで試しました。座標は北緯35度、東経135度の合成値です。AIツールも呼んでいません。見本1枚で、消し方ごとに何が残り、何が壊れるかを見た記録です。
10月2日に書いた商品カバーのWebP変換では、変換すると署名の消える組み合わせもありました。今回は反対に、変換しても座標の残る場面を見ます。
縦長で保存した、横長の写真
見本のsample.jpgは、保存上は幅400、高さ600の縦長です。Orientationに6を入れてあります。これは「右へ90度回して表示する」という指示で、見る側には横長の600×400として映ります。カメラは持ち方に応じて画素を回さず、この値だけを書き込むことがあります。
入っている情報は、メーカー名、機種名、Orientation、GPSタグの緯度経度の4つです。
| 用語 | この回での意味 |
|---|---|
| EXIF | 写真ファイルに入る撮影情報。機種名、向き、撮影位置など |
| GPSタグ | EXIFの中で撮影位置を持つ区画。緯度と経度が入る |
| Orientation | 写真を何度回して見せるかを表す値。6は右に90度 |
| XMP | XMLで書くメタデータ。EXIFとは別の場所に座標を持てる |
-strip | ImageMagickで、プロファイルやコメントを落とすオプション |
-auto-orient | Orientationの指示どおりに画素を回し、指示を初期値へ戻すオプション |
環境は次のとおりです。
mkdir -p "$TMPDIR/photo-meta" && cd "$TMPDIR/photo-meta"
python3 --version
python3 -c "import PIL; print(PIL.__version__)"
magick -version | head -1
Python 3.13.5
11.1.0
Version: ImageMagick 7.1.1-43 Q16 aarch64 22550 https://imagemagick.org
使うスクリプトは5本です。全文は折りたたんであります。
make_sample.py(見本の作成)
from pathlib import Path
from PIL import Image, ImageDraw
# 保存上は縦長の400x600。Orientation=6 により、表示では横長の600x400になる
img = Image.new("RGB", (400, 600), (240, 240, 235))
d = ImageDraw.Draw(img)
d.rectangle([20, 20, 380, 120], fill=(200, 60, 60))
d.ellipse([150, 300, 250, 400], fill=(40, 90, 200))
exif = Image.Exif()
exif[0x010F] = "SampleMaker" # Make
exif[0x0110] = "SampleCam 1" # Model
exif[0x0112] = 6 # Orientation
gps = exif.get_ifd(0x8825) # GPS IFD
gps[1], gps[2] = "N", (35.0, 0.0, 0.0)
gps[3], gps[4] = "E", (135.0, 0.0, 0.0)
exif[0x8825] = gps
Path("src").mkdir(exist_ok=True)
img.save("src/sample.jpg", "JPEG", quality=90, exif=exif)
check_gps.py(座標の検査。XMPの確認を足した現在の版)
import sys
from pathlib import Path
from PIL import Image
EXTS = {".jpg", ".jpeg", ".png", ".webp"}
folder = Path(sys.argv[1])
files = sorted(p for p in folder.iterdir() if p.suffix.lower() in EXTS)
if not files:
print(f"NG: {folder} に画像が0枚です")
sys.exit(1)
bad = 0
for p in files:
try:
exif_gps = Image.open(p).getexif().get_ifd(0x8825)
except Exception as e:
print(f"NG {p.name}: 読めません ({type(e).__name__})")
bad += 1
continue
raw = p.read_bytes()
xmp_gps = b"GPSLatitude" in raw or b"GPSLongitude" in raw
if exif_gps:
print(f"NG {p.name}: EXIFにGPSタグ {sorted(exif_gps)} が残っています")
bad += 1
elif xmp_gps:
print(f"NG {p.name}: XMPに座標の項目が残っています")
bad += 1
else:
print(f"OK {p.name}")
sys.exit(1 if bad else 0)
compare_view.py(表示の向きの検査)
import sys
from PIL import Image, ImageChops, ImageOps, ImageStat
def shown(path):
# 表示側が Orientation を反映した姿に直してから比べる
return ImageOps.exif_transpose(Image.open(path)).convert("RGB")
a, b = shown(sys.argv[1]), shown(sys.argv[2])
if a.size != b.size:
print(f"NG 表示サイズが違います {a.size} -> {b.size}")
sys.exit(1)
mean = sum(ImageStat.Stat(ImageChops.difference(a, b)).mean) / 3
ok = mean <= 8
print(f"{'OK' if ok else 'NG'} 表示サイズ={b.size} 平均差={mean:.2f}")
sys.exit(0 if ok else 1)
pil_plain.py と make_xmp_sample.py
# pil_plain.py
from pathlib import Path
from PIL import Image
Path("pil").mkdir(exist_ok=True)
Image.open("src/sample.jpg").save("pil/sample.jpg", quality=90)
# make_xmp_sample.py
from pathlib import Path
from PIL import Image
xmp = b"""<?xpacket begin='\xef\xbb\xbf' id='W5M0MpCehiHzreSzNTczkc9d'?>
<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description xmlns:exif="http://ns.adobe.com/exif/1.0/"
exif:GPSLatitude="35,0.0000N" exif:GPSLongitude="135,0.0000E"/>
</rdf:RDF></x:xmpmeta>
<?xpacket end='w'?>"""
im = Image.open("src/sample.jpg")
exif = im.getexif()
exif[0x8825] = {} # EXIF側のGPSは空にして、座標をXMPだけに残す
Path("xmp_src").mkdir(exist_ok=True)
im.save("xmp_src/xmp_only.jpg", "JPEG", quality=90, exif=exif, xmp=xmp)
まず見本を作り、座標の検査にかけます。
python3 make_sample.py
python3 check_gps.py src
echo "exit=$?"
NG sample.jpg: EXIFにGPSタグ [1, 2, 3, 4] が残っています
exit=1
タグ1から4は、緯度の南北、緯度、経度の東西、経度です。入れたとおりに見つかりました。
-stripだけで済ませた版
最短で思いつく消し方から試します。
mkdir -p strip orient webp_plain webp_clean
magick src/sample.jpg -strip strip/sample.jpg
python3 check_gps.py strip
echo "exit=$?"
OK sample.jpg
exit=0
座標は消えました。検査も通っています。ここで終えていたら、私は出荷用のフォルダに入れていたと思います。
ただ、check_gps.pyは座標しか見ていません。向きが変わっても、何も言いません。そこで、Orientationを反映した姿に直してから、元の画像と見比べるcompare_view.pyを足しました。
python3 compare_view.py src/sample.jpg strip/sample.jpg
echo "exit=$?"
NG 表示サイズが違います (600, 400) -> (400, 600)
exit=1
元は表示すると600×400です。-stripの後は400×600のままで、回転の指示がありません。つまり、写真は倒れています。
Pillowで開いて何も指定せずに保存し直しても、同じ結果でした。EXIFを引き継がないので、座標も向きも消えます。
python3 pil_plain.py
python3 check_gps.py pil
python3 compare_view.py src/sample.jpg pil/sample.jpg
OK sample.jpg
NG 表示サイズが違います (600, 400) -> (400, 600)
向きを先に確定してから消す
直し方は、画素を回してから消すことでした。-auto-orientで、Orientationの指示を画素に反映します。そのあとに-stripを付けます。
magick src/sample.jpg -auto-orient -strip orient/sample.jpg
python3 check_gps.py orient
python3 compare_view.py src/sample.jpg orient/sample.jpg
OK sample.jpg
OK 表示サイズ=(600, 400) 平均差=0.23
ImageMagickの公式ドキュメントでは、-auto-orientはEXIFのOrientationを読んでリセットする演算子です。-stripはプロファイルやコメントを落とします。
順番が大事だと予想して、-strip -auto-orientの逆順も試しました。
mkdir -p rev
magick src/sample.jpg -strip -auto-orient rev/sample.jpg
python3 check_gps.py rev
python3 compare_view.py src/sample.jpg rev/sample.jpg
OK sample.jpg
OK 表示サイズ=(600, 400) 平均差=0.23
予想は外れました。この版のImageMagickでは、逆順でも同じ結果です。読み手が迷わないよう、本文では-auto-orient -stripの順で書きます。
WebPにしても座標は消えない
次は、サイトに載せる軽い画像を想定して、WebPへ変換します。何も付けない版と、-auto-orient -stripを付けた版を並べました。
magick src/sample.jpg webp_plain/sample.webp
python3 check_gps.py webp_plain
python3 compare_view.py src/sample.jpg webp_plain/sample.webp
NG sample.webp: EXIFにGPSタグ [1, 2, 3, 4] が残っています
OK 表示サイズ=(600, 400) 平均差=0.75
変換しただけでは座標が残ります。しかも、向きは正しく保たれているので、見た目に違和感がありません。気づく手がかりがないまま公開できてしまう形です。
magick src/sample.jpg -auto-orient -strip webp_clean/sample.webp
python3 check_gps.py webp_clean
python3 compare_view.py src/sample.jpg webp_clean/sample.webp
OK sample.webp
OK 表示サイズ=(600, 400) 平均差=0.76
検査が通ったのに、座標が残っていた
最初のcheck_gps.pyは、EXIFのGPSタグだけを見る版でした。上に載せた現在の版から、rawとXMPの判定を除いたものです。ここではcheck_gps_v1.pyという名前で別に置きました。
座標をXMPにだけ入れたJPEGを作って、この初版にかけます。
python3 make_xmp_sample.py
python3 check_gps_v1.py xmp_src
echo "exit=$?"
OK xmp_only.jpg
exit=0
座標が入っているファイルに、OKが出ました。EXIFのGPSタグは空にしてあるので、初版の目には何も映らないのです。
そこでファイルの生のバイト列から、GPSLatitudeとGPSLongitudeの文字列を探す判定を足しました。現在の版で、同じファイルを検査します。
python3 check_gps.py xmp_src
mkdir -p xmp_out
magick xmp_src/xmp_only.jpg -auto-orient -strip xmp_out/xmp_only.jpg
python3 check_gps.py xmp_out
NG xmp_only.jpg: XMPに座標の項目が残っています
OK xmp_only.jpg
-stripは、XMPの座標も落としました。ただし、文字列の検索はかなり粗い検査です。圧縮された形でXMPを持つ画像は、見つけられないかもしれません。この検査が言えるのは「見つけたら止める」までで、「無いと証明する」ことはできません。
0枚と壊れたファイルは通さない
入力の穴も見ておきます。空のフォルダと、画像ではないファイルを.jpgにしたフォルダです。
mkdir -p empty broken
echo "not an image" > broken/fake.jpg
python3 check_gps.py empty; echo "exit=$?"
python3 check_gps.py broken; echo "exit=$?"
NG: empty に画像が0枚です
exit=1
NG fake.jpg: 読めません (UnidentifiedImageError)
exit=1
0枚で通ってしまうと、フォルダの指定を間違えたときに「問題なし」と読んでしまいます。ここは先に塞ぎました。
結果の一覧
| 方法 | 座標 | 表示の向き |
|---|---|---|
magick -strip(JPEG) | 消えた | 倒れた |
| Pillowで指定なしに保存 | 消えた | 倒れた |
magick -auto-orient -strip(JPEG) | 消えた | 保てた |
magickでWebPに変換のみ | 残った | 保てた |
magick -auto-orient -strip(WebP) | 消えた | 保てた |
XMPにだけ座標があるJPEGへ-auto-orient -strip | 消えた | 未検査 |
まだ納得していないこと
平均差が8以下ならOKとした基準は、私の勘です。実測は0.23から0.76でしたが、別の画像でも収まるとは言い切れません。正方形の写真だと、倒れても画素数が変わらないので、サイズ比較は素通りします。その場合に頼れるのは平均差だけです。
実機のスマホ写真も見ていません。機種ごとのMakerNoteや、EXIFに入る縮小画像が残る場合の挙動は、今回の見本には入っていません。座標を消した後に、縮小画像の中に元の写真が残るのか。確かめていないので、ここでは言い切れません。
個人的には、スマホ側で撮影時の位置情報の記録を切るほうが確実だと思っています。ただ、すでに撮り溜めた写真の束には効きません。出荷前にフォルダごと検査する今回の形は、その穴を埋める道具です。


