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
21import unittest
22
23import capture_request_utils
24import image_processing_utils
25import its_session_utils
26
27_CACHE_FILENAME = 'its.target.cfg'
28_REGION_3A = [[0.45, 0.45, 0.1, 0.1, 1]]
29
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  """
55  target_config_filename = os.path.join(output_path, _CACHE_FILENAME)
56
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
76    e1_logging = (f'e1 exp: {e1_expt}, sens: {e1_sens}')
77    logging.debug('%s', e1_logging)
78
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
85    e2_logging = (f'e2 exp: {e2_expt}, sens: {e2_sens}')
86    logging.debug('%s', e2_logging)
87
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
94    e3_logging = (f'e3 exp: {e3_expt}, sens: {e3_sens}')
95    logging.debug('%s', e3_logging)
96
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
103    e4_logging = (f'e4 exp: {e4_expt}, sens: {e4_sens}')
104    logging.debug('%s', e4_logging)
105
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
115    e5_logging = (f'e5 exp: {e5_expt}, sens: {e5_sens}')
116    logging.debug('%s', e5_logging)
117
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
127    e6_logging = (f'e6 exp: {e6_expt}, sens: {e6_sens}')
128    logging.debug('%s', e6_logging)
129
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.
237  sens, exp_time, gains, xform, _ = its_session.do_3a(
238      _REGION_3A, _REGION_3A, _REGION_3A, do_af=False, get_results=True)
239
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]
263
264
265if __name__ == '__main__':
266  unittest.main()
267