普通文本  |  262行  |  9.03 KB

# 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,0,1,1]
    sens, exp_time, gains, xform, _ = its_session.do_3a(r,r,r,True,True,False)

    # 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_yuv_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, return it. If there is no cached value, then
    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 = __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" : (e1_expt, e1_sens),
        "maxExposureTime" : (e2_expt, e2_sens),
        "minSensitivity" : (e3_expt, e3_sens),
        "maxSensitivity" : (e4_expt, e4_sens),
        "midExposureTime" : (e5_expt, e5_sens),
        "midSensitivity" : (e6_expt, 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()