blob: 279b5ea01a155ca9bb608ef794e1853d0af20c2c [file] [log] [blame]
Rucha Katakwarb83e59a2021-02-02 17:16:54 -08001# 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.
Clemenz Portmanncddf2bf2021-03-19 13:21:16 -070014"""Utility functions to calculate targeted exposures based on camera properties.
15"""
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080016
17import json
18import logging
19import os
20import sys
Clemenz Portmann452eead2021-02-08 13:55:12 -080021import unittest
22
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080023import capture_request_utils
24import image_processing_utils
25import its_session_utils
26
Clemenz Portmann9690df12021-06-02 15:02:05 -070027_CACHE_FILENAME = 'its.target.cfg'
28_REGION_3A = [[0.45, 0.45, 0.1, 0.1, 1]]
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080029
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 """
Clemenz Portmann9690df12021-06-02 15:02:05 -070055 target_config_filename = os.path.join(output_path, _CACHE_FILENAME)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080056
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
Clemenz Portmann9690df12021-06-02 15:02:05 -070076 e1_logging = (f'e1 exp: {e1_expt}, sens: {e1_sens}')
77 logging.debug('%s', e1_logging)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080078
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
Clemenz Portmann9690df12021-06-02 15:02:05 -070085 e2_logging = (f'e2 exp: {e2_expt}, sens: {e2_sens}')
86 logging.debug('%s', e2_logging)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080087
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
Clemenz Portmann9690df12021-06-02 15:02:05 -070094 e3_logging = (f'e3 exp: {e3_expt}, sens: {e3_sens}')
95 logging.debug('%s', e3_logging)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080096
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
Clemenz Portmann9690df12021-06-02 15:02:05 -0700103 e4_logging = (f'e4 exp: {e4_expt}, sens: {e4_sens}')
104 logging.debug('%s', e4_logging)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -0800105
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
Clemenz Portmann9690df12021-06-02 15:02:05 -0700115 e5_logging = (f'e5 exp: {e5_expt}, sens: {e5_sens}')
116 logging.debug('%s', e5_logging)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -0800117
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
Clemenz Portmann9690df12021-06-02 15:02:05 -0700127 e6_logging = (f'e6 exp: {e6_expt}, sens: {e6_sens}')
128 logging.debug('%s', e6_logging)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -0800129
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.
Clemenz Portmann9690df12021-06-02 15:02:05 -0700237 sens, exp_time, gains, xform, _ = its_session.do_3a(
238 _REGION_3A, _REGION_3A, _REGION_3A, do_af=False, get_results=True)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -0800239
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]
Clemenz Portmann452eead2021-02-08 13:55:12 -0800263
264
265if __name__ == '__main__':
266 unittest.main()