blob: 112ca77285c49c707f2240a6351abb11fd633001 [file] [log] [blame]
/*
* 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 static android.hardware.camera2.cts.CameraTestUtils.*;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.Size;
import android.media.Image;
import android.media.ImageReader;
import android.os.Environment;
import android.os.Handler;
import android.test.AndroidTestCase;
import android.util.Log;
import android.view.Surface;
import com.android.ex.camera2.blocking.BlockingCameraManager.BlockingOpenException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* <p>Basic test for ImageReader APIs. It uses CameraDevice as producer, camera
* sends the data to the surface provided by imageReader. Below image formats
* are tested:</p>
*
* <p>YUV_420_888: flexible YUV420, it is mandatory format for camera. </p>
* <p>JPEG: used for JPEG still capture, also mandatory format. </p>
* <p>Some invalid access test. </p>
* <p>TODO: Add more format tests? </p>
*/
public class ImageReaderTest extends AndroidTestCase {
private static final String TAG = "ImageReaderTest";
private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
private static final boolean DUMP_FILE = false;
private static final String DEBUG_FILE_NAME_BASE =
Environment.getExternalStorageDirectory().getPath();
// number of frame (for streaming requests) to be verified.
// TODO: Need extend it to bigger number
private static final int NUM_FRAME_VERIFIED = 1;
// Max number of images can be accessed simultaneously from ImageReader.
private static final int MAX_NUM_IMAGES = 5;
private CameraManager mCameraManager;
private CameraDevice mCamera;
private String[] mCameraIds;
private ImageReader mReader = null;
private Handler mHandler = null;
private SimpleImageListener mListener = null;
private CameraTestThread mLooperThread = null;
@Override
public void setContext(Context context) {
super.setContext(context);
mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
assertNotNull("Can't connect to camera manager!", mCameraManager);
}
@Override
protected void setUp() throws Exception {
super.setUp();
mCameraIds = mCameraManager.getCameraIdList();
mLooperThread = new CameraTestThread();
mHandler = mLooperThread.start();
}
@Override
protected void tearDown() throws Exception {
if (mCamera != null) {
mCamera.close();
mCamera = null;
}
if (mReader != null) {
mReader.close();
mReader = null;
}
mLooperThread.close();
mHandler = null;
super.tearDown();
}
public void testImageReaderFromCameraFlexibleYuv() throws Exception {
for (int i = 0; i < mCameraIds.length; i++) {
Log.i(TAG, "Testing Camera " + mCameraIds[i]);
openDevice(mCameraIds[i]);
bufferFormatTestByCamera(ImageFormat.YUV_420_888, mCameraIds[i]);
closeDevice(mCameraIds[i]);
}
}
public void testImageReaderFromCameraJpeg() throws Exception {
for (int i = 0; i < mCameraIds.length; i++) {
Log.v(TAG, "Testing Camera " + mCameraIds[i]);
openDevice(mCameraIds[i]);
bufferFormatTestByCamera(ImageFormat.JPEG, mCameraIds[i]);
closeDevice(mCameraIds[i]);
}
}
public void testImageReaderFromCameraRaw() {
// TODO: can test this once raw is supported
}
public void testImageReaderInvalidAccessTest() {
// TODO: test invalid access case, see if we can receive expected
// exceptions
}
private void bufferFormatTestByCamera(int format, String cameraId) throws Exception {
CameraCharacteristics properties = mCamera.getProperties();
assertNotNull("Can't get camera properties!", properties);
/**
* TODO: cleanup the color format mess, we probably need define formats
* in Image class instead of using ImageFormat for camera. also,
* probably make sense to change the available format type from Enum[]
* to int[]. It'll also be nice to put this into a helper function and
* move to util class.
*/
// Comment out below check to work around b/10406212.
/*
int[] availableFormats = properties.get(CameraCharacteristics.SCALER_AVAILABLE_FORMATS);
assertArrayNotEmpty(availableFormats,
"availableFormats should not be empty");
Arrays.sort(availableFormats);
assertTrue("Can't find the format " + format + " in supported formats " +
Arrays.toString(availableFormats),
Arrays.binarySearch(availableFormats, format) >= 0);
*/
Size[] availableSizes = getSupportedSizeForFormat(format, mCamera);
assertArrayNotEmpty(availableSizes, "availableSizes should not be empty");
// for each resolution, test imageReader:
for (Size sz : availableSizes) {
if (VERBOSE) Log.v(TAG, "Testing size " + sz.toString() + " for camera " + cameraId);
prepareImageReader(sz, format);
CaptureRequest request = prepareCaptureRequest(format);
captureAndValidateImage(request, sz, format);
stopCapture();
}
}
private class SimpleImageListener implements ImageReader.OnImageAvailableListener {
private int mPendingImages = 0;
private final Object mImageSyncObject = new Object();
@Override
public void onImageAvailable(ImageReader reader) {
if (VERBOSE) Log.v(TAG, "new image available");
synchronized (mImageSyncObject) {
mPendingImages++;
mImageSyncObject.notifyAll();
}
}
public boolean isImagePending() {
synchronized (mImageSyncObject) {
return (mPendingImages > 0);
}
}
public void waitForImage() {
final int TIMEOUT_MS = 5000;
synchronized (mImageSyncObject) {
while (mPendingImages == 0) {
try {
if (VERBOSE)
Log.d(TAG, "waiting for next image");
mImageSyncObject.wait(TIMEOUT_MS);
if (mPendingImages == 0) {
fail("wait for next image timed out");
}
} catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
mPendingImages--;
}
}
}
private void prepareImageReader(Size sz, int format) throws Exception {
int width = sz.getWidth();
int height = sz.getHeight();
mReader = ImageReader.newInstance(width, height, format, MAX_NUM_IMAGES);
mListener = new SimpleImageListener();
mReader.setOnImageAvailableListener(mListener, mHandler);
if (VERBOSE) Log.v(TAG, "Preparing ImageReader size " + sz.toString());
}
private CaptureRequest prepareCaptureRequest(int format) throws Exception {
List<Surface> outputSurfaces = new ArrayList<Surface>(1);
Surface surface = mReader.getSurface();
assertNotNull("Fail to get surface from ImageReader", surface);
outputSurfaces.add(surface);
mCamera.configureOutputs(outputSurfaces);
CaptureRequest.Builder captureBuilder =
mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
assertNotNull("Fail to get captureRequest", captureBuilder);
captureBuilder.addTarget(mReader.getSurface());
return captureBuilder.build();
}
private void captureAndValidateImage(CaptureRequest request,
Size sz, int format) throws Exception {
// TODO: Add more format here, and wrap each one as a function.
Image img;
int captureCount = NUM_FRAME_VERIFIED;
// Only verify single image for still capture
if (format == ImageFormat.JPEG) {
captureCount = 1;
mCamera.capture(request, null, null);
} else {
mCamera.setRepeatingRequest(request, null, null);
}
for (int i = 0; i < captureCount; i++) {
assertNotNull("Image listener is null", mListener);
if (VERBOSE) Log.v(TAG, "Waiting for an Image");
mListener.waitForImage();
img = mReader.acquireNextImage();
if (VERBOSE) Log.v(TAG, "Got next image");
validateImage(img, sz.getWidth(), sz.getHeight(), format);
img.close();
// Return the pending images to producer in case the validation is slower
// than the image producing rate. Otherwise, it could cause the producer
// starvation.
while (mListener.isImagePending()) {
mListener.waitForImage();
img = mReader.acquireNextImage();
img.close();
}
}
}
private void stopCapture() throws CameraAccessException {
mCamera.stopRepeating();
mCamera.waitUntilIdle();
mReader.close();
mReader = null;
mListener = null;
}
private void openDevice(String cameraId) {
if (mCamera != null) {
throw new IllegalStateException("Already have open camera device");
}
try {
mCamera = openCamera(mCameraManager, cameraId, mHandler);
} catch (CameraAccessException e) {
mCamera = null;
fail("Fail to open camera, " + Log.getStackTraceString(e));
} catch (BlockingOpenException e) {
mCamera = null;
fail("Fail to open camera, " + Log.getStackTraceString(e));
}
}
private void closeDevice(String cameraId) {
mCamera.close();
mCamera = null;
}
private void validateImage(Image image, int width, int height, int format) {
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);
if (format == ImageFormat.JPEG) {
validateJpegData(data, width, height);
} else {
validateYuvData(data, width, height, format, image.getTimestamp());
}
}
private void validateJpegData(byte[] jpegData, int width, int height) {
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 (DUMP_FILE) {
String fileName =
DEBUG_FILE_NAME_BASE + width + "x" + height + ".yuv";
dumpFile(fileName, jpegData);
}
}
private void validateYuvData(byte[] yuvData, int width, int height, int format, long ts) {
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 if we have test pattern(tracked by b/9625427)
if (DUMP_FILE) {
String fileName =
DEBUG_FILE_NAME_BASE + "/" + width + "x" + height + "_" + ts / 1e6 + ".yuv";
dumpFile(fileName, yuvData);
}
}
}