# Copyright 2022 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. """Verifies that preview FPS reaches minimum under low light conditions.""" import logging import math import os.path from mobly import test_runner import numpy as np import its_base_test import camera_properties_utils import capture_request_utils import image_processing_utils import its_session_utils import lighting_control_utils import opencv_processing_utils import video_processing_utils _NAME = os.path.splitext(os.path.basename(__file__))[0] _PREVIEW_RECORDING_DURATION_SECONDS = 10 _MAX_VAR_FRAME_DELTA = 0.001 # variance of frame deltas, units: seconds^2 _FPS_ATOL = 2 # TODO: b/330158924 - explicitly handle anti-banding _DARKNESS_ATOL = 0.1 * 255 # openCV uses [0:255] images class PreviewMinFrameRateTest(its_base_test.ItsBaseTest): """Tests preview frame rate under dark lighting conditions. Takes preview recording under dark conditions while setting CONTROL_AE_TARGET_FPS_RANGE, and checks that the recording's frame rate is at the minimum of the requested FPS range. """ def test_preview_min_frame_rate(self): with its_session_utils.ItsSession( device_id=self.dut.serial, camera_id=self.camera_id, hidden_physical_id=self.hidden_physical_id) as cam: props = cam.get_camera_properties() props = cam.override_with_hidden_physical_camera_props(props) # check SKIP conditions first_api_level = its_session_utils.get_first_api_level(self.dut.serial) camera_properties_utils.skip_unless( first_api_level >= its_session_utils.ANDROID14_API_LEVEL) # determine acceptable ranges fps_ranges = camera_properties_utils.get_ae_target_fps_ranges(props) ae_target_fps_range = camera_properties_utils.get_fps_range_to_test( fps_ranges) # establish connection with lighting controller arduino_serial_port = lighting_control_utils.lighting_control( self.lighting_cntl, self.lighting_ch) # turn OFF lights to darken scene lighting_control_utils.set_lighting_state( arduino_serial_port, self.lighting_ch, 'OFF') # turn OFF DUT to reduce reflections lighting_control_utils.turn_off_device_screen(self.dut) # Validate lighting cam.do_3a(do_af=False) cap = cam.do_capture( capture_request_utils.auto_capture_request(), cam.CAP_YUV) y_plane, _, _ = image_processing_utils.convert_capture_to_planes(cap) # In the sensor fusion rig, there is no tablet, so tablet_state is OFF. its_session_utils.validate_lighting( y_plane, self.scene, state='OFF', tablet_state='OFF', log_path=self.log_path) logging.debug('Taking preview recording in darkened scene.') # determine camera capabilities for preview preview_sizes = cam.get_supported_preview_sizes( self.camera_id) supported_video_sizes = cam.get_supported_video_sizes_capped( self.camera_id) max_video_size = supported_video_sizes[-1] # largest available size logging.debug('Camera supported video sizes: %s', supported_video_sizes) preview_size = preview_sizes[-1] # choose largest available size if preview_size <= max_video_size: logging.debug('preview_size is supported by video encoder') else: preview_size = max_video_size logging.debug('Doing 3A to ensure AE convergence') cam.do_3a(do_af=False) logging.debug('Testing preview recording FPS for size: %s', preview_size) preview_recording_obj = cam.do_preview_recording( preview_size, _PREVIEW_RECORDING_DURATION_SECONDS, stabilize=False, zoom_ratio=None, ae_target_fps_min=ae_target_fps_range[0], ae_target_fps_max=ae_target_fps_range[1]) logging.debug('preview_recording_obj: %s', preview_recording_obj) # turn lights back ON lighting_control_utils.set_lighting_state( arduino_serial_port, self.lighting_ch, 'ON') # pull the video recording file from the device. self.dut.adb.pull([preview_recording_obj['recordedOutputPath'], self.log_path]) logging.debug('Recorded preview video is available at: %s', self.log_path) preview_file_name = preview_recording_obj[ 'recordedOutputPath'].split('/')[-1] logging.debug('preview_file_name: %s', preview_file_name) preview_file_name_with_path = os.path.join( self.log_path, preview_file_name) preview_frame_rate = video_processing_utils.get_average_frame_rate( preview_file_name_with_path) errors = [] if not math.isclose( preview_frame_rate, ae_target_fps_range[0], abs_tol=_FPS_ATOL): errors.append( f'Preview frame rate was {preview_frame_rate:.3f}. ' f'Expected to be {ae_target_fps_range[0]}, ATOL: {_FPS_ATOL}.' ) frame_deltas = np.array(video_processing_utils.get_frame_deltas( preview_file_name_with_path)) frame_delta_avg = np.average(frame_deltas) frame_delta_var = np.var(frame_deltas) logging.debug('Delta avg: %.4f, delta var: %.4f', frame_delta_avg, frame_delta_var) if frame_delta_var > _MAX_VAR_FRAME_DELTA: errors.append( f'Preview frame delta variance {frame_delta_var:.3f} too large, ' f'maximum allowed: {_MAX_VAR_FRAME_DELTA}.' ) if errors: raise AssertionError('\n'.join(errors)) last_key_frame = video_processing_utils.extract_key_frames_from_video( self.log_path, preview_file_name)[-1] logging.debug('Confirming video brightness in frame %s is low enough.', last_key_frame) last_image = image_processing_utils.convert_image_to_numpy_array( os.path.join(self.log_path, last_key_frame)) y_avg = np.average( opencv_processing_utils.convert_to_y(last_image, 'RGB') ) logging.debug('Last frame y avg: %.4f', y_avg) if not math.isclose(y_avg, 0, abs_tol=_DARKNESS_ATOL): raise AssertionError(f'Last frame y average: {y_avg}, expected: 0, ' f'ATOL: {_DARKNESS_ATOL}') if __name__ == '__main__': test_runner.main()