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"""Verifies changing AE/AWB regions changes images AE/AWB results.""" 15 16 17import logging 18import math 19import os.path 20 21from mobly import test_runner 22import numpy 23 24import camera_properties_utils 25import capture_request_utils 26import image_processing_utils 27import its_base_test 28import its_session_utils 29import opencv_processing_utils 30import video_processing_utils 31 32_AE_CHANGE_THRESH = 1 # Incorrect behavior is empirically < 0.5 percent 33_AWB_CHANGE_THRESH = 2 # Incorrect behavior is empirically < 1.5 percent 34_AE_AWB_METER_WEIGHT = 1000 # 1 - 1000 with 1000 as the highest 35_ARUCO_MARKERS_COUNT = 4 36_AE_AWB_REGIONS_AVAILABLE = 1 # Valid range is >= 0, and unavailable if 0 37_IMG_FORMAT = 'png' 38_MIRRORED_PREVIEW_SENSOR_ORIENTATIONS = (0, 180) 39_NAME = os.path.splitext(os.path.basename(__file__))[0] 40_NUM_AE_AWB_REGIONS = 4 41_NUM_FRAMES = 4 42_PERCENTAGE = 100 43_REGION_DURATION_MS = 1800 # 1.8 seconds 44 45 46def _do_ae_check(light, dark, file_name_with_path): 47 """Checks luma change between two images is above threshold. 48 49 Checks that the Y-average of image with darker metering region 50 is higher than the Y-average of image with lighter metering 51 region. Y stands for brightness, or "luma". 52 53 Args: 54 light: RGB image; metering light region. 55 dark: RGB image; metering dark region. 56 file_name_with_path: str; path to preview recording. 57 """ 58 # Converts img to YUV and returns Y-average 59 light_y = opencv_processing_utils.convert_to_y(light, 'RGB') 60 light_y_avg = numpy.average(light_y) 61 dark_y = opencv_processing_utils.convert_to_y(dark, 'RGB') 62 dark_y_avg = numpy.average(dark_y) 63 logging.debug('Light image Y-average: %.4f', light_y_avg) 64 logging.debug('Dark image Y-average: %.4f', dark_y_avg) 65 # Checks average change in Y-average between two images 66 y_avg_change = ( 67 (dark_y_avg-light_y_avg)/light_y_avg)*_PERCENTAGE 68 logging.debug('Y-average percentage change: %.4f', y_avg_change) 69 70 # Don't change print to logging. Used for KPI. 71 print(f'{_NAME}_ae_y_change: ', y_avg_change) 72 73 if y_avg_change < _AE_CHANGE_THRESH: 74 raise AssertionError( 75 f'Luma change {y_avg_change} is less than the threshold: ' 76 f'{_AE_CHANGE_THRESH}') 77 else: 78 its_session_utils.remove_mp4_file(file_name_with_path) 79 80 81def _do_awb_check(blue, yellow): 82 """Checks the ratio of red over blue between two RGB images. 83 84 Checks that the R/B of image with blue metering region 85 is higher than the R/B of image with yellow metering 86 region. 87 88 Args: 89 blue: RGB image; metering blue region. 90 yellow: RGB image; metering yellow region. 91 Returns: 92 failure_messages: (list of strings) of error messages. 93 """ 94 # Calculates average red value over average blue value in images 95 blue_r_b_ratio = _get_red_blue_ratio(blue) 96 yellow_r_b_ratio = _get_red_blue_ratio(yellow) 97 logging.debug('Blue image R/B ratio: %s', blue_r_b_ratio) 98 logging.debug('Yellow image R/B ratio: %s', yellow_r_b_ratio) 99 # Calculates change in red over blue values between two images 100 r_b_ratio_change = ( 101 (blue_r_b_ratio-yellow_r_b_ratio)/yellow_r_b_ratio)*_PERCENTAGE 102 logging.debug('R/B ratio change in percentage: %.4f', r_b_ratio_change) 103 104 # Don't change print to logging. Used for KPI. 105 print(f'{_NAME}_awb_rb_change: ', r_b_ratio_change) 106 107 if r_b_ratio_change < _AWB_CHANGE_THRESH: 108 raise AssertionError( 109 f'R/B ratio change {r_b_ratio_change} is less than the' 110 f' threshold: {_AWB_CHANGE_THRESH}') 111 112 113def _extract_and_process_select_frames_from_recording( 114 log_path, file_name, video_fps): 115 """Extract key frames (1 frame per 2 seconds) from recordings. 116 117 Args: 118 log_path: str; file location. 119 file_name: str; file name for saved video. 120 video_fps: str; numerical value of supported video fps. 121 Returns: 122 dictionary of images. 123 """ 124 # TODO: b/330382627 - Add function to preview_processing_utils 125 # Extract key frames from video 126 frames = video_processing_utils.extract_all_frames_from_video( 127 log_path, file_name, _IMG_FORMAT, video_fps) 128 logging.debug('Number of frames %d', len(frames)) 129 # Minus one from interval to avoid going out of bounds 130 interval = math.floor(len(frames) / _NUM_FRAMES) - 1 131 logging.debug('Interval %d', interval) 132 133 # Process select frame files 134 select_frames = [] 135 save_files = [os.path.join(log_path, file_name)] 136 for i, frame in enumerate(frames): 137 frame_path = os.path.join(log_path, frame) 138 if (i % interval == 0) and (i > 0): 139 select_frames.append( 140 image_processing_utils.convert_image_to_numpy_array(frame_path)) 141 save_files.append(frame_path) 142 else: 143 continue 144 logging.debug('Frame size %d x %d', select_frames[0].shape[1], 145 select_frames[0].shape[0]) 146 logging.debug('Number of select frames %d', len(select_frames)) 147 its_session_utils.remove_frame_files(log_path, save_files) 148 return select_frames 149 150 151def _get_red_blue_ratio(img): 152 """Computes the ratios of average red over blue in img. 153 154 Args: 155 img: numpy array; RGB image. 156 Returns: 157 r_b_ratio: float; ratio of R and B channel means. 158 """ 159 img_means = image_processing_utils.compute_image_means(img) 160 r_b_ratio = img_means[0]/img_means[2] 161 return r_b_ratio 162 163 164class AeAwbRegions(its_base_test.ItsBaseTest): 165 """Tests that changing AE and AWB regions changes image's RGB values. 166 167 Test records an 8 seconds preview recording, and meters a different 168 AE/AWB region (blue, light, dark, yellow) for every 2 seconds. 169 Extracts a frame from each second of recording with a total of 8 frames 170 (2 from each region). For AE check, a frame from light is compared to the 171 dark region. For AWB check, a frame from blue is compared to the yellow 172 region. 173 174 """ 175 176 def test_ae_awb_regions(self): 177 """Test AE and AWB regions.""" 178 179 with its_session_utils.ItsSession( 180 device_id=self.dut.serial, 181 camera_id=self.camera_id, 182 hidden_physical_id=self.hidden_physical_id) as cam: 183 props = cam.get_camera_properties() 184 props = cam.override_with_hidden_physical_camera_props(props) 185 log_path = self.log_path 186 test_name_with_log_path = os.path.join(log_path, _NAME) 187 188 # Check skip conditions 189 max_ae_regions = props['android.control.maxRegionsAe'] 190 max_awb_regions = props['android.control.maxRegionsAwb'] 191 first_api_level = its_session_utils.get_first_api_level(self.dut.serial) 192 camera_properties_utils.skip_unless( 193 first_api_level >= its_session_utils.ANDROID15_API_LEVEL and 194 camera_properties_utils.ae_regions(props) and 195 (max_awb_regions >= _AE_AWB_REGIONS_AVAILABLE or 196 max_ae_regions >= _AE_AWB_REGIONS_AVAILABLE)) 197 logging.debug('maximum AE regions: %d', max_ae_regions) 198 logging.debug('maximum AWB regions: %d', max_awb_regions) 199 200 # Load chart for scene 201 its_session_utils.load_scene( 202 cam, props, self.scene, self.tablet, self.chart_distance, 203 log_path) 204 205 # Find largest preview size to define capture size to find aruco markers 206 common_preview_size_info = ( 207 video_processing_utils.get_preview_video_sizes_union( 208 cam, self.camera_id)) 209 preview_size = common_preview_size_info.largest_size 210 width = int(preview_size.split('x')[0]) 211 height = int(preview_size.split('x')[1]) 212 req = capture_request_utils.auto_capture_request() 213 fmt = {'format': 'yuv', 'width': width, 'height': height} 214 cam.do_3a() 215 cap = cam.do_capture(req, fmt) 216 217 # Save image and convert to numpy array 218 img = image_processing_utils.convert_capture_to_rgb_image( 219 cap, props=props) 220 img_path = f'{test_name_with_log_path}_aruco_markers.jpg' 221 image_processing_utils.write_image(img, img_path) 222 img = image_processing_utils.convert_image_to_uint8(img) 223 224 # Define AE/AWB metering regions 225 chart_path = f'{test_name_with_log_path}_chart_boundary.jpg' 226 ae_awb_regions = opencv_processing_utils.define_regions( 227 img, img_path, chart_path, props, width, height) 228 229 # Do preview recording with pre-defined AE/AWB regions 230 recording_obj = cam.do_preview_recording_with_dynamic_ae_awb_region( 231 preview_size, ae_awb_regions, _REGION_DURATION_MS) 232 logging.debug('Tested quality: %s', recording_obj['quality']) 233 234 # Grab the video from the save location on DUT 235 self.dut.adb.pull([recording_obj['recordedOutputPath'], log_path]) 236 file_name = recording_obj['recordedOutputPath'].split('/')[-1] 237 file_name_with_path = os.path.join(log_path, file_name) 238 logging.debug('file_name: %s', file_name) 239 240 # Determine acceptable ranges 241 fps_ranges = camera_properties_utils.get_ae_target_fps_ranges(props) 242 ae_target_fps_range = camera_properties_utils.get_fps_range_to_test( 243 fps_ranges) 244 video_fps = str(ae_target_fps_range[0]) 245 246 # Extract 1 frames per 2 seconds of preview recording 247 # Meters each region of 4 (blue, light, dark, yellow) for 2 seconds 248 # Unpack frames based on metering region's color 249 # If testing front camera with preview mirrored, reverse order. 250 # pylint: disable=unbalanced-tuple-unpacking 251 if ((props['android.lens.facing'] == 252 camera_properties_utils.LENS_FACING['FRONT']) and 253 props['android.sensor.orientation'] in 254 _MIRRORED_PREVIEW_SENSOR_ORIENTATIONS): 255 yellow, dark, light, blue = ( 256 _extract_and_process_select_frames_from_recording( 257 log_path, file_name, video_fps)) 258 else: 259 blue, light, dark, yellow = ( 260 _extract_and_process_select_frames_from_recording( 261 log_path, file_name, video_fps)) 262 263 # AWB Check : Verify R/B ratio change is greater than threshold 264 if max_awb_regions >= _AE_AWB_REGIONS_AVAILABLE: 265 _do_awb_check(blue, yellow) 266 267 # AE Check: Extract the Y component from rectangle patch 268 if max_ae_regions >= _AE_AWB_REGIONS_AVAILABLE: 269 _do_ae_check(light, dark, file_name_with_path) 270 271if __name__ == '__main__': 272 test_runner.main() 273