• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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