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