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