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 logging
18import math
19
20_AE_MODE_ON_AUTO_FLASH = 2
21_AE_PRECAPTURE_TRIGGER_START = 1
22_AE_PRECAPTURE_TRIGGER_IDLE = 0
23_CAPTURE_INTENT_STILL_CAPTURE = 2
24_CAPTURE_INTENT_PREVIEW = 1
25_COMMON_IMG_ARS = (4/3, 16/9)
26_COMMON_IMG_ARS_ATOL = 0.01
27_FLASH_MODE_SINGLE = 1
28FMT_CODE_JPEG = 0x100
29FMT_CODE_JPEG_R = 0x1005
30FMT_CODE_PRIV = 0x22
31FMT_CODE_RAW = 0x20
32FMT_CODE_RAW10 = 0x25
33FMT_CODE_RAW12 = 0x26
34FMT_CODE_YUV = 0x23  # YUV_420_888
35FMT_CODE_Y8 = 0x20203859
36_MAX_YUV_SIZE = (1920, 1080)
37_MIN_YUV_SIZE = (640, 360)
38_VGA_W, _VGA_H = (640, 480)
39
40
41def is_common_aspect_ratio(size):
42  """Returns if aspect ratio is a 4:3 or 16:9.
43
44  Args:
45    size: tuple of image (w, h)
46
47  Returns:
48    Boolean
49  """
50  for aspect_ratio in _COMMON_IMG_ARS:
51    if math.isclose(size[0]/size[1], aspect_ratio,
52                    abs_tol=_COMMON_IMG_ARS_ATOL):
53      return True
54  return False
55
56
57def auto_capture_request(linear_tonemap=False, props=None, do_af=True,
58                         do_autoframing=False, zoom_ratio=None):
59  """Returns a capture request with everything set to auto.
60
61  Args:
62   linear_tonemap: [Optional] boolean whether linear tonemap should be used.
63   props: [Optional] object from its_session_utils.get_camera_properties().
64          Must present when linear_tonemap is True.
65   do_af: [Optional] boolean whether af mode should be active.
66   do_autoframing: [Optional] boolean whether autoframing should be active.
67   zoom_ratio: [Optional] zoom ratio to be set in the capture request.
68
69  Returns:
70    Auto capture request, ready to be passed to the
71    its_session_utils.device.do_capture()
72  """
73  req = {
74      'android.control.mode': 1,
75      'android.control.aeMode': 1,
76      'android.control.awbMode': 1,
77      'android.control.afMode': 1 if do_af else 0,
78      'android.colorCorrection.mode': 1,
79      'android.shading.mode': 1,
80      'android.tonemap.mode': 1,
81      'android.lens.opticalStabilizationMode': 0,
82      'android.control.videoStabilizationMode': 0,
83  }
84  if do_autoframing:
85    req['android.control.autoframing'] = 1
86  if not do_af:
87    req['android.lens.focusDistance'] = 0.0
88  if zoom_ratio:
89    req['android.control.zoomRatio'] = zoom_ratio
90  if linear_tonemap:
91    if props is None:
92      raise AssertionError('props is None with linear_tonemap.')
93    # CONTRAST_CURVE mode
94    if 0 in props['android.tonemap.availableToneMapModes']:
95      logging.debug('CONTRAST_CURVE tonemap mode')
96      req['android.tonemap.mode'] = 0
97      req['android.tonemap.curve'] = {
98          'red': [0.0, 0.0, 1.0, 1.0],  # coordinate pairs: x0, y0, x1, y1
99          'green': [0.0, 0.0, 1.0, 1.0],
100          'blue': [0.0, 0.0, 1.0, 1.0]
101      }
102    # GAMMA_VALUE mode
103    elif 3 in props['android.tonemap.availableToneMapModes']:
104      logging.debug('GAMMA_VALUE tonemap mode')
105      req['android.tonemap.mode'] = 3
106      req['android.tonemap.gamma'] = 1.0
107    else:
108      raise AssertionError('Linear tonemap is not supported')
109  return req
110
111
112def manual_capture_request(sensitivity,
113                           exp_time,
114                           f_distance=0.0,
115                           linear_tonemap=False,
116                           props=None):
117  """Returns a capture request with everything set to manual.
118
119  Uses identity/unit color correction, and the default tonemap curve.
120  Optionally, the tonemap can be specified as being linear.
121
122  Args:
123   sensitivity: The sensitivity value to populate the request with.
124   exp_time: The exposure time, in nanoseconds, to populate the request with.
125   f_distance: The focus distance to populate the request with.
126   linear_tonemap: [Optional] whether a linear tonemap should be used in this
127     request.
128   props: [Optional] the object returned from
129     its_session_utils.get_camera_properties(). Must present when linear_tonemap
130     is True.
131
132  Returns:
133    The default manual capture request, ready to be passed to the
134    its_session_utils.device.do_capture function.
135  """
136  req = {
137      'android.control.captureIntent': 6,
138      'android.control.mode': 0,
139      'android.control.aeMode': 0,
140      'android.control.awbMode': 0,
141      'android.control.afMode': 0,
142      'android.control.effectMode': 0,
143      'android.sensor.sensitivity': sensitivity,
144      'android.sensor.exposureTime': exp_time,
145      'android.colorCorrection.mode': 0,
146      'android.colorCorrection.transform':
147          int_to_rational([1, 0, 0, 0, 1, 0, 0, 0, 1]),
148      'android.colorCorrection.gains': [1, 1, 1, 1],
149      'android.lens.focusDistance': f_distance,
150      'android.tonemap.mode': 1,
151      'android.shading.mode': 1,
152      'android.lens.opticalStabilizationMode': 0,
153      'android.control.videoStabilizationMode': 0,
154  }
155  if linear_tonemap:
156    if props is None:
157      raise AssertionError('props is None.')
158    # CONTRAST_CURVE mode
159    if 0 in props['android.tonemap.availableToneMapModes']:
160      logging.debug('CONTRAST_CURVE tonemap mode')
161      req['android.tonemap.mode'] = 0
162      req['android.tonemap.curve'] = {
163          'red': [0.0, 0.0, 1.0, 1.0],
164          'green': [0.0, 0.0, 1.0, 1.0],
165          'blue': [0.0, 0.0, 1.0, 1.0]
166      }
167    # GAMMA_VALUE mode
168    elif 3 in props['android.tonemap.availableToneMapModes']:
169      logging.debug('GAMMA_VALUE tonemap mode')
170      req['android.tonemap.mode'] = 3
171      req['android.tonemap.gamma'] = 1.0
172    else:
173      raise AssertionError('Linear tonemap is not supported')
174  return req
175
176
177def get_available_output_sizes(fmt, props, max_size=None, match_ar_size=None):
178  """Return a sorted list of available output sizes for a given format.
179
180  Args:
181   fmt: the output format, as a string in ['jpg', 'yuv', 'raw', 'raw10',
182     'raw12', 'y8'].
183   props: the object returned from its_session_utils.get_camera_properties().
184   max_size: (Optional) A (w,h) tuple.Sizes larger than max_size (either w or h)
185     will be discarded.
186   match_ar_size: (Optional) A (w,h) tuple.Sizes not matching the aspect ratio
187     of match_ar_size will be discarded.
188
189  Returns:
190    A sorted list of (w,h) tuples (sorted large-to-small).
191  """
192  ar_tolerance = 0.03
193  fmt_codes = {
194      'raw': FMT_CODE_RAW,
195      'raw10': FMT_CODE_RAW10,
196      'raw12': FMT_CODE_RAW12,
197      'yuv': FMT_CODE_YUV,
198      'jpg': FMT_CODE_JPEG,
199      'jpeg': FMT_CODE_JPEG,
200      'jpeg_r': FMT_CODE_JPEG_R,
201      'priv': FMT_CODE_PRIV,
202      'y8': FMT_CODE_Y8
203  }
204  configs = props[
205      'android.scaler.streamConfigurationMap']['availableStreamConfigurations']
206  fmt_configs = [cfg for cfg in configs if cfg['format'] == fmt_codes[fmt]]
207  out_configs = [cfg for cfg in fmt_configs if not cfg['input']]
208  out_sizes = [(cfg['width'], cfg['height']) for cfg in out_configs]
209  if max_size:
210    max_size = [int(i) for i in max_size]
211    out_sizes = [
212        s for s in out_sizes if s[0] <= max_size[0] and s[1] <= max_size[1]
213    ]
214  if match_ar_size:
215    ar = match_ar_size[0] / match_ar_size[1]
216    out_sizes = [
217        s for s in out_sizes if abs(ar - s[0] / float(s[1])) <= ar_tolerance
218    ]
219  out_sizes.sort(reverse=True, key=lambda s: s[0])  # 1st pass, sort by width
220  out_sizes.sort(reverse=True, key=lambda s: s[0] * s[1])  # sort by area
221  logging.debug('Available %s output sizes: %s', fmt, out_sizes)
222  return out_sizes
223
224
225def float_to_rational(f, denom=128):
226  """Function to convert Python floats to Camera2 rationals.
227
228  Args:
229    f: python float or list of floats.
230    denom: (Optional) the denominator to use in the output rationals.
231
232  Returns:
233    Python dictionary or list of dictionaries representing the given
234    float(s) as rationals.
235  """
236  if isinstance(f, list):
237    return [{'numerator': math.floor(val*denom+0.5), 'denominator': denom}
238            for val in f]
239  else:
240    return {'numerator': math.floor(f*denom+0.5), 'denominator': denom}
241
242
243def rational_to_float(r):
244  """Function to convert Camera2 rational objects to Python floats.
245
246  Args:
247   r: Rational or list of rationals, as Python dictionaries.
248
249  Returns:
250   Float or list of floats.
251  """
252  if isinstance(r, list):
253    return [float(val['numerator']) / float(val['denominator']) for val in r]
254  else:
255    return float(r['numerator']) / float(r['denominator'])
256
257
258def get_fastest_manual_capture_settings(props):
259  """Returns a capture request and format spec for the fastest manual capture.
260
261  Args:
262     props: the object returned from its_session_utils.get_camera_properties().
263
264  Returns:
265    Two values, the first is a capture request, and the second is an output
266    format specification, for the fastest possible (legal) capture that
267    can be performed on this device (with the smallest output size).
268  """
269  fmt = 'yuv'
270  size = get_available_output_sizes(fmt, props)[-1]
271  out_spec = {'format': fmt, 'width': size[0], 'height': size[1]}
272  s = min(props['android.sensor.info.sensitivityRange'])
273  e = min(props['android.sensor.info.exposureTimeRange'])
274  req = manual_capture_request(s, e)
275
276  turn_slow_filters_off(props, req)
277
278  return req, out_spec
279
280
281def get_fastest_auto_capture_settings(props):
282  """Returns a capture request and format spec for the fastest auto capture.
283
284  Args:
285     props: the object returned from its_session_utils.get_camera_properties().
286
287  Returns:
288      Two values, the first is a capture request, and the second is an output
289      format specification, for the fastest possible (legal) capture that
290      can be performed on this device (with the smallest output size).
291  """
292  fmt = 'yuv'
293  size = get_available_output_sizes(fmt, props)[-1]
294  out_spec = {'format': fmt, 'width': size[0], 'height': size[1]}
295  req = auto_capture_request()
296
297  turn_slow_filters_off(props, req)
298
299  return req, out_spec
300
301
302def fastest_auto_capture_request(props):
303  """Return an auto capture request for the fastest capture.
304
305  Args:
306    props: the object returned from its.device.get_camera_properties().
307
308  Returns:
309    A capture request with everything set to auto and all filters that
310    may slow down capture set to OFF or FAST if possible
311  """
312  req = auto_capture_request()
313  turn_slow_filters_off(props, req)
314  return req
315
316
317def turn_slow_filters_off(props, req):
318  """Turn filters that may slow FPS down to OFF or FAST in input request.
319
320   This function modifies the request argument, such that filters that may
321   reduce the frames-per-second throughput of the camera device will be set to
322   OFF or FAST if possible.
323
324  Args:
325    props: the object returned from its_session_utils.get_camera_properties().
326    req: the input request.
327
328  Returns:
329    Nothing.
330  """
331  set_filter_off_or_fast_if_possible(
332      props, req, 'android.noiseReduction.availableNoiseReductionModes',
333      'android.noiseReduction.mode')
334  set_filter_off_or_fast_if_possible(
335      props, req, 'android.colorCorrection.availableAberrationModes',
336      'android.colorCorrection.aberrationMode')
337  if 'camera.characteristics.keys' in props:
338    chars_keys = props['camera.characteristics.keys']
339    hot_pixel_modes = 'android.hotPixel.availableHotPixelModes' in chars_keys
340    edge_modes = 'android.edge.availableEdgeModes' in chars_keys
341  if 'camera.characteristics.requestKeys' in props:
342    req_keys = props['camera.characteristics.requestKeys']
343    hot_pixel_mode = 'android.hotPixel.mode' in req_keys
344    edge_mode = 'android.edge.mode' in req_keys
345  if hot_pixel_modes and hot_pixel_mode:
346    set_filter_off_or_fast_if_possible(
347        props, req, 'android.hotPixel.availableHotPixelModes',
348        'android.hotPixel.mode')
349  if edge_modes and edge_mode:
350    set_filter_off_or_fast_if_possible(props, req,
351                                       'android.edge.availableEdgeModes',
352                                       'android.edge.mode')
353
354
355def set_filter_off_or_fast_if_possible(props, req, available_modes, filter_key):
356  """Check and set controlKey to off or fast in req.
357
358  Args:
359    props: the object returned from its.device.get_camera_properties().
360    req: the input request. filter will be set to OFF or FAST if possible.
361    available_modes: the key to check available modes.
362    filter_key: the filter key
363
364  Returns:
365    Nothing.
366  """
367  if available_modes in props:
368    if 0 in props[available_modes]:
369      req[filter_key] = 0
370    elif 1 in props[available_modes]:
371      req[filter_key] = 1
372
373
374def int_to_rational(i):
375  """Function to convert Python integers to Camera2 rationals.
376
377  Args:
378   i: Python integer or list of integers.
379
380  Returns:
381    Python dictionary or list of dictionaries representing the given int(s)
382    as rationals with denominator=1.
383  """
384  if isinstance(i, list):
385    return [{'numerator': val, 'denominator': 1} for val in i]
386  else:
387    return {'numerator': i, 'denominator': 1}
388
389
390def get_largest_yuv_format(props, match_ar=None):
391  """Return a capture request and format spec for the largest yuv size.
392
393  Args:
394    props: object returned from camera_properties_utils.get_camera_properties().
395    match_ar: (Optional) a (w, h) tuple. Aspect ratio to match during search.
396
397  Returns:
398    fmt:   an output format specification for the largest possible yuv format
399           for this device.
400  """
401  size = get_available_output_sizes('yuv', props, match_ar_size=match_ar)[0]
402  fmt = {'format': 'yuv', 'width': size[0], 'height': size[1]}
403
404  return fmt
405
406
407def get_smallest_yuv_format(props, match_ar=None):
408  """Return a capture request and format spec for the smallest yuv size.
409
410  Args:
411    props: object returned from camera_properties_utils.get_camera_properties().
412    match_ar: (Optional) a (w, h) tuple. Aspect ratio to match during search.
413
414  Returns:
415    fmt:   an output format specification for the smallest possible yuv format
416           for this device.
417  """
418  size = get_available_output_sizes('yuv', props, match_ar_size=match_ar)[-1]
419  fmt = {'format': 'yuv', 'width': size[0], 'height': size[1]}
420
421  return fmt
422
423
424def get_near_vga_yuv_format(props, match_ar=None):
425  """Return a capture request and format spec for the smallest yuv size.
426
427  Args:
428    props: object returned from camera_properties_utils.get_camera_properties().
429    match_ar: (Optional) a (w, h) tuple. Aspect ratio to match during search.
430
431  Returns:
432    fmt: an output format specification for the smallest possible yuv format
433           for this device.
434  """
435  sizes = get_available_output_sizes('yuv', props, match_ar_size=match_ar)
436  logging.debug('Available YUV sizes: %s', sizes)
437  max_area = _MAX_YUV_SIZE[1] * _MAX_YUV_SIZE[0]
438  min_area = _MIN_YUV_SIZE[1] * _MIN_YUV_SIZE[0]
439
440  fmt = {'format': 'yuv', 'width': _VGA_W, 'height': _VGA_H}
441  for size in sizes:
442    fmt_area = size[0]*size[1]
443    if fmt_area < min_area or fmt_area > max_area:
444      continue
445    fmt['width'], fmt['height'] = size[0], size[1]
446  logging.debug('YUV format selected: %s', fmt)
447
448  return fmt
449
450
451def get_largest_jpeg_format(props, match_ar=None):
452  """Return a capture request and format spec for the largest jpeg size.
453
454  Args:
455    props: object returned from camera_properties_utils.get_camera_properties().
456    match_ar: (Optional) a (w, h) tuple. Aspect ratio to match during search.
457
458  Returns:
459    fmt:   an output format specification for the largest possible jpeg format
460           for this device.
461  """
462  size = get_available_output_sizes('jpeg', props, match_ar_size=match_ar)[0]
463  fmt = {'format': 'jpeg', 'width': size[0], 'height': size[1]}
464
465  return fmt
466
467
468def get_max_digital_zoom(props):
469  """Returns the maximum amount of zooming possible by the camera device.
470
471  Args:
472    props: the object returned from its.device.get_camera_properties().
473
474  Return:
475    A float indicating the maximum amount of zoom possible by the camera device.
476  """
477
478  max_z = 1.0
479  if 'android.scaler.availableMaxDigitalZoom' in props:
480    max_z = props['android.scaler.availableMaxDigitalZoom']
481
482  return max_z
483
484
485def take_captures_with_flash(cam, out_surface):
486  """Takes capture with auto flash ON.
487
488  Runs precapture sequence by setting the aePrecapture trigger to
489  START and capture intent set to Preview and then take the capture
490  with flash.
491  Args:
492    cam: ItsSession object
493    out_surface: Specifications of the output image format and
494      size to use for the capture.
495
496  Returns:
497    cap: An object which contains following fields:
498      * data: the image data as a numpy array of bytes.
499      * width: the width of the captured image.
500      * height: the height of the captured image.
501      * format: image format
502      * metadata: the capture result object
503  """
504
505  preview_req_start = auto_capture_request()
506  preview_req_start[
507      'android.control.aeMode'] = _AE_MODE_ON_AUTO_FLASH
508  preview_req_start[
509      'android.control.captureIntent'] = _CAPTURE_INTENT_PREVIEW
510  preview_req_start[
511      'android.control.aePrecaptureTrigger'] = _AE_PRECAPTURE_TRIGGER_START
512  # Repeat preview requests with aePrecapture set to IDLE
513  # until AE is converged.
514  preview_req_idle = auto_capture_request()
515  preview_req_idle[
516      'android.control.aeMode'] = _AE_MODE_ON_AUTO_FLASH
517  preview_req_idle[
518      'android.control.captureIntent'] = _CAPTURE_INTENT_PREVIEW
519  preview_req_idle[
520      'android.control.aePrecaptureTrigger'] = _AE_PRECAPTURE_TRIGGER_IDLE
521  # Single still capture request.
522  still_capture_req = auto_capture_request()
523  still_capture_req[
524      'android.control.aeMode'] = _AE_MODE_ON_AUTO_FLASH
525  still_capture_req[
526      'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE
527  still_capture_req[
528      'android.control.aePrecaptureTrigger'] = _AE_PRECAPTURE_TRIGGER_IDLE
529  cap = cam.do_capture_with_flash(preview_req_start,
530                                  preview_req_idle,
531                                  still_capture_req, out_surface)
532  return cap
533
534
535def take_captures_with_flash_strength(cam, out_surface, ae_mode, strength):
536  """Takes capture with desired flash strength.
537
538  Runs precapture sequence by setting the aePrecapture trigger to
539  START and capture intent set to Preview.
540  Then, take the capture with set flash strength.
541  Args:
542    cam: ItsSession object
543    out_surface: Specifications of the output image format and
544      size to use for the capture.
545    ae_mode: AE_mode
546    strength: flash strength
547
548  Returns:
549    cap: An object which contains following fields:
550      * data: the image data as a numpy array of bytes.
551      * width: the width of the captured image.
552      * height: the height of the captured image.
553      * format: image format
554      * metadata: the capture result object
555  """
556  preview_req_start = auto_capture_request()
557  preview_req_start[
558      'android.control.aeMode'] = _AE_MODE_ON_AUTO_FLASH
559  preview_req_start[
560      'android.control.captureIntent'] = _CAPTURE_INTENT_PREVIEW
561  preview_req_start[
562      'android.control.aePrecaptureTrigger'] = _AE_PRECAPTURE_TRIGGER_START
563  # Repeat preview requests with aePrecapture set to IDLE
564  # until AE is converged.
565  preview_req_idle = auto_capture_request()
566  preview_req_idle[
567      'android.control.aeMode'] = _AE_MODE_ON_AUTO_FLASH
568  preview_req_idle[
569      'android.control.captureIntent'] = _CAPTURE_INTENT_PREVIEW
570  preview_req_idle[
571      'android.control.aePrecaptureTrigger'] = _AE_PRECAPTURE_TRIGGER_IDLE
572  # Single still capture request.
573  still_capture_req = auto_capture_request()
574  still_capture_req[
575      'android.control.aeMode'] = ae_mode
576  still_capture_req[
577      'android.flash.mode'] = _FLASH_MODE_SINGLE
578  still_capture_req[
579      'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE
580  still_capture_req[
581      'android.control.aePrecaptureTrigger'] = _AE_PRECAPTURE_TRIGGER_IDLE
582  still_capture_req[
583      'android.flash.strengthLevel'] = strength
584  cap = cam.do_capture_with_flash(preview_req_start,
585                                  preview_req_idle,
586                                  still_capture_req, out_surface)
587  return cap
588
589