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
15import its.device
16import its.image
17import its.objects
18import os
19import os.path
20import sys
21import json
22import unittest
23import json
24
25CACHE_FILENAME = "its.target.cfg"
26
27def __do_target_exposure_measurement(its_session):
28    """Use device 3A and captured shots to determine scene exposure.
29
30    Creates a new ITS device session (so this function should not be called
31    while another session to the device is open).
32
33    Assumes that the camera is pointed at a scene that is reasonably uniform
34    and reasonably lit -- that is, an appropriate target for running the ITS
35    tests that assume such uniformity.
36
37    Measures the scene using device 3A and then by taking a shot to hone in on
38    the exact exposure level that will result in a center 10% by 10% patch of
39    the scene having a intensity level of 0.5 (in the pixel range of [0,1])
40    when a linear tonemap is used. That is, the pixels coming off the sensor
41    should be at approximately 50% intensity (however note that it's actually
42    the luma value in the YUV image that is being targeted to 50%).
43
44    The computed exposure value is the product of the sensitivity (ISO) and
45    exposure time (ns) to achieve that sensor exposure level.
46
47    Args:
48        its_session: Holds an open device session.
49
50    Returns:
51        The measured product of sensitivity and exposure time that results in
52            the luma channel of captured shots having an intensity of 0.5.
53    """
54    print "Measuring target exposure"
55
56    # Get AE+AWB lock first, so the auto values in the capture result are
57    # populated properly.
58    r = [[0.45, 0.45, 0.1, 0.1, 1]]
59    sens, exp_time, gains, xform, _ \
60            = its_session.do_3a(r,r,r,do_af=False,get_results=True)
61
62    # Convert the transform to rational.
63    xform_rat = [{"numerator":int(100*x),"denominator":100} for x in xform]
64
65    # Linear tonemap
66    tmap = sum([[i/63.0,i/63.0] for i in range(64)], [])
67
68    # Capture a manual shot with this exposure, using a linear tonemap.
69    # Use the gains+transform returned by the AWB pass.
70    req = its.objects.manual_capture_request(sens, exp_time)
71    req["android.tonemap.mode"] = 0
72    req["android.tonemap.curveRed"] = tmap
73    req["android.tonemap.curveGreen"] = tmap
74    req["android.tonemap.curveBlue"] = tmap
75    req["android.colorCorrection.transform"] = xform_rat
76    req["android.colorCorrection.gains"] = gains
77    cap = its_session.do_capture(req)
78
79    # Compute the mean luma of a center patch.
80    yimg,uimg,vimg = its.image.convert_capture_to_planes(cap)
81    tile = its.image.get_image_patch(yimg, 0.45, 0.45, 0.1, 0.1)
82    luma_mean = its.image.compute_image_means(tile)
83
84    # Compute the exposure value that would result in a luma of 0.5.
85    return sens * exp_time * 0.5 / luma_mean[0]
86
87def __set_cached_target_exposure(exposure):
88    """Saves the given exposure value to a cached location.
89
90    Once a value is cached, a call to __get_cached_target_exposure will return
91    the value, even from a subsequent test/script run. That is, the value is
92    persisted.
93
94    The value is persisted in a JSON file in the current directory (from which
95    the script calling this function is run).
96
97    Args:
98        exposure: The value to cache.
99    """
100    print "Setting cached target exposure"
101    with open(CACHE_FILENAME, "w") as f:
102        f.write(json.dumps({"exposure":exposure}))
103
104def __get_cached_target_exposure():
105    """Get the cached exposure value.
106
107    Returns:
108        The cached exposure value, or None if there is no valid cached value.
109    """
110    try:
111        with open(CACHE_FILENAME, "r") as f:
112            o = json.load(f)
113            return o["exposure"]
114    except:
115        return None
116
117def clear_cached_target_exposure():
118    """If there is a cached exposure value, clear it.
119    """
120    if os.path.isfile(CACHE_FILENAME):
121        os.remove(CACHE_FILENAME)
122
123def set_hardcoded_exposure(exposure):
124    """Set a hard-coded exposure value, rather than relying on measurements.
125
126    The exposure value is the product of sensitivity (ISO) and eposure time
127    (ns) that will result in a center-patch luma value of 0.5 (using a linear
128    tonemap) for the scene that the camera is pointing at.
129
130    If bringing up a new HAL implementation and the ability use the device to
131    measure the scene isn't there yet (e.g. device 3A doesn't work), then a
132    cache file of the appropriate name can be manually created and populated
133    with a hard-coded value using this function.
134
135    Args:
136        exposure: The hard-coded exposure value to set.
137    """
138    __set_cached_target_exposure(exposure)
139
140def get_target_exposure(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        its_session: Optional, holding an open device session.
149
150    Returns:
151        The target exposure value.
152    """
153    cached_exposure = None
154    for s in sys.argv[1:]:
155        if s == "target":
156            cached_exposure = __get_cached_target_exposure()
157    if cached_exposure is not None:
158        print "Using cached target exposure"
159        return cached_exposure
160    if its_session is None:
161        with its.device.ItsSession() as cam:
162            measured_exposure = __do_target_exposure_measurement(cam)
163    else:
164        measured_exposure = __do_target_exposure_measurement(its_session)
165    __set_cached_target_exposure(measured_exposure)
166    return measured_exposure
167
168def get_target_exposure_combos(its_session=None):
169    """Get a set of legal combinations of target (exposure time, sensitivity).
170
171    Gets the target exposure value, which is a product of sensitivity (ISO) and
172    exposure time, and returns equivalent tuples of (exposure time,sensitivity)
173    that are all legal and that correspond to the four extrema in this 2D param
174    space, as well as to two "middle" points.
175
176    Will open a device session if its_session is None.
177
178    Args:
179        its_session: Optional, holding an open device session.
180
181    Returns:
182        Object containing six legal (exposure time, sensitivity) tuples, keyed
183        by the following strings:
184            "minExposureTime"
185            "midExposureTime"
186            "maxExposureTime"
187            "minSensitivity"
188            "midSensitivity"
189            "maxSensitivity
190    """
191    if its_session is None:
192        with its.device.ItsSession() as cam:
193            exposure = get_target_exposure(cam)
194            props = cam.get_camera_properties()
195    else:
196        exposure = get_target_exposure(its_session)
197        props = its_session.get_camera_properties()
198
199    sens_range = props['android.sensor.info.sensitivityRange']
200    exp_time_range = props['android.sensor.info.exposureTimeRange']
201
202    # Combo 1: smallest legal exposure time.
203    e1_expt = exp_time_range[0]
204    e1_sens = exposure / e1_expt
205    if e1_sens > sens_range[1]:
206        e1_sens = sens_range[1]
207        e1_expt = exposure / e1_sens
208
209    # Combo 2: largest legal exposure time.
210    e2_expt = exp_time_range[1]
211    e2_sens = exposure / e2_expt
212    if e2_sens < sens_range[0]:
213        e2_sens = sens_range[0]
214        e2_expt = exposure / e2_sens
215
216    # Combo 3: smallest legal sensitivity.
217    e3_sens = sens_range[0]
218    e3_expt = exposure / e3_sens
219    if e3_expt > exp_time_range[1]:
220        e3_expt = exp_time_range[1]
221        e3_sens = exposure / e3_expt
222
223    # Combo 4: largest legal sensitivity.
224    e4_sens = sens_range[1]
225    e4_expt = exposure / e4_sens
226    if e4_expt < exp_time_range[0]:
227        e4_expt = exp_time_range[0]
228        e4_sens = exposure / e4_expt
229
230    # Combo 5: middle exposure time.
231    e5_expt = (exp_time_range[0] + exp_time_range[1]) / 2.0
232    e5_sens = exposure / e5_expt
233    if e5_sens > sens_range[1]:
234        e5_sens = sens_range[1]
235        e5_expt = exposure / e5_sens
236    if e5_sens < sens_range[0]:
237        e5_sens = sens_range[0]
238        e5_expt = exposure / e5_sens
239
240    # Combo 6: middle sensitivity.
241    e6_sens = (sens_range[0] + sens_range[1]) / 2.0
242    e6_expt = exposure / e6_sens
243    if e6_expt > exp_time_range[1]:
244        e6_expt = exp_time_range[1]
245        e6_sens = exposure / e6_expt
246    if e6_expt < exp_time_range[0]:
247        e6_expt = exp_time_range[0]
248        e6_sens = exposure / e6_expt
249
250    return {
251        "minExposureTime" : (int(e1_expt), int(e1_sens)),
252        "maxExposureTime" : (int(e2_expt), int(e2_sens)),
253        "minSensitivity" : (int(e3_expt), int(e3_sens)),
254        "maxSensitivity" : (int(e4_expt), int(e4_sens)),
255        "midExposureTime" : (int(e5_expt), int(e5_sens)),
256        "midSensitivity" : (int(e6_expt), int(e6_sens))
257        }
258
259class __UnitTest(unittest.TestCase):
260    """Run a suite of unit tests on this module.
261    """
262    # TODO: Add some unit tests.
263
264if __name__ == '__main__':
265    unittest.main()
266
267