[go: nahoru, domu]

blob: 15c3ab20f2702aff711d524b8fd962e2467261c6 [file] [log] [blame]
/*
* Copyright (C) 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.core;
import android.graphics.ImageFormat;
import android.location.Location;
import android.media.Image;
import android.media.ImageReader;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
import android.util.Rational;
import android.util.Size;
import android.view.Display;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.UiThread;
import androidx.arch.core.util.Function;
import androidx.camera.core.CameraCaptureMetaData.AeState;
import androidx.camera.core.CameraCaptureMetaData.AfMode;
import androidx.camera.core.CameraCaptureMetaData.AfState;
import androidx.camera.core.CameraCaptureMetaData.AwbState;
import androidx.camera.core.CameraCaptureResult.EmptyCameraCaptureResult;
import androidx.camera.core.CameraX.LensFacing;
import androidx.camera.core.ImageOutputConfig.RotationValue;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.AsyncFunction;
import androidx.camera.core.impl.utils.futures.FluentFuture;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.File;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A use case for taking a picture.
*
* <p>This class is designed for basic picture taking. It provides takePicture() functions to take
* a picture to memory or save to a file, and provides image metadata. Pictures are taken in
* automatic mode after focus has converged. The flash mode can additionally be set by the
* application.
*
* <p>TakePicture returns immediately and a listener is called to provide the results after the
* capture completes. Multiple calls to takePicture will take pictures sequentially starting
* after the previous picture is captured.
*
* <p>Note that focus and exposure metering regions can be controlled via {@link Preview}.
*
* <p>When capturing to memory, the captured image is made available through an {@link ImageProxy}
* via an {@link ImageCapture.OnImageCapturedListener}.
*
*/
public class ImageCapture extends UseCase {
/**
* Provides a static configuration with implementation-agnostic options.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public static final Defaults DEFAULT_CONFIG = new Defaults();
private static final String TAG = "ImageCapture";
private static final long CHECK_3A_TIMEOUT_IN_MS = 1000L;
private static final int MAX_IMAGES = 2;
// Empty metadata object used as a placeholder for no user-supplied metadata.
// Should be initialized to all default values.
private static final Metadata EMPTY_METADATA = new Metadata();
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Handler mMainHandler = new Handler(Looper.getMainLooper());
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final ArrayDeque<ImageCaptureRequest> mImageCaptureRequests = new ArrayDeque<>();
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Handler mHandler;
private final SessionConfig.Builder mSessionConfigBuilder;
private final CaptureConfig mCaptureConfig;
private final ExecutorService mExecutor =
Executors.newFixedThreadPool(
1,
new ThreadFactory() {
private final AtomicInteger mId = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(
r,
CameraXThreads.TAG + "image_capture_" + mId.getAndIncrement());
}
});
private final CaptureCallbackChecker mSessionCallbackChecker = new CaptureCallbackChecker();
private final CaptureMode mCaptureMode;
/** The set of requests that will be sent to the camera for the final captured image. */
private final CaptureBundle mCaptureBundle;
private final int mMaxCaptureStages;
/**
* Processing that gets done to the mCaptureBundle to produce the final image that is produced
* by {@link #takePicture(OnImageCapturedListener)}
*/
private final CaptureProcessor mCaptureProcessor;
private final ImageCaptureConfig.Builder mUseCaseConfigBuilder;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
ImageReaderProxy mImageReader;
/** Callback used to match the {@link ImageProxy} with the {@link ImageInfo}. */
private CameraCaptureCallback mMetadataMatchingCaptureCallback;
private ImageCaptureConfig mConfig;
private DeferrableSurface mDeferrableSurface;
/**
* A flag to check 3A converged or not.
*
* <p>In order to speed up the taking picture process, trigger AF / AE should be skipped when
* the flag is disabled. Set it to be enabled in the maximum quality mode and disabled in the
* minimum latency mode.
*/
private boolean mEnableCheck3AConverged;
/** Current flash mode. */
private FlashMode mFlashMode;
/**
* Creates a new image capture use case from the given configuration.
*
* @param userConfig for this use case instance
* @throws IllegalArgumentException if the configuration is invalid.
*/
public ImageCapture(ImageCaptureConfig userConfig) {
super(userConfig);
mUseCaseConfigBuilder = ImageCaptureConfig.Builder.fromConfig(userConfig);
// Ensure we're using the combined configuration (user config + defaults)
mConfig = (ImageCaptureConfig) getUseCaseConfig();
mCaptureMode = mConfig.getCaptureMode();
mFlashMode = mConfig.getFlashMode();
mCaptureProcessor = mConfig.getCaptureProcessor(null);
mMaxCaptureStages = mConfig.getMaxCaptureStages(MAX_IMAGES);
if (mMaxCaptureStages < 1) {
throw new IllegalArgumentException(
"Maximum outstanding image count must be at least 1");
}
Integer bufferFormat = mConfig.getBufferFormat(null);
if (bufferFormat != null) {
if (mCaptureProcessor != null) {
throw new IllegalArgumentException(
"Cannot set buffer format with CaptureProcessor defined.");
} else {
setImageFormat(bufferFormat);
}
} else {
if (mCaptureProcessor != null) {
setImageFormat(ImageFormat.YUV_420_888);
} else {
setImageFormat(ImageReaderFormatRecommender.chooseCombo().imageCaptureFormat());
}
}
mCaptureBundle = mConfig.getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle());
if (mCaptureMode == CaptureMode.MAX_QUALITY) {
mEnableCheck3AConverged = true; // check 3A convergence in MAX_QUALITY mode
} else if (mCaptureMode == CaptureMode.MIN_LATENCY) {
mEnableCheck3AConverged = false; // skip 3A convergence in MIN_LATENCY mode
}
mHandler = mConfig.getCallbackHandler(null);
if (mHandler == null) {
throw new IllegalStateException("No default handler specified.");
}
mSessionConfigBuilder = SessionConfig.Builder.createFrom(mConfig);
mSessionConfigBuilder.addRepeatingCameraCaptureCallback(mSessionCallbackChecker);
CaptureConfig.Builder captureBuilder = CaptureConfig.Builder.createFrom(mConfig);
mCaptureConfig = captureBuilder.build();
}
private static String getCameraIdUnchecked(LensFacing lensFacing) {
try {
return CameraX.getCameraWithLensFacing(lensFacing);
} catch (Exception e) {
throw new IllegalArgumentException(
"Unable to get camera id for camera lens facing " + lensFacing, e);
}
}
/**
* {@inheritDoc}
*
* @hide
*/
@Override
@Nullable
@RestrictTo(Scope.LIBRARY_GROUP)
protected UseCaseConfig.Builder<?, ?, ?> getDefaultBuilder(LensFacing lensFacing) {
ImageCaptureConfig defaults = CameraX.getDefaultUseCaseConfig(
ImageCaptureConfig.class, lensFacing);
if (defaults != null) {
return ImageCaptureConfig.Builder.fromConfig(defaults);
}
return null;
}
private CameraControlInternal getCurrentCameraControl() {
String cameraId = getCameraIdUnchecked(mConfig.getLensFacing());
return getCameraControl(cameraId);
}
/** Configures flash mode to CameraControlInternal once it is ready. */
@Override
protected void onCameraControlReady(String cameraId) {
getCameraControl(cameraId).setFlashMode(mFlashMode);
}
/**
* Get the flash mode.
*
* @return the {@link FlashMode}.
*/
public FlashMode getFlashMode() {
return mFlashMode;
}
/**
* Set the flash mode.
*
* @param flashMode the {@link FlashMode}.
*/
public void setFlashMode(FlashMode flashMode) {
this.mFlashMode = flashMode;
getCurrentCameraControl().setFlashMode(flashMode);
}
/**
* Sets target aspect ratio.
*
* <p>This sets the cropping rectangle returned by {@link ImageProxy#getCropRect()} returned
* from {@link ImageCapture#takePicture(OnImageCapturedListener)}.
*
* <p>This crops the saved image when calling
* {@link ImageCapture#takePicture(File, OnImageSavedListener)} or
* {@link ImageCapture#takePicture(File, OnImageSavedListener, Metadata)}.
*
* <p>Cropping occurs around the center of the image and as though it were in the target
* rotation.
*
* @param aspectRatio New target aspect ratio.
*/
public void setTargetAspectRatio(Rational aspectRatio) {
ImageOutputConfig oldConfig = (ImageOutputConfig) getUseCaseConfig();
Rational oldRatio = oldConfig.getTargetAspectRatio(null);
if (!aspectRatio.equals(oldRatio)) {
mUseCaseConfigBuilder.setTargetAspectRatio(aspectRatio);
updateUseCaseConfig(mUseCaseConfigBuilder.build());
mConfig = (ImageCaptureConfig) getUseCaseConfig();
// TODO(b/122846516): Reconfigure capture session if the ratio is changed drastically.
}
}
/**
* Sets the desired rotation of the output image.
*
* <p>This will affect the EXIF rotation metadata in images saved by takePicture calls and the
* rotation value returned by {@link OnImageCapturedListener}.
*
* <p>In most cases this should be set to the current rotation returned by {@link
* Display#getRotation()}. In that case, the output rotation from takePicture calls will be the
* rotation, which if applied to the output image, will make it match the display orientation.
*
* <p>While rotation can also be set via
* {@link ImageCaptureConfig.Builder#setTargetRotation(int)}, using
* {@link ImageCapture#setTargetRotation(int)} allows the target rotation to be set dynamically.
* This can be useful if an app locks itself to portrait, and uses the orientation sensor
* to set rotation to take landscape images when the device is rotated.
*
* <p>If no target rotation is set by the application, it is set to the value of
* {@link Display#getRotation()} of the default display at the time the
* use case is created.
*
* @param rotation Target rotation of the output image, expressed as one of
* {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
* {@link Surface#ROTATION_180}, or {@link Surface#ROTATION_270}.
*/
public void setTargetRotation(@RotationValue int rotation) {
ImageOutputConfig oldConfig = (ImageOutputConfig) getUseCaseConfig();
int oldRotation = oldConfig.getTargetRotation(ImageOutputConfig.INVALID_ROTATION);
if (oldRotation == ImageOutputConfig.INVALID_ROTATION || oldRotation != rotation) {
mUseCaseConfigBuilder.setTargetRotation(rotation);
updateUseCaseConfig(mUseCaseConfigBuilder.build());
mConfig = (ImageCaptureConfig) getUseCaseConfig();
// TODO(b/122846516): Update session configuration and possibly reconfigure session.
}
}
/**
* Captures a new still image for in memory access.
*
* <p>The listener's callback will be called only once for every invocation of this method. The
* listener is responsible for calling {@link Image#close()} on the returned image.
*
* @param listener Listener to be called for the newly captured image
*/
public void takePicture(final OnImageCapturedListener listener) {
if (Looper.getMainLooper() != Looper.myLooper()) {
mMainHandler.post(
new Runnable() {
@Override
public void run() {
ImageCapture.this.takePicture(listener);
}
});
return;
}
sendImageCaptureRequest(listener, mHandler);
}
/**
* Captures a new still image and saves to a file.
*
* <p>The listener's callback will be called only once for every invocation of this method.
*
* @param saveLocation Location to store the newly captured image.
* @param imageSavedListener Listener to be called for the newly captured image.
*/
public void takePicture(File saveLocation, OnImageSavedListener imageSavedListener) {
takePicture(saveLocation, imageSavedListener, EMPTY_METADATA);
}
/**
* Captures a new still image and saves to a file along with application specified metadata.
*
* <p>The listener's callback will be called only once for every invocation of this method.
*
* <p>This function accepts metadata as a parameter from application code. For JPEGs, this
* metadata will be included in the EXIF.
*
* @param saveLocation Location to store the newly captured image.
* @param imageSavedListener Listener to be called for the newly captured image.
* @param metadata Metadata to be stored with the saved image. For JPEG this will
* be included in the EXIF.
*/
public void takePicture(
final File saveLocation, final OnImageSavedListener imageSavedListener,
final Metadata metadata) {
if (Looper.getMainLooper() != Looper.myLooper()) {
mMainHandler.post(
new Runnable() {
@Override
public void run() {
ImageCapture.this.takePicture(saveLocation, imageSavedListener,
metadata);
}
});
return;
}
/*
* We need to chain the following callbacks to save the image to disk:
*
* +-----------------------+
* | |
* |ImageCapture. |
* |OnImageCapturedListener|
* | |
* +-----------+-----------+
* |
* |
* +-----------v-----------+ +----------------------+
* | | | |
* | ImageSaver. | | ImageCapture. |
* | OnImageSavedListener +------> OnImageSavedListener |
* | | | |
* +-----------------------+ +----------------------+
*/
// Convert the ImageSaver.OnImageSavedListener to ImageCapture.OnImageSavedListener
final ImageSaver.OnImageSavedListener imageSavedListenerWrapper =
new ImageSaver.OnImageSavedListener() {
@Override
public void onImageSaved(File file) {
imageSavedListener.onImageSaved(file);
}
@Override
public void onError(
ImageSaver.SaveError error, String message, @Nullable Throwable cause) {
UseCaseError useCaseError = UseCaseError.UNKNOWN_ERROR;
switch (error) {
case FILE_IO_FAILED:
useCaseError = UseCaseError.FILE_IO_ERROR;
break;
default:
// Keep the useCaseError as UNKNOWN_ERROR
break;
}
imageSavedListener.onError(useCaseError, message, cause);
}
};
// Wrap the ImageCapture.OnImageSavedListener with an OnImageCapturedListener so it can
// be put into the capture request queue
OnImageCapturedListener imageCaptureCallbackWrapper =
new OnImageCapturedListener() {
@Override
public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
Handler completionHandler = (mHandler != null) ? mHandler : mMainHandler;
CameraXExecutors.ioExecutor()
.execute(
new ImageSaver(
image,
saveLocation,
rotationDegrees,
metadata.isReversedHorizontal,
metadata.isReversedVertical,
metadata.location,
imageSavedListenerWrapper,
completionHandler));
}
@Override
public void onError(
UseCaseError error, String message, @Nullable Throwable cause) {
imageSavedListener.onError(error, message, cause);
}
};
// Always use the mMainHandler for the initial callback so we don't need to double post to
// another thread
sendImageCaptureRequest(imageCaptureCallbackWrapper, mMainHandler);
}
@UiThread
private void sendImageCaptureRequest(
OnImageCapturedListener listener, @Nullable Handler listenerHandler) {
String cameraId = getCameraIdUnchecked(mConfig.getLensFacing());
// Get the relative rotation or default to 0 if the camera info is unavailable
int relativeRotation = 0;
try {
CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
relativeRotation =
cameraInfo.getSensorRotationDegrees(
mConfig.getTargetRotation(Surface.ROTATION_0));
} catch (CameraInfoUnavailableException e) {
Log.e(TAG, "Unable to retrieve camera sensor orientation.", e);
}
Rational targetRatio = mConfig.getTargetAspectRatio(null);
targetRatio = ImageUtil.rotate(targetRatio, relativeRotation);
mImageCaptureRequests.offer(
new ImageCaptureRequest(listener, listenerHandler, relativeRotation, targetRatio));
if (mImageCaptureRequests.size() == 1) {
issueImageCaptureRequests();
}
}
/** Issues saved ImageCaptureRequest. */
@UiThread
void issueImageCaptureRequests() {
if (mImageCaptureRequests.isEmpty()) {
return;
}
takePictureInternal();
}
/**
* The take picture flow.
*
* <p>There are three steps to take a picture.
*
* <p>(1) Pre-take picture, which will trigger af/ae scan or open torch if necessary. Then check
* 3A converged if necessary.
*
* <p>(2) Issue take picture single request.
*
* <p>(3) Post-take picture, which will cancel af/ae scan or close torch if necessary.
*/
private void takePictureInternal() {
final TakePictureState state = new TakePictureState();
FluentFuture.from(preTakePicture(state))
.transformAsync(new AsyncFunction<Void, Void>() {
@Override
public ListenableFuture<Void> apply(Void v) throws Exception {
return ImageCapture.this.issueTakePicture();
}
}, mExecutor)
.transformAsync(new AsyncFunction<Void, Void>() {
@Override
public ListenableFuture<Void> apply(Void v) throws Exception {
return ImageCapture.this.postTakePicture(state);
}
}, mExecutor)
.addCallback(
new FutureCallback<Void>() {
@Override
public void onSuccess(Void result) {
}
@Override
public void onFailure(Throwable t) {
Log.e(TAG, "takePictureInternal onFailure", t);
}
},
mExecutor);
}
@Override
public String toString() {
return TAG + ":" + getName();
}
/**
* {@inheritDoc}
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
public void clear() {
if (mDeferrableSurface != null) {
mDeferrableSurface.setOnSurfaceDetachedListener(
CameraXExecutors.mainThreadExecutor(),
new DeferrableSurface.OnSurfaceDetachedListener() {
@Override
public void onSurfaceDetached() {
if (mImageReader != null) {
mImageReader.close();
mImageReader = null;
}
}
});
}
mExecutor.shutdown();
super.clear();
}
/**
* {@inheritDoc}
*
* @hide
*/
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
protected Map<String, Size> onSuggestedResolutionUpdated(
Map<String, Size> suggestedResolutionMap) {
String cameraId = getCameraIdUnchecked(mConfig.getLensFacing());
Size resolution = suggestedResolutionMap.get(cameraId);
if (resolution == null) {
throw new IllegalArgumentException(
"Suggested resolution map missing resolution for camera " + cameraId);
}
if (mImageReader != null) {
if (mImageReader.getHeight() == resolution.getHeight()
&& mImageReader.getWidth() == resolution.getWidth()) {
// Resolution does not need to be updated. Return early.
return suggestedResolutionMap;
}
mImageReader.close();
}
// Setup the ImageReader to do processing
if (mCaptureProcessor != null) {
ProcessingImageReader processingImageReader =
new ProcessingImageReader(
resolution.getWidth(),
resolution.getHeight(),
getImageFormat(), mMaxCaptureStages,
mHandler, getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle()),
mCaptureProcessor);
mMetadataMatchingCaptureCallback = processingImageReader.getCameraCaptureCallback();
mImageReader = processingImageReader;
} else {
MetadataImageReader metadataImageReader = new MetadataImageReader(resolution.getWidth(),
resolution.getHeight(), getImageFormat(), MAX_IMAGES, mHandler);
mMetadataMatchingCaptureCallback = metadataImageReader.getCameraCaptureCallback();
mImageReader = metadataImageReader;
}
mImageReader.setOnImageAvailableListener(
new ImageReaderProxy.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReaderProxy imageReader) {
ImageProxy image = null;
try {
image = imageReader.acquireLatestImage();
} catch (IllegalStateException e) {
Log.e(TAG, "Failed to acquire latest image.", e);
} finally {
if (image != null) {
// Call the head request listener to process the captured image.
ImageCaptureRequest imageCaptureRequest;
if ((imageCaptureRequest = mImageCaptureRequests.poll()) != null) {
imageCaptureRequest.dispatchImage(image);
ImageCapture.this.issueImageCaptureRequests();
} else {
// Discard the image if we have no requests.
image.close();
}
}
}
}
},
mMainHandler);
mSessionConfigBuilder.clearSurfaces();
mDeferrableSurface = new ImmediateSurface(mImageReader.getSurface());
mSessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface);
attachToCamera(cameraId, mSessionConfigBuilder.build());
// In order to speed up the take picture process, notifyActive at an early stage to
// attach the session capture callback to repeating and get capture result all the time.
notifyActive();
return suggestedResolutionMap;
}
/**
* Routine before taking picture.
*
* <p>For example, trigger 3A scan, open torch and check 3A converged if necessary.
*/
private ListenableFuture<Void> preTakePicture(final TakePictureState state) {
return FluentFuture.from(getPreCaptureStateIfNeeded())
.transformAsync(
new AsyncFunction<CameraCaptureResult, Boolean>() {
@Override
public ListenableFuture<Boolean> apply(
CameraCaptureResult captureResult) throws Exception {
state.mPreCaptureState = captureResult;
ImageCapture.this.triggerAfIfNeeded(state);
if (ImageCapture.this.isFlashRequired(state)) {
state.mIsFlashTriggered = true;
ImageCapture.this.triggerAePrecapture(state);
}
return ImageCapture.this.check3AConverged(state);
}
},
mExecutor)
// Ignore the 3A convergence result.
.transform(new Function<Boolean, Void>() {
@Override
public Void apply(Boolean is3AConverged) {
return null;
}
}, mExecutor);
}
/**
* Routine after picture was taken.
*
* <p>For example, cancel 3A scan, close torch if necessary.
*/
ListenableFuture<Void> postTakePicture(final TakePictureState state) {
final Executor executor = mExecutor;
return CallbackToFutureAdapter.getFuture(new CallbackToFutureAdapter.Resolver<Void>() {
@Override
public Object attachCompleter(
@NonNull final CallbackToFutureAdapter.Completer<Void> completer) {
executor.execute(new Runnable() {
@Override
public void run() {
ImageCapture.this.cancelAfAeTrigger(state);
completer.set(null);
}
});
return "postTakePicture[state=" + state + "]";
}
});
}
/**
* Gets a capture result or not according to current configuration.
*
* <p>Conditions to get a capture result.
*
* <p>(1) The enableCheck3AConverged is enabled because it needs to know current AF mode and
* state.
*
* <p>(2) The flashMode is AUTO because it needs to know the current AE state.
*/
// Currently this method is used to prevent there is no repeating surface to get capture result.
// If app is in min-latency mode and flash ALWAYS/OFF mode, it can still take picture without
// checking the capture result. Remove this check once no repeating surface issue is fixed.
private ListenableFuture<CameraCaptureResult> getPreCaptureStateIfNeeded() {
if (mEnableCheck3AConverged || getFlashMode() == FlashMode.AUTO) {
return mSessionCallbackChecker.checkCaptureResult(
new CaptureCallbackChecker.CaptureResultChecker<CameraCaptureResult>() {
@Override
public CameraCaptureResult check(
@NonNull CameraCaptureResult captureResult) {
return captureResult;
}
});
}
return Futures.immediateFuture(null);
}
boolean isFlashRequired(TakePictureState state) {
switch (getFlashMode()) {
case ON:
return true;
case AUTO:
return state.mPreCaptureState.getAeState() == AeState.FLASH_REQUIRED;
case OFF:
return false;
}
throw new AssertionError(getFlashMode());
}
ListenableFuture<Boolean> check3AConverged(TakePictureState state) {
// Besides enableCheck3AConverged == true (MAX_QUALITY), if flash is triggered we also need
// to
// wait for 3A convergence.
if (!mEnableCheck3AConverged && !state.mIsFlashTriggered) {
return Futures.immediateFuture(false);
}
return mSessionCallbackChecker.checkCaptureResult(
new CaptureCallbackChecker.CaptureResultChecker<Boolean>() {
@Override
public Boolean check(@NonNull CameraCaptureResult captureResult) {
// If afMode is CAF, don't check af locked to speed up.
if ((captureResult.getAfMode() == AfMode.ON_CONTINUOUS_AUTO
|| (captureResult.getAfState() == AfState.FOCUSED
|| captureResult.getAfState() == AfState.LOCKED_FOCUSED
|| captureResult.getAfState()
== AfState.LOCKED_NOT_FOCUSED))
&& captureResult.getAeState() == AeState.CONVERGED
&& captureResult.getAwbState() == AwbState.CONVERGED) {
return true;
}
// Return null to continue check.
return null;
}
},
CHECK_3A_TIMEOUT_IN_MS,
false);
}
/**
* Issues the AF scan if needed.
*
* <p>If enableCheck3AConverged is disabled or it is in CAF mode, AF scan should not be
* triggered. Trigger AF scan only in {@link AfMode#ON_MANUAL_AUTO} and current AF state is
* {@link AfState#INACTIVE}. If the AF mode is {@link AfMode#ON_MANUAL_AUTO} and AF state is not
* inactive, it means that a manual or auto focus request may be in progress or completed.
*/
void triggerAfIfNeeded(TakePictureState state) {
if (mEnableCheck3AConverged
&& state.mPreCaptureState.getAfMode() == AfMode.ON_MANUAL_AUTO
&& state.mPreCaptureState.getAfState() == AfState.INACTIVE) {
triggerAf(state);
}
}
/** Issues a request to start auto focus scan. */
private void triggerAf(TakePictureState state) {
state.mIsAfTriggered = true;
getCurrentCameraControl().triggerAf();
}
/** Issues a request to start auto exposure scan. */
void triggerAePrecapture(TakePictureState state) {
state.mIsAePrecaptureTriggered = true;
getCurrentCameraControl().triggerAePrecapture();
}
/** Issues a request to cancel auto focus and/or auto exposure scan. */
void cancelAfAeTrigger(TakePictureState state) {
if (!state.mIsAfTriggered && !state.mIsAePrecaptureTriggered) {
return;
}
getCurrentCameraControl()
.cancelAfAeTrigger(state.mIsAfTriggered, state.mIsAePrecaptureTriggered);
state.mIsAfTriggered = false;
state.mIsAePrecaptureTriggered = false;
}
/** Issues a take picture request. */
ListenableFuture<Void> issueTakePicture() {
final List<ListenableFuture<Void>> futureList = new ArrayList<>();
final List<CaptureConfig> captureConfigs = new ArrayList<>();
CaptureBundle captureBundle;
if (mCaptureProcessor != null) {
// If the Processor is provided, check if we have valid CaptureBundle and update
// ProcessingImageReader before actually issuing a take picture request.
captureBundle = getCaptureBundle(null);
if (captureBundle == null) {
throw new IllegalArgumentException(
"ImageCapture cannot set empty CaptureBundle.");
}
if (captureBundle.getCaptureStages().size() > mMaxCaptureStages) {
throw new IllegalArgumentException(
"ImageCapture has CaptureStages > Max CaptureStage size");
}
((ProcessingImageReader) mImageReader).setCaptureBundle(captureBundle);
} else {
captureBundle = getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle());
if (captureBundle.getCaptureStages().size() > 1) {
throw new IllegalArgumentException(
"ImageCapture have no CaptureProcess set with CaptureBundle size > 1.");
}
}
for (final CaptureStage captureStage : captureBundle.getCaptureStages()) {
final CaptureConfig.Builder builder = new CaptureConfig.Builder();
builder.setTemplateType(mCaptureConfig.getTemplateType());
builder.addImplementationOptions(mCaptureConfig.getImplementationOptions());
builder.addAllCameraCaptureCallbacks(
mSessionConfigBuilder.getSingleCameraCaptureCallbacks());
builder.addSurface(mDeferrableSurface);
builder.addImplementationOptions(
captureStage.getCaptureConfig().getImplementationOptions());
builder.setTag(captureStage.getCaptureConfig().getTag());
builder.addCameraCaptureCallback(mMetadataMatchingCaptureCallback);
ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(
new CallbackToFutureAdapter.Resolver<Void>() {
@Override
public Object attachCompleter(
@NonNull final CallbackToFutureAdapter.Completer<Void> completer) {
CameraCaptureCallback completerCallback = new CameraCaptureCallback() {
@Override
public void onCaptureCompleted(
@NonNull CameraCaptureResult result) {
completer.set(null);
}
@Override
public void onCaptureFailed(
@NonNull CameraCaptureFailure failure) {
Log.e(TAG,
"capture picture get onCaptureFailed with reason "
+ failure.getReason());
completer.set(null);
}
};
builder.addCameraCaptureCallback(completerCallback);
captureConfigs.add(builder.build());
return "issueTakePicture[stage=" + captureStage.getId() + "]";
}
});
futureList.add(future);
}
getCurrentCameraControl().submitCaptureRequests(captureConfigs);
return CallbackToFutureAdapter.getFuture(new CallbackToFutureAdapter.Resolver<Void>() {
@Override
public Object attachCompleter(
@NonNull final CallbackToFutureAdapter.Completer<Void> completer) {
ListenableFuture<List<Void>> combinedFuture = Futures.successfulAsList(futureList);
Futures.addCallback(combinedFuture, new FutureCallback<List<Void>>() {
@Override
public void onSuccess(@Nullable List<Void> result) {
completer.set(null);
}
@Override
public void onFailure(Throwable t) {
completer.setException(t);
}
}, CameraXExecutors.directExecutor());
return "issueTakePicture";
}
});
}
private CaptureBundle getCaptureBundle(CaptureBundle defaultCaptureBundle) {
List<CaptureStage> captureStages = mCaptureBundle.getCaptureStages();
if (captureStages == null || captureStages.isEmpty()) {
return defaultCaptureBundle;
}
return CaptureBundles.createCaptureBundle(captureStages);
}
/**
* Describes the error that occurred during an image capture operation (such as {@link
* ImageCapture#takePicture(OnImageCapturedListener)}).
*
* <p>This is a parameter sent to the error callback functions set in listeners such as {@link
* ImageCapture.OnImageSavedListener#onError(UseCaseError, String, Throwable)}.
*/
public enum UseCaseError {
/**
* An unknown error occurred.
*
* <p>See message parameter in onError callback or log for more details.
*/
UNKNOWN_ERROR,
/**
* An error occurred while attempting to read or write a file, such as when saving an image
* to a File.
*/
FILE_IO_ERROR
}
/**
* Capture mode options for ImageCapture. A picture will always be taken regardless of
* mode, and the mode will be used on devices that support it.
*/
public enum CaptureMode {
/**
* Optimizes capture pipeline to prioritize image quality over latency. When the capture
* mode is set to MAX_QUALITY, images may take longer to capture.
*/
MAX_QUALITY,
/**
* Optimizes capture pipeline to prioritize latency over image quality. When the capture
* mode is set to MIN_LATENCY, images may capture faster but the image quality may be
* reduced.
*/
MIN_LATENCY
}
/** Listener containing callbacks for image file I/O events. */
public interface OnImageSavedListener {
/** Called when an image has been successfully saved. */
void onImageSaved(@NonNull File file);
/** Called when an error occurs while attempting to save an image. */
void onError(
@NonNull UseCaseError useCaseError,
@NonNull String message,
@Nullable Throwable cause);
}
/**
* Listener called when an image capture has completed.
*/
public abstract static class OnImageCapturedListener {
/**
* Callback for when the image has been captured.
*
* <p>The application is responsible for calling {@link ImageProxy#close()} to close the
* image.
*
* <p>The image is of format {@link ImageFormat#JPEG}, queryable via
* {@link ImageProxy#getFormat()}.
*
* <p>The image is provided as captured by the underlying {@link ImageReader} without
* rotation applied. rotationDegrees describes the magnitude of clockwise rotation, which
* if applied to the image will make it match the currently configured target rotation.
*
* <p>For example, if the current target rotation is set to the display rotation,
* rotationDegrees is the rotation to apply to the image to match the display orientation.
* A rotation of 90 degrees would mean rotating the image 90 degrees clockwise produces an
* image that will match the display orientation.
*
* <p>See also {@link ImageCaptureConfig.Builder#setTargetRotation(int)} and
* {@link #setTargetRotation(int)}.
*
* @param image The captured image
* @param rotationDegrees The rotation which if applied to the image will make it match the
* current target rotation. rotationDegrees is expressed as one of
* {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
* {@link Surface#ROTATION_180}, or {@link Surface#ROTATION_270}.
*/
public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
image.close();
}
/** Callback for when an error occurred during image capture. */
public void onError(
UseCaseError useCaseError, String message, @Nullable Throwable cause) {
}
}
/**
* Provides a base static default configuration for the ImageCapture
*
* <p>These values may be overridden by the implementation. They only provide a minimum set of
* defaults that are implementation independent.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public static final class Defaults
implements ConfigProvider<ImageCaptureConfig> {
private static final CaptureMode DEFAULT_CAPTURE_MODE = CaptureMode.MIN_LATENCY;
private static final FlashMode DEFAULT_FLASH_MODE = FlashMode.OFF;
private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper());
private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 4;
private static final ImageCaptureConfig DEFAULT_CONFIG;
static {
ImageCaptureConfig.Builder builder =
new ImageCaptureConfig.Builder()
.setCaptureMode(DEFAULT_CAPTURE_MODE)
.setFlashMode(DEFAULT_FLASH_MODE)
.setCallbackHandler(DEFAULT_HANDLER)
.setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
DEFAULT_CONFIG = builder.build();
}
@Override
public ImageCaptureConfig getConfig(LensFacing lensFacing) {
return DEFAULT_CONFIG;
}
}
/** Holder class for metadata that will be saved with captured images. */
public static final class Metadata {
/**
* Indicates an upside down mirroring, equivalent to a horizontal mirroring (reflection)
* followed by a 180 degree rotation.
*/
public boolean isReversedHorizontal;
/** Indicates a left-right mirroring (reflection). */
public boolean isReversedVertical;
/** Data representing a geographic location. */
@Nullable
public Location location;
}
/**
* An intermediate action recorder while taking picture. It is used to restore certain states.
* For example, cancel AF/AE scan, and close flash light.
*/
static final class TakePictureState {
CameraCaptureResult mPreCaptureState = EmptyCameraCaptureResult.create();
boolean mIsAfTriggered = false;
boolean mIsAePrecaptureTriggered = false;
boolean mIsFlashTriggered = false;
}
/**
* A helper class to check camera capture result.
*
* <p>CaptureCallbackChecker is an implementation of {@link CameraCaptureCallback} that checks a
* specified list of condition and sets a ListenableFuture when the conditions have been met. It
* is mainly used to continuously capture callbacks to detect specific conditions. It also
* handles the timeout condition if the check condition does not satisfy the given timeout, and
* returns the given default value if the timeout is met.
*/
static final class CaptureCallbackChecker extends CameraCaptureCallback {
private static final long NO_TIMEOUT = 0L;
/** Capture listeners. */
private final Set<CaptureResultListener> mCaptureResultListeners = new HashSet<>();
@Override
public void onCaptureCompleted(@NonNull CameraCaptureResult cameraCaptureResult) {
deliverCaptureResultToListeners(cameraCaptureResult);
}
/**
* Check the capture results of current session capture callback by giving a {@link
* CaptureResultChecker}.
*
* @param checker a CaptureResult checker that returns an object with type T if the check is
* complete, returning null to continue the check process.
* @param <T> the type parameter for CaptureResult checker.
* @return a listenable future for capture result check process.
*/
<T> ListenableFuture<T> checkCaptureResult(CaptureResultChecker<T> checker) {
return checkCaptureResult(checker, NO_TIMEOUT, null);
}
/**
* Check the capture results of current session capture callback with timeout limit by
* giving a {@link CaptureResultChecker}.
*
* @param checker a CaptureResult checker that returns an object with type T if the
* check is
* complete, returning null to continue the check process.
* @param timeoutInMs used to force stop checking.
* @param defValue the default return value if timeout occur.
* @param <T> the type parameter for CaptureResult checker.
* @return a listenable future for capture result check process.
*/
<T> ListenableFuture<T> checkCaptureResult(
final CaptureResultChecker<T> checker, final long timeoutInMs, final T defValue) {
if (timeoutInMs < NO_TIMEOUT) {
throw new IllegalArgumentException("Invalid timeout value: " + timeoutInMs);
}
final long startTimeInMs =
(timeoutInMs != NO_TIMEOUT) ? SystemClock.elapsedRealtime() : 0L;
return CallbackToFutureAdapter.getFuture(
new CallbackToFutureAdapter.Resolver<T>() {
@Override
public Object attachCompleter(
@NonNull final CallbackToFutureAdapter.Completer<T> completer) {
addListener(
new CaptureResultListener() {
@Override
public boolean onCaptureResult(
@NonNull CameraCaptureResult captureResult) {
T result = checker.check(captureResult);
if (result != null) {
completer.set(result);
return true;
} else if (startTimeInMs > 0
&& SystemClock.elapsedRealtime() - startTimeInMs
> timeoutInMs) {
completer.set(defValue);
return true;
}
// Return false to continue check.
return false;
}
});
return "checkCaptureResult";
}
});
}
/**
* Delivers camera capture result to {@link CaptureCallbackChecker#mCaptureResultListeners}.
*/
private void deliverCaptureResultToListeners(@NonNull CameraCaptureResult captureResult) {
synchronized (mCaptureResultListeners) {
Set<CaptureResultListener> removeSet = null;
for (CaptureResultListener listener : new HashSet<>(mCaptureResultListeners)) {
// Remove listener if the callback return true
if (listener.onCaptureResult(captureResult)) {
if (removeSet == null) {
removeSet = new HashSet<>();
}
removeSet.add(listener);
}
}
if (removeSet != null) {
mCaptureResultListeners.removeAll(removeSet);
}
}
}
/** Add capture result listener. */
void addListener(CaptureResultListener listener) {
synchronized (mCaptureResultListeners) {
mCaptureResultListeners.add(listener);
}
}
/** An interface to check camera capture result. */
public interface CaptureResultChecker<T> {
/**
* The callback to check camera capture result.
*
* @param captureResult the camera capture result.
* @return the check result, return null to continue checking.
*/
T check(@NonNull CameraCaptureResult captureResult);
}
/** An interface to listen to camera capture results. */
private interface CaptureResultListener {
/**
* Callback to handle camera capture results.
*
* @param captureResult camera capture result.
* @return true to finish listening, false to continue listening.
*/
boolean onCaptureResult(@NonNull CameraCaptureResult captureResult);
}
}
private final class ImageCaptureRequest {
OnImageCapturedListener mListener;
@Nullable
Handler mHandler;
@RotationValue
int mRotationDegrees;
Rational mTargetRatio;
ImageCaptureRequest(
OnImageCapturedListener listener,
@Nullable Handler handler,
@RotationValue int rotationDegrees,
Rational targetRatio) {
mListener = listener;
mHandler = handler;
mRotationDegrees = rotationDegrees;
mTargetRatio = targetRatio;
}
void dispatchImage(final ImageProxy image) {
if (mHandler != null && Looper.myLooper() != mHandler.getLooper()) {
boolean posted =
mHandler.post(
new Runnable() {
@Override
public void run() {
ImageCaptureRequest.this.dispatchImage(image);
}
});
// Unable to post to the supplied handler, close the image.
if (!posted) {
Log.e(TAG, "Unable to post to the supplied handler.");
image.close();
}
return;
}
Size sourceSize = new Size(image.getWidth(), image.getHeight());
if (ImageUtil.isAspectRatioValid(sourceSize, mTargetRatio)) {
image.setCropRect(
ImageUtil.computeCropRectFromAspectRatio(sourceSize, mTargetRatio));
}
mListener.onCaptureSuccess(image, mRotationDegrees);
}
}
}