I have following code in place
GraphicOverlay.kt
open class GraphicOverlay(context: Context?, attrs: AttributeSet?) :
View(context, attrs) {
private val lock = Any()
private val graphics: MutableList<Graphic> = ArrayList()
var mScale: Float? = null
var mOffsetX: Float? = null
var mOffsetY: Float? = null
var cameraSelector: Int = CameraSelector.LENS_FACING_FRONT
abstract class Graphic(private val overlay: GraphicOverlay) {
abstract fun draw(canvas: Canvas?)
fun calculateRect(height: Float, width: Float, boundingBoxT: Rect): RectF {
// for land scape
fun isLandScapeMode(): Boolean {
return overlay.context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
}
fun whenLandScapeModeWidth(): Float {
return when (isLandScapeMode()) {
true -> width
false -> height
}
}
fun whenLandScapeModeHeight(): Float {
return when (isLandScapeMode()) {
true -> height
false -> width
}
}
val scaleX = overlay.width.toFloat() / whenLandScapeModeWidth()
val scaleY = overlay.height.toFloat() / whenLandScapeModeHeight()
val scale = scaleX.coerceAtLeast(scaleY)
overlay.mScale = scale
// Calculate offset (we need to center the overlay on the target)
val offsetX = (overlay.width.toFloat() - ceil(whenLandScapeModeWidth() * scale)) / 2.0f
val offsetY =
(overlay.height.toFloat() - ceil(whenLandScapeModeHeight() * scale)) / 2.0f
overlay.mOffsetX = offsetX
overlay.mOffsetY = offsetY
val mappedBox = RectF().apply {
left = boundingBoxT.right * scale + offsetX
top = boundingBoxT.top * scale + offsetY
right = boundingBoxT.left * scale + offsetX
bottom = boundingBoxT.bottom * scale + offsetY
}
// for front mode
if (overlay.isFrontMode()) {
val centerX = overlay.width.toFloat() / 2
mappedBox.apply {
left = centerX + (centerX - left)
right = centerX - (right - centerX)
}
}
return mappedBox
}
}
fun isFrontMode() = cameraSelector == CameraSelector.LENS_FACING_FRONT
fun toggleSelector() {
cameraSelector =
if (cameraSelector == CameraSelector.LENS_FACING_BACK) CameraSelector.LENS_FACING_FRONT
else CameraSelector.LENS_FACING_BACK
}
fun clear() {
synchronized(lock) { graphics.clear() }
postInvalidate()
}
fun add(graphic: Graphic) {
synchronized(lock) { graphics.add(graphic) }
}
fun remove(graphic: Graphic) {
synchronized(lock) { graphics.remove(graphic) }
postInvalidate()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
synchronized(lock) {
for (graphic in graphics) {
graphic.draw(canvas)
}
}
}
}
FaceContourGraphic.kt
class FaceContourGraphic(
overlay: GraphicOverlay,
private val face: Face,
private val imageRect: Rect
) : GraphicOverlay.Graphic(overlay) {
private val facePositionPaint: Paint
private val idPaint: Paint
private val boxPaint: Paint
init {
val selectedColor = Color.WHITE
facePositionPaint = Paint()
facePositionPaint.color = selectedColor
idPaint = Paint()
idPaint.color = selectedColor
boxPaint = Paint()
boxPaint.color = selectedColor
boxPaint.style = Paint.Style.STROKE
boxPaint.strokeWidth = BOX_STROKE_WIDTH
}
override fun draw(canvas: Canvas?) {
val rect = calculateRect(
imageRect.height().toFloat(),
imageRect.width().toFloat(),
face.boundingBox
)
canvas?.drawRect(rect, boxPaint)
}
companion object {
private const val BOX_STROKE_WIDTH = 5.0f
}
}
SelfieAnalyzer.kt
abstract class SelfieAnalyzer<T> : ImageAnalysis.Analyzer {
abstract val graphicOverlay: GraphicOverlay
#SuppressLint("UnsafeExperimentalUsageError")
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
mediaImage?.let { image ->
detectInImage(InputImage.fromMediaImage(image, imageProxy.imageInfo.rotationDegrees))
.addOnSuccessListener { results ->
onSuccess(
results,
graphicOverlay,
image.cropRect
)
imageProxy.close()
}
.addOnFailureListener {
onFailure(it)
imageProxy.close()
}
}
}
protected abstract fun detectInImage(image: InputImage): Task<T>
abstract fun stop()
protected abstract fun onSuccess(
results: T,
graphicOverlay: GraphicOverlay,
rect: Rect
)
protected abstract fun onFailure(e: Exception)
companion object {
const val TAG: String = "SelfieAnalyzer"
}
}
FaceContourDetectionProcessor.kt
class FaceContourDetectionProcessor(private val view: GraphicOverlay) :
SelfieAnalyzer<List<Face>>() {
private val realTimeOpts = FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_NONE)
.build()
private val detector = FaceDetection.getClient(realTimeOpts)
override val graphicOverlay: GraphicOverlay
get() = view
override fun detectInImage(image: InputImage): Task<List<Face>> {
return detector.process(image)
}
override fun stop() {
try {
detector.close()
} catch (e: IOException) {
Log.e(TAG, "Exception thrown while trying to close Face Detector: $e")
}
}
override fun onSuccess(
results: List<Face>,
graphicOverlay: GraphicOverlay,
rect: Rect
) {
graphicOverlay.clear()
results.forEach { face ->
val faceGraphic = FaceContourGraphic(graphicOverlay, face, rect)
graphicOverlay.add(faceGraphic)
}
graphicOverlay.postInvalidate()
}
override fun onFailure(e: Exception) {
Log.w(TAG, "Face Detector failed.$e")
}
companion object {
private const val TAG = "FaceDetectionProcessor"
}
}
SelfieFragment.kt
class SelfieFragment : Fragment() {
// runs on camera permission grant success
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder().build()
preview.setSurfaceProvider(binding.viewFinder.surfaceProvider)
imageCapture = ImageCapture.Builder().build()
val imageAnalyzer = ImageAnalysis.Builder()
.setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(
cameraExecutor,
selectAnalyzer()
)
}
// Select front camera as a default
val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer
)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(context))
}
}
}
fragment_selfie.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ui.fragments.SelfieFragment">
<androidx.camera.view.PreviewView
android:id="#+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.example.appname.customviews.GraphicOverlay
android:id="#+id/graphic_overlay"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="#id/viewFinder"
app:layout_constraintLeft_toLeftOf="#id/viewFinder"
app:layout_constraintRight_toRightOf="#id/viewFinder"
app:layout_constraintTop_toTopOf="#id/viewFinder" />
<com.google.android.material.button.MaterialButton
android:id="#+id/btnCamera"
style="#style/Widget.MaterialComponents.Button.OutlinedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_marginBottom="16dp"
android:background="#android:color/transparent"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:icon="#drawable/ic_photo_camera_48"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
I want to create bitmap of detected faces but I do not know how, because Face api does not provide an way.
GraphicOverlay extends View and View can be rendered to Bitmap... here's an example. When wanting to render individual FaceContourGraphic, one may have to use a temporary View - so that one can add one FaceContourGraphic, render the output to Bitmap and then proceed with the next one FaceContourGraphic.
Related
How can i implement counterpart of SurfaceView used with another thread to draw and update in a specific interval in Jetpack Compose?
And with coroutines i use it like this
abstract class CoroutineSurfaceView : SurfaceView, SurfaceHolder.Callback,
DefaultLifecycleObserver {
// Handle works in thread that exception is caught that are
private val handler = CoroutineExceptionHandler { coroutineContext, throwable ->
}
internal lateinit var canvas: Canvas
var framePerSecond = 60
private var renderTime = 100L / framePerSecond
private val coroutineScope = CoroutineScope(handler + SupervisorJob() + Dispatchers.Default)
private lateinit var job: Job
private lateinit var surfaceHolder: SurfaceHolder
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context)
}
constructor(context: Context) : super(context) {
init(context)
}
open fun init(context: Context) {
surfaceHolder = this.holder
surfaceHolder.addCallback(this)
setZOrderOnTop(true)
}
override fun surfaceCreated(holder: SurfaceHolder) {
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
private fun startCoroutineRendering() {
job = coroutineScope.launch {
while (isActive) {
if (!surfaceHolder.surface.isValid) {
continue
}
canvas = surfaceHolder.lockCanvas()
update()
render(canvas)
holder.unlockCanvasAndPost(canvas)
delay(renderTime)
}
}
}
internal abstract fun update()
internal abstract fun render(canvas: Canvas)
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
startCoroutineRendering()
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
coroutineScope.launch(Dispatchers.Main.immediate) {
job.cancelAndJoin()
}
}
}
#Composable
fun EachFrameUpdatingCanvas(modifier: Modifier, onDraw: DrawScope.(Long) -> Unit) {
var frameTime by remember { mutableStateOf(0L) }
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(Unit) {
lifecycleOwner.whenStarted {
while (true) {
// this will be called for each frame
// by updating `remember` value we initiating EachFrameUpdatingCanvas redraw
frameTime = withFrameMillis { it }
}
}
}
Canvas(modifier = modifier) {
// you had to use frameTime somewhere in EachFrameUpdatingCanvas
// otherwise it won't be redrawn. But you don't have to pass it to `onDraw` if you don't want
onDraw(frameTime)
}
}
Use it like this:
EachFrameUpdatingCanvas(Modifier.fillMaxSize()) { frameTime ->
drawCircle(
Color.Black,
radius = size.minDimension / 2.0f * (frameTime % 100) / 100f,
)
}
Drawing in compose is done on the background, but if you have some heavy calculations during drawing(inside DrawScope), you probably have to wrap your CoroutineSurfaceView with AndroidView
so I am making a different project, I'm making an app that has a normal camera using CameraX. My goal is when the photo is taken to go to a preview to see the latest photo directly before saving, examples such as "Messanger, Viber, Whatsapp and etc." does anyone has any idea how? Here is the code so far:
Manifest
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera2"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
Gradle implementation
implementation 'androidx.camera:camera-camera2:1.1.0-alpha01'
implementation 'androidx.camera:camera-lifecycle:1.1.0-alpha01'
implementation 'androidx.camera:camera-view:1.0.0-alpha21'
main_activity.xml
<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"
android:orientation="vertical" >
<androidx.camera.view.PreviewView
android:id="#+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/camera_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:importantForAccessibility="no"
app:backgroundTint="#B8AEAE"
app:borderWidth="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
package com.example.watermarkbitmap2
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.graphics.*
import android.graphics.Color.BLACK
import android.graphics.Color.RED
import android.graphics.Paint.ANTI_ALIAS_FLAG
import android.graphics.Paint.DITHER_FLAG
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class MainActivity : AppCompatActivity() {
/**watermark private variables**/
private var paint = Paint()
private val config = Bitmap.Config.ARGB_8888
private val width = 15
private val height = 15
private var bitmap = Bitmap.createBitmap(width, height, config)
/**Camera variables**/
private var imageCapture: ImageCapture? = null
private lateinit var cameraExecutor: ExecutorService
private lateinit var outputDirectory: File
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
/**Checking for all permissions to be granted through method,
* if so it will start the camera, if not checks for a request with permissions and request code**/
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
// Making a variable, connecting to the button and setting up a listener to initiate taking a photo through a method
val imageCaptureBtn = findViewById<FloatingActionButton>(R.id.camera_button)
imageCaptureBtn.setOnClickListener {
takePhoto()
}
// Connecting the late init variable with a new method to get output directory
outputDirectory = getOutputDirectory()
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun getOutputDirectory(): File {
val mediaDir = externalMediaDirs.firstOrNull().let {
File(it, "BitmapWatermark").apply { mkdirs() } }
return if (mediaDir.exists())
mediaDir else filesDir
}
private fun takePhoto() {
val photoFile = File(outputDirectory, SimpleDateFormat(FILENAME_FORMAT, Locale.ENGLISH).format(System.currentTimeMillis()) + ".jpg")
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
imageCapture?.takePicture(
outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val saveUri = Uri.fromFile(photoFile)
val msg = "Photo capture succeeded: $saveUri"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
override fun onError(exception: ImageCaptureException) {
Toast.makeText(baseContext, "Photo capture failed: ${exception.message}", Toast.LENGTH_SHORT).show()
photoFile.delete()
}
}
)
private fun startCamera() {
val viewFinder = findViewById<PreviewView>(R.id.previewView)
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.build()
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it) == PackageManager.PERMISSION_GRANTED
}
#SuppressLint("SetTextI18n")
fun drawBitmap(bitmap: Bitmap, left: Float, top: Float, paint: Paint?) {
val paintText = Paint()
paintText.textAlign
paintText.textSize = 14F
paintText.color = RED
paintText.getTextPath(textText.toString(), 1, 10, 12f, 12f, path)
}
fun addWatermark(
bitmap: Bitmap,
watermarkText: String,
options: WatermarkOptions = WatermarkOptions()
): Bitmap {
val result = bitmap.copy(bitmap.config, true)
val canvas = Canvas(result)
val paint = Paint(ANTI_ALIAS_FLAG or DITHER_FLAG)
paint.textAlign = when (options.corner) {
Corner.TOP_LEFT,
Corner.BOTTOM_LEFT -> Paint.Align.LEFT
Corner.TOP_RIGHT,
Corner.BOTTOM_RIGHT -> Paint.Align.RIGHT
}
val textSize = result.width * options.textSizeToWidthRatio
paint.textSize = textSize
paint.color = options.textColor
if (options.shadowColor != null) {
paint.setShadowLayer(textSize / 2, 0f, 0f, options.shadowColor)
}
if (options.typeface != null) {
paint.typeface = options.typeface
}
val padding = result.width * options.paddingToWidthRation
val coordinates = calculateCoordinates(watermarkText, paint, options, canvas.width, canvas.height, padding)
canvas.drawText(watermarkText, coordinates.x, coordinates.y, paint)
return result
}
private fun calculateCoordinates(
watermarkText: String,
paint: Paint,
options: WatermarkOptions,
width: Int,
height: Int,
padding: Float
): PointF {
val x = when (options.corner) {
Corner.TOP_LEFT,
Corner.BOTTOM_LEFT -> {
padding
}
Corner.TOP_RIGHT,
Corner.BOTTOM_RIGHT -> {
width - padding
}
}
val y = when (options.corner) {
Corner.BOTTOM_LEFT,
Corner.BOTTOM_RIGHT -> {
height - padding
}
Corner.TOP_LEFT,
Corner.TOP_RIGHT -> {
val bounds = Rect()
paint.getTextBounds(watermarkText, 0, watermarkText.length, bounds)
val textHeight = bounds.height()
textHeight + padding
}
}
return PointF(x, y)
}
enum class Corner {
TOP_LEFT,
TOP_RIGHT,
BOTTOM_LEFT,
BOTTOM_RIGHT,
}
data class WatermarkOptions(
val corner: Corner = Corner.BOTTOM_RIGHT,
val textSizeToWidthRatio: Float = 0.04f,
val paddingToWidthRation: Float = 0.03f,
#ColorInt val textColor: Int = Color.WHITE,
#ColorInt val shadowColor: Int? = Color.BLACK,
val typeface: Typeface? = null
)
companion object {
private const val TAG = "ExampleCameraPreview"
private const val FILENAME_FORMAT = "dd.MM.yyyy-HH:mm:ss"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
private var path: Path = Path() // this gets us the new image
}
}
I am using surfaceview inside a fragment, however the methods of the surfaceHolder.Callback(surfaceCreated,surfaceChanged, and surfaceDestroyed) seems to overrides nothing.and I also get the following error " object is not abstract and does not implement abstract member".
Below is the error in the image.
e: /Users/malorimorow/AndroidStudioProjects/CapstoneProject/app/src/main/java/com/example/capstoneproject/main/ScannerFragment.kt: (158, 32): Object is not abstract and does not implement abstract member public abstract fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int): Unit defined in android.view.SurfaceHolder.Callback
Below is the source code and you can find the error in the overlay.apply method.I am new to kotlin and would greatly appreciate the help.
class ScannerFragment : Fragment() {
companion object {
fun newInstance() = ScannerFragment()
// We only need to analyze the part of the image that has text, so we set crop percentages
// to avoid analyze the entire image from the live camera feed.
const val DESIRED_WIDTH_CROP_PERCENT = 8
const val DESIRED_HEIGHT_CROP_PERCENT = 74
// This is an arbitrary number we are using to keep tab of the permission
// request. Where an app has multiple context for requesting permission,
// this can help differentiate the different contexts
private const val REQUEST_CODE_PERMISSIONS = 10
// This is an array of all the permission specified in the manifest
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
private const val RATIO_4_3_VALUE = 4.0 / 3.0
private const val RATIO_16_9_VALUE = 16.0 / 9.0
private const val TAG = "ScannerFragment"
}
private var displayId: Int = -1
private val viewModel: MainViewModel by viewModels()
private var cameraProvider: ProcessCameraProvider? = null
private var camera: Camera? = null
private var imageAnalyzer: ImageAnalysis? = null
private lateinit var container: ConstraintLayout
private lateinit var viewFinder: PreviewView
/** Blocking camera operations are performed using this executor */
private lateinit var cameraExecutor: ExecutorService
private lateinit var scopedExecutor: ScopedExecutor
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_scanner, container, false)
}
override fun onDestroyView() {
super.onDestroyView()
// Shut down our background executor
cameraExecutor.shutdown()
scopedExecutor.shutdown()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
container = view as ConstraintLayout
viewFinder = container.findViewById(R.id.viewfinder)
// Initialize our background executor
cameraExecutor = Executors.newSingleThreadExecutor()
scopedExecutor = ScopedExecutor(cameraExecutor)
// Request camera permissions
if (allPermissionsGranted()) {
// Wait for the views to be properly laid out
viewFinder.post {
// Keep track of the display in which this view is attached
displayId = viewFinder.display.displayId
// Set up the camera and its use cases
setUpCamera()
}
} else {
requestPermissions(
REQUIRED_PERMISSIONS,
REQUEST_CODE_PERMISSIONS
)
}
// Get available language list and set up the target language spinner
// with default selections.
val adapter = ArrayAdapter(
requireContext(),
android.R.layout.simple_spinner_dropdown_item, viewModel.availableLanguages
)
targetLangSelector.adapter = adapter
targetLangSelector.setSelection(adapter.getPosition(Language("en")))
targetLangSelector.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>,
view: View?,
position: Int,
id: Long
) {
viewModel.targetLang.value = adapter.getItem(position)
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
viewModel.sourceLang.observe(viewLifecycleOwner, Observer { srcLang.text = it.displayName })
viewModel.translatedText.observe(viewLifecycleOwner, Observer { resultOrError ->
resultOrError?.let {
if (it.error != null) {
translatedText.error = resultOrError.error?.localizedMessage
} else {
translatedText.text = resultOrError.result
}
}
})
viewModel.modelDownloading.observe(viewLifecycleOwner, Observer { isDownloading ->
progressBar.visibility = if (isDownloading) {
View.VISIBLE
} else {
View.INVISIBLE
}
progressText.visibility = progressBar.visibility
})
overlay.apply {
setZOrderOnTop(true)
holder.setFormat(PixelFormat.TRANSPARENT)
holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceChanged(
holder: SurfaceHolder?,
format: Int,
width: Int,
height: Int
) {
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
}
override fun surfaceCreated(holder: SurfaceHolder?) {
holder?.let { drawOverlay(it,
DESIRED_HEIGHT_CROP_PERCENT,
DESIRED_WIDTH_CROP_PERCENT
) }
}
})
}
}
/** Initialize CameraX, and prepare to bind the camera use cases */
private fun setUpCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener(Runnable {
// CameraProvider
cameraProvider = cameraProviderFuture.get()
// Build and bind the camera use cases
bindCameraUseCases()
}, ContextCompat.getMainExecutor(requireContext()))
}
private fun bindCameraUseCases() {
val cameraProvider = cameraProvider
?: throw IllegalStateException("Camera initialization failed.")
// Get screen metrics used to setup camera for full screen resolution
val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) }
Log.d(TAG, "Screen metrics: ${metrics.widthPixels} x ${metrics.heightPixels}")
val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels)
Log.d(TAG, "Preview aspect ratio: $screenAspectRatio")
val rotation = viewFinder.display.rotation
val preview = Preview.Builder()
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.build()
// Build the image analysis use case and instantiate our analyzer
imageAnalyzer = ImageAnalysis.Builder()
// We request aspect ratio but no resolution
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(
cameraExecutor
, TextAnalyzer(
requireContext(),
lifecycle,
viewModel.sourceText,
viewModel.imageCropPercentages
)
)
}
viewModel.sourceText.observe(viewLifecycleOwner, Observer { srcText.text = it })
viewModel.imageCropPercentages.observe(viewLifecycleOwner,
Observer { drawOverlay(overlay.holder, it.first, it.second) })
// Select back camera since text detection does not work with front camera
val cameraSelector =
CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
camera = cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageAnalyzer
)
preview.setSurfaceProvider(viewFinder.createSurfaceProvider())
} catch (exc: IllegalStateException) {
Log.e(TAG, "Use case binding failed. This must be running on main thread.", exc)
}
}
private fun drawOverlay(
holder: SurfaceHolder,
heightCropPercent: Int,
widthCropPercent: Int
) {
val canvas = holder.lockCanvas()
val bgPaint = Paint().apply {
alpha = 140
}
canvas.drawPaint(bgPaint)
val rectPaint = Paint()
rectPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
rectPaint.style = Paint.Style.FILL
rectPaint.color = Color.WHITE
val outlinePaint = Paint()
outlinePaint.style = Paint.Style.STROKE
outlinePaint.color = Color.WHITE
outlinePaint.strokeWidth = 4f
val surfaceWidth = holder.surfaceFrame.width()
val surfaceHeight = holder.surfaceFrame.height()
val cornerRadius = 25f
// Set rect centered in frame
val rectTop = surfaceHeight * heightCropPercent / 2 / 100f
val rectLeft = surfaceWidth * widthCropPercent / 2 / 100f
val rectRight = surfaceWidth * (1 - widthCropPercent / 2 / 100f)
val rectBottom = surfaceHeight * (1 - heightCropPercent / 2 / 100f)
val rect = RectF(rectLeft, rectTop, rectRight, rectBottom)
canvas.drawRoundRect(
rect, cornerRadius, cornerRadius, rectPaint
)
canvas.drawRoundRect(
rect, cornerRadius, cornerRadius, outlinePaint
)
val textPaint = Paint()
textPaint.color = Color.WHITE
textPaint.textSize = 50F
val overlayText = getString(R.string.overlay_help)
val textBounds = Rect()
textPaint.getTextBounds(overlayText, 0, overlayText.length, textBounds)
val textX = (surfaceWidth - textBounds.width()) / 2f
val textY = rectBottom + textBounds.height() + 15f // put text below rect and 15f padding
canvas.drawText(getString(R.string.overlay_help), textX, textY, textPaint)
holder.unlockCanvasAndPost(canvas)
}
/**
* [androidx.camera.core.ImageAnalysisConfig] requires enum value of
* [androidx.camera.core.AspectRatio]. Currently it has values of 4:3 & 16:9.
*
* Detecting the most suitable ratio for dimensions provided in #params by comparing absolute
* of preview ratio to one of the provided values.
*
* #param width - preview width
* #param height - preview height
* #return suitable aspect ratio
*/
private fun aspectRatio(width: Int, height: Int): Int {
val previewRatio = ln(max(width, height).toDouble() / min(width, height))
if (abs(previewRatio - ln(RATIO_4_3_VALUE))
<= abs(previewRatio - ln(RATIO_16_9_VALUE))
) {
return AspectRatio.RATIO_4_3
}
return AspectRatio.RATIO_16_9
}
/**
* Process result from permission request dialog box, has the request
* been granted? If yes, start Camera. Otherwise display a toast
*/
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray
) {
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
viewFinder.post {
// Keep track of the display in which this view is attached
displayId = viewFinder.display.displayId
// Set up the camera and its use cases
setUpCamera()
}
} else {
Toast.makeText(
context,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT
).show()
}
}
}
/**
* Check if all permission specified in the manifest have been granted
*/
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
requireContext(), it
) == PackageManager.PERMISSION_GRANTED
}
}
Its probably because of Version confict . Just Remove all 3 methods and implement them with ALT+Enter . again .
As the error says surfaceChanged defined as follows
fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int): Unit
But when you are implementing it you are making SurfaceHolder parameter null-able.
holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
override fun surfaceCreated(holder: SurfaceHolder) {
holder?.let { drawOverlay(it,
DESIRED_HEIGHT_CROP_PERCENT,
DESIRED_WIDTH_CROP_PERCENT
) }
}
})
After update from
androidx.camera:camera-core:1.0.0-alpha03
to
androidx.camera:camera-core:1.0.0-alpha06
signatures of methods setTargetAspectRatio (in ImageCaptureConfig.Builder) and takePicture (in ImageCapture) have been changed.
Official documentation and info in web doesn't show how to use new methods (how to specify executor).
Code which broken after update:
...
val captureConfig = ImageCaptureConfig.Builder()
.setTargetAspectRatioCustom(Rational(1, 1)) //this method changed
.setFlashMode(flashMode)
.setLensFacing(lensFacing)
.build()
val capture = ImageCapture(captureConfig)
binding.takeAPhoto.setOnClickListener {
...
val imageFile = createTempFile(System.currentTimeMillis().toString(), ".jpg")
capture.takePicture(imageFile, object : ImageCapture.OnImageSavedListener { //this method also changed
override fun onImageSaved(file: File) {
...
}
override fun onError(useCaseError: ImageCapture.UseCaseError, message: String, cause: Throwable?) {
...
})
}
}
Does anyone have (or know where to find) example of how to use new methods?
Thanks in advance
The official Google Codelabs which obviously have been updated recently use: Executors.newSingleThreadExecutor()
Reference: https://codelabs.developers.google.com/codelabs/camerax-getting-started/#4
Edit: Since #kos's response also makes sense to me, I've added these two official Android docs references:
https://developer.android.com/reference/java/util/concurrent/Executors.html#newSingleThreadExecutor()
https://developer.android.com/reference/java/util/concurrent/Executors.html#newCachedThreadPool()
This way every reader of this topic can make up his/her own mind with respect to executors.
FURTHER EDIT: There are crucial API changes since 1.0.0-alpha07 so I studied some of the docs. There's a GitHub sample showing executor retrieval like so mainExecutor = ContextCompat.getMainExecutor(requireContext())(Source)
If some of you already implemented CameraX and it works fine, I'd definitely wait for the beta release as recommended by Android's release notes
I faced same thing as you are facing. I resolved it from my side.
class MainActivity : AppCompatActivity(), Executor {
private var right: Int = 0
private var bottom: Int = 0
private var left: Int = 0
private var top: Int = 0
private lateinit var preview: Preview
private val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
private lateinit var imageCapture: ImageCapture
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (allPermissionsGranted()) {
viewFinder.post { startCamera() }
} else {
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
viewFinder.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
updateTransform()
}
buttonPlus.setOnClickListener {
if (right < 100) {
right += 100
bottom += 100
left += 100
top += 100
val my = Rect(left, top, right, bottom)
preview.zoom(my)
}
}
buttonMinus.setOnClickListener {
if (right > 0) {
right -= 100
bottom -= 100
left -= 100
top -= 100
val my = Rect(left, top, right, bottom)
preview.zoom(my)
}
}
}
#SuppressLint("RestrictedApi")
private fun startCamera() {
val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) }
val screenAspectRatio = Rational(metrics.widthPixels, metrics.heightPixels)
val previewConfig = PreviewConfig.Builder().apply {
setTargetAspectRatioCustom(screenAspectRatio)
setTargetRotation(viewFinder.display.rotation)
}.build()
preview = Preview(previewConfig)
preview.setOnPreviewOutputUpdateListener {
val parent = viewFinder.parent as ViewGroup
parent.removeView(viewFinder)
parent.addView(viewFinder, 0)
viewFinder.surfaceTexture = it.surfaceTexture
updateTransform()
}
CameraX.bindToLifecycle(this, preview)
captureImage()
}
#SuppressLint("RestrictedApi")
private fun captureImage() {
val imageCaptureConfig = ImageCaptureConfig.Builder()
.apply {
setTargetAspectRatioCustom(Rational(1, 1))
setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
}.build()
imageCapture = ImageCapture(imageCaptureConfig)
CameraX.bindToLifecycle(this, imageCapture)
capture_button.setOnClickListener {
val file = File(this.externalMediaDirs.first(), "${System.currentTimeMillis()}.jpg")
imageCapture.takePicture(file, this, object : ImageCapture.OnImageSavedListener {
override fun onImageSaved(file: File) {
val msg = "Photo capture succeeded: ${file.absolutePath}"
Log.d("CameraXApp", msg)
}
override fun onError(imageCaptureError: ImageCapture.ImageCaptureError, message: String, cause: Throwable?) {
val msg = "Photo capture failed: $message"
Log.e("CameraXApp", msg)
cause?.printStackTrace()
}
})
}
}
override fun execute(command: Runnable) {
command.run()
}
private fun updateTransform() {
val matrix = Matrix()
val centerX = viewFinder.width / 2f
val centerY = viewFinder.height / 2f
val rotationDegrees = when (viewFinder.display.rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> return
}
matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)
viewFinder.setTransform(matrix)
}
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()
}
}
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
}
override fun onDestroy() {
super.onDestroy()
imageCapture.let {
CameraX.unbind(imageCapture)
}
}
}
And the output is (As I print log in onImageSaved method)
Photo capture succeeded: /storage/emulated/0/Android/media/com.akshay.cameraxzoominoutdemo/1571052301192.jpg
It's working fine for me, try out this.
You can do it like this.
imageCapture.takePicture(file, { it.run() }, object : ImageCapture.OnImageSavedListener {
override fun onImageSaved(file: File) {}
override fun onError(useCaseError: ImageCapture.ImageCaptureError, message: String, cause: Throwable?) {}
})
Here is a change log for the changes in alpha06 : https://developer.android.com/jetpack/androidx/releases/camera
setTargetAspectRatio() method now takes AspectRatio enum with 4_3 or 16_9 value.
takePicture() method takes (file, metadata, executor, imageSavedListener) // could use executor as per your case/need. example is val executor = Executors.newSingleThreadExecutor()
instead of useCase.onPreviewOutputUpdateListener = use useCase.setOnPreviewOutputUpdateListener()
FYI : CameraX will be in Beta in Dec 2019
Inside your click listener call this function/method :
private fun saveImage(){
val file = File(this.externalMediaDirs.first(), "${System.currentTimeMillis()}.jpg")
val fileB = ImageCapture.OutputFileOptions.Builder(file).build()
imageCapture.takePicture(fileB, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(fileB: ImageCapture.OutputFileResults) {
val msg = "${fileB.savedUri} - ${file.absolutePath} - ${file.toURI()}"
}
override fun onError(imageCaptureError: ImageCaptureException) {
val msg = "Photo capture failed: ${imageCaptureError.toString()}"
}
})
}
And the msg in onImageSaved will contain something like this :
null - /storage/emulated/0/Android/media/com.mua.camx/1607589430984.jpg - file:/storage/emulated/0/Android/media/com.mua.camx/1607589430984.jpg
You have to only run the command as below.
#Override
public void execute(Runnable command) {
command.run(); // <-- THIS IS NEEDED
}
CameraX provides with built-in executors and take picture can be implemented as below:
imgCaptureButton.setOnClickListener(new View.OnClickListener() {
#Override
#SuppressLint("RestrictedApi")
public void onClick(View v) {
imgCap.takePicture(CameraXExecutors.mainThreadExecutor(),new ImageCapture.OnImageCapturedListener() {
#Override
public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
super.onCaptureSuccess(image, rotationDegrees);
// Play with the Image here.
}
});
}
});
It does not use File to save the image, instead saves the image as a buffer in the memory.
I am using CameraX Analyzer use-case to detect circles in an image using OpenCV Hough Circles, but even though I believe I am using a separate thread to do this analysis, the camera preview gets really slow sometimes when it gets too many detected circles.
I get that my implementation of the detector isn't the most efficient, but shouldn't this processing time be reflected only in the analysis rate, and not on the preview?
Got great part of the code from crysxd CameraX-Object-Tracking.
This is my MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var overlayTextureView: DetectionOverlayView
private val camera
get() = supportFragmentManager.findFragmentById(R.id.cameraFragment) as CameraFragment
override fun onCreate(savedInstanceState: Bundle?) {
OpenCVLoader.initDebug()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (Timber.treeCount() == 0) {
Timber.plant(Timber.DebugTree())
}
overlayTextureView = findViewById(R.id.detectionOverlayView)
camera.imageAnalyzer = ViewModelProviders.of(this).get(HoughPupilDetector::class.java)
}
}
This is my CameraFragment initialization:
open class CameraFragment : Fragment() {
var cameraRunning = false
private set
var imageAnalyzer: ThreadedImageAnalyzer? = null
set(value) {
field = value
if (cameraRunning) {
startCamera()
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_camera, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
CameraPermissionHelper().requestCameraPermission(childFragmentManager) {
if (it) {
startCamera()
} else {
activity?.finish()
}
}
}
override fun onDestroyView() {
super.onDestroyView()
if (cameraRunning) {
CameraX.unbindAll()
cameraRunning = false
Timber.i("Stopping camera")
}
}
private fun startCamera() {
preview.post {
try {
val usesCases = mutableListOf<UseCase>()
// Make sure that there are no other use cases bound to CameraX
CameraX.unbindAll()
// Create configuration object for the viewfinder use case
val previewConfig = onCreatePreivewConfigBuilder().build()
usesCases.add(AutoFitPreviewBuilder.build(previewConfig, preview))
// Setup image analysis pipeline that computes average pixel luminance in real time
if (imageAnalyzer != null) {
val analyzerConfig = onCreateAnalyzerConfigBuilder().build()
usesCases.add(ImageAnalysis(analyzerConfig).apply {
analyzer = imageAnalyzer
})
}
// Bind use cases to lifecycle
CameraX.bindToLifecycle(this, *usesCases.toTypedArray())
cameraRunning = true
Timber.i("Started camera with useCases=$usesCases")
} catch (e: Exception) {
Timber.e(e)
AlertDialog.Builder(context)
.setMessage(getString(R.string.camera_error))
.setPositiveButton(android.R.string.ok) { _, _ ->
activity?.finish()
}
.create()
}
}
}
#Suppress("MemberVisibilityCanBePrivate")
protected open fun onCreateAnalyzerConfigBuilder() = ImageAnalysisConfig.Builder().apply {
// Use a worker thread for image analysis to prevent preview glitches
setCallbackHandler(imageAnalyzer!!.getHandler())
// In our analysis, we care more about the latest image than analyzing *every* image
setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
setTargetAspectRatio(Rational(1, 1))
setTargetResolution(Size(preview.width, preview.height))
}
#Suppress("MemberVisibilityCanBePrivate")
protected open fun onCreatePreivewConfigBuilder() = PreviewConfig.Builder().apply {
setTargetAspectRatio(Rational(1, 1))
setTargetResolution(Size(preview.width, preview.height))
}
}
This is my analyzer interface initialization:
abstract class PupilDetector(listener: PupilDetectionListener? = null) : ViewModel(), ThreadedImageAnalyzer {
private val listeners = ArrayList<PupilDetectionListener>().apply { listener?.let { add(it) } }
private val isBusy = AtomicBoolean(false)
private val handlerThread = HandlerThread("PupilDetector").apply { start() }
fun addListener(listener: PupilDetectionListener) = listeners.add(listener)
override fun analyze(image: ImageProxy, rotationDegrees: Int) {
if (isBusy.compareAndSet(false, true)) {
Timber.d("Running analysis...")
val pupil = detect(image, rotationDegrees)
Timber.d("Analysis done.")
isBusy.set(false)
// listeners.forEach { it(pupil) }
}
}
override fun getHandler() = Handler(handlerThread.looper)
abstract fun detect(image: ImageProxy, rotationDegrees: Int): Pupil?
}
And this is my Hough Circles analyzer:
class HoughPupilDetector(listener: PupilDetectionListener? = null): PupilDetector(listener) {
val maxCircles = 5
override fun detect(image: ImageProxy, rotationDegrees: Int): Pupil? {
val bitmap = image.toBitmap(rotationDegrees)
val circles = detectCircles(bitmap)
if(circles.isNotEmpty()) {
return Pupil(circles[0].point, circles[0].r)
} else {
return null
}
}
private fun detectCircles(bitmap: Bitmap): List<Circle> {
// Generate Mat object
val img = Mat()
Utils.bitmapToMat(bitmap, img)
// Detect circles
val cannyUpperThreshold = 100.0
val minRadius = 10
val maxRadius = 400
val accumulator = 100.0
val circles = Mat()
Imgproc.cvtColor(img, img, Imgproc.COLOR_RGB2GRAY)
Imgproc.GaussianBlur(img, img, org.opencv.core.Size(3.0, 3.0), 1.0)
Imgproc.HoughCircles(img, circles, Imgproc.CV_HOUGH_GRADIENT,
2.0, 2.0 / 8.0, cannyUpperThreshold, accumulator,
minRadius, maxRadius)
Imgproc.cvtColor(img, img, Imgproc.COLOR_GRAY2BGR)
// Convert Mat to list of circles
val result = toCircles(circles)
// Return detection
return result
}
private fun toCircles(circles: Mat): List<Circle>{
if (circles.cols() > 0){
return (0 until circles.cols().coerceAtMost(maxCircles)).map {
val vCircle = circles.get(0, it)
val pt = Point(vCircle[0].toInt(), vCircle[1].toInt())
val radius = Math.round(vCircle[2]).toInt()
// return circle
Circle(pt, radius)
}
} else {
return emptyList()
}
}
}
I updated CameraX dependencies from alpha01 to alpha05 and glitches stopped happening.