• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 os
22import sys
23
24import error_util
25import numpy
26from PIL import Image
27from PIL import ImageCms
28
29import capture_request_utils
30
31
32# The matrix is from JFIF spec
33DEFAULT_YUV_TO_RGB_CCM = numpy.matrix([[1.000, 0.000, 1.402],
34                                       [1.000, -0.344, -0.714],
35                                       [1.000, 1.772, 0.000]])
36
37DEFAULT_YUV_OFFSETS = numpy.array([0, 128, 128])
38MAX_LUT_SIZE = 65536
39DEFAULT_GAMMA_LUT = numpy.array([
40    math.floor((MAX_LUT_SIZE-1) * math.pow(i/(MAX_LUT_SIZE-1), 1/2.2) + 0.5)
41    for i in range(MAX_LUT_SIZE)])
42NUM_TRIES = 2
43NUM_FRAMES = 4
44TEST_IMG_DIR = os.path.join(os.environ['CAMERA_ITS_TOP'], 'test_images')
45
46# Expected adapted primaries in ICC profile per color space
47EXPECTED_RX_P3 = 0.682
48EXPECTED_RY_P3 = 0.319
49EXPECTED_GX_P3 = 0.285
50EXPECTED_GY_P3 = 0.675
51EXPECTED_BX_P3 = 0.156
52EXPECTED_BY_P3 = 0.066
53
54EXPECTED_RX_SRGB = 0.648
55EXPECTED_RY_SRGB = 0.331
56EXPECTED_GX_SRGB = 0.321
57EXPECTED_GY_SRGB = 0.598
58EXPECTED_BX_SRGB = 0.156
59EXPECTED_BY_SRGB = 0.066
60
61# Chosen empirically - tolerance for the point in triangle test for colorspace
62# chromaticities
63COLORSPACE_TRIANGLE_AREA_TOL = 0.00028
64
65
66def convert_image_to_uint8(image):
67  image *= 255
68  return image.astype(numpy.uint8)
69
70
71def assert_props_is_not_none(props):
72  if not props:
73    raise AssertionError('props is None')
74
75
76def convert_capture_to_rgb_image(cap,
77                                 props=None,
78                                 apply_ccm_raw_to_rgb=True):
79  """Convert a captured image object to a RGB image.
80
81  Args:
82     cap: A capture object as returned by its_session_utils.do_capture.
83     props: (Optional) camera properties object (of static values);
84            required for processing raw images.
85     apply_ccm_raw_to_rgb: (Optional) boolean to apply color correction matrix.
86
87  Returns:
88        RGB float-3 image array, with pixel values in [0.0, 1.0].
89  """
90  w = cap['width']
91  h = cap['height']
92  if cap['format'] == 'raw10':
93    assert_props_is_not_none(props)
94    cap = unpack_raw10_capture(cap)
95
96  if cap['format'] == 'raw12':
97    assert_props_is_not_none(props)
98    cap = unpack_raw12_capture(cap)
99
100  if cap['format'] == 'yuv':
101    y = cap['data'][0: w * h]
102    u = cap['data'][w * h: w * h * 5//4]
103    v = cap['data'][w * h * 5//4: w * h * 6//4]
104    return convert_yuv420_planar_to_rgb_image(y, u, v, w, h)
105  elif cap['format'] == 'jpeg' or cap['format'] == 'jpeg_r':
106    return decompress_jpeg_to_rgb_image(cap['data'])
107  elif cap['format'] == 'raw' or cap['format'] == 'rawStats':
108    assert_props_is_not_none(props)
109    r, gr, gb, b = convert_capture_to_planes(cap, props)
110    return convert_raw_to_rgb_image(
111        r, gr, gb, b, props, cap['metadata'], apply_ccm_raw_to_rgb)
112  elif cap['format'] == 'y8':
113    y = cap['data'][0: w * h]
114    return convert_y8_to_rgb_image(y, w, h)
115  else:
116    raise error_util.CameraItsError(f"Invalid format {cap['format']}")
117
118
119def unpack_raw10_capture(cap):
120  """Unpack a raw-10 capture to a raw-16 capture.
121
122  Args:
123    cap: A raw-10 capture object.
124
125  Returns:
126    New capture object with raw-16 data.
127  """
128  # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding
129  # the MSBs of the pixels, and the 5th byte holding 4x2b LSBs.
130  w, h = cap['width'], cap['height']
131  if w % 4 != 0:
132    raise error_util.CameraItsError('Invalid raw-10 buffer width')
133  cap = copy.deepcopy(cap)
134  cap['data'] = unpack_raw10_image(cap['data'].reshape(h, w * 5 // 4))
135  cap['format'] = 'raw'
136  return cap
137
138
139def unpack_raw10_image(img):
140  """Unpack a raw-10 image to a raw-16 image.
141
142  Output image will have the 10 LSBs filled in each 16b word, and the 6 MSBs
143  will be set to zero.
144
145  Args:
146    img: A raw-10 image, as a uint8 numpy array.
147
148  Returns:
149    Image as a uint16 numpy array, with all row padding stripped.
150  """
151  if img.shape[1] % 5 != 0:
152    raise error_util.CameraItsError('Invalid raw-10 buffer width')
153  w = img.shape[1] * 4 // 5
154  h = img.shape[0]
155  # Cut out the 4x8b MSBs and shift to bits [9:2] in 16b words.
156  msbs = numpy.delete(img, numpy.s_[4::5], 1)
157  msbs = msbs.astype(numpy.uint16)
158  msbs = numpy.left_shift(msbs, 2)
159  msbs = msbs.reshape(h, w)
160  # Cut out the 4x2b LSBs and put each in bits [1:0] of their own 8b words.
161  lsbs = img[::, 4::5].reshape(h, w // 4)
162  lsbs = numpy.right_shift(
163      numpy.packbits(numpy.unpackbits(lsbs).reshape((h, w // 4, 4, 2)), 3), 6)
164  # Pair the LSB bits group to 0th pixel instead of 3rd pixel
165  lsbs = lsbs.reshape(h, w // 4, 4)[:, :, ::-1]
166  lsbs = lsbs.reshape(h, w)
167  # Fuse the MSBs and LSBs back together
168  img16 = numpy.bitwise_or(msbs, lsbs).reshape(h, w)
169  return img16
170
171
172def unpack_raw12_capture(cap):
173  """Unpack a raw-12 capture to a raw-16 capture.
174
175  Args:
176    cap: A raw-12 capture object.
177
178  Returns:
179     New capture object with raw-16 data.
180  """
181  # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding
182  # the MSBs of the pixels, and the 5th byte holding 4x2b LSBs.
183  w, h = cap['width'], cap['height']
184  if w % 2 != 0:
185    raise error_util.CameraItsError('Invalid raw-12 buffer width')
186  cap = copy.deepcopy(cap)
187  cap['data'] = unpack_raw12_image(cap['data'].reshape(h, w * 3 // 2))
188  cap['format'] = 'raw'
189  return cap
190
191
192def unpack_raw12_image(img):
193  """Unpack a raw-12 image to a raw-16 image.
194
195  Output image will have the 12 LSBs filled in each 16b word, and the 4 MSBs
196  will be set to zero.
197
198  Args:
199   img: A raw-12 image, as a uint8 numpy array.
200
201  Returns:
202    Image as a uint16 numpy array, with all row padding stripped.
203  """
204  if img.shape[1] % 3 != 0:
205    raise error_util.CameraItsError('Invalid raw-12 buffer width')
206  w = img.shape[1] * 2 // 3
207  h = img.shape[0]
208  # Cut out the 2x8b MSBs and shift to bits [11:4] in 16b words.
209  msbs = numpy.delete(img, numpy.s_[2::3], 1)
210  msbs = msbs.astype(numpy.uint16)
211  msbs = numpy.left_shift(msbs, 4)
212  msbs = msbs.reshape(h, w)
213  # Cut out the 2x4b LSBs and put each in bits [3:0] of their own 8b words.
214  lsbs = img[::, 2::3].reshape(h, w // 2)
215  lsbs = numpy.right_shift(
216      numpy.packbits(numpy.unpackbits(lsbs).reshape((h, w // 2, 2, 4)), 3), 4)
217  # Pair the LSB bits group to pixel 0 instead of pixel 1
218  lsbs = lsbs.reshape(h, w // 2, 2)[:, :, ::-1]
219  lsbs = lsbs.reshape(h, w)
220  # Fuse the MSBs and LSBs back together
221  img16 = numpy.bitwise_or(msbs, lsbs).reshape(h, w)
222  return img16
223
224
225def convert_yuv420_planar_to_rgb_image(y_plane, u_plane, v_plane,
226                                       w, h,
227                                       ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
228                                       yuv_off=DEFAULT_YUV_OFFSETS):
229  """Convert a YUV420 8-bit planar image to an RGB image.
230
231  Args:
232    y_plane: The packed 8-bit Y plane.
233    u_plane: The packed 8-bit U plane.
234    v_plane: The packed 8-bit V plane.
235    w: The width of the image.
236    h: The height of the image.
237    ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
238    yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
239
240  Returns:
241    RGB float-3 image array, with pixel values in [0.0, 1.0].
242  """
243  y = numpy.subtract(y_plane, yuv_off[0])
244  u = numpy.subtract(u_plane, yuv_off[1]).view(numpy.int8)
245  v = numpy.subtract(v_plane, yuv_off[2]).view(numpy.int8)
246  u = u.reshape(h // 2, w // 2).repeat(2, axis=1).repeat(2, axis=0)
247  v = v.reshape(h // 2, w // 2).repeat(2, axis=1).repeat(2, axis=0)
248  yuv = numpy.dstack([y, u.reshape(w * h), v.reshape(w * h)])
249  flt = numpy.empty([h, w, 3], dtype=numpy.float32)
250  flt.reshape(w * h * 3)[:] = yuv.reshape(h * w * 3)[:]
251  flt = numpy.dot(flt.reshape(w * h, 3), ccm_yuv_to_rgb.T).clip(0, 255)
252  rgb = numpy.empty([h, w, 3], dtype=numpy.uint8)
253  rgb.reshape(w * h * 3)[:] = flt.reshape(w * h * 3)[:]
254  return rgb.astype(numpy.float32) / 255.0
255
256
257def decompress_jpeg_to_rgb_image(jpeg_buffer):
258  """Decompress a JPEG-compressed image, returning as an RGB image.
259
260  Args:
261    jpeg_buffer: The JPEG stream.
262
263  Returns:
264     A numpy array for the RGB image, with pixels in [0,1].
265  """
266  img = Image.open(io.BytesIO(jpeg_buffer))
267  w = img.size[0]
268  h = img.size[1]
269  return numpy.array(img).reshape((h, w, 3)) / 255.0
270
271
272def decompress_jpeg_to_yuv_image(jpeg_buffer):
273  """Decompress a JPEG-compressed image, returning as a YUV image.
274
275  Args:
276    jpeg_buffer: The JPEG stream.
277
278  Returns:
279     A numpy array for the YUV image, with pixels in [0,1].
280  """
281  img = Image.open(io.BytesIO(jpeg_buffer))
282  img = img.convert('YCbCr')
283  w = img.size[0]
284  h = img.size[1]
285  return numpy.array(img).reshape((h, w, 3)) / 255.0
286
287
288def extract_luma_from_patch(cap, patch_x, patch_y, patch_w, patch_h):
289  """Extract luma from capture."""
290  y, _, _ = convert_capture_to_planes(cap)
291  patch = get_image_patch(y, patch_x, patch_y, patch_w, patch_h)
292  luma = compute_image_means(patch)[0]
293  return luma
294
295
296def convert_image_to_numpy_array(image_path):
297  """Converts image at image_path to numpy array and returns the array.
298
299  Args:
300    image_path: file path
301  Returns:
302    numpy array
303  """
304  if not os.path.exists(image_path):
305    raise AssertionError(f'{image_path} does not exist.')
306  image = Image.open(image_path)
307  return numpy.array(image)
308
309
310def convert_capture_to_planes(cap, props=None):
311  """Convert a captured image object to separate image planes.
312
313  Decompose an image into multiple images, corresponding to different planes.
314
315  For YUV420 captures ("yuv"):
316        Returns Y,U,V planes, where the Y plane is full-res and the U,V planes
317        are each 1/2 x 1/2 of the full res.
318
319    For Bayer captures ("raw", "raw10", "raw12", or "rawStats"):
320        Returns planes in the order R,Gr,Gb,B, regardless of the Bayer pattern
321        layout. For full-res raw images ("raw", "raw10", "raw12"), each plane
322        is 1/2 x 1/2 of the full res. For "rawStats" images, the mean image
323        is returned.
324
325    For JPEG captures ("jpeg"):
326        Returns R,G,B full-res planes.
327
328  Args:
329    cap: A capture object as returned by its_session_utils.do_capture.
330    props: (Optional) camera properties object (of static values);
331            required for processing raw images.
332
333  Returns:
334    A tuple of float numpy arrays (one per plane), consisting of pixel values
335    in the range [0.0, 1.0].
336  """
337  w = cap['width']
338  h = cap['height']
339  if cap['format'] == 'raw10':
340    assert_props_is_not_none(props)
341    cap = unpack_raw10_capture(cap)
342  if cap['format'] == 'raw12':
343    assert_props_is_not_none(props)
344    cap = unpack_raw12_capture(cap)
345  if cap['format'] == 'yuv':
346    y = cap['data'][0:w * h]
347    u = cap['data'][w * h:w * h * 5 // 4]
348    v = cap['data'][w * h * 5 // 4:w * h * 6 // 4]
349    return ((y.astype(numpy.float32) / 255.0).reshape(h, w, 1),
350            (u.astype(numpy.float32) / 255.0).reshape(h // 2, w // 2, 1),
351            (v.astype(numpy.float32) / 255.0).reshape(h // 2, w // 2, 1))
352  elif cap['format'] == 'jpeg':
353    rgb = decompress_jpeg_to_rgb_image(cap['data']).reshape(w * h * 3)
354    return (rgb[::3].reshape(h, w, 1), rgb[1::3].reshape(h, w, 1),
355            rgb[2::3].reshape(h, w, 1))
356  elif cap['format'] == 'raw':
357    assert_props_is_not_none(props)
358    white_level = float(props['android.sensor.info.whiteLevel'])
359    img = numpy.ndarray(
360        shape=(h * w,), dtype='<u2', buffer=cap['data'][0:w * h * 2])
361    img = img.astype(numpy.float32).reshape(h, w) / white_level
362    # Crop the raw image to the active array region.
363    if (props.get('android.sensor.info.preCorrectionActiveArraySize') is
364        not None and
365        props.get('android.sensor.info.pixelArraySize') is not None):
366      # Note that the Rect class is defined such that the left,top values
367      # are "inside" while the right,bottom values are "outside"; that is,
368      # it's inclusive of the top,left sides only. So, the width is
369      # computed as right-left, rather than right-left+1, etc.
370      wfull = props['android.sensor.info.pixelArraySize']['width']
371      hfull = props['android.sensor.info.pixelArraySize']['height']
372      xcrop = props['android.sensor.info.preCorrectionActiveArraySize']['left']
373      ycrop = props['android.sensor.info.preCorrectionActiveArraySize']['top']
374      wcrop = props['android.sensor.info.preCorrectionActiveArraySize'][
375          'right'] - xcrop
376      hcrop = props['android.sensor.info.preCorrectionActiveArraySize'][
377          'bottom'] - ycrop
378      if not wfull >= wcrop >= 0:
379        raise AssertionError(f'wcrop: {wcrop} not in wfull: {wfull}')
380      if not  hfull >= hcrop >= 0:
381        raise AssertionError(f'hcrop: {hcrop} not in hfull: {hfull}')
382      if not wfull - wcrop >= xcrop >= 0:
383        raise AssertionError(f'xcrop: {xcrop} not in wfull-crop: {wfull-wcrop}')
384      if not hfull - hcrop >= ycrop >= 0:
385        raise AssertionError(f'ycrop: {ycrop} not in hfull-crop: {hfull-hcrop}')
386      if w == wfull and h == hfull:
387        # Crop needed; extract the center region.
388        img = img[ycrop:ycrop + hcrop, xcrop:xcrop + wcrop]
389        w = wcrop
390        h = hcrop
391      elif w == wcrop and h == hcrop:
392        logging.debug('Image is already cropped.No cropping needed.')
393        # pylint: disable=pointless-statement
394        None
395      else:
396        raise error_util.CameraItsError('Invalid image size metadata')
397    # Separate the image planes.
398    imgs = [
399        img[::2].reshape(w * h // 2)[::2].reshape(h // 2, w // 2, 1),
400        img[::2].reshape(w * h // 2)[1::2].reshape(h // 2, w // 2, 1),
401        img[1::2].reshape(w * h // 2)[::2].reshape(h // 2, w // 2, 1),
402        img[1::2].reshape(w * h // 2)[1::2].reshape(h // 2, w // 2, 1)
403    ]
404    idxs = get_canonical_cfa_order(props)
405    return [imgs[i] for i in idxs]
406  elif cap['format'] == 'rawStats':
407    assert_props_is_not_none(props)
408    white_level = float(props['android.sensor.info.whiteLevel'])
409    # pylint: disable=unused-variable
410    mean_image, var_image = unpack_rawstats_capture(cap)
411    idxs = get_canonical_cfa_order(props)
412    return [mean_image[:, :, i] / white_level for i in idxs]
413  else:
414    raise error_util.CameraItsError(f"Invalid format {cap['format']}")
415
416
417def downscale_image(img, f):
418  """Shrink an image by a given integer factor.
419
420  This function computes output pixel values by averaging over rectangular
421  regions of the input image; it doesn't skip or sample pixels, and all input
422  image pixels are evenly weighted.
423
424  If the downscaling factor doesn't cleanly divide the width and/or height,
425  then the remaining pixels on the right or bottom edge are discarded prior
426  to the downscaling.
427
428  Args:
429    img: The input image as an ndarray.
430    f: The downscaling factor, which should be an integer.
431
432  Returns:
433    The new (downscaled) image, as an ndarray.
434  """
435  h, w, chans = img.shape
436  f = int(f)
437  assert f >= 1
438  h = (h//f)*f
439  w = (w//f)*f
440  img = img[0:h:, 0:w:, ::]
441  chs = []
442  for i in range(chans):
443    ch = img.reshape(h*w*chans)[i::chans].reshape(h, w)
444    ch = ch.reshape(h, w//f, f).mean(2).reshape(h, w//f)
445    ch = ch.T.reshape(w//f, h//f, f).mean(2).T.reshape(h//f, w//f)
446    chs.append(ch.reshape(h*w//(f*f)))
447  img = numpy.vstack(chs).T.reshape(h//f, w//f, chans)
448  return img
449
450
451def convert_raw_to_rgb_image(r_plane, gr_plane, gb_plane, b_plane, props,
452                             cap_res, apply_ccm_raw_to_rgb=True):
453  """Convert a Bayer raw-16 image to an RGB image.
454
455  Includes some extremely rudimentary demosaicking and color processing
456  operations; the output of this function shouldn't be used for any image
457  quality analysis.
458
459  Args:
460   r_plane:
461   gr_plane:
462   gb_plane:
463   b_plane: Numpy arrays for each color plane
464            in the Bayer image, with pixels in the [0.0, 1.0] range.
465   props: Camera properties object.
466   cap_res: Capture result (metadata) object.
467   apply_ccm_raw_to_rgb: (Optional) boolean to apply color correction matrix.
468
469  Returns:
470   RGB float-3 image array, with pixel values in [0.0, 1.0]
471  """
472    # Values required for the RAW to RGB conversion.
473  assert_props_is_not_none(props)
474  white_level = float(props['android.sensor.info.whiteLevel'])
475  black_levels = props['android.sensor.blackLevelPattern']
476  gains = cap_res['android.colorCorrection.gains']
477  ccm = cap_res['android.colorCorrection.transform']
478
479  # Reorder black levels and gains to R,Gr,Gb,B, to match the order
480  # of the planes.
481  black_levels = [get_black_level(i, props, cap_res) for i in range(4)]
482  gains = get_gains_in_canonical_order(props, gains)
483
484  # Convert CCM from rational to float, as numpy arrays.
485  ccm = numpy.array(capture_request_utils.rational_to_float(ccm)).reshape(3, 3)
486
487  # Need to scale the image back to the full [0,1] range after subtracting
488  # the black level from each pixel.
489  scale = white_level / (white_level - max(black_levels))
490
491  # Three-channel black levels, normalized to [0,1] by white_level.
492  black_levels = numpy.array(
493      [b / white_level for b in [black_levels[i] for i in [0, 1, 3]]])
494
495  # Three-channel gains.
496  gains = numpy.array([gains[i] for i in [0, 1, 3]])
497
498  h, w = r_plane.shape[:2]
499  img = numpy.dstack([r_plane, (gr_plane + gb_plane) / 2.0, b_plane])
500  img = (((img.reshape(h, w, 3) - black_levels) * scale) * gains).clip(0.0, 1.0)
501  if apply_ccm_raw_to_rgb:
502    img = numpy.dot(
503        img.reshape(w * h, 3), ccm.T).reshape((h, w, 3)).clip(0.0, 1.0)
504  return img
505
506
507def convert_y8_to_rgb_image(y_plane, w, h):
508  """Convert a Y 8-bit image to an RGB image.
509
510  Args:
511    y_plane: The packed 8-bit Y plane.
512    w: The width of the image.
513    h: The height of the image.
514
515  Returns:
516    RGB float-3 image array, with pixel values in [0.0, 1.0].
517  """
518  y3 = numpy.dstack([y_plane, y_plane, y_plane])
519  rgb = numpy.empty([h, w, 3], dtype=numpy.uint8)
520  rgb.reshape(w * h * 3)[:] = y3.reshape(w * h * 3)[:]
521  return rgb.astype(numpy.float32) / 255.0
522
523
524def write_image(img, fname, apply_gamma=False, is_yuv=False):
525  """Save a float-3 numpy array image to a file.
526
527  Supported formats: PNG, JPEG, and others; see PIL docs for more.
528
529  Image can be 3-channel, which is interpreted as RGB or YUV, or can be
530  1-channel, which is greyscale.
531
532  Can optionally specify that the image should be gamma-encoded prior to
533  writing it out; this should be done if the image contains linear pixel
534  values, to make the image look "normal".
535
536  Args:
537   img: Numpy image array data.
538   fname: Path of file to save to; the extension specifies the format.
539   apply_gamma: (Optional) apply gamma to the image prior to writing it.
540   is_yuv: Whether the image is in YUV format.
541  """
542  if apply_gamma:
543    img = apply_lut_to_image(img, DEFAULT_GAMMA_LUT)
544  (h, w, chans) = img.shape
545  if chans == 3:
546    if not is_yuv:
547      Image.fromarray((img * 255.0).astype(numpy.uint8), 'RGB').save(fname)
548    else:
549      Image.fromarray((img * 255.0).astype(numpy.uint8), 'YCbCr').save(fname)
550  elif chans == 1:
551    img3 = (img * 255.0).astype(numpy.uint8).repeat(3).reshape(h, w, 3)
552    Image.fromarray(img3, 'RGB').save(fname)
553  else:
554    raise error_util.CameraItsError('Unsupported image type')
555
556
557def read_image(fname):
558  """Read image function to match write_image() above."""
559  return Image.open(fname)
560
561
562def apply_lut_to_image(img, lut):
563  """Applies a LUT to every pixel in a float image array.
564
565  Internally converts to a 16b integer image, since the LUT can work with up
566  to 16b->16b mappings (i.e. values in the range [0,65535]). The lut can also
567  have fewer than 65536 entries, however it must be sized as a power of 2
568  (and for smaller luts, the scale must match the bitdepth).
569
570  For a 16b lut of 65536 entries, the operation performed is:
571
572  lut[r * 65535] / 65535 -> r'
573  lut[g * 65535] / 65535 -> g'
574  lut[b * 65535] / 65535 -> b'
575
576  For a 10b lut of 1024 entries, the operation becomes:
577
578  lut[r * 1023] / 1023 -> r'
579  lut[g * 1023] / 1023 -> g'
580  lut[b * 1023] / 1023 -> b'
581
582  Args:
583    img: Numpy float image array, with pixel values in [0,1].
584    lut: Numpy table encoding a LUT, mapping 16b integer values.
585
586  Returns:
587    Float image array after applying LUT to each pixel.
588  """
589  n = len(lut)
590  if n <= 0 or n > MAX_LUT_SIZE or (n & (n - 1)) != 0:
591    raise error_util.CameraItsError(f'Invalid arg LUT size: {n}')
592  m = float(n - 1)
593  return (lut[(img * m).astype(numpy.uint16)] / m).astype(numpy.float32)
594
595
596def get_gains_in_canonical_order(props, gains):
597  """Reorders the gains tuple to the canonical R,Gr,Gb,B order.
598
599  Args:
600    props: Camera properties object.
601    gains: List of 4 values, in R,G_even,G_odd,B order.
602
603  Returns:
604    List of gains values, in R,Gr,Gb,B order.
605  """
606  cfa_pat = props['android.sensor.info.colorFilterArrangement']
607  if cfa_pat in [0, 1]:
608    # RGGB or GRBG, so G_even is Gr
609    return gains
610  elif cfa_pat in [2, 3]:
611    # GBRG or BGGR, so G_even is Gb
612    return [gains[0], gains[2], gains[1], gains[3]]
613  else:
614    raise error_util.CameraItsError('Not supported')
615
616
617def get_black_level(chan, props, cap_res=None):
618  """Return the black level to use for a given capture.
619
620  Uses a dynamic value from the capture result if available, else falls back
621  to the static global value in the camera characteristics.
622
623  Args:
624    chan: The channel index, in canonical order (R, Gr, Gb, B).
625    props: The camera properties object.
626    cap_res: A capture result object.
627
628  Returns:
629    The black level value for the specified channel.
630  """
631  if (cap_res is not None and
632      'android.sensor.dynamicBlackLevel' in cap_res and
633      cap_res['android.sensor.dynamicBlackLevel'] is not None):
634    black_levels = cap_res['android.sensor.dynamicBlackLevel']
635  else:
636    black_levels = props['android.sensor.blackLevelPattern']
637  idxs = get_canonical_cfa_order(props)
638  ordered_black_levels = [black_levels[i] for i in idxs]
639  return ordered_black_levels[chan]
640
641
642def get_canonical_cfa_order(props):
643  """Returns a mapping to the standard order R,Gr,Gb,B.
644
645  Returns a mapping from the Bayer 2x2 top-left grid in the CFA to the standard
646  order R,Gr,Gb,B.
647
648  Args:
649    props: Camera properties object.
650
651  Returns:
652     List of 4 integers, corresponding to the positions in the 2x2 top-
653     left Bayer grid of R,Gr,Gb,B, where the 2x2 grid is labeled as
654     0,1,2,3 in row major order.
655  """
656    # Note that raw streams aren't croppable, so the cropRegion doesn't need
657    # to be considered when determining the top-left pixel color.
658  cfa_pat = props['android.sensor.info.colorFilterArrangement']
659  if cfa_pat == 0:
660    # RGGB
661    return [0, 1, 2, 3]
662  elif cfa_pat == 1:
663    # GRBG
664    return [1, 0, 3, 2]
665  elif cfa_pat == 2:
666    # GBRG
667    return [2, 3, 0, 1]
668  elif cfa_pat == 3:
669    # BGGR
670    return [3, 2, 1, 0]
671  else:
672    raise error_util.CameraItsError('Not supported')
673
674
675def unpack_rawstats_capture(cap):
676  """Unpack a rawStats capture to the mean and variance images.
677
678  Args:
679    cap: A capture object as returned by its_session_utils.do_capture.
680
681  Returns:
682    Tuple (mean_image var_image) of float-4 images, with non-normalized
683    pixel values computed from the RAW16 images on the device
684  """
685  if cap['format'] != 'rawStats':
686    raise AssertionError(f"Unpack fmt != rawStats: {cap['format']}")
687  w = cap['width']
688  h = cap['height']
689  img = numpy.ndarray(shape=(2 * h * w * 4,), dtype='<f', buffer=cap['data'])
690  analysis_image = img.reshape((2, h, w, 4))
691  mean_image = analysis_image[0, :, :, :].reshape(h, w, 4)
692  var_image = analysis_image[1, :, :, :].reshape(h, w, 4)
693  return mean_image, var_image
694
695
696def get_image_patch(img, xnorm, ynorm, wnorm, hnorm):
697  """Get a patch (tile) of an image.
698
699  Args:
700   img: Numpy float image array, with pixel values in [0,1].
701   xnorm:
702   ynorm:
703   wnorm:
704   hnorm: Normalized (in [0,1]) coords for the tile.
705
706  Returns:
707     Numpy float image array of the patch.
708  """
709  hfull = img.shape[0]
710  wfull = img.shape[1]
711  xtile = int(math.ceil(xnorm * wfull))
712  ytile = int(math.ceil(ynorm * hfull))
713  wtile = int(math.floor(wnorm * wfull))
714  htile = int(math.floor(hnorm * hfull))
715  if len(img.shape) == 2:
716    return img[ytile:ytile + htile, xtile:xtile + wtile].copy()
717  else:
718    return img[ytile:ytile + htile, xtile:xtile + wtile, :].copy()
719
720
721def compute_image_means(img):
722  """Calculate the mean of each color channel in the image.
723
724  Args:
725    img: Numpy float image array, with pixel values in [0,1].
726
727  Returns:
728     A list of mean values, one per color channel in the image.
729  """
730  means = []
731  chans = img.shape[2]
732  for i in range(chans):
733    means.append(numpy.mean(img[:, :, i], dtype=numpy.float64))
734  return means
735
736
737def compute_image_variances(img):
738  """Calculate the variance of each color channel in the image.
739
740  Args:
741    img: Numpy float image array, with pixel values in [0,1].
742
743  Returns:
744    A list of variance values, one per color channel in the image.
745  """
746  variances = []
747  chans = img.shape[2]
748  for i in range(chans):
749    variances.append(numpy.var(img[:, :, i], dtype=numpy.float64))
750  return variances
751
752
753def compute_image_sharpness(img):
754  """Calculate the sharpness of input image.
755
756  Args:
757    img: numpy float RGB/luma image array, with pixel values in [0,1].
758
759  Returns:
760    Sharpness estimation value based on the average of gradient magnitude.
761    Larger value means the image is sharper.
762  """
763  chans = img.shape[2]
764  if chans != 1 and chans != 3:
765    raise AssertionError(f'Not RGB or MONO image! depth: {chans}')
766  if chans == 1:
767    luma = img[:, :, 0]
768  else:
769    luma = convert_rgb_to_grayscale(img)
770  gy, gx = numpy.gradient(luma)
771  return numpy.average(numpy.sqrt(gy*gy + gx*gx))
772
773
774def compute_image_max_gradients(img):
775  """Calculate the maximum gradient of each color channel in the image.
776
777  Args:
778    img: Numpy float image array, with pixel values in [0,1].
779
780  Returns:
781    A list of gradient max values, one per color channel in the image.
782  """
783  grads = []
784  chans = img.shape[2]
785  for i in range(chans):
786    grads.append(numpy.amax(numpy.gradient(img[:, :, i])))
787  return grads
788
789
790def compute_image_snrs(img):
791  """Calculate the SNR (dB) of each color channel in the image.
792
793  Args:
794    img: Numpy float image array, with pixel values in [0,1].
795
796  Returns:
797    A list of SNR values in dB, one per color channel in the image.
798  """
799  means = compute_image_means(img)
800  variances = compute_image_variances(img)
801  std_devs = [math.sqrt(v) for v in variances]
802  snrs = [20 * math.log10(m/s) for m, s in zip(means, std_devs)]
803  return snrs
804
805
806def convert_rgb_to_grayscale(img):
807  """Convert and 3-D array RGB image to grayscale image.
808
809  Args:
810    img: numpy float RGB/luma image array, with pixel values in [0,1].
811
812  Returns:
813    2-D grayscale image
814  """
815  chans = img.shape[2]
816  if chans != 3:
817    raise AssertionError(f'Not an RGB image! Depth: {chans}')
818  return 0.299*img[:, :, 0] + 0.587*img[:, :, 1] + 0.114*img[:, :, 2]
819
820
821def normalize_img(img):
822  """Normalize the image values to between 0 and 1.
823
824  Args:
825    img: 2-D numpy array of image values
826  Returns:
827    Normalized image
828  """
829  return (img - numpy.amin(img))/(numpy.amax(img) - numpy.amin(img))
830
831
832def rotate_img_per_argv(img):
833  """Rotate an image 180 degrees if "rotate" is in argv.
834
835  Args:
836    img: 2-D numpy array of image values
837  Returns:
838    Rotated image
839  """
840  img_out = img
841  if 'rotate180' in sys.argv:
842    img_out = numpy.fliplr(numpy.flipud(img_out))
843  return img_out
844
845
846def stationary_lens_cap(cam, req, fmt):
847  """Take up to NUM_TRYS caps and save the 1st one with lens stationary.
848
849  Args:
850   cam: open device session
851   req: capture request
852   fmt: format for capture
853
854  Returns:
855    capture
856  """
857  tries = 0
858  done = False
859  reqs = [req] * NUM_FRAMES
860  while not done:
861    logging.debug('Waiting for lens to move to correct location.')
862    cap = cam.do_capture(reqs, fmt)
863    done = (cap[NUM_FRAMES - 1]['metadata']['android.lens.state'] == 0)
864    logging.debug('status: %s', done)
865    tries += 1
866    if tries == NUM_TRIES:
867      raise error_util.CameraItsError('Cannot settle lens after %d tries!' %
868                                      tries)
869  return cap[NUM_FRAMES - 1]
870
871
872def compute_image_rms_difference_1d(rgb_x, rgb_y):
873  """Calculate the RMS difference between 2 RBG images as 1D arrays.
874
875  Args:
876    rgb_x: image array
877    rgb_y: image array
878
879  Returns:
880    rms_diff
881  """
882  len_rgb_x = len(rgb_x)
883  len_rgb_y = len(rgb_y)
884  if len_rgb_y != len_rgb_x:
885    raise AssertionError('RGB images have different number of planes! '
886                         f'x: {len_rgb_x}, y: {len_rgb_y}')
887  return math.sqrt(sum([pow(rgb_x[i] - rgb_y[i], 2.0)
888                        for i in range(len_rgb_x)]) / len_rgb_x)
889
890
891def compute_image_rms_difference_3d(rgb_x, rgb_y):
892  """Calculate the RMS difference between 2 RBG images as 3D arrays.
893
894  Args:
895    rgb_x: image array in the form of w * h * channels
896    rgb_y: image array in the form of w * h * channels
897
898  Returns:
899    rms_diff
900  """
901  shape_rgb_x = numpy.shape(rgb_x)
902  shape_rgb_y = numpy.shape(rgb_y)
903  if shape_rgb_y != shape_rgb_x:
904    raise AssertionError('RGB images have different number of planes! '
905                         f'x: {shape_rgb_x}, y: {shape_rgb_y}')
906  if len(shape_rgb_x) != 3:
907    raise AssertionError(f'RGB images dimension {len(shape_rgb_x)} is not 3!')
908
909  mean_square_sum = 0.0
910  for i in range(shape_rgb_x[0]):
911    for j in range(shape_rgb_x[1]):
912      for k in range(shape_rgb_x[2]):
913        mean_square_sum += pow(rgb_x[i][j][k] - rgb_y[i][j][k], 2.0)
914  return (math.sqrt(mean_square_sum /
915                    (shape_rgb_x[0] * shape_rgb_x[1] * shape_rgb_x[2])))
916
917
918def compute_image_sad(img_x, img_y):
919  """Calculate the sum of absolute differences between 2 images.
920
921  Args:
922    img_x: image array in the form of w * h * channels
923    img_y: image array in the form of w * h * channels
924
925  Returns:
926    sad
927  """
928  img_x = img_x[:, :, 1:].ravel()
929  img_y = img_y[:, :, 1:].ravel()
930  return numpy.sum(numpy.abs(numpy.subtract(img_x, img_y, dtype=float)))
931
932
933def get_img(buffer):
934  """Return a PIL.Image of the capture buffer.
935
936  Args:
937    buffer: data field from the capture result.
938
939  Returns:
940    A PIL.Image
941  """
942  return Image.open(io.BytesIO(buffer))
943
944
945def jpeg_has_icc_profile(jpeg_img):
946  """Checks if a jpeg PIL.Image has an icc profile attached.
947
948  Args:
949    jpeg_img: The PIL.Image.
950
951  Returns:
952    True if an icc profile is present, False otherwise.
953  """
954  return jpeg_img.info.get('icc_profile') is not None
955
956
957def get_primary_chromaticity(primary):
958  """Given an ImageCms primary, returns just the xy chromaticity coordinates.
959
960  Args:
961    primary: The primary from the ImageCms profile.
962
963  Returns:
964    (float, float): The xy chromaticity coordinates of the primary.
965  """
966  ((_, _, _), (x, y, _)) = primary
967  return x, y
968
969
970def is_jpeg_icc_profile_correct(jpeg_img, color_space, icc_profile_path=None):
971  """Compare a jpeg's icc profile to a color space's expected parameters.
972
973  Args:
974    jpeg_img: The PIL.Image.
975    color_space: 'DISPLAY_P3' or 'SRGB'
976    icc_profile_path: Optional path to an icc file to be created with the
977        raw contents.
978
979  Returns:
980    True if the icc profile matches expectations, False otherwise.
981  """
982  icc = jpeg_img.info.get('icc_profile')
983  f = io.BytesIO(icc)
984  icc_profile = ImageCms.getOpenProfile(f)
985
986  if icc_profile_path is not None:
987    raw_icc_bytes = f.getvalue()
988    f = open(icc_profile_path, 'wb')
989    f.write(raw_icc_bytes)
990    f.close()
991
992  cms_profile = icc_profile.profile
993  (rx, ry) = get_primary_chromaticity(cms_profile.red_primary)
994  (gx, gy) = get_primary_chromaticity(cms_profile.green_primary)
995  (bx, by) = get_primary_chromaticity(cms_profile.blue_primary)
996
997  if color_space == 'DISPLAY_P3':
998    # Expected primaries based on Apple's Display P3 primaries
999    expected_rx = EXPECTED_RX_P3
1000    expected_ry = EXPECTED_RY_P3
1001    expected_gx = EXPECTED_GX_P3
1002    expected_gy = EXPECTED_GY_P3
1003    expected_bx = EXPECTED_BX_P3
1004    expected_by = EXPECTED_BY_P3
1005  elif color_space == 'SRGB':
1006    # Expected primaries based on Pixel sRGB profile
1007    expected_rx = EXPECTED_RX_SRGB
1008    expected_ry = EXPECTED_RY_SRGB
1009    expected_gx = EXPECTED_GX_SRGB
1010    expected_gy = EXPECTED_GY_SRGB
1011    expected_bx = EXPECTED_BX_SRGB
1012    expected_by = EXPECTED_BY_SRGB
1013  else:
1014    # Unsupported color space for comparison
1015    return False
1016
1017  cmp_values = [
1018      [rx, expected_rx],
1019      [ry, expected_ry],
1020      [gx, expected_gx],
1021      [gy, expected_gy],
1022      [bx, expected_bx],
1023      [by, expected_by]
1024  ]
1025
1026  for (actual, expected) in cmp_values:
1027    if not math.isclose(actual, expected, abs_tol=0.001):
1028      # Values significantly differ
1029      return False
1030
1031  return True
1032
1033
1034def area_of_triangle(x1, y1, x2, y2, x3, y3):
1035  """Calculates the area of a triangle formed by three points.
1036
1037  Args:
1038    x1 (float): The x-coordinate of the first point.
1039    y1 (float): The y-coordinate of the first point.
1040    x2 (float): The x-coordinate of the second point.
1041    y2 (float): The y-coordinate of the second point.
1042    x3 (float): The x-coordinate of the third point.
1043    y3 (float): The y-coordinate of the third point.
1044
1045  Returns:
1046    float: The area of the triangle.
1047  """
1048  area = abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2.0)
1049  return area
1050
1051
1052def point_in_triangle(x1, y1, x2, y2, x3, y3, xp, yp, abs_tol):
1053  """Checks if the point (xp, yp) is inside the triangle.
1054
1055  Args:
1056    x1 (float): The x-coordinate of the first point.
1057    y1 (float): The y-coordinate of the first point.
1058    x2 (float): The x-coordinate of the second point.
1059    y2 (float): The y-coordinate of the second point.
1060    x3 (float): The x-coordinate of the third point.
1061    y3 (float): The y-coordinate of the third point.
1062    xp (float): The x-coordinate of the point to check.
1063    yp (float): The y-coordinate of the point to check.
1064    abs_tol (float): Absolute tolerance amount.
1065
1066  Returns:
1067    bool: True if the point is inside the triangle, False otherwise.
1068  """
1069  a = area_of_triangle(x1, y1, x2, y2, x3, y3)
1070  a1 = area_of_triangle(xp, yp, x2, y2, x3, y3)
1071  a2 = area_of_triangle(x1, y1, xp, yp, x3, y3)
1072  a3 = area_of_triangle(x1, y1, x2, y2, xp, yp)
1073  return math.isclose(a, (a1 + a2 + a3), abs_tol=abs_tol)
1074
1075
1076def distance(p, q):
1077  """Returns the Euclidean distance from point p to point q.
1078
1079  Args:
1080    p: an Iterable of numbers
1081    q: an Iterable of numbers
1082  """
1083  return math.sqrt(sum((px - qx) ** 2.0 for px, qx in zip(p, q)))
1084
1085
1086def p3_img_has_wide_gamut(wide_img):
1087  """Check if a DISPLAY_P3 image contains wide gamut pixels.
1088
1089  Given a DISPLAY_P3 image that should have a wider gamut than SRGB, checks all
1090  pixel values to see if any reside outside the SRGB gamut.
1091
1092  Args:
1093    wide_img: The PIL.Image in the DISPLAY_P3 color space.
1094
1095  Returns:
1096    True if the gamut of wide_img is greater than that of SRGB.
1097    False otherwise.
1098  """
1099  # Import in this function because this is the only function that uses this
1100  # library in UDC, and the test that calls into this will be skipped on the
1101  # vast majority of devices. In future versions, this is imported at the top.
1102  import colour
1103
1104  w = wide_img.size[0]
1105  h = wide_img.size[1]
1106  wide_arr = numpy.array(wide_img)
1107
1108  img_arr = colour.RGB_to_XYZ(
1109      wide_arr / 255.0,
1110      colour.models.rgb.datasets.display_p3.RGB_COLOURSPACE_DISPLAY_P3.whitepoint,
1111      colour.models.rgb.datasets.display_p3.RGB_COLOURSPACE_DISPLAY_P3.whitepoint,
1112      colour.models.rgb.datasets.display_p3.RGB_COLOURSPACE_DISPLAY_P3.matrix_RGB_to_XYZ,
1113      'Bradford', lambda x: colour.eotf(x, 'sRGB'))
1114
1115  xy_arr = colour.XYZ_to_xy(img_arr)
1116
1117  srgb_colorspace = colour.models.RGB_COLOURSPACE_sRGB
1118  srgb_primaries = srgb_colorspace.primaries
1119
1120  for y in range(h):
1121    for x in range(w):
1122      # Check if the pixel chromaticity is inside or outside the SRGB gamut.
1123      # This check is not guaranteed not to emit false positives / negatives,
1124      # however the probability of either on an arbitrary DISPLAY_P3 camera
1125      # capture is exceedingly unlikely.
1126      if not point_in_triangle(*srgb_primaries.reshape(6),
1127                               xy_arr[y][x][0], xy_arr[y][x][1],
1128                               COLORSPACE_TRIANGLE_AREA_TOL):
1129        return True
1130
1131  return False
1132