@graciousstar/node-red-contrib-vision-tools 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +875 -0
- package/CHANGELOG.md +524 -0
- package/LICENSE +202 -0
- package/README.md +992 -0
- package/barcode-locate.html +256 -0
- package/barcode-locate.js +156 -0
- package/checkerboard-calibrate.html +167 -0
- package/checkerboard-calibrate.js +282 -0
- package/examples/label-crop-with-line-finder.json +239 -0
- package/golden-compare.html +713 -0
- package/golden-compare.js +1126 -0
- package/icons/checkerboard-calibrate.svg +8 -0
- package/icons/golden-compare.svg +6 -0
- package/label-crop.html +1178 -0
- package/label-crop.js +250 -0
- package/lib/align.js +867 -0
- package/lib/checkerboard.js +331 -0
- package/lib/compare.js +1338 -0
- package/lib/components.js +84 -0
- package/lib/dilate.js +65 -0
- package/lib/inspector.js +188 -0
- package/lib/inspectorCore.js +128 -0
- package/lib/inspectorWorker.js +40 -0
- package/lib/integral.js +76 -0
- package/lib/labelCrop.js +1461 -0
- package/lib/lineFinder.js +765 -0
- package/lib/localAlign.js +360 -0
- package/lib/locate.js +302 -0
- package/lib/nativeSeed.js +292 -0
- package/lib/parallel.js +428 -0
- package/lib/pool.js +250 -0
- package/lib/poolWorker.js +324 -0
- package/lib/scaleFile.js +83 -0
- package/lib/shared.js +87 -0
- package/lib/threshold.js +231 -0
- package/lib/transformFile.js +169 -0
- package/lib/warp.js +216 -0
- package/line-finder.html +1361 -0
- package/line-finder.js +280 -0
- package/package.json +73 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="barcode-locate">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
4
|
+
<input type="text" id="node-input-name" placeholder="Name" />
|
|
5
|
+
</div>
|
|
6
|
+
<div class="form-row">
|
|
7
|
+
<label for="node-input-mode"><i class="fa fa-search"></i> Mode</label>
|
|
8
|
+
<select id="node-input-mode" style="width:70%;">
|
|
9
|
+
<option value="regionsThenAuto">Regions, then full image if nothing found</option>
|
|
10
|
+
<option value="regionsOnly">Regions only (no full-image fallback)</option>
|
|
11
|
+
<option value="autoOnly">Full image only (ignore regions)</option>
|
|
12
|
+
</select>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div class="form-row node-input-region-container-row">
|
|
16
|
+
<label><i class="fa fa-crop"></i> Regions</label>
|
|
17
|
+
<span class="node-input-hint" style="color:#999;">
|
|
18
|
+
pixel coordinates on the full-resolution image - find them with any
|
|
19
|
+
image viewer (see Help). Scanned in order, fastest first; skipped
|
|
20
|
+
entirely in "Full image only" mode.
|
|
21
|
+
</span>
|
|
22
|
+
<ol id="node-input-region-container" style="margin-top:8px;"></ol>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="form-row">
|
|
26
|
+
<label><i class="fa fa-list"></i> Formats</label>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="form-row" style="margin-left:110px;">
|
|
29
|
+
<input type="checkbox" id="node-input-Code128" style="width:auto;vertical-align:middle;" />
|
|
30
|
+
<label for="node-input-Code128" style="width:auto;">Code128</label>
|
|
31
|
+
<input type="checkbox" id="node-input-DataMatrix" style="width:auto;vertical-align:middle;margin-left:12px;" />
|
|
32
|
+
<label for="node-input-DataMatrix" style="width:auto;">DataMatrix</label>
|
|
33
|
+
<input type="checkbox" id="node-input-QRCode" style="width:auto;vertical-align:middle;margin-left:12px;" />
|
|
34
|
+
<label for="node-input-QRCode" style="width:auto;">QRCode</label>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="form-row" style="margin-left:110px;">
|
|
37
|
+
<input type="checkbox" id="node-input-EAN13" style="width:auto;vertical-align:middle;" />
|
|
38
|
+
<label for="node-input-EAN13" style="width:auto;">EAN13</label>
|
|
39
|
+
<input type="checkbox" id="node-input-Code39" style="width:auto;vertical-align:middle;margin-left:12px;" />
|
|
40
|
+
<label for="node-input-Code39" style="width:auto;">Code39</label>
|
|
41
|
+
<input type="checkbox" id="node-input-ITF" style="width:auto;vertical-align:middle;margin-left:12px;" />
|
|
42
|
+
<label for="node-input-ITF" style="width:auto;">ITF</label>
|
|
43
|
+
<input type="checkbox" id="node-input-PDF417" style="width:auto;vertical-align:middle;margin-left:12px;" />
|
|
44
|
+
<label for="node-input-PDF417" style="width:auto;">PDF417</label>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="form-row" style="margin-left:110px;">
|
|
47
|
+
<input type="checkbox" id="node-input-EAN8" style="width:auto;vertical-align:middle;" />
|
|
48
|
+
<label for="node-input-EAN8" style="width:auto;">EAN8</label>
|
|
49
|
+
<span class="node-input-hint" style="margin-left:10px; color:#999;"
|
|
50
|
+
>off by default - short enough to have a real chance of misreading noise as a confident wrong result, see Help</span
|
|
51
|
+
>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="form-row">
|
|
55
|
+
<label><i class="fa fa-cogs"></i> Reader</label>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="form-row" style="margin-left:110px;">
|
|
58
|
+
<input type="checkbox" id="node-input-tryHarder" style="width:auto;vertical-align:middle;" />
|
|
59
|
+
<label for="node-input-tryHarder" style="width:auto;">try harder</label>
|
|
60
|
+
<input type="checkbox" id="node-input-tryRotate" style="width:auto;vertical-align:middle;margin-left:12px;" />
|
|
61
|
+
<label for="node-input-tryRotate" style="width:auto;">try rotate</label>
|
|
62
|
+
<span class="node-input-hint" style="margin-left:10px; color:#999;"
|
|
63
|
+
>both needed for full recall - see Help</span
|
|
64
|
+
>
|
|
65
|
+
</div>
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<script type="text/html" data-help-name="barcode-locate">
|
|
69
|
+
<p>
|
|
70
|
+
Finds and decodes barcodes (1D and 2D) in an image using
|
|
71
|
+
<a href="https://github.com/Sec-ant/zxing-wasm" target="_blank">zxing-wasm</a>
|
|
72
|
+
- a WebAssembly build of the real <code>zxing-cpp</code> engine, not the
|
|
73
|
+
much weaker pure-JS <code>@zxing/library</code> port
|
|
74
|
+
<code>node-red-contrib-image-tools</code>'s Barcode Decoder uses. Genuine
|
|
75
|
+
multi-symbol detection, rotation tolerance, and native DataMatrix
|
|
76
|
+
support - no separate localization step needed to get a result out of
|
|
77
|
+
it on a big photo.
|
|
78
|
+
</p>
|
|
79
|
+
|
|
80
|
+
<h3>Why regions</h3>
|
|
81
|
+
<p>
|
|
82
|
+
Scanning a whole multi-megapixel photo takes on the order of a second,
|
|
83
|
+
mostly the detector's own search over the full frame. On a fixed camera
|
|
84
|
+
rig - the same assumption <code>golden-compare</code> and
|
|
85
|
+
<code>checkerboard-calibrate</code> already make - barcodes land in
|
|
86
|
+
roughly the same place shot to shot, so telling this node where to look
|
|
87
|
+
turns that into a handful of single-digit-millisecond crops instead
|
|
88
|
+
(measured ~2-15ms per region vs ~1.4s for the same image scanned whole
|
|
89
|
+
on a 4096x5500 test photo - roughly two orders of magnitude). Mode
|
|
90
|
+
<i>"Regions, then full image if nothing found"</i> keeps the slower
|
|
91
|
+
whole-image scan available as a safety net (label repositioned, a
|
|
92
|
+
region mis-measured) without paying its cost on every normal run.
|
|
93
|
+
</p>
|
|
94
|
+
<p>
|
|
95
|
+
Regions are plain pixel coordinates on the image at its native
|
|
96
|
+
resolution - <code>x</code>/<code>y</code> is the top-left corner,
|
|
97
|
+
<code>width</code>/<code>height</code> its size. Find them by opening a
|
|
98
|
+
sample photo in any image viewer that shows a pixel-coordinate readout
|
|
99
|
+
(or crop candidates and check the crop looks right) and reading off a
|
|
100
|
+
box comfortably around the barcode, including its quiet-zone margin -
|
|
101
|
+
tight crops with no margin can make the reader fail even though the
|
|
102
|
+
barcode itself is perfectly legible. Re-measure regions if the camera,
|
|
103
|
+
lens, or mounting changes.
|
|
104
|
+
</p>
|
|
105
|
+
|
|
106
|
+
<h3>Input</h3>
|
|
107
|
+
<p>
|
|
108
|
+
<code>msg.payload</code> - the image: Buffer / Uint8Array, a file path
|
|
109
|
+
string, or an object with <code>data</code>/<code>buffer</code>/<code>path</code>.
|
|
110
|
+
</p>
|
|
111
|
+
<p>
|
|
112
|
+
Optional per-message overrides: <code>msg.regions</code> (array of
|
|
113
|
+
<code>{ label, x, y, width, height }</code>, replaces the configured
|
|
114
|
+
list for this message) and <code>msg.mode</code>.
|
|
115
|
+
</p>
|
|
116
|
+
|
|
117
|
+
<h3>Output</h3>
|
|
118
|
+
<p>One message per barcode found, in scan order (regions first, then the full-image fallback if it ran):</p>
|
|
119
|
+
<ul>
|
|
120
|
+
<li><code>msg.text</code> - the decoded content</li>
|
|
121
|
+
<li><code>msg.format</code> - e.g. <code>"Code128"</code>, <code>"DataMatrix"</code></li>
|
|
122
|
+
<li><code>msg.roi</code> - <code>{ x, y, width, height }</code> of the region scanned (full-image coordinates)</li>
|
|
123
|
+
<li><code>msg.regionLabel</code> - the region's configured label, or <code>null</code> for a full-image find</li>
|
|
124
|
+
<li><code>msg.source</code> - <code>"region"</code> or <code>"fullImage"</code></li>
|
|
125
|
+
<li><code>msg.decodeMs</code> - time for that specific region/scan (not the whole node's runtime)</li>
|
|
126
|
+
<li><code>msg.timings</code> - <code>{ regionsMs, fullImageMs }</code>, whichever ran</li>
|
|
127
|
+
<li><code>msg.payload</code> - a cropped preview image of that result's region</li>
|
|
128
|
+
</ul>
|
|
129
|
+
<p>
|
|
130
|
+
If nothing is found anywhere, one message is sent with
|
|
131
|
+
<code>msg.text = null</code>, <code>msg.barcodeCount = 0</code>.
|
|
132
|
+
</p>
|
|
133
|
+
|
|
134
|
+
<h3>Notes</h3>
|
|
135
|
+
<p>
|
|
136
|
+
<b>EAN8 is off by default.</b> It's short enough that ZXing
|
|
137
|
+
implementations (both the JS port and, in testing against this
|
|
138
|
+
project's own sample label, zxing-wasm itself) have a real chance of
|
|
139
|
+
matching noise in a region/crop that has no barcode in it at all - a
|
|
140
|
+
confident wrong answer, worse than "not found". Turn it on only if a
|
|
141
|
+
real EAN-8 code is expected.
|
|
142
|
+
</p>
|
|
143
|
+
<p>
|
|
144
|
+
<b>Try harder / try rotate</b> are both on by default and both matter:
|
|
145
|
+
against this project's sample label, <code>tryHarder=false</code>
|
|
146
|
+
missed most of the real barcodes, and <code>tryRotate=false</code>
|
|
147
|
+
missed ones printed upside-down or rotated 90 degrees. Turning either
|
|
148
|
+
off trades recall for speed.
|
|
149
|
+
</p>
|
|
150
|
+
<p>
|
|
151
|
+
The first decode after Node-RED starts (or this flow redeploys) pays a
|
|
152
|
+
one-time warmup cost for zxing-wasm's WASM module - handled internally
|
|
153
|
+
so it doesn't land on whichever message happens to arrive first.
|
|
154
|
+
</p>
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<script type="text/javascript">
|
|
158
|
+
RED.nodes.registerType("barcode-locate", {
|
|
159
|
+
category: "vision",
|
|
160
|
+
color: "#FFAAAA",
|
|
161
|
+
defaults: {
|
|
162
|
+
name: { value: "" },
|
|
163
|
+
mode: { value: "regionsThenAuto" },
|
|
164
|
+
regions: { value: [] },
|
|
165
|
+
Code128: { value: true },
|
|
166
|
+
DataMatrix: { value: true },
|
|
167
|
+
QRCode: { value: true },
|
|
168
|
+
EAN13: { value: true },
|
|
169
|
+
Code39: { value: true },
|
|
170
|
+
ITF: { value: true },
|
|
171
|
+
PDF417: { value: true },
|
|
172
|
+
EAN8: { value: false },
|
|
173
|
+
tryHarder: { value: true },
|
|
174
|
+
tryRotate: { value: true },
|
|
175
|
+
},
|
|
176
|
+
inputs: 1,
|
|
177
|
+
outputs: 1,
|
|
178
|
+
icon: "font-awesome/fa-barcode",
|
|
179
|
+
label: function () {
|
|
180
|
+
return this.name || "barcode locate";
|
|
181
|
+
},
|
|
182
|
+
paletteLabel: function () {
|
|
183
|
+
return "barcode locate";
|
|
184
|
+
},
|
|
185
|
+
oneditprepare: function () {
|
|
186
|
+
const list = $("#node-input-region-container")
|
|
187
|
+
.css("min-height", "150px")
|
|
188
|
+
.css("min-width", "450px")
|
|
189
|
+
.editableList({
|
|
190
|
+
addItem: function (container, index, region) {
|
|
191
|
+
region.label = region.label || "";
|
|
192
|
+
region.x = region.x || 0;
|
|
193
|
+
region.y = region.y || 0;
|
|
194
|
+
region.width = region.width || 0;
|
|
195
|
+
region.height = region.height || 0;
|
|
196
|
+
|
|
197
|
+
container.css({ overflow: "hidden", whiteSpace: "nowrap" });
|
|
198
|
+
|
|
199
|
+
const row1 = $("<div/>", { style: "margin-bottom:4px;" }).appendTo(container);
|
|
200
|
+
const labelInput = $("<input/>", {
|
|
201
|
+
type: "text",
|
|
202
|
+
style: "width:100%;",
|
|
203
|
+
placeholder: "label (optional, e.g. \"main UDI barcode\")",
|
|
204
|
+
})
|
|
205
|
+
.appendTo(row1)
|
|
206
|
+
.val(region.label);
|
|
207
|
+
|
|
208
|
+
const row2 = $("<div/>").appendTo(container);
|
|
209
|
+
function numField(placeholder, value) {
|
|
210
|
+
return $("<input/>", {
|
|
211
|
+
type: "number",
|
|
212
|
+
style: "width:70px;margin-right:6px;",
|
|
213
|
+
placeholder: placeholder,
|
|
214
|
+
})
|
|
215
|
+
.appendTo(row2)
|
|
216
|
+
.val(value);
|
|
217
|
+
}
|
|
218
|
+
$("<span/>", { text: "x:", style: "margin-right:3px;" }).appendTo(row2);
|
|
219
|
+
const xInput = numField("x", region.x);
|
|
220
|
+
$("<span/>", { text: "y:", style: "margin-right:3px;" }).appendTo(row2);
|
|
221
|
+
const yInput = numField("y", region.y);
|
|
222
|
+
$("<span/>", { text: "w:", style: "margin-right:3px;" }).appendTo(row2);
|
|
223
|
+
const wInput = numField("width", region.width);
|
|
224
|
+
$("<span/>", { text: "h:", style: "margin-right:3px;" }).appendTo(row2);
|
|
225
|
+
const hInput = numField("height", region.height);
|
|
226
|
+
|
|
227
|
+
container.data("read", function () {
|
|
228
|
+
return {
|
|
229
|
+
label: labelInput.val(),
|
|
230
|
+
x: parseInt(xInput.val(), 10) || 0,
|
|
231
|
+
y: parseInt(yInput.val(), 10) || 0,
|
|
232
|
+
width: parseInt(wInput.val(), 10) || 0,
|
|
233
|
+
height: parseInt(hInput.val(), 10) || 0,
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
removable: true,
|
|
238
|
+
sortable: true,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const regions = this.regions || [];
|
|
242
|
+
for (let i = 0; i < regions.length; i++) {
|
|
243
|
+
list.editableList("addItem", regions[i]);
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
oneditsave: function () {
|
|
247
|
+
const items = $("#node-input-region-container").editableList("items");
|
|
248
|
+
const regions = [];
|
|
249
|
+
items.each(function () {
|
|
250
|
+
const read = $(this).data("read");
|
|
251
|
+
if (read) regions.push(read());
|
|
252
|
+
});
|
|
253
|
+
this.regions = regions;
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
</script>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* barcode-locate Node-RED node.
|
|
3
|
+
*
|
|
4
|
+
* Finds and decodes barcodes (1D and 2D) in an image using zxing-wasm, a
|
|
5
|
+
* WebAssembly build of the zxing-cpp engine. Detection and decode logic
|
|
6
|
+
* lives in lib/locate.js so it can be exercised outside Node-RED.
|
|
7
|
+
*
|
|
8
|
+
* The point of this node over just calling zxing-wasm directly on the full
|
|
9
|
+
* image every time: on a fixed camera rig (same assumption
|
|
10
|
+
* golden-compare/checkerboard-calibrate already make - see their docs),
|
|
11
|
+
* barcodes land in roughly the same place shot to shot, so pre-defining
|
|
12
|
+
* where to look and scanning only those regions is 1-2 orders of magnitude
|
|
13
|
+
* faster than scanning the whole frame. "Regions, then full image if
|
|
14
|
+
* nothing found" keeps the whole-image scan available as a safety net
|
|
15
|
+
* (label repositioned, region mis-measured, etc.) without paying its cost
|
|
16
|
+
* on every normal run.
|
|
17
|
+
*
|
|
18
|
+
* Input (msg.payload): Buffer / Uint8Array / ArrayBuffer with image bytes,
|
|
19
|
+
* a file path string, or an object { data | buffer | path }.
|
|
20
|
+
* Optional per-message overrides: msg.regions (array), msg.mode.
|
|
21
|
+
*
|
|
22
|
+
* Output: one message per barcode found, in the order regions were scanned
|
|
23
|
+
* (then, if used, the full-image fallback) - msg.text, msg.format,
|
|
24
|
+
* msg.roi, msg.regionLabel, msg.source ("region"|"fullImage"),
|
|
25
|
+
* msg.decodeMs, msg.timings, msg.payload (a preview crop of that barcode's
|
|
26
|
+
* region). If nothing is found at all, sends one message with
|
|
27
|
+
* msg.text = null.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require("fs");
|
|
31
|
+
const fsp = fs.promises;
|
|
32
|
+
const { locateBarcodes, DEFAULT_FORMATS } = require("./lib/locate.js");
|
|
33
|
+
|
|
34
|
+
module.exports = (RED) => {
|
|
35
|
+
// same resolveImage contract as golden-compare.js / checkerboard-calibrate.js
|
|
36
|
+
async function resolveImage(source, label) {
|
|
37
|
+
if (source == null || source === "") {
|
|
38
|
+
throw new Error(`${label} is empty`);
|
|
39
|
+
}
|
|
40
|
+
if (Buffer.isBuffer(source) || source instanceof Uint8Array || source instanceof ArrayBuffer) {
|
|
41
|
+
return Buffer.from(source);
|
|
42
|
+
}
|
|
43
|
+
if (typeof source === "string") {
|
|
44
|
+
if (fs.existsSync(source)) return fsp.readFile(source);
|
|
45
|
+
throw new Error(`${label} does not exist on disk: "${source}"`);
|
|
46
|
+
}
|
|
47
|
+
if (typeof source === "object") {
|
|
48
|
+
const data = source.data || source.buffer;
|
|
49
|
+
if (Buffer.isBuffer(data) || data instanceof Uint8Array) {
|
|
50
|
+
return Buffer.from(data);
|
|
51
|
+
}
|
|
52
|
+
if (typeof source.path === "string" && fs.existsSync(source.path)) {
|
|
53
|
+
return fsp.readFile(source.path);
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`${label} object must contain "data"/"buffer" or an existing "path"`);
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`unsupported ${label} type: ${typeof source}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeRegions(regions) {
|
|
61
|
+
if (!Array.isArray(regions)) return [];
|
|
62
|
+
return regions
|
|
63
|
+
.map((r) => ({
|
|
64
|
+
label: String(r.label || "").trim(),
|
|
65
|
+
x: parseInt(r.x, 10) || 0,
|
|
66
|
+
y: parseInt(r.y, 10) || 0,
|
|
67
|
+
width: parseInt(r.width, 10) || 0,
|
|
68
|
+
height: parseInt(r.height, 10) || 0,
|
|
69
|
+
}))
|
|
70
|
+
.filter((r) => r.width > 0 && r.height > 0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function totalMs(timings) {
|
|
74
|
+
return (timings.regionsMs || 0) + (timings.fullImageMs || 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function BarcodeLocateNode(config) {
|
|
78
|
+
RED.nodes.createNode(this, config);
|
|
79
|
+
const node = this;
|
|
80
|
+
|
|
81
|
+
node.mode = config.mode === "regionsOnly" || config.mode === "autoOnly" ? config.mode : "regionsThenAuto";
|
|
82
|
+
node.regions = normalizeRegions(config.regions);
|
|
83
|
+
|
|
84
|
+
const enabledFormats = DEFAULT_FORMATS.concat(["EAN8"]).filter((f) => config[f]);
|
|
85
|
+
node.readerOptions = {
|
|
86
|
+
tryHarder: config.tryHarder !== false,
|
|
87
|
+
tryRotate: config.tryRotate !== false,
|
|
88
|
+
maxNumberOfSymbols: 20,
|
|
89
|
+
formats: enabledFormats.length ? enabledFormats : DEFAULT_FORMATS,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
node.on("input", async (msg, send, done) => {
|
|
93
|
+
send = send || function () { node.send.apply(node, arguments); };
|
|
94
|
+
try {
|
|
95
|
+
const buffer = await resolveImage(msg.payload, "msg.payload");
|
|
96
|
+
const regions = msg.regions !== undefined ? normalizeRegions(msg.regions) : node.regions;
|
|
97
|
+
const mode = msg.mode || node.mode;
|
|
98
|
+
|
|
99
|
+
node.status({ fill: "blue", shape: "dot", text: `scanning ${regions.length} region${regions.length === 1 ? "" : "s"}…` });
|
|
100
|
+
|
|
101
|
+
const { results, timings, usedFullImage } = await locateBarcodes(buffer, {
|
|
102
|
+
regions,
|
|
103
|
+
mode,
|
|
104
|
+
readerOptions: node.readerOptions,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (results.length === 0) {
|
|
108
|
+
const noneMsg = Object.assign({}, msg, {
|
|
109
|
+
barcodeIndex: 0,
|
|
110
|
+
barcodeCount: 0,
|
|
111
|
+
text: null,
|
|
112
|
+
format: null,
|
|
113
|
+
roi: null,
|
|
114
|
+
regionLabel: null,
|
|
115
|
+
source: usedFullImage ? "fullImage" : "region",
|
|
116
|
+
timings,
|
|
117
|
+
});
|
|
118
|
+
send(noneMsg);
|
|
119
|
+
node.status({ fill: "yellow", shape: "ring", text: `none found (${totalMs(timings)}ms)` });
|
|
120
|
+
done();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < results.length; i++) {
|
|
125
|
+
const r = results[i];
|
|
126
|
+
const outMsg = Object.assign({}, msg, {
|
|
127
|
+
barcodeIndex: i,
|
|
128
|
+
barcodeCount: results.length,
|
|
129
|
+
text: r.text,
|
|
130
|
+
format: r.format,
|
|
131
|
+
roi: r.roi,
|
|
132
|
+
regionLabel: r.regionLabel,
|
|
133
|
+
source: r.source,
|
|
134
|
+
decodeMs: r.decodeMs,
|
|
135
|
+
timings,
|
|
136
|
+
payload: r.previewBuffer,
|
|
137
|
+
});
|
|
138
|
+
send(outMsg);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
node.status({
|
|
142
|
+
fill: "green",
|
|
143
|
+
shape: "dot",
|
|
144
|
+
text: `${results.length} found (${usedFullImage ? "full image" : "regions"}, ${totalMs(timings)}ms)`,
|
|
145
|
+
});
|
|
146
|
+
done();
|
|
147
|
+
} catch (err) {
|
|
148
|
+
node.status({ fill: "red", shape: "ring", text: "error" });
|
|
149
|
+
node.error(`barcode-locate: ${err && err.message ? err.message : err}`, msg);
|
|
150
|
+
done(err);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
RED.nodes.registerType("barcode-locate", BarcodeLocateNode);
|
|
156
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="checkerboard-calibrate">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
4
|
+
<input type="text" id="node-input-name" placeholder="Name" />
|
|
5
|
+
</div>
|
|
6
|
+
<div class="form-row">
|
|
7
|
+
<label for="node-input-targetPitchMm"
|
|
8
|
+
><i class="fa fa-arrows-h"></i> Target pitch [mm]</label
|
|
9
|
+
>
|
|
10
|
+
<input
|
|
11
|
+
type="number"
|
|
12
|
+
id="node-input-targetPitchMm"
|
|
13
|
+
min="0.01"
|
|
14
|
+
step="0.01"
|
|
15
|
+
style="width:90px;"
|
|
16
|
+
/>
|
|
17
|
+
<span class="node-input-hint" style="margin-left:10px; color:#999;"
|
|
18
|
+
>physical distance between adjacent same-colour squares on the
|
|
19
|
+
printed checkerboard</span
|
|
20
|
+
>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="form-row">
|
|
23
|
+
<label for="node-input-checkerboardCols"
|
|
24
|
+
><i class="fa fa-th"></i> Grid size</label
|
|
25
|
+
>
|
|
26
|
+
<input
|
|
27
|
+
type="number"
|
|
28
|
+
id="node-input-checkerboardCols"
|
|
29
|
+
min="2"
|
|
30
|
+
style="width:70px;"
|
|
31
|
+
placeholder="cols"
|
|
32
|
+
/>
|
|
33
|
+
<span style="margin:0 6px;">x</span>
|
|
34
|
+
<input
|
|
35
|
+
type="number"
|
|
36
|
+
id="node-input-checkerboardRows"
|
|
37
|
+
min="3"
|
|
38
|
+
style="width:70px;"
|
|
39
|
+
placeholder="rows"
|
|
40
|
+
/>
|
|
41
|
+
<span class="node-input-hint" style="margin-left:10px; color:#999;"
|
|
42
|
+
>expected dark-square grid (cols = longest row x rows) - see help</span
|
|
43
|
+
>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="form-row">
|
|
46
|
+
<label for="node-input-allowedErrorPercent"
|
|
47
|
+
><i class="fa fa-exclamation-triangle"></i> Allowed error</label
|
|
48
|
+
>
|
|
49
|
+
<input
|
|
50
|
+
type="number"
|
|
51
|
+
id="node-input-allowedErrorPercent"
|
|
52
|
+
min="0"
|
|
53
|
+
max="100"
|
|
54
|
+
step="0.1"
|
|
55
|
+
style="width:80px;"
|
|
56
|
+
/>
|
|
57
|
+
<span class="node-input-hint" style="margin-left:10px; color:#999;">%</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="form-row">
|
|
60
|
+
<label for="node-input-scaleFilePath"
|
|
61
|
+
><i class="fa fa-save"></i> Scale file path</label
|
|
62
|
+
>
|
|
63
|
+
<input
|
|
64
|
+
type="text"
|
|
65
|
+
id="node-input-scaleFilePath"
|
|
66
|
+
placeholder="/data/golden/calibration.json (container path)"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<script type="text/html" data-help-name="checkerboard-calibrate">
|
|
72
|
+
<p>
|
|
73
|
+
Measure how many millimetres a pixel covers, and check that it has not
|
|
74
|
+
drifted: photograph a printed checkerboard of known physical pitch,
|
|
75
|
+
measure the detected pixel pitch, and compare the resulting mm/px scale
|
|
76
|
+
against a previously-saved baseline. Run this once at commissioning and
|
|
77
|
+
again after camera or mechanical maintenance - not per production frame.
|
|
78
|
+
</p>
|
|
79
|
+
<p>
|
|
80
|
+
Detection is centroid/pitch-based (no sub-pixel corner refinement, no
|
|
81
|
+
lens-distortion or perspective correction) - it answers "has the
|
|
82
|
+
scale/geometry drifted since the last calibration," consistent with
|
|
83
|
+
<code>golden-compare</code>'s translation-only, fixed-rig scope.
|
|
84
|
+
</p>
|
|
85
|
+
|
|
86
|
+
<h3>Checkerboard requirements</h3>
|
|
87
|
+
<p>
|
|
88
|
+
<i>Grid size</i> counts <b>dark squares</b>, not total squares or inner
|
|
89
|
+
corners: cols = dark squares in the <b>longest</b> row, rows = number of
|
|
90
|
+
rows. Needs at least 3 rows (a 2-row step is used to measure vertical
|
|
91
|
+
pitch consistently with the horizontal same-colour spacing - see
|
|
92
|
+
<code>lib/checkerboard.js</code>).
|
|
93
|
+
</p>
|
|
94
|
+
<p>
|
|
95
|
+
A board with an <b>even</b> number of square columns holds the same
|
|
96
|
+
number of dark squares in every row: 16 columns means cols = 8. An
|
|
97
|
+
<b>odd</b> number cannot, so its rows alternate - 17 columns gives 9
|
|
98
|
+
dark squares in the rows that start dark and 8 in the rows that start
|
|
99
|
+
light, and cols = 9. Both shapes are accepted; count the longest row
|
|
100
|
+
either way, and the corner colour does not matter.
|
|
101
|
+
</p>
|
|
102
|
+
|
|
103
|
+
<h3>Input</h3>
|
|
104
|
+
<p>
|
|
105
|
+
<code>msg.payload</code> — a photo of the printed checkerboard: Buffer /
|
|
106
|
+
Uint8Array, a file path string, or an object with
|
|
107
|
+
<code>data</code>/<code>buffer</code>/<code>path</code>.
|
|
108
|
+
</p>
|
|
109
|
+
<p>
|
|
110
|
+
<code>msg.save</code> (bool) — persist the freshly detected scale as the
|
|
111
|
+
new baseline (their "Match New Scale" + "Save"). Optional per-message
|
|
112
|
+
overrides: <code>msg.targetPitchMm</code>,
|
|
113
|
+
<code>msg.checkerboardCols</code>, <code>msg.checkerboardRows</code>,
|
|
114
|
+
<code>msg.allowedErrorPercent</code>.
|
|
115
|
+
</p>
|
|
116
|
+
|
|
117
|
+
<h3>Output</h3>
|
|
118
|
+
<ul>
|
|
119
|
+
<li><code>msg.payload</code> — <code>true</code>/<code>false</code> pass</li>
|
|
120
|
+
<li>
|
|
121
|
+
<code>msg.result</code> —
|
|
122
|
+
<code
|
|
123
|
+
>{ checkerboardDetected, currentScale, detectedScale,
|
|
124
|
+
deviationPercent, bootstrap, pass, saved, pitchXPx, pitchYPx,
|
|
125
|
+
nativeWidth, nativeHeight }</code
|
|
126
|
+
>
|
|
127
|
+
(scales in mm/px; <code>bootstrap</code> is true when there's no saved
|
|
128
|
+
baseline yet to compare against - always passes, since there's
|
|
129
|
+
nothing to deviate from)
|
|
130
|
+
</li>
|
|
131
|
+
<li><code>msg.timings</code> — <code>{ totalMs }</code></li>
|
|
132
|
+
</ul>
|
|
133
|
+
|
|
134
|
+
<h3>Notes</h3>
|
|
135
|
+
<p>
|
|
136
|
+
<i>Scale file path</i> is a path <b>inside the container</b> - use
|
|
137
|
+
<code>/data</code> so the calibration baseline survives a rebuild. The
|
|
138
|
+
saved scale is <code>mmPerPixelNative</code> - measured at this
|
|
139
|
+
calibration photo's own native resolution, not any particular
|
|
140
|
+
<code>golden-compare</code> working resolution - so it stays valid
|
|
141
|
+
however <code>golden-compare</code>'s <i>Working size</i> is configured.
|
|
142
|
+
</p>
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<script type="text/javascript">
|
|
146
|
+
RED.nodes.registerType("checkerboard-calibrate", {
|
|
147
|
+
category: "vision",
|
|
148
|
+
color: "#5DADE2",
|
|
149
|
+
defaults: {
|
|
150
|
+
name: { value: "" },
|
|
151
|
+
targetPitchMm: { value: 10, validate: RED.validators.number(true) },
|
|
152
|
+
checkerboardCols: { value: 4, validate: RED.validators.number(true) },
|
|
153
|
+
checkerboardRows: { value: 6, validate: RED.validators.number(true) },
|
|
154
|
+
allowedErrorPercent: { value: 2, validate: RED.validators.number(true) },
|
|
155
|
+
scaleFilePath: { value: "" },
|
|
156
|
+
},
|
|
157
|
+
inputs: 1,
|
|
158
|
+
outputs: 1,
|
|
159
|
+
icon: "checkerboard-calibrate.svg",
|
|
160
|
+
label: function () {
|
|
161
|
+
return this.name || "checkerboard calibrate";
|
|
162
|
+
},
|
|
163
|
+
paletteLabel: function () {
|
|
164
|
+
return "checkerboard calibrate";
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
</script>
|