@blokcert/node-red-contrib-plate-crop 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/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # node-red-contrib-plate-crop
2
+
3
+ Node-RED 節點:輸入車牌圖片的 **base64**,偵測並裁切車牌,輸出裁切圖的 **base64**。
4
+
5
+ 底層呼叫內附的 `yolo-plate` binary(Go + YOLO ONNX,binary 自帶 onnxruntime,目標機免安裝)。
6
+
7
+ ## 安裝
8
+
9
+ ```bash
10
+ cd ~/.node-red
11
+ npm install /path/to/yolo-plate/node-red-contrib-plate-crop
12
+ # 重啟 Node-RED
13
+ ```
14
+
15
+ 安裝後在編輯器的 **function** 類別下會出現「車牌裁切」節點。
16
+
17
+ > 模型檔(`plate.onnx`,約 272MB)**未**打包進節點,請自行放置並在節點設定填入路徑。
18
+
19
+ ## 使用
20
+
21
+ 1. 拉「車牌裁切」節點到流程。
22
+ 2. 雙擊設定 **模型路徑**(`.onnx`,必填)。
23
+ 3. 上游送入 `msg.payload = <base64 圖片字串>`(可含 `data:image/...;base64,` 前綴,支援 PNG/JPEG)。
24
+ 4. 節點輸出 `msg.payload`:
25
+ - 預設:分數最高車牌的 base64 **字串**
26
+ - 勾「全部車牌」:base64 **陣列**
27
+ - 未偵測到車牌:`null`
28
+
29
+ ### 設定項
30
+
31
+ | 項目 | 對應 CLI | 說明 |
32
+ |------|----------|------|
33
+ | 模型路徑 | `-model` | 車牌偵測 `.onnx`(必填) |
34
+ | 信心門檻 | `-conf` | 預設 0.25 |
35
+ | 外擴像素 | `-pad` | 裁切外擴,預設 0 |
36
+ | 全部車牌 | `-all` | 勾選輸出所有車牌 |
37
+ | binary 路徑 | `-lib`/`-model` 的執行檔 | 留空=自動依平台選內附 binary |
38
+
39
+ ## 支援平台
40
+
41
+ 內附 binary:`linux-x64`(一般 x86 伺服器/PC,Node-RED 部署用)、`darwin-arm64`(mac 本機開發/測試)。
42
+ 兩者皆自帶 onnxruntime 1.26,目標機免安裝。
43
+ 其他平台請自行編譯 `yolo-plate`(見上層專案),並在節點設定「binary 路徑」指定。
44
+
45
+ ## 測試
46
+
47
+ ```bash
48
+ npm test # 需要上層 plate.onnx 與一張範例圖,可用環境變數覆寫:
49
+ PLATE_MODEL=/path/plate.onnx PLATE_IMG=/path/img.jpg npm test
50
+ ```
Binary file
Binary file
package/lib/crop.js ADDED
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+
3
+ const { spawn } = require("child_process");
4
+ const path = require("path");
5
+
6
+ // 依執行平台挑選內附的 binary。
7
+ function defaultBinPath() {
8
+ const key = `${process.platform}-${process.arch}`;
9
+ const map = {
10
+ "darwin-arm64": "yolo-plate-darwin-arm64",
11
+ "linux-x64": "yolo-plate-linux-amd64",
12
+ };
13
+ const name = map[key];
14
+ if (!name) {
15
+ throw new Error(`不支援的平台 ${key},請在節點設定手動指定 binary 路徑`);
16
+ }
17
+ return path.join(__dirname, "..", "bin", name);
18
+ }
19
+
20
+ // runPlateCrop 以 stdin 傳入 base64 圖片,回傳裁切車牌的 base64 陣列(每個車牌一個)。
21
+ // opts: { binPath?, modelPath, conf?, iou?, pad?, imgsz?, all? }
22
+ function runPlateCrop(opts, base64) {
23
+ return new Promise((resolve, reject) => {
24
+ if (!opts.modelPath) {
25
+ reject(new Error("未設定模型路徑(modelPath)"));
26
+ return;
27
+ }
28
+ let bin;
29
+ try {
30
+ bin = opts.binPath || defaultBinPath();
31
+ } catch (e) {
32
+ reject(e);
33
+ return;
34
+ }
35
+
36
+ const args = ["-model", opts.modelPath, "-in", "-", "-b64"];
37
+ if (opts.conf != null) args.push("-conf", String(opts.conf));
38
+ if (opts.iou != null) args.push("-iou", String(opts.iou));
39
+ if (opts.pad != null) args.push("-pad", String(opts.pad));
40
+ if (opts.imgsz != null) args.push("-imgsz", String(opts.imgsz));
41
+ if (opts.all) args.push("-all");
42
+
43
+ const child = spawn(bin, args);
44
+ let out = "";
45
+ let err = "";
46
+ child.stdout.on("data", (d) => (out += d));
47
+ child.stderr.on("data", (d) => (err += d));
48
+ child.on("error", reject);
49
+ child.on("close", (code) => {
50
+ if (code === 0) {
51
+ const lines = out
52
+ .split("\n")
53
+ .map((s) => s.trim())
54
+ .filter(Boolean);
55
+ resolve(lines);
56
+ } else {
57
+ // 未偵測到車牌時 binary 以非 0 結束,訊息在 stderr
58
+ reject(new Error(err.trim() || `binary 結束碼 ${code}`));
59
+ }
60
+ });
61
+
62
+ child.stdin.on("error", () => {}); // 忽略 EPIPE(binary 提早結束時)
63
+ child.stdin.write(base64);
64
+ child.stdin.end();
65
+ });
66
+ }
67
+
68
+ module.exports = { runPlateCrop, defaultBinPath };
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@blokcert/node-red-contrib-plate-crop",
3
+ "version": "1.0.0",
4
+ "description": "Node-RED node:輸入車牌圖 base64,輸出裁切後車牌 base64(YOLO ONNX,自帶 onnxruntime)",
5
+ "keywords": ["node-red", "license-plate", "anpr", "yolo", "onnx"],
6
+ "license": "MIT",
7
+ "publishConfig": { "access": "public" },
8
+ "node-red": {
9
+ "nodes": {
10
+ "plate-crop": "plate-crop.js"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "test": "node --test"
15
+ },
16
+ "files": [
17
+ "plate-crop.js",
18
+ "plate-crop.html",
19
+ "lib/",
20
+ "bin/"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ }
25
+ }
@@ -0,0 +1,73 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("plate-crop", {
3
+ category: "function",
4
+ color: "#8fd694",
5
+ defaults: {
6
+ name: { value: "" },
7
+ modelPath: { value: "", required: true },
8
+ binPath: { value: "" },
9
+ conf: { value: "0.25", validate: RED.validators.number(true) },
10
+ pad: { value: "0", validate: RED.validators.number(true) },
11
+ all: { value: false },
12
+ },
13
+ inputs: 1,
14
+ outputs: 1,
15
+ icon: "font-awesome/fa-car",
16
+ label: function () {
17
+ return this.name || "車牌裁切";
18
+ },
19
+ paletteLabel: "車牌裁切",
20
+ });
21
+ </script>
22
+
23
+ <script type="text/html" data-template-name="plate-crop">
24
+ <div class="form-row">
25
+ <label for="node-input-name"><i class="fa fa-tag"></i> 名稱</label>
26
+ <input type="text" id="node-input-name" placeholder="名稱" />
27
+ </div>
28
+ <div class="form-row">
29
+ <label for="node-input-modelPath"><i class="fa fa-cube"></i> 模型路徑</label>
30
+ <input type="text" id="node-input-modelPath" placeholder="/path/to/plate.onnx" />
31
+ </div>
32
+ <div class="form-row">
33
+ <label for="node-input-conf"><i class="fa fa-percent"></i> 信心門檻</label>
34
+ <input type="text" id="node-input-conf" placeholder="0.25" />
35
+ </div>
36
+ <div class="form-row">
37
+ <label for="node-input-pad"><i class="fa fa-expand"></i> 外擴像素</label>
38
+ <input type="text" id="node-input-pad" placeholder="0" />
39
+ </div>
40
+ <div class="form-row">
41
+ <label for="node-input-all"><i class="fa fa-clone"></i> 全部車牌</label>
42
+ <input type="checkbox" id="node-input-all" style="display:inline-block; width:auto; vertical-align:top;" />
43
+ <span>勾選輸出所有車牌(payload 為陣列);否則只輸出分數最高的一個</span>
44
+ </div>
45
+ <div class="form-row">
46
+ <label for="node-input-binPath"><i class="fa fa-terminal"></i> binary 路徑</label>
47
+ <input type="text" id="node-input-binPath" placeholder="留空=自動依平台選用內附 binary" />
48
+ </div>
49
+ </script>
50
+
51
+ <script type="text/html" data-help-name="plate-crop">
52
+ <p>輸入車牌圖片的 base64,偵測並裁切出車牌,輸出裁切圖的 base64。</p>
53
+ <h3>輸入</h3>
54
+ <dl class="message-properties">
55
+ <dt>payload <span class="property-type">string</span></dt>
56
+ <dd>車牌圖片的 base64(可含 <code>data:image/...;base64,</code> 前綴)。支援 PNG / JPEG。</dd>
57
+ </dl>
58
+ <h3>輸出</h3>
59
+ <dl class="message-properties">
60
+ <dt>payload <span class="property-type">string | string[] | null</span></dt>
61
+ <dd>
62
+ 預設輸出分數最高車牌的 base64 字串;勾選「全部車牌」時輸出 base64 陣列。
63
+ 未偵測到車牌時輸出 <code>null</code>。
64
+ </dd>
65
+ </dl>
66
+ <h3>設定</h3>
67
+ <ul>
68
+ <li><b>模型路徑</b>:車牌偵測 <code>.onnx</code> 模型(必填)。</li>
69
+ <li><b>信心門檻</b>/<b>外擴像素</b>/<b>全部車牌</b>:對應 CLI 的 <code>-conf</code>/<code>-pad</code>/<code>-all</code>。</li>
70
+ <li><b>binary 路徑</b>:留空自動依平台(darwin-arm64 / linux-arm64)選用內附 binary。</li>
71
+ </ul>
72
+ <p>底層呼叫內附的 <code>yolo-plate</code> binary(自帶 onnxruntime,目標機免安裝)。</p>
73
+ </script>
package/plate-crop.js ADDED
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ const { runPlateCrop } = require("./lib/crop");
4
+
5
+ module.exports = function (RED) {
6
+ function PlateCropNode(config) {
7
+ RED.nodes.createNode(this, config);
8
+ const node = this;
9
+
10
+ node.modelPath = config.modelPath;
11
+ node.binPath = config.binPath || undefined;
12
+ node.conf = config.conf !== "" ? parseFloat(config.conf) : undefined;
13
+ node.pad = config.pad !== "" ? parseInt(config.pad, 10) : undefined;
14
+ node.all = !!config.all;
15
+
16
+ node.on("input", function (msg, send, done) {
17
+ // 相容舊版 Node-RED(無 send/done)
18
+ send = send || function () { node.send.apply(node, arguments); };
19
+ done = done || function (err) { if (err) node.error(err, msg); };
20
+
21
+ const payload = msg.payload;
22
+ if (typeof payload !== "string" || payload.length === 0) {
23
+ node.status({ fill: "red", shape: "ring", text: "payload 非 base64" });
24
+ done(new Error("msg.payload 必須是 base64 圖片字串"));
25
+ return;
26
+ }
27
+
28
+ node.status({ fill: "blue", shape: "dot", text: "偵測中" });
29
+ runPlateCrop(
30
+ {
31
+ binPath: node.binPath,
32
+ modelPath: node.modelPath,
33
+ conf: node.conf,
34
+ pad: node.pad,
35
+ all: node.all,
36
+ },
37
+ payload
38
+ )
39
+ .then((lines) => {
40
+ // 預設(best)輸出單一 base64 字串;-all 輸出 base64 陣列
41
+ msg.payload = node.all ? lines : lines[0] || null;
42
+ node.status({ fill: "green", shape: "dot", text: `${lines.length} 車牌` });
43
+ send(msg);
44
+ done();
45
+ })
46
+ .catch((e) => {
47
+ const noPlate = /未偵測到車牌/.test(e.message);
48
+ node.status({
49
+ fill: noPlate ? "yellow" : "red",
50
+ shape: "ring",
51
+ text: noPlate ? "無車牌" : "錯誤",
52
+ });
53
+ if (noPlate) {
54
+ // 無車牌不算錯誤:輸出 null,讓下游可判斷
55
+ msg.payload = null;
56
+ send(msg);
57
+ done();
58
+ } else {
59
+ done(e);
60
+ }
61
+ });
62
+ });
63
+ }
64
+
65
+ RED.nodes.registerType("plate-crop", PlateCropNode);
66
+ };