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
) }
}
})
Related
I have a ListView in a Fragment that uses a custom adaptor to create a to do list. Each task in the list has a coloured canvas circle that can be checked or unchecked. I want it so that after 3 seconds of checking a task it will see if it is still checked and if it is, it will then remove that task from the list. My deleteTask function in my ToDoListAdaptor will remove the task from the ArrayList that holds the tasks and will then call notifyDataSetChanged() however this does nothing to update the view itself, it still shows no change. Do I need to throw in funky magic because the ListView is in a Fragment? Or am I missing something simple?
ToDoListFragment.kt
class ToDoListFragment : Fragment()
{
//lateinits for XML objects
lateinit var toDoList : ListView
// other Lateinits
private lateinit var currentContext : Context
lateinit var temps : ArrayList<Task>
lateinit var adaptor : ToDoListAdaptor
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
{
// Inflate the layout for this fragment
val v = inflater.inflate(R.layout.fragment_to_do_list, container, false)
setXMLObjects(v)
return v
}
override fun onAttach(context: Context) {
super.onAttach(context)
currentContext = context
}
private fun setXMLObjects(v : View)
{
toDoList = v.findViewById(R.id.ToDoList)
// temp tasks
temps = ArrayList(4)
temps.add(Task("kick ass", "Desc1", ContextCompat.getColor(currentContext, R.color.blue)))
temps.add(Task("chew bubblegum", "Desc2", ContextCompat.getColor(currentContext, R.color.green)))
temps.add(Task("get a freaken job", "Desc3", ContextCompat.getColor(currentContext, R.color.purple)))
temps.add(Task("become the next Elon Musk", "Desc4", ContextCompat.getColor(currentContext, R.color.red)))
adaptor = ToDoListAdaptor(currentContext, temps)
toDoList.adapter = adaptor
adaptor.notifyDataSetChanged()
}
}
And my ToDoListAdaptor
// extends the BaseAdaptor class
class ToDoListAdaptor(private val context: Context, private val taskArrayList : ArrayList<Task>) : BaseAdapter()
{
private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
//*************************** Override Base adaptors functions *****************************************
override fun getCount(): Int
{
return taskArrayList.size
}
override fun getItem(position: Int): Any
{
return taskArrayList[position]
}
override fun getItemId(position: Int): Long
{
return position.toLong()
}
// this is called for each item in the list
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View
{
val task = taskArrayList[position]
// Get the view associated with the list item at position
//val rowView = inflater.inflate(R.layout.task, parent, false)
val rowView = inflater.inflate(R.layout.task, parent, false)
val taskName = rowView.findViewById<TextView>(R.id.TaskName)
taskName.text = task.getName()
val taskImageButton = rowView.findViewById<ImageButton>(R.id.TaskCompletedImageButton)
rowView.viewTreeObserver.addOnGlobalLayoutListener { setCanvases(position, taskImageButton)}
taskImageButton.setOnClickListener { checkTask(taskImageButton, position) }
return rowView
}
//*************************** End of Override Base adaptors functions ***********************************
fun getTaskName(position : Int) : String
{
return taskArrayList[position].getName()
}
fun getTaskDescription(position : Int) : String
{
return taskArrayList[position].getDescription()
}
fun getTaskColour(position : Int) : Int
{
return taskArrayList[position].getColour()
}
private fun setCanvases(position : Int, image : ImageButton)
{
val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
image.background = BitmapDrawable(bitmap)
val canvasPaint = Paint()
canvasPaint.color = taskArrayList[position].getColour()
canvasPaint.style = Paint.Style.STROKE
canvasPaint.strokeWidth = 10F
canvasPaint.isAntiAlias = true
canvas.drawCircle(image.width / 2f, image.height / 2f, min(image.width * 0.45f, image.height * 0.45f), canvasPaint)
}
private fun checkTask(image : ImageView, position : Int)
{
if( ! taskArrayList[position].isChecked())
{
taskArrayList[position].checkUncheck()
val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
image.background = BitmapDrawable(bitmap)
val canvasPaint = Paint()
canvasPaint.color = taskArrayList[position].getColour()
canvasPaint.style = Paint.Style.STROKE
canvasPaint.strokeWidth = 10F
canvasPaint.isAntiAlias = true
val canvasPaint2 = Paint()
canvasPaint2.color = ContextCompat.getColor(context, R.color.black)
canvasPaint2.style = Paint.Style.STROKE
canvasPaint2.strokeWidth = 10F
canvasPaint2.isAntiAlias = true
val checkMarkPoint = floatArrayOf(
image.width * 0.25f, image.height * 0.4f, image.width * 0.35f, image.height * 0.75f,
image.width * 0.35f, image.height * 0.75f, image.width * 0.85f, image.height * 0.1f
)
canvas.drawCircle(
image.width / 2f,
image.height / 2f,
min(image.width * 0.45f, image.height * 0.45f),
canvasPaint
)
canvas.drawLines(checkMarkPoint, canvasPaint2)
val dThread = DeleteThread(position)
dThread.start()
}
else
{
taskArrayList[position].checkUncheck()
val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
image.background = BitmapDrawable(bitmap)
val canvasPaint = Paint()
canvasPaint.color = taskArrayList[position].getColour()
canvasPaint.style = Paint.Style.STROKE
canvasPaint.strokeWidth = 10F
canvasPaint.isAntiAlias = true
canvas.drawCircle(
image.width / 2f,
image.height / 2f,
min(image.width * 0.45f, image.height * 0.45f),
canvasPaint
)
}
}
private fun deleteTask(position : Int)
{
taskArrayList.removeAt(position)
notifyDataSetChanged()
}
private inner class DeleteThread(private var position: Int) : Thread()
{
override fun run()
{
try
{
sleep(3000)
if(taskArrayList[position].isChecked())
{
deleteTask(position)
}
}
catch(exception : Exception)
{
print(exception.message)
}
}
}
}
when we call notifyDataSetChanged(), it refreshes the whole list. As a result, getView() is called for every item on the list.
the best thing you should use RecyclerView ListAdapter and its method DiffUtil is the secret ingredient that makes it possible for ListAdapter to change the items in the list efficiently. DiffUtil compares the new list with the old list to figure out what was added, moved, and removed and outputs a list of update operations that convert the first list into the second efficiently.
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.
Hi i develop a simple QRcode scanner with CameraX. It works but i'd like show a preiew shape around qrcode.
I create a custom view and send boundBox of barcode but.. dimensions and position are wrong.
I think that it's a coordinate translate problems.. maybe :(
here a little project
https://github.com/giuseppesorce/cameraxscan
Some code:
package com.gs.scancamerax
import android.Manifest.permission.CAMERA
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
import com.gs.scancamerax.databinding.FragmentScanBarcodeBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
typealias BarcodeListener = (barcode: String) -> Unit
class ScanBarcodeFragment : Fragment() {
private var scanningResultListener: ScanningResultListener? = null
private var flashEnabled: Boolean = false
private var camera: Camera? = null
private var processingBarcode = AtomicBoolean(false)
private lateinit var cameraExecutor: ExecutorService
private var _binding: FragmentScanBarcodeBinding? = null
private val binding get() = _binding!!
private val TAG = "CameraXBasic"
private val RATIO_4_3_VALUE = 4.0 / 3.0
private val RATIO_16_9_VALUE = 16.0 / 9.0
private var imageCapture: ImageCapture? = null
private var imageAnalyzer: ImageAnalysis? = null
private var cameraProvider: ProcessCameraProvider? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentScanBarcodeBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onResume() {
super.onResume()
processingBarcode.set(false)
initFragment()
}
private fun startCamera() {
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 aspectRatio(width: Int, height: Int): Int {
val previewRatio = max(width, height).toDouble() / min(width, height)
if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) {
return AspectRatio.RATIO_4_3
}
return AspectRatio.RATIO_16_9
}
private var preview: Preview? = null
private fun bindCameraUseCases() {
// Get screen metrics used to setup camera for full screen resolution
val metrics =
DisplayMetrics().also { binding.fragmentScanBarcodePreviewView.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 = binding.fragmentScanBarcodePreviewView.display.rotation
// CameraProvider
val cameraProvider = cameraProvider
?: throw IllegalStateException("Camera initialization failed.")
// CameraSelector
val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
// Preview
preview = Preview.Builder()
// We request aspect ratio but no resolution
.setTargetAspectRatio(screenAspectRatio)
// Set initial target rotation
.setTargetRotation(rotation)
.build()
// ImageCapture
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
// We request aspect ratio but no resolution to match preview config, but letting
// CameraX optimize for whatever specific resolution best fits our use cases
.setTargetAspectRatio(screenAspectRatio)
// Set initial target rotation, we will have to call this again if rotation changes
// during the lifecycle of this use case
.setTargetRotation(rotation)
.build()
// ImageAnalysis
imageAnalyzer = ImageAnalysis.Builder()
// We request aspect ratio but no resolution
.setTargetAspectRatio(screenAspectRatio)
// Set initial target rotation, we will have to call this again if rotation changes
// during the lifecycle of this use case
.setTargetRotation(rotation)
.build()
// The analyzer can then be assigned to the instance
.also {
it.setAnalyzer(cameraExecutor, BarcodeAnalyzer { luma ->
// Values returned from our analyzer are passed to the attached listener
// We log image analysis results here - you should do something useful
// instead!
Log.d(TAG, "Average luminosity: $luma")
})
}
// Must unbind the use-cases before rebinding them
cameraProvider.unbindAll()
try {
// A variable number of use-cases can be passed here -
// camera provides access to CameraControl & CameraInfo
camera = cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer
)
// Attach the viewfinder's surface provider to preview use case
preview?.setSurfaceProvider(binding.fragmentScanBarcodePreviewView.surfaceProvider)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
requireContext(), it
) == PackageManager.PERMISSION_GRANTED
}
fun initFragment() {
cameraExecutor = Executors.newSingleThreadExecutor()
if (allPermissionsGranted()) {
binding.fragmentScanBarcodePreviewView.post {
startCamera()
}
} else {
requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>,
grantResults: IntArray
) {
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
activity?.let {
Toast.makeText(
it.applicationContext,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT
).show()
}
}
}
}
private fun searchBarcode(barcode: String) {
Log.e("driver", "searchBarcode: $barcode")
}
override fun onDestroy() {
cameraExecutor.shutdown()
camera = null
_binding = null
super.onDestroy()
}
inner class BarcodeAnalyzer(private val barcodeListener: BarcodeListener) :
ImageAnalysis.Analyzer {
private val scanner = BarcodeScanning.getClient()
#SuppressLint("UnsafeExperimentalUsageError")
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
if (mediaImage != null) {
val image =
InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
// Pass image to the scanner and have it do its thing
scanner.process(image)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
barcodeListener(barcode.rawValue ?: "")
binding.myView.setBounds(barcode.boundingBox)
}
}
.addOnFailureListener {
// You should really do something about Exceptions
}
.addOnCompleteListener {
// It's important to close the imageProxy
imageProxy.close()
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
fun setScanResultListener(listener: ScanningResultListener) {
this.scanningResultListener = listener
}
companion object {
private val REQUIRED_PERMISSIONS = arrayOf(CAMERA)
private const val REQUEST_CODE_PERMISSIONS = 10
const val TAG = "BarCodeFragment"
#JvmStatic
fun newInstance() = ScanBarcodeFragment()
}
}
Not sure if you are still having the problem, but now CameraX provides a helper class called CoordinateTransform that transforms the coordinates from one use case to another. For example, you can transform the bounding box of the QR code detected from ImageAnalysis to PreviewView coordinates, and draw a bounding box there.
// ImageProxy is the output of an ImageAnalysis.
OutputTransform source = ImageProxyTransformFactory().getOutputTransform(imageProxy);
OutputTransform target = previewView.getOutputTransform();
// Build the transform from ImageAnalysis to PreviewView
CoordinateTransform coordinateTransform = new CoordinateTransform(source, target);
// Detect barcode in ImageProxy and transform the coordinates to PreviewView.
RectF boundingBox = detectBarcodeInImageProxy(imageProxy);
coordinateTransform.mapRect(boundingBox);
// Now boundingBox contains the coordinates of the bounding box in PreviewView.
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.