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