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 time 21import cv2 22from mobly import test_runner 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 low_light_utils 31 32_NAME = os.path.splitext(os.path.basename(__file__))[0] 33_EXTENSION_NIGHT = 4 # CameraExtensionCharacteristics.EXTENSION_NIGHT 34_TEST_REQUIRED_MPC = 34 35 36_AVG_DELTA_LUMINANCE_THRESH = 17 37_AVG_DELTA_LUMINANCE_THRESH_METERED_REGION = 25 38_AVG_LUMINANCE_THRESH = 85 39_AVG_LUMINANCE_THRESH_METERED_REGION = 80 40 41_IMAGE_FORMATS_TO_CONSTANTS = (('yuv', 35), ('jpeg', 256)) 42 43_BRIGHTNESS_SETTING_CHANGE_WAIT_SEC = 5 # Seconds 44_X_STRING = 'x' 45 46 47def _convert_capture(cap, file_stem=None): 48 """Obtains y plane and numpy image from a capture. 49 50 Args: 51 cap: A capture object as returned by its_session_utils.do_capture. 52 file_stem: str; location and name to save files. 53 Returns: 54 numpy image, with the np.uint8 data type. 55 """ 56 img = image_processing_utils.convert_capture_to_rgb_image(cap) 57 if file_stem: 58 image_processing_utils.write_image(img, f'{file_stem}.jpg') 59 return image_processing_utils.convert_image_to_uint8(img) 60 61 62class NightExtensionTest(its_base_test.ItsBaseTest): 63 """Tests night extension under dark lighting conditions. 64 65 A capture is taken with the night extension ON, after AE converges. 66 The capture is analyzed in the same way as test_low_light_boost_extension, 67 checking luminance and the average difference in luminance between 68 successive boxes. 69 """ 70 71 def _take_capture(self, cam, req, out_surfaces): 72 """Takes capture with night extension ON. 73 74 Args: 75 cam: its_session_utils object. 76 req: capture request. 77 out_surfaces: dictionary of output surfaces. 78 Returns: 79 cap: capture object. 80 """ 81 cap = cam.do_capture_with_extensions(req, _EXTENSION_NIGHT, out_surfaces) 82 metadata = cap['metadata'] 83 logging.debug('capture exposure time: %s', 84 metadata['android.sensor.exposureTime']) 85 logging.debug('capture sensitivity: %s', 86 metadata['android.sensor.sensitivity']) 87 return cap 88 89 def _take_capture_and_analyze(self, cam, req, out_surfaces, file_stem, 90 metering_region, use_metering_region, 91 first_api_level): 92 """Takes capture with night extension ON and analyzes it. 93 94 Args: 95 cam: its_session_utils object. 96 req: capture request. 97 out_surfaces: dictionary of output surfaces. 98 file_stem: File prefix for captured images. 99 metering_region: The metering region to use for the capture. 100 use_metering_region: Whether to use the metering region. 101 first_api_level: The first API level of the device under test. 102 """ 103 avg_luminance_thresh = _AVG_LUMINANCE_THRESH 104 avg_delta_luminance_thresh = _AVG_DELTA_LUMINANCE_THRESH 105 if use_metering_region and metering_region is not None: 106 logging.debug('metering_region: %s', metering_region) 107 req['android.control.aeRegions'] = [metering_region] 108 req['android.control.afRegions'] = [metering_region] 109 req['android.control.awbRegions'] = [metering_region] 110 avg_luminance_thresh = _AVG_LUMINANCE_THRESH_METERED_REGION 111 avg_delta_luminance_thresh = _AVG_DELTA_LUMINANCE_THRESH_METERED_REGION 112 cap = self._take_capture(cam, req, out_surfaces) 113 rgb_night_img = _convert_capture(cap, f'{file_stem}_night') 114 115 # Assert correct behavior and create luminosity plots 116 try: 117 low_light_utils.analyze_low_light_scene_capture( 118 f'{file_stem}_night', 119 cv2.cvtColor(rgb_night_img, cv2.COLOR_RGB2BGR), 120 avg_luminance_thresh, 121 avg_delta_luminance_thresh 122 ) 123 except AssertionError as e: 124 # On Android 15, we initially test without metered region. If it fails, we 125 # fallback to test with metered region. Otherwise, for newer than 126 # Android 15, we always start test with metered region. 127 if ( 128 first_api_level <= its_session_utils.ANDROID15_API_LEVEL 129 and not use_metering_region 130 ): 131 logging.debug('Retrying with metering region: %s', e) 132 self._take_capture_and_analyze(cam, req, out_surfaces, file_stem, 133 metering_region, True, first_api_level) 134 else: 135 raise e 136 137 def test_night_extension(self): 138 # Handle subdirectory 139 self.scene = 'scene_low_light' 140 with its_session_utils.ItsSession( 141 device_id=self.dut.serial, 142 camera_id=self.camera_id, 143 hidden_physical_id=self.hidden_physical_id) as cam: 144 props = cam.get_camera_properties() 145 props = cam.override_with_hidden_physical_camera_props(props) 146 test_name = os.path.join(self.log_path, _NAME) 147 camera_id = self.camera_id 148 149 # Determine camera supported extensions 150 supported_extensions = cam.get_supported_extensions(camera_id) 151 logging.debug('Supported extensions: %s', supported_extensions) 152 153 # Check media performance class 154 should_run = _EXTENSION_NIGHT in supported_extensions 155 media_performance_class = its_session_utils.get_media_performance_class( 156 self.dut.serial) 157 if (media_performance_class >= _TEST_REQUIRED_MPC and 158 cam.is_primary_camera() and 159 not should_run): 160 its_session_utils.raise_mpc_assertion_error( 161 _TEST_REQUIRED_MPC, _NAME, media_performance_class) 162 163 # Check SKIP conditions 164 camera_properties_utils.skip_unless(should_run) 165 166 tablet_name_unencoded = self.tablet.adb.shell( 167 ['getprop', 'ro.product.device'] 168 ) 169 tablet_name = str(tablet_name_unencoded.decode('utf-8')).strip() 170 logging.debug('Tablet name: %s', tablet_name) 171 172 if (tablet_name.lower() not in 173 low_light_utils.TABLET_LOW_LIGHT_SCENES_ALLOWLIST): 174 raise AssertionError('Tablet not supported for low light scenes.') 175 176 if tablet_name == its_session_utils.TABLET_LEGACY_NAME: 177 raise AssertionError(f'Incompatible tablet! Please use a tablet with ' 178 'display brightness of at least ' 179 f'{its_session_utils.TABLET_DEFAULT_BRIGHTNESS} ' 180 'according to ' 181 f'{its_session_utils.TABLET_REQUIREMENTS_URL}.') 182 183 # Establish connection with lighting controller 184 arduino_serial_port = lighting_control_utils.lighting_control( 185 self.lighting_cntl, self.lighting_ch) 186 187 # Turn OFF lights to darken scene 188 lighting_control_utils.set_lighting_state( 189 arduino_serial_port, self.lighting_ch, 'OFF') 190 191 # Check that tablet is connected and turn it off to validate lighting 192 self.turn_off_tablet() 193 194 # Turn off DUT to reduce reflections 195 lighting_control_utils.turn_off_device_screen(self.dut) 196 197 # Validate lighting, then setup tablet 198 cam.do_3a(do_af=False) 199 cap = cam.do_capture( 200 capture_request_utils.auto_capture_request(), cam.CAP_YUV) 201 y_plane, _, _ = image_processing_utils.convert_capture_to_planes(cap) 202 its_session_utils.validate_lighting( 203 y_plane, self.scene, state='OFF', log_path=self.log_path, 204 tablet_state='OFF') 205 self.setup_tablet() 206 207 its_session_utils.load_scene( 208 cam, props, self.scene, self.tablet, self.chart_distance, 209 lighting_check=False, log_path=self.log_path) 210 211 # Determine capture width, height, and format 212 for format_name, format_constant in _IMAGE_FORMATS_TO_CONSTANTS: 213 capture_sizes = capture_request_utils.get_available_output_sizes( 214 format_name, props) 215 extension_capture_sizes_str = cam.get_supported_extension_sizes( 216 camera_id, _EXTENSION_NIGHT, format_constant 217 ) 218 if not extension_capture_sizes_str: 219 continue 220 extension_capture_sizes = [ 221 tuple(int(size_part) for size_part in s.split(_X_STRING)) 222 for s in extension_capture_sizes_str 223 ] 224 # Extension capture sizes ordered in ascending area order by default 225 extension_capture_sizes.reverse() 226 logging.debug('Capture sizes: %s', capture_sizes) 227 logging.debug('Extension capture sizes: %s', extension_capture_sizes) 228 logging.debug('Accepted capture format: %s', format_name) 229 width, height = extension_capture_sizes[0] 230 accepted_format = format_name 231 break 232 else: 233 raise AssertionError('No supported sizes/formats found!') 234 235 file_stem = ( 236 f'{test_name}_{self.camera_id}_{accepted_format}_{width}x{height}' 237 ) 238 out_surfaces = { 239 'format': accepted_format, 'width': width, 'height': height} 240 req = capture_request_utils.auto_capture_request() 241 metering_region = low_light_utils.get_metering_region(cam, file_stem) 242 first_api_level = its_session_utils.get_first_api_level(self.dut.serial) 243 use_metering_region = ( 244 first_api_level > its_session_utils.ANDROID15_API_LEVEL 245 ) 246 247 # Set tablet brightness to darken scene 248 brightness = low_light_utils.TABLET_BRIGHTNESS[tablet_name.lower()] 249 if (props['android.lens.facing'] == 250 camera_properties_utils.LENS_FACING['BACK']): 251 self.set_screen_brightness(brightness[0]) 252 elif (props['android.lens.facing'] == 253 camera_properties_utils.LENS_FACING['FRONT']): 254 self.set_screen_brightness(brightness[1]) 255 else: 256 logging.debug('Only front and rear camera supported. ' 257 'Skipping for camera ID %s', 258 self.camera_id) 259 camera_properties_utils.skip_unless(False) 260 261 logging.debug('Taking auto capture with night mode ON') 262 # Wait for tablet brightness to change 263 time.sleep(_BRIGHTNESS_SETTING_CHANGE_WAIT_SEC) 264 self._take_capture_and_analyze(cam, req, out_surfaces, file_stem, 265 metering_region, use_metering_region, 266 first_api_level) 267 268if __name__ == '__main__': 269 test_runner.main() 270