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

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

このコミットは、Go言語の標準ライブラリである image/jpeg パッケージにおけるJPEGエンコーディング機能の改善に関するものです。具体的には、グレースケール画像(*image.Gray型)を正しくグレースケールJPEGとしてエンコードできるようにするための変更が含まれています。

変更されたファイルは以下の通りです。

  • src/pkg/image/jpeg/writer.go: JPEGエンコーダの主要なロジックが含まれるファイル。グレースケール画像のエンコードに対応するための変更が加えられました。
  • src/pkg/image/jpeg/writer_test.go: writer.goの変更を検証するためのテストファイル。グレースケール画像のエンコード・デコードのラウンドトリップテストが追加されました。

コミット

commit 57964db3cb2ec2f3cbb1011a17a8c71d9d2c5b07
Author: Bill Thiede <couchmoney@gmail.com>
Date:   Thu Jun 19 22:18:24 2014 +1000

    image/jpeg: encode *image.Gray as grayscale JPEGs.
    
    Fixes #8201.
    
    LGTM=nigeltao
    R=nigeltao
    CC=golang-codereviews
    https://golang.org/cl/105990046

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

https://github.com/golang/go/commit/57964db3cb2ec2f3cbb1011a17a8c71d9d2c5b07

元コミット内容

image/jpeg: encode *image.Gray as grayscale JPEGs.

このコミットは、Go言語の image/jpeg パッケージが *image.Gray 型の画像をグレースケールJPEGとしてエンコードできるようにするものです。これにより、以前は正しく処理されなかったグレースケール画像のエンコードに関する問題(Issue #8201)が修正されます。

変更の背景

Go言語の image/jpeg パッケージは、JPEG画像をエンコード・デコードするための機能を提供します。しかし、このコミット以前は、image.Gray 型(グレースケール画像)の入力に対して、適切にグレースケールJPEGとしてエンコードする機能が不足していました。

一般的なカラーJPEG画像は、輝度(Y)と2つの色差(Cb, Cr)の3つのコンポーネントで構成されるYCbCr色空間を使用します。一方、グレースケール画像は輝度情報のみを持ち、色差情報はありません。既存の実装では、image.Gray型の画像が入力された場合でも、カラー画像と同様に3つのコンポーネントを前提とした処理が行われるか、あるいはエラーとなる可能性がありました。

この問題は、GoのIssue #8201として報告されていました。ユーザーは image.Gray 画像をJPEGとして保存したいと考えるのが自然であり、その際に不正確なカラーJPEGとしてエンコードされたり、エンコードに失敗したりすることは、ライブラリの使い勝手を損なうものでした。

このコミットは、image.Gray 型の画像を正しく1コンポーネントのグレースケールJPEGとしてエンコードできるようにすることで、この問題を解決し、ライブラリの堅牢性と利便性を向上させることを目的としています。

前提知識の解説

このコミットの変更内容を理解するためには、以下の前提知識が役立ちます。

1. JPEGエンコーディングの基本

JPEG (Joint Photographic Experts Group) は、主に写真などの連続階調画像を圧縮するための標準的な方法です。そのエンコーディングプロセスは、主に以下のステップで構成されます。

  • 色空間変換: 通常、RGB画像はYCbCr色空間に変換されます。Yは輝度(明るさ)を表し、CbとCrは色差を表します。人間の目は輝度情報に敏感で、色差情報には比較的鈍感であるため、色差情報をより強く圧縮することができます。
  • ダウンサンプリング(クロマサブサンプリング): CbとCrコンポーネントは、Yコンポーネントよりも低い解像度で表現されることがあります(例: 4:2:0サブサンプリング)。これにより、色差情報を間引いてデータ量を削減します。グレースケール画像の場合、CbとCrは存在しないため、このステップは適用されません。
  • DCT (Discrete Cosine Transform): 各コンポーネントの画像データは、8x8ピクセルのブロックに分割されます。これらのブロックに対して離散コサイン変換が適用され、空間領域のピクセル値が周波数領域の係数に変換されます。これにより、画像の低周波成分(滑らかな変化)と高周波成分(細かいディテール)が分離されます。
  • 量子化 (Quantization): DCT係数は、量子化テーブルを使用して量子化されます。これは、係数の精度を低下させる非可逆圧縮のステップです。人間の視覚特性に合わせて、高周波成分はより粗く量子化され、データ量が大幅に削減されます。
  • ハフマン符号化 (Huffman Coding): 量子化された係数は、ハフマン符号化などのエントロピー符号化によってさらに圧縮されます。これは可逆圧縮であり、頻繁に出現するデータには短いコードを割り当て、稀なデータには長いコードを割り当てることで、全体のデータ量を削減します。

2. JPEGマーカー

JPEGファイルは、特定の情報を示すための「マーカー」と呼ばれるバイトシーケンスで構成されています。主要なマーカーには以下のようなものがあります。

  • SOI (Start Of Image, 0xFFD8): 画像データの開始を示す。
  • SOF0 (Start Of Frame 0, 0xFFC0): フレームの開始を示す。画像の幅、高さ、コンポーネント数、各コンポーネントのサンプリング係数などの情報が含まれます。グレースケール画像の場合、コンポーネント数は1となります。
  • DQT (Define Quantization Table, 0xFFDB): 量子化テーブルを定義する。
  • DHT (Define Huffman Table, 0xFFC4): ハフマンテーブルを定義する。
  • SOS (Start Of Scan, 0xFFDA): スキャンの開始を示す。どのコンポーネントがスキャンされるか、およびそれらに適用されるハフマンテーブルの情報が含まれます。
  • EOI (End Of Image, 0xFFD9): 画像データの終了を示す。

3. YCbCr色空間とグレースケール

  • YCbCr: 輝度(Y)と2つの色差(Cb, Cr)で構成される色空間です。JPEG圧縮では、この色空間が一般的に使用されます。
  • グレースケール: 色情報を持たず、輝度情報のみで表現される画像です。YCbCr色空間では、CbとCrの成分がゼロまたは無視され、Y成分のみが使用されます。JPEGでは、グレースケール画像は1つのコンポーネント(Y)としてエンコードされます。

4. Go言語の image パッケージ

Go言語の標準ライブラリには、画像処理のための image パッケージが含まれています。

  • image.Image インターフェース: 画像を表すための共通インターフェース。Bounds()ColorModel()At(x, y int) color.Color などのメソッドを持ちます。
  • image.Gray: image.Image インターフェースを実装する具体的な型の一つで、8ビットのグレースケール画像を表します。各ピクセルは1バイトで輝度値(0-255)が格納されます。
  • image.RGBA: image.Image インターフェースを実装する型で、RGBA(赤、緑、青、アルファ)の4つのチャネルを持つカラー画像を表します。

技術的詳細

このコミットの技術的な変更点は、主に src/pkg/image/jpeg/writer.go におけるJPEGエンコーディングロジックの修正と、src/pkg/image/jpeg/writer_test.go におけるテストケースの追加です。

src/pkg/image/jpeg/writer.go の変更点

  1. writeSOF0 関数の変更:

    • writeSOF0 は、JPEGのSOF0 (Start Of Frame 0) マーカーを書き込む関数です。このマーカーは、画像の幅、高さ、およびコンポーネント数などのフレーム情報を定義します。
    • 変更前は、常に3つのカラーコンポーネント(YCbCr)を前提としていました。
    • 変更後、nComponent という新しい引数が追加されました。この引数は、エンコードする画像のコンポーネント数(グレースケールなら1、カラーなら3)を示します。
    • nComponent が1(グレースケール)の場合、コンポーネントのサンプリング係数(e.buf[7])が 0x11 に設定されます。これは、サブサンプリングを行わないことを意味します。カラー画像の場合は、従来の4:2:0クロマサブサンプリングの設定が維持されます。
  2. writeDHT 関数の変更:

    • writeDHT は、JPEGのDHT (Define Huffman Table) マーカーを書き込む関数です。このマーカーは、画像データの圧縮に使用されるハフマンテーブルを定義します。
    • 変更前は、常に輝度(DC/AC)と色差(DC/AC)の4つのハフマンテーブルを書き込んでいました。
    • 変更後、nComponent 引数が追加されました。nComponent が1(グレースケール)の場合、輝度成分のハフマンテーブル(DCとAC)のみを使用するように theHuffmanSpec スライスが調整されます。これにより、グレースケール画像に不要な色差ハフマンテーブルが書き込まれるのを防ぎます。
  3. grayToY 関数の追加:

    • この新しいヘルパー関数は、*image.Gray 型の画像から8x8ピクセルのブロックを読み込み、輝度(Y)ブロックに変換します。
    • image.Gray のピクセルデータはすでに輝度値であるため、単純にピクセル値を block 型(int32の配列)にコピーします。
  4. sosHeaderY 変数の追加:

    • sosHeaderY は、グレースケール画像用のSOS (Start Of Scan) マーカーヘッダーを定義するバイトスライスです。
    • SOSマーカーは、スキャンされるコンポーネントの数と、それらに適用されるハフマンテーブルのIDなどの情報を含みます。
    • sosHeaderY は、1つのコンポーネント(Y)のみがスキャンされることを示し、そのコンポーネントがDCテーブル0とACテーブル0を使用することを指定します。
  5. writeSOS 関数の変更:

    • writeSOS は、JPEGのSOSマーカーと実際の画像データ(スキャンデータ)を書き込む関数です。
    • 変更前は、常にカラー画像(YCbCr)を前提とした処理を行っていました。
    • 変更後、入力画像 m の型を switch 文でチェックするようになりました。
    • もし m*image.Gray 型であれば、sosHeaderY を使用してSOSマーカーを書き込みます。
    • ピクセルデータの処理ループも変更され、グレースケール画像の場合は8x8ブロック単位で grayToY を呼び出し、Y成分のみをエンコードします。CbとCrのブロックは処理されません。
    • カラー画像(デフォルトケース)の場合は、従来のYCbCr変換と4つの8x8ブロック(Yの4ブロック、Cbの1ブロック、Crの1ブロック)を処理するロジックが維持されます。
  6. Encode 関数の変更:

    • Encode は、image.Image をJPEG形式でエンコードするメイン関数です。
    • 変更前は、常に3つのカラーコンポーネントを前提としていました。
    • 変更後、入力画像 m の型をチェックし、*image.Gray 型であれば nComponent を1に設定します。それ以外の場合は3に設定します。
    • この nComponent の値が、writeSOF0writeDHT 関数に渡されるようになりました。これにより、これらの関数が画像のコンポーネント数に応じて適切なJPEGヘッダーとハフマンテーブルを書き込むことができるようになります。

src/pkg/image/jpeg/writer_test.go の変更点

  1. TestWriteGrayscale 関数の追加:
    • この新しいテスト関数は、image.Gray 型の画像をJPEGエンコードし、その後デコードして、元の画像とデコードされた画像の差分を検証します。
    • image.NewGray で32x32ピクセルのグレースケール画像を生成し、各ピクセルにユニークな輝度値を設定します。
    • Encode 関数でJPEGにエンコードし、Decode 関数でデコードします。
    • デコードされた画像が *image.Gray 型であることを確認します。
    • averageDelta 関数を使用して、元の画像とデコードされた画像の平均差分を計算し、許容範囲内であることをアサートします。これにより、グレースケール画像のエンコード・デコードが正しく行われることを保証します。

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

src/pkg/image/jpeg/writer.go

--- a/src/pkg/image/jpeg/writer.go
+++ b/src/pkg/image/jpeg/writer.go
@@ -312,32 +312,44 @@ func (e *encoder) writeDQT() {
 }
 
 // writeSOF0 writes the Start Of Frame (Baseline) marker.
-func (e *encoder) writeSOF0(size image.Point) {
-	const markerlen = 8 + 3*nColorComponent
+func (e *encoder) writeSOF0(size image.Point, nComponent int) {
+	markerlen := 8 + 3*nComponent
 	e.writeMarkerHeader(sof0Marker, markerlen)
 	e.buf[0] = 8 // 8-bit color.
 	e.buf[1] = uint8(size.Y >> 8)
 	e.buf[2] = uint8(size.Y & 0xff)
 	e.buf[3] = uint8(size.X >> 8)
 	e.buf[4] = uint8(size.X & 0xff)
-	e.buf[5] = nColorComponent
-	for i := 0; i < nColorComponent; i++ {
-		e.buf[3*i+6] = uint8(i + 1)
-		// We use 4:2:0 chroma subsampling.
-		e.buf[3*i+7] = "\x22\x11\x11"[i]
-		e.buf[3*i+8] = "\x00\x01\x01"[i]
+	e.buf[5] = uint8(nComponent)
+	if nComponent == 1 {
+		e.buf[6] = 1
+		// No subsampling for grayscale image.
+		e.buf[7] = 0x11
+		e.buf[8] = 0x00
+	} else {
+		for i := 0; i < nComponent; i++ {
+			e.buf[3*i+6] = uint8(i + 1)
+			// We use 4:2:0 chroma subsampling.
+			e.buf[3*i+7] = "\x22\x11\x11"[i]
+			e.buf[3*i+8] = "\x00\x01\x01"[i]
+		}
 	}
-	e.write(e.buf[:3*(nColorComponent-1)+9])
+	e.write(e.buf[:3*(nComponent-1)+9])
 }
 
 // writeDHT writes the Define Huffman Table marker.
-func (e *encoder) writeDHT() {
+func (e *encoder) writeDHT(nComponent int) {
 	markerlen := 2
-	for _, s := range theHuffmanSpec {
+	specs := theHuffmanSpec[:]
+	if nComponent == 1 {
+		// Drop the Chrominance tables.
+		specs = specs[:2]
+	}
+	for _, s := range specs {
 		markerlen += 1 + 16 + len(s.value)
 	}
 	e.writeMarkerHeader(dhtMarker, markerlen)
-	for i, s := range theHuffmanSpec {
+	for i, s := range specs {
 		e.writeByte("\x00\x10\x01\x11"[i])
 		e.write(s.count[:])
 		e.write(s.value)
@@ -390,6 +402,20 @@ func toYCbCr(m image.Image, p image.Point, yBlock, cbBlock, crBlock *block) {
 	}
 }
 
+// grayToY stores the 8x8 region of m whose top-left corner is p in yBlock.
+func grayToY(m *image.Gray, p image.Point, yBlock *block) {
+	b := m.Bounds()
+	xmax := b.Max.X - 1
+	ymax := b.Max.Y - 1
+	pix := m.Pix
+	for j := 0; j < 8; j++ {
+		for i := 0; i < 8; i++ {
+			idx := m.PixOffset(min(p.X+i, xmax), min(p.Y+j, ymax))
+			yBlock[8*j+i] = int32(pix[idx])
+		}
+	}
+}
+
 // rgbaToYCbCr is a specialized version of toYCbCr for image.RGBA images.
 func rgbaToYCbCr(m *image.RGBA, p image.Point, yBlock, cbBlock, crBlock *block) {
  b := m.Bounds()
@@ -430,7 +456,18 @@ func scale(dst *block, src *[4]block) {
 	}
 }
 
-// sosHeader is the SOS marker "\xff\xda" followed by 12 bytes:
+// sosHeaderY is the SOS marker "\xff\xda" followed by 8 bytes:
+//	- the marker length "\x00\x08",
+//	- the number of components "\x01",
+//	- component 1 uses DC table 0 and AC table 0 "\x01\x00",
+//	- the bytes "\x00\x3f\x00". Section B.2.3 of the spec says that for
+//	  sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al)
+//	  should be 0x00, 0x3f, 0x00<<4 | 0x00.
+var sosHeaderY = []byte{
+	0xff, 0xda, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00,
+}
+
+// sosHeaderYCbCr is the SOS marker "\xff\xda" followed by 12 bytes:
 //	- the marker length "\x00\x0c",
 //	- the number of components "\x03",
 //	- component 1 uses DC table 0 and AC table 0 "\x01\x00",
@@ -439,40 +476,54 @@ func scale(dst *block, src *[4]block) {
 //	- the bytes "\x00\x3f\x00". Section B.2.3 of the spec says that for
 //	  sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al)
 //	  should be 0x00, 0x3f, 0x00<<4 | 0x00.
-var sosHeader = []byte{
+var sosHeaderYCbCr = []byte{
 	0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02,
 	0x11, 0x03, 0x11, 0x00, 0x3f, 0x00,
 }
 
 // writeSOS writes the StartOfScan marker.
 func (e *encoder) writeSOS(m image.Image) {
-\te.write(sosHeader)\n+\tswitch m.(type) {\n+\tcase *image.Gray:\n+\t\te.write(sosHeaderY)\n+\tdefault:\n+\t\te.write(sosHeaderYCbCr)\n+\t}\n 	var (\n 		// Scratch buffers to hold the YCbCr values.\n 		// The blocks are in natural (not zig-zag) order.\n 		b, cb, cr [4]block\n 		prevDCY, prevDCCb, prevDCCr int32
 	)
 	bounds := m.Bounds()
-\trgba, _ := m.(*image.RGBA)\n-\tfor y := bounds.Min.Y; y < bounds.Max.Y; y += 16 {\n-\t\tfor x := bounds.Min.X; x < bounds.Max.X; x += 16 {\n-\t\t\tfor i := 0; i < 4; i++ {\n-\t\t\t\txOff := (i & 1) * 8\n-\t\t\t\tyOff := (i & 2) * 4\n-\t\t\t\tp := image.Pt(x+xOff, y+yOff)\n-\t\t\t\tif rgba != nil {\n-\t\t\t\t\trgbaToYCbCr(rgba, p, &b, &cb[i], &cr[i])\n-\t\t\t\t} else {\n-\t\t\t\t\ttoYCbCr(m, p, &b, &cb[i], &cr[i])\n-\t\t\t\t}\n+\tswitch m := m.(type) {\n+\t// TODO(wathiede): switch on m.ColorModel() instead of type.\n+\tcase *image.Gray:\n+\t\tfor y := bounds.Min.Y; y < bounds.Max.Y; y += 8 {\n+\t\t\tfor x := bounds.Min.X; x < bounds.Max.X; x += 8 {\n+\t\t\t\tp := image.Pt(x, y)\n+\t\t\t\tgrayToY(m, p, &b)\n \t\t\t\tprevDCY = e.writeBlock(&b, 0, prevDCY)\n \t\t\t}\n-\t\t\tscale(&b, &cb)\n-\t\t\tprevDCCb = e.writeBlock(&b, 1, prevDCCb)\n-\t\t\tscale(&b, &cr)\n-\t\t\tprevDCCr = e.writeBlock(&b, 1, prevDCCr)\n+\t\t}\n+\tdefault:\n+\t\trgba, _ := m.(*image.RGBA)\n+\t\tfor y := bounds.Min.Y; y < bounds.Max.Y; y += 16 {\n+\t\t\tfor x := bounds.Min.X; x < bounds.Max.X; x += 16 {\n+\t\t\t\tfor i := 0; i < 4; i++ {\n+\t\t\t\t\txOff := (i & 1) * 8\n+\t\t\t\t\tyOff := (i & 2) * 4\n+\t\t\t\t\tp := image.Pt(x+xOff, y+yOff)\n+\t\t\t\t\tif rgba != nil {\n+\t\t\t\t\t\trgbaToYCbCr(rgba, p, &b, &cb[i], &cr[i])\n+\t\t\t\t\t} else {\n+\t\t\t\t\t\ttoYCbCr(m, p, &b, &cb[i], &cr[i])\n+\t\t\t\t\t}\n+\t\t\t\t\tprevDCY = e.writeBlock(&b, 0, prevDCY)\n+\t\t\t\t}\n+\t\t\t\tscale(&b, &cb)\n+\t\t\t\tprevDCCb = e.writeBlock(&b, 1, prevDCCb)\n+\t\t\t\tscale(&b, &cr)\n+\t\t\t\tprevDCCr = e.writeBlock(&b, 1, prevDCCr)\n+\t\t\t}\n \t\t}\n \t}\n 	// Pad the last byte with 1\'s.\n@@ -532,9 +586,16 @@ func Encode(w io.Writer, m image.Image, o *Options) error {\n \t\t\te.quant[i][j] = uint8(x)\n \t\t}\n \t}\n+\t// Compute number of components based on input image type.\n+\tnComponent := 3\n+\tswitch m.(type) {\n+\t// TODO(wathiede): switch on m.ColorModel() instead of type.\n+\tcase *image.Gray:\n+\t\tnComponent = 1\n+\t}\n \t// Write the Start Of Image marker.\n \te.buf[0] = 0xff\n \te.buf[1] = 0xd8\n \te.write(e.buf[:2])\n \t// Write the quantization tables.\n \te.writeDQT()\n \t// Write the image dimensions.\n-\te.writeSOF0(b.Size())\n+\te.writeSOF0(b.Size(), nComponent)\n \t// Write the Huffman tables.\n-\te.writeDHT()\n+\te.writeDHT(nComponent)\n \t// Write the image data.\n \te.writeSOS(m)\n \t// Write the End Of Image marker.\n```

### `src/pkg/image/jpeg/writer_test.go`

```diff
--- a/src/pkg/image/jpeg/writer_test.go
+++ b/src/pkg/image/jpeg/writer_test.go
@@ -160,6 +160,34 @@ func TestWriter(t *testing.T) {
 	}\n}\n\n+// TestWriteGrayscale tests that a grayscale images survives a round-trip\n+// through encode/decode cycle.\n+func TestWriteGrayscale(t *testing.T) {\n+\tm0 := image.NewGray(image.Rect(0, 0, 32, 32))\n+\tfor i := range m0.Pix {\n+\t\tm0.Pix[i] = uint8(i)\n+\t}\n+\tvar buf bytes.Buffer\n+\tif err := Encode(&buf, m0, nil); err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\tm1, err := Decode(&buf)\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\tif m0.Bounds() != m1.Bounds() {\n+\t\tt.Fatalf(\"bounds differ: %v and %v\", m0.Bounds(), m1.Bounds())\n+\t}\n+\tif _, ok := m1.(*image.Gray); !ok {\n+\t\tt.Errorf(\"got %T, want *image.Gray\", m1)\n+\t}\n+\t// Compare the average delta to the tolerance level.\n+\twant := int64(2 << 8)\n+\tif got := averageDelta(m0, m1); got > want {\n+\t\tt.Errorf(\"average delta too high; got %d, want <= %d\", got, want)\n+\t}\n+}\n+\n // averageDelta returns the average delta in RGB space. The two images must\n // have the same bounds.\n func averageDelta(m0, m1 image.Image) int64 {

コアとなるコードの解説

このコミットの核心は、JPEGエンコーダが入力画像のタイプ(特に image.Gray)を認識し、それに応じてJPEGストリームの構造を動的に調整する能力を獲得した点にあります。

  1. コンポーネント数の動的な決定 (Encode 関数):

    • Encode 関数内で、入力された image.Image の具体的な型が *image.Gray であるかどうかが switch m.(type) を用いてチェックされます。
    • もし *image.Gray であれば、JPEGのコンポーネント数 nComponent1 (輝度のみ) に設定されます。
    • それ以外の型(例: *image.RGBA)であれば、従来の 3 (YCbCr) に設定されます。
    • この nComponent の値が、後続の writeSOF0writeDHT 関数に引数として渡されることで、JPEGヘッダーの生成が適切に分岐されます。
  2. SOF0マーカーの調整 (writeSOF0 関数):

    • writeSOF0 は、JPEGファイルの冒頭に書かれるSOF0マーカーを生成します。このマーカーには、画像の幅、高さ、そして「コンポーネント数」が含まれます。
    • nComponent1 の場合、SOF0マーカー内のコンポーネント数フィールドが 1 に設定されます。
    • さらに重要なのは、グレースケール画像ではクロマサブサンプリングが不要であるため、コンポーネントのサンプリング係数(e.buf[7])が 0x11 に設定される点です。これは、水平・垂直方向ともにサブサンプリングを行わないことを意味します。カラー画像の場合は、従来の 0x22 (4:2:0サブサンプリング) などが維持されます。
  3. ハフマンテーブルの選択 (writeDHT 関数):

    • writeDHT は、JPEG圧縮に使用されるハフマンテーブルを定義します。カラーJPEGでは輝度用と色差用のハフマンテーブルが必要ですが、グレースケールJPEGでは輝度用のみで十分です。
    • nComponent1 の場合、theHuffmanSpec スライスが specs[:2] とスライスされ、輝度成分(DCとAC)のハフマンテーブルのみが書き込まれるように調整されます。これにより、ファイルサイズが最適化され、不要なテーブルの定義が避けられます。
  4. SOSマーカーとスキャンデータの処理分岐 (writeSOS 関数):

    • writeSOS は、実際の画像データ(スキャンデータ)の開始を示すSOSマーカーを書き込み、その後に圧縮されたピクセルデータを続けます。
    • ここでも switch m := m.(type) を使用して、入力画像が *image.Gray 型であるかどうかが判断されます。
    • *image.Gray の場合、新しく定義された sosHeaderY が使用されます。このヘッダーは、1つのコンポーネント(Y)のみがスキャンされることを明示します。
    • ピクセルデータの処理ループも、グレースケール画像の場合は8x8ブロック単位で grayToY 関数を呼び出すように変更されます。grayToYimage.Gray から直接輝度ブロックを生成するため、YCbCr変換は行われません。
    • カラー画像の場合は、従来の16x16マクロブロック単位でのYCbCr変換と、Y、Cb、Crの各ブロックの処理が維持されます。
  5. grayToY ヘルパー関数の導入:

    • grayToY は、*image.Gray 型の画像から8x8ピクセルの領域を抽出し、それをDCT処理に適した block 型(int32の配列)に変換する専用の関数です。これにより、グレースケール画像のピクセルデータ取得が効率的かつ正確に行われます。

これらの変更により、Goの image/jpeg パッケージは、image.Gray 型の画像をJPEG標準に準拠したグレースケールJPEGとして正しくエンコードできるようになり、より幅広い画像タイプに対応できるようになりました。

関連リンク

参考にした情報源リンク