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