blob: ebfd92eec073533184a7a1d10ec560fc095dcbbe [file] [log] [blame]
/*
* Copyright 2016 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 com.android.mediaframeworktest.stress;
import com.android.mediaframeworktest.Camera2SurfaceViewTestCase;
import com.android.mediaframeworktest.helpers.CameraTestUtils.SimpleCaptureCallback;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.util.Log;
import android.util.Size;
import java.util.Arrays;
import static android.hardware.camera2.CameraCharacteristics.CONTROL_AE_MODE_OFF;
import static android.hardware.camera2.CameraCharacteristics.CONTROL_AE_MODE_ON;
import static android.hardware.camera2.CameraCharacteristics.CONTROL_AE_MODE_ON_ALWAYS_FLASH;
import static android.hardware.camera2.CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH;
import static android.hardware.camera2.CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE;
import static com.android.mediaframeworktest.helpers.CameraTestUtils.getValueNotNull;
/**
* <p>
* Basic test for camera CaptureRequest key controls.
* </p>
* <p>
* Several test categories are covered: manual sensor control, 3A control,
* manual ISP control and other per-frame control and synchronization.
* </p>
*
* adb shell am instrument \
* -e class com.android.mediaframeworktest.stress.Camera2CaptureRequestTest#testAeModeAndLock \
* -e iterations 10 \
* -e waitIntervalMs 1000 \
* -e resultToFile false \
* -r -w com.android.mediaframeworktest/.Camera2InstrumentationTestRunner
*/
public class Camera2CaptureRequestTest extends Camera2SurfaceViewTestCase {
private static final String TAG = "CaptureRequestTest";
private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
/** 30ms exposure time must be supported by full capability devices. */
private static final long DEFAULT_EXP_TIME_NS = 30000000L;
private static final int DEFAULT_SENSITIVITY = 100;
private static final long EXPOSURE_TIME_ERROR_MARGIN_NS = 100000L; // 100us, Approximation.
private static final float EXPOSURE_TIME_ERROR_MARGIN_RATE = 0.03f; // 3%, Approximation.
private static final float SENSITIVITY_ERROR_MARGIN_RATE = 0.03f; // 3%, Approximation.
private static final int DEFAULT_NUM_EXPOSURE_TIME_STEPS = 3;
private static final int DEFAULT_NUM_SENSITIVITY_STEPS = 16;
private static final int DEFAULT_SENSITIVITY_STEP_SIZE = 100;
@Override
protected void setUp() throws Exception {
super.setUp();
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
}
/**
* Test AE mode and lock.
*
* <p>
* For AE lock, when it is locked, exposure parameters shouldn't be changed.
* For AE modes, each mode should satisfy the per frame controls defined in
* API specifications.
* </p>
*/
public void testAeModeAndLock() throws Exception {
for (int i = 0; i < mCameraIds.length; i++) {
try {
openDevice(mCameraIds[i]);
if (!mStaticInfo.isColorOutputSupported()) {
Log.i(TAG, "Camera " + mCameraIds[i] +
" does not support color outputs, skipping");
continue;
}
Size maxPreviewSz = mOrderedPreviewSizes.get(0); // Max preview size.
// Update preview surface with given size for all sub-tests.
updatePreviewSurface(maxPreviewSz);
// Test iteration starts...
for (int iteration = 0; iteration < getIterationCount(); ++iteration) {
Log.v(TAG, String.format("AE mode and lock: %d/%d", iteration + 1,
getIterationCount()));
// Test aeMode and lock
int[] aeModes = mStaticInfo.getAeAvailableModesChecked();
for (int mode : aeModes) {
aeModeAndLockTestByMode(mode);
}
getResultPrinter().printStatus(getIterationCount(), iteration + 1, mCameraIds[i]);
Thread.sleep(getTestWaitIntervalMs());
}
} finally {
closeDevice();
}
}
}
/**
* Test the all available AE modes and AE lock.
* <p>
* For manual AE mode, test iterates through different sensitivities and
* exposure times, validate the result exposure time correctness. For
* CONTROL_AE_MODE_ON_ALWAYS_FLASH mode, the AE lock and flash are tested.
* For the rest of the AUTO mode, AE lock is tested.
* </p>
*
* @param mode
*/
private void aeModeAndLockTestByMode(int mode)
throws Exception {
switch (mode) {
case CONTROL_AE_MODE_OFF:
if (mStaticInfo.isCapabilitySupported(
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR)) {
// Test manual exposure control.
aeManualControlTest();
} else {
Log.w(TAG,
"aeModeAndLockTestByMode - can't test AE mode OFF without " +
"manual sensor control");
}
break;
case CONTROL_AE_MODE_ON:
case CONTROL_AE_MODE_ON_AUTO_FLASH:
case CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE:
case CONTROL_AE_MODE_ON_ALWAYS_FLASH:
// Test AE lock for above AUTO modes.
aeAutoModeTestLock(mode);
break;
default:
throw new UnsupportedOperationException("Unhandled AE mode " + mode);
}
}
/**
* Test AE auto modes.
* <p>
* Use single request rather than repeating request to test AE lock per frame control.
* </p>
*/
private void aeAutoModeTestLock(int mode) throws Exception {
CaptureRequest.Builder requestBuilder =
mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
if (mStaticInfo.isAeLockSupported()) {
requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false);
}
requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, mode);
configurePreviewOutput(requestBuilder);
final int MAX_NUM_CAPTURES_DURING_LOCK = 5;
for (int i = 1; i <= MAX_NUM_CAPTURES_DURING_LOCK; i++) {
autoAeMultipleCapturesThenTestLock(requestBuilder, mode, i);
}
}
/**
* Issue multiple auto AE captures, then lock AE, validate the AE lock vs.
* the first capture result after the AE lock. The right AE lock behavior is:
* When it is locked, it locks to the current exposure value, and all subsequent
* request with lock ON will have the same exposure value locked.
*/
private void autoAeMultipleCapturesThenTestLock(
CaptureRequest.Builder requestBuilder, int aeMode, int numCapturesDuringLock)
throws Exception {
if (numCapturesDuringLock < 1) {
throw new IllegalArgumentException("numCapturesBeforeLock must be no less than 1");
}
if (VERBOSE) {
Log.v(TAG, "Camera " + mCamera.getId() + ": Testing auto AE mode and lock for mode "
+ aeMode + " with " + numCapturesDuringLock + " captures before lock");
}
final int NUM_CAPTURES_BEFORE_LOCK = 2;
SimpleCaptureCallback listener = new SimpleCaptureCallback();
CaptureResult[] resultsDuringLock = new CaptureResult[numCapturesDuringLock];
boolean canSetAeLock = mStaticInfo.isAeLockSupported();
// Reset the AE lock to OFF, since we are reusing this builder many times
if (canSetAeLock) {
requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false);
}
// Just send several captures with auto AE, lock off.
CaptureRequest request = requestBuilder.build();
for (int i = 0; i < NUM_CAPTURES_BEFORE_LOCK; i++) {
mSession.capture(request, listener, mHandler);
}
waitForNumResults(listener, NUM_CAPTURES_BEFORE_LOCK);
if (!canSetAeLock) {
// Without AE lock, the remaining tests items won't work
return;
}
// Then fire several capture to lock the AE.
requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true);
int requestCount = captureRequestsSynchronized(
requestBuilder.build(), numCapturesDuringLock, listener, mHandler);
int[] sensitivities = new int[numCapturesDuringLock];
long[] expTimes = new long[numCapturesDuringLock];
Arrays.fill(sensitivities, -1);
Arrays.fill(expTimes, -1L);
// Get the AE lock on result and validate the exposure values.
waitForNumResults(listener, requestCount - numCapturesDuringLock);
for (int i = 0; i < resultsDuringLock.length; i++) {
resultsDuringLock[i] = listener.getCaptureResult(WAIT_FOR_RESULT_TIMEOUT_MS);
}
for (int i = 0; i < numCapturesDuringLock; i++) {
mCollector.expectKeyValueEquals(
resultsDuringLock[i], CaptureResult.CONTROL_AE_LOCK, true);
}
// Can't read manual sensor/exposure settings without manual sensor
if (mStaticInfo.isCapabilitySupported(
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_READ_SENSOR_SETTINGS)) {
int sensitivityLocked =
getValueNotNull(resultsDuringLock[0], CaptureResult.SENSOR_SENSITIVITY);
long expTimeLocked =
getValueNotNull(resultsDuringLock[0], CaptureResult.SENSOR_EXPOSURE_TIME);
for (int i = 1; i < resultsDuringLock.length; i++) {
mCollector.expectKeyValueEquals(
resultsDuringLock[i], CaptureResult.SENSOR_EXPOSURE_TIME, expTimeLocked);
mCollector.expectKeyValueEquals(
resultsDuringLock[i], CaptureResult.SENSOR_SENSITIVITY, sensitivityLocked);
}
}
}
/**
* Iterate through exposure times and sensitivities for manual AE control.
* <p>
* Use single request rather than repeating request to test manual exposure
* value change per frame control.
* </p>
*/
private void aeManualControlTest()
throws Exception {
CaptureRequest.Builder requestBuilder =
mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF);
configurePreviewOutput(requestBuilder);
SimpleCaptureCallback listener = new SimpleCaptureCallback();
long[] expTimes = getExposureTimeTestValues();
int[] sensitivities = getSensitivityTestValues();
// Submit single request at a time, then verify the result.
for (int i = 0; i < expTimes.length; i++) {
for (int j = 0; j < sensitivities.length; j++) {
if (VERBOSE) {
Log.v(TAG, "Camera " + mCamera.getId() + ": Testing sensitivity "
+ sensitivities[j] + ", exposure time " + expTimes[i] + "ns");
}
changeExposure(requestBuilder, expTimes[i], sensitivities[j]);
mSession.capture(requestBuilder.build(), listener, mHandler);
// make sure timeout is long enough for long exposure time
long timeout = WAIT_FOR_RESULT_TIMEOUT_MS + expTimes[i];
CaptureResult result = listener.getCaptureResult(timeout);
long resultExpTime = getValueNotNull(result, CaptureResult.SENSOR_EXPOSURE_TIME);
int resultSensitivity = getValueNotNull(result, CaptureResult.SENSOR_SENSITIVITY);
validateExposureTime(expTimes[i], resultExpTime);
validateSensitivity(sensitivities[j], resultSensitivity);
validateFrameDurationForCapture(result);
}
}
// TODO: Add another case to test where we can submit all requests, then wait for
// results, which will hide the pipeline latency. this is not only faster, but also
// test high speed per frame control and synchronization.
}
//----------------------------------------------------------------
//---------Below are common functions for all tests.--------------
//----------------------------------------------------------------
/**
* Enable exposure manual control and change exposure and sensitivity and
* clamp the value into the supported range.
*/
private void changeExposure(CaptureRequest.Builder requestBuilder,
long expTime, int sensitivity) {
// Check if the max analog sensitivity is available and no larger than max sensitivity.
// The max analog sensitivity is not actually used here. This is only an extra sanity check.
mStaticInfo.getMaxAnalogSensitivityChecked();
expTime = mStaticInfo.getExposureClampToRange(expTime);
sensitivity = mStaticInfo.getSensitivityClampToRange(sensitivity);
requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF);
requestBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, expTime);
requestBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, sensitivity);
}
/**
* Get the exposure time array that contains multiple exposure time steps in
* the exposure time range.
*/
private long[] getExposureTimeTestValues() {
long[] testValues = new long[DEFAULT_NUM_EXPOSURE_TIME_STEPS + 1];
long maxExpTime = mStaticInfo.getExposureMaximumOrDefault(DEFAULT_EXP_TIME_NS);
long minExpTime = mStaticInfo.getExposureMinimumOrDefault(DEFAULT_EXP_TIME_NS);
long range = maxExpTime - minExpTime;
double stepSize = range / (double)DEFAULT_NUM_EXPOSURE_TIME_STEPS;
for (int i = 0; i < testValues.length; i++) {
testValues[i] = maxExpTime - (long)(stepSize * i);
testValues[i] = mStaticInfo.getExposureClampToRange(testValues[i]);
}
return testValues;
}
/**
* Get the sensitivity array that contains multiple sensitivity steps in the
* sensitivity range.
* <p>
* Sensitivity number of test values is determined by
* {@value #DEFAULT_SENSITIVITY_STEP_SIZE} and sensitivity range, and
* bounded by {@value #DEFAULT_NUM_SENSITIVITY_STEPS}.
* </p>
*/
private int[] getSensitivityTestValues() {
int maxSensitivity = mStaticInfo.getSensitivityMaximumOrDefault(
DEFAULT_SENSITIVITY);
int minSensitivity = mStaticInfo.getSensitivityMinimumOrDefault(
DEFAULT_SENSITIVITY);
int range = maxSensitivity - minSensitivity;
int stepSize = DEFAULT_SENSITIVITY_STEP_SIZE;
int numSteps = range / stepSize;
// Bound the test steps to avoid supper long test.
if (numSteps > DEFAULT_NUM_SENSITIVITY_STEPS) {
numSteps = DEFAULT_NUM_SENSITIVITY_STEPS;
stepSize = range / numSteps;
}
int[] testValues = new int[numSteps + 1];
for (int i = 0; i < testValues.length; i++) {
testValues[i] = maxSensitivity - stepSize * i;
testValues[i] = mStaticInfo.getSensitivityClampToRange(testValues[i]);
}
return testValues;
}
/**
* Validate the AE manual control exposure time.
*
* <p>Exposure should be close enough, and only round down if they are not equal.</p>
*
* @param request Request exposure time
* @param result Result exposure time
*/
private void validateExposureTime(long request, long result) {
long expTimeDelta = request - result;
long expTimeErrorMargin = (long)(Math.max(EXPOSURE_TIME_ERROR_MARGIN_NS, request
* EXPOSURE_TIME_ERROR_MARGIN_RATE));
// First, round down not up, second, need close enough.
mCollector.expectTrue("Exposture time is invalid for AE manaul control test, request: "
+ request + " result: " + result,
expTimeDelta < expTimeErrorMargin && expTimeDelta >= 0);
}
/**
* Validate AE manual control sensitivity.
*
* @param request Request sensitivity
* @param result Result sensitivity
*/
private void validateSensitivity(int request, int result) {
float sensitivityDelta = request - result;
float sensitivityErrorMargin = request * SENSITIVITY_ERROR_MARGIN_RATE;
// First, round down not up, second, need close enough.
mCollector.expectTrue("Sensitivity is invalid for AE manaul control test, request: "
+ request + " result: " + result,
sensitivityDelta < sensitivityErrorMargin && sensitivityDelta >= 0);
}
/**
* Validate frame duration for a given capture.
*
* <p>Frame duration should be longer than exposure time.</p>
*
* @param result The capture result for a given capture
*/
private void validateFrameDurationForCapture(CaptureResult result) {
long expTime = getValueNotNull(result, CaptureResult.SENSOR_EXPOSURE_TIME);
long frameDuration = getValueNotNull(result, CaptureResult.SENSOR_FRAME_DURATION);
if (VERBOSE) {
Log.v(TAG, "frame duration: " + frameDuration + " Exposure time: " + expTime);
}
mCollector.expectTrue(String.format("Frame duration (%d) should be longer than exposure"
+ " time (%d) for a given capture", frameDuration, expTime),
frameDuration >= expTime);
validatePipelineDepth(result);
}
/**
* Validate the pipeline depth result.
*
* @param result The capture result to get pipeline depth data
*/
private void validatePipelineDepth(CaptureResult result) {
final byte MIN_PIPELINE_DEPTH = 1;
byte maxPipelineDepth = mStaticInfo.getPipelineMaxDepthChecked();
Byte pipelineDepth = getValueNotNull(result, CaptureResult.REQUEST_PIPELINE_DEPTH);
mCollector.expectInRange(String.format("Pipeline depth must be in the range of [%d, %d]",
MIN_PIPELINE_DEPTH, maxPipelineDepth), pipelineDepth, MIN_PIPELINE_DEPTH,
maxPipelineDepth);
}
}