Add annotate-image node

This commit is contained in:
Nick O'Leary 2020-10-29 22:45:02 +00:00
parent 395bf77441
commit 58e8cdc8d3
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
6 changed files with 432 additions and 0 deletions

View File

@ -0,0 +1,14 @@
Copyright 2016 JS Foundation and other contributors, https://js.foundation/
Copyright 2013-2016 IBM Corp.
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.

View 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]`.
- `label` (*string*) : an optional piece of text to label the annotation with
- `stroke` (*string*) : the 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]
}]
```

Binary file not shown.

View File

@ -0,0 +1,165 @@
<script type="text/x-red" data-template-name="annotate-image">
<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>
<div id="node-input-row-stroke" class="form-row">
<label for="node-input-stroke">Stroke</label>
</div>
<div class="form-row">
<label for="node-input-lineWidth">Line Width</label>
<input type="text" id="node-input-lineWidth">
</div>
<div class="form-row">
<label for="node-input-fontSize">Font Size</label>
<input type="text" id="node-input-fontSize">
</div>
<div id="node-input-row-fontColor" class="form-row">
<label for="node-input-fontColor">Font Color</label>
</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>.</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>Set the 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>

View File

@ -0,0 +1,155 @@
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;
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;
}
}
}
}

View 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"
}
]
}