CameraITS: Add EV compensation test

Basic test checks the luma brightness strict monotonicity only, which can run
on all devices.
Advanced test checks the luma brightness vs expected model values. It can only
run on the devices that support tone map manual control.

Bug: 18293350
Change-Id: Id7e0e58421caf76e585a422d6ab5e49c8d967c8a
(cherry picked from commit 6137f213b59d97388cb2eb219f54251c01affe50)
diff --git a/apps/CameraITS/pymodules/its/device.py b/apps/CameraITS/pymodules/its/device.py
index 6f42051..beba0ae 100644
--- a/apps/CameraITS/pymodules/its/device.py
+++ b/apps/CameraITS/pymodules/its/device.py
@@ -260,7 +260,8 @@
                     regions_af=[[0,0,1,1,1]],
                     do_ae=True, do_awb=True, do_af=True,
                     lock_ae=False, lock_awb=False,
-                    get_results=False):
+                    get_results=False,
+                    ev_comp=0):
         """Perform a 3A operation on the device.
 
         Triggers some or all of AE, AWB, and AF, and returns once they have
@@ -278,6 +279,7 @@
             lock_ae: Request AE lock after convergence, and wait for it.
             lock_awb: Request AWB lock after convergence, and wait for it.
             get_results: Return the 3A results from this function.
+            ev_comp: An EV compensation value to use when running AE.
 
         Region format in args:
             Arguments are lists of weighted regions; each weighted region is a
@@ -307,6 +309,8 @@
             cmd["aeLock"] = True
         if lock_awb:
             cmd["awbLock"] = True
+        if ev_comp != 0:
+            cmd["evComp"] = ev_comp
         self.sock.send(json.dumps(cmd) + "\n")
 
         # Wait for each specified 3A to converge.
diff --git a/apps/CameraITS/tests/inprog/test_ev_compensation.py b/apps/CameraITS/tests/inprog/test_ev_compensation.py
deleted file mode 100644
index f9b0cd3..0000000
--- a/apps/CameraITS/tests/inprog/test_ev_compensation.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright 2014 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 pylab
-import matplotlib
-import matplotlib.pyplot
-import numpy
-
-def main():
-    """Tests that EV compensation is applied.
-    """
-    NAME = os.path.basename(__file__).split(".")[0]
-
-    MAX_LUMA_DELTA_THRESH = 0.01
-    AVG_LUMA_DELTA_THRESH = 0.001
-
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        cam.do_3a()
-
-        # Capture auto shots, but with a linear tonemap.
-        req = its.objects.auto_capture_request()
-        req["android.tonemap.mode"] = 0
-        req["android.tonemap.curveRed"] = (0.0, 0.0, 1.0, 1.0)
-        req["android.tonemap.curveGreen"] = (0.0, 0.0, 1.0, 1.0)
-        req["android.tonemap.curveBlue"] = (0.0, 0.0, 1.0, 1.0)
-
-        evs = range(-4,5)
-        lumas = []
-        for ev in evs:
-            req['android.control.aeExposureCompensation'] = ev
-            cap = cam.do_capture(req)
-            y = its.image.convert_capture_to_planes(cap)[0]
-            tile = its.image.get_image_patch(y, 0.45,0.45,0.1,0.1)
-            lumas.append(its.image.compute_image_means(tile)[0])
-
-        ev_step_size_in_stops = its.objects.rational_to_float(
-                props['android.control.aeCompensationStep'])
-        luma_increase_per_step = pow(2, ev_step_size_in_stops)
-        expected_lumas = [lumas[0] * pow(luma_increase_per_step, i) \
-                for i in range(len(evs))]
-
-        pylab.plot(evs, lumas, 'r')
-        pylab.plot(evs, expected_lumas, 'b')
-        matplotlib.pyplot.savefig("%s_plot_means.png" % (NAME))
-
-        luma_diffs = [expected_lumas[i] - lumas[i] for i in range(len(evs))]
-        max_diff = max(luma_diffs)
-        avg_diff = sum(luma_diffs) / len(luma_diffs)
-        print "Max delta between modeled and measured lumas:", max_diff
-        print "Avg delta between modeled and measured lumas:", avg_diff
-        assert(max_diff < MAX_LUMA_DELTA_THRESH)
-        assert(avg_diff < AVG_LUMA_DELTA_THRESH)
-
-if __name__ == '__main__':
-    main()
diff --git a/apps/CameraITS/tests/scene1/test_ev_compensation_advanced.py b/apps/CameraITS/tests/scene1/test_ev_compensation_advanced.py
new file mode 100644
index 0000000..6341c67
--- /dev/null
+++ b/apps/CameraITS/tests/scene1/test_ev_compensation_advanced.py
@@ -0,0 +1,83 @@
+# Copyright 2014 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.caps
+import its.objects
+import os.path
+import pylab
+import matplotlib
+import matplotlib.pyplot
+import numpy
+
+def main():
+    """Tests that EV compensation is applied.
+    """
+    NAME = os.path.basename(__file__).split(".")[0]
+
+    MAX_LUMA_DELTA_THRESH = 0.02
+
+    with its.device.ItsSession() as cam:
+        props = cam.get_camera_properties()
+        its.caps.skip_unless(its.caps.manual_sensor(props) and
+                             its.caps.manual_post_proc(props) and
+                             its.caps.per_frame_control(props))
+
+        evs = range(-4,5)
+        lumas = []
+        for ev in evs:
+            # Re-converge 3A, and lock AE once converged. skip AF trigger as
+            # dark/bright scene could make AF convergence fail and this test
+            # doesn't care the image sharpness.
+            cam.do_3a(ev_comp=ev, lock_ae=True, do_af=False)
+
+            # Capture a single shot with the same EV comp and locked AE.
+            req = its.objects.auto_capture_request()
+            req['android.control.aeExposureCompensation'] = ev
+            req["android.control.aeLock"] = True
+            # Use linear tone curve to avoid brightness being impacted
+            # by tone curves.
+            req["android.tonemap.mode"] = 0
+            req["android.tonemap.curveRed"] = [0.0,0.0, 1.0,1.0]
+            req["android.tonemap.curveGreen"] = [0.0,0.0, 1.0,1.0]
+            req["android.tonemap.curveBlue"] = [0.0,0.0, 1.0,1.0]
+            cap = cam.do_capture(req)
+            y = its.image.convert_capture_to_planes(cap)[0]
+            tile = its.image.get_image_patch(y, 0.45,0.45,0.1,0.1)
+            lumas.append(its.image.compute_image_means(tile)[0])
+
+        ev_step_size_in_stops = its.objects.rational_to_float(
+                props['android.control.aeCompensationStep'])
+        luma_increase_per_step = pow(2, ev_step_size_in_stops)
+        print "ev_step_size_in_stops", ev_step_size_in_stops
+        imid = len(lumas) / 2
+        expected_lumas = [lumas[imid] / pow(luma_increase_per_step, i)
+                          for i in range(imid , 0, -1)]  + \
+                         [lumas[imid] * pow(luma_increase_per_step, i-imid)
+                          for i in range(imid, len(evs))]
+
+        pylab.plot(evs, lumas, 'r')
+        pylab.plot(evs, expected_lumas, 'b')
+        matplotlib.pyplot.savefig("%s_plot_means.png" % (NAME))
+
+        luma_diffs = [expected_lumas[i] - lumas[i] for i in range(len(evs))]
+        max_diff = max(abs(i) for i in luma_diffs)
+        avg_diff = abs(numpy.array(luma_diffs)).mean()
+        print "Max delta between modeled and measured lumas:", max_diff
+        print "Avg delta between modeled and measured lumas:", avg_diff
+        assert(max_diff < MAX_LUMA_DELTA_THRESH)
+
+if __name__ == '__main__':
+    main()
diff --git a/apps/CameraITS/tests/scene1/test_ev_compensation_basic.py b/apps/CameraITS/tests/scene1/test_ev_compensation_basic.py
new file mode 100644
index 0000000..13f318f
--- /dev/null
+++ b/apps/CameraITS/tests/scene1/test_ev_compensation_basic.py
@@ -0,0 +1,60 @@
+# Copyright 2014 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 pylab
+import matplotlib
+import matplotlib.pyplot
+import numpy
+
+def main():
+    """Tests that EV compensation is applied.
+    """
+    NAME = os.path.basename(__file__).split(".")[0]
+
+    with its.device.ItsSession() as cam:
+        props = cam.get_camera_properties()
+
+        evs = range(-4,5)
+        lumas = []
+        for ev in evs:
+            # Re-converge 3A, and lock AE once converged. skip AF trigger as
+            # dark/bright scene could make AF convergence fail and this test
+            # doesn't care the image sharpness.
+            cam.do_3a(ev_comp=ev, lock_ae=True, do_af=False)
+
+            # Capture a single shot with the same EV comp and locked AE.
+            req = its.objects.auto_capture_request()
+            req['android.control.aeExposureCompensation'] = ev
+            req["android.control.aeLock"] = True
+            cap = cam.do_capture(req)
+            y = its.image.convert_capture_to_planes(cap)[0]
+            tile = its.image.get_image_patch(y, 0.45,0.45,0.1,0.1)
+            lumas.append(its.image.compute_image_means(tile)[0])
+
+        pylab.plot(evs, lumas, 'r')
+        matplotlib.pyplot.savefig("%s_plot_means.png" % (NAME))
+
+        luma_diffs = numpy.diff(lumas)
+        min_luma_diffs = min(luma_diffs)
+        print "Min of the luma value difference between adjacent ev comp: ", \
+                min_luma_diffs
+        # All luma brightness should be increasing with increasing ev comp.
+        assert(min_luma_diffs > 0)
+
+if __name__ == '__main__':
+    main()
diff --git a/apps/CameraITS/tools/run_all_tests.py b/apps/CameraITS/tools/run_all_tests.py
index 2202d5b..f5a53b1 100644
--- a/apps/CameraITS/tools/run_all_tests.py
+++ b/apps/CameraITS/tools/run_all_tests.py
@@ -38,6 +38,8 @@
             "test_ae_precapture_trigger",
             "test_black_white",
             "test_crop_region_raw",
+            "test_ev_compensation_advanced",
+            "test_ev_compensation_basic",
             "test_locked_burst",
             "test_yuv_plus_jpeg"
         ]
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
index e340c8a..a305cd2 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
@@ -112,6 +112,7 @@
     public static final String TRIGGER_AE_KEY = "ae";
     public static final String TRIGGER_AF_KEY = "af";
     public static final String VIB_PATTERN_KEY = "pattern";
+    public static final String EVCOMP_KEY = "evComp";
 
     private CameraManager mCameraManager = null;
     private HandlerThread mCameraThread = null;
@@ -802,6 +803,12 @@
             mNeedsLockedAE = params.optBoolean(LOCK_AE_KEY, false);
             mNeedsLockedAWB = params.optBoolean(LOCK_AWB_KEY, false);
 
+            // An EV compensation can be specified as part of AE convergence.
+            int evComp = params.optInt(EVCOMP_KEY, 0);
+            if (evComp != 0) {
+                Logt.i(TAG, String.format("Running 3A with AE exposure compensation value: %d", evComp));
+            }
+
             // By default, AE and AF both get triggered, but the user can optionally override this.
             // Also, AF won't get triggered if the lens is fixed-focus.
             boolean doAE = true;
@@ -845,7 +852,11 @@
                 // at a time, to simplify the logic here.
                 if (!mInterlock3A.block(TIMEOUT_3A * 1000) ||
                         System.currentTimeMillis() - tstart > TIMEOUT_3A * 1000) {
-                    throw new ItsException("3A failed to converge (timeout)");
+                    throw new ItsException(
+                            "3A failed to converge after " + TIMEOUT_3A + " seconds.\n" +
+                            "AE converge state: " + mConvergedAE + ", \n" +
+                            "AF convergence state: " + mConvergedAF + ", \n" +
+                            "AWB convergence state: " + mConvergedAWB + ".");
                 }
                 mInterlock3A.close();
 
@@ -876,6 +887,10 @@
                     req.set(CaptureRequest.CONTROL_AWB_LOCK, false);
                     req.set(CaptureRequest.CONTROL_AWB_REGIONS, regionAWB);
 
+                    if (evComp != 0) {
+                        req.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, evComp);
+                    }
+
                     if (mConvergedAE && mNeedsLockedAE) {
                         req.set(CaptureRequest.CONTROL_AE_LOCK, true);
                     }