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.curve"] = {
73        "red": tmap, "green": tmap, "blue": tmap}
74    req["android.colorCorrection.transform"] = xform_rat
75    req["android.colorCorrection.gains"] = gains
76    cap = its_session.do_capture(req)
77
78    # Compute the mean luma of a center patch.
79    yimg,uimg,vimg = its.image.convert_capture_to_planes(cap)
80    tile = its.image.get_image_patch(yimg, 0.45, 0.45, 0.1, 0.1)
81    luma_mean = its.image.compute_image_means(tile)
82
83    # Compute the exposure value that would result in a luma of 0.5.
84    return sens * exp_time * 0.5 / luma_mean[0]
85
86def __set_cached_target_exposure(exposure):
87    """Saves the given exposure value to a cached location.
88
89    Once a value is cached, a call to __get_cached_target_exposure will return
90    the value, even from a subsequent test/script run. That is, the value is
91    persisted.
92
93    The value is persisted in a JSON file in the current directory (from which
94    the script calling this function is run).
95
96    Args:
97        exposure: The value to cache.
98    """
99    print "Setting cached target exposure"
100    with open(CACHE_FILENAME, "w") as f:
101        f.write(json.dumps({"exposure":exposure}))
102
103def __get_cached_target_exposure():
104    """Get the cached exposure value.
105
106    Returns:
107        The cached exposure value, or None if there is no valid cached value.
108    """
109    try:
110        with open(CACHE_FILENAME, "r") as f:
111            o = json.load(f)
112            return o["exposure"]
113    except:
114        return None
115
116def clear_cached_target_exposure():
117    """If there is a cached exposure value, clear it.
118    """
119    if os.path.isfile(CACHE_FILENAME):
120        os.remove(CACHE_FILENAME)
121
122def set_hardcoded_exposure(exposure):
123    """Set a hard-coded exposure value, rather than relying on measurements.
124
125    The exposure value is the product of sensitivity (ISO) and eposure time
126    (ns) that will result in a center-patch luma value of 0.5 (using a linear
127    tonemap) for the scene that the camera is pointing at.
128
129    If bringing up a new HAL implementation and the ability use the device to
130    measure the scene isn't there yet (e.g. device 3A doesn't work), then a
131    cache file of the appropriate name can be manually created and populated
132    with a hard-coded value using this function.
133
134    Args:
135        exposure: The hard-coded exposure value to set.
136    """
137    __set_cached_target_exposure(exposure)
138
139def get_target_exposure(its_session=None):
140    """Get the target exposure to use.
141
142    If there is a cached value and if the "target" command line parameter is
143    present, then return the cached value. Otherwise, measure a new value from
144    the scene, cache it, then return it.
145
146    Args:
147        its_session: Optional, holding an open device session.
148
149    Returns:
150        The target exposure value.
151    """
152    cached_exposure = None
153    for s in sys.argv[1:]:
154        if s == "target":
155            cached_exposure = __get_cached_target_exposure()
156    if cached_exposure is not None:
157        print "Using cached target exposure"
158        return cached_exposure
159    if its_session is None:
160        with its.device.ItsSession() as cam:
161            measured_exposure = __do_target_exposure_measurement(cam)
162    else:
163        measured_exposure = __do_target_exposure_measurement(its_session)
164    __set_cached_target_exposure(measured_exposure)
165    return measured_exposure
166
167def get_target_exposure_combos(its_session=None):
168    """Get a set of legal combinations of target (exposure time, sensitivity).
169
170    Gets the target exposure value, which is a product of sensitivity (ISO) and
171    exposure time, and returns equivalent tuples of (exposure time,sensitivity)
172    that are all legal and that correspond to the four extrema in this 2D param
173    space, as well as to two "middle" points.
174
175    Will open a device session if its_session is None.
176
177    Args:
178        its_session: Optional, holding an open device session.
179
180    Returns:
181        Object containing six legal (exposure time, sensitivity) tuples, keyed
182        by the following strings:
183            "minExposureTime"
184            "midExposureTime"
185            "maxExposureTime"
186            "minSensitivity"
187            "midSensitivity"
188            "maxSensitivity
189    """
190    if its_session is None:
191        with its.device.ItsSession() as cam:
192            exposure = get_target_exposure(cam)
193            props = cam.get_camera_properties()
194    else:
195        exposure = get_target_exposure(its_session)
196        props = its_session.get_camera_properties()
197
198    sens_range = props['android.sensor.info.sensitivityRange']
199    exp_time_range = props['android.sensor.info.exposureTimeRange']
200
201    # Combo 1: smallest legal exposure time.
202    e1_expt = exp_time_range[0]
203    e1_sens = exposure / e1_expt
204    if e1_sens > sens_range[1]:
205        e1_sens = sens_range[1]
206        e1_expt = exposure / e1_sens
207
208    # Combo 2: largest legal exposure time.
209    e2_expt = exp_time_range[1]
210    e2_sens = exposure / e2_expt
211    if e2_sens < sens_range[0]:
212        e2_sens = sens_range[0]
213        e2_expt = exposure / e2_sens
214
215    # Combo 3: smallest legal sensitivity.
216    e3_sens = sens_range[0]
217    e3_expt = exposure / e3_sens
218    if e3_expt > exp_time_range[1]:
219        e3_expt = exp_time_range[1]
220        e3_sens = exposure / e3_expt
221
222    # Combo 4: largest legal sensitivity.
223    e4_sens = sens_range[1]
224    e4_expt = exposure / e4_sens
225    if e4_expt < exp_time_range[0]:
226        e4_expt = exp_time_range[0]
227        e4_sens = exposure / e4_expt
228
229    # Combo 5: middle exposure time.
230    e5_expt = (exp_time_range[0] + exp_time_range[1]) / 2.0
231    e5_sens = exposure / e5_expt
232    if e5_sens > sens_range[1]:
233        e5_sens = sens_range[1]
234        e5_expt = exposure / e5_sens
235    if e5_sens < sens_range[0]:
236        e5_sens = sens_range[0]
237        e5_expt = exposure / e5_sens
238
239    # Combo 6: middle sensitivity.
240    e6_sens = (sens_range[0] + sens_range[1]) / 2.0
241    e6_expt = exposure / e6_sens
242    if e6_expt > exp_time_range[1]:
243        e6_expt = exp_time_range[1]
244        e6_sens = exposure / e6_expt
245    if e6_expt < exp_time_range[0]:
246        e6_expt = exp_time_range[0]
247        e6_sens = exposure / e6_expt
248
249    return {
250        "minExposureTime" : (int(e1_expt), int(e1_sens)),
251        "maxExposureTime" : (int(e2_expt), int(e2_sens)),
252        "minSensitivity" : (int(e3_expt), int(e3_sens)),
253        "maxSensitivity" : (int(e4_expt), int(e4_sens)),
254        "midExposureTime" : (int(e5_expt), int(e5_sens)),
255        "midSensitivity" : (int(e6_expt), int(e6_sens))
256        }
257
258class __UnitTest(unittest.TestCase):
259    """Run a suite of unit tests on this module.
260    """
261    # TODO: Add some unit tests.
262
263if __name__ == '__main__':
264    unittest.main()
265
266