Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14820] ファイルの概要

このコミットは、Go言語の image/jpeg パッケージにおいて、サンプリング比率が1x1ではない(通常ではない)グレースケールJPEG画像を適切に処理できるようにするための修正です。具体的には、グレースケール画像の場合、JPEG標準の規定に従い、サンプリング比率を強制的に1x1として扱うように変更されました。これにより、特定のグレースケールJPEG画像のデコード時に発生していた問題(Issue #4259)が解決されます。

コミット

commit 30ff0636b77f0e64084cc976e927355edb6d6f36
Author: Nigel Tao <nigeltao@golang.org>
Date:   Mon Jan 7 16:16:11 2013 +1100

    image/jpeg: handle those (unusual) grayscale images whose sampling
    ratio isn't 1x1.
    
    Fixes #4259.
    
    The test data was generated by
    cjpeg -quality 50 -sample 2x2 video-005.gray.pgm > video-005.gray.q50.2x2.jpeg
    cjpeg -quality 50 -sample 2x2 -progressive video-005.gray.pg0 > video-005.gray.q50.2x2.progressive.jpeg
    
    similarly to video-005.gray.q50.* from
    http://code.google.com/p/go/source/detail?r=51f26e36ba98
    the key difference being the "-sample 2x2".
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/7069045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/30ff0636b77f0e64084cc976e927355edb6d6f36

元コミット内容

image/jpeg: handle those (unusual) grayscale images whose sampling ratio isn't 1x1.

このコミットは、サンプリング比率が1x1ではない(通常ではない)グレースケール画像を処理するように image/jpeg パッケージを修正します。

Fixes #4259.

Issue #4259 を修正します。

The test data was generated by cjpeg -quality 50 -sample 2x2 video-005.gray.pgm > video-005.gray.q50.2x2.jpeg cjpeg -quality 50 -sample 2x2 -progressive video-005.gray.pgm > video-005.gray.q50.2x2.progressive.jpeg

テストデータは、cjpeg コマンドを用いて -sample 2x2 オプションを指定して生成されました。

similarly to video-005.gray.q50.* from http://code.google.com/p/go/source/detail?r=51f26e36ba98 the key difference being the "-sample 2x2".

これは、以前の video-005.gray.q50.* テストデータと同様ですが、-sample 2x2 が指定されている点が異なります。

変更の背景

この変更の背景には、Go言語の image/jpeg パッケージが、特定の(通常ではない)グレースケールJPEG画像をデコードする際にパニックを起こすというバグ(Issue #4259)がありました。この問題は、グレースケール画像であるにもかかわらず、サンプリング比率が1x1ではない形式でエンコードされたJPEGファイルで発生していました。

JPEG標準では、グレースケール画像(コンポーネントが1つのみの画像)の場合、サンプリング比率がどのように設定されていても、実質的には1x1として扱われるべきであると規定されています。しかし、image/jpeg パッケージの実装がこの特殊なケースを適切に処理していなかったため、デコード処理中に予期せぬエラーが発生していました。

このコミットは、この標準の解釈をコードに反映させ、グレースケール画像の場合には常にサンプリング比率を1x1として扱うことで、このバグを修正することを目的としています。

前提知識の解説

JPEG圧縮の基本

JPEG (Joint Photographic Experts Group) は、主に写真などの連続階調画像を効率的に圧縮するための標準です。JPEG圧縮は、主に以下のステップで構成されます。

  1. 色空間変換 (Color Space Conversion): RGBなどの色空間から、輝度 (Y) と色差 (Cb, Cr) に分離するYCbCr色空間に変換します。人間の目は輝度情報に敏感で、色差情報には比較的鈍感であるという特性を利用します。グレースケール画像の場合、輝度成分 (Y) のみを使用します。
  2. クロマサブサンプリング (Chroma Subsampling): 色差情報 (Cb, Cr) は、輝度情報に比べて解像度を下げても視覚的な劣化が少ないため、ダウンサンプリングされます。これが「サンプリング比率」に関わる部分です。例えば、4:2:0サンプリングでは、輝度情報をフル解像度で保持し、色差情報を水平・垂直ともに半分にダウンサンプリングします。
    • サンプリング比率の表記: 一般的に H:V:J の形式で表されます。
      • H: 水平方向の輝度サンプリングの基準ブロック幅(通常4)。
      • V: 垂直方向の輝度サンプリングの基準ブロック高さ(通常1または2)。
      • J: 輝度ブロック内の色差サンプルの数。
      • 例:
        • 4:4:4: 色差情報も輝度情報と同じ解像度で保持(サブサンプリングなし)。
        • 4:2:2: 水平方向に色差情報を半分にダウンサンプリング。
        • 4:2:0: 水平・垂直方向に色差情報を半分にダウンサンプリング。
        • 4:1:1: 水平方向に色差情報を1/4にダウンサンプリング。
    • コンポーネントごとのサンプリングファクタ (h, v): JPEGファイル内部では、各コンポーネント(Y, Cb, Cr)に対して水平サンプリングファクタ h と垂直サンプリングファクタ v が定義されます。これは、そのコンポーネントがMCU (Minimum Coded Unit) 内でどれだけのブロックを占めるかを示します。例えば、h=2, v=2 は、そのコンポーネントがMCU内で2x2のブロックを占めることを意味します。
  3. DCT (Discrete Cosine Transform): 各8x8ピクセルブロックに対して離散コサイン変換を適用し、空間領域のデータを周波数領域のデータに変換します。
  4. 量子化 (Quantization): DCT係数を量子化テーブルで割り、高周波成分(視覚的に重要度の低い情報)を間引きます。これにより非可逆圧縮が実現されます。
  5. エントロピー符号化 (Entropy Encoding): 量子化された係数をハフマン符号化や算術符号化などのエントロピー符号化でさらに圧縮します。

MCU (Minimum Coded Unit)

MCUは、JPEG圧縮における処理の最小単位です。複数のデータユニット(8x8ピクセルブロック)で構成されます。MCUのサイズは、各コンポーネントのサンプリングファクタ (h, v) に依存します。例えば、4:2:0サンプリングの場合、1つのMCUは4つのYブロック、1つのCbブロック、1つのCrブロックで構成されます。

グレースケールJPEGの特殊性

グレースケールJPEG画像は、輝度コンポーネント (Y) のみを持つため、色差情報のサブサンプリングは関係ありません。JPEG標準のセクションA.2およびA.2.2では、単一コンポーネント(グレースケール)の画像の場合、「データは定義上、非インターリーブである」と述べられています。また、スキャン内のデータユニットの順序は、H_1とV_1の値に関わらず、左から右、上から下であるべきだとされています。

さらに、セクション4.8.2では、「非インターリーブデータの場合、MCUは1つのデータユニットとして定義される」と説明されています。これは、グレースケール画像の場合、各MCUが常に1つの8x8ピクセルブロックに対応することを意味します。

セクションA.1.1では、コンポーネントの (h, v) 値は、そのコンポーネントのサンプリングファクタと、すべてのコンポーネントの最大サンプリングファクタとの比率が重要であると説明されています。グレースケール画像の場合、唯一のコンポーネント(Y)が常に最大のサンプリングファクタを持つため、その比率は常に1になります。したがって、グレースケール画像のコンポーネントの (h, v) は、名目上の値が何であっても、実質的には常に (1, 1) として扱われるべきです。

このコミットは、このJPEG標準の規定をGoの image/jpeg パッケージに適用し、グレースケール画像の場合には d.comp[i].hd.comp[i].v を強制的に1に設定することで、この特殊なケースを正しく処理するようにします。

技術的詳細

このコミットの核心は、JPEG標準のグレースケール画像(単一コンポーネント画像)に関する規定をGoの image/jpeg デコーダに適用することです。

通常のカラーJPEG画像では、輝度 (Y) および色差 (Cb, Cr) コンポーネントはそれぞれ異なるサンプリングファクタ (h, v) を持つことがあり、これによりクロマサブサンプリングが実現されます。デコーダは、これらの (h, v) 値に基づいてMCUの構造を決定し、データをデコードします。

しかし、グレースケール画像の場合、コンポーネントは輝度 (Y) のみです。JPEG標準は、このような単一コンポーネントの画像に対して特別な扱いを規定しています。

  • セクションA.2 および A.2.2: グレースケール画像は「非インターリーブ」であり、スキャン内のデータユニットの順序は、名目上のサンプリングファクタ (H_1, V_1) に関係なく、常に左から右、上から下であると述べています。
  • セクション4.8.2: 非インターリーブデータの場合、「MCUは1つのデータユニットとして定義される」と明記しています。これは、グレースケール画像では、各MCUが常に1つの8x8ピクセルブロックに対応することを意味します。
  • セクションA.1.1: コンポーネントの (h, v) 値は、そのコンポーネントのサンプリングファクタと、すべてのコンポーネントの最大サンプリングファクタとの比率が重要であると説明しています。グレースケール画像の場合、唯一のコンポーネントが常に最大のサンプリングファクタを持つため、この比率は常に1になります。したがって、グレースケール画像のコンポーネントの (h, v) は、名目上の値が (2, 1) のようなものであっても、実質的には常に (1, 1) として扱われるべきです。

このコミット以前の image/jpeg パッケージは、グレースケール画像であっても、ファイルヘッダに記述された名目上の (h, v) 値をそのまま使用していました。このため、例えば -sample 2x2 のように、グレースケール画像に対して非1x1のサンプリングファクタが指定された場合、デコーダがJPEG標準の意図と異なるMCU構造を期待し、結果としてパニックや不正なデコードを引き起こしていました。

この修正は、processSOF 関数内で、デコード対象の画像がグレースケール(d.nComp == nGrayComponent)である場合に、コンポーネントの hv の値を強制的に 1 に設定することで、この問題を解決します。これにより、デコーダはグレースケール画像を常に標準に準拠した形で処理し、名目上のサンプリングファクタに惑わされることなく、正しいMCU構造を構築できるようになります。

コアとなるコードの変更箇所

src/pkg/image/jpeg/reader.go ファイルの func (d *decoder) processSOF(n int) error 関数内が変更されています。

--- a/src/pkg/image/jpeg/reader.go
+++ b/src/pkg/image/jpeg/reader.go
@@ -147,14 +147,27 @@ func (d *decoder) processSOF(n int) error {
 		return UnsupportedError("SOF has wrong number of image components")
 	}
 	for i := 0; i < d.nComp; i++ {
-		hv := d.tmp[7+3*i]
-		d.comp[i].h = int(hv >> 4)
-		d.comp[i].v = int(hv & 0x0f)
 		d.comp[i].c = d.tmp[6+3*i]
 		d.comp[i].tq = d.tmp[8+3*i]
 		if d.nComp == nGrayComponent {
+			// If a JPEG image has only one component, section A.2 says "this data
+			// is non-interleaved by definition" and section A.2.2 says "[in this
+			// case...] the order of data units within a scan shall be left-to-right
+			// and top-to-bottom... regardless of the values of H_1 and V_1". Section
+			// 4.8.2 also says "[for non-interleaved data], the MCU is defined to be
+			// one data unit". Similarly, section A.1.1 explains that it is the ratio
+			// of H_i to max_j(H_j) that matters, and similarly for V. For grayscale
+			// images, H_1 is the maximum H_j for all components j, so that ratio is
+			// always 1. The component's (h, v) is effectively always (1, 1): even if
+			// the nominal (h, v) is (2, 1), a 20x5 image is encoded in three 8x8
+			// MCUs, not two 16x8 MCUs.
+			d.comp[i].h = 1
+			d.comp[i].v = 1
+			continue
+		}
+		hv := d.tmp[7+3*i]
+		d.comp[i].h = int(hv >> 4)
+		d.comp[i].v = int(hv & 0x0f)
 		// For color images, we only support 4:4:4, 4:4:0, 4:2:2 or 4:2:0 chroma
 		// downsampling ratios. This implies that the (h, v) values for the Y
 		// component are either (1, 1), (1, 2), (2, 1) or (2, 2), and the (h, v)

また、テストファイル src/pkg/image/jpeg/reader_test.go に新しいテストケースが追加されています。

--- a/src/pkg/image/jpeg/reader_test.go
+++ b/src/pkg/image/jpeg/reader_test.go
@@ -24,6 +24,7 @@ func TestDecodeProgressive(t *testing.T) {
 		"../testdata/video-001.q50.440",
 		"../testdata/video-001.q50.444",
 		"../testdata/video-005.gray.q50",
+		"../testdata/video-005.gray.q50.2x2",
 	}
 	for _, tc := range testCases {
 		m0, err := decodeFile(tc + ".jpeg")

さらに、新しいテストデータファイルが追加されています。

  • src/pkg/image/testdata/video-005.gray.q50.2x2.jpeg
  • src/pkg/image/testdata/video-005.gray.q50.2x2.progressive.jpeg

これらのテストデータは、cjpeg -sample 2x2 オプションを使用して生成された、サンプリング比率が1x1ではないグレースケールJPEG画像です。

コアとなるコードの解説

変更の中心は src/pkg/image/jpeg/reader.goprocessSOF 関数内のループです。このループは、JPEGファイルのSOF (Start Of Frame) マーカーから各画像コンポーネントの情報を読み取ります。

変更前は、すべてのコンポーネントに対して一律に hv バイトから h (水平サンプリングファクタ) と v (垂直サンプリングファクタ) を抽出していました。

hv := d.tmp[7+3*i]
d.comp[i].h = int(hv >> 4)
d.comp[i].v = int(hv & 0x0f)

変更後は、まず現在の画像がグレースケール画像(d.nComp == nGrayComponent、つまりコンポーネント数が1の場合)であるかどうかをチェックします。

if d.nComp == nGrayComponent {
    // ... (コメント) ...
    d.comp[i].h = 1
    d.comp[i].v = 1
    continue
}

もしグレースケール画像であれば、上記の長いコメントで説明されているJPEG標準の規定に基づき、d.comp[i].hd.comp[i].v を強制的に 1 に設定します。そして continue することで、通常の hv からの抽出処理をスキップします。

この変更により、image/jpeg デコーダは、グレースケール画像の場合には、ファイルヘッダに記述された名目上のサンプリングファクタが何であっても、常に (h, v) = (1, 1) として処理するようになります。これにより、JPEG標準に準拠し、非標準的なグレースケールJPEG画像でも正しくデコードできるようになります。

新しいテストデータ (video-005.gray.q50.2x2.jpeg および video-005.gray.q50.2x2.progressive.jpeg) は、この修正が正しく機能することを確認するために追加されました。これらのファイルは、cjpeg ツールで -sample 2x2 オプションを使用して生成されており、修正前のGoのデコーダでは問題を引き起こす可能性がありました。

関連リンク

参考にした情報源リンク