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