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