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"""Verifies valid data return from CaptureResult objects."""
15
16
17import logging
18import os.path
19import matplotlib.pyplot
20from mobly import test_runner
21# mplot3 is required for 3D plots in draw_lsc_plot() though not called directly.
22from mpl_toolkits import mplot3d  # pylint: disable=unused-import
23import numpy as np
24
25# required for 3D plots
26import its_base_test
27import camera_properties_utils
28import capture_request_utils
29import its_session_utils
30
31AWB_GAINS_NUM = 4
32AWB_XFORM_NUM = 9
33ISCLOSE_ATOL = 0.05  # not for absolute ==, but if something grossly wrong
34MANUAL_AWB_GAINS = [1, 1.5, 2.0, 3.0]
35MANUAL_AWB_XFORM = capture_request_utils.float_to_rational([-1.5, -1.0, -0.5,
36                                                            0.0, 0.5, 1.0,
37                                                            1.5, 2.0, 3.0])
38# The camera HAL may not support different gains for two G channels.
39MANUAL_GAINS_OK = [[1, 1.5, 2.0, 3.0],
40                   [1, 1.5, 1.5, 3.0],
41                   [1, 2.0, 2.0, 3.0]]
42MANUAL_TONEMAP = [0, 0, 1, 1]  # Linear tonemap
43MANUAL_REGION = [{'x': 8, 'y': 8, 'width': 128, 'height': 128, 'weight': 1}]
44NAME = os.path.splitext(os.path.basename(__file__))[0]
45
46
47def is_close_rational(n1, n2):
48  return np.isclose(capture_request_utils.rational_to_float(n1),
49                    capture_request_utils.rational_to_float(n2),
50                    atol=ISCLOSE_ATOL)
51
52
53def draw_lsc_plot(lsc_map_w, lsc_map_h, lsc_map, name, log_path):
54  for ch in range(4):
55    fig = matplotlib.pyplot.figure()
56    ax = fig.gca(projection='3d')
57    xs = np.array([range(lsc_map_w)] * lsc_map_h).reshape(lsc_map_h, lsc_map_w)
58    ys = np.array([[i]*lsc_map_w for i in range(lsc_map_h)]).reshape(
59        lsc_map_h, lsc_map_w)
60    zs = np.array(lsc_map[ch::4]).reshape(lsc_map_h, lsc_map_w)
61    ax.plot_wireframe(xs, ys, zs)
62    matplotlib.pyplot.savefig('%s_plot_lsc_%s_ch%d.png' % (
63        os.path.join(log_path, NAME), name, ch))
64
65
66def metadata_checks(metadata, props):
67  """Common checks on AWB color correction matrix.
68
69  Args:
70    metadata: capture metadata
71    props: camera properties
72  """
73  awb_gains = metadata['android.colorCorrection.gains']
74  awb_xform = metadata['android.colorCorrection.transform']
75  logging.debug('AWB gains: %s', str(awb_gains))
76  logging.debug('AWB transform: %s', str(
77      [capture_request_utils.rational_to_float(t) for t in awb_xform]))
78  if props['android.control.maxRegionsAe'] > 0:
79    logging.debug('AE region: %s', str(metadata['android.control.aeRegions']))
80  if props['android.control.maxRegionsAf'] > 0:
81    logging.debug('AF region: %s', str(metadata['android.control.afRegions']))
82  if props['android.control.maxRegionsAwb'] > 0:
83    logging.debug('AWB region: %s', str(metadata['android.control.awbRegions']))
84
85  # Color correction gains and transform should be the same size
86  assert len(awb_gains) == AWB_GAINS_NUM
87  assert len(awb_xform) == AWB_XFORM_NUM
88
89
90def test_auto(cam, props, log_path):
91  """Do auto capture and test values.
92
93  Args:
94    cam: camera object
95    props: camera properties
96    log_path: path for plot directory
97  """
98  logging.debug('Testing auto capture results')
99  req = capture_request_utils.auto_capture_request()
100  req['android.statistics.lensShadingMapMode'] = 1
101  sync_latency = camera_properties_utils.sync_latency(props)
102
103  # Get 3A lock first, so auto values in capture result are populated properly.
104  mono_camera = camera_properties_utils.mono_camera(props)
105  cam.do_3a(do_af=False, mono_camera=mono_camera)
106
107  # Do capture
108  cap = its_session_utils.do_capture_with_latency(cam, req, sync_latency)
109  metadata = cap['metadata']
110
111  ctrl_mode = metadata['android.control.mode']
112  logging.debug('Control mode: %d', ctrl_mode)
113  assert ctrl_mode == 1, 'ctrl_mode: %d' % ctrl_mode
114
115  # Color correction gain and transform must be valid.
116  metadata_checks(metadata, props)
117  awb_gains = metadata['android.colorCorrection.gains']
118  awb_xform = metadata['android.colorCorrection.transform']
119  assert all([g > 0 for g in awb_gains])
120  assert all([t['denominator'] != 0 for t in awb_xform])
121
122  # Color correction should not match the manual settings.
123  assert not np.allclose(awb_gains, MANUAL_AWB_GAINS, atol=ISCLOSE_ATOL)
124  assert not all([is_close_rational(awb_xform[i], MANUAL_AWB_XFORM[i])
125                  for i in range(AWB_XFORM_NUM)])
126
127  # Exposure time must be valid.
128  exp_time = metadata['android.sensor.exposureTime']
129  assert exp_time > 0
130
131  # Draw lens shading correction map
132  lsc_obj = metadata['android.statistics.lensShadingCorrectionMap']
133  lsc_map = lsc_obj['map']
134  lsc_map_w = lsc_obj['width']
135  lsc_map_h = lsc_obj['height']
136  logging.debug('LSC map: %dx%d, %s', lsc_map_w, lsc_map_h, str(lsc_map[:8]))
137  draw_lsc_plot(lsc_map_w, lsc_map_h, lsc_map, 'auto', log_path)
138
139
140def test_manual(cam, props, log_path):
141  """Do manual capture and test results.
142
143  Args:
144    cam: camera object
145    props: camera properties
146    log_path: path for plot directory
147  """
148  logging.debug('Testing manual capture results')
149  exp_min = min(props['android.sensor.info.exposureTimeRange'])
150  sens_min = min(props['android.sensor.info.sensitivityRange'])
151  sync_latency = camera_properties_utils.sync_latency(props)
152  req = {
153      'android.control.mode': 0,
154      'android.control.aeMode': 0,
155      'android.control.awbMode': 0,
156      'android.control.afMode': 0,
157      'android.sensor.sensitivity': sens_min,
158      'android.sensor.exposureTime': exp_min,
159      'android.colorCorrection.mode': 0,
160      'android.colorCorrection.transform': MANUAL_AWB_XFORM,
161      'android.colorCorrection.gains': MANUAL_AWB_GAINS,
162      'android.tonemap.mode': 0,
163      'android.tonemap.curve': {'red': MANUAL_TONEMAP,
164                                'green': MANUAL_TONEMAP,
165                                'blue': MANUAL_TONEMAP},
166      'android.control.aeRegions': MANUAL_REGION,
167      'android.control.afRegions': MANUAL_REGION,
168      'android.control.awbRegions': MANUAL_REGION,
169      'android.statistics.lensShadingMapMode': 1
170      }
171  cap = its_session_utils.do_capture_with_latency(cam, req, sync_latency)
172  metadata = cap['metadata']
173
174  ctrl_mode = metadata['android.control.mode']
175  logging.debug('Control mode: %d', ctrl_mode)
176  assert ctrl_mode == 0, 'ctrl_mode: %d' % ctrl_mode
177
178  # Color correction gains and transform should be the same size and
179  # values as the manually set values.
180  metadata_checks(metadata, props)
181  awb_gains = metadata['android.colorCorrection.gains']
182  awb_xform = metadata['android.colorCorrection.transform']
183  assert (all([np.isclose(awb_gains[i], MANUAL_GAINS_OK[0][i],
184                          atol=ISCLOSE_ATOL) for i in range(AWB_GAINS_NUM)]) or
185          all([np.isclose(awb_gains[i], MANUAL_GAINS_OK[1][i],
186                          atol=ISCLOSE_ATOL) for i in range(AWB_GAINS_NUM)]) or
187          all([np.isclose(awb_gains[i], MANUAL_GAINS_OK[2][i],
188                          atol=ISCLOSE_ATOL) for i in range(AWB_GAINS_NUM)]))
189  assert (all([is_close_rational(awb_xform[i], MANUAL_AWB_XFORM[i])
190               for i in range(AWB_XFORM_NUM)]))
191
192  # The returned tonemap must be linear.
193  curves = [metadata['android.tonemap.curve']['red'],
194            metadata['android.tonemap.curve']['green'],
195            metadata['android.tonemap.curve']['blue']]
196  logging.debug('Tonemap: %s', str(curves[0][1::16]))
197  for c in curves:
198    assert c, 'c in curves is empty.'
199    assert all([np.isclose(c[i], c[i+1], atol=ISCLOSE_ATOL)
200                for i in range(0, len(c), 2)])
201
202  # Exposure time must be close to the requested exposure time.
203  exp_time = metadata['android.sensor.exposureTime']
204  assert np.isclose(exp_time*1.0E-6, exp_min*1.0E-6, atol=ISCLOSE_ATOL)
205
206  # Lens shading map must be valid
207  lsc_obj = metadata['android.statistics.lensShadingCorrectionMap']
208  lsc_map = lsc_obj['map']
209  lsc_map_w = lsc_obj['width']
210  lsc_map_h = lsc_obj['height']
211  logging.debug('LSC map: %dx%d, %s', lsc_map_w, lsc_map_h, str(lsc_map[:8]))
212  assert (lsc_map_w > 0 and lsc_map_h > 0 and
213          lsc_map_w*lsc_map_h*4 == len(lsc_map))
214  assert all([m >= 1 for m in lsc_map])
215
216  # Draw lens shading correction map
217  draw_lsc_plot(lsc_map_w, lsc_map_h, lsc_map, 'manual', log_path)
218
219
220class CaptureResult(its_base_test.ItsBaseTest):
221  """Test that valid data comes back in CaptureResult objects."""
222
223  def test_capture_result(self):
224    logging.debug('Starting %s', NAME)
225    with its_session_utils.ItsSession(
226        device_id=self.dut.serial,
227        camera_id=self.camera_id,
228        hidden_physical_id=self.hidden_physical_id) as cam:
229      props = cam.get_camera_properties()
230      props = cam.override_with_hidden_physical_camera_props(props)
231
232      # Check SKIP conditions
233      camera_properties_utils.skip_unless(
234          camera_properties_utils.manual_sensor(props) and
235          camera_properties_utils.manual_post_proc(props) and
236          camera_properties_utils.per_frame_control(props))
237
238      # Load chart for scene
239      its_session_utils.load_scene(
240          cam, props, self.scene, self.tablet, self.chart_distance)
241
242      # Run tests. Run auto, then manual, then auto. Check correct metadata
243      # values and ensure manual settings do not leak into auto captures.
244      test_auto(cam, props, self.log_path)
245      test_manual(cam, props, self.log_path)
246      test_auto(cam, props, self.log_path)
247
248
249if __name__ == '__main__':
250  test_runner.main()
251
252