出力画像が回転してしまう現象
画像を扱う開発作業などを行っていると画像形式の変換が必要になる場面が時々ある。
例えば、WEBページに画像を載せる時にJPEG画像をそのまま表示するのではなく、サイズ圧縮されたWebPに変換して表示することで読み込み時間を短縮したい場合などである。
こういう時に、Pythonで画像を扱うライブラリとして代表的なPILを使えば下記のようなコードで画像変換を実現できる。
convert_jpeg_to_webp.py
import sys
from PIL import Image
def convert_main(jpeg_path):
# 画像を読み込む
image = Image.open(jpeg_path)
# 入力ファイル名に基づき出力ファイル名を生成する
webp_path = jpeg_path.split('.')[0] + '.webp'
# WebP形式で保存する
image.save(webp_path, 'webp')
print(f"{jpeg_path} から {webp_path} への変換が成功しました")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("変換対象のjpegファイル名を1つだけ指定してください")
else:
jpeg_path = sys.argv[1]
if jpeg_path.lower().endswith(('.jpeg', '.jpg')):
convert_main(jpeg_path)
else:
print("指定可能なファイル形式はjpeg, jpgのみです")
ただ変換するだけであれば最低限こんな感じで良い。
しかし、元画像によっては変換後に意図せず回転した状態になってしまうことがある。
例えば、このsample.jpgを上記のコードで変換してみる。
python convert_jpeg_to_webp.py sample.jpg
すると、このように回転した状態のWebP画像になってしまう。
このように変換後に画像が回転してしまうのは、JPEG画像が持つExif情報のOrientation
という属性が関係してくる。
Exif情報のOrientation属性について
ExifのOrientation
は、画像の回転や鏡像反転の向きを指定する属性であり、デジカメやスマホで写真を撮影した時などに付加されることがある。
Orientation
については下記の記事がわかりやすい。
Orientation
は8種類あり、それぞれの番号と対応する文字列と意味は下記のようになる。
番号 | 文字列 | 意味 |
---|
1 | Horizontal (normal) | オリジナルのまま |
2 | Mirror horizontal | 左右反転 |
3 | Rotate 180 | 180°回転 |
4 | Mirror vertical | 上下反転 |
5 | Mirror horizontal and rotate 270 CW | 左右反転+時計回り270°回転 |
6 | Rotate 90 CW | 時計回り90°回転 |
7 | Mirror horizontal and rotate 90 CW | 左右反転+時計回り90°回転 |
8 | Rotate 270 CW | 時計回り270°回転 |
ExifはJPEG画像が持つ情報であり、他の形式に変換するとそれが失われる。
実は上のsample.jpgは元々Orientation
に6が設定されていて、それが変換処理によって失われてしまったことにより回転したように見えていた。
見た目は回転していないように見えるsample.jpgだが、それ自体がOrientation
の付加によって回転された後のものであり、WebP変換でExif情報が削除されて本来の向きに戻ったと言うのが正確かもしれない。
Orientationを考慮して調整する
この問題を解消する方法として、元のJPEG画像に含まれるExifのOrientation
の値に応じてあらかじめ元画像を回転・反転させておくやり方がある。
回転の場合はImage.rotate()
、反転の場合はImageOps.mirror()
やImageOps.flip()
を使う。
コードでは鏡像反転を含む全てのOrientation
に対する調整をしているが、特に回転だけ修正すればいいというのであればOrientation
の3, 6, 8だけを対象にすれば良い。
なお、Orientation
での回転方向(時計回りが正)とrotate()
での回転方向(反時計回りが正)が逆になることと、回転と反転の両方が絡む5と7は変換の順番を取り違えると結果も変わってしまうことに注意が必要。
修正後のコードでは、これらの変換操作を行うcorrect_orientation()
という関数を追加し、WebP形式で保存する前に元画像のオブジェクトに適用している。
convert_jpeg_to_webp.py
import sys
from PIL import Image, ExifTags, ImageOps
def correct_orientation(image):
# ExifタグからOrientationのタグ番号を取得
for orientation in ExifTags.TAGS.keys():
if ExifTags.TAGS[orientation] == 'Orientation':
break
# Exifデータを取得
exif = image.getexif()
# Orientationタグが存在する場合、その値に応じて画像の向きを修正
if exif is not None and orientation in exif:
ori_num = exif[orientation]
if ori_num == 2:
# 左右反転
image = ImageOps.mirror(image)
elif ori_num == 3:
# 180°回転
image = image.rotate(180, expand=True)
elif ori_num == 4:
# 上下反転
image = ImageOps.flip(image)
elif ori_num == 5:
# 左右反転 → 時計回り270°回転 = 反時計回り90°回転 → 上下反転
image = ImageOps.flip(image.rotate(90, expand=True))
elif ori_num == 6:
# 時計回り90°回転 = 反時計回り270°回転
image = image.rotate(270, expand=True)
elif ori_num == 7:
# 左右反転 → 時計回り90°回転 = 反時計回り270°回転 → 上下反転
image = ImageOps.flip(image.rotate(270, expand=True))
elif ori_num == 8:
# 時計回り270° = 反時計回り90°回転
image = image.rotate(90, expand=True)
return image
def convert_main(jpeg_path):
# 画像を読み込む
image = Image.open(jpeg_path)
# Exifデータに基づき画像の向きを修正
image = correct_orientation(image)
# 入力ファイル名に基づき出力ファイル名を生成する
webp_path = jpeg_path.split('.')[0] + '.webp'
# WebP形式で保存する
image.save(webp_path, 'webp')
print(f"{jpeg_path} から {webp_path} への変換が成功しました")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("変換対象のjpegファイル名を1つだけ指定してください")
else:
jpeg_path = sys.argv[1]
if jpeg_path.lower().endswith(('.jpeg', '.jpg')):
convert_main(jpeg_path)
else:
print("指定可能なファイル形式はjpeg, jpgのみです")
これにより、全てのOrientation
に対応した画像形式の変換を行うことができ、出力画像が回転してしまう問題が解消される。
補足
画像のExif情報の確認や設定変更を簡単に行うことができるツールとしてExifToolというものがる。
これをインストールすることでコマンド操作により画像の各種情報を確認することができる。
sample.jpgのOrientation
を確認、変更したい場合は下記のコマンドになる。
▫️orientationの値を確認(-で対象の属性を指定しない場合は全ての属性値が表示される)
exiftool -orientation sample.jpg
▫️orientationを変更(-nオプションを付けて番号で指定する場合)
exiftool -orientation=3 -n sample.jpg
▫️orientationを変更(文字列で指定する場合)
exiftool -orientation="Rotate 180" sample.jpg
Exifタグの指定
ExifTags
にはこのように「タグ番号: タグ名称」の形で各属性の組が定義されている。
今回のようにOrientation
が必要なのであれば、直接タグ番号0x0112
または274
を指定することもできるが、将来ExifTags
の仕様が変更される可能性も考えると、上記のようにその都度タグ番号を参照するやり方が推奨される。
ExifTags.py
TAGS = {
# possibly incomplete
0x0001: "InteropIndex",
0x000B: "ProcessingSoftware",
0x00FE: "NewSubfileType",
0x00FF: "SubfileType",
0x0100: "ImageWidth",
0x0101: "ImageLength",
# 省略
0x0112: "Orientation",
# 省略
}