Last active
July 5, 2025 12:44
-
-
Save flushpot1125/e111040e63dce48c666666bfaa34f222 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.example.objrecognition | |
| import android.Manifest | |
| import android.app.Activity | |
| import android.content.pm.PackageManager | |
| import android.graphics.Bitmap | |
| import android.hardware.camera2.CameraCaptureSession | |
| import android.hardware.camera2.CameraDevice | |
| import android.hardware.camera2.CameraManager | |
| import android.os.Build | |
| import android.os.Bundle | |
| import android.os.Handler | |
| import android.os.HandlerThread | |
| import android.util.Log | |
| import android.view.SurfaceHolder | |
| import android.view.SurfaceView | |
| import android.view.View | |
| import android.widget.Button | |
| import android.widget.ImageView | |
| import android.widget.TextView | |
| import android.widget.Toast | |
| import androidx.annotation.RequiresPermission | |
| import com.google.mlkit.vision.common.InputImage | |
| import com.google.mlkit.vision.objects.DetectedObject | |
| import com.google.mlkit.vision.objects.ObjectDetection | |
| import com.google.mlkit.vision.objects.defaults.ObjectDetectorOptions | |
| import java.util.concurrent.TimeUnit | |
| import android.view.Surface | |
| class MainActivity : Activity(), SurfaceHolder.Callback { | |
| companion object { | |
| private const val REQUEST_CAMERA_PERMISSION = 100 | |
| private const val TAG = "ObjectDetectionApp" | |
| } | |
| // UI要素 | |
| private lateinit var previewView: SurfaceView | |
| private lateinit var captureButton: Button | |
| private lateinit var resultText: TextView | |
| private lateinit var capturedImageView: ImageView | |
| // カメラ関連 | |
| private var cameraDevice: CameraDevice? = null | |
| private lateinit var cameraManager: CameraManager | |
| private lateinit var handler: Handler | |
| private lateinit var handlerThread: HandlerThread | |
| // 物体検出器(ML Kit) | |
| private lateinit var objectDetector: com.google.mlkit.vision.objects.ObjectDetector | |
| // カスタムの画像分類器(Teachable Machine モデル用) | |
| private lateinit var imageClassifier: ImageClassifier | |
| override fun onCreate(savedInstanceState: Bundle?) { | |
| super.onCreate(savedInstanceState) | |
| // フルスクリーン表示の設定 | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { | |
| window.setDecorFitsSystemWindows(false) | |
| } else { | |
| @Suppress("DEPRECATION") | |
| window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or | |
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or | |
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE | |
| } | |
| setContentView(R.layout.activity_main) | |
| // UI要素の初期化 | |
| previewView = findViewById(R.id.preview_view) | |
| captureButton = findViewById(R.id.capture_button) | |
| resultText = findViewById(R.id.result_text) | |
| capturedImageView = findViewById(R.id.captured_image) | |
| // サーフェスビューのコールバックを設定 | |
| previewView.holder.addCallback(this) | |
| // 物体検出器の初期化(MLKit - バックアップとして保持) | |
| val options = ObjectDetectorOptions.Builder() | |
| .setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE) | |
| .enableMultipleObjects() | |
| .enableClassification() | |
| .build() | |
| objectDetector = ObjectDetection.getClient(options) | |
| // カスタムの画像分類器を初期化 | |
| imageClassifier = ImageClassifier(this) | |
| // カメラボタンのクリックリスナー | |
| captureButton.setOnClickListener { | |
| takePicture() | |
| } | |
| // 権限チェック | |
| if (!hasRequiredPermissions()) { | |
| requestPermissions( | |
| arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE), | |
| REQUEST_CAMERA_PERMISSION | |
| ) | |
| } | |
| } | |
| override fun onResume() { | |
| super.onResume() | |
| startBackgroundThread() | |
| } | |
| override fun onPause() { | |
| stopBackgroundThread() | |
| super.onPause() | |
| } | |
| override fun onDestroy() { | |
| super.onDestroy() | |
| imageClassifier.close() // リソースを解放 | |
| } | |
| private fun startBackgroundThread() { | |
| handlerThread = HandlerThread("CameraBackground") | |
| handlerThread.start() | |
| handler = Handler(handlerThread.looper) | |
| } | |
| private fun stopBackgroundThread() { | |
| handlerThread.quitSafely() | |
| try { | |
| handlerThread.join() | |
| } catch (e: InterruptedException) { | |
| Log.e(TAG, "Error stopping background thread: ${e.message}") | |
| } | |
| } | |
| private fun hasRequiredPermissions(): Boolean { | |
| val cameraPermission = checkSelfPermission(Manifest.permission.CAMERA) | |
| val storagePermission = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { | |
| checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) | |
| } else { | |
| PackageManager.PERMISSION_GRANTED | |
| } | |
| return cameraPermission == PackageManager.PERMISSION_GRANTED && | |
| storagePermission == PackageManager.PERMISSION_GRANTED | |
| } | |
| @RequiresPermission(Manifest.permission.CAMERA) | |
| override fun onRequestPermissionsResult( | |
| requestCode: Int, | |
| permissions: Array<out String>, | |
| grantResults: IntArray | |
| ) { | |
| super.onRequestPermissionsResult(requestCode, permissions, grantResults) | |
| if (requestCode == REQUEST_CAMERA_PERMISSION) { | |
| if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { | |
| // 権限が付与された場合、カメラプレビューを再開 | |
| if (previewView.holder.surface.isValid) { | |
| openCamera() | |
| } | |
| } else { | |
| Toast.makeText(this, "カメラとストレージの権限が必要です", Toast.LENGTH_LONG).show() | |
| finish() | |
| } | |
| } | |
| } | |
| // SurfaceHolder.Callback の実装 | |
| @RequiresPermission(Manifest.permission.CAMERA) | |
| override fun surfaceCreated(holder: SurfaceHolder) { | |
| if (hasRequiredPermissions()) { | |
| openCamera() | |
| } | |
| } | |
| override fun surfaceDestroyed(holder: SurfaceHolder) { | |
| closeCamera() | |
| } | |
| @RequiresPermission(Manifest.permission.CAMERA) | |
| private fun openCamera() { | |
| try { | |
| cameraManager = getSystemService(CAMERA_SERVICE) as CameraManager | |
| // 利用可能なカメラをログに出力 | |
| logAvailableCameras() | |
| // 適切なカメラを選択(前面カメラがあればそれを優先) | |
| val cameraId = selectCamera() | |
| val stateCallback = object : CameraDevice.StateCallback() { | |
| override fun onOpened(camera: CameraDevice) { | |
| cameraDevice = camera | |
| createCameraPreviewSession() | |
| } | |
| override fun onDisconnected(camera: CameraDevice) { | |
| camera.close() | |
| cameraDevice = null | |
| } | |
| override fun onError(camera: CameraDevice, error: Int) { | |
| camera.close() | |
| cameraDevice = null | |
| Log.e(TAG, "Camera error: $error") | |
| } | |
| } | |
| cameraManager.openCamera(cameraId, stateCallback, handler) | |
| } catch (e: Exception) { | |
| Log.e(TAG, "Error opening camera: ${e.message}") | |
| } | |
| } | |
| private fun logAvailableCameras() { | |
| try { | |
| val cameraIdList = cameraManager.cameraIdList | |
| Log.d(TAG, "利用可能なカメラ数: ${cameraIdList.size}") | |
| cameraIdList.forEach { id -> | |
| val characteristics = cameraManager.getCameraCharacteristics(id) | |
| val facing = characteristics.get(android.hardware.camera2.CameraCharacteristics.LENS_FACING) | |
| val facingStr = when (facing) { | |
| android.hardware.camera2.CameraCharacteristics.LENS_FACING_FRONT -> "前面" | |
| android.hardware.camera2.CameraCharacteristics.LENS_FACING_BACK -> "背面" | |
| else -> "その他" | |
| } | |
| Log.d(TAG, "カメラID: $id, 向き: $facingStr") | |
| } | |
| } catch (e: Exception) { | |
| Log.e(TAG, "カメラ情報取得エラー: ${e.message}") | |
| } | |
| } | |
| private fun selectCamera(): String { | |
| val cameraIds = cameraManager.cameraIdList | |
| // 背面カメラを探す | |
| for (id in cameraIds) { | |
| val characteristics = cameraManager.getCameraCharacteristics(id) | |
| val facing = characteristics.get(android.hardware.camera2.CameraCharacteristics.LENS_FACING) | |
| if (facing == android.hardware.camera2.CameraCharacteristics.LENS_FACING_BACK) { | |
| Log.d(TAG, "背面カメラを使用します: $id") | |
| return id | |
| } | |
| } | |
| // 背面カメラがない場合は最初のカメラを使用 | |
| Log.d(TAG, "背面カメラが見つからないため、最初のカメラを使用します: ${cameraIds[0]}") | |
| return cameraIds[0] | |
| } | |
| // 既存のcreateCameraPreviewSessionメソッドを修正 | |
| private fun createCameraPreviewSession() { | |
| try { | |
| // カメラの最適なプレビューサイズを設定 | |
| adjustPreviewSize() | |
| val surface = previewView.holder.surface | |
| val captureCallback = object : CameraCaptureSession.StateCallback() { | |
| override fun onConfigured(session: CameraCaptureSession) { | |
| try { | |
| val captureRequest = cameraDevice?.createCaptureRequest( | |
| CameraDevice.TEMPLATE_PREVIEW | |
| )?.apply { | |
| addTarget(surface) | |
| }?.build() | |
| session.setRepeatingRequest(captureRequest!!, null, handler) | |
| } catch (e: Exception) { | |
| Log.e(TAG, "Error setting up preview: ${e.message}") | |
| } | |
| } | |
| override fun onConfigureFailed(session: CameraCaptureSession) { | |
| Log.e(TAG, "Camera configure failed") | |
| } | |
| } | |
| cameraDevice?.createCaptureSession(listOf(surface), captureCallback, handler) | |
| } catch (e: Exception) { | |
| Log.e(TAG, "Error creating camera session: ${e.message}") | |
| } | |
| } | |
| // 新しいメソッドを追加:プレビューサイズを適切に調整 | |
| private fun adjustPreviewSize() { | |
| try { | |
| val cameraId = selectCamera() | |
| val characteristics = cameraManager.getCameraCharacteristics(cameraId) | |
| // カメラの出力サイズの設定を取得 | |
| val map = characteristics.get(android.hardware.camera2.CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) | |
| // プレビューサイズの取得(最大サイズを使用) | |
| val sizes = map?.getOutputSizes(SurfaceHolder::class.java) | |
| if (sizes != null && sizes.isNotEmpty()) { | |
| // 適切なプレビューサイズを選択(例:最大サイズ) | |
| val previewSize = getOptimalPreviewSize(sizes, previewView.width, previewView.height) | |
| Log.d(TAG, "選択されたプレビューサイズ: ${previewSize.width}x${previewSize.height}") | |
| // SurfaceViewのサイズをカメラのアスペクト比に合わせて調整 | |
| runOnUiThread { | |
| adjustSurfaceViewSize(previewSize.width, previewSize.height) | |
| } | |
| } | |
| } catch (e: Exception) { | |
| Log.e(TAG, "プレビューサイズの調整エラー: ${e.message}") | |
| } | |
| } | |
| // 最適なプレビューサイズを計算 | |
| private fun getOptimalPreviewSize( | |
| sizes: Array<android.util.Size>, | |
| targetWidth: Int, | |
| targetHeight: Int | |
| ): android.util.Size { | |
| // ターゲットのアスペクト比 | |
| val targetRatio = targetHeight.toDouble() / targetWidth | |
| // 候補サイズとそのサイズ差分 | |
| var optimalSize = sizes[0] | |
| var minDiff = Double.MAX_VALUE | |
| for (size in sizes) { | |
| val ratio = size.height.toDouble() / size.width | |
| val diff = Math.abs(ratio - targetRatio) | |
| // より近いアスペクト比を持つサイズを選択 | |
| if (diff < minDiff) { | |
| optimalSize = size | |
| minDiff = diff | |
| } | |
| } | |
| return optimalSize | |
| } | |
| // adjustSurfaceViewSizeメソッドを修正 | |
| private fun adjustSurfaceViewSize(previewWidth: Int, previewHeight: Int) { | |
| try { | |
| // 画面サイズを取得 | |
| val displayMetrics = resources.displayMetrics | |
| val screenWidth = displayMetrics.widthPixels | |
| val screenHeight = displayMetrics.heightPixels | |
| Log.d(TAG, "画面サイズ: ${screenWidth}x${screenHeight}") | |
| // カメラのアスペクト比 | |
| val cameraRatio = previewHeight.toFloat() / previewWidth.toFloat() | |
| // 画面のアスペクト比 | |
| val screenRatio = screenHeight.toFloat() / screenWidth.toFloat() | |
| // 新しいビューサイズを計算 | |
| val newWidth: Int | |
| val newHeight: Int | |
| // カメラと画面のアスペクト比を比較して最適なサイズを決定 | |
| if (cameraRatio > screenRatio) { | |
| // カメラのアスペクト比が画面より縦長の場合、幅に合わせる | |
| newWidth = screenWidth | |
| newHeight = (screenWidth * cameraRatio).toInt() | |
| } else { | |
| // カメラのアスペクト比が画面より横長の場合、高さに合わせる | |
| newHeight = screenHeight | |
| newWidth = (screenHeight / cameraRatio).toInt() | |
| } | |
| Log.d(TAG, "計算されたカメラビューサイズ: ${newWidth}x${newHeight}") | |
| // レイアウトパラメータを更新 | |
| val layoutParams = previewView.layoutParams | |
| layoutParams.width = newWidth | |
| layoutParams.height = newHeight | |
| previewView.layoutParams = layoutParams | |
| // SurfaceViewを画面の中央に配置 | |
| if (previewView.parent is android.view.ViewGroup) { | |
| val parent = previewView.parent as android.view.ViewGroup | |
| if (parent is android.widget.RelativeLayout) { | |
| val params = previewView.layoutParams as android.widget.RelativeLayout.LayoutParams | |
| params.addRule(android.widget.RelativeLayout.CENTER_IN_PARENT) | |
| previewView.layoutParams = params | |
| } | |
| } | |
| } catch (e: Exception) { | |
| Log.e(TAG, "カメラビューサイズの調整エラー: ${e.message}") | |
| } | |
| } | |
| // surfaceChangedメソッドを修正(重複調整を防ぐ) | |
| override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { | |
| // 初回のみサイズを調整(撮影ごとに繰り返さない) | |
| if (cameraDevice != null && (width > 0 && height > 0)) { | |
| // 既存のサイズがあるかチェック(初回のみ実行) | |
| if (previewView.layoutParams.width <= 0 || previewView.layoutParams.height <= 0) { | |
| adjustPreviewSize() | |
| } | |
| } | |
| } | |
| private fun takePicture() { | |
| if (cameraDevice == null) { | |
| Toast.makeText(this, "カメラが利用できません", Toast.LENGTH_SHORT).show() | |
| return | |
| } | |
| try { | |
| // 処理中であることを表示 | |
| runOnUiThread { | |
| resultText.text = "画像を処理中..." | |
| } | |
| // スクリーンショットを取得 | |
| val bitmap = getScreenShot() | |
| if (bitmap != null) { | |
| // UIを更新 | |
| runOnUiThread { | |
| capturedImageView.setImageBitmap(bitmap) | |
| capturedImageView.visibility = View.VISIBLE | |
| Toast.makeText(this, "画像を撮影しました", Toast.LENGTH_SHORT).show() | |
| } | |
| // カスタムモデルを使用した画像分類 | |
| processImageWithCustomModel(bitmap) | |
| } else { | |
| Toast.makeText(this, "画像の取得に失敗しました", Toast.LENGTH_SHORT).show() | |
| } | |
| } catch (e: Exception) { | |
| Log.e(TAG, "Error taking picture: ${e.message}") | |
| Toast.makeText(this, "写真の撮影に失敗しました: ${e.message}", Toast.LENGTH_SHORT).show() | |
| } | |
| } | |
| private fun getScreenShot(): Bitmap? { | |
| try { | |
| val view = previewView | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | |
| // Android 8.0 (API 26)以上では、PixelCopy APIを使用 | |
| val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) | |
| val latch = java.util.concurrent.CountDownLatch(1) | |
| val success = intArrayOf(0) | |
| android.view.PixelCopy.request(view, bitmap, { copyResult -> | |
| success[0] = copyResult | |
| latch.countDown() | |
| }, handler) | |
| try { | |
| latch.await(1, TimeUnit.SECONDS) | |
| return if (success[0] == android.view.PixelCopy.SUCCESS) bitmap else null | |
| } catch (e: InterruptedException) { | |
| Log.e(TAG, "PixelCopy timeout: ${e.message}") | |
| return null | |
| } | |
| } else { | |
| // 古いバージョンでのフォールバック方法 | |
| val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) | |
| val canvas = android.graphics.Canvas(bitmap) | |
| view.draw(canvas) | |
| return bitmap | |
| } | |
| } catch (e: Exception) { | |
| Log.e(TAG, "Error taking screenshot: ${e.message}") | |
| return null | |
| } | |
| } | |
| // カスタムモデルを使用して画像を処理 | |
| private fun processImageWithCustomModel(bitmap: Bitmap) { | |
| try { | |
| // バックグラウンドスレッドで処理 | |
| Thread { | |
| val results = imageClassifier.classify(bitmap) | |
| // ML Kitも併用する場合(オプション) | |
| val image = InputImage.fromBitmap(bitmap, 0) | |
| objectDetector.process(image) | |
| .addOnSuccessListener { detectedObjects -> | |
| // カスタムモデルの結果を優先表示 | |
| if (results.isNotEmpty()) { | |
| displayCustomModelResults(results) | |
| } else { | |
| // カスタムモデルで結果が得られなかった場合のフォールバック | |
| handleDetectionResult(detectedObjects) | |
| } | |
| } | |
| .addOnFailureListener { e -> | |
| // MLKitが失敗した場合でもカスタムモデルの結果を表示 | |
| if (results.isNotEmpty()) { | |
| displayCustomModelResults(results) | |
| } else { | |
| runOnUiThread { | |
| resultText.text = "物体検出に失敗しました: ${e.message}" | |
| } | |
| } | |
| } | |
| }.start() | |
| } catch (e: Exception) { | |
| Log.e(TAG, "Error processing image: ${e.message}") | |
| runOnUiThread { | |
| resultText.text = "画像処理中にエラーが発生しました: ${e.message}" | |
| } | |
| } | |
| } | |
| // カスタムモデルの結果を表示 | |
| // displayCustomModelResultsメソッドの修正(MainActivityクラス内) | |
| private fun displayCustomModelResults(results: List<ImageClassifier.Classification>) { | |
| runOnUiThread { | |
| val sb = StringBuilder() | |
| if (results.isEmpty()) { | |
| sb.append("認識できる物体はありませんでした") | |
| } else if (results.size == 1 && results[0].label == "不明な物体") { | |
| sb.append("不明な物体です(学習していない物体の可能性があります)") | |
| } else { | |
| sb.append("検出された物体:\n") | |
| // まず最も信頼度の高い結果を表示 | |
| val topResult = results.maxByOrNull { it.confidence } | |
| if (topResult != null) { | |
| sb.append("【最も可能性が高い】\n") | |
| sb.append("${topResult.label} (確率: ${(topResult.confidence * 100).toInt()}%)\n\n") | |
| } | |
| // 全ての認識結果と信頼度を表示 | |
| sb.append("【すべての認識結果】\n") | |
| results.forEachIndexed { index, classification -> | |
| sb.append("${index + 1}. ") | |
| sb.append("${classification.label} (確率: ${(classification.confidence * 100).toInt()}%)") | |
| sb.append("\n") | |
| } | |
| } | |
| resultText.text = sb.toString() | |
| } | |
| } | |
| // ML Kitの検出結果を処理(バックアップ機能) | |
| private fun handleDetectionResult(detectedObjects: List<DetectedObject>) { | |
| runOnUiThread { | |
| val sb = StringBuilder() | |
| if (detectedObjects.isEmpty()) { | |
| sb.append("認識できる物体はありませんでした") | |
| } else { | |
| sb.append("検出された物体(${detectedObjects.size}個):\n") | |
| detectedObjects.forEachIndexed { index, detectedObject -> | |
| sb.append("${index + 1}. ") | |
| if (detectedObject.labels.isNotEmpty()) { | |
| val label = detectedObject.labels[0] | |
| sb.append("${label.text} (確率: ${(label.confidence * 100).toInt()}%)") | |
| } else { | |
| sb.append("不明な物体") | |
| } | |
| sb.append("\n") | |
| } | |
| } | |
| resultText.text = sb.toString() | |
| } | |
| } | |
| private fun closeCamera() { | |
| cameraDevice?.close() | |
| cameraDevice = null | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment