@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.
@@ -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>