1# Copyright 2024 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 low light boost api is activated correctly when requested.""" 15 16 17import cv2 18import logging 19import os.path 20import time 21 22from mobly import test_runner 23import numpy as np 24 25import its_base_test 26import camera_properties_utils 27import capture_request_utils 28import image_processing_utils 29import its_session_utils 30import lighting_control_utils 31import low_light_utils 32import preview_processing_utils 33 34_AE_LOW_LIGHT_BOOST_MODE = 6 35 36_CONTROL_AF_MODE_AUTO = 1 37_CONTROL_AWB_MODE_AUTO = 1 38_CONTROL_MODE_AUTO = 1 39_CONTROL_VIDEO_STABILIZATION_MODE_OFF = 0 40_LENS_OPTICAL_STABILIZATION_MODE_OFF = 0 41 42_EXTENSION_NIGHT = 4 # CameraExtensionCharacteristics#EXTENSION_NIGHT 43_EXTENSION_NONE = -1 # Use Camera2 instead of a Camera Extension 44_NAME = os.path.splitext(os.path.basename(__file__))[0] 45_NUM_FRAMES_TO_WAIT = 40 # The preview frame number to capture 46_BRIGHTNESS_SETTING_CHANGE_WAIT_SEC = 5 # Seconds 47 48_AVG_DELTA_LUMINANCE_THRESH = 18 49_AVG_DELTA_LUMINANCE_THRESH_METERED_REGION = 17 50_AVG_LUMINANCE_THRESH = 70 51_AVG_LUMINANCE_THRESH_METERED_REGION = 54 52 53_CAPTURE_REQUEST = { 54 'android.control.mode': _CONTROL_MODE_AUTO, 55 'android.control.aeMode': _AE_LOW_LIGHT_BOOST_MODE, 56 'android.control.awbMode': _CONTROL_AWB_MODE_AUTO, 57 'android.control.afMode': _CONTROL_AF_MODE_AUTO, 58 'android.lens.opticalStabilizationMode': 59 _LENS_OPTICAL_STABILIZATION_MODE_OFF, 60 'android.control.videoStabilizationMode': 61 _CONTROL_VIDEO_STABILIZATION_MODE_OFF, 62} 63 64 65def _capture_and_analyze(cam, file_stem, camera_id, preview_size, extension, 66 mirror_output, metering_region, use_metering_region, 67 first_api_level): 68 """Capture a preview frame and then analyze it. 69 70 Args: 71 cam: ItsSession object to send commands. 72 file_stem: File prefix for captured images. 73 camera_id: Camera ID under test. 74 preview_size: Target size of preview. 75 extension: Extension mode or -1 to use Camera2. 76 mirror_output: If the output should be mirrored across the vertical axis. 77 metering_region: The metering region to use for the capture. 78 use_metering_region: Whether to use the metering region. 79 first_api_level: The first API level of the device under test. 80 """ 81 luminance_thresh = _AVG_LUMINANCE_THRESH 82 delta_luminance_thresh = _AVG_DELTA_LUMINANCE_THRESH 83 capture_request = dict(_CAPTURE_REQUEST) 84 if use_metering_region and metering_region is not None: 85 logging.debug('metering_region: %s', metering_region) 86 capture_request['android.control.aeRegions'] = [metering_region] 87 capture_request['android.control.afRegions'] = [metering_region] 88 capture_request['android.control.awbRegions'] = [metering_region] 89 luminance_thresh = _AVG_LUMINANCE_THRESH_METERED_REGION 90 delta_luminance_thresh = _AVG_DELTA_LUMINANCE_THRESH_METERED_REGION 91 92 frame_bytes = cam.do_capture_preview_frame( 93 camera_id, preview_size, _NUM_FRAMES_TO_WAIT, extension, capture_request 94 ) 95 np_array = np.frombuffer(frame_bytes, dtype=np.uint8) 96 img_rgb = cv2.imdecode(np_array, cv2.IMREAD_COLOR) 97 98 if mirror_output: 99 img_rgb = cv2.flip(img_rgb, 1) 100 try: 101 low_light_utils.analyze_low_light_scene_capture( 102 file_stem, img_rgb, luminance_thresh, delta_luminance_thresh 103 ) 104 except AssertionError as e: 105 # On Android 15, we initially test without metered region. If it fails, we 106 # fallback to test with metered region. Otherwise, for newer than 107 # Android 15, we always start test with metered region. 108 if ( 109 first_api_level == its_session_utils.ANDROID15_API_LEVEL 110 and not use_metering_region 111 ): 112 logging.debug('Retrying with metering region: %s', e) 113 _capture_and_analyze(cam, file_stem, camera_id, preview_size, extension, 114 mirror_output, metering_region, True, 115 first_api_level) 116 else: 117 raise e 118 119 120class LowLightBoostTest(its_base_test.ItsBaseTest): 121 """Tests low light boost mode under dark lighting conditions. 122 123 The test checks if low light boost AE mode is available. The test is skipped 124 if it is not available for Camera2 and Camera Extensions Night Mode. 125 126 Low light boost is enabled and a frame from the preview stream is captured 127 for analysis. The analysis applies the following operations: 128 1. Crops the region defined by a red square outline 129 2. Detects the presence of 20 boxes 130 3. Computes the luminance bounded by each box 131 4. Determines the average luminance of the 6 darkest boxes according to the 132 Hilbert curve arrangement of the grid. 133 5. Determines the average difference in luminance of the 6 successive 134 darkest boxes. 135 6. Checks for passing criteria: the avg luminance must be at least 90 or 136 greater, the avg difference in luminance between successive boxes must be 137 at least 18 or greater. 138 """ 139 140 def test_low_light_boost(self): 141 self.scene = 'scene_low_light' 142 with its_session_utils.ItsSession( 143 device_id=self.dut.serial, 144 camera_id=self.camera_id, 145 hidden_physical_id=self.hidden_physical_id) as cam: 146 props = cam.get_camera_properties() 147 props = cam.override_with_hidden_physical_camera_props(props) 148 test_name = os.path.join(self.log_path, _NAME) 149 150 # Check SKIP conditions 151 # Determine if DUT is at least Android 15 152 first_api_level = its_session_utils.get_first_api_level(self.dut.serial) 153 camera_properties_utils.skip_unless( 154 first_api_level >= its_session_utils.ANDROID15_API_LEVEL) 155 156 # Determine if low light boost is available 157 is_low_light_boost_supported = ( 158 cam.is_low_light_boost_available(self.camera_id, _EXTENSION_NONE)) 159 is_low_light_boost_supported_night = ( 160 cam.is_low_light_boost_available(self.camera_id, _EXTENSION_NIGHT)) 161 should_run = (is_low_light_boost_supported or 162 is_low_light_boost_supported_night) 163 camera_properties_utils.skip_unless(should_run) 164 165 tablet_name_unencoded = self.tablet.adb.shell( 166 ['getprop', 'ro.product.device'] 167 ) 168 tablet_name = str(tablet_name_unencoded.decode('utf-8')).strip() 169 logging.debug('Tablet name: %s', tablet_name) 170 171 if (tablet_name.lower() not in 172 low_light_utils.TABLET_LOW_LIGHT_SCENES_ALLOWLIST): 173 raise AssertionError('Tablet not supported for low light scenes.') 174 175 if tablet_name == its_session_utils.TABLET_LEGACY_NAME: 176 raise AssertionError(f'Incompatible tablet! Please use a tablet with ' 177 'display brightness of at least ' 178 f'{its_session_utils.TABLET_DEFAULT_BRIGHTNESS} ' 179 'according to ' 180 f'{its_session_utils.TABLET_REQUIREMENTS_URL}.') 181 182 # Establish connection with lighting controller 183 arduino_serial_port = lighting_control_utils.lighting_control( 184 self.lighting_cntl, self.lighting_ch) 185 186 # Turn OFF lights to darken scene 187 lighting_control_utils.set_lighting_state( 188 arduino_serial_port, self.lighting_ch, 'OFF') 189 190 # Check that tablet is connected and turn it off to validate lighting 191 self.turn_off_tablet() 192 193 # Turn off DUT to reduce reflections 194 lighting_control_utils.turn_off_device_screen(self.dut) 195 196 # Validate lighting, then setup tablet 197 cam.do_3a(do_af=False) 198 cap = cam.do_capture( 199 capture_request_utils.auto_capture_request(), cam.CAP_YUV) 200 y_plane, _, _ = image_processing_utils.convert_capture_to_planes(cap) 201 its_session_utils.validate_lighting( 202 y_plane, self.scene, state='OFF', log_path=self.log_path, 203 tablet_state='OFF') 204 self.setup_tablet() 205 206 its_session_utils.load_scene( 207 cam, props, self.scene, self.tablet, self.chart_distance, 208 lighting_check=False, log_path=self.log_path) 209 metering_region = low_light_utils.get_metering_region( 210 cam, f'{test_name}_{self.camera_id}') 211 use_metering_region = ( 212 first_api_level > its_session_utils.ANDROID15_API_LEVEL 213 ) 214 215 # Set tablet brightness to darken scene 216 props = cam.get_camera_properties() 217 brightness = low_light_utils.TABLET_BRIGHTNESS[tablet_name.lower()] 218 if (props['android.lens.facing'] == 219 camera_properties_utils.LENS_FACING['BACK']): 220 self.set_screen_brightness(brightness[0]) 221 elif (props['android.lens.facing'] == 222 camera_properties_utils.LENS_FACING['FRONT']): 223 self.set_screen_brightness(brightness[1]) 224 else: 225 logging.debug('Only front and rear camera supported. ' 226 'Skipping for camera ID %s', 227 self.camera_id) 228 camera_properties_utils.skip_unless(False) 229 230 cam.do_3a() 231 232 # Mirror the capture across the vertical axis if captured by front facing 233 # camera 234 should_mirror = (props['android.lens.facing'] == 235 camera_properties_utils.LENS_FACING['FRONT']) 236 237 # Since low light boost can be supported by Camera2 and Night Mode 238 # Extensions, run the test for both (if supported) 239 # Wait for tablet brightness to change 240 time.sleep(_BRIGHTNESS_SETTING_CHANGE_WAIT_SEC) 241 if is_low_light_boost_supported: 242 # Determine preview width and height to test 243 target_preview_size = ( 244 preview_processing_utils.get_max_preview_test_size( 245 cam, self.camera_id)) 246 logging.debug('target_preview_size: %s', target_preview_size) 247 248 logging.debug('capture frame using camera2') 249 file_stem = f'{test_name}_{self.camera_id}_camera2' 250 _capture_and_analyze(cam, file_stem, self.camera_id, 251 target_preview_size, _EXTENSION_NONE, 252 should_mirror, metering_region, 253 use_metering_region, first_api_level) 254 255 if is_low_light_boost_supported_night: 256 # Determine preview width and height to test 257 target_preview_size = ( 258 preview_processing_utils.get_max_extension_preview_test_size( 259 cam, self.camera_id, _EXTENSION_NIGHT 260 ) 261 ) 262 logging.debug('target_preview_size: %s', target_preview_size) 263 264 logging.debug('capture frame using night mode extension') 265 file_stem = f'{test_name}_{self.camera_id}_camera_extension' 266 _capture_and_analyze(cam, file_stem, self.camera_id, 267 target_preview_size, _EXTENSION_NIGHT, 268 should_mirror, metering_region, 269 use_metering_region, first_api_level) 270 271 272if __name__ == '__main__': 273 test_runner.main() 274