aquastat/picamera/display.py
2023-02-01 21:37:42 +00:00

321 lines
12 KiB
Python

# 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 <dave@waveform.org.uk>
#
# 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)