blob: 1c13c8f858c9ce241832a9c36f8ca4124be423de [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')
Ruben Brunk370e2432014-10-14 18:33:23 -070017import its.error
Ruben Brunk370e2432014-10-14 18:33:23 -070018import sys
Yin-Chia Yehaccf5662016-10-06 17:20:57 -070019from PIL import Image
Ruben Brunk370e2432014-10-14 18:33:23 -070020import numpy
21import math
22import unittest
23import cStringIO
Ruben Brunk370e2432014-10-14 18:33:23 -070024import copy
Clemenz Portmann9b7f3552017-09-21 11:13:02 -070025import random
Ruben Brunk370e2432014-10-14 18:33:23 -070026
27DEFAULT_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
32DEFAULT_YUV_OFFSETS = numpy.array([0, 128, 128])
33
34DEFAULT_GAMMA_LUT = numpy.array(
35 [math.floor(65535 * math.pow(i/65535.0, 1/2.2) + 0.5)
36 for i in xrange(65536)])
37
38DEFAULT_INVGAMMA_LUT = numpy.array(
39 [math.floor(65535 * math.pow(i/65535.0, 2.2) + 0.5)
40 for i in xrange(65536)])
41
42MAX_LUT_SIZE = 65536
43
Clemenz Portmann812236f2016-07-19 17:51:44 -070044NUM_TRYS = 2
45NUM_FRAMES = 4
leslieshawad758902020-09-01 19:45:16 -070046G_CHANNEL = 1
47LIGHT_ON_THRESHOLD = 0.1
48IMG_L = 0
49IMG_R = 1
50IMG_T = 0
51IMG_B = 1
Clemenz Portmann812236f2016-07-19 17:51:44 -070052
53
Ruben Brunk370e2432014-10-14 18:33:23 -070054def convert_capture_to_rgb_image(cap,
55 ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
56 yuv_off=DEFAULT_YUV_OFFSETS,
57 props=None):
58 """Convert a captured image object to a RGB image.
59
60 Args:
61 cap: A capture object as returned by its.device.do_capture.
62 ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
63 yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
64 props: (Optional) camera properties object (of static values);
65 required for processing raw images.
66
67 Returns:
68 RGB float-3 image array, with pixel values in [0.0, 1.0].
69 """
70 w = cap["width"]
71 h = cap["height"]
72 if cap["format"] == "raw10":
73 assert(props is not None)
74 cap = unpack_raw10_capture(cap, props)
Yin-Chia Yeh76dd1432015-04-27 16:42:03 -070075 if cap["format"] == "raw12":
76 assert(props is not None)
77 cap = unpack_raw12_capture(cap, props)
Ruben Brunk370e2432014-10-14 18:33:23 -070078 if cap["format"] == "yuv":
79 y = cap["data"][0:w*h]
80 u = cap["data"][w*h:w*h*5/4]
81 v = cap["data"][w*h*5/4:w*h*6/4]
Timothy Knighte1025902015-07-07 12:46:24 -070082 return convert_yuv420_planar_to_rgb_image(y, u, v, w, h)
Ruben Brunk370e2432014-10-14 18:33:23 -070083 elif cap["format"] == "jpeg":
84 return decompress_jpeg_to_rgb_image(cap["data"])
Timothy Knight600077e2017-02-01 14:05:05 -080085 elif cap["format"] == "raw" or cap["format"] == "rawStats":
Ruben Brunk370e2432014-10-14 18:33:23 -070086 assert(props is not None)
87 r,gr,gb,b = convert_capture_to_planes(cap, props)
88 return convert_raw_to_rgb_image(r,gr,gb,b, props, cap["metadata"])
Shuzhen Wang09849ba2018-10-08 15:13:26 -070089 elif cap["format"] == "y8":
90 y = cap["data"][0:w*h]
91 return convert_y8_to_rgb_image(y, w, h)
Ruben Brunk370e2432014-10-14 18:33:23 -070092 else:
93 raise its.error.Error('Invalid format %s' % (cap["format"]))
94
Clemenz Portmann9b7f3552017-09-21 11:13:02 -070095
Timothy Knight67d8ec92015-08-31 13:14:46 -070096def unpack_rawstats_capture(cap):
97 """Unpack a rawStats capture to the mean and variance images.
98
99 Args:
100 cap: A capture object as returned by its.device.do_capture.
101
102 Returns:
103 Tuple (mean_image var_image) of float-4 images, with non-normalized
104 pixel values computed from the RAW16 images on the device
105 """
106 assert(cap["format"] == "rawStats")
107 w = cap["width"]
108 h = cap["height"]
109 img = numpy.ndarray(shape=(2*h*w*4,), dtype='<f', buffer=cap["data"])
110 analysis_image = img.reshape(2,h,w,4)
111 mean_image = analysis_image[0,:,:,:].reshape(h,w,4)
112 var_image = analysis_image[1,:,:,:].reshape(h,w,4)
113 return mean_image, var_image
114
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700115
Ruben Brunk370e2432014-10-14 18:33:23 -0700116def unpack_raw10_capture(cap, props):
117 """Unpack a raw-10 capture to a raw-16 capture.
118
119 Args:
120 cap: A raw-10 capture object.
Chien-Yu Chen682faa22014-10-22 17:34:44 -0700121 props: Camera properties object.
Ruben Brunk370e2432014-10-14 18:33:23 -0700122
123 Returns:
124 New capture object with raw-16 data.
125 """
126 # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding
127 # the MSPs of the pixels, and the 5th byte holding 4x2b LSBs.
128 w,h = cap["width"], cap["height"]
129 if w % 4 != 0:
130 raise its.error.Error('Invalid raw-10 buffer width')
131 cap = copy.deepcopy(cap)
132 cap["data"] = unpack_raw10_image(cap["data"].reshape(h,w*5/4))
133 cap["format"] = "raw"
134 return cap
135
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700136
Ruben Brunk370e2432014-10-14 18:33:23 -0700137def unpack_raw10_image(img):
138 """Unpack a raw-10 image to a raw-16 image.
139
140 Output image will have the 10 LSBs filled in each 16b word, and the 6 MSBs
141 will be set to zero.
142
143 Args:
144 img: A raw-10 image, as a uint8 numpy array.
145
146 Returns:
147 Image as a uint16 numpy array, with all row padding stripped.
148 """
149 if img.shape[1] % 5 != 0:
150 raise its.error.Error('Invalid raw-10 buffer width')
151 w = img.shape[1]*4/5
152 h = img.shape[0]
Yin-Chia Yeh76dd1432015-04-27 16:42:03 -0700153 # Cut out the 4x8b MSBs and shift to bits [9:2] in 16b words.
Ruben Brunk370e2432014-10-14 18:33:23 -0700154 msbs = numpy.delete(img, numpy.s_[4::5], 1)
155 msbs = msbs.astype(numpy.uint16)
156 msbs = numpy.left_shift(msbs, 2)
157 msbs = msbs.reshape(h,w)
Yin-Chia Yeh76dd1432015-04-27 16:42:03 -0700158 # Cut out the 4x2b LSBs and put each in bits [1:0] of their own 8b words.
Ruben Brunk370e2432014-10-14 18:33:23 -0700159 lsbs = img[::, 4::5].reshape(h,w/4)
160 lsbs = numpy.right_shift(
161 numpy.packbits(numpy.unpackbits(lsbs).reshape(h,w/4,4,2),3), 6)
Clemenz Portmann15a3a1b2018-06-28 20:08:01 -0700162 # Pair the LSB bits group to 0th pixel instead of 3rd pixel
Yin-Chia Yehd8682ec2017-09-22 16:31:15 -0700163 lsbs = lsbs.reshape(h,w/4,4)[:,:,::-1]
Ruben Brunk370e2432014-10-14 18:33:23 -0700164 lsbs = lsbs.reshape(h,w)
165 # Fuse the MSBs and LSBs back together
166 img16 = numpy.bitwise_or(msbs, lsbs).reshape(h,w)
167 return img16
168
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700169
Yin-Chia Yeh76dd1432015-04-27 16:42:03 -0700170def unpack_raw12_capture(cap, props):
171 """Unpack a raw-12 capture to a raw-16 capture.
172
173 Args:
174 cap: A raw-12 capture object.
175 props: Camera properties object.
176
177 Returns:
178 New capture object with raw-16 data.
179 """
180 # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding
181 # the MSBs of the pixels, and the 5th byte holding 4x2b LSBs.
182 w,h = cap["width"], cap["height"]
183 if w % 2 != 0:
184 raise its.error.Error('Invalid raw-12 buffer width')
185 cap = copy.deepcopy(cap)
186 cap["data"] = unpack_raw12_image(cap["data"].reshape(h,w*3/2))
187 cap["format"] = "raw"
188 return cap
189
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700190
Yin-Chia Yeh76dd1432015-04-27 16:42:03 -0700191def unpack_raw12_image(img):
192 """Unpack a raw-12 image to a raw-16 image.
193
194 Output image will have the 12 LSBs filled in each 16b word, and the 4 MSBs
195 will be set to zero.
196
197 Args:
198 img: A raw-12 image, as a uint8 numpy array.
199
200 Returns:
201 Image as a uint16 numpy array, with all row padding stripped.
202 """
203 if img.shape[1] % 3 != 0:
204 raise its.error.Error('Invalid raw-12 buffer width')
205 w = img.shape[1]*2/3
206 h = img.shape[0]
207 # Cut out the 2x8b MSBs and shift to bits [11:4] in 16b words.
208 msbs = numpy.delete(img, numpy.s_[2::3], 1)
209 msbs = msbs.astype(numpy.uint16)
210 msbs = numpy.left_shift(msbs, 4)
211 msbs = msbs.reshape(h,w)
212 # Cut out the 2x4b LSBs and put each in bits [3:0] of their own 8b words.
213 lsbs = img[::, 2::3].reshape(h,w/2)
214 lsbs = numpy.right_shift(
215 numpy.packbits(numpy.unpackbits(lsbs).reshape(h,w/2,2,4),3), 4)
Yin-Chia Yehd8682ec2017-09-22 16:31:15 -0700216 # Pair the LSB bits group to pixel 0 instead of pixel 1
217 lsbs = lsbs.reshape(h,w/2,2)[:,:,::-1]
Yin-Chia Yeh76dd1432015-04-27 16:42:03 -0700218 lsbs = lsbs.reshape(h,w)
219 # Fuse the MSBs and LSBs back together
220 img16 = numpy.bitwise_or(msbs, lsbs).reshape(h,w)
221 return img16
222
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700223
Ruben Brunk370e2432014-10-14 18:33:23 -0700224def convert_capture_to_planes(cap, props=None):
225 """Convert a captured image object to separate image planes.
226
227 Decompose an image into multiple images, corresponding to different planes.
228
229 For YUV420 captures ("yuv"):
230 Returns Y,U,V planes, where the Y plane is full-res and the U,V planes
231 are each 1/2 x 1/2 of the full res.
232
Timothy Knight600077e2017-02-01 14:05:05 -0800233 For Bayer captures ("raw", "raw10", "raw12", or "rawStats"):
Ruben Brunk370e2432014-10-14 18:33:23 -0700234 Returns planes in the order R,Gr,Gb,B, regardless of the Bayer pattern
Timothy Knight600077e2017-02-01 14:05:05 -0800235 layout. For full-res raw images ("raw", "raw10", "raw12"), each plane
236 is 1/2 x 1/2 of the full res. For "rawStats" images, the mean image
237 is returned.
Ruben Brunk370e2432014-10-14 18:33:23 -0700238
239 For JPEG captures ("jpeg"):
240 Returns R,G,B full-res planes.
241
242 Args:
243 cap: A capture object as returned by its.device.do_capture.
244 props: (Optional) camera properties object (of static values);
245 required for processing raw images.
246
247 Returns:
248 A tuple of float numpy arrays (one per plane), consisting of pixel
249 values in the range [0.0, 1.0].
250 """
251 w = cap["width"]
252 h = cap["height"]
253 if cap["format"] == "raw10":
254 assert(props is not None)
255 cap = unpack_raw10_capture(cap, props)
Timothy Knightac702422015-07-01 21:33:34 -0700256 if cap["format"] == "raw12":
257 assert(props is not None)
258 cap = unpack_raw12_capture(cap, props)
Ruben Brunk370e2432014-10-14 18:33:23 -0700259 if cap["format"] == "yuv":
260 y = cap["data"][0:w*h]
261 u = cap["data"][w*h:w*h*5/4]
262 v = cap["data"][w*h*5/4:w*h*6/4]
263 return ((y.astype(numpy.float32) / 255.0).reshape(h, w, 1),
264 (u.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1),
265 (v.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1))
266 elif cap["format"] == "jpeg":
267 rgb = decompress_jpeg_to_rgb_image(cap["data"]).reshape(w*h*3)
268 return (rgb[::3].reshape(h,w,1),
269 rgb[1::3].reshape(h,w,1),
270 rgb[2::3].reshape(h,w,1))
271 elif cap["format"] == "raw":
272 assert(props is not None)
273 white_level = float(props['android.sensor.info.whiteLevel'])
274 img = numpy.ndarray(shape=(h*w,), dtype='<u2',
275 buffer=cap["data"][0:w*h*2])
276 img = img.astype(numpy.float32).reshape(h,w) / white_level
Timothy Knightac702422015-07-01 21:33:34 -0700277 # Crop the raw image to the active array region.
Clemenz Portmann15a3a1b2018-06-28 20:08:01 -0700278 if props.has_key("android.sensor.info.preCorrectionActiveArraySize") \
279 and props["android.sensor.info.preCorrectionActiveArraySize"] is not None \
Timothy Knightac702422015-07-01 21:33:34 -0700280 and props.has_key("android.sensor.info.pixelArraySize") \
281 and props["android.sensor.info.pixelArraySize"] is not None:
282 # Note that the Rect class is defined such that the left,top values
283 # are "inside" while the right,bottom values are "outside"; that is,
284 # it's inclusive of the top,left sides only. So, the width is
285 # computed as right-left, rather than right-left+1, etc.
286 wfull = props["android.sensor.info.pixelArraySize"]["width"]
287 hfull = props["android.sensor.info.pixelArraySize"]["height"]
Clemenz Portmann15a3a1b2018-06-28 20:08:01 -0700288 xcrop = props["android.sensor.info.preCorrectionActiveArraySize"]["left"]
289 ycrop = props["android.sensor.info.preCorrectionActiveArraySize"]["top"]
290 wcrop = props["android.sensor.info.preCorrectionActiveArraySize"]["right"]-xcrop
291 hcrop = props["android.sensor.info.preCorrectionActiveArraySize"]["bottom"]-ycrop
Timothy Knightac702422015-07-01 21:33:34 -0700292 assert(wfull >= wcrop >= 0)
293 assert(hfull >= hcrop >= 0)
294 assert(wfull - wcrop >= xcrop >= 0)
295 assert(hfull - hcrop >= ycrop >= 0)
296 if w == wfull and h == hfull:
297 # Crop needed; extract the center region.
298 img = img[ycrop:ycrop+hcrop,xcrop:xcrop+wcrop]
299 w = wcrop
300 h = hcrop
301 elif w == wcrop and h == hcrop:
302 # No crop needed; image is already cropped to the active array.
303 None
304 else:
305 raise its.error.Error('Invalid image size metadata')
306 # Separate the image planes.
Ruben Brunk370e2432014-10-14 18:33:23 -0700307 imgs = [img[::2].reshape(w*h/2)[::2].reshape(h/2,w/2,1),
308 img[::2].reshape(w*h/2)[1::2].reshape(h/2,w/2,1),
309 img[1::2].reshape(w*h/2)[::2].reshape(h/2,w/2,1),
310 img[1::2].reshape(w*h/2)[1::2].reshape(h/2,w/2,1)]
311 idxs = get_canonical_cfa_order(props)
312 return [imgs[i] for i in idxs]
Timothy Knight600077e2017-02-01 14:05:05 -0800313 elif cap["format"] == "rawStats":
314 assert(props is not None)
315 white_level = float(props['android.sensor.info.whiteLevel'])
316 mean_image, var_image = its.image.unpack_rawstats_capture(cap)
317 idxs = get_canonical_cfa_order(props)
318 return [mean_image[:,:,i] / white_level for i in idxs]
Ruben Brunk370e2432014-10-14 18:33:23 -0700319 else:
320 raise its.error.Error('Invalid format %s' % (cap["format"]))
321
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700322
Ruben Brunk370e2432014-10-14 18:33:23 -0700323def get_canonical_cfa_order(props):
324 """Returns a mapping from the Bayer 2x2 top-left grid in the CFA to
325 the standard order R,Gr,Gb,B.
326
327 Args:
328 props: Camera properties object.
329
330 Returns:
331 List of 4 integers, corresponding to the positions in the 2x2 top-
332 left Bayer grid of R,Gr,Gb,B, where the 2x2 grid is labeled as
333 0,1,2,3 in row major order.
334 """
335 # Note that raw streams aren't croppable, so the cropRegion doesn't need
336 # to be considered when determining the top-left pixel color.
337 cfa_pat = props['android.sensor.info.colorFilterArrangement']
338 if cfa_pat == 0:
339 # RGGB
340 return [0,1,2,3]
341 elif cfa_pat == 1:
342 # GRBG
343 return [1,0,3,2]
344 elif cfa_pat == 2:
345 # GBRG
346 return [2,3,0,1]
347 elif cfa_pat == 3:
348 # BGGR
349 return [3,2,1,0]
350 else:
351 raise its.error.Error("Not supported")
352
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700353
Ruben Brunk370e2432014-10-14 18:33:23 -0700354def get_gains_in_canonical_order(props, gains):
355 """Reorders the gains tuple to the canonical R,Gr,Gb,B order.
356
357 Args:
358 props: Camera properties object.
359 gains: List of 4 values, in R,G_even,G_odd,B order.
360
361 Returns:
362 List of gains values, in R,Gr,Gb,B order.
363 """
364 cfa_pat = props['android.sensor.info.colorFilterArrangement']
365 if cfa_pat in [0,1]:
366 # RGGB or GRBG, so G_even is Gr
367 return gains
368 elif cfa_pat in [2,3]:
369 # GBRG or BGGR, so G_even is Gb
370 return [gains[0], gains[2], gains[1], gains[3]]
371 else:
372 raise its.error.Error("Not supported")
373
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700374
Ruben Brunk370e2432014-10-14 18:33:23 -0700375def convert_raw_to_rgb_image(r_plane, gr_plane, gb_plane, b_plane,
376 props, cap_res):
377 """Convert a Bayer raw-16 image to an RGB image.
378
379 Includes some extremely rudimentary demosaicking and color processing
380 operations; the output of this function shouldn't be used for any image
381 quality analysis.
382
383 Args:
384 r_plane,gr_plane,gb_plane,b_plane: Numpy arrays for each color plane
385 in the Bayer image, with pixels in the [0.0, 1.0] range.
386 props: Camera properties object.
387 cap_res: Capture result (metadata) object.
388
389 Returns:
390 RGB float-3 image array, with pixel values in [0.0, 1.0]
391 """
392 # Values required for the RAW to RGB conversion.
393 assert(props is not None)
394 white_level = float(props['android.sensor.info.whiteLevel'])
395 black_levels = props['android.sensor.blackLevelPattern']
396 gains = cap_res['android.colorCorrection.gains']
397 ccm = cap_res['android.colorCorrection.transform']
398
399 # Reorder black levels and gains to R,Gr,Gb,B, to match the order
400 # of the planes.
Timothy Knightfa785872016-07-12 16:49:47 -0700401 black_levels = [get_black_level(i,props,cap_res) for i in range(4)]
Ruben Brunk370e2432014-10-14 18:33:23 -0700402 gains = get_gains_in_canonical_order(props, gains)
403
404 # Convert CCM from rational to float, as numpy arrays.
405 ccm = numpy.array(its.objects.rational_to_float(ccm)).reshape(3,3)
406
407 # Need to scale the image back to the full [0,1] range after subtracting
408 # the black level from each pixel.
409 scale = white_level / (white_level - max(black_levels))
410
411 # Three-channel black levels, normalized to [0,1] by white_level.
412 black_levels = numpy.array([b/white_level for b in [
413 black_levels[i] for i in [0,1,3]]])
414
415 # Three-channel gains.
416 gains = numpy.array([gains[i] for i in [0,1,3]])
417
418 h,w = r_plane.shape[:2]
419 img = numpy.dstack([r_plane,(gr_plane+gb_plane)/2.0,b_plane])
420 img = (((img.reshape(h,w,3) - black_levels) * scale) * gains).clip(0.0,1.0)
421 img = numpy.dot(img.reshape(w*h,3), ccm.T).reshape(h,w,3).clip(0.0,1.0)
422 return img
423
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700424
Yin-Chia Yehe36bc162018-02-16 18:10:39 -0800425def get_black_level(chan, props, cap_res=None):
Timothy Knightfa785872016-07-12 16:49:47 -0700426 """Return the black level to use for a given capture.
427
428 Uses a dynamic value from the capture result if available, else falls back
429 to the static global value in the camera characteristics.
430
431 Args:
432 chan: The channel index, in canonical order (R, Gr, Gb, B).
433 props: The camera properties object.
434 cap_res: A capture result object.
435
436 Returns:
437 The black level value for the specified channel.
438 """
Yin-Chia Yehe36bc162018-02-16 18:10:39 -0800439 if (cap_res is not None and cap_res.has_key('android.sensor.dynamicBlackLevel') and
Clemenz Portmanne4a09cf2016-10-18 12:57:46 -0700440 cap_res['android.sensor.dynamicBlackLevel'] is not None):
441 black_levels = cap_res['android.sensor.dynamicBlackLevel']
Timothy Knightfa785872016-07-12 16:49:47 -0700442 else:
443 black_levels = props['android.sensor.blackLevelPattern']
444 idxs = its.image.get_canonical_cfa_order(props)
445 ordered_black_levels = [black_levels[i] for i in idxs]
446 return ordered_black_levels[chan]
447
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700448
Timothy Knighte1025902015-07-07 12:46:24 -0700449def convert_yuv420_planar_to_rgb_image(y_plane, u_plane, v_plane,
450 w, h,
451 ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
452 yuv_off=DEFAULT_YUV_OFFSETS):
Ruben Brunk370e2432014-10-14 18:33:23 -0700453 """Convert a YUV420 8-bit planar image to an RGB image.
454
455 Args:
456 y_plane: The packed 8-bit Y plane.
457 u_plane: The packed 8-bit U plane.
458 v_plane: The packed 8-bit V plane.
459 w: The width of the image.
460 h: The height of the image.
461 ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
462 yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
463
464 Returns:
465 RGB float-3 image array, with pixel values in [0.0, 1.0].
466 """
467 y = numpy.subtract(y_plane, yuv_off[0])
468 u = numpy.subtract(u_plane, yuv_off[1]).view(numpy.int8)
469 v = numpy.subtract(v_plane, yuv_off[2]).view(numpy.int8)
470 u = u.reshape(h/2, w/2).repeat(2, axis=1).repeat(2, axis=0)
471 v = v.reshape(h/2, w/2).repeat(2, axis=1).repeat(2, axis=0)
472 yuv = numpy.dstack([y, u.reshape(w*h), v.reshape(w*h)])
473 flt = numpy.empty([h, w, 3], dtype=numpy.float32)
474 flt.reshape(w*h*3)[:] = yuv.reshape(h*w*3)[:]
475 flt = numpy.dot(flt.reshape(w*h,3), ccm_yuv_to_rgb.T).clip(0, 255)
476 rgb = numpy.empty([h, w, 3], dtype=numpy.uint8)
477 rgb.reshape(w*h*3)[:] = flt.reshape(w*h*3)[:]
478 return rgb.astype(numpy.float32) / 255.0
479
Shuzhen Wang09849ba2018-10-08 15:13:26 -0700480def convert_y8_to_rgb_image(y_plane, w, h):
481 """Convert a Y 8-bit image to an RGB image.
482
483 Args:
484 y_plane: The packed 8-bit Y plane.
485 w: The width of the image.
486 h: The height of the image.
487
488 Returns:
489 RGB float-3 image array, with pixel values in [0.0, 1.0].
490 """
491 y3 = numpy.dstack([y_plane, y_plane, y_plane])
492 rgb = numpy.empty([h, w, 3], dtype=numpy.uint8)
493 rgb.reshape(w*h*3)[:] = y3.reshape(w*h*3)[:]
494 return rgb.astype(numpy.float32) / 255.0
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700495
Timothy Knight36fba9c2015-06-22 14:46:38 -0700496def load_rgb_image(fname):
497 """Load a standard image file (JPG, PNG, etc.).
498
499 Args:
500 fname: The path of the file to load.
501
502 Returns:
503 RGB float-3 image array, with pixel values in [0.0, 1.0].
504 """
505 img = Image.open(fname)
506 w = img.size[0]
507 h = img.size[1]
508 a = numpy.array(img)
509 if len(a.shape) == 3 and a.shape[2] == 3:
510 # RGB
511 return a.reshape(h,w,3) / 255.0
512 elif len(a.shape) == 2 or len(a.shape) == 3 and a.shape[2] == 1:
513 # Greyscale; convert to RGB
514 return a.reshape(h*w).repeat(3).reshape(h,w,3) / 255.0
515 else:
516 raise its.error.Error('Unsupported image type')
517
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700518
Ruben Brunk370e2432014-10-14 18:33:23 -0700519def load_yuv420_to_rgb_image(yuv_fname,
520 w, h,
Timothy Knighte1025902015-07-07 12:46:24 -0700521 layout="planar",
Ruben Brunk370e2432014-10-14 18:33:23 -0700522 ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
523 yuv_off=DEFAULT_YUV_OFFSETS):
524 """Load a YUV420 image file, and return as an RGB image.
525
Timothy Knighte1025902015-07-07 12:46:24 -0700526 Supported layouts include "planar" and "nv21". The "yuv" formatted captures
527 returned from the device via do_capture are in the "planar" layout; other
528 layouts may only be needed for loading files from other sources.
529
Ruben Brunk370e2432014-10-14 18:33:23 -0700530 Args:
531 yuv_fname: The path of the YUV420 file.
532 w: The width of the image.
533 h: The height of the image.
Timothy Knighte1025902015-07-07 12:46:24 -0700534 layout: (Optional) the layout of the YUV data (as a string).
Ruben Brunk370e2432014-10-14 18:33:23 -0700535 ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
536 yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
537
538 Returns:
539 RGB float-3 image array, with pixel values in [0.0, 1.0].
540 """
541 with open(yuv_fname, "rb") as f:
Timothy Knighte1025902015-07-07 12:46:24 -0700542 if layout == "planar":
543 # Plane of Y, plane of V, plane of U.
544 y = numpy.fromfile(f, numpy.uint8, w*h, "")
545 v = numpy.fromfile(f, numpy.uint8, w*h/4, "")
546 u = numpy.fromfile(f, numpy.uint8, w*h/4, "")
547 elif layout == "nv21":
548 # Plane of Y, plane of interleaved VUVUVU...
549 y = numpy.fromfile(f, numpy.uint8, w*h, "")
550 vu = numpy.fromfile(f, numpy.uint8, w*h/2, "")
551 v = vu[0::2]
552 u = vu[1::2]
553 else:
554 raise its.error.Error('Unsupported image layout')
555 return convert_yuv420_planar_to_rgb_image(
556 y,u,v,w,h,ccm_yuv_to_rgb,yuv_off)
Ruben Brunk370e2432014-10-14 18:33:23 -0700557
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700558
Timothy Knighte1025902015-07-07 12:46:24 -0700559def load_yuv420_planar_to_yuv_planes(yuv_fname, w, h):
560 """Load a YUV420 planar image file, and return Y, U, and V plane images.
Ruben Brunk370e2432014-10-14 18:33:23 -0700561
562 Args:
563 yuv_fname: The path of the YUV420 file.
564 w: The width of the image.
565 h: The height of the image.
566
567 Returns:
568 Separate Y, U, and V images as float-1 Numpy arrays, pixels in [0,1].
569 Note that pixel (0,0,0) is not black, since U,V pixels are centered at
570 0.5, and also that the Y and U,V plane images returned are different
571 sizes (due to chroma subsampling in the YUV420 format).
572 """
573 with open(yuv_fname, "rb") as f:
574 y = numpy.fromfile(f, numpy.uint8, w*h, "")
575 v = numpy.fromfile(f, numpy.uint8, w*h/4, "")
576 u = numpy.fromfile(f, numpy.uint8, w*h/4, "")
577 return ((y.astype(numpy.float32) / 255.0).reshape(h, w, 1),
578 (u.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1),
579 (v.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1))
580
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700581
Ruben Brunk370e2432014-10-14 18:33:23 -0700582def decompress_jpeg_to_rgb_image(jpeg_buffer):
583 """Decompress a JPEG-compressed image, returning as an RGB image.
584
585 Args:
586 jpeg_buffer: The JPEG stream.
587
588 Returns:
589 A numpy array for the RGB image, with pixels in [0,1].
590 """
591 img = Image.open(cStringIO.StringIO(jpeg_buffer))
592 w = img.size[0]
593 h = img.size[1]
594 return numpy.array(img).reshape(h,w,3) / 255.0
595
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700596
Ruben Brunk370e2432014-10-14 18:33:23 -0700597def apply_lut_to_image(img, lut):
598 """Applies a LUT to every pixel in a float image array.
599
600 Internally converts to a 16b integer image, since the LUT can work with up
601 to 16b->16b mappings (i.e. values in the range [0,65535]). The lut can also
602 have fewer than 65536 entries, however it must be sized as a power of 2
603 (and for smaller luts, the scale must match the bitdepth).
604
605 For a 16b lut of 65536 entries, the operation performed is:
606
607 lut[r * 65535] / 65535 -> r'
608 lut[g * 65535] / 65535 -> g'
609 lut[b * 65535] / 65535 -> b'
610
611 For a 10b lut of 1024 entries, the operation becomes:
612
613 lut[r * 1023] / 1023 -> r'
614 lut[g * 1023] / 1023 -> g'
615 lut[b * 1023] / 1023 -> b'
616
617 Args:
618 img: Numpy float image array, with pixel values in [0,1].
619 lut: Numpy table encoding a LUT, mapping 16b integer values.
620
621 Returns:
622 Float image array after applying LUT to each pixel.
623 """
624 n = len(lut)
625 if n <= 0 or n > MAX_LUT_SIZE or (n & (n - 1)) != 0:
626 raise its.error.Error('Invalid arg LUT size: %d' % (n))
627 m = float(n-1)
628 return (lut[(img * m).astype(numpy.uint16)] / m).astype(numpy.float32)
629
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700630
Ruben Brunk370e2432014-10-14 18:33:23 -0700631def apply_matrix_to_image(img, mat):
632 """Multiplies a 3x3 matrix with each float-3 image pixel.
633
634 Each pixel is considered a column vector, and is left-multiplied by
635 the given matrix:
636
637 [ ] r r'
638 [ mat ] * g -> g'
639 [ ] b b'
640
641 Args:
642 img: Numpy float image array, with pixel values in [0,1].
643 mat: Numpy 3x3 matrix.
644
645 Returns:
646 The numpy float-3 image array resulting from the matrix mult.
647 """
648 h = img.shape[0]
649 w = img.shape[1]
650 img2 = numpy.empty([h, w, 3], dtype=numpy.float32)
651 img2.reshape(w*h*3)[:] = (numpy.dot(img.reshape(h*w, 3), mat.T)
652 ).reshape(w*h*3)[:]
653 return img2
654
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700655
Ruben Brunk370e2432014-10-14 18:33:23 -0700656def get_image_patch(img, xnorm, ynorm, wnorm, hnorm):
657 """Get a patch (tile) of an image.
658
659 Args:
660 img: Numpy float image array, with pixel values in [0,1].
661 xnorm,ynorm,wnorm,hnorm: Normalized (in [0,1]) coords for the tile.
662
663 Returns:
664 Float image array of the patch.
665 """
666 hfull = img.shape[0]
667 wfull = img.shape[1]
Clemenz Portmann6d2e7292018-01-30 14:02:11 -0800668 xtile = int(math.ceil(xnorm * wfull))
669 ytile = int(math.ceil(ynorm * hfull))
670 wtile = int(math.floor(wnorm * wfull))
671 htile = int(math.floor(hnorm * hfull))
Clemenz Portmannc47c8022017-04-04 09:10:30 -0700672 if len(img.shape)==2:
673 return img[ytile:ytile+htile,xtile:xtile+wtile].copy()
674 else:
675 return img[ytile:ytile+htile,xtile:xtile+wtile,:].copy()
Ruben Brunk370e2432014-10-14 18:33:23 -0700676
leslieshawad758902020-09-01 19:45:16 -0700677def validate_lighting(img):
678 """Evaluate four corner patches of image to check if light is ON or OFF.
679 Args:
680 img: numpy float array of RGB image, with pixel values in [0, 1].
681
682 Returns:
683 True if the G channel of the RGB mean is <LIGHT_ON_THRESHOLD;
684 otherwise assertion fails.
685 """
686
687 patch_w = 0.05
688 patch_h = 0.05
689 img_b = IMG_B - patch_h
690 img_r = IMG_R - patch_w
691
692 patch_tl = its.image.get_image_patch(img, IMG_L, IMG_T, patch_w, patch_h)
693 patch_tr = its.image.get_image_patch(img, img_r, IMG_T, patch_w, patch_h)
694 patch_bl = its.image.get_image_patch(img, IMG_L, img_b, patch_w, patch_h)
695 patch_br = its.image.get_image_patch(img, img_r, img_b, patch_w, patch_h)
696 g_mean_tl = its.image.compute_image_means(patch_tl)[G_CHANNEL]
697 g_mean_tr = its.image.compute_image_means(patch_tr)[G_CHANNEL]
698 g_mean_bl = its.image.compute_image_means(patch_bl)[G_CHANNEL]
699 g_mean_br = its.image.compute_image_means(patch_br)[G_CHANNEL]
700 print "Corner patch green values. TL: %3.3f, TR: %.3f, BL: %.3f, BR: %.3f" % (
701 g_mean_tl, g_mean_tr, g_mean_bl, g_mean_br)
702 if (g_mean_tl > LIGHT_ON_THRESHOLD or
703 g_mean_tr > LIGHT_ON_THRESHOLD or
704 g_mean_bl > LIGHT_ON_THRESHOLD or
705 g_mean_br > LIGHT_ON_THRESHOLD):
706 print "Lights are ON in test rig."
707 return True
708 else:
709 assert 0, "Lights are OFF in test rig. Please turn lights on and retry."
710 return False
711
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700712
Ruben Brunk370e2432014-10-14 18:33:23 -0700713def compute_image_means(img):
714 """Calculate the mean of each color channel in the image.
715
716 Args:
717 img: Numpy float image array, with pixel values in [0,1].
718
719 Returns:
720 A list of mean values, one per color channel in the image.
721 """
722 means = []
723 chans = img.shape[2]
724 for i in xrange(chans):
725 means.append(numpy.mean(img[:,:,i], dtype=numpy.float64))
726 return means
727
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700728
Ruben Brunk370e2432014-10-14 18:33:23 -0700729def compute_image_variances(img):
730 """Calculate the variance of each color channel in the image.
731
732 Args:
733 img: Numpy float image array, with pixel values in [0,1].
734
735 Returns:
736 A list of mean values, one per color channel in the image.
737 """
738 variances = []
739 chans = img.shape[2]
740 for i in xrange(chans):
741 variances.append(numpy.var(img[:,:,i], dtype=numpy.float64))
742 return variances
743
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700744
Yin-Chia Yeh619f2eb2015-09-17 17:13:09 -0700745def compute_image_snrs(img):
746 """Calculate the SNR (db) of each color channel in the image.
747
748 Args:
749 img: Numpy float image array, with pixel values in [0,1].
750
751 Returns:
752 A list of SNR value, one per color channel in the image.
753 """
754 means = compute_image_means(img)
755 variances = compute_image_variances(img)
756 std_devs = [math.sqrt(v) for v in variances]
757 snr = [20 * math.log10(m/s) for m,s in zip(means, std_devs)]
758 return snr
759
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700760
Clemenz Portmann08e78ab2017-12-01 15:50:56 -0800761def compute_image_max_gradients(img):
762 """Calculate the maximum gradient of each color channel in the image.
763
764 Args:
765 img: Numpy float image array, with pixel values in [0,1].
766
767 Returns:
768 A list of gradient max values, one per color channel in the image.
769 """
770 grads = []
771 chans = img.shape[2]
772 for i in xrange(chans):
773 grads.append(numpy.amax(numpy.gradient(img[:, :, i])))
774 return grads
775
776
Ruben Brunk370e2432014-10-14 18:33:23 -0700777def write_image(img, fname, apply_gamma=False):
778 """Save a float-3 numpy array image to a file.
779
780 Supported formats: PNG, JPEG, and others; see PIL docs for more.
781
782 Image can be 3-channel, which is interpreted as RGB, or can be 1-channel,
783 which is greyscale.
784
785 Can optionally specify that the image should be gamma-encoded prior to
786 writing it out; this should be done if the image contains linear pixel
787 values, to make the image look "normal".
788
789 Args:
790 img: Numpy image array data.
791 fname: Path of file to save to; the extension specifies the format.
792 apply_gamma: (Optional) apply gamma to the image prior to writing it.
793 """
794 if apply_gamma:
795 img = apply_lut_to_image(img, DEFAULT_GAMMA_LUT)
796 (h, w, chans) = img.shape
797 if chans == 3:
798 Image.fromarray((img * 255.0).astype(numpy.uint8), "RGB").save(fname)
799 elif chans == 1:
800 img3 = (img * 255.0).astype(numpy.uint8).repeat(3).reshape(h,w,3)
801 Image.fromarray(img3, "RGB").save(fname)
802 else:
803 raise its.error.Error('Unsupported image type')
804
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700805
Ruben Brunk370e2432014-10-14 18:33:23 -0700806def downscale_image(img, f):
807 """Shrink an image by a given integer factor.
808
809 This function computes output pixel values by averaging over rectangular
810 regions of the input image; it doesn't skip or sample pixels, and all input
811 image pixels are evenly weighted.
812
813 If the downscaling factor doesn't cleanly divide the width and/or height,
814 then the remaining pixels on the right or bottom edge are discarded prior
815 to the downscaling.
816
817 Args:
818 img: The input image as an ndarray.
819 f: The downscaling factor, which should be an integer.
820
821 Returns:
822 The new (downscaled) image, as an ndarray.
823 """
824 h,w,chans = img.shape
825 f = int(f)
826 assert(f >= 1)
827 h = (h/f)*f
828 w = (w/f)*f
829 img = img[0:h:,0:w:,::]
830 chs = []
831 for i in xrange(chans):
832 ch = img.reshape(h*w*chans)[i::chans].reshape(h,w)
833 ch = ch.reshape(h,w/f,f).mean(2).reshape(h,w/f)
834 ch = ch.T.reshape(w/f,h/f,f).mean(2).T.reshape(h/f,w/f)
835 chs.append(ch.reshape(h*w/(f*f)))
836 img = numpy.vstack(chs).T.reshape(h/f,w/f,chans)
837 return img
838
Clemenz Portmann9beecac2016-06-07 19:56:13 -0700839
Chien-Yu Chen32678602015-06-25 15:10:52 -0700840def compute_image_sharpness(img):
841 """Calculate the sharpness of input image.
842
843 Args:
844 img: Numpy float RGB/luma image array, with pixel values in [0,1].
845
846 Returns:
847 A sharpness estimation value based on the average of gradient magnitude.
848 Larger value means the image is sharper.
849 """
850 chans = img.shape[2]
851 assert(chans == 1 or chans == 3)
Clemenz Portmann9beecac2016-06-07 19:56:13 -0700852 if (chans == 1):
853 luma = img[:, :, 0]
854 elif (chans == 3):
Chien-Yu Chen32678602015-06-25 15:10:52 -0700855 luma = 0.299 * img[:,:,0] + 0.587 * img[:,:,1] + 0.114 * img[:,:,2]
856
857 [gy, gx] = numpy.gradient(luma)
858 return numpy.average(numpy.sqrt(gy*gy + gx*gx))
859
Clemenz Portmannc47c8022017-04-04 09:10:30 -0700860
Clemenz Portmann812236f2016-07-19 17:51:44 -0700861def normalize_img(img):
862 """Normalize the image values to between 0 and 1.
863
864 Args:
865 img: 2-D numpy array of image values
866 Returns:
867 Normalized image
868 """
869 return (img - numpy.amin(img))/(numpy.amax(img) - numpy.amin(img))
870
Clemenz Portmann51d765f2017-07-14 14:56:45 -0700871
872def chart_located_per_argv():
873 """Determine if chart already located outside of test.
874
875 If chart info provided, return location and size. If not, return None.
876
877 Args:
878 None
879 Returns:
Clemenz Portmannc47c8022017-04-04 09:10:30 -0700880 chart_loc: float converted xnorm,ynorm,wnorm,hnorm,scale from argv text.
881 argv is of form 'chart_loc=0.45,0.45,0.1,0.1,1.0'
Clemenz Portmann51d765f2017-07-14 14:56:45 -0700882 """
883 for s in sys.argv[1:]:
884 if s[:10] == "chart_loc=" and len(s) > 10:
885 chart_loc = s[10:].split(",")
886 return map(float, chart_loc)
Clemenz Portmannc47c8022017-04-04 09:10:30 -0700887 return None, None, None, None, None
Clemenz Portmann51d765f2017-07-14 14:56:45 -0700888
889
Clemenz Portmannc47c8022017-04-04 09:10:30 -0700890def rotate_img_per_argv(img):
891 """Rotate an image 180 degrees if "rotate" is in argv
Yin-Chia Yehf3506572016-10-10 15:46:46 -0700892
893 Args:
894 img: 2-D numpy array of image values
895 Returns:
Clemenz Portmannc47c8022017-04-04 09:10:30 -0700896 Rotated image
Yin-Chia Yehf3506572016-10-10 15:46:46 -0700897 """
898 img_out = img
Clemenz Portmannc47c8022017-04-04 09:10:30 -0700899 if "rotate180" in sys.argv:
900 img_out = numpy.fliplr(numpy.flipud(img_out))
Yin-Chia Yehf3506572016-10-10 15:46:46 -0700901 return img_out
902
Clemenz Portmannc47c8022017-04-04 09:10:30 -0700903
Clemenz Portmann812236f2016-07-19 17:51:44 -0700904def stationary_lens_cap(cam, req, fmt):
905 """Take up to NUM_TRYS caps and save the 1st one with lens stationary.
906
907 Args:
908 cam: open device session
909 req: capture request
910 fmt: format for capture
911
912 Returns:
913 capture
914 """
915 trys = 0
916 done = False
917 reqs = [req] * NUM_FRAMES
918 while not done:
919 print 'Waiting for lens to move to correct location...'
920 cap = cam.do_capture(reqs, fmt)
921 done = (cap[NUM_FRAMES-1]['metadata']['android.lens.state'] == 0)
922 print ' status: ', done
923 trys += 1
924 if trys == NUM_TRYS:
925 raise its.error.Error('Cannot settle lens after %d trys!' % trys)
926 return cap[NUM_FRAMES-1]
927
Clemenz Portmannc47c8022017-04-04 09:10:30 -0700928
Ruben Brunk370e2432014-10-14 18:33:23 -0700929class __UnitTest(unittest.TestCase):
930 """Run a suite of unit tests on this module.
931 """
932
933 # TODO: Add more unit tests.
934
935 def test_apply_matrix_to_image(self):
936 """Unit test for apply_matrix_to_image.
937
938 Test by using a canned set of values on a 1x1 pixel image.
939
940 [ 1 2 3 ] [ 0.1 ] [ 1.4 ]
941 [ 4 5 6 ] * [ 0.2 ] = [ 3.2 ]
942 [ 7 8 9 ] [ 0.3 ] [ 5.0 ]
943 mat x y
944 """
Clemenz Portmann9beecac2016-06-07 19:56:13 -0700945 mat = numpy.array([[1,2,3], [4,5,6], [7,8,9]])
Ruben Brunk370e2432014-10-14 18:33:23 -0700946 x = numpy.array([0.1,0.2,0.3]).reshape(1,1,3)
947 y = apply_matrix_to_image(x, mat).reshape(3).tolist()
948 y_ref = [1.4,3.2,5.0]
949 passed = all([math.fabs(y[i] - y_ref[i]) < 0.001 for i in xrange(3)])
950 self.assertTrue(passed)
951
952 def test_apply_lut_to_image(self):
Clemenz Portmann9beecac2016-06-07 19:56:13 -0700953 """Unit test for apply_lut_to_image.
Ruben Brunk370e2432014-10-14 18:33:23 -0700954
955 Test by using a canned set of values on a 1x1 pixel image. The LUT will
956 simply double the value of the index:
957
958 lut[x] = 2*x
959 """
960 lut = numpy.array([2*i for i in xrange(65536)])
961 x = numpy.array([0.1,0.2,0.3]).reshape(1,1,3)
962 y = apply_lut_to_image(x, lut).reshape(3).tolist()
963 y_ref = [0.2,0.4,0.6]
964 passed = all([math.fabs(y[i] - y_ref[i]) < 0.001 for i in xrange(3)])
965 self.assertTrue(passed)
966
Clemenz Portmann9b7f3552017-09-21 11:13:02 -0700967 def test_unpack_raw10_image(self):
968 """Unit test for unpack_raw10_image.
969
970 RAW10 bit packing format
971 bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0
972 Byte 0: P0[9] P0[8] P0[7] P0[6] P0[5] P0[4] P0[3] P0[2]
973 Byte 1: P1[9] P1[8] P1[7] P1[6] P1[5] P1[4] P1[3] P1[2]
974 Byte 2: P2[9] P2[8] P2[7] P2[6] P2[5] P2[4] P2[3] P2[2]
975 Byte 3: P3[9] P3[8] P3[7] P3[6] P3[5] P3[4] P3[3] P3[2]
976 Byte 4: P3[1] P3[0] P2[1] P2[0] P1[1] P1[0] P0[1] P0[0]
977 """
978 # test by using a random 4x4 10-bit image
979 H = 4
980 W = 4
981 check_list = random.sample(range(0, 1024), H*W)
982 img_check = numpy.array(check_list).reshape(H, W)
983 # pack bits
984 for row_start in range(0, len(check_list), W):
985 msbs = []
986 lsbs = ""
987 for pixel in range(W):
988 val = numpy.binary_repr(check_list[row_start+pixel], 10)
989 msbs.append(int(val[:8], base=2))
990 lsbs = val[8:] + lsbs
991 packed = msbs
992 packed.append(int(lsbs, base=2))
993 chunk_raw10 = numpy.array(packed, dtype="uint8").reshape(1, 5)
994 if row_start == 0:
995 img_raw10 = chunk_raw10
996 else:
997 img_raw10 = numpy.vstack((img_raw10, chunk_raw10))
998 # unpack and check against original
999 self.assertTrue(numpy.array_equal(unpack_raw10_image(img_raw10),
1000 img_check))
1001
1002if __name__ == "__main__":
Ruben Brunk370e2432014-10-14 18:33:23 -07001003 unittest.main()