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