Blue
Blue

Reputation: 1448

How to auto-focus with Android CameraX

Android has released a new API camerax in recent months. I'm trying to understand how to get auto-focusing for the camera to work.

https://groups.google.com/a/android.com/forum/#!searchin/camerax-developers/auto$20focus|sort:date/camerax-developers/IQ3KZd8iOIY/LIbrRIqEBgAJ

Here is a discussion on the topic but there is almost no specific documentation on it.

https://github.com/android/camera-samples/tree/master/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic

Here is also the basic camerax app but I couldn't find any file dealing with the auto focusing.

Any tips or points to documentation is helpful. Also I'm fairly new to android so its very possible I'm missing something that makes the above links more useful.

Upvotes: 16

Views: 22397

Answers (7)

MatPag
MatPag

Reputation: 44813

With the current CameraX 1.0.0, you can proceed in this 2 ways:

  1. Auto-focus every X seconds:

     previewView.afterMeasured {
         val autoFocusPoint = SurfaceOrientedMeteringPointFactory(1f, 1f)
                 .createPoint(.5f, .5f)
         try {
             val autoFocusAction = FocusMeteringAction.Builder(
                 autoFocusPoint,
                 FocusMeteringAction.FLAG_AF
             ).apply {
                 //start auto-focusing after 2 seconds
                 setAutoCancelDuration(2, TimeUnit.SECONDS)
             }.build()
             camera.cameraControl.startFocusAndMetering(autoFocusAction)
         } catch (e: CameraInfoUnavailableException) {
             Log.d("ERROR", "cannot access camera", e)
         }
     }
    
  2. Focus on-tap:

     previewView.afterMeasured {
         previewView.setOnTouchListener { _, event ->
             return@setOnTouchListener when (event.action) {
                 MotionEvent.ACTION_DOWN -> {
                     true
                 }
                 MotionEvent.ACTION_UP -> {
                     val factory: MeteringPointFactory = SurfaceOrientedMeteringPointFactory(
                         previewView.width.toFloat(), previewView.height.toFloat()
                     )
                     val autoFocusPoint = factory.createPoint(event.x, event.y)
                     try {
                         camera.cameraControl.startFocusAndMetering(
                             FocusMeteringAction.Builder(
                                 autoFocusPoint,
                                 FocusMeteringAction.FLAG_AF
                             ).apply {
                                 //focus only when the user tap the preview
                                 disableAutoCancel()
                             }.build()
                         )
                     } catch (e: CameraInfoUnavailableException) {
                         Log.d("ERROR", "cannot access camera", e)
                     }
                     true
                 }
                 else -> false // Unhandled event.
             }
         }
     }
    

afterMeasured extension function is a simple utility: (thanks ch271828n for improving it)

inline fun View.afterMeasured(crossinline block: () -> Unit) {
    if (measuredWidth > 0 && measuredHeight > 0) {
        block()
    } else {
        viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.GlobalLayoutListener {
            override fun onGlobalLayout() {
                if (measuredWidth > 0 && measuredHeight > 0) {
                    viewTreeObserver.removeOnGlobalLayoutListener(this)
                    block()
                } 
            }
        })
    }
}

A Camera object can be obtained with

val camera = cameraProvider.bindToLifecycle(
    this@Activity, cameraSelector, previewView //this is a PreviewView
)

Upvotes: 50

ch271828n
ch271828n

Reputation: 17567

The afterMeasured function in highest voted answer has a serious bug: Frequently its callback is never called.

The very simple fix:

inline fun View.afterMeasured(crossinline block: () -> Unit) {
    if (measuredWidth > 0 && measuredHeight > 0) {
        block()
    } else {
        viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                if (measuredWidth > 0 && measuredHeight > 0) {
                    viewTreeObserver.removeOnGlobalLayoutListener(this)
                    block()
                }
            }
        })
    }
}

Explanation: I have observed (in an app in production) that, sometimes the view is already measured and no ui changes so onGlobalLayout will never be called later. Then the afterMeasured's callback will never be called, so the camera is not initialized.

Upvotes: 2

Anshul mittal
Anshul mittal

Reputation: 121

With current 1.0.0-rc03 and 1.0.0-alpha22 artifacts

This solution assumes that camera is already setup including bindToLifecycle. After that we need to check whether previewView streamState is STREAMING before trying to focus the camera

 previewView.getPreviewStreamState().observe(getActivity(), value -> {
        if (value.equals(STREAMING)) {
            setUpCameraAutoFocus();
        }
    });

private void setUpCameraAutoFocus() {
    final float x =  previewView.getX() + previewView.getWidth() / 2f;
    final float y =  previewView.getY() + previewView.getHeight() / 2f;

  MeteringPointFactory pointFactory = previewView.getMeteringPointFactory();
  float afPointWidth = 1.0f / 6.0f;  // 1/6 total area
  float aePointWidth = afPointWidth * 1.5f;
  MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth);
  MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth);
  ListenableFuture<FocusMeteringResult> future = cameraControl.startFocusAndMetering(
          new FocusMeteringAction.Builder(afPoint,
                  FocusMeteringAction.FLAG_AF).addPoint(aePoint,
                  FocusMeteringAction.FLAG_AE).build());
  Futures.addCallback(future, new FutureCallback<FocusMeteringResult>() {
    @Override
    public void onSuccess(@Nullable FocusMeteringResult result) {
    }

    @Override
    public void onFailure(Throwable t) {
      // Throw the unexpected error.
      throw new RuntimeException(t);
    }
  }, CameraXExecutors.directExecutor());
}

Upvotes: 1

NevrGivApp
NevrGivApp

Reputation: 39

I ran into the same issue and I set up this solution (even if it looks pretty dumb).

val displayMetrics = resources.displayMetrics
val factory = SurfaceOrientedMeteringPointFactory(
    displayMetrics.widthPixels.toFloat(),
    displayMetrics.heightPixels.toFloat()
)
val point = factory.createPoint(
    displayMetrics.widthPixels / 2f,
    displayMetrics.heightPixels / 2f
)
val action = FocusMeteringAction
            .Builder(point, FocusMeteringAction.FLAG_AF)
            .build()

try {
    camera = cameraProvider.bindToLifecycle(
        lifecycleOwner,
        cameraSelector,
        preview,
        imageAnalyzer
    )
    GlobalScope.launch(Dispatchers.Default) {
        while (workflowModel.isCameraLive) {
            camera?.cameraControl?.startFocusAndMetering(action)?
            delay(3000)
        }
    }
} catch (e: Exception) {
    Log.e(mTag, "Use case binding failed", e)
}

Basically, I restart the focusing action every 3s in a while loop.

isCameraLive is a boolean variable I store in my viewModel and I set true when I start the camera and false when I stop it by calling cameraProvider.unbindAll().

Upvotes: 0

Bonco
Bonco

Reputation: 298

Just point out, to get the "Tap to focus" working with PreviewView, you need to use DisplayOrientedMeteringPointFactory. Otherwise you'll get messed up coordinates.

val factory = DisplayOrientedMeteringPointFactory(activity.display, camera.cameraInfo, previewView.width.toFloat(), previewView.height.toFloat())

For the rest use the MatPag's answer.

Upvotes: 6

Blue
Blue

Reputation: 1448

There is an issue with some android devices where the camera's aren't auto-focusing with CameraX. The CameraX team is aware of it and are tracking it with an internal ticket and hopefully will have a fix soon.

Upvotes: 3

JonathanG
JonathanG

Reputation: 11

You can find the doc here about Focus as it was added in "1.0.0-alpha05" https://developer.android.com/jetpack/androidx/releases/camera#camera2-core-1.0.0-alpha05

Basically you have to set a touch listener on your view and grab the clicked position

private boolean onTouchToFocus(View viewA, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_UP:
                    return focus(event);
                break;
            default:
                // Unhandled event.
                return false;
        }
        return true;
    }

And translate this position into point

private boolean focus(MotionEvent event) {
        final float x = (event != null) ? event.getX() : getView().getX() + getView().getWidth() / 2f;
        final float y = (event != null) ? event.getY() : getView().getY() + getView().getHeight() / 2f;

        TextureViewMeteringPointFactory pointFactory = new TextureViewMeteringPointFactory(textureView);
        float afPointWidth = 1.0f / 6.0f;  // 1/6 total area
        float aePointWidth = afPointWidth * 1.5f;
        MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth, 1.0f);
        MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth, 1.0f);

           try {
            CameraX.getCameraControl(lensFacing).startFocusAndMetering(
                FocusMeteringAction.Builder.from(afPoint, FocusMeteringAction.MeteringMode.AF_ONLY)
                                           .addPoint(aePoint, FocusMeteringAction.MeteringMode.AE_ONLY)
                                           .build());
        } catch (CameraInfoUnavailableException e) {
            Log.d(TAG, "cannot access camera", e);
        }

        return true;
    }

Upvotes: 1

Related Questions