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