forked from platypush/platypush
Added web plugin for mlx90640 infrared camera
This commit is contained in:
parent
c6384b3b65
commit
d7dc74beed
6 changed files with 176 additions and 75 deletions
|
@ -0,0 +1 @@
|
||||||
|
camera
|
|
@ -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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
<main>
|
<main>
|
||||||
<div class="plugins-container">
|
<div class="plugins-container">
|
||||||
<plugin v-for="(conf, plugin) in {{ utils.to_json(templates) }}"
|
<plugin v-for="(conf, plugin) in {{ utils.to_json(templates) }}"
|
||||||
:tag="plugin.replace('.', '-')"
|
:tag="plugin.replace(/\./g, '-')"
|
||||||
:key="plugin"
|
:key="plugin"
|
||||||
:config="conf"
|
:config="conf"
|
||||||
:class="{hidden: plugin != selectedPlugin}">
|
:class="{hidden: plugin != selectedPlugin}">
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{%
|
{%
|
||||||
with pluginIcons = {
|
with pluginIcons = {
|
||||||
'camera': 'fas fa-camera',
|
'camera': 'fas fa-camera',
|
||||||
|
'camera.ir.mlx90640': 'fas fa-sun',
|
||||||
'light.hue': 'fa fa-lightbulb',
|
'light.hue': 'fa fa-lightbulb',
|
||||||
'media.mplayer': 'fa fa-film',
|
'media.mplayer': 'fa fa-film',
|
||||||
'media.mpv': 'fa fa-film',
|
'media.mpv': 'fa fa-film',
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script type="text/x-template" id="tmpl-camera-ir-mlx90640">
|
||||||
|
<div class="camera">
|
||||||
|
<div class="camera-container">
|
||||||
|
<div class="no-frame" v-if="!capturing">The camera is not active</div>
|
||||||
|
<img class="frame" ref="frame">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button type="button" @click="startStreaming" v-if="!capturing">
|
||||||
|
<i class="fa fa-play"></i> Start streaming
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" @click="stopStreaming" v-else>
|
||||||
|
<i class="fa fa-stop"></i> Stop streaming
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<select ref="rotate" @change="rotate = $event.target.value" :disabled="capturing">
|
||||||
|
<option value="0" :selected="rotate == 0">0 degrees</option>
|
||||||
|
<option value="90" :selected="rotate == 90">90 degrees</option>
|
||||||
|
<option value="180" :selected="rotate == 180">180 degrees</option>
|
||||||
|
<option value="270" :selected="rotate == 270">270 degrees</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input type="checkbox" :checked="grayscale" :disabled="capturing"
|
||||||
|
@change="grayscale = $event.target.checked">
|
||||||
|
Grayscale
|
||||||
|
</input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import base64
|
import base64
|
||||||
|
import io
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
@ -34,13 +35,13 @@ class CameraIrMlx90640Plugin(Plugin):
|
||||||
* **PIL** image library (``pip install Pillow``)
|
* **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 fps: Frames per seconds (default: 16)
|
||||||
:param skip_frames: Number of frames to be skipped on sensor initialization/warmup (default: 2)
|
: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 rotate: Rotation angle in degrees (default: 0)
|
||||||
:param rawrgb_path: Specify it if the rawrgb executable compiled from
|
:param rawrgb_path: Specify it if the rawrgb executable compiled from
|
||||||
https://github.com/pimoroni/mlx90640-library is in another folder than
|
https://github.com/pimoroni/mlx90640-library is in another folder than
|
||||||
|
@ -61,77 +62,106 @@ class CameraIrMlx90640Plugin(Plugin):
|
||||||
self.skip_frames = skip_frames
|
self.skip_frames = skip_frames
|
||||||
self.scale_factor = scale_factor
|
self.scale_factor = scale_factor
|
||||||
self.rawrgb_path = rawrgb_path
|
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
|
@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
|
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)
|
: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)
|
: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 if fps is None else fps
|
||||||
fps = self.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:
|
size = self._img_size
|
||||||
skip_frames = self.skip_frames
|
|
||||||
|
|
||||||
input_size = self._img_size[0] * self._img_size[1] * 3
|
|
||||||
sleep_time = 1.0 / self.fps
|
sleep_time = 1.0 / self.fps
|
||||||
captured_frames = []
|
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,
|
while (frames is not None and n_captured_frames < frames) or (frames is None and self._is_capture_proc_running()):
|
||||||
stderr=subprocess.PIPE) as camera:
|
frame = camera.stdout.read(size[0] * size[1] * 3)
|
||||||
while len(captured_frames) < frames:
|
|
||||||
frame = camera.stdout.read(input_size)
|
|
||||||
|
|
||||||
if skip_frames > 0:
|
if skip_frames > 0:
|
||||||
time.sleep(sleep_time)
|
time.sleep(sleep_time)
|
||||||
skip_frames -= 1
|
skip_frames -= 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
frame = base64.encodebytes(frame).decode()
|
|
||||||
captured_frames.append(frame)
|
|
||||||
time.sleep(sleep_time)
|
|
||||||
|
|
||||||
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)
|
image = Image.frombytes('RGB', size, frame)
|
||||||
new_image = Image.new('L', image.size)
|
|
||||||
|
|
||||||
if grayscale:
|
if grayscale:
|
||||||
|
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)
|
||||||
|
|
||||||
|
frame = image.getdata()
|
||||||
|
|
||||||
|
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 i in range(0, image.size[0]):
|
||||||
for j in range(0, image.size[1]):
|
for j in range(0, image.size[1]):
|
||||||
r, g, b = image.getpixel((i, j))
|
r, g, b = image.getpixel((i, j))
|
||||||
|
@ -144,20 +174,20 @@ class CameraIrMlx90640Plugin(Plugin):
|
||||||
|
|
||||||
new_image.putpixel((i, j), value)
|
new_image.putpixel((i, j), value)
|
||||||
|
|
||||||
image = new_image
|
return new_image
|
||||||
|
|
||||||
if rotate:
|
@action
|
||||||
image = image.rotate(rotate)
|
def stop(self):
|
||||||
|
"""
|
||||||
|
Stop an ongoing capture session
|
||||||
|
"""
|
||||||
|
if not self._is_capture_proc_running():
|
||||||
|
return
|
||||||
|
|
||||||
if scale_factor != 1:
|
self._capture_proc.terminate()
|
||||||
size = tuple(i*scale_factor for i in size)
|
self._capture_proc.kill()
|
||||||
image = image.resize(size, Image.ANTIALIAS)
|
self._capture_proc.wait()
|
||||||
|
self._capture_proc = None
|
||||||
filename = os.path.abspath(os.path.expanduser(output_image.format(i)))
|
|
||||||
image.save(filename)
|
|
||||||
files.append(filename)
|
|
||||||
|
|
||||||
return files
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
Loading…
Reference in a new issue