├── README.md ├── __init__.py ├── __pycache__ ├── __init__.cpython-311.pyc └── nodes.cpython-311.pyc ├── js ├── fabric.js └── openPoseEditorPlus.js ├── nodes.py └── pyproject.toml /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI-Openpose-Editor-Plus 2 | It is expected to add the functions of background reference and imported poses on the basis of editing character actions, but it is currently busy and unsure when it will be done. 3 | ![image](https://github.com/whmc76/ComfyUI-Openpose-Editor-Plus/assets/129386342/993893a1-c044-49fc-bdef-070d71bc6ad2) 4 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import filecmp 3 | import shutil 4 | 5 | import __main__ 6 | 7 | from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS 8 | 9 | __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] 10 | 11 | 12 | def update_javascript(): 13 | extensions_folder = os.path.join(os.path.dirname(os.path.realpath(__main__.__file__)), 14 | "web" + os.sep + "extensions" + os.sep + "ComfyUI-OpenPose-Editor-Plus") 15 | javascript_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "js") 16 | 17 | if not os.path.exists(extensions_folder): 18 | print("Creating frontend extension folder: " + extensions_folder) 19 | os.mkdir(extensions_folder) 20 | 21 | result = filecmp.dircmp(javascript_folder, extensions_folder) 22 | 23 | if result.left_only or result.diff_files: 24 | print('Update to javascripts files detected') 25 | file_list = list(result.left_only) 26 | file_list.extend(x for x in result.diff_files if x not in file_list) 27 | 28 | for file in file_list: 29 | print(f'Copying {file} to extensions folder') 30 | src_file = os.path.join(javascript_folder, file) 31 | dst_file = os.path.join(extensions_folder, file) 32 | if os.path.exists(dst_file): 33 | os.remove(dst_file) 34 | shutil.copy(src_file, dst_file) 35 | 36 | 37 | update_javascript() 38 | 39 | print('\033[34mOpenpose Editor Plus: \033[92mLoaded\033[0m') 40 | -------------------------------------------------------------------------------- /__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whmc76/ComfyUI-Openpose-Editor-Plus/3edd484c4b4b3cf10b8e8245c579823898da25c4/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /__pycache__/nodes.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whmc76/ComfyUI-Openpose-Editor-Plus/3edd484c4b4b3cf10b8e8245c579823898da25c4/__pycache__/nodes.cpython-311.pyc -------------------------------------------------------------------------------- /js/openPoseEditorPlus.js: -------------------------------------------------------------------------------- 1 | import { app } from "../../../scripts/app.js"; 2 | import { fabric } from "./fabric.js"; 3 | 4 | fabric.Object.prototype.transparentCorners = false; 5 | fabric.Object.prototype.cornerColor = "#108ce6"; 6 | fabric.Object.prototype.borderColor = "#108ce6"; 7 | fabric.Object.prototype.cornerSize = 10; 8 | 9 | let connect_keypoints = [ 10 | [0, 1], 11 | [1, 2], 12 | [2, 3], 13 | [3, 4], 14 | [1, 5], 15 | [5, 6], 16 | [6, 7], 17 | [1, 8], 18 | [8, 9], 19 | [9, 10], 20 | [1, 11], 21 | [11, 12], 22 | [12, 13], 23 | [0, 14], 24 | [14, 16], 25 | [0, 15], 26 | [15, 17], 27 | ]; 28 | 29 | let connect_color = [ 30 | [0, 0, 255], 31 | [255, 0, 0], 32 | [255, 170, 0], 33 | [255, 255, 0], 34 | [255, 85, 0], 35 | [170, 255, 0], 36 | [85, 255, 0], 37 | [0, 255, 0], 38 | [0, 255, 85], 39 | [0, 255, 170], 40 | [0, 255, 255], 41 | [0, 170, 255], 42 | [0, 85, 255], 43 | [85, 0, 255], 44 | [170, 0, 255], 45 | [255, 0, 255], 46 | [255, 0, 170], 47 | [255, 0, 85], 48 | ]; 49 | 50 | const default_keypoints = [ 51 | [241, 77], 52 | [241, 120], 53 | [191, 118], 54 | [177, 183], 55 | [163, 252], 56 | [298, 118], 57 | [317, 182], 58 | [332, 245], 59 | [225, 241], 60 | [213, 359], 61 | [215, 454], 62 | [270, 240], 63 | [282, 360], 64 | [286, 456], 65 | [232, 59], 66 | [253, 60], 67 | [225, 70], 68 | [260, 72], 69 | ]; 70 | 71 | class OpenPose { 72 | constructor(node, canvasElement) { 73 | this.lockMode = false; 74 | this.visibleEyes = true; 75 | this.flipped = false; 76 | this.node = node; 77 | this.undo_history = LS_Poses[node.name].undo_history || []; 78 | this.redo_history = LS_Poses[node.name].redo_history || []; 79 | this.history_change = false; 80 | this.canvas = this.initCanvas(canvasElement); 81 | this.image = node.widgets.find((w) => w.name === "image"); 82 | // 创建用于选择图片的input元素 83 | this.backgroundInput = document.createElement("input"); 84 | this.backgroundInput.type = "file"; 85 | this.backgroundInput.accept = "image/*"; 86 | this.backgroundInput.style.display = "none"; 87 | this.backgroundInput.addEventListener("change", this.onLoadBackground.bind(this)); 88 | document.body.appendChild(this.backgroundInput); 89 | 90 | } 91 | 92 | // 创建更换背景的按钮 93 | //referenceImage() { 94 | // button.addEventListener("click", () => this.backgroundInput.click()); 95 | //} 96 | 97 | // 处理背景图片的加载 98 | onLoadBackground(e) { 99 | const file = this.backgroundInput.files[0]; 100 | const url = URL.createObjectURL(file); 101 | this.setBackgroundImage(url); 102 | } 103 | 104 | // 设置背景图片 105 | setBackgroundImage(url) { 106 | fabric.Image.fromURL(url, (img) => { 107 | img.set({ 108 | originX: 'left', 109 | originY: 'top', 110 | opacity: 0.5 111 | }); 112 | /* 113 | var width = img.width; // 图片的宽度 114 | var height = img.height; // 图片的高度 115 | var minSideLength; // 最小边长 116 | 117 | if (width < height) { 118 | // 宽度小于高度,所以最短边是宽度 119 | minSideLength = width; 120 | img.scaleX = canvas.width / img.width; 121 | img.scaleY = canvas.width / img.width; 122 | } else { 123 | // 宽度大于或等于高度,所以最短边是高度或两者相等 124 | minSideLength = height; 125 | img.scaleX = this.canvas.height / img.height; 126 | img.scaleY = this.canvas.height / img.height; 127 | } 128 | */ 129 | this.canvas.setWidth(img.width); 130 | this.canvas.setHeight(img.height); 131 | 132 | this.canvas.setBackgroundImage(img, this.canvas.renderAll.bind(this.canvas)); 133 | }); 134 | } 135 | 136 | setPose(keypoints) { 137 | this.canvas.clear(); 138 | 139 | this.canvas.backgroundColor = "#000"; 140 | 141 | const res = []; 142 | for (let i = 0; i < keypoints.length; i += 18) { 143 | const chunk = keypoints.slice(i, i + 18); 144 | res.push(chunk); 145 | } 146 | 147 | for (let item of res) { 148 | this.addPose(item); 149 | this.canvas.discardActiveObject(); 150 | } 151 | } 152 | 153 | addPose(keypoints = undefined) { 154 | if (keypoints === undefined) { 155 | keypoints = default_keypoints; 156 | } 157 | 158 | const group = new fabric.Group(); 159 | 160 | const makeCircle = ( 161 | color, 162 | left, 163 | top, 164 | line1, 165 | line2, 166 | line3, 167 | line4, 168 | line5 169 | ) => { 170 | let c = new fabric.Circle({ 171 | left: left, 172 | top: top, 173 | strokeWidth: 1, 174 | radius: 5, 175 | fill: color, 176 | stroke: color, 177 | }); 178 | 179 | c.hasControls = c.hasBorders = false; 180 | c.line1 = line1; 181 | c.line2 = line2; 182 | c.line3 = line3; 183 | c.line4 = line4; 184 | c.line5 = line5; 185 | 186 | return c; 187 | }; 188 | 189 | const makeLine = (coords, color) => { 190 | return new fabric.Line(coords, { 191 | fill: color, 192 | stroke: color, 193 | strokeWidth: 10, 194 | selectable: false, 195 | evented: false, 196 | }); 197 | }; 198 | 199 | const lines = []; 200 | const circles = []; 201 | 202 | for (let i = 0; i < connect_keypoints.length; i++) { 203 | // 接続されるidxを指定 [0, 1]なら0と1つなぐ 204 | const item = connect_keypoints[i]; 205 | const line = makeLine( 206 | keypoints[item[0]].concat(keypoints[item[1]]), 207 | `rgba(${connect_color[i].join(", ")}, 0.7)` 208 | ); 209 | lines.push(line); 210 | this.canvas.add(line); 211 | } 212 | 213 | for (let i = 0; i < keypoints.length; i++) { 214 | let list = []; 215 | 216 | connect_keypoints.filter((item, idx) => { 217 | if (item.includes(i)) { 218 | list.push(lines[idx]); 219 | return idx; 220 | } 221 | }); 222 | const circle = makeCircle( 223 | `rgb(${connect_color[i].join(", ")})`, 224 | keypoints[i][0], 225 | keypoints[i][1], 226 | ...list 227 | ); 228 | circle["id"] = i; 229 | circles.push(circle); 230 | group.addWithUpdate(circle); 231 | } 232 | 233 | this.canvas.discardActiveObject(); 234 | this.canvas.setActiveObject(group); 235 | this.canvas.add(group); 236 | group.toActiveSelection(); 237 | this.canvas.requestRenderAll(); 238 | } 239 | 240 | initCanvas() { 241 | this.canvas = new fabric.Canvas(this.canvas, { 242 | backgroundColor: "#000", 243 | preserveObjectStacking: true, 244 | }); 245 | 246 | const updateLines = (target) => { 247 | if ("_objects" in target) { 248 | const flipX = target.flipX ? -1 : 1; 249 | const flipY = target.flipY ? -1 : 1; 250 | this.flipped = flipX * flipY === -1; 251 | const showEyes = this.flipped ? !this.visibleEyes : this.visibleEyes; 252 | 253 | if (target.angle === 0) { 254 | const rtop = target.top; 255 | const rleft = target.left; 256 | for (const item of target._objects) { 257 | let p = item; 258 | p.scaleX = 1; 259 | p.scaleY = 1; 260 | const top = 261 | rtop + 262 | p.top * target.scaleY * flipY + 263 | (target.height * target.scaleY) / 2; 264 | const left = 265 | rleft + 266 | p.left * target.scaleX * flipX + 267 | (target.width * target.scaleX) / 2; 268 | p["_top"] = top; 269 | p["_left"] = left; 270 | if (p["id"] === 0) { 271 | p.line1 && p.line1.set({ x1: left, y1: top }); 272 | } else { 273 | p.line1 && p.line1.set({ x2: left, y2: top }); 274 | } 275 | if (p["id"] === 14 || p["id"] === 15) { 276 | p.radius = showEyes ? 5 : 0; 277 | if (p.line1) p.line1.strokeWidth = showEyes ? 10 : 0; 278 | if (p.line2) p.line2.strokeWidth = showEyes ? 10 : 0; 279 | } 280 | p.line2 && p.line2.set({ x1: left, y1: top }); 281 | p.line3 && p.line3.set({ x1: left, y1: top }); 282 | p.line4 && p.line4.set({ x1: left, y1: top }); 283 | p.line5 && p.line5.set({ x1: left, y1: top }); 284 | } 285 | } else { 286 | const aCoords = target.aCoords; 287 | const center = { 288 | x: (aCoords.tl.x + aCoords.br.x) / 2, 289 | y: (aCoords.tl.y + aCoords.br.y) / 2, 290 | }; 291 | const rad = (target.angle * Math.PI) / 180; 292 | const sin = Math.sin(rad); 293 | const cos = Math.cos(rad); 294 | 295 | for (const item of target._objects) { 296 | let p = item; 297 | const p_top = p.top * target.scaleY * flipY; 298 | const p_left = p.left * target.scaleX * flipX; 299 | const left = center.x + p_left * cos - p_top * sin; 300 | const top = center.y + p_left * sin + p_top * cos; 301 | p["_top"] = top; 302 | p["_left"] = left; 303 | if (p["id"] === 0) { 304 | p.line1 && p.line1.set({ x1: left, y1: top }); 305 | } else { 306 | p.line1 && p.line1.set({ x2: left, y2: top }); 307 | } 308 | if (p["id"] === 14 || p["id"] === 15) { 309 | p.radius = showEyes ? 5 : 0.3; 310 | if (p.line1) p.line1.strokeWidth = showEyes ? 10 : 0; 311 | if (p.line2) p.line2.strokeWidth = showEyes ? 10 : 0; 312 | } 313 | p.line2 && p.line2.set({ x1: left, y1: top }); 314 | p.line3 && p.line3.set({ x1: left, y1: top }); 315 | p.line4 && p.line4.set({ x1: left, y1: top }); 316 | p.line5 && p.line5.set({ x1: left, y1: top }); 317 | } 318 | } 319 | } else { 320 | var p = target; 321 | if (p["id"] === 0) { 322 | p.line1 && p.line1.set({ x1: p.left, y1: p.top }); 323 | } else { 324 | p.line1 && p.line1.set({ x2: p.left, y2: p.top }); 325 | } 326 | p.line2 && p.line2.set({ x1: p.left, y1: p.top }); 327 | p.line3 && p.line3.set({ x1: p.left, y1: p.top }); 328 | p.line4 && p.line4.set({ x1: p.left, y1: p.top }); 329 | p.line5 && p.line5.set({ x1: p.left, y1: p.top }); 330 | } 331 | this.canvas.renderAll(); 332 | }; 333 | 334 | this.canvas.on("object:moving", (e) => { 335 | updateLines(e.target); 336 | }); 337 | 338 | this.canvas.on("object:scaling", (e) => { 339 | updateLines(e.target); 340 | this.canvas.renderAll(); 341 | }); 342 | 343 | this.canvas.on("object:rotating", (e) => { 344 | updateLines(e.target); 345 | this.canvas.renderAll(); 346 | }); 347 | 348 | this.canvas.on("object:modified", () => { 349 | if ( 350 | this.lockMode || 351 | this.canvas.getActiveObject().type == "activeSelection" 352 | ) 353 | return; 354 | this.undo_history.push(this.getJSON()); 355 | this.redo_history.length = 0; 356 | this.history_change = true; 357 | this.uploadPoseFile(this.node.name); 358 | }); 359 | 360 | if (!LS_Poses[this.node.name].undo_history.length) { 361 | this.setPose(default_keypoints); 362 | this.undo_history.push(this.getJSON()); 363 | } 364 | return this.canvas; 365 | } 366 | 367 | undo() { 368 | if (this.undo_history.length > 0) { 369 | this.lockMode = true; 370 | if (this.undo_history.length > 1) 371 | this.redo_history.push(this.undo_history.pop()); 372 | 373 | const content = this.undo_history[this.undo_history.length - 1]; 374 | this.loadPreset(content); 375 | this.canvas.renderAll(); 376 | this.lockMode = false; 377 | this.history_change = true; 378 | this.uploadPoseFile(this.node.name); 379 | } 380 | } 381 | 382 | redo() { 383 | if (this.redo_history.length > 0) { 384 | this.lockMode = true; 385 | const content = this.redo_history.pop(); 386 | this.undo_history.push(content); 387 | this.loadPreset(content); 388 | this.canvas.renderAll(); 389 | this.lockMode = false; 390 | this.history_change = true; 391 | this.uploadPoseFile(this.node.name); 392 | } 393 | } 394 | 395 | resetCanvas() { 396 | this.canvas.clear(); 397 | this.canvas.backgroundColor = "#000"; 398 | this.addPose(); 399 | } 400 | 401 | updateHistoryData() { 402 | if (this.history_change) { 403 | LS_Poses[this.node.name].undo_history = this.undo_history; 404 | LS_Poses[this.node.name].redo_history = this.redo_history; 405 | LS_Save(); 406 | this.history_change = false; 407 | } 408 | } 409 | 410 | uploadPoseFile(fileName) { 411 | // Upload pose to temp folder ComfyUI 412 | 413 | const uploadFile = async (blobFile) => { 414 | try { 415 | const resp = await fetch("/upload/image", { 416 | method: "POST", 417 | body: blobFile, 418 | }); 419 | 420 | if (resp.status === 200) { 421 | const data = await resp.json(); 422 | 423 | if (!this.image.options.values.includes(data.name)) { 424 | this.image.options.values.push(data.name); 425 | } 426 | 427 | this.image.value = data.name; 428 | this.updateHistoryData(); 429 | } else { 430 | alert(resp.status + " - " + resp.statusText); 431 | } 432 | } catch (error) { 433 | console.error(error); 434 | } 435 | }; 436 | 437 | this.canvas.lowerCanvasEl.toBlob(function (blob) { 438 | let formData = new FormData(); 439 | formData.append("image", blob, fileName); 440 | formData.append("overwrite", "true"); 441 | formData.append("type", "temp"); 442 | uploadFile(formData); 443 | }, "image/png"); 444 | // - end 445 | 446 | const callb = this.node.callback, 447 | self = this; 448 | this.image.callback = function () { 449 | this.image.value = self.node.name; 450 | if (callb) { 451 | return callb.apply(this, arguments); 452 | } 453 | }; 454 | } 455 | 456 | getJSON() { 457 | const json = { 458 | keypoints: this.canvas 459 | .getObjects() 460 | .filter((item) => { 461 | if (item.type === "circle") return item; 462 | }) 463 | .map((item) => { 464 | return [Math.round(item.left), Math.round(item.top)]; 465 | }), 466 | }; 467 | 468 | return json; 469 | } 470 | 471 | loadPreset(json) { 472 | try { 473 | if (json["keypoints"].length % 18 === 0) { 474 | this.setPose(json["keypoints"]); 475 | } else { 476 | throw new Error("keypoints is invalid"); 477 | } 478 | } catch (e) { 479 | console.error(e); 480 | } 481 | } 482 | } 483 | 484 | // Create OpenPose widget 485 | function createOpenPose(node, inputName, inputData, app) { 486 | node.name = inputName; 487 | const widget = { 488 | type: "openpose", 489 | name: `w${inputName}`, 490 | 491 | draw: function (ctx, _, widgetWidth, y, widgetHeight) { 492 | const margin = 10, 493 | visible = app.canvas.ds.scale > 0.5 && this.type === "openpose", 494 | clientRectBound = ctx.canvas.getBoundingClientRect(), 495 | transform = new DOMMatrix() 496 | .scaleSelf( 497 | clientRectBound.width / ctx.canvas.width, 498 | clientRectBound.height / ctx.canvas.height 499 | ) 500 | .multiplySelf(ctx.getTransform()) 501 | .translateSelf(margin, margin + y), 502 | w = (widgetWidth - margin * 2 - 3) * transform.a; 503 | 504 | Object.assign(this.openpose.style, { 505 | left: `${transform.a * margin + transform.e}px`, 506 | top: `${transform.d + transform.f}px`, 507 | width: w + "px", 508 | height: w + "px", 509 | position: "absolute", 510 | zIndex: app.graph._nodes.indexOf(node), 511 | }); 512 | 513 | Object.assign(this.openpose.children[0].style, { 514 | width: w + "px", 515 | height: w + "px", 516 | }); 517 | 518 | Object.assign(this.openpose.children[1].style, { 519 | width: w + "px", 520 | height: w + "px", 521 | }); 522 | 523 | Array.from(this.openpose.children[2].children).forEach((element) => { 524 | Object.assign(element.style, { 525 | width: `${28.0 * transform.a}px`, 526 | height: `${22.0 * transform.d}px`, 527 | fontSize: `${transform.d * 10.0}px`, 528 | }); 529 | element.hidden = !visible; 530 | }); 531 | }, 532 | }; 533 | 534 | // Fabric canvas 535 | let canvasOpenPose = document.createElement("canvas"); 536 | node.openPose = new OpenPose(node, canvasOpenPose); 537 | 538 | node.openPose.canvas.setWidth(512); 539 | node.openPose.canvas.setHeight(512); 540 | 541 | let widgetCombo = node.widgets.filter((w) => w.type === "combo"); 542 | widgetCombo[0].value = node.name; 543 | 544 | widget.openpose = node.openPose.canvas.wrapperEl; 545 | widget.parent = node; 546 | 547 | // Create elements undo, redo, clear history 548 | let panelButtons = document.createElement("div"), 549 | refButton = document.createElement("button"), 550 | undoButton = document.createElement("button"), 551 | redoButton = document.createElement("button"), 552 | historyClearButton = document.createElement("button"); 553 | 554 | panelButtons.className = "panelButtons comfy-menu-btns"; 555 | refButton.textContent = "Ref"; 556 | undoButton.textContent = "⟲"; 557 | redoButton.textContent = "⟳"; 558 | historyClearButton.textContent = "✖"; 559 | refButton.title = "Ref"; 560 | undoButton.title = "Undo"; 561 | redoButton.title = "Redo"; 562 | historyClearButton.title = "Clear History"; 563 | 564 | refButton.addEventListener("click", () => node.openPose.backgroundInput.click()); 565 | undoButton.addEventListener("click", () => node.openPose.undo()); 566 | redoButton.addEventListener("click", () => node.openPose.redo()); 567 | historyClearButton.addEventListener("click", () => { 568 | if (confirm(`Delete all pose history of a node "${node.name}"?`)) { 569 | node.openPose.undo_history = []; 570 | node.openPose.redo_history = []; 571 | node.openPose.setPose(default_keypoints); 572 | node.openPose.undo_history.push(node.openPose.getJSON()); 573 | node.openPose.history_change = true; 574 | node.openPose.updateHistoryData(); 575 | } 576 | }); 577 | 578 | panelButtons.appendChild(refButton); 579 | panelButtons.appendChild(undoButton); 580 | panelButtons.appendChild(redoButton); 581 | panelButtons.appendChild(historyClearButton); 582 | node.openPose.canvas.wrapperEl.appendChild(panelButtons); 583 | 584 | document.body.appendChild(widget.openpose); 585 | 586 | // Add buttons add, reset, undo, redo poses 587 | node.addWidget("button", "Add pose", "add_pose", () => { 588 | node.openPose.addPose(); 589 | }); 590 | 591 | node.addWidget("button", "Reset pose", "reset_pose", () => { 592 | node.openPose.resetCanvas(); 593 | }); 594 | // Add buttons Reference image 595 | // node.addWidget("button", "Reference image", "reference_image", () => { 596 | // node.openPose.referenceImage(); 597 | //}); 598 | 599 | // Add customWidget to node 600 | node.addCustomWidget(widget); 601 | 602 | node.onRemoved = () => { 603 | if (Object.hasOwn(LS_Poses, node.name)) { 604 | delete LS_Poses[node.name]; 605 | LS_Save(); 606 | } 607 | 608 | // When removing this node we need to remove the input from the DOM 609 | for (let y in node.widgets) { 610 | if (node.widgets[y].openpose) { 611 | node.widgets[y].openpose.remove(); 612 | } 613 | } 614 | }; 615 | 616 | widget.onRemove = () => { 617 | widget.openpose?.remove(); 618 | }; 619 | 620 | app.canvas.onDrawBackground = function () { 621 | // Draw node isnt fired once the node is off the screen 622 | // if it goes off screen quickly, the input may not be removed 623 | // this shifts it off screen so it can be moved back if the node is visible. 624 | for (let n in app.graph._nodes) { 625 | n = graph._nodes[n]; 626 | for (let w in n.widgets) { 627 | let wid = n.widgets[w]; 628 | if (Object.hasOwn(wid, "openpose")) { 629 | wid.openpose.style.left = -8000 + "px"; 630 | wid.openpose.style.position = "absolute"; 631 | } 632 | } 633 | } 634 | }; 635 | return { widget: widget }; 636 | } 637 | 638 | window.LS_Poses = {}; 639 | function LS_Save() { 640 | ///console.log("Save:", LS_Poses); 641 | localStorage.setItem("ComfyUI_Poses", JSON.stringify(LS_Poses)); 642 | } 643 | 644 | app.registerExtension({ 645 | name: "CDL.OpenPoseEditorPlus", 646 | async init(app) { 647 | // Any initial setup to run as soon as the page loads 648 | let style = document.createElement("style"); 649 | style.innerText = `.panelButtons{ 650 | position: absolute; 651 | padding: 4px; 652 | display: flex; 653 | gap: 4px; 654 | flex-direction: column; 655 | width: fit-content; 656 | } 657 | .panelButtons button:last-child{ 658 | border-color: var(--error-text); 659 | color: var(--error-text) !important; 660 | } 661 | 662 | `; 663 | document.head.appendChild(style); 664 | }, 665 | async setup(app) { 666 | let openPoseNode = app.graph._nodes.filter((wi) => wi.type == "CDL.OpenPoseEditorPlus"); 667 | 668 | if (openPoseNode.length) { 669 | openPoseNode.map((n) => { 670 | console.log(`Setup PoseNode: ${n.name}`); 671 | let widgetImage = n.widgets.find((w) => w.name == "image"); 672 | if (widgetImage && Object.hasOwn(LS_Poses, n.name)) { 673 | let pose_ls = LS_Poses[n.name].undo_history; 674 | n.openPose.loadPreset( 675 | pose_ls.length > 0 676 | ? pose_ls[pose_ls.length - 1] 677 | : { keypoints: default_keypoints } 678 | ); 679 | } 680 | }); 681 | } 682 | }, 683 | async beforeRegisterNodeDef(nodeType, nodeData, app) { 684 | if (nodeData.name === "CDL.OpenPoseEditorPlus") { 685 | const onNodeCreated = nodeType.prototype.onNodeCreated; 686 | 687 | nodeType.prototype.onNodeCreated = function () { 688 | const r = onNodeCreated 689 | ? onNodeCreated.apply(this, arguments) 690 | : undefined; 691 | 692 | let openPoseNode = app.graph._nodes.filter( 693 | (wi) => {wi.type == "CDL.OpenPoseEditorPlus"} 694 | ), 695 | nodeName = `Pose_${openPoseNode.length}`, 696 | nodeNamePNG = `${nodeName}.png`; 697 | 698 | console.log(`Create PoseNode: ${nodeName}`); 699 | 700 | LS_Poses = 701 | localStorage.getItem("ComfyUI_Poses") && 702 | JSON.parse(localStorage.getItem("ComfyUI_Poses")); 703 | if (!LS_Poses) { 704 | localStorage.setItem("ComfyUI_Poses", JSON.stringify({})); 705 | LS_Poses = JSON.parse(localStorage.getItem("ComfyUI_Poses")); 706 | } 707 | 708 | if (!Object.hasOwn(LS_Poses, nodeNamePNG)) { 709 | LS_Poses[nodeNamePNG] = { 710 | undo_history: [], 711 | redo_history: [], 712 | }; 713 | LS_Save(); 714 | } 715 | 716 | createOpenPose.apply(this, [this, nodeNamePNG, {}, app]); 717 | setTimeout(() => { 718 | this.openPose.uploadPoseFile(nodeNamePNG); 719 | }, 1); 720 | 721 | this.setSize([530, 620]); 722 | 723 | return r; 724 | }; 725 | } 726 | }, 727 | }); 728 | -------------------------------------------------------------------------------- /nodes.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import os 3 | import folder_paths 4 | import hashlib 5 | import torch 6 | import numpy as np 7 | 8 | class openPoseEditorPlus: 9 | @classmethod 10 | def INPUT_TYPES(self): 11 | temp_dir = folder_paths.get_temp_directory() 12 | 13 | if not os.path.isdir(temp_dir): 14 | os.makedirs(temp_dir) 15 | 16 | temp_dir = folder_paths.get_temp_directory() 17 | 18 | return {"required": 19 | {"image": (sorted(os.listdir(temp_dir)),)}, 20 | } 21 | 22 | RETURN_TYPES = ("IMAGE",) 23 | FUNCTION = "output_pose" 24 | 25 | CATEGORY = "image" 26 | 27 | def output_pose(self, image): 28 | image_path = os.path.join(folder_paths.get_temp_directory(), image) 29 | # print(f"Create: {image_path}") 30 | 31 | i = Image.open(image_path) 32 | image = i.convert("RGB") 33 | image = np.array(image).astype(np.float32) / 255.0 34 | image = torch.from_numpy(image)[None,] 35 | 36 | return (image,) 37 | 38 | @classmethod 39 | def IS_CHANGED(self, image): 40 | image_path = os.path.join( 41 | folder_paths.get_temp_directory(), image) 42 | # print(f'Change: {image_path}') 43 | 44 | m = hashlib.sha256() 45 | with open(image_path, 'rb') as f: 46 | m.update(f.read()) 47 | return m.digest().hex() 48 | 49 | 50 | NODE_CLASS_MAPPINGS = { 51 | "CDL.OpenPoseEditorPlus": openPoseEditorPlus 52 | } 53 | 54 | NODE_DISPLAY_NAME_MAPPINGS = { 55 | "CDL.OpenPoseEditorPlus": "Openpose Editor Plus", 56 | } 57 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui-openpose-editor-plus" 3 | description = "Nodes:Openpose Editor Plus" 4 | version = "1.0.0" 5 | license = "LICENSE" 6 | 7 | [project.urls] 8 | Repository = "https://github.com/whmc76/ComfyUI-Openpose-Editor-Plus" 9 | # Used by Comfy Registry https://comfyregistry.org 10 | 11 | [tool.comfy] 12 | PublisherId = "" 13 | DisplayName = "ComfyUI-Openpose-Editor-Plus" 14 | Icon = "" 15 | --------------------------------------------------------------------------------