Gradle dependencies
compile group: 'androidx.camera', name: 'camera-core', version: '1.2.0-alpha01'
- groupId: androidx.camera
- artifactId: camera-core
- version: 1.2.0-alpha01
Artifact androidx.camera:camera-core:1.2.0-alpha01 it located at Google repository (https://maven.google.com/)
Overview
A use case for taking a picture.
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.
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.
Note that focus and exposure metering regions can be controlled via Preview.
When capturing to memory, the captured image is made available through an ImageProxy
via an ImageCapture.OnImageCapturedCallback.
Summary
Methods |
---|
public int | getCaptureMode()
Returns the set capture mode. |
public UseCaseConfig<UseCase> | getDefaultConfig(boolean applyDefaultConfig, UseCaseConfigFactory factory)
|
public int | getFlashMode()
Get the flash mode. |
public int | getJpegQuality()
Returns the JPEG quality setting. |
public ResolutionInfo | getResolutionInfo()
Gets selected resolution information of the ImageCapture. |
protected ResolutionInfo | getResolutionInfoInternal()
|
public int | getTargetRotation()
Returns the desired rotation of the output image. |
public UseCaseConfig.Builder<UseCase, UseCaseConfig, java.lang.Object> | getUseCaseConfigBuilder(Config config)
|
public void | onAttached()
|
protected void | onCameraControlReady()
Configures flash mode to CameraControlInternal once it is ready. |
public void | onDetached()
|
protected UseCaseConfig<UseCase> | onMergeConfig(CameraInfoInternal cameraInfo, UseCaseConfig.Builder<UseCase, UseCaseConfig, java.lang.Object> builder)
|
public void | onStateDetached()
|
protected Size | onSuggestedResolutionUpdated(Size suggestedResolution)
|
public void | setCropAspectRatio(Rational aspectRatio)
Sets target cropping aspect ratio for output image. |
public void | setFlashMode(int flashMode)
Set the flash mode. |
public void | setSensorToBufferTransformMatrix(Matrix sensorToBufferTransformMatrix)
|
public void | setTargetRotation(int rotation)
Sets the desired rotation of the output image. |
public void | takePicture(java.util.concurrent.Executor executor, ImageCapture.OnImageCapturedCallback callback)
Captures a new still image for in memory access. |
public void | takePicture(ImageCapture.OutputFileOptions outputFileOptions, java.util.concurrent.Executor executor, ImageCapture.OnImageSavedCallback imageSavedCallback)
Captures a new still image and saves to a file along with application specified metadata. |
public java.lang.String | toString()
|
from UseCase | getAppTargetRotation, getAttachedSurfaceResolution, getCamera, getCameraControl, getCameraId, getCurrentConfig, getImageFormat, getName, getRelativeRotation, getSessionConfig, getTargetRotationInternal, getViewPortCropRect, isCurrentCamera, mergeConfigs, notifyActive, notifyInactive, notifyReset, notifyState, notifyUpdated, onAttach, onDetach, onStateAttached, setTargetRotationInternal, setViewPortCropRect, updateSessionConfig, updateSuggestedResolution |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, wait, wait, wait |
Fields
public static final int
ERROR_UNKNOWNAn unknown error occurred.
See message parameter in onError callback or log for more details.
public static final int
ERROR_FILE_IOAn error occurred while attempting to read or write a file, such as when saving an image
to a File.
public static final int
ERROR_CAPTURE_FAILEDAn error reported by camera framework indicating the capture request is failed.
public static final int
ERROR_CAMERA_CLOSEDAn error indicating the request cannot be done due to camera is closed.
public static final int
ERROR_INVALID_CAMERAAn error indicating this ImageCapture is not bound to a valid camera.
public static final int
CAPTURE_MODE_MAXIMIZE_QUALITYOptimizes capture pipeline to prioritize image quality over latency. When the capture
mode is set to MAX_QUALITY, images may take longer to capture.
public static final int
CAPTURE_MODE_MINIMIZE_LATENCYOptimizes 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.
public static final int
CAPTURE_MODE_ZERO_SHUTTER_LAGOptimizes capture pipeline to prioritize latency over image quality. When the capture
mode is set to ZERO_SHUTTER_LAG, the latency between the shutter button is clicked and the
picture is recorded is minimized.
public static final int
FLASH_MODE_AUTOpublic static final int
FLASH_MODE_ONAlways flash. The flash will always be used when taking a picture.
public static final int
FLASH_MODE_OFFNo flash. The flash will never be used when taking a picture.
public static final int
FLASH_TYPE_ONE_SHOT_FLASHWhen flash is required for taking a picture, a normal one shot flash will be used.
public static final int
FLASH_TYPE_USE_TORCH_AS_FLASHWhen flash is required for taking a picture, torch will be used as flash.
Provides a static configuration with implementation-agnostic options.
Methods
protected void
onCameraControlReady()
Configures flash mode to CameraControlInternal once it is ready.
public int
getFlashMode()
Get the flash mode.
Returns:
the flashMode. Value is ImageCapture.FLASH_MODE_AUTO, ImageCapture.FLASH_MODE_ON, or
ImageCapture.FLASH_MODE_OFF.
public void
setFlashMode(int flashMode)
Set the flash mode.
The flash control for the subsequent photo capture requests. Applications can check if
there is a flash unit via CameraInfo.hasFlashUnit() and update UI component if
necessary. If there is no flash unit, then calling this API will take no effect for the
subsequent photo capture requests and they will act like ImageCapture.FLASH_MODE_OFF.
When the torch is enabled via CameraControl.enableTorch(boolean), the torch
will remain enabled during photo capture regardless of flashMode setting. When
the torch is disabled, flash will function as specified by ImageCapture.setFlashMode(int).
On some LEGACY devices like Samsung A3, taking pictures with ImageCapture.FLASH_MODE_AUTO
mode could cause a crash. To workaround this CameraX will disable the auto flash behavior
internally on devices that have this issue.
Parameters:
flashMode: the flash mode. Value is ImageCapture.FLASH_MODE_AUTO, ImageCapture.FLASH_MODE_ON,
or ImageCapture.FLASH_MODE_OFF.
public void
setCropAspectRatio(Rational aspectRatio)
Sets target cropping aspect ratio for output image.
This aspect ratio is orientation-dependent. It should be expressed in the coordinate
frame after rotating the image by the target rotation.
This sets the cropping rectangle returned by ImageProxy.getCropRect() returned
from ImageCapture.takePicture(Executor, ImageCapture.OnImageCapturedCallback).
For example, assume the aspectRatio of 3x4. If an image has a resolution of
480x640 after applying the target rotation, then the output ImageProxy of
ImageCapture.takePicture(Executor, ImageCapture.OnImageCapturedCallback) would have a cropping
rectangle of 480x640 after applying the rotation degrees. However, if an image has a
resolution of 640x480 after applying the target rotation, then the cropping rectangle
of the output ImageProxy would be 360x480 after applying the rotation degrees.
This crops the saved image when calling
ImageCapture.takePicture(ImageCapture.OutputFileOptions, Executor, ImageCapture.OnImageSavedCallback). Note
that the cropping will introduce an additional latency.
Cropping occurs around the center of the image and as though it were in the target
rotation. For example, assume the aspectRatio of 3x4. If an image has a resolution
of 480x640 after applying the target rotation, then the saved output image would be
480x640 after applying the EXIF orientation value. However, if an image has a resolution
of 640x480 after applying the target rotation, then the saved output image would be
360x480 after applying the EXIF orientation value.
This setting value will be automatically updated to match the new target rotation value
when ImageCapture.setTargetRotation(int) is called.
Parameters:
aspectRatio: New target aspect ratio.
public int
getTargetRotation()
Returns the desired rotation of the output image.
The rotation can be set prior to constructing an ImageCapture using
ImageCapture.Builder.setTargetRotation(int) or dynamically by calling
ImageCapture.setTargetRotation(int). The rotation of an image taken is determined
by the rotation value set at the time image capture is initiated, such as when calling
ImageCapture.takePicture(Executor, ImageCapture.OnImageCapturedCallback).
If no target rotation is set by the application, it is set to the value of
Display
of the default display at the time the use case is
created. The use case is fully created once it has been attached to a camera.
Returns:
The rotation of the intended target.
public void
setTargetRotation(int rotation)
Sets the desired rotation of the output image.
This will affect the EXIF rotation metadata in images saved by takePicture calls and the
ImageInfo.getRotationDegrees() value of the ImageProxy returned by
ImageCapture.OnImageCapturedCallback. These will be set to be the rotation, which if applied to
the output image data, will make the image match target rotation specified here.
While rotation can also be set via ImageCapture.setTargetRotation(int), using
ImageCapture.setTargetRotation(int) allows the target rotation to be set dynamically.
In general, it is best to use an to
set the target rotation. This way, the rotation output will indicate which way is down for
a given image. This is important since display orientation may be locked by device
default, user setting, or app configuration, and some devices may not transition to a
reverse-portrait display orientation. In these cases,
use ImageCapture.setTargetRotation(int) to set target rotation dynamically according to
the , without re-creating the use case. Note
the OrientationEventListener output of degrees in the range [0..359] should be converted to
a surface rotation. The mapping values are listed as the following.
: orientation == -1
: orientation >= 315 || orientation < 45
: orientation >= 225 && orientation < 315
: orientation >= 135 && orientation < 225
: orientation >= 45 && orientation < 135
When this function is called, value set by
ImageCapture.Builder.setTargetResolution(Size) will be updated automatically to make
sure the suitable resolution can be selected when the use case is bound. Value set by
ImageCapture.setCropAspectRatio(Rational) will also be updated automatically to
make sure the output image is cropped into expected aspect ratio.
If no target rotation is set by the application, it is set to the value of
Display
of the default display at the time the use case is created. The
use case is fully created once it has been attached to a camera.
takePicture uses the target rotation at the time it begins executing (which may be delayed
waiting on a previous takePicture call to complete).
Parameters:
rotation: Target rotation of the output image, expressed as one of
, ,
, or .
public int
getCaptureMode()
Returns the set capture mode.
This is set when constructing an ImageCapture using
ImageCapture.Builder.setCaptureMode(int). This is static for an instance of
ImageCapture.
public int
getJpegQuality()
Returns the JPEG quality setting.
This is set when constructing an ImageCapture using
ImageCapture.Builder.setJpegQuality(int). If not set, a default value will be set
according to the capture mode setting. JPEG compression quality 95 is set for
ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY and 100 is set for
ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY. This is static for an instance of ImageCapture.
Gets selected resolution information of the ImageCapture.
The returned ResolutionInfo will be expressed in the coordinates of the camera
sensor. Note that the resolution might not be the same as the resolution of the received
image by calling ImageCapture.takePicture(Executor, ImageCapture.OnImageCapturedCallback) because the received image might have been rotated
to the upright orientation using the target rotation setting by the device.
The resolution information might change if the use case is unbound and then rebound,
ImageCapture.setTargetRotation(int) is called to change the target rotation setting, or
ImageCapture.setCropAspectRatio(Rational) is called to change the crop aspect ratio setting.
The application needs to call ImageCapture.getResolutionInfo() again to get the latest
ResolutionInfo for the changes.
Returns:
the resolution information if the use case has been bound by the
ProcessCameraProvider.bindToLifecycle(LifecycleOwner, CameraSelector, UseCase...) API, or null if the use case is not bound yet.
public void
setSensorToBufferTransformMatrix(Matrix sensorToBufferTransformMatrix)
Captures a new still image for in memory access.
The callback will be called only once for every invocation of this method. The listener
is responsible for calling Image
on the returned image.
Parameters:
executor: The executor in which the callback methods will be run.
callback: Callback to be invoked for the newly captured image
Captures a new still image and saves to a file along with application specified metadata.
The callback will be called only once for every invocation of this method.
If the ImageCapture is in a UseCaseGroup where ViewPort is
set, or ImageCapture.setCropAspectRatio(Rational) is used, the image may be cropped before saving to
disk which causes an additional latency.
Parameters:
outputFileOptions: Options to store the newly captured image.
executor: The executor in which the callback methods will be run.
imageSavedCallback: Callback to be called for the newly captured image.
See also: ViewPort
public void
onStateDetached()
public java.lang.String
toString()
protected Size
onSuggestedResolutionUpdated(Size suggestedResolution)
Source
/*
* 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 static androidx.camera.core.impl.ImageCaptureConfig.OPTION_BUFFER_FORMAT;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_CAPTURE_BUNDLE;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_CAPTURE_PROCESSOR;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_DEFAULT_SESSION_CONFIG;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_FLASH_MODE;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_FLASH_TYPE;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_IMAGE_READER_PROXY_PROVIDER;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_IO_EXECUTOR;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_JPEG_COMPRESSION_QUALITY;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_MAX_CAPTURE_STAGES;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_MAX_RESOLUTION;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SESSION_CONFIG_UNPACKER;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SUPPORTED_RESOLUTIONS;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_TARGET_ASPECT_RATIO;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_TARGET_CLASS;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_TARGET_NAME;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_TARGET_RESOLUTION;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_TARGET_ROTATION;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_USE_CASE_EVENT_CALLBACK;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_USE_SOFTWARE_JPEG_ENCODER;
import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.location.Location;
import android.media.Image;
import android.media.ImageReader;
import android.net.Uri;
import android.os.Build;
import android.os.Looper;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Pair;
import android.util.Rational;
import android.util.Size;
import android.view.Display;
import android.view.Surface;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraConfig;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.CaptureBundle;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.CaptureProcessor;
import androidx.camera.core.impl.CaptureStage;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.ConfigProvider;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.ImageCaptureConfig;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
import androidx.camera.core.impl.ImageReaderProxy;
import androidx.camera.core.impl.ImmediateSurface;
import androidx.camera.core.impl.MutableConfig;
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.MutableTagBundle;
import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.core.impl.utils.CameraOrientationUtil;
import androidx.camera.core.impl.utils.Exif;
import androidx.camera.core.impl.utils.Threads;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.camera.core.internal.IoConfig;
import androidx.camera.core.internal.TargetConfig;
import androidx.camera.core.internal.YuvToJpegProcessor;
import androidx.camera.core.internal.compat.quirk.SoftwareJpegEncodingPreferredQuirk;
import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability;
import androidx.camera.core.internal.utils.ImageUtil;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.core.util.Preconditions;
import androidx.lifecycle.LifecycleOwner;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* 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.OnImageCapturedCallback}.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class ImageCapture extends UseCase {
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase lifetime constant] - Stays constant for the lifetime of the UseCase. Which means
// they could be created in the constructor.
////////////////////////////////////////////////////////////////////////////////////////////
/**
* An unknown error occurred.
*
* <p>See message parameter in onError callback or log for more details.
*/
public static final int ERROR_UNKNOWN = 0;
/**
* An error occurred while attempting to read or write a file, such as when saving an image
* to a File.
*/
public static final int ERROR_FILE_IO = 1;
/**
* An error reported by camera framework indicating the capture request is failed.
*/
public static final int ERROR_CAPTURE_FAILED = 2;
/**
* An error indicating the request cannot be done due to camera is closed.
*/
public static final int ERROR_CAMERA_CLOSED = 3;
/**
* An error indicating this ImageCapture is not bound to a valid camera.
*/
public static final int ERROR_INVALID_CAMERA = 4;
/**
* Optimizes capture pipeline to prioritize image quality over latency. When the capture
* mode is set to MAX_QUALITY, images may take longer to capture.
*/
public static final int CAPTURE_MODE_MAXIMIZE_QUALITY = 0;
/**
* 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.
*/
public static final int CAPTURE_MODE_MINIMIZE_LATENCY = 1;
/**
* Optimizes capture pipeline to prioritize latency over image quality. When the capture
* mode is set to ZERO_SHUTTER_LAG, the latency between the shutter button is clicked and the
* picture is recorded is minimized.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public static final int CAPTURE_MODE_ZERO_SHUTTER_LAG = 2;
/**
* Auto flash. The flash will be used according to the camera system's determination when taking
* a picture.
*/
private static final int FLASH_MODE_UNKNOWN = -1;
public static final int FLASH_MODE_AUTO = 0;
/** Always flash. The flash will always be used when taking a picture. */
public static final int FLASH_MODE_ON = 1;
/** No flash. The flash will never be used when taking a picture. */
public static final int FLASH_MODE_OFF = 2;
/**
* When flash is required for taking a picture, a normal one shot flash will be used.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public static final int FLASH_TYPE_ONE_SHOT_FLASH = 0;
/**
* When flash is required for taking a picture, torch will be used as flash.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public static final int FLASH_TYPE_USE_TORCH_AS_FLASH = 1;
/**
* 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";
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
private static final int MAX_IMAGES = 2;
// TODO(b/149336664) Move the quality to a compatibility class when there is a per device case.
private static final byte JPEG_QUALITY_MAXIMIZE_QUALITY_MODE = 100;
private static final byte JPEG_QUALITY_MINIMIZE_LATENCY_MODE = 95;
@CaptureMode
private static final int DEFAULT_CAPTURE_MODE = CAPTURE_MODE_MINIMIZE_LATENCY;
@FlashMode
private static final int DEFAULT_FLASH_MODE = FLASH_MODE_OFF;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
static final ExifRotationAvailability EXIF_ROTATION_AVAILABILITY =
new ExifRotationAvailability();
private final ImageReaderProxy.OnImageAvailableListener mClosingListener = (imageReader -> {
try (ImageProxy image = imageReader.acquireLatestImage()) {
Log.d(TAG, "Discarding ImageProxy which was inadvertently acquired: " + image);
} catch (IllegalStateException e) {
Log.e(TAG, "Failed to acquire latest image.", e);
}
});
@NonNull
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Executor mIoExecutor;
@CaptureMode
private final int mCaptureMode;
@GuardedBy("mLockedFlashMode")
private final AtomicReference<Integer> mLockedFlashMode = new AtomicReference<>(null);
@FlashType
private final int mFlashType;
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase lifetime dynamic] - Dynamic variables which could change during anytime during
// the UseCase lifetime.
////////////////////////////////////////////////////////////////////////////////////////////
/** Current flash mode. */
@GuardedBy("mLockedFlashMode")
@FlashMode
private int mFlashMode = FLASH_MODE_UNKNOWN;
private Rational mCropAspectRatio = null;
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase attached constant] - Is only valid when the UseCase is attached to a camera.
////////////////////////////////////////////////////////////////////////////////////////////
private ExecutorService mExecutor;
private CaptureConfig mCaptureConfig;
/** The set of requests that will be sent to the camera for the final captured image. */
private CaptureBundle mCaptureBundle;
private int mMaxCaptureStages;
/**
* Processing that gets done to the mCaptureBundle to produce the final image that is produced
* by {@link #takePicture(Executor, OnImageCapturedCallback)}
*/
private CaptureProcessor mCaptureProcessor;
/**
* Whether the software JPEG pipeline will be used.
*/
private boolean mUseSoftwareJpeg = false;
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
////////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
SessionConfig.Builder mSessionConfigBuilder;
/** synthetic accessor */
@SuppressWarnings("WeakerAccess")
SafeCloseImageReaderProxy mImageReader;
@SuppressWarnings("WeakerAccess")
ProcessingImageReader mProcessingImageReader;
private ListenableFuture<Void> mImageReaderCloseFuture = Futures.immediateFuture(null);
/** Callback used to match the {@link ImageProxy} with the {@link ImageInfo}. */
private CameraCaptureCallback mMetadataMatchingCaptureCallback;
private DeferrableSurface mDeferrableSurface;
private ImageCaptureRequestProcessor mImageCaptureRequestProcessor;
// Synthetic access
@SuppressWarnings("WeakerAccess")
final Executor mSequentialIoExecutor;
private Matrix mSensorToBufferTransformMatrix = new Matrix();
/**
* 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.
*/
ImageCapture(@NonNull ImageCaptureConfig userConfig) {
super(userConfig);
ImageCaptureConfig useCaseConfig = (ImageCaptureConfig) getCurrentConfig();
if (useCaseConfig.containsOption(OPTION_IMAGE_CAPTURE_MODE)) {
mCaptureMode = useCaseConfig.getCaptureMode();
} else {
mCaptureMode = DEFAULT_CAPTURE_MODE;
}
mFlashType = useCaseConfig.getFlashType(FLASH_TYPE_ONE_SHOT_FLASH);
mIoExecutor = Preconditions.checkNotNull(
useCaseConfig.getIoExecutor(CameraXExecutors.ioExecutor()));
mSequentialIoExecutor = CameraXExecutors.newSequentialExecutor(mIoExecutor);
}
@UiThread
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
SessionConfig.Builder createPipeline(@NonNull String cameraId,
@NonNull ImageCaptureConfig config, @NonNull Size resolution) {
Threads.checkMainThread();
SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
YuvToJpegProcessor softwareJpegProcessor = null;
if (Build.VERSION.SDK_INT >= 23 && getCaptureMode() == CAPTURE_MODE_ZERO_SHUTTER_LAG) {
getCameraControl().addZslConfig(resolution, sessionConfigBuilder);
}
// Setup the ImageReader to do processing
if (config.getImageReaderProxyProvider() != null) {
mImageReader =
new SafeCloseImageReaderProxy(
config.getImageReaderProxyProvider().newInstance(resolution.getWidth(),
resolution.getHeight(), getImageFormat(), MAX_IMAGES, 0));
mMetadataMatchingCaptureCallback = new CameraCaptureCallback() {
};
} else if (isSessionProcessorEnabledInCurrentCamera()) {
ImageReaderProxy imageReader;
if (getImageFormat() == ImageFormat.JPEG) {
imageReader =
new AndroidImageReaderProxy(ImageReader.newInstance(resolution.getWidth(),
resolution.getHeight(), getImageFormat(), MAX_IMAGES));
} else if (getImageFormat() == ImageFormat.YUV_420_888) { // convert it into Jpeg
if (Build.VERSION.SDK_INT >= 26) {
// Jpeg rotation / quality will be set to softwareJpegProcessor later in
// ImageCaptureRequestProcessor.
softwareJpegProcessor =
new YuvToJpegProcessor(getJpegQualityInternal(), MAX_IMAGES);
ModifiableImageReaderProxy inputReader =
new ModifiableImageReaderProxy(
ImageReader.newInstance(resolution.getWidth(),
resolution.getHeight(),
ImageFormat.YUV_420_888,
MAX_IMAGES));
CaptureBundle captureBundle = CaptureBundles.singleDefaultCaptureBundle();
ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(
inputReader,
captureBundle,
softwareJpegProcessor
).setPostProcessExecutor(mExecutor).setOutputFormat(ImageFormat.JPEG).build();
// Ensure the ImageProxy contains the same capture stage id expected from the
// ProcessingImageReader.
MutableTagBundle tagBundle = MutableTagBundle.create();
tagBundle.putTag(processingImageReader.getTagBundleKey(),
captureBundle.getCaptureStages().get(0).getId());
inputReader.setImageTagBundle(tagBundle);
imageReader = processingImageReader;
} else {
throw new UnsupportedOperationException("Does not support API level < 26");
}
} else {
throw new IllegalArgumentException("Unsupported image format:" + getImageFormat());
}
mMetadataMatchingCaptureCallback = new CameraCaptureCallback() {};
mImageReader = new SafeCloseImageReaderProxy(imageReader);
} else if (mCaptureProcessor != null || mUseSoftwareJpeg) {
// Capture processor set from configuration takes precedence over software JPEG.
CaptureProcessor captureProcessor = mCaptureProcessor;
int inputFormat = getImageFormat();
int outputFormat = getImageFormat();
if (mUseSoftwareJpeg) {
// API check to satisfy linter
if (Build.VERSION.SDK_INT >= 26) {
Logger.i(TAG, "Using software JPEG encoder.");
if (mCaptureProcessor != null) {
softwareJpegProcessor = new YuvToJpegProcessor(getJpegQualityInternal(),
mMaxCaptureStages);
captureProcessor = new CaptureProcessorPipeline(
mCaptureProcessor, mMaxCaptureStages, softwareJpegProcessor,
mExecutor);
} else {
captureProcessor = softwareJpegProcessor =
new YuvToJpegProcessor(getJpegQualityInternal(), mMaxCaptureStages);
}
outputFormat = ImageFormat.JPEG;
} else {
// Note: This should never be hit due to SDK_INT check before setting
// useSoftwareJpeg.
throw new IllegalStateException("Software JPEG only supported on API 26+");
}
}
// TODO: To allow user to use an Executor for the image processing.
mProcessingImageReader = new ProcessingImageReader.Builder(
resolution.getWidth(),
resolution.getHeight(),
inputFormat,
mMaxCaptureStages,
getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle()),
captureProcessor
).setPostProcessExecutor(mExecutor).setOutputFormat(outputFormat).build();
mMetadataMatchingCaptureCallback = mProcessingImageReader.getCameraCaptureCallback();
mImageReader = new SafeCloseImageReaderProxy(mProcessingImageReader);
} else {
MetadataImageReader metadataImageReader = new MetadataImageReader(resolution.getWidth(),
resolution.getHeight(), getImageFormat(), MAX_IMAGES);
mMetadataMatchingCaptureCallback = metadataImageReader.getCameraCaptureCallback();
mImageReader = new SafeCloseImageReaderProxy(metadataImageReader);
}
if (mImageCaptureRequestProcessor != null) {
mImageCaptureRequestProcessor.cancelRequests(
new CancellationException("Request is canceled."));
}
final YuvToJpegProcessor finalSoftwareJpegProcessor = softwareJpegProcessor;
mImageCaptureRequestProcessor = new ImageCaptureRequestProcessor(MAX_IMAGES,
request -> takePictureInternal(request), finalSoftwareJpegProcessor == null ? null :
new ImageCaptureRequestProcessor.RequestProcessCallback() {
@Override
public void onPreProcessRequest(
@NonNull ImageCaptureRequest imageCaptureRequest) {
if (Build.VERSION.SDK_INT >= 26) {
// Updates output JPEG compression quality of YuvToJpegProcessor
// according to current request. This was determined by whether the
// final output image needs to be cropped (uncompress and recompress)
// again when the capture request was created.
finalSoftwareJpegProcessor.setJpegQuality(
imageCaptureRequest.mJpegQuality);
// Updates output rotation degrees value to the YuvToJpegProcessor so
// that it can write the correct value to the ExifData in the output
// JPEG image file.
finalSoftwareJpegProcessor.setRotationDegrees(
imageCaptureRequest.mRotationDegrees);
}
}
});
// By default close images that come from the listener.
mImageReader.setOnImageAvailableListener(mClosingListener,
CameraXExecutors.mainThreadExecutor());
if (mDeferrableSurface != null) {
mDeferrableSurface.close();
}
mDeferrableSurface = new ImmediateSurface(
mImageReader.getSurface(),
new Size(mImageReader.getWidth(),
mImageReader.getHeight()),
/* get the surface image format using getImageFormat */
getImageFormat());
mImageReaderCloseFuture =
mProcessingImageReader != null ? mProcessingImageReader.getCloseFuture()
: Futures.immediateFuture(null);
mDeferrableSurface.getTerminationFuture().addListener(mImageReader::safeClose,
CameraXExecutors.mainThreadExecutor());
sessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface);
sessionConfigBuilder.addErrorListener((sessionConfig, error) -> {
clearPipeline();
// Ensure the attached camera has not changed before resetting.
// TODO(b/143915543): Ensure this never gets called by a camera that is not attached
// to this use case so we don't need to do this check.
if (isCurrentCamera(cameraId)) {
// Only reset the pipeline when the bound camera is the same.
mSessionConfigBuilder = createPipeline(cameraId, config, resolution);
updateSessionConfig(mSessionConfigBuilder.build());
notifyReset();
}
});
return sessionConfigBuilder;
}
private boolean isSessionProcessorEnabledInCurrentCamera() {
if (getCamera() == null) {
return false;
}
CameraConfig cameraConfig = getCamera().getExtendedConfig();
return cameraConfig == null ? false : cameraConfig.getSessionProcessor(null) != null;
}
/**
* Clear the internal pipeline so that the pipeline can be set up again.
*/
@UiThread
@SuppressWarnings("WeakerAccess")
void clearPipeline() {
Threads.checkMainThread();
if (mImageCaptureRequestProcessor != null) {
mImageCaptureRequestProcessor.cancelRequests(
new CancellationException("Request is canceled."));
mImageCaptureRequestProcessor = null;
}
DeferrableSurface deferrableSurface = mDeferrableSurface;
mDeferrableSurface = null;
mImageReader = null;
mProcessingImageReader = null;
mImageReaderCloseFuture = Futures.immediateFuture(null);
if (deferrableSurface != null) {
deferrableSurface.close();
}
}
/**
* {@inheritDoc}
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@Nullable
public UseCaseConfig<?> getDefaultConfig(boolean applyDefaultConfig,
@NonNull UseCaseConfigFactory factory) {
Config captureConfig = factory.getConfig(
UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE,
getCaptureMode());
if (applyDefaultConfig) {
captureConfig = Config.mergeConfigs(captureConfig, DEFAULT_CONFIG.getConfig());
}
return captureConfig == null ? null :
getUseCaseConfigBuilder(captureConfig).getUseCaseConfig();
}
/**
* {@inheritDoc}
*
* @hide
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
public UseCaseConfig.Builder<?, ?, ?> getUseCaseConfigBuilder(@NonNull Config config) {
return Builder.fromConfig(config);
}
/**
* {@inheritDoc}
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@Override
protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
@NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
if (builder.getUseCaseConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null)
!= null && Build.VERSION.SDK_INT >= 29) {
// TODO: The API level check can be removed if the ImageWriterCompat issue on API
// level 28 devices (b182363220/) can be resolved.
Logger.i(TAG, "Requesting software JPEG due to a CaptureProcessor is set.");
builder.getMutableConfig().insertOption(OPTION_USE_SOFTWARE_JPEG_ENCODER, true);
} else if (cameraInfo.getCameraQuirks().contains(
SoftwareJpegEncodingPreferredQuirk.class)) {
// Request software JPEG encoder if quirk exists on this device and the software JPEG
// option has not already been explicitly set.
if (!builder.getMutableConfig().retrieveOption(OPTION_USE_SOFTWARE_JPEG_ENCODER,
true)) {
Logger.w(TAG, "Device quirk suggests software JPEG encoder, but it has been "
+ "explicitly disabled.");
} else {
Logger.i(TAG, "Requesting software JPEG due to device quirk.");
builder.getMutableConfig().insertOption(OPTION_USE_SOFTWARE_JPEG_ENCODER, true);
}
}
// If software JPEG is requested, disable if it can't be supported on current API level.
boolean useSoftwareJpeg = enforceSoftwareJpegConstraints(builder.getMutableConfig());
// Update the input format base on the other options set (mainly whether processing
// is done)
Integer bufferFormat = builder.getMutableConfig().retrieveOption(OPTION_BUFFER_FORMAT,
null);
if (bufferFormat != null) {
Preconditions.checkArgument(
builder.getMutableConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null)
== null,
"Cannot set buffer format with CaptureProcessor defined.");
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
useSoftwareJpeg ? ImageFormat.YUV_420_888 : bufferFormat);
} else {
if (builder.getMutableConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null) != null
|| useSoftwareJpeg) {
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
ImageFormat.YUV_420_888);
} else {
List<Pair<Integer, Size[]>> supportedSizes =
builder.getMutableConfig().retrieveOption(OPTION_SUPPORTED_RESOLUTIONS,
null);
if (supportedSizes == null) {
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG);
} else {
// Use Jpeg first if supported.
if (isImageFormatSupported(supportedSizes, ImageFormat.JPEG)) {
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
ImageFormat.JPEG);
} else if (isImageFormatSupported(supportedSizes, ImageFormat.YUV_420_888)) {
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
ImageFormat.YUV_420_888);
}
}
}
}
Preconditions.checkArgument(
builder.getMutableConfig().retrieveOption(OPTION_MAX_CAPTURE_STAGES, MAX_IMAGES)
>= 1,
"Maximum outstanding image count must be at least 1");
return builder.getUseCaseConfig();
}
private static boolean isImageFormatSupported(List<Pair<Integer, Size[]>> supportedSizes,
int imageFormat) {
if (supportedSizes == null) {
return false;
}
for (Pair<Integer, Size[]> supportedSize : supportedSizes) {
if (supportedSize.first.equals(imageFormat)) {
return true;
}
}
return false;
}
/**
* Configures flash mode to CameraControlInternal once it is ready.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
protected void onCameraControlReady() {
trySetFlashModeToCameraControl();
}
/**
* Get the flash mode.
*
* @return the flashMode. Value is {@link #FLASH_MODE_AUTO}, {@link #FLASH_MODE_ON}, or
* {@link #FLASH_MODE_OFF}.
*/
@FlashMode
public int getFlashMode() {
synchronized (mLockedFlashMode) {
return mFlashMode != FLASH_MODE_UNKNOWN ? mFlashMode
: ((ImageCaptureConfig) getCurrentConfig()).getFlashMode(DEFAULT_FLASH_MODE);
}
}
/**
* Set the flash mode.
*
* <p>The flash control for the subsequent photo capture requests. Applications can check if
* there is a flash unit via {@link CameraInfo#hasFlashUnit()} and update UI component if
* necessary. If there is no flash unit, then calling this API will take no effect for the
* subsequent photo capture requests and they will act like {@link #FLASH_MODE_OFF}.
*
* <p>When the torch is enabled via {@link CameraControl#enableTorch(boolean)}, the torch
* will remain enabled during photo capture regardless of flashMode setting. When
* the torch is disabled, flash will function as specified by {@link #setFlashMode(int)}.
*
* <p>On some LEGACY devices like Samsung A3, taking pictures with {@link #FLASH_MODE_AUTO}
* mode could cause a crash. To workaround this CameraX will disable the auto flash behavior
* internally on devices that have this issue.
*
* @param flashMode the flash mode. Value is {@link #FLASH_MODE_AUTO}, {@link #FLASH_MODE_ON},
* or {@link #FLASH_MODE_OFF}.
*/
public void setFlashMode(@FlashMode int flashMode) {
if (flashMode != FLASH_MODE_AUTO && flashMode != FLASH_MODE_ON
&& flashMode != FLASH_MODE_OFF) {
throw new IllegalArgumentException("Invalid flash mode: " + flashMode);
}
synchronized (mLockedFlashMode) {
mFlashMode = flashMode;
trySetFlashModeToCameraControl();
}
}
/**
* Sets target cropping aspect ratio for output image.
*
* <p>This aspect ratio is orientation-dependent. It should be expressed in the coordinate
* frame after rotating the image by the target rotation.
*
* <p>This sets the cropping rectangle returned by {@link ImageProxy#getCropRect()} returned
* from {@link ImageCapture#takePicture(Executor, OnImageCapturedCallback)}.
*
* <p>For example, assume the {@code aspectRatio} of 3x4. If an image has a resolution of
* 480x640 after applying the target rotation, then the output {@link ImageProxy} of
* {@link ImageCapture#takePicture(Executor, OnImageCapturedCallback)} would have a cropping
* rectangle of 480x640 after applying the rotation degrees. However, if an image has a
* resolution of 640x480 after applying the target rotation, then the cropping rectangle
* of the output {@link ImageProxy} would be 360x480 after applying the rotation degrees.
*
* <p>This crops the saved image when calling
* {@link ImageCapture#takePicture(OutputFileOptions, Executor, OnImageSavedCallback)}. Note
* that the cropping will introduce an additional latency.
*
* <p>Cropping occurs around the center of the image and as though it were in the target
* rotation. For example, assume the {@code aspectRatio} of 3x4. If an image has a resolution
* of 480x640 after applying the target rotation, then the saved output image would be
* 480x640 after applying the EXIF orientation value. However, if an image has a resolution
* of 640x480 after applying the target rotation, then the saved output image would be
* 360x480 after applying the EXIF orientation value.
*
* <p>This setting value will be automatically updated to match the new target rotation value
* when {@link ImageCapture#setTargetRotation(int)} is called.
*
* @param aspectRatio New target aspect ratio.
*/
public void setCropAspectRatio(@NonNull Rational aspectRatio) {
mCropAspectRatio = aspectRatio;
}
/**
* Returns the desired rotation of the output image.
*
* <p>The rotation can be set prior to constructing an ImageCapture using
* {@link ImageCapture.Builder#setTargetRotation(int)} or dynamically by calling
* {@link ImageCapture#setTargetRotation(int)}. The rotation of an image taken is determined
* by the rotation value set at the time image capture is initiated, such as when calling
* {@link #takePicture(Executor, OnImageCapturedCallback)}.
*
* <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. The use case is fully created once it has been attached to a camera.
*
* @return The rotation of the intended target.
*/
@RotationValue
public int getTargetRotation() {
return getTargetRotationInternal();
}
/**
* Sets the desired rotation of the output image.
*
* <p>This will affect the EXIF rotation metadata in images saved by takePicture calls and the
* {@link ImageInfo#getRotationDegrees()} value of the {@link ImageProxy} returned by
* {@link OnImageCapturedCallback}. These will be set to be the rotation, which if applied to
* the output image data, will make the image match target rotation specified here.
*
* <p>While rotation can also be set via {@link Builder#setTargetRotation(int)}, using
* {@link ImageCapture#setTargetRotation(int)} allows the target rotation to be set dynamically.
*
* <p>In general, it is best to use an {@link android.view.OrientationEventListener} to
* set the target rotation. This way, the rotation output will indicate which way is down for
* a given image. This is important since display orientation may be locked by device
* default, user setting, or app configuration, and some devices may not transition to a
* reverse-portrait display orientation. In these cases,
* use {@link ImageCapture#setTargetRotation} to set target rotation dynamically according to
* the {@link android.view.OrientationEventListener}, without re-creating the use case. Note
* the OrientationEventListener output of degrees in the range [0..359] should be converted to
* a surface rotation. The mapping values are listed as the following.
* <p>{@link android.view.OrientationEventListener#ORIENTATION_UNKNOWN}: orientation == -1
* <p>{@link Surface#ROTATION_0}: orientation >= 315 || orientation < 45
* <p>{@link Surface#ROTATION_90}: orientation >= 225 && orientation < 315
* <p>{@link Surface#ROTATION_180}: orientation >= 135 && orientation < 225
* <p>{@link Surface#ROTATION_270}: orientation >= 45 && orientation < 135
*
* <p>When this function is called, value set by
* {@link ImageCapture.Builder#setTargetResolution(Size)} will be updated automatically to make
* sure the suitable resolution can be selected when the use case is bound. Value set by
* {@link ImageCapture#setCropAspectRatio(Rational)} will also be updated automatically to
* make sure the output image is cropped into expected aspect ratio.
*
* <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. The
* use case is fully created once it has been attached to a camera.
*
* <p>takePicture uses the target rotation at the time it begins executing (which may be delayed
* waiting on a previous takePicture call to complete).
*
* @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) {
int oldRotation = getTargetRotation();
if (setTargetRotationInternal(rotation)) {
// For the crop aspect ratio value, the numerator and denominator of original setting
// value will be swapped then set back. It is an orientation-dependent value that will
// be used to crop ImageCapture's output image.
if (mCropAspectRatio != null) {
int oldRotationDegrees = CameraOrientationUtil.surfaceRotationToDegrees(
oldRotation);
int newRotationDegrees = CameraOrientationUtil.surfaceRotationToDegrees(rotation);
mCropAspectRatio = ImageUtil.getRotatedAspectRatio(
Math.abs(newRotationDegrees - oldRotationDegrees), mCropAspectRatio);
}
// TODO(b/122846516): Update session configuration and possibly reconfigure session.
}
}
/**
* Returns the set capture mode.
*
* <p>This is set when constructing an ImageCapture using
* {@link ImageCapture.Builder#setCaptureMode(int)}. This is static for an instance of
* ImageCapture.
*/
@CaptureMode
public int getCaptureMode() {
return mCaptureMode;
}
/**
* Returns the JPEG quality setting.
*
* <p>This is set when constructing an ImageCapture using
* {@link ImageCapture.Builder#setJpegQuality(int)}. If not set, a default value will be set
* according to the capture mode setting. JPEG compression quality 95 is set for
* {@link #CAPTURE_MODE_MINIMIZE_LATENCY} and 100 is set for
* {@link #CAPTURE_MODE_MAXIMIZE_QUALITY}. This is static for an instance of ImageCapture.
*/
@IntRange(from = 1, to = 100)
public int getJpegQuality() {
return getJpegQualityInternal();
}
/**
* Gets selected resolution information of the {@link ImageCapture}.
*
* <p>The returned {@link ResolutionInfo} will be expressed in the coordinates of the camera
* sensor. Note that the resolution might not be the same as the resolution of the received
* image by calling {@link #takePicture} because the received image might have been rotated
* to the upright orientation using the target rotation setting by the device.
*
* <p>The resolution information might change if the use case is unbound and then rebound,
* {@link #setTargetRotation(int)} is called to change the target rotation setting, or
* {@link #setCropAspectRatio(Rational)} is called to change the crop aspect ratio setting.
* The application needs to call {@link #getResolutionInfo()} again to get the latest
* {@link ResolutionInfo} for the changes.
*
* @return the resolution information if the use case has been bound by the
* {@link androidx.camera.lifecycle.ProcessCameraProvider#bindToLifecycle(LifecycleOwner
* , CameraSelector, UseCase...)} API, or null if the use case is not bound yet.
*/
@Nullable
@Override
public ResolutionInfo getResolutionInfo() {
return super.getResolutionInfo();
}
/**
* {@inheritDoc}
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
public void setSensorToBufferTransformMatrix(@NonNull Matrix sensorToBufferTransformMatrix) {
mSensorToBufferTransformMatrix = sensorToBufferTransformMatrix;
}
/**
* {@inheritDoc}
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Nullable
@Override
protected ResolutionInfo getResolutionInfoInternal() {
CameraInternal camera = getCamera();
Size resolution = getAttachedSurfaceResolution();
if (camera == null || resolution == null) {
return null;
}
Rect cropRect = getViewPortCropRect();
Rational cropAspectRatio = mCropAspectRatio;
if (cropRect == null) {
if (cropAspectRatio != null) {
cropRect = ImageUtil.computeCropRectFromAspectRatio(resolution, cropAspectRatio);
} else {
cropRect = new Rect(0, 0, resolution.getWidth(), resolution.getHeight());
}
}
int rotationDegrees = getRelativeRotation(camera);
return ResolutionInfo.create(resolution, cropRect, rotationDegrees);
}
/**
* Captures a new still image for in memory access.
*
* <p>The 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 executor The executor in which the callback methods will be run.
* @param callback Callback to be invoked for the newly captured image
*/
public void takePicture(@NonNull Executor executor,
final @NonNull OnImageCapturedCallback callback) {
if (Looper.getMainLooper() != Looper.myLooper()) {
CameraXExecutors.mainThreadExecutor().execute(() -> takePicture(executor, callback));
return;
}
sendImageCaptureRequest(executor, callback, /*saveImage=*/false);
}
/**
* Captures a new still image and saves to a file along with application specified metadata.
*
* <p> The callback will be called only once for every invocation of this method.
*
* <p> If the {@link ImageCapture} is in a {@link UseCaseGroup} where {@link ViewPort} is
* set, or {@link #setCropAspectRatio} is used, the image may be cropped before saving to
* disk which causes an additional latency.
*
* @param outputFileOptions Options to store the newly captured image.
* @param executor The executor in which the callback methods will be run.
* @param imageSavedCallback Callback to be called for the newly captured image.
* @see ViewPort
*/
public void takePicture(
final @NonNull OutputFileOptions outputFileOptions,
final @NonNull Executor executor,
final @NonNull OnImageSavedCallback imageSavedCallback) {
if (Looper.getMainLooper() != Looper.myLooper()) {
CameraXExecutors.mainThreadExecutor().execute(
() -> takePicture(outputFileOptions, executor, imageSavedCallback));
return;
}
/*
* We need to chain the following callbacks to save the image to disk:
*
* +-----------------------+
* | |
* |ImageCapture. |
* |OnImageCapturedCallback|
* | |
* +-----------+-----------+
* |
* |
* +-----------v-----------+ +----------------------+
* | | | |
* | ImageSaver. | | ImageCapture. |
* | OnImageSavedCallback +------> OnImageSavedCallback |
* | | | |
* +-----------------------+ +----------------------+
*/
// Convert the ImageSaver.OnImageSavedCallback to ImageCapture.OnImageSavedCallback
final ImageSaver.OnImageSavedCallback imageSavedCallbackWrapper =
new ImageSaver.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull OutputFileResults outputFileResults) {
imageSavedCallback.onImageSaved(outputFileResults);
}
@Override
public void onError(@NonNull ImageSaver.SaveError error,
@NonNull String message,
@Nullable Throwable cause) {
@ImageCaptureError int imageCaptureError = ERROR_UNKNOWN;
switch (error) {
case FILE_IO_FAILED:
imageCaptureError = ERROR_FILE_IO;
break;
default:
// Keep the imageCaptureError as UNKNOWN_ERROR
break;
}
imageSavedCallback.onError(
new ImageCaptureException(imageCaptureError, message, cause));
}
};
int outputJpegQuality = getJpegQualityInternal();
// Wrap the ImageCapture.OnImageSavedCallback with an OnImageCapturedCallback so it can
// be put into the capture request queue
OnImageCapturedCallback imageCaptureCallbackWrapper =
new OnImageCapturedCallback() {
@Override
public void onCaptureSuccess(@NonNull ImageProxy image) {
mIoExecutor.execute(
new ImageSaver(
image,
outputFileOptions,
image.getImageInfo().getRotationDegrees(),
outputJpegQuality,
executor,
mSequentialIoExecutor,
imageSavedCallbackWrapper));
}
@Override
public void onError(@NonNull final ImageCaptureException exception) {
imageSavedCallback.onError(exception);
}
};
// Always use the mainThreadExecutor for the initial callback so we don't need to double
// post to another thread
sendImageCaptureRequest(CameraXExecutors.mainThreadExecutor(),
imageCaptureCallbackWrapper, /*saveImage=*/true);
}
@NonNull
static Rect computeDispatchCropRect(@Nullable Rect viewPortCropRect,
@Nullable Rational cropAspectRatio, int rotationDegrees,
@NonNull Size dispatchResolution, int dispatchRotationDegrees) {
if (viewPortCropRect != null) {
return ImageUtil.computeCropRectFromDispatchInfo(viewPortCropRect, rotationDegrees,
dispatchResolution, dispatchRotationDegrees);
} else if (cropAspectRatio != null) {
// Fall back to crop aspect ratio if view port is not available.
Rational aspectRatio = cropAspectRatio;
if ((dispatchRotationDegrees % 180) != 0) {
aspectRatio = new Rational(
/* invert the ratio numerator=*/ cropAspectRatio.getDenominator(),
/* invert the ratio denominator=*/ cropAspectRatio.getNumerator());
}
if (ImageUtil.isAspectRatioValid(dispatchResolution, aspectRatio)) {
return ImageUtil.computeCropRectFromAspectRatio(dispatchResolution,
aspectRatio);
}
}
return new Rect(0, 0, dispatchResolution.getWidth(), dispatchResolution.getHeight());
}
/**
* {@inheritDoc}
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@UiThread
@Override
public void onStateDetached() {
abortImageCaptureRequests();
}
@UiThread
private void abortImageCaptureRequests() {
if (mImageCaptureRequestProcessor != null) {
Throwable throwable = new CameraClosedException("Camera is closed.");
mImageCaptureRequestProcessor.cancelRequests(throwable);
}
}
@UiThread
private void sendImageCaptureRequest(@NonNull Executor callbackExecutor,
@NonNull OnImageCapturedCallback callback, boolean saveImage) {
// TODO(b/143734846): From here on, the image capture request should be
// self-contained and use this camera for everything. Currently the pre-capture
// sequence does not follow this approach and could fail if this use case is unbound
// or reattached to a different camera in the middle of pre-capture.
CameraInternal attachedCamera = getCamera();
if (attachedCamera == null) {
// Not bound. Notify callback.
callbackExecutor.execute(
() -> callback.onError(new ImageCaptureException(ERROR_INVALID_CAMERA,
"Not bound to a valid Camera [" + ImageCapture.this + "]", null)));
return;
}
if (mImageCaptureRequestProcessor == null) {
callbackExecutor.execute(
() -> callback.onError(
new ImageCaptureException(ERROR_UNKNOWN, "Request is canceled", null)));
return;
}
mImageCaptureRequestProcessor.sendRequest(new ImageCaptureRequest(
getRelativeRotation(attachedCamera),
getJpegQualityForImageCaptureRequest(attachedCamera, saveImage),
mCropAspectRatio,
getViewPortCropRect(),
mSensorToBufferTransformMatrix,
callbackExecutor,
callback));
}
@UiThread
private int getJpegQualityForImageCaptureRequest(@NonNull CameraInternal cameraInternal,
boolean saveImage) {
int jpegQuality;
if (saveImage) {
int rotationDegrees = getRelativeRotation(cameraInternal);
Size dispatchResolution = getAttachedSurfaceResolution();
// At this point, we can't know whether HAL will rotate the captured image or not. No
// matter HAL will rotate the image byte array or not, it won't affect whether the final
// image needs cropping or not. Therefore, we can still use the attached surface
// resolution and its relative rotation degrees against to the target rotation
// setting to calculate the possible crop rectangle and then use it to determine
// whether the final image will need cropping or not.
Rect cropRect = computeDispatchCropRect(getViewPortCropRect(), mCropAspectRatio,
rotationDegrees, dispatchResolution, rotationDegrees);
boolean shouldCropImage = ImageUtil.shouldCropImage(dispatchResolution.getWidth(),
dispatchResolution.getHeight(), cropRect.width(), cropRect.height());
if (shouldCropImage) {
// When cropping is required, jpeg compression will occur twice:
// 1. Jpeg quality set to camera HAL by camera capture request.
// 2. Bitmap compression during cropping process in ImageSaver.
// Here we need to define the first compression value and be careful to lose too
// much quality due to double compression.
// Setting 100 for the first compression can minimize quality loss, but will result
// in poor performance during cropping than setting 95 (see b/206348741 for more
// detail). As a trade-off, max quality mode is set to 100, and the others are set
// to 95.
jpegQuality = mCaptureMode == CAPTURE_MODE_MAXIMIZE_QUALITY ? 100 : 95;
} else {
jpegQuality = getJpegQualityInternal();
}
} else {
// The captured image will be directly provided to the app via the
// OnImageCapturedCallback callback. It won't be uncompressed and compressed again
// after the image is captured. The JPEG quality setting will be directly provided to
// the HAL to compress the output JPEG image.
jpegQuality = getJpegQualityInternal();
}
return jpegQuality;
}
private void lockFlashMode() {
synchronized (mLockedFlashMode) {
if (mLockedFlashMode.get() != null) {
// FlashMode is locked.
return;
}
mLockedFlashMode.set(getFlashMode());
}
}
void unlockFlashMode() {
synchronized (mLockedFlashMode) {
Integer lockedFlashMode = mLockedFlashMode.getAndSet(null);
if (lockedFlashMode == null) {
// FlashMode is not locked yet.
return;
}
if (lockedFlashMode.intValue() != getFlashMode()) {
// Flash Mode is changed during lock session.
trySetFlashModeToCameraControl();
}
}
}
private void trySetFlashModeToCameraControl() {
synchronized (mLockedFlashMode) {
if (mLockedFlashMode.get() != null) {
// Flash Mode is locked.
return;
}
getCameraControl().setFlashMode(getFlashMode());
}
}
/**
* Gets the JPEG quality based on {@link #mCaptureMode}.
*
* <p> Range is 1-100; larger is higher quality.
*
* @return Compression quality of the captured JPEG image.
*/
@IntRange(from = 1, to = 100)
private int getJpegQualityInternal() {
ImageCaptureConfig imageCaptureConfig = (ImageCaptureConfig) getCurrentConfig();
if (imageCaptureConfig.containsOption(OPTION_JPEG_COMPRESSION_QUALITY)) {
return imageCaptureConfig.getJpegQuality();
}
switch (mCaptureMode) {
case CAPTURE_MODE_MAXIMIZE_QUALITY:
return JPEG_QUALITY_MAXIMIZE_QUALITY_MODE;
case CAPTURE_MODE_MINIMIZE_LATENCY:
case CAPTURE_MODE_ZERO_SHUTTER_LAG:
return JPEG_QUALITY_MINIMIZE_LATENCY_MODE;
default:
throw new IllegalStateException("CaptureMode " + mCaptureMode + " is invalid");
}
}
/**
* 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 ListenableFuture<ImageProxy> takePictureInternal(
@NonNull ImageCaptureRequest imageCaptureRequest) {
return CallbackToFutureAdapter.getFuture(
completer -> {
mImageReader.setOnImageAvailableListener(
(imageReader) -> {
try {
ImageProxy image = imageReader.acquireLatestImage();
if (image != null) {
if (!completer.set(image)) {
// If the future is already complete (probably be
// cancelled), then close the image.
image.close();
}
} else {
completer.setException(new IllegalStateException(
"Unable to acquire image"));
}
} catch (IllegalStateException e) {
completer.setException(e);
}
},
CameraXExecutors.mainThreadExecutor());
lockFlashMode();
ListenableFuture<Void> future = issueTakePicture(imageCaptureRequest);
Futures.addCallback(future,
new FutureCallback<Void>() {
@Override
public void onSuccess(Void result) {
unlockFlashMode();
}
@Override
public void onFailure(Throwable throwable) {
unlockFlashMode();
completer.setException(throwable);
}
},
mExecutor);
completer.addCancellationListener(() -> future.cancel(true),
CameraXExecutors.directExecutor());
return "takePictureInternal";
});
}
/**
* A processor that manages and issues the pending {@link ImageCaptureRequest}s.
*
* <p>It ensures that only one single {@link ImageCaptureRequest} is in progress at a time
* and is able to process next request only when there is not over the maximum number of
* dispatched image.
*/
@VisibleForTesting
static class ImageCaptureRequestProcessor implements OnImageCloseListener {
@GuardedBy("mLock")
private final Deque<ImageCaptureRequest> mPendingRequests = new ArrayDeque<>();
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@GuardedBy("mLock")
ImageCaptureRequest mCurrentRequest = null;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@GuardedBy("mLock")
ListenableFuture<ImageProxy> mCurrentRequestFuture = null;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@GuardedBy("mLock")
int mOutstandingImages = 0;
@GuardedBy("mLock")
private final ImageCaptor mImageCaptor;
private final int mMaxImages;
@Nullable
private final RequestProcessCallback mRequestProcessCallback;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Object mLock = new Object();
ImageCaptureRequestProcessor(int maxImages, @NonNull ImageCaptor imageCaptor) {
this(maxImages, imageCaptor, null);
}
ImageCaptureRequestProcessor(int maxImages, @NonNull ImageCaptor imageCaptor,
@Nullable RequestProcessCallback requestProcessCallback) {
mMaxImages = maxImages;
mImageCaptor = imageCaptor;
mRequestProcessCallback = requestProcessCallback;
}
/**
* Sends an {@link ImageCaptureRequest} to queue.
*
* @param imageCaptureRequest the image request
*/
public void sendRequest(@NonNull ImageCaptureRequest imageCaptureRequest) {
synchronized (mLock) {
mPendingRequests.offer(imageCaptureRequest);
Logger.d(TAG, String.format(Locale.US,
"Send image capture request [current, pending] = [%d, %d]",
mCurrentRequest != null ? 1 : 0, mPendingRequests.size()));
processNextRequest();
}
}
/** Cancels current processing and pending requests. */
public void cancelRequests(@NonNull Throwable throwable) {
ImageCaptureRequest currentRequest;
ListenableFuture<ImageProxy> currentRequestFuture;
List<ImageCaptureRequest> pendingRequests;
synchronized (mLock) {
currentRequest = mCurrentRequest;
mCurrentRequest = null;
currentRequestFuture = mCurrentRequestFuture;
mCurrentRequestFuture = null;
pendingRequests = new ArrayList<>(mPendingRequests);
mPendingRequests.clear();
}
if (currentRequest != null && currentRequestFuture != null) {
currentRequest.notifyCallbackError(getError(throwable), throwable.getMessage(),
throwable);
currentRequestFuture.cancel(true);
}
for (ImageCaptureRequest request : pendingRequests) {
request.notifyCallbackError(getError(throwable), throwable.getMessage(), throwable);
}
}
@Override
public void onImageClose(ImageProxy image) {
synchronized (mLock) {
mOutstandingImages--;
processNextRequest();
}
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
void processNextRequest() {
synchronized (mLock) {
// Unable to issue request if there is currently a request in flight
if (mCurrentRequest != null) {
return;
}
// Unable to issue request if the ImageReader has no available image buffer left.
if (mOutstandingImages >= mMaxImages) {
Logger.w(TAG,
"Too many acquire images. Close image to be able to process next.");
return;
}
ImageCaptureRequest imageCaptureRequest = mPendingRequests.poll();
if (imageCaptureRequest == null) {
return;
}
mCurrentRequest = imageCaptureRequest;
if (mRequestProcessCallback != null) {
mRequestProcessCallback.onPreProcessRequest(mCurrentRequest);
}
mCurrentRequestFuture = mImageCaptor.capture(imageCaptureRequest);
Futures.addCallback(mCurrentRequestFuture, new FutureCallback<ImageProxy>() {
@Override
public void onSuccess(@Nullable ImageProxy image) {
synchronized (mLock) {
Preconditions.checkNotNull(image);
SingleCloseImageProxy wrappedImage = new SingleCloseImageProxy(image);
wrappedImage.addOnImageCloseListener(ImageCaptureRequestProcessor.this);
mOutstandingImages++;
imageCaptureRequest.dispatchImage(wrappedImage);
mCurrentRequest = null;
mCurrentRequestFuture = null;
processNextRequest();
}
}
@Override
public void onFailure(Throwable t) {
synchronized (mLock) {
if (t instanceof CancellationException) {
// Do not trigger callback which should be done in cancelRequests()
// with a given throwable.
} else {
imageCaptureRequest.notifyCallbackError(getError(t),
t != null ? t.getMessage() : "Unknown error", t);
}
mCurrentRequest = null;
mCurrentRequestFuture = null;
processNextRequest();
}
}
}, CameraXExecutors.directExecutor());
}
}
/** An interface of an {@link ImageProxy} captor. */
interface ImageCaptor {
/**
* Captures an {@link ImageProxy} by giving a {@link ImageCaptureRequest}.
*
* @param imageCaptureRequest an {@link ImageCaptureRequest} contains required
* parameters for this capture.
* @return a {@link ListenableFuture represents the capture result. Cancellation to
* the future should cancel the capture task.
*/
@NonNull
ListenableFuture<ImageProxy> capture(@NonNull ImageCaptureRequest imageCaptureRequest);
}
/**
* An interface to provide callbacks when processing each capture request.
*/
interface RequestProcessCallback {
/**
* This will be called before starting to process the
* ImageCaptureRequest.
*/
void onPreProcessRequest(@NonNull ImageCaptureRequest imageCaptureRequest);
}
}
@NonNull
@Override
public String toString() {
return TAG + ":" + getName();
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ImageCaptureError
static int getError(Throwable throwable) {
if (throwable instanceof CameraClosedException) {
return ERROR_CAMERA_CLOSED;
} else if (throwable instanceof ImageCaptureException) {
return ((ImageCaptureException) throwable).getImageCaptureError();
} else {
return ERROR_UNKNOWN;
}
}
/**
* Disables software JPEG if it is requested and the provided config and/or device
* characteristics are incompatible.
*
* @return {@code true} if software JPEG will be used after applying constraints.
*/
static boolean enforceSoftwareJpegConstraints(@NonNull MutableConfig mutableConfig) {
// Software encoder currently only supports API 26+.
if (mutableConfig.retrieveOption(OPTION_USE_SOFTWARE_JPEG_ENCODER, false)) {
boolean supported = true;
if (Build.VERSION.SDK_INT < 26) {
Logger.w(TAG, "Software JPEG only supported on API 26+, but current API level is "
+ Build.VERSION.SDK_INT);
supported = false;
}
Integer bufferFormat = mutableConfig.retrieveOption(OPTION_BUFFER_FORMAT, null);
if (bufferFormat != null && bufferFormat != ImageFormat.JPEG) {
Logger.w(TAG, "Software JPEG cannot be used with non-JPEG output buffer format.");
supported = false;
}
if (!supported) {
Logger.w(TAG, "Unable to support software JPEG. Disabling.");
mutableConfig.insertOption(OPTION_USE_SOFTWARE_JPEG_ENCODER, false);
}
return supported;
}
return false;
}
/**
* {@inheritDoc}
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
public void onDetached() {
ListenableFuture<Void> imageReaderCloseFuture = mImageReaderCloseFuture;
abortImageCaptureRequests();
clearPipeline();
mUseSoftwareJpeg = false;
// Shutdowns the executor after mImageReader is closed. This can avoid
// RejectedExecutionException if a ProcessingImageReader is used to processing the
// captured images.
ExecutorService executorService = mExecutor;
imageReaderCloseFuture.addListener(() -> executorService.shutdown(),
CameraXExecutors.directExecutor());
}
/**
* {@inheritDoc}
*
* @hide
*/
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
public void onAttached() {
ImageCaptureConfig useCaseConfig = (ImageCaptureConfig) getCurrentConfig();
CaptureConfig.Builder captureBuilder = CaptureConfig.Builder.createFrom(useCaseConfig);
mCaptureConfig = captureBuilder.build();
// Retrieve camera specific settings.
mCaptureProcessor = useCaseConfig.getCaptureProcessor(null);
mMaxCaptureStages = useCaseConfig.getMaxCaptureStages(MAX_IMAGES);
mCaptureBundle = useCaseConfig.getCaptureBundle(
CaptureBundles.singleDefaultCaptureBundle());
// This will only be set to true if software JPEG was requested and
// enforceSoftwareJpegConstraints() hasn't removed the request.
mUseSoftwareJpeg = useCaseConfig.isSoftwareJpegEncoderRequested();
CameraInternal camera = getCamera();
Preconditions.checkNotNull(camera, "Attached camera cannot be null");
mExecutor =
Executors.newFixedThreadPool(
1,
new ThreadFactory() {
private final AtomicInteger mId = new AtomicInteger(0);
@Override
public Thread newThread(@NonNull Runnable r) {
return new Thread(
r,
CameraXThreads.TAG + "image_capture_"
+ mId.getAndIncrement());
}
});
}
/**
* {@inheritDoc}
*
* @hide
*/
@NonNull
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
mSessionConfigBuilder = createPipeline(getCameraId(),
(ImageCaptureConfig) getCurrentConfig(), suggestedResolution);
updateSessionConfig(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 suggestedResolution;
}
/**
* Initiates a set of captures that will be used to create the output of
* {@link #takePicture(OutputFileOptions, Executor, OnImageSavedCallback)} and its variants.
*
* <p> This returns a {@link ListenableFuture} whose completion indicates that the
* captures are finished. Before the future is complete, any modification to the camera state
* such as 3A could affect the result of the captures. After the future is complete, then it
* is safe to reset or modify the 3A state.
*/
ListenableFuture<Void> issueTakePicture(@NonNull ImageCaptureRequest imageCaptureRequest) {
Logger.d(TAG, "issueTakePicture");
final List<CaptureConfig> captureConfigs = new ArrayList<>();
String tagBundleKey = null;
CaptureBundle captureBundle;
if (mProcessingImageReader != null) {
// If the Processor is provided, check if we have valid CaptureBundle and update
// ProcessingImageReader before actually issuing a take picture request.
captureBundle = getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle());
if (captureBundle == null) {
return Futures.immediateFailedFuture(new IllegalArgumentException(
"ImageCapture cannot set empty CaptureBundle."));
}
if (mCaptureProcessor == null && captureBundle.getCaptureStages().size() > 1) {
return Futures.immediateFailedFuture(new IllegalArgumentException(
"No CaptureProcessor can be found to process the images captured for "
+ "multiple CaptureStages."));
}
if (captureBundle.getCaptureStages().size() > mMaxCaptureStages) {
return Futures.immediateFailedFuture(new IllegalArgumentException(
"ImageCapture has CaptureStages > Max CaptureStage size"));
}
mProcessingImageReader.setCaptureBundle(captureBundle);
mProcessingImageReader.setOnProcessingErrorCallback(
CameraXExecutors.directExecutor(),
(message, cause) -> {
Logger.e(TAG, "Processing image failed! " + message);
imageCaptureRequest.notifyCallbackError(ERROR_CAPTURE_FAILED, message,
cause);
});
tagBundleKey = mProcessingImageReader.getTagBundleKey();
} else {
captureBundle = getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle());
if (captureBundle.getCaptureStages().size() > 1) {
return Futures.immediateFailedFuture(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());
// Add the default implementation options of ImageCapture
builder.addImplementationOptions(mCaptureConfig.getImplementationOptions());
builder.addAllCameraCaptureCallbacks(
mSessionConfigBuilder.getSingleCameraCaptureCallbacks());
builder.addSurface(mDeferrableSurface);
// Only sets the JPEG rotation and quality capture request options when capturing
// images in JPEG format. Some devices do not handle these CaptureRequest key values
// when capturing a non-JPEG image. Setting these capture requests and checking the
// returned capture results for specific purpose might cause problems. See b/204375890.
if (getImageFormat() == ImageFormat.JPEG) {
// Add the dynamic implementation options of ImageCapture
if (EXIF_ROTATION_AVAILABILITY.isRotationOptionSupported()) {
builder.addImplementationOption(CaptureConfig.OPTION_ROTATION,
imageCaptureRequest.mRotationDegrees);
}
builder.addImplementationOption(CaptureConfig.OPTION_JPEG_QUALITY,
imageCaptureRequest.mJpegQuality);
}
// Add the implementation options required by the CaptureStage
builder.addImplementationOptions(
captureStage.getCaptureConfig().getImplementationOptions());
// Use CaptureBundle object as the key for TagBundle
if (tagBundleKey != null) {
builder.addTag(tagBundleKey, captureStage.getId());
}
builder.addCameraCaptureCallback(mMetadataMatchingCaptureCallback);
captureConfigs.add(builder.build());
}
return Futures.transform(getCameraControl().submitStillCaptureRequests(
captureConfigs, mCaptureMode, mFlashType),
input -> null, CameraXExecutors.directExecutor());
}
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(Executor, OnImageCapturedCallback)}).
*
* <p>This is a parameter sent to the error callback functions set in listeners such as {@link
* ImageCapture.OnImageSavedCallback#onError(ImageCaptureException)}.
*
* @hide
*/
@IntDef({ERROR_UNKNOWN, ERROR_FILE_IO, ERROR_CAPTURE_FAILED, ERROR_CAMERA_CLOSED,
ERROR_INVALID_CAMERA})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(Scope.LIBRARY_GROUP)
public @interface ImageCaptureError {
}
/**
* 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.
*
* @hide
*/
@IntDef({CAPTURE_MODE_MAXIMIZE_QUALITY, CAPTURE_MODE_MINIMIZE_LATENCY,
CAPTURE_MODE_ZERO_SHUTTER_LAG})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(Scope.LIBRARY_GROUP)
public @interface CaptureMode {
}
/**
* The flash mode options when taking a picture using ImageCapture.
*
* <p>Applications can check if there is a flash unit via {@link CameraInfo#hasFlashUnit()} and
* update UI component if necessary. If there is no flash unit, then the FlashMode set to
* {@link #setFlashMode(int)} will take no effect for the subsequent photo capture requests
* and they will act like {@link #FLASH_MODE_OFF}.
*
* <p>When the torch is enabled via {@link CameraControl#enableTorch(boolean)}, the torch
* will remain enabled during photo capture regardless of flash mode setting. When
* the torch is disabled, flash will function as specified by
* {@link #setFlashMode(int)}.
*
* @hide
*/
@IntDef({FLASH_MODE_UNKNOWN, FLASH_MODE_AUTO, FLASH_MODE_ON, FLASH_MODE_OFF})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public @interface FlashMode {
}
/**
* The flash type options when flash is required for taking a picture.
*
* @hide
*/
@IntDef({FLASH_TYPE_ONE_SHOT_FLASH, FLASH_TYPE_USE_TORCH_AS_FLASH})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public @interface FlashType {
}
/** Listener containing callbacks for image file I/O events. */
public interface OnImageSavedCallback {
/** Called when an image has been successfully saved. */
void onImageSaved(@NonNull OutputFileResults outputFileResults);
/**
* Called when an error occurs while attempting to save an image.
*
* @param exception An {@link ImageCaptureException} that contains the type of error, the
* error message and the throwable that caused it.
*/
void onError(@NonNull ImageCaptureException exception);
}
/**
* Callback for when an image capture has completed.
*/
public abstract static class OnImageCapturedCallback {
/**
* 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. The value in {@code image.getImageInfo().getRotationDegrees()}
* 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 Builder#setTargetRotation(int)} and
* {@link #setTargetRotation(int)}.
*
* <p>Timestamps are in nanoseconds and monotonic and can be compared to timestamps from
* images produced from UseCases bound to the same camera instance. More detail is
* available depending on the implementation. For example with CameraX using a
* {@link androidx.camera.camera2} implementation additional detail can be found in
* {@link android.hardware.camera2.CameraDevice} documentation.
*
* @param image The captured image
*/
public void onCaptureSuccess(@NonNull ImageProxy image) {
}
/**
* Callback for when an error occurred during image capture.
*
* @param exception An {@link ImageCaptureException} that contains the type of error, the
* error message and the throwable that caused it.
*/
public void onError(@NonNull final ImageCaptureException exception) {
}
}
/**
* 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 int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 4;
private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_4_3;
private static final ImageCaptureConfig DEFAULT_CONFIG;
static {
Builder builder = new Builder().setSurfaceOccupancyPriority(
DEFAULT_SURFACE_OCCUPANCY_PRIORITY).setTargetAspectRatio(DEFAULT_ASPECT_RATIO);
DEFAULT_CONFIG = builder.getUseCaseConfig();
}
@NonNull
@Override
public ImageCaptureConfig getConfig() {
return DEFAULT_CONFIG;
}
}
/**
* Options for saving newly captured image.
*
* <p> this class is used to configure save location and metadata. Save location can be
* either a {@link File}, {@link MediaStore} or a {@link OutputStream}. The metadata will be
* stored with the saved image. For JPEG this will be included in the EXIF.
*/
public static final class OutputFileOptions {
@Nullable
private final File mFile;
@Nullable
private final ContentResolver mContentResolver;
@Nullable
private final Uri mSaveCollection;
@Nullable
private final ContentValues mContentValues;
@Nullable
private final OutputStream mOutputStream;
@NonNull
private final Metadata mMetadata;
OutputFileOptions(@Nullable File file,
@Nullable ContentResolver contentResolver,
@Nullable Uri saveCollection,
@Nullable ContentValues contentValues,
@Nullable OutputStream outputStream,
@Nullable Metadata metadata) {
mFile = file;
mContentResolver = contentResolver;
mSaveCollection = saveCollection;
mContentValues = contentValues;
mOutputStream = outputStream;
mMetadata = metadata == null ? new Metadata() : metadata;
}
@Nullable
File getFile() {
return mFile;
}
@Nullable
ContentResolver getContentResolver() {
return mContentResolver;
}
@Nullable
Uri getSaveCollection() {
return mSaveCollection;
}
@Nullable
ContentValues getContentValues() {
return mContentValues;
}
@Nullable
OutputStream getOutputStream() {
return mOutputStream;
}
/**
* Exposed internally so that CameraController can overwrite the flip horizontal flag for
* front camera. External core API users shouldn't need this because they are the ones who
* created the {@link Metadata}.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public Metadata getMetadata() {
return mMetadata;
}
/**
* Builder class for {@link OutputFileOptions}.
*/
public static final class Builder {
@Nullable
private File mFile;
@Nullable
private ContentResolver mContentResolver;
@Nullable
private Uri mSaveCollection;
@Nullable
private ContentValues mContentValues;
@Nullable
private OutputStream mOutputStream;
@Nullable
private Metadata mMetadata;
/**
* Creates options to write captured image to a {@link File}.
*
* @param file save location of the image.
*/
public Builder(@NonNull File file) {
mFile = file;
}
/**
* Creates options to write captured image to {@link MediaStore}.
*
* Example:
*
* <pre>{@code
*
* ContentValues contentValues = new ContentValues();
* contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "NEW_IMAGE");
* contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
*
* ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder(
* getContentResolver(),
* MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
* contentValues).build();
*
* }</pre>
*
* @param contentResolver to access {@link MediaStore}
* @param saveCollection The URL of the table to insert into.
* @param contentValues to be included in the created image file.
*/
public Builder(@NonNull ContentResolver contentResolver,
@NonNull Uri saveCollection,
@NonNull ContentValues contentValues) {
mContentResolver = contentResolver;
mSaveCollection = saveCollection;
mContentValues = contentValues;
}
/**
* Creates options that write captured image to a {@link OutputStream}.
*
* @param outputStream save location of the image.
*/
public Builder(@NonNull OutputStream outputStream) {
mOutputStream = outputStream;
}
/**
* Sets the metadata to be stored with the saved image.
*
* <p> For JPEG this will be included in the EXIF.
*
* @param metadata Metadata to be stored with the saved image. For JPEG this will
* be included in the EXIF.
*/
@NonNull
public Builder setMetadata(@NonNull Metadata metadata) {
mMetadata = metadata;
return this;
}
/**
* Builds {@link OutputFileOptions}.
*/
@NonNull
public OutputFileOptions build() {
return new OutputFileOptions(mFile, mContentResolver, mSaveCollection,
mContentValues, mOutputStream, mMetadata);
}
}
}
/**
* Info about the saved image file.
*/
public static class OutputFileResults {
@Nullable
private Uri mSavedUri;
OutputFileResults(@Nullable Uri savedUri) {
mSavedUri = savedUri;
}
/**
* Returns the {@link Uri} of the saved file.
*
* <p> Returns null if the {@link OutputFileOptions} is constructed with
* {@link androidx.camera.core.ImageCapture.OutputFileOptions.Builder
* #Builder(OutputStream)}.
*/
@Nullable
public Uri getSavedUri() {
return mSavedUri;
}
}
/** Holder class for metadata that will be saved with captured images. */
public static final class Metadata {
/**
* Indicates a left-right mirroring (reflection).
*
* <p>The reflection is meant to be applied to the upright image (after rotation to the
* target orientation). When saving the image to file, it is combined with the rotation
* degrees, to generate the corresponding EXIF orientation value.
*/
private boolean mIsReversedHorizontal;
/**
* Whether the mIsReversedHorizontal has been set by the app explicitly.
*/
private boolean mIsReversedHorizontalSet = false;
/**
* Indicates an upside down mirroring, equivalent to a horizontal mirroring (reflection)
* followed by a 180 degree rotation.
*
* <p>The reflection is meant to be applied to the upright image (after rotation to the
* target orientation). When saving the image to file, it is combined with the rotation
* degrees, to generate the corresponding EXIF orientation value.
*/
private boolean mIsReversedVertical;
/** Data representing a geographic location. */
@Nullable
private Location mLocation;
/**
* Gets left-right mirroring of the capture.
*
* @return true if the capture is left-right mirrored.
*/
public boolean isReversedHorizontal() {
return mIsReversedHorizontal;
}
/**
* Returns true if {@link #setReversedHorizontal} has been called.
*
* <p> CameraController's default behavior is mirroring the picture when front camera is
* used. This method is used to check if reverseHorizontal is set explicitly by the app.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public boolean isReversedHorizontalSet() {
return mIsReversedHorizontalSet;
}
/**
* Sets left-right mirroring of the capture.
*
* @param isReversedHorizontal true if the capture is left-right mirrored.
*/
public void setReversedHorizontal(boolean isReversedHorizontal) {
mIsReversedHorizontal = isReversedHorizontal;
mIsReversedHorizontalSet = true;
}
/**
* Gets upside-down mirroring of the capture.
*
* @return true if the capture is upside-down.
*/
public boolean isReversedVertical() {
return mIsReversedVertical;
}
/**
* Sets upside-down mirroring of the capture.
*
* @param isReversedVertical true if the capture is upside-down.
*/
public void setReversedVertical(boolean isReversedVertical) {
mIsReversedVertical = isReversedVertical;
}
/**
* Gets the geographic location of the capture.
*
* @return the geographic location.
*/
@Nullable
public Location getLocation() {
return mLocation;
}
/**
* Sets the geographic location of the capture.
*
* @param location the geographic location.
*/
public void setLocation(@Nullable Location location) {
mLocation = location;
}
}
@VisibleForTesting
static class ImageCaptureRequest {
@RotationValue
final int mRotationDegrees;
@IntRange(from = 1, to = 100)
final int mJpegQuality;
private final Rational mTargetRatio;
@NonNull
private final Executor mListenerExecutor;
@NonNull
private final OnImageCapturedCallback mCallback;
AtomicBoolean mDispatched = new AtomicBoolean(false);
private final Rect mViewPortCropRect;
@NonNull
private final Matrix mSensorToBufferTransformMatrix;
/**
*
* @param rotationDegrees The degrees to rotate the image buffer from sensor
* coordinates into the final output coordinate space.
* @param jpegQuality The requested output JPEG image compression quality. The value must
* be in range [1..100] which larger is higher quality.
* @param targetRatio The aspect ratio of the image in final output coordinate space.
* This must be a non-negative, non-zero value.
* @param viewPortCropRect The cropped rect of the field of view.
* @param sensorToBufferTransformMatrix The sensor to buffer transform matrix.
* @param executor The {@link Executor} which will be used for the listener.
* @param callback The {@link OnImageCapturedCallback} for the quest.
* @throws IllegalArgumentException If targetRatio is not a valid value.
*/
ImageCaptureRequest(
@RotationValue int rotationDegrees,
@IntRange(from = 1, to = 100) int jpegQuality,
Rational targetRatio,
@Nullable Rect viewPortCropRect,
@NonNull Matrix sensorToBufferTransformMatrix,
@NonNull Executor executor,
@NonNull OnImageCapturedCallback callback) {
mRotationDegrees = rotationDegrees;
mJpegQuality = jpegQuality;
if (targetRatio != null) {
Preconditions.checkArgument(!targetRatio.isZero(), "Target ratio cannot be zero");
Preconditions.checkArgument(targetRatio.floatValue() > 0, "Target ratio must be "
+ "positive");
}
mTargetRatio = targetRatio;
mViewPortCropRect = viewPortCropRect;
mSensorToBufferTransformMatrix = sensorToBufferTransformMatrix;
mListenerExecutor = executor;
mCallback = callback;
}
void dispatchImage(final ImageProxy image) {
// Check to make sure image hasn't been already dispatched or error has been notified
if (!mDispatched.compareAndSet(false, true)) {
image.close();
return;
}
Size dispatchResolution;
int dispatchRotationDegrees = 0;
// Retrieve the dimension and rotation values from the embedded EXIF data in the
// captured image only if those information is available.
if (EXIF_ROTATION_AVAILABILITY.shouldUseExifOrientation(image)) {
// JPEG needs to have rotation/crop based on the EXIF
try {
ImageProxy.PlaneProxy[] planes = image.getPlanes();
ByteBuffer buffer = planes[0].getBuffer();
Exif exif;
buffer.rewind();
byte[] data = new byte[buffer.capacity()];
buffer.get(data);
exif = Exif.createFromInputStream(new ByteArrayInputStream(data));
buffer.rewind();
dispatchResolution = new Size(exif.getWidth(), exif.getHeight());
dispatchRotationDegrees = exif.getRotation();
} catch (IOException e) {
notifyCallbackError(ERROR_FILE_IO, "Unable to parse JPEG exif", e);
image.close();
return;
}
} else {
// All other formats take the rotation based simply on the target rotation
dispatchResolution = new Size(image.getWidth(), image.getHeight());
dispatchRotationDegrees = mRotationDegrees;
}
// Construct the ImageProxy with the updated rotation & crop for the output
ImageInfo imageInfo = ImmutableImageInfo.create(
image.getImageInfo().getTagBundle(),
image.getImageInfo().getTimestamp(),
dispatchRotationDegrees,
mSensorToBufferTransformMatrix);
final ImageProxy dispatchedImageProxy = new SettableImageProxy(image,
dispatchResolution, imageInfo);
// Update the crop rect aspect ratio after it has been rotated into the buffer
// orientation
Rect cropRect = computeDispatchCropRect(mViewPortCropRect, mTargetRatio,
mRotationDegrees, dispatchResolution, dispatchRotationDegrees);
dispatchedImageProxy.setCropRect(cropRect);
try {
mListenerExecutor.execute(() -> {
mCallback.onCaptureSuccess(dispatchedImageProxy);
});
} catch (RejectedExecutionException e) {
Logger.e(TAG, "Unable to post to the supplied executor.");
// Unable to execute on the supplied executor, close the image.
image.close();
}
}
void notifyCallbackError(final @ImageCaptureError int imageCaptureError,
final String message, final Throwable cause) {
// Check to make sure image hasn't been already dispatched or error has been notified
if (!mDispatched.compareAndSet(false, true)) {
return;
}
try {
mListenerExecutor.execute(() -> mCallback.onError(
new ImageCaptureException(imageCaptureError, message, cause)));
} catch (RejectedExecutionException e) {
Logger.e(TAG, "Unable to post to the supplied executor.");
}
}
}
/** Builder for an {@link ImageCapture}. */
@SuppressWarnings("ObjectToString")
public static final class Builder implements
UseCaseConfig.Builder<ImageCapture, ImageCaptureConfig, Builder>,
ImageOutputConfig.Builder<Builder>,
IoConfig.Builder<Builder> {
private final MutableOptionsBundle mMutableConfig;
/** Creates a new Builder object. */
public Builder() {
this(MutableOptionsBundle.create());
}
private Builder(MutableOptionsBundle mutableConfig) {
mMutableConfig = mutableConfig;
Class<?> oldConfigClass =
mutableConfig.retrieveOption(TargetConfig.OPTION_TARGET_CLASS, null);
if (oldConfigClass != null && !oldConfigClass.equals(ImageCapture.class)) {
throw new IllegalArgumentException(
"Invalid target class configuration for "
+ Builder.this
+ ": "
+ oldConfigClass);
}
setTargetClass(ImageCapture.class);
}
/**
* Generates a Builder from another Config object
*
* @param configuration An immutable configuration to pre-populate this builder.
* @return The new Builder.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public static Builder fromConfig(@NonNull Config configuration) {
return new Builder(MutableOptionsBundle.from(configuration));
}
/**
* Generates a Builder from another Config object
*
* @param configuration An immutable configuration to pre-populate this builder.
* @return The new Builder.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
static Builder fromConfig(@NonNull ImageCaptureConfig configuration) {
return new Builder(MutableOptionsBundle.from(configuration));
}
/**
* {@inheritDoc}
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public MutableConfig getMutableConfig() {
return mMutableConfig;
}
/**
* {@inheritDoc}
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@Override
public ImageCaptureConfig getUseCaseConfig() {
return new ImageCaptureConfig(OptionsBundle.from(mMutableConfig));
}
/**
* Builds an immutable {@link ImageCapture} from the current state.
*
* @return A {@link ImageCapture} populated with the current state.
* @throws IllegalArgumentException if attempting to set both target aspect ratio and
* target resolution.
*/
@Override
@NonNull
public ImageCapture build() {
// Error at runtime for using both setTargetResolution and setTargetAspectRatio on
// the same config.
if (getMutableConfig().retrieveOption(OPTION_TARGET_ASPECT_RATIO, null) != null
&& getMutableConfig().retrieveOption(OPTION_TARGET_RESOLUTION, null) != null) {
throw new IllegalArgumentException(
"Cannot use both setTargetResolution and setTargetAspectRatio on the same "
+ "config.");
}
// Update the input format base on the other options set (mainly whether processing
// is done)
Integer bufferFormat = getMutableConfig().retrieveOption(OPTION_BUFFER_FORMAT, null);
if (bufferFormat != null) {
Preconditions.checkArgument(
getMutableConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null) == null,
"Cannot set buffer format with CaptureProcessor defined.");
getMutableConfig().insertOption(OPTION_INPUT_FORMAT, bufferFormat);
} else {
if (getMutableConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null) != null) {
getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.YUV_420_888);
} else {
getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG);
}
}
ImageCapture imageCapture = new ImageCapture(getUseCaseConfig());
// Makes the crop aspect ratio match the target resolution setting as what mentioned
// in javadoc of setTargetResolution(). When the target resolution is set, {@link
// ImageCapture#setCropAspectRatio(Rational)} will be automatically called to set
// corresponding value.
Size targetResolution = getMutableConfig().retrieveOption(OPTION_TARGET_RESOLUTION,
null);
if (targetResolution != null) {
imageCapture.setCropAspectRatio(new Rational(targetResolution.getWidth(),
targetResolution.getHeight()));
}
Preconditions.checkArgument(
getMutableConfig().retrieveOption(OPTION_MAX_CAPTURE_STAGES, MAX_IMAGES) >= 1,
"Maximum outstanding image count must be at least 1");
Preconditions.checkNotNull(getMutableConfig().retrieveOption(OPTION_IO_EXECUTOR,
CameraXExecutors.ioExecutor()), "The IO executor can't be null");
if (getMutableConfig().containsOption(OPTION_FLASH_MODE)) {
int flashMode = getMutableConfig().retrieveOption(OPTION_FLASH_MODE);
if (flashMode != FLASH_MODE_AUTO && flashMode != FLASH_MODE_ON
&& flashMode != FLASH_MODE_OFF) {
throw new IllegalArgumentException(
"The flash mode is not allowed to set: " + flashMode);
}
}
return imageCapture;
}
/**
* Sets the image capture mode.
*
* <p>Valid capture modes are {@link CaptureMode#CAPTURE_MODE_MINIMIZE_LATENCY}, which
* prioritizes
* latency over image quality, or {@link CaptureMode#CAPTURE_MODE_MAXIMIZE_QUALITY},
* which prioritizes
* image quality over latency.
*
* <p>If not set, the capture mode will default to
* {@link CaptureMode#CAPTURE_MODE_MINIMIZE_LATENCY}.
*
* @param captureMode The requested image capture mode.
* @return The current Builder.
*/
@NonNull
public Builder setCaptureMode(@CaptureMode int captureMode) {
getMutableConfig().insertOption(OPTION_IMAGE_CAPTURE_MODE, captureMode);
return this;
}
/**
* Sets the flashMode.
*
* <p>If not set, the flash mode will default to {@link #FLASH_MODE_OFF}.
*
* <p>See {@link ImageCapture#setFlashMode(int)} for more information.
*
* @param flashMode The requested flash mode. Value is {@link #FLASH_MODE_AUTO},
* {@link #FLASH_MODE_ON}, or {@link #FLASH_MODE_OFF}.
* @return The current Builder.
*/
@NonNull
public Builder setFlashMode(@FlashMode int flashMode) {
getMutableConfig().insertOption(OPTION_FLASH_MODE, flashMode);
return this;
}
/**
* Sets the {@link CaptureBundle}.
*
* @param captureBundle The requested capture bundle for extension.
* @return The current Builder.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public Builder setCaptureBundle(@NonNull CaptureBundle captureBundle) {
getMutableConfig().insertOption(OPTION_CAPTURE_BUNDLE, captureBundle);
return this;
}
/**
* Sets the {@link CaptureProcessor}.
*
* @param captureProcessor The requested capture processor for extension.
* @return The current Builder.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public Builder setCaptureProcessor(@NonNull CaptureProcessor captureProcessor) {
getMutableConfig().insertOption(OPTION_CAPTURE_PROCESSOR, captureProcessor);
return this;
}
/**
* Sets the {@link ImageFormat} of the {@link ImageProxy} returned by the
* {@link ImageCapture.OnImageCapturedCallback}.
*
* <p>Warning. This could lead to an invalid configuration as image format support is per
* device. Also, setting the buffer format in conjuncture with image capture extensions will
* result in an invalid configuration. In this case {@link
* ImageCapture#ImageCapture(ImageCaptureConfig)} will throw an
* {@link IllegalArgumentException}.
*
* @param bufferImageFormat The image format for captured images.
* @return The current Builder.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public Builder setBufferFormat(int bufferImageFormat) {
getMutableConfig().insertOption(OPTION_BUFFER_FORMAT, bufferImageFormat);
return this;
}
/**
* Sets the max number of {@link CaptureStage}.
*
* @param maxCaptureStages The max CaptureStage number.
* @return The current Builder.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public Builder setMaxCaptureStages(int maxCaptureStages) {
getMutableConfig().insertOption(OPTION_MAX_CAPTURE_STAGES, maxCaptureStages);
return this;
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setSupportedResolutions(@NonNull List<Pair<Integer, Size[]>> resolutions) {
getMutableConfig().insertOption(OPTION_SUPPORTED_RESOLUTIONS, resolutions);
return this;
}
// Implementations of TargetConfig.Builder default methods
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setTargetClass(@NonNull Class<ImageCapture> targetClass) {
getMutableConfig().insertOption(OPTION_TARGET_CLASS, targetClass);
// If no name is set yet, then generate a unique name
if (null == getMutableConfig().retrieveOption(OPTION_TARGET_NAME, null)) {
String targetName = targetClass.getCanonicalName() + "-" + UUID.randomUUID();
setTargetName(targetName);
}
return this;
}
/**
* Sets the name of the target object being configured, used only for debug logging.
*
* <p>The name should be a value that can uniquely identify an instance of the object being
* configured.
*
* <p>If not set, the target name will default to a unique name automatically generated
* with the class canonical name and random UUID.
*
* @param targetName A unique string identifier for the instance of the class being
* configured.
* @return the current Builder.
*/
@Override
@NonNull
public Builder setTargetName(@NonNull String targetName) {
getMutableConfig().insertOption(OPTION_TARGET_NAME, targetName);
return this;
}
// Implementations of ImageOutputConfig.Builder default methods
/**
* Sets the aspect ratio of the intended target for images from this configuration.
*
* <p>The aspect ratio is the ratio of width to height in the sensor orientation.
*
* <p>It is not allowed to set both target aspect ratio and target resolution on the same
* use case. Attempting so will throw an IllegalArgumentException when building the Config.
*
* <p>The target aspect ratio is used as a hint when determining the resulting output aspect
* ratio which may differ from the request, possibly due to device constraints.
* Application code should check the resulting output's resolution and the resulting
* aspect ratio may not be exactly as requested.
*
* <p>If not set, resolutions with aspect ratio 4:3 will be considered in higher
* priority.
*
* @param aspectRatio The desired ImageCapture {@link AspectRatio}
* @return The current Builder.
*/
@NonNull
@Override
public Builder setTargetAspectRatio(@AspectRatio.Ratio int aspectRatio) {
getMutableConfig().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
return this;
}
/**
* Sets the rotation of the intended target for images from this configuration.
*
* <p>This will affect the EXIF rotation metadata in images saved by takePicture calls and
* the {@link ImageInfo#getRotationDegrees()} value of the {@link ImageProxy} returned by
* {@link OnImageCapturedCallback}. These will be set to be the rotation, which if
* applied to the output image data, will make the image match the target rotation
* specified here.
*
* <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link
* Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
* Rotation values are relative to the "natural" rotation, {@link Surface#ROTATION_0}.
*
* <p>In general, it is best to additionally set the target rotation dynamically on the use
* case. See {@link androidx.camera.core.ImageCapture#setTargetRotation(int)} for
* additional documentation.
*
* <p>If not set, the target rotation will default to the value of
* {@link android.view.Display#getRotation()} of the default display at the time the use
* case is created. The use case is fully created once it has been attached to a camera.
*
* @param rotation The rotation of the intended target.
* @return The current Builder.
* @see androidx.camera.core.ImageCapture#setTargetRotation(int)
* @see android.view.OrientationEventListener
*/
@NonNull
@Override
public Builder setTargetRotation(@RotationValue int rotation) {
getMutableConfig().insertOption(OPTION_TARGET_ROTATION, rotation);
return this;
}
/**
* Sets the intended output target resolution.
*
* <p>The target resolution attempts to establish a minimum bound for the image resolution.
* The actual image resolution will be the closest available resolution in size that is not
* smaller than the target resolution, as determined by the Camera implementation. However,
* if no resolution exists that is equal to or larger than the target resolution, the
* nearest available resolution smaller than the target resolution will be chosen.
* Resolutions with the same aspect ratio of the provided {@link Size} will be considered in
* higher priority before resolutions of different aspect ratios.
*
* <p>It is not allowed to set both target aspect ratio and target resolution on the same
* use case. Attempting so will throw an IllegalArgumentException when building the Config.
*
* <p>The resolution {@link Size} should be expressed in the coordinate frame after
* rotating the supported sizes by the target rotation. For example, a device with
* portrait natural orientation in natural target rotation requesting a portrait image
* may specify 480x640, and the same device, rotated 90 degrees and targeting landscape
* orientation may specify 640x480.
*
* <p>When the target resolution is set,
* {@link ImageCapture#setCropAspectRatio(Rational)} will be automatically called to set
* corresponding value. Such that the output image will be cropped into the desired
* aspect ratio.
*
* <p>The maximum available resolution that could be selected for an {@link ImageCapture}
* will depend on the camera device's capability.
*
* <p>If not set, the largest available resolution will be selected to use. Usually,
* users will intend to get the largest still image that the camera device can support.
*
* <p>When using the <code>camera-camera2</code> CameraX implementation, which resolution
* will be finally selected will depend on the camera device's hardware level and the
* bound use cases combination. For more details see the guaranteed supported
* configurations tables in {@link android.hardware.camera2.CameraDevice}'s
* <a href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture">Regular capture</a> section.
*
* @param resolution The target resolution to choose from supported output sizes list.
* @return The current Builder.
*/
@NonNull
@Override
public Builder setTargetResolution(@NonNull Size resolution) {
getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION, resolution);
return this;
}
/**
* Sets the default resolution of the intended target from this configuration.
*
* @param resolution The default resolution to choose from supported output sizes list.
* @return The current Builder.
* @hide
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
public Builder setDefaultResolution(@NonNull Size resolution) {
getMutableConfig().insertOption(ImageOutputConfig.OPTION_DEFAULT_RESOLUTION,
resolution);
return this;
}
/** @hide */
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
public Builder setMaxResolution(@NonNull Size resolution) {
getMutableConfig().insertOption(OPTION_MAX_RESOLUTION, resolution);
return this;
}
/** @hide */
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
public Builder setImageReaderProxyProvider(
@NonNull ImageReaderProxyProvider imageReaderProxyProvider) {
getMutableConfig().insertOption(OPTION_IMAGE_READER_PROXY_PROVIDER,
imageReaderProxyProvider);
return this;
}
/** @hide */
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
public Builder setSoftwareJpegEncoderRequested(boolean requestSoftwareJpeg) {
getMutableConfig().insertOption(OPTION_USE_SOFTWARE_JPEG_ENCODER,
requestSoftwareJpeg);
return this;
}
/**
* Sets the flashType.
*
* <p>If not set, the flash type will default to {@link #FLASH_TYPE_ONE_SHOT_FLASH}.
*
* @param flashType The requested flash mode. Value is {@link #FLASH_TYPE_ONE_SHOT_FLASH}
* or {@link #FLASH_TYPE_USE_TORCH_AS_FLASH}.
* @return The current Builder.
* @hide
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
public Builder setFlashType(@FlashType int flashType) {
getMutableConfig().insertOption(OPTION_FLASH_TYPE, flashType);
return this;
}
/**
* Sets the output JPEG image compression quality.
*
* <p>This is used for the {@link ImageProxy} which is returned by
* {@link #takePicture(Executor, OnImageCapturedCallback)} or the output JPEG image which
* is saved by {@link #takePicture(OutputFileOptions, Executor, OnImageSavedCallback)}.
* The saved JPEG image might be cropped according to the {@link ViewPort} setting or
* the crop aspect ratio set by {@link #setCropAspectRatio(Rational)}. The JPEG quality
* setting will also be used to compress the cropped output image.
*
* <p>If not set, a default value will be used according to the capture mode setting.
* JPEG compression quality 95 is used for {@link #CAPTURE_MODE_MINIMIZE_LATENCY} and 100
* is used for {@link #CAPTURE_MODE_MAXIMIZE_QUALITY}.
*
* @param jpegQuality The requested output JPEG image compression quality. The value must
* be in range [1..100] which larger is higher quality.
* @return The current Builder.
* @throws IllegalArgumentException if the input value is not in range [1..100].
*/
@NonNull
public Builder setJpegQuality(@IntRange(from = 1, to = 100) int jpegQuality) {
Preconditions.checkArgumentInRange(jpegQuality, 1, 100, "jpegQuality");
getMutableConfig().insertOption(OPTION_JPEG_COMPRESSION_QUALITY,
jpegQuality);
return this;
}
// Implementations of IoConfig.Builder default methods
/**
* Sets the default executor that will be used for IO tasks.
*
* <p> This executor will be used for any IO tasks specifically for ImageCapture, such as
* {@link ImageCapture#takePicture(OutputFileOptions, Executor,
* ImageCapture.OnImageSavedCallback)}. If no executor is set, then a default Executor
* specifically for IO will be used instead.
*
* @param executor The executor which will be used for IO tasks.
* @return the current Builder.
*/
@Override
@NonNull
public Builder setIoExecutor(@NonNull Executor executor) {
getMutableConfig().insertOption(OPTION_IO_EXECUTOR, executor);
return this;
}
// Implementations of UseCaseConfig.Builder default methods
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setDefaultSessionConfig(@NonNull SessionConfig sessionConfig) {
getMutableConfig().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
return this;
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setDefaultCaptureConfig(@NonNull CaptureConfig captureConfig) {
getMutableConfig().insertOption(OPTION_DEFAULT_CAPTURE_CONFIG, captureConfig);
return this;
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setSessionOptionUnpacker(
@NonNull SessionConfig.OptionUnpacker optionUnpacker) {
getMutableConfig().insertOption(OPTION_SESSION_CONFIG_UNPACKER, optionUnpacker);
return this;
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setCaptureOptionUnpacker(
@NonNull CaptureConfig.OptionUnpacker optionUnpacker) {
getMutableConfig().insertOption(OPTION_CAPTURE_CONFIG_UNPACKER, optionUnpacker);
return this;
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setCameraSelector(@NonNull CameraSelector cameraSelector) {
getMutableConfig().insertOption(OPTION_CAMERA_SELECTOR, cameraSelector);
return this;
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setSurfaceOccupancyPriority(int priority) {
getMutableConfig().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
return this;
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setUseCaseEventCallback(
@NonNull UseCase.EventCallback useCaseEventCallback) {
getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_CALLBACK, useCaseEventCallback);
return this;
}
}
}