| # 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 |
| _CONTROL_AE_ANTIBANDING_MODE_OFF = 0 |
| _MAX_VAR_FRAME_DELTA = 0.001 # variance of frame deltas, units: seconds^2 |
| _FPS_ATOL = 1 |
| _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) |
| # Check for flickering frequency |
| scene_flicker_freq = cap['metadata']['android.statistics.sceneFlicker'] |
| logging.debug('Detected flickering frequency: %d', scene_flicker_freq) |
| 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) |
| antibanding_mode_to_set = scene_flicker_freq |
| if _CONTROL_AE_ANTIBANDING_MODE_OFF in props.get( |
| 'android.control.aeAvailableAntibandingModes', []): |
| antibanding_mode_to_set = _CONTROL_AE_ANTIBANDING_MODE_OFF |
| 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], |
| antibanding_mode=antibanding_mode_to_set |
| ) |
| 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_avg_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() |