| /* |
| * Copyright 2013 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 android.hardware.camera2.cts; |
| |
| import androidx.annotation.NonNull; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.ImageFormat; |
| import android.graphics.PointF; |
| import android.graphics.Rect; |
| import android.graphics.SurfaceTexture; |
| import android.hardware.camera2.CameraAccessException; |
| import android.hardware.camera2.CameraCaptureSession; |
| import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession; |
| import android.hardware.camera2.CameraDevice; |
| import android.hardware.camera2.CameraManager; |
| import android.hardware.camera2.CameraMetadata; |
| import android.hardware.camera2.CameraCharacteristics; |
| import android.hardware.camera2.CaptureFailure; |
| import android.hardware.camera2.CaptureRequest; |
| import android.hardware.camera2.CaptureResult; |
| import android.hardware.camera2.MultiResolutionImageReader; |
| import android.hardware.camera2.cts.helpers.CameraErrorCollector; |
| import android.hardware.camera2.cts.helpers.StaticMetadata; |
| import android.hardware.camera2.params.InputConfiguration; |
| import android.hardware.camera2.TotalCaptureResult; |
| import android.hardware.cts.helpers.CameraUtils; |
| import android.hardware.camera2.params.MeteringRectangle; |
| import android.hardware.camera2.params.MandatoryStreamCombination; |
| import android.hardware.camera2.params.MandatoryStreamCombination.MandatoryStreamInformation; |
| import android.hardware.camera2.params.MultiResolutionStreamConfigurationMap; |
| import android.hardware.camera2.params.MultiResolutionStreamInfo; |
| import android.hardware.camera2.params.OutputConfiguration; |
| import android.hardware.camera2.params.SessionConfiguration; |
| import android.hardware.camera2.params.StreamConfigurationMap; |
| import android.location.Location; |
| import android.location.LocationManager; |
| import android.media.ExifInterface; |
| import android.media.Image; |
| import android.media.ImageReader; |
| import android.media.ImageWriter; |
| import android.media.Image.Plane; |
| import android.os.Build; |
| import android.os.ConditionVariable; |
| import android.os.Handler; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.util.Size; |
| import android.util.Range; |
| import android.view.Display; |
| import android.view.Surface; |
| import android.view.WindowManager; |
| |
| import com.android.ex.camera2.blocking.BlockingCameraManager; |
| import com.android.ex.camera2.blocking.BlockingCameraManager.BlockingOpenException; |
| import com.android.ex.camera2.blocking.BlockingSessionCallback; |
| import com.android.ex.camera2.blocking.BlockingStateCallback; |
| import com.android.ex.camera2.exceptions.TimeoutRuntimeException; |
| |
| import junit.framework.Assert; |
| |
| import org.mockito.Mockito; |
| |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.lang.reflect.Array; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.atomic.AtomicLong; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.Semaphore; |
| import java.util.concurrent.TimeUnit; |
| import java.text.ParseException; |
| import java.text.SimpleDateFormat; |
| |
| /** |
| * A package private utility class for wrapping up the camera2 cts test common utility functions |
| */ |
| public class CameraTestUtils extends Assert { |
| private static final String TAG = "CameraTestUtils"; |
| private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); |
| private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); |
| public static final Size SIZE_BOUND_720P = new Size(1280, 720); |
| public static final Size SIZE_BOUND_1080P = new Size(1920, 1088); |
| public static final Size SIZE_BOUND_2K = new Size(2048, 1088); |
| public static final Size SIZE_BOUND_QHD = new Size(2560, 1440); |
| public static final Size SIZE_BOUND_2160P = new Size(3840, 2160); |
| // Only test the preview size that is no larger than 1080p. |
| public static final Size PREVIEW_SIZE_BOUND = SIZE_BOUND_1080P; |
| // Default timeouts for reaching various states |
| public static final int CAMERA_OPEN_TIMEOUT_MS = 3000; |
| public static final int CAMERA_CLOSE_TIMEOUT_MS = 3000; |
| public static final int CAMERA_IDLE_TIMEOUT_MS = 3000; |
| public static final int CAMERA_ACTIVE_TIMEOUT_MS = 1000; |
| public static final int CAMERA_BUSY_TIMEOUT_MS = 1000; |
| public static final int CAMERA_UNCONFIGURED_TIMEOUT_MS = 1000; |
| public static final int CAMERA_CONFIGURE_TIMEOUT_MS = 3000; |
| public static final int CAPTURE_RESULT_TIMEOUT_MS = 3000; |
| public static final int CAPTURE_IMAGE_TIMEOUT_MS = 3000; |
| |
| public static final int SESSION_CONFIGURE_TIMEOUT_MS = 3000; |
| public static final int SESSION_CLOSE_TIMEOUT_MS = 3000; |
| public static final int SESSION_READY_TIMEOUT_MS = 5000; |
| public static final int SESSION_ACTIVE_TIMEOUT_MS = 1000; |
| |
| public static final int MAX_READER_IMAGES = 5; |
| |
| public static final int INDEX_ALGORITHM_AE = 0; |
| public static final int INDEX_ALGORITHM_AWB = 1; |
| public static final int INDEX_ALGORITHM_AF = 2; |
| public static final int NUM_ALGORITHMS = 3; // AE, AWB and AF |
| |
| public static final String OFFLINE_CAMERA_ID = "offline_camera_id"; |
| public static final String REPORT_LOG_NAME = "CtsCameraTestCases"; |
| public static final String MPC_REPORT_LOG_NAME = "MediaPerformanceClassLogs"; |
| public static final String MPC_STREAM_NAME = "CameraCts"; |
| |
| private static final int EXIF_DATETIME_LENGTH = 19; |
| private static final int EXIF_DATETIME_ERROR_MARGIN_SEC = 60; |
| private static final float EXIF_FOCAL_LENGTH_ERROR_MARGIN = 0.001f; |
| private static final float EXIF_EXPOSURE_TIME_ERROR_MARGIN_RATIO = 0.05f; |
| private static final float EXIF_EXPOSURE_TIME_MIN_ERROR_MARGIN_SEC = 0.002f; |
| private static final float EXIF_APERTURE_ERROR_MARGIN = 0.001f; |
| |
| private static final float ZOOM_RATIO_THRESHOLD = 0.01f; |
| |
| private static final Location sTestLocation0 = new Location(LocationManager.GPS_PROVIDER); |
| private static final Location sTestLocation1 = new Location(LocationManager.GPS_PROVIDER); |
| private static final Location sTestLocation2 = new Location(LocationManager.NETWORK_PROVIDER); |
| |
| static { |
| sTestLocation0.setTime(1199145600000L); |
| sTestLocation0.setLatitude(37.736071); |
| sTestLocation0.setLongitude(-122.441983); |
| sTestLocation0.setAltitude(21.0); |
| |
| sTestLocation1.setTime(1199145601000L); |
| sTestLocation1.setLatitude(0.736071); |
| sTestLocation1.setLongitude(0.441983); |
| sTestLocation1.setAltitude(1.0); |
| |
| sTestLocation2.setTime(1199145602000L); |
| sTestLocation2.setLatitude(-89.736071); |
| sTestLocation2.setLongitude(-179.441983); |
| sTestLocation2.setAltitude(100000.0); |
| } |
| |
| // Exif test data vectors. |
| public static final ExifTestData[] EXIF_TEST_DATA = { |
| new ExifTestData( |
| /*gpsLocation*/ sTestLocation0, |
| /* orientation */90, |
| /* jpgQuality */(byte) 80, |
| /* thumbQuality */(byte) 75), |
| new ExifTestData( |
| /*gpsLocation*/ sTestLocation1, |
| /* orientation */180, |
| /* jpgQuality */(byte) 90, |
| /* thumbQuality */(byte) 85), |
| new ExifTestData( |
| /*gpsLocation*/ sTestLocation2, |
| /* orientation */270, |
| /* jpgQuality */(byte) 100, |
| /* thumbQuality */(byte) 100) |
| }; |
| |
| /** |
| * Create an {@link android.media.ImageReader} object and get the surface. |
| * |
| * @param size The size of this ImageReader to be created. |
| * @param format The format of this ImageReader to be created |
| * @param maxNumImages The max number of images that can be acquired simultaneously. |
| * @param listener The listener used by this ImageReader to notify callbacks. |
| * @param handler The handler to use for any listener callbacks. |
| */ |
| public static ImageReader makeImageReader(Size size, int format, int maxNumImages, |
| ImageReader.OnImageAvailableListener listener, Handler handler) { |
| ImageReader reader; |
| reader = ImageReader.newInstance(size.getWidth(), size.getHeight(), format, |
| maxNumImages); |
| reader.setOnImageAvailableListener(listener, handler); |
| if (VERBOSE) Log.v(TAG, "Created ImageReader size " + size); |
| return reader; |
| } |
| |
| /** |
| * Create an ImageWriter and hook up the ImageListener. |
| * |
| * @param inputSurface The input surface of the ImageWriter. |
| * @param maxImages The max number of Images that can be dequeued simultaneously. |
| * @param listener The listener used by this ImageWriter to notify callbacks |
| * @param handler The handler to post listener callbacks. |
| * @return ImageWriter object created. |
| */ |
| public static ImageWriter makeImageWriter( |
| Surface inputSurface, int maxImages, |
| ImageWriter.OnImageReleasedListener listener, Handler handler) { |
| ImageWriter writer = ImageWriter.newInstance(inputSurface, maxImages); |
| writer.setOnImageReleasedListener(listener, handler); |
| return writer; |
| } |
| |
| /** |
| * Utility class to store the targets for mandatory stream combination test. |
| */ |
| public static class StreamCombinationTargets { |
| public List<SurfaceTexture> mPrivTargets = new ArrayList<>(); |
| public List<ImageReader> mJpegTargets = new ArrayList<>(); |
| public List<ImageReader> mYuvTargets = new ArrayList<>(); |
| public List<ImageReader> mY8Targets = new ArrayList<>(); |
| public List<ImageReader> mRawTargets = new ArrayList<>(); |
| public List<ImageReader> mHeicTargets = new ArrayList<>(); |
| public List<ImageReader> mDepth16Targets = new ArrayList<>(); |
| |
| public List<MultiResolutionImageReader> mPrivMultiResTargets = new ArrayList<>(); |
| public List<MultiResolutionImageReader> mJpegMultiResTargets = new ArrayList<>(); |
| public List<MultiResolutionImageReader> mYuvMultiResTargets = new ArrayList<>(); |
| public List<MultiResolutionImageReader> mRawMultiResTargets = new ArrayList<>(); |
| |
| public void close() { |
| for (SurfaceTexture target : mPrivTargets) { |
| target.release(); |
| } |
| for (ImageReader target : mJpegTargets) { |
| target.close(); |
| } |
| for (ImageReader target : mYuvTargets) { |
| target.close(); |
| } |
| for (ImageReader target : mY8Targets) { |
| target.close(); |
| } |
| for (ImageReader target : mRawTargets) { |
| target.close(); |
| } |
| for (ImageReader target : mHeicTargets) { |
| target.close(); |
| } |
| for (ImageReader target : mDepth16Targets) { |
| target.close(); |
| } |
| |
| for (MultiResolutionImageReader target : mPrivMultiResTargets) { |
| target.close(); |
| } |
| for (MultiResolutionImageReader target : mJpegMultiResTargets) { |
| target.close(); |
| } |
| for (MultiResolutionImageReader target : mYuvMultiResTargets) { |
| target.close(); |
| } |
| for (MultiResolutionImageReader target : mRawMultiResTargets) { |
| target.close(); |
| } |
| } |
| } |
| |
| private static void configureTarget(StreamCombinationTargets targets, |
| List<OutputConfiguration> outputConfigs, List<Surface> outputSurfaces, |
| int format, Size targetSize, int numBuffers, String overridePhysicalCameraId, |
| MultiResolutionStreamConfigurationMap multiResStreamConfig, |
| boolean createMultiResiStreamConfig, ImageDropperListener listener, Handler handler) { |
| if (createMultiResiStreamConfig) { |
| Collection<MultiResolutionStreamInfo> multiResolutionStreams = |
| multiResStreamConfig.getOutputInfo(format); |
| MultiResolutionImageReader multiResReader = new MultiResolutionImageReader( |
| multiResolutionStreams, format, numBuffers); |
| multiResReader.setOnImageAvailableListener(listener, new HandlerExecutor(handler)); |
| Collection<OutputConfiguration> configs = |
| OutputConfiguration.createInstancesForMultiResolutionOutput(multiResReader); |
| outputConfigs.addAll(configs); |
| outputSurfaces.add(multiResReader.getSurface()); |
| switch (format) { |
| case ImageFormat.PRIVATE: |
| targets.mPrivMultiResTargets.add(multiResReader); |
| break; |
| case ImageFormat.JPEG: |
| targets.mJpegMultiResTargets.add(multiResReader); |
| break; |
| case ImageFormat.YUV_420_888: |
| targets.mYuvMultiResTargets.add(multiResReader); |
| break; |
| case ImageFormat.RAW_SENSOR: |
| targets.mRawMultiResTargets.add(multiResReader); |
| break; |
| default: |
| fail("Unknown/Unsupported output format " + format); |
| } |
| } else { |
| if (format == ImageFormat.PRIVATE) { |
| SurfaceTexture target = new SurfaceTexture(/*random int*/1); |
| target.setDefaultBufferSize(targetSize.getWidth(), targetSize.getHeight()); |
| OutputConfiguration config = new OutputConfiguration(new Surface(target)); |
| if (overridePhysicalCameraId != null) { |
| config.setPhysicalCameraId(overridePhysicalCameraId); |
| } |
| outputConfigs.add(config); |
| outputSurfaces.add(config.getSurface()); |
| targets.mPrivTargets.add(target); |
| } else { |
| ImageReader target = ImageReader.newInstance(targetSize.getWidth(), |
| targetSize.getHeight(), format, numBuffers); |
| target.setOnImageAvailableListener(listener, handler); |
| OutputConfiguration config = new OutputConfiguration(target.getSurface()); |
| if (overridePhysicalCameraId != null) { |
| config.setPhysicalCameraId(overridePhysicalCameraId); |
| } |
| outputConfigs.add(config); |
| outputSurfaces.add(config.getSurface()); |
| |
| switch (format) { |
| case ImageFormat.JPEG: |
| targets.mJpegTargets.add(target); |
| break; |
| case ImageFormat.YUV_420_888: |
| targets.mYuvTargets.add(target); |
| break; |
| case ImageFormat.Y8: |
| targets.mY8Targets.add(target); |
| break; |
| case ImageFormat.RAW_SENSOR: |
| targets.mRawTargets.add(target); |
| break; |
| case ImageFormat.HEIC: |
| targets.mHeicTargets.add(target); |
| break; |
| case ImageFormat.DEPTH16: |
| targets.mDepth16Targets.add(target); |
| break; |
| default: |
| fail("Unknown/Unsupported output format " + format); |
| } |
| } |
| } |
| } |
| |
| public static void setupConfigurationTargets(List<MandatoryStreamInformation> streamsInfo, |
| StreamCombinationTargets targets, |
| List<OutputConfiguration> outputConfigs, |
| List<Surface> outputSurfaces, int numBuffers, |
| boolean substituteY8, boolean substituteHeic, String overridenPhysicalCameraId, |
| MultiResolutionStreamConfigurationMap multiResStreamConfig, Handler handler) { |
| List<Surface> uhSurfaces = new ArrayList<Surface>(); |
| setupConfigurationTargets(streamsInfo, targets, outputConfigs, outputSurfaces, uhSurfaces, |
| numBuffers, substituteY8, substituteHeic, overridenPhysicalCameraId, |
| multiResStreamConfig, handler); |
| } |
| |
| public static void setupConfigurationTargets(List<MandatoryStreamInformation> streamsInfo, |
| StreamCombinationTargets targets, |
| List<OutputConfiguration> outputConfigs, |
| List<Surface> outputSurfaces, List<Surface> uhSurfaces, int numBuffers, |
| boolean substituteY8, boolean substituteHeic, String overridePhysicalCameraId, |
| MultiResolutionStreamConfigurationMap multiResStreamConfig, Handler handler) { |
| |
| ImageDropperListener imageDropperListener = new ImageDropperListener(); |
| List<Surface> chosenSurfaces; |
| for (MandatoryStreamInformation streamInfo : streamsInfo) { |
| if (streamInfo.isInput()) { |
| continue; |
| } |
| chosenSurfaces = outputSurfaces; |
| if (streamInfo.isUltraHighResolution()) { |
| chosenSurfaces = uhSurfaces; |
| } |
| int format = streamInfo.getFormat(); |
| if (substituteY8 && (format == ImageFormat.YUV_420_888)) { |
| format = ImageFormat.Y8; |
| } else if (substituteHeic && (format == ImageFormat.JPEG)) { |
| format = ImageFormat.HEIC; |
| } |
| Size[] availableSizes = new Size[streamInfo.getAvailableSizes().size()]; |
| availableSizes = streamInfo.getAvailableSizes().toArray(availableSizes); |
| Size targetSize = CameraTestUtils.getMaxSize(availableSizes); |
| boolean createMultiResReader = |
| (multiResStreamConfig != null && |
| !multiResStreamConfig.getOutputInfo(format).isEmpty() && |
| streamInfo.isMaximumSize()); |
| switch (format) { |
| case ImageFormat.PRIVATE: |
| case ImageFormat.JPEG: |
| case ImageFormat.YUV_420_888: |
| case ImageFormat.Y8: |
| case ImageFormat.HEIC: |
| case ImageFormat.DEPTH16: |
| { |
| configureTarget(targets, outputConfigs, chosenSurfaces, format, |
| targetSize, numBuffers, overridePhysicalCameraId, multiResStreamConfig, |
| createMultiResReader, imageDropperListener, handler); |
| break; |
| } |
| case ImageFormat.RAW_SENSOR: { |
| // targetSize could be null in the logical camera case where only |
| // physical camera supports RAW stream. |
| if (targetSize != null) { |
| configureTarget(targets, outputConfigs, chosenSurfaces, format, |
| targetSize, numBuffers, overridePhysicalCameraId, |
| multiResStreamConfig, createMultiResReader, imageDropperListener, |
| handler); |
| } |
| break; |
| } |
| default: |
| fail("Unknown output format " + format); |
| } |
| } |
| } |
| |
| /** |
| * Close pending images and clean up an {@link android.media.ImageReader} object. |
| * @param reader an {@link android.media.ImageReader} to close. |
| */ |
| public static void closeImageReader(ImageReader reader) { |
| if (reader != null) { |
| reader.close(); |
| } |
| } |
| |
| /** |
| * Close the pending images then close current active {@link ImageReader} objects. |
| */ |
| public static void closeImageReaders(ImageReader[] readers) { |
| if ((readers != null) && (readers.length > 0)) { |
| for (ImageReader reader : readers) { |
| CameraTestUtils.closeImageReader(reader); |
| } |
| } |
| } |
| |
| /** |
| * Close pending images and clean up an {@link android.media.ImageWriter} object. |
| * @param writer an {@link android.media.ImageWriter} to close. |
| */ |
| public static void closeImageWriter(ImageWriter writer) { |
| if (writer != null) { |
| writer.close(); |
| } |
| } |
| |
| /** |
| * Dummy listener that release the image immediately once it is available. |
| * |
| * <p> |
| * It can be used for the case where we don't care the image data at all. |
| * </p> |
| */ |
| public static class ImageDropperListener implements ImageReader.OnImageAvailableListener { |
| @Override |
| public synchronized void onImageAvailable(ImageReader reader) { |
| Image image = null; |
| try { |
| image = reader.acquireNextImage(); |
| } finally { |
| if (image != null) { |
| image.close(); |
| mImagesDropped++; |
| } |
| } |
| } |
| |
| public synchronized int getImageCount() { |
| return mImagesDropped; |
| } |
| |
| public synchronized void resetImageCount() { |
| mImagesDropped = 0; |
| } |
| |
| private int mImagesDropped = 0; |
| } |
| |
| /** |
| * Image listener that release the image immediately after validating the image |
| */ |
| public static class ImageVerifierListener implements ImageReader.OnImageAvailableListener { |
| private Size mSize; |
| private int mFormat; |
| // Whether the parent ImageReader is valid or not. If the parent ImageReader |
| // is destroyed, the acquired Image may become invalid. |
| private boolean mReaderIsValid; |
| |
| public ImageVerifierListener(Size sz, int format) { |
| mSize = sz; |
| mFormat = format; |
| mReaderIsValid = true; |
| } |
| |
| public synchronized void onReaderDestroyed() { |
| mReaderIsValid = false; |
| } |
| |
| @Override |
| public synchronized void onImageAvailable(ImageReader reader) { |
| Image image = null; |
| try { |
| image = reader.acquireNextImage(); |
| } finally { |
| if (image != null) { |
| // Should only do some quick validity checks in callback, as the ImageReader |
| // could be closed asynchronously, which will close all images acquired from |
| // this ImageReader. |
| checkImage(image, mSize.getWidth(), mSize.getHeight(), mFormat); |
| // checkAndroidImageFormat calls into underlying Image object, which could |
| // become invalid if the ImageReader is destroyed. |
| if (mReaderIsValid) { |
| checkAndroidImageFormat(image); |
| } |
| image.close(); |
| } |
| } |
| } |
| } |
| |
| public static class SimpleImageReaderListener |
| implements ImageReader.OnImageAvailableListener { |
| private final LinkedBlockingQueue<Image> mQueue = |
| new LinkedBlockingQueue<Image>(); |
| // Indicate whether this listener will drop images or not, |
| // when the queued images reaches the reader maxImages |
| private final boolean mAsyncMode; |
| // maxImages held by the queue in async mode. |
| private final int mMaxImages; |
| |
| /** |
| * Create a synchronous SimpleImageReaderListener that queues the images |
| * automatically when they are available, no image will be dropped. If |
| * the caller doesn't call getImage(), the producer will eventually run |
| * into buffer starvation. |
| */ |
| public SimpleImageReaderListener() { |
| mAsyncMode = false; |
| mMaxImages = 0; |
| } |
| |
| /** |
| * Create a synchronous/asynchronous SimpleImageReaderListener that |
| * queues the images automatically when they are available. For |
| * asynchronous listener, image will be dropped if the queued images |
| * reach to maxImages queued. If the caller doesn't call getImage(), the |
| * producer will not be blocked. For synchronous listener, no image will |
| * be dropped. If the caller doesn't call getImage(), the producer will |
| * eventually run into buffer starvation. |
| * |
| * @param asyncMode If the listener is operating at asynchronous mode. |
| * @param maxImages The max number of images held by this listener. |
| */ |
| /** |
| * |
| * @param asyncMode |
| */ |
| public SimpleImageReaderListener(boolean asyncMode, int maxImages) { |
| mAsyncMode = asyncMode; |
| mMaxImages = maxImages; |
| } |
| |
| @Override |
| public void onImageAvailable(ImageReader reader) { |
| try { |
| Image imge = reader.acquireNextImage(); |
| if (imge == null) { |
| return; |
| } |
| mQueue.put(imge); |
| if (mAsyncMode && mQueue.size() >= mMaxImages) { |
| Image img = mQueue.poll(); |
| img.close(); |
| } |
| } catch (InterruptedException e) { |
| throw new UnsupportedOperationException( |
| "Can't handle InterruptedException in onImageAvailable"); |
| } |
| } |
| |
| /** |
| * Get an image from the image reader. |
| * |
| * @param timeout Timeout value for the wait. |
| * @return The image from the image reader. |
| */ |
| public Image getImage(long timeout) throws InterruptedException { |
| Image image = mQueue.poll(timeout, TimeUnit.MILLISECONDS); |
| assertNotNull("Wait for an image timed out in " + timeout + "ms", image); |
| return image; |
| } |
| |
| /** |
| * Drain the pending images held by this listener currently. |
| * |
| */ |
| public void drain() { |
| while (!mQueue.isEmpty()) { |
| Image image = mQueue.poll(); |
| assertNotNull("Unable to get an image", image); |
| image.close(); |
| } |
| } |
| } |
| |
| public static class SimpleImageWriterListener implements ImageWriter.OnImageReleasedListener { |
| private final Semaphore mImageReleasedSema = new Semaphore(0); |
| private final ImageWriter mWriter; |
| @Override |
| public void onImageReleased(ImageWriter writer) { |
| if (writer != mWriter) { |
| return; |
| } |
| |
| if (VERBOSE) { |
| Log.v(TAG, "Input image is released"); |
| } |
| mImageReleasedSema.release(); |
| } |
| |
| public SimpleImageWriterListener(ImageWriter writer) { |
| if (writer == null) { |
| throw new IllegalArgumentException("writer cannot be null"); |
| } |
| mWriter = writer; |
| } |
| |
| public void waitForImageReleased(long timeoutMs) throws InterruptedException { |
| if (!mImageReleasedSema.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS)) { |
| fail("wait for image available timed out after " + timeoutMs + "ms"); |
| } |
| } |
| } |
| |
| public static class ImageAndMultiResStreamInfo { |
| public final Image image; |
| public final MultiResolutionStreamInfo streamInfo; |
| |
| public ImageAndMultiResStreamInfo(Image image, MultiResolutionStreamInfo streamInfo) { |
| this.image = image; |
| this.streamInfo = streamInfo; |
| } |
| } |
| |
| public static class SimpleMultiResolutionImageReaderListener |
| implements ImageReader.OnImageAvailableListener { |
| public SimpleMultiResolutionImageReaderListener(MultiResolutionImageReader owner, |
| int maxBuffers, boolean acquireLatest) { |
| mOwner = owner; |
| mMaxBuffers = maxBuffers; |
| mAcquireLatest = acquireLatest; |
| } |
| |
| @Override |
| public void onImageAvailable(ImageReader reader) { |
| if (VERBOSE) Log.v(TAG, "new image available"); |
| |
| if (mAcquireLatest) { |
| mLastReader = reader; |
| mImageAvailable.open(); |
| } else { |
| if (mQueue.size() < mMaxBuffers) { |
| Image image = reader.acquireNextImage(); |
| MultiResolutionStreamInfo multiResStreamInfo = |
| mOwner.getStreamInfoForImageReader(reader); |
| mQueue.offer(new ImageAndMultiResStreamInfo(image, multiResStreamInfo)); |
| } |
| } |
| } |
| |
| public ImageAndMultiResStreamInfo getAnyImageAndInfoAvailable(long timeoutMs) |
| throws Exception { |
| if (mAcquireLatest) { |
| Image image = null; |
| if (mImageAvailable.block(timeoutMs)) { |
| if (mLastReader != null) { |
| image = mLastReader.acquireLatestImage(); |
| if (VERBOSE) Log.v(TAG, "acquireLatestImage"); |
| } else { |
| fail("invalid image reader"); |
| } |
| mImageAvailable.close(); |
| } else { |
| fail("wait for image available time out after " + timeoutMs + "ms"); |
| } |
| return new ImageAndMultiResStreamInfo(image, |
| mOwner.getStreamInfoForImageReader(mLastReader)); |
| } else { |
| ImageAndMultiResStreamInfo imageAndInfo = mQueue.poll(timeoutMs, |
| java.util.concurrent.TimeUnit.MILLISECONDS); |
| if (imageAndInfo == null) { |
| fail("wait for image available timed out after " + timeoutMs + "ms"); |
| } |
| return imageAndInfo; |
| } |
| } |
| |
| public void reset() { |
| while (!mQueue.isEmpty()) { |
| ImageAndMultiResStreamInfo imageAndInfo = mQueue.poll(); |
| assertNotNull("Acquired image is not valid", imageAndInfo.image); |
| imageAndInfo.image.close(); |
| } |
| mImageAvailable.close(); |
| mLastReader = null; |
| } |
| |
| private LinkedBlockingQueue<ImageAndMultiResStreamInfo> mQueue = |
| new LinkedBlockingQueue<ImageAndMultiResStreamInfo>(); |
| private final MultiResolutionImageReader mOwner; |
| private final int mMaxBuffers; |
| private final boolean mAcquireLatest; |
| private ConditionVariable mImageAvailable = new ConditionVariable(); |
| private ImageReader mLastReader = null; |
| } |
| |
| public static class SimpleCaptureCallback extends CameraCaptureSession.CaptureCallback { |
| private final LinkedBlockingQueue<TotalCaptureResult> mQueue = |
| new LinkedBlockingQueue<TotalCaptureResult>(); |
| private final LinkedBlockingQueue<CaptureFailure> mFailureQueue = |
| new LinkedBlockingQueue<>(); |
| // (Surface, framenumber) pair for lost buffers |
| private final LinkedBlockingQueue<Pair<Surface, Long>> mBufferLostQueue = |
| new LinkedBlockingQueue<>(); |
| private final LinkedBlockingQueue<Integer> mAbortQueue = |
| new LinkedBlockingQueue<>(); |
| // Pair<CaptureRequest, Long> is a pair of capture request and timestamp. |
| private final LinkedBlockingQueue<Pair<CaptureRequest, Long>> mCaptureStartQueue = |
| new LinkedBlockingQueue<>(); |
| // Pair<Int, Long> is a pair of sequence id and frame number |
| private final LinkedBlockingQueue<Pair<Integer, Long>> mCaptureSequenceCompletedQueue = |
| new LinkedBlockingQueue<>(); |
| |
| private AtomicLong mNumFramesArrived = new AtomicLong(0); |
| |
| @Override |
| public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, |
| long timestamp, long frameNumber) { |
| try { |
| mCaptureStartQueue.put(new Pair(request, timestamp)); |
| } catch (InterruptedException e) { |
| throw new UnsupportedOperationException( |
| "Can't handle InterruptedException in onCaptureStarted"); |
| } |
| } |
| |
| @Override |
| public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, |
| TotalCaptureResult result) { |
| try { |
| mNumFramesArrived.incrementAndGet(); |
| mQueue.put(result); |
| } catch (InterruptedException e) { |
| throw new UnsupportedOperationException( |
| "Can't handle InterruptedException in onCaptureCompleted"); |
| } |
| } |
| |
| @Override |
| public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, |
| CaptureFailure failure) { |
| try { |
| mFailureQueue.put(failure); |
| } catch (InterruptedException e) { |
| throw new UnsupportedOperationException( |
| "Can't handle InterruptedException in onCaptureFailed"); |
| } |
| } |
| |
| @Override |
| public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) { |
| try { |
| mAbortQueue.put(sequenceId); |
| } catch (InterruptedException e) { |
| throw new UnsupportedOperationException( |
| "Can't handle InterruptedException in onCaptureAborted"); |
| } |
| } |
| |
| @Override |
| public void onCaptureSequenceCompleted(CameraCaptureSession session, int sequenceId, |
| long frameNumber) { |
| try { |
| mCaptureSequenceCompletedQueue.put(new Pair(sequenceId, frameNumber)); |
| } catch (InterruptedException e) { |
| throw new UnsupportedOperationException( |
| "Can't handle InterruptedException in onCaptureSequenceCompleted"); |
| } |
| } |
| |
| @Override |
| public void onCaptureBufferLost(CameraCaptureSession session, |
| CaptureRequest request, Surface target, long frameNumber) { |
| try { |
| mBufferLostQueue.put(new Pair<>(target, frameNumber)); |
| } catch (InterruptedException e) { |
| throw new UnsupportedOperationException( |
| "Can't handle InterruptedException in onCaptureBufferLost"); |
| } |
| } |
| |
| public long getTotalNumFrames() { |
| return mNumFramesArrived.get(); |
| } |
| |
| public CaptureResult getCaptureResult(long timeout) { |
| return getTotalCaptureResult(timeout); |
| } |
| |
| public TotalCaptureResult getCaptureResult(long timeout, long timestamp) { |
| try { |
| long currentTs = -1L; |
| TotalCaptureResult result; |
| while (true) { |
| result = mQueue.poll(timeout, TimeUnit.MILLISECONDS); |
| if (result == null) { |
| throw new RuntimeException( |
| "Wait for a capture result timed out in " + timeout + "ms"); |
| } |
| currentTs = result.get(CaptureResult.SENSOR_TIMESTAMP); |
| if (currentTs == timestamp) { |
| return result; |
| } |
| } |
| |
| } catch (InterruptedException e) { |
| throw new UnsupportedOperationException("Unhandled interrupted exception", e); |
| } |
| } |
| |
| public TotalCaptureResult getTotalCaptureResult(long timeout) { |
| try { |
| TotalCaptureResult result = mQueue.poll(timeout, TimeUnit.MILLISECONDS); |
| assertNotNull("Wait for a capture result timed out in " + timeout + "ms", result); |
| return result; |
| } catch (InterruptedException e) { |
| throw new UnsupportedOperationException("Unhandled interrupted exception", e); |
| } |
| } |
| |
| /** |
| * Get the {@link #CaptureResult capture result} for a given |
| * {@link #CaptureRequest capture request}. |
| * |
| * @param myRequest The {@link #CaptureRequest capture request} whose |
| * corresponding {@link #CaptureResult capture result} was |
| * being waited for |
| * @param numResultsWait Number of frames to wait for the capture result |
| * before timeout. |
| * @throws TimeoutRuntimeException If more than numResultsWait results are |
| * seen before the result matching myRequest arrives, or each |
| * individual wait for result times out after |
| * {@value #CAPTURE_RESULT_TIMEOUT_MS}ms. |
| */ |
| public CaptureResult getCaptureResultForRequest(CaptureRequest myRequest, |
| int numResultsWait) { |
| return getTotalCaptureResultForRequest(myRequest, numResultsWait); |
| } |
| |
| /** |
| * Get the {@link #TotalCaptureResult total capture result} for a given |
| * {@link #CaptureRequest capture request}. |
| * |
| * @param myRequest The {@link #CaptureRequest capture request} whose |
| * corresponding {@link #TotalCaptureResult capture result} was |
| * being waited for |
| * @param numResultsWait Number of frames to wait for the capture result |
| * before timeout. |
| * @throws TimeoutRuntimeException If more than numResultsWait results are |
| * seen before the result matching myRequest arrives, or each |
| * individual wait for result times out after |
| * {@value #CAPTURE_RESULT_TIMEOUT_MS}ms. |
| */ |
| public TotalCaptureResult getTotalCaptureResultForRequest(CaptureRequest myRequest, |
| int numResultsWait) { |
| ArrayList<CaptureRequest> captureRequests = new ArrayList<>(1); |
| captureRequests.add(myRequest); |
| return getTotalCaptureResultsForRequests(captureRequests, numResultsWait)[0]; |
| } |
| |
| /** |
| * Get an array of {@link #TotalCaptureResult total capture results} for a given list of |
| * {@link #CaptureRequest capture requests}. This can be used when the order of results |
| * may not the same as the order of requests. |
| * |
| * @param captureRequests The list of {@link #CaptureRequest capture requests} whose |
| * corresponding {@link #TotalCaptureResult capture results} are |
| * being waited for. |
| * @param numResultsWait Number of frames to wait for the capture results |
| * before timeout. |
| * @throws TimeoutRuntimeException If more than numResultsWait results are |
| * seen before all the results matching captureRequests arrives. |
| */ |
| public TotalCaptureResult[] getTotalCaptureResultsForRequests( |
| List<CaptureRequest> captureRequests, int numResultsWait) { |
| if (numResultsWait < 0) { |
| throw new IllegalArgumentException("numResultsWait must be no less than 0"); |
| } |
| if (captureRequests == null || captureRequests.size() == 0) { |
| throw new IllegalArgumentException("captureRequests must have at least 1 request."); |
| } |
| |
| // Create a request -> a list of result indices map that it will wait for. |
| HashMap<CaptureRequest, ArrayList<Integer>> remainingResultIndicesMap = new HashMap<>(); |
| for (int i = 0; i < captureRequests.size(); i++) { |
| CaptureRequest request = captureRequests.get(i); |
| ArrayList<Integer> indices = remainingResultIndicesMap.get(request); |
| if (indices == null) { |
| indices = new ArrayList<>(); |
| remainingResultIndicesMap.put(request, indices); |
| } |
| indices.add(i); |
| } |
| |
| TotalCaptureResult[] results = new TotalCaptureResult[captureRequests.size()]; |
| int i = 0; |
| do { |
| TotalCaptureResult result = getTotalCaptureResult(CAPTURE_RESULT_TIMEOUT_MS); |
| CaptureRequest request = result.getRequest(); |
| ArrayList<Integer> indices = remainingResultIndicesMap.get(request); |
| if (indices != null) { |
| results[indices.get(0)] = result; |
| indices.remove(0); |
| |
| // Remove the entry if all results for this request has been fulfilled. |
| if (indices.isEmpty()) { |
| remainingResultIndicesMap.remove(request); |
| } |
| } |
| |
| if (remainingResultIndicesMap.isEmpty()) { |
| return results; |
| } |
| } while (i++ < numResultsWait); |
| |
| throw new TimeoutRuntimeException("Unable to get the expected capture result after " |
| + "waiting for " + numResultsWait + " results"); |
| } |
| |
| /** |
| * Get an array list of {@link #CaptureFailure capture failure} with maxNumFailures entries |
| * at most. If it times out before maxNumFailures failures are received, return the failures |
| * received so far. |
| * |
| * @param maxNumFailures The maximal number of failures to return. If it times out before |
| * the maximal number of failures are received, return the received |
| * failures so far. |
| * @throws UnsupportedOperationException If an error happens while waiting on the failure. |
| */ |
| public ArrayList<CaptureFailure> getCaptureFailures(long maxNumFailures) { |
| ArrayList<CaptureFailure> failures = new ArrayList<>(); |
| try { |
| for (int i = 0; i < maxNumFailures; i++) { |
| CaptureFailure failure = mFailureQueue.poll(CAPTURE_RESULT_TIMEOUT_MS, |
| TimeUnit.MILLISECONDS); |
| if (failure == null) { |
| // If waiting on a failure times out, return the failures so far. |
| break; |
| } |
| failures.add(failure); |
| } |
| } catch (InterruptedException e) { |
| throw new UnsupportedOperationException("Unhandled interrupted exception", e); |
| } |
| |
| return failures; |
| } |
| |
| /** |
| * Get an array list of lost buffers with maxNumLost entries at most. |
| * If it times out before maxNumLost buffer lost callbacks are received, return the |
| * lost callbacks received so far. |
| * |
| * @param maxNumLost The maximal number of buffer lost failures to return. If it times out |
| * before the maximal number of failures are received, return the received |
| * buffer lost failures so far. |
| * @throws UnsupportedOperationException If an error happens while waiting on the failure. |
| */ |
| public ArrayList<Pair<Surface, Long>> getLostBuffers(long maxNumLost) { |
| ArrayList<Pair<Surface, Long>> failures = new ArrayList<>(); |
| try { |
| for (int i = 0; i < maxNumLost; i++) { |
| Pair<Surface, Long> failure = mBufferLostQueue.poll(CAPTURE_RESULT_TIMEOUT_MS, |
| TimeUnit.MILLISECONDS); |
| if (failure == null) { |
| // If waiting on a failure times out, return the failures so far. |
| break; |
| } |
| failures.add(failure); |
| } |
| } catch (InterruptedException e) { |
| throw new UnsupportedOperationException("Unhandled interrupted exception", e); |
| } |
| |
| return failures; |
| } |
| |
| /** |
| * Get an array list of aborted capture sequence ids with maxNumAborts entries |
| * at most. If it times out before maxNumAborts are received, return the aborted sequences |
| * received so far. |
| * |
| * @param maxNumAborts The maximal number of aborted sequences to return. If it times out |
| * before the maximal number of aborts are received, return the received |
| * failed sequences so far. |
| * @throws UnsupportedOperationException If an error happens while waiting on the failed |
| * sequences. |
| */ |
| public ArrayList<Integer> geAbortedSequences(long maxNumAborts) { |
| ArrayList<Integer> abortList = new ArrayList<>(); |
| try { |
| for (int i = 0; i < maxNumAborts; i++) { |
| Integer abortSequence = mAbortQueue.poll(CAPTURE_RESULT_TIMEOUT_MS, |
| TimeUnit.MILLISECONDS); |
| if (abortSequence == null) { |
| break; |
| } |
| abortList.add(abortSequence); |
| } |
| } catch (InterruptedException e) { |
| throw new UnsupportedOperationException("Unhandled interrupted exception", e); |
| } |
| |
| return abortList; |
| } |
| |
| /** |
| * Wait until the capture start of a request and expected timestamp arrives or it times |
| * out after a number of capture starts. |
| * |
| * @param request The request for the capture start to wait for. |
| * @param timestamp The timestamp for the capture start to wait for. |
| * @param numCaptureStartsWait The number of capture start events to wait for before timing |
| * out. |
| */ |
| public void waitForCaptureStart(CaptureRequest request, Long timestamp, |
| int numCaptureStartsWait) throws Exception { |
| Pair<CaptureRequest, Long> expectedShutter = new Pair<>(request, timestamp); |
| |
| int i = 0; |
| do { |
| Pair<CaptureRequest, Long> shutter = mCaptureStartQueue.poll( |
| CAPTURE_RESULT_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| |
| if (shutter == null) { |
| throw new TimeoutRuntimeException("Unable to get any more capture start " + |
| "event after waiting for " + CAPTURE_RESULT_TIMEOUT_MS + " ms."); |
| } else if (expectedShutter.equals(shutter)) { |
| return; |
| } |
| |
| } while (i++ < numCaptureStartsWait); |
| |
| throw new TimeoutRuntimeException("Unable to get the expected capture start " + |
| "event after waiting for " + numCaptureStartsWait + " capture starts"); |
| } |
| |
| /** |
| * Wait until it receives capture sequence completed callback for a given squence ID. |
| * |
| * @param sequenceId The sequence ID of the capture sequence completed callback to wait for. |
| * @param timeoutMs Time to wait for each capture sequence complete callback before |
| * timing out. |
| */ |
| public long getCaptureSequenceLastFrameNumber(int sequenceId, long timeoutMs) { |
| try { |
| while (true) { |
| Pair<Integer, Long> completedSequence = |
| mCaptureSequenceCompletedQueue.poll(timeoutMs, TimeUnit.MILLISECONDS); |
| assertNotNull("Wait for a capture sequence completed timed out in " + |
| timeoutMs + "ms", completedSequence); |
| |
| if (completedSequence.first.equals(sequenceId)) { |
| return completedSequence.second.longValue(); |
| } |
| } |
| } catch (InterruptedException e) { |
| throw new UnsupportedOperationException("Unhandled interrupted exception", e); |
| } |
| } |
| |
| public boolean hasMoreResults() |
| { |
| return !mQueue.isEmpty(); |
| } |
| |
| public boolean hasMoreFailures() |
| { |
| return !mFailureQueue.isEmpty(); |
| } |
| |
| public int getNumLostBuffers() |
| { |
| return mBufferLostQueue.size(); |
| } |
| |
| public boolean hasMoreAbortedSequences() |
| { |
| return !mAbortQueue.isEmpty(); |
| } |
| |
| public void drain() { |
| mQueue.clear(); |
| mNumFramesArrived.getAndSet(0); |
| mFailureQueue.clear(); |
| mBufferLostQueue.clear(); |
| mCaptureStartQueue.clear(); |
| mAbortQueue.clear(); |
| } |
| } |
| |
| public static boolean hasCapability(CameraCharacteristics characteristics, int capability) { |
| int [] capabilities = |
| characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES); |
| for (int c : capabilities) { |
| if (c == capability) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public static boolean isSystemCamera(CameraManager manager, String cameraId) |
| throws CameraAccessException { |
| CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); |
| return hasCapability(characteristics, |
| CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_SYSTEM_CAMERA); |
| } |
| |
| public static String[] getCameraIdListForTesting(CameraManager manager, |
| boolean getSystemCameras) |
| throws CameraAccessException { |
| String [] ids = manager.getCameraIdListNoLazy(); |
| List<String> idsForTesting = new ArrayList<String>(); |
| for (String id : ids) { |
| boolean isSystemCamera = isSystemCamera(manager, id); |
| if (getSystemCameras == isSystemCamera) { |
| idsForTesting.add(id); |
| } |
| } |
| return idsForTesting.toArray(new String[idsForTesting.size()]); |
| } |
| |
| public static Set<Set<String>> getConcurrentCameraIds(CameraManager manager, |
| boolean getSystemCameras) |
| throws CameraAccessException { |
| Set<String> cameraIds = new HashSet<String>(Arrays.asList(getCameraIdListForTesting(manager, getSystemCameras))); |
| Set<Set<String>> combinations = manager.getConcurrentCameraIds(); |
| Set<Set<String>> correctComb = new HashSet<Set<String>>(); |
| for (Set<String> comb : combinations) { |
| Set<String> filteredIds = new HashSet<String>(); |
| for (String id : comb) { |
| if (cameraIds.contains(id)) { |
| filteredIds.add(id); |
| } |
| } |
| if (filteredIds.isEmpty()) { |
| continue; |
| } |
| correctComb.add(filteredIds); |
| } |
| return correctComb; |
| } |
| |
| /** |
| * Block until the camera is opened. |
| * |
| * <p>Don't use this to test #onDisconnected/#onError since this will throw |
| * an AssertionError if it fails to open the camera device.</p> |
| * |
| * @return CameraDevice opened camera device |
| * |
| * @throws IllegalArgumentException |
| * If the handler is null, or if the handler's looper is current. |
| * @throws CameraAccessException |
| * If open fails immediately. |
| * @throws BlockingOpenException |
| * If open fails after blocking for some amount of time. |
| * @throws TimeoutRuntimeException |
| * If opening times out. Typically unrecoverable. |
| */ |
| public static CameraDevice openCamera(CameraManager manager, String cameraId, |
| CameraDevice.StateCallback listener, Handler handler) throws CameraAccessException, |
| BlockingOpenException { |
| |
| /** |
| * Although camera2 API allows 'null' Handler (it will just use the current |
| * thread's Looper), this is not what we want for CTS. |
| * |
| * In CTS the default looper is used only to process events in between test runs, |
| * so anything sent there would not be executed inside a test and the test would fail. |
| * |
| * In this case, BlockingCameraManager#openCamera performs the check for us. |
| */ |
| return (new BlockingCameraManager(manager)).openCamera(cameraId, listener, handler); |
| } |
| |
| |
| /** |
| * Block until the camera is opened. |
| * |
| * <p>Don't use this to test #onDisconnected/#onError since this will throw |
| * an AssertionError if it fails to open the camera device.</p> |
| * |
| * @throws IllegalArgumentException |
| * If the handler is null, or if the handler's looper is current. |
| * @throws CameraAccessException |
| * If open fails immediately. |
| * @throws BlockingOpenException |
| * If open fails after blocking for some amount of time. |
| * @throws TimeoutRuntimeException |
| * If opening times out. Typically unrecoverable. |
| */ |
| public static CameraDevice openCamera(CameraManager manager, String cameraId, Handler handler) |
| throws CameraAccessException, |
| BlockingOpenException { |
| return openCamera(manager, cameraId, /*listener*/null, handler); |
| } |
| |
| /** |
| * Configure a new camera session with output surfaces and type. |
| * |
| * @param camera The CameraDevice to be configured. |
| * @param outputSurfaces The surface list that used for camera output. |
| * @param listener The callback CameraDevice will notify when capture results are available. |
| */ |
| public static CameraCaptureSession configureCameraSession(CameraDevice camera, |
| List<Surface> outputSurfaces, boolean isHighSpeed, |
| CameraCaptureSession.StateCallback listener, Handler handler) |
| throws CameraAccessException { |
| BlockingSessionCallback sessionListener = new BlockingSessionCallback(listener); |
| if (isHighSpeed) { |
| camera.createConstrainedHighSpeedCaptureSession(outputSurfaces, |
| sessionListener, handler); |
| } else { |
| camera.createCaptureSession(outputSurfaces, sessionListener, handler); |
| } |
| CameraCaptureSession session = |
| sessionListener.waitAndGetSession(SESSION_CONFIGURE_TIMEOUT_MS); |
| assertFalse("Camera session should not be a reprocessable session", |
| session.isReprocessable()); |
| String sessionType = isHighSpeed ? "High Speed" : "Normal"; |
| assertTrue("Capture session type must be " + sessionType, |
| isHighSpeed == |
| CameraConstrainedHighSpeedCaptureSession.class.isAssignableFrom(session.getClass())); |
| |
| return session; |
| } |
| |
| /** |
| * Build a new constrained camera session with output surfaces, type and recording session |
| * parameters. |
| * |
| * @param camera The CameraDevice to be configured. |
| * @param outputSurfaces The surface list that used for camera output. |
| * @param listener The callback CameraDevice will notify when capture results are available. |
| * @param initialRequest Initial request settings to use as session parameters. |
| */ |
| public static CameraCaptureSession buildConstrainedCameraSession(CameraDevice camera, |
| List<Surface> outputSurfaces, CameraCaptureSession.StateCallback listener, |
| Handler handler, CaptureRequest initialRequest) throws CameraAccessException { |
| BlockingSessionCallback sessionListener = new BlockingSessionCallback(listener); |
| |
| List<OutputConfiguration> outConfigurations = new ArrayList<>(outputSurfaces.size()); |
| for (Surface surface : outputSurfaces) { |
| outConfigurations.add(new OutputConfiguration(surface)); |
| } |
| SessionConfiguration sessionConfig = new SessionConfiguration( |
| SessionConfiguration.SESSION_HIGH_SPEED, outConfigurations, |
| new HandlerExecutor(handler), sessionListener); |
| sessionConfig.setSessionParameters(initialRequest); |
| camera.createCaptureSession(sessionConfig); |
| |
| CameraCaptureSession session = |
| sessionListener.waitAndGetSession(SESSION_CONFIGURE_TIMEOUT_MS); |
| assertFalse("Camera session should not be a reprocessable session", |
| session.isReprocessable()); |
| assertTrue("Capture session type must be High Speed", |
| CameraConstrainedHighSpeedCaptureSession.class.isAssignableFrom( |
| session.getClass())); |
| |
| return session; |
| } |
| |
| /** |
| * Configure a new camera session with output configurations. |
| * |
| * @param camera The CameraDevice to be configured. |
| * @param outputs The OutputConfiguration list that is used for camera output. |
| * @param listener The callback CameraDevice will notify when capture results are available. |
| */ |
| public static CameraCaptureSession configureCameraSessionWithConfig(CameraDevice camera, |
| List<OutputConfiguration> outputs, |
| CameraCaptureSession.StateCallback listener, Handler handler) |
| throws CameraAccessException { |
| BlockingSessionCallback sessionListener = new BlockingSessionCallback(listener); |
| camera.createCaptureSessionByOutputConfigurations(outputs, sessionListener, handler); |
| CameraCaptureSession session = |
| sessionListener.waitAndGetSession(SESSION_CONFIGURE_TIMEOUT_MS); |
| assertFalse("Camera session should not be a reprocessable session", |
| session.isReprocessable()); |
| return session; |
| } |
| |
| /** |
| * Try configure a new camera session with output configurations. |
| * |
| * @param camera The CameraDevice to be configured. |
| * @param outputs The OutputConfiguration list that is used for camera output. |
| * @param initialRequest The session parameters passed in during stream configuration |
| * @param listener The callback CameraDevice will notify when capture results are available. |
| */ |
| public static CameraCaptureSession tryConfigureCameraSessionWithConfig(CameraDevice camera, |
| List<OutputConfiguration> outputs, CaptureRequest initialRequest, |
| CameraCaptureSession.StateCallback listener, Handler handler) |
| throws CameraAccessException { |
| BlockingSessionCallback sessionListener = new BlockingSessionCallback(listener); |
| SessionConfiguration sessionConfig = new SessionConfiguration( |
| SessionConfiguration.SESSION_REGULAR, outputs, new HandlerExecutor(handler), |
| sessionListener); |
| sessionConfig.setSessionParameters(initialRequest); |
| camera.createCaptureSession(sessionConfig); |
| |
| Integer[] sessionStates = {BlockingSessionCallback.SESSION_READY, |
| BlockingSessionCallback.SESSION_CONFIGURE_FAILED}; |
| int state = sessionListener.getStateWaiter().waitForAnyOfStates( |
| Arrays.asList(sessionStates), SESSION_CONFIGURE_TIMEOUT_MS); |
| |
| CameraCaptureSession session = null; |
| if (state == BlockingSessionCallback.SESSION_READY) { |
| session = sessionListener.waitAndGetSession(SESSION_CONFIGURE_TIMEOUT_MS); |
| assertFalse("Camera session should not be a reprocessable session", |
| session.isReprocessable()); |
| } |
| return session; |
| } |
| |
| /** |
| * Configure a new camera session with output surfaces and initial session parameters. |
| * |
| * @param camera The CameraDevice to be configured. |
| * @param outputSurfaces The surface list that used for camera output. |
| * @param listener The callback CameraDevice will notify when session is available. |
| * @param handler The handler used to notify callbacks. |
| * @param initialRequest Initial request settings to use as session parameters. |
| */ |
| public static CameraCaptureSession configureCameraSessionWithParameters(CameraDevice camera, |
| List<Surface> outputSurfaces, BlockingSessionCallback listener, |
| Handler handler, CaptureRequest initialRequest) throws CameraAccessException { |
| List<OutputConfiguration> outConfigurations = new ArrayList<>(outputSurfaces.size()); |
| for (Surface surface : outputSurfaces) { |
| outConfigurations.add(new OutputConfiguration(surface)); |
| } |
| SessionConfiguration sessionConfig = new SessionConfiguration( |
| SessionConfiguration.SESSION_REGULAR, outConfigurations, |
| new HandlerExecutor(handler), listener); |
| sessionConfig.setSessionParameters(initialRequest); |
| camera.createCaptureSession(sessionConfig); |
| |
| CameraCaptureSession session = listener.waitAndGetSession(SESSION_CONFIGURE_TIMEOUT_MS); |
| assertFalse("Camera session should not be a reprocessable session", |
| session.isReprocessable()); |
| assertFalse("Capture session type must be regular", |
| CameraConstrainedHighSpeedCaptureSession.class.isAssignableFrom( |
| session.getClass())); |
| |
| return session; |
| } |
| |
| /** |
| * Configure a new camera session with output surfaces. |
| * |
| * @param camera The CameraDevice to be configured. |
| * @param outputSurfaces The surface list that used for camera output. |
| * @param listener The callback CameraDevice will notify when capture results are available. |
| */ |
| public static CameraCaptureSession configureCameraSession(CameraDevice camera, |
| List<Surface> outputSurfaces, |
| CameraCaptureSession.StateCallback listener, Handler handler) |
| throws CameraAccessException { |
| |
| return configureCameraSession(camera, outputSurfaces, /*isHighSpeed*/false, |
| listener, handler); |
| } |
| |
| public static CameraCaptureSession configureReprocessableCameraSession(CameraDevice camera, |
| InputConfiguration inputConfiguration, List<Surface> outputSurfaces, |
| CameraCaptureSession.StateCallback listener, Handler handler) |
| throws CameraAccessException { |
| List<OutputConfiguration> outputConfigs = new ArrayList<OutputConfiguration>(); |
| for (Surface surface : outputSurfaces) { |
| outputConfigs.add(new OutputConfiguration(surface)); |
| } |
| CameraCaptureSession session = configureReprocessableCameraSessionWithConfigurations( |
| camera, inputConfiguration, outputConfigs, listener, handler); |
| |
| return session; |
| } |
| |
| public static CameraCaptureSession configureReprocessableCameraSessionWithConfigurations( |
| CameraDevice camera, InputConfiguration inputConfiguration, |
| List<OutputConfiguration> outputConfigs, CameraCaptureSession.StateCallback listener, |
| Handler handler) throws CameraAccessException { |
| BlockingSessionCallback sessionListener = new BlockingSessionCallback(listener); |
| SessionConfiguration sessionConfig = new SessionConfiguration( |
| SessionConfiguration.SESSION_REGULAR, outputConfigs, new HandlerExecutor(handler), |
| sessionListener); |
| sessionConfig.setInputConfiguration(inputConfiguration); |
| camera.createCaptureSession(sessionConfig); |
| |
| Integer[] sessionStates = {BlockingSessionCallback.SESSION_READY, |
| BlockingSessionCallback.SESSION_CONFIGURE_FAILED}; |
| int state = sessionListener.getStateWaiter().waitForAnyOfStates( |
| Arrays.asList(sessionStates), SESSION_CONFIGURE_TIMEOUT_MS); |
| |
| assertTrue("Creating a reprocessable session failed.", |
| state == BlockingSessionCallback.SESSION_READY); |
| CameraCaptureSession session = |
| sessionListener.waitAndGetSession(SESSION_CONFIGURE_TIMEOUT_MS); |
| assertTrue("Camera session should be a reprocessable session", session.isReprocessable()); |
| |
| return session; |
| } |
| |
| /** |
| * Create a reprocessable camera session with input and output configurations. |
| * |
| * @param camera The CameraDevice to be configured. |
| * @param inputConfiguration The input configuration used to create this session. |
| * @param outputs The output configurations used to create this session. |
| * @param listener The callback CameraDevice will notify when capture results are available. |
| * @param handler The handler used to notify callbacks. |
| * @return The session ready to use. |
| * @throws CameraAccessException |
| */ |
| public static CameraCaptureSession configureReprocCameraSessionWithConfig(CameraDevice camera, |
| InputConfiguration inputConfiguration, List<OutputConfiguration> outputs, |
| CameraCaptureSession.StateCallback listener, Handler handler) |
| throws CameraAccessException { |
| BlockingSessionCallback sessionListener = new BlockingSessionCallback(listener); |
| camera.createReprocessableCaptureSessionByConfigurations(inputConfiguration, outputs, |
| sessionListener, handler); |
| |
| Integer[] sessionStates = {BlockingSessionCallback.SESSION_READY, |
| BlockingSessionCallback.SESSION_CONFIGURE_FAILED}; |
| int state = sessionListener.getStateWaiter().waitForAnyOfStates( |
| Arrays.asList(sessionStates), SESSION_CONFIGURE_TIMEOUT_MS); |
| |
| assertTrue("Creating a reprocessable session failed.", |
| state == BlockingSessionCallback.SESSION_READY); |
| |
| CameraCaptureSession session = |
| sessionListener.waitAndGetSession(SESSION_CONFIGURE_TIMEOUT_MS); |
| assertTrue("Camera session should be a reprocessable session", session.isReprocessable()); |
| |
| return session; |
| } |
| |
| public static <T> void assertArrayNotEmpty(T arr, String message) { |
| assertTrue(message, arr != null && Array.getLength(arr) > 0); |
| } |
| |
| /** |
| * Check if the format is a legal YUV format camera supported. |
| */ |
| public static void checkYuvFormat(int format) { |
| if ((format != ImageFormat.YUV_420_888) && |
| (format != ImageFormat.NV21) && |
| (format != ImageFormat.YV12)) { |
| fail("Wrong formats: " + format); |
| } |
| } |
| |
| /** |
| * Check if image size and format match given size and format. |
| */ |
| public static void checkImage(Image image, int width, int height, int format) { |
| // Image reader will wrap YV12/NV21 image by YUV_420_888 |
| if (format == ImageFormat.NV21 || format == ImageFormat.YV12) { |
| format = ImageFormat.YUV_420_888; |
| } |
| assertNotNull("Input image is invalid", image); |
| assertEquals("Format doesn't match", format, image.getFormat()); |
| assertEquals("Width doesn't match", width, image.getWidth()); |
| assertEquals("Height doesn't match", height, image.getHeight()); |
| } |
| |
| /** |
| * <p>Read data from all planes of an Image into a contiguous unpadded, unpacked |
| * 1-D linear byte array, such that it can be write into disk, or accessed by |
| * software conveniently. It supports YUV_420_888/NV21/YV12 and JPEG input |
| * Image format.</p> |
| * |
| * <p>For YUV_420_888/NV21/YV12/Y8/Y16, it returns a byte array that contains |
| * the Y plane data first, followed by U(Cb), V(Cr) planes if there is any |
| * (xstride = width, ystride = height for chroma and luma components).</p> |
| * |
| * <p>For JPEG, it returns a 1-D byte array contains a complete JPEG image.</p> |
| * |
| * <p>For YUV P010, it returns a byte array that contains Y plane first, followed |
| * by the interleaved U(Cb)/V(Cr) plane.</p> |
| */ |
| public static byte[] getDataFromImage(Image image) { |
| assertNotNull("Invalid image:", image); |
| int format = image.getFormat(); |
| int width = image.getWidth(); |
| int height = image.getHeight(); |
| int rowStride, pixelStride; |
| byte[] data = null; |
| |
| // Read image data |
| Plane[] planes = image.getPlanes(); |
| assertTrue("Fail to get image planes", planes != null && planes.length > 0); |
| |
| // Check image validity |
| checkAndroidImageFormat(image); |
| |
| ByteBuffer buffer = null; |
| // JPEG doesn't have pixelstride and rowstride, treat it as 1D buffer. |
| // Same goes for DEPTH_POINT_CLOUD, RAW_PRIVATE, DEPTH_JPEG, and HEIC |
| if (format == ImageFormat.JPEG || format == ImageFormat.DEPTH_POINT_CLOUD || |
| format == ImageFormat.RAW_PRIVATE || format == ImageFormat.DEPTH_JPEG || |
| format == ImageFormat.HEIC) { |
| buffer = planes[0].getBuffer(); |
| assertNotNull("Fail to get jpeg/depth/heic ByteBuffer", buffer); |
| data = new byte[buffer.remaining()]; |
| buffer.get(data); |
| buffer.rewind(); |
| return data; |
| } else if (format == ImageFormat.YCBCR_P010) { |
| // P010 samples are stored within 16 bit values |
| int offset = 0; |
| int bytesPerPixelRounded = (ImageFormat.getBitsPerPixel(format) + 7) / 8; |
| data = new byte[width * height * bytesPerPixelRounded]; |
| assertTrue("Unexpected number of planes, expected " + 3 + " actual " + planes.length, |
| planes.length == 3); |
| for (int i = 0; i < 2; i++) { |
| buffer = planes[i].getBuffer(); |
| assertNotNull("Fail to get bytebuffer from plane", buffer); |
| buffer.rewind(); |
| rowStride = planes[i].getRowStride(); |
| if (VERBOSE) { |
| Log.v(TAG, "rowStride " + rowStride); |
| Log.v(TAG, "width " + width); |
| Log.v(TAG, "height " + height); |
| } |
| int h = (i == 0) ? height : height / 2; |
| for (int row = 0; row < h; row++) { |
| // Each 10-bit pixel occupies 2 bytes |
| int length = 2 * width; |
| buffer.get(data, offset, length); |
| offset += length; |
| if (row < h - 1) { |
| buffer.position(buffer.position() + rowStride - length); |
| } |
| } |
| if (VERBOSE) Log.v(TAG, "Finished reading data from plane " + i); |
| buffer.rewind(); |
| } |
| return data; |
| } |
| |
| int offset = 0; |
| data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8]; |
| int maxRowSize = planes[0].getRowStride(); |
| for (int i = 0; i < planes.length; i++) { |
| if (maxRowSize < planes[i].getRowStride()) { |
| maxRowSize = planes[i].getRowStride(); |
| } |
| } |
| byte[] rowData = new byte[maxRowSize]; |
| if(VERBOSE) Log.v(TAG, "get data from " + planes.length + " planes"); |
| for (int i = 0; i < planes.length; i++) { |
| buffer = planes[i].getBuffer(); |
| assertNotNull("Fail to get bytebuffer from plane", buffer); |
| buffer.rewind(); |
| rowStride = planes[i].getRowStride(); |
| pixelStride = planes[i].getPixelStride(); |
| assertTrue("pixel stride " + pixelStride + " is invalid", pixelStride > 0); |
| if (VERBOSE) { |
| Log.v(TAG, "pixelStride " + pixelStride); |
| Log.v(TAG, "rowStride " + rowStride); |
| Log.v(TAG, "width " + width); |
| Log.v(TAG, "height " + height); |
| } |
| // For multi-planar yuv images, assuming yuv420 with 2x2 chroma subsampling. |
| int w = (i == 0) ? width : width / 2; |
| int h = (i == 0) ? height : height / 2; |
| assertTrue("rowStride " + rowStride + " should be >= width " + w , rowStride >= w); |
| for (int row = 0; row < h; row++) { |
| int bytesPerPixel = ImageFormat.getBitsPerPixel(format) / 8; |
| int length; |
| if (pixelStride == bytesPerPixel) { |
| // Special case: optimized read of the entire row |
| length = w * bytesPerPixel; |
| buffer.get(data, offset, length); |
| offset += length; |
| } else { |
| // Generic case: should work for any pixelStride but slower. |
| // Use intermediate buffer to avoid read byte-by-byte from |
| // DirectByteBuffer, which is very bad for performance |
| length = (w - 1) * pixelStride + bytesPerPixel; |
| buffer.get(rowData, 0, length); |
| for (int col = 0; col < w; col++) { |
| data[offset++] = rowData[col * pixelStride]; |
| } |
| } |
| // Advance buffer the remainder of the row stride |
| if (row < h - 1) { |
| buffer.position(buffer.position() + rowStride - length); |
| } |
| } |
| if (VERBOSE) Log.v(TAG, "Finished reading data from plane " + i); |
| buffer.rewind(); |
| } |
| return data; |
| } |
| |
| /** |
| * <p>Check android image format validity for an image, only support below formats:</p> |
| * |
| * <p>YUV_420_888/NV21/YV12, can add more for future</p> |
| */ |
| public static void checkAndroidImageFormat(Image image) { |
| int format = image.getFormat(); |
| Plane[] planes = image.getPlanes(); |
| switch (format) { |
| case ImageFormat.YUV_420_888: |
| case ImageFormat.NV21: |
| case ImageFormat.YV12: |
| case ImageFormat.YCBCR_P010: |
| assertEquals("YUV420 format Images should have 3 planes", 3, planes.length); |
| break; |
| case ImageFormat.JPEG: |
| case ImageFormat.RAW_SENSOR: |
| case ImageFormat.RAW_PRIVATE: |
| case ImageFormat.DEPTH16: |
| case ImageFormat.DEPTH_POINT_CLOUD: |
| case ImageFormat.DEPTH_JPEG: |
| case ImageFormat.Y8: |
| case ImageFormat.HEIC: |
| assertEquals("JPEG/RAW/depth/Y8 Images should have one plane", 1, planes.length); |
| break; |
| default: |
| fail("Unsupported Image Format: " + format); |
| } |
| } |
| |
| public static void dumpFile(String fileName, Bitmap data) { |
| FileOutputStream outStream; |
| try { |
| Log.v(TAG, "output will be saved as " + fileName); |
| outStream = new FileOutputStream(fileName); |
| } catch (IOException ioe) { |
| throw new RuntimeException("Unable to create debug output file " + fileName, ioe); |
| } |
| |
| try { |
| data.compress(Bitmap.CompressFormat.JPEG, /*quality*/90, outStream); |
| outStream.close(); |
| } catch (IOException ioe) { |
| throw new RuntimeException("failed writing data to file " + fileName, ioe); |
| } |
| } |
| |
| public static void dumpFile(String fileName, byte[] data) { |
| FileOutputStream outStream; |
| try { |
| Log.v(TAG, "output will be saved as " + fileName); |
| outStream = new FileOutputStream(fileName); |
| } catch (IOException ioe) { |
| throw new RuntimeException("Unable to create debug output file " + fileName, ioe); |
| } |
| |
| try { |
| outStream.write(data); |
| outStream.close(); |
| } catch (IOException ioe) { |
| throw new RuntimeException("failed writing data to file " + fileName, ioe); |
| } |
| } |
| |
| /** |
| * Get the available output sizes for the user-defined {@code format}. |
| * |
| * <p>Note that implementation-defined/hidden formats are not supported.</p> |
| */ |
| public static Size[] getSupportedSizeForFormat(int format, String cameraId, |
| CameraManager cameraManager) throws CameraAccessException { |
| return getSupportedSizeForFormat(format, cameraId, cameraManager, |
| /*maxResolution*/false); |
| } |
| |
| public static Size[] getSupportedSizeForFormat(int format, String cameraId, |
| CameraManager cameraManager, boolean maxResolution) throws CameraAccessException { |
| CameraCharacteristics properties = cameraManager.getCameraCharacteristics(cameraId); |
| assertNotNull("Can't get camera characteristics!", properties); |
| if (VERBOSE) { |
| Log.v(TAG, "get camera characteristics for camera: " + cameraId); |
| } |
| CameraCharacteristics.Key<StreamConfigurationMap> configMapTag = maxResolution ? |
| CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION : |
| CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP; |
| StreamConfigurationMap configMap = properties.get(configMapTag); |
| if (configMap == null) { |
| assertTrue("SCALER_STREAM_CONFIGURATION_MAP is null!", maxResolution); |
| return null; |
| } |
| |
| Size[] availableSizes = configMap.getOutputSizes(format); |
| if (!maxResolution) { |
| assertArrayNotEmpty(availableSizes, "availableSizes should not be empty for format: " |
| + format); |
| } |
| Size[] highResAvailableSizes = configMap.getHighResolutionOutputSizes(format); |
| if (highResAvailableSizes != null && highResAvailableSizes.length > 0) { |
| Size[] allSizes = new Size[availableSizes.length + highResAvailableSizes.length]; |
| System.arraycopy(availableSizes, 0, allSizes, 0, |
| availableSizes.length); |
| System.arraycopy(highResAvailableSizes, 0, allSizes, availableSizes.length, |
| highResAvailableSizes.length); |
| availableSizes = allSizes; |
| } |
| if (VERBOSE) Log.v(TAG, "Supported sizes are: " + Arrays.deepToString(availableSizes)); |
| return availableSizes; |
| } |
| |
| /** |
| * Get the available output sizes for the given class. |
| * |
| */ |
| public static Size[] getSupportedSizeForClass(Class klass, String cameraId, |
| CameraManager cameraManager) throws CameraAccessException { |
| CameraCharacteristics properties = cameraManager.getCameraCharacteristics(cameraId); |
| assertNotNull("Can't get camera characteristics!", properties); |
| if (VERBOSE) { |
| Log.v(TAG, "get camera characteristics for camera: " + cameraId); |
| } |
| StreamConfigurationMap configMap = |
| properties.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); |
| Size[] availableSizes = configMap.getOutputSizes(klass); |
| assertArrayNotEmpty(availableSizes, "availableSizes should not be empty for class: " |
| + klass); |
| Size[] highResAvailableSizes = configMap.getHighResolutionOutputSizes(ImageFormat.PRIVATE); |
| if (highResAvailableSizes != null && highResAvailableSizes.length > 0) { |
| Size[] allSizes = new Size[availableSizes.length + highResAvailableSizes.length]; |
| System.arraycopy(availableSizes, 0, allSizes, 0, |
| availableSizes.length); |
| System.arraycopy(highResAvailableSizes, 0, allSizes, availableSizes.length, |
| highResAvailableSizes.length); |
| availableSizes = allSizes; |
| } |
| if (VERBOSE) Log.v(TAG, "Supported sizes are: " + Arrays.deepToString(availableSizes)); |
| return availableSizes; |
| } |
| |
| /** |
| * Size comparator that compares the number of pixels it covers. |
| * |
| * <p>If two the areas of two sizes are same, compare the widths.</p> |
| */ |
| public static class SizeComparator implements Comparator<Size> { |
| @Override |
| public int compare(Size lhs, Size rhs) { |
| return CameraUtils |
| .compareSizes(lhs.getWidth(), lhs.getHeight(), rhs.getWidth(), rhs.getHeight()); |
| } |
| } |
| |
| /** |
| * Get sorted size list in descending order. Remove the sizes larger than |
| * the bound. If the bound is null, don't do the size bound filtering. |
| */ |
| static public List<Size> getSupportedPreviewSizes(String cameraId, |
| CameraManager cameraManager, Size bound) throws CameraAccessException { |
| |
| Size[] rawSizes = getSupportedSizeForClass(android.view.SurfaceHolder.class, cameraId, |
| cameraManager); |
| assertArrayNotEmpty(rawSizes, |
| "Available sizes for SurfaceHolder class should not be empty"); |
| if (VERBOSE) { |
| Log.v(TAG, "Supported sizes are: " + Arrays.deepToString(rawSizes)); |
| } |
| |
| if (bound == null) { |
| return getAscendingOrderSizes(Arrays.asList(rawSizes), /*ascending*/false); |
| } |
| |
| List<Size> sizes = new ArrayList<Size>(); |
| for (Size sz: rawSizes) { |
| if (sz.getWidth() <= bound.getWidth() && sz.getHeight() <= bound.getHeight()) { |
| sizes.add(sz); |
| } |
| } |
| return getAscendingOrderSizes(sizes, /*ascending*/false); |
| } |
| |
| /** |
| * Get a sorted list of sizes from a given size list. |
| * |
| * <p> |
| * The size is compare by area it covers, if the areas are same, then |
| * compare the widths. |
| * </p> |
| * |
| * @param sizeList The input size list to be sorted |
| * @param ascending True if the order is ascending, otherwise descending order |
| * @return The ordered list of sizes |
| */ |
| static public List<Size> getAscendingOrderSizes(final List<Size> sizeList, boolean ascending) { |
| if (sizeList == null) { |
| throw new IllegalArgumentException("sizeList shouldn't be null"); |
| } |
| |
| Comparator<Size> comparator = new SizeComparator(); |
| List<Size> sortedSizes = new ArrayList<Size>(); |
| sortedSizes.addAll(sizeList); |
| Collections.sort(sortedSizes, comparator); |
| if (!ascending) { |
| Collections.reverse(sortedSizes); |
| } |
| |
| return sortedSizes; |
| } |
| /** |
| * Get sorted (descending order) size list for given format. Remove the sizes larger than |
| * the bound. If the bound is null, don't do the size bound filtering. |
| */ |
| static public List<Size> getSortedSizesForFormat(String cameraId, |
| CameraManager cameraManager, int format, Size bound) throws CameraAccessException { |
| return getSortedSizesForFormat(cameraId, cameraManager, format, /*maxResolution*/false, |
| bound); |
| } |
| |
| /** |
| * Get sorted (descending order) size list for given format (with an option to get sizes from |
| * the maximum resolution stream configuration map). Remove the sizes larger than |
| * the bound. If the bound is null, don't do the size bound filtering. |
| */ |
| static public List<Size> getSortedSizesForFormat(String cameraId, |
| CameraManager cameraManager, int format, boolean maxResolution, Size bound) |
| throws CameraAccessException { |
| Comparator<Size> comparator = new SizeComparator(); |
| Size[] sizes = getSupportedSizeForFormat(format, cameraId, cameraManager, maxResolution); |
| List<Size> sortedSizes = null; |
| if (bound != null) { |
| sortedSizes = new ArrayList<Size>(/*capacity*/1); |
| for (Size sz : sizes) { |
| if (comparator.compare(sz, bound) <= 0) { |
| sortedSizes.add(sz); |
| } |
| } |
| } else { |
| sortedSizes = Arrays.asList(sizes); |
| } |
| assertTrue("Supported size list should have at least one element", |
| sortedSizes.size() > 0); |
| |
| Collections.sort(sortedSizes, comparator); |
| // Make it in descending order. |
| Collections.reverse(sortedSizes); |
| return sortedSizes; |
| } |
| |
| /** |
| * Get supported video size list for a given camera device. |
| * |
| * <p> |
| * Filter out the sizes that are larger than the bound. If the bound is |
| * null, don't do the size bound filtering. |
| * </p> |
| */ |
| static public List<Size> getSupportedVideoSizes(String cameraId, |
| CameraManager cameraManager, Size bound) throws CameraAccessException { |
| |
| Size[] rawSizes = getSupportedSizeForClass(android.media.MediaRecorder.class, |
| cameraId, cameraManager); |
| assertArrayNotEmpty(rawSizes, |
| "Available sizes for MediaRecorder class should not be empty"); |
| if (VERBOSE) { |
| Log.v(TAG, "Supported sizes are: " + Arrays.deepToString(rawSizes)); |
| } |
| |
| if (bound == null) { |
| return getAscendingOrderSizes(Arrays.asList(rawSizes), /*ascending*/false); |
| } |
| |
| List<Size> sizes = new ArrayList<Size>(); |
| for (Size sz: rawSizes) { |
| if (sz.getWidth() <= bound.getWidth() && sz.getHeight() <= bound.getHeight()) { |
| sizes.add(sz); |
| } |
| } |
| return getAscendingOrderSizes(sizes, /*ascending*/false); |
| } |
| |
| /** |
| * Get supported video size list (descending order) for a given camera device. |
| * |
| * <p> |
| * Filter out the sizes that are larger than the bound. If the bound is |
| * null, don't do the size bound filtering. |
| * </p> |
| */ |
| static public List<Size> getSupportedStillSizes(String cameraId, |
| CameraManager cameraManager, Size bound) throws CameraAccessException { |
| return getSortedSizesForFormat(cameraId, cameraManager, ImageFormat.JPEG, bound); |
| } |
| |
| static public List<Size> getSupportedHeicSizes(String cameraId, |
| CameraManager cameraManager, Size bound) throws CameraAccessException { |
| return getSortedSizesForFormat(cameraId, cameraManager, ImageFormat.HEIC, bound); |
| } |
| |
| static public Size getMinPreviewSize(String cameraId, CameraManager cameraManager) |
| throws CameraAccessException { |
| List<Size> sizes = getSupportedPreviewSizes(cameraId, cameraManager, null); |
| return sizes.get(sizes.size() - 1); |
| } |
| |
| /** |
| * Get max supported preview size for a camera device. |
| */ |
| static public Size getMaxPreviewSize(String cameraId, CameraManager cameraManager) |
| throws CameraAccessException { |
| return getMaxPreviewSize(cameraId, cameraManager, /*bound*/null); |
| } |
| |
| /** |
| * Get max preview size for a camera device in the supported sizes that are no larger |
| * than the bound. |
| */ |
| static public Size getMaxPreviewSize(String cameraId, CameraManager cameraManager, Size bound) |
| throws CameraAccessException { |
| List<Size> sizes = getSupportedPreviewSizes(cameraId, cameraManager, bound); |
| return sizes.get(0); |
| } |
| |
| /** |
| * Get max depth size for a camera device. |
| */ |
| static public Size getMaxDepthSize(String cameraId, CameraManager cameraManager) |
| throws CameraAccessException { |
| List<Size> sizes = getSortedSizesForFormat(cameraId, cameraManager, ImageFormat.DEPTH16, |
| /*bound*/ null); |
| return sizes.get(0); |
| } |
| |
| /** |
| * Return the lower size |
| * @param a first size |
| * |
| * @param b second size |
| * |
| * @return Size the smaller size |
| * |
| * @throws IllegalArgumentException if either param was null. |
| * |
| */ |
| @NonNull public static Size getMinSize(Size a, Size b) { |
| if (a == null || b == null) { |
| throw new IllegalArgumentException("sizes was empty"); |
| } |
| if (a.getWidth() * a.getHeight() < b.getHeight() * b.getWidth()) { |
| return a; |
| } |
| return b; |
| } |
| |
| /** |
| * Get the largest size by area. |
| * |
| * @param sizes an array of sizes, must have at least 1 element |
| * |
| * @return Largest Size |
| * |
| * @throws IllegalArgumentException if sizes was null or had 0 elements |
| */ |
| public static Size getMaxSize(Size... sizes) { |
| if (sizes == null || sizes.length == 0) { |
| throw new IllegalArgumentException("sizes was empty"); |
| } |
| |
| Size sz = sizes[0]; |
| for (Size size : sizes) { |
| if (size.getWidth() * size.getHeight() > sz.getWidth() * sz.getHeight()) { |
| sz = size; |
| } |
| } |
| |
| return sz; |
| } |
| |
| /** |
| * Get the largest size by area within (less than) bound |
| * |
| * @param sizes an array of sizes, must have at least 1 element |
| * |
| * @return Largest Size. Null if no such size exists within bound. |
| * |
| * @throws IllegalArgumentException if sizes was null or had 0 elements, or bound is invalid. |
| */ |
| public static Size getMaxSizeWithBound(Size[] sizes, int bound) { |
| if (sizes == null || sizes.length == 0) { |
| throw new IllegalArgumentException("sizes was empty"); |
| } |
| if (bound <= 0) { |
| throw new IllegalArgumentException("bound is invalid"); |
| } |
| |
| Size sz = null; |
| for (Size size : sizes) { |
| if (size.getWidth() * size.getHeight() >= bound) { |
| continue; |
| } |
| |
| if (sz == null || |
| size.getWidth() * size.getHeight() > sz.getWidth() * sz.getHeight()) { |
| sz = size; |
| } |
| } |
| |
| return sz; |
| } |
| |
| /** |
| * Returns true if the given {@code array} contains the given element. |
| * |
| * @param array {@code array} to check for {@code elem} |
| * @param elem {@code elem} to test for |
| * @return {@code true} if the given element is contained |
| */ |
| public static boolean contains(int[] array, int elem) { |
| if (array == null) return false; |
| for (int i = 0; i < array.length; i++) { |
| if (elem == array[i]) return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Get object array from byte array. |
| * |
| * @param array Input byte array to be converted |
| * @return Byte object array converted from input byte array |
| */ |
| public static Byte[] toObject(byte[] array) { |
| return convertPrimitiveArrayToObjectArray(array, Byte.class); |
| } |
| |
| /** |
| * Get object array from int array. |
| * |
| * @param array Input int array to be converted |
| * @return Integer object array converted from input int array |
| */ |
| public static Integer[] toObject(int[] array) { |
| return convertPrimitiveArrayToObjectArray(array, Integer.class); |
| } |
| |
| /** |
| * Get object array from float array. |
| * |
| * @param array Input float array to be converted |
| * @return Float object array converted from input float array |
| */ |
| public static Float[] toObject(float[] array) { |
| return convertPrimitiveArrayToObjectArray(array, Float.class); |
| } |
| |
| /** |
| * Get object array from double array. |
| * |
| * @param array Input double array to be converted |
| * @return Double object array converted from input double array |
| */ |
| public static Double[] toObject(double[] array) { |
| return convertPrimitiveArrayToObjectArray(array, Double.class); |
| } |
| |
| /** |
| * Convert a primitive input array into its object array version (e.g. from int[] to Integer[]). |
| * |
| * @param array Input array object |
| * @param wrapperClass The boxed class it converts to |
| * @return Boxed version of primitive array |
| */ |
| private static <T> T[] convertPrimitiveArrayToObjectArray(final Object array, |
| final Class<T> wrapperClass) { |
| // getLength does the null check and isArray check already. |
| int arrayLength = Array.getLength(array); |
| if (arrayLength == 0) { |
| throw new IllegalArgumentException("Input array shouldn't be empty"); |
| } |
| |
| @SuppressWarnings("unchecked") |
| final T[] result = (T[]) Array.newInstance(wrapperClass, arrayLength); |
| for (int i = 0; i < arrayLength; i++) { |
| Array.set(result, i, Array.get(array, i)); |
| } |
| return result; |
| } |
| |
| /** |
| * Update one 3A region in capture request builder if that region is supported. Do nothing |
| * if the specified 3A region is not supported by camera device. |
| * @param requestBuilder The request to be updated |
| * @param algoIdx The index to the algorithm. (AE: 0, AWB: 1, AF: 2) |
| * @param regions The 3A regions to be set |
| * @param staticInfo static metadata characteristics |
| */ |
| public static void update3aRegion( |
| CaptureRequest.Builder requestBuilder, int algoIdx, MeteringRectangle[] regions, |
| StaticMetadata staticInfo) |
| { |
| int maxRegions; |
| CaptureRequest.Key<MeteringRectangle[]> key; |
| |
| if (regions == null || regions.length == 0 || staticInfo == null) { |
| throw new IllegalArgumentException("Invalid input 3A region!"); |
| } |
| |
| switch (algoIdx) { |
| case INDEX_ALGORITHM_AE: |
| maxRegions = staticInfo.getAeMaxRegionsChecked(); |
| key = CaptureRequest.CONTROL_AE_REGIONS; |
| break; |
| case INDEX_ALGORITHM_AWB: |
| maxRegions = staticInfo.getAwbMaxRegionsChecked(); |
| key = CaptureRequest.CONTROL_AWB_REGIONS; |
| break; |
| case INDEX_ALGORITHM_AF: |
| maxRegions = staticInfo.getAfMaxRegionsChecked(); |
| key = CaptureRequest.CONTROL_AF_REGIONS; |
| break; |
| default: |
| throw new IllegalArgumentException("Unknown 3A Algorithm!"); |
| } |
| |
| if (maxRegions >= regions.length) { |
| requestBuilder.set(key, regions); |
| } |
| } |
| |
| /** |
| * Validate one 3A region in capture result equals to expected region if that region is |
| * supported. Do nothing if the specified 3A region is not supported by camera device. |
| * @param result The capture result to be validated |
| * @param partialResults The partial results to be validated |
| * @param algoIdx The index to the algorithm. (AE: 0, AWB: 1, AF: 2) |
| * @param expectRegions The 3A regions expected in capture result |
| * @param scaleByZoomRatio whether to scale the error threshold by zoom ratio |
| * @param staticInfo static metadata characteristics |
| */ |
| public static void validate3aRegion( |
| CaptureResult result, List<CaptureResult> partialResults, int algoIdx, |
| MeteringRectangle[] expectRegions, boolean scaleByZoomRatio, StaticMetadata staticInfo) |
| { |
| // There are multiple cases where result 3A region could be slightly different than the |
| // request: |
| // 1. Distortion correction, |
| // 2. Adding smaller 3a region in the test exposes existing devices' offset is larger |
| // than 1. |
| // 3. Precision loss due to converting to HAL zoom ratio and back |
| // 4. Error magnification due to active array scale-up when zoom ratio API is used. |
| // |
| // To handle all these scenarios, make the threshold larger, and scale the threshold based |
| // on zoom ratio. The scaling factor should be relatively tight, and shouldn't be smaller |
| // than 1x. |
| final int maxCoordOffset = 5; |
| int maxRegions; |
| CaptureResult.Key<MeteringRectangle[]> key; |
| MeteringRectangle[] actualRegion; |
| |
| switch (algoIdx) { |
| case INDEX_ALGORITHM_AE: |
| maxRegions = staticInfo.getAeMaxRegionsChecked(); |
| key = CaptureResult.CONTROL_AE_REGIONS; |
| break; |
| case INDEX_ALGORITHM_AWB: |
| maxRegions = staticInfo.getAwbMaxRegionsChecked(); |
| key = CaptureResult.CONTROL_AWB_REGIONS; |
| break; |
| case INDEX_ALGORITHM_AF: |
| maxRegions = staticInfo.getAfMaxRegionsChecked(); |
| key = CaptureResult.CONTROL_AF_REGIONS; |
| break; |
| default: |
| throw new IllegalArgumentException("Unknown 3A Algorithm!"); |
| } |
| |
| int maxDist = maxCoordOffset; |
| if (scaleByZoomRatio) { |
| Float zoomRatio = result.get(CaptureResult.CONTROL_ZOOM_RATIO); |
| for (CaptureResult partialResult : partialResults) { |
| Float zoomRatioInPartial = partialResult.get(CaptureResult.CONTROL_ZOOM_RATIO); |
| if (zoomRatioInPartial != null) { |
| assertEquals("CONTROL_ZOOM_RATIO in partial result must match" |
| + " that in final result", zoomRatio, zoomRatioInPartial); |
| } |
| } |
| maxDist = (int)Math.ceil(maxDist * Math.max(zoomRatio / 2, 1.0f)); |
| } |
| |
| if (maxRegions > 0) |
| { |
| actualRegion = getValueNotNull(result, key); |
| for (CaptureResult partialResult : partialResults) { |
| MeteringRectangle[] actualRegionInPartial = partialResult.get(key); |
| if (actualRegionInPartial != null) { |
| assertEquals("Key " + key.getName() + " in partial result must match" |
| + " that in final result", actualRegionInPartial, actualRegion); |
| } |
| } |
| |
| for (int i = 0; i < actualRegion.length; i++) { |
| // If the expected region's metering weight is 0, allow the camera device |
| // to override it. |
| if (expectRegions[i].getMeteringWeight() == 0) { |
| continue; |
| } |
| |
| Rect a = actualRegion[i].getRect(); |
| Rect e = expectRegions[i].getRect(); |
| |
| if (VERBOSE) { |
| Log.v(TAG, "Actual region " + actualRegion[i].toString() + |
| ", expected region " + expectRegions[i].toString() + |
| ", maxDist " + maxDist); |
| } |
| assertTrue( |
| "Expected 3A regions: " + Arrays.toString(expectRegions) + |
| " are not close enough to the actual one: " + Arrays.toString(actualRegion), |
| maxDist >= Math.abs(a.left - e.left)); |
| |
| assertTrue( |
| "Expected 3A regions: " + Arrays.toString(expectRegions) + |
| " are not close enough to the actual one: " + Arrays.toString(actualRegion), |
| maxDist >= Math.abs(a.right - e.right)); |
| |
| assertTrue( |
| "Expected 3A regions: " + Arrays.toString(expectRegions) + |
| " are not close enough to the actual one: " + Arrays.toString(actualRegion), |
| maxDist >= Math.abs(a.top - e.top)); |
| assertTrue( |
| "Expected 3A regions: " + Arrays.toString(expectRegions) + |
| " are not close enough to the actual one: " + Arrays.toString(actualRegion), |
| maxDist >= Math.abs(a.bottom - e.bottom)); |
| } |
| } |
| } |
| |
| |
| /** |
| * Validate image based on format and size. |
| * |
| * @param image The image to be validated. |
| * @param width The image width. |
| * @param height The image height. |
| * @param format The image format. |
| * @param filePath The debug dump file path, null if don't want to dump to |
| * file. |
| * @throws UnsupportedOperationException if calling with an unknown format |
| */ |
| public static void validateImage(Image image, int width, int height, int format, |
| String filePath) { |
| checkImage(image, width, height, format); |
| |
| /** |
| * TODO: validate timestamp: |
| * 1. capture result timestamp against the image timestamp (need |
| * consider frame drops) |
| * 2. timestamps should be monotonically increasing for different requests |
| */ |
| if(VERBOSE) Log.v(TAG, "validating Image"); |
| byte[] data = getDataFromImage(image); |
| assertTrue("Invalid image data", data != null && data.length > 0); |
| |
| switch (format) { |
| // Clients must be able to process and handle depth jpeg images like any other |
| // regular jpeg. |
| case ImageFormat.DEPTH_JPEG: |
| case ImageFormat.JPEG: |
| validateJpegData(data, width, height, filePath); |
| break; |
| case ImageFormat.YCBCR_P010: |
| validateP010Data(data, width, height, format, image.getTimestamp(), filePath); |
| break; |
| case ImageFormat.YUV_420_888: |
| case ImageFormat.YV12: |
| validateYuvData(data, width, height, format, image.getTimestamp(), filePath); |
| break; |
| case ImageFormat.RAW_SENSOR: |
| validateRaw16Data(data, width, height, format, image.getTimestamp(), filePath); |
| break; |
| case ImageFormat.DEPTH16: |
| validateDepth16Data(data, width, height, format, image.getTimestamp(), filePath); |
| break; |
| case ImageFormat.DEPTH_POINT_CLOUD: |
| validateDepthPointCloudData(data, width, height, format, image.getTimestamp(), filePath); |
| break; |
| case ImageFormat.RAW_PRIVATE: |
| validateRawPrivateData(data, width, height, image.getTimestamp(), filePath); |
| break; |
| case ImageFormat.Y8: |
| validateY8Data(data, width, height, format, image.getTimestamp(), filePath); |
| break; |
| case ImageFormat.HEIC: |
| validateHeicData(data, width, height, filePath); |
| break; |
| default: |
| throw new UnsupportedOperationException("Unsupported format for validation: " |
| + format); |
| } |
| } |
| |
| public static class HandlerExecutor implements Executor { |
| private final Handler mHandler; |
| |
| public HandlerExecutor(Handler handler) { |
| assertNotNull("handler must be valid", handler); |
| mHandler = handler; |
| } |
| |
| @Override |
| public void execute(Runnable runCmd) { |
| mHandler.post(runCmd); |
| } |
| } |
| |
| /** |
| * Provide a mock for {@link CameraDevice.StateCallback}. |
| * |
| * <p>Only useful because mockito can't mock {@link CameraDevice.StateCallback} which is an |
| * abstract class.</p> |
| * |
| * <p> |
| * Use this instead of other classes when needing to verify interactions, since |
| * trying to spy on {@link BlockingStateCallback} (or others) will cause unnecessary extra |
| * interactions which will cause false test failures. |
| * </p> |
| * |
| */ |
| public static class MockStateCallback extends CameraDevice.StateCallback { |
| |
| @Override |
| public void onOpened(CameraDevice camera) { |
| } |
| |
| @Override |
| public void onDisconnected(CameraDevice camera) { |
| } |
| |
| @Override |
| public void onError(CameraDevice camera, int error) { |
| } |
| |
| private MockStateCallback() {} |
| |
| /** |
| * Create a Mockito-ready mocked StateCallback. |
| */ |
| public static MockStateCallback mock() { |
| return Mockito.spy(new MockStateCallback()); |
| } |
| } |
| |
| public static void validateJpegData(byte[] jpegData, int width, int height, String filePath) { |
| BitmapFactory.Options bmpOptions = new BitmapFactory.Options(); |
| // DecodeBound mode: only parse the frame header to get width/height. |
| // it doesn't decode the pixel. |
| bmpOptions.inJustDecodeBounds = true; |
| BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length, bmpOptions); |
| assertEquals(width, bmpOptions.outWidth); |
| assertEquals(height, bmpOptions.outHeight); |
| |
| // Pixel decoding mode: decode whole image. check if the image data |
| // is decodable here. |
| assertNotNull("Decoding jpeg failed", |
| BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length)); |
| if (DEBUG && filePath != null) { |
| String fileName = |
| filePath + "/" + width + "x" + height + ".jpeg"; |
| dumpFile(fileName, jpegData); |
| } |
| } |
| |
| private static void validateYuvData(byte[] yuvData, int width, int height, int format, |
| long ts, String filePath) { |
| checkYuvFormat(format); |
| if (VERBOSE) Log.v(TAG, "Validating YUV data"); |
| int expectedSize = width * height * ImageFormat.getBitsPerPixel(format) / 8; |
| assertEquals("Yuv data doesn't match", expectedSize, yuvData.length); |
| |
| // TODO: Can add data validation for test pattern. |
| |
| if (DEBUG && filePath != null) { |
| String fileName = |
| filePath + "/" + width + "x" + height + "_" + ts / 1e6 + ".yuv"; |
| dumpFile(fileName, yuvData); |
| } |
| } |
| |
| private static void validateP010Data(byte[] p010Data, int width, int height, int format, |
| long ts, String filePath) { |
| if (VERBOSE) Log.v(TAG, "Validating P010 data"); |
| // The P010 10 bit samples are stored in two bytes so the size needs to be adjusted |
| // accordingly. |
| int bytesPerPixelRounded = (ImageFormat.getBitsPerPixel(format) + 7) / 8; |
| int expectedSize = width * height * bytesPerPixelRounded; |
| assertEquals("P010 data doesn't match", expectedSize, p010Data.length); |
| |
| if (DEBUG && filePath != null) { |
| String fileName = |
| filePath + "/" + width + "x" + height + "_" + ts / 1e6 + ".p010"; |
| dumpFile(fileName, p010Data); |
| } |
| } |
| private static void validateRaw16Data(byte[] rawData, int width, int height, int format, |
| long ts, String filePath) { |
| if (VERBOSE) Log.v(TAG, "Validating raw data"); |
| int expectedSize = width * height * ImageFormat.getBitsPerPixel(format) / 8; |
| assertEquals("Raw data doesn't match", expectedSize, rawData.length); |
| |
| // TODO: Can add data validation for test pattern. |
| |
| if (DEBUG && filePath != null) { |
| String fileName = |
| filePath + "/" + width + "x" + height + "_" + ts / 1e6 + ".raw16"; |
| dumpFile(fileName, rawData); |
| } |
| |
| return; |
| } |
| |
| private static void validateY8Data(byte[] rawData, int width, int height, int format, |
| long ts, String filePath) { |
| if (VERBOSE) Log.v(TAG, "Validating Y8 data"); |
| int expectedSize = width * height * ImageFormat.getBitsPerPixel(format) / 8; |
| assertEquals("Y8 data doesn't match", expectedSize, rawData.length); |
| |
| // TODO: Can add data validation for test pattern. |
| |
| if (DEBUG && filePath != null) { |
| String fileName = |
| filePath + "/" + width + "x" + height + "_" + ts / 1e6 + ".y8"; |
| dumpFile(fileName, rawData); |
| } |
| |
| return; |
| } |
| |
| private static void validateRawPrivateData(byte[] rawData, int width, int height, |
| long ts, String filePath) { |
| if (VERBOSE) Log.v(TAG, "Validating private raw data"); |
| // Expect each RAW pixel should occupy at least one byte and no more than 30 bytes |
| int expectedSizeMin = width * height; |
| int expectedSizeMax = width * height * 30; |
| |
| assertTrue("Opaque RAW size " + rawData.length + "out of normal bound [" + |
| expectedSizeMin + "," + expectedSizeMax + "]", |
| expectedSizeMin <= rawData.length && rawData.length <= expectedSizeMax); |
| |
| if (DEBUG && filePath != null) { |
| String fileName = |
| filePath + "/" + width + "x" + height + "_" + ts / 1e6 + ".rawPriv"; |
| dumpFile(fileName, rawData); |
| } |
| |
| return; |
| } |
| |
| private static void validateDepth16Data(byte[] depthData, int width, int height, int format, |
| long ts, String filePath) { |
| |
| if (VERBOSE) Log.v(TAG, "Validating depth16 data"); |
| int expectedSize = width * height * ImageFormat.getBitsPerPixel(format) / 8; |
| assertEquals("Depth data doesn't match", expectedSize, depthData.length); |
| |
| |
| if (DEBUG && filePath != null) { |
| String fileName = |
| filePath + "/" + width + "x" + height + "_" + ts / 1e6 + ".depth16"; |
| dumpFile(fileName, depthData); |
| } |
| |
| return; |
| |
| } |
| |
| private static void validateDepthPointCloudData(byte[] depthData, int width, int height, int format, |
| long ts, String filePath) { |
| |
| if (VERBOSE) Log.v(TAG, "Validating depth point cloud data"); |
| |
| // Can't validate size since it is variable |
| |
| if (DEBUG && filePath != null) { |
| String fileName = |
| filePath + "/" + width + "x" + height + "_" + ts / 1e6 + ".depth_point_cloud"; |
| dumpFile(fileName, depthData); |
| } |
| |
| return; |
| |
| } |
| |
| private static void validateHeicData(byte[] heicData, int width, int height, String filePath) { |
| BitmapFactory.Options bmpOptions = new BitmapFactory.Options(); |
| // DecodeBound mode: only parse the frame header to get width/height. |
| // it doesn't decode the pixel. |
| bmpOptions.inJustDecodeBounds = true; |
| BitmapFactory.decodeByteArray(heicData, 0, heicData.length, bmpOptions); |
| assertEquals(width, bmpOptions.outWidth); |
| assertEquals(height, bmpOptions.outHeight); |
| |
| // Pixel decoding mode: decode whole image. check if the image data |
| // is decodable here. |
| assertNotNull("Decoding heic failed", |
| BitmapFactory.decodeByteArray(heicData, 0, heicData.length)); |
| if (DEBUG && filePath != null) { |
| String fileName = |
| filePath + "/" + width + "x" + height + ".heic"; |
| dumpFile(fileName, heicData); |
| } |
| } |
| |
| public static <T> T getValueNotNull(CaptureResult result, CaptureResult.Key<T> key) { |
| if (result == null) { |
| throw new IllegalArgumentException("Result must not be null"); |
| } |
| |
| T value = result.get(key); |
| assertNotNull("Value of Key " + key.getName() + "shouldn't be null", value); |
| return value; |
| } |
| |
| public static <T> T getValueNotNull(CameraCharacteristics characteristics, |
| CameraCharacteristics.Key<T> key) { |
| if (characteristics == null) { |
| throw new IllegalArgumentException("Camera characteristics must not be null"); |
| } |
| |
| T value = characteristics.get(key); |
| assertNotNull("Value of Key " + key.getName() + "shouldn't be null", value); |
| return value; |
| } |
| |
| /** |
| * Get a crop region for a given zoom factor and center position. |
| * <p> |
| * The center position is normalized position in range of [0, 1.0], where |
| * (0, 0) represents top left corner, (1.0. 1.0) represents bottom right |
| * corner. The center position could limit the effective minimal zoom |
| * factor, for example, if the center position is (0.75, 0.75), the |
| * effective minimal zoom position becomes 2.0. If the requested zoom factor |
| * is smaller than 2.0, a crop region with 2.0 zoom factor will be returned. |
| * </p> |
| * <p> |
| * The aspect ratio of the crop region is maintained the same as the aspect |
| * ratio of active array. |
| * </p> |
| * |
| * @param zoomFactor The zoom factor to generate the crop region, it must be |
| * >= 1.0 |
| * @param center The normalized zoom center point that is in the range of [0, 1]. |
| * @param maxZoom The max zoom factor supported by this device. |
| * @param activeArray The active array size of this device. |
| * @return crop region for the given normalized center and zoom factor. |
| */ |
| public static Rect getCropRegionForZoom(float zoomFactor, final PointF center, |
| final float maxZoom, final Rect activeArray) { |
| if (zoomFactor < 1.0) { |
| throw new IllegalArgumentException("zoom factor " + zoomFactor + " should be >= 1.0"); |
| } |
| if (center.x > 1.0 || center.x < 0) { |
| throw new IllegalArgumentException("center.x " + center.x |
| + " should be in range of [0, 1.0]"); |
| } |
| if (center.y > 1.0 || center.y < 0) { |
| throw new IllegalArgumentException("center.y " + center.y |
| + " should be in range of [0, 1.0]"); |
| } |
| if (maxZoom < 1.0) { |
| throw new IllegalArgumentException("max zoom factor " + maxZoom + " should be >= 1.0"); |
| } |
| if (activeArray == null) { |
| throw new IllegalArgumentException("activeArray must not be null"); |
| } |
| |
| float minCenterLength = Math.min(Math.min(center.x, 1.0f - center.x), |
| Math.min(center.y, 1.0f - center.y)); |
| float minEffectiveZoom = 0.5f / minCenterLength; |
| if (minEffectiveZoom > maxZoom) { |
| throw new IllegalArgumentException("Requested center " + center.toString() + |
| " has minimal zoomable factor " + minEffectiveZoom + ", which exceeds max" |
| + " zoom factor " + maxZoom); |
| } |
| |
| if (zoomFactor < minEffectiveZoom) { |
| Log.w(TAG, "Requested zoomFactor " + zoomFactor + " < minimal zoomable factor " |
| + minEffectiveZoom + ". It will be overwritten by " + minEffectiveZoom); |
| zoomFactor = minEffectiveZoom; |
| } |
| |
| int cropCenterX = (int)(activeArray.width() * center.x); |
| int cropCenterY = (int)(activeArray.height() * center.y); |
| int cropWidth = (int) (activeArray.width() / zoomFactor); |
| int cropHeight = (int) (activeArray.height() / zoomFactor); |
| |
| return new Rect( |
| /*left*/cropCenterX - cropWidth / 2, |
| /*top*/cropCenterY - cropHeight / 2, |
| /*right*/ cropCenterX + cropWidth / 2, |
| /*bottom*/cropCenterY + cropHeight / 2); |
| } |
| |
| /** |
| * Get AeAvailableTargetFpsRanges and sort them in descending order by max fps |
| * |
| * @param staticInfo camera static metadata |
| * @return AeAvailableTargetFpsRanges in descending order by max fps |
| */ |
| public static Range<Integer>[] getDescendingTargetFpsRanges(StaticMetadata staticInfo) { |
| Range<Integer>[] fpsRanges = staticInfo.getAeAvailableTargetFpsRangesChecked(); |
| Arrays.sort(fpsRanges, new Comparator<Range<Integer>>() { |
| public int compare(Range<Integer> r1, Range<Integer> r2) { |
| return r2.getUpper() - r1.getUpper(); |
| } |
| }); |
| return fpsRanges; |
| } |
| |
| /** |
| * Get AeAvailableTargetFpsRanges with max fps not exceeding 30 |
| * |
| * @param staticInfo camera static metadata |
| * @return AeAvailableTargetFpsRanges with max fps not exceeding 30 |
| */ |
| public static List<Range<Integer>> getTargetFpsRangesUpTo30(StaticMetadata staticInfo) { |
| Range<Integer>[] fpsRanges = staticInfo.getAeAvailableTargetFpsRangesChecked(); |
| ArrayList<Range<Integer>> fpsRangesUpTo30 = new ArrayList<Range<Integer>>(); |
| for (Range<Integer> fpsRange : fpsRanges) { |
| if (fpsRange.getUpper() <= 30) { |
| fpsRangesUpTo30.add(fpsRange); |
| } |
| } |
| return fpsRangesUpTo30; |
| } |
| |
| /** |
| * Get AeAvailableTargetFpsRanges with max fps greater than 30 |
| * |
| * @param staticInfo camera static metadata |
| * @return AeAvailableTargetFpsRanges with max fps greater than 30 |
| */ |
| public static List<Range<Integer>> getTargetFpsRangesGreaterThan30(StaticMetadata staticInfo) { |
| Range<Integer>[] fpsRanges = staticInfo.getAeAvailableTargetFpsRangesChecked(); |
| ArrayList<Range<Integer>> fpsRangesGreaterThan30 = new ArrayList<Range<Integer>>(); |
| for (Range<Integer> fpsRange : fpsRanges) { |
| if (fpsRange.getUpper() > 30) { |
| fpsRangesGreaterThan30.add(fpsRange); |
| } |
| } |
| return fpsRangesGreaterThan30; |
| } |
| |
| /** |
| * Calculate output 3A region from the intersection of input 3A region and cropped region. |
| * |
| * @param requestRegions The input 3A regions |
| * @param cropRect The cropped region |
| * @return expected 3A regions output in capture result |
| */ |
| public static MeteringRectangle[] getExpectedOutputRegion( |
| MeteringRectangle[] requestRegions, Rect cropRect){ |
| MeteringRectangle[] resultRegions = new MeteringRectangle[requestRegions.length]; |
| for (int i = 0; i < requestRegions.length; i++) { |
| Rect requestRect = requestRegions[i].getRect(); |
| Rect resultRect = new Rect(); |
| boolean intersect = resultRect.setIntersect(requestRect, cropRect); |
| resultRegions[i] = new MeteringRectangle( |
| resultRect, |
| intersect ? requestRegions[i].getMeteringWeight() : 0); |
| } |
| return resultRegions; |
| } |
| |
| /** |
| * Copy source image data to destination image. |
| * |
| * @param src The source image to be copied from. |
| * @param dst The destination image to be copied to. |
| * @throws IllegalArgumentException If the source and destination images have |
| * different format, size, or one of the images is not copyable. |
| */ |
| public static void imageCopy(Image src, Image dst) { |
| if (src == null || dst == null) { |
| throw new IllegalArgumentException("Images should be non-null"); |
| } |
| if (src.getFormat() != dst.getFormat()) { |
| throw new IllegalArgumentException("Src and dst images should have the same format"); |
| } |
| if (src.getFormat() == ImageFormat.PRIVATE || |
| dst.getFormat() == ImageFormat.PRIVATE) { |
| throw new IllegalArgumentException("PRIVATE format images are not copyable"); |
| } |
| |
| Size srcSize = new Size(src.getWidth(), src.getHeight()); |
| Size dstSize = new Size(dst.getWidth(), dst.getHeight()); |
| if (!srcSize.equals(dstSize)) { |
| throw new IllegalArgumentException("source image size " + srcSize + " is different" |
| + " with " + "destination image size " + dstSize); |
| } |
| |
| // TODO: check the owner of the dst image, it must be from ImageWriter, other source may |
| // not be writable. Maybe we should add an isWritable() method in image class. |
| |
| Plane[] srcPlanes = src.getPlanes(); |
| Plane[] dstPlanes = dst.getPlanes(); |
| ByteBuffer srcBuffer = null; |
| ByteBuffer dstBuffer = null; |
| for (int i = 0; i < srcPlanes.length; i++) { |
| srcBuffer = srcPlanes[i].getBuffer(); |
| dstBuffer = dstPlanes[i].getBuffer(); |
| int srcPos = srcBuffer.position(); |
| srcBuffer.rewind(); |
| dstBuffer.rewind(); |
| int srcRowStride = srcPlanes[i].getRowStride(); |
| int dstRowStride = dstPlanes[i].getRowStride(); |
| int srcPixStride = srcPlanes[i].getPixelStride(); |
| int dstPixStride = dstPlanes[i].getPixelStride(); |
| |
| if (srcPixStride > 2 || dstPixStride > 2) { |
| throw new IllegalArgumentException("source pixel stride " + srcPixStride + |
| " with destination pixel stride " + dstPixStride + |
| " is not supported"); |
| } |
| |
| if (srcRowStride == dstRowStride && srcPixStride == dstPixStride && |
| srcPixStride == 1) { |
| // Fast path, just copy the content in the byteBuffer all together. |
| dstBuffer.put(srcBuffer); |
| } else { |
| Size effectivePlaneSize = getEffectivePlaneSizeForImage(src, i); |
| int srcRowByteCount = srcRowStride; |
| int dstRowByteCount = dstRowStride; |
| byte[] srcDataRow = new byte[Math.max(srcRowStride, dstRowStride)]; |
| |
| if (srcPixStride == dstPixStride && srcPixStride == 1) { |
| // Row by row copy case |
| for (int row = 0; row < effectivePlaneSize.getHeight(); row++) { |
| if (row == effectivePlaneSize.getHeight() - 1) { |
| // Special case for interleaved planes: need handle the last row |
| // carefully to avoid memory corruption. Check if we have enough bytes |
| // to copy. |
| srcRowByteCount = Math.min(srcRowByteCount, srcBuffer.remaining()); |
| dstRowByteCount = Math.min(dstRowByteCount, dstBuffer.remaining()); |
| } |
| srcBuffer.get(srcDataRow, /*offset*/0, srcRowByteCount); |
| dstBuffer.put(srcDataRow, /*offset*/0, dstRowByteCount); |
| } |
| } else { |
| // Row by row per pixel copy case |
| byte[] dstDataRow = new byte[dstRowByteCount]; |
| for (int row = 0; row < effectivePlaneSize.getHeight(); row++) { |
| if (row == effectivePlaneSize.getHeight() - 1) { |
| // Special case for interleaved planes: need handle the last row |
| // carefully to avoid memory corruption. Check if we have enough bytes |
| // to copy. |
| int remainingBytes = srcBuffer.remaining(); |
| if (srcRowByteCount > remainingBytes) { |
| srcRowByteCount = remainingBytes; |
| } |
| remainingBytes = dstBuffer.remaining(); |
| if (dstRowByteCount > remainingBytes) { |
| dstRowByteCount = remainingBytes; |
| } |
| } |
| srcBuffer.get(srcDataRow, /*offset*/0, srcRowByteCount); |
| int pos = dstBuffer.position(); |
| dstBuffer.get(dstDataRow, /*offset*/0, dstRowByteCount); |
| dstBuffer.position(pos); |
| for (int x = 0; x < effectivePlaneSize.getWidth(); x++) { |
| dstDataRow[x * dstPixStride] = srcDataRow[x * srcPixStride]; |
| } |
| dstBuffer.put(dstDataRow, /*offset*/0, dstRowByteCount); |
| } |
| } |
| } |
| srcBuffer.position(srcPos); |
| dstBuffer.rewind(); |
| } |
| } |
| |
| private static Size getEffectivePlaneSizeForImage(Image image, int planeIdx) { |
| switch (image.getFormat()) { |
| case ImageFormat.YUV_420_888: |
| if (planeIdx == 0) { |
| return new Size(image.getWidth(), image.getHeight()); |
| } else { |
| return new Size(image.getWidth() / 2, image.getHeight() / 2); |
| } |
| case ImageFormat.JPEG: |
| case ImageFormat.RAW_SENSOR: |
| case ImageFormat.RAW10: |
| case ImageFormat.RAW12: |
| case ImageFormat.DEPTH16: |
| return new Size(image.getWidth(), image.getHeight()); |
| case ImageFormat.PRIVATE: |
| return new Size(0, 0); |
| default: |
| throw new UnsupportedOperationException( |
| String.format("Invalid image format %d", image.getFormat())); |
| } |
| } |
| |
| /** |
| * <p> |
| * Checks whether the two images are strongly equal. |
| * </p> |
| * <p> |
| * Two images are strongly equal if and only if the data, formats, sizes, |
| * and timestamps are same. For {@link ImageFormat#PRIVATE PRIVATE} format |
| * images, the image data is not not accessible thus the data comparison is |
| * effectively skipped as the number of planes is zero. |
| * </p> |
| * <p> |
| * Note that this method compares the pixel data even outside of the crop |
| * region, which may not be necessary for general use case. |
| * </p> |
| * |
| * @param lhsImg First image to be compared with. |
| * @param rhsImg Second image to be compared with. |
| * @return true if the two images are equal, false otherwise. |
| * @throws IllegalArgumentException If either of image is null. |
| */ |
| public static boolean isImageStronglyEqual(Image lhsImg, Image rhsImg) { |
| if (lhsImg == null || rhsImg == null) { |
| throw new IllegalArgumentException("Images should be non-null"); |
| } |
| |
| if (lhsImg.getFormat() != rhsImg.getFormat()) { |
| Log.i(TAG, "lhsImg format " + lhsImg.getFormat() + " is different with rhsImg format " |
| + rhsImg.getFormat()); |
| return false; |
| } |
| |
| if (lhsImg.getWidth() != rhsImg.getWidth()) { |
| Log.i(TAG, "lhsImg width " + lhsImg.getWidth() + " is different with rhsImg width " |
| + rhsImg.getWidth()); |
| return false; |
| } |
| |
| if (lhsImg.getHeight() != rhsImg.getHeight()) { |
| Log.i(TAG, "lhsImg height " + lhsImg.getHeight() + " is different with rhsImg height " |
| + rhsImg.getHeight()); |
| return false; |
| } |
| |
| if (lhsImg.getTimestamp() != rhsImg.getTimestamp()) { |
| Log.i(TAG, "lhsImg timestamp " + lhsImg.getTimestamp() |
| + " is different with rhsImg timestamp " + rhsImg.getTimestamp()); |
| return false; |
| } |
| |
| if (!lhsImg.getCropRect().equals(rhsImg.getCropRect())) { |
| Log.i(TAG, "lhsImg crop rect " + lhsImg.getCropRect() |
| + " is different with rhsImg crop rect " + rhsImg.getCropRect()); |
| return false; |
| } |
| |
| // Compare data inside of the image. |
| Plane[] lhsPlanes = lhsImg.getPlanes(); |
| Plane[] rhsPlanes = rhsImg.getPlanes(); |
| ByteBuffer lhsBuffer = null; |
| ByteBuffer rhsBuffer = null; |
| for (int i = 0; i < lhsPlanes.length; i++) { |
| lhsBuffer = lhsPlanes[i].getBuffer(); |
| rhsBuffer = rhsPlanes[i].getBuffer(); |
| lhsBuffer.rewind(); |
| rhsBuffer.rewind(); |
| // Special case for YUV420_888 buffer with different layout or |
| // potentially differently interleaved U/V planes. |
| if (lhsImg.getFormat() == ImageFormat.YUV_420_888 && |
| (lhsPlanes[i].getPixelStride() != rhsPlanes[i].getPixelStride() || |
| lhsPlanes[i].getRowStride() != rhsPlanes[i].getRowStride() || |
| (lhsPlanes[i].getPixelStride() != 1))) { |
| int width = getEffectivePlaneSizeForImage(lhsImg, i).getWidth(); |
| int height = getEffectivePlaneSizeForImage(lhsImg, i).getHeight(); |
| int rowSizeL = lhsPlanes[i].getRowStride(); |
| int rowSizeR = rhsPlanes[i].getRowStride(); |
| byte[] lhsRow = new byte[rowSizeL]; |
| byte[] rhsRow = new byte[rowSizeR]; |
| int pixStrideL = lhsPlanes[i].getPixelStride(); |
| int pixStrideR = rhsPlanes[i].getPixelStride(); |
| for (int r = 0; r < height; r++) { |
| if (r == height -1) { |
| rowSizeL = lhsBuffer.remaining(); |
| rowSizeR = rhsBuffer.remaining(); |
| } |
| lhsBuffer.get(lhsRow, /*offset*/0, rowSizeL); |
| rhsBuffer.get(rhsRow, /*offset*/0, rowSizeR); |
| for (int c = 0; c < width; c++) { |
| if (lhsRow[c * pixStrideL] != rhsRow[c * pixStrideR]) { |
| Log.i(TAG, String.format( |
| "byte buffers for plane %d row %d col %d don't match.", |
| i, r, c)); |
| return false; |
| } |
| } |
| } |
| } else { |
| // Compare entire buffer directly |
| if (!lhsBuffer.equals(rhsBuffer)) { |
| Log.i(TAG, "byte buffers for plane " + i + " don't match."); |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Set jpeg related keys in a capture request builder. |
| * |
| * @param builder The capture request builder to set the keys inl |
| * @param exifData The exif data to set. |
| * @param thumbnailSize The thumbnail size to set. |
| * @param collector The camera error collector to collect errors. |
| */ |
| public static void setJpegKeys(CaptureRequest.Builder builder, ExifTestData exifData, |
| Size thumbnailSize, CameraErrorCollector collector) { |
| builder.set(CaptureRequest.JPEG_THUMBNAIL_SIZE, thumbnailSize); |
| builder.set(CaptureRequest.JPEG_GPS_LOCATION, exifData.gpsLocation); |
| builder.set(CaptureRequest.JPEG_ORIENTATION, exifData.jpegOrientation); |
| builder.set(CaptureRequest.JPEG_QUALITY, exifData.jpegQuality); |
| builder.set(CaptureRequest.JPEG_THUMBNAIL_QUALITY, |
| exifData.thumbnailQuality); |
| |
| // Validate request set and get. |
| collector.expectEquals("JPEG thumbnail size request set and get should match", |
| thumbnailSize, builder.get(CaptureRequest.JPEG_THUMBNAIL_SIZE)); |
| collector.expectTrue("GPS locations request set and get should match.", |
| areGpsFieldsEqual(exifData.gpsLocation, |
| builder.get(CaptureRequest.JPEG_GPS_LOCATION))); |
| collector.expectEquals("JPEG orientation request set and get should match", |
| exifData.jpegOrientation, |
| builder.get(CaptureRequest.JPEG_ORIENTATION)); |
| collector.expectEquals("JPEG quality request set and get should match", |
| exifData.jpegQuality, builder.get(CaptureRequest.JPEG_QUALITY)); |
| collector.expectEquals("JPEG thumbnail quality request set and get should match", |
| exifData.thumbnailQuality, |
| builder.get(CaptureRequest.JPEG_THUMBNAIL_QUALITY)); |
| } |
| |
| /** |
| * Simple validation of JPEG image size and format. |
| * <p> |
| * Only validate the image object basic correctness. It is fast, but doesn't actually |
| * check the buffer data. Assert is used here as it make no sense to |
| * continue the test if the jpeg image captured has some serious failures. |
| * </p> |
| * |
| * @param image The captured JPEG/HEIC image |
| * @param expectedSize Expected capture JEPG/HEIC size |
| * @param format JPEG/HEIC image format |
| */ |
| public static void basicValidateBlobImage(Image image, Size expectedSize, int format) { |
| Size imageSz = new Size(image.getWidth(), image.getHeight()); |
| assertTrue( |
| String.format("Image size doesn't match (expected %s, actual %s) ", |
| expectedSize.toString(), imageSz.toString()), expectedSize.equals(imageSz)); |
| assertEquals("Image format should be " + ((format == ImageFormat.HEIC) ? "HEIC" : "JPEG"), |
| format, image.getFormat()); |
| assertNotNull("Image plane shouldn't be null", image.getPlanes()); |
| assertEquals("Image plane number should be 1", 1, image.getPlanes().length); |
| |
| // Jpeg/Heic decoding validate was done in ImageReaderTest, |
| // no need to duplicate the test here. |
| } |
| |
| /** |
| * Verify the EXIF and JPEG related keys in a capture result are expected. |
| * - Capture request get values are same as were set. |
| * - capture result's exif data is the same as was set by |
| * the capture request. |
| * - new tags in the result set by the camera service are |
| * present and semantically correct. |
| * |
| * @param image The output JPEG/HEIC image to verify. |
| * @param captureResult The capture result to verify. |
| * @param expectedSize The expected JPEG/HEIC size. |
| * @param expectedThumbnailSize The expected thumbnail size. |
| * @param expectedExifData The expected EXIF data |
| * @param staticInfo The static metadata for the camera device. |
| * @param blobFilename The filename to dump the jpeg/heic to. |
| * @param collector The camera error collector to collect errors. |
| * @param format JPEG/HEIC format |
| */ |
| public static void verifyJpegKeys(Image image, CaptureResult captureResult, Size expectedSize, |
| Size expectedThumbnailSize, ExifTestData expectedExifData, StaticMetadata staticInfo, |
| CameraErrorCollector collector, String debugFileNameBase, int format) throws Exception { |
| |
| basicValidateBlobImage(image, expectedSize, format); |
| |
| byte[] blobBuffer = getDataFromImage(image); |
| // Have to dump into a file to be able to use ExifInterface |
| String filePostfix = (format == ImageFormat.HEIC ? ".heic" : ".jpeg"); |
| String blobFilename = debugFileNameBase + "/verifyJpegKeys" + filePostfix; |
| dumpFile(blobFilename, blobBuffer); |
| ExifInterface exif = new ExifInterface(blobFilename); |
| |
| if (expectedThumbnailSize.equals(new Size(0,0))) { |
| collector.expectTrue("Jpeg shouldn't have thumbnail when thumbnail size is (0, 0)", |
| !exif.hasThumbnail()); |
| } else { |
| collector.expectTrue("Jpeg must have thumbnail for thumbnail size " + |
| expectedThumbnailSize, exif.hasThumbnail()); |
| } |
| |
| // Validate capture result vs. request |
| Size resultThumbnailSize = captureResult.get(CaptureResult.JPEG_THUMBNAIL_SIZE); |
| int orientationTested = expectedExifData.jpegOrientation; |
| // Legacy shim always doesn't rotate thumbnail size |
| if ((orientationTested == 90 || orientationTested == 270) && |
| staticInfo.isHardwareLevelAtLeastLimited()) { |
| int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, |
| /*defaultValue*/-1); |
| if (exifOrientation == ExifInterface.ORIENTATION_UNDEFINED) { |
| // Device physically rotated image+thumbnail data |
| // Expect thumbnail size to be also rotated |
| resultThumbnailSize = new Size(resultThumbnailSize.getHeight(), |
| resultThumbnailSize.getWidth()); |
| } |
| } |
| |
| collector.expectEquals("JPEG thumbnail size result and request should match", |
| expectedThumbnailSize, resultThumbnailSize); |
| if (collector.expectKeyValueNotNull(captureResult, CaptureResult.JPEG_GPS_LOCATION) != |
| null) { |
| collector.expectTrue("GPS location result and request should match.", |
| areGpsFieldsEqual(expectedExifData.gpsLocation, |
| captureResult.get(CaptureResult.JPEG_GPS_LOCATION))); |
| } |
| collector.expectEquals("JPEG orientation result and request should match", |
| expectedExifData.jpegOrientation, |
| captureResult.get(CaptureResult.JPEG_ORIENTATION)); |
| collector.expectEquals("JPEG quality result and request should match", |
| expectedExifData.jpegQuality, captureResult.get(CaptureResult.JPEG_QUALITY)); |
| collector.expectEquals("JPEG thumbnail quality result and request should match", |
| expectedExifData.thumbnailQuality, |
| captureResult.get(CaptureResult.JPEG_THUMBNAIL_QUALITY)); |
| |
| // Validate other exif tags for all non-legacy devices |
| if (!staticInfo.isHardwareLevelLegacy()) { |
| verifyJpegExifExtraTags(exif, expectedSize, captureResult, staticInfo, collector, |
| expectedExifData); |
| } |
| } |
| |
| /** |
| * Get the degree of an EXIF orientation. |
| */ |
| private static int getExifOrientationInDegree(int exifOrientation, |
| CameraErrorCollector collector) { |
| switch (exifOrientation) { |
| case ExifInterface.ORIENTATION_NORMAL: |
| return 0; |
| case ExifInterface.ORIENTATION_ROTATE_90: |
| return 90; |
| case ExifInterface.ORIENTATION_ROTATE_180: |
| return 180; |
| case ExifInterface.ORIENTATION_ROTATE_270: |
| return 270; |
| default: |
| collector.addMessage("It is impossible to get non 0, 90, 180, 270 degress exif" + |
| "info based on the request orientation range"); |
| return 0; |
| } |
| } |
| |
| /** |
| * Validate and return the focal length. |
| * |
| * @param result Capture result to get the focal length |
| * @return Focal length from capture result or -1 if focal length is not available. |
| */ |
| private static float validateFocalLength(CaptureResult result, StaticMetadata staticInfo, |
| CameraErrorCollector collector) { |
| float[] focalLengths = staticInfo.getAvailableFocalLengthsChecked(); |
| Float resultFocalLength = result.get(CaptureResult.LENS_FOCAL_LENGTH); |
| if (collector.expectTrue("Focal length is invalid", |
| resultFocalLength != null && resultFocalLength > 0)) { |
| List<Float> focalLengthList = |
| Arrays.asList(CameraTestUtils.toObject(focalLengths)); |
| collector.expectTrue("Focal length should be one of the available focal length", |
| focalLengthList.contains(resultFocalLength)); |
| return resultFocalLength; |
| } |
| return -1; |
| } |
| |
| /** |
| * Validate and return the aperture. |
| * |
| * @param result Capture result to get the aperture |
| * @return Aperture from capture result or -1 if aperture is not available. |
| */ |
| private static float validateAperture(CaptureResult result, StaticMetadata staticInfo, |
| CameraErrorCollector collector) { |
| float[] apertures = staticInfo.getAvailableAperturesChecked(); |
| Float resultAperture = result.get(CaptureResult.LENS_APERTURE); |
| if (collector.expectTrue("Capture result aperture is invalid", |
| resultAperture != null && resultAperture > 0)) { |
| List<Float> apertureList = |
| Arrays.asList(CameraTestUtils.toObject(apertures)); |
| collector.expectTrue("Aperture should be one of the available apertures", |
| apertureList.contains(resultAperture)); |
| return resultAperture; |
| } |
| return -1; |
| } |
| |
| /** |
| * Return the closest value in an array of floats. |
| */ |
| private static float getClosestValueInArray(float[] values, float target) { |
| int minIdx = 0; |
| float minDistance = Math.abs(values[0] - target); |
| for(int i = 0; i < values.length; i++) { |
| float distance = Math.abs(values[i] - target); |
| if (minDistance > distance) { |
| minDistance = distance; |
| minIdx = i; |
| } |
| } |
| |
| return values[minIdx]; |
| } |
| |
| /** |
| * Return if two Location's GPS field are the same. |
| */ |
| private static boolean areGpsFieldsEqual(Location a, Location b) { |
| if (a == null || b == null) { |
| return false; |
| } |
| |
| return a.getTime() == b.getTime() && a.getLatitude() == b.getLatitude() && |
| a.getLongitude() == b.getLongitude() && a.getAltitude() == b.getAltitude() && |
| a.getProvider() == b.getProvider(); |
| } |
| |
| /** |
| * Verify extra tags in JPEG EXIF |
| */ |
| private static void verifyJpegExifExtraTags(ExifInterface exif, Size jpegSize, |
| CaptureResult result, StaticMetadata staticInfo, CameraErrorCollector collector, |
| ExifTestData expectedExifData) |
| throws ParseException { |
| /** |
| * TAG_IMAGE_WIDTH and TAG_IMAGE_LENGTH and TAG_ORIENTATION. |
| * Orientation and exif width/height need to be tested carefully, two cases: |
| * |
| * 1. Device rotate the image buffer physically, then exif width/height may not match |
| * the requested still capture size, we need swap them to check. |
| * |
| * 2. Device use the exif tag to record the image orientation, it doesn't rotate |
| * the jpeg image buffer itself. In this case, the exif width/height should always match |
| * the requested still capture size, and the exif orientation should always match the |
| * requested orientation. |
| * |
| */ |
| int exifWidth = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, /*defaultValue*/0); |
| int exifHeight = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, /*defaultValue*/0); |
| Size exifSize = new Size(exifWidth, exifHeight); |
| // Orientation could be missing, which is ok, default to 0. |
| int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, |
| /*defaultValue*/-1); |
| // Get requested orientation from result, because they should be same. |
| if (collector.expectKeyValueNotNull(result, CaptureResult.JPEG_ORIENTATION) != null) { |
| int requestedOrientation = result.get(CaptureResult.JPEG_ORIENTATION); |
| final int ORIENTATION_MIN = ExifInterface.ORIENTATION_UNDEFINED; |
| final int ORIENTATION_MAX = ExifInterface.ORIENTATION_ROTATE_270; |
| boolean orientationValid = collector.expectTrue(String.format( |
| "Exif orientation must be in range of [%d, %d]", |
| ORIENTATION_MIN, ORIENTATION_MAX), |
| exifOrientation >= ORIENTATION_MIN && exifOrientation <= ORIENTATION_MAX); |
| if (orientationValid) { |
| /** |
| * Device captured image doesn't respect the requested orientation, |
| * which means it rotates the image buffer physically. Then we |
| * should swap the exif width/height accordingly to compare. |
| */ |
| boolean deviceRotatedImage = exifOrientation == ExifInterface.ORIENTATION_UNDEFINED; |
| |
| if (deviceRotatedImage) { |
| // Case 1. |
| boolean needSwap = (requestedOrientation % 180 == 90); |
| if (needSwap) { |
| exifSize = new Size(exifHeight, exifWidth); |
| } |
| } else { |
| // Case 2. |
| collector.expectEquals("Exif orientaiton should match requested orientation", |
| requestedOrientation, getExifOrientationInDegree(exifOrientation, |
| collector)); |
| } |
| } |
| } |
| |
| /** |
| * Ideally, need check exifSize == jpegSize == actual buffer size. But |
| * jpegSize == jpeg decode bounds size(from jpeg jpeg frame |
| * header, not exif) was validated in ImageReaderTest, no need to |
| * validate again here. |
| */ |
| collector.expectEquals("Exif size should match jpeg capture size", jpegSize, exifSize); |
| |
| // TAG_DATETIME, it should be local time |
| long currentTimeInMs = System.currentTimeMillis(); |
| long currentTimeInSecond = currentTimeInMs / 1000; |
| Date date = new Date(currentTimeInMs); |
| String localDatetime = new SimpleDateFormat("yyyy:MM:dd HH:").format(date); |
| String dateTime = exif.getAttribute(ExifInterface.TAG_DATETIME); |
| if (collector.expectTrue("Exif TAG_DATETIME shouldn't be null", dateTime != null)) { |
| collector.expectTrue("Exif TAG_DATETIME is wrong", |
| dateTime.length() == EXIF_DATETIME_LENGTH); |
| long exifTimeInSecond = |
| new SimpleDateFormat("yyyy:MM:dd HH:mm:ss").parse(dateTime).getTime() / 1000; |
| long delta = currentTimeInSecond - exifTimeInSecond; |
| collector.expectTrue("Capture time deviates too much from the current time", |
| Math.abs(delta) < EXIF_DATETIME_ERROR_MARGIN_SEC); |
| // It should be local time. |
| collector.expectTrue("Exif date time should be local time", |
| dateTime.startsWith(localDatetime)); |
| } |
| |
| boolean isExternalCamera = staticInfo.isExternalCamera(); |
| if (!isExternalCamera) { |
| // TAG_FOCAL_LENGTH. |
| float[] focalLengths = staticInfo.getAvailableFocalLengthsChecked(); |
| float exifFocalLength = (float)exif.getAttributeDouble( |
| ExifInterface.TAG_FOCAL_LENGTH, -1); |
| collector.expectEquals("Focal length should match", |
| getClosestValueInArray(focalLengths, exifFocalLength), |
| exifFocalLength, EXIF_FOCAL_LENGTH_ERROR_MARGIN); |
| // More checks for focal length. |
| collector.expectEquals("Exif focal length should match capture result", |
| validateFocalLength(result, staticInfo, collector), |
| exifFocalLength, EXIF_FOCAL_LENGTH_ERROR_MARGIN); |
| |
| // TAG_EXPOSURE_TIME |
| // ExifInterface API gives exposure time value in the form of float instead of rational |
| String exposureTime = exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME); |
| collector.expectNotNull("Exif TAG_EXPOSURE_TIME shouldn't be null", exposureTime); |
| if (staticInfo.areKeysAvailable(CaptureResult.SENSOR_EXPOSURE_TIME)) { |
| if (exposureTime != null) { |
| double exposureTimeValue = Double.parseDouble(exposureTime); |
| long expTimeResult = result.get(CaptureResult.SENSOR_EXPOSURE_TIME); |
| double expected = expTimeResult / 1e9; |
| double tolerance = expected * EXIF_EXPOSURE_TIME_ERROR_MARGIN_RATIO; |
| tolerance = Math.max(tolerance, EXIF_EXPOSURE_TIME_MIN_ERROR_MARGIN_SEC); |
| collector.expectEquals("Exif exposure time doesn't match", expected, |
| exposureTimeValue, tolerance); |
| } |
| } |
| |
| // TAG_APERTURE |
| // ExifInterface API gives aperture value in the form of float instead of rational |
| String exifAperture = exif.getAttribute(ExifInterface.TAG_APERTURE); |
| collector.expectNotNull("Exif TAG_APERTURE shouldn't be null", exifAperture); |
| if (staticInfo.areKeysAvailable(CameraCharacteristics.LENS_INFO_AVAILABLE_APERTURES)) { |
| float[] apertures = staticInfo.getAvailableAperturesChecked(); |
| if (exifAperture != null) { |
| float apertureValue = Float.parseFloat(exifAperture); |
| collector.expectEquals("Aperture value should match", |
| getClosestValueInArray(apertures, apertureValue), |
| apertureValue, EXIF_APERTURE_ERROR_MARGIN); |
| // More checks for aperture. |
| collector.expectEquals("Exif aperture length should match capture result", |
| validateAperture(result, staticInfo, collector), |
| apertureValue, EXIF_APERTURE_ERROR_MARGIN); |
| } |
| } |
| |
| // TAG_MAKE |
| String make = exif.getAttribute(ExifInterface.TAG_MAKE); |
| collector.expectEquals("Exif TAG_MAKE is incorrect", Build.MANUFACTURER, make); |
| |
| // TAG_MODEL |
| String model = exif.getAttribute(ExifInterface.TAG_MODEL); |
| collector.expectEquals("Exif TAG_MODEL is incorrect", Build.MODEL, model); |
| |
| |
| // TAG_ISO |
| int iso = exif.getAttributeInt(ExifInterface.TAG_ISO, /*defaultValue*/-1); |
| if (staticInfo.areKeysAvailable(CaptureResult.SENSOR_SENSITIVITY) || |
| staticInfo.areKeysAvailable(CaptureResult.CONTROL_POST_RAW_SENSITIVITY_BOOST)) { |
| int expectedIso = 100; |
| if (staticInfo.areKeysAvailable(CaptureResult.SENSOR_SENSITIVITY)) { |
| expectedIso = result.get(CaptureResult.SENSOR_SENSITIVITY); |
| } |
| if (staticInfo.areKeysAvailable(CaptureResult.CONTROL_POST_RAW_SENSITIVITY_BOOST)) { |
| expectedIso = expectedIso * |
| result.get(CaptureResult.CONTROL_POST_RAW_SENSITIVITY_BOOST); |
| } else { |
| expectedIso *= 100; |
| } |
| collector.expectInRange("Exif TAG_ISO is incorrect", iso, |
| expectedIso/100, (expectedIso+50)/100); |
| } |
| } else { |
| // External camera specific checks |
| // TAG_MAKE |
| String make = exif.getAttribute(ExifInterface.TAG_MAKE); |
| collector.expectNotNull("Exif TAG_MAKE is null", make); |
| |
| // TAG_MODEL |
| String model = exif.getAttribute(ExifInterface.TAG_MODEL); |
| collector.expectNotNull("Exif TAG_MODEL is nuill", model); |
| } |
| |
| |
| /** |
| * TAG_FLASH. TODO: For full devices, can check a lot more info |
| * (http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/EXIF.html#Flash) |
| */ |
| String flash = exif.getAttribute(ExifInterface.TAG_FLASH); |
| collector.expectNotNull("Exif TAG_FLASH shouldn't be null", flash); |
| |
| /** |
| * TAG_WHITE_BALANCE. TODO: For full devices, with the DNG tags, we |
| * should be able to cross-check android.sensor.referenceIlluminant. |
| */ |
| String whiteBalance = exif.getAttribute(ExifInterface.TAG_WHITE_BALANCE); |
| collector.expectNotNull("Exif TAG_WHITE_BALANCE shouldn't be null", whiteBalance); |
| |
| // TAG_DATETIME_DIGITIZED (a.k.a Create time for digital cameras). |
| String digitizedTime = exif.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED); |
| collector.expectNotNull("Exif TAG_DATETIME_DIGITIZED shouldn't be null", digitizedTime); |
| if (digitizedTime != null) { |
| String expectedDateTime = exif.getAttribute(ExifInterface.TAG_DATETIME); |
| collector.expectNotNull("Exif TAG_DATETIME shouldn't be null", expectedDateTime); |
| if (expectedDateTime != null) { |
| collector.expectEquals("dataTime should match digitizedTime", |
| expectedDateTime, digitizedTime); |
| } |
| } |
| |
| /** |
| * TAG_SUBSEC_TIME. Since the sub second tag strings are truncated to at |
| * most 9 digits in ExifInterface implementation, use getAttributeInt to |
| * sanitize it. When the default value -1 is returned, it means that |
| * this exif tag either doesn't exist or is a non-numerical invalid |
| * string. Same rule applies to the rest of sub second tags. |
| */ |
| int subSecTime = exif.getAttributeInt(ExifInterface.TAG_SUBSEC_TIME, /*defaultValue*/-1); |
| collector.expectTrue("Exif TAG_SUBSEC_TIME value is null or invalid!", subSecTime >= 0); |
| |
| // TAG_SUBSEC_TIME_ORIG |
| int subSecTimeOrig = exif.getAttributeInt(ExifInterface.TAG_SUBSEC_TIME_ORIG, |
| /*defaultValue*/-1); |
| collector.expectTrue("Exif TAG_SUBSEC_TIME_ORIG value is null or invalid!", |
| subSecTimeOrig >= 0); |
| |
| // TAG_SUBSEC_TIME_DIG |
| int subSecTimeDig = exif.getAttributeInt(ExifInterface.TAG_SUBSEC_TIME_DIG, |
| /*defaultValue*/-1); |
| collector.expectTrue( |
| "Exif TAG_SUBSEC_TIME_DIG value is null or invalid!", subSecTimeDig >= 0); |
| |
| /** |
| * TAG_GPS_DATESTAMP & TAG_GPS_TIMESTAMP. |
| * The GPS timestamp information should be in seconds UTC time. |
| */ |
| String gpsDatestamp = exif.getAttribute(ExifInterface.TAG_GPS_DATESTAMP); |
| collector.expectNotNull("Exif TAG_GPS_DATESTAMP shouldn't be null", gpsDatestamp); |
| String gpsTimestamp = exif.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP); |
| collector.expectNotNull("Exif TAG_GPS_TIMESTAMP shouldn't be null", gpsTimestamp); |
| |
| SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy:MM:dd hh:mm:ss z"); |
| String gpsExifTimeString = gpsDatestamp + " " + gpsTimestamp + " UTC"; |
| Date gpsDateTime = dateFormat.parse(gpsExifTimeString); |
| Date expected = new Date(expectedExifData.gpsLocation.getTime()); |
| collector.expectEquals("Jpeg EXIF GPS time should match", expected, gpsDateTime); |
| } |
| |
| |
| /** |
| * Immutable class wrapping the exif test data. |
| */ |
| public static class ExifTestData { |
| public final Location gpsLocation; |
| public final int jpegOrientation; |
| public final byte jpegQuality; |
| public final byte thumbnailQuality; |
| |
| public ExifTestData(Location location, int orientation, |
| byte jpgQuality, byte thumbQuality) { |
| gpsLocation = location; |
| jpegOrientation = orientation; |
| jpegQuality = jpgQuality; |
| thumbnailQuality = thumbQuality; |
| } |
| } |
| |
| public static Size getPreviewSizeBound(WindowManager windowManager, Size bound) { |
| Display display = windowManager.getDefaultDisplay(); |
| |
| int width = display.getWidth(); |
| int height = display.getHeight(); |
| |
| if (height > width) { |
| height = width; |
| width = display.getHeight(); |
| } |
| |
| if (bound.getWidth() <= width && |
| bound.getHeight() <= height) |
| return bound; |
| else |
| return new Size(width, height); |
| } |
| |
| /** |
| * Check if a particular stream configuration is supported by configuring it |
| * to the device. |
| */ |
| public static boolean isStreamConfigurationSupported(CameraDevice camera, |
| List<Surface> outputSurfaces, |
| CameraCaptureSession.StateCallback listener, Handler handler) { |
| try { |
| configureCameraSession(camera, outputSurfaces, listener, handler); |
| return true; |
| } catch (Exception e) { |
| Log.i(TAG, "This stream configuration is not supported due to " + e.getMessage()); |
| return false; |
| } |
| } |
| |
| public final static class SessionConfigSupport { |
| public final boolean error; |
| public final boolean callSupported; |
| public final boolean configSupported; |
| |
| public SessionConfigSupport(boolean error, |
| boolean callSupported, boolean configSupported) { |
| this.error = error; |
| this.callSupported = callSupported; |
| this.configSupported = configSupported; |
| } |
| } |
| |
| /** |
| * Query whether a particular stream combination is supported. |
| */ |
| public static void checkSessionConfigurationWithSurfaces(CameraDevice camera, |
| Handler handler, List<Surface> outputSurfaces, InputConfiguration inputConfig, |
| int operatingMode, boolean defaultSupport, String msg) { |
| List<OutputConfiguration> outConfigurations = new ArrayList<>(outputSurfaces.size()); |
| for (Surface surface : outputSurfaces) { |
| outConfigurations.add(new OutputConfiguration(surface)); |
| } |
| |
| checkSessionConfigurationSupported(camera, handler, outConfigurations, |
| inputConfig, operatingMode, defaultSupport, msg); |
| } |
| |
| public static void checkSessionConfigurationSupported(CameraDevice camera, |
| Handler handler, List<OutputConfiguration> outputConfigs, |
| InputConfiguration inputConfig, int operatingMode, boolean defaultSupport, |
| String msg) { |
| SessionConfigSupport sessionConfigSupported = |
| isSessionConfigSupported(camera, handler, outputConfigs, inputConfig, |
| operatingMode, defaultSupport); |
| |
| assertTrue(msg, !sessionConfigSupported.error && sessionConfigSupported.configSupported); |
| } |
| |
| /** |
| * Query whether a particular stream combination is supported. |
| */ |
| public static SessionConfigSupport isSessionConfigSupported(CameraDevice camera, |
| Handler handler, List<OutputConfiguration> outputConfigs, |
| InputConfiguration inputConfig, int operatingMode, boolean defaultSupport) { |
| boolean ret; |
| BlockingSessionCallback sessionListener = new BlockingSessionCallback(); |
| |
| SessionConfiguration sessionConfig = new SessionConfiguration(operatingMode, outputConfigs, |
| new HandlerExecutor(handler), sessionListener); |
| if (inputConfig != null) { |
| sessionConfig.setInputConfiguration(inputConfig); |
| } |
| |
| try { |
| ret = camera.isSessionConfigurationSupported(sessionConfig); |
| } catch (UnsupportedOperationException e) { |
| // Camera doesn't support session configuration query |
| return new SessionConfigSupport(false/*error*/, |
| false/*callSupported*/, defaultSupport/*configSupported*/); |
| } catch (IllegalArgumentException e) { |
| return new SessionConfigSupport(true/*error*/, |
| false/*callSupported*/, false/*configSupported*/); |
| } catch (android.hardware.camera2.CameraAccessException e) { |
| return new SessionConfigSupport(true/*error*/, |
| false/*callSupported*/, false/*configSupported*/); |
| } |
| |
| return new SessionConfigSupport(false/*error*/, |
| true/*callSupported*/, ret/*configSupported*/); |
| } |
| |
| /** |
| * Wait for numResultWait frames |
| * |
| * @param resultListener The capture listener to get capture result back. |
| * @param numResultsWait Number of frame to wait |
| * @param timeout Wait timeout in ms. |
| * |
| * @return the last result, or {@code null} if there was none |
| */ |
| public static CaptureResult waitForNumResults(SimpleCaptureCallback resultListener, |
| int numResultsWait, int timeout) { |
| if (numResultsWait < 0 || resultListener == null) { |
| throw new IllegalArgumentException( |
| "Input must be positive number and listener must be non-null"); |
| } |
| |
| CaptureResult result = null; |
| for (int i = 0; i < numResultsWait; i++) { |
| result = resultListener.getCaptureResult(timeout); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Wait for any expected result key values available in a certain number of results. |
| * |
| * <p> |
| * Check the result immediately if numFramesWait is 0. |
| * </p> |
| * |
| * @param listener The capture listener to get capture result. |
| * @param resultKey The capture result key associated with the result value. |
| * @param expectedValues The list of result value need to be waited for, |
| * return immediately if the list is empty. |
| * @param numResultsWait Number of frame to wait before times out. |
| * @param timeout result wait time out in ms. |
| * @throws TimeoutRuntimeException If more than numResultsWait results are. |
| * seen before the result matching myRequest arrives, or each individual wait |
| * for result times out after 'timeout' ms. |
| */ |
| public static <T> void waitForAnyResultValue(SimpleCaptureCallback listener, |
| CaptureResult.Key<T> resultKey, List<T> expectedValues, int numResultsWait, |
| int timeout) { |
| if (numResultsWait < 0 || listener == null || expectedValues == null) { |
| throw new IllegalArgumentException( |
| "Input must be non-negative number and listener/expectedValues " |
| + "must be non-null"); |
| } |
| |
| int i = 0; |
| CaptureResult result; |
| do { |
| result = listener.getCaptureResult(timeout); |
| T value = result.get(resultKey); |
| for ( T expectedValue : expectedValues) { |
| if (VERBOSE) { |
| Log.v(TAG, "Current result value for key " + resultKey.getName() + " is: " |
| + value.toString()); |
| } |
| if (value.equals(expectedValue)) { |
| return; |
| } |
| } |
| } while (i++ < numResultsWait); |
| |
| throw new TimeoutRuntimeException( |
| "Unable to get the expected result value " + expectedValues + " for key " + |
| resultKey.getName() + " after waiting for " + numResultsWait + " results"); |
| } |
| |
| /** |
| * Wait for expected result key value available in a certain number of results. |
| * |
| * <p> |
| * Check the result immediately if numFramesWait is 0. |
| * </p> |
| * |
| * @param listener The capture listener to get capture result |
| * @param resultKey The capture result key associated with the result value |
| * @param expectedValue The result value need to be waited for |
| * @param numResultsWait Number of frame to wait before times out |
| * @param timeout Wait time out. |
| * @throws TimeoutRuntimeException If more than numResultsWait results are |
| * seen before the result matching myRequest arrives, or each individual wait |
| * for result times out after 'timeout' ms. |
| */ |
| public static <T> void waitForResultValue(SimpleCaptureCallback listener, |
| CaptureResult.Key<T> resultKey, T expectedValue, int numResultsWait, int timeout) { |
| List<T> expectedValues = new ArrayList<T>(); |
| expectedValues.add(expectedValue); |
| waitForAnyResultValue(listener, resultKey, expectedValues, numResultsWait, timeout); |
| } |
| |
| /** |
| * Wait for AE to be stabilized before capture: CONVERGED or FLASH_REQUIRED. |
| * |
| * <p>Waits for {@code android.sync.maxLatency} number of results first, to make sure |
| * that the result is synchronized (or {@code numResultWaitForUnknownLatency} if the latency |
| * is unknown.</p> |
| * |
| * <p>This is a no-op for {@code LEGACY} devices since they don't report |
| * the {@code aeState} result.</p> |
| * |
| * @param resultListener The capture listener to get capture result back. |
| * @param numResultWaitForUnknownLatency Number of frame to wait if camera device latency is |
| * unknown. |
| * @param staticInfo corresponding camera device static metadata. |
| * @param settingsTimeout wait timeout for settings application in ms. |
| * @param resultTimeout wait timeout for result in ms. |
| * @param numResultsWait Number of frame to wait before times out. |
| */ |
| public static void waitForAeStable(SimpleCaptureCallback resultListener, |
| int numResultWaitForUnknownLatency, StaticMetadata staticInfo, |
| int settingsTimeout, int numResultWait) { |
| waitForSettingsApplied(resultListener, numResultWaitForUnknownLatency, staticInfo, |
| settingsTimeout); |
| |
| if (!staticInfo.isHardwareLevelAtLeastLimited()) { |
| // No-op for metadata |
| return; |
| } |
| List<Integer> expectedAeStates = new ArrayList<Integer>(); |
| expectedAeStates.add(new Integer(CaptureResult.CONTROL_AE_STATE_CONVERGED)); |
| expectedAeStates.add(new Integer(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED)); |
| waitForAnyResultValue(resultListener, CaptureResult.CONTROL_AE_STATE, expectedAeStates, |
| numResultWait, settingsTimeout); |
| } |
| |
| /** |
| * Wait for enough results for settings to be applied |
| * |
| * @param resultListener The capture listener to get capture result back. |
| * @param numResultWaitForUnknownLatency Number of frame to wait if camera device latency is |
| * unknown. |
| * @param staticInfo corresponding camera device static metadata. |
| * @param timeout wait timeout in ms. |
| */ |
| public static void waitForSettingsApplied(SimpleCaptureCallback resultListener, |
| int numResultWaitForUnknownLatency, StaticMetadata staticInfo, int timeout) { |
| int maxLatency = staticInfo.getSyncMaxLatency(); |
| if (maxLatency == CameraMetadata.SYNC_MAX_LATENCY_UNKNOWN) { |
| maxLatency = numResultWaitForUnknownLatency; |
| } |
| // Wait for settings to take effect |
| waitForNumResults(resultListener, maxLatency, timeout); |
| } |
| |
| public static Range<Integer> getSuitableFpsRangeForDuration(String cameraId, |
| long frameDuration, StaticMetadata staticInfo) { |
| // Add 0.05 here so Fps like 29.99 evaluated to 30 |
| int minBurstFps = (int) Math.floor(1e9 / frameDuration + 0.05f); |
| boolean foundConstantMaxYUVRange = false; |
| boolean foundYUVStreamingRange = false; |
| boolean isExternalCamera = staticInfo.isExternalCamera(); |
| boolean isNIR = staticInfo.isNIRColorFilter(); |
| |
| // Find suitable target FPS range - as high as possible that covers the max YUV rate |
| // Also verify that there's a good preview rate as well |
| List<Range<Integer> > fpsRanges = Arrays.asList( |
| staticInfo.getAeAvailableTargetFpsRangesChecked()); |
| Range<Integer> targetRange = null; |
| for (Range<Integer> fpsRange : fpsRanges) { |
| if (fpsRange.getLower() == minBurstFps && fpsRange.getUpper() == minBurstFps) { |
| foundConstantMaxYUVRange = true; |
| targetRange = fpsRange; |
| } else if (isExternalCamera && fpsRange.getUpper() == minBurstFps) { |
| targetRange = fpsRange; |
| } |
| if (fpsRange.getLower() <= 15 && fpsRange.getUpper() == minBurstFps) { |
| foundYUVStreamingRange = true; |
| } |
| |
| } |
| |
| if (!isExternalCamera) { |
| assertTrue(String.format("Cam %s: Target FPS range of (%d, %d) must be supported", |
| cameraId, minBurstFps, minBurstFps), foundConstantMaxYUVRange); |
| } |
| |
| if (!isNIR) { |
| assertTrue(String.format( |
| "Cam %s: Target FPS range of (x, %d) where x <= 15 must be supported", |
| cameraId, minBurstFps), foundYUVStreamingRange); |
| } |
| return targetRange; |
| } |
| /** |
| * Get the candidate supported zoom ratios for testing |
| * |
| * <p> |
| * This function returns the bounary values of supported zoom ratio range in addition to 1.0x |
| * zoom ratio. |
| * </p> |
| */ |
| public static List<Float> getCandidateZoomRatios(StaticMetadata staticInfo) { |
| List<Float> zoomRatios = new ArrayList<Float>(); |
| Range<Float> zoomRatioRange = staticInfo.getZoomRatioRangeChecked(); |
| zoomRatios.add(zoomRatioRange.getLower()); |
| if (zoomRatioRange.contains(1.0f) && |
| 1.0f - zoomRatioRange.getLower() > ZOOM_RATIO_THRESHOLD && |
| zoomRatioRange.getUpper() - 1.0f > ZOOM_RATIO_THRESHOLD) { |
| zoomRatios.add(1.0f); |
| } |
| zoomRatios.add(zoomRatioRange.getUpper()); |
| |
| return zoomRatios; |
| } |
| |
| public static final int PERFORMANCE_CLASS_NOT_MET = 0; |
| public static final int PERFORMANCE_CLASS_R = Build.VERSION_CODES.R; |
| public static final int PERFORMANCE_CLASS_S = Build.VERSION_CODES.R + 1; |
| public static final int PERFORMANCE_CLASS_CURRENT = PERFORMANCE_CLASS_S; |
| |
| /** |
| * Check whether this mobile device is R performance class as defined in CDD |
| */ |
| public static boolean isRPerfClass() { |
| return Build.VERSION.MEDIA_PERFORMANCE_CLASS == PERFORMANCE_CLASS_R; |
| } |
| |
| /** |
| * Check whether this mobile device is S performance class as defined in CDD |
| */ |
| public static boolean isSPerfClass() { |
| return Build.VERSION.MEDIA_PERFORMANCE_CLASS == PERFORMANCE_CLASS_S; |
| } |
| |
| /** |
| * Check whether a camera Id is a primary rear facing camera |
| */ |
| public static boolean isPrimaryRearFacingCamera(CameraManager manager, String cameraId) |
| throws Exception { |
| return isPrimaryCamera(manager, cameraId, CameraCharacteristics.LENS_FACING_BACK); |
| } |
| |
| /** |
| * Check whether a camera Id is a primary front facing camera |
| */ |
| public static boolean isPrimaryFrontFacingCamera(CameraManager manager, String cameraId) |
| throws Exception { |
| return isPrimaryCamera(manager, cameraId, CameraCharacteristics.LENS_FACING_FRONT); |
| } |
| |
| private static boolean isPrimaryCamera(CameraManager manager, String cameraId, |
| Integer lensFacing) throws Exception { |
| CameraCharacteristics characteristics; |
| Integer facing; |
| |
| String [] ids = manager.getCameraIdList(); |
| for (String id : ids) { |
| characteristics = manager.getCameraCharacteristics(id); |
| facing = characteristics.get(CameraCharacteristics.LENS_FACING); |
| if (lensFacing.equals(facing)) { |
| if (cameraId.equals(id)) { |
| return true; |
| } else { |
| return false; |
| } |
| } |
| } |
| return false; |
| } |
| } |