diff options
28 files changed, 596 insertions, 277 deletions
diff --git a/apps/CameraITS/pymodules/its/device.py b/apps/CameraITS/pymodules/its/device.py index c15f302..f773451 100644 --- a/apps/CameraITS/pymodules/its/device.py +++ b/apps/CameraITS/pymodules/its/device.py @@ -48,6 +48,10 @@ class ItsSession(object): # to adb, which causes it to fail if there is more than one device. ADB = "adb -d" + # Set to True to take a pre-shot before capture and throw it away (for + # debug purposes). + CAPTURE_THROWAWAY_SHOTS = False + DEVICE_FOLDER_ROOT = '/sdcard/its' DEVICE_FOLDER_CAPTURE = 'captures' INTENT_CAPTURE = 'com.android.camera2.its.CAPTURE' @@ -344,7 +348,7 @@ class ItsSession(object): local_fname = os.path.basename(remote_fname) return self.__parse_captured_json([local_fname])[0]['cameraProperties'] - def do_3a(self, region_ae, region_af, region_awb, + def do_3a(self, region_ae, region_awb, region_af, do_ae=True, do_awb=True, do_af=True): """Perform a 3A operation on the device. @@ -355,8 +359,8 @@ class ItsSession(object): Args: region_ae: Normalized rect. (x,y,w,h) specifying the AE region. - region_af: Normalized rect. (x,y,w,h) specifying the AF region. region_awb: Normalized rect. (x,y,w,h) specifying the AWB region. + region_af: Normalized rect. (x,y,w,h) specifying the AF region. Returns: Five values: @@ -456,14 +460,11 @@ class ItsSession(object): burst capture) containing the metadata of the captured image(s). """ if request.has_key("captureRequest"): - - print "Capturing image (including a pre-shot for settings synch)" - - # HACK: Take a pre-shot, to make sure the settings stick. - # TODO: Remove this code once it is no longer needed. - self.__start_capture(request) - self.__wait_for_capture_done_single() - + if self.CAPTURE_THROWAWAY_SHOTS: + print "Capturing throw-away image" + self.__start_capture(request) + self.__wait_for_capture_done_single() + print "Capturing image" self.__start_capture(request) remote_fname, w, h = self.__wait_for_capture_done_single() local_fname = self.__copy_captured_files([remote_fname])[0] diff --git a/apps/CameraITS/pymodules/its/objects.py b/apps/CameraITS/pymodules/its/objects.py index f50b9d2..9183bc8 100644 --- a/apps/CameraITS/pymodules/its/objects.py +++ b/apps/CameraITS/pymodules/its/objects.py @@ -59,6 +59,48 @@ def capture_request_list(obj_list): """ return {"captureRequestList": obj_list} +def manual_capture_request(sensitivity, exp_time_ms): + """Return a capture request with everything set to manual. + + Uses identity/unit color correction, and the default tonemap curve. + + Args: + sensitivity: The sensitivity value to populate the request with. + exp_time_ms: The exposure time, in milliseconds, to populate the + request with. + + Returns: + The default manual capture request, ready to be passed to the + its.device.do_capture function. + """ + return capture_request( { + "android.control.mode": 0, + "android.control.aeMode": 0, + "android.control.awbMode": 0, + "android.control.afMode": 0, + "android.control.effectMode": 0, + "android.sensor.frameDuration": 0, + "android.sensor.sensitivity": sensitivity, + "android.sensor.exposureTime": exp_time_ms*1000*1000, + "android.colorCorrection.mode": 0, + "android.colorCorrection.transform": + int_to_rational([1,0,0, 0,1,0, 0,0,1]), + "android.colorCorrection.gains": [1,1,1,1], + "android.tonemap.mode": 1, + }) + +def auto_capture_request(): + """Return a capture request with everything set to auto. + """ + return capture_request( { + "android.control.mode": 1, + "android.control.aeMode": 1, + "android.control.awbMode": 1, + "android.control.afMode": 1, + "android.colorCorrection.mode": 1, + "android.tonemap.mode": 1, + }) + class __UnitTest(unittest.TestCase): """Run a suite of unit tests on this module. """ diff --git a/apps/CameraITS/service/Android.mk b/apps/CameraITS/service/Android.mk index 56b0233..5bb67a1 100644 --- a/apps/CameraITS/service/Android.mk +++ b/apps/CameraITS/service/Android.mk @@ -22,6 +22,8 @@ LOCAL_MODULE_TAGS := optional LOCAL_SDK_VERSION := current +LOCAL_STATIC_JAVA_LIBRARIES := android-ex-camera2 + LOCAL_SRC_FILES := \ $(call all-java-files-under, src) diff --git a/apps/CameraITS/service/src/com/android/camera2/its/ItsService.java b/apps/CameraITS/service/src/com/android/camera2/its/ItsService.java index e522202..f3a7ac5 100644 --- a/apps/CameraITS/service/src/com/android/camera2/its/ItsService.java +++ b/apps/CameraITS/service/src/com/android/camera2/its/ItsService.java @@ -21,9 +21,10 @@ import android.content.Context; import android.content.Intent; import android.graphics.ImageFormat; import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; -import android.hardware.camera2.CameraProperties; +import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.Rational; @@ -38,6 +39,10 @@ import android.os.Message; import android.util.Log; import android.view.Surface; +import com.android.ex.camera2.blocking.BlockingCameraManager; +import com.android.ex.camera2.blocking.BlockingCameraManager.BlockingOpenException; +import com.android.ex.camera2.blocking.BlockingStateListener; + import org.json.JSONObject; import java.io.File; @@ -64,6 +69,10 @@ public class ItsService extends Service { public static final int TIMEOUT_CAPTURE = 10; public static final int TIMEOUT_3A = 10; + // State transition timeouts, in ms. + private static final long TIMEOUT_IDLE_MS = 2000; + private static final long TIMEOUT_STATE_MS = 500; + private static final int MAX_CONCURRENT_READER_BUFFERS = 8; public static final String REGION_KEY = "regions"; @@ -75,9 +84,12 @@ public class ItsService extends Service { public static final String TRIGGER_AF_KEY = "af"; private CameraManager mCameraManager = null; + private HandlerThread mCameraThread = null; + private BlockingCameraManager mBlockingCameraManager = null; + private BlockingStateListener mCameraListener = null; private CameraDevice mCamera = null; private ImageReader mCaptureReader = null; - private CameraProperties mCameraProperties = null; + private CameraCharacteristics mCameraCharacteristics = null; private HandlerThread mCommandThread; private Handler mCommandHandler; @@ -114,6 +126,8 @@ public class ItsService extends Service { if (mCameraManager == null) { throw new ItsException("Failed to connect to camera manager"); } + mBlockingCameraManager = new BlockingCameraManager(mCameraManager); + mCameraListener = new BlockingStateListener(); // Open the camera device, and get its properties. String[] devices; @@ -122,12 +136,23 @@ public class ItsService extends Service { if (devices == null || devices.length == 0) { throw new ItsException("No camera devices"); } + } catch (CameraAccessException e) { + throw new ItsException("Failed to get device ID list", e); + } + + mCameraThread = new HandlerThread("ItsCameraThread"); + try { + mCameraThread.start(); + Handler cameraHandler = new Handler(mCameraThread.getLooper()); // TODO: Add support for specifying which device to open. - mCamera = mCameraManager.openCamera(devices[0]); - mCameraProperties = mCamera.getProperties(); + mCamera = mBlockingCameraManager.openCamera(devices[0], mCameraListener, + cameraHandler); + mCameraCharacteristics = mCameraManager.getCameraCharacteristics(devices[0]); } catch (CameraAccessException e) { - throw new ItsException("Failed to get device ID list"); + throw new ItsException("Failed to open camera", e); + } catch (BlockingOpenException e) { + throw new ItsException("Failed to open camera (after blocking)", e); } // Create a thread to receive images and save them. @@ -187,7 +212,10 @@ public class ItsService extends Service { mSaveThread.quit(); mSaveThread = null; } - + if (mCameraThread != null) { + mCameraThread.quitSafely(); + mCameraThread = null; + } try { mCamera.close(); } catch (Exception e) { @@ -226,15 +254,6 @@ public class ItsService extends Service { return START_STICKY; } - public void idleCamera() throws ItsException { - try { - mCamera.stopRepeating(); - mCamera.waitUntilIdle(); - } catch (CameraAccessException e) { - throw new ItsException("Error waiting for camera idle", e); - } - } - private ImageReader.OnImageAvailableListener createAvailableListener(final CaptureListener listener) { return new ImageReader.OnImageAvailableListener() { @@ -267,7 +286,7 @@ public class ItsService extends Service { private void doGetProps() throws ItsException { String fileName = ItsUtils.getMetadataFileName(0); File mdFile = ItsUtils.getOutputFile(ItsService.this, fileName); - ItsUtils.storeCameraProperties(mCameraProperties, mdFile); + ItsUtils.storeCameraCharacteristics(mCameraCharacteristics, mdFile); Log.i(PYTAG, String.format("### FILE %s", ItsUtils.getExternallyVisiblePath(ItsService.this, mdFile.toString()))); @@ -292,14 +311,12 @@ public class ItsService extends Service { throw new ItsException("Invalid URI: " + uri); } - idleCamera(); - // Start a 3A action, and wait for it to converge. // Get the converged values for each "A", and package into JSON result for caller. // 3A happens on full-res frames. - android.hardware.camera2.Size sizes[] = mCameraProperties.get( - CameraProperties.SCALER_AVAILABLE_JPEG_SIZES); + android.hardware.camera2.Size sizes[] = mCameraCharacteristics.get( + CameraCharacteristics.SCALER_AVAILABLE_JPEG_SIZES); int width = sizes[0].getWidth(); int height = sizes[0].getHeight(); int format = ImageFormat.YUV_420_888; @@ -308,6 +325,10 @@ public class ItsService extends Service { List<Surface> outputSurfaces = new ArrayList<Surface>(1); outputSurfaces.add(mCaptureReader.getSurface()); mCamera.configureOutputs(outputSurfaces); + mCameraListener.waitForState(BlockingStateListener.STATE_BUSY, + TIMEOUT_STATE_MS); + mCameraListener.waitForState(BlockingStateListener.STATE_IDLE, + TIMEOUT_IDLE_MS); // Add a listener that just recycles buffers; they aren't saved anywhere. ImageReader.OnImageAvailableListener readerListener = @@ -439,8 +460,6 @@ public class ItsService extends Service { throw new ItsException("Invalid URI: " + uri); } - idleCamera(); - // Parse the JSON to get the list of capture requests. List<CaptureRequest.Builder> requests = ItsUtils.loadRequestList(mCamera, uri); @@ -450,8 +469,8 @@ public class ItsService extends Service { // Capture full-frame images. Use the reported JPEG size rather than the sensor // size since this is more likely to be the unscaled size; the crop from sensor // size is probably for the ISP (e.g. demosaicking) rather than the encoder. - android.hardware.camera2.Size sizes[] = mCameraProperties.get( - CameraProperties.SCALER_AVAILABLE_JPEG_SIZES); + android.hardware.camera2.Size sizes[] = mCameraCharacteristics.get( + CameraCharacteristics.SCALER_AVAILABLE_JPEG_SIZES); int width = sizes[0].getWidth(); int height = sizes[0].getHeight(); int format = ImageFormat.YUV_420_888; @@ -485,6 +504,10 @@ public class ItsService extends Service { List<Surface> outputSurfaces = new ArrayList<Surface>(1); outputSurfaces.add(mCaptureReader.getSurface()); mCamera.configureOutputs(outputSurfaces); + mCameraListener.waitForState(BlockingStateListener.STATE_BUSY, + TIMEOUT_STATE_MS); + mCameraListener.waitForState(BlockingStateListener.STATE_IDLE, + TIMEOUT_IDLE_MS); ImageReader.OnImageAvailableListener readerListener = createAvailableListener(mCaptureListener); @@ -656,7 +679,7 @@ public class ItsService extends Service { String fileName = ItsUtils.getMetadataFileName( result.get(CaptureResult.SENSOR_TIMESTAMP)); File mdFile = ItsUtils.getOutputFile(ItsService.this, fileName); - ItsUtils.storeResults(mCameraProperties, request, result, mdFile); + ItsUtils.storeResults(mCameraCharacteristics, request, result, mdFile); mCaptureCallbackLatch.countDown(); } } catch (ItsException e) { @@ -669,7 +692,8 @@ public class ItsService extends Service { } @Override - public void onCaptureFailed(CameraDevice camera, CaptureRequest request) { + public void onCaptureFailed(CameraDevice camera, CaptureRequest request, + CaptureFailure failure) { mCaptureCallbackLatch.countDown(); Log.e(TAG, "Script error: capture failed"); Log.e(PYTAG, "### FAIL"); diff --git a/apps/CameraITS/service/src/com/android/camera2/its/ItsUtils.java b/apps/CameraITS/service/src/com/android/camera2/its/ItsUtils.java index 2160590..861f4ae 100644 --- a/apps/CameraITS/service/src/com/android/camera2/its/ItsUtils.java +++ b/apps/CameraITS/service/src/com/android/camera2/its/ItsUtils.java @@ -19,7 +19,7 @@ package com.android.camera2.its; import android.content.Context; import android.graphics.ImageFormat; import android.hardware.camera2.CameraDevice; -import android.hardware.camera2.CameraProperties; +import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.media.Image; @@ -61,7 +61,7 @@ public class ItsUtils { // The indent amount to use when printing the JSON objects out as strings. private static final int PPRINT_JSON_INDENT = 2; - public static void storeCameraProperties(CameraProperties props, + public static void storeCameraCharacteristics(CameraCharacteristics props, File file) throws ItsException { try { @@ -73,7 +73,7 @@ public class ItsUtils { } } - public static void storeResults(CameraProperties props, + public static void storeResults(CameraCharacteristics props, CaptureRequest request, CaptureResult result, File file) diff --git a/apps/CameraITS/tests/regress.sh b/apps/CameraITS/tests/regress.sh index 73529cb..7d434d6 100755 --- a/apps/CameraITS/tests/regress.sh +++ b/apps/CameraITS/tests/regress.sh @@ -16,15 +16,15 @@ # The tests exercised in this file all assert/exit on failure, and terminate # cleanly on success. The device is rebooted for each test, to ensure that -# a problem in one test doesn't propagate into subsequent tests. If any tests -# assert/exit (i.e. fail), this script will exit. - -set -e +# a problem in one test doesn't propagate into subsequent tests. rm -rf out mkdir -p out cd out +testcount=0 +failcount=0 + for T in \ test_3a.py \ test_black_white.py \ @@ -42,21 +42,30 @@ for T in \ test_latching.py \ test_linearity.py \ test_param_edge_mode.py \ + test_param_flash_mode.py \ + test_predicted_wb.py \ do + let testcount=testcount+1 echo "" echo "--------------------------------------------------------------------" echo "Running test: $T" echo "--------------------------------------------------------------------" python ../"$T" reboot + code=$? + if [ $code -ne 0 ]; then + let failcount=failcount+1 + echo "" + echo "###############" + echo "# Test failed #" + echo "###############" + fi echo "" done -cd .. - echo "" -echo "####################" -echo "# All tests passed #" -echo "####################" +echo "$failcount out of $testcount tests failed" echo "" +cd .. + diff --git a/apps/CameraITS/tests/test_3a.py b/apps/CameraITS/tests/test_3a.py index 2ec28b1..72ede98 100644 --- a/apps/CameraITS/tests/test_3a.py +++ b/apps/CameraITS/tests/test_3a.py @@ -17,13 +17,9 @@ import its.device def main(): """Basic test for bring-up of 3A. - Will be updated or removed once 3A is working. Simply calls the function to - initiate the 3A intent, and exits. Watch logcat (once the script exits) to - see how the 3A operation fared. + To pass, 3A must converge. Check that the returned 3A values are legal. """ - # TODO: Finish this test - with its.device.ItsSession() as cam: rect = [0,0,1,1] sens, exp, gains, xform, focus = cam.do_3a(rect, rect, rect) @@ -34,9 +30,7 @@ def main(): assert(exp > 0) assert(len(gains) == 4) assert(len(xform) == 9) - - # TODO: Uncomment assertion once AF returns the focus distance. - #assert(focus > 0) + assert(focus > 0) if __name__ == '__main__': main() diff --git a/apps/CameraITS/tests/test_3a_remote.py b/apps/CameraITS/tests/test_3a_remote.py index 216f525..6d424aa 100644 --- a/apps/CameraITS/tests/test_3a_remote.py +++ b/apps/CameraITS/tests/test_3a_remote.py @@ -27,16 +27,6 @@ def main(): """ NAME = os.path.basename(__file__).split(".")[0] - auto_req = its.objects.capture_request( { - "android.control.mode": 1, - "android.control.aeMode": 1, - "android.control.awbMode": 1, - "android.control.afMode": 1, - "android.colorCorrection.mode": 1, - "android.tonemap.mode": 1, - "android.statistics.lensShadingMapMode":1 - }) - def r2f(r): return float(r["numerator"]) / float(r["denominator"]) @@ -47,8 +37,17 @@ def main(): # TODO: Test for 3A convergence, and exit this test once converged. + triggered = False while True: - fname, w, h, md_obj = cam.do_capture(auto_req) + req = its.objects.auto_capture_request() + req["captureRequest"]["android.statistics.lensShadingMapMode"] = 1 + req['captureRequest']['android.control.aePrecaptureTrigger'] = ( + 0 if triggered else 1) + req['captureRequest']['android.control.afTrigger'] = ( + 0 if triggered else 1) + triggered = True + + fname, w, h, md_obj = cam.do_capture(req) cap_res = md_obj["captureResult"] ae_state = cap_res["android.control.aeState"] @@ -58,6 +57,8 @@ def main(): transform = cap_res["android.colorCorrection.transform"] exp_time = cap_res['android.sensor.exposureTime'] lsc_map = cap_res["android.statistics.lensShadingMap"] + foc_dist = cap_res['android.lens.focusDistance'] + foc_range = cap_res['android.lens.focusRange'] print "States (AE,AWB,AF):", ae_state, awb_state, af_state print "Gains:", gains @@ -66,6 +67,7 @@ def main(): print "AF region:", cap_res['android.control.afRegions'] print "AWB region:", cap_res['android.control.awbRegions'] print "LSC map:", w_map, h_map, lsc_map[:8] + print "Focus (dist,range):", foc_dist, foc_range print "" if __name__ == '__main__': diff --git a/apps/CameraITS/tests/test_black_level.py b/apps/CameraITS/tests/test_black_level.py index 440b080..cb208d7 100644 --- a/apps/CameraITS/tests/test_black_level.py +++ b/apps/CameraITS/tests/test_black_level.py @@ -40,16 +40,6 @@ def main(): # Only check the center part where LSC has little effects. R = 200 - req = its.objects.capture_request( { - "android.blackLevel.lock": True, - "android.control.mode": 0, - "android.control.aeMode": 0, - "android.control.awbMode": 0, - "android.control.afMode": 0, - "android.sensor.frameDuration": 0, - "android.sensor.exposureTime": 1*1000*1000 - }) - # The most frequent pixel value in each image; assume this is the black # level, since the images are all dark (shot with the lens covered). ymodes = [] @@ -66,10 +56,18 @@ def main(): print "Sensitivities:", sensitivities for si, s in enumerate(sensitivities): for rep in xrange(NUM_REPEAT): + req = its.objects.manual_capture_request(100, 1) + req["captureRequest"]["android.blackLevel.lock"] = True req["captureRequest"]["android.sensor.sensitivity"] = s fname, w, h, cap_md = cam.do_capture(req) yimg,uimg,vimg = its.image.load_yuv420_to_yuv_planes(fname,w,h) + # Magnify the noise in saved images to help visualize. + its.image.write_image(yimg * 2, + "%s_s=%05d_y.jpg" % (NAME, s), True) + its.image.write_image(numpy.absolute(uimg - 0.5) * 2, + "%s_s=%05d_u.jpg" % (NAME, s), True) + yimg = yimg[w/2-R:w/2+R, h/2-R:h/2+R] uimg = uimg[w/4-R/2:w/4+R/2, w/4-R/2:w/4+R/2] vimg = vimg[w/4-R/2:w/4+R/2, w/4-R/2:w/4+R/2] @@ -95,8 +93,6 @@ def main(): print "U black levels:", umodes print "V black levels:", vmodes - assert(all([ymodes[i] == ymodes[0] for i in range(len(ymodes))])) - if __name__ == '__main__': main() diff --git a/apps/CameraITS/tests/test_black_white.py b/apps/CameraITS/tests/test_black_white.py index 2b7b2d0..7886d75 100644 --- a/apps/CameraITS/tests/test_black_white.py +++ b/apps/CameraITS/tests/test_black_white.py @@ -30,17 +30,6 @@ def main(): """ NAME = os.path.basename(__file__).split(".")[0] - req = its.objects.capture_request( { - "android.sensor.exposureTime": 10*1000, # 0.01ms - "android.sensor.sensitivity": 100, - "android.sensor.frameDuration": 0, - "android.control.mode": 0, - "android.control.aeMode": 0, - "android.control.awbMode": 0, - "android.control.afMode": 0, - "android.control.effectMode": 0, - }) - r_means = [] g_means = [] b_means = [] @@ -48,6 +37,7 @@ def main(): with its.device.ItsSession() as cam: # Take a shot with very low ISO and exposure time. Expect it to # be black. + req = its.objects.manual_capture_request(100, 0.1) fname, w, h, cap_md = cam.do_capture(req) img = its.image.load_yuv420_to_rgb_image(fname, w, h) its.image.write_image(img, "%s_black.jpg" % (NAME)) @@ -60,8 +50,7 @@ def main(): # Take a shot with very high ISO and exposure time. Expect it to # be black. - req["captureRequest"]["android.sensor.sensitivity"] = 10000 - req["captureRequest"]["android.sensor.exposureTime"] = 1000*1000*1000 + req = its.objects.manual_capture_request(10000, 1000) fname, w, h, cap_md = cam.do_capture(req) img = its.image.load_yuv420_to_rgb_image(fname, w, h) its.image.write_image(img, "%s_white.jpg" % (NAME)) diff --git a/apps/CameraITS/tests/test_capture_result.py b/apps/CameraITS/tests/test_capture_result.py index fbc1bfc..d0ff74e 100644 --- a/apps/CameraITS/tests/test_capture_result.py +++ b/apps/CameraITS/tests/test_capture_result.py @@ -27,28 +27,15 @@ def main(): """ NAME = os.path.basename(__file__).split(".")[0] - # TODO: Query the allowable tonemap curve sizes; here, it's hardcoded to - # a length=64 list of tuples. The max allowed length should be inside the - # camera properties object. - L = 32 - LM1 = float(L-1) - - manual_tonemap = sum([[i/LM1, i/LM1] for i in range(L)], []) + manual_tonemap = [0,0, 1,1] # Linear manual_transform = its.objects.int_to_rational([1,2,3, 4,5,6, 7,8,9]) manual_gains = [1,2,3,4] manual_region = [8,8,128,128] manual_exp_time = 100*1000*1000 manual_sensitivity = 100 - auto_req = its.objects.capture_request( { - "android.control.mode": 1, - "android.control.aeMode": 1, - "android.control.awbMode": 1, - "android.control.afMode": 1, - "android.colorCorrection.mode": 1, - "android.tonemap.mode": 1, - "android.statistics.lensShadingMapMode":1 - }) + auto_req = its.objects.auto_capture_request() + auto_req["captureRequest"]["android.statistics.lensShadingMapMode"] = 1 manual_req = its.objects.capture_request( { "android.control.mode": 0, @@ -97,13 +84,20 @@ def main(): matplotlib.pyplot.savefig("%s_plot_lsc_%s.png" % (NAME, name)) def test_auto(cam, w_map, h_map): + # Get 3A lock first, so the auto values in the capture result are + # populated properly. + rect = [0,0,1,1] + cam.do_3a(rect, rect, rect, True, True, False) + fname, w, h, md_obj = cam.do_capture(auto_req) cap_res = md_obj["captureResult"] gains = cap_res["android.colorCorrection.gains"] transform = cap_res["android.colorCorrection.transform"] exp_time = cap_res['android.sensor.exposureTime'] lsc_map = cap_res["android.statistics.lensShadingMap"] + ctrl_mode = cap_res["android.control.mode"] + print "Control mode:", ctrl_mode print "Gains:", gains print "Transform:", [r2f(t) for t in transform] print "AE region:", cap_res['android.control.aeRegions'] @@ -111,6 +105,8 @@ def main(): print "AWB region:", cap_res['android.control.awbRegions'] print "LSC map:", w_map, h_map, lsc_map[:8] + assert(ctrl_mode == 1) + # Color correction gain and transform must be valid. assert(len(gains) == 4) assert(len(transform) == 9) @@ -149,15 +145,23 @@ def main(): cap_res["android.tonemap.curveBlue"]] exp_time = cap_res['android.sensor.exposureTime'] lsc_map = cap_res["android.statistics.lensShadingMap"] + pred_gains = cap_res["android.statistics.predictedColorGains"] + pred_transform = cap_res["android.statistics.predictedColorTransform"] + ctrl_mode = cap_res["android.control.mode"] + print "Control mode:", ctrl_mode print "Gains:", gains print "Transform:", [r2f(t) for t in transform] + print "Predicted gains:", pred_gains + print "Predicted transform:", [r2f(t) for t in pred_transform] print "Tonemap:", curves[0][1::16] print "AE region:", cap_res['android.control.aeRegions'] print "AF region:", cap_res['android.control.afRegions'] print "AWB region:", cap_res['android.control.awbRegions'] print "LSC map:", w_map, h_map, lsc_map[:8] + assert(ctrl_mode == 0) + # Color correction gain and transform must be valid. # Color correction gains and transform should be the same size and # values as the manually set values. @@ -168,23 +172,25 @@ def main(): assert(all([is_close_rational(transform[i], manual_transform[i]) for i in xrange(9)])) + # The predicted gains and transform must also be valid. + assert(len(pred_gains) == 4) + assert(len(pred_transform) == 9) + # Tonemap must be valid. - # The returned tonemap can be interpolated from the provided values. + # The returned tonemap must be linear. for c in curves: assert(len(c) > 0) - for i, val in enumerate(c): - ii = int(math.floor( - ((i+0.5) / float(len(c))*float(len(manual_tonemap))))) - assert(is_close_float(c[i], manual_tonemap[ii])) + assert(all([is_close_float(c[i], c[i+1]) + for i in xrange(0,len(c),2)])) # Exposure time must be close to the requested exposure time. assert(is_close_float(exp_time/1000000.0, manual_exp_time/1000000.0)) - # 3A regions must be valid, and must match the manual regions. - # TODO: Uncomment these assertions once the bug is fixed. - #assert(cap_res['android.control.aeRegions'][:4] == manual_region) - #assert(cap_res['android.control.afRegions'][:4] == manual_region) - #assert(cap_res['android.control.awbRegions'][:4] == manual_region) + # 3A regions must be valid. They don't need to actually match what was + # requesed, since the SOC may not support those regions exactly. + assert(len(cap_res['android.control.aeRegions']) == 5) + assert(len(cap_res['android.control.afRegions']) == 5) + assert(len(cap_res['android.control.awbRegions']) == 5) # Lens shading map must be valid. assert(w_map > 0 and h_map > 0 and w_map * h_map * 4 == len(lsc_map)) @@ -193,8 +199,7 @@ def main(): # Lens shading map must take into account the manual color correction # settings. Test this by ensuring that the map is different between # the auto and manual test cases. - # TODO: Uncomment these assertions once the bug is fixed. - #assert(lsc_map != lsc_map_auto) + assert(lsc_map != lsc_map_auto) draw_lsc_plot(w_map, h_map, lsc_map, "manual") diff --git a/apps/CameraITS/tests/test_exposure.py b/apps/CameraITS/tests/test_exposure.py index 40861ed..c01ce47 100644 --- a/apps/CameraITS/tests/test_exposure.py +++ b/apps/CameraITS/tests/test_exposure.py @@ -35,14 +35,6 @@ def main(): THRESHOLD_MAX_LEVEL = 0.9 THRESHOLD_MAX_ABS_GRAD = 0.001 - req = its.objects.capture_request( { - "android.control.mode": 0, - "android.control.aeMode": 0, - "android.control.awbMode": 0, - "android.control.afMode": 0, - "android.sensor.frameDuration": 0 - }) - mults = range(1, 100, 9) r_means = [] g_means = [] @@ -50,8 +42,7 @@ def main(): with its.device.ItsSession() as cam: for m in mults: - req["captureRequest"]["android.sensor.sensitivity"] = 100*m - req["captureRequest"]["android.sensor.exposureTime"] = 40*1000*1000/m + req = its.objects.manual_capture_request(100*m, 40.0/m) fname, w, h, md_obj = cam.do_capture(req) img = its.image.load_yuv420_to_rgb_image(fname, w, h) its.image.write_image(img, "%s_mult=%02d.jpg" % (NAME, m)) diff --git a/apps/CameraITS/tests/test_formats.py b/apps/CameraITS/tests/test_formats.py index d05443b..b9b8ae1 100644 --- a/apps/CameraITS/tests/test_formats.py +++ b/apps/CameraITS/tests/test_formats.py @@ -26,7 +26,7 @@ def main(): with its.device.ItsSession() as cam: props = cam.get_camera_properties() for size in props['android.scaler.availableProcessedSizes']: - req = its.objects.capture_request({"android.control.aeMode": 0}) + req = its.objects.manual_capture_request(100,10) req["outputSurface"] = size req["outputSurface"]["format"] = "yuv" fname, w, h, cap_md = cam.do_capture(req) @@ -35,7 +35,7 @@ def main(): assert(os.path.getsize(fname) == w*h*3/2) print "Successfully captured YUV %dx%d" % (w, h) for size in props['android.scaler.availableJpegSizes']: - req = its.objects.capture_request({"android.control.aeMode": 0}) + req = its.objects.manual_capture_request(100,10) req["outputSurface"] = size req["outputSurface"]["format"] = "jpg" fname, w, h, cap_md = cam.do_capture(req) diff --git a/apps/CameraITS/tests/test_jpeg.py b/apps/CameraITS/tests/test_jpeg.py index 0e2db08..870fda4 100644 --- a/apps/CameraITS/tests/test_jpeg.py +++ b/apps/CameraITS/tests/test_jpeg.py @@ -28,20 +28,11 @@ def main(): THRESHOLD_MAX_RMS_DIFF = 0.1 - req = its.objects.capture_request( { - "android.control.mode": 0, - "android.control.aeMode": 0, - "android.control.awbMode": 0, - "android.control.afMode": 0, - "android.sensor.frameDuration": 0, - "android.sensor.sensitivity": 100, - "android.sensor.exposureTime": 10*1000*1000 - }) - with its.device.ItsSession() as cam: props = cam.get_camera_properties() # YUV + req = its.objects.manual_capture_request(100,100) size = props['android.scaler.availableProcessedSizes'][0] req["outputSurface"] = size req["outputSurface"]["format"] = "yuv" @@ -52,6 +43,7 @@ def main(): rgb0 = its.image.compute_image_means(tile) # JPEG + req = its.objects.manual_capture_request(100,100) size = props['android.scaler.availableJpegSizes'][0] req["outputSurface"] = size req["outputSurface"]["format"] = "jpg" diff --git a/apps/CameraITS/tests/test_latching.py b/apps/CameraITS/tests/test_latching.py index d985b2b..14dbed8 100644 --- a/apps/CameraITS/tests/test_latching.py +++ b/apps/CameraITS/tests/test_latching.py @@ -30,41 +30,23 @@ def main(): """ NAME = os.path.basename(__file__).split(".")[0] - req = { - "android.control.mode": 0, - "android.control.aeMode": 0, - "android.control.awbMode": 0, - "android.control.afMode": 0, - "android.sensor.frameDuration": 0, - "android.sensor.sensitivity": 150, - "android.sensor.exposureTime": 10*1000*1000 - } - - reqs = its.objects.capture_request_list([copy.deepcopy(req)]*2) - - req["android.sensor.sensitivity"] *= 10 - reqs["captureRequestList"] += [copy.deepcopy(req)]*2 - - req["android.sensor.exposureTime"] /= 10 - reqs["captureRequestList"] += [copy.deepcopy(req)]*2 - - req["android.sensor.exposureTime"] *= 10 - reqs["captureRequestList"] += [copy.deepcopy(req)]*2 - - req["android.sensor.sensitivity"] /= 10 - reqs["captureRequestList"] += [copy.deepcopy(req)]*2 - - req["android.sensor.sensitivity"] *= 10 - reqs["captureRequestList"] += [copy.deepcopy(req)]*2 - - req["android.sensor.exposureTime"] /= 10 - reqs["captureRequestList"] += [copy.deepcopy(req)]*2 - - req["android.sensor.exposureTime"] *= 10 - reqs["captureRequestList"] += [copy.deepcopy(req)]*2 - - req["android.sensor.sensitivity"] /= 10 - reqs["captureRequestList"] += [copy.deepcopy(req)]*2 + S = 150 # Sensitivity + E = 10 # Exposure time, ms + + reqs = its.objects.capture_request_list([ + its.objects.manual_capture_request(S, E )["captureRequest"], + its.objects.manual_capture_request(S, E )["captureRequest"], + its.objects.manual_capture_request(S*8,E )["captureRequest"], + its.objects.manual_capture_request(S*8,E )["captureRequest"], + its.objects.manual_capture_request(S, E )["captureRequest"], + its.objects.manual_capture_request(S, E )["captureRequest"], + its.objects.manual_capture_request(S, E*8)["captureRequest"], + its.objects.manual_capture_request(S, E )["captureRequest"], + its.objects.manual_capture_request(S*8,E )["captureRequest"], + its.objects.manual_capture_request(S, E )["captureRequest"], + its.objects.manual_capture_request(S, E*8)["captureRequest"], + its.objects.manual_capture_request(S, E )["captureRequest"], + ]) r_means = [] g_means = [] diff --git a/apps/CameraITS/tests/test_param_exposure_time.py b/apps/CameraITS/tests/test_param_exposure_time.py index ddeb304..6b91206 100644 --- a/apps/CameraITS/tests/test_param_exposure_time.py +++ b/apps/CameraITS/tests/test_param_exposure_time.py @@ -35,7 +35,7 @@ def main(): "android.control.awbMode": 0, "android.control.afMode": 0, "android.sensor.frameDuration": 0, - "android.sensor.sensitivity": 100 + "android.sensor.sensitivity": 200 }) exposures = range(1,101,20) # ms diff --git a/apps/CameraITS/tests/test_param_exposure_time_burst.py b/apps/CameraITS/tests/test_param_exposure_time_burst.py new file mode 100644 index 0000000..fda31bf --- /dev/null +++ b/apps/CameraITS/tests/test_param_exposure_time_burst.py @@ -0,0 +1,43 @@ +# 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. + +import its.image +import its.device +import its.objects +import pylab +import os.path +import matplotlib +import matplotlib.pyplot + +def main(): + """Test that the android.sensor.exposureTime parameter is applied properly + within a burst. Inspects the output metadata. + """ + NAME = os.path.basename(__file__).split(".")[0] + + exp_times = range(1, 100, 9) + reqs = its.objects.capture_request_list([ + its.objects.manual_capture_request(100,e)["captureRequest"] + for e in exp_times]) + + with its.device.ItsSession() as cam: + fnames, w, h, cap_mds = cam.do_capture(reqs) + for i,md in enumerate(cap_mds): + e_req = exp_times[i]*1000*1000 + e_res = md["captureResult"]["android.sensor.exposureTime"] + print e_req, e_res + +if __name__ == '__main__': + main() + diff --git a/apps/CameraITS/tests/test_param_flash_mode.py b/apps/CameraITS/tests/test_param_flash_mode.py index cfc1a0d..091a896 100644 --- a/apps/CameraITS/tests/test_param_flash_mode.py +++ b/apps/CameraITS/tests/test_param_flash_mode.py @@ -25,23 +25,30 @@ def main(): """ NAME = os.path.basename(__file__).split(".")[0] - req = its.objects.capture_request( { - "android.control.mode": 0, - "android.control.aeMode": 0, - "android.control.awbMode": 0, - "android.control.afMode": 0, - "android.sensor.frameDuration": 0, - "android.sensor.exposureTime": 100*1000*1000, - "android.sensor.sensitivity": 100 - }) + req = its.objects.auto_capture_request() + + flash_modes_reported = [] + flash_states_reported = [] with its.device.ItsSession() as cam: for f in [0,1,2]: req["captureRequest"]["android.flash.mode"] = f fname, w, h, cap_md = cam.do_capture(req) + flash_modes_reported.append( + cap_md["captureResult"]["android.flash.mode"]) + flash_states_reported.append( + cap_md["captureResult"]["android.flash.state"]) img = its.image.load_yuv420_to_rgb_image(fname, w, h) its.image.write_image(img, "%s_mode=%d.jpg" % (NAME, f)) + assert(flash_modes_reported == [0,1,2]) + + # TODO: Add check on flash_states_reported values. + + # TODO: Add an image check on the brightness of the captured shots, as well + # as the exposure values in the capture result, to test that flash was + # fired as expected (i.e.) on the shots it was expected to be fired for. + if __name__ == '__main__': main() diff --git a/apps/CameraITS/tests/test_param_noise_reduction.py b/apps/CameraITS/tests/test_param_noise_reduction.py index d43d4ed..63d663a 100644 --- a/apps/CameraITS/tests/test_param_noise_reduction.py +++ b/apps/CameraITS/tests/test_param_noise_reduction.py @@ -32,7 +32,7 @@ def main(): """ NAME = os.path.basename(__file__).split(".")[0] - THRESHOLD_MIN_VARIANCE_RATIO = 0.5 + THRESHOLD_MIN_VARIANCE_RATIO = 0.7 req = its.objects.capture_request( { "android.control.mode": 0, @@ -48,6 +48,8 @@ def main(): # Reference (baseline) variance for each of Y,U,V. ref_variance = [] + nr_modes_reported = [] + with its.device.ItsSession() as cam: # NR mode 0 with low gain req["captureRequest"]["android.noiseReduction.mode"] = 0 @@ -66,10 +68,12 @@ def main(): for i in range(3): # NR modes 0, 1, 2 with high gain req["captureRequest"]["android.noiseReduction.mode"] = i - req["captureRequest"]["android.sensor.sensitivity"] = 1600 + req["captureRequest"]["android.sensor.sensitivity"] = 100*16 req["captureRequest"]["android.sensor.exposureTime"] = ( 20*1000*1000/16) fname, w, h, md_obj = cam.do_capture(req) + nr_modes_reported.append( + md_obj["captureResult"]["android.noiseReduction.mode"]) its.image.write_image( its.image.load_yuv420_to_rgb_image(fname, w, h), "%s_high_gain_nr=%d.jpg" % (NAME, i)) @@ -85,6 +89,8 @@ def main(): pylab.plot(range(3), variances[j], "rgb"[j]) matplotlib.pyplot.savefig("%s_plot_variances.png" % (NAME)) + assert(nr_modes_reported == [0,1,2]) + # Check that the variance of the NR=0 image is much higher than for the # NR=1 and NR=2 images. for j in range(3): diff --git a/apps/CameraITS/tests/test_param_sensitivity.py b/apps/CameraITS/tests/test_param_sensitivity.py index 88be747..bff0c88 100644 --- a/apps/CameraITS/tests/test_param_sensitivity.py +++ b/apps/CameraITS/tests/test_param_sensitivity.py @@ -37,7 +37,7 @@ def main(): "android.control.awbMode": 0, "android.control.afMode": 0, "android.sensor.frameDuration": 0, - "android.sensor.exposureTime": 1*1000*1000 + "android.sensor.exposureTime": 2*1000*1000 }) sensitivities = None diff --git a/apps/CameraITS/tests/test_param_sensitivity_burst.py b/apps/CameraITS/tests/test_param_sensitivity_burst.py new file mode 100644 index 0000000..de1119e --- /dev/null +++ b/apps/CameraITS/tests/test_param_sensitivity_burst.py @@ -0,0 +1,43 @@ +# 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. + +import its.image +import its.device +import its.objects +import pylab +import os.path +import matplotlib +import matplotlib.pyplot + +def main(): + """Test that the android.sensor.sensitivity parameter is applied properly + within a burst. Inspects the output metadata. + """ + NAME = os.path.basename(__file__).split(".")[0] + + sensitivities = range(350, 400, 7) + reqs = its.objects.capture_request_list([ + its.objects.manual_capture_request(s,10)["captureRequest"] + for s in sensitivities]) + + with its.device.ItsSession() as cam: + fnames, w, h, cap_mds = cam.do_capture(reqs) + for i,md in enumerate(cap_mds): + s_req = sensitivities[i] + s_res = md["captureResult"]["android.sensor.sensitivity"] + print s_req, s_res + +if __name__ == '__main__': + main() + diff --git a/apps/CameraITS/tests/test_param_tonemap_mode.py b/apps/CameraITS/tests/test_param_tonemap_mode.py index fc7186e..4000292 100644 --- a/apps/CameraITS/tests/test_param_tonemap_mode.py +++ b/apps/CameraITS/tests/test_param_tonemap_mode.py @@ -40,17 +40,6 @@ def main(): L = 32 LM1 = float(L-1) - req = its.objects.capture_request( { - "android.tonemap.mode": 0, - "android.control.mode": 0, - "android.control.aeMode": 0, - "android.control.awbMode": 0, - "android.control.afMode": 0, - "android.sensor.sensitivity": 100, - "android.sensor.exposureTime": 50*1000*1000, - "android.sensor.frameDuration": 0 - }) - with its.device.ItsSession() as cam: # Test 1: that the tonemap curves have the expected effect. Take two @@ -61,6 +50,8 @@ def main(): rgb_means = [] for n in [0,1]: + req = its.objects.manual_capture_request(100,50) + req["captureRequest"]["android.tonemap.mode"] = 0 req["captureRequest"]["android.tonemap.curveRed"] = ( sum([[i/LM1, (1+0.5*n)*i/LM1] for i in range(L)], [])) req["captureRequest"]["android.tonemap.curveGreen"] = ( @@ -87,6 +78,8 @@ def main(): for size in [32,64]: m = float(size-1) curve = sum([[i/m, i/m] for i in range(size)], []) + req = its.objects.manual_capture_request(100,50) + req["captureRequest"]["android.tonemap.mode"] = 0 req["captureRequest"]["android.tonemap.curveRed"] = curve req["captureRequest"]["android.tonemap.curveGreen"] = curve req["captureRequest"]["android.tonemap.curveBlue"] = curve diff --git a/apps/CameraITS/tests/test_predicted_wb.py b/apps/CameraITS/tests/test_predicted_wb.py new file mode 100644 index 0000000..3aade2b --- /dev/null +++ b/apps/CameraITS/tests/test_predicted_wb.py @@ -0,0 +1,107 @@ +# 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. + +import its.image +import its.device +import its.objects +import os.path +import pprint +import math +import numpy +import matplotlib.pyplot +import mpl_toolkits.mplot3d + +def main(): + """Test that valid data comes back in CaptureResult objects. + """ + NAME = os.path.basename(__file__).split(".")[0] + + def r2f(r): + return float(r["numerator"]) / float(r["denominator"]) + + if not its.device.reboot_device_on_argv(): + its.device.reboot_device() + + # Run a first pass, which starts with a 3A convergence step. + with its.device.ItsSession() as cam: + # Get 3A lock first, so the auto values in the capture result are + # populated properly. + r = [0,0,1,1] + sens,exp,awb_gains,awb_transform,_ = cam.do_3a(r,r,r,True,True,False) + + # Capture an auto shot using the converged 3A. + req = its.objects.auto_capture_request() + fname, w, h, md_obj = cam.do_capture(req) + img = its.image.load_yuv420_to_rgb_image(fname, w, h) + its.image.write_image(img, "%s_n=1_pass=1_auto.jpg" % (NAME)) + cap_res = md_obj["captureResult"] + auto_gains = cap_res["android.colorCorrection.gains"] + auto_transform = cap_res["android.colorCorrection.transform"] + + # Capture a request using default (unit/identify) gains, and get the + # predicted gains and transform. + req = its.objects.manual_capture_request(sens, exp/(1000.0*1000.0)) + fname, w, h, md_obj = cam.do_capture(req) + img = its.image.load_yuv420_to_rgb_image(fname, w, h) + its.image.write_image(img, "%s_n=2_pass=1_identity.jpg" % (NAME)) + cap_res = md_obj["captureResult"] + pred_gains_1 = cap_res["android.statistics.predictedColorGains"] + pred_transform_1 = cap_res["android.statistics.predictedColorTransform"] + + # Capture a request using the predicted gains/transform. + req = its.objects.manual_capture_request(sens, exp/(1000.0*1000.0)) + req["captureRequest"]["android.colorCorrection.transform"] = \ + pred_transform_1 + req["captureRequest"]["android.colorCorrection.gains"] = pred_gains_1 + fname, w, h, md_obj = cam.do_capture(req) + img = its.image.load_yuv420_to_rgb_image(fname, w, h) + its.image.write_image(img, "%s_n=3_pass=1_predicted.jpg" % (NAME)) + + print "Pass 1 metering gains:", awb_gains + print "Pass 1 metering transform:", awb_transform + print "Pass 1 auto shot gains:", auto_gains + print "Pass 1 auto shot transform:", [r2f(t) for t in auto_transform] + print "Pass 1 predicted gains:", pred_gains_1 + print "Pass 1 predicted transform:", [r2f(t) for t in pred_transform_1] + + if not its.device.reboot_device_on_argv(): + its.device.reboot_device() + + # Run a second pass after rebooting that doesn't start with 3A convergence. + with its.device.ItsSession() as cam: + # Capture a request using default (unit/identify) gains, and get the + # predicted gains and transform. + req = its.objects.manual_capture_request(sens, exp/(1000.0*1000.0)) + fname, w, h, md_obj = cam.do_capture(req) + img = its.image.load_yuv420_to_rgb_image(fname, w, h) + its.image.write_image(img, "%s_n=4_pass=2_identity.jpg" % (NAME)) + cap_res = md_obj["captureResult"] + pred_gains_2 = cap_res["android.statistics.predictedColorGains"] + pred_transform_2 = cap_res["android.statistics.predictedColorTransform"] + + # Capture a request using the predicted gains/transform. + req = its.objects.manual_capture_request(sens, exp/(1000.0*1000.0)) + req["captureRequest"]["android.colorCorrection.transform"] = \ + pred_transform_2 + req["captureRequest"]["android.colorCorrection.gains"] = pred_gains_2 + fname, w, h, md_obj = cam.do_capture(req) + img = its.image.load_yuv420_to_rgb_image(fname, w, h) + its.image.write_image(img, "%s_n=5_pass=2_predicted.jpg" % (NAME)) + + print "Pass 2 predicted gains:", pred_gains_2 + print "Pass 2 predicted transform:", [r2f(t) for t in pred_transform_2] + +if __name__ == '__main__': + main() + diff --git a/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java b/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java index 6500823..1c2d5c7 100644 --- a/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java +++ b/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java @@ -1458,7 +1458,13 @@ public class TestingCamera extends Activity mRecordHandoffCheckBox.setEnabled(true); mRecordToggle.setChecked(false); if (mRecorder != null) { - mRecorder.stop(); + try { + mRecorder.stop(); + } catch (RuntimeException e) { + // this can happen if there were no frames received by recorder + logE("Could not create output file"); + error = true; + } if (mRecordHandoffCheckBox.isChecked()) { mState = CAMERA_UNINITIALIZED; diff --git a/apps/TestingCamera2/Android.mk b/apps/TestingCamera2/Android.mk index cdc27ff..868de25 100644 --- a/apps/TestingCamera2/Android.mk +++ b/apps/TestingCamera2/Android.mk @@ -22,6 +22,8 @@ LOCAL_MODULE_TAGS := optional LOCAL_SDK_VERSION := current +LOCAL_STATIC_JAVA_LIBRARIES := android-ex-camera2 + LOCAL_SRC_FILES := \ $(call all-java-files-under, src) \ $(call all-renderscript-files-under, src) diff --git a/apps/TestingCamera2/AndroidManifest.xml b/apps/TestingCamera2/AndroidManifest.xml index 3da7d2a..0bc7f4e 100644 --- a/apps/TestingCamera2/AndroidManifest.xml +++ b/apps/TestingCamera2/AndroidManifest.xml @@ -34,8 +34,8 @@ > <activity android:name=".TestingCamera2" - android:label="@string/app_name" - android:screenOrientation="landscape"> + android:configChanges="orientation|keyboardHidden|screenSize" + android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> diff --git a/apps/TestingCamera2/src/com/android/testingcamera2/CameraOps.java b/apps/TestingCamera2/src/com/android/testingcamera2/CameraOps.java index 855f6c8..0b0470d 100644 --- a/apps/TestingCamera2/src/com/android/testingcamera2/CameraOps.java +++ b/apps/TestingCamera2/src/com/android/testingcamera2/CameraOps.java @@ -22,18 +22,23 @@ import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.CameraProperties; +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.Handler; +import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; +import com.android.ex.camera2.blocking.BlockingCameraManager; +import com.android.ex.camera2.blocking.BlockingCameraManager.BlockingOpenException; +import com.android.ex.camera2.blocking.BlockingStateListener; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -46,14 +51,18 @@ public class CameraOps { private static final String TAG = "CameraOps"; - private final Thread mOpsThread; - private Handler mOpsHandler; + private final HandlerThread mOpsThread; + private final Handler mOpsHandler; private final CameraManager mCameraManager; + private final BlockingCameraManager mBlockingCameraManager; + private final BlockingStateListener mDeviceListener = + new BlockingStateListener(); + private CameraDevice mCamera; private ImageReader mCaptureReader; - private CameraProperties mCameraProperties; + private CameraCharacteristics mCameraCharacteristics; private int mEncodingBitRate; @@ -74,6 +83,10 @@ public class CameraOps { private static final Size DEFAULT_SIZE = new Size(640, 480); private static final Size HIGH_RESOLUTION_SIZE = new Size(1920, 1080); + private static final long IDLE_WAIT_MS = 2000; + // General short wait timeout for most state transitions + private static final long STATE_WAIT_MS = 500; + private int mStatus = STATUS_UNINITIALIZED; CameraRecordingStream mRecordingStream; @@ -84,28 +97,16 @@ public class CameraOps { } } - private class OpsHandler extends Handler { - @Override - public void handleMessage(Message msg) { - - } - } - private CameraOps(Context ctx) throws ApiFailureException { mCameraManager = (CameraManager) ctx.getSystemService(Context.CAMERA_SERVICE); if (mCameraManager == null) { throw new ApiFailureException("Can't connect to camera manager!"); } + mBlockingCameraManager = new BlockingCameraManager(mCameraManager); - mOpsThread = new Thread(new Runnable() { - @Override - public void run() { - Looper.prepare(); - mOpsHandler = new OpsHandler(); - Looper.loop(); - } - }, "CameraOpsThread"); + mOpsThread = new HandlerThread("CameraOpsThread"); mOpsThread.start(); + mOpsHandler = new Handler(mOpsThread.getLooper()); mRecordingStream = new CameraRecordingStream(); mStatus = STATUS_OK; @@ -130,29 +131,18 @@ public class CameraOps { mCameraManager.addAvailabilityListener(listener, mOpsHandler); } - public CameraProperties getCameraProperties() { - checkOk(); - if (mCameraProperties == null) { - throw new IllegalStateException("CameraProperties is not available"); - } - return mCameraProperties; - } - - public void openDevice(String cameraId) - throws CameraAccessException, ApiFailureException { + public CameraCharacteristics getCameraCharacteristics() { checkOk(); - - if (mCamera != null) { - throw new IllegalStateException("Already have open camera device"); + if (mCameraCharacteristics == null) { + throw new IllegalStateException("CameraCharacteristics is not available"); } - - mCamera = mCameraManager.openCamera(cameraId); + return mCameraCharacteristics; } public void closeDevice() throws ApiFailureException { checkOk(); - mCameraProperties = null; + mCameraCharacteristics = null; if (mCamera == null) return; @@ -172,16 +162,27 @@ public class CameraOps { if (devices == null || devices.length == 0) { throw new ApiFailureException("no devices"); } - mCamera = mCameraManager.openCamera(devices[0]); - mCameraProperties = mCamera.getProperties(); + mCamera = mBlockingCameraManager.openCamera(devices[0], + mDeviceListener, mOpsHandler); + mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCamera.getId()); } catch (CameraAccessException e) { throw new ApiFailureException("open failure", e); + } catch (BlockingOpenException e) { + throw new ApiFailureException("open async failure", e); } } mStatus = STATUS_OK; } + private void configureOutputs(List<Surface> outputs) throws CameraAccessException { + mCamera.configureOutputs(outputs); + mDeviceListener.waitForState(BlockingStateListener.STATE_BUSY, + STATE_WAIT_MS); + mDeviceListener.waitForState(BlockingStateListener.STATE_IDLE, + IDLE_WAIT_MS); + } + /** * Set up SurfaceView dimensions for camera preview */ @@ -189,13 +190,14 @@ public class CameraOps { minimalOpenCamera(); try { - CameraProperties properties = mCamera.getProperties(); + CameraCharacteristics properties = + mCameraManager.getCameraCharacteristics(mCamera.getId()); Size[] previewSizes = null; Size sz = DEFAULT_SIZE; if (properties != null) { previewSizes = properties.get( - CameraProperties.SCALER_AVAILABLE_PROCESSED_SIZES); + CameraCharacteristics.SCALER_AVAILABLE_PROCESSED_SIZES); } if (previewSizes != null && previewSizes.length != 0 && @@ -231,17 +233,15 @@ public class CameraOps { public void minimalPreview(SurfaceHolder previewHolder) throws ApiFailureException { minimalOpenCamera(); + if (mPreviewSurface == null) { throw new ApiFailureException("Preview surface is not created"); } try { - mCamera.stopRepeating(); - mCamera.waitUntilIdle(); - List<Surface> outputSurfaces = new ArrayList<Surface>(/*capacity*/1); outputSurfaces.add(mPreviewSurface); - mCamera.configureOutputs(outputSurfaces); + configureOutputs(outputSurfaces); CaptureRequest.Builder previewBuilder; mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); @@ -259,14 +259,12 @@ public class CameraOps { minimalOpenCamera(); try { - mCamera.stopRepeating(); - mCamera.waitUntilIdle(); - - CameraProperties properties = mCamera.getProperties(); + CameraCharacteristics properties = + mCameraManager.getCameraCharacteristics(mCamera.getId()); Size[] jpegSizes = null; if (properties != null) { jpegSizes = properties.get( - CameraProperties.SCALER_AVAILABLE_JPEG_SIZES); + CameraCharacteristics.SCALER_AVAILABLE_JPEG_SIZES); } int width = 640; int height = 480; @@ -288,7 +286,7 @@ public class CameraOps { List<Surface> outputSurfaces = new ArrayList<Surface>(/*capacity*/1); outputSurfaces.add(mCaptureReader.getSurface()); - mCamera.configureOutputs(outputSurfaces); + configureOutputs(outputSurfaces); CaptureRequest.Builder captureBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); @@ -315,7 +313,6 @@ public class CameraOps { mCaptureReader.setOnImageAvailableListener(readerListener, h); mCamera.capture(captureBuilder.build(), l, mOpsHandler); - } catch (CameraAccessException e) { throw new ApiFailureException("Error in minimal JPEG capture", e); } @@ -339,7 +336,7 @@ public class CameraOps { mRecordingRequestBuilder.addTarget(mPreviewSurface); // Start camera streaming and recording. - mCamera.configureOutputs(mOutputSurfaces); + configureOutputs(mOutputSurfaces); mCamera.setRepeatingRequest(mRecordingRequestBuilder.build(), null, null); mRecordingStream.start(); } catch (CameraAccessException e) { @@ -362,11 +359,13 @@ public class CameraOps { */ mRecordingStream.onConfiguringRequest(mRecordingRequestBuilder, /* detach */true); mRecordingStream.onConfiguringOutputs(mOutputSurfaces, /* detach */true); - mCamera.stopRepeating(); - mCamera.waitUntilIdle(); + + // Remove recording surface before calling RecordingStream.stop, + // since that invalidates the surface. + configureOutputs(mOutputSurfaces); + mRecordingStream.stop(); - mCamera.configureOutputs(mOutputSurfaces); mCamera.setRepeatingRequest(mRecordingRequestBuilder.build(), null, null); } catch (CameraAccessException e) { throw new ApiFailureException("Error stop recording", e); @@ -375,12 +374,13 @@ public class CameraOps { private Size getRecordingSize() throws ApiFailureException { try { - CameraProperties properties = mCamera.getProperties(); + CameraCharacteristics properties = + mCameraManager.getCameraCharacteristics(mCamera.getId()); Size[] recordingSizes = null; if (properties != null) { recordingSizes = properties.get( - CameraProperties.SCALER_AVAILABLE_PROCESSED_SIZES); + CameraCharacteristics.SCALER_AVAILABLE_PROCESSED_SIZES); } mEncodingBitRate = ENC_BIT_RATE_LOW; diff --git a/apps/TestingCamera2/src/com/android/testingcamera2/TestingCamera2.java b/apps/TestingCamera2/src/com/android/testingcamera2/TestingCamera2.java index 596dd28..a3882df 100644 --- a/apps/TestingCamera2/src/com/android/testingcamera2/TestingCamera2.java +++ b/apps/TestingCamera2/src/com/android/testingcamera2/TestingCamera2.java @@ -17,11 +17,13 @@ package com.android.testingcamera2; import android.app.Activity; +import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.ImageFormat; +import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; -import android.hardware.camera2.CameraProperties; +import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.media.Image; @@ -32,6 +34,7 @@ import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; +import android.view.ViewGroup.LayoutParams; import android.view.WindowManager; import android.widget.Button; import android.widget.ImageView; @@ -58,7 +61,9 @@ public class TestingCamera2 extends Activity implements SurfaceHolder.Callback { // Min and max sensitivity ISO values private static final int MIN_SENSITIVITY = 100; private static final int MAX_SENSITIVITY = 1600; + private static final int ORIENTATION_UNINITIALIZED = -1; + private int mLastOrientation = ORIENTATION_UNINITIALIZED; private SurfaceView mPreviewView; private ImageView mStillView; @@ -78,7 +83,7 @@ public class TestingCamera2 extends Activity implements SurfaceHolder.Callback { private ToggleButton mManualCtrlToggle; private CameraControls mCameraControl = null; - private Set<View> mManualControls = new HashSet<View>(); + private final Set<View> mManualControls = new HashSet<View>(); Handler mMainHandler; @@ -135,12 +140,18 @@ public class TestingCamera2 extends Activity implements SurfaceHolder.Callback { } catch(ApiFailureException e) { logException("Cannot create camera ops!",e); } + + // Process the initial configuration (for i.e. initial orientation) + // We need this because #onConfigurationChanged doesn't get called when the app launches + maybeUpdateConfiguration(getResources().getConfiguration()); } @Override public void onResume() { super.onResume(); try { + if (VERBOSE) Log.v(TAG, String.format("onResume")); + mCameraOps.minimalPreviewConfig(mPreviewView.getHolder()); mCurrentPreviewHolder = mPreviewView.getHolder(); } catch (ApiFailureException e) { @@ -152,6 +163,8 @@ public class TestingCamera2 extends Activity implements SurfaceHolder.Callback { public void onPause() { super.onPause(); try { + if (VERBOSE) Log.v(TAG, String.format("onPause")); + mCameraOps.closeDevice(); } catch (ApiFailureException e) { logException("Can't close device: ",e); @@ -159,12 +172,81 @@ public class TestingCamera2 extends Activity implements SurfaceHolder.Callback { mCurrentPreviewHolder = null; } + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + if (VERBOSE) { + Log.v(TAG, String.format("onConfiguredChanged: orientation %x", + newConfig.orientation)); + } + + maybeUpdateConfiguration(newConfig); + } + + private void maybeUpdateConfiguration(Configuration newConfig) { + if (VERBOSE) { + Log.v(TAG, String.format("handleConfiguration: orientation %x", + newConfig.orientation)); + } + + if (mLastOrientation != newConfig.orientation) { + mLastOrientation = newConfig.orientation; + updatePreviewOrientation(); + } + } + + private void updatePreviewOrientation() { + LayoutParams params = mPreviewView.getLayoutParams(); + int width = params.width; + int height = params.height; + + if (VERBOSE) { + Log.v(TAG, String.format( + "onConfiguredChanged: current layout is %dx%d", width, + height)); + } + /** + * Force wide aspect ratios for landscape + * Force narrow aspect ratios for portrait + */ + if (mLastOrientation == Configuration.ORIENTATION_LANDSCAPE) { + if (height > width) { + int tmp = width; + width = height; + height = tmp; + } + } else if (mLastOrientation == Configuration.ORIENTATION_PORTRAIT) { + if (width > height) { + int tmp = width; + width = height; + height = tmp; + } + } + + if (width != params.width && height != params.height) { + if (VERBOSE) { + Log.v(TAG, String.format( + "onConfiguredChanged: updating preview size to %dx%d", width, + height)); + } + params.width = width; + params.height = height; + + mPreviewView.setLayoutParams(params); + } + } + /** SurfaceHolder.Callback methods */ @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + if (VERBOSE) { + Log.v(TAG, String.format("surfaceChanged: format %x, width %d, height %d", format, + width, height)); + } if (mCurrentPreviewHolder != null && holder == mCurrentPreviewHolder) { try { mCameraOps.minimalPreview(holder); @@ -183,7 +265,7 @@ public class TestingCamera2 extends Activity implements SurfaceHolder.Callback { public void surfaceDestroyed(SurfaceHolder holder) { } - private Button.OnClickListener mInfoButtonListener = new Button.OnClickListener() { + private final Button.OnClickListener mInfoButtonListener = new Button.OnClickListener() { @Override public void onClick(View v) { final Handler uiHandler = new Handler(); @@ -213,7 +295,7 @@ public class TestingCamera2 extends Activity implements SurfaceHolder.Callback { } } - private CameraOps.CaptureListener mCaptureListener = new CameraOps.CaptureListener() { + private final CameraOps.CaptureListener mCaptureListener = new CameraOps.CaptureListener() { @Override public void onCaptureAvailable(Image capture) { if (capture.getFormat() != ImageFormat.JPEG) { @@ -230,7 +312,7 @@ public class TestingCamera2 extends Activity implements SurfaceHolder.Callback { }; // TODO: this callback is not called for each capture, need figure out why. - private CameraOps.CaptureResultListener mCaptureResultListener = + private final CameraOps.CaptureResultListener mCaptureResultListener = new CameraOps.CaptureResultListener() { @Override @@ -283,7 +365,8 @@ public class TestingCamera2 extends Activity implements SurfaceHolder.Callback { } @Override - public void onCaptureFailed(CameraDevice camera, CaptureRequest request) { + public void onCaptureFailed(CameraDevice camera, CaptureRequest request, + CaptureFailure failure) { Log.e(TAG, "Capture failed"); } }; @@ -292,15 +375,15 @@ public class TestingCamera2 extends Activity implements SurfaceHolder.Callback { Log.e(TAG, msg + Log.getStackTraceString(e)); } - private OnSeekBarChangeListener mSensitivitySeekBarListener = + private final OnSeekBarChangeListener mSensitivitySeekBarListener = new OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { int[] defaultRange = {MIN_SENSITIVITY, MAX_SENSITIVITY}; - CameraProperties properties = mCameraOps.getCameraProperties(); + CameraCharacteristics properties = mCameraOps.getCameraCharacteristics(); int[] sensitivityRange = properties.get( - CameraProperties.SENSOR_INFO_SENSITIVITY_RANGE); + CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE); if (sensitivityRange == null || sensitivityRange.length < 2 || sensitivityRange[0] > MIN_SENSITIVITY || sensitivityRange[1] < MAX_SENSITIVITY) { Log.e(TAG, "unable to get sensitivity range, use default range"); @@ -328,15 +411,15 @@ public class TestingCamera2 extends Activity implements SurfaceHolder.Callback { } }; - private OnSeekBarChangeListener mExposureSeekBarListener = + private final OnSeekBarChangeListener mExposureSeekBarListener = new OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { long[] defaultRange = {MIN_EXPOSURE, MAX_EXPOSURE}; - CameraProperties properties = mCameraOps.getCameraProperties(); + CameraCharacteristics properties = mCameraOps.getCameraCharacteristics(); long[] exposureRange = properties.get( - CameraProperties.SENSOR_INFO_EXPOSURE_TIME_RANGE); + CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE); // Not enforce the max value check here, most of the devices don't support // larger than 30s exposure time if (exposureRange == null || exposureRange.length < 2 || @@ -365,14 +448,14 @@ public class TestingCamera2 extends Activity implements SurfaceHolder.Callback { } }; - private OnSeekBarChangeListener mFrameDurationSeekBarListener = + private final OnSeekBarChangeListener mFrameDurationSeekBarListener = new OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - CameraProperties properties = mCameraOps.getCameraProperties(); + CameraCharacteristics properties = mCameraOps.getCameraCharacteristics(); Long frameDurationMax = properties.get( - CameraProperties.SENSOR_INFO_MAX_FRAME_DURATION); + CameraCharacteristics.SENSOR_INFO_MAX_FRAME_DURATION); if (frameDurationMax == null || frameDurationMax <= 0) { frameDurationMax = MAX_FRAME_DURATION; Log.e(TAG, "max frame duration is invalid, set to " + frameDurationMax); @@ -399,7 +482,7 @@ public class TestingCamera2 extends Activity implements SurfaceHolder.Callback { } }; - private View.OnClickListener mControlToggleListener = + private final View.OnClickListener mControlToggleListener = new View.OnClickListener() { @Override public void onClick(View v) { @@ -415,7 +498,7 @@ public class TestingCamera2 extends Activity implements SurfaceHolder.Callback { } }; - private View.OnClickListener mRecordingToggleListener = + private final View.OnClickListener mRecordingToggleListener = new View.OnClickListener() { @Override public void onClick(View v) { |