blob: c1bc0e2c453dfa1e5a7d204be93c5b7c886a3ca1 [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
Ruben Brunk370e2432014-10-14 18:33:23 -070019import sys
Yin-Chia Yehaccf5662016-10-06 17:20:57 -070020from PIL import Image
Ruben Brunk370e2432014-10-14 18:33:23 -070021import numpy
22import math
23import unittest
24import cStringIO
Ruben Brunk370e2432014-10-14 18:33:23 -070025import copy
Clemenz Portmann9b7f3552017-09-21 11:13:02 -070026import random
Ruben Brunk370e2432014-10-14 18:33:23 -070027
28DEFAULT_YUV_TO_RGB_CCM = numpy.matrix([
29 [1.000, 0.000, 1.402],
30 [1.000, -0.344, -0.714],
31 [1.000, 1.772, 0.000]])
32
33DEFAULT_YUV_OFFSETS = numpy.array([0, 128, 128])
34
35DEFAULT_GAMMA_LUT = numpy.array(
36 [math.floor(65535 * math.pow(i/65535.0, 1/2.2) + 0.5)
37 for i in xrange(65536)])
38
39DEFAULT_INVGAMMA_LUT = numpy.array(
40 [math.floor(65535 * math.pow(i/65535.0, 2.2) + 0.5)
41 for i in xrange(65536)])
42
43MAX_LUT_SIZE = 65536
44
Clemenz Portmann812236f2016-07-19 17:51:44 -070045NUM_TRYS = 2
46NUM_FRAMES = 4
47
48
Ruben Brunk370e2432014-10-14 18:33:23 -070049def convert_capture_to_rgb_image(cap,
50 ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
51 yuv_off=DEFAULT_YUV_OFFSETS,
52 props=None):
53 """Convert a captured image object to a RGB image.
54
55 Args:
56 cap: A capture object as returned by its.device.do_capture.
57 ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
58 yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
59 props: (Optional) camera properties object (of static values);
60 required for processing raw images.
61
62 Returns:
63 RGB float-3 image array, with pixel values in [0.0, 1.0].
64 """
65 w = cap["width"]
66 h = cap["height"]
67 if cap["format"] == "raw10":
68 assert(props is not None)
69 cap = unpack_raw10_capture(cap, props)
Yin-Chia Yeh76dd1432015-04-27 16:42:03 -070070 if cap["format"] == "raw12":
71 assert(props is not None)
72 cap = unpack_raw12_capture(cap, props)
Ruben Brunk370e2432014-10-14 18:33:23 -070073 if cap["format"] == "yuv":
74 y = cap["data"][0:w*h]
75 u = cap["data"][w*h:w*h*5/4]
76 v = cap["data"][w*h*5/4:w*h*6/4]
Timothy Knighte1025902015-07-07 12:46:24 -070077 return convert_yuv420_planar_to_rgb_image(y, u, v, w, h)
Ruben Brunk370e2432014-10-14 18:33:23 -070078 elif cap["format"] == "jpeg":
79 return decompress_jpeg_to_rgb_image(cap["data"])
Timothy Knight600077e2017-02-01 14:05:05 -080080 elif cap["format"] == "raw" or cap["format"] == "rawStats":
Ruben Brunk370e2432014-10-14 18:33:23 -070081 assert(props is not None)
82 r,gr,gb,b = convert_capture_to_planes(cap, props)
83 return convert_raw_to_rgb_image(r,gr,gb,b, props, cap["metadata"])
84 else:
85 raise its.error.Error('Invalid format %s' % (cap["format"]))
86
Clemenz Portmann9b7f3552017-09-21 11:13:02 -070087
Timothy Knight67d8ec92015-08-31 13:14:46 -070088def unpack_rawstats_capture(cap):
89 """Unpack a rawStats capture to the mean and variance images.
90
91 Args:
92 cap: A capture object as returned by its.device.do_capture.
93
94 Returns:
95 Tuple (mean_image var_image) of float-4 images, with non-normalized
96 pixel values computed from the RAW16 images on the device
97 """
98 assert(cap["format"] == "rawStats")
99 w = cap["width"]
100 h = cap["height"]
101 img = numpy.ndarray(shape=(2*h*w*4,), dtype='<f', buffer=cap["data"])
102 analysis_image = img.reshape(2,h,w,4)
103 mean_image = analysis_image[0,:,:,:].reshape(h,w,4)
104 var_image = analysis_image[1,:,:,:].reshape(h,w,4)
105 return mean_image, var_image
106
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700107
Ruben Brunk370e2432014-10-14 18:33:23 -0700108def 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.
Chien-Yu Chen682faa22014-10-22 17:34:44 -0700113 props: Camera properties object.
Ruben Brunk370e2432014-10-14 18:33:23 -0700114
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
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700128
Ruben Brunk370e2432014-10-14 18:33:23 -0700129def unpack_raw10_image(img):
130 """Unpack a raw-10 image to a raw-16 image.
131
132 Output image will have the 10 LSBs filled in each 16b word, and the 6 MSBs
133 will be set to zero.
134
135 Args:
136 img: A raw-10 image, as a uint8 numpy array.
137
138 Returns:
139 Image as a uint16 numpy array, with all row padding stripped.
140 """
141 if img.shape[1] % 5 != 0:
142 raise its.error.Error('Invalid raw-10 buffer width')
143 w = img.shape[1]*4/5
144 h = img.shape[0]
Yin-Chia Yeh76dd1432015-04-27 16:42:03 -0700145 # Cut out the 4x8b MSBs and shift to bits [9:2] in 16b words.
Ruben Brunk370e2432014-10-14 18:33:23 -0700146 msbs = numpy.delete(img, numpy.s_[4::5], 1)
147 msbs = msbs.astype(numpy.uint16)
148 msbs = numpy.left_shift(msbs, 2)
149 msbs = msbs.reshape(h,w)
Yin-Chia Yeh76dd1432015-04-27 16:42:03 -0700150 # Cut out the 4x2b LSBs and put each in bits [1:0] of their own 8b words.
Ruben Brunk370e2432014-10-14 18:33:23 -0700151 lsbs = img[::, 4::5].reshape(h,w/4)
152 lsbs = numpy.right_shift(
153 numpy.packbits(numpy.unpackbits(lsbs).reshape(h,w/4,4,2),3), 6)
Yin-Chia Yehd8682ec2017-09-22 16:31:15 -0700154 # Pair the LSB bits group to pixel 0 instead of pixel 3
155 lsbs = lsbs.reshape(h,w/4,4)[:,:,::-1]
Ruben Brunk370e2432014-10-14 18:33:23 -0700156 lsbs = lsbs.reshape(h,w)
157 # Fuse the MSBs and LSBs back together
158 img16 = numpy.bitwise_or(msbs, lsbs).reshape(h,w)
159 return img16
160
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700161
Yin-Chia Yeh76dd1432015-04-27 16:42:03 -0700162def unpack_raw12_capture(cap, props):
163 """Unpack a raw-12 capture to a raw-16 capture.
164
165 Args:
166 cap: A raw-12 capture object.
167 props: Camera properties object.
168
169 Returns:
170 New capture object with raw-16 data.
171 """
172 # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding
173 # the MSBs of the pixels, and the 5th byte holding 4x2b LSBs.
174 w,h = cap["width"], cap["height"]
175 if w % 2 != 0:
176 raise its.error.Error('Invalid raw-12 buffer width')
177 cap = copy.deepcopy(cap)
178 cap["data"] = unpack_raw12_image(cap["data"].reshape(h,w*3/2))
179 cap["format"] = "raw"
180 return cap
181
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700182
Yin-Chia Yeh76dd1432015-04-27 16:42:03 -0700183def unpack_raw12_image(img):
184 """Unpack a raw-12 image to a raw-16 image.
185
186 Output image will have the 12 LSBs filled in each 16b word, and the 4 MSBs
187 will be set to zero.
188
189 Args:
190 img: A raw-12 image, as a uint8 numpy array.
191
192 Returns:
193 Image as a uint16 numpy array, with all row padding stripped.
194 """
195 if img.shape[1] % 3 != 0:
196 raise its.error.Error('Invalid raw-12 buffer width')
197 w = img.shape[1]*2/3
198 h = img.shape[0]
199 # Cut out the 2x8b MSBs and shift to bits [11:4] in 16b words.
200 msbs = numpy.delete(img, numpy.s_[2::3], 1)
201 msbs = msbs.astype(numpy.uint16)
202 msbs = numpy.left_shift(msbs, 4)
203 msbs = msbs.reshape(h,w)
204 # Cut out the 2x4b LSBs and put each in bits [3:0] of their own 8b words.
205 lsbs = img[::, 2::3].reshape(h,w/2)
206 lsbs = numpy.right_shift(
207 numpy.packbits(numpy.unpackbits(lsbs).reshape(h,w/2,2,4),3), 4)
Yin-Chia Yehd8682ec2017-09-22 16:31:15 -0700208 # Pair the LSB bits group to pixel 0 instead of pixel 1
209 lsbs = lsbs.reshape(h,w/2,2)[:,:,::-1]
Yin-Chia Yeh76dd1432015-04-27 16:42:03 -0700210 lsbs = lsbs.reshape(h,w)
211 # Fuse the MSBs and LSBs back together
212 img16 = numpy.bitwise_or(msbs, lsbs).reshape(h,w)
213 return img16
214
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700215
Ruben Brunk370e2432014-10-14 18:33:23 -0700216def convert_capture_to_planes(cap, props=None):
217 """Convert a captured image object to separate image planes.
218
219 Decompose an image into multiple images, corresponding to different planes.
220
221 For YUV420 captures ("yuv"):
222 Returns Y,U,V planes, where the Y plane is full-res and the U,V planes
223 are each 1/2 x 1/2 of the full res.
224
Timothy Knight600077e2017-02-01 14:05:05 -0800225 For Bayer captures ("raw", "raw10", "raw12", or "rawStats"):
Ruben Brunk370e2432014-10-14 18:33:23 -0700226 Returns planes in the order R,Gr,Gb,B, regardless of the Bayer pattern
Timothy Knight600077e2017-02-01 14:05:05 -0800227 layout. For full-res raw images ("raw", "raw10", "raw12"), each plane
228 is 1/2 x 1/2 of the full res. For "rawStats" images, the mean image
229 is returned.
Ruben Brunk370e2432014-10-14 18:33:23 -0700230
231 For JPEG captures ("jpeg"):
232 Returns R,G,B full-res planes.
233
234 Args:
235 cap: A capture object as returned by its.device.do_capture.
236 props: (Optional) camera properties object (of static values);
237 required for processing raw images.
238
239 Returns:
240 A tuple of float numpy arrays (one per plane), consisting of pixel
241 values in the range [0.0, 1.0].
242 """
243 w = cap["width"]
244 h = cap["height"]
245 if cap["format"] == "raw10":
246 assert(props is not None)
247 cap = unpack_raw10_capture(cap, props)
Timothy Knightac702422015-07-01 21:33:34 -0700248 if cap["format"] == "raw12":
249 assert(props is not None)
250 cap = unpack_raw12_capture(cap, props)
Ruben Brunk370e2432014-10-14 18:33:23 -0700251 if cap["format"] == "yuv":
252 y = cap["data"][0:w*h]
253 u = cap["data"][w*h:w*h*5/4]
254 v = cap["data"][w*h*5/4:w*h*6/4]
255 return ((y.astype(numpy.float32) / 255.0).reshape(h, w, 1),
256 (u.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1),
257 (v.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1))
258 elif cap["format"] == "jpeg":
259 rgb = decompress_jpeg_to_rgb_image(cap["data"]).reshape(w*h*3)
260 return (rgb[::3].reshape(h,w,1),
261 rgb[1::3].reshape(h,w,1),
262 rgb[2::3].reshape(h,w,1))
263 elif cap["format"] == "raw":
264 assert(props is not None)
265 white_level = float(props['android.sensor.info.whiteLevel'])
266 img = numpy.ndarray(shape=(h*w,), dtype='<u2',
267 buffer=cap["data"][0:w*h*2])
268 img = img.astype(numpy.float32).reshape(h,w) / white_level
Timothy Knightac702422015-07-01 21:33:34 -0700269 # Crop the raw image to the active array region.
270 if props.has_key("android.sensor.info.activeArraySize") \
271 and props["android.sensor.info.activeArraySize"] is not None \
272 and props.has_key("android.sensor.info.pixelArraySize") \
273 and props["android.sensor.info.pixelArraySize"] is not None:
274 # Note that the Rect class is defined such that the left,top values
275 # are "inside" while the right,bottom values are "outside"; that is,
276 # it's inclusive of the top,left sides only. So, the width is
277 # computed as right-left, rather than right-left+1, etc.
278 wfull = props["android.sensor.info.pixelArraySize"]["width"]
279 hfull = props["android.sensor.info.pixelArraySize"]["height"]
280 xcrop = props["android.sensor.info.activeArraySize"]["left"]
281 ycrop = props["android.sensor.info.activeArraySize"]["top"]
282 wcrop = props["android.sensor.info.activeArraySize"]["right"]-xcrop
283 hcrop = props["android.sensor.info.activeArraySize"]["bottom"]-ycrop
284 assert(wfull >= wcrop >= 0)
285 assert(hfull >= hcrop >= 0)
286 assert(wfull - wcrop >= xcrop >= 0)
287 assert(hfull - hcrop >= ycrop >= 0)
288 if w == wfull and h == hfull:
289 # Crop needed; extract the center region.
290 img = img[ycrop:ycrop+hcrop,xcrop:xcrop+wcrop]
291 w = wcrop
292 h = hcrop
293 elif w == wcrop and h == hcrop:
294 # No crop needed; image is already cropped to the active array.
295 None
296 else:
297 raise its.error.Error('Invalid image size metadata')
298 # Separate the image planes.
Ruben Brunk370e2432014-10-14 18:33:23 -0700299 imgs = [img[::2].reshape(w*h/2)[::2].reshape(h/2,w/2,1),
300 img[::2].reshape(w*h/2)[1::2].reshape(h/2,w/2,1),
301 img[1::2].reshape(w*h/2)[::2].reshape(h/2,w/2,1),
302 img[1::2].reshape(w*h/2)[1::2].reshape(h/2,w/2,1)]
303 idxs = get_canonical_cfa_order(props)
304 return [imgs[i] for i in idxs]
Timothy Knight600077e2017-02-01 14:05:05 -0800305 elif cap["format"] == "rawStats":
306 assert(props is not None)
307 white_level = float(props['android.sensor.info.whiteLevel'])
308 mean_image, var_image = its.image.unpack_rawstats_capture(cap)
309 idxs = get_canonical_cfa_order(props)
310 return [mean_image[:,:,i] / white_level for i in idxs]
Ruben Brunk370e2432014-10-14 18:33:23 -0700311 else:
312 raise its.error.Error('Invalid format %s' % (cap["format"]))
313
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700314
Ruben Brunk370e2432014-10-14 18:33:23 -0700315def get_canonical_cfa_order(props):
316 """Returns a mapping from the Bayer 2x2 top-left grid in the CFA to
317 the standard order R,Gr,Gb,B.
318
319 Args:
320 props: Camera properties object.
321
322 Returns:
323 List of 4 integers, corresponding to the positions in the 2x2 top-
324 left Bayer grid of R,Gr,Gb,B, where the 2x2 grid is labeled as
325 0,1,2,3 in row major order.
326 """
327 # Note that raw streams aren't croppable, so the cropRegion doesn't need
328 # to be considered when determining the top-left pixel color.
329 cfa_pat = props['android.sensor.info.colorFilterArrangement']
330 if cfa_pat == 0:
331 # RGGB
332 return [0,1,2,3]
333 elif cfa_pat == 1:
334 # GRBG
335 return [1,0,3,2]
336 elif cfa_pat == 2:
337 # GBRG
338 return [2,3,0,1]
339 elif cfa_pat == 3:
340 # BGGR
341 return [3,2,1,0]
342 else:
343 raise its.error.Error("Not supported")
344
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700345
Ruben Brunk370e2432014-10-14 18:33:23 -0700346def get_gains_in_canonical_order(props, gains):
347 """Reorders the gains tuple to the canonical R,Gr,Gb,B order.
348
349 Args:
350 props: Camera properties object.
351 gains: List of 4 values, in R,G_even,G_odd,B order.
352
353 Returns:
354 List of gains values, in R,Gr,Gb,B order.
355 """
356 cfa_pat = props['android.sensor.info.colorFilterArrangement']
357 if cfa_pat in [0,1]:
358 # RGGB or GRBG, so G_even is Gr
359 return gains
360 elif cfa_pat in [2,3]:
361 # GBRG or BGGR, so G_even is Gb
362 return [gains[0], gains[2], gains[1], gains[3]]
363 else:
364 raise its.error.Error("Not supported")
365
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700366
Ruben Brunk370e2432014-10-14 18:33:23 -0700367def convert_raw_to_rgb_image(r_plane, gr_plane, gb_plane, b_plane,
368 props, cap_res):
369 """Convert a Bayer raw-16 image to an RGB image.
370
371 Includes some extremely rudimentary demosaicking and color processing
372 operations; the output of this function shouldn't be used for any image
373 quality analysis.
374
375 Args:
376 r_plane,gr_plane,gb_plane,b_plane: Numpy arrays for each color plane
377 in the Bayer image, with pixels in the [0.0, 1.0] range.
378 props: Camera properties object.
379 cap_res: Capture result (metadata) object.
380
381 Returns:
382 RGB float-3 image array, with pixel values in [0.0, 1.0]
383 """
384 # Values required for the RAW to RGB conversion.
385 assert(props is not None)
386 white_level = float(props['android.sensor.info.whiteLevel'])
387 black_levels = props['android.sensor.blackLevelPattern']
388 gains = cap_res['android.colorCorrection.gains']
389 ccm = cap_res['android.colorCorrection.transform']
390
391 # Reorder black levels and gains to R,Gr,Gb,B, to match the order
392 # of the planes.
Timothy Knightfa785872016-07-12 16:49:47 -0700393 black_levels = [get_black_level(i,props,cap_res) for i in range(4)]
Ruben Brunk370e2432014-10-14 18:33:23 -0700394 gains = get_gains_in_canonical_order(props, gains)
395
396 # Convert CCM from rational to float, as numpy arrays.
397 ccm = numpy.array(its.objects.rational_to_float(ccm)).reshape(3,3)
398
399 # Need to scale the image back to the full [0,1] range after subtracting
400 # the black level from each pixel.
401 scale = white_level / (white_level - max(black_levels))
402
403 # Three-channel black levels, normalized to [0,1] by white_level.
404 black_levels = numpy.array([b/white_level for b in [
405 black_levels[i] for i in [0,1,3]]])
406
407 # Three-channel gains.
408 gains = numpy.array([gains[i] for i in [0,1,3]])
409
410 h,w = r_plane.shape[:2]
411 img = numpy.dstack([r_plane,(gr_plane+gb_plane)/2.0,b_plane])
412 img = (((img.reshape(h,w,3) - black_levels) * scale) * gains).clip(0.0,1.0)
413 img = numpy.dot(img.reshape(w*h,3), ccm.T).reshape(h,w,3).clip(0.0,1.0)
414 return img
415
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700416
Timothy Knightfa785872016-07-12 16:49:47 -0700417def get_black_level(chan, props, cap_res):
418 """Return the black level to use for a given capture.
419
420 Uses a dynamic value from the capture result if available, else falls back
421 to the static global value in the camera characteristics.
422
423 Args:
424 chan: The channel index, in canonical order (R, Gr, Gb, B).
425 props: The camera properties object.
426 cap_res: A capture result object.
427
428 Returns:
429 The black level value for the specified channel.
430 """
Clemenz Portmanne4a09cf2016-10-18 12:57:46 -0700431 if (cap_res.has_key('android.sensor.dynamicBlackLevel') and
432 cap_res['android.sensor.dynamicBlackLevel'] is not None):
433 black_levels = cap_res['android.sensor.dynamicBlackLevel']
Timothy Knightfa785872016-07-12 16:49:47 -0700434 else:
435 black_levels = props['android.sensor.blackLevelPattern']
436 idxs = its.image.get_canonical_cfa_order(props)
437 ordered_black_levels = [black_levels[i] for i in idxs]
438 return ordered_black_levels[chan]
439
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700440
Timothy Knighte1025902015-07-07 12:46:24 -0700441def convert_yuv420_planar_to_rgb_image(y_plane, u_plane, v_plane,
442 w, h,
443 ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
444 yuv_off=DEFAULT_YUV_OFFSETS):
Ruben Brunk370e2432014-10-14 18:33:23 -0700445 """Convert a YUV420 8-bit planar image to an RGB image.
446
447 Args:
448 y_plane: The packed 8-bit Y plane.
449 u_plane: The packed 8-bit U plane.
450 v_plane: The packed 8-bit V plane.
451 w: The width of the image.
452 h: The height of the image.
453 ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
454 yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
455
456 Returns:
457 RGB float-3 image array, with pixel values in [0.0, 1.0].
458 """
459 y = numpy.subtract(y_plane, yuv_off[0])
460 u = numpy.subtract(u_plane, yuv_off[1]).view(numpy.int8)
461 v = numpy.subtract(v_plane, yuv_off[2]).view(numpy.int8)
462 u = u.reshape(h/2, w/2).repeat(2, axis=1).repeat(2, axis=0)
463 v = v.reshape(h/2, w/2).repeat(2, axis=1).repeat(2, axis=0)
464 yuv = numpy.dstack([y, u.reshape(w*h), v.reshape(w*h)])
465 flt = numpy.empty([h, w, 3], dtype=numpy.float32)
466 flt.reshape(w*h*3)[:] = yuv.reshape(h*w*3)[:]
467 flt = numpy.dot(flt.reshape(w*h,3), ccm_yuv_to_rgb.T).clip(0, 255)
468 rgb = numpy.empty([h, w, 3], dtype=numpy.uint8)
469 rgb.reshape(w*h*3)[:] = flt.reshape(w*h*3)[:]
470 return rgb.astype(numpy.float32) / 255.0
471
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700472
Timothy Knight36fba9c2015-06-22 14:46:38 -0700473def load_rgb_image(fname):
474 """Load a standard image file (JPG, PNG, etc.).
475
476 Args:
477 fname: The path of the file to load.
478
479 Returns:
480 RGB float-3 image array, with pixel values in [0.0, 1.0].
481 """
482 img = Image.open(fname)
483 w = img.size[0]
484 h = img.size[1]
485 a = numpy.array(img)
486 if len(a.shape) == 3 and a.shape[2] == 3:
487 # RGB
488 return a.reshape(h,w,3) / 255.0
489 elif len(a.shape) == 2 or len(a.shape) == 3 and a.shape[2] == 1:
490 # Greyscale; convert to RGB
491 return a.reshape(h*w).repeat(3).reshape(h,w,3) / 255.0
492 else:
493 raise its.error.Error('Unsupported image type')
494
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700495
Ruben Brunk370e2432014-10-14 18:33:23 -0700496def load_yuv420_to_rgb_image(yuv_fname,
497 w, h,
Timothy Knighte1025902015-07-07 12:46:24 -0700498 layout="planar",
Ruben Brunk370e2432014-10-14 18:33:23 -0700499 ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
500 yuv_off=DEFAULT_YUV_OFFSETS):
501 """Load a YUV420 image file, and return as an RGB image.
502
Timothy Knighte1025902015-07-07 12:46:24 -0700503 Supported layouts include "planar" and "nv21". The "yuv" formatted captures
504 returned from the device via do_capture are in the "planar" layout; other
505 layouts may only be needed for loading files from other sources.
506
Ruben Brunk370e2432014-10-14 18:33:23 -0700507 Args:
508 yuv_fname: The path of the YUV420 file.
509 w: The width of the image.
510 h: The height of the image.
Timothy Knighte1025902015-07-07 12:46:24 -0700511 layout: (Optional) the layout of the YUV data (as a string).
Ruben Brunk370e2432014-10-14 18:33:23 -0700512 ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
513 yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
514
515 Returns:
516 RGB float-3 image array, with pixel values in [0.0, 1.0].
517 """
518 with open(yuv_fname, "rb") as f:
Timothy Knighte1025902015-07-07 12:46:24 -0700519 if layout == "planar":
520 # Plane of Y, plane of V, plane of U.
521 y = numpy.fromfile(f, numpy.uint8, w*h, "")
522 v = numpy.fromfile(f, numpy.uint8, w*h/4, "")
523 u = numpy.fromfile(f, numpy.uint8, w*h/4, "")
524 elif layout == "nv21":
525 # Plane of Y, plane of interleaved VUVUVU...
526 y = numpy.fromfile(f, numpy.uint8, w*h, "")
527 vu = numpy.fromfile(f, numpy.uint8, w*h/2, "")
528 v = vu[0::2]
529 u = vu[1::2]
530 else:
531 raise its.error.Error('Unsupported image layout')
532 return convert_yuv420_planar_to_rgb_image(
533 y,u,v,w,h,ccm_yuv_to_rgb,yuv_off)
Ruben Brunk370e2432014-10-14 18:33:23 -0700534
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700535
Timothy Knighte1025902015-07-07 12:46:24 -0700536def load_yuv420_planar_to_yuv_planes(yuv_fname, w, h):
537 """Load a YUV420 planar image file, and return Y, U, and V plane images.
Ruben Brunk370e2432014-10-14 18:33:23 -0700538
539 Args:
540 yuv_fname: The path of the YUV420 file.
541 w: The width of the image.
542 h: The height of the image.
543
544 Returns:
545 Separate Y, U, and V images as float-1 Numpy arrays, pixels in [0,1].
546 Note that pixel (0,0,0) is not black, since U,V pixels are centered at
547 0.5, and also that the Y and U,V plane images returned are different
548 sizes (due to chroma subsampling in the YUV420 format).
549 """
550 with open(yuv_fname, "rb") as f:
551 y = numpy.fromfile(f, numpy.uint8, w*h, "")
552 v = numpy.fromfile(f, numpy.uint8, w*h/4, "")
553 u = numpy.fromfile(f, numpy.uint8, w*h/4, "")
554 return ((y.astype(numpy.float32) / 255.0).reshape(h, w, 1),
555 (u.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1),
556 (v.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1))
557
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700558
Ruben Brunk370e2432014-10-14 18:33:23 -0700559def decompress_jpeg_to_rgb_image(jpeg_buffer):
560 """Decompress a JPEG-compressed image, returning as an RGB image.
561
562 Args:
563 jpeg_buffer: The JPEG stream.
564
565 Returns:
566 A numpy array for the RGB image, with pixels in [0,1].
567 """
568 img = Image.open(cStringIO.StringIO(jpeg_buffer))
569 w = img.size[0]
570 h = img.size[1]
571 return numpy.array(img).reshape(h,w,3) / 255.0
572
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700573
Ruben Brunk370e2432014-10-14 18:33:23 -0700574def apply_lut_to_image(img, lut):
575 """Applies a LUT to every pixel in a float image array.
576
577 Internally converts to a 16b integer image, since the LUT can work with up
578 to 16b->16b mappings (i.e. values in the range [0,65535]). The lut can also
579 have fewer than 65536 entries, however it must be sized as a power of 2
580 (and for smaller luts, the scale must match the bitdepth).
581
582 For a 16b lut of 65536 entries, the operation performed is:
583
584 lut[r * 65535] / 65535 -> r'
585 lut[g * 65535] / 65535 -> g'
586 lut[b * 65535] / 65535 -> b'
587
588 For a 10b lut of 1024 entries, the operation becomes:
589
590 lut[r * 1023] / 1023 -> r'
591 lut[g * 1023] / 1023 -> g'
592 lut[b * 1023] / 1023 -> b'
593
594 Args:
595 img: Numpy float image array, with pixel values in [0,1].
596 lut: Numpy table encoding a LUT, mapping 16b integer values.
597
598 Returns:
599 Float image array after applying LUT to each pixel.
600 """
601 n = len(lut)
602 if n <= 0 or n > MAX_LUT_SIZE or (n & (n - 1)) != 0:
603 raise its.error.Error('Invalid arg LUT size: %d' % (n))
604 m = float(n-1)
605 return (lut[(img * m).astype(numpy.uint16)] / m).astype(numpy.float32)
606
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700607
Ruben Brunk370e2432014-10-14 18:33:23 -0700608def apply_matrix_to_image(img, mat):
609 """Multiplies a 3x3 matrix with each float-3 image pixel.
610
611 Each pixel is considered a column vector, and is left-multiplied by
612 the given matrix:
613
614 [ ] r r'
615 [ mat ] * g -> g'
616 [ ] b b'
617
618 Args:
619 img: Numpy float image array, with pixel values in [0,1].
620 mat: Numpy 3x3 matrix.
621
622 Returns:
623 The numpy float-3 image array resulting from the matrix mult.
624 """
625 h = img.shape[0]
626 w = img.shape[1]
627 img2 = numpy.empty([h, w, 3], dtype=numpy.float32)
628 img2.reshape(w*h*3)[:] = (numpy.dot(img.reshape(h*w, 3), mat.T)
629 ).reshape(w*h*3)[:]
630 return img2
631
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700632
Ruben Brunk370e2432014-10-14 18:33:23 -0700633def get_image_patch(img, xnorm, ynorm, wnorm, hnorm):
634 """Get a patch (tile) of an image.
635
636 Args:
637 img: Numpy float image array, with pixel values in [0,1].
638 xnorm,ynorm,wnorm,hnorm: Normalized (in [0,1]) coords for the tile.
639
640 Returns:
641 Float image array of the patch.
642 """
643 hfull = img.shape[0]
644 wfull = img.shape[1]
Clemenz Portmannc89ea052018-01-30 14:02:11 -0800645 xtile = int(math.ceil(xnorm * wfull))
646 ytile = int(math.ceil(ynorm * hfull))
647 wtile = int(math.floor(wnorm * wfull))
648 htile = int(math.floor(hnorm * hfull))
Ruben Brunk370e2432014-10-14 18:33:23 -0700649 return img[ytile:ytile+htile,xtile:xtile+wtile,:].copy()
650
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700651
Ruben Brunk370e2432014-10-14 18:33:23 -0700652def compute_image_means(img):
653 """Calculate the mean of each color channel in the image.
654
655 Args:
656 img: Numpy float image array, with pixel values in [0,1].
657
658 Returns:
659 A list of mean values, one per color channel in the image.
660 """
661 means = []
662 chans = img.shape[2]
663 for i in xrange(chans):
664 means.append(numpy.mean(img[:,:,i], dtype=numpy.float64))
665 return means
666
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700667
Ruben Brunk370e2432014-10-14 18:33:23 -0700668def compute_image_variances(img):
669 """Calculate the variance of each color channel in the image.
670
671 Args:
672 img: Numpy float image array, with pixel values in [0,1].
673
674 Returns:
675 A list of mean values, one per color channel in the image.
676 """
677 variances = []
678 chans = img.shape[2]
679 for i in xrange(chans):
680 variances.append(numpy.var(img[:,:,i], dtype=numpy.float64))
681 return variances
682
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700683
Yin-Chia Yeh619f2eb2015-09-17 17:13:09 -0700684def compute_image_snrs(img):
685 """Calculate the SNR (db) of each color channel in the image.
686
687 Args:
688 img: Numpy float image array, with pixel values in [0,1].
689
690 Returns:
691 A list of SNR value, one per color channel in the image.
692 """
693 means = compute_image_means(img)
694 variances = compute_image_variances(img)
695 std_devs = [math.sqrt(v) for v in variances]
696 snr = [20 * math.log10(m/s) for m,s in zip(means, std_devs)]
697 return snr
698
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700699
Ruben Brunk370e2432014-10-14 18:33:23 -0700700def write_image(img, fname, apply_gamma=False):
701 """Save a float-3 numpy array image to a file.
702
703 Supported formats: PNG, JPEG, and others; see PIL docs for more.
704
705 Image can be 3-channel, which is interpreted as RGB, or can be 1-channel,
706 which is greyscale.
707
708 Can optionally specify that the image should be gamma-encoded prior to
709 writing it out; this should be done if the image contains linear pixel
710 values, to make the image look "normal".
711
712 Args:
713 img: Numpy image array data.
714 fname: Path of file to save to; the extension specifies the format.
715 apply_gamma: (Optional) apply gamma to the image prior to writing it.
716 """
717 if apply_gamma:
718 img = apply_lut_to_image(img, DEFAULT_GAMMA_LUT)
719 (h, w, chans) = img.shape
720 if chans == 3:
721 Image.fromarray((img * 255.0).astype(numpy.uint8), "RGB").save(fname)
722 elif chans == 1:
723 img3 = (img * 255.0).astype(numpy.uint8).repeat(3).reshape(h,w,3)
724 Image.fromarray(img3, "RGB").save(fname)
725 else:
726 raise its.error.Error('Unsupported image type')
727
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700728
Ruben Brunk370e2432014-10-14 18:33:23 -0700729def downscale_image(img, f):
730 """Shrink an image by a given integer factor.
731
732 This function computes output pixel values by averaging over rectangular
733 regions of the input image; it doesn't skip or sample pixels, and all input
734 image pixels are evenly weighted.
735
736 If the downscaling factor doesn't cleanly divide the width and/or height,
737 then the remaining pixels on the right or bottom edge are discarded prior
738 to the downscaling.
739
740 Args:
741 img: The input image as an ndarray.
742 f: The downscaling factor, which should be an integer.
743
744 Returns:
745 The new (downscaled) image, as an ndarray.
746 """
747 h,w,chans = img.shape
748 f = int(f)
749 assert(f >= 1)
750 h = (h/f)*f
751 w = (w/f)*f
752 img = img[0:h:,0:w:,::]
753 chs = []
754 for i in xrange(chans):
755 ch = img.reshape(h*w*chans)[i::chans].reshape(h,w)
756 ch = ch.reshape(h,w/f,f).mean(2).reshape(h,w/f)
757 ch = ch.T.reshape(w/f,h/f,f).mean(2).T.reshape(h/f,w/f)
758 chs.append(ch.reshape(h*w/(f*f)))
759 img = numpy.vstack(chs).T.reshape(h/f,w/f,chans)
760 return img
761
Clemenz Portmann9beecac2016-06-07 19:56:13 -0700762
Chien-Yu Chen32678602015-06-25 15:10:52 -0700763def compute_image_sharpness(img):
764 """Calculate the sharpness of input image.
765
766 Args:
767 img: Numpy float RGB/luma image array, with pixel values in [0,1].
768
769 Returns:
770 A sharpness estimation value based on the average of gradient magnitude.
771 Larger value means the image is sharper.
772 """
773 chans = img.shape[2]
774 assert(chans == 1 or chans == 3)
Clemenz Portmann9beecac2016-06-07 19:56:13 -0700775 if (chans == 1):
776 luma = img[:, :, 0]
777 elif (chans == 3):
Chien-Yu Chen32678602015-06-25 15:10:52 -0700778 luma = 0.299 * img[:,:,0] + 0.587 * img[:,:,1] + 0.114 * img[:,:,2]
779
780 [gy, gx] = numpy.gradient(luma)
781 return numpy.average(numpy.sqrt(gy*gy + gx*gx))
782
Clemenz Portmann812236f2016-07-19 17:51:44 -0700783def normalize_img(img):
784 """Normalize the image values to between 0 and 1.
785
786 Args:
787 img: 2-D numpy array of image values
788 Returns:
789 Normalized image
790 """
791 return (img - numpy.amin(img))/(numpy.amax(img) - numpy.amin(img))
792
Yin-Chia Yehf3506572016-10-10 15:46:46 -0700793def flip_mirror_img_per_argv(img):
794 """Flip/mirror an image if "flip" or "mirror" is in argv
795
796 Args:
797 img: 2-D numpy array of image values
798 Returns:
799 Flip/mirrored image
800 """
801 img_out = img
802 if "flip" in sys.argv:
Clemenz Portmanne2ef6232017-05-04 09:54:56 -0700803 img_out = numpy.flipud(img_out)
Yin-Chia Yehf3506572016-10-10 15:46:46 -0700804 if "mirror" in sys.argv:
Clemenz Portmanne2ef6232017-05-04 09:54:56 -0700805 img_out = numpy.fliplr(img_out)
Yin-Chia Yehf3506572016-10-10 15:46:46 -0700806 return img_out
807
Clemenz Portmann812236f2016-07-19 17:51:44 -0700808def stationary_lens_cap(cam, req, fmt):
809 """Take up to NUM_TRYS caps and save the 1st one with lens stationary.
810
811 Args:
812 cam: open device session
813 req: capture request
814 fmt: format for capture
815
816 Returns:
817 capture
818 """
819 trys = 0
820 done = False
821 reqs = [req] * NUM_FRAMES
822 while not done:
823 print 'Waiting for lens to move to correct location...'
824 cap = cam.do_capture(reqs, fmt)
825 done = (cap[NUM_FRAMES-1]['metadata']['android.lens.state'] == 0)
826 print ' status: ', done
827 trys += 1
828 if trys == NUM_TRYS:
829 raise its.error.Error('Cannot settle lens after %d trys!' % trys)
830 return cap[NUM_FRAMES-1]
831
Ruben Brunk370e2432014-10-14 18:33:23 -0700832class __UnitTest(unittest.TestCase):
833 """Run a suite of unit tests on this module.
834 """
835
836 # TODO: Add more unit tests.
837
838 def test_apply_matrix_to_image(self):
839 """Unit test for apply_matrix_to_image.
840
841 Test by using a canned set of values on a 1x1 pixel image.
842
843 [ 1 2 3 ] [ 0.1 ] [ 1.4 ]
844 [ 4 5 6 ] * [ 0.2 ] = [ 3.2 ]
845 [ 7 8 9 ] [ 0.3 ] [ 5.0 ]
846 mat x y
847 """
Clemenz Portmann9beecac2016-06-07 19:56:13 -0700848 mat = numpy.array([[1,2,3], [4,5,6], [7,8,9]])
Ruben Brunk370e2432014-10-14 18:33:23 -0700849 x = numpy.array([0.1,0.2,0.3]).reshape(1,1,3)
850 y = apply_matrix_to_image(x, mat).reshape(3).tolist()
851 y_ref = [1.4,3.2,5.0]
852 passed = all([math.fabs(y[i] - y_ref[i]) < 0.001 for i in xrange(3)])
853 self.assertTrue(passed)
854
855 def test_apply_lut_to_image(self):
Clemenz Portmann9beecac2016-06-07 19:56:13 -0700856 """Unit test for apply_lut_to_image.
Ruben Brunk370e2432014-10-14 18:33:23 -0700857
858 Test by using a canned set of values on a 1x1 pixel image. The LUT will
859 simply double the value of the index:
860
861 lut[x] = 2*x
862 """
863 lut = numpy.array([2*i for i in xrange(65536)])
864 x = numpy.array([0.1,0.2,0.3]).reshape(1,1,3)
865 y = apply_lut_to_image(x, lut).reshape(3).tolist()
866 y_ref = [0.2,0.4,0.6]
867 passed = all([math.fabs(y[i] - y_ref[i]) < 0.001 for i in xrange(3)])
868 self.assertTrue(passed)
869
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700870 def test_unpack_raw10_image(self):
871 """Unit test for unpack_raw10_image.
872
873 RAW10 bit packing format
874 bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0
875 Byte 0: P0[9] P0[8] P0[7] P0[6] P0[5] P0[4] P0[3] P0[2]
876 Byte 1: P1[9] P1[8] P1[7] P1[6] P1[5] P1[4] P1[3] P1[2]
877 Byte 2: P2[9] P2[8] P2[7] P2[6] P2[5] P2[4] P2[3] P2[2]
878 Byte 3: P3[9] P3[8] P3[7] P3[6] P3[5] P3[4] P3[3] P3[2]
879 Byte 4: P3[1] P3[0] P2[1] P2[0] P1[1] P1[0] P0[1] P0[0]
880 """
881 # test by using a random 4x4 10-bit image
882 H = 4
883 W = 4
884 check_list = random.sample(range(0, 1024), H*W)
885 img_check = numpy.array(check_list).reshape(H, W)
886 # pack bits
887 for row_start in range(0, len(check_list), W):
888 msbs = []
889 lsbs = ""
890 for pixel in range(W):
891 val = numpy.binary_repr(check_list[row_start+pixel], 10)
892 msbs.append(int(val[:8], base=2))
893 lsbs = val[8:] + lsbs
894 packed = msbs
895 packed.append(int(lsbs, base=2))
896 chunk_raw10 = numpy.array(packed, dtype="uint8").reshape(1, 5)
897 if row_start == 0:
898 img_raw10 = chunk_raw10
899 else:
900 img_raw10 = numpy.vstack((img_raw10, chunk_raw10))
901 # unpack and check against original
902 self.assertTrue(numpy.array_equal(unpack_raw10_image(img_raw10),
903 img_check))
904
905if __name__ == "__main__":
Ruben Brunk370e2432014-10-14 18:33:23 -0700906 unittest.main()