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