From d7dc74beedbbc8d93ad7ecf14c6a54b00a8773a3 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Thu, 26 Sep 2019 11:15:18 +0200 Subject: [PATCH] Added web plugin for mlx90640 infrared camera --- .../webpanel/plugins/camera.ir.mlx90640 | 1 + .../js/plugins/camera.ir.mlx90640/index.js | 38 ++++ platypush/backend/http/templates/index.html | 2 +- platypush/backend/http/templates/nav.html | 1 + .../plugins/camera.ir.mlx90640/index.html | 31 +++ .../plugins/camera/ir/mlx90640/__init__.py | 178 ++++++++++-------- 6 files changed, 176 insertions(+), 75 deletions(-) create mode 120000 platypush/backend/http/static/css/source/webpanel/plugins/camera.ir.mlx90640 create mode 100644 platypush/backend/http/static/js/plugins/camera.ir.mlx90640/index.js create mode 100644 platypush/backend/http/templates/plugins/camera.ir.mlx90640/index.html diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/camera.ir.mlx90640 b/platypush/backend/http/static/css/source/webpanel/plugins/camera.ir.mlx90640 new file mode 120000 index 000000000..5f839334f --- /dev/null +++ b/platypush/backend/http/static/css/source/webpanel/plugins/camera.ir.mlx90640 @@ -0,0 +1 @@ +camera \ No newline at end of file diff --git a/platypush/backend/http/static/js/plugins/camera.ir.mlx90640/index.js b/platypush/backend/http/static/js/plugins/camera.ir.mlx90640/index.js new file mode 100644 index 000000000..d081c70f7 --- /dev/null +++ b/platypush/backend/http/static/js/plugins/camera.ir.mlx90640/index.js @@ -0,0 +1,38 @@ +Vue.component('camera-ir-mlx90640', { + template: '#tmpl-camera-ir-mlx90640', + props: ['config'], + + data: function() { + return { + bus: new Vue({}), + capturing: false, + rotate: this.config.rotate || 0, + grayscale: false, + }; + }, + + methods: { + startStreaming: async function() { + if (this.capturing) + return; + + this.capturing = true; + + while (this.capturing) { + const img = await request('camera.ir.mlx90640.capture', { + format: 'png', + rotate: this.rotate, + grayscale: this.grayscale, + }); + + this.$refs.frame.setAttribute('src', 'data:image/png;base64,' + img); + } + }, + + stopStreaming: async function() { + await request('camera.ir.mlx90640.stop'); + this.capturing = false; + }, + }, +}); + diff --git a/platypush/backend/http/templates/index.html b/platypush/backend/http/templates/index.html index 95a5ea429..cd13a4c03 100644 --- a/platypush/backend/http/templates/index.html +++ b/platypush/backend/http/templates/index.html @@ -66,7 +66,7 @@
diff --git a/platypush/backend/http/templates/nav.html b/platypush/backend/http/templates/nav.html index 2a4fc26f9..981b560b8 100644 --- a/platypush/backend/http/templates/nav.html +++ b/platypush/backend/http/templates/nav.html @@ -1,6 +1,7 @@ {% with pluginIcons = { 'camera': 'fas fa-camera', + 'camera.ir.mlx90640': 'fas fa-sun', 'light.hue': 'fa fa-lightbulb', 'media.mplayer': 'fa fa-film', 'media.mpv': 'fa fa-film', diff --git a/platypush/backend/http/templates/plugins/camera.ir.mlx90640/index.html b/platypush/backend/http/templates/plugins/camera.ir.mlx90640/index.html new file mode 100644 index 000000000..6b7df9226 --- /dev/null +++ b/platypush/backend/http/templates/plugins/camera.ir.mlx90640/index.html @@ -0,0 +1,31 @@ + + diff --git a/platypush/plugins/camera/ir/mlx90640/__init__.py b/platypush/plugins/camera/ir/mlx90640/__init__.py index fcbc84f93..2b4563a68 100644 --- a/platypush/plugins/camera/ir/mlx90640/__init__.py +++ b/platypush/plugins/camera/ir/mlx90640/__init__.py @@ -1,4 +1,5 @@ import base64 +import io import math import os import subprocess @@ -34,13 +35,13 @@ class CameraIrMlx90640Plugin(Plugin): * **PIL** image library (``pip install Pillow``) """ - _img_size = (24, 32) + _img_size = (32, 24) - def __init__(self, fps=16, skip_frames=2, scale_factor=10, rotate=0, rawrgb_path=None, **kwargs): + def __init__(self, fps=16, skip_frames=2, scale_factor=1, rotate=0, rawrgb_path=None, **kwargs): """ :param fps: Frames per seconds (default: 16) :param skip_frames: Number of frames to be skipped on sensor initialization/warmup (default: 2) - :param scale_factor: The camera outputs 24x32 pixels artifacts. Use scale_factor to scale them up to a larger image (default: 10) + :param scale_factor: The camera outputs 24x32 pixels artifacts. Use scale_factor to scale them up to a larger image (default: 1) :param rotate: Rotation angle in degrees (default: 0) :param rawrgb_path: Specify it if the rawrgb executable compiled from https://github.com/pimoroni/mlx90640-library is in another folder than @@ -61,103 +62,132 @@ class CameraIrMlx90640Plugin(Plugin): self.skip_frames = skip_frames self.scale_factor = scale_factor self.rawrgb_path = rawrgb_path + self._capture_proc = None + + def _is_capture_proc_running(self): + return self._capture_proc != None and self._capture_proc.poll() == None + + def _get_capture_proc(self, fps): + if not self._is_capture_proc_running(): + self._capture_proc = subprocess.Popen([self.rawrgb_path, '{}'.format(self.fps)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + return self._capture_proc @action - def capture(self, frames=1, fps=None, skip_frames=None): + def capture(self, output_file=None, frames=1, grayscale=False, fps=None, skip_frames=None, scale_factor=None, rotate=None, format=None): """ Capture one or multiple frames and return them as raw RGB - :param frames: Number of frames to be captured (default: 1) + :param output_file: Can be either the path to a single image file or a format string (e.g. 'snapshots/image-{:04d}') in case of multiple frames. + If not set the function will return a list of base64 encoded representations of the raw RGB frames, otherwise the list of captured files. + :type output_file: str + + :param frames: Number of frames to be captured (default: 1). If None the capture process will proceed until `stop` is called. + :type frames: int + + :param grayscale: Save the image as grayscale - black pixels will be colder, white pixels warmer (default: False) + :type grayscale: bool + :param fps: If set it overrides the fps parameter specified on the object (default: None) + :type fps: int + :param skip_frames: If set it overrides the skip_frames parameter specified on the object (default: None) - :returns: list[str]. Each item is a base64 encoded raw RGB representation of a frame + :type skip_frames: int + + :param scale_factor: If set it overrides the scale_factor parameter specified on the object (default: None) + :type scale_factor: float + + :param rotate: If set it overrides the rotate parameter specified on the object (default: None) + :type rotate: int + + :param format: Output image format if output_file is not specified(default: None, raw RGB). + It can be jpg, png, gif or any format supported by PIL + :type format: str + + :returns: list[str]. Each item is a base64 encoded raw RGB representation of a frame if output_file is not set, otherwise a list with + the captured image files will be returned. """ - if fps is None: - fps = self.fps + fps = self.fps if fps is None else fps + skip_frames = self.skip_frames if skip_frames is None else skip_frames + scale_factor = self.scale_factor if scale_factor is None else scale_factor + rotate = self.rotate if rotate is None else rotate - if skip_frames is None: - skip_frames = self.skip_frames - - input_size = self._img_size[0] * self._img_size[1] * 3 + size = self._img_size sleep_time = 1.0 / self.fps captured_frames = [] + n_captured_frames = 0 + files = set() + camera = self._get_capture_proc(fps) - with subprocess.Popen([self.rawrgb_path, '{}'.format(self.fps)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as camera: - while len(captured_frames) < frames: - frame = camera.stdout.read(input_size) + while (frames is not None and n_captured_frames < frames) or (frames is None and self._is_capture_proc_running()): + frame = camera.stdout.read(size[0] * size[1] * 3) - if skip_frames > 0: - time.sleep(sleep_time) - skip_frames -= 1 - continue - - frame = base64.encodebytes(frame).decode() - captured_frames.append(frame) + if skip_frames > 0: time.sleep(sleep_time) + skip_frames -= 1 + continue - camera.terminate() - - return captured_frames - - @action - def capture_to_file(self, output_image, frames=1, grayscale=False, fps=None, skip_frames=None, scale_factor=None, rotate=None): - """ - Capture one or multiple frames to one or multiple image files. - - :param output_image: Can be either the path to a single image file or a format string (e.g. 'snapshots/image-{:04d}') in case of multiple frames - :param fps: If set it overrides the fps parameter specified on the object (default: None) - :param frames: Number of frames to be captured (default: 1) - :param grayscale: Save the image as grayscale - black pixels will be colder, white pixels warmer (default: False) - :param skip_frames: If set it overrides the skip_frames parameter specified on the object (default: None) - :param scale_factor: If set it overrides the scale_factor parameter specified on the object (default: None) - :param rotate: If set it overrides the rotate parameter specified on the object (default: None) - :returns: list[str] containing the saved image file names - """ - - if scale_factor is None: - scale_factor = self.scale_factor - - if rotate is None: - rotate = self.rotate - - files = [] - - for i in range(0, frames): - encoded_frame = self.capture(frames=1, fps=fps, skip_frames=skip_frames).output[0] - frame = base64.decodebytes(encoded_frame.encode()) - size = (self._img_size[1], self._img_size[0]) image = Image.frombytes('RGB', size, frame) - new_image = Image.new('L', image.size) if grayscale: - for i in range(0, image.size[0]): - for j in range(0, image.size[1]): - r, g, b = image.getpixel((i, j)) - value = int(2.0*r - 0.5*g - 1.5*b) - - if value > 255: - value = 255 - if value < 0: - value = 0 - - new_image.putpixel((i, j), value) - - image = new_image - + image = self._convert_to_grayscale(image) if rotate: image = image.rotate(rotate) - if scale_factor != 1: size = tuple(i*scale_factor for i in size) image = image.resize(size, Image.ANTIALIAS) - filename = os.path.abspath(os.path.expanduser(output_image.format(i))) - image.save(filename) - files.append(filename) + frame = image.getdata() - return files + if not output_file: + if format: + temp = io.BytesIO() + image.save(temp, format=format) + frame = temp.getvalue() + + frame = base64.encodebytes(frame).decode() + captured_frames.append(frame) + else: + image_file = os.path.abspath(os.path.expanduser(output_file.format(n_captured_frames))) + image.save(image_file) + files.add(image_file) + + n_captured_frames += 1 + time.sleep(sleep_time) + + self.stop() + return sorted([f for f in files]) if output_file else captured_frames + + def _convert_to_grayscale(self, image): + new_image = Image.new('L', image.size) + + for i in range(0, image.size[0]): + for j in range(0, image.size[1]): + r, g, b = image.getpixel((i, j)) + value = int(2.0*r - 0.5*g - 1.5*b) + + if value > 255: + value = 255 + if value < 0: + value = 0 + + new_image.putpixel((i, j), value) + + return new_image + + @action + def stop(self): + """ + Stop an ongoing capture session + """ + if not self._is_capture_proc_running(): + return + + self._capture_proc.terminate() + self._capture_proc.kill() + self._capture_proc.wait() + self._capture_proc = None # vim:sw=4:ts=4:et: