久しぶりにiPhoneを買い替えて、写真を撮ってみたところファイルフォーマットがheicなるものになっていました。扱うのにあまりに不便だったので、python使ってjpegへの変換スクリプトを書いたところ、いろいろ罠に引っかかったのでメモとして残しておきます。世の中にある単純な変換だと少し痛い目見ます。
exifデータと埋め込みicc profileもjpegにちゃんと記録します。(そうしないと色が変)
HEIC
High Efficiency Image File FormatでWikipediaに記載があります。Appleエコシステムでメジャーな画像フォーマットなんですね。知らなかったけど。
なんでもJPEGの倍の圧縮効率だとか。ただ諸々扱いにくいので潔くJPEGへコンバートします。
pillow_heif
Pythonでheicを扱うあればひとまず
これを入れてやりましょう。これでpillow互換のデータを取り出すことができます。
python3 -m pip install pillow-heif
あとjpegでsaveするためにpillow(PIL)も入れておきましょう。
from PIL import Image from pillow_heif import HeifImagePlugin def heiftojpeg(src, dst): im = Image.open(src) im.save(dst, "JPEG", quality=95) heiftojpeg('src.heic', 'dst.jpeg')
これでクラシックな保存は可能です。めでたしめでたし。HeifImagePluginってのが勝手にPILに飲み込まれるようです。仕組みはよくわかりません。
ココに記載がありますので、詳しくはこちらを参照してください。
ただ…。色がおかしいし、exif情報も落ちています。これではいまいちなので、まずはexif情報の保持から
exif
これも言わずもがな、撮影情報のメタ情報です。ファイルヘッダに撮影日時だの、カメラの情報だの、GPS情報だのが記述されています。こいつももとのheicからコピーして保存します。
from PIL import Image from pillow_heif import HeifImagePlugin def heiftojpeg(src, dst): im = Image.open(src) im.save(dst, "JPEG", quality=95, exif=im.info.get("exif"))
これでOK。ちゃんと撮影情報が保持されています。なぜ同じimオブジェクトなのに引き継がれないのかよくわかりません。ファイルフォーマットが違うからでしょうかね。
icc_profile
いわゆるファイルに埋め込まれたカラープロファイルです。これも引き継ぐ必要があります。
iPhoneで撮影するといわゆるsRGBではなく、Display P3なるプロファイルが埋め込まれています。詳細はググってください。
このプロファイルを引き継ぎましょう。といってもコードは単純で。
from PIL import Image from pillow_heif import HeifImagePlugin def heiftojpeg(src, dst): im = Image.open(src) im.save(dst, "JPEG", quality=95, exif=im.info.get("exif"), icc_profile=im.info.get("icc_profile"))
これでくすんで見えてたJPEG画像がくっきり鮮やかになりました。このDisplay P3なる色空間はsRGBより広い空間を定義しており、同じRGBの値でも表現される色が異なるため、見た目が違って見えるんですねぇ。これで画像列だけでなく、メタデータも完全コピーが可能になりました。
なんなら他にもxmpだのoriginal_orientationだのいろいろなタグがありますが、いったんiPhoneのデータからであれば上記の2つのタグを引き継げばおおよそよさそうです。
これで色もEXIFも引き継いだJPEG画像の保存が可能になります。
普通に使う分にはこれで問題ないと思います。
罠(exif書き換えやがる。データ並び替えやがる。)
上の方法でheicを開いて、JPEGにそのEXIFを書き戻すと、どうやらEXIFの回転情報を書き換えてやがりますね。そして画像データも並び変わっている。
元のHEICが縦向き撮影(90度回転)してあるファイルを開いた際に、上の方法だと書き出されたJPEGの回転情報が水平に書き換えられていました。まぁ画像のデータ列も並び替えてくるので矛盾は発生していないのでよいのですが、余計なお世話です。しかもHEICファイルの開き方でEXIFのorientation上書きの挙動が変わるから困ったものです。
以下のコードで、2通りのファイルオープンで、orientationの値が違うことを確認できました。ん~書き換えなくてもいいんだけどなぁ…。
import pillow_heif from PIL import Image import piexif from pillow_heif import HeifImagePlugin def dump_heic_exif_orientation(src): im = Image.open(src) exif = im.info.get("exif") # exif情報取得 if exif is not None: exif_dict = piexif.load(exif) orientation = exif_dict['0th'][piexif.ImageIFD.Orientation] print("orientation : ", orientation) heic = pillow_heif.open_heif(src, bgr_mode=False) exif = heic.info.get("exif") if exif is not None: exif_dict = piexif.load(exif) orientation = exif_dict['0th'][piexif.ImageIFD.Orientation] print("orientation : ", orientation)
orientation : 1 orientation : 6
このようにorientationの値が異なります・・・。もとのHEIC画像では6が入っていたので、open_heifは書き換えない仕様。でもデータは並び変わってる。
openのやり方(Image.open か pillow_heif.open_heif)だけの違いですけど・・・。
さらにややこしいことに、ソース画像がHEICではなくJPEGだった場合には、pillowの仕様ではどうやら、データ列の並び替えをしてくれません。もちろんEXIFも書き換えない。詳細はこちら。
Image.openの動きに一貫性がない・・・。まとめると、EXIFのorientationに関して
ソースがJPEGでImage.open | ソースがHEICでImage.open | ソースがHEICでopen_heif | |
画像列 | 並び替えない | 並び替える | 並び替える |
EXIF | 書き換えない | 書き換える | 書き換えない |
困ったものです。別のフラグを見て何とかするのが正しいのかなぁ・・・。
おまけ(OpenCV)
俺はOpenCVだ!って人のための読み込み
def heiftojpeg(src, dst): heif_file = pillow_heif.open_heif(src, bgr_mode=True) np_array = np.asarray(heif_file) cv2.imwrite(dst, np_array)
これでOK。もちろんexif/icc_profileが落ちているのでそこは考慮してください。あと前述のとおり親切にEXIFタグ見て画像を回転してくれるので、縦撮りしてたら縦向きの画像にしてくれます。
先に述べたように、この並び替えの挙動は先のopenのやり方の違いによりません。どちらの方法(Image.open か pillow_heif.open_heif)で開いても画像データは向きを並び替えてよこしてきます。
bgr_modeのオプションがある分open_heifを使った方がopenCV向きですかね。ただopen_heifで開いたときのexifタグには騙されないようにしましょう。
コメント