前回のラブライブ!
JPEG画像を無劣化(ロスレス)で切り貼りしたい ~ハフマン符号の読み方の答え~ #画像処理 - Qiita
- JPEG 画像の圧縮データに用いられるハフマン符号の読み方がわかった
- JPEGsnoop を用いて JPEG 画像の圧縮データをデコードできることがわかった
今回やること
様々なサイズの JPEG 画像を作り、圧縮データに記録される各コンポーネントのデータ数と配置を調べる。
サブサンプリングと画像の劣化の関係の確認
GIMP で JPEG 画像をエクスポートする際、「詳細設定」から「サブサンプリング」を指定できる。
GIMP 2.10.38 では、以下の選択肢がある。
- 4:4:4 (最高品質)
- 4:2:2 水平 (1/2 クロマ)
- 4:2:2 垂直 (1/2 クロマ)
- 4:2:0 (1/4 クロマ)
これらの違いによる画質への影響を試した。
まず、以下の PNG 画像を用意した。(文字は美咲フォント (美咲ゴシック) を使用した)
| 原寸 | 4倍 |
|---|---|
![]() |
![]() |
この画像を、それぞれの「サブサンプリング」を選択し、品質 90 で JPEG 画像に変換した。
以下がその結果である。
| 4:4:4 | 4:2:2 水平 | 4:2:2 垂直 | 4:2:0 |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
見やすいように4倍に拡大すると、以下のようになる。
| 4:4:4 | 4:2:2 水平 |
|---|---|
![]() |
![]() |
| 4:2:2 垂直 | 4:2:0 |
![]() |
![]() |
「4:2:2 水平」では横方向への色の染み出しが、「4:2:2 垂直」では縦方向の色の染み出しが目立つ。
「4:2:0」では全体的に画質が落ちているが、縦横のバランスはとれている印象である。
画像サイズとデータ数の関係の調査
8×8~64×64で画像のサイズを8ピクセルずつ変え、サブサンプリングごとの各コンポーネントのデータ数を調べる。
各種画像を生成する
ImageMagickで単色画像を作成する方法
ImageMagick の JPEG オプション #ImageMagick - Qiita
を参考に、以下の Perl プログラムで、各サブサンプリング・各サイズの画像を生成した。
#!/usr/bin/perl
use strict;
use warnings;
mkdir "444";
mkdir "422";
mkdir "420";
for (my $h = 8; $h <= 64; $h += 8) {
for (my $w = 8; $w <= 64; $w += 8) {
my $size = sprintf("%dx%d", $w, $h);
system "magick convert -size $size \"xc:#00ff00\" -sampling-factor 4:4:4 444/$size.jpg";
system "magick convert -size $size \"xc:#00ff00\" -sampling-factor 4:2:2 422/$size.jpg";
system "magick convert -size $size \"xc:#00ff00\" -sampling-factor 4:2:0 420/$size.jpg";
}
}
サブサンプリングの情報は、TSXBIN で JPEG ファイルを開き、「SOF」の「Component(数字)」の2バイト目を見ることで確認できる。
09E ★SOF[0] FF C0
0A0 SizeOfThis[0] 00 11
0A2 DataPrecision 08
0A3 PicHeight[0] 00 08
0A5 PicWidth[0] 00 08
0A7 Nf 03
0A8 Component0[0] 01 21 00
0AB Component1[0] 02 11 01
0AE Component2[0] 03 11 01
これを確認した結果、ImageMagick における「4:2:2」は GIMP の「4:2:2 水平」に相当するようだったので、以下のプログラムを追加し、「4:2:2 垂直」にあたる画像を生成した。
#!/usr/bin/perl
use strict;
use warnings;
mkdir "422_suichoku";
for (my $h = 8; $h <= 64; $h += 8) {
for (my $w = 8; $w <= 64; $w += 8) {
my $size = sprintf("%dx%d", $w, $h);
system "magick convert -size $size \"xc:#00ff00\" -sampling-factor 1x2,1x1,1x1 422_suichoku/$size.jpg";
}
}
各種画像をデコードする
JPEGsnoop を用いて、生成した画像をデコードする。
この際、圧縮データをデコードできるよう、メニューの「Options → Scan Segment → Detailed Decode...」を選択すると現れるダイアログで、「Enable detailed Scan Decode?」をオンにする。
また、デコードが途中で止まらないよう、「# MCUs =」を大きい値 (たとえば 100000) にする。
設定後、「File → Batch Process...」を用いて各画像のデコードを……行ったところ、なぜか肝心の圧縮データのデコード結果が出力されなかった。
残念ながら、今回の目的でこの機能を使用するのは難しそうである。
そこで、キーボードシミュレータを用い、「画像を開いて解析結果を保存する」操作を自動で繰り返すことで、画像群の解析を行なうことにした。
以下のプログラムにより、キーボードシミュレータに入力する操作列を生成する。
#!/usr/bin/perl
use strict;
use warnings;
for (my $h = 8; $h <= 64; $h += 8) {
for (my $w = 8; $w <= 64; $w += 8) {
printf "[VK_CONTROL)O(VK_CONTROL][WAIT:1000]%dX%d\n", $w, $h;
print "[WAIT:1000][VK_CONTROL)S(VK_CONTROL][WAIT:1000]\n";
}
}
これにより、各ファイルについて以下を行う操作列が生成できる。
- Ctrl+O を押し、解析を行う画像を選択するダイアログを開く
- ファイル名を入力する
- Enter を押し、ダイアログを閉じて解析を行う
- Ctrl+S を押し、解析結果の保存先を選択するダイアログを開く
- Enter を押し、ダイアログを閉じて解析結果を保存する
解析結果の保存先のファイル名は開いた画像に応じて自動で設定されるので、それをそのまま用いる。
キーボードシミュレータを「キーボードシミュレートモード」にし、生成した操作列を貼り付ける。
改行の場所で Enter を入力したいので、「Enterを入力しない」はオフにしておく。
JPEGsnoop で、メニューの「File → Open Image...」から解析を行う画像があるディレクトリにある適当な画像を1個開く。
これにより、解析を行う画像を選択するダイアログの初期ディレクトリが設定される。
そして、キーボードシミュレータで「入力開始」し、JPEGsnoop をアクティブにする。
操作が完了するまで、手動での操作は行わずに待つ。
解析結果を保存する際に、上書き確認のダイアログが出ると、操作がうまくいかない。
キーボードシミュレータによる解析実行の前は、解析結果は保存しないか、保存した場合は削除しておく。
解析結果は解析対象の画像と同じディレクトリに保存されるので、エクスプローラの機能でファイルの種類順に並べ、まとめて別のディレクトリに移動する。
デコード結果から各コンポーネントのデータ数を数える
以下のプログラムを解析結果を保存したディレクトリで実行し、その中に含まれる Lum と Chr(0) のコンポーネントのデータ数を数えた。
このプログラムにより、データ数を数えるだけでなく、その結果をテーブルにまとめる処理も行う。
#!/usr/bin/perl
use strict;
use warnings;
print "<table>\n";
print "<tr><th>高さ<br>幅\</th>";
for (my $w = 8; $w <= 64; $w += 8) {
print "<th>$w</th>";
}
print "</tr>\n";
for (my $h = 8; $h <= 64; $h += 8) {
print "<tr><th>$h</th>";
for (my $w = 8; $w <= 64; $w += 8) {
my $file = sprintf("%dx%d.jpg.txt", $w, $h);
open(IN, "< $file") or die "failed to open $file\n";
my $lum = 0;
my $chr = 0;
while (my $line = <IN>) {
if ($line =~ /^ Lum/) { $lum++; }
if ($line =~ /^ Chr/) { $chr++; }
}
close(IN);
print "<td>Lum:$lum<br>Chr:$chr</td>";
}
print "</tr>\n";
}
print "</table>\n";
4:4:4
SOF0 に記録された Component 情報では、サブサンプリングは全て 0x11 (1×1) となっていた。
Number of Img components = 3
Component[1]: ID=0x01, Samp Fac=0x11 (Subsamp 1 x 1), Quant Tbl Sel=0x00 (Lum: Y)
Component[2]: ID=0x02, Samp Fac=0x11 (Subsamp 1 x 1), Quant Tbl Sel=0x01 (Chrom: Cb)
Component[3]: ID=0x03, Samp Fac=0x11 (Subsamp 1 x 1), Quant Tbl Sel=0x01 (Chrom: Cr)
画像サイズとデータ数の関係は、以下のようになった。
| 高さ 幅\ |
8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 |
|---|---|---|---|---|---|---|---|---|
| 8 | Lum:1 Chr:2 |
Lum:2 Chr:4 |
Lum:3 Chr:6 |
Lum:4 Chr:8 |
Lum:5 Chr:10 |
Lum:6 Chr:12 |
Lum:7 Chr:14 |
Lum:8 Chr:16 |
| 16 | Lum:2 Chr:4 |
Lum:4 Chr:8 |
Lum:6 Chr:12 |
Lum:8 Chr:16 |
Lum:10 Chr:20 |
Lum:12 Chr:24 |
Lum:14 Chr:28 |
Lum:16 Chr:32 |
| 24 | Lum:3 Chr:6 |
Lum:6 Chr:12 |
Lum:9 Chr:18 |
Lum:12 Chr:24 |
Lum:15 Chr:30 |
Lum:18 Chr:36 |
Lum:21 Chr:42 |
Lum:24 Chr:48 |
| 32 | Lum:4 Chr:8 |
Lum:8 Chr:16 |
Lum:12 Chr:24 |
Lum:16 Chr:32 |
Lum:20 Chr:40 |
Lum:24 Chr:48 |
Lum:28 Chr:56 |
Lum:32 Chr:64 |
| 40 | Lum:5 Chr:10 |
Lum:10 Chr:20 |
Lum:15 Chr:30 |
Lum:20 Chr:40 |
Lum:25 Chr:50 |
Lum:30 Chr:60 |
Lum:35 Chr:70 |
Lum:40 Chr:80 |
| 48 | Lum:6 Chr:12 |
Lum:12 Chr:24 |
Lum:18 Chr:36 |
Lum:24 Chr:48 |
Lum:30 Chr:60 |
Lum:36 Chr:72 |
Lum:42 Chr:84 |
Lum:48 Chr:96 |
| 56 | Lum:7 Chr:14 |
Lum:14 Chr:28 |
Lum:21 Chr:42 |
Lum:28 Chr:56 |
Lum:35 Chr:70 |
Lum:42 Chr:84 |
Lum:49 Chr:98 |
Lum:56 Chr:112 |
| 64 | Lum:8 Chr:16 |
Lum:16 Chr:32 |
Lum:24 Chr:48 |
Lum:32 Chr:64 |
Lum:40 Chr:80 |
Lum:48 Chr:96 |
Lum:56 Chr:112 |
Lum:64 Chr:128 |
8×8 のブロックそれぞれにつき、Lum のデータが1個、Chr(0) のデータが2個あるようである。
16×16 の画像では、以下のようなデータの配置になっていた。(デコード結果の詳細は省略)
Lum (Tbl #0), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Lum (Tbl #0), MCU=[1,0]
Chr(0) (Tbl #1), MCU=[1,0]
Chr(0) (Tbl #1), MCU=[1,0]
Lum (Tbl #0), MCU=[0,1]
Chr(0) (Tbl #1), MCU=[0,1]
Chr(0) (Tbl #1), MCU=[0,1]
Lum (Tbl #0), MCU=[1,1]
Chr(0) (Tbl #1), MCU=[1,1]
Chr(0) (Tbl #1), MCU=[1,1]
各ブロックごとに、まず Lum のデータが配置され、続いて Chr(0) のデータが配置されている。
MCU の数値はまず1個目が増え、1個目が増えきると2個目が増えて1個目が0に戻るようである。
4:2:2 (水平)
SOF0 に記録された Component 情報では、最初のサブサンプリングが 0x21 (1×1)、残りのサブサンプリングが 0x11 (2×1) となっていた。
Number of Img components = 3
Component[1]: ID=0x01, Samp Fac=0x21 (Subsamp 1 x 1), Quant Tbl Sel=0x00 (Lum: Y)
Component[2]: ID=0x02, Samp Fac=0x11 (Subsamp 2 x 1), Quant Tbl Sel=0x01 (Chrom: Cb)
Component[3]: ID=0x03, Samp Fac=0x11 (Subsamp 2 x 1), Quant Tbl Sel=0x01 (Chrom: Cr)
画像サイズとデータ数の関係は、以下のようになった。
| 高さ 幅\ |
8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 |
|---|---|---|---|---|---|---|---|---|
| 8 | Lum:2 Chr:2 |
Lum:2 Chr:2 |
Lum:4 Chr:4 |
Lum:4 Chr:4 |
Lum:6 Chr:6 |
Lum:6 Chr:6 |
Lum:8 Chr:8 |
Lum:8 Chr:8 |
| 16 | Lum:4 Chr:4 |
Lum:4 Chr:4 |
Lum:8 Chr:8 |
Lum:8 Chr:8 |
Lum:12 Chr:12 |
Lum:12 Chr:12 |
Lum:16 Chr:16 |
Lum:16 Chr:16 |
| 24 | Lum:6 Chr:6 |
Lum:6 Chr:6 |
Lum:12 Chr:12 |
Lum:12 Chr:12 |
Lum:18 Chr:18 |
Lum:18 Chr:18 |
Lum:24 Chr:24 |
Lum:24 Chr:24 |
| 32 | Lum:8 Chr:8 |
Lum:8 Chr:8 |
Lum:16 Chr:16 |
Lum:16 Chr:16 |
Lum:24 Chr:24 |
Lum:24 Chr:24 |
Lum:32 Chr:32 |
Lum:32 Chr:32 |
| 40 | Lum:10 Chr:10 |
Lum:10 Chr:10 |
Lum:20 Chr:20 |
Lum:20 Chr:20 |
Lum:30 Chr:30 |
Lum:30 Chr:30 |
Lum:40 Chr:40 |
Lum:40 Chr:40 |
| 48 | Lum:12 Chr:12 |
Lum:12 Chr:12 |
Lum:24 Chr:24 |
Lum:24 Chr:24 |
Lum:36 Chr:36 |
Lum:36 Chr:36 |
Lum:48 Chr:48 |
Lum:48 Chr:48 |
| 56 | Lum:14 Chr:14 |
Lum:14 Chr:14 |
Lum:28 Chr:28 |
Lum:28 Chr:28 |
Lum:42 Chr:42 |
Lum:42 Chr:42 |
Lum:56 Chr:56 |
Lum:56 Chr:56 |
| 64 | Lum:16 Chr:16 |
Lum:16 Chr:16 |
Lum:32 Chr:32 |
Lum:32 Chr:32 |
Lum:48 Chr:48 |
Lum:48 Chr:48 |
Lum:64 Chr:64 |
Lum:64 Chr:64 |
16×8 のブロック (端数切り上げ) それぞれにつき、Lum のデータが2個、Chr(0) のデータが2個あるようである。
24×24 の画像では、以下のようなデータの配置になっていた。(デコード結果の詳細は省略)
Lum (Tbl #0), MCU=[0,0]
Lum (Tbl #0), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Lum (Tbl #0), MCU=[1,0]
Lum (Tbl #0), MCU=[1,0]
Chr(0) (Tbl #1), MCU=[1,0]
Chr(0) (Tbl #1), MCU=[1,0]
Lum (Tbl #0), MCU=[0,1]
Lum (Tbl #0), MCU=[0,1]
Chr(0) (Tbl #1), MCU=[0,1]
Chr(0) (Tbl #1), MCU=[0,1]
Lum (Tbl #0), MCU=[1,1]
Lum (Tbl #0), MCU=[1,1]
Chr(0) (Tbl #1), MCU=[1,1]
Chr(0) (Tbl #1), MCU=[1,1]
Lum (Tbl #0), MCU=[0,2]
Lum (Tbl #0), MCU=[0,2]
Chr(0) (Tbl #1), MCU=[0,2]
Chr(0) (Tbl #1), MCU=[0,2]
Lum (Tbl #0), MCU=[1,2]
Lum (Tbl #0), MCU=[1,2]
Chr(0) (Tbl #1), MCU=[1,2]
Chr(0) (Tbl #1), MCU=[1,2]
各ブロックごとに、まず Lum のデータが配置され、続いて Chr(0) のデータが配置されている。
MCU の数値は、1個目が1まで、2個目が2までである。
これは、横方向には幅16ピクセルのブロックが2個、縦方向には幅8ピクセルのブロックが3個あることに対応していそうである。
よって、圧縮データはブロックの各行のデータを連続して記録し、1行終わると次の行に行くらしいことが読み取れる。
4:2:2 (垂直)
SOF0 に記録された Component 情報では、最初のサブサンプリングが 0x12 (1×1)、残りのサブサンプリングが 0x11 (1×2) となっていた。
Number of Img components = 3
Component[1]: ID=0x01, Samp Fac=0x12 (Subsamp 1 x 1), Quant Tbl Sel=0x00 (Lum: Y)
Component[2]: ID=0x02, Samp Fac=0x11 (Subsamp 1 x 2), Quant Tbl Sel=0x01 (Chrom: Cb)
Component[3]: ID=0x03, Samp Fac=0x11 (Subsamp 1 x 2), Quant Tbl Sel=0x01 (Chrom: Cr)
画像サイズとデータ数の関係は、以下のようになった。
| 高さ 幅\ |
8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 |
|---|---|---|---|---|---|---|---|---|
| 8 | Lum:2 Chr:2 |
Lum:4 Chr:4 |
Lum:6 Chr:6 |
Lum:8 Chr:8 |
Lum:10 Chr:10 |
Lum:12 Chr:12 |
Lum:14 Chr:14 |
Lum:16 Chr:16 |
| 16 | Lum:2 Chr:2 |
Lum:4 Chr:4 |
Lum:6 Chr:6 |
Lum:8 Chr:8 |
Lum:10 Chr:10 |
Lum:12 Chr:12 |
Lum:14 Chr:14 |
Lum:16 Chr:16 |
| 24 | Lum:4 Chr:4 |
Lum:8 Chr:8 |
Lum:12 Chr:12 |
Lum:16 Chr:16 |
Lum:20 Chr:20 |
Lum:24 Chr:24 |
Lum:28 Chr:28 |
Lum:32 Chr:32 |
| 32 | Lum:4 Chr:4 |
Lum:8 Chr:8 |
Lum:12 Chr:12 |
Lum:16 Chr:16 |
Lum:20 Chr:20 |
Lum:24 Chr:24 |
Lum:28 Chr:28 |
Lum:32 Chr:32 |
| 40 | Lum:6 Chr:6 |
Lum:12 Chr:12 |
Lum:18 Chr:18 |
Lum:24 Chr:24 |
Lum:30 Chr:30 |
Lum:36 Chr:36 |
Lum:42 Chr:42 |
Lum:48 Chr:48 |
| 48 | Lum:6 Chr:6 |
Lum:12 Chr:12 |
Lum:18 Chr:18 |
Lum:24 Chr:24 |
Lum:30 Chr:30 |
Lum:36 Chr:36 |
Lum:42 Chr:42 |
Lum:48 Chr:48 |
| 56 | Lum:8 Chr:8 |
Lum:16 Chr:16 |
Lum:24 Chr:24 |
Lum:32 Chr:32 |
Lum:40 Chr:40 |
Lum:48 Chr:48 |
Lum:56 Chr:56 |
Lum:64 Chr:64 |
| 64 | Lum:8 Chr:8 |
Lum:16 Chr:16 |
Lum:24 Chr:24 |
Lum:32 Chr:32 |
Lum:40 Chr:40 |
Lum:48 Chr:48 |
Lum:56 Chr:56 |
Lum:64 Chr:64 |
8×16 のブロック (端数切り上げ) それぞれにつき、Lum のデータが2個、Chr(0) のデータが2個あるようである。
24×24 の画像では、以下のようなデータの配置になっていた。(デコード結果の詳細は省略)
Lum (Tbl #0), MCU=[0,0]
Lum (Tbl #0), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Lum (Tbl #0), MCU=[1,0]
Lum (Tbl #0), MCU=[1,0]
Chr(0) (Tbl #1), MCU=[1,0]
Chr(0) (Tbl #1), MCU=[1,0]
Lum (Tbl #0), MCU=[2,0]
Lum (Tbl #0), MCU=[2,0]
Chr(0) (Tbl #1), MCU=[2,0]
Chr(0) (Tbl #1), MCU=[2,0]
Lum (Tbl #0), MCU=[0,1]
Lum (Tbl #0), MCU=[0,1]
Chr(0) (Tbl #1), MCU=[0,1]
Chr(0) (Tbl #1), MCU=[0,1]
Lum (Tbl #0), MCU=[1,1]
Lum (Tbl #0), MCU=[1,1]
Chr(0) (Tbl #1), MCU=[1,1]
Chr(0) (Tbl #1), MCU=[1,1]
Lum (Tbl #0), MCU=[2,1]
Lum (Tbl #0), MCU=[2,1]
Chr(0) (Tbl #1), MCU=[2,1]
Chr(0) (Tbl #1), MCU=[2,1]
各ブロックごとに、まず Lum のデータが配置され、続いて Chr(0) のデータが配置されている。
MCU の数値は、1個目が2まで、2個目が1までである。
これは、横方向には幅8ピクセルのブロックが3個、縦方向には幅16ピクセルのブロックが2個あることに対応していそうである。
4:2:0
SOF0 に記録された Component 情報では、最初のサブサンプリングが 0x22 (1×1)、残りのサブサンプリングが 0x11 (2×2) となっていた。
Number of Img components = 3
Component[1]: ID=0x01, Samp Fac=0x22 (Subsamp 1 x 1), Quant Tbl Sel=0x00 (Lum: Y)
Component[2]: ID=0x02, Samp Fac=0x11 (Subsamp 2 x 2), Quant Tbl Sel=0x01 (Chrom: Cb)
Component[3]: ID=0x03, Samp Fac=0x11 (Subsamp 2 x 2), Quant Tbl Sel=0x01 (Chrom: Cr)
画像サイズとデータ数の関係は、以下のようになった。
| 高さ 幅\ |
8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 |
|---|---|---|---|---|---|---|---|---|
| 8 | Lum:4 Chr:2 |
Lum:4 Chr:2 |
Lum:8 Chr:4 |
Lum:8 Chr:4 |
Lum:12 Chr:6 |
Lum:12 Chr:6 |
Lum:16 Chr:8 |
Lum:16 Chr:8 |
| 16 | Lum:4 Chr:2 |
Lum:4 Chr:2 |
Lum:8 Chr:4 |
Lum:8 Chr:4 |
Lum:12 Chr:6 |
Lum:12 Chr:6 |
Lum:16 Chr:8 |
Lum:16 Chr:8 |
| 24 | Lum:8 Chr:4 |
Lum:8 Chr:4 |
Lum:16 Chr:8 |
Lum:16 Chr:8 |
Lum:24 Chr:12 |
Lum:24 Chr:12 |
Lum:32 Chr:16 |
Lum:32 Chr:16 |
| 32 | Lum:8 Chr:4 |
Lum:8 Chr:4 |
Lum:16 Chr:8 |
Lum:16 Chr:8 |
Lum:24 Chr:12 |
Lum:24 Chr:12 |
Lum:32 Chr:16 |
Lum:32 Chr:16 |
| 40 | Lum:12 Chr:6 |
Lum:12 Chr:6 |
Lum:24 Chr:12 |
Lum:24 Chr:12 |
Lum:36 Chr:18 |
Lum:36 Chr:18 |
Lum:48 Chr:24 |
Lum:48 Chr:24 |
| 48 | Lum:12 Chr:6 |
Lum:12 Chr:6 |
Lum:24 Chr:12 |
Lum:24 Chr:12 |
Lum:36 Chr:18 |
Lum:36 Chr:18 |
Lum:48 Chr:24 |
Lum:48 Chr:24 |
| 56 | Lum:16 Chr:8 |
Lum:16 Chr:8 |
Lum:32 Chr:16 |
Lum:32 Chr:16 |
Lum:48 Chr:24 |
Lum:48 Chr:24 |
Lum:64 Chr:32 |
Lum:64 Chr:32 |
| 64 | Lum:16 Chr:8 |
Lum:16 Chr:8 |
Lum:32 Chr:16 |
Lum:32 Chr:16 |
Lum:48 Chr:24 |
Lum:48 Chr:24 |
Lum:64 Chr:32 |
Lum:64 Chr:32 |
16×16 のブロック (端数切り上げ) それぞれにつき、Lum のデータが4個、Chr(0) のデータが2個あるようである。
24×24 の画像では、以下のようなデータの配置になっていた。(デコード結果の詳細は省略)
Lum (Tbl #0), MCU=[0,0]
Lum (Tbl #0), MCU=[0,0]
Lum (Tbl #0), MCU=[0,0]
Lum (Tbl #0), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Lum (Tbl #0), MCU=[1,0]
Lum (Tbl #0), MCU=[1,0]
Lum (Tbl #0), MCU=[1,0]
Lum (Tbl #0), MCU=[1,0]
Chr(0) (Tbl #1), MCU=[1,0]
Chr(0) (Tbl #1), MCU=[1,0]
Lum (Tbl #0), MCU=[0,1]
Lum (Tbl #0), MCU=[0,1]
Lum (Tbl #0), MCU=[0,1]
Lum (Tbl #0), MCU=[0,1]
Chr(0) (Tbl #1), MCU=[0,1]
Chr(0) (Tbl #1), MCU=[0,1]
Lum (Tbl #0), MCU=[1,1]
Lum (Tbl #0), MCU=[1,1]
Lum (Tbl #0), MCU=[1,1]
Lum (Tbl #0), MCU=[1,1]
Chr(0) (Tbl #1), MCU=[1,1]
Chr(0) (Tbl #1), MCU=[1,1]
各ブロックごとに、まず Lum のデータが配置され、続いて Chr(0) のデータが配置されている。
追加調査
サブサンプリングの設定パターンは、これまでに挙げたものだけではない。
JPEG のクロマサブサンプリングと YUVabc - awm-Tech
を参照すると、4ブロックに1個のデータを記録するパターンもあるようである。
そこで、様々なサブサンプリングを設定し、観察を行ってみた。
YUV411
magick convert -size 32x32 "xc:#00ff00" -sampling-factor 4x1,1x1,1x1 green_4x1.jpg
SOF0 のコンポーネント情報は、以下のようになった。
Number of Img components = 3
Component[1]: ID=0x01, Samp Fac=0x41 (Subsamp 1 x 1), Quant Tbl Sel=0x00 (Lum: Y)
Component[2]: ID=0x02, Samp Fac=0x11 (Subsamp 4 x 1), Quant Tbl Sel=0x01 (Chrom: Cb)
Component[3]: ID=0x03, Samp Fac=0x11 (Subsamp 4 x 1), Quant Tbl Sel=0x01 (Chrom: Cr)
圧縮データの内容は、以下のようになった。
32×8のブロックごとに記録されているようだ。
Lum (Tbl #0), MCU=[0,0]
Lum (Tbl #0), MCU=[0,0]
Lum (Tbl #0), MCU=[0,0]
Lum (Tbl #0), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Lum (Tbl #0), MCU=[0,1]
Lum (Tbl #0), MCU=[0,1]
Lum (Tbl #0), MCU=[0,1]
Lum (Tbl #0), MCU=[0,1]
Chr(0) (Tbl #1), MCU=[0,1]
Chr(0) (Tbl #1), MCU=[0,1]
Lum (Tbl #0), MCU=[0,2]
Lum (Tbl #0), MCU=[0,2]
Lum (Tbl #0), MCU=[0,2]
Lum (Tbl #0), MCU=[0,2]
Chr(0) (Tbl #1), MCU=[0,2]
Chr(0) (Tbl #1), MCU=[0,2]
Lum (Tbl #0), MCU=[0,3]
Lum (Tbl #0), MCU=[0,3]
Lum (Tbl #0), MCU=[0,3]
Lum (Tbl #0), MCU=[0,3]
Chr(0) (Tbl #1), MCU=[0,3]
Chr(0) (Tbl #1), MCU=[0,3]
横8分の1
magick convert -size 64x32 "xc:#00ff00" -sampling-factor 8x1,1x1,1x1 green_8x1.jpg
エラーになり、空のファイルが生成された。
convert: Bogus sampling factors `green_8x1.jpg' @ error/jpeg.c/JPEGErrorHandler/348.
横3分の1、縦2分の1
magick convert -size 48x48 "xc:#00ff00" -sampling-factor 3x2,1x1,1x1 green_3x2.jpg
画像が出力された。
SOF0 のコンポーネント情報は、以下のようになった。
Number of Img components = 3
Component[1]: ID=0x01, Samp Fac=0x32 (Subsamp 1 x 1), Quant Tbl Sel=0x00 (Lum: Y)
Component[2]: ID=0x02, Samp Fac=0x11 (Subsamp 3 x 2), Quant Tbl Sel=0x01 (Chrom: Cb)
Component[3]: ID=0x03, Samp Fac=0x11 (Subsamp 3 x 2), Quant Tbl Sel=0x01 (Chrom: Cr)
圧縮データの内容は、以下のようになった。
24×16のブロックごとに記録されているようだ。
Lum (Tbl #0), MCU=[0,0]
Lum (Tbl #0), MCU=[0,0]
Lum (Tbl #0), MCU=[0,0]
Lum (Tbl #0), MCU=[0,0]
Lum (Tbl #0), MCU=[0,0]
Lum (Tbl #0), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Lum (Tbl #0), MCU=[1,0]
Lum (Tbl #0), MCU=[1,0]
Lum (Tbl #0), MCU=[1,0]
Lum (Tbl #0), MCU=[1,0]
Lum (Tbl #0), MCU=[1,0]
Lum (Tbl #0), MCU=[1,0]
Chr(0) (Tbl #1), MCU=[1,0]
Chr(0) (Tbl #1), MCU=[1,0]
Lum (Tbl #0), MCU=[0,1]
Lum (Tbl #0), MCU=[0,1]
Lum (Tbl #0), MCU=[0,1]
Lum (Tbl #0), MCU=[0,1]
Lum (Tbl #0), MCU=[0,1]
Lum (Tbl #0), MCU=[0,1]
Chr(0) (Tbl #1), MCU=[0,1]
Chr(0) (Tbl #1), MCU=[0,1]
Lum (Tbl #0), MCU=[1,1]
Lum (Tbl #0), MCU=[1,1]
Lum (Tbl #0), MCU=[1,1]
Lum (Tbl #0), MCU=[1,1]
Lum (Tbl #0), MCU=[1,1]
Lum (Tbl #0), MCU=[1,1]
Chr(0) (Tbl #1), MCU=[1,1]
Chr(0) (Tbl #1), MCU=[1,1]
Lum (Tbl #0), MCU=[0,2]
Lum (Tbl #0), MCU=[0,2]
Lum (Tbl #0), MCU=[0,2]
Lum (Tbl #0), MCU=[0,2]
Lum (Tbl #0), MCU=[0,2]
Lum (Tbl #0), MCU=[0,2]
Chr(0) (Tbl #1), MCU=[0,2]
Chr(0) (Tbl #1), MCU=[0,2]
Lum (Tbl #0), MCU=[1,2]
Lum (Tbl #0), MCU=[1,2]
Lum (Tbl #0), MCU=[1,2]
Lum (Tbl #0), MCU=[1,2]
Lum (Tbl #0), MCU=[1,2]
Lum (Tbl #0), MCU=[1,2]
Chr(0) (Tbl #1), MCU=[1,2]
Chr(0) (Tbl #1), MCU=[1,2]
最初と次で比率を変える
magick convert -size 48x48 "xc:#00ff00" -sampling-factor 3x2,2x3,1x1 green_3x2_2x3.jpg
エラーになり、空のファイルが生成された。
convert: Fractional sampling not implemented yet `green_3x2_2x3.jpg' @ error/jpeg.c/JPEGErrorHandler/348.
間引く位置を変える
magick convert -size 16x16 "xc:#00ff00" -sampling-factor 1x1,2x2,1x1 green_1x1_2x2.jpg
画像が出力された。
SOF0 のコンポーネント情報は、以下のようになった。
Number of Img components = 3
Component[1]: ID=0x01, Samp Fac=0x11 (Subsamp 2 x 2), Quant Tbl Sel=0x00 (Lum: Y)
Component[2]: ID=0x02, Samp Fac=0x22 (Subsamp 1 x 1), Quant Tbl Sel=0x01 (Chrom: Cb)
Component[3]: ID=0x03, Samp Fac=0x11 (Subsamp 2 x 2), Quant Tbl Sel=0x01 (Chrom: Cr)
圧縮データの内容は、以下のようになった。
Lum (Tbl #0), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Chr(0) (Tbl #1), MCU=[0,0]
Chr(0) のデータが、間引かない2番目のコンポーネント分の1個と、間引く3番目のコンポーネント分の4個、合計5個記録されている。
また、Lum のデータも間引かれ、1個だけ記録されている。
Chr(0) のデータの Matrix の左上は、上から順に -672, 0, 0, 0, -855 となっていた。
JPEGのハフマン符号 (1) DC成分 - Tech と Culture
より、ここの値は前のブロックの値からの差分のようである。
一方、今回エンコードしているのは単色の画像なので、同じコンポーネントの値は同じになることが期待できる。
よって、記録されている5個の Chr(0) のうち
- 最初の4個が、2番目のコンポーネントに対応する
- その後の1個が、3番目のコンポーネントに対応する
と推測できる。
3種類の間引き
magick convert -size 32x32 "xc:#00ff00" -sampling-factor 1x1,2x2,4x4 green_1x1_2x2_4x4.jpg
エラーになり、空のファイルが生成された。
convert: Sampling factors too large for interleaved scan `green_1x1_2x2_4x4.jpg' @ error/jpeg.c/JPEGErrorHandler/348.
SOFとSOSでコンポーネントの順番が違う場合
まず、GIMPで8×8のグレー (#808080) のJPEG画像を作成した。
JPEGsnoopで確認した結果、SOF のコンポーネント情報は以下のようになった。
Number of Img components = 3
Component[1]: ID=0x01, Samp Fac=0x11 (Subsamp 1 x 1), Quant Tbl Sel=0x00 (Lum: Y)
Component[2]: ID=0x02, Samp Fac=0x11 (Subsamp 1 x 1), Quant Tbl Sel=0x01 (Chrom: Cb)
Component[3]: ID=0x03, Samp Fac=0x11 (Subsamp 1 x 1), Quant Tbl Sel=0x01 (Chrom: Cr)
さらに、SOS のコンポーネント情報は以下のようになった。
Number of img components = 3
Component[1]: selector=0x01, table=0(DC),0(AC)
Component[2]: selector=0x02, table=1(DC),1(AC)
Component[3]: selector=0x03, table=1(DC),1(AC)
圧縮データは以下のようになった。
全部ゼロのため、順番を変えてもデータが壊れず、今回の実験に適している。
Lum (Tbl #0), MCU=[0,0]
[0x00000117.0]: ZRL=[ 0] Val=[ 0] Coef=[00= DC] Data=[0x 03 FF D9 00 = 0b (0------- -------- -------- --------)] EOB
[0x00000117.1]: ZRL=[ 0] Val=[ 0] Coef=[01..01] Data=[0x 03 FF D9 00 = 0b (-0------ -------- -------- --------)] EOB
DCT Matrix=[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
Chr(0) (Tbl #1), MCU=[0,0]
[0x00000117.2]: ZRL=[ 0] Val=[ 0] Coef=[00= DC] Data=[0x 03 FF D9 00 = 0b (--0----- -------- -------- --------)] EOB
[0x00000117.3]: ZRL=[ 0] Val=[ 0] Coef=[01..01] Data=[0x 03 FF D9 00 = 0b (---0---- -------- -------- --------)] EOB
DCT Matrix=[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
Chr(0) (Tbl #1), MCU=[0,0]
[0x00000117.4]: ZRL=[ 0] Val=[ 0] Coef=[00= DC] Data=[0x 03 FF D9 00 = 0b (----0--- -------- -------- --------)] EOB
[0x00000117.5]: ZRL=[ 0] Val=[ 0] Coef=[01..01] Data=[0x 03 FF D9 00 = 0b (-----0-- -------- -------- --------)] EOB
DCT Matrix=[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
さて、TSXBINでみると、この画像ファイルの SOS は以下のようになっている。
109 ★SOS[0] FF DA
10B SizeOfThis[0] 00 0C
10D CompCount 03
10E CompNumber0 01
10F TdTa0 00
110 CompNumber1 02
111 TdTa1 11
112 CompNumber2 03
113 TdTa2 11
114 Maybe0x00 00
115 Maybe0x3F 3F
116 Maybe0x00 00
ここに記録されているコンポーネントの情報を入れ替え、以下のようにしてみた。
109 ★SOS[0] FF DA
10B SizeOfThis[0] 00 0C
10D CompCount 03
10E CompNumber0 03
10F TdTa0 11
110 CompNumber1 01
111 TdTa1 00
112 CompNumber2 02
113 TdTa2 11
114 Maybe0x00 00
115 Maybe0x3F 3F
116 Maybe0x00 00
その結果、フォト (Windows 11)、ペイント (Windows 11)、GIMP、ImageMagick 全てにおいて、ファイルが壊れている扱いとなった。
そこで、さらに SOF もこの順番に合わせるため、以下のように書き換えた。
すると、画像が読み込めるようになった。
09E ★SOF[0] FF C0
0A0 SizeOfThis[0] 00 11
0A2 DataPrecision 08
0A3 PicHeight[0] 00 08
0A5 PicWidth[0] 00 08
0A7 Nf 03
0A8 Component0[0] 03 11 01
0AB Component1[0] 01 11 00
0AE Component2[0] 02 11 01
JPEGsnoopでみると、SOF のコンポーネント情報は以下のようになっていた。
Number of Img components = 3
Component[1]: ID=0x03, Samp Fac=0x11 (Subsamp 1 x 1), Quant Tbl Sel=0x01 (Lum: Y)
Component[2]: ID=0x01, Samp Fac=0x11 (Subsamp 1 x 1), Quant Tbl Sel=0x00 (Chrom: Cb)
Component[3]: ID=0x02, Samp Fac=0x11 (Subsamp 1 x 1), Quant Tbl Sel=0x01 (Chrom: Cr)
SOS のコンポーネント情報は以下のようになっていた。
Number of img components = 3
Component[1]: selector=0x03, table=1(DC),1(AC)
Component[2]: selector=0x01, table=0(DC),0(AC)
Component[3]: selector=0x02, table=1(DC),1(AC)
圧縮データは、以下のようになっていた。
Chr(0) (Tbl #1), MCU=[0,0]
[0x00000117.0]: ZRL=[ 0] Val=[ 0] Coef=[00= DC] Data=[0x 03 FF D9 00 = 0b (0------- -------- -------- --------)] EOB
[0x00000117.1]: ZRL=[ 0] Val=[ 0] Coef=[01..01] Data=[0x 03 FF D9 00 = 0b (-0------ -------- -------- --------)] EOB
DCT Matrix=[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
Lum (Tbl #0), MCU=[0,0]
[0x00000117.2]: ZRL=[ 0] Val=[ 0] Coef=[00= DC] Data=[0x 03 FF D9 00 = 0b (--0----- -------- -------- --------)] EOB
[0x00000117.3]: ZRL=[ 0] Val=[ 0] Coef=[01..01] Data=[0x 03 FF D9 00 = 0b (---0---- -------- -------- --------)] EOB
DCT Matrix=[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
Chr(0) (Tbl #1), MCU=[0,0]
[0x00000117.4]: ZRL=[ 0] Val=[ 0] Coef=[00= DC] Data=[0x 03 FF D9 00 = 0b (----0--- -------- -------- --------)] EOB
[0x00000117.5]: ZRL=[ 0] Val=[ 0] Coef=[01..01] Data=[0x 03 FF D9 00 = 0b (-----0-- -------- -------- --------)] EOB
DCT Matrix=[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
SOF では1番目のデータとされている Lum が、圧縮データでは2番目のデータとされている。
これは、SOF に記録された順番である1番目でも、IDの昇順である3番目でもなく、法則はよくわからない。
このような (IDが昇順になっていない) JPEGファイルは、一旦非対応とするのがよさそうだ。
データ数と配置の求め方 (まとめ)
観察と調査の結果、JPEGの圧縮データは8×8のブロックをいくつかセットにした「かたまり」ごとに記録されているらしいことがわかった。
この「かたまり」内のデータの数と配置は、以下のようになっている。
SOF 内に、各コンポーネントのサブサンプリング情報が記録されている。
これは各コンポーネントについて1バイトで表され、上位ニブル (4ビット) が横方向の情報、下位ニブルが縦方向の情報である。
これらのサブサンプリング情報のうち、横方向・縦方向それぞれについて最大のものが、「かたまり」のサイズ (8×8のブロックの数) である。
たとえば、サブサンプリング情報がそれぞれ 0x21, 0x11, 0x11 の場合、横方向の最大値は2、縦方向の最大値は1なので、8×8のブロックを横に2個、縦に1個並べたものを「かたまり」とする。
サブサンプリング情報は、「かたまり」内のそれぞれの方向に、そのコンポーネントのデータ (8×8のブロック) を何個記録するかを表す。
たとえば、上記の例の場合、1個の「かたまり」内に最初のコンポーネントは2個、残りのコンポーネントは1個ずつ記録されている。
「かたまり」内では、SOF で記録されている順番に、各コンポーネントのデータをまとめて配置する。
すなわち、上記の例の場合は、以下の順で配置する。
- 1番目のコンポーネントのデータ1
- 1番目のコンポーネントのデータ2
- 2番目のコンポーネントのデータ1
- 3番目のコンポーネントのデータ1
SOF や SOS に記録されている各コンポーネントのIDが昇順になっていない場合、この法則が成り立たない疑いがある。
このようなファイルは通常作られないと考えられ、非対応としておくのがよさそうだ。
なお、SOF と SOS でコンポーネントのIDの順番が異なる場合は、そもそもファイルが壊れている扱いになるようである。
画像全体では、各行を表す「かたまり」をまとめて配置する。
横方向・縦方向の「かたまり」の数は、それぞれ画像の横サイズ・縦サイズ (ピクセル数) を「かたまり」の横サイズ・縦サイズ (ピクセル数) で割って端数を切り上げた数である。
プログレッシブ……?
「サブサンプリングと画像の劣化の関係の確認」で用いた PNG 画像を、GIMP を用いて JPEG 画像に変換する際に、「プログレッシブ」をオンにしてエクスポートしてみた。
すると、以下の画像が出力された。
ImageMagick の magick compare で比較した結果、画像の内容は「プログレッシブ」オフ、他の条件は同じで出力した画像と完全に一致していた。
「プログレッシブ」でない JPEG 画像には、画像のサイズやコンポーネントなどの情報が格納された「SOF」(FF C0) が含まれている。
一方、「プログレッシブ」である JPEG 画像では、同様の情報を「SOF2」(FF C2) で表現する。
また、「プログレッシブ」でない JPEG 画像では、「DHT」(FF C4) が1セットと、「SOS」(FF DA) + 圧縮データが1セットだけというのが基本である。
一方、「プログレッシブ」である JPEG 画像では、「DHT」と「SOS」が何度も繰り返される (ことがある) ようである。
「プログレッシブ」である JPEG 画像を JPEGsnoop で開いてみると、以下の出力がされた。(抜粋)
*** Marker: SOS (Start of Scan) (xFFDA) ***
OFFSET: 0x000000E4
Scan header length = 12
Number of img components = 3
Component[1]: selector=0x01, table=0(DC),0(AC)
Component[2]: selector=0x02, table=1(DC),0(AC)
Component[3]: selector=0x03, table=1(DC),0(AC)
Spectral selection = 0 .. 0
Successive approximation = 0x01
NOTE: Scan parsing doesn't support this SOF mode.
「プログレッシブ」である JPEG 画像は、JPEGsnoop でも対応していないほど複雑な形式のようである。
まあ、当面の間対応しなくていいか。
今後の展望
これで圧縮データに格納されているデータの種類と数がわかるはずなので、ハフマン符号のデコーダが書けるはずである。
まずは、様々な操作のため要求される、ハフマン符号のデコーダおよびエンコーダを実装するべきだろう。
その応用として、たとえば、サブサンプリングの理解を確認するため、サブサンプリングにより間引かれたデータを補完し、画像の中身を変えずに全て1×1のサブサンプリングに変換してみたい。
JPEG のクロマサブサンプリングと YUVabc - awm-Tech
によれば、間引かれたデータは前のデータと同じものを入れるようである。
また、各ブロックのデータを取り出して単独の画像にするなどして、ブロックの位置と画像内の位置の関係も調べたい。
これには、画像全体の中の位置だけでなく、サブサンプリングにより拡大したブロック内における画像中の位置と8×8ブロックの位置の関係も含まれる。
いずれにせよ、ブロックのデータのエンコードとデコードができるようになれば、目的である「JPEG 画像の無劣化での切り貼り」にかなり近づきそうだ。










