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 15import matplotlib 16matplotlib.use('Agg') 17 18import its.error 19from matplotlib import pylab 20import sys 21from PIL import Image 22import numpy 23import math 24import unittest 25import cStringIO 26import scipy.stats 27import copy 28import os 29 30DEFAULT_YUV_TO_RGB_CCM = numpy.matrix([ 31 [1.000, 0.000, 1.402], 32 [1.000, -0.344, -0.714], 33 [1.000, 1.772, 0.000]]) 34 35DEFAULT_YUV_OFFSETS = numpy.array([0, 128, 128]) 36 37DEFAULT_GAMMA_LUT = numpy.array( 38 [math.floor(65535 * math.pow(i/65535.0, 1/2.2) + 0.5) 39 for i in xrange(65536)]) 40 41DEFAULT_INVGAMMA_LUT = numpy.array( 42 [math.floor(65535 * math.pow(i/65535.0, 2.2) + 0.5) 43 for i in xrange(65536)]) 44 45MAX_LUT_SIZE = 65536 46 47NUM_TRYS = 2 48NUM_FRAMES = 4 49 50 51def convert_capture_to_rgb_image(cap, 52 ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM, 53 yuv_off=DEFAULT_YUV_OFFSETS, 54 props=None): 55 """Convert a captured image object to a RGB image. 56 57 Args: 58 cap: A capture object as returned by its.device.do_capture. 59 ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB. 60 yuv_off: (Optional) offsets to subtract from each of Y,U,V values. 61 props: (Optional) camera properties object (of static values); 62 required for processing raw images. 63 64 Returns: 65 RGB float-3 image array, with pixel values in [0.0, 1.0]. 66 """ 67 w = cap["width"] 68 h = cap["height"] 69 if cap["format"] == "raw10": 70 assert(props is not None) 71 cap = unpack_raw10_capture(cap, props) 72 if cap["format"] == "raw12": 73 assert(props is not None) 74 cap = unpack_raw12_capture(cap, props) 75 if cap["format"] == "yuv": 76 y = cap["data"][0:w*h] 77 u = cap["data"][w*h:w*h*5/4] 78 v = cap["data"][w*h*5/4:w*h*6/4] 79 return convert_yuv420_planar_to_rgb_image(y, u, v, w, h) 80 elif cap["format"] == "jpeg": 81 return decompress_jpeg_to_rgb_image(cap["data"]) 82 elif cap["format"] == "raw" or cap["format"] == "rawStats": 83 assert(props is not None) 84 r,gr,gb,b = convert_capture_to_planes(cap, props) 85 return convert_raw_to_rgb_image(r,gr,gb,b, props, cap["metadata"]) 86 else: 87 raise its.error.Error('Invalid format %s' % (cap["format"])) 88 89def unpack_rawstats_capture(cap): 90 """Unpack a rawStats capture to the mean and variance images. 91 92 Args: 93 cap: A capture object as returned by its.device.do_capture. 94 95 Returns: 96 Tuple (mean_image var_image) of float-4 images, with non-normalized 97 pixel values computed from the RAW16 images on the device 98 """ 99 assert(cap["format"] == "rawStats") 100 w = cap["width"] 101 h = cap["height"] 102 img = numpy.ndarray(shape=(2*h*w*4,), dtype='<f', buffer=cap["data"]) 103 analysis_image = img.reshape(2,h,w,4) 104 mean_image = analysis_image[0,:,:,:].reshape(h,w,4) 105 var_image = analysis_image[1,:,:,:].reshape(h,w,4) 106 return mean_image, var_image 107 108def unpack_raw10_capture(cap, props): 109 """Unpack a raw-10 capture to a raw-16 capture. 110 111 Args: 112 cap: A raw-10 capture object. 113 props: Camera properties object. 114 115 Returns: 116 New capture object with raw-16 data. 117 """ 118 # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding 119 # the MSPs of the pixels, and the 5th byte holding 4x2b LSBs. 120 w,h = cap["width"], cap["height"] 121 if w % 4 != 0: 122 raise its.error.Error('Invalid raw-10 buffer width') 123 cap = copy.deepcopy(cap) 124 cap["data"] = unpack_raw10_image(cap["data"].reshape(h,w*5/4)) 125 cap["format"] = "raw" 126 return cap 127 128def unpack_raw10_image(img): 129 """Unpack a raw-10 image to a raw-16 image. 130 131 Output image will have the 10 LSBs filled in each 16b word, and the 6 MSBs 132 will be set to zero. 133 134 Args: 135 img: A raw-10 image, as a uint8 numpy array. 136 137 Returns: 138 Image as a uint16 numpy array, with all row padding stripped. 139 """ 140 if img.shape[1] % 5 != 0: 141 raise its.error.Error('Invalid raw-10 buffer width') 142 w = img.shape[1]*4/5 143 h = img.shape[0] 144 # Cut out the 4x8b MSBs and shift to bits [9:2] in 16b words. 145 msbs = numpy.delete(img, numpy.s_[4::5], 1) 146 msbs = msbs.astype(numpy.uint16) 147 msbs = numpy.left_shift(msbs, 2) 148 msbs = msbs.reshape(h,w) 149 # Cut out the 4x2b LSBs and put each in bits [1:0] of their own 8b words. 150 lsbs = img[::, 4::5].reshape(h,w/4) 151 lsbs = numpy.right_shift( 152 numpy.packbits(numpy.unpackbits(lsbs).reshape(h,w/4,4,2),3), 6) 153 lsbs = lsbs.reshape(h,w) 154 # Fuse the MSBs and LSBs back together 155 img16 = numpy.bitwise_or(msbs, lsbs).reshape(h,w) 156 return img16 157 158def unpack_raw12_capture(cap, props): 159 """Unpack a raw-12 capture to a raw-16 capture. 160 161 Args: 162 cap: A raw-12 capture object. 163 props: Camera properties object. 164 165 Returns: 166 New capture object with raw-16 data. 167 """ 168 # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding 169 # the MSBs of the pixels, and the 5th byte holding 4x2b LSBs. 170 w,h = cap["width"], cap["height"] 171 if w % 2 != 0: 172 raise its.error.Error('Invalid raw-12 buffer width') 173 cap = copy.deepcopy(cap) 174 cap["data"] = unpack_raw12_image(cap["data"].reshape(h,w*3/2)) 175 cap["format"] = "raw" 176 return cap 177 178def unpack_raw12_image(img): 179 """Unpack a raw-12 image to a raw-16 image. 180 181 Output image will have the 12 LSBs filled in each 16b word, and the 4 MSBs 182 will be set to zero. 183 184 Args: 185 img: A raw-12 image, as a uint8 numpy array. 186 187 Returns: 188 Image as a uint16 numpy array, with all row padding stripped. 189 """ 190 if img.shape[1] % 3 != 0: 191 raise its.error.Error('Invalid raw-12 buffer width') 192 w = img.shape[1]*2/3 193 h = img.shape[0] 194 # Cut out the 2x8b MSBs and shift to bits [11:4] in 16b words. 195 msbs = numpy.delete(img, numpy.s_[2::3], 1) 196 msbs = msbs.astype(numpy.uint16) 197 msbs = numpy.left_shift(msbs, 4) 198 msbs = msbs.reshape(h,w) 199 # Cut out the 2x4b LSBs and put each in bits [3:0] of their own 8b words. 200 lsbs = img[::, 2::3].reshape(h,w/2) 201 lsbs = numpy.right_shift( 202 numpy.packbits(numpy.unpackbits(lsbs).reshape(h,w/2,2,4),3), 4) 203 lsbs = lsbs.reshape(h,w) 204 # Fuse the MSBs and LSBs back together 205 img16 = numpy.bitwise_or(msbs, lsbs).reshape(h,w) 206 return img16 207 208def convert_capture_to_planes(cap, props=None): 209 """Convert a captured image object to separate image planes. 210 211 Decompose an image into multiple images, corresponding to different planes. 212 213 For YUV420 captures ("yuv"): 214 Returns Y,U,V planes, where the Y plane is full-res and the U,V planes 215 are each 1/2 x 1/2 of the full res. 216 217 For Bayer captures ("raw", "raw10", "raw12", or "rawStats"): 218 Returns planes in the order R,Gr,Gb,B, regardless of the Bayer pattern 219 layout. For full-res raw images ("raw", "raw10", "raw12"), each plane 220 is 1/2 x 1/2 of the full res. For "rawStats" images, the mean image 221 is returned. 222 223 For JPEG captures ("jpeg"): 224 Returns R,G,B full-res planes. 225 226 Args: 227 cap: A capture object as returned by its.device.do_capture. 228 props: (Optional) camera properties object (of static values); 229 required for processing raw images. 230 231 Returns: 232 A tuple of float numpy arrays (one per plane), consisting of pixel 233 values in the range [0.0, 1.0]. 234 """ 235 w = cap["width"] 236 h = cap["height"] 237 if cap["format"] == "raw10": 238 assert(props is not None) 239 cap = unpack_raw10_capture(cap, props) 240 if cap["format"] == "raw12": 241 assert(props is not None) 242 cap = unpack_raw12_capture(cap, props) 243 if cap["format"] == "yuv": 244 y = cap["data"][0:w*h] 245 u = cap["data"][w*h:w*h*5/4] 246 v = cap["data"][w*h*5/4:w*h*6/4] 247 return ((y.astype(numpy.float32) / 255.0).reshape(h, w, 1), 248 (u.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1), 249 (v.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1)) 250 elif cap["format"] == "jpeg": 251 rgb = decompress_jpeg_to_rgb_image(cap["data"]).reshape(w*h*3) 252 return (rgb[::3].reshape(h,w,1), 253 rgb[1::3].reshape(h,w,1), 254 rgb[2::3].reshape(h,w,1)) 255 elif cap["format"] == "raw": 256 assert(props is not None) 257 white_level = float(props['android.sensor.info.whiteLevel']) 258 img = numpy.ndarray(shape=(h*w,), dtype='<u2', 259 buffer=cap["data"][0:w*h*2]) 260 img = img.astype(numpy.float32).reshape(h,w) / white_level 261 # Crop the raw image to the active array region. 262 if props.has_key("android.sensor.info.activeArraySize") \ 263 and props["android.sensor.info.activeArraySize"] is not None \ 264 and props.has_key("android.sensor.info.pixelArraySize") \ 265 and props["android.sensor.info.pixelArraySize"] is not None: 266 # Note that the Rect class is defined such that the left,top values 267 # are "inside" while the right,bottom values are "outside"; that is, 268 # it's inclusive of the top,left sides only. So, the width is 269 # computed as right-left, rather than right-left+1, etc. 270 wfull = props["android.sensor.info.pixelArraySize"]["width"] 271 hfull = props["android.sensor.info.pixelArraySize"]["height"] 272 xcrop = props["android.sensor.info.activeArraySize"]["left"] 273 ycrop = props["android.sensor.info.activeArraySize"]["top"] 274 wcrop = props["android.sensor.info.activeArraySize"]["right"]-xcrop 275 hcrop = props["android.sensor.info.activeArraySize"]["bottom"]-ycrop 276 assert(wfull >= wcrop >= 0) 277 assert(hfull >= hcrop >= 0) 278 assert(wfull - wcrop >= xcrop >= 0) 279 assert(hfull - hcrop >= ycrop >= 0) 280 if w == wfull and h == hfull: 281 # Crop needed; extract the center region. 282 img = img[ycrop:ycrop+hcrop,xcrop:xcrop+wcrop] 283 w = wcrop 284 h = hcrop 285 elif w == wcrop and h == hcrop: 286 # No crop needed; image is already cropped to the active array. 287 None 288 else: 289 raise its.error.Error('Invalid image size metadata') 290 # Separate the image planes. 291 imgs = [img[::2].reshape(w*h/2)[::2].reshape(h/2,w/2,1), 292 img[::2].reshape(w*h/2)[1::2].reshape(h/2,w/2,1), 293 img[1::2].reshape(w*h/2)[::2].reshape(h/2,w/2,1), 294 img[1::2].reshape(w*h/2)[1::2].reshape(h/2,w/2,1)] 295 idxs = get_canonical_cfa_order(props) 296 return [imgs[i] for i in idxs] 297 elif cap["format"] == "rawStats": 298 assert(props is not None) 299 white_level = float(props['android.sensor.info.whiteLevel']) 300 mean_image, var_image = its.image.unpack_rawstats_capture(cap) 301 idxs = get_canonical_cfa_order(props) 302 return [mean_image[:,:,i] / white_level for i in idxs] 303 else: 304 raise its.error.Error('Invalid format %s' % (cap["format"])) 305 306def get_canonical_cfa_order(props): 307 """Returns a mapping from the Bayer 2x2 top-left grid in the CFA to 308 the standard order R,Gr,Gb,B. 309 310 Args: 311 props: Camera properties object. 312 313 Returns: 314 List of 4 integers, corresponding to the positions in the 2x2 top- 315 left Bayer grid of R,Gr,Gb,B, where the 2x2 grid is labeled as 316 0,1,2,3 in row major order. 317 """ 318 # Note that raw streams aren't croppable, so the cropRegion doesn't need 319 # to be considered when determining the top-left pixel color. 320 cfa_pat = props['android.sensor.info.colorFilterArrangement'] 321 if cfa_pat == 0: 322 # RGGB 323 return [0,1,2,3] 324 elif cfa_pat == 1: 325 # GRBG 326 return [1,0,3,2] 327 elif cfa_pat == 2: 328 # GBRG 329 return [2,3,0,1] 330 elif cfa_pat == 3: 331 # BGGR 332 return [3,2,1,0] 333 else: 334 raise its.error.Error("Not supported") 335 336def get_gains_in_canonical_order(props, gains): 337 """Reorders the gains tuple to the canonical R,Gr,Gb,B order. 338 339 Args: 340 props: Camera properties object. 341 gains: List of 4 values, in R,G_even,G_odd,B order. 342 343 Returns: 344 List of gains values, in R,Gr,Gb,B order. 345 """ 346 cfa_pat = props['android.sensor.info.colorFilterArrangement'] 347 if cfa_pat in [0,1]: 348 # RGGB or GRBG, so G_even is Gr 349 return gains 350 elif cfa_pat in [2,3]: 351 # GBRG or BGGR, so G_even is Gb 352 return [gains[0], gains[2], gains[1], gains[3]] 353 else: 354 raise its.error.Error("Not supported") 355 356def convert_raw_to_rgb_image(r_plane, gr_plane, gb_plane, b_plane, 357 props, cap_res): 358 """Convert a Bayer raw-16 image to an RGB image. 359 360 Includes some extremely rudimentary demosaicking and color processing 361 operations; the output of this function shouldn't be used for any image 362 quality analysis. 363 364 Args: 365 r_plane,gr_plane,gb_plane,b_plane: Numpy arrays for each color plane 366 in the Bayer image, with pixels in the [0.0, 1.0] range. 367 props: Camera properties object. 368 cap_res: Capture result (metadata) object. 369 370 Returns: 371 RGB float-3 image array, with pixel values in [0.0, 1.0] 372 """ 373 # Values required for the RAW to RGB conversion. 374 assert(props is not None) 375 white_level = float(props['android.sensor.info.whiteLevel']) 376 black_levels = props['android.sensor.blackLevelPattern'] 377 gains = cap_res['android.colorCorrection.gains'] 378 ccm = cap_res['android.colorCorrection.transform'] 379 380 # Reorder black levels and gains to R,Gr,Gb,B, to match the order 381 # of the planes. 382 black_levels = [get_black_level(i,props,cap_res) for i in range(4)] 383 gains = get_gains_in_canonical_order(props, gains) 384 385 # Convert CCM from rational to float, as numpy arrays. 386 ccm = numpy.array(its.objects.rational_to_float(ccm)).reshape(3,3) 387 388 # Need to scale the image back to the full [0,1] range after subtracting 389 # the black level from each pixel. 390 scale = white_level / (white_level - max(black_levels)) 391 392 # Three-channel black levels, normalized to [0,1] by white_level. 393 black_levels = numpy.array([b/white_level for b in [ 394 black_levels[i] for i in [0,1,3]]]) 395 396 # Three-channel gains. 397 gains = numpy.array([gains[i] for i in [0,1,3]]) 398 399 h,w = r_plane.shape[:2] 400 img = numpy.dstack([r_plane,(gr_plane+gb_plane)/2.0,b_plane]) 401 img = (((img.reshape(h,w,3) - black_levels) * scale) * gains).clip(0.0,1.0) 402 img = numpy.dot(img.reshape(w*h,3), ccm.T).reshape(h,w,3).clip(0.0,1.0) 403 return img 404 405def get_black_level(chan, props, cap_res): 406 """Return the black level to use for a given capture. 407 408 Uses a dynamic value from the capture result if available, else falls back 409 to the static global value in the camera characteristics. 410 411 Args: 412 chan: The channel index, in canonical order (R, Gr, Gb, B). 413 props: The camera properties object. 414 cap_res: A capture result object. 415 416 Returns: 417 The black level value for the specified channel. 418 """ 419 if (cap_res.has_key('android.sensor.dynamicBlackLevel') and 420 cap_res['android.sensor.dynamicBlackLevel'] is not None): 421 black_levels = cap_res['android.sensor.dynamicBlackLevel'] 422 else: 423 black_levels = props['android.sensor.blackLevelPattern'] 424 idxs = its.image.get_canonical_cfa_order(props) 425 ordered_black_levels = [black_levels[i] for i in idxs] 426 return ordered_black_levels[chan] 427 428def convert_yuv420_planar_to_rgb_image(y_plane, u_plane, v_plane, 429 w, h, 430 ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM, 431 yuv_off=DEFAULT_YUV_OFFSETS): 432 """Convert a YUV420 8-bit planar image to an RGB image. 433 434 Args: 435 y_plane: The packed 8-bit Y plane. 436 u_plane: The packed 8-bit U plane. 437 v_plane: The packed 8-bit V plane. 438 w: The width of the image. 439 h: The height of the image. 440 ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB. 441 yuv_off: (Optional) offsets to subtract from each of Y,U,V values. 442 443 Returns: 444 RGB float-3 image array, with pixel values in [0.0, 1.0]. 445 """ 446 y = numpy.subtract(y_plane, yuv_off[0]) 447 u = numpy.subtract(u_plane, yuv_off[1]).view(numpy.int8) 448 v = numpy.subtract(v_plane, yuv_off[2]).view(numpy.int8) 449 u = u.reshape(h/2, w/2).repeat(2, axis=1).repeat(2, axis=0) 450 v = v.reshape(h/2, w/2).repeat(2, axis=1).repeat(2, axis=0) 451 yuv = numpy.dstack([y, u.reshape(w*h), v.reshape(w*h)]) 452 flt = numpy.empty([h, w, 3], dtype=numpy.float32) 453 flt.reshape(w*h*3)[:] = yuv.reshape(h*w*3)[:] 454 flt = numpy.dot(flt.reshape(w*h,3), ccm_yuv_to_rgb.T).clip(0, 255) 455 rgb = numpy.empty([h, w, 3], dtype=numpy.uint8) 456 rgb.reshape(w*h*3)[:] = flt.reshape(w*h*3)[:] 457 return rgb.astype(numpy.float32) / 255.0 458 459def load_rgb_image(fname): 460 """Load a standard image file (JPG, PNG, etc.). 461 462 Args: 463 fname: The path of the file to load. 464 465 Returns: 466 RGB float-3 image array, with pixel values in [0.0, 1.0]. 467 """ 468 img = Image.open(fname) 469 w = img.size[0] 470 h = img.size[1] 471 a = numpy.array(img) 472 if len(a.shape) == 3 and a.shape[2] == 3: 473 # RGB 474 return a.reshape(h,w,3) / 255.0 475 elif len(a.shape) == 2 or len(a.shape) == 3 and a.shape[2] == 1: 476 # Greyscale; convert to RGB 477 return a.reshape(h*w).repeat(3).reshape(h,w,3) / 255.0 478 else: 479 raise its.error.Error('Unsupported image type') 480 481def load_yuv420_to_rgb_image(yuv_fname, 482 w, h, 483 layout="planar", 484 ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM, 485 yuv_off=DEFAULT_YUV_OFFSETS): 486 """Load a YUV420 image file, and return as an RGB image. 487 488 Supported layouts include "planar" and "nv21". The "yuv" formatted captures 489 returned from the device via do_capture are in the "planar" layout; other 490 layouts may only be needed for loading files from other sources. 491 492 Args: 493 yuv_fname: The path of the YUV420 file. 494 w: The width of the image. 495 h: The height of the image. 496 layout: (Optional) the layout of the YUV data (as a string). 497 ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB. 498 yuv_off: (Optional) offsets to subtract from each of Y,U,V values. 499 500 Returns: 501 RGB float-3 image array, with pixel values in [0.0, 1.0]. 502 """ 503 with open(yuv_fname, "rb") as f: 504 if layout == "planar": 505 # Plane of Y, plane of V, plane of U. 506 y = numpy.fromfile(f, numpy.uint8, w*h, "") 507 v = numpy.fromfile(f, numpy.uint8, w*h/4, "") 508 u = numpy.fromfile(f, numpy.uint8, w*h/4, "") 509 elif layout == "nv21": 510 # Plane of Y, plane of interleaved VUVUVU... 511 y = numpy.fromfile(f, numpy.uint8, w*h, "") 512 vu = numpy.fromfile(f, numpy.uint8, w*h/2, "") 513 v = vu[0::2] 514 u = vu[1::2] 515 else: 516 raise its.error.Error('Unsupported image layout') 517 return convert_yuv420_planar_to_rgb_image( 518 y,u,v,w,h,ccm_yuv_to_rgb,yuv_off) 519 520def load_yuv420_planar_to_yuv_planes(yuv_fname, w, h): 521 """Load a YUV420 planar image file, and return Y, U, and V plane images. 522 523 Args: 524 yuv_fname: The path of the YUV420 file. 525 w: The width of the image. 526 h: The height of the image. 527 528 Returns: 529 Separate Y, U, and V images as float-1 Numpy arrays, pixels in [0,1]. 530 Note that pixel (0,0,0) is not black, since U,V pixels are centered at 531 0.5, and also that the Y and U,V plane images returned are different 532 sizes (due to chroma subsampling in the YUV420 format). 533 """ 534 with open(yuv_fname, "rb") as f: 535 y = numpy.fromfile(f, numpy.uint8, w*h, "") 536 v = numpy.fromfile(f, numpy.uint8, w*h/4, "") 537 u = numpy.fromfile(f, numpy.uint8, w*h/4, "") 538 return ((y.astype(numpy.float32) / 255.0).reshape(h, w, 1), 539 (u.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1), 540 (v.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1)) 541 542def decompress_jpeg_to_rgb_image(jpeg_buffer): 543 """Decompress a JPEG-compressed image, returning as an RGB image. 544 545 Args: 546 jpeg_buffer: The JPEG stream. 547 548 Returns: 549 A numpy array for the RGB image, with pixels in [0,1]. 550 """ 551 img = Image.open(cStringIO.StringIO(jpeg_buffer)) 552 w = img.size[0] 553 h = img.size[1] 554 return numpy.array(img).reshape(h,w,3) / 255.0 555 556def apply_lut_to_image(img, lut): 557 """Applies a LUT to every pixel in a float image array. 558 559 Internally converts to a 16b integer image, since the LUT can work with up 560 to 16b->16b mappings (i.e. values in the range [0,65535]). The lut can also 561 have fewer than 65536 entries, however it must be sized as a power of 2 562 (and for smaller luts, the scale must match the bitdepth). 563 564 For a 16b lut of 65536 entries, the operation performed is: 565 566 lut[r * 65535] / 65535 -> r' 567 lut[g * 65535] / 65535 -> g' 568 lut[b * 65535] / 65535 -> b' 569 570 For a 10b lut of 1024 entries, the operation becomes: 571 572 lut[r * 1023] / 1023 -> r' 573 lut[g * 1023] / 1023 -> g' 574 lut[b * 1023] / 1023 -> b' 575 576 Args: 577 img: Numpy float image array, with pixel values in [0,1]. 578 lut: Numpy table encoding a LUT, mapping 16b integer values. 579 580 Returns: 581 Float image array after applying LUT to each pixel. 582 """ 583 n = len(lut) 584 if n <= 0 or n > MAX_LUT_SIZE or (n & (n - 1)) != 0: 585 raise its.error.Error('Invalid arg LUT size: %d' % (n)) 586 m = float(n-1) 587 return (lut[(img * m).astype(numpy.uint16)] / m).astype(numpy.float32) 588 589def apply_matrix_to_image(img, mat): 590 """Multiplies a 3x3 matrix with each float-3 image pixel. 591 592 Each pixel is considered a column vector, and is left-multiplied by 593 the given matrix: 594 595 [ ] r r' 596 [ mat ] * g -> g' 597 [ ] b b' 598 599 Args: 600 img: Numpy float image array, with pixel values in [0,1]. 601 mat: Numpy 3x3 matrix. 602 603 Returns: 604 The numpy float-3 image array resulting from the matrix mult. 605 """ 606 h = img.shape[0] 607 w = img.shape[1] 608 img2 = numpy.empty([h, w, 3], dtype=numpy.float32) 609 img2.reshape(w*h*3)[:] = (numpy.dot(img.reshape(h*w, 3), mat.T) 610 ).reshape(w*h*3)[:] 611 return img2 612 613def get_image_patch(img, xnorm, ynorm, wnorm, hnorm): 614 """Get a patch (tile) of an image. 615 616 Args: 617 img: Numpy float image array, with pixel values in [0,1]. 618 xnorm,ynorm,wnorm,hnorm: Normalized (in [0,1]) coords for the tile. 619 620 Returns: 621 Float image array of the patch. 622 """ 623 hfull = img.shape[0] 624 wfull = img.shape[1] 625 xtile = math.ceil(xnorm * wfull) 626 ytile = math.ceil(ynorm * hfull) 627 wtile = math.floor(wnorm * wfull) 628 htile = math.floor(hnorm * hfull) 629 return img[ytile:ytile+htile,xtile:xtile+wtile,:].copy() 630 631def compute_image_means(img): 632 """Calculate the mean of each color channel in the image. 633 634 Args: 635 img: Numpy float image array, with pixel values in [0,1]. 636 637 Returns: 638 A list of mean values, one per color channel in the image. 639 """ 640 means = [] 641 chans = img.shape[2] 642 for i in xrange(chans): 643 means.append(numpy.mean(img[:,:,i], dtype=numpy.float64)) 644 return means 645 646def compute_image_variances(img): 647 """Calculate the variance of each color channel in the image. 648 649 Args: 650 img: Numpy float image array, with pixel values in [0,1]. 651 652 Returns: 653 A list of mean values, one per color channel in the image. 654 """ 655 variances = [] 656 chans = img.shape[2] 657 for i in xrange(chans): 658 variances.append(numpy.var(img[:,:,i], dtype=numpy.float64)) 659 return variances 660 661def compute_image_snrs(img): 662 """Calculate the SNR (db) of each color channel in the image. 663 664 Args: 665 img: Numpy float image array, with pixel values in [0,1]. 666 667 Returns: 668 A list of SNR value, one per color channel in the image. 669 """ 670 means = compute_image_means(img) 671 variances = compute_image_variances(img) 672 std_devs = [math.sqrt(v) for v in variances] 673 snr = [20 * math.log10(m/s) for m,s in zip(means, std_devs)] 674 return snr 675 676def write_image(img, fname, apply_gamma=False): 677 """Save a float-3 numpy array image to a file. 678 679 Supported formats: PNG, JPEG, and others; see PIL docs for more. 680 681 Image can be 3-channel, which is interpreted as RGB, or can be 1-channel, 682 which is greyscale. 683 684 Can optionally specify that the image should be gamma-encoded prior to 685 writing it out; this should be done if the image contains linear pixel 686 values, to make the image look "normal". 687 688 Args: 689 img: Numpy image array data. 690 fname: Path of file to save to; the extension specifies the format. 691 apply_gamma: (Optional) apply gamma to the image prior to writing it. 692 """ 693 if apply_gamma: 694 img = apply_lut_to_image(img, DEFAULT_GAMMA_LUT) 695 (h, w, chans) = img.shape 696 if chans == 3: 697 Image.fromarray((img * 255.0).astype(numpy.uint8), "RGB").save(fname) 698 elif chans == 1: 699 img3 = (img * 255.0).astype(numpy.uint8).repeat(3).reshape(h,w,3) 700 Image.fromarray(img3, "RGB").save(fname) 701 else: 702 raise its.error.Error('Unsupported image type') 703 704def downscale_image(img, f): 705 """Shrink an image by a given integer factor. 706 707 This function computes output pixel values by averaging over rectangular 708 regions of the input image; it doesn't skip or sample pixels, and all input 709 image pixels are evenly weighted. 710 711 If the downscaling factor doesn't cleanly divide the width and/or height, 712 then the remaining pixels on the right or bottom edge are discarded prior 713 to the downscaling. 714 715 Args: 716 img: The input image as an ndarray. 717 f: The downscaling factor, which should be an integer. 718 719 Returns: 720 The new (downscaled) image, as an ndarray. 721 """ 722 h,w,chans = img.shape 723 f = int(f) 724 assert(f >= 1) 725 h = (h/f)*f 726 w = (w/f)*f 727 img = img[0:h:,0:w:,::] 728 chs = [] 729 for i in xrange(chans): 730 ch = img.reshape(h*w*chans)[i::chans].reshape(h,w) 731 ch = ch.reshape(h,w/f,f).mean(2).reshape(h,w/f) 732 ch = ch.T.reshape(w/f,h/f,f).mean(2).T.reshape(h/f,w/f) 733 chs.append(ch.reshape(h*w/(f*f))) 734 img = numpy.vstack(chs).T.reshape(h/f,w/f,chans) 735 return img 736 737 738def compute_image_sharpness(img): 739 """Calculate the sharpness of input image. 740 741 Args: 742 img: Numpy float RGB/luma image array, with pixel values in [0,1]. 743 744 Returns: 745 A sharpness estimation value based on the average of gradient magnitude. 746 Larger value means the image is sharper. 747 """ 748 chans = img.shape[2] 749 assert(chans == 1 or chans == 3) 750 if (chans == 1): 751 luma = img[:, :, 0] 752 elif (chans == 3): 753 luma = 0.299 * img[:,:,0] + 0.587 * img[:,:,1] + 0.114 * img[:,:,2] 754 755 [gy, gx] = numpy.gradient(luma) 756 return numpy.average(numpy.sqrt(gy*gy + gx*gx)) 757 758def normalize_img(img): 759 """Normalize the image values to between 0 and 1. 760 761 Args: 762 img: 2-D numpy array of image values 763 Returns: 764 Normalized image 765 """ 766 return (img - numpy.amin(img))/(numpy.amax(img) - numpy.amin(img)) 767 768def flip_mirror_img_per_argv(img): 769 """Flip/mirror an image if "flip" or "mirror" is in argv 770 771 Args: 772 img: 2-D numpy array of image values 773 Returns: 774 Flip/mirrored image 775 """ 776 img_out = img 777 if "flip" in sys.argv: 778 img_out = numpy.flipud(img_out) 779 if "mirror" in sys.argv: 780 img_out = numpy.fliplr(img_out) 781 return img_out 782 783def stationary_lens_cap(cam, req, fmt): 784 """Take up to NUM_TRYS caps and save the 1st one with lens stationary. 785 786 Args: 787 cam: open device session 788 req: capture request 789 fmt: format for capture 790 791 Returns: 792 capture 793 """ 794 trys = 0 795 done = False 796 reqs = [req] * NUM_FRAMES 797 while not done: 798 print 'Waiting for lens to move to correct location...' 799 cap = cam.do_capture(reqs, fmt) 800 done = (cap[NUM_FRAMES-1]['metadata']['android.lens.state'] == 0) 801 print ' status: ', done 802 trys += 1 803 if trys == NUM_TRYS: 804 raise its.error.Error('Cannot settle lens after %d trys!' % trys) 805 return cap[NUM_FRAMES-1] 806 807class __UnitTest(unittest.TestCase): 808 """Run a suite of unit tests on this module. 809 """ 810 811 # TODO: Add more unit tests. 812 813 def test_apply_matrix_to_image(self): 814 """Unit test for apply_matrix_to_image. 815 816 Test by using a canned set of values on a 1x1 pixel image. 817 818 [ 1 2 3 ] [ 0.1 ] [ 1.4 ] 819 [ 4 5 6 ] * [ 0.2 ] = [ 3.2 ] 820 [ 7 8 9 ] [ 0.3 ] [ 5.0 ] 821 mat x y 822 """ 823 mat = numpy.array([[1,2,3], [4,5,6], [7,8,9]]) 824 x = numpy.array([0.1,0.2,0.3]).reshape(1,1,3) 825 y = apply_matrix_to_image(x, mat).reshape(3).tolist() 826 y_ref = [1.4,3.2,5.0] 827 passed = all([math.fabs(y[i] - y_ref[i]) < 0.001 for i in xrange(3)]) 828 self.assertTrue(passed) 829 830 def test_apply_lut_to_image(self): 831 """Unit test for apply_lut_to_image. 832 833 Test by using a canned set of values on a 1x1 pixel image. The LUT will 834 simply double the value of the index: 835 836 lut[x] = 2*x 837 """ 838 lut = numpy.array([2*i for i in xrange(65536)]) 839 x = numpy.array([0.1,0.2,0.3]).reshape(1,1,3) 840 y = apply_lut_to_image(x, lut).reshape(3).tolist() 841 y_ref = [0.2,0.4,0.6] 842 passed = all([math.fabs(y[i] - y_ref[i]) < 0.001 for i in xrange(3)]) 843 self.assertTrue(passed) 844 845if __name__ == '__main__': 846 unittest.main() 847