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