1# 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 os.path
16
17import its.caps
18import its.device
19import its.image
20import its.objects
21import its.target
22import matplotlib
23from matplotlib import pylab
24import numpy
25
26IMG_STATS_GRID = 9  # find used to find the center 11.11%
27NAME = os.path.basename(__file__).split('.')[0]
28THRESHOLD_MAX_OUTLIER_DIFF = 0.1
29THRESHOLD_MIN_LEVEL = 0.1
30THRESHOLD_MAX_LEVEL = 0.9
31THRESHOLD_MAX_LEVEL_DIFF = 0.045
32THRESHOLD_MAX_LEVEL_DIFF_WIDE_RANGE = 0.06
33THRESHOLD_ROUND_DOWN_GAIN = 0.1
34THRESHOLD_ROUND_DOWN_EXP = 0.05
35
36
37def get_raw_active_array_size(props):
38    """Return the active array w, h from props."""
39    aaw = (props['android.sensor.info.activeArraySize']['right'] -
40           props['android.sensor.info.activeArraySize']['left'])
41    aah = (props['android.sensor.info.activeArraySize']['bottom'] -
42           props['android.sensor.info.activeArraySize']['top'])
43    return aaw, aah
44
45
46def main():
47    """Test that a constant exposure is seen as ISO and exposure time vary.
48
49    Take a series of shots that have ISO and exposure time chosen to balance
50    each other; result should be the same brightness, but over the sequence
51    the images should get noisier.
52    """
53    mults = []
54    r_means = []
55    g_means = []
56    b_means = []
57    raw_r_means = []
58    raw_gr_means = []
59    raw_gb_means = []
60    raw_b_means = []
61    threshold_max_level_diff = THRESHOLD_MAX_LEVEL_DIFF
62
63    with its.device.ItsSession() as cam:
64        props = cam.get_camera_properties()
65        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
66                             its.caps.per_frame_control(props))
67
68        process_raw = (its.caps.compute_target_exposure(props) and
69                       its.caps.per_frame_control(props) and
70                       its.caps.raw16(props) and
71                       its.caps.manual_sensor(props))
72
73        debug = its.caps.debug_mode()
74        largest_yuv = its.objects.get_largest_yuv_format(props)
75        if debug:
76            fmt = largest_yuv
77        else:
78            match_ar = (largest_yuv['width'], largest_yuv['height'])
79            fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar)
80
81        e, s = its.target.get_target_exposure_combos(cam)['minSensitivity']
82        s_e_product = s*e
83        expt_range = props['android.sensor.info.exposureTimeRange']
84        sens_range = props['android.sensor.info.sensitivityRange']
85
86        m = 1.0
87        while s*m < sens_range[1] and e/m > expt_range[0]:
88            mults.append(m)
89            s_test = round(s*m)
90            e_test = s_e_product / s_test
91            print 'Testing s:', s_test, 'e:', e_test
92            req = its.objects.manual_capture_request(
93                s_test, e_test, 0.0, True, props)
94            cap = cam.do_capture(req, fmt)
95            s_res = cap['metadata']['android.sensor.sensitivity']
96            e_res = cap['metadata']['android.sensor.exposureTime']
97            assert 0 <= s_test - s_res < s_test * THRESHOLD_ROUND_DOWN_GAIN
98            assert 0 <= e_test - e_res < e_test * THRESHOLD_ROUND_DOWN_EXP
99            s_e_product_res = s_res * e_res
100            request_result_ratio = s_e_product / s_e_product_res
101            print 'Capture result s:', s_test, 'e:', e_test
102            img = its.image.convert_capture_to_rgb_image(cap)
103            its.image.write_image(img, '%s_mult=%3.2f.jpg' % (NAME, m))
104            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
105            rgb_means = its.image.compute_image_means(tile)
106            # Adjust for the difference between request and result
107            r_means.append(rgb_means[0] * request_result_ratio)
108            g_means.append(rgb_means[1] * request_result_ratio)
109            b_means.append(rgb_means[2] * request_result_ratio)
110            # do same in RAW space if possible
111            if process_raw and debug:
112                aaw, aah = get_raw_active_array_size(props)
113                raw_cap = cam.do_capture(req,
114                                         {'format': 'rawStats',
115                                          'gridWidth': aaw/IMG_STATS_GRID,
116                                          'gridHeight': aah/IMG_STATS_GRID})
117                r, gr, gb, b = its.image.convert_capture_to_planes(raw_cap,
118                                                                   props)
119                raw_r_means.append(r[IMG_STATS_GRID/2, IMG_STATS_GRID/2]
120                                   * request_result_ratio)
121                raw_gr_means.append(gr[IMG_STATS_GRID/2, IMG_STATS_GRID/2]
122                                    * request_result_ratio)
123                raw_gb_means.append(gb[IMG_STATS_GRID/2, IMG_STATS_GRID/2]
124                                    * request_result_ratio)
125                raw_b_means.append(b[IMG_STATS_GRID/2, IMG_STATS_GRID/2]
126                                   * request_result_ratio)
127            # Test 3 steps per 2x gain
128            m *= pow(2, 1.0 / 3)
129
130        # Allow more threshold for devices with wider exposure range
131        if m >= 64.0:
132            threshold_max_level_diff = THRESHOLD_MAX_LEVEL_DIFF_WIDE_RANGE
133
134    # Draw plots
135    pylab.figure('rgb data')
136    pylab.plot(mults, r_means, 'ro-')
137    pylab.plot(mults, g_means, 'go-')
138    pylab.plot(mults, b_means, 'bo-')
139    pylab.title(NAME + 'RGB Data')
140    pylab.xlabel('Gain Multiplier')
141    pylab.ylabel('Normalized RGB Plane Avg')
142    pylab.ylim([0, 1])
143    matplotlib.pyplot.savefig('%s_plot_means.png' % (NAME))
144
145    if process_raw and debug:
146        pylab.figure('raw data')
147        pylab.plot(mults, raw_r_means, 'ro-', label='R')
148        pylab.plot(mults, raw_gr_means, 'go-', label='GR')
149        pylab.plot(mults, raw_gb_means, 'ko-', label='GB')
150        pylab.plot(mults, raw_b_means, 'bo-', label='B')
151        pylab.title(NAME + 'RAW Data')
152        pylab.xlabel('Gain Multiplier')
153        pylab.ylabel('Normalized RAW Plane Avg')
154        pylab.ylim([0, 1])
155        pylab.legend(numpoints=1)
156        matplotlib.pyplot.savefig('%s_plot_raw_means.png' % (NAME))
157
158    # Check for linearity. Verify sample pixel mean values are close to each
159    # other. Also ensure that the images aren't clamped to 0 or 1
160    # (which would make them look like flat lines).
161    for chan in xrange(3):
162        values = [r_means, g_means, b_means][chan]
163        m, b = numpy.polyfit(mults, values, 1).tolist()
164        max_val = max(values)
165        min_val = min(values)
166        max_diff = max_val - min_val
167        print 'Channel %d line fit (y = mx+b): m = %f, b = %f' % (chan, m, b)
168        print 'Channel max %f min %f diff %f' % (max_val, min_val, max_diff)
169        assert max_diff < threshold_max_level_diff
170        assert b > THRESHOLD_MIN_LEVEL and b < THRESHOLD_MAX_LEVEL
171        for v in values:
172            assert v > THRESHOLD_MIN_LEVEL and v < THRESHOLD_MAX_LEVEL
173            assert abs(v - b) < THRESHOLD_MAX_OUTLIER_DIFF
174    if process_raw and debug:
175        for chan in xrange(4):
176            values = [raw_r_means, raw_gr_means, raw_gb_means,
177                      raw_b_means][chan]
178            m, b = numpy.polyfit(mults, values, 1).tolist()
179            max_val = max(values)
180            min_val = min(values)
181            max_diff = max_val - min_val
182            print 'Channel %d line fit (y = mx+b): m = %f, b = %f' % (chan,
183                                                                      m, b)
184            print 'Channel max %f min %f diff %f' % (max_val, min_val, max_diff)
185            assert max_diff < threshold_max_level_diff
186            assert b > THRESHOLD_MIN_LEVEL and b < THRESHOLD_MAX_LEVEL
187            for v in values:
188                assert v > THRESHOLD_MIN_LEVEL and v < THRESHOLD_MAX_LEVEL
189                assert abs(v - b) < THRESHOLD_MAX_OUTLIER_DIFF
190
191if __name__ == '__main__':
192    main()
193