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