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"""Utility functions to create custom capture requests."""
15
16
17import math
18import unittest
19
20
21def auto_capture_request(linear_tonemap=False, props=None):
22  """Returns a capture request with everything set to auto.
23
24  Args:
25   linear_tonemap: [Optional] boolean whether linear tonemap should be used.
26   props: [Optional] object from its_session_utils.get_camera_properties().
27          Must present when linear_tonemap is True.
28
29  Returns:
30    Auto capture request, ready to be passed to the
31    its_session_utils.device.do_capture()
32  """
33  req = {
34      'android.control.mode': 1,
35      'android.control.aeMode': 1,
36      'android.control.awbMode': 1,
37      'android.control.afMode': 1,
38      'android.colorCorrection.mode': 1,
39      'android.tonemap.mode': 1,
40      'android.lens.opticalStabilizationMode': 0,
41      'android.control.videoStabilizationMode': 0
42  }
43  if linear_tonemap:
44    if props is None:
45      raise AssertionError('props is None with linear_tonemap.')
46    # CONTRAST_CURVE mode
47    if 0 in props['android.tonemap.availableToneMapModes']:
48      req['android.tonemap.mode'] = 0
49      req['android.tonemap.curve'] = {
50          'red': [0.0, 0.0, 1.0, 1.0],  # coordinate pairs: x0, y0, x1, y1
51          'green': [0.0, 0.0, 1.0, 1.0],
52          'blue': [0.0, 0.0, 1.0, 1.0]
53      }
54    # GAMMA_VALUE mode
55    elif 3 in props['android.tonemap.availableToneMapModes']:
56      req['android.tonemap.mode'] = 3
57      req['android.tonemap.gamma'] = 1.0
58    else:
59      raise AssertionError('Linear tonemap is not supported')
60  return req
61
62
63def manual_capture_request(sensitivity,
64                           exp_time,
65                           f_distance=0.0,
66                           linear_tonemap=False,
67                           props=None):
68  """Returns a capture request with everything set to manual.
69
70  Uses identity/unit color correction, and the default tonemap curve.
71  Optionally, the tonemap can be specified as being linear.
72
73  Args:
74   sensitivity: The sensitivity value to populate the request with.
75   exp_time: The exposure time, in nanoseconds, to populate the request with.
76   f_distance: The focus distance to populate the request with.
77   linear_tonemap: [Optional] whether a linear tonemap should be used in this
78     request.
79   props: [Optional] the object returned from
80     its_session_utils.get_camera_properties(). Must present when linear_tonemap
81     is True.
82
83  Returns:
84    The default manual capture request, ready to be passed to the
85    its_session_utils.device.do_capture function.
86  """
87  req = {
88      'android.control.captureIntent': 6,
89      'android.control.mode': 0,
90      'android.control.aeMode': 0,
91      'android.control.awbMode': 0,
92      'android.control.afMode': 0,
93      'android.control.effectMode': 0,
94      'android.sensor.sensitivity': sensitivity,
95      'android.sensor.exposureTime': exp_time,
96      'android.colorCorrection.mode': 0,
97      'android.colorCorrection.transform':
98          int_to_rational([1, 0, 0, 0, 1, 0, 0, 0, 1]),
99      'android.colorCorrection.gains': [1, 1, 1, 1],
100      'android.lens.focusDistance': f_distance,
101      'android.tonemap.mode': 1,
102      'android.shading.mode': 1,
103      'android.lens.opticalStabilizationMode': 0,
104      'android.control.videoStabilizationMode': 0,
105  }
106  if linear_tonemap:
107    if props is None:
108      raise AssertionError('props is None.')
109    # CONTRAST_CURVE mode
110    if 0 in props['android.tonemap.availableToneMapModes']:
111      req['android.tonemap.mode'] = 0
112      req['android.tonemap.curve'] = {
113          'red': [0.0, 0.0, 1.0, 1.0],
114          'green': [0.0, 0.0, 1.0, 1.0],
115          'blue': [0.0, 0.0, 1.0, 1.0]
116      }
117    # GAMMA_VALUE mode
118    elif 3 in props['android.tonemap.availableToneMapModes']:
119      req['android.tonemap.mode'] = 3
120      req['android.tonemap.gamma'] = 1.0
121    else:
122      raise AssertionError('Linear tonemap is not supported')
123  return req
124
125
126def get_available_output_sizes(fmt, props, max_size=None, match_ar_size=None):
127  """Return a sorted list of available output sizes for a given format.
128
129  Args:
130   fmt: the output format, as a string in ['jpg', 'yuv', 'raw', 'raw10',
131     'raw12', 'y8'].
132   props: the object returned from its_session_utils.get_camera_properties().
133   max_size: (Optional) A (w,h) tuple.Sizes larger than max_size (either w or h)
134     will be discarded.
135   match_ar_size: (Optional) A (w,h) tuple.Sizes not matching the aspect ratio
136     of match_ar_size will be discarded.
137
138  Returns:
139    A sorted list of (w,h) tuples (sorted large-to-small).
140  """
141  ar_tolerance = 0.03
142  fmt_codes = {
143      'raw': 0x20,
144      'raw10': 0x25,
145      'raw12': 0x26,
146      'yuv': 0x23,
147      'jpg': 0x100,
148      'jpeg': 0x100,
149      'y8': 0x20203859
150  }
151  configs = props[
152      'android.scaler.streamConfigurationMap']['availableStreamConfigurations']
153  fmt_configs = [cfg for cfg in configs if cfg['format'] == fmt_codes[fmt]]
154  out_configs = [cfg for cfg in fmt_configs if not cfg['input']]
155  out_sizes = [(cfg['width'], cfg['height']) for cfg in out_configs]
156  if max_size:
157    out_sizes = [
158        s for s in out_sizes if s[0] <= max_size[0] and s[1] <= max_size[1]
159    ]
160  if match_ar_size:
161    ar = match_ar_size[0] / float(match_ar_size[1])
162    out_sizes = [
163        s for s in out_sizes if abs(ar - s[0] / float(s[1])) <= ar_tolerance
164    ]
165  out_sizes.sort(reverse=True, key=lambda s: s[0])  # 1st pass, sort by width
166  out_sizes.sort(reverse=True, key=lambda s: s[0] * s[1])  # sort by area
167  return out_sizes
168
169
170def float_to_rational(f, denom=128):
171  """Function to convert Python floats to Camera2 rationals.
172
173  Args:
174    f: python float or list of floats.
175    denom: (Optional) the denominator to use in the output rationals.
176
177  Returns:
178    Python dictionary or list of dictionaries representing the given
179    float(s) as rationals.
180  """
181  if isinstance(f, list):
182    return [{'numerator': math.floor(val*denom+0.5), 'denominator': denom}
183            for val in f]
184  else:
185    return {'numerator': math.floor(f*denom+0.5), 'denominator': denom}
186
187
188def rational_to_float(r):
189  """Function to convert Camera2 rational objects to Python floats.
190
191  Args:
192   r: Rational or list of rationals, as Python dictionaries.
193
194  Returns:
195   Float or list of floats.
196  """
197  if isinstance(r, list):
198    return [float(val['numerator']) / float(val['denominator']) for val in r]
199  else:
200    return float(r['numerator']) / float(r['denominator'])
201
202
203def get_fastest_manual_capture_settings(props):
204  """Returns a capture request and format spec for the fastest manual capture.
205
206  Args:
207     props: the object returned from its_session_utils.get_camera_properties().
208
209  Returns:
210    Two values, the first is a capture request, and the second is an output
211    format specification, for the fastest possible (legal) capture that
212    can be performed on this device (with the smallest output size).
213  """
214  fmt = 'yuv'
215  size = get_available_output_sizes(fmt, props)[-1]
216  out_spec = {'format': fmt, 'width': size[0], 'height': size[1]}
217  s = min(props['android.sensor.info.sensitivityRange'])
218  e = min(props['android.sensor.info.exposureTimeRange'])
219  req = manual_capture_request(s, e)
220
221  turn_slow_filters_off(props, req)
222
223  return req, out_spec
224
225
226def get_fastest_auto_capture_settings(props):
227  """Returns a capture request and format spec for the fastest auto capture.
228
229  Args:
230     props: the object returned from its_session_utils.get_camera_properties().
231
232  Returns:
233      Two values, the first is a capture request, and the second is an output
234      format specification, for the fastest possible (legal) capture that
235      can be performed on this device (with the smallest output size).
236  """
237  fmt = 'yuv'
238  size = get_available_output_sizes(fmt, props)[-1]
239  out_spec = {'format': fmt, 'width': size[0], 'height': size[1]}
240  req = auto_capture_request()
241
242  turn_slow_filters_off(props, req)
243
244  return req, out_spec
245
246
247def fastest_auto_capture_request(props):
248  """Return an auto capture request for the fastest capture.
249
250  Args:
251    props: the object returned from its.device.get_camera_properties().
252
253  Returns:
254    A capture request with everything set to auto and all filters that
255    may slow down capture set to OFF or FAST if possible
256  """
257  req = auto_capture_request()
258  turn_slow_filters_off(props, req)
259  return req
260
261
262def turn_slow_filters_off(props, req):
263  """Turn filters that may slow FPS down to OFF or FAST in input request.
264
265   This function modifies the request argument, such that filters that may
266   reduce the frames-per-second throughput of the camera device will be set to
267   OFF or FAST if possible.
268
269  Args:
270    props: the object returned from its_session_utils.get_camera_properties().
271    req: the input request.
272
273  Returns:
274    Nothing.
275  """
276  set_filter_off_or_fast_if_possible(
277      props, req, 'android.noiseReduction.availableNoiseReductionModes',
278      'android.noiseReduction.mode')
279  set_filter_off_or_fast_if_possible(
280      props, req, 'android.colorCorrection.availableAberrationModes',
281      'android.colorCorrection.aberrationMode')
282  if 'camera.characteristics.keys' in props:
283    chars_keys = props['camera.characteristics.keys']
284    hot_pixel_modes = 'android.hotPixel.availableHotPixelModes' in chars_keys
285    edge_modes = 'android.edge.availableEdgeModes' in chars_keys
286  if 'camera.characteristics.requestKeys' in props:
287    req_keys = props['camera.characteristics.requestKeys']
288    hot_pixel_mode = 'android.hotPixel.mode' in req_keys
289    edge_mode = 'android.edge.mode' in req_keys
290  if hot_pixel_modes and hot_pixel_mode:
291    set_filter_off_or_fast_if_possible(
292        props, req, 'android.hotPixel.availableHotPixelModes',
293        'android.hotPixel.mode')
294  if edge_modes and edge_mode:
295    set_filter_off_or_fast_if_possible(props, req,
296                                       'android.edge.availableEdgeModes',
297                                       'android.edge.mode')
298
299
300def set_filter_off_or_fast_if_possible(props, req, available_modes, filter_key):
301  """Check and set controlKey to off or fast in req.
302
303  Args:
304    props: the object returned from its.device.get_camera_properties().
305    req: the input request. filter will be set to OFF or FAST if possible.
306    available_modes: the key to check available modes.
307    filter_key: the filter key
308
309  Returns:
310    Nothing.
311  """
312  if available_modes in props:
313    if 0 in props[available_modes]:
314      req[filter_key] = 0
315    elif 1 in props[available_modes]:
316      req[filter_key] = 1
317
318
319def int_to_rational(i):
320  """Function to convert Python integers to Camera2 rationals.
321
322  Args:
323   i: Python integer or list of integers.
324
325  Returns:
326    Python dictionary or list of dictionaries representing the given int(s)
327    as rationals with denominator=1.
328  """
329  if isinstance(i, list):
330    return [{'numerator': val, 'denominator': 1} for val in i]
331  else:
332    return {'numerator': i, 'denominator': 1}
333
334
335def get_largest_yuv_format(props, match_ar=None):
336  """Return a capture request and format spec for the largest yuv size.
337
338  Args:
339    props: object returned from camera_properties_utils.get_camera_properties().
340    match_ar: (Optional) a (w, h) tuple. Aspect ratio to match during search.
341
342  Returns:
343    fmt:   an output format specification for the largest possible yuv format
344           for this device.
345  """
346  size = get_available_output_sizes('yuv', props, match_ar_size=match_ar)[0]
347  fmt = {'format': 'yuv', 'width': size[0], 'height': size[1]}
348
349  return fmt
350
351
352def get_smallest_yuv_format(props, match_ar=None):
353  """Return a capture request and format spec for the smallest yuv size.
354
355  Args:
356    props: object returned from camera_properties_utils.get_camera_properties().
357    match_ar: (Optional) a (w, h) tuple. Aspect ratio to match during search.
358
359  Returns:
360    fmt:   an output format specification for the smallest possible yuv format
361           for this device.
362  """
363  size = get_available_output_sizes('yuv', props, match_ar_size=match_ar)[-1]
364  fmt = {'format': 'yuv', 'width': size[0], 'height': size[1]}
365
366  return fmt
367
368
369def get_largest_jpeg_format(props, match_ar=None):
370  """Return a capture request and format spec for the largest jpeg size.
371
372  Args:
373    props: object returned from camera_properties_utils.get_camera_properties().
374    match_ar: (Optional) a (w, h) tuple. Aspect ratio to match during search.
375
376  Returns:
377    fmt:   an output format specification for the largest possible jpeg format
378           for this device.
379  """
380  size = get_available_output_sizes('jpeg', props, match_ar_size=match_ar)[0]
381  fmt = {'format': 'jpeg', 'width': size[0], 'height': size[1]}
382
383  return fmt
384
385
386def get_max_digital_zoom(props):
387  """Returns the maximum amount of zooming possible by the camera device.
388
389  Args:
390    props: the object returned from its.device.get_camera_properties().
391
392  Return:
393    A float indicating the maximum amount of zoom possible by the camera device.
394  """
395
396  max_z = 1.0
397  if 'android.scaler.availableMaxDigitalZoom' in props:
398    max_z = props['android.scaler.availableMaxDigitalZoom']
399
400  return max_z
401
402
403class CaptureRequestUtilsTest(unittest.TestCase):
404  """Unit tests for this module.
405
406  Ensures rational number conversion dicts are created properly.
407  """
408  _FLOAT_HALF = 0.5
409  # No immutable container: frozendict requires package install on partner host
410  _RATIONAL_HALF = {'numerator': 32, 'denominator': 64}
411
412  def test_float_to_rational(self):
413    """Unit test for float_to_rational."""
414    self.assertEqual(
415        float_to_rational(self._FLOAT_HALF, 64), self._RATIONAL_HALF)
416
417  def test_rational_to_float(self):
418    """Unit test for rational_to_float."""
419    self.assertTrue(
420        math.isclose(rational_to_float(self._RATIONAL_HALF),
421                     self._FLOAT_HALF, abs_tol=0.0001))
422
423  def test_int_to_rational(self):
424    """Unit test for int_to_rational."""
425    rational_10 = {'numerator': 10, 'denominator': 1}
426    rational_1 = {'numerator': 1, 'denominator': 1}
427    rational_2 = {'numerator': 2, 'denominator': 1}
428    # Simple test
429    self.assertEqual(int_to_rational(10), rational_10)
430    # Handle list entries
431    self.assertEqual(
432        int_to_rational([1, 2]), [rational_1, rational_2])
433
434if __name__ == '__main__':
435  unittest.main()
436