diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 1c87cea..285b4e2 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -42,3 +42,9 @@ flutter { source = "../.." } + +dependencies { + // ImageStatistics の数理ロジック検証用(実機不要の JVM 単体テスト). + // 実行: android ディレクトリで `./gradlew test` + testImplementation("junit:junit:4.13.2") +} diff --git a/android/app/src/main/kotlin/com/example/mini_tias/ImageStatistics.kt b/android/app/src/main/kotlin/com/example/mini_tias/ImageStatistics.kt index cf3afe2..b3ab4bc 100644 --- a/android/app/src/main/kotlin/com/example/mini_tias/ImageStatistics.kt +++ b/android/app/src/main/kotlin/com/example/mini_tias/ImageStatistics.kt @@ -2,6 +2,7 @@ import android.media.Image import java.nio.ByteOrder +import java.nio.ShortBuffer /** * RAW_SENSOR 画像から画素統計を計算するユーティリティ. @@ -28,11 +29,26 @@ val buffer = plane.buffer buffer.rewind() buffer.order(ByteOrder.LITTLE_ENDIAN) - val shortBuf = buffer.asShortBuffer() - val rowStrideShorts = plane.rowStride / 2 - val srcW = image.width - val srcH = image.height + return computeFromShortBuffer( + buffer.asShortBuffer(), + plane.rowStride / 2, + image.width, + image.height, + ) + } + /** + * [shortBuf](10-bit RAW を short で保持)から画素統計を計算する. + * + * `android.media.Image` に依存しない純粋な数理計算のため,JVM 単体テスト(JUnit)で + * 検証できる.[compute] はバッファ読み出しのみを担い,本関数に委譲する. + */ + internal fun computeFromShortBuffer( + shortBuf: ShortBuffer, + rowStrideShorts: Int, + srcW: Int, + srcH: Int, + ): Map { // 集計用変数 var sumR = 0.0; var sumG = 0.0; var sumB = 0.0 var countR = 0; var countG = 0; var countB = 0 diff --git a/android/app/src/test/kotlin/com/example/mini_tias/ImageStatisticsTest.kt b/android/app/src/test/kotlin/com/example/mini_tias/ImageStatisticsTest.kt new file mode 100644 index 0000000..67967f8 --- /dev/null +++ b/android/app/src/test/kotlin/com/example/mini_tias/ImageStatisticsTest.kt @@ -0,0 +1,89 @@ +package com.example.mini_tias + +import java.nio.ShortBuffer +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * [ImageStatistics] の数理ロジック(mean/max/p99/飽和率・低露光率)の単体テスト. + * + * 合成した 10-bit BGGR Bayer データを `computeFromShortBuffer` に与えて期待値を検証する. + * 実機・Android Framework に依存しない純粋な JVM テスト(実行: `./gradlew test`). + * + * BGGR 配列の 2×2 ブロック走査: + * (row, col) 偶数行: B G B G ... + * 奇数行: G R G R ... + * 1 ブロック = B=(0,0), G=(0,1), G=(1,0), R=(1,1).G はブロック内 2 サンプルの平均. + */ +class ImageStatisticsTest { + + private fun compute(values: ShortArray, width: Int, height: Int): Map = + ImageStatistics.computeFromShortBuffer( + ShortBuffer.wrap(values), + width, // rowStrideShorts(パディングなし) + width, + height, + ) + + @Test + fun `一様な値の場合 mean と max が一致し飽和・低露光は 0 になる`() { + // 4×4 すべて 500 + val values = ShortArray(16) { 500 } + val result = compute(values, 4, 4) + + val mean = result["mean_per_channel"] as Map<*, *> + assertEquals(500.0, mean["R"] as Double, 1e-9) + assertEquals(500.0, mean["G"] as Double, 1e-9) + assertEquals(500.0, mean["B"] as Double, 1e-9) + + val max = result["max_per_channel"] as Map<*, *> + assertEquals(500, max["R"] as Int) + assertEquals(500, max["G"] as Int) + assertEquals(500, max["B"] as Int) + + assertEquals(0.0, result["saturated_pixel_ratio"] as Double, 1e-9) + assertEquals(0.0, result["underexposed_pixel_ratio"] as Double, 1e-9) + + // p99: 500 は bin 125(500*256/1024)→ (125+1)*1024/256 - 1 = 503 + val p99 = result["p99_per_channel"] as Map<*, *> + assertEquals(503, p99["R"] as Int) + assertEquals(503, p99["G"] as Int) + assertEquals(503, p99["B"] as Int) + } + + @Test + fun `飽和画素と低露光画素がチャネル別 mean・max・比率に正しく反映される`() { + // 4×4.左上ブロックの B=1000(飽和), G の片側=50(低露光).他はすべて 500. + // row0: 1000 50 500 500 + // row1: 500 500 500 500 + // row2: 500 500 500 500 + // row3: 500 500 500 500 + val values = shortArrayOf( + 1000, 50, 500, 500, + 500, 500, 500, 500, + 500, 500, 500, 500, + 500, 500, 500, 500, + ) + val result = compute(values, 4, 4) + + val mean = result["mean_per_channel"] as Map<*, *> + // B: (1000+500+500+500)/4 = 625 + assertEquals(625.0, mean["B"] as Double, 1e-9) + // G: ((50+500)/2 + 500 + 500 + 500)/4 = (275+1500)/4 = 443.75 + assertEquals(443.75, mean["G"] as Double, 1e-9) + // R: すべて 500 + assertEquals(500.0, mean["R"] as Double, 1e-9) + + val max = result["max_per_channel"] as Map<*, *> + assertEquals(1000, max["B"] as Int) + assertEquals(500, max["R"] as Int) + + // 飽和は B=1000 の 1 画素,低露光は G=50 の 1 画素.総 Bayer 画素 = 16. + assertEquals(1.0 / 16.0, result["saturated_pixel_ratio"] as Double, 1e-9) + assertEquals(1.0 / 16.0, result["underexposed_pixel_ratio"] as Double, 1e-9) + + val thresholds = result["thresholds"] as Map<*, *> + assertEquals(1000, thresholds["saturated"] as Int) + assertEquals(100, thresholds["underexposed"] as Int) + } +} diff --git "a/docs/01_GUIDE/GUIDE_07_\343\203\206\343\202\271\343\203\210\346\226\271\351\207\235.md" "b/docs/01_GUIDE/GUIDE_07_\343\203\206\343\202\271\343\203\210\346\226\271\351\207\235.md" index ba62567..a1d40e1 100644 --- "a/docs/01_GUIDE/GUIDE_07_\343\203\206\343\202\271\343\203\210\346\226\271\351\207\235.md" +++ "b/docs/01_GUIDE/GUIDE_07_\343\203\206\343\202\271\343\203\210\346\226\271\351\207\235.md" @@ -7,6 +7,7 @@ - 初期フェーズではカメラ・ストレージ等のハードウェア依存が大きいため,**実機での手動確認を主体** とする - ハードウェアに依存しないロジック(ファイル命名,重複回避等)には **Unit テスト** を書く - テストは `flutter test` で実行できる状態を維持する +- ネイティブ(Kotlin)コードは原則として実機手動確認に委ねるが,**研究データの正確性に直結する純粋な数理ロジック**(例: 画素統計の mean/max/p99/飽和率)は Android Framework に依存しない形に分離し,**JUnit で単体テスト**する(実機不要・`./gradlew test` で実行) ## テストの種類と対象 (Test Types)