/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.camera.camera2.impl;
import android.graphics.PointF;
import android.graphics.Rect;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import android.util.Rational;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.camera.camera2.Camera2Config;
import androidx.camera.core.CaptureConfig;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.MeteringPoint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Implementation of focus and metering.
*
* <p>It is intended to be used within {@link Camera2CameraControl} to implement the
* functionality of {@link Camera2CameraControl#startFocusAndMetering(FocusMeteringAction)} and
* {@link Camera2CameraControl#cancelFocusAndMetering()}. This class depends on
* {@link Camera2CameraControl} to provide some low-level methods such as updateSessionConfig,
* triggerAfInternal and cancelAfAeTriggerInternal to achieve the focus and metering functions.
*
* <p>To wait for the auto-focus lock, it calls
* {@link Camera2CameraControl#addCaptureResultListener(Camera2CameraControl.CaptureResultListener)}
* to monitor the capture result. It also requires {@link ScheduledExecutorService} to schedule the
* auto-cancel event and {@link Executor} to ensure all the methods within this class are called
* in the same thread as the Camera2CameraControl.
*
* <p>The {@link Camera2CameraControl} calls {@link FocusMeteringControl#addFocusMeteringOptions} to
* construct the 3A regions and append them to all repeating requests and single requests.
*/
class FocusMeteringControl {
private static final String TAG = "FocusMeteringControl";
private final Camera2CameraControl mCameraControl;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Executor mExecutor;
private final ScheduledExecutorService mScheduler;
//******************** Should only be accessed by executor (WorkThread) ****************//
private FocusMeteringAction mCurrentFocusMeteringAction;
private boolean mIsInAfAutoMode = false;
Integer mCurrentAfState = CaptureResult.CONTROL_AF_STATE_INACTIVE;
private ScheduledFuture<?> mAutoCancelHandle;
long mFocusTimeoutCounter = 0;
Camera2CameraControl.CaptureResultListener mSessionListenerForFocus = null;
private MeteringRectangle[] mAfRects = new MeteringRectangle[]{};
private MeteringRectangle[] mAeRects = new MeteringRectangle[]{};
private MeteringRectangle[] mAwbRects = new MeteringRectangle[]{};
//**************************************************************************************//
FocusMeteringControl(@NonNull Camera2CameraControl cameraControl,
Executor executor, ScheduledExecutorService scheduler) {
mCameraControl = cameraControl;
mExecutor = executor;
mScheduler = scheduler;
}
/**
* Called by {@link Camera2CameraControl} to append the 3A regions to the shared options. It
* applies to all repeating requests and single requests.
*/
@WorkerThread
void addFocusMeteringOptions(@NonNull Camera2Config.Builder configBuilder) {
int afMode = mIsInAfAutoMode
? CaptureRequest.CONTROL_AF_MODE_AUTO
: CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE;
configBuilder.setCaptureRequestOption(
CaptureRequest.CONTROL_AF_MODE, mCameraControl.getSupportedAfMode(afMode));
if (mAfRects.length != 0) {
configBuilder.setCaptureRequestOption(
CaptureRequest.CONTROL_AF_REGIONS, mAfRects);
}
if (mAeRects.length != 0) {
configBuilder.setCaptureRequestOption(
CaptureRequest.CONTROL_AE_REGIONS, mAeRects);
}
if (mAwbRects.length != 0) {
configBuilder.setCaptureRequestOption(
CaptureRequest.CONTROL_AWB_REGIONS, mAwbRects);
}
}
@WorkerThread
private PointF getFOVAdjustedPoint(@NonNull MeteringPoint meteringPoint,
@NonNull Rational cropRegionAspectRatio,
@NonNull Rational defaultAspectRatio) {
// Use default aspect ratio unless there is a custom aspect ratio in MeteringPoint.
Rational fovAspectRatio = defaultAspectRatio;
if (meteringPoint.getFOVAspectRatio() != null) {
fovAspectRatio = meteringPoint.getFOVAspectRatio();
}
PointF adjustedPoint = new PointF(meteringPoint.getNormalizedCropRegionX(),
meteringPoint.getNormalizedCropRegionY());
if (!fovAspectRatio.equals(cropRegionAspectRatio)) {
if (fovAspectRatio.compareTo(cropRegionAspectRatio) > 0) {
// FOV is more narrow than crop region, top and down side of FOV is cropped.
float heightOfCropRegion =
(float) (fovAspectRatio.doubleValue()
/ cropRegionAspectRatio.doubleValue());
float top_padding = (float) ((heightOfCropRegion - 1.0) / 2);
adjustedPoint.y = (top_padding + adjustedPoint.y) * (1 / heightOfCropRegion);
} else {
// FOV is wider than crop region, left and right side of FOV is cropped.
float widthOfCropRegion =
(float) (cropRegionAspectRatio.doubleValue()
/ fovAspectRatio.doubleValue());
float left_padding = (float) ((widthOfCropRegion - 1.0) / 2);
adjustedPoint.x = (left_padding + adjustedPoint.x) * (1f / widthOfCropRegion);
}
}
return adjustedPoint;
}
@WorkerThread
private MeteringRectangle getMeteringRect(MeteringPoint meteringPoint, PointF adjustedPoint,
Rect cropRegion) {
int centerX = (int) (cropRegion.left + adjustedPoint.x * cropRegion.width());
int centerY = (int) (cropRegion.top + adjustedPoint.y * cropRegion.height());
int width = (int) (meteringPoint.getSize() * cropRegion.width());
int height = (int) (meteringPoint.getSize() * cropRegion.height());
Rect focusRect = new Rect(centerX - width / 2, centerY - height / 2, centerX + width / 2,
centerY + height / 2);
focusRect.left = rangeLimit(focusRect.left, cropRegion.right, cropRegion.left);
focusRect.right = rangeLimit(focusRect.right, cropRegion.right, cropRegion.left);
focusRect.top = rangeLimit(focusRect.top, cropRegion.bottom, cropRegion.top);
focusRect.bottom = rangeLimit(focusRect.bottom, cropRegion.bottom, cropRegion.top);
int weight = (int) (meteringPoint.getWeight() * MeteringRectangle.METERING_WEIGHT_MAX);
weight = rangeLimit(weight, MeteringRectangle.METERING_WEIGHT_MAX,
MeteringRectangle.METERING_WEIGHT_MIN);
return new MeteringRectangle(focusRect, weight);
}
@WorkerThread
private int rangeLimit(int val, int max, int min) {
return Math.min(Math.max(val, min), max);
}
@WorkerThread
void startFocusAndMetering(@NonNull FocusMeteringAction action,
@Nullable Rational defaultAspectRatio) {
if (mCurrentFocusMeteringAction != null) {
cancelFocusAndMetering();
}
mCurrentFocusMeteringAction = action;
Rect cropSensorRegion = mCameraControl.getCropSensorRegion();
Rational cropRegionAspectRatio = new Rational(cropSensorRegion.width(),
cropSensorRegion.height());
if (defaultAspectRatio == null) {
defaultAspectRatio = cropRegionAspectRatio;
}
List<MeteringRectangle> meteringRectanglesListAF = new ArrayList<>();
List<MeteringRectangle> meteringRectanglesListAE = new ArrayList<>();
List<MeteringRectangle> meteringRectanglesListAWB = new ArrayList<>();
for (MeteringPoint meteringPoint : action.getMeteringPointsAF()) {
PointF adjustedPoint = getFOVAdjustedPoint(meteringPoint, cropRegionAspectRatio,
defaultAspectRatio);
MeteringRectangle meteringRectangle = getMeteringRect(meteringPoint, adjustedPoint,
cropSensorRegion);
meteringRectanglesListAF.add(meteringRectangle);
}
for (MeteringPoint meteringPoint : action.getMeteringPointsAE()) {
PointF adjustedPoint = getFOVAdjustedPoint(meteringPoint, cropRegionAspectRatio,
defaultAspectRatio);
MeteringRectangle meteringRectangle = getMeteringRect(meteringPoint, adjustedPoint,
cropSensorRegion);
meteringRectanglesListAE.add(meteringRectangle);
}
for (MeteringPoint meteringPoint : action.getMeteringPointsAWB()) {
PointF adjustedPoint = getFOVAdjustedPoint(meteringPoint, cropRegionAspectRatio,
defaultAspectRatio);
MeteringRectangle meteringRectangle = getMeteringRect(meteringPoint, adjustedPoint,
cropSensorRegion);
meteringRectanglesListAWB.add(meteringRectangle);
}
executeMeteringAction(meteringRectanglesListAF.toArray(
new MeteringRectangle[meteringRectanglesListAF.size()]),
meteringRectanglesListAE.toArray(
new MeteringRectangle[meteringRectanglesListAE.size()]),
meteringRectanglesListAWB.toArray(
new MeteringRectangle[meteringRectanglesListAWB.size()]),
action
);
}
@WorkerThread
private int getDefaultTemplate() {
return CameraDevice.TEMPLATE_PREVIEW;
}
@WorkerThread
void triggerAf() {
CaptureConfig.Builder builder = new CaptureConfig.Builder();
builder.setTemplateType(getDefaultTemplate());
builder.setUseRepeatingSurface(true);
Camera2Config.Builder configBuilder = new Camera2Config.Builder();
configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AF_TRIGGER,
CaptureRequest.CONTROL_AF_TRIGGER_START);
builder.addImplementationOptions(configBuilder.build());
mCameraControl.submitCaptureRequestsInternal(Collections.singletonList(builder.build()));
}
@WorkerThread
void triggerAePrecapture() {
CaptureConfig.Builder builder = new CaptureConfig.Builder();
builder.setTemplateType(getDefaultTemplate());
builder.setUseRepeatingSurface(true);
Camera2Config.Builder configBuilder = new Camera2Config.Builder();
configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
builder.addImplementationOptions(configBuilder.build());
mCameraControl.submitCaptureRequestsInternal(Collections.singletonList(builder.build()));
}
@WorkerThread
void cancelAfAeTrigger(final boolean cancelAfTrigger,
final boolean cancelAePrecaptureTrigger) {
CaptureConfig.Builder builder = new CaptureConfig.Builder();
builder.setUseRepeatingSurface(true);
builder.setTemplateType(getDefaultTemplate());
Camera2Config.Builder configBuilder = new Camera2Config.Builder();
if (cancelAfTrigger) {
configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AF_TRIGGER,
CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
}
if (Build.VERSION.SDK_INT >= 23 && cancelAePrecaptureTrigger) {
configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL);
}
builder.addImplementationOptions(configBuilder.build());
mCameraControl.submitCaptureRequestsInternal(Collections.singletonList(builder.build()));
}
@WorkerThread
private void disableAutoCancel() {
if (mAutoCancelHandle != null) {
mAutoCancelHandle.cancel(/*mayInterruptIfRunning=*/true);
mAutoCancelHandle = null;
}
}
@WorkerThread
void executeMeteringAction(
@Nullable MeteringRectangle[] afRects,
@Nullable MeteringRectangle[] aeRects,
@Nullable MeteringRectangle[] awbRects,
FocusMeteringAction focusMeteringAction) {
mCameraControl.removeCaptureResultListener(mSessionListenerForFocus);
disableAutoCancel();
if (afRects == null) {
mAfRects = new MeteringRectangle[]{};
} else {
mAfRects = afRects;
}
if (aeRects == null) {
mAeRects = new MeteringRectangle[]{};
} else {
mAeRects = aeRects;
}
if (awbRects == null) {
mAwbRects = new MeteringRectangle[]{};
} else {
mAwbRects = awbRects;
}
// Trigger AF scan if any AF points are added.
if (shouldTriggerAF()) {
mCurrentAfState = CaptureResult.CONTROL_AF_STATE_INACTIVE;
if (focusMeteringAction.getOnAutoFocusListener() != null) {
mSessionListenerForFocus =
new Camera2CameraControl.CaptureResultListener() {
// Will be called on mExecutor since mSessionCallback was created with
// mExecutor
@WorkerThread
@Override
public boolean onCaptureResult(@NonNull TotalCaptureResult result) {
Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
if (afState == null) {
return false;
}
if (mCurrentAfState == CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN) {
if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED) {
focusMeteringAction.notifyAutoFocusCompleted(true);
return true; // finished
} else if (afState
== CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
focusMeteringAction.notifyAutoFocusCompleted(false);
return true; // finished
}
}
if (!mCurrentAfState.equals(afState)) {
mCurrentAfState = afState;
}
return false; // continue checking
}
};
mCameraControl.addCaptureResultListener(mSessionListenerForFocus);
}
mIsInAfAutoMode = true;
mCameraControl.updateSessionConfig();
triggerAf();
} else {
// Still calls OnAutoFocusActionListener when AF is not enabled.
focusMeteringAction.notifyAutoFocusCompleted(false);
mCameraControl.updateSessionConfig();
}
if (focusMeteringAction.isAutoCancelEnabled()) {
final long timeoutId = ++mFocusTimeoutCounter;
final Runnable autoCancelRunnable = new Runnable() {
@Override
public void run() {
mExecutor.execute(new Runnable() {
@WorkerThread
@Override
public void run() {
if (timeoutId == mFocusTimeoutCounter) {
cancelFocusAndMetering();
}
}
});
}
};
mAutoCancelHandle = mScheduler.schedule(autoCancelRunnable,
focusMeteringAction.getAutoCancelDurationInMs(),
TimeUnit.MILLISECONDS);
}
}
@WorkerThread
private boolean shouldTriggerAF() {
return mAfRects.length > 0;
}
@WorkerThread
void cancelFocusAndMetering() {
mCameraControl.removeCaptureResultListener(mSessionListenerForFocus);
if (mCurrentFocusMeteringAction != null) {
mCurrentFocusMeteringAction.notifyAutoFocusCompleted(false);
}
disableAutoCancel();
if (shouldTriggerAF()) {
cancelAfAeTrigger(true, false);
}
mAfRects = new MeteringRectangle[]{};
mAeRects = new MeteringRectangle[]{};
mAwbRects = new MeteringRectangle[]{};
mIsInAfAutoMode = false;
mCameraControl.updateSessionConfig();
mCurrentFocusMeteringAction = null;
}
}