mirror of
https://github.com/node-red/node-red-nodes.git
synced 2023-10-10 13:36:58 +02:00
Merge pull request #712 from node-red/annotate-image
Add annotate-image node
This commit is contained in:
commit
e198c5d16f
13
utility/annotate-image/LICENSE
Normal file
13
utility/annotate-image/LICENSE
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Copyright 2020 OpenJS Foundation and other contributors, https://openjsf.org/
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
72
utility/annotate-image/README.md
Normal file
72
utility/annotate-image/README.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
node-red-node-annotate-image
|
||||||
|
==================
|
||||||
|
|
||||||
|
A <a href="http://nodered.org" target="_new">Node-RED</a> node that can annotate
|
||||||
|
a JPEG image.
|
||||||
|
|
||||||
|
The node is currently limited to drawing rectangles and circles over the image.
|
||||||
|
That can be used, for example, to annotate an image with bounding boxes of features
|
||||||
|
detected in the image by a TensorFlow node.
|
||||||
|
|
||||||
|
Install
|
||||||
|
-------
|
||||||
|
|
||||||
|
Run the following command in your Node-RED user directory - typically `~/.node-red`
|
||||||
|
|
||||||
|
npm install node-red-node-annotate-image
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
The JPEG image should be passed to the node as a Buffer object under `msg.payload`.
|
||||||
|
|
||||||
|
The annotations are provided in <code>msg.annotations</code> and are applied in order.
|
||||||
|
|
||||||
|
Each annotation is an object with the following properties:
|
||||||
|
|
||||||
|
- `type` (*string*) : the type of the annotation - `rect` or `circle`
|
||||||
|
- `x`/`y` (*number*) : the top-left corner of a `rect` annotation, or the center of a `circle` annotation.
|
||||||
|
- `w`/`h` (*number*) : the width and height of a `rect` annotation
|
||||||
|
- `r` (*number*) : the radius of a `circle` annotation
|
||||||
|
- `bbox` (*array*) : this can be used instead of `x`, `y`, `w`, `h` and `r`.
|
||||||
|
It should be an array of four values giving the bounding box of the annotation:
|
||||||
|
`[x, y, w, h]`. If this property is set and `type` is not set, it will default to `rect`.
|
||||||
|
- `label` (*string*) : an optional piece of text to label the annotation with
|
||||||
|
- `stroke` (*string*) : the line color of the annotation. Default: `#ffC000`
|
||||||
|
- `lineWidth` (*number*) : the stroke width used to draw the annotation. Default: `5`
|
||||||
|
- `fontSize` (*number*) : the font size to use for the label. Default: `24`
|
||||||
|
- `fontColor` (*string*) : the color of the font to use for the label. Default: `#ffC000`
|
||||||
|
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
msg.annotations = [ {
|
||||||
|
type: "rect",
|
||||||
|
x: 10, y: 10, w: 50, h: 50,
|
||||||
|
label: "hello"
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
```javascript
|
||||||
|
msg.annotations = [
|
||||||
|
{
|
||||||
|
type: "circle",
|
||||||
|
x: 50, y: 50, r: 20,
|
||||||
|
lineWidth: 10
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "rect",
|
||||||
|
x: 30, y: 30, w: 40, h: 40,
|
||||||
|
stroke: "blue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
```javascript
|
||||||
|
msg.annotations = [ {
|
||||||
|
type: "rect",
|
||||||
|
bbox: [ 10, 10, 50, 50]
|
||||||
|
}]
|
||||||
|
```
|
BIN
utility/annotate-image/SourceSansPro-Regular.ttf
Normal file
BIN
utility/annotate-image/SourceSansPro-Regular.ttf
Normal file
Binary file not shown.
166
utility/annotate-image/annotate.html
Normal file
166
utility/annotate-image/annotate.html
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
|
||||||
|
<script type="text/x-red" data-template-name="annotate-image">
|
||||||
|
<div class="form-row">
|
||||||
|
<span id="node-input-row-stroke">
|
||||||
|
<label for="node-input-stroke">Line Color</label>
|
||||||
|
</span>
|
||||||
|
<label style="margin-left: 20px" for="node-input-lineWidth">Line Width</label>
|
||||||
|
<input style="width: 50px" type="text" id="node-input-lineWidth">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<span id="node-input-row-fontColor">
|
||||||
|
<label for="node-input-fontColor">Font Color</label>
|
||||||
|
</span>
|
||||||
|
<label style="margin-left: 20px" for="node-input-fontSize">Font Size</label>
|
||||||
|
<input style="width: 50px" type="text" id="node-input-fontSize">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/x-red" data-help-name="annotate-image">
|
||||||
|
<p>A node that can annotate JPEG images with simple shapes and labels.</p>
|
||||||
|
<h3>Inputs</h3>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>payload<span class="property-type">Buffer</span></dt>
|
||||||
|
<dd>A Buffer containing a JPEG image. Support for PNG will come soon.</dd>
|
||||||
|
<dt>annotations<span class="property-type">Array</span></dt>
|
||||||
|
<dd>An array of annotations to apply to the image. See below for details
|
||||||
|
of the annotation format.</dd>
|
||||||
|
</dl>
|
||||||
|
<h3>Outputs</h3>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>payload<span class="property-type">Buffer</span></dt>
|
||||||
|
<dd>The image with any annotations applied.</dd>
|
||||||
|
</dl>
|
||||||
|
<h3>Details</h3>
|
||||||
|
<p>The annotations provided in <code>msg.annotations</code> are applied in order.
|
||||||
|
Each annotation is an object with the following properties:</p>
|
||||||
|
<dl class="message-properties">
|
||||||
|
<dt>type<span class="property-type">string</span></dt>
|
||||||
|
<dd><ul>
|
||||||
|
<li><code>"rect"</code> - draws a rectangle</li>
|
||||||
|
<li><code>"circle"</code> - draws a circle</li>
|
||||||
|
</dd>
|
||||||
|
<dt>x,y <span class="property-type">number</span></dt>
|
||||||
|
<dd>The top-left corner of a <code>rect</code> annotation, or the center of a <code>circle</code> annotation.</dd>
|
||||||
|
<dt>w,h <span class="property-type">number</span></dt>
|
||||||
|
<dd>The width and height of a <code>rect</code> annotation.</dd>
|
||||||
|
<dt>r <span class="property-type">number</span></dt>
|
||||||
|
<dd>The radius of a <code>circle</code> annotation.</dd>
|
||||||
|
<dt>bbox <span class="property-type">array</span></dt>
|
||||||
|
<dd>This can be used instead of <code>x</code>,<code>y</code>,<code>w</code>,<code>h</code> and <code>r</code>. It should
|
||||||
|
be an array of four values giving the bounding box of the annotation: <code>[x, y, w, h]</code>.<br>
|
||||||
|
If this property is set and <code>type</code> is not set, it will default to <code>rect</code>.</dd>
|
||||||
|
<dt>label <span class="property-type">string</span></dt>
|
||||||
|
<dd>An optional piece of text to label the annotation with</dd>
|
||||||
|
<dt>stroke <span class="property-type">string</span></dt>
|
||||||
|
<dd>The line color of the annotation. Default: <code>"#ffC000"</code></dd>
|
||||||
|
<dt>lineWidth <span class="property-type">number</span></dt>
|
||||||
|
<dd>The stroke width used to draw the annotation. Default: <code>5</code></dd>
|
||||||
|
<dt>fontSize <span class="property-type">number</span></dt>
|
||||||
|
<dd>The font size to use for the label. Default: <code>24</code></dd>
|
||||||
|
<dt>fontColor <span class="property-type">string</span></dt>
|
||||||
|
<dd>The color of the font to use for the label. Default: <code>"#ffC000"</code></dd>
|
||||||
|
</dl>
|
||||||
|
<h3>Examples</h3>
|
||||||
|
<pre> msg.annotations = [ {
|
||||||
|
type: "rect",
|
||||||
|
x: 10, y: 10, w: 50, h: 50,
|
||||||
|
label: "hello"
|
||||||
|
}]</pre>
|
||||||
|
<pre> msg.annotations = [ {
|
||||||
|
type: "circle",
|
||||||
|
x: 50, y: 50, r: 20
|
||||||
|
}]</pre>
|
||||||
|
<pre> msg.annotations = [ {
|
||||||
|
type: "rect",
|
||||||
|
bbox: [ 10, 10, 50, 50]
|
||||||
|
}]</pre>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
// Lifted from @node-red/editor-client/.../group.js
|
||||||
|
// Need to make this a default built-in palette so we don't have to copy
|
||||||
|
// it around.
|
||||||
|
var colorPalette = [
|
||||||
|
"#ff0000",
|
||||||
|
"#ffC000",
|
||||||
|
"#ffff00",
|
||||||
|
"#92d04f",
|
||||||
|
"#0070c0",
|
||||||
|
"#001f60",
|
||||||
|
"#6f2fa0",
|
||||||
|
"#000000",
|
||||||
|
"#777777"
|
||||||
|
]
|
||||||
|
var colorSteps = 3;
|
||||||
|
var colorCount = colorPalette.length;
|
||||||
|
for (var i=0,len=colorPalette.length*colorSteps;i<len;i++) {
|
||||||
|
var ci = i%colorCount;
|
||||||
|
var j = Math.floor(i/colorCount)+1;
|
||||||
|
var c = colorPalette[ci];
|
||||||
|
var r = parseInt(c.substring(1, 3), 16);
|
||||||
|
var g = parseInt(c.substring(3, 5), 16);
|
||||||
|
var b = parseInt(c.substring(5, 7), 16);
|
||||||
|
var dr = (255-r)/(colorSteps+((ci===colorCount-1) ?0:1));
|
||||||
|
var dg = (255-g)/(colorSteps+((ci===colorCount-1) ?0:1));
|
||||||
|
var db = (255-b)/(colorSteps+((ci===colorCount-1) ?0:1));
|
||||||
|
r = Math.min(255,Math.floor(r+j*dr));
|
||||||
|
g = Math.min(255,Math.floor(g+j*dg));
|
||||||
|
b = Math.min(255,Math.floor(b+j*db));
|
||||||
|
var s = ((r<<16) + (g<<8) + b).toString(16);
|
||||||
|
colorPalette.push('#'+'000000'.slice(0, 6-s.length)+s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
RED.nodes.registerType('annotate-image',{
|
||||||
|
category: 'utility',
|
||||||
|
color:"#f1c2f0",
|
||||||
|
defaults: {
|
||||||
|
name: {value:""},
|
||||||
|
fill: {value:""},
|
||||||
|
stroke: {value:"#ffC000"},
|
||||||
|
lineWidth: {value:5},
|
||||||
|
fontSize: {value: 24},
|
||||||
|
fontColor: {value: "#ffC000"}
|
||||||
|
|
||||||
|
},
|
||||||
|
inputs:1,
|
||||||
|
outputs:1,
|
||||||
|
icon: "font-awesome/fa-object-group",
|
||||||
|
label: function() {
|
||||||
|
return this.name||"annotate image";
|
||||||
|
},
|
||||||
|
labelStyle: function() {
|
||||||
|
return this.name?"node_label_italic":"";
|
||||||
|
},
|
||||||
|
oneditprepare: function() {
|
||||||
|
RED.colorPicker.create({
|
||||||
|
id:"node-input-stroke",
|
||||||
|
value: this.stroke || "#ffC000",
|
||||||
|
palette: colorPalette,
|
||||||
|
cellPerRow: colorCount,
|
||||||
|
cellWidth: 16,
|
||||||
|
cellHeight: 16,
|
||||||
|
cellMargin: 3
|
||||||
|
}).appendTo("#node-input-row-stroke");
|
||||||
|
RED.colorPicker.create({
|
||||||
|
id:"node-input-fontColor",
|
||||||
|
value: this.fontColor || "#ffC000",
|
||||||
|
palette: colorPalette,
|
||||||
|
cellPerRow: colorCount,
|
||||||
|
cellWidth: 16,
|
||||||
|
cellHeight: 16,
|
||||||
|
cellMargin: 3
|
||||||
|
}).appendTo("#node-input-row-fontColor");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
160
utility/annotate-image/annotate.js
Normal file
160
utility/annotate-image/annotate.js
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
"use strict";
|
||||||
|
const pureimage = require("pureimage");
|
||||||
|
const Readable = require("stream").Readable;
|
||||||
|
const Writable = require("stream").Writable;
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
let fontLoaded = false;
|
||||||
|
function loadFont() {
|
||||||
|
if (!fontLoaded) {
|
||||||
|
const fnt = pureimage.registerFont(path.join(__dirname,'./SourceSansPro-Regular.ttf'),'Source Sans Pro');
|
||||||
|
fnt.load();
|
||||||
|
fontLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnnotateNode(n) {
|
||||||
|
RED.nodes.createNode(this,n);
|
||||||
|
var node = this;
|
||||||
|
const defaultFill = n.fill || "";
|
||||||
|
const defaultStroke = n.stroke || "#ffC000";
|
||||||
|
const defaultLineWidth = parseInt(n.lineWidth) || 5;
|
||||||
|
const defaultFontSize = n.fontSize || 24;
|
||||||
|
const defaultFontColor = n.fontColor || "#ffC000"
|
||||||
|
loadFont();
|
||||||
|
|
||||||
|
this.on("input", function(msg) {
|
||||||
|
if (Buffer.isBuffer(msg.payload)) {
|
||||||
|
if (msg.payload[0] !== 0xFF || msg.payload[1] !== 0xD8) {
|
||||||
|
node.error("Not a JPEG image",msg);
|
||||||
|
} else if (Array.isArray(msg.annotations) && msg.annotations.length > 0) {
|
||||||
|
const stream = new Readable();
|
||||||
|
stream.push(msg.payload);
|
||||||
|
stream.push(null);
|
||||||
|
pureimage.decodeJPEGFromStream(stream).then(img => {
|
||||||
|
const c = pureimage.make(img.width, img.height);
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
ctx.drawImage(img,0,0,img.width,img.height);
|
||||||
|
|
||||||
|
ctx.lineJoin = 'bevel';
|
||||||
|
|
||||||
|
msg.annotations.forEach(function(annotation) {
|
||||||
|
ctx.fillStyle = annotation.fill || defaultFill;
|
||||||
|
ctx.strokeStyle = annotation.stroke || defaultStroke;
|
||||||
|
ctx.lineWidth = annotation.lineWidth || defaultLineWidth;
|
||||||
|
ctx.lineJoin = 'bevel';
|
||||||
|
let x,y,r,w,h;
|
||||||
|
|
||||||
|
if (!annotation.type && annotation.bbox) {
|
||||||
|
annotation.type = 'rect';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(annotation.type) {
|
||||||
|
case 'rect':
|
||||||
|
if (annotation.bbox) {
|
||||||
|
x = annotation.bbox[0]
|
||||||
|
y = annotation.bbox[1]
|
||||||
|
w = annotation.bbox[2]
|
||||||
|
h = annotation.bbox[3]
|
||||||
|
} else {
|
||||||
|
x = annotation.x
|
||||||
|
y = annotation.y
|
||||||
|
w = annotation.w
|
||||||
|
h = annotation.h
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x < 0) {
|
||||||
|
w += x;
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
if (y < 0) {
|
||||||
|
h += y;
|
||||||
|
y = 0;
|
||||||
|
}
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.lineWidth = annotation.lineWidth || defaultLineWidth;
|
||||||
|
ctx.rect(x,y,w,h);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
if (annotation.label) {
|
||||||
|
ctx.font = `${annotation.fontSize || defaultFontSize}pt 'Source Sans Pro'`;
|
||||||
|
ctx.fillStyle = annotation.fontColor || defaultFontColor;
|
||||||
|
ctx.textBaseline = "top";
|
||||||
|
ctx.textAlign = "left";
|
||||||
|
ctx.fillText(annotation.label, x+2,y)
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'circle':
|
||||||
|
if (annotation.bbox) {
|
||||||
|
x = annotation.bbox[0] + annotation.bbox[2]/2
|
||||||
|
y = annotation.bbox[1] + annotation.bbox[3]/2
|
||||||
|
r = Math.min(annotation.bbox[2],annotation.bbox[3])/2;
|
||||||
|
} else {
|
||||||
|
x = annotation.x
|
||||||
|
y = annotation.y
|
||||||
|
r = annotation.r;
|
||||||
|
}
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.lineWidth = annotation.lineWidth || defaultLineWidth;
|
||||||
|
ctx.arc(x,y,r,0,Math.PI*2);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.stroke();
|
||||||
|
if (annotation.label) {
|
||||||
|
ctx.font = `${annotation.fontSize || defaultFontSize}pt 'Source Sans Pro'`;
|
||||||
|
ctx.fillStyle = annotation.fontColor || defaultFontColor;
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.fillText(annotation.label, x+2,y)
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const bufferOutput = getWritableBuffer();
|
||||||
|
pureimage.encodeJPEGToStream(c,bufferOutput.stream,90).then(() => {
|
||||||
|
msg.payload = bufferOutput.getBuffer();
|
||||||
|
node.send(msg);
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
node.error(err,msg);
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// No annotations to make - send the message on
|
||||||
|
node.send(msg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node.error("Payload not a Buffer",msg)
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("annotate-image",AnnotateNode);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function getWritableBuffer() {
|
||||||
|
var currentSize = 0;
|
||||||
|
var buffer = null;
|
||||||
|
const stream = new Writable({
|
||||||
|
write(chunk, encoding, callback) {
|
||||||
|
if (!buffer) {
|
||||||
|
buffer = Buffer.from(chunk);
|
||||||
|
} else {
|
||||||
|
var newBuffer = Buffer.allocUnsafe(currentSize + chunk.length);
|
||||||
|
buffer.copy(newBuffer);
|
||||||
|
chunk.copy(newBuffer,currentSize);
|
||||||
|
buffer = newBuffer;
|
||||||
|
}
|
||||||
|
currentSize += chunk.length
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
stream: stream,
|
||||||
|
getBuffer: function() {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
utility/annotate-image/package.json
Normal file
26
utility/annotate-image/package.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "node-red-node-annotate-image",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "A Node-RED node that can annotate an image",
|
||||||
|
"dependencies": {
|
||||||
|
"pureimage": "^0.2.5"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/node-red/node-red-nodes/tree/master/utility/iamge-annotate"
|
||||||
|
},
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"keywords": [
|
||||||
|
"node-red"
|
||||||
|
],
|
||||||
|
"node-red": {
|
||||||
|
"nodes": {
|
||||||
|
"annotate": "annotate.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Nick O'Leary"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user