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