• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2019 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
21
22import matplotlib
23from matplotlib import pylab
24import numpy as np
25
26AE_STATE_CONVERGED = 2
27AE_STATE_FLASH_REQUIRED = 4
28DELTA_GAIN_THRESH = 0.03  # >3% gain change --> luma change in same dir
29DELTA_LUMA_THRESH = 0.03  # 3% frame-to-frame noise test_burst_sameness_manual
30DELTA_NO_GAIN_THRESH = 0.01  # <1% gain change --> min luma change
31LSC_TOL = 0.005  # allow <0.5% change in lens shading correction
32NAME = os.path.basename(__file__).split('.')[0]
33NUM_CAPS = 1
34NUM_FRAMES = 30
35VALID_STABLE_LUMA_MIN = 0.1
36VALID_STABLE_LUMA_MAX = 0.9
37
38
39def lsc_unchanged(lsc_avlb, lsc, idx):
40    """Determine if lens shading correction unchanged.
41
42    Args:
43        lsc_avlb:   bool; True if lens shading correction available
44        lsc:        list; lens shading correction matrix
45        idx:        int; frame index
46    Returns:
47        boolean
48    """
49    if lsc_avlb:
50        diff = list((np.array(lsc[idx]) - np.array(lsc[idx-1])) /
51                    np.array(lsc[idx-1]))
52        diff = map(abs, diff)
53        max_abs_diff = max(diff)
54        if max_abs_diff > LSC_TOL:
55            print '  max abs(LSC) change:', round(max_abs_diff, 4)
56            return False
57        else:
58            return True
59    else:
60        return True
61
62
63def tonemap_unchanged(raw_cap, tonemap_g, idx):
64    """Determine if tonemap unchanged.
65
66    Args:
67        raw_cap:    bool; True if RAW capture
68        tonemap_g:  list; green tonemap
69        idx:        int; frame index
70    Returns:
71        boolean
72    """
73    if not raw_cap:
74        return tonemap_g[idx-1] == tonemap_g[idx]
75    else:
76        return True
77
78
79def is_awb_af_stable(cap_info, i):
80    awb_gains_0 = cap_info[i-1]['awb_gains']
81    awb_gains_1 = cap_info[i]['awb_gains']
82    ccm_0 = cap_info[i-1]['ccm']
83    ccm_1 = cap_info[i]['ccm']
84    fd_0 = cap_info[i-1]['fd']
85    fd_1 = cap_info[i]['fd']
86
87    return (np.allclose(awb_gains_0, awb_gains_1, rtol=0.01) and
88            ccm_0 == ccm_1 and np.isclose(fd_0, fd_1, rtol=0.01))
89
90
91def main():
92    """Tests PER_FRAME_CONTROL properties for auto capture requests.
93
94    If debug is required, MANUAL_POSTPROCESSING capability is implied
95    since its.caps.read_3a is valid for test. Debug can performed with
96    a defined tonemap curve:
97    req['android.tonemap.mode'] = 0
98    gamma = sum([[i/63.0,math.pow(i/63.0,1/2.2)] for i in xrange(64)],[])
99    req['android.tonemap.curve'] = {
100            'red': gamma, 'green': gamma, 'blue': gamma}
101    """
102
103    with its.device.ItsSession() as cam:
104        props = cam.get_camera_properties()
105        its.caps.skip_unless(its.caps.per_frame_control(props) and
106                             its.caps.read_3a(props))
107        debug = its.caps.debug_mode()
108        raw_avlb = its.caps.raw16(props)
109        largest_yuv = its.objects.get_largest_yuv_format(props)
110        match_ar = (largest_yuv['width'], largest_yuv['height'])
111        fmts = [its.objects.get_smallest_yuv_format(props, match_ar=match_ar)]
112        if raw_avlb and debug:
113            fmts.insert(0, cam.CAP_RAW)
114
115        failed = []
116        for f, fmt in enumerate(fmts):
117            print 'fmt:', fmt['format']
118            cam.do_3a()
119            req = its.objects.auto_capture_request()
120            cap_info = {}
121            ae_states = []
122            lumas = []
123            total_gains = []
124            tonemap_g = []
125            lsc = []
126            num_caps = NUM_CAPS
127            num_frames = NUM_FRAMES
128            raw_cap = f == 0 and raw_avlb and debug
129            lsc_avlb = its.caps.lsc_map(props) and not raw_cap
130            print 'lens shading correction available:', lsc_avlb
131            if lsc_avlb:
132                req['android.statistics.lensShadingMapMode'] = 1
133            name_suffix = 'YUV'
134            if raw_cap:
135                name_suffix = 'RAW'
136                # break up caps if RAW to reduce load
137                num_caps = NUM_CAPS * 6
138                num_frames = NUM_FRAMES / 6
139            for j in range(num_caps):
140                caps = cam.do_capture([req]*num_frames, fmt)
141                for i, cap in enumerate(caps):
142                    frame = {}
143                    idx = i + j * num_frames
144                    print '=========== frame %d ==========' % idx
145                    # RAW --> GR, YUV --> Y plane
146                    if raw_cap:
147                        plane = its.image.convert_capture_to_planes(
148                                cap, props=props)[1]
149                    else:
150                        plane = its.image.convert_capture_to_planes(cap)[0]
151                    tile = its.image.get_image_patch(
152                            plane, 0.45, 0.45, 0.1, 0.1)
153                    luma = its.image.compute_image_means(tile)[0]
154                    ae_state = cap['metadata']['android.control.aeState']
155                    iso = cap['metadata']['android.sensor.sensitivity']
156                    isp_gain = cap['metadata']['android.control.postRawSensitivityBoost']
157                    exp_time = cap['metadata']['android.sensor.exposureTime']
158                    total_gain = iso * isp_gain / 100.0 * exp_time / 1000000.0
159                    if raw_cap:
160                        total_gain = iso * exp_time / 1000000.0
161                    awb_state = cap['metadata']['android.control.awbState']
162                    frame['awb_gains'] = cap['metadata']['android.colorCorrection.gains']
163                    frame['ccm'] = cap['metadata']['android.colorCorrection.transform']
164                    frame['fd'] = cap['metadata']['android.lens.focusDistance']
165
166                    # Convert CCM from rational to float, as numpy arrays.
167                    awb_ccm = np.array(its.objects.rational_to_float(frame['ccm'])).reshape(3, 3)
168
169                    print 'AE: %d ISO: %d ISP_sen: %d exp(ns): %d tot_gain: %f' % (
170                            ae_state, iso, isp_gain, exp_time, total_gain),
171                    print 'luma: %f' % luma
172                    print 'fd: %f' % frame['fd']
173                    print 'AWB state: %d, AWB gains: %s\n AWB matrix: %s' % (
174                            awb_state, str(frame['awb_gains']),
175                            str(awb_ccm))
176                    if not raw_cap:
177                        tonemap = cap['metadata']['android.tonemap.curve']
178                        tonemap_g.append(tonemap['green'])
179                        print 'G tonemap curve:', tonemap_g[idx]
180                    if lsc_avlb:
181                        lsc.append(cap['metadata']['android.statistics.lensShadingCorrectionMap']['map'])
182
183                    img = its.image.convert_capture_to_rgb_image(
184                            cap, props=props)
185                    its.image.write_image(img, '%s_frame_%s_%d.jpg' % (
186                            NAME, name_suffix, idx))
187                    cap_info[idx] = frame
188                    ae_states.append(ae_state)
189                    lumas.append(luma)
190                    total_gains.append(total_gain)
191
192            norm_gains = [x/max(total_gains)*max(lumas) for x in total_gains]
193            pylab.figure(name_suffix)
194            pylab.plot(range(len(lumas)), lumas, '-g.',
195                       label='Center patch brightness')
196            pylab.plot(range(len(norm_gains)), norm_gains, '-r.',
197                       label='Metadata AE setting product')
198            pylab.title(NAME + ' ' + name_suffix)
199            pylab.xlabel('frame index')
200
201            # expand y axis for low delta results
202            ymin = min(norm_gains + lumas)
203            ymax = max(norm_gains + lumas)
204            yavg = (ymax + ymin)/2.0
205            if ymax-ymin < 3*DELTA_LUMA_THRESH:
206                ymin = round(yavg - 1.5*DELTA_LUMA_THRESH, 3)
207                ymax = round(yavg + 1.5*DELTA_LUMA_THRESH, 3)
208                pylab.ylim(ymin, ymax)
209            pylab.legend()
210            matplotlib.pyplot.savefig('%s_plot_%s.png' % (NAME, name_suffix))
211
212            print '\nfmt:', fmt['format']
213            for i in range(1, num_caps*num_frames):
214                if is_awb_af_stable(cap_info, i):
215                    prev_total_gain = total_gains[i-1]
216                    total_gain = total_gains[i]
217                    delta_gain = total_gain - prev_total_gain
218                    prev_luma = lumas[i-1]
219                    luma = lumas[i]
220                    delta_luma = luma - prev_luma
221                    delta_gain_rel = delta_gain / prev_total_gain
222                    delta_luma_rel = delta_luma / prev_luma
223                    # luma and total_gain should change in same direction
224                    msg = '%s: frame %d: gain %.1f -> %.1f (%.1f%%), ' % (
225                            fmt['format'], i, prev_total_gain, total_gain,
226                            delta_gain_rel*100)
227                    msg += 'luma %f -> %f (%.2f%%)  GAIN/LUMA OPPOSITE DIR' % (
228                            prev_luma, luma, delta_luma_rel*100)
229                    # Threshold change to trigger check. Small delta_gain might
230                    # not be enough to generate a reliable delta_luma to
231                    # overcome frame-to-frame variation.
232                    if (tonemap_unchanged(raw_cap, tonemap_g, i) and
233                                lsc_unchanged(lsc_avlb, lsc, i)):
234                        if abs(delta_gain_rel) > DELTA_GAIN_THRESH:
235                            print ' frame %d: %.2f%% delta gain,' % (
236                                    i, delta_gain_rel*100),
237                            print '%.2f%% delta luma' % (delta_luma_rel*100)
238                            if delta_gain * delta_luma < 0.0:
239                                failed.append(msg)
240                        elif abs(delta_gain_rel) < DELTA_NO_GAIN_THRESH:
241                            print ' frame %d: <|%.1f%%| delta gain,' % (
242                                    i, DELTA_NO_GAIN_THRESH*100),
243                            print '%.2f%% delta luma' % (delta_luma_rel*100)
244                            msg = '%s: ' % fmt['format']
245                            msg += 'frame %d: gain %.1f -> %.1f (%.1f%%), ' % (
246                                    i, prev_total_gain, total_gain,
247                                    delta_gain_rel*100)
248                            msg += 'luma %f -> %f (%.1f%%)  ' % (
249                                    prev_luma, luma, delta_luma_rel*100)
250                            msg += '<|%.1f%%| GAIN, >|%.f%%| LUMA DELTA' % (
251                                    DELTA_NO_GAIN_THRESH*100, DELTA_LUMA_THRESH*100)
252                            if abs(delta_luma_rel) > DELTA_LUMA_THRESH:
253                                failed.append(msg)
254                        else:
255                            print ' frame %d: %.1f%% delta gain,' % (
256                                    i, delta_gain_rel*100),
257                            print '%.2f%% delta luma' % (delta_luma_rel*100)
258                    else:
259                        print ' frame %d -> %d: tonemap' % (i-1, i),
260                        print 'or lens shading correction changed'
261                else:
262                    print ' frame %d -> %d: AWB/AF changed' % (i-1, i)
263
264            for i in range(len(lumas)):
265                luma = lumas[i]
266                ae_state = ae_states[i]
267                if (ae_state == AE_STATE_CONVERGED or
268                            ae_state == AE_STATE_FLASH_REQUIRED):
269                    msg = '%s: frame %d AE converged ' % (fmt['format'], i)
270                    msg += 'luma %f. valid range: (%f, %f)' % (
271                            luma, VALID_STABLE_LUMA_MIN, VALID_STABLE_LUMA_MAX)
272                    if VALID_STABLE_LUMA_MIN > luma > VALID_STABLE_LUMA_MAX:
273                        failed.append(msg)
274        if failed:
275            print '\nError summary'
276            for fail in failed:
277                print fail
278            assert not failed
279
280if __name__ == '__main__':
281    main()
282