1# Copyright 2022 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"""Verifies that preview FPS reaches minimum under low light conditions."""
15
16
17import logging
18import math
19import os.path
20
21from mobly import test_runner
22import numpy as np
23
24import its_base_test
25import camera_properties_utils
26import capture_request_utils
27import image_processing_utils
28import its_session_utils
29import lighting_control_utils
30import opencv_processing_utils
31import video_processing_utils
32
33_NAME = os.path.splitext(os.path.basename(__file__))[0]
34_PREVIEW_RECORDING_DURATION_SECONDS = 10
35_MAX_VAR_FRAME_DELTA = 0.001  # variance of frame deltas, units: seconds^2
36_FPS_ATOL = 2  # TODO: b/330158924 - explicitly handle anti-banding
37_DARKNESS_ATOL = 0.1 * 255  # openCV uses [0:255] images
38
39
40class PreviewMinFrameRateTest(its_base_test.ItsBaseTest):
41  """Tests preview frame rate under dark lighting conditions.
42
43  Takes preview recording under dark conditions while setting
44  CONTROL_AE_TARGET_FPS_RANGE, and checks that the
45  recording's frame rate is at the minimum of the requested FPS range.
46  """
47
48  def test_preview_min_frame_rate(self):
49    with its_session_utils.ItsSession(
50        device_id=self.dut.serial,
51        camera_id=self.camera_id,
52        hidden_physical_id=self.hidden_physical_id) as cam:
53      props = cam.get_camera_properties()
54      props = cam.override_with_hidden_physical_camera_props(props)
55
56      # check SKIP conditions
57      first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
58      camera_properties_utils.skip_unless(
59          first_api_level >= its_session_utils.ANDROID14_API_LEVEL)
60
61      # determine acceptable ranges
62      fps_ranges = camera_properties_utils.get_ae_target_fps_ranges(props)
63      ae_target_fps_range = camera_properties_utils.get_fps_range_to_test(
64          fps_ranges)
65
66      # establish connection with lighting controller
67      arduino_serial_port = lighting_control_utils.lighting_control(
68          self.lighting_cntl, self.lighting_ch)
69
70      # turn OFF lights to darken scene
71      lighting_control_utils.set_lighting_state(
72          arduino_serial_port, self.lighting_ch, 'OFF')
73
74      # turn OFF DUT to reduce reflections
75      lighting_control_utils.turn_off_device_screen(self.dut)
76
77      # Validate lighting
78      cam.do_3a(do_af=False)
79      cap = cam.do_capture(
80          capture_request_utils.auto_capture_request(), cam.CAP_YUV)
81      y_plane, _, _ = image_processing_utils.convert_capture_to_planes(cap)
82      # In the sensor fusion rig, there is no tablet, so tablet_state is OFF.
83      its_session_utils.validate_lighting(
84          y_plane, self.scene, state='OFF', tablet_state='OFF',
85          log_path=self.log_path)
86
87      logging.debug('Taking preview recording in darkened scene.')
88      # determine camera capabilities for preview
89      preview_sizes = cam.get_supported_preview_sizes(
90          self.camera_id)
91      supported_video_sizes = cam.get_supported_video_sizes_capped(
92          self.camera_id)
93      max_video_size = supported_video_sizes[-1]  # largest available size
94      logging.debug('Camera supported video sizes: %s', supported_video_sizes)
95
96      preview_size = preview_sizes[-1]  # choose largest available size
97      if preview_size <= max_video_size:
98        logging.debug('preview_size is supported by video encoder')
99      else:
100        preview_size = max_video_size
101      logging.debug('Doing 3A to ensure AE convergence')
102      cam.do_3a(do_af=False)
103      logging.debug('Testing preview recording FPS for size: %s', preview_size)
104      preview_recording_obj = cam.do_preview_recording(
105          preview_size, _PREVIEW_RECORDING_DURATION_SECONDS, stabilize=False,
106          zoom_ratio=None,
107          ae_target_fps_min=ae_target_fps_range[0],
108          ae_target_fps_max=ae_target_fps_range[1])
109      logging.debug('preview_recording_obj: %s', preview_recording_obj)
110
111      # turn lights back ON
112      lighting_control_utils.set_lighting_state(
113          arduino_serial_port, self.lighting_ch, 'ON')
114
115      # pull the video recording file from the device.
116      self.dut.adb.pull([preview_recording_obj['recordedOutputPath'],
117                         self.log_path])
118      logging.debug('Recorded preview video is available at: %s',
119                    self.log_path)
120      preview_file_name = preview_recording_obj[
121          'recordedOutputPath'].split('/')[-1]
122      logging.debug('preview_file_name: %s', preview_file_name)
123      preview_file_name_with_path = os.path.join(
124          self.log_path, preview_file_name)
125      preview_frame_rate = video_processing_utils.get_average_frame_rate(
126          preview_file_name_with_path)
127      errors = []
128      if not math.isclose(
129          preview_frame_rate, ae_target_fps_range[0], abs_tol=_FPS_ATOL):
130        errors.append(
131            f'Preview frame rate was {preview_frame_rate:.3f}. '
132            f'Expected to be {ae_target_fps_range[0]}, ATOL: {_FPS_ATOL}.'
133        )
134      frame_deltas = np.array(video_processing_utils.get_frame_deltas(
135          preview_file_name_with_path))
136      frame_delta_avg = np.average(frame_deltas)
137      frame_delta_var = np.var(frame_deltas)
138      logging.debug('Delta avg: %.4f, delta var: %.4f',
139                    frame_delta_avg, frame_delta_var)
140      if frame_delta_var > _MAX_VAR_FRAME_DELTA:
141        errors.append(
142            f'Preview frame delta variance {frame_delta_var:.3f} too large, '
143            f'maximum allowed: {_MAX_VAR_FRAME_DELTA}.'
144        )
145      if errors:
146        raise AssertionError('\n'.join(errors))
147
148      last_key_frame = video_processing_utils.extract_key_frames_from_video(
149          self.log_path, preview_file_name)[-1]
150      logging.debug('Confirming video brightness in frame %s is low enough.',
151                    last_key_frame)
152      last_image = image_processing_utils.convert_image_to_numpy_array(
153          os.path.join(self.log_path, last_key_frame))
154      y_avg = np.average(
155          opencv_processing_utils.convert_to_y(last_image, 'RGB')
156      )
157      logging.debug('Last frame y avg: %.4f', y_avg)
158      if not math.isclose(y_avg, 0, abs_tol=_DARKNESS_ATOL):
159        raise AssertionError(f'Last frame y average: {y_avg}, expected: 0, '
160                             f'ATOL: {_DARKNESS_ATOL}')
161
162if __name__ == '__main__':
163  test_runner.main()
164