Skip to content

Instantly share code, notes, and snippets.

@flushpot1125
Last active July 5, 2025 12:44
Show Gist options
  • Select an option

  • Save flushpot1125/e111040e63dce48c666666bfaa34f222 to your computer and use it in GitHub Desktop.

Select an option

Save flushpot1125/e111040e63dce48c666666bfaa34f222 to your computer and use it in GitHub Desktop.
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