Exif Orientation のうんちく

# はじめに

画像を表示する際に意図せぬ回転して困る事で有名な Exif Orientation に関するうんちくです。(2021年12月5日作成記事)

# Exif Orientation とは

カメラ撮影で保存する JPEG 画像ファイルに関する主な規格として、DCF と Exif があります。
そのうち Exif の Orientation 情報は、カメラの向き等によって画像が本来意図するものと違う回転/鏡像で JPEG ファイルに保存される事を示すメタデータです。
図のように横長のカメラを縦にして撮影した場合、本来の映像を横に倒した画像が JPEG に保存されます。これを表示する際に元へと戻すのが一般的な使われ方です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/d1c36ef2-bc04-98cd-486d-09568db9d0c6.png)

画像ビューアによっては Orientation が無視される事もあるので、世の中の画像アップローダは Orientation 補正して画像を保存する事が多いでしょう。
また多くの画像ライブラリは画像を読み込む際に自動では Orientation 補正をしないので、縦と横で違いが出るフィルタを実装する場合や、画像認識をする場合にも、Orientation を意識するケースがあります。

# Exif Orientation 仕様

Exif Orientation の仕様が含まれる Exif 2.3 (2012年12月改定版)は以下の URL でアクセスできます。

- https://www.cipa.jp/std/documents/j/DC-008-2012_J.pdf

尚、現時点(2021年12月5日)の最新は 2.32 で正式版は売り物です。(ちなみに ￥24,574)。ドラフトの方は PDF 公開されています。

- https://www.cipa.jp/std/documents/j/DC-X008-2019-J.pdf (ドラフト版)
- https://www.jeita.or.jp/cgi-bin/standard/search.cgi ここで Exif で検索すると正式版が見つかります。立ち読みができます。

## Exif バイナリ形式 (おまけ)

Exif バイナリ形式での Orientation フィールドの入れ方はこちらです。

|タグ名称|タグ番号|タイプ|カウント
|---|---|---|---|
|画像方向 Orientation | 274(0x0112) | SHORT(=2byte) | 1|

Exif のバイナリ形式は TIFF のサブセットで、その TIFF の画像フォーマットの仕様は　Adobe のサイトにあります。

- https://www.adobe.io/open/standards/TIFF.html

この手のドキュメントに読み慣れてない方は、まずは以下のエントリを参照すると良いでしょう。噛み砕いた説明で分かりやすいです。

- Exif データにアクセスするコードを自作してみる
  - http://dsas.blog.klab.org/archives/52123322.html

# サンプル画像

サンプル画像は ImageMagick で以下のように作成しました。文字を入れると鏡写しにすぐ気付けるのでお勧めです。

```shell
% convert -size 100x75 -font .New-York-Italic -pointsize 64 \
     -fill white -stroke black -strokewidth 1 -gravity center \
     \( \( xc:red  -annotate 0 R \) \( xc:green1 -annotate 0 G \) +append \) \
     \( \( xc:blue -annotate 0 B \) \( xc:yellow -annotate 0 Y \) +append \) \
     -append -depth 1 RGBY.png
```

![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png)

Orientation つきの JPEG 画像の作り方は、以下のサイトをご参考までに。

- https://blog.awm.jp/2016/01/07/orient/

```shell
% convert RGBY.png RGBY.jpg
% wget https://raw.githubusercontent.com/yoya/misc/master/bash/severalorientation.sh
% sh severalorientation.sh RGBY.jpg
```

これで全種類の 1〜8までの Orientation のついた JPEG 画像が生成できます。

# Orientation の値

90度単位の回転だけだと 4 種類ですが、鏡像反転もあるので 1〜8 の 8 種類の値を持ちます。このおかげでインカメラのように左右鏡像反転した方が直感的なケースにも対応できます。

1 origin の番号体系である事に注意が必要です。
おそらく、0 は Exif 内に Orientation フィールド不在な場合と区別しにくいので、特別扱いしたものと思われます。(ただ、処理的には Orientation 無しと 0 と 1 は全部同じなので、区別する必要があったかというと微妙)

## 回転と鏡像反転 (前提知識)

実は画像の90度単位での回転は、鏡像反転処理だけで実現できます。
例えば、90度回転は上下反転と斜め反転の組み合わせで可能です。

| 元画像 | 上下反転 | 更に斜め反転 |
|---|---|---|
 ![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png) | ![RGBY-4のコピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/9d414cb6-261c-3622-4c03-8f5d08687b2c.png) | ![RGBY-8のコピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/425a5d4c-9f4b-8272-1253-9b38f23d4544.png) |

つまり、同じ鏡像反転を２回実行すると元に戻りますが、違う種類の鏡像反転を適用すると、回転するのです。

|上下+斜め => 90度回転 | 上下+左右 = 180度回転 | 左右+斜め = 270度回転 |
|---|---|---
![RGBY-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/4fdbaf2b-e4ca-9513-6543-c3ef475cae86.png)| ![RGBY-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/ea6bd07e-710d-b635-c64a-ba673215ae1a.png) | ![RGBY-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/e537a720-716a-f50e-a6f9-f9cb4254ef7f.png) |

そんな訳で、Orientation の全ては、上下/左右/斜めの鏡像反転の組み合わせで実現できますし、それら３つのフラグと相互変換もできます。

さて、具体的な画像の変換を紹介します。

## 変換表

### JPEG ファイル内の実画像

Orientation 適用前の JPEG 実画像と、Orientation を適用した時の比較です。
Orientation の番号は回転の順には並んでいません。画像の反転をベースに考えているようです。

| Orientaton | JPEG 実画像(Orientation無視表示) | Orientation適用表示 | 操作 |
|---|---|---|---|
|1|![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png) | ![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png)| 無し |
|2|![RGBY-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/a3c665c8-f90d-2b4e-aae7-edcebfe7114d.png)|![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png)|左右反転|
|3|![RGBY-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/ea6bd07e-710d-b635-c64a-ba673215ae1a.png)|![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png)|上下左右反転 = 180度回転|
|4|![RGBY-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/ec781a7c-ffe7-8b79-2320-8496b6c7ee48.png)|![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png)|上下反転|
|5|![RGBY-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/0cc8f154-e03e-1451-5b5c-7ae1769bd5ef.png)|![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png)| 斜め反転 |
|6|![RGBY-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/e537a720-716a-f50e-a6f9-f9cb4254ef7f.png)|![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png)|2 + 斜め反転 = 270回転|
|7|![RGBY-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/93c61574-42b7-57be-913f-d70d3dfec49b.png)|![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png)|3 + 斜め反転 |
|8|![RGBY-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/4fdbaf2b-e4ca-9513-6543-c3ef475cae86.png)|![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png)|4 + 斜め反転 = 90度回転 |

## 早見表

２つに分けます。

### 実画像の配置

Orientation 操作すると ![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png) になる、元画像を以下に並べます。

| 1 | 2 | 3 | 4 |
|---|---|---|---|
|![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png) |![RGBY-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/a3c665c8-f90d-2b4e-aae7-edcebfe7114d.png)|![RGBY-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/ea6bd07e-710d-b635-c64a-ba673215ae1a.png)|![RGBY-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/ec781a7c-ffe7-8b79-2320-8496b6c7ee48.png)|
|5 | 6 | 7 | 8|
|![RGBY-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/0cc8f154-e03e-1451-5b5c-7ae1769bd5ef.png)|![RGBY-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/e537a720-716a-f50e-a6f9-f9cb4254ef7f.png)|![RGBY-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/93c61574-42b7-57be-913f-d70d3dfec49b.png)|![RGBY-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/4fdbaf2b-e4ca-9513-6543-c3ef475cae86.png)|

### 変換操作

実際に Orientation 適用の実装をする場合、こちらの表が便利だと思います。
元が ![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png) で、Orientation 操作した後の画像テーブルです。

| 1 | 2 | 3 | 4 |
|---|---|---|---|
|![RGBY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f678e3b3-d0b6-4c9a-764c-5e21e568317c.png) |![RGBY-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/a3c665c8-f90d-2b4e-aae7-edcebfe7114d.png)|![RGBY-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/ea6bd07e-710d-b635-c64a-ba673215ae1a.png)|![RGBY-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/ec781a7c-ffe7-8b79-2320-8496b6c7ee48.png)|
|5 | 6 | 7 | 8|
|![RGBY-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/0cc8f154-e03e-1451-5b5c-7ae1769bd5ef.png)|![RGBY-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/4fdbaf2b-e4ca-9513-6543-c3ef475cae86.png)|![RGBY-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/93c61574-42b7-57be-913f-d70d3dfec49b.png)|![RGBY-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/e537a720-716a-f50e-a6f9-f9cb4254ef7f.png)||

一つ目の早見表との違いは、6 と 8 が入れ替わっているだけです。

# Orientation と鏡像反転フラグ

画像のテーブルを見ると分かりますが、Orientation 操作は左右反転、上下反転、斜め反転の３つの鏡像反転で表現できます。
Orientation から 1 を引いてビットで分解すると、その反転のフラグに近いデータが出てきます。

| 1 | 2 | 3 | 4 |
|---|---|---|---|
|そのまま|左右反転|上下左右反転|上下反転|
| 5 | 6 | 7 | 8 |
|1の斜め反転|2の斜め反転|3の斜め反転|4の斜め反転|

つまり、Orientation から 1 を引くと、ビットで処理できます。
3 と 4 の意味が逆なら、もっと素直なビット処理になるのが少し残念ですが、仕方なく頑張ると、以下のようなコードで相互変換可能です。

- Orientation から反転ビットを取り出す処理

```javascript
function fromOrientation(orientation) {
    const o = orientation - 1;
    const reverse = (o & 1)? true: false;
    const vertical = (o & 2)? true: false;
    const horizontal = reverse !== vertical;  // xor
    const diagonal = (o & 4)? true: false;
    return [horizontal, vertical, diagonal];
}
```

- 反転ビットから Orientation 値に戻る処理

```javascript
function toOrientation(horizontal, vertical, diagonal) {
    const reverse = horizontal !== vertical;  // xor
    const o = (reverse? 1: 0) + (vertical? 2: 0) + (diagonal? 4: 0);
    return o + 1;  // orientation;
}
```

Orientation 値と反転フラグの関係が実感できるデモを作りました。任意の画像をドロップして試せます。

- https://app.awm.jp/image.js/orientation.html