ITS: create test to test for scene change Add test to check for afSceneChange flag. Testing done with Pixel 4 cam[0] --> PASS with auto and manual Pixel 3 cam[0] --> SKIP Tecno Spark 3 cam[0] --> SKIP, but correct behavior less 3A bug: 69932800 Change-Id: Ia3eddb8f2d20366c46991cfce6c45ad8edb64cf3
diff --git a/apps/CameraITS/pymodules/its/caps.py b/apps/CameraITS/pymodules/its/caps.py index 4c527d9..06e1174 100644 --- a/apps/CameraITS/pymodules/its/caps.py +++ b/apps/CameraITS/pymodules/its/caps.py
@@ -572,6 +572,18 @@ sensor_fusion(props)]) +def continuous_picture(props): + """Returns whether a device supports CONTINUOUS_PICTURE.""" + return props.has_key('android.control.afAvailableModes') and \ + 4 in props['android.control.afAvailableModes'] + + +def af_scene_change(props): + """Returns whether a device supports afSceneChange.""" + return props.has_key('camera.characteristics.resultKeys') and \ + 'android.control.afSceneChange' in props['camera.characteristics.resultKeys'] + + class __UnitTest(unittest.TestCase): """Run a suite of unit tests on this module. """
diff --git a/apps/CameraITS/tests/scene_change/scene_change.pdf b/apps/CameraITS/tests/scene_change/scene_change.pdf new file mode 100644 index 0000000..a5786d6 --- /dev/null +++ b/apps/CameraITS/tests/scene_change/scene_change.pdf Binary files differ
diff --git a/apps/CameraITS/tests/scene_change/scene_change_0.5x_scaled.pdf b/apps/CameraITS/tests/scene_change/scene_change_0.5x_scaled.pdf new file mode 100644 index 0000000..ca92a4e --- /dev/null +++ b/apps/CameraITS/tests/scene_change/scene_change_0.5x_scaled.pdf Binary files differ
diff --git a/apps/CameraITS/tests/scene_change/scene_change_0.67x_scaled.pdf b/apps/CameraITS/tests/scene_change/scene_change_0.67x_scaled.pdf new file mode 100644 index 0000000..eca9d3e --- /dev/null +++ b/apps/CameraITS/tests/scene_change/scene_change_0.67x_scaled.pdf Binary files differ
diff --git a/apps/CameraITS/tests/scene_change/test_scene_change.py b/apps/CameraITS/tests/scene_change/test_scene_change.py new file mode 100644 index 0000000..2304759 --- /dev/null +++ b/apps/CameraITS/tests/scene_change/test_scene_change.py
@@ -0,0 +1,200 @@ +# Copyright 2020 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 multiprocessing +import os.path +import subprocess +import sys +import time +import its.caps +import its.device +import its.image +import its.objects + +BRIGHT_CHANGE_TOL = 0.2 +CONTINUOUS_PICTURE_MODE = 4 +CONVERGED_3A = [[2, 2, 2], [2, 5, 2], [2, 6, 2]] # [AE, AF, AWB] +# AE_STATES: {0: INACTIVE, 1: SEARCHING, 2: CONVERGED, 3: LOCKED, +# 4: FLASH_REQ, 5: PRECAPTURE} +# AF_STATES: {0: INACTIVE, 1: PASSIVE_SCAN, 2: PASSIVE_FOCUSED, +# 3: PASSIVE_UNFOCUSED, 4: ACTIVE_SCAN, 5: FOCUS_LOCKED, +# 6: NOT_FOCUSED_LOCKED} +# AWB_STATES: {0: INACTIVE, 1: SEARCHING, 2: CONVERGED, 3: LOCKED} +DELAY_CAPTURE = 0.55 # delay in first capture to sync events (sec) +DELAY_DISPLAY = 3.0 # time when display turns OFF (sec) +FPS = 30 +FRAME_SHIFT = 5.0 # number of frames to shift to try and find scene change +NAME = os.path.basename(__file__).split('.')[0] +NUM_BURSTS = 6 +NUM_FRAMES = 50 +W, H = 640, 480 + + +def get_cmd_line_args(): + chart_host_id = None + for s in list(sys.argv[1:]): + if s[:6] == 'chart=' and len(s) > 6: + chart_host_id = s[6:] + return chart_host_id + + +def mask_3a_settling_frames(cap_data): + converged_frame = -1 + for i, cap in enumerate(cap_data): + if cap['3a_state'] in CONVERGED_3A: + converged_frame = i + break + print 'Frames index where 3A converges: %d' % converged_frame + assert converged_frame != -1, '3A does not converge' + return converged_frame + + +def determine_if_scene_changed(cap_data, converged_frame): + scene_changed = False + bright_changed = False + settled_frame_brightness = cap_data[converged_frame]['avg'] + for i in range(converged_frame, len(cap_data)): + if cap_data[i]['avg'] <= ( + settled_frame_brightness * (1.0 - BRIGHT_CHANGE_TOL)): + bright_changed = True + if cap_data[i]['flag'] == 1: + scene_changed = True + return scene_changed, bright_changed + + +def toggle_screen(chart_host_id, state, delay): + t0 = time.time() + print 'tablet event start' + screen_id_arg = ('screen=%s' % chart_host_id) + state_id_arg = 'state=%s' % state + delay_arg = 'delay=%.3f' % delay + cmd = ['python', os.path.join(os.environ['CAMERA_ITS_TOP'], 'tools', + 'toggle_screen.py'), screen_id_arg, + state_id_arg, delay_arg] + screen_cmd_code = subprocess.call(cmd) + assert screen_cmd_code == 0 + t = time.time() - t0 + print 'tablet event %s: %.3f' % (state, t) + + +def capture_frames(delay, burst): + """Capture frames.""" + cap_data_list = [] + with its.device.ItsSession() as cam: + req = its.objects.auto_capture_request() + req['android.control.afMode'] = CONTINUOUS_PICTURE_MODE + fmt = {'format': 'yuv', 'width': W, 'height': H} + t0 = time.time() + time.sleep(delay) + print 'cap event start:', time.time() - t0 + caps = cam.do_capture([req]*NUM_FRAMES, fmt) + print 'cap event stop:', time.time() - t0 + # extract frame metadata and frame + for i, cap in enumerate(caps): + cap_data = {} + md = cap['metadata'] + exp = md['android.sensor.exposureTime'] + iso = md['android.sensor.sensitivity'] + fd = md['android.lens.focalLength'] + ae_state = md['android.control.aeState'] + af_state = md['android.control.afState'] + awb_state = md['android.control.awbState'] + fd_str = 'infinity' + if fd != 0.0: + fd_str = str(round(1.0E2/fd, 2)) + 'cm' + scene_change_flag = md['android.control.afSceneChange'] + assert scene_change_flag in [0, 1], 'afSceneChange not in [0,1]' + img = its.image.convert_capture_to_rgb_image(cap) + its.image.write_image(img, '%s_%d_%d.jpg' % (NAME, burst+1, i)) + tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1) + g = its.image.compute_image_means(tile)[1] + print '%d, iso: %d, exp: %.2fms, fd: %s, avg: %.3f' % ( + i, iso, exp*1E-6, fd_str, g), + print '[ae,af,awb]: [%d,%d,%d], change: %d' % ( + ae_state, af_state, awb_state, scene_change_flag) + cap_data['exp'] = exp + cap_data['iso'] = iso + cap_data['fd'] = fd + cap_data['3a_state'] = [ae_state, af_state, awb_state] + cap_data['avg'] = g + cap_data['flag'] = scene_change_flag + cap_data_list.append(cap_data) + return cap_data_list + + +def main(): + """Test scene change. + + Do auto capture with face scene. Power down tablet and recapture. + Confirm android.control.afSceneChangeDetected is True. + """ + # check for skip conditions and do 3a up front + with its.device.ItsSession() as cam: + props = cam.get_camera_properties() + props = cam.override_with_hidden_physical_camera_props(props) + its.caps.skip_unless(its.caps.continuous_picture(props) and + its.caps.af_scene_change(props) and + its.caps.read_3a(props)) + cam.do_3a() + + # do captures with scene change + chart_host_id = get_cmd_line_args() + cap_delay = DELAY_CAPTURE + scene_delay = DELAY_DISPLAY + for burst in range(NUM_BURSTS): + if chart_host_id: + print '\nToggling tablet. Scene change at %.3fs.' % scene_delay + multiprocessing.Process(name='p1', target=toggle_screen, + args=(chart_host_id, 'OFF', + scene_delay,)).start() + else: + print '\nWave hand in front of camera to create scene change.' + cap_data = capture_frames(cap_delay, burst+1) + + # find frame where 3A converges + converged_frame = mask_3a_settling_frames(cap_data) + + # turn tablet back on to return to baseline scene state + if chart_host_id: + toggle_screen(chart_host_id, 'ON', 0) + + # determine if brightness changed and/or scene change flag asserted + scene_changed, bright_changed = determine_if_scene_changed( + cap_data, converged_frame) + if not scene_changed: + if bright_changed: + print ' No scene change, but brightness change.' + scene_delay -= FRAME_SHIFT/FPS # tablet-off earlier + else: + print ' No scene change, no brightness change.' + if cap_data[NUM_FRAMES-1]['avg'] < 0.1: + print ' Scene dark entire capture. Shift later.' + scene_delay += FRAME_SHIFT/FPS # tablet-off later + else: + print ' Scene light entire capture. Shift earlier.' + scene_delay -= FRAME_SHIFT/FPS # tablet-off earlier + print ' Retry with tablet turning OFF earlier.' + elif scene_changed and bright_changed: + print ' scene & brightness change on burst %d.' % (burst+1) + break + elif scene_changed and not bright_changed: + msg = ' scene change, but no brightness change.' + assert False, msg + if burst == NUM_BURSTS - 1: + msg = 'No scene change in %dx tries' % NUM_BURSTS + assert False, msg + + +if __name__ == '__main__': + main()
diff --git a/apps/CameraITS/tools/run_all_tests.py b/apps/CameraITS/tools/run_all_tests.py index ef8a91c..83177f2 100644 --- a/apps/CameraITS/tools/run_all_tests.py +++ b/apps/CameraITS/tools/run_all_tests.py
@@ -56,7 +56,7 @@ # scene*_a/b/... are similar scenes that share one or more tests ALL_SCENES = ['scene0', 'scene1_1', 'scene1_2', 'scene2_a', 'scene2_b', 'scene2_c', 'scene2_d', 'scene2_e', 'scene3', 'scene4', - 'scene5', 'sensor_fusion'] + 'scene5', 'sensor_fusion', 'scene_change'] # Scenes that are logically grouped and can be called as group GROUPED_SCENES = { @@ -66,7 +66,8 @@ # Scenes that can be automated through tablet display AUTO_SCENES = ['scene0', 'scene1_1', 'scene1_2', 'scene2_a', 'scene2_b', - 'scene2_c', 'scene2_d', 'scene2_e', 'scene3', 'scene4'] + 'scene2_c', 'scene2_d', 'scene2_e', 'scene3', 'scene4', + 'scene_change'] SCENE_REQ = { 'scene0': None, @@ -86,7 +87,8 @@ 'sensor_fusion': 'Rotating checkboard pattern. See ' 'sensor_fusion/SensorFusion.pdf for detailed ' 'instructions.\nNote that this test will be skipped ' - 'on devices not supporting REALTIME camera timestamp.' + 'on devices not supporting REALTIME camera timestamp.', + 'scene_change': 'The picture in tests/scene_change.pdf with faces' } SCENE_EXTRA_ARGS = { @@ -119,7 +121,10 @@ 'scene3': [], 'scene4': [], 'scene5': [], - 'sensor_fusion': [] + 'sensor_fusion': [], + 'scene_change': [ + ['test_scene_change', 31] + ] } # Must match mHiddenPhysicalCameraSceneIds in ItsTestActivity.java @@ -155,7 +160,8 @@ 'scene5': [], 'sensor_fusion': [ 'test_sensor_fusion' - ] + ], + 'scene_change': [] } # Tests run in more than 1 scene. @@ -181,7 +187,8 @@ 'scene3': [], 'scene4': [], 'scene5': [], - 'sensor_fusion': [] + 'sensor_fusion': [], + 'scene_change': [] } @@ -568,7 +575,8 @@ if cmd is not None: valid_scene_code = subprocess.call(cmd, cwd=topdir) assert valid_scene_code == 0 - print 'Start running ITS on camera %s, %s' % (id_combo_string, scene) + print 'Start running ITS on camera %s, %s' % ( + id_combo_string, scene) # Extract chart from scene for scene3 once up front chart_loc_arg = '' chart_height = CHART_HEIGHT @@ -582,6 +590,8 @@ chart_loc_arg = 'chart_loc=%.2f,%.2f,%.2f,%.2f,%.3f' % ( chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm, chart.scale) + if scene == 'scene_change' and not auto_scene_switch: + print '\nWave hand over camera to create scene change' # Run each test, capturing stdout and stderr. for (testname, testpath) in tests: # Only pick predefined tests for hidden physical camera @@ -729,14 +739,16 @@ print 'Shutting down chart screen: ', chart_host_id screen_id_arg = ('screen=%s' % chart_host_id) cmd = ['python', os.path.join(os.environ['CAMERA_ITS_TOP'], 'tools', - 'turn_off_screen.py'), screen_id_arg] + 'toggle_screen.py'), screen_id_arg, + 'state=OFF'] screen_off_code = subprocess.call(cmd) assert screen_off_code == 0 print 'Shutting down DUT screen: ', device_id screen_id_arg = ('screen=%s' % device_id) cmd = ['python', os.path.join(os.environ['CAMERA_ITS_TOP'], 'tools', - 'turn_off_screen.py'), screen_id_arg] + 'toggle_screen.py'), screen_id_arg, + 'state=OFF'] screen_off_code = subprocess.call(cmd) assert screen_off_code == 0
diff --git a/apps/CameraITS/tools/turn_off_screen.py b/apps/CameraITS/tools/toggle_screen.py similarity index 73% rename from apps/CameraITS/tools/turn_off_screen.py rename to apps/CameraITS/tools/toggle_screen.py index 1faef9e..e91fadb 100644 --- a/apps/CameraITS/tools/turn_off_screen.py +++ b/apps/CameraITS/tools/toggle_screen.py
@@ -17,31 +17,39 @@ import sys import time -TURN_OFF_DELAY = 1 # seconds. Needed for back to back runs +SCREEN_DELAY = 1 # seconds. Needed for back to back runs def main(): """Put screen to sleep.""" screen_id = '' + state = 'OFF' # turn OFF by default + delay_sec = 0 # no delay by default for s in sys.argv[1:]: if s[:7] == 'screen=' and len(s) > 7: screen_id = s[7:] + elif s[:6] == 'state=' and len(s) > 6: + state = s[6:] + elif s[:6] == 'delay=' and len(s) > 6: + delay_sec = float(s[6:]) if not screen_id: print 'Error: need to specify screen serial' assert False + time.sleep(delay_sec) cmd = ('adb -s %s shell dumpsys power | egrep "Display Power"' % screen_id) process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) cmd_ret = process.stdout.read() screen_state = re.split(r'[s|=]', cmd_ret)[-1] - if 'OFF' in screen_state: - print 'Screen already OFF.' + if state in screen_state: + print 'Screen already %s.' % state else: + print 'Turning screen %s.' % state pwrdn = ('adb -s %s shell input keyevent POWER' % screen_id) subprocess.Popen(pwrdn.split()) - time.sleep(TURN_OFF_DELAY) + time.sleep(SCREEN_DELAY) if __name__ == '__main__': main()