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