1 /*
2  * Copyright 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.hardware.camera2.cts;
18 
19 import static android.graphics.ImageFormat.YUV_420_888;
20 import static android.hardware.camera2.cts.helpers.Preconditions.*;
21 import static android.hardware.camera2.cts.helpers.AssertHelpers.*;
22 import static android.hardware.camera2.cts.CameraTestUtils.*;
23 import static com.android.ex.camera2.blocking.BlockingStateCallback.*;
24 
25 import android.content.Context;
26 import android.graphics.ImageFormat;
27 import android.graphics.RectF;
28 import android.hardware.camera2.CameraAccessException;
29 import android.hardware.camera2.CameraCaptureSession;
30 import android.hardware.camera2.CameraCharacteristics;
31 import android.hardware.camera2.CameraDevice;
32 import android.hardware.camera2.CameraManager;
33 import android.hardware.camera2.CameraMetadata;
34 import android.hardware.camera2.CaptureRequest;
35 import android.hardware.camera2.CaptureResult;
36 import android.hardware.camera2.TotalCaptureResult;
37 import android.hardware.camera2.params.ColorSpaceTransform;
38 import android.hardware.camera2.params.RggbChannelVector;
39 import android.hardware.camera2.params.StreamConfigurationMap;
40 import android.util.Size;
41 import android.hardware.camera2.cts.helpers.MaybeNull;
42 import android.hardware.camera2.cts.helpers.StaticMetadata;
43 import android.hardware.camera2.cts.rs.RenderScriptSingleton;
44 import android.hardware.camera2.cts.rs.ScriptGraph;
45 import android.hardware.camera2.cts.rs.ScriptYuvCrop;
46 import android.hardware.camera2.cts.rs.ScriptYuvMeans1d;
47 import android.hardware.camera2.cts.rs.ScriptYuvMeans2dTo1d;
48 import android.hardware.camera2.cts.rs.ScriptYuvToRgb;
49 import android.os.Handler;
50 import android.os.HandlerThread;
51 import android.renderscript.Allocation;
52 import android.renderscript.Script.LaunchOptions;
53 import android.test.AndroidTestCase;
54 import android.util.Log;
55 import android.util.Rational;
56 import android.view.Surface;
57 
58 import com.android.ex.camera2.blocking.BlockingCameraManager.BlockingOpenException;
59 import com.android.ex.camera2.blocking.BlockingStateCallback;
60 import com.android.ex.camera2.blocking.BlockingSessionCallback;
61 
62 import java.util.ArrayList;
63 import java.util.Arrays;
64 import java.util.List;
65 
66 /**
67  * Suite of tests for camera2 -> RenderScript APIs.
68  *
69  * <p>It uses CameraDevice as producer, camera sends the data to the surface provided by
70  * Allocation. Only the below format is tested:</p>
71  *
72  * <p>YUV_420_888: flexible YUV420, it is a mandatory format for camera.</p>
73  */
74 public class AllocationTest extends AndroidTestCase {
75     private static final String TAG = "AllocationTest";
76     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
77 
78     private CameraManager mCameraManager;
79     private CameraDevice mCamera;
80     private CameraCaptureSession mSession;
81     private BlockingStateCallback mCameraListener;
82     private BlockingSessionCallback mSessionListener;
83 
84     private String[] mCameraIds;
85 
86     private Handler mHandler;
87     private HandlerThread mHandlerThread;
88 
89     private CameraIterable mCameraIterable;
90     private SizeIterable mSizeIterable;
91     private ResultIterable mResultIterable;
92 
93     @Override
setContext(Context context)94     public synchronized void setContext(Context context) {
95         super.setContext(context);
96         mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
97         assertNotNull("Can't connect to camera manager!", mCameraManager);
98     }
99 
100     @Override
setUp()101     protected void setUp() throws Exception {
102         super.setUp();
103         mCameraIds = mCameraManager.getCameraIdList();
104         mHandlerThread = new HandlerThread("AllocationTest");
105         mHandlerThread.start();
106         mHandler = new Handler(mHandlerThread.getLooper());
107         mCameraListener = new BlockingStateCallback();
108 
109         mCameraIterable = new CameraIterable();
110         mSizeIterable = new SizeIterable();
111         mResultIterable = new ResultIterable();
112 
113         RenderScriptSingleton.setContext(getContext());
114     }
115 
116     @Override
tearDown()117     protected void tearDown() throws Exception {
118         MaybeNull.close(mCamera);
119         RenderScriptSingleton.clearContext();
120         mHandlerThread.quitSafely();
121         mHandler = null;
122         super.tearDown();
123     }
124 
125     /**
126      * Update the request with a default manual request template.
127      *
128      * @param request A builder for a CaptureRequest
129      * @param sensitivity ISO gain units (e.g. 100)
130      * @param expTimeNs Exposure time in nanoseconds
131      */
setManualCaptureRequest(CaptureRequest.Builder request, int sensitivity, long expTimeNs)132     private static void setManualCaptureRequest(CaptureRequest.Builder request, int sensitivity,
133             long expTimeNs) {
134         final Rational ONE = new Rational(1, 1);
135         final Rational ZERO = new Rational(0, 1);
136 
137         if (VERBOSE) {
138             Log.v(TAG, String.format("Create manual capture request, sensitivity = %d, expTime = %f",
139                     sensitivity, expTimeNs / (1000.0 * 1000)));
140         }
141 
142         request.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_OFF);
143         request.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF);
144         request.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF);
145         request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF);
146         request.set(CaptureRequest.CONTROL_EFFECT_MODE, CaptureRequest.CONTROL_EFFECT_MODE_OFF);
147         request.set(CaptureRequest.SENSOR_FRAME_DURATION, 0L);
148         request.set(CaptureRequest.SENSOR_SENSITIVITY, sensitivity);
149         request.set(CaptureRequest.SENSOR_EXPOSURE_TIME, expTimeNs);
150         request.set(CaptureRequest.COLOR_CORRECTION_MODE,
151                 CaptureRequest.COLOR_CORRECTION_MODE_TRANSFORM_MATRIX);
152 
153         // Identity transform
154         request.set(CaptureRequest.COLOR_CORRECTION_TRANSFORM,
155             new ColorSpaceTransform(new Rational[] {
156                 ONE, ZERO, ZERO,
157                 ZERO, ONE, ZERO,
158                 ZERO, ZERO, ONE
159             }));
160 
161         // Identity gains
162         request.set(CaptureRequest.COLOR_CORRECTION_GAINS,
163                 new RggbChannelVector(1.0f, 1.0f, 1.0f, 1.0f ));
164         request.set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_FAST);
165     }
166 
167     /**
168      * Calculate the absolute crop window from a {@link Size},
169      * and configure {@link LaunchOptions} for it.
170      */
171     // TODO: split patch crop window and the application against a particular size into 2 classes
172     public static class Patch {
173         /**
174          * Create a new {@link Patch} from relative crop coordinates.
175          *
176          * <p>All float values must be normalized coordinates between [0, 1].</p>
177          *
178          * @param size Size of the original rectangle that is being cropped.
179          * @param xNorm The X coordinate defining the left side of the rectangle (in [0, 1]).
180          * @param yNorm The Y coordinate defining the top side of the rectangle (in [0, 1]).
181          * @param wNorm The width of the crop rectangle (normalized between [0, 1]).
182          * @param hNorm The height of the crop rectangle (normalized between [0, 1]).
183          *
184          * @throws NullPointerException if size was {@code null}.
185          * @throws AssertionError if any of the normalized coordinates were out of range
186          */
Patch(Size size, float xNorm, float yNorm, float wNorm, float hNorm)187         public Patch(Size size, float xNorm, float yNorm, float wNorm, float hNorm) {
188             checkNotNull("size", size);
189 
190             assertInRange(xNorm, 0.0f, 1.0f);
191             assertInRange(yNorm, 0.0f, 1.0f);
192             assertInRange(wNorm, 0.0f, 1.0f);
193             assertInRange(hNorm, 0.0f, 1.0f);
194 
195             wFull = size.getWidth();
196             hFull = size.getWidth();
197 
198             xTile = (int)Math.ceil(xNorm * wFull);
199             yTile = (int)Math.ceil(yNorm * hFull);
200 
201             wTile = (int)Math.ceil(wNorm * wFull);
202             hTile = (int)Math.ceil(hNorm * hFull);
203 
204             mSourceSize = size;
205         }
206 
207         /**
208          * Get the original size used to create this {@link Patch}.
209          *
210          * @return source size
211          */
getSourceSize()212         public Size getSourceSize() {
213             return mSourceSize;
214         }
215 
216         /**
217          * Get the cropped size after applying the normalized crop window.
218          *
219          * @return cropped size
220          */
getSize()221         public Size getSize() {
222             return new Size(wFull, hFull);
223         }
224 
225         /**
226          * Get the {@link LaunchOptions} that can be used with a {@link android.renderscript.Script}
227          * to apply a kernel over a subset of an {@link Allocation}.
228          *
229          * @return launch options
230          */
getLaunchOptions()231         public LaunchOptions getLaunchOptions() {
232             return (new LaunchOptions())
233                     .setX(xTile, xTile + wTile)
234                     .setY(yTile, yTile + hTile);
235         }
236 
237         /**
238          * Get the cropped width after applying the normalized crop window.
239          *
240          * @return cropped width
241          */
getWidth()242         public int getWidth() {
243             return wTile;
244         }
245 
246         /**
247          * Get the cropped height after applying the normalized crop window.
248          *
249          * @return cropped height
250          */
getHeight()251         public int getHeight() {
252             return hTile;
253         }
254 
255         /**
256          * Convert to a {@link RectF} where each corner is represented by a
257          * normalized coordinate in between [0.0, 1.0] inclusive.
258          *
259          * @return a new rectangle
260          */
toRectF()261         public RectF toRectF() {
262             return new RectF(
263                     xTile * 1.0f / wFull,
264                     yTile * 1.0f / hFull,
265                     (xTile + wTile) * 1.0f / wFull,
266                     (yTile + hTile) * 1.0f / hFull);
267         }
268 
269         private final Size mSourceSize;
270         private final int wFull;
271         private final int hFull;
272         private final int xTile;
273         private final int yTile;
274         private final int wTile;
275         private final int hTile;
276     }
277 
278     /**
279      * Convert a single YUV pixel (3 byte elements) to an RGB pixel.
280      *
281      * <p>The color channels must be in the following order:
282      * <ul><li>Y - 0th channel
283      * <li>U - 1st channel
284      * <li>V - 2nd channel
285      * </ul></p>
286      *
287      * <p>Each channel has data in the range 0-255.</p>
288      *
289      * <p>Output data is a 3-element pixel with each channel in the range of [0,1].
290      * Each channel is saturated to avoid over/underflow.</p>
291      *
292      * <p>The conversion is done using JFIF File Interchange Format's "Conversion to and from RGB":
293      * <ul>
294      * <li>R = Y + 1.042 (Cr - 128)
295      * <li>G = Y - 0.34414 (Cb - 128) - 0.71414 (Cr - 128)
296      * <li>B = Y + 1.772 (Cb - 128)
297      * </ul>
298      *
299      * Where Cr and Cb are aliases of V and U respectively.
300      * </p>
301      *
302      * @param yuvData An array of a YUV pixel (at least 3 bytes large)
303      *
304      * @return an RGB888 pixel with each channel in the range of [0,1]
305      */
convertPixelYuvToRgb(byte[] yuvData)306     private static float[] convertPixelYuvToRgb(byte[] yuvData) {
307         final int CHANNELS = 3; // yuv
308         final float COLOR_RANGE = 255f;
309 
310         assertTrue("YUV pixel must be at least 3 bytes large", CHANNELS <= yuvData.length);
311 
312         float[] rgb = new float[CHANNELS];
313 
314         float y = yuvData[0] & 0xFF;  // Y channel
315         float cb = yuvData[1] & 0xFF; // U channel
316         float cr = yuvData[2] & 0xFF; // V channel
317 
318         // convert YUV -> RGB (from JFIF's "Conversion to and from RGB" section)
319         float r = y + 1.402f * (cr - 128);
320         float g = y - 0.34414f * (cb - 128) - 0.71414f * (cr - 128);
321         float b = y + 1.772f * (cb - 128);
322 
323         // normalize [0,255] -> [0,1]
324         rgb[0] = r / COLOR_RANGE;
325         rgb[1] = g / COLOR_RANGE;
326         rgb[2] = b / COLOR_RANGE;
327 
328         // Clamp to range [0,1]
329         for (int i = 0; i < CHANNELS; ++i) {
330             rgb[i] = Math.max(0.0f, Math.min(1.0f, rgb[i]));
331         }
332 
333         if (VERBOSE) {
334             Log.v(TAG, String.format("RGB calculated (r,g,b) = (%f, %f, %f)", rgb[0], rgb[1],
335                     rgb[2]));
336         }
337 
338         return rgb;
339     }
340 
341     /**
342      * Configure the camera with the target surface;
343      * create a capture request builder with {@code cameraTarget} as the sole surface target.
344      *
345      * <p>Outputs are configured with the new surface targets, and this function blocks until
346      * the camera has finished configuring.</p>
347      *
348      * <p>The capture request is created from the {@link CameraDevice#TEMPLATE_PREVIEW} template.
349      * No other keys are set.
350      * </p>
351      */
configureAndCreateRequestForSurface(Surface cameraTarget)352     private CaptureRequest.Builder configureAndCreateRequestForSurface(Surface cameraTarget)
353             throws CameraAccessException {
354         List<Surface> outputSurfaces = new ArrayList<Surface>(/*capacity*/1);
355         assertNotNull("Failed to get Surface", cameraTarget);
356         outputSurfaces.add(cameraTarget);
357 
358         mSessionListener = new BlockingSessionCallback();
359         mCamera.createCaptureSession(outputSurfaces, mSessionListener, mHandler);
360         mSession = mSessionListener.waitAndGetSession(SESSION_CONFIGURE_TIMEOUT_MS);
361         CaptureRequest.Builder captureBuilder =
362                 mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
363         assertNotNull("Fail to create captureRequest", captureBuilder);
364         captureBuilder.addTarget(cameraTarget);
365 
366         if (VERBOSE) Log.v(TAG, "configureAndCreateRequestForSurface - done");
367 
368         return captureBuilder;
369     }
370 
371     /**
372      * Submit a single request to the camera, block until the buffer is available.
373      *
374      * <p>Upon return from this function, script has been executed against the latest buffer.
375      * </p>
376      */
captureSingleShotAndExecute(CaptureRequest request, ScriptGraph graph)377     private void captureSingleShotAndExecute(CaptureRequest request, ScriptGraph graph)
378             throws CameraAccessException {
379         checkNotNull("request", request);
380         checkNotNull("graph", graph);
381 
382         mSession.capture(request, new CameraCaptureSession.CaptureCallback() {
383             @Override
384             public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request,
385                     TotalCaptureResult result) {
386                 if (VERBOSE) Log.v(TAG, "Capture completed");
387             }
388         }, mHandler);
389 
390         if (VERBOSE) Log.v(TAG, "Waiting for single shot buffer");
391         graph.advanceInputWaiting();
392         if (VERBOSE) Log.v(TAG, "Got the buffer");
393         graph.execute();
394     }
395 
stopCapture()396     private void stopCapture() throws CameraAccessException {
397         if (VERBOSE) Log.v(TAG, "Stopping capture and waiting for idle");
398         // Stop repeat, wait for captures to complete, and disconnect from surfaces
399         mSession.close();
400         mSessionListener.getStateWaiter().waitForState(BlockingSessionCallback.SESSION_CLOSED,
401                 SESSION_CLOSE_TIMEOUT_MS);
402         mSession = null;
403         mSessionListener = null;
404     }
405 
406     /**
407      * Extremely dumb validator. Makes sure there is at least one non-zero RGB pixel value.
408      */
validateInputOutputNotZeroes(ScriptGraph scriptGraph, Size size)409     private void validateInputOutputNotZeroes(ScriptGraph scriptGraph, Size size) {
410         final int BPP = 8; // bits per pixel
411 
412         int width = size.getWidth();
413         int height = size.getHeight();
414         /**
415          * Check the input allocation is sane.
416          * - Byte size matches what we expect.
417          * - The input is not all zeroes.
418          */
419 
420         // Check that input data was updated first. If it wasn't, the rest of the test will fail.
421         byte[] data = scriptGraph.getInputData();
422         assertArrayNotAllZeroes("Input allocation data was not updated", data);
423 
424         // Minimal required size to represent YUV 4:2:0 image
425         int packedSize =
426                 width * height * ImageFormat.getBitsPerPixel(YUV_420_888) / BPP;
427         if (VERBOSE) Log.v(TAG, "Expected image size = " + packedSize);
428         int actualSize = data.length;
429         // Actual size may be larger due to strides or planes being non-contiguous
430         assertTrue(
431                 String.format(
432                         "YUV 420 packed size (%d) should be at least as large as the actual size " +
433                         "(%d)", packedSize, actualSize), packedSize <= actualSize);
434         /**
435          * Check the output allocation by converting to RGBA.
436          * - Byte size matches what we expect
437          * - The output is not all zeroes
438          */
439         final int RGBA_CHANNELS = 4;
440 
441         int actualSizeOut = scriptGraph.getOutputAllocation().getBytesSize();
442         int packedSizeOut = width * height * RGBA_CHANNELS;
443 
444         byte[] dataOut = scriptGraph.getOutputData();
445         assertEquals("RGB mismatched byte[] and expected size",
446                 packedSizeOut, dataOut.length);
447 
448         if (VERBOSE) {
449             Log.v(TAG, "checkAllocationByConvertingToRgba - RGB data size " + dataOut.length);
450         }
451 
452         assertArrayNotAllZeroes("RGBA data was not updated", dataOut);
453         // RGBA8888 stride should be equal to the width
454         assertEquals("RGBA 8888 mismatched byte[] and expected size", packedSizeOut, actualSizeOut);
455 
456         if (VERBOSE) Log.v(TAG, "validating Buffer , size = " + actualSize);
457     }
458 
testAllocationFromCameraFlexibleYuv()459     public void testAllocationFromCameraFlexibleYuv() throws Exception {
460 
461         /** number of frame (for streaming requests) to be verified. */
462         final int NUM_FRAME_VERIFIED = 1;
463 
464         mCameraIterable.forEachCamera(new CameraBlock() {
465             @Override
466             public void run(CameraDevice camera) throws CameraAccessException {
467 
468                 // Iterate over each size in the camera
469                 mSizeIterable.forEachSize(YUV_420_888, new SizeBlock() {
470                     @Override
471                     public void run(final Size size) throws CameraAccessException {
472                         // Create a script graph that converts YUV to RGB
473                         try (ScriptGraph scriptGraph = ScriptGraph.create()
474                                 .configureInputWithSurface(size, YUV_420_888)
475                                 .chainScript(ScriptYuvToRgb.class)
476                                 .buildGraph()) {
477 
478                             if (VERBOSE) Log.v(TAG, "Prepared ScriptYuvToRgb for size " + size);
479 
480                             // Run the graph against camera input and validate we get some input
481                             CaptureRequest request =
482                                     configureAndCreateRequestForSurface(scriptGraph.getInputSurface()).build();
483 
484                             // Block until we get 1 result, then iterate over the result
485                             mResultIterable.forEachResultRepeating(
486                                     request, NUM_FRAME_VERIFIED, new ResultBlock() {
487                                 @Override
488                                 public void run(CaptureResult result) throws CameraAccessException {
489                                     scriptGraph.advanceInputWaiting();
490                                     scriptGraph.execute();
491                                     validateInputOutputNotZeroes(scriptGraph, size);
492                                     scriptGraph.advanceInputAndDrop();
493                                 }
494                             });
495 
496                             stopCapture();
497                         }
498                     }
499                 });
500             }
501         });
502     }
503 
504     /**
505      * Take two shots and ensure per-frame-control with exposure/gain is working correctly.
506      *
507      * <p>Takes a shot with very low ISO and exposure time. Expect it to be black.</p>
508      *
509      * <p>Take a shot with very high ISO and exposure time. Expect it to be white.</p>
510      *
511      * @throws Exception
512      */
testBlackWhite()513     public void testBlackWhite() throws CameraAccessException {
514 
515         /** low iso + low exposure (first shot) */
516         final float THRESHOLD_LOW = 0.025f;
517         /** high iso + high exposure (second shot) */
518         final float THRESHOLD_HIGH = 0.975f;
519 
520         mCameraIterable.forEachCamera(/*fullHwLevel*/false, new CameraBlock() {
521             @Override
522             public void run(CameraDevice camera) throws CameraAccessException {
523                 final StaticMetadata staticInfo =
524                         new StaticMetadata(mCameraManager.getCameraCharacteristics(camera.getId()));
525 
526                 // This test requires PFC and manual sensor control
527                 if (!staticInfo.isCapabilitySupported(
528                         CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR) ||
529                         !staticInfo.isPerFrameControlSupported()) {
530                     return;
531                 }
532 
533                 final Size maxSize = getMaxSize(
534                         getSupportedSizeForFormat(YUV_420_888, camera.getId(), mCameraManager));
535 
536                 try (ScriptGraph scriptGraph = createGraphForYuvCroppedMeans(maxSize)) {
537 
538                     CaptureRequest.Builder req =
539                             configureAndCreateRequestForSurface(scriptGraph.getInputSurface());
540 
541                     // Take a shot with very low ISO and exposure time. Expect it to be black.
542                     int minimumSensitivity = staticInfo.getSensitivityMinimumOrDefault();
543                     long minimumExposure = staticInfo.getExposureMinimumOrDefault();
544                     setManualCaptureRequest(req, minimumSensitivity, minimumExposure);
545 
546                     CaptureRequest lowIsoExposureShot = req.build();
547                     captureSingleShotAndExecute(lowIsoExposureShot, scriptGraph);
548 
549                     float[] blackMeans = convertPixelYuvToRgb(scriptGraph.getOutputData());
550 
551                     // Take a shot with very high ISO and exposure time. Expect it to be white.
552                     int maximumSensitivity = staticInfo.getSensitivityMaximumOrDefault();
553                     long maximumExposure = staticInfo.getExposureMaximumOrDefault();
554                     setManualCaptureRequest(req, maximumSensitivity, maximumExposure);
555 
556                     CaptureRequest highIsoExposureShot = req.build();
557                     captureSingleShotAndExecute(highIsoExposureShot, scriptGraph);
558 
559                     float[] whiteMeans = convertPixelYuvToRgb(scriptGraph.getOutputData());
560 
561                     // low iso + low exposure (first shot)
562                     assertArrayWithinUpperBound("Black means too high", blackMeans, THRESHOLD_LOW);
563 
564                     // high iso + high exposure (second shot)
565                     assertArrayWithinLowerBound("White means too low", whiteMeans, THRESHOLD_HIGH);
566                 }
567             }
568         });
569     }
570 
571     /**
572      * Test that the android.sensitivity.parameter is applied.
573      */
testParamSensitivity()574     public void testParamSensitivity() throws CameraAccessException {
575         final float THRESHOLD_MAX_MIN_DIFF = 0.3f;
576         final float THRESHOLD_MAX_MIN_RATIO = 2.0f;
577         final int NUM_STEPS = 5;
578         final long EXPOSURE_TIME_NS = 2000000; // 2 seconds
579         final int RGB_CHANNELS = 3;
580 
581         mCameraIterable.forEachCamera(/*fullHwLevel*/false, new CameraBlock() {
582 
583 
584             @Override
585             public void run(CameraDevice camera) throws CameraAccessException {
586                 final StaticMetadata staticInfo =
587                         new StaticMetadata(mCameraManager.getCameraCharacteristics(camera.getId()));
588                 // This test requires PFC and manual sensor control
589                 if (!staticInfo.isCapabilitySupported(
590                         CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR) ||
591                         !staticInfo.isPerFrameControlSupported()) {
592                     return;
593                 }
594 
595                 final List<float[]> rgbMeans = new ArrayList<float[]>();
596                 final Size maxSize = getMaxSize(
597                         getSupportedSizeForFormat(YUV_420_888, camera.getId(), mCameraManager));
598 
599                 final int sensitivityMin = staticInfo.getSensitivityMinimumOrDefault();
600                 final int sensitivityMax = staticInfo.getSensitivityMaximumOrDefault();
601 
602                 // List each sensitivity from min-max in NUM_STEPS increments
603                 int[] sensitivities = new int[NUM_STEPS];
604                 for (int i = 0; i < NUM_STEPS; ++i) {
605                     int delta = (sensitivityMax - sensitivityMin) / (NUM_STEPS - 1);
606                     sensitivities[i] = sensitivityMin + delta * i;
607                 }
608 
609                 try (ScriptGraph scriptGraph = createGraphForYuvCroppedMeans(maxSize)) {
610 
611                     CaptureRequest.Builder req =
612                             configureAndCreateRequestForSurface(scriptGraph.getInputSurface());
613 
614                     // Take burst shots with increasing sensitivity one after other.
615                     for (int i = 0; i < NUM_STEPS; ++i) {
616                         setManualCaptureRequest(req, sensitivities[i], EXPOSURE_TIME_NS);
617                         captureSingleShotAndExecute(req.build(), scriptGraph);
618                         float[] means = convertPixelYuvToRgb(scriptGraph.getOutputData());
619                         rgbMeans.add(means);
620 
621                         if (VERBOSE) {
622                             Log.v(TAG, "testParamSensitivity - captured image " + i +
623                                     " with RGB means: " + Arrays.toString(means));
624                         }
625                     }
626 
627                     // Test that every consecutive image gets brighter.
628                     for (int i = 0; i < rgbMeans.size() - 1; ++i) {
629                         float[] curMeans = rgbMeans.get(i);
630                         float[] nextMeans = rgbMeans.get(i+1);
631 
632                         assertArrayNotGreater(
633                                 String.format("Shot with sensitivity %d should not have higher " +
634                                         "average means than shot with sensitivity %d",
635                                         sensitivities[i], sensitivities[i+1]),
636                                 curMeans, nextMeans);
637                     }
638 
639                     // Test the min-max diff and ratios are within expected thresholds
640                     float[] lastMeans = rgbMeans.get(NUM_STEPS - 1);
641                     float[] firstMeans = rgbMeans.get(/*location*/0);
642                     for (int i = 0; i < RGB_CHANNELS; ++i) {
643                         assertTrue(
644                                 String.format("Sensitivity max-min diff too small (max=%f, min=%f)",
645                                         lastMeans[i], firstMeans[i]),
646                                 lastMeans[i] - firstMeans[i] > THRESHOLD_MAX_MIN_DIFF);
647                         assertTrue(
648                                 String.format("Sensitivity max-min ratio too small (max=%f, min=%f)",
649                                         lastMeans[i], firstMeans[i]),
650                                 lastMeans[i] / firstMeans[i] > THRESHOLD_MAX_MIN_RATIO);
651                     }
652                 }
653             }
654         });
655 
656     }
657 
658     /**
659      * Common script graph for manual-capture based tests that determine the average pixel
660      * values of a cropped sub-region.
661      *
662      * <p>Processing chain:
663      *
664      * <pre>
665      * input:  YUV_420_888 surface
666      * output: mean YUV value of a central section of the image,
667      *         YUV 4:4:4 encoded as U8_3
668      * steps:
669      *      1) crop [0.45,0.45] - [0.55, 0.55]
670      *      2) average columns
671      *      3) average rows
672      * </pre>
673      * </p>
674      */
createGraphForYuvCroppedMeans(final Size size)675     private static ScriptGraph createGraphForYuvCroppedMeans(final Size size) {
676         ScriptGraph scriptGraph = ScriptGraph.create()
677                 .configureInputWithSurface(size, YUV_420_888)
678                 .configureScript(ScriptYuvCrop.class)
679                     .set(ScriptYuvCrop.CROP_WINDOW,
680                             new Patch(size, /*x*/0.45f, /*y*/0.45f, /*w*/0.1f, /*h*/0.1f).toRectF())
681                     .buildScript()
682                 .chainScript(ScriptYuvMeans2dTo1d.class)
683                 .chainScript(ScriptYuvMeans1d.class)
684                 // TODO: Make a script for YUV 444 -> RGB 888 conversion
685                 .buildGraph();
686         return scriptGraph;
687     }
688 
689     /*
690      * TODO: Refactor below code into separate classes and to not depend on AllocationTest
691      * inner variables.
692      *
693      * TODO: add javadocs to below methods
694      *
695      * TODO: Figure out if there's some elegant way to compose these forEaches together, so that
696      * the callers don't have to do a ton of nesting
697      */
698 
699     interface CameraBlock {
run(CameraDevice camera)700         void run(CameraDevice camera) throws CameraAccessException;
701     }
702 
703     class CameraIterable {
forEachCamera(CameraBlock runnable)704         public void forEachCamera(CameraBlock runnable)
705                 throws CameraAccessException {
706             forEachCamera(/*fullHwLevel*/false, runnable);
707         }
708 
forEachCamera(boolean fullHwLevel, CameraBlock runnable)709         public void forEachCamera(boolean fullHwLevel, CameraBlock runnable)
710                 throws CameraAccessException {
711             assertNotNull("No camera manager", mCameraManager);
712             assertNotNull("No camera IDs", mCameraIds);
713 
714             for (int i = 0; i < mCameraIds.length; i++) {
715                 // Don't execute the runnable against non-FULL cameras if FULL is required
716                 CameraCharacteristics properties =
717                         mCameraManager.getCameraCharacteristics(mCameraIds[i]);
718                 StaticMetadata staticInfo = new StaticMetadata(properties);
719                 if (fullHwLevel && !staticInfo.isHardwareLevelAtLeastFull()) {
720                     Log.i(TAG, String.format(
721                             "Skipping this test for camera %s, needs FULL hw level",
722                             mCameraIds[i]));
723                     continue;
724                 }
725                 if (!staticInfo.isColorOutputSupported()) {
726                     Log.i(TAG, String.format(
727                         "Skipping this test for camera %s, does not support regular outputs",
728                         mCameraIds[i]));
729                     continue;
730                 }
731                 // Open camera and execute test
732                 Log.i(TAG, "Testing Camera " + mCameraIds[i]);
733                 try {
734                     openDevice(mCameraIds[i]);
735 
736                     runnable.run(mCamera);
737                 } finally {
738                     closeDevice(mCameraIds[i]);
739                 }
740             }
741         }
742 
openDevice(String cameraId)743         private void openDevice(String cameraId) {
744             if (mCamera != null) {
745                 throw new IllegalStateException("Already have open camera device");
746             }
747             try {
748                 mCamera = openCamera(
749                     mCameraManager, cameraId, mCameraListener, mHandler);
750             } catch (CameraAccessException e) {
751                 fail("Fail to open camera synchronously, " + Log.getStackTraceString(e));
752             } catch (BlockingOpenException e) {
753                 fail("Fail to open camera asynchronously, " + Log.getStackTraceString(e));
754             }
755             mCameraListener.waitForState(STATE_OPENED, CAMERA_OPEN_TIMEOUT_MS);
756         }
757 
closeDevice(String cameraId)758         private void closeDevice(String cameraId) {
759             if (mCamera != null) {
760                 mCamera.close();
761                 mCameraListener.waitForState(STATE_CLOSED, CAMERA_CLOSE_TIMEOUT_MS);
762                 mCamera = null;
763             }
764         }
765     }
766 
767     interface SizeBlock {
run(Size size)768         void run(Size size) throws CameraAccessException;
769     }
770 
771     class SizeIterable {
forEachSize(int format, SizeBlock runnable)772         public void forEachSize(int format, SizeBlock runnable) throws CameraAccessException {
773             assertNotNull("No camera opened", mCamera);
774             assertNotNull("No camera manager", mCameraManager);
775 
776             CameraCharacteristics properties =
777                     mCameraManager.getCameraCharacteristics(mCamera.getId());
778 
779             assertNotNull("Can't get camera properties!", properties);
780 
781             StreamConfigurationMap config =
782                     properties.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
783             int[] availableOutputFormats = config.getOutputFormats();
784             assertArrayNotEmpty(availableOutputFormats,
785                     "availableOutputFormats should not be empty");
786             Arrays.sort(availableOutputFormats);
787             assertTrue("Can't find the format " + format + " in supported formats " +
788                     Arrays.toString(availableOutputFormats),
789                     Arrays.binarySearch(availableOutputFormats, format) >= 0);
790 
791             Size[] availableSizes = getSupportedSizeForFormat(format, mCamera.getId(),
792                     mCameraManager);
793             assertArrayNotEmpty(availableSizes, "availableSizes should not be empty");
794 
795             for (Size size : availableSizes) {
796 
797                 if (VERBOSE) {
798                     Log.v(TAG, "Testing size " + size.toString() +
799                             " for camera " + mCamera.getId());
800                 }
801                 runnable.run(size);
802             }
803         }
804     }
805 
806     interface ResultBlock {
run(CaptureResult result)807         void run(CaptureResult result) throws CameraAccessException;
808     }
809 
810     class ResultIterable {
forEachResultOnce(CaptureRequest request, ResultBlock block)811         public void forEachResultOnce(CaptureRequest request, ResultBlock block)
812                 throws CameraAccessException {
813             forEachResult(request, /*count*/1, /*repeating*/false, block);
814         }
815 
forEachResultRepeating(CaptureRequest request, int count, ResultBlock block)816         public void forEachResultRepeating(CaptureRequest request, int count, ResultBlock block)
817                 throws CameraAccessException {
818             forEachResult(request, count, /*repeating*/true, block);
819         }
820 
forEachResult(CaptureRequest request, int count, boolean repeating, ResultBlock block)821         public void forEachResult(CaptureRequest request, int count, boolean repeating,
822                 ResultBlock block) throws CameraAccessException {
823 
824             // TODO: start capture, i.e. configureOutputs
825 
826             SimpleCaptureCallback listener = new SimpleCaptureCallback();
827 
828             if (!repeating) {
829                 for (int i = 0; i < count; ++i) {
830                     mSession.capture(request, listener, mHandler);
831                 }
832             } else {
833                 mSession.setRepeatingRequest(request, listener, mHandler);
834             }
835 
836             // Assume that the device is already IDLE.
837             mSessionListener.getStateWaiter().waitForState(BlockingSessionCallback.SESSION_ACTIVE,
838                     CAMERA_ACTIVE_TIMEOUT_MS);
839 
840             for (int i = 0; i < count; ++i) {
841                 if (VERBOSE) {
842                     Log.v(TAG, String.format("Testing with result %d of %d for camera %s",
843                             i, count, mCamera.getId()));
844                 }
845 
846                 CaptureResult result = listener.getCaptureResult(CAPTURE_RESULT_TIMEOUT_MS);
847                 block.run(result);
848             }
849 
850             if (repeating) {
851                 mSession.stopRepeating();
852                 mSessionListener.getStateWaiter().waitForState(
853                     BlockingSessionCallback.SESSION_READY, CAMERA_IDLE_TIMEOUT_MS);
854             }
855 
856             // TODO: Make a Configure decorator or some such for configureOutputs
857         }
858     }
859 }
860