UNPKG

node-red-node-rdk-tools

Version:

配合RDK硬件及TROS使用的Node-RED功能包(Node-RED nodes for using TROS on a RDK hardware and TROS)

560 lines (558 loc) 21.4 kB
[ { "id": "mobilesam_tab", "type": "tab", "label": "MobileSAM 分割一切", "disabled": false, "info": "# MobileSAM 分割一切\n\n## 功能介绍\n\nMobileSAM分割算法,支持USB摄像头拍照、USB/MIPI实时视频流和本地图片回灌三种模式。MobileSAM依赖检测框输入进行分割,无需指定目标的类别信息,仅需提供框。\n\n## 使用模式\n\n### 📷 拍照模式\n1. 点击\"📷 USB摄像头拍照\"按钮\n2. 系统会自动对照片进行MobileSAM分割\n3. 分割结果会直接显示在Node-RED编辑器中\n\n### 🎥 USB视频流模式(实时)\n1. **启动视频流**:点击\"🎥 USB摄像头视频流\"按钮\n2. **查看可视化**:点击\"打开可视化页面\"在浏览器中查看实时分割结果\n3. **停止服务**:完成后点击\"停止分割服务\"\n\n### 📹 MIPI视频流模式(实时)\n1. **启动视频流**:点击\"📹 MIPI摄像头视频流\"按钮\n2. **查看可视化**:点击\"打开可视化页面\"在浏览器中查看实时分割结果\n3. **停止服务**:完成后点击\"停止分割服务\"\n\n### 🖼️ 回灌模式\n1. 点击\"启动本地图片分割\"按钮\n2. 系统会对指定图片进行分割\n3. 分割结果会直接显示在Node-RED编辑器中\n\n## 支持平台\n\n- RDK X5\n- RDK X5 Module\n\n## 算法信息\n\n| 模型 | 平台 | 输入尺寸 | 推理帧率(fps) |\n|------|------|---------|-------------|\n| mobilesam | X5 | 1×3×384×384 | 6.6 |\n\n## 核心通信配置\n\n### WebSocket视频流\n- **视频流地址**: `http://{host}:8000`\n- **图像Topic**: `/image` (JPEG编码的图像流)\n- **性能**: 实时检测,延迟低\n\n## 注意事项\n\n- 分割结果会自动保存到 `/tmp/mobilesam/` 目录\n- MobileSAM依赖检测框输入,本示例使用固定框(图片中央)进行分割\n- 视频流模式需要先启动视频流,再打开可视化页面\n- 分割过程需要几秒钟时间,请耐心等待\n- 需要从tros安装路径复制配置文件:`cp -r /opt/tros/humble/lib/mono_mobilesam/config/ .`", "env": [] }, { "id": "comment_photo_section", "type": "comment", "z": "mobilesam_tab", "name": "📷 拍照分割流程", "info": "点击拍照按钮,系统会自动进行分割并显示结果", "x": 150, "y": 40, "wires": [] }, { "id": "inject_take_photo", "type": "inject", "z": "mobilesam_tab", "name": "📷 USB摄像头拍照", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 140, "y": 100, "wires": [ [ "rdk_camera_take_photo" ] ] }, { "id": "rdk_camera_take_photo", "type": "rdk-camera takephoto", "z": "mobilesam_tab", "cameratype": "1", "filemode": "2", "filename": "photo.jpg", "filedefpath": "0", "filepath": "/tmp/mobilesam", "fileformat": "jpeg", "resolution": "2", "rotation": "0", "fliph": "0", "flipv": "0", "brightness": "50", "contrast": "0", "sharpness": "0", "quality": "80", "imageeffect": "none", "exposuremode": "auto", "iso": "0", "agcwait": "1.0", "led": "0", "awb": "auto", "name": "拍照", "x": 350, "y": 100, "wires": [ [ "debug_camera_output", "function_prepare_segmentation" ] ] }, { "id": "debug_camera_output", "type": "debug", "z": "mobilesam_tab", "name": "调试:拍照节点输出", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "true", "targetType": "full", "x": 550, "y": 60, "wires": [] }, { "id": "debug_prepare_seg", "type": "debug", "z": "mobilesam_tab", "name": "调试:准备分割", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "true", "targetType": "full", "x": 750, "y": 60, "wires": [] }, { "id": "function_prepare_segmentation", "type": "function", "z": "mobilesam_tab", "name": "准备分割", "func": "// 确保目录存在并获取照片路径\n// fs 和 path 已在 libs 中声明,直接使用\nconst saveDir = '/tmp/mobilesam';\n\n// 确保目录存在\nif (!fs.existsSync(saveDir)) {\n fs.mkdirSync(saveDir, { recursive: true });\n}\n\n// rdk-camera takephoto节点会在msg.payload中返回实际保存的文件路径\n// 例如: \"/tmp/mobilesam/image-20251121-154610.jpg\"\nlet photoPath = null;\nlet photoFileName = null;\n\n// 优先使用msg.payload中的文件路径(字符串)\nif (msg.payload && typeof msg.payload === 'string' && msg.payload.trim()) {\n photoPath = msg.payload.trim();\n \n // 检查文件是否存在\n if (fs.existsSync(photoPath)) {\n photoFileName = path.basename(photoPath);\n node.status({ fill: 'green', shape: 'dot', text: '照片: ' + photoFileName });\n \n // 文件存在,直接使用\n msg.photoPath = photoPath;\n msg.photoFileName = photoFileName;\n msg.photoDir = saveDir;\n \n return msg;\n } else {\n // 文件不存在,等待一下(最多2.5秒)\n const self = node;\n let retryCount = 0;\n const maxRetries = 5;\n \n const checkFile = function() {\n if (fs.existsSync(photoPath)) {\n const newMsg = {\n photoPath: photoPath,\n photoFileName: path.basename(photoPath),\n photoDir: saveDir\n };\n self.status({ fill: 'green', shape: 'dot', text: '照片已找到' });\n self.send(newMsg);\n } else if (retryCount < maxRetries) {\n retryCount++;\n setTimeout(checkFile, 500);\n } else {\n self.error('照片文件不存在: ' + photoPath);\n self.status({ fill: 'red', shape: 'dot', text: '照片不存在' });\n }\n };\n \n setTimeout(checkFile, 500);\n return null;\n }\n} else if (msg.payload && Buffer.isBuffer(msg.payload)) {\n // 如果payload是Buffer,保存到指定目录\n photoFileName = 'photo.jpg';\n photoPath = path.join(saveDir, photoFileName);\n try {\n fs.writeFileSync(photoPath, msg.payload);\n node.status({ fill: 'green', shape: 'dot', text: '照片已保存' });\n \n msg.photoPath = photoPath;\n msg.photoFileName = photoFileName;\n msg.photoDir = saveDir;\n \n return msg;\n } catch (err) {\n node.error('保存照片失败: ' + err.message);\n return null;\n }\n} else {\n // 如果都没有,尝试使用默认路径\n photoFileName = 'photo.jpg';\n photoPath = path.join(saveDir, photoFileName);\n if (!fs.existsSync(photoPath)) {\n node.error('无法找到照片文件,请检查拍照节点配置');\n node.status({ fill: 'red', shape: 'dot', text: '照片不存在' });\n return null;\n }\n node.status({ fill: 'green', shape: 'dot', text: '使用默认路径' });\n \n msg.photoPath = photoPath;\n msg.photoFileName = photoFileName;\n msg.photoDir = saveDir;\n \n return msg;\n}", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [ { "var": "fs", "module": "fs" }, { "var": "path", "module": "path" } ], "x": 550, "y": 100, "wires": [ [ "debug_prepare_seg", "function_build_seg_command" ] ] }, { "id": "function_build_seg_command", "type": "function", "z": "mobilesam_tab", "name": "构建分割命令", "func": "// 构建分割命令\n// MobileSAM使用 mono_mobilesam sam.launch.py\n// 回灌模式使用环境变量 CAM_TYPE=fb\n// 官方示例:export CAM_TYPE=fb && ros2 launch mono_mobilesam sam.launch.py\n// 注意:需要先复制配置文件\n// 修复:添加超时机制,防止分割服务一直运行导致卡住\nconst photoDir = path.normalize(msg.photoDir || '/tmp/mobilesam');\nconst photoFileName = msg.photoFileName || 'photo.jpg';\nconst photoPath = path.normalize(msg.photoPath || path.join(photoDir, photoFileName));\n\n// 规范化目标路径(用于比较)\nconst targetPath = path.normalize(path.join(photoDir, photoFileName));\n\n// 记录分割开始时间,用于后续只显示本次分割的结果\nif (typeof global.segmentationStartTime === 'undefined') {\n global.segmentationStartTime = Date.now();\n} else {\n global.segmentationStartTime = Date.now(); // 每次分割都更新开始时间\n}\n\n// 确保图片文件在工作目录下(如果不在,需要复制或移动)\n// 构建完整的launch命令\n// 工作目录设置为 photoDir,图片文件应该在 photoDir 下\n// publish_image_source 参数使用相对路径(相对于工作目录)\n// 如果文件不在工作目录,先复制到工作目录\n// 添加超时机制(60秒),确保分割服务不会一直运行\n// 参考 EdgeSAM 的实现方式,使用 timeout 命令\n// 修复:使用规范化后的路径进行比较,并正确转义路径中的特殊字符\nconst needCopy = photoPath !== targetPath;\nconst copyCmd = needCopy ? 'echo \"复制图片文件到工作目录...\" && cp \"' + photoPath.replace(/\"/g, '\\\"') + '\" \"' + targetPath.replace(/\"/g, '\\\"') + '\" 2>&1 && echo \"图片已复制\" || echo \"复制失败或文件已存在\"; ' : '';\n\nmsg.payload = 'cd \"' + photoDir.replace(/\"/g, '\\\"') + '\" && source /opt/tros/humble/setup.bash && cp -r /opt/tros/humble/lib/mono_mobilesam/config/ . 2>/dev/null || true && echo \"=== 开始MobileSAM分割 ===\" && echo \"工作目录: $(pwd)\" && echo \"原始图片路径: ' + photoPath + '\" && echo \"目标文件名: ' + photoFileName + '\" && ' + copyCmd + 'echo \"检查图片是否存在...\" && ls -lh \"' + photoFileName.replace(/\"/g, '\\\"') + '\" 2>&1 && echo \"启动分割(60秒超时)...\" && export CAM_TYPE=fb && timeout 60 ros2 launch mono_mobilesam sam.launch.py publish_image_source:=' + photoFileName + ' publish_image_format:=jpg 2>&1 || echo \"分割完成或超时(60秒)\"';\n\nnode.status({ fill: 'blue', shape: 'dot', text: '命令已构建(含超时)' });\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [ { "var": "path", "module": "path" } ], "x": 750, "y": 100, "wires": [ [ "debug_build_command", "exec_start_segmentation" ] ] }, { "id": "debug_build_command", "type": "debug", "z": "mobilesam_tab", "name": "调试:构建的命令", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 950, "y": 60, "wires": [] }, { "id": "exec_start_segmentation", "type": "exec", "z": "mobilesam_tab", "name": "启动分割", "command": "", "addpay": true, "append": "", "useSpawn": true, "timer": "0", "oldrc": false, "x": 950, "y": 100, "wires": [ [ "debug_seg_output" ], [ "debug_seg_error" ], [] ] }, { "id": "debug_seg_output", "type": "debug", "z": "mobilesam_tab", "name": "分割输出", "active": true, "tosidebar": true, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "payload", "statusType": "auto", "x": 1180, "y": 80, "wires": [] }, { "id": "debug_seg_error", "type": "debug", "z": "mobilesam_tab", "name": "分割错误", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 1180, "y": 120, "wires": [] }, { "id": "comment_video_section", "type": "comment", "z": "mobilesam_tab", "name": "🎥 USB视频流分割流程", "info": "启动USB摄像头实时视频流,进行连续分割", "x": 150, "y": 360, "wires": [] }, { "id": "inject_usb_video", "type": "inject", "z": "mobilesam_tab", "name": "🎥 USB摄像头视频流", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "usb_video", "payload": "start", "payloadType": "str", "x": 140, "y": 420, "wires": [ [ "exec_usb_video" ] ] }, { "id": "exec_usb_video", "type": "exec", "z": "mobilesam_tab", "name": "USB视频流启动器", "command": "source /opt/tros/humble/setup.bash && cp -r /opt/tros/humble/lib/mono_mobilesam/config/ . 2>/dev/null || true && export CAM_TYPE=usb && ros2 launch mono_mobilesam sam.launch.py", "addpay": false, "append": "", "useSpawn": true, "timer": "120", "oldrc": false, "x": 380, "y": 420, "wires": [ [ "debug_usb_video_output" ], [ "debug_usb_video_error" ], [] ] }, { "id": "debug_usb_video_output", "type": "debug", "z": "mobilesam_tab", "name": "USB视频流输出", "active": true, "tosidebar": true, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "payload", "statusType": "auto", "x": 650, "y": 400, "wires": [] }, { "id": "debug_usb_video_error", "type": "debug", "z": "mobilesam_tab", "name": "USB视频流错误", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 650, "y": 440, "wires": [] }, { "id": "comment_mipi_video_section", "type": "comment", "z": "mobilesam_tab", "name": "📹 MIPI视频流分割流程", "info": "启动MIPI摄像头实时视频流,进行连续分割", "x": 150, "y": 500, "wires": [] }, { "id": "inject_mipi_video", "type": "inject", "z": "mobilesam_tab", "name": "📹 MIPI摄像头视频流", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "mipi_video", "payload": "start", "payloadType": "str", "x": 140, "y": 560, "wires": [ [ "exec_mipi_video" ] ] }, { "id": "exec_mipi_video", "type": "exec", "z": "mobilesam_tab", "name": "MIPI视频流启动器", "command": "source /opt/tros/humble/setup.bash && cp -r /opt/tros/humble/lib/mono_mobilesam/config/ . 2>/dev/null || true && export CAM_TYPE=mipi && ros2 launch mono_mobilesam sam.launch.py", "addpay": false, "append": "", "useSpawn": true, "timer": "120", "oldrc": false, "x": 380, "y": 560, "wires": [ [ "debug_mipi_video_output" ], [ "debug_mipi_video_error" ], [] ] }, { "id": "debug_mipi_video_output", "type": "debug", "z": "mobilesam_tab", "name": "MIPI视频流输出", "active": true, "tosidebar": true, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "payload", "statusType": "auto", "x": 650, "y": 540, "wires": [] }, { "id": "debug_mipi_video_error", "type": "debug", "z": "mobilesam_tab", "name": "MIPI视频流错误", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 650, "y": 580, "wires": [] }, { "id": "comment_browser_section", "type": "comment", "z": "mobilesam_tab", "name": "2️⃣ 查看检测结果", "info": "在浏览器中查看实时检测画面", "x": 200, "y": 640, "wires": [] }, { "id": "inject_browser", "type": "inject", "z": "mobilesam_tab", "name": "打开可视化页面", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "http://{host}:8000", "payloadType": "str", "x": 140, "y": 680, "wires": [ [ "openurl_browser" ] ] }, { "id": "openurl_browser", "type": "rdk-tools openurl", "z": "mobilesam_tab", "name": "", "x": 380, "y": 680, "wires": [] }, { "id": "comment_control_section", "type": "comment", "z": "mobilesam_tab", "name": "🎮 控制功能", "info": "停止分割服务", "x": 150, "y": 720, "wires": [] }, { "id": "inject_stop", "type": "inject", "z": "mobilesam_tab", "name": "停止分割服务", "props": [ { "p": "payload" } ], "repeat": "", "crontab": "", "once": false, "topic": "", "payload": "stop", "payloadType": "str", "x": 140, "y": 780, "wires": [ [ "exec_stop_all" ] ] }, { "id": "exec_stop_all", "type": "exec", "z": "mobilesam_tab", "name": "停止服务", "command": "sudo pkill -9 -f hobot_usb_cam; sudo pkill -9 -f mono_mobilesam; sudo pkill -9 -f hobot_codec_republish; sudo pkill -9 -f websocket; sudo pkill -9 -f python3; sudo pkill -9 -f 'ros2 launch'; sudo pkill -9 -f 'ros2 run'; sleep 2; sudo rm -rf /dev/shm/*; ros2 daemon stop 2>/dev/null; sleep 1; echo '✓ 服务已彻底停止,资源已释放'", "addpay": false, "append": "", "useSpawn": false, "timer": "10", "oldrc": false, "x": 350, "y": 780, "wires": [ [ "debug_stop_info" ], [], [] ] }, { "id": "debug_stop_info", "type": "debug", "z": "mobilesam_tab", "name": "停止状态", "active": true, "tosidebar": true, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "payload", "statusType": "auto", "x": 550, "y": 780, "wires": [] }, { "id": "global_config_mobilesam", "type": "global-config", "env": [], "modules": { "node-red-node-rdk-camera": "0.0.17", "node-red-contrib-image-tools": "2.1.1" } } ]