1# Copyright 2016 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 post RAW sensitivity boost.""" 15 16 17import logging 18import math 19import os.path 20import matplotlib 21from matplotlib import pylab 22from mobly import test_runner 23 24import its_base_test 25import camera_properties_utils 26import capture_request_utils 27import error_util 28import image_processing_utils 29import its_session_utils 30import target_exposure_utils 31 32_COLORS = ('R', 'G', 'B') 33_MAX_YUV_SIZE = (1920, 1080) 34_NAME = os.path.splitext(os.path.basename(__file__))[0] 35_PATCH_H = 0.1 # center 10% 36_PATCH_W = 0.1 37_PATCH_X = 0.5 - _PATCH_W/2 38_PATCH_Y = 0.5 - _PATCH_H/2 39_RATIO_RTOL = 0.1 # +/-10% TOL on images vs expected values 40_RAW_PIXEL_THRESH = 0.03 # Waive check if RAW [0, 1] value below this thresh 41 42 43def create_requests(cam, props, log_path): 44 """Create the requests and settings lists.""" 45 w, h = capture_request_utils.get_available_output_sizes( 46 'yuv', props, _MAX_YUV_SIZE)[0] 47 48 if camera_properties_utils.raw16(props): 49 raw_format = 'raw' 50 elif camera_properties_utils.raw10(props): 51 raw_format = 'raw10' 52 elif camera_properties_utils.raw12(props): 53 raw_format = 'raw12' 54 else: # should not reach here 55 raise error_util.Error('Cannot find available RAW output format') 56 57 out_surfaces = [{'format': raw_format}, 58 {'format': 'yuv', 'width': w, 'height': h}] 59 sens_min, sens_max = props['android.sensor.info.sensitivityRange'] 60 sens_boost_min, sens_boost_max = props[ 61 'android.control.postRawSensitivityBoostRange'] 62 exp_target, sens_target = target_exposure_utils.get_target_exposure_combos( 63 log_path, cam)['midSensitivity'] 64 65 reqs = [] 66 settings = [] 67 sens_boost = sens_boost_min 68 while sens_boost <= sens_boost_max: 69 sens_raw = int(round(sens_target * 100.0 / sens_boost)) 70 if sens_raw < sens_min or sens_raw > sens_max: 71 break 72 req = capture_request_utils.manual_capture_request(sens_raw, exp_target) 73 req['android.control.postRawSensitivityBoost'] = sens_boost 74 reqs.append(req) 75 settings.append((sens_raw, sens_boost)) 76 if sens_boost == sens_boost_max: 77 break 78 sens_boost *= 2 79 # Always try to test maximum sensitivity boost value 80 if sens_boost > sens_boost_max: 81 sens_boost = sens_boost_max 82 83 return settings, reqs, out_surfaces 84 85 86def compute_patch_means(cap, props, file_name): 87 """Compute the RGB means for center patch of capture.""" 88 89 rgb_img = image_processing_utils.convert_capture_to_rgb_image( 90 cap, props=props) 91 patch = image_processing_utils.get_image_patch( 92 rgb_img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H) 93 image_processing_utils.write_image(patch, file_name) 94 return image_processing_utils.compute_image_means(patch) 95 96 97def create_plots(idx, raw_means, yuv_means, name_with_log_path): 98 """Create plots from data. 99 100 Args: 101 idx: capture request indices for x-axis. 102 raw_means: array of RAW capture RGB converted means. 103 yuv_means: array of YUV capture RGB converted means. 104 name_with_log_path: file name & path to save files. 105 """ 106 107 pylab.clf() 108 for i, _ in enumerate(_COLORS): 109 pylab.plot(idx, [ch[i] for ch in yuv_means], '-'+'rgb'[i]+'s', label='YUV', 110 alpha=0.7) 111 pylab.plot(idx, [ch[i] for ch in raw_means], '-'+'rgb'[i]+'o', label='RAW', 112 alpha=0.7) 113 pylab.ylim([0, 1]) 114 pylab.title(_NAME) 115 pylab.xlabel('requests') 116 pylab.ylabel('RGB means') 117 pylab.legend(loc='lower right', numpoints=1, fancybox=True) 118 matplotlib.pyplot.savefig(f'{name_with_log_path}_plot_means.png') 119 120 121class PostRawSensitivityBoost(its_base_test.ItsBaseTest): 122 """Check post RAW sensitivity boost. 123 124 Captures a set of RAW/YUV images with different sensitivity/post RAW 125 sensitivity boost combination and checks if output means match req settings 126 127 RAW images should get brighter. YUV images should stay about the same. 128 asserts RAW is ~2x brighter per step 129 asserts YUV is about the same per step 130 """ 131 132 def test_post_raw_sensitivity_boost(self): 133 logging.debug('Starting %s', _NAME) 134 with its_session_utils.ItsSession( 135 device_id=self.dut.serial, 136 camera_id=self.camera_id, 137 hidden_physical_id=self.hidden_physical_id) as cam: 138 props = cam.get_camera_properties() 139 props = cam.override_with_hidden_physical_camera_props(props) 140 camera_properties_utils.skip_unless( 141 camera_properties_utils.raw_output(props) and 142 camera_properties_utils.post_raw_sensitivity_boost(props) and 143 camera_properties_utils.compute_target_exposure(props) and 144 camera_properties_utils.per_frame_control(props) and 145 not camera_properties_utils.mono_camera(props)) 146 log_path = self.log_path 147 name_with_log_path = os.path.join(log_path, _NAME) 148 149 # Load chart for scene 150 its_session_utils.load_scene( 151 cam, props, self.scene, self.tablet, 152 its_session_utils.CHART_DISTANCE_NO_SCALING) 153 154 # Create reqs & do caps 155 settings, reqs, out_surfaces = create_requests(cam, props, log_path) 156 raw_caps, yuv_caps = cam.do_capture(reqs, out_surfaces) 157 if not isinstance(raw_caps, list): 158 raw_caps = [raw_caps] 159 if not isinstance(yuv_caps, list): 160 yuv_caps = [yuv_caps] 161 162 # Extract data 163 raw_means = [] 164 yuv_means = [] 165 for i in range(len(reqs)): 166 sens, sens_boost = settings[i] 167 sens_and_boost = f's={sens:04d}_boost={sens_boost:04d}' 168 raw_file_name = f'{name_with_log_path}_raw_{sens_and_boost}.jpg' 169 raw_means.append(compute_patch_means(raw_caps[i], props, raw_file_name)) 170 171 yuv_file_name = f'{name_with_log_path}_yuv_{sens_and_boost}.jpg' 172 yuv_means.append(compute_patch_means(yuv_caps[i], props, yuv_file_name)) 173 174 logging.debug('s=%d, s_boost=%d: raw_means %s, yuv_means %s', 175 sens, sens_boost, str(raw_means[-1]), str(yuv_means[-1])) 176 cap_idxs = range(len(reqs)) 177 178 # Create plots 179 create_plots(cap_idxs, raw_means, yuv_means, name_with_log_path) 180 181 # RAW asserts 182 for step in range(1, len(reqs)): 183 sens_prev, _ = settings[step - 1] 184 sens, sens_boost = settings[step] 185 expected_ratio = sens_prev / sens 186 for ch, _ in enumerate(_COLORS): 187 ratio_per_step = raw_means[step-1][ch] / raw_means[step][ch] 188 logging.debug('Step: (%d, %d) %s channel: (%f, %f), ratio: %f,', 189 step - 1, step, _COLORS[ch], raw_means[step - 1][ch], 190 raw_means[step][ch], ratio_per_step) 191 if raw_means[step][ch] <= _RAW_PIXEL_THRESH: 192 continue 193 if not math.isclose(ratio_per_step, expected_ratio, 194 rel_tol=_RATIO_RTOL): 195 raise AssertionError( 196 f'step: {step}, ratio: {ratio_per_step}, expected ratio: ' 197 f'{expected_ratio:.3f}, RTOL: {_RATIO_RTOL}') 198 199 # YUV asserts 200 for ch, _ in enumerate(_COLORS): 201 vals = [val[ch] for val in yuv_means] 202 for idx in cap_idxs: 203 if raw_means[idx][ch] <= _RAW_PIXEL_THRESH: 204 vals = vals[:idx] 205 mean = sum(vals) / len(vals) 206 logging.debug('%s channel vals %s mean %f', _COLORS[ch], vals, mean) 207 for step in range(len(vals)): 208 ratio_mean = vals[step] / mean 209 if not math.isclose(1.0, ratio_mean, rel_tol=_RATIO_RTOL): 210 raise AssertionError( 211 f'Capture vs mean ratio: {ratio_mean}, RTOL: +/- {_RATIO_RTOL}') 212 213if __name__ == '__main__': 214 test_runner.main() 215