1# Copyright 2018 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 os.path 16import its.caps 17import its.device 18import its.image 19import its.objects 20from matplotlib import pylab 21import matplotlib.pyplot 22import numpy as np 23 24IMG_STATS_GRID = 9 # find used to find the center 11.11% 25NAME = os.path.basename(__file__).split(".")[0] 26NUM_ISO_STEPS = 5 27SATURATION_TOL = 0.01 28BLK_LVL_TOL = 0.1 29EXP_MULT_SHORT = pow(2, 1.0/3) # Test 3 steps per 2x exposure 30EXP_MULT_LONG = pow(10, 1.0/3) # Test 3 steps per 10x exposure 31EXP_LONG = 1E6 # 1ms 32INCREASING_THR = 0.99 33# slice captures into burst of SLICE_LEN requests 34SLICE_LEN = 10 35 36 37def main(): 38 """Capture a set of raw images with increasing exposure time and measure the pixel values. 39 """ 40 41 with its.device.ItsSession() as cam: 42 43 props = cam.get_camera_properties() 44 props = cam.override_with_hidden_physical_camera_props(props) 45 its.caps.skip_unless(its.caps.raw16(props) and 46 its.caps.manual_sensor(props) and 47 its.caps.per_frame_control(props) and 48 not its.caps.mono_camera(props)) 49 debug = its.caps.debug_mode() 50 51 # Expose for the scene with min sensitivity 52 exp_min, exp_max = props["android.sensor.info.exposureTimeRange"] 53 sens_min, _ = props["android.sensor.info.sensitivityRange"] 54 # Digital gains might not be visible on RAW data 55 sens_max = props["android.sensor.maxAnalogSensitivity"] 56 sens_step = (sens_max - sens_min) / NUM_ISO_STEPS 57 white_level = float(props["android.sensor.info.whiteLevel"]) 58 black_levels = [its.image.get_black_level(i, props) for i in range(4)] 59 # Get the active array width and height. 60 aax = props["android.sensor.info.preCorrectionActiveArraySize"]["left"] 61 aay = props["android.sensor.info.preCorrectionActiveArraySize"]["top"] 62 aaw = props["android.sensor.info.preCorrectionActiveArraySize"]["right"]-aax 63 aah = props["android.sensor.info.preCorrectionActiveArraySize"]["bottom"]-aay 64 raw_stat_fmt = {"format": "rawStats", 65 "gridWidth": aaw/IMG_STATS_GRID, 66 "gridHeight": aah/IMG_STATS_GRID} 67 68 e_test = [] 69 mult = 1.0 70 while exp_min*mult < exp_max: 71 e_test.append(int(exp_min*mult)) 72 if exp_min*mult < EXP_LONG: 73 mult *= EXP_MULT_SHORT 74 else: 75 mult *= EXP_MULT_LONG 76 if e_test[-1] < exp_max * INCREASING_THR: 77 e_test.append(int(exp_max)) 78 e_test_ms = [e / 1000000.0 for e in e_test] 79 80 for s in range(sens_min, sens_max, sens_step): 81 means = [] 82 means.append(black_levels) 83 reqs = [its.objects.manual_capture_request(s, e, 0) for e in e_test] 84 # Capture raw in debug mode, rawStats otherwise 85 caps = [] 86 slice_len = SLICE_LEN 87 # Eliminate cap burst of 1: returns [[]], not [{}, ...] 88 while len(reqs) % slice_len == 1: 89 slice_len -= 1 90 # Break caps into smaller bursts 91 for i in range(len(reqs) / slice_len): 92 if debug: 93 caps += cam.do_capture(reqs[i*slice_len:(i+1)*slice_len], cam.CAP_RAW) 94 else: 95 caps += cam.do_capture(reqs[i*slice_len:(i+1)*slice_len], raw_stat_fmt) 96 last_n = len(reqs) % slice_len 97 if last_n: 98 if debug: 99 caps += cam.do_capture(reqs[-last_n:], cam.CAP_RAW) 100 else: 101 caps += cam.do_capture(reqs[-last_n:], raw_stat_fmt) 102 103 # Measure the mean of each channel. 104 # Each shot should be brighter (except underexposed/overexposed scene) 105 for i, cap in enumerate(caps): 106 if debug: 107 planes = its.image.convert_capture_to_planes(cap, props) 108 tiles = [its.image.get_image_patch(p, 0.445, 0.445, 0.11, 0.11) for p in planes] 109 mean = [m * white_level for tile in tiles 110 for m in its.image.compute_image_means(tile)] 111 img = its.image.convert_capture_to_rgb_image(cap, props=props) 112 its.image.write_image(img, "%s_s=%d_e=%05d.jpg" 113 % (NAME, s, e_test[i])) 114 else: 115 mean_image, _ = its.image.unpack_rawstats_capture(cap) 116 mean = mean_image[IMG_STATS_GRID/2, IMG_STATS_GRID/2] 117 print "ISO=%d, exposure time=%.3fms, mean=%s" % ( 118 s, e_test[i] / 1000000.0, str(mean)) 119 means.append(mean) 120 121 # means[0] is black level value 122 r = [m[0] for m in means[1:]] 123 gr = [m[1] for m in means[1:]] 124 gb = [m[2] for m in means[1:]] 125 b = [m[3] for m in means[1:]] 126 127 pylab.plot(e_test_ms, r, "r.-") 128 pylab.plot(e_test_ms, b, "b.-") 129 pylab.plot(e_test_ms, gr, "g.-") 130 pylab.plot(e_test_ms, gb, "k.-") 131 pylab.xscale("log") 132 pylab.yscale("log") 133 pylab.title("%s ISO=%d" % (NAME, s)) 134 pylab.xlabel("Exposure time (ms)") 135 pylab.ylabel("Center patch pixel mean") 136 matplotlib.pyplot.savefig("%s_s=%d.png" % (NAME, s)) 137 pylab.clf() 138 139 allow_under_saturated = True 140 for i in xrange(1, len(means)): 141 prev_mean = means[i-1] 142 mean = means[i] 143 144 if np.isclose(max(mean), white_level, rtol=SATURATION_TOL): 145 print "Saturated: white_level %f, max_mean %f"% (white_level, max(mean)) 146 break 147 148 if allow_under_saturated and np.allclose(mean, black_levels, rtol=BLK_LVL_TOL): 149 # All channel means are close to black level 150 continue 151 152 allow_under_saturated = False 153 # Check pixel means are increasing (with small tolerance) 154 channels = ["Red", "Gr", "Gb", "Blue"] 155 for chan in range(4): 156 err_msg = "ISO=%d, %s, exptime %3fms mean: %.2f, %s mean: %.2f, TOL=%.f%%" % ( 157 s, channels[chan], 158 e_test_ms[i-1], mean[chan], 159 "black level" if i == 1 else "exptime %3fms"%e_test_ms[i-2], 160 prev_mean[chan], 161 INCREASING_THR*100) 162 assert mean[chan] > prev_mean[chan] * INCREASING_THR, err_msg 163 164if __name__ == "__main__": 165 main() 166