3
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?

スマホの縦写真がAIには横倒しに見えていた話(EXIFのOrientationを反映してから渡す)

3
Last updated at Posted at 2026-10-28

はじめに

普通に立って撮った全身写真が、「脚が検出できません」「座っている可能性があります」で弾かれる。テスト環境で社内の検証が始まった初日に、そういう事態がおきました。

弊社で開発した LINE 上の写真キャンペーンでの話です。アップロードされた全身写真を、サーバ側で姿勢推定モデルにかけて「全身が写っているか」「立っているか」をチェックしています。

原因は写真の中身ではなく、JPEG の EXIF に入っている Orientation(向き)でした。アップロード前にブラウザで Orientation どおりに回転して保存し直し、失敗したときのためにサーバ側にも同じ処理を入れて直しました。

何が起きていたか

iPhone を縦に構えて撮った写真は、ピクセルは横向きのまま保存され、EXIF の Orientation に「表示するときは時計回りに90度回す」(値 6)と書かれます。

from PIL import Image

img = Image.open("rejected.jpg")
print(img.size, img.getexif().get(0x0112))  # (4032, 3024) 6

ブラウザやスマホの写真アプリはこの値を見て回してから表示するので、人の目には普通の縦写真に見えます。ところが姿勢推定モデルにはピクセルをそのまま渡していたので、モデルからは人が横に寝ているように見えていました。

公開後の約3週間(10/8〜10/27)の本番で、ブラウザ側の記録が残っている2,583枚を数えると、次のとおりでした。

写真の状態 枚数
EXIF あり・Orientation=6 730
EXIF あり・Orientation=8 31
EXIF あり・Orientation=2〜5(反転・180度) 10
EXIF あり・Orientation=1(回転不要) 1,490
EXIF なし(スクリーンショットやアプリで書き出した画像など) 320
ブラウザでの変換に失敗(後述) 2

回転が必要な写真は771枚、約3割です。

ブラウザで回転してからアップロードする

サーバ側では姿勢推定・背景除去・合成と、いくつもの処理が同じ画像を読みます。処理ごとに回転させると回し忘れや向きのずれが起きるので、アップロード前にブラウザで回転を済ませることにしました。

ブラウザには画像を回転して保存する機能がないので、canvas に描いて JPEG に書き出します。createImageBitmap に imageOrientation: 'from-image' を渡すと、向きを反映した状態でデコードされます。

// EXIF の向きを反映した状態でデコードする
const bitmap = await createImageBitmap(file, { imageOrientation: 'from-image' })

const canvas = document.createElement('canvas')
canvas.width = bitmap.width
canvas.height = bitmap.height
canvas.getContext('2d')?.drawImage(bitmap, 0, 0)

// canvas に描いた時点で正しい向きになっている。書き出した JPEG に EXIF は付かない
const blob = await new Promise<Blob | null>((resolve) =>
  canvas.toBlob(resolve, 'image/jpeg', 0.92),
)

書き出した JPEG には EXIF が付かないので、サーバ側で二重に回転されることはありません。位置情報も一緒に消えます。

引っかかったところ

Orientation の値ではなく、EXIF の有無で判定する

最初は「Orientation が 1 以外なら回転する」条件でした。しかし自前の EXIF の読み取りが値を取りこぼすと、横倒しのまま通ってしまいます。そこで、EXIF を持つ JPEG は値に関係なく必ず canvas を通すように変えました(JPEG 先頭の APP1 セグメントが Exif\0\0 で始まるかで見ています)。回転のいらない写真も再エンコードされますが、品質0.92・解像度そのままなので見た目は変わりません。

古い Android の WebView では from-image が TypeError になる

更新が止まった Android System WebView では、imageOrientation: 'from-image' が TypeError になります。最初はこの例外を握りつぶして元の画像を送っていたので、iOS では通る写真が Android では失敗するという事態がおきました。

今は、オプションなしの createImageBitmap と、<img> に読み込んで canvas に描く方法を順に試します。古いブラウザではどちらも向きを無視することがあるので、回転が必要な写真のときは、今のブラウザなら向きを反映する <img> を先に使います。本番で1つ目の方法が使えなかったのは25枚(約1%)で、そのうち回転が必要だった3枚は <img> で正しい向きになりました。

サーバ側でも回転する

ブラウザでの変換に失敗したときは、元の画像をそのまま送ります(止めると、ユーザーが先に進めなくなるため)。公開前の検証では、ある Android 端末の縦写真が7枚中5枚、横倒しのまま届いて弾かれました。

そこで検閲の前に、サーバ側でも Orientation を確認しています。検閲をまとめる Lambda は画像処理ライブラリを持たないので、S3 から先頭128KBだけを Range 指定で読んで Orientation を取り出し、回転が必要なときだけ Pillow を入れた別の Lambda で回転して、同じキーに上書きします。

import io

import boto3
from PIL import Image, ImageOps

s3 = boto3.client("s3")

# Orientation の確認は先頭だけ読めば足りる(EXIF は先頭の APP1 に入っている)
head = s3.get_object(Bucket=bucket, Key=key, Range="bytes=0-131071")["Body"].read()

# 回転が必要なときだけ、全体を読んで回転する
img = Image.open(io.BytesIO(body))
fixed = ImageOps.exif_transpose(img)
out = io.BytesIO()
# EXIF は落とす(位置情報も残さない)。色の見え方を変えないよう ICC プロファイルだけ引き継ぐ
icc = img.info.get("icc_profile")
fixed.convert("RGB").save(out, format="JPEG", quality=92, **({"icc_profile": icc} if icc else {}))
s3.put_object(Bucket=bucket, Key=key, Body=out.getvalue(), ContentType="image/jpeg")

回転に失敗しても検閲は止めず、ログを残すようにしています。

公開後の約3週間で、ブラウザでの変換に失敗したのは2枚でした。どちらも iPhone(iOS 17.6.1・LINE アプリ内ブラウザ)の 5712×4284 の写真で、canvas.toBlob が null を返していました(canvas のサイズ上限に当たったと見ていますが、未確認です)。2枚ともサーバ側で回転され、横倒しで弾かれずに済んでいます。

おわりに

写真を開いてもスクリーンショットを送ってもらっても正しい向きで表示されるので、見た目からは原因に気づけませんでした。「普通の写真なのに弾かれる」と言われたら、判定の基準をいじる前に、画像の幅・高さと Orientation を見てみてください。

アップロードされた写真を EXIF の向きどおりに回転してから処理するのは、よく使われるやり方です(下の参考文献を参照)。

参考文献

3
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
3
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?