Camera ITS mobly + python3
Bug:173651550

PiperOrigin-RevId: 355286652
Change-Id: Ie2ecdc351d2f82e4f01127590341758f1f4ac690
diff --git a/apps/CameraITS2.0/utils/target_exposure_utils.py b/apps/CameraITS2.0/utils/target_exposure_utils.py
new file mode 100644
index 0000000..a31f951
--- /dev/null
+++ b/apps/CameraITS2.0/utils/target_exposure_utils.py
@@ -0,0 +1,247 @@
+# Copyright 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import json
+import logging
+import os
+import sys
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+CACHE_FILENAME = 'its.target.cfg'
+
+
+def get_target_exposure_combos(output_path, its_session=None):
+  """Get a set of legal combinations of target (exposure time, sensitivity).
+
+  Gets the target exposure value, which is a product of sensitivity (ISO) and
+  exposure time, and returns equivalent tuples of (exposure time,sensitivity)
+  that are all legal and that correspond to the four extrema in this 2D param
+  space, as well as to two "middle" points.
+
+  Will open a device session if its_session is None.
+
+  Args:
+   output_path: String, path where the target.cfg file will be saved.
+   its_session: Optional, holding an open device session.
+
+  Returns:
+    Object containing six legal (exposure time, sensitivity) tuples, keyed
+    by the following strings:
+    'minExposureTime'
+    'midExposureTime'
+    'maxExposureTime'
+    'minSensitivity'
+    'midSensitivity'
+    'maxSensitivity'
+  """
+  target_config_filename = os.path.join(output_path, CACHE_FILENAME)
+
+  if its_session is None:
+    with its_session_utils.ItsSession() as cam:
+      exposure = get_target_exposure(target_config_filename, cam)
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+  else:
+    exposure = get_target_exposure(target_config_filename, its_session)
+    props = its_session.get_camera_properties()
+    props = its_session.override_with_hidden_physical_camera_props(props)
+
+    sens_range = props['android.sensor.info.sensitivityRange']
+    exp_time_range = props['android.sensor.info.exposureTimeRange']
+
+    # Combo 1: smallest legal exposure time.
+    e1_expt = exp_time_range[0]
+    e1_sens = exposure / e1_expt
+    if e1_sens > sens_range[1]:
+      e1_sens = sens_range[1]
+      e1_expt = exposure / e1_sens
+
+    # Combo 2: largest legal exposure time.
+    e2_expt = exp_time_range[1]
+    e2_sens = exposure / e2_expt
+    if e2_sens < sens_range[0]:
+      e2_sens = sens_range[0]
+      e2_expt = exposure / e2_sens
+
+    # Combo 3: smallest legal sensitivity.
+    e3_sens = sens_range[0]
+    e3_expt = exposure / e3_sens
+    if e3_expt > exp_time_range[1]:
+      e3_expt = exp_time_range[1]
+      e3_sens = exposure / e3_expt
+
+    # Combo 4: largest legal sensitivity.
+    e4_sens = sens_range[1]
+    e4_expt = exposure / e4_sens
+    if e4_expt < exp_time_range[0]:
+      e4_expt = exp_time_range[0]
+      e4_sens = exposure / e4_expt
+
+    # Combo 5: middle exposure time.
+    e5_expt = (exp_time_range[0] + exp_time_range[1]) / 2.0
+    e5_sens = exposure / e5_expt
+    if e5_sens > sens_range[1]:
+      e5_sens = sens_range[1]
+      e5_expt = exposure / e5_sens
+    if e5_sens < sens_range[0]:
+      e5_sens = sens_range[0]
+      e5_expt = exposure / e5_sens
+
+    # Combo 6: middle sensitivity.
+    e6_sens = (sens_range[0] + sens_range[1]) / 2.0
+    e6_expt = exposure / e6_sens
+    if e6_expt > exp_time_range[1]:
+      e6_expt = exp_time_range[1]
+      e6_sens = exposure / e6_expt
+    if e6_expt < exp_time_range[0]:
+      e6_expt = exp_time_range[0]
+      e6_sens = exposure / e6_expt
+
+    return {
+        'minExposureTime': (int(e1_expt), int(e1_sens)),
+        'maxExposureTime': (int(e2_expt), int(e2_sens)),
+        'minSensitivity': (int(e3_expt), int(e3_sens)),
+        'maxSensitivity': (int(e4_expt), int(e4_sens)),
+        'midExposureTime': (int(e5_expt), int(e5_sens)),
+        'midSensitivity': (int(e6_expt), int(e6_sens))
+    }
+
+
+def get_target_exposure(target_config_filename, its_session=None):
+  """Get the target exposure to use.
+
+  If there is a cached value and if the "target" command line parameter is
+  present, then return the cached value. Otherwise, measure a new value from
+  the scene, cache it, then return it.
+
+  Args:
+    target_config_filename: String, target config file name.
+    its_session: Optional, holding an open device session.
+
+  Returns:
+    The target exposure value.
+  """
+  cached_exposure = None
+  for s in sys.argv[1:]:
+    if s == 'target':
+      cached_exposure = get_cached_target_exposure(target_config_filename)
+  if cached_exposure is not None:
+    logging.debug('Using cached target exposure')
+    return cached_exposure
+  if its_session is None:
+    with its_session_utils.ItsSession() as cam:
+      measured_exposure = do_target_exposure_measurement(cam)
+  else:
+    measured_exposure = do_target_exposure_measurement(its_session)
+  set_cached_target_exposure(target_config_filename, measured_exposure)
+  return measured_exposure
+
+
+def set_cached_target_exposure(target_config_filename, exposure):
+  """Saves the given exposure value to a cached location.
+
+  Once a value is cached, a call to get_cached_target_exposure will return
+  the value, even from a subsequent test/script run. That is, the value is
+  persisted.
+
+  The value is persisted in a JSON file in the current directory (from which
+  the script calling this function is run).
+
+  Args:
+   target_config_filename: String, target config file name.
+   exposure: The value to cache.
+  """
+  logging.debug('Setting cached target exposure')
+  with open(target_config_filename, 'w') as f:
+    f.write(json.dumps({'exposure': exposure}))
+
+
+def get_cached_target_exposure(target_config_filename):
+  """Get the cached exposure value.
+
+  Args:
+   target_config_filename: String, target config file name.
+
+  Returns:
+    The cached exposure value, or None if there is no valid cached value.
+  """
+  try:
+    with open(target_config_filename, 'r') as f:
+      o = json.load(f)
+      return o['exposure']
+  except IOError:
+    return None
+
+
+def do_target_exposure_measurement(its_session):
+  """Use device 3A and captured shots to determine scene exposure.
+
+    Creates a new ITS device session (so this function should not be called
+    while another session to the device is open).
+
+    Assumes that the camera is pointed at a scene that is reasonably uniform
+    and reasonably lit -- that is, an appropriate target for running the ITS
+    tests that assume such uniformity.
+
+    Measures the scene using device 3A and then by taking a shot to hone in on
+    the exact exposure level that will result in a center 10% by 10% patch of
+    the scene having a intensity level of 0.5 (in the pixel range of [0,1])
+    when a linear tonemap is used. That is, the pixels coming off the sensor
+    should be at approximately 50% intensity (however note that it's actually
+    the luma value in the YUV image that is being targeted to 50%).
+
+    The computed exposure value is the product of the sensitivity (ISO) and
+    exposure time (ns) to achieve that sensor exposure level.
+
+  Args:
+    its_session: Holds an open device session.
+
+  Returns:
+    The measured product of sensitivity and exposure time that results in
+    the luma channel of captured shots having an intensity of 0.5.
+  """
+  logging.debug('Measuring target exposure')
+
+  # Get AE+AWB lock first, so the auto values in the capture result are
+  # populated properly.
+  r = [[0.45, 0.45, 0.1, 0.1, 1]]
+  sens, exp_time, gains, xform, _ \
+          = its_session.do_3a(r, r, r, do_af=False, get_results=True)
+
+  # Convert the transform to rational.
+  xform_rat = [{'numerator': int(100 * x), 'denominator': 100} for x in xform]
+
+  # Linear tonemap
+  tmap = sum([[i / 63.0, i / 63.0] for i in range(64)], [])
+
+  # Capture a manual shot with this exposure, using a linear tonemap.
+  # Use the gains+transform returned by the AWB pass.
+  req = capture_request_utils.manual_capture_request(sens, exp_time)
+  req['android.tonemap.mode'] = 0
+  req['android.tonemap.curve'] = {'red': tmap, 'green': tmap, 'blue': tmap}
+  req['android.colorCorrection.transform'] = xform_rat
+  req['android.colorCorrection.gains'] = gains
+  cap = its_session.do_capture(req)
+
+  # Compute the mean luma of a center patch.
+  yimg, _, _ = image_processing_utils.convert_capture_to_planes(
+      cap)
+  tile = image_processing_utils.get_image_patch(yimg, 0.45, 0.45, 0.1, 0.1)
+  luma_mean = image_processing_utils.compute_image_means(tile)
+
+  # Compute the exposure value that would result in a luma of 0.5.
+  return sens * exp_time * 0.5 / luma_mean[0]