| /* |
| * 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); |
| } |
| } |
| } |