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