| # Copyright 2013 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Verifies valid data return from CaptureResult objects.""" |
| |
| |
| import logging |
| import os.path |
| import matplotlib.pyplot |
| from mobly import test_runner |
| # mplot3 is required for 3D plots in draw_lsc_plot() though not called directly. |
| from mpl_toolkits import mplot3d # pylint: disable=unused-import |
| import numpy as np |
| |
| # required for 3D plots |
| import its_base_test |
| import camera_properties_utils |
| import capture_request_utils |
| import its_session_utils |
| |
| AWB_GAINS_NUM = 4 |
| AWB_XFORM_NUM = 9 |
| ISCLOSE_ATOL = 0.05 # not for absolute ==, but if something grossly wrong |
| MANUAL_AWB_GAINS = [1, 1.5, 2.0, 3.0] |
| MANUAL_AWB_XFORM = capture_request_utils.float_to_rational([-1.5, -1.0, -0.5, |
| 0.0, 0.5, 1.0, |
| 1.5, 2.0, 3.0]) |
| # The camera HAL may not support different gains for two G channels. |
| MANUAL_GAINS_OK = [[1, 1.5, 2.0, 3.0], |
| [1, 1.5, 1.5, 3.0], |
| [1, 2.0, 2.0, 3.0]] |
| MANUAL_TONEMAP = [0, 0, 1, 1] # Linear tonemap |
| MANUAL_REGION = [{'x': 8, 'y': 8, 'width': 128, 'height': 128, 'weight': 1}] |
| NAME = os.path.splitext(os.path.basename(__file__))[0] |
| |
| |
| def is_close_rational(n1, n2): |
| return np.isclose(capture_request_utils.rational_to_float(n1), |
| capture_request_utils.rational_to_float(n2), |
| atol=ISCLOSE_ATOL) |
| |
| |
| def draw_lsc_plot(lsc_map_w, lsc_map_h, lsc_map, name, log_path): |
| for ch in range(4): |
| fig = matplotlib.pyplot.figure() |
| ax = fig.gca(projection='3d') |
| xs = np.array([range(lsc_map_w)] * lsc_map_h).reshape(lsc_map_h, lsc_map_w) |
| ys = np.array([[i]*lsc_map_w for i in range(lsc_map_h)]).reshape( |
| lsc_map_h, lsc_map_w) |
| zs = np.array(lsc_map[ch::4]).reshape(lsc_map_h, lsc_map_w) |
| ax.plot_wireframe(xs, ys, zs) |
| matplotlib.pyplot.savefig('%s_plot_lsc_%s_ch%d.png' % ( |
| os.path.join(log_path, NAME), name, ch)) |
| |
| |
| def metadata_checks(metadata, props): |
| """Common checks on AWB color correction matrix. |
| |
| Args: |
| metadata: capture metadata |
| props: camera properties |
| """ |
| awb_gains = metadata['android.colorCorrection.gains'] |
| awb_xform = metadata['android.colorCorrection.transform'] |
| logging.debug('AWB gains: %s', str(awb_gains)) |
| logging.debug('AWB transform: %s', str( |
| [capture_request_utils.rational_to_float(t) for t in awb_xform])) |
| if props['android.control.maxRegionsAe'] > 0: |
| logging.debug('AE region: %s', str(metadata['android.control.aeRegions'])) |
| if props['android.control.maxRegionsAf'] > 0: |
| logging.debug('AF region: %s', str(metadata['android.control.afRegions'])) |
| if props['android.control.maxRegionsAwb'] > 0: |
| logging.debug('AWB region: %s', str(metadata['android.control.awbRegions'])) |
| |
| # Color correction gains and transform should be the same size |
| if len(awb_gains) != AWB_GAINS_NUM: |
| raise AssertionError(f'AWB gains wrong length! {awb_gains}') |
| if len(awb_xform) != AWB_XFORM_NUM: |
| raise AssertionError(f'AWB transform wrong length! {awb_xform}') |
| |
| |
| def test_auto(cam, props, log_path): |
| """Do auto capture and test values. |
| |
| Args: |
| cam: camera object |
| props: camera properties |
| log_path: path for plot directory |
| """ |
| logging.debug('Testing auto capture results') |
| req = capture_request_utils.auto_capture_request() |
| req['android.statistics.lensShadingMapMode'] = 1 |
| sync_latency = camera_properties_utils.sync_latency(props) |
| |
| # Get 3A lock first, so auto values in capture result are populated properly. |
| mono_camera = camera_properties_utils.mono_camera(props) |
| cam.do_3a(do_af=False, mono_camera=mono_camera) |
| |
| # Do capture |
| cap = its_session_utils.do_capture_with_latency(cam, req, sync_latency) |
| metadata = cap['metadata'] |
| |
| ctrl_mode = metadata['android.control.mode'] |
| logging.debug('Control mode: %d', ctrl_mode) |
| if ctrl_mode != 1: |
| raise AssertionError(f'ctrl_mode != 1: {ctrl_mode}') |
| |
| # Color correction gain and transform must be valid. |
| metadata_checks(metadata, props) |
| awb_gains = metadata['android.colorCorrection.gains'] |
| awb_xform = metadata['android.colorCorrection.transform'] |
| if not all([g > 0 for g in awb_gains]): |
| raise AssertionError(f'AWB gains has negative terms: {awb_gains}') |
| if not all([t['denominator'] != 0 for t in awb_xform]): |
| raise AssertionError(f'AWB transform has 0 denominators: {awb_xform}') |
| |
| # Color correction should not match the manual settings. |
| if np.allclose(awb_gains, MANUAL_AWB_GAINS, atol=ISCLOSE_ATOL): |
| raise AssertionError('Manual and automatic AWB gains are same! ' |
| f'manual: {MANUAL_AWB_GAINS}, auto: {awb_gains}, ' |
| f'ATOL: {ISCLOSE_ATOL}') |
| if all([is_close_rational(awb_xform[i], MANUAL_AWB_XFORM[i]) |
| for i in range(AWB_XFORM_NUM)]): |
| raise AssertionError('Manual and automatic AWB transforms are same! ' |
| f'manual: {MANUAL_AWB_XFORM}, auto: {awb_xform}, ' |
| f'ATOL: {ISCLOSE_ATOL}') |
| |
| # Exposure time must be valid. |
| exp_time = metadata['android.sensor.exposureTime'] |
| if exp_time <= 0: |
| raise AssertionError(f'exposure time is <= 0! {exp_time}') |
| |
| # Draw lens shading correction map |
| lsc_obj = metadata['android.statistics.lensShadingCorrectionMap'] |
| lsc_map = lsc_obj['map'] |
| lsc_map_w = lsc_obj['width'] |
| lsc_map_h = lsc_obj['height'] |
| logging.debug('LSC map: %dx%d, %s', lsc_map_w, lsc_map_h, str(lsc_map[:8])) |
| draw_lsc_plot(lsc_map_w, lsc_map_h, lsc_map, 'auto', log_path) |
| |
| |
| def test_manual(cam, props, log_path): |
| """Do manual capture and test results. |
| |
| Args: |
| cam: camera object |
| props: camera properties |
| log_path: path for plot directory |
| """ |
| logging.debug('Testing manual capture results') |
| exp_min = min(props['android.sensor.info.exposureTimeRange']) |
| sens_min = min(props['android.sensor.info.sensitivityRange']) |
| sync_latency = camera_properties_utils.sync_latency(props) |
| req = { |
| 'android.control.mode': 0, |
| 'android.control.aeMode': 0, |
| 'android.control.awbMode': 0, |
| 'android.control.afMode': 0, |
| 'android.sensor.sensitivity': sens_min, |
| 'android.sensor.exposureTime': exp_min, |
| 'android.colorCorrection.mode': 0, |
| 'android.colorCorrection.transform': MANUAL_AWB_XFORM, |
| 'android.colorCorrection.gains': MANUAL_AWB_GAINS, |
| 'android.tonemap.mode': 0, |
| 'android.tonemap.curve': {'red': MANUAL_TONEMAP, |
| 'green': MANUAL_TONEMAP, |
| 'blue': MANUAL_TONEMAP}, |
| 'android.control.aeRegions': MANUAL_REGION, |
| 'android.control.afRegions': MANUAL_REGION, |
| 'android.control.awbRegions': MANUAL_REGION, |
| 'android.statistics.lensShadingMapMode': 1 |
| } |
| cap = its_session_utils.do_capture_with_latency(cam, req, sync_latency) |
| metadata = cap['metadata'] |
| |
| ctrl_mode = metadata['android.control.mode'] |
| logging.debug('Control mode: %d', ctrl_mode) |
| if ctrl_mode != 0: |
| raise AssertionError(f'ctrl_mode: {ctrl_mode}') |
| |
| # Color correction gains and transform should be the same size and |
| # values as the manually set values. |
| metadata_checks(metadata, props) |
| awb_gains = metadata['android.colorCorrection.gains'] |
| awb_xform = metadata['android.colorCorrection.transform'] |
| if not (all([np.isclose(awb_gains[i], MANUAL_GAINS_OK[0][i], |
| atol=ISCLOSE_ATOL) for i in range(AWB_GAINS_NUM)]) or |
| all([np.isclose(awb_gains[i], MANUAL_GAINS_OK[1][i], |
| atol=ISCLOSE_ATOL) for i in range(AWB_GAINS_NUM)]) or |
| all([np.isclose(awb_gains[i], MANUAL_GAINS_OK[2][i], |
| atol=ISCLOSE_ATOL) for i in range(AWB_GAINS_NUM)])): |
| raise AssertionError('request/capture mismatch in AWB gains! ' |
| f'req: {MANUAL_GAINS_OK}, cap: {awb_gains}, ' |
| f'ATOL: {ISCLOSE_ATOL}') |
| if not (all([is_close_rational(awb_xform[i], MANUAL_AWB_XFORM[i]) |
| for i in range(AWB_XFORM_NUM)])): |
| raise AssertionError('request/capture mismatch in AWB transforms! ' |
| f'req: {MANUAL_AWB_XFORM}, cap: {awb_xform}, ' |
| f'ATOL: {ISCLOSE_ATOL}') |
| |
| # The returned tonemap must be linear. |
| curves = [metadata['android.tonemap.curve']['red'], |
| metadata['android.tonemap.curve']['green'], |
| metadata['android.tonemap.curve']['blue']] |
| logging.debug('Tonemap: %s', str(curves[0][1::16])) |
| for j, c in enumerate(curves): |
| if not c: |
| raise AssertionError('c in curves is empty.') |
| if not all([np.isclose(c[i], c[i+1], atol=ISCLOSE_ATOL) |
| for i in range(0, len(c), 2)]): |
| raise AssertionError(f"tonemap 'RGB'[i] is not linear! {c}") |
| |
| # Exposure time must be close to the requested exposure time. |
| exp_time = metadata['android.sensor.exposureTime'] |
| if not np.isclose(exp_time, exp_min, atol=ISCLOSE_ATOL/1E-06): |
| raise AssertionError('request/capture exposure time mismatch! ' |
| f'req: {exp_min}, cap: {exp_time}, ' |
| f'ATOL: {ISCLOSE_ATOL/1E-6}') |
| |
| # Lens shading map must be valid |
| lsc_obj = metadata['android.statistics.lensShadingCorrectionMap'] |
| lsc_map = lsc_obj['map'] |
| lsc_map_w = lsc_obj['width'] |
| lsc_map_h = lsc_obj['height'] |
| logging.debug('LSC map: %dx%d, %s', lsc_map_w, lsc_map_h, str(lsc_map[:8])) |
| if not (lsc_map_w > 0 and lsc_map_h > 0 and |
| lsc_map_w*lsc_map_h*4 == len(lsc_map)): |
| raise AssertionError(f'Incorrect lens shading map size! {lsc_map}') |
| if not all([m >= 1 for m in lsc_map]): |
| raise AssertionError(f'Lens shading map has negative vals! {lsc_map}') |
| |
| # Draw lens shading correction map |
| draw_lsc_plot(lsc_map_w, lsc_map_h, lsc_map, 'manual', log_path) |
| |
| |
| class CaptureResult(its_base_test.ItsBaseTest): |
| """Test that valid data comes back in CaptureResult objects.""" |
| |
| def test_capture_result(self): |
| logging.debug('Starting %s', NAME) |
| with its_session_utils.ItsSession( |
| device_id=self.dut.serial, |
| camera_id=self.camera_id, |
| hidden_physical_id=self.hidden_physical_id) as cam: |
| props = cam.get_camera_properties() |
| props = cam.override_with_hidden_physical_camera_props(props) |
| |
| # Check SKIP conditions |
| camera_properties_utils.skip_unless( |
| camera_properties_utils.manual_sensor(props) and |
| camera_properties_utils.manual_post_proc(props) and |
| camera_properties_utils.per_frame_control(props)) |
| |
| # Load chart for scene |
| its_session_utils.load_scene( |
| cam, props, self.scene, self.tablet, self.chart_distance) |
| |
| # Run tests. Run auto, then manual, then auto. Check correct metadata |
| # values and ensure manual settings do not leak into auto captures. |
| test_auto(cam, props, self.log_path) |
| test_manual(cam, props, self.log_path) |
| test_auto(cam, props, self.log_path) |
| |
| |
| if __name__ == '__main__': |
| test_runner.main() |
| |