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