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"""Utility functions for Default camera app and JCA image Parity metrics.""" 15 16import logging 17import math 18 19import cv2 20import numpy as np 21 22_DYNAMIC_PATCH_MID_TONE_START_IDX = 5 23_DYNAMIC_PATCH_MID_TONE_END_IDX = 15 24AR_REL_TOL = 0.1 25EXPECTED_BRIGHTNESS_50 = 50.0 26MAX_BRIGHTNESS_DIFF_ABSOLUTE_ERROR = 10.0 27MAX_BRIGHTNESS_DIFF_RELATIVE_ERROR = 8.0 28MAX_DELTA_AB_WHITE_BALANCE_ABSOLUTE_ERROR = 6.0 29MAX_DELTA_AB_WHITE_BALANCE_RELATIVE_ERROR = 3.0 30# This is the height of center QR code on feature chart in cm 31CENTER_QR_CODE_CM = 5 32FOV_REL_TOL = 0.1 33 34 35def check_if_qr_code_size_match(img1, img2): 36 """Checks if the size of two images are the same or not. 37 38 Args: 39 img1: first image array in BGRA format 40 img2: second image array in BGRA format 41 Returns: 42 True if the size of two images are the same, False otherwise 43 """ 44 # Extract the alpha channel 45 alpha_channel_1 = img1[:, :, 3] 46 alpha_channel_2 = img2[:, :, 3] 47 48 # Find the non-zero (non-transparent) pixels 49 y1_indices, x1_indices = np.where(alpha_channel_1 != 0) 50 y2_indices, x2_indices = np.where(alpha_channel_2 != 0) 51 52 # Get the bounding box of the non-transparent region 53 min_x1 = np.min(x1_indices) 54 min_y1 = np.min(y1_indices) 55 max_x1 = np.max(x1_indices) 56 max_y1 = np.max(y1_indices) 57 58 min_x2 = np.min(x2_indices) 59 min_y2 = np.min(y2_indices) 60 max_x2 = np.max(x2_indices) 61 max_y2 = np.max(y2_indices) 62 63 # Crop the image to the bounding box 64 non_tranpsarent_patch_1 = img1[min_y1:max_y1 + 1, min_x1:max_x1 + 1] 65 non_tranpsarent_patch_2 = img2[min_y2:max_y2 + 1, min_x2:max_x2 + 1] 66 67 height1, width1 = non_tranpsarent_patch_1.shape[:2] 68 logging.debug('Height 1: %s, Width 1: %s', height1, width1) 69 ar_1 = width1 / height1 70 logging.debug('Aspect ratio 1: %.2f', ar_1) 71 if not math.isclose(ar_1, 1, rel_tol=AR_REL_TOL): 72 raise ValueError( 73 'Aspect ratio of the non-transparent region of the image 1 is not 1:1.' 74 ) 75 height2, width2 = non_tranpsarent_patch_2.shape[:2] 76 logging.debug('Height 2: %s, Width 2: %s', height2, width2) 77 ar_2 = width2 / height2 78 logging.debug('Aspect ratio 2: %.2f', ar_2) 79 if not math.isclose(ar_2, 1, rel_tol=AR_REL_TOL): 80 raise ValueError( 81 'Aspect ratio of the non-transparent region of the image 2 is not 1:1.' 82 ) 83 return math.isclose(height1, height2, rel_tol=AR_REL_TOL) 84 85 86def get_lab_mean_values(img): 87 """Computes the mean values of the 'L', 'A', and 'B' channels. 88 89 Converts the img from RGB to CIELAB color space and calculates the mean values 90 of L, A and B channels only for the non-transparent regions of the image 91 92 Args: 93 img: img array in RGB colorspace. 94 Returns: 95 mean_l, mean_a, mean_b: mean value of l, a, b channels 96 """ 97 img_lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB) 98 img_lab = img_lab.astype(np.uint32) 99 mean_l = np.mean(img_lab[:, :, 0]) * 100 / 255 100 mean_a = np.mean(img_lab[:, :, 1]) - 128 101 mean_b = np.mean(img_lab[:, :, 2]) - 128 102 logging.debug('L, A, B values: %.2f %.2f %.2f', mean_l, mean_a, mean_b) 103 return mean_l, mean_a, mean_b 104 105 106def get_brightness_variation( 107 default_brightness_values, jca_brightness_values 108): 109 """Gets the brightness variation between default and jca color cells. 110 111 Args: 112 default_brightness_values: The default brightness values of the greyscale 113 cells 114 jca_brightness_values: The jca brightness values of the greyscale cells 115 116 Returns: 117 mean_delta_ab_diff: mean delta ab diff between default and jca rounded 118 upto 2 places 119 """ 120 default_brightness = np.mean(default_brightness_values) 121 jca_brightness = np.mean(jca_brightness_values) 122 123 default_ref_brightness_diff = default_brightness - EXPECTED_BRIGHTNESS_50 124 jca_ref_brightness_diff = jca_brightness - EXPECTED_BRIGHTNESS_50 125 default_jca_brightness_diff = jca_brightness - default_brightness 126 logging.debug('default_ref_brightness_diff: %.2f', 127 default_ref_brightness_diff) 128 logging.debug('jca_ref_brightness_diff: %.2f', 129 jca_ref_brightness_diff) 130 logging.debug('default_jca_brightness_diff: %.2f', 131 default_jca_brightness_diff) 132 133 # Check that the brightness difference default and jca to the reference do not 134 # exceed the max absolute error 135 if (default_ref_brightness_diff > MAX_BRIGHTNESS_DIFF_ABSOLUTE_ERROR) or ( 136 jca_ref_brightness_diff > MAX_BRIGHTNESS_DIFF_ABSOLUTE_ERROR 137 ): 138 e_msg = ( 139 f'The brightness of default and jca for greyscale cells exceeds the' 140 f' threshold. Actual default: {default_ref_brightness_diff:.2f}, Actual' 141 f' jca: {default_jca_brightness_diff:.2f}, Expected:' 142 f' {MAX_BRIGHTNESS_DIFF_ABSOLUTE_ERROR:.1f}' 143 ) 144 logging.debug(e_msg) 145 # Check that the brightness between default and jca does not exceed the 146 # max relative error 147 if (default_jca_brightness_diff > MAX_BRIGHTNESS_DIFF_RELATIVE_ERROR): 148 e_msg = ( 149 f'The brightness difference between default and jca for greyscale cells' 150 f' exceeds the threshold. Actual: {default_jca_brightness_diff:.2f}, ' 151 f'Expected: {MAX_BRIGHTNESS_DIFF_RELATIVE_ERROR:.1f}' 152 ) 153 logging.debug(e_msg) 154 return default_jca_brightness_diff 155 156 157def do_brightness_check(default_patch_list, jca_patch_list): 158 """Computes brightness diff between default and jca capture images. 159 160 Args: 161 default_patch_list: default camera dynamic range patch cells 162 jca_patch_list: jca camera dynamic range patch cells 163 164 Returns: 165 mean_brightness_diff: mean brightness diff between default and jca 166 """ 167 default_brightness_values = [] 168 for patch in default_patch_list: 169 mean_l, _, _ = get_lab_mean_values(patch) 170 default_brightness_values.append(mean_l) 171 jca_brightness_values = [] 172 for patch in jca_patch_list: 173 mean_l, _, _ = get_lab_mean_values(patch) 174 jca_brightness_values.append(mean_l) 175 176 default_rounded_values = [round(float(x), 2) 177 for x in default_brightness_values] 178 jca_rounded_values = [round(float(x), 2) for x in jca_brightness_values] 179 180 logging.debug('default_brightness_values: %s', default_rounded_values) 181 logging.debug('jca_brightness_values: %s', jca_rounded_values) 182 183 mean_brightness_diff = get_brightness_variation( 184 default_brightness_values[ 185 _DYNAMIC_PATCH_MID_TONE_START_IDX:_DYNAMIC_PATCH_MID_TONE_END_IDX 186 ], 187 jca_brightness_values[ 188 _DYNAMIC_PATCH_MID_TONE_START_IDX:_DYNAMIC_PATCH_MID_TONE_END_IDX 189 ], 190 ) 191 logging.debug( 192 'Brightness difference between default and jca: %.2f', 193 mean_brightness_diff, 194 ) 195 return round(float(mean_brightness_diff), 2) 196 197 198def get_neutral_delta_ab(greyscale_cells): 199 """Returns the delta ab value for grey scale cells compared to reference. 200 201 Args: 202 greyscale_cells: list of grey scale cells 203 204 Returns: 205 neutral_delta_ab_values: list of neutral delta ab values for each color cell 206 """ 207 neutral_delta_ab_values = [] 208 for i, greyscale_cell in enumerate(greyscale_cells): 209 _, mean_a, mean_b = get_lab_mean_values(greyscale_cell) 210 neutral_delta_ab = np.sqrt(mean_a**2 + mean_b**2) 211 logging.debug( 212 'Reference delta AB value for greyscale cell %d: %.2f', 213 i + 1, 214 neutral_delta_ab, 215 ) 216 neutral_delta_ab_values.append(neutral_delta_ab) 217 return neutral_delta_ab_values 218 219 220def get_delta_ab(color_cells_1, color_cells_2): 221 """Computes the delta ab value between two color cells. 222 223 Args: 224 color_cells_1: first color cells array 225 color_cells_2: second color cells array 226 227 Returns: 228 delta_ab_values: list of delta ab values for each color cell 229 """ 230 delta_ab_values = [] 231 for i, (color_cell_1, color_cell_2) in enumerate( 232 zip(color_cells_1, color_cells_2) 233 ): 234 _, mean_a_1, mean_b_1 = get_lab_mean_values(color_cell_1) 235 _, mean_a_2, mean_b_2 = get_lab_mean_values(color_cell_2) 236 delta_ab = np.sqrt((mean_a_1 - mean_a_2) ** 2 + (mean_b_1 - mean_b_2) ** 2) 237 logging.debug('Delta AB value for color cell %d: %.2f', i + 1, delta_ab) 238 delta_ab_values.append(delta_ab) 239 return delta_ab_values 240 241 242def get_white_balance_variation( 243 default_greyscale_cells, jca_greyscale_cells 244): 245 """Gets the white balance variation between default and jca color cells. 246 247 Args: 248 default_greyscale_cells: list of default greyscale cells 249 jca_greyscale_cells: list of jca greyscale cells 250 251 Returns: 252 mean_delta_ab_diff: mean delta ab diff between default and jca 253 """ 254 default_neutral_delta_ab = np.mean( 255 get_neutral_delta_ab(default_greyscale_cells) 256 ) 257 jca_neutral_delta_ab = np.mean(get_neutral_delta_ab(jca_greyscale_cells)) 258 default_jca_neutral_delta_ab = np.mean( 259 get_delta_ab(default_greyscale_cells, jca_greyscale_cells) 260 ) 261 logging.debug('default_neutral_delta_ab_rounded_values: %.2f', 262 default_neutral_delta_ab) 263 logging.debug('jca_neutral_delta_ab_rounded_values: %.2f', 264 jca_neutral_delta_ab) 265 logging.debug('default_jca_neutral_delta_ab_rounded_values: %.2f', 266 default_jca_neutral_delta_ab) 267 268 # Check that the white balance between default and jca does not exceed the 269 # max absolute error. 270 if (default_neutral_delta_ab > MAX_DELTA_AB_WHITE_BALANCE_ABSOLUTE_ERROR) or ( 271 jca_neutral_delta_ab > MAX_DELTA_AB_WHITE_BALANCE_ABSOLUTE_ERROR 272 ): 273 e_msg = ( 274 f'White balance of default and jca images exceeds the threshold.' 275 f'Actual default value: {default_neutral_delta_ab:.2f},' 276 f'Actual jca value: {jca_neutral_delta_ab:.2f}, ' 277 f'Expected maximum: {MAX_DELTA_AB_WHITE_BALANCE_ABSOLUTE_ERROR:.1f}' 278 ) 279 logging.debug(e_msg) 280 # Check that the white balance between default and jca does not exceed the 281 # max relative error. 282 if (default_jca_neutral_delta_ab > MAX_DELTA_AB_WHITE_BALANCE_RELATIVE_ERROR): 283 e_msg = ( 284 f'White balance between default and jca for greyscale cells exceeds the' 285 f' threshold. Actual default: {default_jca_neutral_delta_ab:.2f}, ' 286 f'Expected: {MAX_DELTA_AB_WHITE_BALANCE_RELATIVE_ERROR:.1f}' 287 ) 288 logging.debug(e_msg) 289 return default_jca_neutral_delta_ab 290 291 292def do_white_balance_check(default_patch_list, jca_patch_list): 293 """Computes white balance diff between default and jca images. 294 295 Args: 296 default_patch_list: default camera dynamic range patch cells 297 jca_patch_list: jca camera dynamic range patch cells 298 299 Returns: 300 mean_neutral_delta_ab: mean neutral delta ab between default and jca 301 rounded to 2 places 302 """ 303 default_a_values = [] 304 default_b_values = [] 305 default_middle_tone_patch_list = default_patch_list[ 306 _DYNAMIC_PATCH_MID_TONE_START_IDX:_DYNAMIC_PATCH_MID_TONE_END_IDX 307 ] 308 for patch in default_middle_tone_patch_list: 309 _, mean_a, mean_b = get_lab_mean_values(patch) 310 default_a_values.append(mean_a) 311 default_b_values.append(mean_b) 312 jca_a_values = [] 313 jca_b_values = [] 314 jca_middle_tone_patch_list = jca_patch_list[ 315 _DYNAMIC_PATCH_MID_TONE_START_IDX:_DYNAMIC_PATCH_MID_TONE_END_IDX 316 ] 317 for patch in jca_middle_tone_patch_list: 318 _, mean_a, mean_b = get_lab_mean_values(patch) 319 jca_a_values.append(mean_a) 320 jca_b_values.append(mean_b) 321 322 default_rounded_a_values = [round(float(x), 2) 323 for x in default_a_values] 324 default_rounded_b_values = [round(float(x), 2) 325 for x in default_b_values] 326 jca_rounded_a_values = [round(float(x), 2) 327 for x in jca_a_values] 328 jca_rounded_b_values = [round(float(x), 2) 329 for x in jca_b_values] 330 logging.debug('default_rounded_a_values: %s', default_rounded_a_values) 331 logging.debug('default_rounded_b_values: %s', default_rounded_b_values) 332 logging.debug('jca_rounded_a_values: %s', jca_rounded_a_values) 333 logging.debug('jca_rounded_b_values: %s', jca_rounded_b_values) 334 335 mean_neutral_delta_ab = get_white_balance_variation( 336 default_middle_tone_patch_list, 337 jca_middle_tone_patch_list, 338 ) 339 logging.debug( 340 'White balance difference between default and jca: %.2f', 341 mean_neutral_delta_ab, 342 ) 343 return round(float(mean_neutral_delta_ab), 2) 344 345 346def _get_non_transparent_pixels(img): 347 """Returns the non transparent pixels from BGRA image. 348 """ 349 alpha_channel = img[:, :, 3] 350 351 # Find the non-zero (non-transparent) pixels 352 y_indices, x_indices = np.where(alpha_channel != 0) 353 354 # Get the bounding box of the non-transparent region 355 min_x = np.min(x_indices) 356 min_y = np.min(y_indices) 357 max_x = np.max(x_indices) 358 max_y = np.max(y_indices) 359 360 # Crop the image to the bounding box 361 non_tranpsarent_patch = img[min_y:max_y + 1, min_x:max_x + 1] 362 return non_tranpsarent_patch 363 364 365def get_fov_in_degrees(img_path, qr_code_img, chart_distance): 366 """Returns fov measurement in degrees. 367 368 Args: 369 img_path: captured img path 370 qr_code_img: Extracted center QR code img 371 chart_distance: distance between phone and chart in cm 372 Returns: 373 fov_degrees: FoV measurement in degrees 374 """ 375 img = cv2.imread(img_path) 376 img_height, _ = img.shape[:2] 377 logging.debug('Height of captured img in pixels: %d', img_height) 378 379 nt_qr_code_img = _get_non_transparent_pixels(qr_code_img) 380 qr_code_height, _ = nt_qr_code_img.shape[:2] 381 logging.debug('Height of QR code in pixels: %d', qr_code_height) 382 383 # Get captured image height in cm 384 height_in_cm = (img_height / qr_code_height) * CENTER_QR_CODE_CM 385 logging.debug('Height of captured img in cm: %d', height_in_cm) 386 angle_radians = 2 * math.atan(height_in_cm / (2 * chart_distance)) 387 fov_degrees = math.degrees(angle_radians) 388 return fov_degrees 389