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