package com.example.mini_tias import android.media.Image import java.nio.ByteOrder import java.nio.ShortBuffer /** * RAW_SENSOR 画像から画素統計を計算するユーティリティ. * * Bayer 配列 (BGGR) を 2×2 ブロック単位で 1 パス走査し,チャネル別の * 平均・最大・P99 値,飽和率・低露光率を返す.meta.json の `image_statistics` に格納する. */ internal object ImageStatistics { const val SATURATION_THRESHOLD = 1000 const val UNDEREXPOSED_THRESHOLD = 100 const val STATISTICS_HISTOGRAM_BINS = 256 /** 10-bit RAW センサー値の範囲(0〜1023 の 1024 通り).ヒストグラム bin 変換に使用する.*/ const val RAW_10BIT_RANGE = 1024 /** * [image](RAW_SENSOR)から画素統計を計算する. * * 計算失敗時は呼び出し元で null として扱う. */ fun compute(image: Image): Map { val plane = image.planes[0] val buffer = plane.buffer buffer.rewind() buffer.order(ByteOrder.LITTLE_ENDIAN) 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 var maxR = 0; var maxG = 0; var maxB = 0 var saturatedCount = 0 var underexposedCount = 0 val histR = IntArray(STATISTICS_HISTOGRAM_BINS) val histG = IntArray(STATISTICS_HISTOGRAM_BINS) val histB = IntArray(STATISTICS_HISTOGRAM_BINS) val row1 = ShortArray(rowStrideShorts) val row2 = ShortArray(rowStrideShorts) // 2 行ずつ読み,2×2 BGGR ブロックを処理 var y = 0 while (y < srcH - 1) { shortBuf.position(y * rowStrideShorts) shortBuf.get(row1, 0, rowStrideShorts) shortBuf.position((y + 1) * rowStrideShorts) shortBuf.get(row2, 0, rowStrideShorts) var x = 0 while (x < srcW - 1) { val b = row1[x].toInt() and 0x3FF // (0,0) B val g1 = row1[x + 1].toInt() and 0x3FF // (0,1) G val g2 = row2[x].toInt() and 0x3FF // (1,0) G val r = row2[x + 1].toInt() and 0x3FF // (1,1) R // チャネル別集計 sumR += r; countR++; if (r > maxR) maxR = r sumB += b; countB++; if (b > maxB) maxB = b val gAvg = (g1 + g2) / 2 sumG += gAvg; countG++; if (gAvg > maxG) maxG = gAvg // ヒストグラム(10-bit 値 → 256 bin にマップ) histR[ (r * STATISTICS_HISTOGRAM_BINS / RAW_10BIT_RANGE) .coerceIn(0, STATISTICS_HISTOGRAM_BINS - 1) ]++ histG[ (gAvg * STATISTICS_HISTOGRAM_BINS / RAW_10BIT_RANGE) .coerceIn(0, STATISTICS_HISTOGRAM_BINS - 1) ]++ histB[ (b * STATISTICS_HISTOGRAM_BINS / RAW_10BIT_RANGE) .coerceIn(0, STATISTICS_HISTOGRAM_BINS - 1) ]++ // 飽和率・低露光率は全 Bayer 画素を独立にカウント(4 画素分) for (v in intArrayOf(b, g1, g2, r)) { if (v >= SATURATION_THRESHOLD) saturatedCount++ if (v <= UNDEREXPOSED_THRESHOLD) underexposedCount++ } x += 2 } y += 2 } val totalBayerPixels = srcW * srcH val meanR = if (countR > 0) sumR / countR else 0.0 val meanG = if (countG > 0) sumG / countG else 0.0 val meanB = if (countB > 0) sumB / countB else 0.0 val p99R = p99Bin(histR, countR) val p99G = p99Bin(histG, countG) val p99B = p99Bin(histB, countB) return mapOf( "mean_per_channel" to mapOf("R" to meanR, "G" to meanG, "B" to meanB), "max_per_channel" to mapOf("R" to maxR, "G" to maxG, "B" to maxB), "p99_per_channel" to mapOf("R" to p99R, "G" to p99G, "B" to p99B), "saturated_pixel_ratio" to (saturatedCount.toDouble() / totalBayerPixels), "underexposed_pixel_ratio" to (underexposedCount.toDouble() / totalBayerPixels), "thresholds" to mapOf( "saturated" to SATURATION_THRESHOLD, "underexposed" to UNDEREXPOSED_THRESHOLD, ), ) } /** ヒストグラム [hist](総数 [totalCount])から P99 に相当する 10-bit 値を求める. */ private fun p99Bin(hist: IntArray, totalCount: Int): Int { val target = (totalCount * 0.99).toInt() var cumulative = 0 for (bin in hist.indices) { cumulative += hist[bin] if (cumulative >= target) return (bin + 1) * RAW_10BIT_RANGE / STATISTICS_HISTOGRAM_BINS - 1 } return RAW_10BIT_RANGE - 1 } }