diff --git a/CLAUDE.md b/CLAUDE.md
index 0c614c3..6f96b8f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,4 +1,4 @@
-# MiniTIAS
+# SmTIAS-Capture
千葉大学 TIAS(舌画像撮影・解析システム)のスマートフォン版.Flutter で開発し,初期は撮影・保存に特化する.
diff --git a/README.md b/README.md
index 1ea374e..1732676 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# MiniTIAS
+# SmTIAS-Capture
千葉大学フロンティア医工学センターの舌画像撮影・解析システム(TIAS)のスマートフォン版。
PC 側のシミュレーション(SmTIAS-LightSim)と実機撮影を定量比較するため、Camera2 API の
@@ -38,8 +38,8 @@
```powershell
# 1. リポジトリをクローン
-git clone https://github.com/rintoHasegawa/MiniTias.git
-cd MiniTias
+git clone https://github.com/rintoHasegawa/SmTIAS-Capture.git
+cd SmTIAS-Capture
# 2. セットアップスクリプトを実行(Flutter・Android SDK 等を自動インストール)
./scripts/setup.ps1
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 285b4e2..50e1441 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -6,7 +6,7 @@
}
android {
- namespace = "com.example.mini_tias"
+ namespace = "com.example.smtias_capture"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -21,7 +21,7 @@
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
- applicationId = "com.example.mini_tias"
+ applicationId = "com.example.smtias_capture"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5d084aa..4f5713b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -3,7 +3,7 @@
? = null,
- hotPixelMap: Map? = null,
- ): Map {
- val settingsMap = mapOf(
- "control_mode" to "OFF",
- "ae_mode" to "OFF",
- "exposure_time_ns" to exposureTimeNs,
- "sensor_sensitivity_iso" to sensitivity,
- "frame_duration_ns" to frameDurationNs,
- "awb_mode" to "OFF",
- "color_correction_gains" to listOf(1.0, 1.0, 1.0, 1.0),
- "color_correction_transform" to "identity",
- "noise_reduction_mode" to "OFF",
- "edge_mode" to "OFF",
- "tonemap_mode" to "CONTRAST_CURVE_LINEAR",
- "shading_mode" to "HIGH_QUALITY",
- "statistics_lens_shading_map_mode" to "ON",
- "statistics_hot_pixel_map_mode" to "ON",
- "af_mode" to "OFF",
- "black_level_lock" to true,
- )
-
- val actualMap = mapOf(
- "exposure_time_ns" to captureResult.get(CaptureResult.SENSOR_EXPOSURE_TIME),
- "sensor_sensitivity" to captureResult.get(CaptureResult.SENSOR_SENSITIVITY),
- "frame_duration_ns" to captureResult.get(CaptureResult.SENSOR_FRAME_DURATION),
- "sensor_timestamp" to captureResult.get(CaptureResult.SENSOR_TIMESTAMP),
- "dynamic_black_level" to
- captureResult.get(CaptureResult.SENSOR_DYNAMIC_BLACK_LEVEL)?.toList(),
- "dynamic_white_level" to
- captureResult.get(CaptureResult.SENSOR_DYNAMIC_WHITE_LEVEL),
- "neutral_color_point" to
- captureResult.get(CaptureResult.SENSOR_NEUTRAL_COLOR_POINT)?.map {
- "${it.numerator}/${it.denominator}"
- },
- "color_gains" to captureResult.get(CaptureResult.COLOR_CORRECTION_GAINS)?.let {
- listOf(
- it.red.toDouble(),
- it.greenEven.toDouble(),
- it.greenOdd.toDouble(),
- it.blue.toDouble()
- )
- },
- "color_transform" to
- captureResult.get(CaptureResult.COLOR_CORRECTION_TRANSFORM)?.let { transform ->
- (0 until 3).map { row ->
- (0 until 3).map { col ->
- val r = transform.getElement(col, row)
- "${r.numerator}/${r.denominator}"
- }
- }
- },
- )
-
- val sensorCharacteristicsMap = mapOf(
- "color_filter_arrangement" to
- characteristics.get(
- CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT
- ),
- "black_level_pattern" to
- characteristics.get(
- CameraCharacteristics.SENSOR_BLACK_LEVEL_PATTERN
- )?.toString(),
- "white_level" to characteristics.get(CameraCharacteristics.SENSOR_INFO_WHITE_LEVEL),
- "physical_size" to
- characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)?.let {
- listOf(it.width, it.height)
- },
- "pixel_array_size" to
- characteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE)?.let {
- listOf(it.width, it.height)
- },
- "active_array_size" to
- characteristics.get(
- CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE
- )?.toString(),
- )
-
- return mapOf(
- "captureTimestampUtc" to Instant.now().toString(),
- "deviceModel" to Build.MODEL,
- "androidVersion" to Build.VERSION.RELEASE,
- "cameraId" to cameraId,
- "hardwareLevel" to
- characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL),
- "captureMode" to "quantitative",
- "imageFormat" to "RAW_SENSOR",
- "imageSize" to listOf(image.width, image.height),
- "settings" to settingsMap,
- "actual" to actualMap,
- "sensorCharacteristics" to sensorCharacteristicsMap,
- "image_statistics" to imageStatistics,
- "hot_pixel_map" to hotPixelMap,
- )
- }
-}
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
deleted file mode 100644
index b3ab4bc..0000000
--- a/android/app/src/main/kotlin/com/example/mini_tias/ImageStatistics.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-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
- }
-}
diff --git a/android/app/src/main/kotlin/com/example/mini_tias/MainActivity.kt b/android/app/src/main/kotlin/com/example/mini_tias/MainActivity.kt
deleted file mode 100644
index 3b19bf4..0000000
--- a/android/app/src/main/kotlin/com/example/mini_tias/MainActivity.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.example.mini_tias
-
-import io.flutter.embedding.android.FlutterActivity
-import io.flutter.embedding.engine.FlutterEngine
-import io.flutter.plugin.common.MethodChannel
-
-class MainActivity : FlutterActivity() {
- override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
- super.configureFlutterEngine(flutterEngine)
-
- val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.mini_tias/raw_capture")
- channel.setMethodCallHandler(RawCapturePlugin(this))
- }
-}
diff --git a/android/app/src/main/kotlin/com/example/mini_tias/PngEncoder.kt b/android/app/src/main/kotlin/com/example/mini_tias/PngEncoder.kt
deleted file mode 100644
index b0c8902..0000000
--- a/android/app/src/main/kotlin/com/example/mini_tias/PngEncoder.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-package com.example.mini_tias
-
-import android.graphics.Bitmap
-import java.io.ByteArrayOutputStream
-import java.nio.ByteBuffer
-import java.util.zip.CRC32
-import java.util.zip.Deflater
-import java.util.zip.DeflaterOutputStream
-
-/**
- * Bitmap を非圧縮 PNG(Deflater.NO_COMPRESSION)としてエンコードするユーティリティ.
- *
- * 圧縮率より速度を優先する研究用途向け.撮影フローのボトルネック解消のため
- * 標準の `Bitmap.compress` を使わず自前で IDAT を非圧縮 zlib でラップする.
- */
-internal object PngEncoder {
-
- /** [bitmap] を非圧縮 PNG のバイト列に変換して返す. */
- fun encodeUncompressed(bitmap: Bitmap): ByteArray {
- val w = bitmap.width
- val h = bitmap.height
- val out = ByteArrayOutputStream(w * h * 3 + 1024)
-
- // PNG シグネチャ
- out.write(byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A))
-
- // IHDR チャンク
- val ihdrData = ByteBuffer.allocate(13)
- .putInt(w)
- .putInt(h)
- .put(8) // bit depth
- .put(2) // color type: RGB
- .put(0) // compression method
- .put(0) // filter method
- .put(0) // interlace method
- .array()
- writeChunk(out, "IHDR", ihdrData)
-
- // IDAT チャンク: 非圧縮 zlib でラップした RGB データ
- val idatStream = ByteArrayOutputStream(w * h * 3 + h + 64)
- val deflater = Deflater(Deflater.NO_COMPRESSION)
- val deflaterOut = DeflaterOutputStream(idatStream, deflater, 65536)
-
- val pixels = IntArray(w)
- val rowBytes = ByteArray(1 + w * 3) // フィルタバイト(0) + RGB
- rowBytes[0] = 0 // フィルタ: None
-
- for (y in 0 until h) {
- bitmap.getPixels(pixels, 0, w, 0, y, w, 1)
- for (x in 0 until w) {
- val pixel = pixels[x]
- rowBytes[1 + x * 3] = ((pixel shr 16) and 0xFF).toByte() // R
- rowBytes[1 + x * 3 + 1] = ((pixel shr 8) and 0xFF).toByte() // G
- rowBytes[1 + x * 3 + 2] = (pixel and 0xFF).toByte() // B
- }
- deflaterOut.write(rowBytes)
- }
- deflaterOut.finish()
- deflaterOut.close()
- deflater.end()
-
- writeChunk(out, "IDAT", idatStream.toByteArray())
-
- // IEND チャンク
- writeChunk(out, "IEND", byteArrayOf())
-
- return out.toByteArray()
- }
-
- /** PNG チャンクを書き込む(length + type + data + CRC). */
- private fun writeChunk(out: ByteArrayOutputStream, type: String, data: ByteArray) {
- val typeBytes = type.toByteArray(Charsets.US_ASCII)
- // length (4 bytes, big-endian)
- out.write(ByteBuffer.allocate(4).putInt(data.size).array())
- // type (4 bytes)
- out.write(typeBytes)
- // data
- out.write(data)
- // CRC (type + data)
- val crc = CRC32()
- crc.update(typeBytes)
- crc.update(data)
- out.write(ByteBuffer.allocate(4).putInt(crc.value.toInt()).array())
- }
-}
diff --git a/android/app/src/main/kotlin/com/example/mini_tias/RawCapturePlugin.kt b/android/app/src/main/kotlin/com/example/mini_tias/RawCapturePlugin.kt
deleted file mode 100644
index 1edd0e1..0000000
--- a/android/app/src/main/kotlin/com/example/mini_tias/RawCapturePlugin.kt
+++ /dev/null
@@ -1,856 +0,0 @@
-package com.example.mini_tias
-
-import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.Color
-import android.graphics.ImageFormat
-import android.hardware.camera2.*
-import android.hardware.camera2.params.ColorSpaceTransform
-import android.hardware.camera2.params.RggbChannelVector
-import android.hardware.camera2.params.TonemapCurve
-import android.media.ImageReader
-import android.media.MediaActionSound
-import android.media.MediaScannerConnection
-import android.media.RingtoneManager
-import android.os.Handler
-import android.os.HandlerThread
-import android.util.Log
-import io.flutter.plugin.common.MethodCall
-import io.flutter.plugin.common.MethodChannel
-import java.io.File
-import java.io.FileOutputStream
-import kotlin.math.roundToInt
-
-/** フレーム取得後に処理するコールバックの型エイリアス.*/
-private typealias FrameProcessor = (
- yBytes: ByteArray, uBytes: ByteArray, vBytes: ByteArray,
- width: Int, height: Int,
- yRowStride: Int, uvRowStride: Int, uvPixelStride: Int,
- reader: ImageReader, camera: CameraDevice,
- result: MethodChannel.Result
-) -> Unit
-
-/**
- * Camera2 API を使用してフロントカメラから YUV_420_888 フォーマットで
- * フル解像度の画像を 1 フレームキャプチャする.
- */
-class RawCapturePlugin(private val context: Context) : MethodChannel.MethodCallHandler {
-
- private var backgroundThread: HandlerThread? = null
- private var backgroundHandler: Handler? = null
- private var cameraDevice: CameraDevice? = null
- private val mediaActionSound: MediaActionSound by lazy { MediaActionSound() }
-
- private companion object {
- const val DEFAULT_EXPOSURE_TIME_NS = 8_333_333L // 1/120s
- const val DEFAULT_SENSITIVITY = 40
- const val DEFAULT_FRAME_DURATION_NS = 33_333_333L
- const val RAW_WIDTH = 3264
- const val RAW_HEIGHT = 2448
- const val FRAMES_TO_DISCARD = 2
- }
-
- override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
- when (call.method) {
- "captureFullResolutionPng" -> captureFullResolutionPng(result)
- "captureQuantitativeDng" -> {
- val baseName = call.argument("baseName")
- val directoryPath = call.argument("directoryPath")
- if (baseName == null || directoryPath == null) {
- result.error("INVALID_ARG", "baseName と directoryPath が必要です", null)
- } else {
- captureQuantitativeDng(baseName, directoryPath, result)
- }
- }
- "convertYuvToJpeg" -> convertYuvToJpeg(call, result)
- "scanFile" -> {
- val path = call.argument("path")
- if (path != null) {
- MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf("image/png")) { _, _ ->
- result.success(null)
- }
- } else {
- result.error("INVALID_PATH", "パスが指定されていません", null)
- }
- }
- "playShutterSound" -> {
- try {
- mediaActionSound.play(MediaActionSound.SHUTTER_CLICK)
- result.success(null)
- } catch (e: Exception) {
- result.error("SOUND_ERROR", "シャッター音の再生に失敗: ${e.message}", null)
- }
- }
- "playSaveCompleteSound" -> {
- try {
- val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
- val ringtone = RingtoneManager.getRingtone(context, notification)
- ringtone?.play()
- result.success(null)
- } catch (e: Exception) {
- result.error("SOUND_ERROR", "保存完了音の再生に失敗: ${e.message}", null)
- }
- }
- "getCameraDiagnostics" -> getCameraDiagnostics(result)
- else -> result.notImplemented()
- }
- }
-
- /// YUV_420_888 を NV21 経由で JPEG 化して返す(実処理は [YuvJpegConverter]).
- private fun convertYuvToJpeg(call: MethodCall, result: MethodChannel.Result) {
- try {
- val jpegBytes = YuvJpegConverter.convert(
- width = call.argument("width")!!,
- height = call.argument("height")!!,
- yPlane = call.argument("yPlane")!!,
- uPlane = call.argument("uPlane")!!,
- vPlane = call.argument("vPlane")!!,
- yRowStride = call.argument("yRowStride")!!,
- uvRowStride = call.argument("uvRowStride")!!,
- uvPixelStride = call.argument("uvPixelStride")!!,
- rotation = call.argument("rotation") ?: 0,
- mirror = call.argument("mirror") ?: false,
- quality = call.argument("quality") ?: 85,
- )
- result.success(jpegBytes)
- } catch (e: Exception) {
- result.error("CONVERT_ERROR", "YUV to JPEG 変換に失敗: ${e.message}", null)
- }
- }
-
- private fun captureFullResolutionPng(result: MethodChannel.Result) {
- openCameraAndCapture(result) { yBytes, uBytes, vBytes, width, height, yRowStride, uvRowStride, uvPixelStride, reader, camera, res ->
- // YUV → RGB 変換 + 90° 時計回り回転を統合
- // 元の(x,y) → 回転後の(height-1-y, x)
- // 出力サイズ: width_out=height, height_out=width
- val rotatedPixels = IntArray(width * height)
- for (y in 0 until height) {
- for (x in 0 until width) {
- val yIndex = y * yRowStride + x
- val uvIndex = (y / 2) * uvRowStride + (x / 2) * uvPixelStride
-
- val yValue = (yBytes[yIndex].toInt() and 0xFF).toDouble()
- val uValue = (uBytes[uvIndex].toInt() and 0xFF).toDouble()
- val vValue = (vBytes[uvIndex].toInt() and 0xFF).toDouble()
-
- val r = (yValue + 1.402 * (vValue - 128)).roundToInt().coerceIn(0, 255)
- val g = (yValue - 0.344 * (uValue - 128) - 0.714 * (vValue - 128)).roundToInt().coerceIn(0, 255)
- val b = (yValue + 1.772 * (uValue - 128)).roundToInt().coerceIn(0, 255)
-
- rotatedPixels[x * height + (height - 1 - y)] = Color.rgb(r, g, b)
- }
- }
-
- val rotatedWidth = height
- val rotatedHeight = width
- val bitmap = Bitmap.createBitmap(rotatedWidth, rotatedHeight, Bitmap.Config.ARGB_8888)
- bitmap.setPixels(rotatedPixels, 0, rotatedWidth, 0, 0, rotatedWidth, rotatedHeight)
-
- // 非圧縮 PNG エンコード(Deflater.NO_COMPRESSION で高速化)
- val pngBytes = PngEncoder.encodeUncompressed(bitmap)
- bitmap.recycle()
-
- reader.close()
- camera.close()
- cameraDevice = null
-
- Handler(context.mainLooper).post {
- res.success(pngBytes)
- stopBackgroundThread()
- }
- }
- }
-
- /**
- * 定量モード撮影.RAW_SENSOR (DNG) で保存し,メタデータと LSC マップを返す.
- *
- * CONTROL_MODE=OFF など完全マニュアル制御で連続 3 フレームを投げ,
- * 最初の 2 フレーム(設定反映待ち)を捨てて 3 枚目を DNG 化する.
- * DNG ファイルは Kotlin 側で [directoryPath]/[baseName].dng に直接書き込む.
- */
- private fun captureQuantitativeDng(
- baseName: String,
- directoryPath: String,
- result: MethodChannel.Result,
- ) {
- startBackgroundThread()
-
- val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
-
- val cameraId = findFrontCameraId(cameraManager)
-
- if (cameraId == null) {
- returnError(result, "NO_CAMERA", "フロントカメラが見つかりません")
- return
- }
-
- val characteristics = cameraManager.getCameraCharacteristics(cameraId)
-
- // RAW_SENSOR サポート確認
- val capabilities =
- characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
- val supportsRaw =
- capabilities?.contains(
- CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW
- ) == true
- if (!supportsRaw) {
- returnError(result, "NOT_SUPPORTED", "このカメラは RAW_SENSOR をサポートしていません")
- return
- }
-
- val imageReader = ImageReader.newInstance(
- RAW_WIDTH, RAW_HEIGHT, ImageFormat.RAW_SENSOR, 3
- )
-
- var resultSent = false
-
- try {
- cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
- override fun onOpened(camera: CameraDevice) {
- cameraDevice = camera
- try {
- camera.createCaptureSession(
- listOf(imageReader.surface),
- object : CameraCaptureSession.StateCallback() {
- override fun onConfigured(session: CameraCaptureSession) {
- try {
- openCameraAndCaptureRaw(
- camera, session, imageReader, characteristics,
- cameraId, baseName, directoryPath,
- result, { sent -> resultSent = sent }
- )
- } catch (e: Exception) {
- if (!resultSent) {
- resultSent = true
- cleanup()
- imageReader.close()
- Handler(context.mainLooper).post {
- result.error(
- "CAPTURE_ERROR",
- "定量キャプチャに失敗: ${e.message}",
- null
- )
- }
- }
- }
- }
-
- override fun onConfigureFailed(session: CameraCaptureSession) {
- if (!resultSent) {
- resultSent = true
- cleanup()
- imageReader.close()
- Handler(context.mainLooper).post {
- result.error(
- "SESSION_ERROR",
- "カメラセッションの設定に失敗",
- null
- )
- }
- }
- }
- },
- backgroundHandler
- )
- } catch (e: Exception) {
- if (!resultSent) {
- resultSent = true
- cleanup()
- imageReader.close()
- Handler(context.mainLooper).post {
- result.error(
- "CAPTURE_ERROR",
- "定量キャプチャに失敗: ${e.message}",
- null
- )
- }
- }
- }
- }
-
- override fun onDisconnected(camera: CameraDevice) {
- camera.close()
- cameraDevice = null
- imageReader.close()
- stopBackgroundThread()
- }
-
- override fun onError(camera: CameraDevice, error: Int) {
- camera.close()
- cameraDevice = null
- imageReader.close()
- if (!resultSent) {
- resultSent = true
- Handler(context.mainLooper).post {
- result.error("CAMERA_ERROR", "カメラエラー: $error", null)
- stopBackgroundThread()
- }
- }
- }
- }, backgroundHandler)
- } catch (e: SecurityException) {
- imageReader.close()
- returnError(result, "PERMISSION_ERROR", "カメラの権限がありません")
- }
- }
-
- /**
- * 定量モード RAW キャプチャの実処理.
- *
- * session が onConfigured 後に呼ばれる.FRAMES_TO_DISCARD 枚を捨てた後,
- * 3 枚目の Image と TotalCaptureResult を DngCreator に渡して DNG バイト列を生成する.
- */
- private fun openCameraAndCaptureRaw(
- camera: CameraDevice,
- session: CameraCaptureSession,
- imageReader: ImageReader,
- characteristics: CameraCharacteristics,
- cameraId: String,
- baseName: String,
- directoryPath: String,
- result: MethodChannel.Result,
- setResultSent: (Boolean) -> Unit,
- ) {
- var capturedImage: android.media.Image? = null
- var captureResult: TotalCaptureResult? = null
- // 画像到着カウント(ImageAvailableListener 側)
- var imageArrivalCount = 0
- // CaptureResult 到着カウント(CaptureCallback 側)
- var resultArrivalCount = 0
- var resultSentFlag = false
-
- fun sendResultSafe(action: () -> Unit) {
- if (!resultSentFlag) {
- resultSentFlag = true
- setResultSent(true)
- action()
- }
- }
-
- // 完全マニュアル制御の CaptureRequest を構築
- val captureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE).apply {
- addTarget(imageReader.surface)
-
- set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_OFF)
- set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
- set(CaptureRequest.SENSOR_EXPOSURE_TIME, DEFAULT_EXPOSURE_TIME_NS)
- set(CaptureRequest.SENSOR_SENSITIVITY, DEFAULT_SENSITIVITY)
- set(CaptureRequest.SENSOR_FRAME_DURATION, DEFAULT_FRAME_DURATION_NS)
- set(CaptureRequest.BLACK_LEVEL_LOCK, true)
- set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF)
- set(CaptureRequest.COLOR_CORRECTION_MODE,
- CaptureRequest.COLOR_CORRECTION_MODE_TRANSFORM_MATRIX)
- set(CaptureRequest.COLOR_CORRECTION_GAINS, RggbChannelVector(1.0f, 1.0f, 1.0f, 1.0f))
- // 単位行列(3×3,Rational 型)
- set(
- CaptureRequest.COLOR_CORRECTION_TRANSFORM,
- ColorSpaceTransform(
- intArrayOf(
- 1, 1, 0, 1, 0, 1,
- 0, 1, 1, 1, 0, 1,
- 0, 1, 0, 1, 1, 1,
- )
- )
- )
- set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_OFF)
- set(CaptureRequest.EDGE_MODE, CaptureRequest.EDGE_MODE_OFF)
- set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_CONTRAST_CURVE)
- // 線形トーンマップカーブ(各チャネル: (0,0)→(1,1))
- set(
- CaptureRequest.TONEMAP_CURVE,
- TonemapCurve(
- floatArrayOf(0.0f, 0.0f, 1.0f, 1.0f),
- floatArrayOf(0.0f, 0.0f, 1.0f, 1.0f),
- floatArrayOf(0.0f, 0.0f, 1.0f, 1.0f),
- )
- )
- set(CaptureRequest.SHADING_MODE, CaptureRequest.SHADING_MODE_HIGH_QUALITY)
- set(CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE,
- CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE_ON)
- // Hot pixel map を有効化(STATISTICS_HOT_PIXEL_MAP_MODE は Boolean 型)
- set(CaptureRequest.STATISTICS_HOT_PIXEL_MAP_MODE, true)
- set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF)
- set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF)
- set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
- CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF)
- }.build()
-
- val captureCallback = object : CameraCaptureSession.CaptureCallback() {
- override fun onCaptureCompleted(
- session: CameraCaptureSession,
- request: CaptureRequest,
- totalResult: TotalCaptureResult,
- ) {
- resultArrivalCount++
- // FRAMES_TO_DISCARD + 1 枚目(最後)だけ CaptureResult を保持
- if (resultArrivalCount == FRAMES_TO_DISCARD + 1) {
- captureResult = totalResult
- tryProcessRaw(
- capturedImage, captureResult, imageReader, camera,
- characteristics, cameraId, baseName, directoryPath,
- result, ::sendResultSafe
- )
- }
- }
-
- override fun onCaptureFailed(
- session: CameraCaptureSession,
- request: CaptureRequest,
- failure: CaptureFailure,
- ) {
- sendResultSafe {
- capturedImage?.close()
- imageReader.close()
- cleanup()
- Handler(context.mainLooper).post {
- result.error("CAPTURE_FAILED", "キャプチャに失敗しました: ${failure.reason}", null)
- }
- }
- }
- }
-
- imageReader.setOnImageAvailableListener({ reader ->
- val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener
- imageArrivalCount++
- // 最初の FRAMES_TO_DISCARD 枚は捨てる(設定反映待ち)
- if (imageArrivalCount <= FRAMES_TO_DISCARD) {
- image.close()
- return@setOnImageAvailableListener
- }
- // FRAMES_TO_DISCARD + 1 枚目だけ保持
- if (capturedImage == null) {
- capturedImage = image
- tryProcessRaw(
- capturedImage, captureResult, imageReader, camera,
- characteristics, cameraId, baseName, directoryPath,
- result, ::sendResultSafe
- )
- } else {
- image.close()
- }
- }, backgroundHandler)
-
- // 連続 3 回キャプチャを投げる
- repeat(FRAMES_TO_DISCARD + 1) {
- session.capture(captureRequest, captureCallback, backgroundHandler)
- }
- }
-
- /**
- * 画像と CaptureResult の両方が揃ったら DNG を生成して result に返す.
- *
- * 片方がまだない場合は何もしない(もう一方の到着を待つ).
- */
- private fun tryProcessRaw(
- image: android.media.Image?,
- captureResult: TotalCaptureResult?,
- imageReader: ImageReader,
- camera: CameraDevice,
- characteristics: CameraCharacteristics,
- cameraId: String,
- baseName: String,
- directoryPath: String,
- result: MethodChannel.Result,
- sendResultSafe: (() -> Unit) -> Unit,
- ) {
- if (image == null || captureResult == null) return
-
- sendResultSafe {
- try {
- // DNG をファイルに直接書き込む(MethodChannel 越しの巨大バイト転送を回避)
- val dir = File(directoryPath)
- if (!dir.exists()) dir.mkdirs()
- val dngFile = File(dir, "$baseName.dng")
- FileOutputStream(dngFile).use { fos ->
- DngCreator(characteristics, captureResult).use { dngCreator ->
- dngCreator.writeImage(fos, image)
- }
- }
- Log.d("RawCapturePlugin", "DNG written: ${dngFile.length()} bytes to ${dngFile.absolutePath}")
-
- // 画素統計を計算(DNG 書き込み後・image.close() 前)
- val imageStatistics: Map? = try {
- ImageStatistics.compute(image)
- } catch (e: Exception) {
- Log.w("RawCapturePlugin", "Image statistics computation failed", e)
- null
- }
-
- // Hot pixel map を CaptureResult から取得
- val hotPixelMapData: Map = try {
- val hotPixelArray = captureResult.get(CaptureResult.STATISTICS_HOT_PIXEL_MAP)
- mapOf(
- "mode" to "ON",
- "count" to (hotPixelArray?.size ?: 0),
- "coordinates" to (hotPixelArray?.map { listOf(it.x, it.y) }
- ?: emptyList>()),
- )
- } catch (e: Exception) {
- Log.w("RawCapturePlugin", "Hot pixel map retrieval failed", e)
- mapOf("mode" to "ON", "count" to 0,
- "coordinates" to emptyList>())
- }
-
- Log.d("RawCapturePlugin",
- "Image stats: $imageStatistics, hot pixels: ${hotPixelMapData["count"]}")
-
- // LSC マップを取得
- val lscMap =
- captureResult.get(CaptureResult.STATISTICS_LENS_SHADING_CORRECTION_MAP)
- val lscData: List>? = lscMap?.let { map ->
- val rows = map.rowCount
- val cols = map.columnCount
- val gainFactors = FloatArray(rows * cols * 4)
- map.copyGainFactors(gainFactors, 0)
- (0 until rows).map { row ->
- gainFactors.slice(row * cols * 4 until (row + 1) * cols * 4)
- }
- }
-
- // メタデータを構築
- val metadataMap = CaptureMetadata.build(
- cameraId = cameraId,
- characteristics = characteristics,
- captureResult = captureResult,
- image = image,
- exposureTimeNs = DEFAULT_EXPOSURE_TIME_NS,
- sensitivity = DEFAULT_SENSITIVITY,
- frameDurationNs = DEFAULT_FRAME_DURATION_NS,
- imageStatistics = imageStatistics,
- hotPixelMap = hotPixelMapData,
- )
-
- image.close()
- imageReader.close()
- camera.close()
- cameraDevice = null
-
- val resultMap = mapOf(
- "dngPath" to dngFile.absolutePath,
- "dngFileSize" to dngFile.length(),
- "metadata" to metadataMap,
- "lscMap" to lscData,
- "lscMapRowCount" to lscMap?.rowCount,
- "lscMapColumnCount" to lscMap?.columnCount,
- )
-
- Handler(context.mainLooper).post {
- result.success(resultMap)
- stopBackgroundThread()
- }
- } catch (e: Exception) {
- image.close()
- imageReader.close()
- cleanup()
- Handler(context.mainLooper).post {
- result.error("DNG_ERROR", "DNG 生成に失敗: ${e.message}", null)
- stopBackgroundThread()
- }
- }
- }
- }
-
- /** フロントカメラの ID を返す.見つからない場合は null を返す.*/
- private fun findFrontCameraId(cameraManager: CameraManager): String? =
- cameraManager.cameraIdList.firstOrNull { id ->
- val characteristics = cameraManager.getCameraCharacteristics(id)
- characteristics.get(CameraCharacteristics.LENS_FACING) ==
- CameraCharacteristics.LENS_FACING_FRONT
- }
-
- /**
- * フロントカメラを開いてフル解像度の YUV フレームを 1 枚取得し,[onFrame] を呼び出す.
- *
- * カメラのオープン・セッション作成・AE/AF 安定待機(1 秒)・本番キャプチャの
- * 共通フローを担う.フレーム取得後の処理は [onFrame] で差異化する.
- * [onFrame] 内で reader と camera のクローズおよび result への応答を行うこと.
- */
- private fun openCameraAndCapture(
- result: MethodChannel.Result,
- onFrame: FrameProcessor,
- ) {
- startBackgroundThread()
-
- val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
-
- val cameraId = findFrontCameraId(cameraManager)
-
- if (cameraId == null) {
- returnError(result, "NO_CAMERA", "フロントカメラが見つかりません")
- return
- }
-
- val characteristics = cameraManager.getCameraCharacteristics(cameraId)
- val streamConfigMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
-
- if (streamConfigMap == null) {
- returnError(result, "NO_CONFIG", "カメラの設定を取得できません")
- return
- }
-
- val yuvSizes = streamConfigMap.getOutputSizes(ImageFormat.YUV_420_888)
- val maxSize = yuvSizes.maxByOrNull { it.width * it.height }
-
- if (maxSize == null) {
- returnError(result, "NO_SIZE", "YUV の解像度を取得できません")
- return
- }
-
- val imageReader = ImageReader.newInstance(
- maxSize.width, maxSize.height, ImageFormat.YUV_420_888, 2
- )
-
- var resultSent = false
-
- try {
- cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
- override fun onOpened(camera: CameraDevice) {
- cameraDevice = camera
- try {
- camera.createCaptureSession(
- listOf(imageReader.surface),
- object : CameraCaptureSession.StateCallback() {
- override fun onConfigured(session: CameraCaptureSession) {
- try {
- // Phase 1: プレビューを流して AE/AF を安定させる
- val previewRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
- addTarget(imageReader.surface)
- set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
- set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF)
- }
- session.setRepeatingRequest(previewRequest.build(), null, backgroundHandler)
-
- // Phase 2: 1 秒後にリスナーを設定してから本番キャプチャ
- backgroundHandler?.postDelayed({
- try {
- session.stopRepeating()
-
- // プレビュー中のバッファを捨てる
- while (true) {
- val stale = imageReader.acquireLatestImage()
- if (stale != null) {
- stale.close()
- } else {
- break
- }
- }
-
- // ここでリスナーを設定(本番フレームのみ受信)
- imageReader.setOnImageAvailableListener({ reader ->
- if (resultSent) return@setOnImageAvailableListener
- val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener
- resultSent = true
-
- try {
- val yPlane = image.planes[0]
- val uPlane = image.planes[1]
- val vPlane = image.planes[2]
-
- val width = image.width
- val height = image.height
-
- val yBytes = ByteArray(yPlane.buffer.remaining())
- val uBytes = ByteArray(uPlane.buffer.remaining())
- val vBytes = ByteArray(vPlane.buffer.remaining())
-
- yPlane.buffer.get(yBytes)
- uPlane.buffer.get(uBytes)
- vPlane.buffer.get(vBytes)
-
- val yRowStride = yPlane.rowStride
- val uvRowStride = uPlane.rowStride
- val uvPixelStride = uPlane.pixelStride
-
- image.close()
-
- onFrame(
- yBytes, uBytes, vBytes,
- width, height,
- yRowStride, uvRowStride, uvPixelStride,
- reader, camera, result
- )
- } catch (e: Exception) {
- image.close()
- reader.close()
- cleanup()
- Handler(context.mainLooper).post {
- result.error("PROCESS_ERROR", "画像データの処理に失敗: ${e.message}", null)
- }
- }
- }, backgroundHandler)
-
- // 本番キャプチャ実行
- val captureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE).apply {
- addTarget(imageReader.surface)
- set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
- set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF)
- }
- session.capture(captureRequest.build(), null, backgroundHandler)
- } catch (e: Exception) {
- cleanup()
- imageReader.close()
- Handler(context.mainLooper).post {
- result.error("CAPTURE_ERROR", "キャプチャに失敗: ${e.message}", null)
- }
- }
- }, 1000)
- } catch (e: Exception) {
- cleanup()
- imageReader.close()
- Handler(context.mainLooper).post {
- result.error("CAPTURE_ERROR", "キャプチャに失敗: ${e.message}", null)
- }
- }
- }
-
- override fun onConfigureFailed(session: CameraCaptureSession) {
- cleanup()
- imageReader.close()
- Handler(context.mainLooper).post {
- result.error("SESSION_ERROR", "カメラセッションの設定に失敗", null)
- }
- }
- },
- backgroundHandler
- )
- } catch (e: Exception) {
- cleanup()
- imageReader.close()
- Handler(context.mainLooper).post {
- result.error("CAPTURE_ERROR", "キャプチャに失敗: ${e.message}", null)
- }
- }
- }
-
- override fun onDisconnected(camera: CameraDevice) {
- camera.close()
- cameraDevice = null
- imageReader.close()
- stopBackgroundThread()
- }
-
- override fun onError(camera: CameraDevice, error: Int) {
- camera.close()
- cameraDevice = null
- imageReader.close()
- Handler(context.mainLooper).post {
- result.error("CAMERA_ERROR", "カメラエラー: $error", null)
- stopBackgroundThread()
- }
- }
- }, backgroundHandler)
- } catch (e: SecurityException) {
- imageReader.close()
- returnError(result, "PERMISSION_ERROR", "カメラの権限がありません")
- }
- }
-
- /**
- * フロントカメラの Camera2 API 能力を読み出して Map で返す.
- *
- * CameraCharacteristics から各種パラメータを取得し,null セーフに処理する.
- * フロントカメラが見つからない場合は error を返す.
- */
- private fun getCameraDiagnostics(result: MethodChannel.Result) {
- val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
-
- val cameraId = findFrontCameraId(cameraManager)
-
- if (cameraId == null) {
- result.error("NO_CAMERA", "フロントカメラが見つかりません", null)
- return
- }
-
- val c = cameraManager.getCameraCharacteristics(cameraId)
- val streamConfigMap = c.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
-
- // rawOutputSizes
- val rawOutputSizes = streamConfigMap
- ?.getOutputSizes(ImageFormat.RAW_SENSOR)
- ?.map { "${it.width}x${it.height}" }
- ?: emptyList()
-
- // yuvOutputSizes
- val yuvOutputSizes = streamConfigMap
- ?.getOutputSizes(ImageFormat.YUV_420_888)
- ?.map { "${it.width}x${it.height}" }
- ?: emptyList()
-
- // jpegOutputSizes
- val jpegOutputSizes = streamConfigMap
- ?.getOutputSizes(ImageFormat.JPEG)
- ?.map { "${it.width}x${it.height}" }
- ?: emptyList()
-
- val diagnostics = mapOf(
- "cameraId" to cameraId,
- "hardwareLevel" to c.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL),
- "capabilities" to c.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)?.toList(),
- "exposureTimeRangeNs" to c.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE)
- ?.let { listOf(it.lower, it.upper) },
- "sensitivityRange" to c.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE)
- ?.let { listOf(it.lower, it.upper) },
- "lscMapSize" to null, // 実キャプチャ後に CaptureResult から取得する必要があるため診断段階では null
- "availableLensShadingMapModes" to c.get(
- CameraCharacteristics.STATISTICS_INFO_AVAILABLE_LENS_SHADING_MAP_MODES
- )?.map { it },
- "availableNoiseReductionModes" to c.get(
- CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES
- )?.toList(),
- "availableEdgeModes" to c.get(CameraCharacteristics.EDGE_AVAILABLE_EDGE_MODES)?.toList(),
- "availableTonemapModes" to c.get(
- CameraCharacteristics.TONEMAP_AVAILABLE_TONE_MAP_MODES
- )?.toList(),
- "availableShadingModes" to c.get(CameraCharacteristics.SHADING_AVAILABLE_MODES)?.toList(),
- "availableAwbModes" to c.get(
- CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES
- )?.toList(),
- "availableAfModes" to c.get(
- CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES
- )?.toList(),
- "minFocusDistance" to c.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE),
- "hyperfocalDistance" to c.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE),
- "rawOutputSizes" to rawOutputSizes,
- "yuvOutputSizes" to yuvOutputSizes,
- "jpegOutputSizes" to jpegOutputSizes,
- "colorFilterArrangement" to c.get(
- CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT
- ),
- "blackLevelPattern" to c.get(CameraCharacteristics.SENSOR_BLACK_LEVEL_PATTERN)?.toString(),
- "whiteLevel" to c.get(CameraCharacteristics.SENSOR_INFO_WHITE_LEVEL),
- "physicalSize" to c.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)
- ?.let { listOf(it.width, it.height) },
- "pixelArraySize" to c.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE)
- ?.let { listOf(it.width, it.height) },
- "activeArraySize" to c.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)?.toString(),
- )
-
- result.success(diagnostics)
- }
-
- private fun cleanup() {
- cameraDevice?.close()
- cameraDevice = null
- stopBackgroundThread()
- }
-
- private fun returnError(result: MethodChannel.Result, code: String, message: String) {
- result.error(code, message, null)
- stopBackgroundThread()
- }
-
- private fun startBackgroundThread() {
- backgroundThread = HandlerThread("RawCaptureThread").also { it.start() }
- backgroundHandler = Handler(backgroundThread!!.looper)
- }
-
- private fun stopBackgroundThread() {
- backgroundThread?.quitSafely()
- try {
- backgroundThread?.join(3000)
- } catch (_: InterruptedException) {
- }
- backgroundThread = null
- backgroundHandler = null
- }
-}
diff --git a/android/app/src/main/kotlin/com/example/mini_tias/YuvJpegConverter.kt b/android/app/src/main/kotlin/com/example/mini_tias/YuvJpegConverter.kt
deleted file mode 100644
index 68ee0d5..0000000
--- a/android/app/src/main/kotlin/com/example/mini_tias/YuvJpegConverter.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.example.mini_tias
-
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import android.graphics.ImageFormat
-import android.graphics.Matrix
-import android.graphics.Rect
-import android.graphics.YuvImage
-import java.io.ByteArrayOutputStream
-
-/**
- * YUV_420_888 のプレーンデータを NV21 経由で JPEG に変換するユーティリティ.
- *
- * ライブプレビューのスナップショット保存に使用する(撮影本体の RAW とは独立).
- * 回転・左右反転にも対応する.
- */
-internal object YuvJpegConverter {
-
- /**
- * YUV_420_888 のプレーン群を JPEG バイト列に変換する.
- *
- * [rotation] は時計回りの度数,[mirror] が true なら左右反転を適用する.
- */
- fun convert(
- width: Int,
- height: Int,
- yPlane: ByteArray,
- uPlane: ByteArray,
- vPlane: ByteArray,
- yRowStride: Int,
- uvRowStride: Int,
- uvPixelStride: Int,
- rotation: Int,
- mirror: Boolean,
- quality: Int,
- ): ByteArray {
- // YUV_420_888 → NV21 変換
- val nv21 = ByteArray(width * height * 3 / 2)
-
- // Y プレーンをコピー
- for (row in 0 until height) {
- System.arraycopy(yPlane, row * yRowStride, nv21, row * width, width)
- }
-
- // UV プレーンを NV21 形式(VUVU...)にインターリーブ
- val uvOffset = width * height
- for (row in 0 until height / 2) {
- for (col in 0 until width / 2) {
- val uvIndex = row * uvRowStride + col * uvPixelStride
- nv21[uvOffset + row * width + col * 2] = vPlane[uvIndex]
- nv21[uvOffset + row * width + col * 2 + 1] = uPlane[uvIndex]
- }
- }
-
- // NV21 → JPEG
- val yuvImage = YuvImage(nv21, ImageFormat.NV21, width, height, null)
- val jpegStream = ByteArrayOutputStream()
- yuvImage.compressToJpeg(Rect(0, 0, width, height), quality, jpegStream)
-
- // 回転・反転が必要な場合
- return if (rotation != 0 || mirror) {
- val bitmap = BitmapFactory.decodeByteArray(jpegStream.toByteArray(), 0, jpegStream.size())
- val matrix = Matrix()
- if (rotation != 0) matrix.postRotate(rotation.toFloat())
- if (mirror) matrix.postScale(-1f, 1f)
- val transformed = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
- bitmap.recycle()
-
- val outStream = ByteArrayOutputStream()
- transformed.compress(Bitmap.CompressFormat.JPEG, quality, outStream)
- transformed.recycle()
- outStream.toByteArray()
- } else {
- jpegStream.toByteArray()
- }
- }
-}
diff --git a/android/app/src/main/kotlin/com/example/smtias_capture/CaptureMetadata.kt b/android/app/src/main/kotlin/com/example/smtias_capture/CaptureMetadata.kt
new file mode 100644
index 0000000..de31b06
--- /dev/null
+++ b/android/app/src/main/kotlin/com/example/smtias_capture/CaptureMetadata.kt
@@ -0,0 +1,122 @@
+package com.example.smtias_capture
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import android.media.Image
+import android.os.Build
+import java.time.Instant
+
+/**
+ * 定量モード撮影のメタデータ Map(meta.json の元データ)を構築するユーティリティ.
+ *
+ * 撮影設定([exposureTimeNs] 等は呼び出し元が適用した値)・実適用値(CaptureResult)・
+ * センサ特性(CameraCharacteristics)・画素統計・Hot pixel map をまとめる.
+ */
+internal object CaptureMetadata {
+
+ /** メタデータ Map を構築する. */
+ fun build(
+ cameraId: String,
+ characteristics: CameraCharacteristics,
+ captureResult: TotalCaptureResult,
+ image: Image,
+ exposureTimeNs: Long,
+ sensitivity: Int,
+ frameDurationNs: Long,
+ imageStatistics: Map? = null,
+ hotPixelMap: Map? = null,
+ ): Map {
+ val settingsMap = mapOf(
+ "control_mode" to "OFF",
+ "ae_mode" to "OFF",
+ "exposure_time_ns" to exposureTimeNs,
+ "sensor_sensitivity_iso" to sensitivity,
+ "frame_duration_ns" to frameDurationNs,
+ "awb_mode" to "OFF",
+ "color_correction_gains" to listOf(1.0, 1.0, 1.0, 1.0),
+ "color_correction_transform" to "identity",
+ "noise_reduction_mode" to "OFF",
+ "edge_mode" to "OFF",
+ "tonemap_mode" to "CONTRAST_CURVE_LINEAR",
+ "shading_mode" to "HIGH_QUALITY",
+ "statistics_lens_shading_map_mode" to "ON",
+ "statistics_hot_pixel_map_mode" to "ON",
+ "af_mode" to "OFF",
+ "black_level_lock" to true,
+ )
+
+ val actualMap = mapOf(
+ "exposure_time_ns" to captureResult.get(CaptureResult.SENSOR_EXPOSURE_TIME),
+ "sensor_sensitivity" to captureResult.get(CaptureResult.SENSOR_SENSITIVITY),
+ "frame_duration_ns" to captureResult.get(CaptureResult.SENSOR_FRAME_DURATION),
+ "sensor_timestamp" to captureResult.get(CaptureResult.SENSOR_TIMESTAMP),
+ "dynamic_black_level" to
+ captureResult.get(CaptureResult.SENSOR_DYNAMIC_BLACK_LEVEL)?.toList(),
+ "dynamic_white_level" to
+ captureResult.get(CaptureResult.SENSOR_DYNAMIC_WHITE_LEVEL),
+ "neutral_color_point" to
+ captureResult.get(CaptureResult.SENSOR_NEUTRAL_COLOR_POINT)?.map {
+ "${it.numerator}/${it.denominator}"
+ },
+ "color_gains" to captureResult.get(CaptureResult.COLOR_CORRECTION_GAINS)?.let {
+ listOf(
+ it.red.toDouble(),
+ it.greenEven.toDouble(),
+ it.greenOdd.toDouble(),
+ it.blue.toDouble()
+ )
+ },
+ "color_transform" to
+ captureResult.get(CaptureResult.COLOR_CORRECTION_TRANSFORM)?.let { transform ->
+ (0 until 3).map { row ->
+ (0 until 3).map { col ->
+ val r = transform.getElement(col, row)
+ "${r.numerator}/${r.denominator}"
+ }
+ }
+ },
+ )
+
+ val sensorCharacteristicsMap = mapOf(
+ "color_filter_arrangement" to
+ characteristics.get(
+ CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT
+ ),
+ "black_level_pattern" to
+ characteristics.get(
+ CameraCharacteristics.SENSOR_BLACK_LEVEL_PATTERN
+ )?.toString(),
+ "white_level" to characteristics.get(CameraCharacteristics.SENSOR_INFO_WHITE_LEVEL),
+ "physical_size" to
+ characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)?.let {
+ listOf(it.width, it.height)
+ },
+ "pixel_array_size" to
+ characteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE)?.let {
+ listOf(it.width, it.height)
+ },
+ "active_array_size" to
+ characteristics.get(
+ CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE
+ )?.toString(),
+ )
+
+ return mapOf(
+ "captureTimestampUtc" to Instant.now().toString(),
+ "deviceModel" to Build.MODEL,
+ "androidVersion" to Build.VERSION.RELEASE,
+ "cameraId" to cameraId,
+ "hardwareLevel" to
+ characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL),
+ "captureMode" to "quantitative",
+ "imageFormat" to "RAW_SENSOR",
+ "imageSize" to listOf(image.width, image.height),
+ "settings" to settingsMap,
+ "actual" to actualMap,
+ "sensorCharacteristics" to sensorCharacteristicsMap,
+ "image_statistics" to imageStatistics,
+ "hot_pixel_map" to hotPixelMap,
+ )
+ }
+}
diff --git a/android/app/src/main/kotlin/com/example/smtias_capture/ImageStatistics.kt b/android/app/src/main/kotlin/com/example/smtias_capture/ImageStatistics.kt
new file mode 100644
index 0000000..3883b57
--- /dev/null
+++ b/android/app/src/main/kotlin/com/example/smtias_capture/ImageStatistics.kt
@@ -0,0 +1,143 @@
+package com.example.smtias_capture
+
+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
+ }
+}
diff --git a/android/app/src/main/kotlin/com/example/smtias_capture/MainActivity.kt b/android/app/src/main/kotlin/com/example/smtias_capture/MainActivity.kt
new file mode 100644
index 0000000..84e639a
--- /dev/null
+++ b/android/app/src/main/kotlin/com/example/smtias_capture/MainActivity.kt
@@ -0,0 +1,14 @@
+package com.example.smtias_capture
+
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.MethodChannel
+
+class MainActivity : FlutterActivity() {
+ override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+ super.configureFlutterEngine(flutterEngine)
+
+ val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.smtias_capture/raw_capture")
+ channel.setMethodCallHandler(RawCapturePlugin(this))
+ }
+}
diff --git a/android/app/src/main/kotlin/com/example/smtias_capture/PngEncoder.kt b/android/app/src/main/kotlin/com/example/smtias_capture/PngEncoder.kt
new file mode 100644
index 0000000..5209c47
--- /dev/null
+++ b/android/app/src/main/kotlin/com/example/smtias_capture/PngEncoder.kt
@@ -0,0 +1,85 @@
+package com.example.smtias_capture
+
+import android.graphics.Bitmap
+import java.io.ByteArrayOutputStream
+import java.nio.ByteBuffer
+import java.util.zip.CRC32
+import java.util.zip.Deflater
+import java.util.zip.DeflaterOutputStream
+
+/**
+ * Bitmap を非圧縮 PNG(Deflater.NO_COMPRESSION)としてエンコードするユーティリティ.
+ *
+ * 圧縮率より速度を優先する研究用途向け.撮影フローのボトルネック解消のため
+ * 標準の `Bitmap.compress` を使わず自前で IDAT を非圧縮 zlib でラップする.
+ */
+internal object PngEncoder {
+
+ /** [bitmap] を非圧縮 PNG のバイト列に変換して返す. */
+ fun encodeUncompressed(bitmap: Bitmap): ByteArray {
+ val w = bitmap.width
+ val h = bitmap.height
+ val out = ByteArrayOutputStream(w * h * 3 + 1024)
+
+ // PNG シグネチャ
+ out.write(byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A))
+
+ // IHDR チャンク
+ val ihdrData = ByteBuffer.allocate(13)
+ .putInt(w)
+ .putInt(h)
+ .put(8) // bit depth
+ .put(2) // color type: RGB
+ .put(0) // compression method
+ .put(0) // filter method
+ .put(0) // interlace method
+ .array()
+ writeChunk(out, "IHDR", ihdrData)
+
+ // IDAT チャンク: 非圧縮 zlib でラップした RGB データ
+ val idatStream = ByteArrayOutputStream(w * h * 3 + h + 64)
+ val deflater = Deflater(Deflater.NO_COMPRESSION)
+ val deflaterOut = DeflaterOutputStream(idatStream, deflater, 65536)
+
+ val pixels = IntArray(w)
+ val rowBytes = ByteArray(1 + w * 3) // フィルタバイト(0) + RGB
+ rowBytes[0] = 0 // フィルタ: None
+
+ for (y in 0 until h) {
+ bitmap.getPixels(pixels, 0, w, 0, y, w, 1)
+ for (x in 0 until w) {
+ val pixel = pixels[x]
+ rowBytes[1 + x * 3] = ((pixel shr 16) and 0xFF).toByte() // R
+ rowBytes[1 + x * 3 + 1] = ((pixel shr 8) and 0xFF).toByte() // G
+ rowBytes[1 + x * 3 + 2] = (pixel and 0xFF).toByte() // B
+ }
+ deflaterOut.write(rowBytes)
+ }
+ deflaterOut.finish()
+ deflaterOut.close()
+ deflater.end()
+
+ writeChunk(out, "IDAT", idatStream.toByteArray())
+
+ // IEND チャンク
+ writeChunk(out, "IEND", byteArrayOf())
+
+ return out.toByteArray()
+ }
+
+ /** PNG チャンクを書き込む(length + type + data + CRC). */
+ private fun writeChunk(out: ByteArrayOutputStream, type: String, data: ByteArray) {
+ val typeBytes = type.toByteArray(Charsets.US_ASCII)
+ // length (4 bytes, big-endian)
+ out.write(ByteBuffer.allocate(4).putInt(data.size).array())
+ // type (4 bytes)
+ out.write(typeBytes)
+ // data
+ out.write(data)
+ // CRC (type + data)
+ val crc = CRC32()
+ crc.update(typeBytes)
+ crc.update(data)
+ out.write(ByteBuffer.allocate(4).putInt(crc.value.toInt()).array())
+ }
+}
diff --git a/android/app/src/main/kotlin/com/example/smtias_capture/RawCapturePlugin.kt b/android/app/src/main/kotlin/com/example/smtias_capture/RawCapturePlugin.kt
new file mode 100644
index 0000000..1bf9a6d
--- /dev/null
+++ b/android/app/src/main/kotlin/com/example/smtias_capture/RawCapturePlugin.kt
@@ -0,0 +1,856 @@
+package com.example.smtias_capture
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.ImageFormat
+import android.hardware.camera2.*
+import android.hardware.camera2.params.ColorSpaceTransform
+import android.hardware.camera2.params.RggbChannelVector
+import android.hardware.camera2.params.TonemapCurve
+import android.media.ImageReader
+import android.media.MediaActionSound
+import android.media.MediaScannerConnection
+import android.media.RingtoneManager
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Log
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import java.io.File
+import java.io.FileOutputStream
+import kotlin.math.roundToInt
+
+/** フレーム取得後に処理するコールバックの型エイリアス.*/
+private typealias FrameProcessor = (
+ yBytes: ByteArray, uBytes: ByteArray, vBytes: ByteArray,
+ width: Int, height: Int,
+ yRowStride: Int, uvRowStride: Int, uvPixelStride: Int,
+ reader: ImageReader, camera: CameraDevice,
+ result: MethodChannel.Result
+) -> Unit
+
+/**
+ * Camera2 API を使用してフロントカメラから YUV_420_888 フォーマットで
+ * フル解像度の画像を 1 フレームキャプチャする.
+ */
+class RawCapturePlugin(private val context: Context) : MethodChannel.MethodCallHandler {
+
+ private var backgroundThread: HandlerThread? = null
+ private var backgroundHandler: Handler? = null
+ private var cameraDevice: CameraDevice? = null
+ private val mediaActionSound: MediaActionSound by lazy { MediaActionSound() }
+
+ private companion object {
+ const val DEFAULT_EXPOSURE_TIME_NS = 8_333_333L // 1/120s
+ const val DEFAULT_SENSITIVITY = 40
+ const val DEFAULT_FRAME_DURATION_NS = 33_333_333L
+ const val RAW_WIDTH = 3264
+ const val RAW_HEIGHT = 2448
+ const val FRAMES_TO_DISCARD = 2
+ }
+
+ override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
+ when (call.method) {
+ "captureFullResolutionPng" -> captureFullResolutionPng(result)
+ "captureQuantitativeDng" -> {
+ val baseName = call.argument("baseName")
+ val directoryPath = call.argument("directoryPath")
+ if (baseName == null || directoryPath == null) {
+ result.error("INVALID_ARG", "baseName と directoryPath が必要です", null)
+ } else {
+ captureQuantitativeDng(baseName, directoryPath, result)
+ }
+ }
+ "convertYuvToJpeg" -> convertYuvToJpeg(call, result)
+ "scanFile" -> {
+ val path = call.argument("path")
+ if (path != null) {
+ MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf("image/png")) { _, _ ->
+ result.success(null)
+ }
+ } else {
+ result.error("INVALID_PATH", "パスが指定されていません", null)
+ }
+ }
+ "playShutterSound" -> {
+ try {
+ mediaActionSound.play(MediaActionSound.SHUTTER_CLICK)
+ result.success(null)
+ } catch (e: Exception) {
+ result.error("SOUND_ERROR", "シャッター音の再生に失敗: ${e.message}", null)
+ }
+ }
+ "playSaveCompleteSound" -> {
+ try {
+ val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
+ val ringtone = RingtoneManager.getRingtone(context, notification)
+ ringtone?.play()
+ result.success(null)
+ } catch (e: Exception) {
+ result.error("SOUND_ERROR", "保存完了音の再生に失敗: ${e.message}", null)
+ }
+ }
+ "getCameraDiagnostics" -> getCameraDiagnostics(result)
+ else -> result.notImplemented()
+ }
+ }
+
+ /// YUV_420_888 を NV21 経由で JPEG 化して返す(実処理は [YuvJpegConverter]).
+ private fun convertYuvToJpeg(call: MethodCall, result: MethodChannel.Result) {
+ try {
+ val jpegBytes = YuvJpegConverter.convert(
+ width = call.argument("width")!!,
+ height = call.argument("height")!!,
+ yPlane = call.argument("yPlane")!!,
+ uPlane = call.argument("uPlane")!!,
+ vPlane = call.argument("vPlane")!!,
+ yRowStride = call.argument("yRowStride")!!,
+ uvRowStride = call.argument("uvRowStride")!!,
+ uvPixelStride = call.argument("uvPixelStride")!!,
+ rotation = call.argument("rotation") ?: 0,
+ mirror = call.argument("mirror") ?: false,
+ quality = call.argument("quality") ?: 85,
+ )
+ result.success(jpegBytes)
+ } catch (e: Exception) {
+ result.error("CONVERT_ERROR", "YUV to JPEG 変換に失敗: ${e.message}", null)
+ }
+ }
+
+ private fun captureFullResolutionPng(result: MethodChannel.Result) {
+ openCameraAndCapture(result) { yBytes, uBytes, vBytes, width, height, yRowStride, uvRowStride, uvPixelStride, reader, camera, res ->
+ // YUV → RGB 変換 + 90° 時計回り回転を統合
+ // 元の(x,y) → 回転後の(height-1-y, x)
+ // 出力サイズ: width_out=height, height_out=width
+ val rotatedPixels = IntArray(width * height)
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val yIndex = y * yRowStride + x
+ val uvIndex = (y / 2) * uvRowStride + (x / 2) * uvPixelStride
+
+ val yValue = (yBytes[yIndex].toInt() and 0xFF).toDouble()
+ val uValue = (uBytes[uvIndex].toInt() and 0xFF).toDouble()
+ val vValue = (vBytes[uvIndex].toInt() and 0xFF).toDouble()
+
+ val r = (yValue + 1.402 * (vValue - 128)).roundToInt().coerceIn(0, 255)
+ val g = (yValue - 0.344 * (uValue - 128) - 0.714 * (vValue - 128)).roundToInt().coerceIn(0, 255)
+ val b = (yValue + 1.772 * (uValue - 128)).roundToInt().coerceIn(0, 255)
+
+ rotatedPixels[x * height + (height - 1 - y)] = Color.rgb(r, g, b)
+ }
+ }
+
+ val rotatedWidth = height
+ val rotatedHeight = width
+ val bitmap = Bitmap.createBitmap(rotatedWidth, rotatedHeight, Bitmap.Config.ARGB_8888)
+ bitmap.setPixels(rotatedPixels, 0, rotatedWidth, 0, 0, rotatedWidth, rotatedHeight)
+
+ // 非圧縮 PNG エンコード(Deflater.NO_COMPRESSION で高速化)
+ val pngBytes = PngEncoder.encodeUncompressed(bitmap)
+ bitmap.recycle()
+
+ reader.close()
+ camera.close()
+ cameraDevice = null
+
+ Handler(context.mainLooper).post {
+ res.success(pngBytes)
+ stopBackgroundThread()
+ }
+ }
+ }
+
+ /**
+ * 定量モード撮影.RAW_SENSOR (DNG) で保存し,メタデータと LSC マップを返す.
+ *
+ * CONTROL_MODE=OFF など完全マニュアル制御で連続 3 フレームを投げ,
+ * 最初の 2 フレーム(設定反映待ち)を捨てて 3 枚目を DNG 化する.
+ * DNG ファイルは Kotlin 側で [directoryPath]/[baseName].dng に直接書き込む.
+ */
+ private fun captureQuantitativeDng(
+ baseName: String,
+ directoryPath: String,
+ result: MethodChannel.Result,
+ ) {
+ startBackgroundThread()
+
+ val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+
+ val cameraId = findFrontCameraId(cameraManager)
+
+ if (cameraId == null) {
+ returnError(result, "NO_CAMERA", "フロントカメラが見つかりません")
+ return
+ }
+
+ val characteristics = cameraManager.getCameraCharacteristics(cameraId)
+
+ // RAW_SENSOR サポート確認
+ val capabilities =
+ characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
+ val supportsRaw =
+ capabilities?.contains(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW
+ ) == true
+ if (!supportsRaw) {
+ returnError(result, "NOT_SUPPORTED", "このカメラは RAW_SENSOR をサポートしていません")
+ return
+ }
+
+ val imageReader = ImageReader.newInstance(
+ RAW_WIDTH, RAW_HEIGHT, ImageFormat.RAW_SENSOR, 3
+ )
+
+ var resultSent = false
+
+ try {
+ cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
+ override fun onOpened(camera: CameraDevice) {
+ cameraDevice = camera
+ try {
+ camera.createCaptureSession(
+ listOf(imageReader.surface),
+ object : CameraCaptureSession.StateCallback() {
+ override fun onConfigured(session: CameraCaptureSession) {
+ try {
+ openCameraAndCaptureRaw(
+ camera, session, imageReader, characteristics,
+ cameraId, baseName, directoryPath,
+ result, { sent -> resultSent = sent }
+ )
+ } catch (e: Exception) {
+ if (!resultSent) {
+ resultSent = true
+ cleanup()
+ imageReader.close()
+ Handler(context.mainLooper).post {
+ result.error(
+ "CAPTURE_ERROR",
+ "定量キャプチャに失敗: ${e.message}",
+ null
+ )
+ }
+ }
+ }
+ }
+
+ override fun onConfigureFailed(session: CameraCaptureSession) {
+ if (!resultSent) {
+ resultSent = true
+ cleanup()
+ imageReader.close()
+ Handler(context.mainLooper).post {
+ result.error(
+ "SESSION_ERROR",
+ "カメラセッションの設定に失敗",
+ null
+ )
+ }
+ }
+ }
+ },
+ backgroundHandler
+ )
+ } catch (e: Exception) {
+ if (!resultSent) {
+ resultSent = true
+ cleanup()
+ imageReader.close()
+ Handler(context.mainLooper).post {
+ result.error(
+ "CAPTURE_ERROR",
+ "定量キャプチャに失敗: ${e.message}",
+ null
+ )
+ }
+ }
+ }
+ }
+
+ override fun onDisconnected(camera: CameraDevice) {
+ camera.close()
+ cameraDevice = null
+ imageReader.close()
+ stopBackgroundThread()
+ }
+
+ override fun onError(camera: CameraDevice, error: Int) {
+ camera.close()
+ cameraDevice = null
+ imageReader.close()
+ if (!resultSent) {
+ resultSent = true
+ Handler(context.mainLooper).post {
+ result.error("CAMERA_ERROR", "カメラエラー: $error", null)
+ stopBackgroundThread()
+ }
+ }
+ }
+ }, backgroundHandler)
+ } catch (e: SecurityException) {
+ imageReader.close()
+ returnError(result, "PERMISSION_ERROR", "カメラの権限がありません")
+ }
+ }
+
+ /**
+ * 定量モード RAW キャプチャの実処理.
+ *
+ * session が onConfigured 後に呼ばれる.FRAMES_TO_DISCARD 枚を捨てた後,
+ * 3 枚目の Image と TotalCaptureResult を DngCreator に渡して DNG バイト列を生成する.
+ */
+ private fun openCameraAndCaptureRaw(
+ camera: CameraDevice,
+ session: CameraCaptureSession,
+ imageReader: ImageReader,
+ characteristics: CameraCharacteristics,
+ cameraId: String,
+ baseName: String,
+ directoryPath: String,
+ result: MethodChannel.Result,
+ setResultSent: (Boolean) -> Unit,
+ ) {
+ var capturedImage: android.media.Image? = null
+ var captureResult: TotalCaptureResult? = null
+ // 画像到着カウント(ImageAvailableListener 側)
+ var imageArrivalCount = 0
+ // CaptureResult 到着カウント(CaptureCallback 側)
+ var resultArrivalCount = 0
+ var resultSentFlag = false
+
+ fun sendResultSafe(action: () -> Unit) {
+ if (!resultSentFlag) {
+ resultSentFlag = true
+ setResultSent(true)
+ action()
+ }
+ }
+
+ // 完全マニュアル制御の CaptureRequest を構築
+ val captureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE).apply {
+ addTarget(imageReader.surface)
+
+ set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_OFF)
+ set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
+ set(CaptureRequest.SENSOR_EXPOSURE_TIME, DEFAULT_EXPOSURE_TIME_NS)
+ set(CaptureRequest.SENSOR_SENSITIVITY, DEFAULT_SENSITIVITY)
+ set(CaptureRequest.SENSOR_FRAME_DURATION, DEFAULT_FRAME_DURATION_NS)
+ set(CaptureRequest.BLACK_LEVEL_LOCK, true)
+ set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF)
+ set(CaptureRequest.COLOR_CORRECTION_MODE,
+ CaptureRequest.COLOR_CORRECTION_MODE_TRANSFORM_MATRIX)
+ set(CaptureRequest.COLOR_CORRECTION_GAINS, RggbChannelVector(1.0f, 1.0f, 1.0f, 1.0f))
+ // 単位行列(3×3,Rational 型)
+ set(
+ CaptureRequest.COLOR_CORRECTION_TRANSFORM,
+ ColorSpaceTransform(
+ intArrayOf(
+ 1, 1, 0, 1, 0, 1,
+ 0, 1, 1, 1, 0, 1,
+ 0, 1, 0, 1, 1, 1,
+ )
+ )
+ )
+ set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_OFF)
+ set(CaptureRequest.EDGE_MODE, CaptureRequest.EDGE_MODE_OFF)
+ set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_CONTRAST_CURVE)
+ // 線形トーンマップカーブ(各チャネル: (0,0)→(1,1))
+ set(
+ CaptureRequest.TONEMAP_CURVE,
+ TonemapCurve(
+ floatArrayOf(0.0f, 0.0f, 1.0f, 1.0f),
+ floatArrayOf(0.0f, 0.0f, 1.0f, 1.0f),
+ floatArrayOf(0.0f, 0.0f, 1.0f, 1.0f),
+ )
+ )
+ set(CaptureRequest.SHADING_MODE, CaptureRequest.SHADING_MODE_HIGH_QUALITY)
+ set(CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE,
+ CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE_ON)
+ // Hot pixel map を有効化(STATISTICS_HOT_PIXEL_MAP_MODE は Boolean 型)
+ set(CaptureRequest.STATISTICS_HOT_PIXEL_MAP_MODE, true)
+ set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF)
+ set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF)
+ set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
+ CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF)
+ }.build()
+
+ val captureCallback = object : CameraCaptureSession.CaptureCallback() {
+ override fun onCaptureCompleted(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ totalResult: TotalCaptureResult,
+ ) {
+ resultArrivalCount++
+ // FRAMES_TO_DISCARD + 1 枚目(最後)だけ CaptureResult を保持
+ if (resultArrivalCount == FRAMES_TO_DISCARD + 1) {
+ captureResult = totalResult
+ tryProcessRaw(
+ capturedImage, captureResult, imageReader, camera,
+ characteristics, cameraId, baseName, directoryPath,
+ result, ::sendResultSafe
+ )
+ }
+ }
+
+ override fun onCaptureFailed(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ failure: CaptureFailure,
+ ) {
+ sendResultSafe {
+ capturedImage?.close()
+ imageReader.close()
+ cleanup()
+ Handler(context.mainLooper).post {
+ result.error("CAPTURE_FAILED", "キャプチャに失敗しました: ${failure.reason}", null)
+ }
+ }
+ }
+ }
+
+ imageReader.setOnImageAvailableListener({ reader ->
+ val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener
+ imageArrivalCount++
+ // 最初の FRAMES_TO_DISCARD 枚は捨てる(設定反映待ち)
+ if (imageArrivalCount <= FRAMES_TO_DISCARD) {
+ image.close()
+ return@setOnImageAvailableListener
+ }
+ // FRAMES_TO_DISCARD + 1 枚目だけ保持
+ if (capturedImage == null) {
+ capturedImage = image
+ tryProcessRaw(
+ capturedImage, captureResult, imageReader, camera,
+ characteristics, cameraId, baseName, directoryPath,
+ result, ::sendResultSafe
+ )
+ } else {
+ image.close()
+ }
+ }, backgroundHandler)
+
+ // 連続 3 回キャプチャを投げる
+ repeat(FRAMES_TO_DISCARD + 1) {
+ session.capture(captureRequest, captureCallback, backgroundHandler)
+ }
+ }
+
+ /**
+ * 画像と CaptureResult の両方が揃ったら DNG を生成して result に返す.
+ *
+ * 片方がまだない場合は何もしない(もう一方の到着を待つ).
+ */
+ private fun tryProcessRaw(
+ image: android.media.Image?,
+ captureResult: TotalCaptureResult?,
+ imageReader: ImageReader,
+ camera: CameraDevice,
+ characteristics: CameraCharacteristics,
+ cameraId: String,
+ baseName: String,
+ directoryPath: String,
+ result: MethodChannel.Result,
+ sendResultSafe: (() -> Unit) -> Unit,
+ ) {
+ if (image == null || captureResult == null) return
+
+ sendResultSafe {
+ try {
+ // DNG をファイルに直接書き込む(MethodChannel 越しの巨大バイト転送を回避)
+ val dir = File(directoryPath)
+ if (!dir.exists()) dir.mkdirs()
+ val dngFile = File(dir, "$baseName.dng")
+ FileOutputStream(dngFile).use { fos ->
+ DngCreator(characteristics, captureResult).use { dngCreator ->
+ dngCreator.writeImage(fos, image)
+ }
+ }
+ Log.d("RawCapturePlugin", "DNG written: ${dngFile.length()} bytes to ${dngFile.absolutePath}")
+
+ // 画素統計を計算(DNG 書き込み後・image.close() 前)
+ val imageStatistics: Map? = try {
+ ImageStatistics.compute(image)
+ } catch (e: Exception) {
+ Log.w("RawCapturePlugin", "Image statistics computation failed", e)
+ null
+ }
+
+ // Hot pixel map を CaptureResult から取得
+ val hotPixelMapData: Map = try {
+ val hotPixelArray = captureResult.get(CaptureResult.STATISTICS_HOT_PIXEL_MAP)
+ mapOf(
+ "mode" to "ON",
+ "count" to (hotPixelArray?.size ?: 0),
+ "coordinates" to (hotPixelArray?.map { listOf(it.x, it.y) }
+ ?: emptyList>()),
+ )
+ } catch (e: Exception) {
+ Log.w("RawCapturePlugin", "Hot pixel map retrieval failed", e)
+ mapOf("mode" to "ON", "count" to 0,
+ "coordinates" to emptyList>())
+ }
+
+ Log.d("RawCapturePlugin",
+ "Image stats: $imageStatistics, hot pixels: ${hotPixelMapData["count"]}")
+
+ // LSC マップを取得
+ val lscMap =
+ captureResult.get(CaptureResult.STATISTICS_LENS_SHADING_CORRECTION_MAP)
+ val lscData: List>? = lscMap?.let { map ->
+ val rows = map.rowCount
+ val cols = map.columnCount
+ val gainFactors = FloatArray(rows * cols * 4)
+ map.copyGainFactors(gainFactors, 0)
+ (0 until rows).map { row ->
+ gainFactors.slice(row * cols * 4 until (row + 1) * cols * 4)
+ }
+ }
+
+ // メタデータを構築
+ val metadataMap = CaptureMetadata.build(
+ cameraId = cameraId,
+ characteristics = characteristics,
+ captureResult = captureResult,
+ image = image,
+ exposureTimeNs = DEFAULT_EXPOSURE_TIME_NS,
+ sensitivity = DEFAULT_SENSITIVITY,
+ frameDurationNs = DEFAULT_FRAME_DURATION_NS,
+ imageStatistics = imageStatistics,
+ hotPixelMap = hotPixelMapData,
+ )
+
+ image.close()
+ imageReader.close()
+ camera.close()
+ cameraDevice = null
+
+ val resultMap = mapOf(
+ "dngPath" to dngFile.absolutePath,
+ "dngFileSize" to dngFile.length(),
+ "metadata" to metadataMap,
+ "lscMap" to lscData,
+ "lscMapRowCount" to lscMap?.rowCount,
+ "lscMapColumnCount" to lscMap?.columnCount,
+ )
+
+ Handler(context.mainLooper).post {
+ result.success(resultMap)
+ stopBackgroundThread()
+ }
+ } catch (e: Exception) {
+ image.close()
+ imageReader.close()
+ cleanup()
+ Handler(context.mainLooper).post {
+ result.error("DNG_ERROR", "DNG 生成に失敗: ${e.message}", null)
+ stopBackgroundThread()
+ }
+ }
+ }
+ }
+
+ /** フロントカメラの ID を返す.見つからない場合は null を返す.*/
+ private fun findFrontCameraId(cameraManager: CameraManager): String? =
+ cameraManager.cameraIdList.firstOrNull { id ->
+ val characteristics = cameraManager.getCameraCharacteristics(id)
+ characteristics.get(CameraCharacteristics.LENS_FACING) ==
+ CameraCharacteristics.LENS_FACING_FRONT
+ }
+
+ /**
+ * フロントカメラを開いてフル解像度の YUV フレームを 1 枚取得し,[onFrame] を呼び出す.
+ *
+ * カメラのオープン・セッション作成・AE/AF 安定待機(1 秒)・本番キャプチャの
+ * 共通フローを担う.フレーム取得後の処理は [onFrame] で差異化する.
+ * [onFrame] 内で reader と camera のクローズおよび result への応答を行うこと.
+ */
+ private fun openCameraAndCapture(
+ result: MethodChannel.Result,
+ onFrame: FrameProcessor,
+ ) {
+ startBackgroundThread()
+
+ val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+
+ val cameraId = findFrontCameraId(cameraManager)
+
+ if (cameraId == null) {
+ returnError(result, "NO_CAMERA", "フロントカメラが見つかりません")
+ return
+ }
+
+ val characteristics = cameraManager.getCameraCharacteristics(cameraId)
+ val streamConfigMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
+
+ if (streamConfigMap == null) {
+ returnError(result, "NO_CONFIG", "カメラの設定を取得できません")
+ return
+ }
+
+ val yuvSizes = streamConfigMap.getOutputSizes(ImageFormat.YUV_420_888)
+ val maxSize = yuvSizes.maxByOrNull { it.width * it.height }
+
+ if (maxSize == null) {
+ returnError(result, "NO_SIZE", "YUV の解像度を取得できません")
+ return
+ }
+
+ val imageReader = ImageReader.newInstance(
+ maxSize.width, maxSize.height, ImageFormat.YUV_420_888, 2
+ )
+
+ var resultSent = false
+
+ try {
+ cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
+ override fun onOpened(camera: CameraDevice) {
+ cameraDevice = camera
+ try {
+ camera.createCaptureSession(
+ listOf(imageReader.surface),
+ object : CameraCaptureSession.StateCallback() {
+ override fun onConfigured(session: CameraCaptureSession) {
+ try {
+ // Phase 1: プレビューを流して AE/AF を安定させる
+ val previewRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
+ addTarget(imageReader.surface)
+ set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
+ set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF)
+ }
+ session.setRepeatingRequest(previewRequest.build(), null, backgroundHandler)
+
+ // Phase 2: 1 秒後にリスナーを設定してから本番キャプチャ
+ backgroundHandler?.postDelayed({
+ try {
+ session.stopRepeating()
+
+ // プレビュー中のバッファを捨てる
+ while (true) {
+ val stale = imageReader.acquireLatestImage()
+ if (stale != null) {
+ stale.close()
+ } else {
+ break
+ }
+ }
+
+ // ここでリスナーを設定(本番フレームのみ受信)
+ imageReader.setOnImageAvailableListener({ reader ->
+ if (resultSent) return@setOnImageAvailableListener
+ val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener
+ resultSent = true
+
+ try {
+ val yPlane = image.planes[0]
+ val uPlane = image.planes[1]
+ val vPlane = image.planes[2]
+
+ val width = image.width
+ val height = image.height
+
+ val yBytes = ByteArray(yPlane.buffer.remaining())
+ val uBytes = ByteArray(uPlane.buffer.remaining())
+ val vBytes = ByteArray(vPlane.buffer.remaining())
+
+ yPlane.buffer.get(yBytes)
+ uPlane.buffer.get(uBytes)
+ vPlane.buffer.get(vBytes)
+
+ val yRowStride = yPlane.rowStride
+ val uvRowStride = uPlane.rowStride
+ val uvPixelStride = uPlane.pixelStride
+
+ image.close()
+
+ onFrame(
+ yBytes, uBytes, vBytes,
+ width, height,
+ yRowStride, uvRowStride, uvPixelStride,
+ reader, camera, result
+ )
+ } catch (e: Exception) {
+ image.close()
+ reader.close()
+ cleanup()
+ Handler(context.mainLooper).post {
+ result.error("PROCESS_ERROR", "画像データの処理に失敗: ${e.message}", null)
+ }
+ }
+ }, backgroundHandler)
+
+ // 本番キャプチャ実行
+ val captureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE).apply {
+ addTarget(imageReader.surface)
+ set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
+ set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF)
+ }
+ session.capture(captureRequest.build(), null, backgroundHandler)
+ } catch (e: Exception) {
+ cleanup()
+ imageReader.close()
+ Handler(context.mainLooper).post {
+ result.error("CAPTURE_ERROR", "キャプチャに失敗: ${e.message}", null)
+ }
+ }
+ }, 1000)
+ } catch (e: Exception) {
+ cleanup()
+ imageReader.close()
+ Handler(context.mainLooper).post {
+ result.error("CAPTURE_ERROR", "キャプチャに失敗: ${e.message}", null)
+ }
+ }
+ }
+
+ override fun onConfigureFailed(session: CameraCaptureSession) {
+ cleanup()
+ imageReader.close()
+ Handler(context.mainLooper).post {
+ result.error("SESSION_ERROR", "カメラセッションの設定に失敗", null)
+ }
+ }
+ },
+ backgroundHandler
+ )
+ } catch (e: Exception) {
+ cleanup()
+ imageReader.close()
+ Handler(context.mainLooper).post {
+ result.error("CAPTURE_ERROR", "キャプチャに失敗: ${e.message}", null)
+ }
+ }
+ }
+
+ override fun onDisconnected(camera: CameraDevice) {
+ camera.close()
+ cameraDevice = null
+ imageReader.close()
+ stopBackgroundThread()
+ }
+
+ override fun onError(camera: CameraDevice, error: Int) {
+ camera.close()
+ cameraDevice = null
+ imageReader.close()
+ Handler(context.mainLooper).post {
+ result.error("CAMERA_ERROR", "カメラエラー: $error", null)
+ stopBackgroundThread()
+ }
+ }
+ }, backgroundHandler)
+ } catch (e: SecurityException) {
+ imageReader.close()
+ returnError(result, "PERMISSION_ERROR", "カメラの権限がありません")
+ }
+ }
+
+ /**
+ * フロントカメラの Camera2 API 能力を読み出して Map で返す.
+ *
+ * CameraCharacteristics から各種パラメータを取得し,null セーフに処理する.
+ * フロントカメラが見つからない場合は error を返す.
+ */
+ private fun getCameraDiagnostics(result: MethodChannel.Result) {
+ val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+
+ val cameraId = findFrontCameraId(cameraManager)
+
+ if (cameraId == null) {
+ result.error("NO_CAMERA", "フロントカメラが見つかりません", null)
+ return
+ }
+
+ val c = cameraManager.getCameraCharacteristics(cameraId)
+ val streamConfigMap = c.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
+
+ // rawOutputSizes
+ val rawOutputSizes = streamConfigMap
+ ?.getOutputSizes(ImageFormat.RAW_SENSOR)
+ ?.map { "${it.width}x${it.height}" }
+ ?: emptyList()
+
+ // yuvOutputSizes
+ val yuvOutputSizes = streamConfigMap
+ ?.getOutputSizes(ImageFormat.YUV_420_888)
+ ?.map { "${it.width}x${it.height}" }
+ ?: emptyList()
+
+ // jpegOutputSizes
+ val jpegOutputSizes = streamConfigMap
+ ?.getOutputSizes(ImageFormat.JPEG)
+ ?.map { "${it.width}x${it.height}" }
+ ?: emptyList()
+
+ val diagnostics = mapOf(
+ "cameraId" to cameraId,
+ "hardwareLevel" to c.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL),
+ "capabilities" to c.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)?.toList(),
+ "exposureTimeRangeNs" to c.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE)
+ ?.let { listOf(it.lower, it.upper) },
+ "sensitivityRange" to c.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE)
+ ?.let { listOf(it.lower, it.upper) },
+ "lscMapSize" to null, // 実キャプチャ後に CaptureResult から取得する必要があるため診断段階では null
+ "availableLensShadingMapModes" to c.get(
+ CameraCharacteristics.STATISTICS_INFO_AVAILABLE_LENS_SHADING_MAP_MODES
+ )?.map { it },
+ "availableNoiseReductionModes" to c.get(
+ CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES
+ )?.toList(),
+ "availableEdgeModes" to c.get(CameraCharacteristics.EDGE_AVAILABLE_EDGE_MODES)?.toList(),
+ "availableTonemapModes" to c.get(
+ CameraCharacteristics.TONEMAP_AVAILABLE_TONE_MAP_MODES
+ )?.toList(),
+ "availableShadingModes" to c.get(CameraCharacteristics.SHADING_AVAILABLE_MODES)?.toList(),
+ "availableAwbModes" to c.get(
+ CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES
+ )?.toList(),
+ "availableAfModes" to c.get(
+ CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES
+ )?.toList(),
+ "minFocusDistance" to c.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE),
+ "hyperfocalDistance" to c.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE),
+ "rawOutputSizes" to rawOutputSizes,
+ "yuvOutputSizes" to yuvOutputSizes,
+ "jpegOutputSizes" to jpegOutputSizes,
+ "colorFilterArrangement" to c.get(
+ CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT
+ ),
+ "blackLevelPattern" to c.get(CameraCharacteristics.SENSOR_BLACK_LEVEL_PATTERN)?.toString(),
+ "whiteLevel" to c.get(CameraCharacteristics.SENSOR_INFO_WHITE_LEVEL),
+ "physicalSize" to c.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)
+ ?.let { listOf(it.width, it.height) },
+ "pixelArraySize" to c.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE)
+ ?.let { listOf(it.width, it.height) },
+ "activeArraySize" to c.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)?.toString(),
+ )
+
+ result.success(diagnostics)
+ }
+
+ private fun cleanup() {
+ cameraDevice?.close()
+ cameraDevice = null
+ stopBackgroundThread()
+ }
+
+ private fun returnError(result: MethodChannel.Result, code: String, message: String) {
+ result.error(code, message, null)
+ stopBackgroundThread()
+ }
+
+ private fun startBackgroundThread() {
+ backgroundThread = HandlerThread("RawCaptureThread").also { it.start() }
+ backgroundHandler = Handler(backgroundThread!!.looper)
+ }
+
+ private fun stopBackgroundThread() {
+ backgroundThread?.quitSafely()
+ try {
+ backgroundThread?.join(3000)
+ } catch (_: InterruptedException) {
+ }
+ backgroundThread = null
+ backgroundHandler = null
+ }
+}
diff --git a/android/app/src/main/kotlin/com/example/smtias_capture/YuvJpegConverter.kt b/android/app/src/main/kotlin/com/example/smtias_capture/YuvJpegConverter.kt
new file mode 100644
index 0000000..733c2af
--- /dev/null
+++ b/android/app/src/main/kotlin/com/example/smtias_capture/YuvJpegConverter.kt
@@ -0,0 +1,77 @@
+package com.example.smtias_capture
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.ImageFormat
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.graphics.YuvImage
+import java.io.ByteArrayOutputStream
+
+/**
+ * YUV_420_888 のプレーンデータを NV21 経由で JPEG に変換するユーティリティ.
+ *
+ * ライブプレビューのスナップショット保存に使用する(撮影本体の RAW とは独立).
+ * 回転・左右反転にも対応する.
+ */
+internal object YuvJpegConverter {
+
+ /**
+ * YUV_420_888 のプレーン群を JPEG バイト列に変換する.
+ *
+ * [rotation] は時計回りの度数,[mirror] が true なら左右反転を適用する.
+ */
+ fun convert(
+ width: Int,
+ height: Int,
+ yPlane: ByteArray,
+ uPlane: ByteArray,
+ vPlane: ByteArray,
+ yRowStride: Int,
+ uvRowStride: Int,
+ uvPixelStride: Int,
+ rotation: Int,
+ mirror: Boolean,
+ quality: Int,
+ ): ByteArray {
+ // YUV_420_888 → NV21 変換
+ val nv21 = ByteArray(width * height * 3 / 2)
+
+ // Y プレーンをコピー
+ for (row in 0 until height) {
+ System.arraycopy(yPlane, row * yRowStride, nv21, row * width, width)
+ }
+
+ // UV プレーンを NV21 形式(VUVU...)にインターリーブ
+ val uvOffset = width * height
+ for (row in 0 until height / 2) {
+ for (col in 0 until width / 2) {
+ val uvIndex = row * uvRowStride + col * uvPixelStride
+ nv21[uvOffset + row * width + col * 2] = vPlane[uvIndex]
+ nv21[uvOffset + row * width + col * 2 + 1] = uPlane[uvIndex]
+ }
+ }
+
+ // NV21 → JPEG
+ val yuvImage = YuvImage(nv21, ImageFormat.NV21, width, height, null)
+ val jpegStream = ByteArrayOutputStream()
+ yuvImage.compressToJpeg(Rect(0, 0, width, height), quality, jpegStream)
+
+ // 回転・反転が必要な場合
+ return if (rotation != 0 || mirror) {
+ val bitmap = BitmapFactory.decodeByteArray(jpegStream.toByteArray(), 0, jpegStream.size())
+ val matrix = Matrix()
+ if (rotation != 0) matrix.postRotate(rotation.toFloat())
+ if (mirror) matrix.postScale(-1f, 1f)
+ val transformed = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
+ bitmap.recycle()
+
+ val outStream = ByteArrayOutputStream()
+ transformed.compress(Bitmap.CompressFormat.JPEG, quality, outStream)
+ transformed.recycle()
+ outStream.toByteArray()
+ } else {
+ jpegStream.toByteArray()
+ }
+ }
+}
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
deleted file mode 100644
index 67967f8..0000000
--- a/android/app/src/test/kotlin/com/example/mini_tias/ImageStatisticsTest.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-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/android/app/src/test/kotlin/com/example/smtias_capture/ImageStatisticsTest.kt b/android/app/src/test/kotlin/com/example/smtias_capture/ImageStatisticsTest.kt
new file mode 100644
index 0000000..f7085d5
--- /dev/null
+++ b/android/app/src/test/kotlin/com/example/smtias_capture/ImageStatisticsTest.kt
@@ -0,0 +1,89 @@
+package com.example.smtias_capture
+
+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_06_\343\202\263\343\203\274\343\203\207\343\202\243\343\203\263\343\202\260\350\246\217\347\264\204.md" "b/docs/01_GUIDE/GUIDE_06_\343\202\263\343\203\274\343\203\207\343\202\243\343\203\263\343\202\260\350\246\217\347\264\204.md"
index 93fddf0..93ad50a 100644
--- "a/docs/01_GUIDE/GUIDE_06_\343\202\263\343\203\274\343\203\207\343\202\243\343\203\263\343\202\260\350\246\217\347\264\204.md"
+++ "b/docs/01_GUIDE/GUIDE_06_\343\202\263\343\203\274\343\203\207\343\202\243\343\203\263\343\202\260\350\246\217\347\264\204.md"
@@ -1,6 +1,6 @@
# コーディング規約 (Coding Standards)
-本ドキュメントでは,MiniTIAS プロジェクトの Dart / Flutter コーディング規約を定義する.
+本ドキュメントでは,SmTIAS-Capture プロジェクトの Dart / Flutter コーディング規約を定義する.
基本方針として [Effective Dart](https://dart.dev/effective-dart) に準拠し,プロジェクト固有のルールのみ本書で定める.
## フォーマット (Formatting)
@@ -42,8 +42,8 @@
import 'package:provider/provider.dart';
// 4. プロジェクト内
-import 'package:mini_tias/providers/camera_provider.dart';
-import 'package:mini_tias/services/file_service.dart';
+import 'package:smtias_capture/providers/camera_provider.dart';
+import 'package:smtias_capture/services/file_service.dart';
```
※ `dart fix --apply` および `dart format` で自動整列されるため,手動調整は不要.
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 a1d40e1..cca870c 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"
@@ -1,6 +1,6 @@
# テスト方針 (Testing Strategy)
-本ドキュメントでは,MiniTIAS プロジェクトのテスト方針を定義する.
+本ドキュメントでは,SmTIAS-Capture プロジェクトのテスト方針を定義する.
## 基本方針 (Principles)
diff --git "a/docs/02_ENV/ENV_01_\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.md" "b/docs/02_ENV/ENV_01_\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.md"
index 5273dfc..986fdde 100644
--- "a/docs/02_ENV/ENV_01_\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.md"
+++ "b/docs/02_ENV/ENV_01_\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.md"
@@ -2,7 +2,7 @@
## 概要 (Overview)
-MiniTIAS は Flutter を用いたモバイルアプリケーションである.撮影の中核(Camera2 / `DngCreator` / 画素統計)を Android ネイティブで実装しているため,当面は **Android 専用** とする.iOS へ展開する場合は撮影パイプラインの再実装が必要になる.
+SmTIAS-Capture は Flutter を用いたモバイルアプリケーションである.撮影の中核(Camera2 / `DngCreator` / 画素統計)を Android ネイティブで実装しているため,当面は **Android 専用** とする.iOS へ展開する場合は撮影パイプラインの再実装が必要になる.
## 試験端末 (Test Device)
diff --git "a/docs/02_ENV/ENV_02_\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.md" "b/docs/02_ENV/ENV_02_\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.md"
index 29a8bdd..92e849d 100644
--- "a/docs/02_ENV/ENV_02_\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.md"
+++ "b/docs/02_ENV/ENV_02_\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.md"
@@ -28,8 +28,8 @@
### リポジトリの取得 (Clone Repository)
```bash
-git clone https://github.com//MiniTIAS.git
-cd MiniTIAS
+git clone https://github.com//SmTIAS-Capture.git
+cd SmTIAS-Capture
```
※ Git が未インストールの場合は `scoop install git` を先に実行する.
diff --git "a/docs/02_ENV/ENV_03_\347\256\241\347\220\206\350\200\205\347\224\250\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.md" "b/docs/02_ENV/ENV_03_\347\256\241\347\220\206\350\200\205\347\224\250\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.md"
index e8887f2..6e057d1 100644
--- "a/docs/02_ENV/ENV_03_\347\256\241\347\220\206\350\200\205\347\224\250\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.md"
+++ "b/docs/02_ENV/ENV_03_\347\256\241\347\220\206\350\200\205\347\224\250\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.md"
@@ -5,7 +5,7 @@
## GitHub リポジトリの作成 (Create GitHub Repository)
```bash
-gh repo create MiniTIAS --private
+gh repo create SmTIAS-Capture --private
```
※ GitHub CLI(`gh`)が未認証の場合は `gh auth login` を先に実行する.
@@ -17,7 +17,7 @@
### ディレクトリ構成
```text
-MiniTIAS/
+SmTIAS-Capture/
├── scripts/
│ └── setup.ps1
```
@@ -73,7 +73,7 @@
## Flutter プロジェクトの作成 (Create Flutter Project)
```bash
-flutter create --org com.example mini_tias
+flutter create --org com.example smtias_capture
```
※ パッケージ名・組織名は正式に決定してから実行する.
diff --git "a/docs/03_PLAN/PLAN_01_\350\246\201\344\273\266\345\256\232\347\276\251\346\233\270.md" "b/docs/03_PLAN/PLAN_01_\350\246\201\344\273\266\345\256\232\347\276\251\346\233\270.md"
index 73ffc2f..2e6dd8b 100644
--- "a/docs/03_PLAN/PLAN_01_\350\246\201\344\273\266\345\256\232\347\276\251\346\233\270.md"
+++ "b/docs/03_PLAN/PLAN_01_\350\246\201\344\273\266\345\256\232\347\276\251\346\233\270.md"
@@ -4,7 +4,7 @@
### プロジェクト名
-MiniTIAS(ミニティアス)
+SmTIAS-Capture(SmTIAS 撮影アプリ)
### 背景 (Background)
@@ -12,7 +12,7 @@
### 目的 (Objective)
-TIAS のスマートフォン版(MiniTIAS)を開発し,舌画像撮影の小型化・簡便化を実現する.研究の初期段階として,既存 TIAS と MiniTIAS で同一対象を撮影し,画像品質の差を定量的に検証する.その結果をもとに差を埋める改善を行う.
+TIAS のスマートフォン版(SmTIAS-Capture)を開発し,舌画像撮影の小型化・簡便化を実現する.研究の初期段階として,既存 TIAS と SmTIAS-Capture で同一対象を撮影し,画像品質の差を定量的に検証する.その結果をもとに差を埋める改善を行う.
### 対象ユーザー (Target Users)
diff --git "a/docs/04_SPEC/SPEC_01_\347\224\273\351\235\242\346\251\237\350\203\275\344\273\225\346\247\230\346\233\270.md" "b/docs/04_SPEC/SPEC_01_\347\224\273\351\235\242\346\251\237\350\203\275\344\273\225\346\247\230\346\233\270.md"
index ab5a31a..f10978e 100644
--- "a/docs/04_SPEC/SPEC_01_\347\224\273\351\235\242\346\251\237\350\203\275\344\273\225\346\247\230\346\233\270.md"
+++ "b/docs/04_SPEC/SPEC_01_\347\224\273\351\235\242\346\251\237\350\203\275\344\273\225\346\247\230\346\233\270.md"
@@ -1,6 +1,6 @@
# 画面機能仕様書 (Screen Specification)
-本ドキュメントでは,MiniTIAS アプリの画面設計・画面遷移・各画面の UI と動作・パーミッションを定義する.
+本ドキュメントでは,SmTIAS-Capture アプリの画面設計・画面遷移・各画面の UI と動作・パーミッションを定義する.
- カメラ制御・撮影フロー・ファイル管理は [SPEC_02 カメラ撮影仕様書](SPEC_02_カメラ撮影仕様書.md) を参照
- アーキテクチャ・非機能要件は [SPEC_03 アーキテクチャと非機能](SPEC_03_アーキテクチャと非機能.md) を参照
diff --git "a/docs/04_SPEC/SPEC_02_\343\202\253\343\203\241\343\203\251\346\222\256\345\275\261\344\273\225\346\247\230\346\233\270.md" "b/docs/04_SPEC/SPEC_02_\343\202\253\343\203\241\343\203\251\346\222\256\345\275\261\344\273\225\346\247\230\346\233\270.md"
index 888e9e8..a649254 100644
--- "a/docs/04_SPEC/SPEC_02_\343\202\253\343\203\241\343\203\251\346\222\256\345\275\261\344\273\225\346\247\230\346\233\270.md"
+++ "b/docs/04_SPEC/SPEC_02_\343\202\253\343\203\241\343\203\251\346\222\256\345\275\261\344\273\225\346\247\230\346\233\270.md"
@@ -1,6 +1,6 @@
# カメラ・撮影仕様書 (Camera & Capture Specification)
-本ドキュメントでは,MiniTIAS のカメラ制御・撮影フロー・保存ファイルの仕様を定義する.
+本ドキュメントでは,SmTIAS-Capture のカメラ制御・撮影フロー・保存ファイルの仕様を定義する.
- 画面・UI・パーミッションは [SPEC_01 画面機能仕様書](SPEC_01_画面機能仕様書.md) を参照
- アーキテクチャ・非機能要件は [SPEC_03 アーキテクチャと非機能](SPEC_03_アーキテクチャと非機能.md) を参照
diff --git "a/docs/04_SPEC/SPEC_03_\343\202\242\343\203\274\343\202\255\343\203\206\343\202\257\343\203\201\343\203\243\343\201\250\351\235\236\346\251\237\350\203\275.md" "b/docs/04_SPEC/SPEC_03_\343\202\242\343\203\274\343\202\255\343\203\206\343\202\257\343\203\201\343\203\243\343\201\250\351\235\236\346\251\237\350\203\275.md"
index 724b121..5e69362 100644
--- "a/docs/04_SPEC/SPEC_03_\343\202\242\343\203\274\343\202\255\343\203\206\343\202\257\343\203\201\343\203\243\343\201\250\351\235\236\346\251\237\350\203\275.md"
+++ "b/docs/04_SPEC/SPEC_03_\343\202\242\343\203\274\343\202\255\343\203\206\343\202\257\343\203\201\343\203\243\343\201\250\351\235\236\346\251\237\350\203\275.md"
@@ -1,6 +1,6 @@
# アーキテクチャ・非機能仕様書 (Architecture & Non-functional Specification)
-本ドキュメントでは,MiniTIAS のアーキテクチャ(コード構成)と非機能要件を定義する.
+本ドキュメントでは,SmTIAS-Capture のアーキテクチャ(コード構成)と非機能要件を定義する.
- 画面・UI・パーミッションは [SPEC_01 画面機能仕様書](SPEC_01_画面機能仕様書.md) を参照
- カメラ制御・撮影フロー・ファイル管理は [SPEC_02 カメラ撮影仕様書](SPEC_02_カメラ撮影仕様書.md) を参照
diff --git "a/docs/05_TECH/TECH_01_\345\256\232\351\207\217\343\203\242\343\203\274\343\203\211\350\250\255\350\250\210\343\201\256\347\265\214\347\267\257\343\201\250\345\220\210\346\204\217.md" "b/docs/05_TECH/TECH_01_\345\256\232\351\207\217\343\203\242\343\203\274\343\203\211\350\250\255\350\250\210\343\201\256\347\265\214\347\267\257\343\201\250\345\220\210\346\204\217.md"
index 035d393..b91ee8b 100644
--- "a/docs/05_TECH/TECH_01_\345\256\232\351\207\217\343\203\242\343\203\274\343\203\211\350\250\255\350\250\210\343\201\256\347\265\214\347\267\257\343\201\250\345\220\210\346\204\217.md"
+++ "b/docs/05_TECH/TECH_01_\345\256\232\351\207\217\343\203\242\343\203\274\343\203\211\350\250\255\350\250\210\343\201\256\347\265\214\347\267\257\343\201\250\345\220\210\346\204\217.md"
@@ -1,6 +1,6 @@
# 定量モード 設計の経緯と合意 (Quantitative Mode: Rationale & Agreements)
-本書は SmTIAS-LightSim 側と MiniTIAS 側の**往復書簡(旧 TECH_01 要求仕様 / TECH_03 実装報告・相談 / TECH_04 回答)を統合・要約したアーカイブ**である.定量撮影モードがなぜ・どう決まったかの記録に絞る.
+本書は SmTIAS-LightSim 側と SmTIAS-Capture 側の**往復書簡(旧 TECH_01 要求仕様 / TECH_03 実装報告・相談 / TECH_04 回答)を統合・要約したアーカイブ**である.定量撮影モードがなぜ・どう決まったかの記録に絞る.
- **現行の確定仕様** → [SPEC_02 カメラ撮影仕様書](../04_SPEC/SPEC_02_カメラ撮影仕様書.md)(画面は [SPEC_01](../04_SPEC/SPEC_01_画面機能仕様書.md)、設計・非機能は [SPEC_03](../04_SPEC/SPEC_03_アーキテクチャと非機能.md))
- **実装の経緯・設計判断・失敗パターン** → [PROGRESS.md](../PROGRESS.md)
@@ -10,7 +10,7 @@
## 背景:なぜ定量モードが必要だったか
-SmTIAS-LightSim(Mitsuba 3 による光散乱シミュ)と実機撮影を定量比較する研究で、シミュ vs 実機の残差が縮まらなかった。原因は、MiniTIAS の通常撮影(PNG)が **AE/AWB/NR/EDGE/TONEMAP すべて自動**で、同じシーンを撮っても値が揺らいでいたこと。つまり「比較対象の実機データ自体が定量的に不安定」だった。
+SmTIAS-LightSim(Mitsuba 3 による光散乱シミュ)と実機撮影を定量比較する研究で、シミュ vs 実機の残差が縮まらなかった。原因は、SmTIAS-Capture の通常撮影(PNG)が **AE/AWB/NR/EDGE/TONEMAP すべて自動**で、同じシーンを撮っても値が揺らいでいたこと。つまり「比較対象の実機データ自体が定量的に不安定」だった。
→ Camera2 API を**完全マニュアル制御**し、ISP 介入を排した **RAW(DNG)**で撮影するモードを追加して固定化する、という方針になった。
@@ -47,7 +47,7 @@
- 実装中の観察:`SHADING_MODE=OFF` だと LSC マップが**全要素 1.0**(Android 仕様)になり情報が取れない。`HIGH_QUALITY` だと ISP が LSC を適用し、**実ゲインマップ**が取得できる。
- 物理的整合性:SmTIAS-LightSim の Mitsuba **orthographic 出力**は平行光線サンプリングで cos⁴ falloff を持たず、**「ISP が完璧に LSC 補正できた場合の理想値」相当**。これと **HIGH_QUALITY DNG** を直接比較するのが物理的に最も整合する(残差=「実機 LSC 補正の不完全度+シミュ物理モデル誤差」=まさに評価したい本質)。
- 逆に `OFF`(生 Bayer)を使うとシミュ側に物理 cos⁴ モデル(絞り径・口径食など)を足す必要があり、**自由度が増えて overfitting リスク**が上がる。
-- 結論:**HIGH_QUALITY 維持。MiniTIAS 側の実装変更は不要**。LSC マップは検証用に meta.json へ温存する。
+- 結論:**HIGH_QUALITY 維持。SmTIAS-Capture 側の実装変更は不要**。LSC マップは検証用に meta.json へ温存する。
### 将来 SHADING_OFF を追加検証する条件(フォールバック C1–C3)
@@ -70,7 +70,7 @@
「再現性より先に、そもそもシミュと対応が取れるかを 1 ショットで確認する」二段階方式:
-- **フェーズ A(1 ショット対応検証)**: MiniTIAS 側は ①画素統計を meta.json に追加 ②Hot pixel map 取得 ③白板 1 枚を試し撮りして adb pull 転送。SmTIAS 側が DNG/メタ読み込みを実装し 1 ショット突合 → 残差 2.1× がどう変化するか観察。**①②は実装済み**。
+- **フェーズ A(1 ショット対応検証)**: SmTIAS-Capture 側は ①画素統計を meta.json に追加 ②Hot pixel map 取得 ③白板 1 枚を試し撮りして adb pull 転送。SmTIAS 側が DNG/メタ読み込みを実装し 1 ショット突合 → 残差 2.1× がどう変化するか観察。**①②は実装済み**。
- **フェーズ B(連射 V1 再現性)**: フェーズ A で対応の見通しが立った場合のみ着手。10 連射 UI(`*_burst{0..9}.dng`)+ 再現性検証(mean CoV < 1%)。
## データ転送
diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md
index a8ac6de..e4e4419 100644
--- a/docs/PROGRESS.md
+++ b/docs/PROGRESS.md
@@ -32,7 +32,7 @@
## 定量モード撮影機能(DNG 経路)完成
- 状況: シャッターで `MiniTIAS_QM_YYYYMMDD_HHmmss.dng`(10-bit BGGR Bayer 3264×2448,約 16MB)と `.meta.json`(settings/actual/sensorCharacteristics/lscMap)を `Pictures/MiniTIAS/` に保存.既存 PNG 経路は `captureFullResolutionPng` として温存
-- 動機: SmTIAS-LightSim 側でのシミュ vs 実機の定量比較で残差 2.1× が解消しない問題.原因は MiniTIAS 通常モードが AE/AWB/NR/EDGE/TONEMAP 全自動で撮影ごとに値が揺らいでいたこと(TECH_01 参照)
+- 動機: SmTIAS-LightSim 側でのシミュ vs 実機の定量比較で残差 2.1× が解消しない問題.原因は SmTIAS-Capture 通常モードが AE/AWB/NR/EDGE/TONEMAP 全自動で撮影ごとに値が揺らいでいたこと(TECH_01 参照)
- 設計判断: Camera2 マニュアル制御で `CONTROL_MODE=OFF` + AE/AWB/AF/NR/EDGE OFF + TONEMAP 線形 + 露光 1/60s 固定 + ISO 40 固定.`SHADING_MODE` は HIGH_QUALITY を採用(LSC マップ実値を取りたいため.OFF だと単位マップしか返らない).フォーマットは DNG (RAW_SENSOR) を採用(8bit PNG では舌の色再現には情報量不足)
- 失敗パターン: ① MTP コピー直後の DNG が PC で 0 バイトに見える→「MethodChannel 16MB 転送が失敗」と誤診断.実際は MTP コピー遅延で,30 秒待てば全データあった.② `SHADING_MODE = OFF` 初回採用で LSC マップが全 1.0(Android 仕様)→ HIGH_QUALITY に変更で実値取得
- 後段: 露光時間・ISO のキャリブ機能(白板撮影で AE 一回走らせて値を固定する半自動モード),撮影連射再現性検証 V1(CoV < 1%),他機種展開時の `getCameraDiagnostics` 再実行
diff --git a/lib/app.dart b/lib/app.dart
index 35b393c..95b222d 100644
--- a/lib/app.dart
+++ b/lib/app.dart
@@ -2,15 +2,15 @@
import 'package:flutter/material.dart';
-import 'package:mini_tias/screens/home_screen.dart';
+import 'package:smtias_capture/screens/home_screen.dart';
-/// MiniTIAS アプリのルートウィジェット.
+/// SmTIAS-Capture アプリのルートウィジェット.
///
/// 端末を逆さに置いて使用するため,アプリ全体を 180° 回転する.
/// これにより画面・ダイアログ・スナックバー等すべての UI が
/// 操作者から見て正しい向きで表示される.
-class MiniTiasApp extends StatelessWidget {
- const MiniTiasApp({super.key});
+class SmtiasCaptureApp extends StatelessWidget {
+ const SmtiasCaptureApp({super.key});
@override
Widget build(BuildContext context) {
@@ -19,7 +19,7 @@
child: Transform.rotate(
angle: math.pi,
child: MaterialApp(
- title: 'MiniTIAS',
+ title: 'SmTIAS-Capture',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blueGrey),
useMaterial3: true,
diff --git a/lib/main.dart b/lib/main.dart
index d024a8a..555d9b6 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -2,10 +2,10 @@
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
-import 'package:mini_tias/app.dart';
-import 'package:mini_tias/providers/camera_provider.dart';
-import 'package:mini_tias/providers/diagnostics_provider.dart';
-import 'package:mini_tias/providers/gallery_provider.dart';
+import 'package:smtias_capture/app.dart';
+import 'package:smtias_capture/providers/camera_provider.dart';
+import 'package:smtias_capture/providers/diagnostics_provider.dart';
+import 'package:smtias_capture/providers/gallery_provider.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
@@ -18,7 +18,7 @@
ChangeNotifierProvider(create: (_) => GalleryProvider()),
ChangeNotifierProvider(create: (_) => DiagnosticsProvider()),
],
- child: const MiniTiasApp(),
+ child: const SmtiasCaptureApp(),
),
);
}
diff --git a/lib/providers/camera_provider.dart b/lib/providers/camera_provider.dart
index 5d18d5e..f90f639 100644
--- a/lib/providers/camera_provider.dart
+++ b/lib/providers/camera_provider.dart
@@ -1,11 +1,11 @@
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
-import 'package:mini_tias/models/preview_frame.dart';
-import 'package:mini_tias/services/file_service.dart';
-import 'package:mini_tias/services/permission_service.dart';
-import 'package:mini_tias/services/raw_capture_service.dart';
-import 'package:mini_tias/services/sound_service.dart';
+import 'package:smtias_capture/models/preview_frame.dart';
+import 'package:smtias_capture/services/file_service.dart';
+import 'package:smtias_capture/services/permission_service.dart';
+import 'package:smtias_capture/services/raw_capture_service.dart';
+import 'package:smtias_capture/services/sound_service.dart';
/// カメラの初期化・プレビュー制御・撮影実行・ライフサイクルを管理する.
class CameraProvider extends ChangeNotifier {
diff --git a/lib/providers/diagnostics_provider.dart b/lib/providers/diagnostics_provider.dart
index 05011bd..720710a 100644
--- a/lib/providers/diagnostics_provider.dart
+++ b/lib/providers/diagnostics_provider.dart
@@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart';
-import 'package:mini_tias/services/file_service.dart';
-import 'package:mini_tias/services/raw_capture_service.dart';
+import 'package:smtias_capture/services/file_service.dart';
+import 'package:smtias_capture/services/raw_capture_service.dart';
/// カメラ診断機能の状態管理.
/// Camera2 API の能力を読み出し JSON ファイルに保存する.
diff --git a/lib/screens/capture_screen.dart b/lib/screens/capture_screen.dart
index 16499a0..8957ad0 100644
--- a/lib/screens/capture_screen.dart
+++ b/lib/screens/capture_screen.dart
@@ -4,9 +4,9 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
-import 'package:mini_tias/providers/camera_provider.dart';
-import 'package:mini_tias/widgets/camera_preview.dart';
-import 'package:mini_tias/widgets/shutter_button.dart';
+import 'package:smtias_capture/providers/camera_provider.dart';
+import 'package:smtias_capture/widgets/camera_preview.dart';
+import 'package:smtias_capture/widgets/shutter_button.dart';
/// 撮影画面.カメラプレビューとシャッターボタンを表示する.
class CaptureScreen extends StatefulWidget {
diff --git a/lib/screens/gallery_screen.dart b/lib/screens/gallery_screen.dart
index 93aea80..5a08891 100644
--- a/lib/screens/gallery_screen.dart
+++ b/lib/screens/gallery_screen.dart
@@ -3,8 +3,8 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
-import 'package:mini_tias/providers/gallery_provider.dart';
-import 'package:mini_tias/widgets/image_grid.dart';
+import 'package:smtias_capture/providers/gallery_provider.dart';
+import 'package:smtias_capture/widgets/image_grid.dart';
/// 一覧画面.撮影済み画像のサムネイルグリッドを表示する.
class GalleryScreen extends StatefulWidget {
diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart
index 7cb181d..9119031 100644
--- a/lib/screens/home_screen.dart
+++ b/lib/screens/home_screen.dart
@@ -2,10 +2,10 @@
import 'package:provider/provider.dart';
import 'package:screen_brightness/screen_brightness.dart';
-import 'package:mini_tias/providers/camera_provider.dart';
-import 'package:mini_tias/providers/gallery_provider.dart';
-import 'package:mini_tias/screens/capture_screen.dart';
-import 'package:mini_tias/screens/gallery_screen.dart';
+import 'package:smtias_capture/providers/camera_provider.dart';
+import 'package:smtias_capture/providers/gallery_provider.dart';
+import 'package:smtias_capture/screens/capture_screen.dart';
+import 'package:smtias_capture/screens/gallery_screen.dart';
/// BottomNavigationBar で撮影画面と一覧画面を切り替えるホーム画面.
///
diff --git a/lib/services/raw_capture_service.dart b/lib/services/raw_capture_service.dart
index 223eab1..0e3b8a1 100644
--- a/lib/services/raw_capture_service.dart
+++ b/lib/services/raw_capture_service.dart
@@ -2,7 +2,7 @@
/// Camera2 API を使用してフル解像度の画像をキャプチャするサービス.
class RawCaptureService {
- static const _channel = MethodChannel('com.example.mini_tias/raw_capture');
+ static const _channel = MethodChannel('com.example.smtias_capture/raw_capture');
/// フロントカメラからフル解像度の PNG バイト列をキャプチャする(ネイティブ変換).
Future captureFullResolutionPng() async {
diff --git a/lib/services/sound_service.dart b/lib/services/sound_service.dart
index f8ba387..ea1ee2b 100644
--- a/lib/services/sound_service.dart
+++ b/lib/services/sound_service.dart
@@ -1,6 +1,6 @@
import 'package:shared_preferences/shared_preferences.dart';
-import 'package:mini_tias/services/raw_capture_service.dart';
+import 'package:smtias_capture/services/raw_capture_service.dart';
/// サウンドフィードバックの ON/OFF を管理し,再生を担うサービス.
///
diff --git a/pubspec.yaml b/pubspec.yaml
index d56b6e4..cfe0796 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,4 +1,4 @@
-name: mini_tias
+name: smtias_capture
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
diff --git a/scripts/setup.ps1 b/scripts/setup.ps1
index 5516a28..3419aed 100644
--- a/scripts/setup.ps1
+++ b/scripts/setup.ps1
@@ -1,7 +1,7 @@
-# MiniTIAS Development Environment Setup Script
+# SmTIAS-Capture Development Environment Setup Script
# Usage: .\scripts\setup.ps1
-Write-Host "=== MiniTIAS Setup ===" -ForegroundColor Cyan
+Write-Host "=== SmTIAS-Capture Setup ===" -ForegroundColor Cyan
# Check Scoop
if (-not (Get-Command scoop -ErrorAction SilentlyContinue)) {
diff --git a/test/providers/gallery_provider_test.dart b/test/providers/gallery_provider_test.dart
index 68824a8..db08dca 100644
--- a/test/providers/gallery_provider_test.dart
+++ b/test/providers/gallery_provider_test.dart
@@ -99,7 +99,7 @@
setUp(() async {
tempDir = await Directory.systemTemp.createTemp(
- 'mini_tias_gallery_test_',
+ 'smtias_capture_gallery_test_',
);
provider = _GalleryProviderTestable(tempDir.path);
});
@@ -224,7 +224,7 @@
late _GalleryProviderTestable provider;
setUp(() async {
- tempDir = await Directory.systemTemp.createTemp('mini_tias_del_test_');
+ tempDir = await Directory.systemTemp.createTemp('smtias_capture_del_test_');
provider = _GalleryProviderTestable(tempDir.path);
});
diff --git a/test/services/file_service_test.dart b/test/services/file_service_test.dart
index 7e88747..842e219 100644
--- a/test/services/file_service_test.dart
+++ b/test/services/file_service_test.dart
@@ -4,7 +4,7 @@
import 'package:flutter_test/flutter_test.dart';
-import 'package:mini_tias/services/file_service.dart';
+import 'package:smtias_capture/services/file_service.dart';
void main() {
group('FileService.generateFileNameSync', () {
@@ -82,7 +82,7 @@
late _FileServiceTestable service;
setUp(() async {
- tempDir = await Directory.systemTemp.createTemp('mini_tias_test_');
+ tempDir = await Directory.systemTemp.createTemp('smtias_capture_test_');
service = _FileServiceTestable(tempDir.path);
});
@@ -173,7 +173,7 @@
setUp(() async {
tempDir = await Directory.systemTemp.createTemp(
- 'mini_tias_preview_test_',
+ 'smtias_capture_preview_test_',
);
service = _FileServiceTestablePreview(tempDir.path);
});
@@ -335,7 +335,7 @@
late _FileServiceTestableQuantitative service;
setUp(() async {
- tempDir = await Directory.systemTemp.createTemp('mini_tias_qm_test_');
+ tempDir = await Directory.systemTemp.createTemp('smtias_capture_qm_test_');
service = _FileServiceTestableQuantitative(tempDir.path);
});
diff --git a/test/services/raw_capture_service_test.dart b/test/services/raw_capture_service_test.dart
index 68daee8..3b0c729 100644
--- a/test/services/raw_capture_service_test.dart
+++ b/test/services/raw_capture_service_test.dart
@@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
-import 'package:mini_tias/services/raw_capture_service.dart';
+import 'package:smtias_capture/services/raw_capture_service.dart';
void main() {
group('QuantitativeCaptureResult.fromMap', () {
diff --git a/test/services/sound_service_test.dart b/test/services/sound_service_test.dart
index 150706c..100e03b 100644
--- a/test/services/sound_service_test.dart
+++ b/test/services/sound_service_test.dart
@@ -1,8 +1,8 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
-import 'package:mini_tias/services/raw_capture_service.dart';
-import 'package:mini_tias/services/sound_service.dart';
+import 'package:smtias_capture/services/raw_capture_service.dart';
+import 'package:smtias_capture/services/sound_service.dart';
/// テスト用: RawCaptureService をスタブする.
///
diff --git a/test/widget_test.dart b/test/widget_test.dart
index cff4d50..60ed8c2 100644
--- a/test/widget_test.dart
+++ b/test/widget_test.dart
@@ -2,9 +2,9 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
-import 'package:mini_tias/app.dart';
-import 'package:mini_tias/providers/camera_provider.dart';
-import 'package:mini_tias/providers/gallery_provider.dart';
+import 'package:smtias_capture/app.dart';
+import 'package:smtias_capture/providers/camera_provider.dart';
+import 'package:smtias_capture/providers/gallery_provider.dart';
void main() {
testWidgets('アプリ起動時にナビゲーションバーが表示される', (WidgetTester tester) async {
@@ -14,7 +14,7 @@
ChangeNotifierProvider(create: (_) => CameraProvider()),
ChangeNotifierProvider(create: (_) => GalleryProvider()),
],
- child: const MiniTiasApp(),
+ child: const SmtiasCaptureApp(),
),
);
@@ -29,7 +29,7 @@
ChangeNotifierProvider(create: (_) => CameraProvider()),
ChangeNotifierProvider(create: (_) => GalleryProvider()),
],
- child: const MiniTiasApp(),
+ child: const SmtiasCaptureApp(),
),
);