blob: 4e2252fa5a133d7cb9e715e42cfd1edc7bb9b034 [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 -080021
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080022import capture_request_utils
23import image_processing_utils
24import its_session_utils
25
Clemenz Portmann9690df12021-06-02 15:02:05 -070026_CACHE_FILENAME = 'its.target.cfg'
27_REGION_3A = [[0.45, 0.45, 0.1, 0.1, 1]]
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080028
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 """
Clemenz Portmann9690df12021-06-02 15:02:05 -070054 target_config_filename = os.path.join(output_path, _CACHE_FILENAME)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080055
56 if its_session is None:
57 with its_session_utils.ItsSession() as cam:
Clemenz Portmann978d0cd2022-06-16 14:31:22 -070058 exp_sens_prod = get_target_exposure(target_config_filename, cam)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080059 props = cam.get_camera_properties()
60 props = cam.override_with_hidden_physical_camera_props(props)
61 else:
Clemenz Portmann978d0cd2022-06-16 14:31:22 -070062 exp_sens_prod = get_target_exposure(target_config_filename, its_session)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080063 props = its_session.get_camera_properties()
64 props = its_session.override_with_hidden_physical_camera_props(props)
65
Clemenz Portmann978d0cd2022-06-16 14:31:22 -070066 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))
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080071
Clemenz Portmann978d0cd2022-06-16 14:31:22 -070072 # 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)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080079
Clemenz Portmann978d0cd2022-06-16 14:31:22 -070080 # 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)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080087
Clemenz Portmann978d0cd2022-06-16 14:31:22 -070088 # 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)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -080095
Clemenz Portmann978d0cd2022-06-16 14:31:22 -070096 # 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)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -0800103
Clemenz Portmann978d0cd2022-06-16 14:31:22 -0700104 # 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)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -0800114
Clemenz Portmann978d0cd2022-06-16 14:31:22 -0700115 # 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)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -0800125
Clemenz Portmann978d0cd2022-06-16 14:31:22 -0700126 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 }
Rucha Katakwarb83e59a2021-02-02 17:16:54 -0800134
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:
Clemenz Portmann978d0cd2022-06-16 14:31:22 -0700148 The target exposure*sensitivity value.
Rucha Katakwarb83e59a2021-02-02 17:16:54 -0800149 """
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.
Clemenz Portmann9690df12021-06-02 15:02:05 -0700233 sens, exp_time, gains, xform, _ = its_session.do_3a(
234 _REGION_3A, _REGION_3A, _REGION_3A, do_af=False, get_results=True)
Rucha Katakwarb83e59a2021-02-02 17:16:54 -0800235
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)
Clemenz Portmann978d0cd2022-06-16 14:31:22 -0700256 logging.debug('Target exposure cap luma: %.4f', luma_mean[0])
Rucha Katakwarb83e59a2021-02-02 17:16:54 -0800257
258 # Compute the exposure value that would result in a luma of 0.5.
259 return sens * exp_time * 0.5 / luma_mean[0]
Clemenz Portmann452eead2021-02-08 13:55:12 -0800260