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