今回のアプリ
サンプルに用いた画像は、以下のものをリサイズして用いた。
何をするアプリか?
JPEG画像を、手を抜いた処理で縦に無劣化で結合する。
手抜きなので、全く同じ仕様の画像の結合にのみ対応する。
また、JPEG画像であっても対応できないものもある。
詳細
JPEG 画像を無劣化で回転やトリミングするソフトウェアはよくあるが、結合できるソフトウェアはあまり見つからなかった。
そこで、JPEG 画像を無劣化で結合するソフトウェアの開発を目指し、仕様を調べていた。
JPEG (JFIF) ファイルの読み方まとめ #画像処理 - Qiita
その過程で、ある仮説を思いついた。
「条件が同じであれば、圧縮データのバイト列をリスタートマーカーを挟んで並べることで、わざわざハフマン符号を解読しなくても画像を結合できるのでは?」
というわけで、今回のアプリではこれをやってみた。
「条件が同じ」とは、画像の以下のパラメータが同じであることを意味する。
- チャンネル数
- 各チャンネルの量子化テーブル
- 各チャンネルのサブサンプリング情報
- 各チャンネルのハフマン符号テーブル
- 画像の幅 (MCU 数)
- 画像の高さ (MCU 数)
- サンプル精度
リスタートマーカーを入れるには、リスタートマーカーを入れるまでの MCU (JPEG 画像のデータを記録する単位となるブロック) の数を指定しなければならない。
そこで、この数を決めるための最低限のヘッダの解析を行い、MCU のサイズと画像のサイズから MCU の数を求めるようにした。
本来であれば、各セグメントの長さの情報を用い、扱わないセグメントを読み飛ばしながら解析を行うべきである。
しかし、今回は手抜きのため、セグメントの区切りは無視していきなりセグメントの種類を表すデータを検索する仕様にした。
この仕様により、セグメントの中にセグメントの種類を表すデータが含まれる場合、今回は非対応となる。
例えば、JPEG画像がサムネイルとして埋め込まれている場合、この制限に引っかかる。
「条件が同じ」かどうかの判定も、単純に圧縮データの前の部分のバイト列が全く同じかどうかで判定するようにした。
圧縮データの前の部分のバイト列が同じであれば、「条件が同じ」はずである。
一方、「条件が同じ」であってもバイト列が同じとは限らず、「条件が同じ」ではないと誤判定する可能性がある。
圧縮データをリスタートマーカーを挟んでそのまま結合するという仕組み上、以下の場合も非対応となる。
- 画像に既にリスタートマーカーが入っている場合
- 結合前の1個の画像中の MCU の数が 65,535 を超える場合
リスタートマーカーを入れるまでの MCU の数の記録に用いるデータサイズが 2 バイトであり、65,535 個までしか指定できない。
作ってみて
世の中のJPEGファイルを入力してみると、今回のアプリでは以下の原因で非対応のものがあった。
- Exif情報が違う
- ハフマン符号テーブルが違う
- JPEG形式のサムネイルが埋め込まれている
- プログレッシブJPEGである
- リスタートマーカーが含まれている
これらは、今回の手抜きアプリでは対応できないが、きちんとセグメントの処理やハフマン符号・量子化テーブルのデコードなどを行うようにすれば対応できるはずである。
(プログレッシブJPEGへの対応は、他の項目への対応より難しそうだが……)
普通にスマホで撮影したJPEGファイルも含め、今回のアプリでは対応できないJPEGファイルが多く、やはり今回作ったのはクソアプリである。
余裕ができたら、きちんと処理を行い、より多くのJPEGファイルに対応できる無劣化結合ソフトウェアを作りたい。
なお、今回のアプリでの結合時、結合部分にもとの画像には無かったピクセル群が現れることがある。
これは、もとの画像の高さが MCU のサイズ (ピクセル数) の倍数でなかったときに現れる、画像の最下行の MCU のうち隠されていたピクセル群である。
無劣化で加工を行う都合上、MCU の加工は行えず、もとの画像のサイズによってこのような「隙間」ができてしまうのは避けられないことである。
逆に、このような場合最下行の MCU を捨てる (画像を一部切る) ことにより「隙間」ができるのは防ぐ、という選択肢はある。
たとえば、1920×1080、クロマ・サブサンプリング 4:2:0 の画像では、画像の高さ 1080 ピクセルが MCU の高さ 16 ピクセルで割り切れず、8 余るので、結合すると高さ 8 ピクセルの余計な「隙間」が挟まることになる。
