diff --git a/docs/api_vcsmobj.rst b/docs/api_vcsmobj.rst new file mode 100644 index 00000000..ae3715fc --- /dev/null +++ b/docs/api_vcsmobj.rst @@ -0,0 +1,105 @@ +.. _api_mmalobj: + +============= +API - vcsmobj +============= + +.. module:: picamera.vcsmobj + +.. currentmodule:: picamera.vcsmobj + +This module provides an object-oriented interface to the VideoCore shared +memory API, to make it simpler to use from Python. + +.. warning:: + + This part of the API is still experimental and subject to change in future + versions. Backwards compatibility is not (yet) guaranteed. + + +The Shared Memory Interface +=========================== +Some MMAL functions (see :mod:`~picamera.mmalobj`) need to pass larger amounts +of information to the GPU, which we can achieve using shared memory. The API +doesn't provide a persistent way to "hold on" to a block of shared memory, which +means this transfer is effectively one way, i.e. it allows us to transfer data +onto the GPU, but not back off again. + +The process of getting some data onto the GPU usually consists of: + +#. Allocate a block of shared memory on the GPU. +#. Get a "handle" that identifies the block of memory. +#. "Lock" the block of memory - stop the GPU from accessing it, and make it + available to the CPU. +#. Copy the data into the block of memory. +#. Unlock the memory. +#. Unallocate the block of memory so it can be re-used later. + +It's important that all of these steps are done in sequence; interrupting the +process can leave blocks of memory allocated (or worse, locked) when they are +no longer needed, which eventually causes crashes. That's the purpose of this +module; it wraps up the above process in higher-level functions, making it much +harder to accidentally crash the GPU. This module also makes sure the VCSM +interface is initialised and shut down cleanly. + +A Simple Example +---------------- +The :class:`VideoCoreSharedMemory` class represents a block of memory. This +block is initialised when the object is created, and freed when the object +is destroyed. It also provides functions that will copy data in from either a +`ctypes` buffer object or a `numpy` array. Our example starts by importing +the shared memory class (usually the only part of the module that is required) +and creating an array to send to the GPU. This uses ``numpy`` to create a +10x10 array of bytes: + +.. code-block:: pycon + + >>> from picamera.vcsmobj import VideoCoreSharedMemory + >>> import numpy as np + >>> w = 10 + >>> h = 10 + >>> data_to_send = np.ones((w, h), dtype=np.uint8) + +Next, we allocate some shared memory and copy our ``numpy`` array into it: + +.. code-block:: pycon + + >>> shared_mem = VideoCoreSharedMemory(w*h, "test_data") + >>> shared_mem.copy_from_array(data_to_send) + +You can then use this block of shared memory in a function (usually in the +MMAL library) by referring to ``shared_mem.videocore_handle`` which returns +a handle that identifies the block of memory to functions that run on the +GPU. The block of memory is freed when the object is garbage-collected by +Python, so there is no need to explicitly free it again. The first time +you allocate shared memory, the interface is initialised, and the module +will take care of closing down the shared memory interface when Python exits. + + +Classes +======= + +The VCSM wrapper can be used through one class, :class:`VideoCoreSharedMemory`. +This handles allocating and freeing the memory, as well as locking it and +copying in the data. There is a further class defined, used only to manage +initialising and closing the low-level VCSM interface +(:class:`VideoCoreSharedMemoryServiceManager`). + +.. autoclass:: VideoCoreSharedMemory + +.. autoclass:: VideoCoreSharedMemoryServiceManager + + +Functions +========= + +It is possible and recommended to use this module only through +:class:`VideoCoreSharedMemory`. However, there are a couple of functions +defined to explicitly initialise and shut down the shared memory interface. +These don't need to be called unless you are worried about the (very small) +overhead of starting the interface the first time you allocate memory. + +.. autofunction:: ensure_vcsm_init + +.. autofunction:: vcsm_exit + diff --git a/picamera/bcm_host.py b/picamera/bcm_host.py index 7ecd362b..3253f218 100644 --- a/picamera/bcm_host.py +++ b/picamera/bcm_host.py @@ -91,11 +91,11 @@ def VCOS_ALIGN_UP(value, round_to): # Note: this function assumes round_to is some power of 2. - return (value + (round_to - 1)) & ~(round_to - 1) + return int(value + (round_to - 1)) & ~(round_to - 1) def VCOS_ALIGN_DOWN(value, round_to): # Note: this function assumes round_to is some power of 2. - return value & ~(round_to - 1) + return int(value) & ~(round_to - 1) # vc_image_types.h ########################################################### diff --git a/picamera/camera.py b/picamera/camera.py index 01ec65fd..13ae4717 100644 --- a/picamera/camera.py +++ b/picamera/camera.py @@ -46,7 +46,7 @@ from operator import itemgetter from collections import namedtuple -from . import bcm_host, mmal, mmalobj as mo +from . import bcm_host, mmal, mmalobj as mo, vcsmobj from .exc import ( PiCameraError, PiCameraValueError, @@ -115,9 +115,10 @@ class PiCamera(object): will represent. Only the Raspberry Pi compute module currently supports more than one camera. - The *sensor_mode*, *resolution*, *framerate*, *framerate_range*, and - *clock_mode* parameters provide initial values for the :attr:`sensor_mode`, - :attr:`resolution`, :attr:`framerate`, :attr:`framerate_range`, and + The *sensor_mode*, *resolution*, *framerate*, *framerate_range*, + *lens_shading_table*, and *clock_mode* parameters provide initial values + for the :attr:`sensor_mode`, :attr:`resolution`, :attr:`framerate`, + :attr:`framerate_range`, :attr:`lens_shading_table` and :attr:`clock_mode` attributes of the class (these attributes are all relatively expensive to set individually, hence setting them all upon construction is a speed optimization). Please refer to the attribute @@ -182,6 +183,9 @@ class PiCamera(object): .. versionchanged:: 1.13 Added *framerate_range* parameter. + + .. versionchanged:: 1.14 + Made *analog_gain* and *digital_gain* writeable and added *lens_shading_table* argument. .. _Compute Module: https://www.raspberrypi.org/documentation/hardware/computemodule/cmio-camera.md """ @@ -323,12 +327,13 @@ class PiCamera(object): '_raw_format', '_image_effect_params', '_exif_tags', + '_lens_shading_table', ) def __init__( self, camera_num=0, stereo_mode='none', stereo_decimate=False, resolution=None, framerate=None, sensor_mode=0, led_pin=None, - clock_mode='reset', framerate_range=None): + clock_mode='reset', lens_shading_table=None, framerate_range=None): bcm_host.bcm_host_init() mimetypes.add_type('application/h264', '.h264', False) mimetypes.add_type('application/mjpeg', '.mjpg', False) @@ -364,6 +369,7 @@ def __init__( self._overlays = [] self._raw_format = 'yuv' self._image_effect_params = None + self._lens_shading_table = None with mo.MMALCameraInfo() as camera_info: info = camera_info.control.params[mmal.MMAL_PARAMETER_CAMERA_INFO] self._revision = 'ov5647' @@ -429,7 +435,7 @@ def __init__( raise PiCameraValueError('Invalid clock mode: %s' % clock_mode) try: self._init_camera(camera_num, stereo_mode, stereo_decimate) - self._configure_camera(sensor_mode, framerate, resolution, clock_mode) + self._configure_camera(sensor_mode, framerate, resolution, clock_mode, lens_shading_table) self._init_preview() self._init_splitter() self._camera.enable() @@ -2004,7 +2010,7 @@ def _control_callback(self, port, buf): def _configure_camera( self, sensor_mode, framerate, resolution, clock_mode, - old_sensor_mode=0): + lens_shading_table=None, old_sensor_mode=0): """ An internal method for setting a new camera mode, framerate, resolution, and/or clock_mode. @@ -2021,6 +2027,7 @@ def _configure_camera( (port.framesize, port.framerate, port.params[mmal.MMAL_PARAMETER_FPS_RANGE]) for port in self._camera.outputs ] + self._upload_lens_shading_table(lens_shading_table) if old_sensor_mode != 0 or sensor_mode != 0: self._camera.control.params[mmal.MMAL_PARAMETER_CAMERA_CUSTOM_SENSOR_CONFIG] = sensor_mode if not self._camera.control.enabled: @@ -2107,10 +2114,11 @@ def _set_framerate(self, value): sensor_mode = self.sensor_mode clock_mode = self.CLOCK_MODES[self.clock_mode] resolution = self.resolution + lens_shading_table = self.lens_shading_table self._disable_camera() self._configure_camera( sensor_mode=sensor_mode, framerate=value, resolution=resolution, - clock_mode=clock_mode) + clock_mode=clock_mode, lens_shading_table=lens_shading_table) self._configure_splitter() self._enable_camera() framerate = property(_get_framerate, _set_framerate, doc="""\ @@ -2182,13 +2190,14 @@ def _set_sensor_mode(self, value): clock_mode = self.CLOCK_MODES[self.clock_mode] resolution = self.resolution framerate = Fraction(self.framerate) + lens_shading_table = self.lens_shading_table if framerate == 0: framerate = self.framerate_range self._disable_camera() self._configure_camera( old_sensor_mode=sensor_mode, sensor_mode=value, framerate=framerate, resolution=resolution, - clock_mode=clock_mode) + clock_mode=clock_mode, lens_shading_table=lens_shading_table) self._configure_splitter() self._enable_camera() sensor_mode = property(_get_sensor_mode, _set_sensor_mode, doc="""\ @@ -2216,6 +2225,118 @@ def _set_sensor_mode(self, value): .. versionadded:: 1.9 """) + + def _lens_shading_table_shape(self, sensor_mode=None): + """Calculate the correct shape for a lens shading table. + + The lens shading table is not the full resolution of the camera - it + is defined with one point per 64x64 pixel block. This means the table + should be 1/64 times the size of the sensor, rounding **up** to the + nearest integer. + """ + if sensor_mode is None: + sensor_mode = self.sensor_mode + #TODO: make sure the resolution is appropriate to the camera mode! + full_resolution = [self.MAX_RESOLUTION.width, self.MAX_RESOLUTION.height] + return (4,) + tuple([(r // 64) + 1 for r in full_resolution[::-1]]) + + def _validate_lens_shading_table(self, lens_shading_table, sensor_mode): + """Check a lens shading table is valid and raise an exception if not.""" + table_shape = self._lens_shading_table_shape(sensor_mode) + if lens_shading_table.shape != table_shape: + raise PiCameraValueError("The lens shading table should have shape {} " + "for mode {}".format(table_shape, sensor_mode)) + # Ensure the array is uint8. NB the slightly odd string comparison + # avoids a hard dependency on numpy. + if lens_shading_table.dtype.name != "uint8": + raise PiCameraValueError("Lens shading tables must be uint8") + if not lens_shading_table.flags['C_CONTIGUOUS']: + raise ValueError("The lens shading table must be a C-contiguous numpy array") # make sure the array is contiguous in memory + + def _upload_lens_shading_table(self, lens_shading_table, sensor_mode=None): + """Actually commit the lens shading table to the camera.""" + if lens_shading_table is None: + self._lens_shading_table = None + # Given that we reset the camera each time anyway, hopefully we revert + # to built-in lens shading correction by simply doing nothing here! + return + + self._validate_lens_shading_table(lens_shading_table, sensor_mode) + nchannels, grid_height, grid_width = lens_shading_table.shape + # This sets the lens shading table based on the example code by 6by9 + # https://github.com/6by9/lens_shading/ + shared_memory = vcsmobj.VideoCoreSharedMemory(grid_width*grid_height*4, "ls_grid") # allocate shared memory on the GPU + + lens_shading_parameters = mmal.MMAL_PARAMETER_LENS_SHADING_T( + hdr = mmal.MMAL_PARAMETER_HEADER_T( + mmal.MMAL_PARAMETER_LENS_SHADING_OVERRIDE, + ct.sizeof(mmal.MMAL_PARAMETER_LENS_SHADING_T), + ), + enabled = mmal.MMAL_TRUE, + grid_cell_size = 64, + grid_width = grid_width, + grid_stride = grid_width, + grid_height = grid_height, + mem_handle_table = shared_memory.videocore_handle, + ref_transform = 3,# TODO: figure out what this should be properly!!! + ) + + shared_memory.copy_from_array(lens_shading_table) # copy in the array + self._camera.control.params[mmal.MMAL_PARAMETER_LENS_SHADING_OVERRIDE] = lens_shading_parameters + self._lens_shading_table = lens_shading_table + + def _get_lens_shading_table(self): + self._check_camera_open() + return self._lens_shading_table + def _set_lens_shading_table(self, value): + self._check_camera_open() + self._check_recording_stopped() + #TODO: validate the table here? + sensor_mode = self.sensor_mode + clock_mode = self.CLOCK_MODES[self.clock_mode] + resolution = self.resolution + framerate = Fraction(self.framerate) + if framerate == 0: + framerate = self.framerate_range + self._disable_camera() + self._configure_camera( + old_sensor_mode=sensor_mode, sensor_mode=sensor_mode, + framerate=framerate, resolution=resolution, + clock_mode=clock_mode, lens_shading_table=value) + self._configure_splitter() + self._enable_camera() + lens_shading_table = property(_get_lens_shading_table, + _set_lens_shading_table, doc="""\ + Retrieves or sets the lens shading correction table. + + This is an advanced property which can be used to control the camera's + lens shading correction. By default, images from the camera are + corrected for vignetting by applying different amounts of gain to each + pixel. The lens shading table sets this gain, so if you are using a + lens other than the one supplied with the camera, this property should + allow you to remove vignetting artefacts from your images. NB this + correction is not applied to the raw Bayer data captured with the + option ``bayer=True``. + + + .. note:: + + By default, this property is None, and the camera's built-in lens + shading is used, which is correct for the lens supplied with the + camera module. It is not currently possible to read the lens + shading table back from the GPU, so this property will only have a + useful value if you have previously set it manually. + + Also, using this property with binned or cropped modes of the + camera may produce unpredictable results. + + The initial value of this property can be specified with the + *lens_shading_table* parameter in the :class:`PiCamera` constructor. + As it is an expensive parameter to set otherwise, it is best to use + the constructor rather than to change the property afterwards. + + .. versionadded:: 1.14 + """) def _get_clock_mode(self): self._check_camera_open() @@ -2228,6 +2349,7 @@ def _set_clock_mode(self, value): except KeyError: raise PiCameraValueError("Invalid clock mode %s" % value) sensor_mode = self.sensor_mode + lens_shading_table = self.lens_shading_table framerate = Fraction(self.framerate) if framerate == 0: framerate = self.framerate_range @@ -2235,7 +2357,8 @@ def _set_clock_mode(self, value): self._disable_camera() self._configure_camera( sensor_mode=sensor_mode, framerate=framerate, - resolution=resolution, clock_mode=clock_mode) + resolution=resolution, clock_mode=clock_mode, + lens_shading_table=lens_shading_table) self._configure_splitter() self._enable_camera() clock_mode = property(_get_clock_mode, _set_clock_mode, doc="""\ @@ -2272,12 +2395,14 @@ def _set_resolution(self, value): sensor_mode = self.sensor_mode clock_mode = self.CLOCK_MODES[self.clock_mode] framerate = Fraction(self.framerate) + lens_shading_table = self.lens_shading_table if framerate == 0: framerate = self.framerate_range self._disable_camera() self._configure_camera( sensor_mode=sensor_mode, framerate=framerate, - resolution=value, clock_mode=clock_mode) + resolution=value, clock_mode=clock_mode, + lens_shading_table=lens_shading_table) self._configure_splitter() self._enable_camera() resolution = property(_get_resolution, _set_resolution, doc=""" @@ -2351,10 +2476,12 @@ def _set_framerate_range(self, value): sensor_mode = self.sensor_mode clock_mode = self.CLOCK_MODES[self.clock_mode] resolution = self.resolution + lens_shading_table = self.lens_shading_table self._disable_camera() self._configure_camera( sensor_mode=sensor_mode, framerate=(low, high), - resolution=resolution, clock_mode=clock_mode) + resolution=resolution, clock_mode=clock_mode, + lens_shading_table=lens_shading_table) self._configure_splitter() self._enable_camera() framerate_range = property(_get_framerate_range, _set_framerate_range, doc="""\ @@ -2641,14 +2768,33 @@ def _get_analog_gain(self): self._check_camera_open() return mo.to_fraction( self._camera.control.params[mmal.MMAL_PARAMETER_CAMERA_SETTINGS].analog_gain) - analog_gain = property(_get_analog_gain, doc="""\ - Retrieves the current analog gain of the camera. + def _set_analog_gain(self, value): + self._check_camera_open() + self._camera.control.params[mmal.MMAL_PARAMETER_ANALOG_GAIN] = value + analog_gain = property(_get_analog_gain, _set_analog_gain, doc="""\ + Retrieves or sets the current analog gain of the camera. When queried, this property returns the analog gain currently being used by the camera. The value represents the analog gain of the sensor prior to digital conversion. The value is returned as a :class:`~fractions.Fraction` instance. + + When set, the property adjusts the analog gain of the camera, which + most obviously affects the brightness and noise of subsequently captured + images. Analog gain can be adjusted while previews or recordings are + running. + + .. note:: + Setting the analog gain requires up-to-date userland libraries and + firmware on your Raspberry Pi. Setting the analog gain will raise + a PiCameraMMALError if your userland libraries do not support setting + the analog gain. + + Also, it may be necessary to set the camera's ``iso`` property to 0 + in order for setting the analog gain to work properly, due to the way + it is implemented in the GPU firmware. + .. versionadded:: 1.6 """) @@ -2656,13 +2802,29 @@ def _get_digital_gain(self): self._check_camera_open() return mo.to_fraction( self._camera.control.params[mmal.MMAL_PARAMETER_CAMERA_SETTINGS].digital_gain) - digital_gain = property(_get_digital_gain, doc="""\ + def _set_digital_gain(self, value): + self._check_camera_open() + self._camera.control.params[mmal.MMAL_PARAMETER_DIGITAL_GAIN] = value + digital_gain = property(_get_digital_gain, _set_digital_gain, doc="""\ Retrieves the current digital gain of the camera. When queried, this property returns the digital gain currently being used by the camera. The value represents the digital gain the camera applies after conversion of the sensor's analog output. The value is returned as a :class:`~fractions.Fraction` instance. + + When set, the property adjusts the digital gain of the camera, which + most obviously affects the brightness and noise of subsequently captured + images. Digital gain can be adjusted while previews or recordings are + running. + + .. note:: + + Setting the digital gain requires up-to-date userland libraries and + firmware on your Raspberry Pi. Setting the digital gain will raise + a PiCameraMMALError if your userland libraries do not support setting + the digital gain. + .. versionadded:: 1.6 """) diff --git a/picamera/mmal.py b/picamera/mmal.py index 013d1341..0540275c 100644 --- a/picamera/mmal.py +++ b/picamera/mmal.py @@ -617,7 +617,17 @@ class MMAL_PARAMETER_LOGGING_T(ct.Structure): MMAL_PARAMETER_CAMERA_RX_TIMING, MMAL_PARAMETER_DPF_CONFIG, MMAL_PARAMETER_JPEG_RESTART_INTERVAL, -) = range(MMAL_PARAMETER_GROUP_CAMERA, MMAL_PARAMETER_GROUP_CAMERA + 81) + MMAL_PARAMETER_CAMERA_ISP_BLOCK_OVERRIDE, + MMAL_PARAMETER_LENS_SHADING_OVERRIDE, + MMAL_PARAMETER_BLACK_LEVEL, + MMAL_PARAMETER_RESIZE_PARAMS, + MMAL_PARAMETER_CROP, + MMAL_PARAMETER_OUTPUT_SHIFT, + MMAL_PARAMETER_CCM_SHIFT, + MMAL_PARAMETER_CUSTOM_CCM, + MMAL_PARAMETER_ANALOG_GAIN, + MMAL_PARAMETER_DIGITAL_GAIN, +) = range(MMAL_PARAMETER_GROUP_CAMERA, MMAL_PARAMETER_GROUP_CAMERA + 91) class MMAL_PARAMETER_THUMBNAIL_CONFIG_T(ct.Structure): _fields_ = [ @@ -1326,6 +1336,18 @@ class MMAL_PARAMETER_CAMERA_RX_TIMING_T(ct.Structure): ('cpi_timing2', ct.c_uint32), ] +class MMAL_PARAMETER_LENS_SHADING_T(ct.Structure): + _fields_ = [ + ('hdr', MMAL_PARAMETER_HEADER_T), + ('enabled', MMAL_BOOL_T), + ('grid_cell_size', ct.c_uint32), + ('grid_width', ct.c_uint32), + ('grid_stride', ct.c_uint32), + ('grid_height', ct.c_uint32), + ('mem_handle_table', ct.c_uint32), + ('ref_transform', ct.c_uint32), + ] + # mmal_parameters_video.h #################################################### ( diff --git a/picamera/mmalobj.py b/picamera/mmalobj.py index 223474e9..f87efad1 100644 --- a/picamera/mmalobj.py +++ b/picamera/mmalobj.py @@ -73,10 +73,12 @@ # MMALControlPort.params attribute. PARAM_TYPES = { mmal.MMAL_PARAMETER_ALGORITHM_CONTROL: mmal.MMAL_PARAMETER_ALGORITHM_CONTROL_T, + mmal.MMAL_PARAMETER_ANALOG_GAIN: mmal.MMAL_PARAMETER_RATIONAL_T, mmal.MMAL_PARAMETER_ANNOTATE: None, # adjusted by MMALCamera.annotate_rev mmal.MMAL_PARAMETER_ANTISHAKE: mmal.MMAL_PARAMETER_BOOLEAN_T, mmal.MMAL_PARAMETER_AUDIO_LATENCY_TARGET: mmal.MMAL_PARAMETER_AUDIO_LATENCY_TARGET_T, mmal.MMAL_PARAMETER_AWB_MODE: mmal.MMAL_PARAMETER_AWBMODE_T, + mmal.MMAL_PARAMETER_BLACK_LEVEL: mmal.MMAL_PARAMETER_UINT32_T, mmal.MMAL_PARAMETER_BRIGHTNESS: mmal.MMAL_PARAMETER_RATIONAL_T, mmal.MMAL_PARAMETER_BUFFER_FLAG_FILTER: mmal.MMAL_PARAMETER_UINT32_T, mmal.MMAL_PARAMETER_BUFFER_REQUIREMENTS: mmal.MMAL_PARAMETER_BUFFER_REQUIREMENTS_T, @@ -86,6 +88,7 @@ mmal.MMAL_PARAMETER_CAMERA_CUSTOM_SENSOR_CONFIG: mmal.MMAL_PARAMETER_UINT32_T, mmal.MMAL_PARAMETER_CAMERA_INFO: None, # adjusted by MMALCameraInfo.info_rev mmal.MMAL_PARAMETER_CAMERA_INTERFACE: mmal.MMAL_PARAMETER_CAMERA_INTERFACE_T, + mmal.MMAL_PARAMETER_CAMERA_ISP_BLOCK_OVERRIDE: mmal.MMAL_PARAMETER_UINT32_T, mmal.MMAL_PARAMETER_CAMERA_MIN_ISO: mmal.MMAL_PARAMETER_UINT32_T, mmal.MMAL_PARAMETER_CAMERA_NUM: mmal.MMAL_PARAMETER_INT32_T, mmal.MMAL_PARAMETER_CAMERA_RX_CONFIG: mmal.MMAL_PARAMETER_CAMERA_RX_CONFIG_T, @@ -97,6 +100,7 @@ mmal.MMAL_PARAMETER_CAPTURE_MODE: mmal.MMAL_PARAMETER_CAPTUREMODE_T, mmal.MMAL_PARAMETER_CAPTURE_STATS_PASS: mmal.MMAL_PARAMETER_BOOLEAN_T, mmal.MMAL_PARAMETER_CAPTURE_STATUS: mmal.MMAL_PARAMETER_CAPTURE_STATUS_T, + mmal.MMAL_PARAMETER_CCM_SHIFT: mmal.MMAL_PARAMETER_INT32_T, mmal.MMAL_PARAMETER_CHANGE_EVENT_REQUEST: mmal.MMAL_PARAMETER_CHANGE_EVENT_REQUEST_T, mmal.MMAL_PARAMETER_CLOCK_ACTIVE: mmal.MMAL_PARAMETER_BOOLEAN_T, mmal.MMAL_PARAMETER_CLOCK_DISCONT_THRESHOLD: mmal.MMAL_PARAMETER_CLOCK_DISCONT_THRESHOLD_T, @@ -110,7 +114,10 @@ mmal.MMAL_PARAMETER_COLOUR_EFFECT: mmal.MMAL_PARAMETER_COLOURFX_T, mmal.MMAL_PARAMETER_CONTRAST: mmal.MMAL_PARAMETER_RATIONAL_T, mmal.MMAL_PARAMETER_CORE_STATISTICS: mmal.MMAL_PARAMETER_CORE_STATISTICS_T, +# mmal.MMAL_PARAMETER_CROP: mmal.MMAL_PARAMETER_CROP_T, mmal.MMAL_PARAMETER_CUSTOM_AWB_GAINS: mmal.MMAL_PARAMETER_AWB_GAINS_T, +# mmal.MMAL_PARAMETER_CUSTOM_CCM: mmal.MMAL_PARAMETER_CUSTOM_CCM_T, + mmal.MMAL_PARAMETER_DIGITAL_GAIN: mmal.MMAL_PARAMETER_RATIONAL_T, mmal.MMAL_PARAMETER_DISPLAYREGION: mmal.MMAL_DISPLAYREGION_T, mmal.MMAL_PARAMETER_DPF_CONFIG: mmal.MMAL_PARAMETER_UINT32_T, mmal.MMAL_PARAMETER_DYNAMIC_RANGE_COMPRESSION: mmal.MMAL_PARAMETER_DRC_T, @@ -139,6 +146,7 @@ mmal.MMAL_PARAMETER_JPEG_ATTACH_LOG: mmal.MMAL_PARAMETER_BOOLEAN_T, mmal.MMAL_PARAMETER_JPEG_Q_FACTOR: mmal.MMAL_PARAMETER_UINT32_T, mmal.MMAL_PARAMETER_JPEG_RESTART_INTERVAL: mmal.MMAL_PARAMETER_UINT32_T, + mmal.MMAL_PARAMETER_LENS_SHADING_OVERRIDE: mmal.MMAL_PARAMETER_LENS_SHADING_T, mmal.MMAL_PARAMETER_LOCKSTEP_ENABLE: mmal.MMAL_PARAMETER_BOOLEAN_T, mmal.MMAL_PARAMETER_LOGGING: mmal.MMAL_PARAMETER_LOGGING_T, mmal.MMAL_PARAMETER_MB_ROWS_PER_SLICE: mmal.MMAL_PARAMETER_UINT32_T, @@ -147,11 +155,13 @@ mmal.MMAL_PARAMETER_MIRROR: mmal.MMAL_PARAMETER_UINT32_T, # actually mmal.MMAL_PARAMETER_MIRROR_T but this just contains a uint32 mmal.MMAL_PARAMETER_NALUNITFORMAT: mmal.MMAL_PARAMETER_VIDEO_NALUNITFORMAT_T, mmal.MMAL_PARAMETER_NO_IMAGE_PADDING: mmal.MMAL_PARAMETER_BOOLEAN_T, + mmal.MMAL_PARAMETER_OUTPUT_SHIFT: mmal.MMAL_PARAMETER_INT32_T, mmal.MMAL_PARAMETER_POWERMON_ENABLE: mmal.MMAL_PARAMETER_BOOLEAN_T, mmal.MMAL_PARAMETER_PRIVACY_INDICATOR: mmal.MMAL_PARAMETER_PRIVACY_INDICATOR_T, mmal.MMAL_PARAMETER_PROFILE: mmal.MMAL_PARAMETER_VIDEO_PROFILE_T, mmal.MMAL_PARAMETER_RATECONTROL: mmal.MMAL_PARAMETER_VIDEO_RATECONTROL_T, mmal.MMAL_PARAMETER_REDEYE: mmal.MMAL_PARAMETER_REDEYE_T, +# mmal.MMAL_PARAMETER_RESIZE_PARAMS: mmal.MMAL_PARAMETER_RESIZE_T, mmal.MMAL_PARAMETER_ROTATION: mmal.MMAL_PARAMETER_INT32_T, mmal.MMAL_PARAMETER_SATURATION: mmal.MMAL_PARAMETER_RATIONAL_T, mmal.MMAL_PARAMETER_SEEK: mmal.MMAL_PARAMETER_SEEK_T, @@ -1336,8 +1346,8 @@ def _set_framesize(self, value): video = self._port[0].format[0].es[0].video video.width = bcm_host.VCOS_ALIGN_UP(value.width, 32) video.height = bcm_host.VCOS_ALIGN_UP(value.height, 16) - video.crop.width = value.width - video.crop.height = value.height + video.crop.width = int(value.width) + video.crop.height = int(value.height) framesize = property(_get_framesize, _set_framesize, doc="""\ Retrieves or sets the size of the port's video frames as a (width, height) tuple. This attribute implicitly handles scaling the given diff --git a/picamera/user_vcsm.py b/picamera/user_vcsm.py new file mode 100644 index 00000000..b58f12be --- /dev/null +++ b/picamera/user_vcsm.py @@ -0,0 +1,123 @@ +# vim: set et sw=4 sts=4 fileencoding=utf-8: +# +# Python header conversion +# Copyright (c) 2013-2017 Dave Jones +# Please blame this particular file on +# Richard Bowman +# +# Original headers +# Copyright (c) 2012, Broadcom Europe Ltd +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +""" +Wraps the VideoCore Shared Memory library in Python. + +This Python module wraps the necessary functions from the Raspberry Pi +``userland`` module to allow shared memory use in ``picamera``. Currently +this is only used to load a custom lens shading table. Please see the +comments in [user_vcsm.h](https://github.com/raspberrypi/userland/ +blob/master/host_applications/linux/libs/sm/user-vcsm.h). +""" + +from __future__ import ( + unicode_literals, + print_function, + division, + absolute_import, + ) + +# Make Py2's str equivalent to Py3's +str = type('') + +import ctypes as ct +import warnings + +from .bcm_host import VCOS_UNSIGNED + +_lib = ct.CDLL('libvcsm.so') + +VCSM_STATUS_T = ct.c_uint32 # enum +( + VCSM_STATUS_VC_WALK_ALLOC, + VCSM_STATUS_HOST_WALK_MAP, + VCSM_STATUS_HOST_WALK_PID_MAP, + VCSM_STATUS_HOST_WALK_PID_ALLOC, + VCSM_STATUS_VC_MAP_ALL, + VCSM_STATUS_NONE, +) = range(6) + +VCSM_CACHE_TYPE_T = ct.c_uint32 # enum +( + VCSM_CACHE_TYPE_NONE, + VCSM_CACHE_TYPE_HOST, + VCSM_CACHE_TYPE_VC, + VCSM_CACHE_TYPE_HOST_AND_VC, +) = range(4) + +vcsm_init = _lib.vcsm_init +vcsm_init.argtypes = [] +vcsm_init.restype = ct.c_int + +vcsm_exit = _lib.vcsm_exit +vcsm_exit.argtypes = [] +vcsm_exit.restype = None + +vcsm_status = _lib.vcsm_status +vcsm_status.argtypes = [VCSM_STATUS_T, ct.c_int] +vcsm_status.restype = None + +vcsm_malloc = _lib.vcsm_malloc +vcsm_malloc.argtypes = [ct.c_uint, ct.c_char_p] +vcsm_malloc.restype = ct.c_uint + +vcsm_malloc_share = _lib.vcsm_malloc_share +vcsm_malloc_share.argtypes = [ct.c_uint] +vcsm_malloc_share.restype = ct.c_uint + +vcsm_free = _lib.vcsm_free +vcsm_free.argtypes = [ct.c_uint] +vcsm_free.restype = None + +vcsm_vc_hdl_from_ptr = _lib.vcsm_vc_hdl_from_ptr +vcsm_vc_hdl_from_ptr.argtypes = [ct.c_void_p] +vcsm_vc_hdl_from_ptr.restype = ct.c_uint + +vcsm_vc_hdl_from_hdl = _lib.vcsm_vc_hdl_from_hdl +vcsm_vc_hdl_from_hdl.argtypes = [ct.c_uint] +vcsm_vc_hdl_from_hdl.restype = ct.c_uint + +vcsm_lock = _lib.vcsm_lock +vcsm_lock.argtypes = [ct.c_uint] +vcsm_lock.restype = ct.c_void_p + +vcsm_unlock_ptr = _lib.vcsm_unlock_ptr +vcsm_unlock_ptr.argtypes = [ct.c_void_p] +vcsm_unlock_ptr.restype = ct.c_int + +vcsm_unlock_hdl = _lib.vcsm_unlock_hdl +vcsm_unlock_hdl.argtypes = [ct.c_uint] +vcsm_unlock_hdl.restype = ct.c_int diff --git a/picamera/vcsmobj.py b/picamera/vcsmobj.py new file mode 100644 index 00000000..d1eee160 --- /dev/null +++ b/picamera/vcsmobj.py @@ -0,0 +1,234 @@ +# vim: set et sw=4 sts=4 fileencoding=utf-8: +# +# Python header conversion +# Copyright (c) 2013-2017 Dave Jones +# Please blame this particular file on +# Richard Bowman +# +# Original headers +# Copyright (c) 2012, Broadcom Europe Ltd +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +""" +An object-oriented wrapper for the VideoCore Shared Memory library. + +This module is intended as a friendly Python wrapper for the VideoCore shared +memory API exposed by [user_vcsm.h](https://github.com/raspberrypi/userland/ +blob/master/host_applications/linux/libs/sm/user-vcsm.h) in the Raspberry Pi +userland code. +""" + + +from __future__ import ( + unicode_literals, + print_function, + division, + absolute_import, + ) + +# Make Py2's str equivalent to Py3's +str = type('') + +from . import user_vcsm as vcsm +import warnings +import contextlib +import ctypes + +# All the next bit of code does is attempt to open/close the library. +# I am sure there ought to be a nice way to hook into the module unloading +# or the interpreter closing, but I have not yet found it. + +class VideoCoreSharedMemoryServiceManager(): + """Class to manage initialising/closing the VCSM service. + + .. versionadded:: 1.14 + """ + def __init__(self): + ret = vcsm.vcsm_init() + if ret == 0: + self._initialised = True + else: + raise Error("Error initialising VideoCore Shared Memory " + "interface. Code {}".format(ret)) + + def exit(self): + """Shut down the VCSM service. Should be called only once.""" + assert self._initialised, "The VCSM service is not running." + vcsm.vcsm_exit() + self._initialised = False + + def __del__(self): + #TODO: find a reliable way to call this before Python shuts down! + #print("Closing VideoCore Shared Memory service on garbage collection.") + if self._initialised: + self.exit() + +_vcsm_manager = None +def ensure_vcsm_init(): + """Initialise the shared memory interface if required. + + The VideoCore shared memory service must be initialised in order to + use any of the other functions. When this module is unloaded or + Python closes, it should release the library. + + .. versionadded:: 1.14 + """ + #TODO: find a better way to cleanly close the library. + global _vcsm_manager + if _vcsm_manager is None: + _vcsm_manager = VideoCoreSharedMemoryServiceManager() + +def vcsm_exit(): + """Close the VideoCore shared memory service down. + + It is not clear whether multiple init/close cycles are allowed in + one run of Python. This method should only be called once. It is + also probably not required - the library should be shut down cleanly + when the garbage collector cleans up after the module. + + You probably do not want to call this function manually, but it is + here for completeness. + + .. versionadded:: 1.14 + """ + if _vcsm_manager is not None: + _vcsm_manager.exit() + _vcsm_manager = None + else: + warnings.warn("The VCSM service can't be closed - it's not open.") + +class VideoCoreSharedMemory(): + """This class manages a chunk of VideoCore shared memory.""" + def __init__(self, size, name): + """Create a chunk of shared memory. + + Arguments: + size: unsigned integer + The size of the block of memory, in bytes + name: string + A name for the block of shared memory. + + On creation, this object will create some VC shared memory by + calling vcsm_malloc. It will call vcsm_free to free the memory + when the object is deleted. + + .. versionadded:: 1.14 + """ + ensure_vcsm_init() + self._handle = vcsm.vcsm_malloc(size, name.encode()) + self._size = size + if self._handle == 0: + raise Error("Could not allocate VC shared memory block " + "'{}' with size {} bytes".format(name, size)) + + def __del__(self): + vcsm.vcsm_free(self._handle) + + def _get_handle(self): + return self._handle + handle = property(_get_handle, doc="""\ + The handle of the underlying VideoCore shared memory + + The handle identifies the block of shared memory, and is used by + the various functions wrapped in ``user_vcsm.py``. + + .. versionadded:: 1.14 + """) + + def _get_videocore_handle(self): + return vcsm.vcsm_vc_hdl_from_hdl(self._handle) + + videocore_handle = property(_get_videocore_handle, doc="""\ + A handle to access the shared memory from the GPU + + The handle identifies the block of shared memory to the GPU. It + cannot, for safety reasons, be used to read or write memory from + the CPU, so it is only useful when passing data to the GPU. + + .. versionadded:: 1.14 + """) + + @contextlib.contextmanager + def lock(self): + """Lock the shared memory and return a pointer to it. + + Usage: + ``` + sm = VideoCoreSharedMemory(128, "test") + with sm.lock() as pointer: + #copy stuff into the block + ``` + + .. versionadded:: 1.14 + """ + pointer = vcsm.vcsm_lock(self._handle) + try: + yield pointer + finally: + vcsm.vcsm_unlock_hdl(self._handle) + + def copy_from_buffer(self, source, size=None, warn_if_small=True): + """Copy data from a buffer to shared memory. + + Arguments: + buffer: ctypes.c_void_p + A pointer to the location of the memory you want to copy in. + size: integer (optional) + If specified, copy this much memory. It will not copy more + than the size of the shared memory, and will raise an exception + if you try to do so. + warn_if_small: boolean (optional) + By default, a warning will be raised if you copy in a buffer + that is smaller than the allocated memory. Set this to False + to suppress the warning. + + .. versionadded:: 1.14 + """ + if size is None: + size = self._size + if size > self._size: + raise ValueError("Attempted to copy in more bytes than the buffer holds.") + if size < self._size: + warnings.warn("The allocated memory won't be filled by the array passed in.") + with self.lock() as destination: + ctypes.memmove(destination, source, size) + + def copy_from_array(self, source): + """Copy the contents of a numpy array into the buffer. + + Arguments: + source: numpy.ndarray + The data to copy into the buffer. Must be np.uint8 datatype. + + NB the array must be contiguous. This will be checked but, in order to avoid + a hard dependency on numpy, it will not be made contiguous automatically. + + .. versionadded:: 1.14 + """ + if not source.flags['C_CONTIGUOUS']: + raise ValueError("Only contiguous arrays can be copied to shared memory.") + self.copy_from_buffer(source.ctypes.data_as(ctypes.c_void_p), source.size) diff --git a/setup.py b/setup.py index d3d4bf25..3156a927 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ pass __project__ = 'picamera' -__version__ = '1.13' +__version__ = '1.13.1b0' __author__ = 'Dave Jones' __author_email__ = 'dave@waveform.org.uk' __url__ = 'http://picamera.readthedocs.io/'