1# Copyright 2023 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"""Verify night extension is activated correctly when requested.""" 15 16 17import logging 18import os.path 19 20import cv2 21from mobly import test_runner 22 23import its_base_test 24import camera_properties_utils 25import capture_request_utils 26import image_processing_utils 27import its_session_utils 28import lighting_control_utils 29import low_light_utils 30 31_NAME = os.path.splitext(os.path.basename(__file__))[0] 32_EXTENSION_NIGHT = 4 # CameraExtensionCharacteristics.EXTENSION_NIGHT 33_TABLET_BRIGHTNESS = '6' # Highest minimum brightness on a supported tablet 34_TAP_COORDINATES = (500, 500) # Location to tap tablet screen via adb 35_TEST_REQUIRED_MPC = 34 36 37_AVG_DELTA_LUMINANCE_THRESH = 17 38_AVG_LUMINANCE_THRESH = 90 39 40_IMAGE_FORMATS_TO_CONSTANTS = (('yuv', 35), ('jpeg', 256)) 41 42_X_STRING = 'x' 43 44 45def _convert_capture(cap, file_stem=None): 46 """Obtains y plane and numpy image from a capture. 47 48 Args: 49 cap: A capture object as returned by its_session_utils.do_capture. 50 file_stem: str; location and name to save files. 51 Returns: 52 numpy image, with the np.uint8 data type. 53 """ 54 img = image_processing_utils.convert_capture_to_rgb_image(cap) 55 if file_stem: 56 image_processing_utils.write_image(img, f'{file_stem}.jpg') 57 return image_processing_utils.convert_image_to_uint8(img) 58 59 60class NightExtensionTest(its_base_test.ItsBaseTest): 61 """Tests night extension under dark lighting conditions. 62 63 A capture is taken with the night extension ON, after AE converges. 64 The capture is analyzed in the same way as test_low_light_boost_extension, 65 checking luminance and the average difference in luminance between 66 successive boxes. 67 """ 68 69 def _take_capture(self, cam, req, out_surfaces): 70 """Takes capture with Night extension ON. 71 72 Args: 73 cam: its_session_utils object. 74 req: capture request. 75 out_surfaces: dictionary of output surfaces. 76 Returns: 77 cap: capture object. 78 """ 79 cap = cam.do_capture_with_extensions(req, _EXTENSION_NIGHT, out_surfaces) 80 metadata = cap['metadata'] 81 logging.debug('capture exposure time: %s', 82 metadata['android.sensor.exposureTime']) 83 logging.debug('capture sensitivity: %s', 84 metadata['android.sensor.sensitivity']) 85 return cap 86 87 def test_night_extension(self): 88 # Handle subdirectory 89 self.scene = 'scene_low_light' 90 with its_session_utils.ItsSession( 91 device_id=self.dut.serial, 92 camera_id=self.camera_id, 93 hidden_physical_id=self.hidden_physical_id) as cam: 94 props = cam.get_camera_properties() 95 props = cam.override_with_hidden_physical_camera_props(props) 96 test_name = os.path.join(self.log_path, _NAME) 97 98 # Determine camera supported extensions 99 supported_extensions = cam.get_supported_extensions(self.camera_id) 100 logging.debug('Supported extensions: %s', supported_extensions) 101 102 # Check media performance class 103 should_run = _EXTENSION_NIGHT in supported_extensions 104 media_performance_class = its_session_utils.get_media_performance_class( 105 self.dut.serial) 106 if (media_performance_class >= _TEST_REQUIRED_MPC and 107 cam.is_primary_camera() and 108 not should_run): 109 its_session_utils.raise_mpc_assertion_error( 110 _TEST_REQUIRED_MPC, _NAME, media_performance_class) 111 112 # Check SKIP conditions 113 camera_properties_utils.skip_unless(should_run) 114 115 tablet_name_unencoded = self.tablet.adb.shell( 116 ['getprop', 'ro.product.device'] 117 ) 118 tablet_name = str(tablet_name_unencoded.decode('utf-8')).strip() 119 logging.debug('Tablet name: %s', tablet_name) 120 121 if (tablet_name.lower() not in 122 low_light_utils.TABLET_LOW_LIGHT_SCENES_ALLOWLIST): 123 raise AssertionError('Tablet not supported for low light scenes.') 124 125 if tablet_name == its_session_utils.TABLET_LEGACY_NAME: 126 raise AssertionError(f'Incompatible tablet! Please use a tablet with ' 127 'display brightness of at least ' 128 f'{its_session_utils.TABLET_DEFAULT_BRIGHTNESS} ' 129 'according to ' 130 f'{its_session_utils.TABLET_REQUIREMENTS_URL}.') 131 132 # Establish connection with lighting controller 133 arduino_serial_port = lighting_control_utils.lighting_control( 134 self.lighting_cntl, self.lighting_ch) 135 136 # Turn OFF lights to darken scene 137 lighting_control_utils.set_lighting_state( 138 arduino_serial_port, self.lighting_ch, 'OFF') 139 140 # Check that tablet is connected and turn it off to validate lighting 141 self.turn_off_tablet() 142 143 # Turn off DUT to reduce reflections 144 lighting_control_utils.turn_off_device_screen(self.dut) 145 146 # Validate lighting, then setup tablet 147 cam.do_3a(do_af=False) 148 cap = cam.do_capture( 149 capture_request_utils.auto_capture_request(), cam.CAP_YUV) 150 y_plane, _, _ = image_processing_utils.convert_capture_to_planes(cap) 151 its_session_utils.validate_lighting( 152 y_plane, self.scene, state='OFF', log_path=self.log_path, 153 tablet_state='OFF') 154 self.setup_tablet() 155 156 its_session_utils.load_scene( 157 cam, props, self.scene, self.tablet, self.chart_distance, 158 lighting_check=False, log_path=self.log_path) 159 160 # Tap tablet to remove gallery buttons 161 if self.tablet: 162 self.tablet.adb.shell( 163 f'input tap {_TAP_COORDINATES[0]} {_TAP_COORDINATES[1]}') 164 165 # Determine capture width, height, and format 166 for format_name, format_constant in _IMAGE_FORMATS_TO_CONSTANTS: 167 capture_sizes = capture_request_utils.get_available_output_sizes( 168 format_name, props) 169 extension_capture_sizes_str = cam.get_supported_extension_sizes( 170 self.camera_id, _EXTENSION_NIGHT, format_constant 171 ) 172 if not extension_capture_sizes_str: 173 continue 174 extension_capture_sizes = [ 175 tuple(int(size_part) for size_part in s.split(_X_STRING)) 176 for s in extension_capture_sizes_str 177 ] 178 # Extension capture sizes ordered in ascending area order by default 179 extension_capture_sizes.reverse() 180 logging.debug('Capture sizes: %s', capture_sizes) 181 logging.debug('Extension capture sizes: %s', extension_capture_sizes) 182 logging.debug('Accepted capture format: %s', format_name) 183 width, height = extension_capture_sizes[0] 184 accepted_format = format_name 185 break 186 else: 187 raise AssertionError('No supported sizes/formats found!') 188 189 # Set tablet brightness to darken scene 190 self.set_screen_brightness(_TABLET_BRIGHTNESS) 191 192 file_stem = f'{test_name}_{self.camera_id}_{accepted_format}_{width}x{height}' 193 out_surfaces = { 194 'format': accepted_format, 'width': width, 'height': height} 195 req = capture_request_utils.auto_capture_request() 196 197 logging.debug('Taking auto capture with night mode ON') 198 night_cap = self._take_capture( 199 cam, req, out_surfaces) 200 rgb_night_img = _convert_capture(night_cap, f'{file_stem}_night') 201 202 # Assert correct behavior and create luminosity plots 203 low_light_utils.analyze_low_light_scene_capture( 204 f'{file_stem}_night', 205 cv2.cvtColor(rgb_night_img, cv2.COLOR_RGB2BGR), 206 _AVG_LUMINANCE_THRESH, 207 _AVG_DELTA_LUMINANCE_THRESH 208 ) 209 210if __name__ == '__main__': 211 test_runner.main() 212