1# Copyright 2020 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Verify that the android.control.afSceneChange asserted on scene change."""
15
16
17import logging
18import multiprocessing
19import os.path
20import time
21
22from mobly import test_runner
23
24import its_base_test
25import camera_properties_utils
26import capture_request_utils
27import image_processing_utils
28import its_session_utils
29import scene_change_utils
30
31_BRIGHT_CHANGE_TOL = 0.2
32_CONTINUOUS_PICTURE_MODE = 4
33_CONVERGED_3A = (2, 2, 2)  # (AE, AF, AWB)
34_DELAY_CAPTURE = 1.5  # Delay in first capture to sync events (sec).
35_DELAY_DISPLAY = 3.0  # Time when display turns OFF (sec).
36_FPS = 30  # Frames Per Second
37_M_TO_CM = 100
38_NAME = os.path.splitext(os.path.basename(__file__))[0]
39_NSEC_TO_MSEC = 1E-6
40_NUM_TRIES = 6
41_NUM_FRAMES = 50
42_PATCH_H = 0.1  # Center 10%.
43_PATCH_W = 0.1
44_PATCH_X = 0.5 - _PATCH_W/2
45_PATCH_Y = 0.5 - _PATCH_H/2
46_RGB_G_CH = 1
47_SCENE_CHANGE_FLAG_TRUE = 1
48_VALID_SCENE_CHANGE_VALS = (0, 1)
49_VGA_W, _VGA_H = 640, 480
50
51
52def find_3a_converged_frame(cap_data):
53  converged_frame = -1
54  for i, cap in enumerate(cap_data):
55    if cap['3a_state'] == _CONVERGED_3A:
56      converged_frame = i
57      break
58  logging.debug('Frame index where 3A converges: %d', converged_frame)
59  return converged_frame
60
61
62def determine_if_scene_changed(cap_data, converged_frame):
63  """Determine if the scene has changed during captures.
64
65  Args:
66    cap_data: Camera capture object.
67    converged_frame: Integer indicating when 3A converged.
68
69  Returns:
70    A 2-tuple of booleans where the first is for AF scene change flag asserted
71    and the second is for whether brightness in images changed.
72  """
73  scene_change_flag = False
74  bright_change_flag = False
75  start_frame_brightness = cap_data[0]['avg']
76  for i in range(converged_frame, len(cap_data)):
77    if cap_data[i]['avg'] <= (
78        start_frame_brightness * (1.0 - _BRIGHT_CHANGE_TOL)):
79      bright_change_flag = True
80    if cap_data[i]['flag'] == _SCENE_CHANGE_FLAG_TRUE:
81      scene_change_flag = True
82  return scene_change_flag, bright_change_flag
83
84
85def toggle_screen(tablet, delay=1):
86  """Sets the chart host screen display level .
87
88  Args:
89    tablet: Object for screen tablet.
90    delay: Float value for time delay. Default is 1 second.
91  """
92  t0 = time.time()
93  if delay >= 0:
94    time.sleep(delay)
95  else:
96    raise ValueError(f'Screen toggle time shifted to {delay} w/o scene change. '
97                     'Tablet does not appear to be toggling. Check setup.')
98  tablet.adb.shell('input keyevent KEYCODE_POWER')
99  t = time.time() - t0
100  logging.debug('Toggling display at %.3f.', t)
101
102
103def capture_frames(cam, delay, burst, log_path):
104  """Capture NUM_FRAMES frames and log metadata.
105
106  3A state information:
107    AE_STATES: {0: INACTIVE, 1: SEARCHING, 2: CONVERGED, 3: LOCKED,
108                4: FLASH_REQ, 5: PRECAPTURE}
109    AF_STATES: {0: INACTIVE, 1: PASSIVE_SCAN, 2: PASSIVE_FOCUSED,
110                3: ACTIVE_SCAN, 4: FOCUS_LOCKED, 5: NOT_FOCUSED_LOCKED,
111                6: PASSIVE_UNFOCUSED}
112    AWB_STATES: {0: INACTIVE, 1: SEARCHING, 2: CONVERGED, 3: LOCKED}
113
114  Args:
115    cam: Camera object.
116    delay: Float value for time delay in seconds.
117    burst: Integer number of burst index.
118    log_path: String location to save images.
119  Returns:
120    cap_data_list. List of dicts for each capture.
121  """
122  cap_data_list = []
123  req = capture_request_utils.auto_capture_request()
124  req['android.control.afMode'] = _CONTINUOUS_PICTURE_MODE
125  fmt = {'format': 'yuv', 'width': _VGA_W, 'height': _VGA_H}
126  t0 = time.time()
127  time.sleep(delay)
128  logging.debug('cap event start: %.6f', time.time() - t0)
129  caps = cam.do_capture([req]*_NUM_FRAMES, fmt)
130  logging.debug('cap event stop: %.6f', time.time() - t0)
131
132  # Extract frame metadata.
133  for i, cap in enumerate(caps):
134    cap_data = {}
135    exp = cap['metadata']['android.sensor.exposureTime'] * _NSEC_TO_MSEC
136    iso = cap['metadata']['android.sensor.sensitivity']
137    focal_length = cap['metadata']['android.lens.focalLength']
138    ae_state = cap['metadata']['android.control.aeState']
139    af_state = cap['metadata']['android.control.afState']
140    awb_state = cap['metadata']['android.control.awbState']
141    if focal_length:
142      fl_str = str(round(_M_TO_CM/focal_length, 2)) + 'cm'
143    else:
144      fl_str = 'infinity'
145    flag = cap['metadata']['android.control.afSceneChange']
146    if flag not in _VALID_SCENE_CHANGE_VALS:
147      raise AssertionError(f'afSceneChange not a valid value: {flag}.')
148    img = image_processing_utils.convert_capture_to_rgb_image(cap)
149    image_processing_utils.write_image(
150        img, '%s_%d_%d.jpg' % (os.path.join(log_path, _NAME), burst, i))
151    patch = image_processing_utils.get_image_patch(
152        img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
153    green_avg = image_processing_utils.compute_image_means(patch)[_RGB_G_CH]
154    logging.debug(
155        '%d, iso: %d, exp: %.2fms, fd: %s, avg: %.3f, 3A: [%d,%d,%d], flag: %d',
156        i, iso, exp, fl_str, green_avg, ae_state, af_state, awb_state, flag)
157    cap_data['3a_state'] = (ae_state, af_state, awb_state)
158    cap_data['avg'] = green_avg
159    cap_data['flag'] = flag
160    cap_data_list.append(cap_data)
161  return cap_data_list
162
163
164class SceneChangeTest(its_base_test.ItsBaseTest):
165  """Tests that AF scene change detected metadata changes for scene change.
166
167  Confirm android.control.afSceneChangeDetected is asserted when scene changes.
168
169  Does continuous capture with face scene during scene change. With no scene
170  change, behavior should be similar to scene2_b/test_continuous_picture.
171  Scene change is modeled with scene tablet powered down during continuous
172  capture. If tablet does not exist, scene change can be modeled with hand wave
173  in front of camera.
174
175  Depending on scene brightness changes and scene change flag assertions during
176  test, adjust tablet timing to move scene change to appropriate timing for
177  test.
178  """
179
180  def test_scene_change(self):
181    logging.debug('Starting %s', _NAME)
182    with its_session_utils.ItsSession(
183        device_id=self.dut.serial,
184        camera_id=self.camera_id,
185        hidden_physical_id=self.hidden_physical_id) as cam:
186      props = cam.get_camera_properties()
187      props = cam.override_with_hidden_physical_camera_props(props)
188      log_path = self.log_path
189      tablet = self.tablet
190
191      # Check SKIP conditions.
192      camera_properties_utils.skip_unless(
193          camera_properties_utils.continuous_picture(props) and
194          camera_properties_utils.af_scene_change(props))
195
196      # Load chart for scene.
197      its_session_utils.load_scene(
198          cam, props, self.scene, tablet, self.chart_distance)
199
200      # Do captures with scene change.
201      tablet_level = int(self.tablet_screen_brightness)
202      logging.debug('Tablet brightness: %d', tablet_level)
203      scene_change_delay = _DELAY_DISPLAY
204      cam.do_3a()  # Do 3A up front to settle camera.
205      for burst in range(_NUM_TRIES):
206        logging.debug('burst number: %d', burst)
207        # Create scene change by turning off chart display & capture frames
208        if tablet:
209          multiprocessing.Process(name='p1', target=toggle_screen,
210                                  args=(tablet, scene_change_delay,)).start()
211        else:
212          print('Wave hand in front of camera to create scene change.')
213        cap_data = capture_frames(cam, _DELAY_CAPTURE, burst, log_path)
214
215        # Find frame where 3A converges and final brightness.
216        converged_frame = find_3a_converged_frame(cap_data)
217        converged_flag = True if converged_frame != -1 else False
218        bright_final = cap_data[_NUM_FRAMES - 1]['avg']
219
220        # Determine if scene changed.
221        scene_change_flag, bright_change_flag = determine_if_scene_changed(
222            cap_data, converged_frame)
223
224        # Adjust timing based on captured frames and scene change flags.
225        timing_adjustment = scene_change_utils.calc_timing_adjustment(
226            converged_flag, scene_change_flag, bright_change_flag, bright_final)
227        if timing_adjustment == scene_change_utils.SCENE_CHANGE_PASS_CODE:
228          break
229        elif timing_adjustment == scene_change_utils.SCENE_CHANGE_FAIL_CODE:
230          raise AssertionError('Test fails. Check logging.error.')
231        else:
232          if burst == _NUM_TRIES-1:  # FAIL out after NUM_TRIES.
233            raise AssertionError(f'No scene change in {_NUM_TRIES}x tries.')
234          else:
235            scene_change_delay += timing_adjustment / _FPS
236
237        if tablet:
238          logging.debug('Turning screen back ON.')
239          toggle_screen(tablet)
240
241
242if __name__ == '__main__':
243  test_runner.main()
244