1# Copyright 2013 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"""Image processing utility functions."""
15
16
17import copy
18import io
19import logging
20import math
21import matplotlib
22from matplotlib import pylab
23import matplotlib.pyplot
24import os
25import sys
26
27import capture_request_utils
28import colour
29import error_util
30import noise_model_constants
31import numpy
32from PIL import Image
33from PIL import ImageCms
34
35
36_CMAP_BLUE = ('black', 'blue', 'lightblue')
37_CMAP_GREEN = ('black', 'green', 'lightgreen')
38_CMAP_RED = ('black', 'red', 'lightcoral')
39_CMAP_SIZE = 6  # 6 inches
40_NUM_RAW_CHANNELS = 4  # R, Gr, Gb, B
41
42LENS_SHADING_MAP_ON = 1
43
44# The matrix is from JFIF spec
45DEFAULT_YUV_TO_RGB_CCM = numpy.matrix([[1.000, 0.000, 1.402],
46                                       [1.000, -0.344, -0.714],
47                                       [1.000, 1.772, 0.000]])
48
49DEFAULT_YUV_OFFSETS = numpy.array([0, 128, 128])
50MAX_LUT_SIZE = 65536
51DEFAULT_GAMMA_LUT = numpy.array([
52    math.floor((MAX_LUT_SIZE-1) * math.pow(i/(MAX_LUT_SIZE-1), 1/2.2) + 0.5)
53    for i in range(MAX_LUT_SIZE)])
54NUM_TRIES = 2
55NUM_FRAMES = 4
56RGB2GRAY_WEIGHTS = (0.299, 0.587, 0.114)
57TEST_IMG_DIR = os.path.join(os.environ['CAMERA_ITS_TOP'], 'test_images')
58
59# Expected adapted primaries in ICC profile per color space
60EXPECTED_RX_P3 = 0.682
61EXPECTED_RY_P3 = 0.319
62EXPECTED_GX_P3 = 0.285
63EXPECTED_GY_P3 = 0.675
64EXPECTED_BX_P3 = 0.156
65EXPECTED_BY_P3 = 0.066
66
67EXPECTED_RX_SRGB = 0.648
68EXPECTED_RY_SRGB = 0.331
69EXPECTED_GX_SRGB = 0.321
70EXPECTED_GY_SRGB = 0.598
71EXPECTED_BX_SRGB = 0.156
72EXPECTED_BY_SRGB = 0.066
73
74# Chosen empirically - tolerance for the point in triangle test for colorspace
75# chromaticities
76COLORSPACE_TRIANGLE_AREA_TOL = 0.00028
77
78
79def plot_lsc_maps(lsc_maps, plot_name, test_name_with_log_path):
80  """Plot the lens shading correction maps.
81
82  Args:
83    lsc_maps: 4D np array; r, gr, gb, b lens shading correction maps.
84    plot_name: str; identifier for maps ('full_scale' or 'metadata').
85    test_name_with_log_path: str; test name with log_path location.
86
87  Returns:
88    None, but generates and saves plots.
89  """
90  aspect_ratio = lsc_maps[:, :, 0].shape[1] / lsc_maps[:, :, 0].shape[0]
91  plot_w = 1 + aspect_ratio * _CMAP_SIZE  # add 1 for heatmap legend
92  matplotlib.pyplot.figure(plot_name, figsize=(plot_w, _CMAP_SIZE))
93  pylab.suptitle(plot_name)
94
95  pylab.subplot(2, 2, 1)  # 2x2 top left
96  pylab.title('R')
97  cmap = matplotlib.colors.LinearSegmentedColormap.from_list('', _CMAP_RED)
98  matplotlib.pyplot.pcolormesh(lsc_maps[:, :, 0], cmap=cmap)
99  matplotlib.pyplot.colorbar()
100
101  pylab.subplot(2, 2, 2)  # 2x2 top right
102  pylab.title('Gr')
103  cmap = matplotlib.colors.LinearSegmentedColormap.from_list('', _CMAP_GREEN)
104  matplotlib.pyplot.pcolormesh(lsc_maps[:, :, 1], cmap=cmap)
105  matplotlib.pyplot.colorbar()
106
107  pylab.subplot(2, 2, 3)  # 2x2 bottom left
108  pylab.title('Gb')
109  cmap = matplotlib.colors.LinearSegmentedColormap.from_list('', _CMAP_GREEN)
110  matplotlib.pyplot.pcolormesh(lsc_maps[:, :, 2], cmap=cmap)
111  matplotlib.pyplot.colorbar()
112
113  pylab.subplot(2, 2, 4)  # 2x2 bottom right
114  pylab.title('B')
115  cmap = matplotlib.colors.LinearSegmentedColormap.from_list('', _CMAP_BLUE)
116  matplotlib.pyplot.pcolormesh(lsc_maps[:, :, 3], cmap=cmap)
117  matplotlib.pyplot.colorbar()
118
119  matplotlib.pyplot.savefig(f'{test_name_with_log_path}_{plot_name}_cmaps.png')
120
121
122def capture_scene_image(cam, props, name_with_log_path):
123  """Take a picture of the scene on test FAIL."""
124  req = capture_request_utils.auto_capture_request()
125  img = convert_capture_to_rgb_image(
126      cam.do_capture(req, cam.CAP_YUV), props=props)
127  write_image(img, f'{name_with_log_path}_scene.jpg', True)
128
129
130def convert_image_to_uint8(image):
131  image *= 255
132  return image.astype(numpy.uint8)
133
134
135def assert_props_is_not_none(props):
136  if not props:
137    raise AssertionError('props is None')
138
139
140def assert_capture_width_and_height(cap, width, height):
141  if cap['width'] != width or cap['height'] != height:
142    raise AssertionError(
143        'Unexpected capture WxH size, expected [{}x{}], actual [{}x{}]'.format(
144            width, height, cap['width'], cap['height']
145        )
146    )
147
148
149def apply_lens_shading_map(color_plane, black_level, white_level, lsc_map):
150  """Apply the lens shading map to the color plane.
151
152  Args:
153    color_plane: 2D np array for color plane with values [0.0, 1.0].
154    black_level: float; black level for the color plane.
155    white_level: int; full scale for the color plane.
156    lsc_map: 2D np array lens shading matching size of color_plane.
157
158  Returns:
159    color_plane with lsc applied.
160  """
161  logging.debug('color plane pre-lsc min, max: %.4f, %.4f',
162                numpy.min(color_plane), numpy.max(color_plane))
163  color_plane = (numpy.multiply((color_plane * white_level - black_level),
164                                lsc_map)
165                 + black_level) / white_level
166  logging.debug('color plane post-lsc min, max: %.4f, %.4f',
167                numpy.min(color_plane), numpy.max(color_plane))
168  return color_plane
169
170
171def populate_lens_shading_map(img_shape, lsc_map):
172  """Helper function to create LSC coeifficients for RAW image.
173
174  Args:
175    img_shape: tuple; RAW image shape.
176    lsc_map: 2D low resolution array with lens shading map values.
177
178  Returns:
179    value for lens shading map at point (x, y) in the image.
180  """
181  img_w, img_h = img_shape[1], img_shape[0]
182  map_w, map_h = lsc_map.shape[1], lsc_map.shape[0]
183
184  x, y = numpy.meshgrid(numpy.arange(img_w), numpy.arange(img_h))
185
186  # (u,v) is lsc map location, values [0, map_w-1], [0, map_h-1]
187  # Vectorized calculations
188  u = x * (map_w - 1) / (img_w - 1)
189  v = y * (map_h - 1) / (img_h - 1)
190  u_min = numpy.floor(u).astype(int)
191  v_min = numpy.floor(v).astype(int)
192  u_frac = u - u_min
193  v_frac = v - v_min
194  u_max = numpy.where(u_frac > 0, u_min + 1, u_min)
195  v_max = numpy.where(v_frac > 0, v_min + 1, v_min)
196
197  # Gather LSC values, handling edge cases (optional)
198  lsc_tl = lsc_map[(v_min, u_min)]
199  lsc_tr = lsc_map[(v_min, u_max)]
200  lsc_bl = lsc_map[(v_max, u_min)]
201  lsc_br = lsc_map[(v_max, u_max)]
202
203  # Bilinear interpolation (vectorized)
204  lsc_t = lsc_tl * (1 - u_frac) + lsc_tr * u_frac
205  lsc_b = lsc_bl * (1 - u_frac) + lsc_br * u_frac
206
207  return lsc_t * (1 - v_frac) + lsc_b * v_frac
208
209
210def unpack_lsc_map_from_metadata(metadata):
211  """Get lens shading correction map from metadata and turn into 3D array.
212
213  Args:
214    metadata: dict; metadata from RAW capture.
215
216  Returns:
217    3D numpy array of lens shading maps.
218  """
219  lsc_metadata = metadata['android.statistics.lensShadingCorrectionMap']
220  lsc_map_w, lsc_map_h = lsc_metadata['width'], lsc_metadata['height']
221  lsc_map = lsc_metadata['map']
222  logging.debug(
223      'lensShadingCorrectionMap (H, W): (%d, %d)', lsc_map_h, lsc_map_w
224  )
225  return numpy.array(lsc_map).reshape(lsc_map_h, lsc_map_w, _NUM_RAW_CHANNELS)
226
227
228def convert_raw_capture_to_rgb_image(cap_raw, props, raw_fmt,
229                                     log_path_with_name):
230  """Convert a RAW captured image object to a RGB image.
231
232  Args:
233    cap_raw: A RAW capture object as returned by its_session_utils.do_capture.
234    props: camera properties object (of static values).
235    raw_fmt: string of type 'raw', 'raw10', 'raw12'.
236    log_path_with_name: string with test name and save location.
237
238  Returns:
239    RGB float-3 image array, with pixel values in [0.0, 1.0].
240  """
241  shading_mode = cap_raw['metadata']['android.shading.mode']
242  lens_shading_map_mode = cap_raw[
243      'metadata'].get('android.statistics.lensShadingMapMode')
244  lens_shading_applied = props['android.sensor.info.lensShadingApplied']
245  control_af_mode = cap_raw['metadata']['android.control.afMode']
246  focus_distance = cap_raw['metadata']['android.lens.focusDistance']
247  logging.debug('%s capture AF mode: %s', raw_fmt, control_af_mode)
248  logging.debug('%s capture focus distance: %s', raw_fmt, focus_distance)
249  logging.debug('%s capture shading mode: %d', raw_fmt, shading_mode)
250  logging.debug('lensShadingMapApplied: %r', lens_shading_applied)
251  logging.debug('lensShadingMapMode: %s', lens_shading_map_mode)
252
253  # Split RAW to RGB conversion in 2 to allow LSC application (if needed).
254  r, gr, gb, b = convert_capture_to_planes(cap_raw, props=props)
255
256  # get from metadata, upsample, and apply
257  if lens_shading_map_mode == LENS_SHADING_MAP_ON:
258    logging.debug('Applying lens shading map')
259    plot_name_stem_with_log_path = f'{log_path_with_name}_{raw_fmt}'
260    black_levels = get_black_levels(props, cap_raw)
261    white_level = int(props['android.sensor.info.whiteLevel'])
262    lsc_maps = unpack_lsc_map_from_metadata(cap_raw['metadata'])
263    plot_lsc_maps(lsc_maps, 'metadata', plot_name_stem_with_log_path)
264    lsc_map_fs_r = populate_lens_shading_map(r.shape, lsc_maps[:, :, 0])
265    lsc_map_fs_gr = populate_lens_shading_map(gr.shape, lsc_maps[:, :, 1])
266    lsc_map_fs_gb = populate_lens_shading_map(gb.shape, lsc_maps[:, :, 2])
267    lsc_map_fs_b = populate_lens_shading_map(b.shape, lsc_maps[:, :, 3])
268    plot_lsc_maps(
269        numpy.dstack((lsc_map_fs_r, lsc_map_fs_gr, lsc_map_fs_gb,
270                      lsc_map_fs_b)),
271        'fullscale', plot_name_stem_with_log_path
272    )
273    r = apply_lens_shading_map(
274        r[:, :, 0], black_levels[0], white_level, lsc_map_fs_r
275    )
276    gr = apply_lens_shading_map(
277        gr[:, :, 0], black_levels[1], white_level, lsc_map_fs_gr
278    )
279    gb = apply_lens_shading_map(
280        gb[:, :, 0], black_levels[2], white_level, lsc_map_fs_gb
281    )
282    b = apply_lens_shading_map(
283        b[:, :, 0], black_levels[3], white_level, lsc_map_fs_b
284    )
285  img = convert_raw_to_rgb_image(r, gr, gb, b, props, cap_raw['metadata'])
286  return img
287
288
289def convert_capture_to_rgb_image(cap,
290                                 props=None,
291                                 apply_ccm_raw_to_rgb=True):
292  """Convert a captured image object to a RGB image.
293
294  Args:
295     cap: A capture object as returned by its_session_utils.do_capture.
296     props: (Optional) camera properties object (of static values);
297            required for processing raw images.
298     apply_ccm_raw_to_rgb: (Optional) boolean to apply color correction matrix.
299
300  Returns:
301        RGB float-3 image array, with pixel values in [0.0, 1.0].
302  """
303  w = cap['width']
304  h = cap['height']
305  if cap['format'] == 'raw10' or cap['format'] == 'raw10QuadBayer':
306    assert_props_is_not_none(props)
307    is_quad_bayer = cap['format'] == 'raw10QuadBayer'
308    cap = unpack_raw10_capture(cap, is_quad_bayer)
309
310  if cap['format'] == 'raw12':
311    assert_props_is_not_none(props)
312    cap = unpack_raw12_capture(cap)
313
314  if cap['format'] == 'yuv':
315    y = cap['data'][0: w * h]
316    u = cap['data'][w * h: w * h * 5//4]
317    v = cap['data'][w * h * 5//4: w * h * 6//4]
318    return convert_yuv420_planar_to_rgb_image(y, u, v, w, h)
319  elif cap['format'] == 'jpeg' or cap['format'] == 'jpeg_r':
320    return decompress_jpeg_to_rgb_image(cap['data'])
321  elif (cap['format'] in ('raw', 'rawQuadBayer') or
322        cap['format'] in noise_model_constants.VALID_RAW_STATS_FORMATS):
323    assert_props_is_not_none(props)
324    r, gr, gb, b = convert_capture_to_planes(cap, props)
325    return convert_raw_to_rgb_image(
326        r, gr, gb, b, props, cap['metadata'], apply_ccm_raw_to_rgb)
327  elif cap['format'] == 'y8':
328    y = cap['data'][0: w * h]
329    return convert_y8_to_rgb_image(y, w, h)
330  else:
331    raise error_util.CameraItsError(f"Invalid format {cap['format']}")
332
333
334def unpack_raw10_capture(cap, is_quad_bayer=False):
335  """Unpack a raw-10 capture to a raw-16 capture.
336
337  Args:
338    cap: A raw-10 capture object.
339    is_quad_bayer: Boolean flag for Bayer or Quad Bayer capture.
340
341  Returns:
342    New capture object with raw-16 data.
343  """
344  # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding
345  # the MSBs of the pixels, and the 5th byte holding 4x2b LSBs.
346  w, h = cap['width'], cap['height']
347  if w % 4 != 0:
348    raise error_util.CameraItsError('Invalid raw-10 buffer width')
349  cap = copy.deepcopy(cap)
350  cap['data'] = unpack_raw10_image(cap['data'].reshape(h, w * 5 // 4))
351  cap['format'] = 'rawQuadBayer' if is_quad_bayer else 'raw'
352  return cap
353
354
355def unpack_raw10_image(img):
356  """Unpack a raw-10 image to a raw-16 image.
357
358  Output image will have the 10 LSBs filled in each 16b word, and the 6 MSBs
359  will be set to zero.
360
361  Args:
362    img: A raw-10 image, as a uint8 numpy array.
363
364  Returns:
365    Image as a uint16 numpy array, with all row padding stripped.
366  """
367  if img.shape[1] % 5 != 0:
368    raise error_util.CameraItsError('Invalid raw-10 buffer width')
369  w = img.shape[1] * 4 // 5
370  h = img.shape[0]
371  # Cut out the 4x8b MSBs and shift to bits [9:2] in 16b words.
372  msbs = numpy.delete(img, numpy.s_[4::5], 1)
373  msbs = msbs.astype(numpy.uint16)
374  msbs = numpy.left_shift(msbs, 2)
375  msbs = msbs.reshape(h, w)
376  # Cut out the 4x2b LSBs and put each in bits [1:0] of their own 8b words.
377  lsbs = img[::, 4::5].reshape(h, w // 4)
378  lsbs = numpy.right_shift(
379      numpy.packbits(numpy.unpackbits(lsbs).reshape((h, w // 4, 4, 2)), 3), 6)
380  # Pair the LSB bits group to 0th pixel instead of 3rd pixel
381  lsbs = lsbs.reshape(h, w // 4, 4)[:, :, ::-1]
382  lsbs = lsbs.reshape(h, w)
383  # Fuse the MSBs and LSBs back together
384  img16 = numpy.bitwise_or(msbs, lsbs).reshape(h, w)
385  return img16
386
387
388def unpack_raw12_capture(cap):
389  """Unpack a raw-12 capture to a raw-16 capture.
390
391  Args:
392    cap: A raw-12 capture object.
393
394  Returns:
395     New capture object with raw-16 data.
396  """
397  # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding
398  # the MSBs of the pixels, and the 5th byte holding 4x2b LSBs.
399  w, h = cap['width'], cap['height']
400  if w % 2 != 0:
401    raise error_util.CameraItsError('Invalid raw-12 buffer width')
402  cap = copy.deepcopy(cap)
403  cap['data'] = unpack_raw12_image(cap['data'].reshape(h, w * 3 // 2))
404  cap['format'] = 'raw'
405  return cap
406
407
408def unpack_raw12_image(img):
409  """Unpack a raw-12 image to a raw-16 image.
410
411  Output image will have the 12 LSBs filled in each 16b word, and the 4 MSBs
412  will be set to zero.
413
414  Args:
415   img: A raw-12 image, as a uint8 numpy array.
416
417  Returns:
418    Image as a uint16 numpy array, with all row padding stripped.
419  """
420  if img.shape[1] % 3 != 0:
421    raise error_util.CameraItsError('Invalid raw-12 buffer width')
422  w = img.shape[1] * 2 // 3
423  h = img.shape[0]
424  # Cut out the 2x8b MSBs and shift to bits [11:4] in 16b words.
425  msbs = numpy.delete(img, numpy.s_[2::3], 1)
426  msbs = msbs.astype(numpy.uint16)
427  msbs = numpy.left_shift(msbs, 4)
428  msbs = msbs.reshape(h, w)
429  # Cut out the 2x4b LSBs and put each in bits [3:0] of their own 8b words.
430  lsbs = img[::, 2::3].reshape(h, w // 2)
431  lsbs = numpy.right_shift(
432      numpy.packbits(numpy.unpackbits(lsbs).reshape((h, w // 2, 2, 4)), 3), 4)
433  # Pair the LSB bits group to pixel 0 instead of pixel 1
434  lsbs = lsbs.reshape(h, w // 2, 2)[:, :, ::-1]
435  lsbs = lsbs.reshape(h, w)
436  # Fuse the MSBs and LSBs back together
437  img16 = numpy.bitwise_or(msbs, lsbs).reshape(h, w)
438  return img16
439
440
441def convert_yuv420_planar_to_rgb_image(y_plane, u_plane, v_plane,
442                                       w, h,
443                                       ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
444                                       yuv_off=DEFAULT_YUV_OFFSETS):
445  """Convert a YUV420 8-bit planar image to an RGB image.
446
447  Args:
448    y_plane: The packed 8-bit Y plane.
449    u_plane: The packed 8-bit U plane.
450    v_plane: The packed 8-bit V plane.
451    w: The width of the image.
452    h: The height of the image.
453    ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
454    yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
455
456  Returns:
457    RGB float-3 image array, with pixel values in [0.0, 1.0].
458  """
459  y = numpy.subtract(y_plane, yuv_off[0])
460  u = numpy.subtract(u_plane, yuv_off[1]).view(numpy.int8)
461  v = numpy.subtract(v_plane, yuv_off[2]).view(numpy.int8)
462  u = u.reshape(h // 2, w // 2).repeat(2, axis=1).repeat(2, axis=0)
463  v = v.reshape(h // 2, w // 2).repeat(2, axis=1).repeat(2, axis=0)
464  yuv = numpy.dstack([y, u.reshape(w * h), v.reshape(w * h)])
465  flt = numpy.empty([h, w, 3], dtype=numpy.float32)
466  flt.reshape(w * h * 3)[:] = yuv.reshape(h * w * 3)[:]
467  flt = numpy.dot(flt.reshape(w * h, 3), ccm_yuv_to_rgb.T).clip(0, 255)
468  rgb = numpy.empty([h, w, 3], dtype=numpy.uint8)
469  rgb.reshape(w * h * 3)[:] = flt.reshape(w * h * 3)[:]
470  return rgb.astype(numpy.float32) / 255.0
471
472
473def decompress_jpeg_to_rgb_image(jpeg_buffer):
474  """Decompress a JPEG-compressed image, returning as an RGB image.
475
476  Args:
477    jpeg_buffer: The JPEG stream.
478
479  Returns:
480     A numpy array for the RGB image, with pixels in [0,1].
481  """
482  img = Image.open(io.BytesIO(jpeg_buffer))
483  w = img.size[0]
484  h = img.size[1]
485  return numpy.array(img).reshape((h, w, 3)) / 255.0
486
487
488def decompress_jpeg_to_yuv_image(jpeg_buffer):
489  """Decompress a JPEG-compressed image, returning as a YUV image.
490
491  Args:
492    jpeg_buffer: The JPEG stream.
493
494  Returns:
495     A numpy array for the YUV image, with pixels in [0,1].
496  """
497  img = Image.open(io.BytesIO(jpeg_buffer))
498  img = img.convert('YCbCr')
499  w = img.size[0]
500  h = img.size[1]
501  return numpy.array(img).reshape((h, w, 3)) / 255.0
502
503
504def extract_luma_from_patch(cap, patch_x, patch_y, patch_w, patch_h):
505  """Extract luma from capture."""
506  y, _, _ = convert_capture_to_planes(cap)
507  patch = get_image_patch(y, patch_x, patch_y, patch_w, patch_h)
508  luma = compute_image_means(patch)[0]
509  return luma
510
511
512def convert_image_to_numpy_array(image_path):
513  """Converts image at image_path to numpy array and returns the array.
514
515  Args:
516    image_path: file path
517
518  Returns:
519    numpy array
520  """
521  if not os.path.exists(image_path):
522    raise AssertionError(f'{image_path} does not exist.')
523  image = Image.open(image_path)
524  return numpy.array(image)
525
526
527def _convert_quad_bayer_img_to_bayer_channels(quad_bayer_img, props=None):
528  """Convert a quad Bayer image to the Bayer image channels.
529
530  Args:
531      quad_bayer_img: The quad Bayer image.
532      props: The camera properties.
533
534  Returns:
535      A list of reordered standard Bayer channels of the Bayer image.
536  """
537  height, width, num_channels = quad_bayer_img.shape
538
539  if num_channels != noise_model_constants.NUM_QUAD_BAYER_CHANNELS:
540    raise AssertionError(
541        'The number of channels in the quad Bayer image must be '
542        f'{noise_model_constants.NUM_QUAD_BAYER_CHANNELS}.'
543    )
544  quad_bayer_cfa_order = get_canonical_cfa_order(props, is_quad_bayer=True)
545
546  # Bayer channels are in the order of R, Gr, Gb and B.
547  bayer_channels = []
548  for ch in range(4):
549    channel_img = numpy.zeros(shape=(height, width), dtype='<f')
550    # Average every four quad Bayer channels into a standard Bayer channel.
551    for i in quad_bayer_cfa_order[4 * ch: 4 * (ch + 1)]:
552      channel_img[:, :] += quad_bayer_img[:, :, i]
553    bayer_channels.append(channel_img / 4)
554  return bayer_channels
555
556
557def subsample(image, num_channels=4):
558  """Subsamples the image to separate its color channels.
559
560  Args:
561    image:        2-D numpy array of raw image.
562    num_channels: The number of channels in the image.
563
564  Returns:
565    3-D numpy image with each channel separated.
566  """
567  if num_channels not in noise_model_constants.VALID_NUM_CHANNELS:
568    raise error_util.CameraItsError(
569        f'Invalid number of channels {num_channels}, which should be in '
570        f'{noise_model_constants.VALID_NUM_CHANNELS}.'
571    )
572
573  size_h, size_v = image.shape[1], image.shape[0]
574
575  # Subsample step size, which is the horizontal or vertical pixel interval
576  # between two adjacent pixels of the same channel.
577  stride = int(numpy.sqrt(num_channels))
578  subsample_img = lambda img, i, h, v, s: img[i // s: v: s, i % s: h: s]
579  channel_img = numpy.empty((
580      image.shape[0] // stride,
581      image.shape[1] // stride,
582      num_channels,
583  ))
584
585  for i in range(num_channels):
586    sub_img = subsample_img(image, i, size_h, size_v, stride)
587    channel_img[:, :, i] = sub_img
588
589  return channel_img
590
591
592def convert_capture_to_planes(cap, props=None):
593  """Convert a captured image object to separate image planes.
594
595  Decompose an image into multiple images, corresponding to different planes.
596
597  For YUV420 captures ("yuv"):
598        Returns Y,U,V planes, where the Y plane is full-res and the U,V planes
599        are each 1/2 x 1/2 of the full res.
600
601    For standard Bayer or quad Bayer captures ("raw", "raw10", "raw12",
602    "rawQuadBayer", "rawStats", "rawQuadBayerStats", "raw10QuadBayer",
603    "raw10Stats", "raw10QuadBayerStats"):
604        Returns planes in the order R, Gr, Gb, B, regardless of the Bayer
605        pattern layout.
606        For full-res raw images ("raw", "rawQuadBayer", "raw10",
607        "raw10QuadBayer", "raw12"), each plane is 1/2 x 1/2 of the full res.
608        For standard Bayer stats images, the mean image is returned.
609        For quad Bayer stats images, the average mean image is returned.
610
611    For JPEG captures ("jpeg"):
612        Returns R,G,B full-res planes.
613
614  Args:
615    cap: A capture object as returned by its_session_utils.do_capture.
616    props: (Optional) camera properties object (of static values);
617            required for processing raw images.
618
619  Returns:
620    A tuple of float numpy arrays (one per plane), consisting of pixel values
621    in the range [0.0, 1.0].
622  """
623  w = cap['width']
624  h = cap['height']
625  if cap['format'] in ('raw10', 'raw10QuadBayer'):
626    assert_props_is_not_none(props)
627    is_quad_bayer = cap['format'] == 'raw10QuadBayer'
628    cap = unpack_raw10_capture(cap, is_quad_bayer)
629
630  if cap['format'] == 'raw12':
631    assert_props_is_not_none(props)
632    cap = unpack_raw12_capture(cap)
633  if cap['format'] == 'yuv':
634    y = cap['data'][0:w * h]
635    u = cap['data'][w * h:w * h * 5 // 4]
636    v = cap['data'][w * h * 5 // 4:w * h * 6 // 4]
637    return ((y.astype(numpy.float32) / 255.0).reshape(h, w, 1),
638            (u.astype(numpy.float32) / 255.0).reshape(h // 2, w // 2, 1),
639            (v.astype(numpy.float32) / 255.0).reshape(h // 2, w // 2, 1))
640  elif cap['format'] == 'jpeg':
641    rgb = decompress_jpeg_to_rgb_image(cap['data']).reshape(w * h * 3)
642    return (rgb[::3].reshape(h, w, 1), rgb[1::3].reshape(h, w, 1),
643            rgb[2::3].reshape(h, w, 1))
644  elif cap['format'] in ('raw', 'rawQuadBayer'):
645    assert_props_is_not_none(props)
646    is_quad_bayer = 'QuadBayer' in cap['format']
647    white_level = get_white_level(props, cap['metadata'])
648    img = numpy.ndarray(
649        shape=(h * w,), dtype='<u2', buffer=cap['data'][0:w * h * 2])
650    img = img.astype(numpy.float32).reshape(h, w) / white_level
651    if is_quad_bayer:
652      pixel_array_size = props.get(
653          'android.sensor.info.pixelArraySizeMaximumResolution'
654      )
655      active_array_size = props.get(
656          'android.sensor.info.preCorrectionActiveArraySizeMaximumResolution'
657      )
658    else:
659      pixel_array_size = props.get('android.sensor.info.pixelArraySize')
660      active_array_size = props.get(
661          'android.sensor.info.preCorrectionActiveArraySize'
662      )
663    # Crop the raw image to the active array region.
664    if pixel_array_size and active_array_size:
665      # Note that the Rect class is defined such that the left,top values
666      # are "inside" while the right,bottom values are "outside"; that is,
667      # it's inclusive of the top,left sides only. So, the width is
668      # computed as right-left, rather than right-left+1, etc.
669      wfull = pixel_array_size['width']
670      hfull = pixel_array_size['height']
671      xcrop = active_array_size['left']
672      ycrop = active_array_size['top']
673      wcrop = active_array_size['right'] - xcrop
674      hcrop = active_array_size['bottom'] - ycrop
675      if not wfull >= wcrop >= 0:
676        raise AssertionError(f'wcrop: {wcrop} not in wfull: {wfull}')
677      if not hfull >= hcrop >= 0:
678        raise AssertionError(f'hcrop: {hcrop} not in hfull: {hfull}')
679      if not wfull - wcrop >= xcrop >= 0:
680        raise AssertionError(f'xcrop: {xcrop} not in wfull-crop: {wfull-wcrop}')
681      if not hfull - hcrop >= ycrop >= 0:
682        raise AssertionError(f'ycrop: {ycrop} not in hfull-crop: {hfull-hcrop}')
683      if w == wfull and h == hfull:
684        # Crop needed; extract the center region.
685        img = img[ycrop:ycrop + hcrop, xcrop:xcrop + wcrop]
686        w = wcrop
687        h = hcrop
688      elif w == wcrop and h == hcrop:
689        logging.debug('Image is already cropped. No cropping needed.')
690      else:
691        raise error_util.CameraItsError('Invalid image size metadata')
692
693    idxs = get_canonical_cfa_order(props, is_quad_bayer)
694    if is_quad_bayer:
695      # Subsample image array based on the color map.
696      quad_bayer_img = subsample(
697          img, noise_model_constants.NUM_QUAD_BAYER_CHANNELS
698      )
699      bayer_channels = _convert_quad_bayer_img_to_bayer_channels(
700          quad_bayer_img, props
701      )
702      return bayer_channels
703    else:
704      # Separate the image planes.
705      imgs = [
706          img[::2].reshape(w * h // 2)[::2].reshape(h // 2, w // 2, 1),
707          img[::2].reshape(w * h // 2)[1::2].reshape(h // 2, w // 2, 1),
708          img[1::2].reshape(w * h // 2)[::2].reshape(h // 2, w // 2, 1),
709          img[1::2].reshape(w * h // 2)[1::2].reshape(h // 2, w // 2, 1),
710      ]
711      return [imgs[i] for i in idxs]
712  elif cap['format'] in (
713      'rawStats',
714      'raw10Stats',
715      'rawQuadBayerStats',
716      'raw10QuadBayerStats',
717  ):
718    assert_props_is_not_none(props)
719    is_quad_bayer = 'QuadBayer' in cap['format']
720    white_level = get_white_level(props, cap['metadata'])
721    if is_quad_bayer:
722      num_channels = noise_model_constants.NUM_QUAD_BAYER_CHANNELS
723    else:
724      num_channels = noise_model_constants.NUM_BAYER_CHANNELS
725    mean_image, _ = unpack_rawstats_capture(cap, num_channels)
726    if is_quad_bayer:
727      bayer_channels = _convert_quad_bayer_img_to_bayer_channels(
728          mean_image, props
729      )
730      bayer_channels = [
731          bayer_channels[i] / white_level for i in range(len(bayer_channels))
732      ]
733      return bayer_channels
734    else:
735      # Standard Bayer canonical color channel indices.
736      idxs = get_canonical_cfa_order(props, is_quad_bayer=False)
737      # Normalizes the range to [0, 1] without subtracting the black level.
738      return [mean_image[:, :, i] / white_level for i in idxs]
739  else:
740    raise error_util.CameraItsError(f"Invalid format {cap['format']}")
741
742
743def downscale_image(img, f):
744  """Shrink an image by a given integer factor.
745
746  This function computes output pixel values by averaging over rectangular
747  regions of the input image; it doesn't skip or sample pixels, and all input
748  image pixels are evenly weighted.
749
750  If the downscaling factor doesn't cleanly divide the width and/or height,
751  then the remaining pixels on the right or bottom edge are discarded prior
752  to the downscaling.
753
754  Args:
755    img: The input image as an ndarray.
756    f: The downscaling factor, which should be an integer.
757
758  Returns:
759    The new (downscaled) image, as an ndarray.
760  """
761  h, w, chans = img.shape
762  f = int(f)
763  assert f >= 1
764  h = (h//f)*f
765  w = (w//f)*f
766  img = img[0:h:, 0:w:, ::]
767  chs = []
768  for i in range(chans):
769    ch = img.reshape(h*w*chans)[i::chans].reshape(h, w)
770    ch = ch.reshape(h, w//f, f).mean(2).reshape(h, w//f)
771    ch = ch.T.reshape(w//f, h//f, f).mean(2).T.reshape(h//f, w//f)
772    chs.append(ch.reshape(h*w//(f*f)))
773  img = numpy.vstack(chs).T.reshape(h//f, w//f, chans)
774  return img
775
776
777def convert_raw_to_rgb_image(r_plane, gr_plane, gb_plane, b_plane, props,
778                             cap_res, apply_ccm_raw_to_rgb=True):
779  """Convert a Bayer raw-16 image to an RGB image.
780
781  Includes some extremely rudimentary demosaicking and color processing
782  operations; the output of this function shouldn't be used for any image
783  quality analysis.
784
785  Args:
786   r_plane:
787   gr_plane:
788   gb_plane:
789   b_plane: Numpy arrays for each color plane
790            in the Bayer image, with pixels in the [0.0, 1.0] range.
791   props: Camera properties object.
792   cap_res: Capture result (metadata) object.
793   apply_ccm_raw_to_rgb: (Optional) boolean to apply color correction matrix.
794
795  Returns:
796   RGB float-3 image array, with pixel values in [0.0, 1.0]
797  """
798  # Values required for the RAW to RGB conversion.
799  assert_props_is_not_none(props)
800  white_level = get_white_level(props, cap_res)
801  gains = cap_res['android.colorCorrection.gains']
802  ccm = cap_res['android.colorCorrection.transform']
803
804  # Reorder black levels and gains to R,Gr,Gb,B, to match the order
805  # of the planes.
806  black_levels = get_black_levels(props, cap_res, is_quad_bayer=False)
807  logging.debug('dynamic black levels: %s', black_levels)
808  gains = get_gains_in_canonical_order(props, gains)
809
810  # Convert CCM from rational to float, as numpy arrays.
811  ccm = numpy.array(capture_request_utils.rational_to_float(ccm)).reshape(3, 3)
812
813  # Need to scale the image back to the full [0,1] range after subtracting
814  # the black level from each pixel.
815  scale = white_level / (white_level - max(black_levels))
816
817  # Three-channel black levels, normalized to [0,1] by white_level.
818  black_levels = numpy.array(
819      [b / white_level for b in [black_levels[i] for i in [0, 1, 3]]])
820
821  # Three-channel gains.
822  gains = numpy.array([gains[i] for i in [0, 1, 3]])
823
824  h, w = r_plane.shape[:2]
825  img = numpy.dstack([r_plane, (gr_plane + gb_plane) / 2.0, b_plane])
826  img = (((img.reshape(h, w, 3) - black_levels) * scale) * gains).clip(0.0, 1.0)
827  if apply_ccm_raw_to_rgb:
828    img = numpy.dot(
829        img.reshape(w * h, 3), ccm.T).reshape((h, w, 3)).clip(0.0, 1.0)
830  return img
831
832
833def convert_y8_to_rgb_image(y_plane, w, h):
834  """Convert a Y 8-bit image to an RGB image.
835
836  Args:
837    y_plane: The packed 8-bit Y plane.
838    w: The width of the image.
839    h: The height of the image.
840
841  Returns:
842    RGB float-3 image array, with pixel values in [0.0, 1.0].
843  """
844  y3 = numpy.dstack([y_plane, y_plane, y_plane])
845  rgb = numpy.empty([h, w, 3], dtype=numpy.uint8)
846  rgb.reshape(w * h * 3)[:] = y3.reshape(w * h * 3)[:]
847  return rgb.astype(numpy.float32) / 255.0
848
849
850def write_rgb_uint8_image(img, file_name):
851  """Save a uint8 numpy array image to a file.
852
853  Supported formats: PNG, JPEG, and others; see PIL docs for more.
854
855  Args:
856   img: numpy image array data.
857   file_name: path of file to save to; the extension specifies the format.
858  """
859  if img.dtype != 'uint8':
860    raise AssertionError(f'Incorrect input type: {img.dtype}! Expected: uint8')
861  else:
862    Image.fromarray(img, 'RGB').save(file_name)
863
864
865def write_image(img, fname, apply_gamma=False, is_yuv=False):
866  """Save a float-3 numpy array image to a file.
867
868  Supported formats: PNG, JPEG, and others; see PIL docs for more.
869
870  Image can be 3-channel, which is interpreted as RGB or YUV, or can be
871  1-channel, which is greyscale.
872
873  Can optionally specify that the image should be gamma-encoded prior to
874  writing it out; this should be done if the image contains linear pixel
875  values, to make the image look "normal".
876
877  Args:
878   img: Numpy image array data.
879   fname: Path of file to save to; the extension specifies the format.
880   apply_gamma: (Optional) apply gamma to the image prior to writing it.
881   is_yuv: Whether the image is in YUV format.
882  """
883  if apply_gamma:
884    img = apply_lut_to_image(img, DEFAULT_GAMMA_LUT)
885  (h, w, chans) = img.shape
886  if chans == 3:
887    if not is_yuv:
888      Image.fromarray((img * 255.0).astype(numpy.uint8), 'RGB').save(fname)
889    else:
890      Image.fromarray((img * 255.0).astype(numpy.uint8), 'YCbCr').save(fname)
891  elif chans == 1:
892    img3 = (img * 255.0).astype(numpy.uint8).repeat(3).reshape(h, w, 3)
893    Image.fromarray(img3, 'RGB').save(fname)
894  else:
895    raise error_util.CameraItsError('Unsupported image type')
896
897
898def read_image(fname):
899  """Read image function to match write_image() above."""
900  return Image.open(fname)
901
902
903def apply_lut_to_image(img, lut):
904  """Applies a LUT to every pixel in a float image array.
905
906  Internally converts to a 16b integer image, since the LUT can work with up
907  to 16b->16b mappings (i.e. values in the range [0,65535]). The lut can also
908  have fewer than 65536 entries, however it must be sized as a power of 2
909  (and for smaller luts, the scale must match the bitdepth).
910
911  For a 16b lut of 65536 entries, the operation performed is:
912
913  lut[r * 65535] / 65535 -> r'
914  lut[g * 65535] / 65535 -> g'
915  lut[b * 65535] / 65535 -> b'
916
917  For a 10b lut of 1024 entries, the operation becomes:
918
919  lut[r * 1023] / 1023 -> r'
920  lut[g * 1023] / 1023 -> g'
921  lut[b * 1023] / 1023 -> b'
922
923  Args:
924    img: Numpy float image array, with pixel values in [0,1].
925    lut: Numpy table encoding a LUT, mapping 16b integer values.
926
927  Returns:
928    Float image array after applying LUT to each pixel.
929  """
930  n = len(lut)
931  if n <= 0 or n > MAX_LUT_SIZE or (n & (n - 1)) != 0:
932    raise error_util.CameraItsError(f'Invalid arg LUT size: {n}')
933  m = float(n - 1)
934  return (lut[(img * m).astype(numpy.uint16)] / m).astype(numpy.float32)
935
936
937def get_gains_in_canonical_order(props, gains):
938  """Reorders the gains tuple to the canonical R,Gr,Gb,B order.
939
940  Args:
941    props: Camera properties object.
942    gains: List of 4 values, in R,G_even,G_odd,B order.
943
944  Returns:
945    List of gains values, in R,Gr,Gb,B order.
946  """
947  cfa_pat = props['android.sensor.info.colorFilterArrangement']
948  if cfa_pat in [0, 1]:
949    # RGGB or GRBG, so G_even is Gr
950    return gains
951  elif cfa_pat in [2, 3]:
952    # GBRG or BGGR, so G_even is Gb
953    return [gains[0], gains[2], gains[1], gains[3]]
954  else:
955    raise error_util.CameraItsError('Not supported')
956
957
958def get_white_level(props, cap_metadata=None):
959  """Gets white level to use for a given capture.
960
961  Uses a dynamic value from the capture result if available, else falls back
962  to the static global value in the camera characteristics.
963
964  Args:
965    props: The camera properties object.
966    cap_metadata: A capture results metadata object.
967
968  Returns:
969    Float white level value.
970  """
971  if (cap_metadata is not None and
972      'android.sensor.dynamicWhiteLevel' in cap_metadata and
973      cap_metadata['android.sensor.dynamicWhiteLevel'] is not None):
974    white_level = cap_metadata['android.sensor.dynamicWhiteLevel']
975    logging.debug('dynamic white level: %.2f', white_level)
976  else:
977    white_level = props['android.sensor.info.whiteLevel']
978    logging.debug('white level: %.2f', white_level)
979  return float(white_level)
980
981
982def get_black_levels(props, cap=None, is_quad_bayer=False):
983  """Gets black levels to use for a given capture.
984
985  Uses a dynamic value from the capture result if available, else falls back
986  to the static global value in the camera characteristics.
987
988  Args:
989    props: The camera properties object.
990    cap: A capture object.
991    is_quad_bayer: Boolean flag for Bayer or Quad Bayer capture.
992
993  Returns:
994    A list of black level values reordered in canonical order.
995  """
996  if (cap is not None and
997      'android.sensor.dynamicBlackLevel' in cap and
998      cap['android.sensor.dynamicBlackLevel'] is not None):
999    black_levels = cap['android.sensor.dynamicBlackLevel']
1000  else:
1001    black_levels = props['android.sensor.blackLevelPattern']
1002
1003  idxs = get_canonical_cfa_order(props, is_quad_bayer)
1004  if is_quad_bayer:
1005    ordered_black_levels = [black_levels[i // 4] for i in idxs]
1006  else:
1007    ordered_black_levels = [black_levels[i] for i in idxs]
1008  return ordered_black_levels
1009
1010
1011def get_canonical_cfa_order(props, is_quad_bayer=False):
1012  """Returns a list of channel indices according to color filter arrangement.
1013
1014  Color filter arrangement index is a integer ranging from 0 to 3, which maps
1015  the color filter arrangement in the following way.
1016    0: R, Gr, Gb, B,
1017    1: Gr, R, B, Gb,
1018    2: Gb, B, R, Gr,
1019    3: B, Gb, Gr, R.
1020
1021  This function return a list of channel indices that can be used to reorder
1022  the stats data as the canonical order:
1023    (1) For standard Bayer: R, Gr, Gb, B.
1024    (2) For quad Bayer: R0, R1, R2, R3,
1025                        Gr0, Gr1, Gr2, Gr3,
1026                        Gb0, Gb1, Gb2, Gb3,
1027                        B0, B1, B2, B3.
1028
1029  Args:
1030    props: Camera properties object.
1031    is_quad_bayer: Boolean flag for Bayer or Quad Bayer capture.
1032
1033  Returns:
1034    A list of channel indices with values ranging from:
1035      (1) [0, 3] for standard Bayer,
1036      (2) [0, 15] for quad Bayer.
1037  """
1038  cfa_pat = props['android.sensor.info.colorFilterArrangement']
1039  if not 0 <= cfa_pat < 4:
1040    raise error_util.CameraItsError('Not supported')
1041
1042  channel_indices = []
1043  if is_quad_bayer:
1044    color_map = noise_model_constants.QUAD_BAYER_COLOR_FILTER_MAP[cfa_pat]
1045    for ch in noise_model_constants.BAYER_COLORS:
1046      channel_indices.extend(color_map[ch])
1047  else:
1048    color_map = noise_model_constants.BAYER_COLOR_FILTER_MAP[cfa_pat]
1049    channel_indices = [
1050        color_map[ch] for ch in noise_model_constants.BAYER_COLORS
1051    ]
1052  return channel_indices
1053
1054
1055def unpack_rawstats_capture(cap, num_channels=4):
1056  """Unpacks a stats image capture to the mean and variance images.
1057
1058  Args:
1059    cap: A capture object as returned by its_session_utils.do_capture.
1060    num_channels: The number of color channels in the stats image capture, which
1061      can be one of noise_model_constants.VALID_NUM_CHANNELS.
1062
1063  Returns:
1064    Tuple (mean_image var_image) of float-4 images, with non-normalized
1065    pixel values computed from the RAW10/RAW16 images on the device
1066  """
1067  if cap['format'] not in noise_model_constants.VALID_RAW_STATS_FORMATS:
1068    raise AssertionError(f"Unsupported stats format: {cap['format']}")
1069
1070  if num_channels not in noise_model_constants.VALID_NUM_CHANNELS:
1071    raise AssertionError(
1072        f'Unsupported number of channels {num_channels}, which should be in'
1073        f' {noise_model_constants.VALID_NUM_CHANNELS}.'
1074    )
1075
1076  w = cap['width']
1077  h = cap['height']
1078  img = numpy.ndarray(
1079      shape=(2 * h * w * num_channels,), dtype='<f', buffer=cap['data']
1080  )
1081  analysis_image = img.reshape((2, h, w, num_channels))
1082  mean_image = analysis_image[0, :, :, :].reshape(h, w, num_channels)
1083  var_image = analysis_image[1, :, :, :].reshape(h, w, num_channels)
1084  return mean_image, var_image
1085
1086
1087def get_image_patch(img, xnorm, ynorm, wnorm, hnorm):
1088  """Get a patch (tile) of an image.
1089
1090  Args:
1091   img: Numpy float image array, with pixel values in [0,1].
1092   xnorm:
1093   ynorm:
1094   wnorm:
1095   hnorm: Normalized (in [0,1]) coords for the tile.
1096
1097  Returns:
1098     Numpy float image array of the patch.
1099  """
1100  hfull = img.shape[0]
1101  wfull = img.shape[1]
1102  xtile = int(math.ceil(xnorm * wfull))
1103  ytile = int(math.ceil(ynorm * hfull))
1104  wtile = int(math.floor(wnorm * wfull))
1105  htile = int(math.floor(hnorm * hfull))
1106  if len(img.shape) == 2:
1107    return img[ytile:ytile + htile, xtile:xtile + wtile].copy()
1108  else:
1109    return img[ytile:ytile + htile, xtile:xtile + wtile, :].copy()
1110
1111
1112def compute_image_means(img):
1113  """Calculate the mean of each color channel in the image.
1114
1115  Args:
1116    img: Numpy float image array, with pixel values in [0,1].
1117
1118  Returns:
1119     A list of mean values, one per color channel in the image.
1120  """
1121  means = []
1122  chans = img.shape[2]
1123  for i in range(chans):
1124    means.append(numpy.mean(img[:, :, i], dtype=numpy.float64))
1125  return means
1126
1127
1128def compute_image_variances(img):
1129  """Calculate the variance of each color channel in the image.
1130
1131  Args:
1132    img: Numpy float image array, with pixel values in [0,1].
1133
1134  Returns:
1135    A list of variance values, one per color channel in the image.
1136  """
1137  variances = []
1138  chans = img.shape[2]
1139  for i in range(chans):
1140    variances.append(numpy.var(img[:, :, i], dtype=numpy.float64))
1141  return variances
1142
1143
1144def compute_image_sharpness(img):
1145  """Calculate the sharpness of input image.
1146
1147  Args:
1148    img: numpy float RGB/luma image array, with pixel values in [0,1].
1149
1150  Returns:
1151    Sharpness estimation value based on the average of gradient magnitude.
1152    Larger value means the image is sharper.
1153  """
1154  chans = img.shape[2]
1155  if chans != 1 and chans != 3:
1156    raise AssertionError(f'Not RGB or MONO image! depth: {chans}')
1157  if chans == 1:
1158    luma = img[:, :, 0]
1159  else:
1160    luma = convert_rgb_to_grayscale(img)
1161  gy, gx = numpy.gradient(luma)
1162  return numpy.average(numpy.sqrt(gy*gy + gx*gx))
1163
1164
1165def compute_image_max_gradients(img):
1166  """Calculate the maximum gradient of each color channel in the image.
1167
1168  Args:
1169    img: Numpy float image array, with pixel values in [0,1].
1170
1171  Returns:
1172    A list of gradient max values, one per color channel in the image.
1173  """
1174  grads = []
1175  chans = img.shape[2]
1176  for i in range(chans):
1177    grads.append(numpy.amax(numpy.gradient(img[:, :, i])))
1178  return grads
1179
1180
1181def compute_image_snrs(img):
1182  """Calculate the SNR (dB) of each color channel in the image.
1183
1184  Args:
1185    img: Numpy float image array, with pixel values in [0,1].
1186
1187  Returns:
1188    A list of SNR values in dB, one per color channel in the image.
1189  """
1190  means = compute_image_means(img)
1191  variances = compute_image_variances(img)
1192  std_devs = [math.sqrt(v) for v in variances]
1193  snrs = [20 * math.log10(m/s) for m, s in zip(means, std_devs)]
1194  return snrs
1195
1196
1197def convert_rgb_to_grayscale(img):
1198  """Convert a 3-D array RGB image to grayscale image.
1199
1200  Args:
1201    img: numpy 3-D array RGB image of type [0.0, 1.0] float or [0, 255] uint8.
1202
1203  Returns:
1204    2-D grayscale image of same type as input.
1205  """
1206  chans = img.shape[2]
1207  if chans != 3:
1208    raise AssertionError(f'Not an RGB image! Depth: {chans}')
1209  img_gray = numpy.dot(img[..., :3], RGB2GRAY_WEIGHTS)
1210  if img.dtype == 'uint8':
1211    return img_gray.round().astype(numpy.uint8)
1212  else:
1213    return img_gray
1214
1215
1216def normalize_img(img):
1217  """Normalize the image values to between 0 and 1.
1218
1219  Args:
1220    img: 2-D numpy array of image values
1221  Returns:
1222    Normalized image
1223  """
1224  return (img - numpy.amin(img))/(numpy.amax(img) - numpy.amin(img))
1225
1226
1227def rotate_img_per_argv(img):
1228  """Rotate an image 180 degrees if "rotate" is in argv.
1229
1230  Args:
1231    img: 2-D numpy array of image values
1232  Returns:
1233    Rotated image
1234  """
1235  img_out = img
1236  if 'rotate180' in sys.argv:
1237    img_out = numpy.fliplr(numpy.flipud(img_out))
1238  return img_out
1239
1240
1241def stationary_lens_cap(cam, req, fmt):
1242  """Take up to NUM_TRYS caps and save the 1st one with lens stationary.
1243
1244  Args:
1245   cam: open device session
1246   req: capture request
1247   fmt: format for capture
1248
1249  Returns:
1250    capture
1251  """
1252  tries = 0
1253  done = False
1254  reqs = [req] * NUM_FRAMES
1255  while not done:
1256    logging.debug('Waiting for lens to move to correct location.')
1257    cap = cam.do_capture(reqs, fmt)
1258    done = (cap[NUM_FRAMES - 1]['metadata']['android.lens.state'] == 0)
1259    logging.debug('status: %s', done)
1260    tries += 1
1261    if tries == NUM_TRIES:
1262      raise error_util.CameraItsError('Cannot settle lens after %d tries!' %
1263                                      tries)
1264  return cap[NUM_FRAMES - 1]
1265
1266
1267def compute_image_rms_difference_1d(rgb_x, rgb_y):
1268  """Calculate the RMS difference between 2 RBG images as 1D arrays.
1269
1270  Args:
1271    rgb_x: image array
1272    rgb_y: image array
1273
1274  Returns:
1275    rms_diff
1276  """
1277  len_rgb_x = len(rgb_x)
1278  len_rgb_y = len(rgb_y)
1279  if len_rgb_y != len_rgb_x:
1280    raise AssertionError('RGB images have different number of planes! '
1281                         f'x: {len_rgb_x}, y: {len_rgb_y}')
1282  return math.sqrt(sum([pow(rgb_x[i] - rgb_y[i], 2.0)
1283                        for i in range(len_rgb_x)]) / len_rgb_x)
1284
1285
1286def compute_image_rms_difference_3d(rgb_x, rgb_y):
1287  """Calculate the RMS difference between 2 RBG images as 3D arrays.
1288
1289  Args:
1290    rgb_x: image array in the form of w * h * channels
1291    rgb_y: image array in the form of w * h * channels
1292
1293  Returns:
1294    rms_diff
1295  """
1296  shape_rgb_x = numpy.shape(rgb_x)
1297  shape_rgb_y = numpy.shape(rgb_y)
1298  if shape_rgb_y != shape_rgb_x:
1299    raise AssertionError('RGB images have different number of planes! '
1300                         f'x: {shape_rgb_x}, y: {shape_rgb_y}')
1301  if len(shape_rgb_x) != 3:
1302    raise AssertionError(f'RGB images dimension {len(shape_rgb_x)} is not 3!')
1303
1304  mean_square_sum = 0.0
1305  for i in range(shape_rgb_x[0]):
1306    for j in range(shape_rgb_x[1]):
1307      for k in range(shape_rgb_x[2]):
1308        mean_square_sum += pow(float(rgb_x[i][j][k]) - float(rgb_y[i][j][k]),
1309                               2.0)
1310  return (math.sqrt(mean_square_sum /
1311                    (shape_rgb_x[0] * shape_rgb_x[1] * shape_rgb_x[2])))
1312
1313
1314def compute_image_sad(img_x, img_y):
1315  """Calculate the sum of absolute differences between 2 images.
1316
1317  Args:
1318    img_x: image array in the form of w * h * channels
1319    img_y: image array in the form of w * h * channels
1320
1321  Returns:
1322    sad
1323  """
1324  img_x = img_x[:, :, 1:].ravel()
1325  img_y = img_y[:, :, 1:].ravel()
1326  return numpy.sum(numpy.abs(numpy.subtract(img_x, img_y, dtype=float)))
1327
1328
1329def get_img(buffer):
1330  """Return a PIL.Image of the capture buffer.
1331
1332  Args:
1333    buffer: data field from the capture result.
1334
1335  Returns:
1336    A PIL.Image
1337  """
1338  return Image.open(io.BytesIO(buffer))
1339
1340
1341def jpeg_has_icc_profile(jpeg_img):
1342  """Checks if a jpeg PIL.Image has an icc profile attached.
1343
1344  Args:
1345    jpeg_img: The PIL.Image.
1346
1347  Returns:
1348    True if an icc profile is present, False otherwise.
1349  """
1350  return jpeg_img.info.get('icc_profile') is not None
1351
1352
1353def get_primary_chromaticity(primary):
1354  """Given an ImageCms primary, returns just the xy chromaticity coordinates.
1355
1356  Args:
1357    primary: The primary from the ImageCms profile.
1358
1359  Returns:
1360    (float, float): The xy chromaticity coordinates of the primary.
1361  """
1362  ((_, _, _), (x, y, _)) = primary
1363  return x, y
1364
1365
1366def is_jpeg_icc_profile_correct(jpeg_img, color_space, icc_profile_path=None):
1367  """Compare a jpeg's icc profile to a color space's expected parameters.
1368
1369  Args:
1370    jpeg_img: The PIL.Image.
1371    color_space: 'DISPLAY_P3' or 'SRGB'
1372    icc_profile_path: Optional path to an icc file to be created with the
1373        raw contents.
1374
1375  Returns:
1376    True if the icc profile matches expectations, False otherwise.
1377  """
1378  icc = jpeg_img.info.get('icc_profile')
1379  f = io.BytesIO(icc)
1380  icc_profile = ImageCms.getOpenProfile(f)
1381
1382  if icc_profile_path is not None:
1383    raw_icc_bytes = f.getvalue()
1384    f = open(icc_profile_path, 'wb')
1385    f.write(raw_icc_bytes)
1386    f.close()
1387
1388  cms_profile = icc_profile.profile
1389  (rx, ry) = get_primary_chromaticity(cms_profile.red_primary)
1390  (gx, gy) = get_primary_chromaticity(cms_profile.green_primary)
1391  (bx, by) = get_primary_chromaticity(cms_profile.blue_primary)
1392
1393  if color_space == 'DISPLAY_P3':
1394    # Expected primaries based on Apple's Display P3 primaries
1395    expected_rx = EXPECTED_RX_P3
1396    expected_ry = EXPECTED_RY_P3
1397    expected_gx = EXPECTED_GX_P3
1398    expected_gy = EXPECTED_GY_P3
1399    expected_bx = EXPECTED_BX_P3
1400    expected_by = EXPECTED_BY_P3
1401  elif color_space == 'SRGB':
1402    # Expected primaries based on Pixel sRGB profile
1403    expected_rx = EXPECTED_RX_SRGB
1404    expected_ry = EXPECTED_RY_SRGB
1405    expected_gx = EXPECTED_GX_SRGB
1406    expected_gy = EXPECTED_GY_SRGB
1407    expected_bx = EXPECTED_BX_SRGB
1408    expected_by = EXPECTED_BY_SRGB
1409  else:
1410    # Unsupported color space for comparison
1411    return False
1412
1413  cmp_values = [
1414      [rx, expected_rx],
1415      [ry, expected_ry],
1416      [gx, expected_gx],
1417      [gy, expected_gy],
1418      [bx, expected_bx],
1419      [by, expected_by]
1420  ]
1421
1422  for (actual, expected) in cmp_values:
1423    if not math.isclose(actual, expected, abs_tol=0.001):
1424      # Values significantly differ
1425      return False
1426
1427  return True
1428
1429
1430def area_of_triangle(x1, y1, x2, y2, x3, y3):
1431  """Calculates the area of a triangle formed by three points.
1432
1433  Args:
1434    x1 (float): The x-coordinate of the first point.
1435    y1 (float): The y-coordinate of the first point.
1436    x2 (float): The x-coordinate of the second point.
1437    y2 (float): The y-coordinate of the second point.
1438    x3 (float): The x-coordinate of the third point.
1439    y3 (float): The y-coordinate of the third point.
1440
1441  Returns:
1442    float: The area of the triangle.
1443  """
1444  area = abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2.0)
1445  return area
1446
1447
1448def point_in_triangle(x1, y1, x2, y2, x3, y3, xp, yp, abs_tol):
1449  """Checks if the point (xp, yp) is inside the triangle.
1450
1451  Args:
1452    x1 (float): The x-coordinate of the first point.
1453    y1 (float): The y-coordinate of the first point.
1454    x2 (float): The x-coordinate of the second point.
1455    y2 (float): The y-coordinate of the second point.
1456    x3 (float): The x-coordinate of the third point.
1457    y3 (float): The y-coordinate of the third point.
1458    xp (float): The x-coordinate of the point to check.
1459    yp (float): The y-coordinate of the point to check.
1460    abs_tol (float): Absolute tolerance amount.
1461
1462  Returns:
1463    bool: True if the point is inside the triangle, False otherwise.
1464  """
1465  a = area_of_triangle(x1, y1, x2, y2, x3, y3)
1466  a1 = area_of_triangle(xp, yp, x2, y2, x3, y3)
1467  a2 = area_of_triangle(x1, y1, xp, yp, x3, y3)
1468  a3 = area_of_triangle(x1, y1, x2, y2, xp, yp)
1469  return math.isclose(a, (a1 + a2 + a3), abs_tol=abs_tol)
1470
1471
1472def distance(p, q):
1473  """Returns the Euclidean distance from point p to point q.
1474
1475  Args:
1476    p: an Iterable of numbers
1477    q: an Iterable of numbers
1478  """
1479  return math.sqrt(sum((px - qx) ** 2.0 for px, qx in zip(p, q)))
1480
1481
1482def p3_img_has_wide_gamut(wide_img):
1483  """Check if a DISPLAY_P3 image contains wide gamut pixels.
1484
1485  Given a DISPLAY_P3 image that should have a wider gamut than SRGB, checks all
1486  pixel values to see if any reside outside the SRGB gamut. This is done by
1487  converting to CIE xy chromaticities using a Bradford chromatic adaptation for
1488  consistency with ICC profiles.
1489
1490  Args:
1491    wide_img: The PIL.Image in the DISPLAY_P3 color space.
1492
1493  Returns:
1494    True if the gamut of wide_img is greater than that of SRGB.
1495    False otherwise.
1496  """
1497  w = wide_img.size[0]
1498  h = wide_img.size[1]
1499  wide_arr = numpy.array(wide_img)
1500
1501  img_arr = colour.RGB_to_XYZ(
1502      wide_arr / 255.0,
1503      colour.models.rgb.datasets.display_p3.RGB_COLOURSPACE_DISPLAY_P3.whitepoint,
1504      colour.models.rgb.datasets.display_p3.RGB_COLOURSPACE_DISPLAY_P3.whitepoint,
1505      colour.models.rgb.datasets.display_p3.RGB_COLOURSPACE_DISPLAY_P3.matrix_RGB_to_XYZ,
1506      'Bradford', lambda x: colour.eotf(x, 'sRGB'))
1507
1508  xy_arr = colour.XYZ_to_xy(img_arr)
1509
1510  srgb_colorspace = colour.models.RGB_COLOURSPACE_sRGB
1511  srgb_primaries = srgb_colorspace.primaries
1512
1513  for y in range(h):
1514    for x in range(w):
1515      # Check if the pixel chromaticity is inside or outside the SRGB gamut.
1516      # This check is not guaranteed not to emit false positives / negatives,
1517      # however the probability of either on an arbitrary DISPLAY_P3 camera
1518      # capture is exceedingly unlikely.
1519      if not point_in_triangle(*srgb_primaries.reshape(6),
1520                               xy_arr[y][x][0], xy_arr[y][x][1],
1521                               COLORSPACE_TRIANGLE_AREA_TOL):
1522        return True
1523
1524  return False
1525