Ccmmutty logo
Commutty IT
1
0 pv8 min read

画像変換処理で出力画像が回転してしまう問題の解消

https://cdn.magicode.io/media/notebox/f3a90de3-b8be-4724-8179-0312584fb9a5.jpeg

出力画像が回転してしまう現象

画像を扱う開発作業などを行っていると画像形式の変換が必要になる場面が時々ある。
例えば、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種類あり、それぞれの番号と対応する文字列と意味は下記のようになる。
番号文字列意味
1Horizontal (normal)オリジナルのまま
2Mirror horizontal左右反転
3Rotate 180180°回転
4Mirror vertical上下反転
5Mirror horizontal and rotate 270 CW左右反転+時計回り270°回転
6Rotate 90 CW時計回り90°回転
7Mirror horizontal and rotate 90 CW左右反転+時計回り90°回転
8Rotate 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に対応した画像形式の変換を行うことができ、出力画像が回転してしまう問題が解消される。

補足

ExifToolでの属性の確認や変更

画像の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",
# 省略
}

Discussion

コメントにはログインが必要です。