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