Home | History | Annotate | Download | only in its
      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 
     15 import matplotlib
     16 matplotlib.use('Agg')
     17 
     18 import its.error
     19 import pylab
     20 import sys
     21 import Image
     22 import numpy
     23 import math
     24 import unittest
     25 import cStringIO
     26 
     27 DEFAULT_YUV_TO_RGB_CCM = numpy.matrix([
     28                                 [1.000,  0.000,  1.402],
     29                                 [1.000, -0.344, -0.714],
     30                                 [1.000,  1.772,  0.000]])
     31 
     32 DEFAULT_YUV_OFFSETS = numpy.array([0, 128, 128])
     33 
     34 DEFAULT_GAMMA_LUT = numpy.array(
     35         [math.floor(65535 * math.pow(i/65535.0, 1/2.2) + 0.5) for i in xrange(65536)])
     36 
     37 DEFAULT_INVGAMMA_LUT = numpy.array(
     38         [math.floor(65535 * math.pow(i/65535.0, 2.2) + 0.5) for i in xrange(65536)])
     39 
     40 MAX_LUT_SIZE = 65536
     41 
     42 def convert_capture_to_rgb_image(cap,
     43                                  ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
     44                                  yuv_off=DEFAULT_YUV_OFFSETS):
     45     """Convert a captured image object to a RGB image.
     46 
     47     Args:
     48         cap: A capture object as returned by its.device.do_capture.
     49         ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
     50         yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
     51 
     52     Returns:
     53         RGB float-3 image array, with pixel values in [0.0, 1.0].
     54     """
     55     w = cap["width"]
     56     h = cap["height"]
     57     if cap["format"] == "yuv":
     58         y = cap["data"][0:w*h]
     59         u = cap["data"][w*h:w*h*5/4]
     60         v = cap["data"][w*h*5/4:w*h*6/4]
     61         return convert_yuv420_to_rgb_image(y, u, v, w, h)
     62     elif cap["format"] == "jpeg":
     63         # TODO: Convert JPEG to RGB.
     64         raise its.error.Error('Invalid format %s' % (cap["format"]))
     65     else:
     66         raise its.error.Error('Invalid format %s' % (cap["format"]))
     67 
     68 def convert_capture_to_yuv_planes(cap):
     69     """Convert a captured image object to separate Y,U,V image planes.
     70 
     71     The only input format that is supported is planar YUV420, and the planes
     72     that are returned are such that the U,V planes are 1/2 x 1/2 of the Y
     73     plane size.
     74 
     75     Args:
     76         cap: A capture object as returned by its.device.do_capture.
     77 
     78     Returns:
     79         Three float arrays, for the Y,U,V planes, with pixel values in [0,1].
     80     """
     81     w = cap["width"]
     82     h = cap["height"]
     83     if cap["format"] == "yuv":
     84         y = cap["data"][0:w*h]
     85         u = cap["data"][w*h:w*h*5/4]
     86         v = cap["data"][w*h*5/4:w*h*6/4]
     87         return ((y.astype(numpy.float32) / 255.0).reshape(h, w, 1),
     88                 (u.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1),
     89                 (v.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1))
     90     else:
     91         raise its.error.Error('Invalid format %s' % (cap["format"]))
     92 
     93 def convert_yuv420_to_rgb_image(y_plane, u_plane, v_plane,
     94                                 w, h,
     95                                 ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
     96                                 yuv_off=DEFAULT_YUV_OFFSETS):
     97     """Convert a YUV420 8-bit planar image to an RGB image.
     98 
     99     Args:
    100         y_plane: The packed 8-bit Y plane.
    101         u_plane: The packed 8-bit U plane.
    102         v_plane: The packed 8-bit V plane.
    103         w: The width of the image.
    104         h: The height of the image.
    105         ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
    106         yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
    107 
    108     Returns:
    109         RGB float-3 image array, with pixel values in [0.0, 1.0].
    110     """
    111     y = numpy.subtract(y_plane, yuv_off[0])
    112     u = numpy.subtract(u_plane, yuv_off[1]).view(numpy.int8)
    113     v = numpy.subtract(v_plane, yuv_off[2]).view(numpy.int8)
    114     u = u.reshape(h/2, w/2).repeat(2, axis=1).repeat(2, axis=0)
    115     v = v.reshape(h/2, w/2).repeat(2, axis=1).repeat(2, axis=0)
    116     yuv = numpy.dstack([y, u.reshape(w*h), v.reshape(w*h)])
    117     flt = numpy.empty([h, w, 3], dtype=numpy.float32)
    118     flt.reshape(w*h*3)[:] = yuv.reshape(h*w*3)[:]
    119     flt = numpy.dot(flt.reshape(w*h,3), ccm_yuv_to_rgb.T).clip(0, 255)
    120     rgb = numpy.empty([h, w, 3], dtype=numpy.uint8)
    121     rgb.reshape(w*h*3)[:] = flt.reshape(w*h*3)[:]
    122     return rgb.astype(numpy.float32) / 255.0
    123 
    124 def load_yuv420_to_rgb_image(yuv_fname,
    125                              w, h,
    126                              ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
    127                              yuv_off=DEFAULT_YUV_OFFSETS):
    128     """Load a YUV420 image file, and return as an RGB image.
    129 
    130     Args:
    131         yuv_fname: The path of the YUV420 file.
    132         w: The width of the image.
    133         h: The height of the image.
    134         ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
    135         yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
    136 
    137     Returns:
    138         RGB float-3 image array, with pixel values in [0.0, 1.0].
    139     """
    140     with open(yuv_fname, "rb") as f:
    141         y = numpy.fromfile(f, numpy.uint8, w*h, "")
    142         v = numpy.fromfile(f, numpy.uint8, w*h/4, "")
    143         u = numpy.fromfile(f, numpy.uint8, w*h/4, "")
    144         return convert_yuv420_to_rgb_image(y,u,v,w,h,ccm_yuv_to_rgb,yuv_off)
    145 
    146 def load_yuv420_to_yuv_planes(yuv_fname, w, h):
    147     """Load a YUV420 image file, and return separate Y, U, and V plane images.
    148 
    149     Args:
    150         yuv_fname: The path of the YUV420 file.
    151         w: The width of the image.
    152         h: The height of the image.
    153 
    154     Returns:
    155         Separate Y, U, and V images as float-1 Numpy arrays, pixels in [0,1].
    156         Note that pixel (0,0,0) is not black, since U,V pixels are centered at
    157         0.5, and also that the Y and U,V plane images returned are different
    158         sizes (due to chroma subsampling in the YUV420 format).
    159     """
    160     with open(yuv_fname, "rb") as f:
    161         y = numpy.fromfile(f, numpy.uint8, w*h, "")
    162         v = numpy.fromfile(f, numpy.uint8, w*h/4, "")
    163         u = numpy.fromfile(f, numpy.uint8, w*h/4, "")
    164         return ((y.astype(numpy.float32) / 255.0).reshape(h, w, 1),
    165                 (u.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1),
    166                 (v.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1))
    167 
    168 def decompress_jpeg_to_rgb_image(jpeg_buffer):
    169     """Decompress a JPEG-compressed image, returning as an RGB image.
    170 
    171     Args:
    172         jpeg_buffer: The JPEG stream.
    173 
    174     Returns:
    175         A numpy array for the RGB image, with pixels in [0,1].
    176     """
    177     img = Image.open(cStringIO.StringIO(jpeg_buffer))
    178     w = img.size[0]
    179     h = img.size[1]
    180     return numpy.array(img).reshape(h,w,3) / 255.0
    181 
    182 def apply_lut_to_image(img, lut):
    183     """Applies a LUT to every pixel in a float image array.
    184 
    185     Internally converts to a 16b integer image, since the LUT can work with up
    186     to 16b->16b mappings (i.e. values in the range [0,65535]). The lut can also
    187     have fewer than 65536 entries, however it must be sized as a power of 2
    188     (and for smaller luts, the scale must match the bitdepth).
    189 
    190     For a 16b lut of 65536 entries, the operation performed is:
    191 
    192         lut[r * 65535] / 65535 -> r'
    193         lut[g * 65535] / 65535 -> g'
    194         lut[b * 65535] / 65535 -> b'
    195 
    196     For a 10b lut of 1024 entries, the operation becomes:
    197 
    198         lut[r * 1023] / 1023 -> r'
    199         lut[g * 1023] / 1023 -> g'
    200         lut[b * 1023] / 1023 -> b'
    201 
    202     Args:
    203         img: Numpy float image array, with pixel values in [0,1].
    204         lut: Numpy table encoding a LUT, mapping 16b integer values.
    205 
    206     Returns:
    207         Float image array after applying LUT to each pixel.
    208     """
    209     n = len(lut)
    210     if n <= 0 or n > MAX_LUT_SIZE or (n & (n - 1)) != 0:
    211         raise its.error.Error('Invalid arg LUT size: %d' % (n))
    212     m = float(n-1)
    213     return (lut[(img * m).astype(numpy.uint16)] / m).astype(numpy.float32)
    214 
    215 def apply_matrix_to_image(img, mat):
    216     """Multiplies a 3x3 matrix with each float-3 image pixel.
    217 
    218     Each pixel is considered a column vector, and is left-multiplied by
    219     the given matrix:
    220 
    221         [     ]   r    r'
    222         [ mat ] * g -> g'
    223         [     ]   b    b'
    224 
    225     Args:
    226         img: Numpy float image array, with pixel values in [0,1].
    227         mat: Numpy 3x3 matrix.
    228 
    229     Returns:
    230         The numpy float-3 image array resulting from the matrix mult.
    231     """
    232     h = img.shape[0]
    233     w = img.shape[1]
    234     img2 = numpy.empty([h, w, 3], dtype=numpy.float32)
    235     img2.reshape(w*h*3)[:] = (numpy.dot(img.reshape(h*w, 3), mat.T)
    236                              ).reshape(w*h*3)[:]
    237     return img2
    238 
    239 def get_image_patch(img, xnorm, ynorm, wnorm, hnorm):
    240     """Get a patch (tile) of an image.
    241 
    242     Args:
    243         img: Numpy float image array, with pixel values in [0,1].
    244         xnorm,ynorm,wnorm,hnorm: Normalized (in [0,1]) coords for the tile.
    245 
    246     Returns:
    247         Float image array of the patch.
    248     """
    249     hfull = img.shape[0]
    250     wfull = img.shape[1]
    251     xtile = math.ceil(xnorm * wfull)
    252     ytile = math.ceil(ynorm * hfull)
    253     wtile = math.floor(wnorm * wfull)
    254     htile = math.floor(hnorm * hfull)
    255     return img[ytile:ytile+htile,xtile:xtile+wtile,:].copy()
    256 
    257 def compute_image_means(img):
    258     """Calculate the mean of each color channel in the image.
    259 
    260     Args:
    261         img: Numpy float image array, with pixel values in [0,1].
    262 
    263     Returns:
    264         A list of mean values, one per color channel in the image.
    265     """
    266     means = []
    267     chans = img.shape[2]
    268     for i in xrange(chans):
    269         means.append(numpy.mean(img[:,:,i], dtype=numpy.float64))
    270     return means
    271 
    272 def compute_image_variances(img):
    273     """Calculate the variance of each color channel in the image.
    274 
    275     Args:
    276         img: Numpy float image array, with pixel values in [0,1].
    277 
    278     Returns:
    279         A list of mean values, one per color channel in the image.
    280     """
    281     variances = []
    282     chans = img.shape[2]
    283     for i in xrange(chans):
    284         variances.append(numpy.var(img[:,:,i], dtype=numpy.float64))
    285     return variances
    286 
    287 def write_image(img, fname, apply_gamma=False):
    288     """Save a float-3 numpy array image to a file.
    289 
    290     Supported formats: PNG, JPEG, and others; see PIL docs for more.
    291 
    292     Image can be 3-channel, which is interpreted as RGB, or can be 1-channel,
    293     which is greyscale.
    294 
    295     Can optionally specify that the image should be gamma-encoded prior to
    296     writing it out; this should be done if the image contains linear pixel
    297     values, to make the image look "normal".
    298 
    299     Args:
    300         img: Numpy image array data.
    301         fname: Path of file to save to; the extension specifies the format.
    302         apply_gamma: (Optional) apply gamma to the image prior to writing it.
    303     """
    304     if apply_gamma:
    305         img = apply_lut_to_image(img, DEFAULT_GAMMA_LUT)
    306     (h, w, chans) = img.shape
    307     if chans == 3:
    308         Image.fromarray((img * 255.0).astype(numpy.uint8), "RGB").save(fname)
    309     elif chans == 1:
    310         img3 = (img * 255.0).astype(numpy.uint8).repeat(3).reshape(h,w,3)
    311         Image.fromarray(img3, "RGB").save(fname)
    312     else:
    313         raise its.error.Error('Unsupported image type')
    314 
    315 class __UnitTest(unittest.TestCase):
    316     """Run a suite of unit tests on this module.
    317     """
    318 
    319     # TODO: Add more unit tests.
    320 
    321     def test_apply_matrix_to_image(self):
    322         """Unit test for apply_matrix_to_image.
    323 
    324         Test by using a canned set of values on a 1x1 pixel image.
    325 
    326             [ 1 2 3 ]   [ 0.1 ]   [ 1.4 ]
    327             [ 4 5 6 ] * [ 0.2 ] = [ 3.2 ]
    328             [ 7 8 9 ]   [ 0.3 ]   [ 5.0 ]
    329                mat         x         y
    330         """
    331         mat = numpy.array([[1,2,3],[4,5,6],[7,8,9]])
    332         x = numpy.array([0.1,0.2,0.3]).reshape(1,1,3)
    333         y = apply_matrix_to_image(x, mat).reshape(3).tolist()
    334         y_ref = [1.4,3.2,5.0]
    335         passed = all([math.fabs(y[i] - y_ref[i]) < 0.001 for i in xrange(3)])
    336         self.assertTrue(passed)
    337 
    338     def test_apply_lut_to_image(self):
    339         """ Unit test for apply_lut_to_image.
    340 
    341         Test by using a canned set of values on a 1x1 pixel image. The LUT will
    342         simply double the value of the index:
    343 
    344             lut[x] = 2*x
    345         """
    346         lut = numpy.array([2*i for i in xrange(65536)])
    347         x = numpy.array([0.1,0.2,0.3]).reshape(1,1,3)
    348         y = apply_lut_to_image(x, lut).reshape(3).tolist()
    349         y_ref = [0.2,0.4,0.6]
    350         passed = all([math.fabs(y[i] - y_ref[i]) < 0.001 for i in xrange(3)])
    351         self.assertTrue(passed)
    352 
    353 if __name__ == '__main__':
    354     unittest.main()
    355 
    356