1# Copyright 2014 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 RAW and YUV images are similar.""" 15 16 17import logging 18import os.path 19from mobly import test_runner 20 21import its_base_test 22import camera_properties_utils 23import capture_request_utils 24import image_processing_utils 25import its_session_utils 26 27_MAX_IMG_SIZE = (1920, 1080) 28_NAME = os.path.splitext(os.path.basename(__file__))[0] 29_NUM_RAW_CHANNELS = 4 # r, gr, gb, b 30_PATCH_H = 0.1 # center 10% 31_PATCH_W = 0.1 32_PATCH_X = 0.5 - _PATCH_W/2 33_PATCH_Y = 0.5 - _PATCH_H/2 34_POST_RAW_BOOST_REF = 100 # numbers larger than 100 increase YUV brightness 35_THRESHOLD_MAX_RMS_DIFF = 0.035 36 37 38def convert_and_compare_captures(cap_raw, cap_yuv, props, 39 log_path_with_name, raw_fmt): 40 """Helper function to convert and compare RAW and YUV captures. 41 42 Args: 43 cap_raw: capture request object with RAW/RAW10/RAW12 format specified 44 cap_yuv: capture capture request object with YUV format specified 45 props: object from its_session_utils.get_camera_properties(). 46 log_path_with_name: logging path where artifacts should be stored. 47 raw_fmt: string 'raw', 'raw10', or 'raw12' to include in file name 48 49 Returns: 50 string "PASS" if test passed, else message for AssertionError. 51 """ 52 shading_mode = cap_raw['metadata']['android.shading.mode'] 53 54 # YUV 55 img = image_processing_utils.convert_capture_to_rgb_image(cap_yuv) 56 image_processing_utils.write_image( 57 img, f'{log_path_with_name}_shading={shading_mode}_yuv.jpg', True) 58 patch = image_processing_utils.get_image_patch( 59 img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H) 60 rgb_means_yuv = image_processing_utils.compute_image_means(patch) 61 logging.debug('%s YUV RGB means: %s', raw_fmt, rgb_means_yuv) 62 63 # RAW 64 img = image_processing_utils.convert_raw_capture_to_rgb_image( 65 cap_raw, props, raw_fmt, log_path_with_name 66 ) 67 image_processing_utils.write_image( 68 img, f'{log_path_with_name}_shading={shading_mode}_{raw_fmt}.jpg', True) 69 70 # Shots are 1/2 x 1/2 smaller after conversion to RGB, but patch 71 # cropping is relative. 72 patch = image_processing_utils.get_image_patch( 73 img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H) 74 rgb_means_raw = image_processing_utils.compute_image_means(patch) 75 logging.debug('%s RAW RGB means: %s', raw_fmt, rgb_means_raw) 76 77 # Compensate for postRawSensitivityBoost in YUV. 78 if cap_yuv['metadata'].get('android.control.postRawSensitivityBoost'): 79 boost = cap_yuv['metadata']['android.control.postRawSensitivityBoost'] 80 logging.debug('postRawSensitivityBoost: %d', boost) 81 if boost != _POST_RAW_BOOST_REF: 82 rgb_means_raw = [m * boost / _POST_RAW_BOOST_REF for m in rgb_means_raw] 83 logging.debug('Post-boost %s RAW RGB means: %s', raw_fmt, rgb_means_raw) 84 85 rms_diff = image_processing_utils.compute_image_rms_difference_1d( 86 rgb_means_yuv, rgb_means_raw) 87 msg = f'{raw_fmt} diff: {rms_diff:.4f}' 88 # Log rms-diff, so that it can be written to the report log. 89 print(f'test_yuv_plus_{raw_fmt}_rms_diff: {rms_diff:.4f}') 90 logging.debug('%s', msg) 91 if rms_diff >= _THRESHOLD_MAX_RMS_DIFF: 92 return f'{msg}, spec: {_THRESHOLD_MAX_RMS_DIFF}' 93 else: 94 return 'PASS' 95 96 97class YuvPlusRawTest(its_base_test.ItsBaseTest): 98 """Test capturing a single frame as both YUV and various RAW formats. 99 100 Tests RAW, RAW10 and RAW12 as available. 101 """ 102 103 def test_yuv_plus_raw(self): 104 failure_messages = [] 105 with its_session_utils.ItsSession( 106 device_id=self.dut.serial, 107 camera_id=self.camera_id, 108 hidden_physical_id=self.hidden_physical_id) as cam: 109 props = cam.get_camera_properties() 110 props = cam.override_with_hidden_physical_camera_props(props) 111 log_path = os.path.join(self.log_path, _NAME) 112 113 # check SKIP conditions 114 camera_properties_utils.skip_unless( 115 camera_properties_utils.raw_output(props) and 116 camera_properties_utils.linear_tonemap(props) and 117 not camera_properties_utils.mono_camera(props)) 118 119 # Load chart for scene 120 its_session_utils.load_scene( 121 cam, props, self.scene, self.tablet, 122 its_session_utils.CHART_DISTANCE_NO_SCALING) 123 124 # determine compatible RAW formats 125 raw_formats = [] 126 if camera_properties_utils.raw16(props): 127 raw_formats.append('raw') 128 else: 129 logging.debug('Skipping test_yuv_plus_raw') 130 if camera_properties_utils.raw10(props): 131 raw_formats.append('raw10') 132 else: 133 logging.debug('Skipping test_yuv_plus_raw10') 134 if camera_properties_utils.raw12(props): 135 raw_formats.append('raw12') 136 else: 137 logging.debug('Skipping test_yuv_plus_raw12') 138 139 for raw_fmt in raw_formats: 140 req = capture_request_utils.auto_capture_request( 141 linear_tonemap=True, props=props, do_af=False) 142 max_raw_size = capture_request_utils.get_available_output_sizes( 143 raw_fmt, props)[0] 144 if capture_request_utils.is_common_aspect_ratio(max_raw_size): 145 w, h = capture_request_utils.get_available_output_sizes( 146 'yuv', props, _MAX_IMG_SIZE, max_raw_size)[0] 147 else: 148 w, h = capture_request_utils.get_available_output_sizes( 149 'yuv', props, max_size=_MAX_IMG_SIZE)[0] 150 out_surfaces = [{'format': raw_fmt}, 151 {'format': 'yuv', 'width': w, 'height': h}] 152 cam.do_3a(do_af=False) 153 req['android.statistics.lensShadingMapMode'] = ( 154 image_processing_utils.LENS_SHADING_MAP_ON) 155 cap_raw, cap_yuv = cam.do_capture(req, out_surfaces) 156 msg = convert_and_compare_captures(cap_raw, cap_yuv, props, 157 log_path, raw_fmt) 158 if msg != 'PASS': 159 failure_messages.append(msg) 160 161 if failure_messages: 162 raise AssertionError('\n'.join(failure_messages)) 163 164 165if __name__ == '__main__': 166 test_runner.main() 167 168