xref: /aosp_15_r20/cts/apps/CameraITS/utils/target_exposure_utils.py (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
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
21
22import capture_request_utils
23import image_processing_utils
24import its_session_utils
25
26_CACHE_FILENAME = 'its.target.cfg'
27_REGION_3A = [[0.45, 0.45, 0.1, 0.1, 1]]
28
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  """
54  target_config_filename = os.path.join(output_path, _CACHE_FILENAME)
55
56  if its_session is None:
57    with its_session_utils.ItsSession() as cam:
58      exp_sens_prod = get_target_exposure(target_config_filename, cam)
59      props = cam.get_camera_properties()
60      props = cam.override_with_hidden_physical_camera_props(props)
61  else:
62    exp_sens_prod = get_target_exposure(target_config_filename, its_session)
63    props = its_session.get_camera_properties()
64    props = its_session.override_with_hidden_physical_camera_props(props)
65
66  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))
71
72  # 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)
79
80  # 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)
87
88  # 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)
95
96  # 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)
103
104  # 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)
114
115  # 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)
125
126  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  }
134
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:
148    The target exposure*sensitivity value.
149  """
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.
233  sens, exp_time, gains, xform, _ = its_session.do_3a(
234      _REGION_3A, _REGION_3A, _REGION_3A, do_af=False, get_results=True)
235
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)
256  logging.debug('Target exposure cap luma: %.4f', luma_mean[0])
257
258  # Compute the exposure value that would result in a luma of 0.5.
259  return sens * exp_time * 0.5 / luma_mean[0]
260
261