# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Python camera library for the Rasperry-Pi camera module # Copyright (c) 2013-2017 Dave Jones # # 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. from __future__ import ( unicode_literals, print_function, division, absolute_import, ) # Make Py2's str equivalent to Py3's str = type('') import mimetypes import ctypes as ct from functools import reduce from operator import or_ from . import bcm_host, mmalobj as mo, mmal from .encoders import PiCookedOneImageEncoder, PiRawOneImageEncoder from .exc import PiCameraRuntimeError, PiCameraValueError class PiDisplay(object): __slots__ = ( '_display', '_info', '_transform', '_exif_tags', ) _ROTATIONS = { bcm_host.DISPMANX_NO_ROTATE: 0, bcm_host.DISPMANX_ROTATE_90: 90, bcm_host.DISPMANX_ROTATE_180: 180, bcm_host.DISPMANX_ROTATE_270: 270, } _ROTATIONS_R = {v: k for k, v in _ROTATIONS.items()} _ROTATIONS_MASK = reduce(or_, _ROTATIONS.keys(), 0) RAW_FORMATS = { 'yuv', 'rgb', 'rgba', 'bgr', 'bgra', } def __init__(self, display_num=0): bcm_host.bcm_host_init() self._exif_tags = {} self._display = bcm_host.vc_dispmanx_display_open(display_num) self._transform = bcm_host.DISPMANX_NO_ROTATE if not self._display: raise PiCameraRuntimeError('unable to open display %d' % display_num) self._info = bcm_host.DISPMANX_MODEINFO_T() if bcm_host.vc_dispmanx_display_get_info(self._display, self._info): raise PiCameraRuntimeError('unable to get display info') def close(self): bcm_host.vc_dispmanx_display_close(self._display) self._display = None @property def closed(self): return self._display is None def __enter__(self): return self def __exit__(self, exc_type, exc_value, exc_tb): self.close() def _get_output_format(self, output): """ Given an output object, attempt to determine the requested format. We attempt to determine the filename of the *output* object and derive a MIME type from the extension. If *output* has no filename, an error is raised. """ if isinstance(output, bytes): filename = output.decode('utf-8') elif isinstance(output, str): filename = output else: try: filename = output.name except AttributeError: raise PiCameraValueError( 'Format must be specified when output has no filename') (type, encoding) = mimetypes.guess_type(filename, strict=False) if not type: raise PiCameraValueError( 'Unable to determine type from filename %s' % filename) return type def _get_image_format(self, output, format=None): """ Given an output object and an optional format, attempt to determine the requested image format. This method is used by all capture methods to determine the requested output format. If *format* is specified as a MIME-type the "image/" prefix is stripped. If *format* is not specified, then :meth:`_get_output_format` will be called to attempt to determine format from the *output* object. """ if isinstance(format, bytes): format = format.decode('utf-8') format = format or self._get_output_format(output) format = ( format[6:] if format.startswith('image/') else format) if format == 'x-ms-bmp': format = 'bmp' return format def _get_image_encoder(self, output_port, format, resize, **options): """ Construct an image encoder for the requested parameters. This method is called by :meth:`capture`. The *output_port* parameter gives the MMAL port that the encoder should read output from. The *format* parameter indicates the image format and will be one of: * ``'jpeg'`` * ``'png'`` * ``'gif'`` * ``'bmp'`` * ``'yuv'`` * ``'rgb'`` * ``'rgba'`` * ``'bgr'`` * ``'bgra'`` The *resize* parameter indicates the size that the encoder should resize the output to (presumably by including a resizer in the pipeline). Finally, *options* includes extra keyword arguments that should be passed verbatim to the encoder. """ encoder_class = ( PiRawOneImageEncoder if format in self.RAW_FORMATS else PiCookedOneImageEncoder) return encoder_class( self, None, output_port, format, resize, **options) def capture(self, output, format=None, resize=None, **options): format = self._get_image_format(output, format) if format == 'yuv': raise PiCameraValueError('YUV format is unsupported at this time') res = self.resolution if (self._info.transform & bcm_host.DISPMANX_ROTATE_90) or ( self._info.transform & bcm_host.DISPMANX_ROTATE_270): res = res.transpose() transform = self._transform if (transform & bcm_host.DISPMANX_ROTATE_90) or ( transform & bcm_host.DISPMANX_ROTATE_270): res = res.transpose() source = mo.MMALPythonSource() source.outputs[0].format = mmal.MMAL_ENCODING_RGB24 if format == 'bgr': source.outputs[0].format = mmal.MMAL_ENCODING_BGR24 transform |= bcm_host.DISPMANX_SNAPSHOT_SWAP_RED_BLUE source.outputs[0].framesize = res source.outputs[0].commit() encoder = self._get_image_encoder( source.outputs[0], format, resize, **options) try: encoder.start(output) try: pitch = res.pad(width=16).width * 3 image_ptr = ct.c_uint32() resource = bcm_host.vc_dispmanx_resource_create( bcm_host.VC_IMAGE_RGB888, res.width, res.height, image_ptr) if not resource: raise PiCameraRuntimeError( 'unable to allocate resource for capture') try: buf = source.outputs[0].get_buffer() if bcm_host.vc_dispmanx_snapshot(self._display, resource, transform): raise PiCameraRuntimeError('failed to capture snapshot') rect = bcm_host.VC_RECT_T(0, 0, res.width, res.height) if bcm_host.vc_dispmanx_resource_read_data(resource, rect, buf._buf[0].data, pitch): raise PiCameraRuntimeError('failed to read snapshot') buf._buf[0].length = pitch * res.height buf._buf[0].flags = ( mmal.MMAL_BUFFER_HEADER_FLAG_EOS | mmal.MMAL_BUFFER_HEADER_FLAG_FRAME_END ) finally: bcm_host.vc_dispmanx_resource_delete(resource) source.outputs[0].send_buffer(buf) # XXX Anything more intelligent than a 10 second default? encoder.wait(10) finally: encoder.stop() finally: encoder.close() def _calculate_transform(self): """ Calculates a reverse transform to undo any that the boot configuration applies (presumably the user has altered the boot configuration to match their screen orientation so they want any capture to appear correctly oriented by default). This is then modified by the transforms specified in the :attr:`rotation`, :attr:`hflip` and :attr:`vflip` attributes. """ r = PiDisplay._ROTATIONS[self._info.transform & PiDisplay._ROTATIONS_MASK] r = (360 - r) % 360 # undo the native rotation r = (r + self.rotation) % 360 # add selected rotation result = PiDisplay._ROTATIONS_R[r] result |= self._info.transform & ( # undo flips by re-doing them bcm_host.DISPMANX_FLIP_HRIZ | bcm_host.DISPMANX_FLIP_VERT ) return result @property def resolution(self): """ Retrieves the resolution of the display device. """ return mo.PiResolution(width=self._info.width, height=self._info.height) def _get_hflip(self): return bool(self._info.transform & bcm_host.DISPMANX_FLIP_HRIZ) def _set_hflip(self, value): if value: self._info.transform |= bcm_host.DISPMANX_FLIP_HRIZ else: self._info.transform &= ~bcm_host.DISPMANX_FLIP_HRIZ hflip = property(_get_hflip, _set_hflip, doc="""\ Retrieves or sets whether snapshots are horizontally flipped. When queried, the :attr:`vflip` property returns a boolean indicating whether or not the output of :meth:`capture` is horizontally flipped. The default is ``False``. .. note:: This property only affects snapshots; it does not affect the display output itself. """) def _get_vflip(self): return bool(self._info.transform & bcm_host.DISPMANX_FLIP_VERT) def _set_vflip(self, value): if value: self._info.transform |= bcm_host.DISPMANX_FLIP_VERT else: self._info.transform &= ~bcm_host.DISPMANX_FLIP_VERT vflip = property(_get_vflip, _set_vflip, doc="""\ Retrieves or sets whether snapshots are vertically flipped. When queried, the :attr:`vflip` property returns a boolean indicating whether or not the output of :meth:`capture` is vertically flipped. The default is ``False``. .. note:: This property only affects snapshots; it does not affect the display output itself. """) def _get_rotation(self): return PiDisplay._ROTATIONS[self._transform & PiDisplay._ROTATIONS_MASK] def _set_rotation(self, value): try: self._transform = ( self._transform & ~PiDisplay._ROTATIONS_MASK) | PiDisplay._ROTATIONS_R[value] except KeyError: raise PiCameraValueError('invalid rotation %d' % value) rotation = property(_get_rotation, _set_rotation, doc="""\ Retrieves or sets the rotation of snapshots. When queried, the :attr:`rotation` property returns the rotation applied to the result of :meth:`capture`. Valid values are 0, 90, 180, and 270. When set, the property changes the rotation applied to the result of :meth:`capture`. The default is 0. .. note:: This property only affects snapshots; it does not affect the display itself. To rotate the display itself, modify the ``display_rotate`` value in :file:`/boot/config.txt`. """) def _get_exif_tags(self): return self._exif_tags def _set_exif_tags(self, value): self._exif_tags = {k: v for k, v in value.items()} exif_tags = property(_get_exif_tags, _set_exif_tags)