# 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 its.device
import its.image
import its.objects
import os
import os.path
import sys
import json
import unittest
import json
CACHE_FILENAME = "its.target.cfg"
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.
"""
print "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 = its.objects.manual_capture_request(sens, exp_time)
req["android.tonemap.mode"] = 0
req["android.tonemap.curveRed"] = tmap
req["android.tonemap.curveGreen"] = tmap
req["android.tonemap.curveBlue"] = 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,uimg,vimg = its.image.convert_capture_to_planes(cap)
tile = its.image.get_image_patch(yimg, 0.45, 0.45, 0.1, 0.1)
luma_mean = its.image.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]
def __set_cached_target_exposure(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:
exposure: The value to cache.
"""
print "Setting cached target exposure"
with open(CACHE_FILENAME, "w") as f:
f.write(json.dumps({"exposure":exposure}))
def __get_cached_target_exposure():
"""Get the cached exposure value.
Returns:
The cached exposure value, or None if there is no valid cached value.
"""
try:
with open(CACHE_FILENAME, "r") as f:
o = json.load(f)
return o["exposure"]
except:
return None
def clear_cached_target_exposure():
"""If there is a cached exposure value, clear it.
"""
if os.path.isfile(CACHE_FILENAME):
os.remove(CACHE_FILENAME)
def set_hardcoded_exposure(exposure):
"""Set a hard-coded exposure value, rather than relying on measurements.
The exposure value is the product of sensitivity (ISO) and eposure time
(ns) that will result in a center-patch luma value of 0.5 (using a linear
tonemap) for the scene that the camera is pointing at.
If bringing up a new HAL implementation and the ability use the device to
measure the scene isn't there yet (e.g. device 3A doesn't work), then a
cache file of the appropriate name can be manually created and populated
with a hard-coded value using this function.
Args:
exposure: The hard-coded exposure value to set.
"""
__set_cached_target_exposure(exposure)
def get_target_exposure(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:
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()
if cached_exposure is not None:
print "Using cached target exposure"
return cached_exposure
if its_session is None:
with its.device.ItsSession() as cam:
measured_exposure = __do_target_exposure_measurement(cam)
else:
measured_exposure = __do_target_exposure_measurement(its_session)
__set_cached_target_exposure(measured_exposure)
return measured_exposure
def get_target_exposure_combos(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:
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
"""
if its_session is None:
with its.device.ItsSession() as cam:
exposure = get_target_exposure(cam)
props = cam.get_camera_properties()
else:
exposure = get_target_exposure(its_session)
props = its_session.get_camera_properties()
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))
}
class __UnitTest(unittest.TestCase):
"""Run a suite of unit tests on this module.
"""
# TODO: Add some unit tests.
if __name__ == '__main__':
unittest.main()