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"""Utility functions to calculate targeted exposures based on camera properties. 15""" 16 17import json 18import logging 19import os 20import sys 21import unittest 22 23import capture_request_utils 24import image_processing_utils 25import its_session_utils 26 27_CACHE_FILENAME = 'its.target.cfg' 28_REGION_3A = [[0.45, 0.45, 0.1, 0.1, 1]] 29 30 31def get_target_exposure_combos(output_path, its_session=None): 32 """Get a set of legal combinations of target (exposure time, sensitivity). 33 34 Gets the target exposure value, which is a product of sensitivity (ISO) and 35 exposure time, and returns equivalent tuples of (exposure time,sensitivity) 36 that are all legal and that correspond to the four extrema in this 2D param 37 space, as well as to two "middle" points. 38 39 Will open a device session if its_session is None. 40 41 Args: 42 output_path: String, path where the target.cfg file will be saved. 43 its_session: Optional, holding an open device session. 44 45 Returns: 46 Object containing six legal (exposure time, sensitivity) tuples, keyed 47 by the following strings: 48 'minExposureTime' 49 'midExposureTime' 50 'maxExposureTime' 51 'minSensitivity' 52 'midSensitivity' 53 'maxSensitivity' 54 """ 55 target_config_filename = os.path.join(output_path, _CACHE_FILENAME) 56 57 if its_session is None: 58 with its_session_utils.ItsSession() as cam: 59 exposure = get_target_exposure(target_config_filename, cam) 60 props = cam.get_camera_properties() 61 props = cam.override_with_hidden_physical_camera_props(props) 62 else: 63 exposure = get_target_exposure(target_config_filename, its_session) 64 props = its_session.get_camera_properties() 65 props = its_session.override_with_hidden_physical_camera_props(props) 66 67 sens_range = props['android.sensor.info.sensitivityRange'] 68 exp_time_range = props['android.sensor.info.exposureTimeRange'] 69 70 # Combo 1: smallest legal exposure time. 71 e1_expt = exp_time_range[0] 72 e1_sens = exposure / e1_expt 73 if e1_sens > sens_range[1]: 74 e1_sens = sens_range[1] 75 e1_expt = exposure / e1_sens 76 e1_logging = (f'e1 exp: {e1_expt}, sens: {e1_sens}') 77 logging.debug('%s', e1_logging) 78 79 # Combo 2: largest legal exposure time. 80 e2_expt = exp_time_range[1] 81 e2_sens = exposure / e2_expt 82 if e2_sens < sens_range[0]: 83 e2_sens = sens_range[0] 84 e2_expt = exposure / e2_sens 85 e2_logging = (f'e2 exp: {e2_expt}, sens: {e2_sens}') 86 logging.debug('%s', e2_logging) 87 88 # Combo 3: smallest legal sensitivity. 89 e3_sens = sens_range[0] 90 e3_expt = exposure / e3_sens 91 if e3_expt > exp_time_range[1]: 92 e3_expt = exp_time_range[1] 93 e3_sens = exposure / e3_expt 94 e3_logging = (f'e3 exp: {e3_expt}, sens: {e3_sens}') 95 logging.debug('%s', e3_logging) 96 97 # Combo 4: largest legal sensitivity. 98 e4_sens = sens_range[1] 99 e4_expt = exposure / e4_sens 100 if e4_expt < exp_time_range[0]: 101 e4_expt = exp_time_range[0] 102 e4_sens = exposure / e4_expt 103 e4_logging = (f'e4 exp: {e4_expt}, sens: {e4_sens}') 104 logging.debug('%s', e4_logging) 105 106 # Combo 5: middle exposure time. 107 e5_expt = (exp_time_range[0] + exp_time_range[1]) / 2.0 108 e5_sens = exposure / e5_expt 109 if e5_sens > sens_range[1]: 110 e5_sens = sens_range[1] 111 e5_expt = exposure / e5_sens 112 if e5_sens < sens_range[0]: 113 e5_sens = sens_range[0] 114 e5_expt = exposure / e5_sens 115 e5_logging = (f'e5 exp: {e5_expt}, sens: {e5_sens}') 116 logging.debug('%s', e5_logging) 117 118 # Combo 6: middle sensitivity. 119 e6_sens = (sens_range[0] + sens_range[1]) / 2.0 120 e6_expt = exposure / e6_sens 121 if e6_expt > exp_time_range[1]: 122 e6_expt = exp_time_range[1] 123 e6_sens = exposure / e6_expt 124 if e6_expt < exp_time_range[0]: 125 e6_expt = exp_time_range[0] 126 e6_sens = exposure / e6_expt 127 e6_logging = (f'e6 exp: {e6_expt}, sens: {e6_sens}') 128 logging.debug('%s', e6_logging) 129 130 return { 131 'minExposureTime': (int(e1_expt), int(e1_sens)), 132 'maxExposureTime': (int(e2_expt), int(e2_sens)), 133 'minSensitivity': (int(e3_expt), int(e3_sens)), 134 'maxSensitivity': (int(e4_expt), int(e4_sens)), 135 'midExposureTime': (int(e5_expt), int(e5_sens)), 136 'midSensitivity': (int(e6_expt), int(e6_sens)) 137 } 138 139 140def get_target_exposure(target_config_filename, its_session=None): 141 """Get the target exposure to use. 142 143 If there is a cached value and if the "target" command line parameter is 144 present, then return the cached value. Otherwise, measure a new value from 145 the scene, cache it, then return it. 146 147 Args: 148 target_config_filename: String, target config file name. 149 its_session: Optional, holding an open device session. 150 151 Returns: 152 The target exposure value. 153 """ 154 cached_exposure = None 155 for s in sys.argv[1:]: 156 if s == 'target': 157 cached_exposure = get_cached_target_exposure(target_config_filename) 158 if cached_exposure is not None: 159 logging.debug('Using cached target exposure') 160 return cached_exposure 161 if its_session is None: 162 with its_session_utils.ItsSession() as cam: 163 measured_exposure = do_target_exposure_measurement(cam) 164 else: 165 measured_exposure = do_target_exposure_measurement(its_session) 166 set_cached_target_exposure(target_config_filename, measured_exposure) 167 return measured_exposure 168 169 170def set_cached_target_exposure(target_config_filename, exposure): 171 """Saves the given exposure value to a cached location. 172 173 Once a value is cached, a call to get_cached_target_exposure will return 174 the value, even from a subsequent test/script run. That is, the value is 175 persisted. 176 177 The value is persisted in a JSON file in the current directory (from which 178 the script calling this function is run). 179 180 Args: 181 target_config_filename: String, target config file name. 182 exposure: The value to cache. 183 """ 184 logging.debug('Setting cached target exposure') 185 with open(target_config_filename, 'w') as f: 186 f.write(json.dumps({'exposure': exposure})) 187 188 189def get_cached_target_exposure(target_config_filename): 190 """Get the cached exposure value. 191 192 Args: 193 target_config_filename: String, target config file name. 194 195 Returns: 196 The cached exposure value, or None if there is no valid cached value. 197 """ 198 try: 199 with open(target_config_filename, 'r') as f: 200 o = json.load(f) 201 return o['exposure'] 202 except IOError: 203 return None 204 205 206def do_target_exposure_measurement(its_session): 207 """Use device 3A and captured shots to determine scene exposure. 208 209 Creates a new ITS device session (so this function should not be called 210 while another session to the device is open). 211 212 Assumes that the camera is pointed at a scene that is reasonably uniform 213 and reasonably lit -- that is, an appropriate target for running the ITS 214 tests that assume such uniformity. 215 216 Measures the scene using device 3A and then by taking a shot to hone in on 217 the exact exposure level that will result in a center 10% by 10% patch of 218 the scene having a intensity level of 0.5 (in the pixel range of [0,1]) 219 when a linear tonemap is used. That is, the pixels coming off the sensor 220 should be at approximately 50% intensity (however note that it's actually 221 the luma value in the YUV image that is being targeted to 50%). 222 223 The computed exposure value is the product of the sensitivity (ISO) and 224 exposure time (ns) to achieve that sensor exposure level. 225 226 Args: 227 its_session: Holds an open device session. 228 229 Returns: 230 The measured product of sensitivity and exposure time that results in 231 the luma channel of captured shots having an intensity of 0.5. 232 """ 233 logging.debug('Measuring target exposure') 234 235 # Get AE+AWB lock first, so the auto values in the capture result are 236 # populated properly. 237 sens, exp_time, gains, xform, _ = its_session.do_3a( 238 _REGION_3A, _REGION_3A, _REGION_3A, do_af=False, get_results=True) 239 240 # Convert the transform to rational. 241 xform_rat = [{'numerator': int(100 * x), 'denominator': 100} for x in xform] 242 243 # Linear tonemap 244 tmap = sum([[i / 63.0, i / 63.0] for i in range(64)], []) 245 246 # Capture a manual shot with this exposure, using a linear tonemap. 247 # Use the gains+transform returned by the AWB pass. 248 req = capture_request_utils.manual_capture_request(sens, exp_time) 249 req['android.tonemap.mode'] = 0 250 req['android.tonemap.curve'] = {'red': tmap, 'green': tmap, 'blue': tmap} 251 req['android.colorCorrection.transform'] = xform_rat 252 req['android.colorCorrection.gains'] = gains 253 cap = its_session.do_capture(req) 254 255 # Compute the mean luma of a center patch. 256 yimg, _, _ = image_processing_utils.convert_capture_to_planes( 257 cap) 258 tile = image_processing_utils.get_image_patch(yimg, 0.45, 0.45, 0.1, 0.1) 259 luma_mean = image_processing_utils.compute_image_means(tile) 260 261 # Compute the exposure value that would result in a luma of 0.5. 262 return sens * exp_time * 0.5 / luma_mean[0] 263 264 265if __name__ == '__main__': 266 unittest.main() 267