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