# Copyright 2018 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os.path import its.caps import its.device import its.image import its.objects from matplotlib import pylab import matplotlib.pyplot import numpy as np IMG_STATS_GRID = 9 # find used to find the center 11.11% NAME = os.path.basename(__file__).split(".")[0] NUM_ISO_STEPS = 5 SATURATION_TOL = 0.01 BLK_LVL_TOL = 0.1 EXP_MULT_SHORT = pow(2, 1.0/3) # Test 3 steps per 2x exposure EXP_MULT_LONG = pow(10, 1.0/3) # Test 3 steps per 10x exposure EXP_LONG = 1E6 # 1ms INCREASING_THR = 0.99 # slice captures into burst of SLICE_LEN requests SLICE_LEN = 10 def main(): """Capture a set of raw images with increasing exposure time and measure the pixel values. """ with its.device.ItsSession() as cam: props = cam.get_camera_properties() props = cam.override_with_hidden_physical_camera_props(props) its.caps.skip_unless(its.caps.raw16(props) and its.caps.manual_sensor(props) and its.caps.per_frame_control(props) and not its.caps.mono_camera(props)) debug = its.caps.debug_mode() # Expose for the scene with min sensitivity exp_min, exp_max = props["android.sensor.info.exposureTimeRange"] sens_min, _ = props["android.sensor.info.sensitivityRange"] # Digital gains might not be visible on RAW data sens_max = props["android.sensor.maxAnalogSensitivity"] sens_step = (sens_max - sens_min) / NUM_ISO_STEPS white_level = float(props["android.sensor.info.whiteLevel"]) black_levels = [its.image.get_black_level(i, props) for i in range(4)] # Get the active array width and height. aax = props["android.sensor.info.preCorrectionActiveArraySize"]["left"] aay = props["android.sensor.info.preCorrectionActiveArraySize"]["top"] aaw = props["android.sensor.info.preCorrectionActiveArraySize"]["right"]-aax aah = props["android.sensor.info.preCorrectionActiveArraySize"]["bottom"]-aay raw_stat_fmt = {"format": "rawStats", "gridWidth": aaw/IMG_STATS_GRID, "gridHeight": aah/IMG_STATS_GRID} e_test = [] mult = 1.0 while exp_min*mult < exp_max: e_test.append(int(exp_min*mult)) if exp_min*mult < EXP_LONG: mult *= EXP_MULT_SHORT else: mult *= EXP_MULT_LONG if e_test[-1] < exp_max * INCREASING_THR: e_test.append(int(exp_max)) e_test_ms = [e / 1000000.0 for e in e_test] for s in range(sens_min, sens_max, sens_step): means = [] means.append(black_levels) reqs = [its.objects.manual_capture_request(s, e, 0) for e in e_test] # Capture raw in debug mode, rawStats otherwise caps = [] slice_len = SLICE_LEN # Eliminate cap burst of 1: returns [[]], not [{}, ...] while len(reqs) % slice_len == 1: slice_len -= 1 # Break caps into smaller bursts for i in range(len(reqs) / slice_len): if debug: caps += cam.do_capture(reqs[i*slice_len:(i+1)*slice_len], cam.CAP_RAW) else: caps += cam.do_capture(reqs[i*slice_len:(i+1)*slice_len], raw_stat_fmt) last_n = len(reqs) % slice_len if last_n: if debug: caps += cam.do_capture(reqs[-last_n:], cam.CAP_RAW) else: caps += cam.do_capture(reqs[-last_n:], raw_stat_fmt) # Measure the mean of each channel. # Each shot should be brighter (except underexposed/overexposed scene) for i, cap in enumerate(caps): if debug: planes = its.image.convert_capture_to_planes(cap, props) tiles = [its.image.get_image_patch(p, 0.445, 0.445, 0.11, 0.11) for p in planes] mean = [m * white_level for tile in tiles for m in its.image.compute_image_means(tile)] img = its.image.convert_capture_to_rgb_image(cap, props=props) its.image.write_image(img, "%s_s=%d_e=%05d.jpg" % (NAME, s, e_test[i])) else: mean_image, _ = its.image.unpack_rawstats_capture(cap) mean = mean_image[IMG_STATS_GRID/2, IMG_STATS_GRID/2] print "ISO=%d, exposure time=%.3fms, mean=%s" % ( s, e_test[i] / 1000000.0, str(mean)) means.append(mean) # means[0] is black level value r = [m[0] for m in means[1:]] gr = [m[1] for m in means[1:]] gb = [m[2] for m in means[1:]] b = [m[3] for m in means[1:]] pylab.plot(e_test_ms, r, "r.-") pylab.plot(e_test_ms, b, "b.-") pylab.plot(e_test_ms, gr, "g.-") pylab.plot(e_test_ms, gb, "k.-") pylab.xscale("log") pylab.yscale("log") pylab.title("%s ISO=%d" % (NAME, s)) pylab.xlabel("Exposure time (ms)") pylab.ylabel("Center patch pixel mean") matplotlib.pyplot.savefig("%s_s=%d.png" % (NAME, s)) pylab.clf() allow_under_saturated = True for i in xrange(1, len(means)): prev_mean = means[i-1] mean = means[i] if np.isclose(max(mean), white_level, rtol=SATURATION_TOL): print "Saturated: white_level %f, max_mean %f"% (white_level, max(mean)) break if allow_under_saturated and np.allclose(mean, black_levels, rtol=BLK_LVL_TOL): # All channel means are close to black level continue allow_under_saturated = False # Check pixel means are increasing (with small tolerance) channels = ["Red", "Gr", "Gb", "Blue"] for chan in range(4): err_msg = "ISO=%d, %s, exptime %3fms mean: %.2f, %s mean: %.2f, TOL=%.f%%" % ( s, channels[chan], e_test_ms[i-1], mean[chan], "black level" if i == 1 else "exptime %3fms"%e_test_ms[i-2], prev_mean[chan], INCREASING_THR*100) assert mean[chan] > prev_mean[chan] * INCREASING_THR, err_msg if __name__ == "__main__": main()