📸CameraX Kullanımı

Android üzerinde güncel beta sürmü olan CameraX kullanımı (👨‍🔬 Beta)

📦 Bağımlılıkları Dahil Etme

  • ➕ Projenizin build.gradle dosyasındaki dependencies alanına alttaki implementation bilgilerini ekleyin

  • 📢 CameraX, java 8 kütüphanelerini de kullandığı için compileOptions da eklenmelidir

build.gradle (app)

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

dependencies {

	// ...
		
	implementation "androidx.camera:camera-camera2:1.0.0-beta01"
	implementation "androidx.camera:camera-core:1.0.0-beta01"
	implementation "androidx.camera:camera-extensions:1.0.0-alpha08"
	implementation "androidx.camera:camera-lifecycle:1.0.0-beta01"
	implementation "androidx.camera:camera-view:1.0.0-alpha08"		
}

‍🧙‍♂ Detaylı bilgi için Add the Gradle dependencies alanına bakabilirsin.

📃 CameraX XML Kodları

  • 👮‍♂️ Buradaki XML kodları, Activity java sınıfının temsil ettiği layout dosyasına yazılmalıdır

  • 😥 Android layout editörü PreviewView'i henüz desteklememektedir, IDE görsel çıktı sunmaz

  • 💁‍♂️ Ama çalışır

  • ⭐ Alttaki fotoğrafta XML'in temsil ettiği çıktı gösterilmiştir

activity:camerax.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:app="http://schemas.android.com/apk/res-auto"
	xmlns:tools="http://schemas.android.com/tools"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	tools:context=".CameraXActivity">
	
	<androidx.camera.view.PreviewView
		android:id="@+id/pvCameraX"
		android:layout_width="match_parent"
		android:layout_height="match_parent"
		app:layout_constraintBottom_toBottomOf="parent"
		app:layout_constraintEnd_toEndOf="parent"
		app:layout_constraintStart_toStartOf="parent"
		app:layout_constraintTop_toTopOf="parent" />
	
	<ImageButton
		android:id="@+id/ibTakePicture"
		android:layout_width="72dp"
		android:layout_height="72dp"
		android:layout_margin="24dp"
		android:contentDescription="Take picture"
		app:layout_constraintBottom_toBottomOf="parent"
		app:layout_constraintEnd_toEndOf="parent"
		app:layout_constraintStart_toStartOf="parent"
		app:srcCompat="@android:drawable/ic_menu_camera" />


</androidx.constraintlayout.widget.ConstraintLayout>

‍🧙‍♂ Detaylı bilgi için Create the viewfinder layout alanına bakabilirsin.

👮‍♂️ Gerekli İzinlerin Alınması

📜 Manifest izinlerini alma

  • 😅 Kamera ile çalışacağımızdan, haliyle kamera iznine ihtiyacımız olacaktır

  • 📜 Android manifest dosyanıza alttaki izin satırını ekleyin

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
	package="com.yemreak.example">
	
	<uses-permission android:name="android.permission.CAMERA" /> 
	
	<!-- application alanı -->
</manifest>

👮‍♂️ Uygulama içinden izin isteme

class CameraXActivity : AppCompatActivity() {

    // Çok fazla istek olursa, isteklerin karışmasını engellemek için kullanılır
    private const val REQUEST_CODE_PERMISSIONS = 10
    
    // Kamera için gereken izinler
    private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        
        
        // Kamera izinleri alındysa işlemleri yapma
        if (allPermissionsGranted()) {
            // Layouta kamerayı ekleme
            pvCameraX.post { startCamera() }
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        }
    }
    
    /**
     * İzin alındıysa aktiviteyi açma ve preview'i başlatma
     * İzin alınmadıysa bildirim gösterip, activityi kapatma
     */
    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                viewFinder.post { startCamera() }
            } else {
                Toast.makeText(this,
                    "Permissions not granted by the user.", 
                    Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }
    
    /**
     * Kamera için gereken tüm izinleri kontrol etme
     */
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
               baseContext, it) == PackageManager.PERMISSION_GRANTED
    }
    
    private fun startCamera() {
        // TODO: CameraX işlemleri eklenecek
    }
    
}

‍🧙‍♂ Detaylı bilgi için Request camera permissions alanına bakabilirsin.

👀 CameraX Ön İzlemesi

  • 📸 Alttaki kod ile kameraya gelen görüntüyü ekrana basacağız

  • 👮‍♂️ cameraProviderFuture.get() ile kameranın olduğundan emin oluyoruz

  • 🎳 PreviewView.ImplementationMode.TEXTURE_VIEW animasyonları ve dönüşümleri destekler, daha fazla memory kullanır

  • 🕊️ PreviewView.ImplementationMode.SURFACE_VIEW daha hızlı ve basit çalışan bir yapıdır

class CameraXActivity : AppCompatActivity() {
	
	private lateinit var cameraProviderFuture: 
		ListenableFuture<ProcessCameraProvider>
	
	// ...
	
	/**
	 * Initialize CameraX provider
	 */
	private fun startCamera() {
		cameraProviderFuture = ProcessCameraProvider.getInstance(this)
		
		cameraProviderFuture.addListener(Runnable {
			// Kamera sağlayıcı ile kameranın aktif olduğundan emin oluyoruz
			val cameraProvider = cameraProviderFuture.get()
			
			// Daha kullanışlı ama daha çok memory harcar
			// https://stackoverflow.com/a/28620918
			pvCameraX.implementationMode = PreviewView.ImplementationMode.TEXTURE_VIEW
			
			// Kamera ön izlemesini tanımlama
			val cameraPreview = Preview.Builder().apply {
				setTargetRotation(pvCameraX.display.rotation)
				setTargetAspectRatio(AspectRatio.RATIO_16_9)
				setTargetName("Preview")
			}.build().apply { setSurfaceProvider(pvCameraX.previewSurfaceProvider) }
			
			// Ön-arka kamera seçimini yapıyoruz
			val cameraSelector = CameraSelector.Builder()
				.requireLensFacing(CameraSelector.LENS_FACING_BACK)
				.build()
			
			// Kamera kullanım durumlarını kameranın yaşam döngüsüne dahil ediyoruz
			val camera = cameraProvider.bindToLifecycle(
				this as LifecycleOwner, cameraSelector, cameraPreview
			)
			
		}, ContextCompat.getMainExecutor(this))
	}
	
}

‍🧙‍♂ Detaylı bilgi için

alanlarına bakabilirsin.

📸 Resim Çekme Özelliği Ekleme

  • ImageCapture objesi oluşturup, onu kameramıza dahil edeceğiz

  • 💠 Resmin alındığı metodu takePicture olarak tanımlayacağız

  • 💫 Daha önceden XML üzerinde tanımladığımız ImageButton'a tıklandığında takePicture metodu çalışacak

  • 💎 Alınan resimleri kayıt edileceği yeri ayarlamak için companion object tanımlayacağız

  • 👷‍♂️ Executor çeşitlerini açıkladığım Thread Pool ~ Lib - YEmreAk yazısına bakmanda fayda var

class CameraXActivity : AppCompatActivity() {

	companion object {
			
			private const val TAG = "MlkitActivity"
			
			private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
			private const val PHOTO_EXTENSION = ".jpg"
			
			fun getOutputDirectory(context: Context): File {
				val appContext = context.applicationContext
				val mediaDir = context.externalMediaDirs.firstOrNull()?.let {
					File(it, appContext.resources.getString(R.string.app_name)).apply {
					 mkdirs() 
					}
				}
				return if (mediaDir != null && mediaDir.exists()) mediaDir 
							 else appContext.filesDir
			}
			
		fun createFile(baseFolder: File, format: String, extension: String) =
			File(
				baseFolder, SimpleDateFormat(format, Locale.US)
					.format(System.currentTimeMillis()) + extension
			)
		
	}
	
	/**
	 * Thread Pool ~ Lib - YEmreAk, alanına bakınız
	 */
	private val executor = Executors.newSingleThreadExecutor()
	
	private lateinit var cameraProviderFuture: 
			ListenableFuture<ProcessCameraProvider>
			
	private lateinit var imageCapture: ImageCapture
	private lateinit var outputDirectory: File
	
	override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        
        // Çekilen fotoğraların kaydedileceği yeri tanımlama
        outputDirectory = getOutputDirectory(this)
        
        // Kamera izinleri alındysa işlemleri yapma
        if (allPermissionsGranted()) {
            // Layouta kamerayı ekleme
            pvCameraX.post { startCamera() }
            ibTakePicture.setOnClickListener {
							takePicture()
						}
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        }
    }
	
	
	private fun startCamera() {
	
		// ...
		cameraProviderFuture.addListener(Runnable {
			
			// ...
			
			imageCapture = ImageCapture.Builder().apply {
				setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
			}.build()
			
			// Attach use cases to camera with the same lifecycle owner
			val camera = cameraProvider.bindToLifecycle(
				this as LifecycleOwner, cameraSelector, cameraPreview, imageCapture
			)
			
			// ...
			
		}, ContextCompat.getMainExecutor(this))
	}
	
	/**
	 * Save image that is shown in camera preview to [outputDirectory]
	 */
	private fun takePicture() {
		val file = createFile(
			outputDirectory,
			FILENAME_FORMAT,
			PHOTO_EXTENSION
		)
		
		val outputFileOptions = ImageCapture.OutputFileOptions.Builder(file).build()
		
		imageCapture.takePicture(
			outputFileOptions,
			executor,
			object : ImageCapture.OnImageSavedCallback {
				
				override fun onImageSaved(outputFileResults: 
					ImageCapture.OutputFileResults) {
						val msg = "Photo capture succeeded: ${file.absolutePath}"
					
						pvCameraX.post {
							Toast.makeText(this@MlkitActivity, msg, Toast.LENGTH_SHORT).show()
						}
					}
				
				override fun onError(exception: ImageCaptureException) {
					val msg = "Photo capture failed: ${file.absolutePath}"
					
					pvCameraX.post {
						Toast.makeText(this@MlkitActivity, msg, Toast.LENGTH_SHORT).show()
					}
				}
				
			})
	}
}

‍🧙‍♂ Detaylı bilgi için Implement image capture use case alanına bakabilirsin.

🔥 ML Kit ile Resmi Analiz Etme

  • 👮‍♂️ Yüz yanıma işlemleri için resim boyutunun en az 480x360 olması gerekmektedir

  • ✨ Resmin analiz işlemleri için ilk olarak imageAnalyser objesi tanımlanır

  • 🐥 Firebase kurulum işlemlerini 🔥 Firebase ML Kit yazım ile uygulayabilirsin

  • 😅 Firebase hakimiyetin olduğunu varsayarak devam ediyorum

  • 👨‍💼 Oluşturulan imageAnalyser objesi içerisinde resim Firebase resmine dönüştürülüp işlenir

  • 👨‍🎨 Preview üzerine çıktıları göstermek için canvas işlemlerini araştırınız

⭐ Analiz örneği istersen MLKit Demo ~ AsmaaMirkhan projesindeki MLKitFaceAnalyser java sınıfını inceleyebilirsin.

imageAnalysis.setAnalyzer(executor,MLKitFaceAnalyser())

şeklinde kullanılır.

private fun startCamera() {

	// ...

	imageAnalysis.setAnalyzer(
		executor,
		ImageAnalysis.Analyzer { imageProxy ->
			
			// Process image if exists
			imageProxy.image?.let { image ->
				val fvImage =
					image.toFvImage(imageProxy.imageInfo.rotationDegrees, isDegree = true)
				fvImage.detectFaces {
					Log.i(TAG, "startCamera: Face count: ${it.size}")
				}
			}
			
			// val rotationDegree = image.imageInfo.rotationDegrees
			// Log.i("TEMP", "startCamera: Image received ${System.currentTimeMillis()}")
			
			//  Once the image being analyzed is closed by calling ImageProxy.close(),
			//  the next latest image will be delivered.
			//  Important: The Analyzer method implementation must call image.close()
			//  on received images when finished using them.
			//  Otherwise, new images may not be received or the camera may stall,
			//  depending on back pressure setting.
			imageProxy.close()
	})
	
		// ...	
}

/**
 * Resim içerisinde bulunan yüzleri algılar, algılama tamamlandığında [onDetected] metodu
 * çalışır
 */
fun FirebaseVisionImage.detectFaces(onDetected: (List<FirebaseVisionFace>) -> Unit): Task<MutableList<FirebaseVisionFace>> {
	val options = FirebaseVisionFaceDetectorOptions.Builder()
		.setClassificationMode(FirebaseVisionFaceDetectorOptions.ACCURATE)
		.setLandmarkMode(FirebaseVisionFaceDetectorOptions.ALL_LANDMARKS)
		.setClassificationMode(FirebaseVisionFaceDetectorOptions.ALL_CLASSIFICATIONS)
		.setMinFaceSize(0.15f)
		.enableTracking()
		.build()
	
	val detector = FirebaseVision.getInstance().getVisionFaceDetector(options)
	return detector.detectInImage(this)
		.addOnSuccessListener(onDetected)
		.addOnFailureListener(Throwable::printStackTrace)
}

fun Image.toFvImage(rotation: Int, isDegree: Boolean = false): FirebaseVisionImage {
	return when (isDegree) {
		false -> FirebaseVisionImage.fromMediaImage(this, rotation)
		true -> FirebaseVisionImage.fromMediaImage(
			this,
			degreesToFirebaseRotation(rotation)
		)
	}
}

fun degreesToFirebaseRotation(degrees: Int): Int {
	return when (degrees) {
		0 -> FirebaseVisionImageMetadata.ROTATION_0
		90 -> FirebaseVisionImageMetadata.ROTATION_90
		180 -> FirebaseVisionImageMetadata.ROTATION_180
		270 -> FirebaseVisionImageMetadata.ROTATION_270
		else -> throw IllegalArgumentException("Rotation must be 0, 90, 180, or 270.")
	}
}

‍🧙‍♂ Detaylı bilgi için

alanlarına bakabilirsin.

⭐ Uygulamanın Son Çıktısı

🖊️ Kamera Çıktısına Çizim Yapma

  • 👮‍♂️ İlk olarak PreviewView üzerine çizim yapamazsın, çünkü kamera ile kitlenmiş durumdadır

  • 🐣 Yeni bir SurfaceView tanımlayıp, onun üzerine çizim yapmalısın

  • 🖼️ FrameLayout ile her ikisini üst üste koymalı ve çizim yaptığını daha önde göstermelisin

‍🧙‍♂ Detaylı bilgi için Android draw on camera preview alanına bakabilirsin.

🔗 Faydalı Kaynaklar

🚀 Bu alandaki bağlantılar YEmoji ~Bağlantılar yapısına uygundur

Last updated

© 2024 ~ Yunus Emre Ak ~ yEmreAk