add ref button
This commit is contained in:
@@ -6,7 +6,6 @@ import __main__
|
||||
|
||||
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Vendored
-15
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,707 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { fabric } from "./fabric.js";
|
||||
|
||||
fabric.Object.prototype.transparentCorners = false;
|
||||
fabric.Object.prototype.cornerColor = "#108ce6";
|
||||
fabric.Object.prototype.borderColor = "#108ce6";
|
||||
fabric.Object.prototype.cornerSize = 10;
|
||||
|
||||
let connect_keypoints = [
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
[2, 3],
|
||||
[3, 4],
|
||||
[1, 5],
|
||||
[5, 6],
|
||||
[6, 7],
|
||||
[1, 8],
|
||||
[8, 9],
|
||||
[9, 10],
|
||||
[1, 11],
|
||||
[11, 12],
|
||||
[12, 13],
|
||||
[0, 14],
|
||||
[14, 16],
|
||||
[0, 15],
|
||||
[15, 17],
|
||||
];
|
||||
|
||||
let connect_color = [
|
||||
[0, 0, 255],
|
||||
[255, 0, 0],
|
||||
[255, 170, 0],
|
||||
[255, 255, 0],
|
||||
[255, 85, 0],
|
||||
[170, 255, 0],
|
||||
[85, 255, 0],
|
||||
[0, 255, 0],
|
||||
[0, 255, 85],
|
||||
[0, 255, 170],
|
||||
[0, 255, 255],
|
||||
[0, 170, 255],
|
||||
[0, 85, 255],
|
||||
[85, 0, 255],
|
||||
[170, 0, 255],
|
||||
[255, 0, 255],
|
||||
[255, 0, 170],
|
||||
[255, 0, 85],
|
||||
];
|
||||
|
||||
const default_keypoints = [
|
||||
[241, 77],
|
||||
[241, 120],
|
||||
[191, 118],
|
||||
[177, 183],
|
||||
[163, 252],
|
||||
[298, 118],
|
||||
[317, 182],
|
||||
[332, 245],
|
||||
[225, 241],
|
||||
[213, 359],
|
||||
[215, 454],
|
||||
[270, 240],
|
||||
[282, 360],
|
||||
[286, 456],
|
||||
[232, 59],
|
||||
[253, 60],
|
||||
[225, 70],
|
||||
[260, 72],
|
||||
];
|
||||
|
||||
class OpenPose {
|
||||
constructor(node, canvasElement) {
|
||||
this.lockMode = false;
|
||||
this.visibleEyes = true;
|
||||
this.flipped = false;
|
||||
this.node = node;
|
||||
this.undo_history = LS_Poses[node.name].undo_history || [];
|
||||
this.redo_history = LS_Poses[node.name].redo_history || [];
|
||||
this.history_change = false;
|
||||
this.canvas = this.initCanvas(canvasElement);
|
||||
this.image = node.widgets.find((w) => w.name === "image");
|
||||
// 创建用于选择图片的input元素
|
||||
this.backgroundInput = document.createElement("input");
|
||||
this.backgroundInput.type = "file";
|
||||
this.backgroundInput.accept = "image/*";
|
||||
this.backgroundInput.style.display = "none";
|
||||
this.backgroundInput.addEventListener("change", this.onLoadBackground.bind(this));
|
||||
document.body.appendChild(this.backgroundInput);
|
||||
|
||||
}
|
||||
|
||||
// 创建更换背景的按钮
|
||||
referenceImage() {
|
||||
button.addEventListener("click", () => this.backgroundInput.click());
|
||||
}
|
||||
|
||||
// 处理背景图片的加载
|
||||
onLoadBackground(e) {
|
||||
const file = this.backgroundInput.files[0];
|
||||
const url = URL.createObjectURL(file);
|
||||
this.setBackgroundImage(url);
|
||||
}
|
||||
|
||||
// 设置背景图片
|
||||
setBackgroundImage(url) {
|
||||
fabric.Image.fromURL(url, (img) => {
|
||||
img.set({
|
||||
originX: 'left',
|
||||
originY: 'top',
|
||||
opacity: 0.5
|
||||
});
|
||||
this.canvas.setBackgroundImage(img, this.canvas.renderAll.bind(this.canvas));
|
||||
});
|
||||
}
|
||||
|
||||
setPose(keypoints) {
|
||||
this.canvas.clear();
|
||||
|
||||
this.canvas.backgroundColor = "#000";
|
||||
|
||||
const res = [];
|
||||
for (let i = 0; i < keypoints.length; i += 18) {
|
||||
const chunk = keypoints.slice(i, i + 18);
|
||||
res.push(chunk);
|
||||
}
|
||||
|
||||
for (let item of res) {
|
||||
this.addPose(item);
|
||||
this.canvas.discardActiveObject();
|
||||
}
|
||||
}
|
||||
|
||||
addPose(keypoints = undefined) {
|
||||
if (keypoints === undefined) {
|
||||
keypoints = default_keypoints;
|
||||
}
|
||||
|
||||
const group = new fabric.Group();
|
||||
|
||||
const makeCircle = (
|
||||
color,
|
||||
left,
|
||||
top,
|
||||
line1,
|
||||
line2,
|
||||
line3,
|
||||
line4,
|
||||
line5
|
||||
) => {
|
||||
let c = new fabric.Circle({
|
||||
left: left,
|
||||
top: top,
|
||||
strokeWidth: 1,
|
||||
radius: 5,
|
||||
fill: color,
|
||||
stroke: color,
|
||||
});
|
||||
|
||||
c.hasControls = c.hasBorders = false;
|
||||
c.line1 = line1;
|
||||
c.line2 = line2;
|
||||
c.line3 = line3;
|
||||
c.line4 = line4;
|
||||
c.line5 = line5;
|
||||
|
||||
return c;
|
||||
};
|
||||
|
||||
const makeLine = (coords, color) => {
|
||||
return new fabric.Line(coords, {
|
||||
fill: color,
|
||||
stroke: color,
|
||||
strokeWidth: 10,
|
||||
selectable: false,
|
||||
evented: false,
|
||||
});
|
||||
};
|
||||
|
||||
const lines = [];
|
||||
const circles = [];
|
||||
|
||||
for (let i = 0; i < connect_keypoints.length; i++) {
|
||||
// 接続されるidxを指定 [0, 1]なら0と1つなぐ
|
||||
const item = connect_keypoints[i];
|
||||
const line = makeLine(
|
||||
keypoints[item[0]].concat(keypoints[item[1]]),
|
||||
`rgba(${connect_color[i].join(", ")}, 0.7)`
|
||||
);
|
||||
lines.push(line);
|
||||
this.canvas.add(line);
|
||||
}
|
||||
|
||||
for (let i = 0; i < keypoints.length; i++) {
|
||||
let list = [];
|
||||
|
||||
connect_keypoints.filter((item, idx) => {
|
||||
if (item.includes(i)) {
|
||||
list.push(lines[idx]);
|
||||
return idx;
|
||||
}
|
||||
});
|
||||
const circle = makeCircle(
|
||||
`rgb(${connect_color[i].join(", ")})`,
|
||||
keypoints[i][0],
|
||||
keypoints[i][1],
|
||||
...list
|
||||
);
|
||||
circle["id"] = i;
|
||||
circles.push(circle);
|
||||
group.addWithUpdate(circle);
|
||||
}
|
||||
|
||||
this.canvas.discardActiveObject();
|
||||
this.canvas.setActiveObject(group);
|
||||
this.canvas.add(group);
|
||||
group.toActiveSelection();
|
||||
this.canvas.requestRenderAll();
|
||||
}
|
||||
|
||||
initCanvas() {
|
||||
this.canvas = new fabric.Canvas(this.canvas, {
|
||||
backgroundColor: "#000",
|
||||
preserveObjectStacking: true,
|
||||
});
|
||||
|
||||
const updateLines = (target) => {
|
||||
if ("_objects" in target) {
|
||||
const flipX = target.flipX ? -1 : 1;
|
||||
const flipY = target.flipY ? -1 : 1;
|
||||
this.flipped = flipX * flipY === -1;
|
||||
const showEyes = this.flipped ? !this.visibleEyes : this.visibleEyes;
|
||||
|
||||
if (target.angle === 0) {
|
||||
const rtop = target.top;
|
||||
const rleft = target.left;
|
||||
for (const item of target._objects) {
|
||||
let p = item;
|
||||
p.scaleX = 1;
|
||||
p.scaleY = 1;
|
||||
const top =
|
||||
rtop +
|
||||
p.top * target.scaleY * flipY +
|
||||
(target.height * target.scaleY) / 2;
|
||||
const left =
|
||||
rleft +
|
||||
p.left * target.scaleX * flipX +
|
||||
(target.width * target.scaleX) / 2;
|
||||
p["_top"] = top;
|
||||
p["_left"] = left;
|
||||
if (p["id"] === 0) {
|
||||
p.line1 && p.line1.set({ x1: left, y1: top });
|
||||
} else {
|
||||
p.line1 && p.line1.set({ x2: left, y2: top });
|
||||
}
|
||||
if (p["id"] === 14 || p["id"] === 15) {
|
||||
p.radius = showEyes ? 5 : 0;
|
||||
if (p.line1) p.line1.strokeWidth = showEyes ? 10 : 0;
|
||||
if (p.line2) p.line2.strokeWidth = showEyes ? 10 : 0;
|
||||
}
|
||||
p.line2 && p.line2.set({ x1: left, y1: top });
|
||||
p.line3 && p.line3.set({ x1: left, y1: top });
|
||||
p.line4 && p.line4.set({ x1: left, y1: top });
|
||||
p.line5 && p.line5.set({ x1: left, y1: top });
|
||||
}
|
||||
} else {
|
||||
const aCoords = target.aCoords;
|
||||
const center = {
|
||||
x: (aCoords.tl.x + aCoords.br.x) / 2,
|
||||
y: (aCoords.tl.y + aCoords.br.y) / 2,
|
||||
};
|
||||
const rad = (target.angle * Math.PI) / 180;
|
||||
const sin = Math.sin(rad);
|
||||
const cos = Math.cos(rad);
|
||||
|
||||
for (const item of target._objects) {
|
||||
let p = item;
|
||||
const p_top = p.top * target.scaleY * flipY;
|
||||
const p_left = p.left * target.scaleX * flipX;
|
||||
const left = center.x + p_left * cos - p_top * sin;
|
||||
const top = center.y + p_left * sin + p_top * cos;
|
||||
p["_top"] = top;
|
||||
p["_left"] = left;
|
||||
if (p["id"] === 0) {
|
||||
p.line1 && p.line1.set({ x1: left, y1: top });
|
||||
} else {
|
||||
p.line1 && p.line1.set({ x2: left, y2: top });
|
||||
}
|
||||
if (p["id"] === 14 || p["id"] === 15) {
|
||||
p.radius = showEyes ? 5 : 0.3;
|
||||
if (p.line1) p.line1.strokeWidth = showEyes ? 10 : 0;
|
||||
if (p.line2) p.line2.strokeWidth = showEyes ? 10 : 0;
|
||||
}
|
||||
p.line2 && p.line2.set({ x1: left, y1: top });
|
||||
p.line3 && p.line3.set({ x1: left, y1: top });
|
||||
p.line4 && p.line4.set({ x1: left, y1: top });
|
||||
p.line5 && p.line5.set({ x1: left, y1: top });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var p = target;
|
||||
if (p["id"] === 0) {
|
||||
p.line1 && p.line1.set({ x1: p.left, y1: p.top });
|
||||
} else {
|
||||
p.line1 && p.line1.set({ x2: p.left, y2: p.top });
|
||||
}
|
||||
p.line2 && p.line2.set({ x1: p.left, y1: p.top });
|
||||
p.line3 && p.line3.set({ x1: p.left, y1: p.top });
|
||||
p.line4 && p.line4.set({ x1: p.left, y1: p.top });
|
||||
p.line5 && p.line5.set({ x1: p.left, y1: p.top });
|
||||
}
|
||||
this.canvas.renderAll();
|
||||
};
|
||||
|
||||
this.canvas.on("object:moving", (e) => {
|
||||
updateLines(e.target);
|
||||
});
|
||||
|
||||
this.canvas.on("object:scaling", (e) => {
|
||||
updateLines(e.target);
|
||||
this.canvas.renderAll();
|
||||
});
|
||||
|
||||
this.canvas.on("object:rotating", (e) => {
|
||||
updateLines(e.target);
|
||||
this.canvas.renderAll();
|
||||
});
|
||||
|
||||
this.canvas.on("object:modified", () => {
|
||||
if (
|
||||
this.lockMode ||
|
||||
this.canvas.getActiveObject().type == "activeSelection"
|
||||
)
|
||||
return;
|
||||
this.undo_history.push(this.getJSON());
|
||||
this.redo_history.length = 0;
|
||||
this.history_change = true;
|
||||
this.uploadPoseFile(this.node.name);
|
||||
});
|
||||
|
||||
if (!LS_Poses[this.node.name].undo_history.length) {
|
||||
this.setPose(default_keypoints);
|
||||
this.undo_history.push(this.getJSON());
|
||||
}
|
||||
return this.canvas;
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.undo_history.length > 0) {
|
||||
this.lockMode = true;
|
||||
if (this.undo_history.length > 1)
|
||||
this.redo_history.push(this.undo_history.pop());
|
||||
|
||||
const content = this.undo_history[this.undo_history.length - 1];
|
||||
this.loadPreset(content);
|
||||
this.canvas.renderAll();
|
||||
this.lockMode = false;
|
||||
this.history_change = true;
|
||||
this.uploadPoseFile(this.node.name);
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (this.redo_history.length > 0) {
|
||||
this.lockMode = true;
|
||||
const content = this.redo_history.pop();
|
||||
this.undo_history.push(content);
|
||||
this.loadPreset(content);
|
||||
this.canvas.renderAll();
|
||||
this.lockMode = false;
|
||||
this.history_change = true;
|
||||
this.uploadPoseFile(this.node.name);
|
||||
}
|
||||
}
|
||||
|
||||
resetCanvas() {
|
||||
this.canvas.clear();
|
||||
this.canvas.backgroundColor = "#000";
|
||||
this.addPose();
|
||||
}
|
||||
|
||||
updateHistoryData() {
|
||||
if (this.history_change) {
|
||||
LS_Poses[this.node.name].undo_history = this.undo_history;
|
||||
LS_Poses[this.node.name].redo_history = this.redo_history;
|
||||
LS_Save();
|
||||
this.history_change = false;
|
||||
}
|
||||
}
|
||||
|
||||
uploadPoseFile(fileName) {
|
||||
// Upload pose to temp folder ComfyUI
|
||||
|
||||
const uploadFile = async (blobFile) => {
|
||||
try {
|
||||
const resp = await fetch("/upload/image", {
|
||||
method: "POST",
|
||||
body: blobFile,
|
||||
});
|
||||
|
||||
if (resp.status === 200) {
|
||||
const data = await resp.json();
|
||||
|
||||
if (!this.image.options.values.includes(data.name)) {
|
||||
this.image.options.values.push(data.name);
|
||||
}
|
||||
|
||||
this.image.value = data.name;
|
||||
this.updateHistoryData();
|
||||
} else {
|
||||
alert(resp.status + " - " + resp.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
this.canvas.lowerCanvasEl.toBlob(function (blob) {
|
||||
let formData = new FormData();
|
||||
formData.append("image", blob, fileName);
|
||||
formData.append("overwrite", "true");
|
||||
formData.append("type", "temp");
|
||||
uploadFile(formData);
|
||||
}, "image/png");
|
||||
// - end
|
||||
|
||||
const callb = this.node.callback,
|
||||
self = this;
|
||||
this.image.callback = function () {
|
||||
this.image.value = self.node.name;
|
||||
if (callb) {
|
||||
return callb.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getJSON() {
|
||||
const json = {
|
||||
keypoints: this.canvas
|
||||
.getObjects()
|
||||
.filter((item) => {
|
||||
if (item.type === "circle") return item;
|
||||
})
|
||||
.map((item) => {
|
||||
return [Math.round(item.left), Math.round(item.top)];
|
||||
}),
|
||||
};
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
loadPreset(json) {
|
||||
try {
|
||||
if (json["keypoints"].length % 18 === 0) {
|
||||
this.setPose(json["keypoints"]);
|
||||
} else {
|
||||
throw new Error("keypoints is invalid");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create OpenPose widget
|
||||
function createOpenPose(node, inputName, inputData, app) {
|
||||
node.name = inputName;
|
||||
const widget = {
|
||||
type: "openpose",
|
||||
name: `w${inputName}`,
|
||||
|
||||
draw: function (ctx, _, widgetWidth, y, widgetHeight) {
|
||||
const margin = 10,
|
||||
visible = app.canvas.ds.scale > 0.5 && this.type === "openpose",
|
||||
clientRectBound = ctx.canvas.getBoundingClientRect(),
|
||||
transform = new DOMMatrix()
|
||||
.scaleSelf(
|
||||
clientRectBound.width / ctx.canvas.width,
|
||||
clientRectBound.height / ctx.canvas.height
|
||||
)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(margin, margin + y),
|
||||
w = (widgetWidth - margin * 2 - 3) * transform.a;
|
||||
|
||||
Object.assign(this.openpose.style, {
|
||||
left: `${transform.a * margin + transform.e}px`,
|
||||
top: `${transform.d + transform.f}px`,
|
||||
width: w + "px",
|
||||
height: w + "px",
|
||||
position: "absolute",
|
||||
zIndex: app.graph._nodes.indexOf(node),
|
||||
});
|
||||
|
||||
Object.assign(this.openpose.children[0].style, {
|
||||
width: w + "px",
|
||||
height: w + "px",
|
||||
});
|
||||
|
||||
Object.assign(this.openpose.children[1].style, {
|
||||
width: w + "px",
|
||||
height: w + "px",
|
||||
});
|
||||
|
||||
Array.from(this.openpose.children[2].children).forEach((element) => {
|
||||
Object.assign(element.style, {
|
||||
width: `${28.0 * transform.a}px`,
|
||||
height: `${22.0 * transform.d}px`,
|
||||
fontSize: `${transform.d * 10.0}px`,
|
||||
});
|
||||
element.hidden = !visible;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Fabric canvas
|
||||
let canvasOpenPose = document.createElement("canvas");
|
||||
node.openPose = new OpenPose(node, canvasOpenPose);
|
||||
|
||||
node.openPose.canvas.setWidth(512);
|
||||
node.openPose.canvas.setHeight(512);
|
||||
|
||||
let widgetCombo = node.widgets.filter((w) => w.type === "combo");
|
||||
widgetCombo[0].value = node.name;
|
||||
|
||||
widget.openpose = node.openPose.canvas.wrapperEl;
|
||||
widget.parent = node;
|
||||
|
||||
// Create elements undo, redo, clear history
|
||||
let panelButtons = document.createElement("div"),
|
||||
refButton = document.createElement("button"),
|
||||
undoButton = document.createElement("button"),
|
||||
redoButton = document.createElement("button"),
|
||||
historyClearButton = document.createElement("button");
|
||||
|
||||
panelButtons.className = "panelButtons comfy-menu-btns";
|
||||
refButton.textContent = "Ref";
|
||||
undoButton.textContent = "⟲";
|
||||
redoButton.textContent = "⟳";
|
||||
historyClearButton.textContent = "✖";
|
||||
refButton.title = "Ref";
|
||||
undoButton.title = "Undo";
|
||||
redoButton.title = "Redo";
|
||||
historyClearButton.title = "Clear History";
|
||||
|
||||
refButton.addEventListener("click", () => node.openPose.backgroundInput.click());
|
||||
undoButton.addEventListener("click", () => node.openPose.undo());
|
||||
redoButton.addEventListener("click", () => node.openPose.redo());
|
||||
historyClearButton.addEventListener("click", () => {
|
||||
if (confirm(`Delete all pose history of a node "${node.name}"?`)) {
|
||||
node.openPose.undo_history = [];
|
||||
node.openPose.redo_history = [];
|
||||
node.openPose.setPose(default_keypoints);
|
||||
node.openPose.undo_history.push(node.openPose.getJSON());
|
||||
node.openPose.history_change = true;
|
||||
node.openPose.updateHistoryData();
|
||||
}
|
||||
});
|
||||
|
||||
panelButtons.appendChild(refButton);
|
||||
panelButtons.appendChild(undoButton);
|
||||
panelButtons.appendChild(redoButton);
|
||||
panelButtons.appendChild(historyClearButton);
|
||||
node.openPose.canvas.wrapperEl.appendChild(panelButtons);
|
||||
|
||||
document.body.appendChild(widget.openpose);
|
||||
|
||||
// Add buttons add, reset, undo, redo poses
|
||||
node.addWidget("button", "Add pose", "add_pose", () => {
|
||||
node.openPose.addPose();
|
||||
});
|
||||
|
||||
node.addWidget("button", "Reset pose", "reset_pose", () => {
|
||||
node.openPose.resetCanvas();
|
||||
});
|
||||
// Add buttons Reference image
|
||||
// node.addWidget("button", "Reference image", "reference_image", () => {
|
||||
// node.openPose.referenceImage();
|
||||
//});
|
||||
|
||||
// Add customWidget to node
|
||||
node.addCustomWidget(widget);
|
||||
|
||||
node.onRemoved = () => {
|
||||
if (Object.hasOwn(LS_Poses, node.name)) {
|
||||
delete LS_Poses[node.name];
|
||||
LS_Save();
|
||||
}
|
||||
|
||||
// When removing this node we need to remove the input from the DOM
|
||||
for (let y in node.widgets) {
|
||||
if (node.widgets[y].openpose) {
|
||||
node.widgets[y].openpose.remove();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
widget.onRemove = () => {
|
||||
widget.openpose?.remove();
|
||||
};
|
||||
|
||||
app.canvas.onDrawBackground = function () {
|
||||
// Draw node isnt fired once the node is off the screen
|
||||
// if it goes off screen quickly, the input may not be removed
|
||||
// this shifts it off screen so it can be moved back if the node is visible.
|
||||
for (let n in app.graph._nodes) {
|
||||
n = graph._nodes[n];
|
||||
for (let w in n.widgets) {
|
||||
let wid = n.widgets[w];
|
||||
if (Object.hasOwn(wid, "openpose")) {
|
||||
wid.openpose.style.left = -8000 + "px";
|
||||
wid.openpose.style.position = "absolute";
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return { widget: widget };
|
||||
}
|
||||
|
||||
window.LS_Poses = {};
|
||||
function LS_Save() {
|
||||
///console.log("Save:", LS_Poses);
|
||||
localStorage.setItem("ComfyUI_Poses", JSON.stringify(LS_Poses));
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "CDL.OpenPoseEditorPlus",
|
||||
async init(app) {
|
||||
// Any initial setup to run as soon as the page loads
|
||||
let style = document.createElement("style");
|
||||
style.innerText = `.panelButtons{
|
||||
position: absolute;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
}
|
||||
.panelButtons button:last-child{
|
||||
border-color: var(--error-text);
|
||||
color: var(--error-text) !important;
|
||||
}
|
||||
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
},
|
||||
async setup(app) {
|
||||
let openPoseNode = app.graph._nodes.filter((wi) => wi.type == "CDL.OpenPoseEditorPlus");
|
||||
|
||||
if (openPoseNode.length) {
|
||||
openPoseNode.map((n) => {
|
||||
console.log(`Setup PoseNode: ${n.name}`);
|
||||
let widgetImage = n.widgets.find((w) => w.name == "image");
|
||||
if (widgetImage && Object.hasOwn(LS_Poses, n.name)) {
|
||||
let pose_ls = LS_Poses[n.name].undo_history;
|
||||
n.openPose.loadPreset(
|
||||
pose_ls.length > 0
|
||||
? pose_ls[pose_ls.length - 1]
|
||||
: { keypoints: default_keypoints }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "CDL.OpenPoseEditorPlus") {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const r = onNodeCreated
|
||||
? onNodeCreated.apply(this, arguments)
|
||||
: undefined;
|
||||
|
||||
let openPoseNode = app.graph._nodes.filter(
|
||||
(wi) => {wi.type == "CDL.OpenPoseEditorPlus"}
|
||||
),
|
||||
nodeName = `Pose_${openPoseNode.length}`,
|
||||
nodeNamePNG = `${nodeName}.png`;
|
||||
|
||||
console.log(`Create PoseNode: ${nodeName}`);
|
||||
|
||||
LS_Poses =
|
||||
localStorage.getItem("ComfyUI_Poses") &&
|
||||
JSON.parse(localStorage.getItem("ComfyUI_Poses"));
|
||||
if (!LS_Poses) {
|
||||
localStorage.setItem("ComfyUI_Poses", JSON.stringify({}));
|
||||
LS_Poses = JSON.parse(localStorage.getItem("ComfyUI_Poses"));
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(LS_Poses, nodeNamePNG)) {
|
||||
LS_Poses[nodeNamePNG] = {
|
||||
undo_history: [],
|
||||
redo_history: [],
|
||||
};
|
||||
LS_Save();
|
||||
}
|
||||
|
||||
createOpenPose.apply(this, [this, nodeNamePNG, {}, app]);
|
||||
setTimeout(() => {
|
||||
this.openPose.uploadPoseFile(nodeNamePNG);
|
||||
}, 1);
|
||||
|
||||
this.setSize([530, 620]);
|
||||
|
||||
return r;
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
-791
@@ -1,791 +0,0 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { ComfyWidgets } from "/scripts/widgets.js";
|
||||
import "./fabricnew.min.js";
|
||||
|
||||
const connect_keypoints = [
|
||||
[0, 1], [1, 2], [2, 3], [3, 4],
|
||||
[1, 5], [5, 6], [6, 7], [1, 8],
|
||||
[8, 9], [9, 10], [1, 11], [11, 12],
|
||||
[12, 13], [14, 0], [14, 16], [15, 0],
|
||||
[15, 17]
|
||||
]
|
||||
|
||||
const connect_color = [
|
||||
[ 0, 0, 255],
|
||||
[255, 0, 0],
|
||||
[255, 170, 0],
|
||||
[255, 255, 0],
|
||||
[255, 85, 0],
|
||||
[170, 255, 0],
|
||||
[ 85, 255, 0],
|
||||
[ 0, 255, 0],
|
||||
|
||||
[ 0, 255, 85],
|
||||
[ 0, 255, 170],
|
||||
[ 0, 255, 255],
|
||||
[ 0, 170, 255],
|
||||
[ 0, 85, 255],
|
||||
[ 85, 0, 255],
|
||||
|
||||
[170, 0, 255],
|
||||
[255, 0, 255],
|
||||
[255, 0, 170],
|
||||
[255, 0, 85]
|
||||
]
|
||||
|
||||
const DEFAULT_KEYPOINTS = [
|
||||
[241, 77], [241, 120], [191, 118], [177, 183],
|
||||
[163, 252], [298, 118], [317, 182], [332, 245],
|
||||
[225, 241], [213, 359], [215, 454], [270, 240],
|
||||
[282, 360], [286, 456], [232, 59], [253, 60],
|
||||
[225, 70], [260, 72]
|
||||
]
|
||||
|
||||
async function readFileToText(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.onerror = async () => {
|
||||
reject(reader.error);
|
||||
}
|
||||
reader.readAsText(file);
|
||||
})
|
||||
}
|
||||
|
||||
async function loadImageAsync(imageURL) {
|
||||
return new Promise((resolve) => {
|
||||
const e = new Image();
|
||||
e.setAttribute('crossorigin', 'anonymous');
|
||||
e.addEventListener("load", () => { resolve(e); });
|
||||
e.src = imageURL;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
async function canvasToBlob(canvas) {
|
||||
return new Promise(function(resolve) {
|
||||
canvas.toBlob(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
class OpenPosePanel {
|
||||
node = null;
|
||||
canvas = null;
|
||||
canvasElem = null
|
||||
panel = null
|
||||
|
||||
undo_history = []
|
||||
redo_history = []
|
||||
|
||||
visibleEyes = true;
|
||||
flipped = false;
|
||||
lockMode = false;
|
||||
|
||||
constructor(panel, node) {
|
||||
this.panel = panel;
|
||||
this.node = node;
|
||||
|
||||
const width = 750;
|
||||
const height = 750;
|
||||
this.panel.style.width = `${width}px`;
|
||||
this.panel.style.height = `${height}px`;
|
||||
this.panel.style.left = `calc(50% - ${width/4}px)`
|
||||
this.panel.style.top = `calc(50% - ${height/4}px)`
|
||||
|
||||
const rootHtml = `
|
||||
<canvas class="openpose-editor-canvas" />
|
||||
<div class="canvas-drag-overlay" />
|
||||
<input bind:this={fileInput} class="openpose-file-input" type="file" />
|
||||
//<input bind:this={sampleInput} class="sample-file-input" type="file" accept="image" />
|
||||
`;
|
||||
const container = this.panel.addHTML(rootHtml, "openpose-container");
|
||||
container.style.width = "auto";
|
||||
container.style.height = "auto";
|
||||
container.style.margin = "auto";
|
||||
container.style.display = "flex";
|
||||
|
||||
const dragOverlay = container.querySelector(".canvas-drag-overlay")
|
||||
dragOverlay.style.pointerEvents = "none";
|
||||
dragOverlay.style.visibility = "hidden";
|
||||
dragOverlay.style.display = "flex";
|
||||
dragOverlay.style.alignItems = "center";
|
||||
dragOverlay.style.justifyContent = "center";
|
||||
dragOverlay.style.width = "80%";
|
||||
dragOverlay.style.height = "80%";
|
||||
dragOverlay.style.color = "white";
|
||||
dragOverlay.style.fontSize = "2.5em";
|
||||
dragOverlay.style.fontFamily = "inherit";
|
||||
dragOverlay.style.fontWeight = "600";
|
||||
dragOverlay.style.lineHeight = "100%";
|
||||
dragOverlay.style.background = "rgba(0,0,0,0.5)";
|
||||
dragOverlay.style.margin = "0.25rem";
|
||||
dragOverlay.style.borderRadius = "0.25rem";
|
||||
dragOverlay.style.border = "0.5px solid";
|
||||
dragOverlay.style.position = "absolute";
|
||||
|
||||
this.canvasWidth = 512;
|
||||
this.canvasHeight = 512;
|
||||
|
||||
this.canvasElem = container.querySelector(".openpose-editor-canvas")
|
||||
this.canvasElem.width = this.canvasWidth
|
||||
this.canvasElem.height = this.canvasHeight
|
||||
this.canvasElem.style.margin = "0.25rem"
|
||||
this.canvasElem.style.borderRadius = "0.25rem"
|
||||
this.canvasElem.style.border = "0.5px solid"
|
||||
|
||||
this.canvas = this.initCanvas(this.canvasElem)
|
||||
|
||||
this.fileInput = container.querySelector(".openpose-file-input");
|
||||
this.fileInput.style.display = "none";
|
||||
this.fileInput.addEventListener("change", this.onLoad.bind(this))
|
||||
|
||||
this.sampleInput = container.querySelector(".openpose-file-input");
|
||||
this.sampleInput.style.display = "none";
|
||||
this.sampleInput.addEventListener("change", this.onLoadSample.bind(this))
|
||||
|
||||
this.panel.addButton("Add", () => {
|
||||
this.addPose()
|
||||
this.saveToNode();
|
||||
});
|
||||
this.panel.addButton("Remove", () => {
|
||||
this.removePose()
|
||||
this.saveToNode();
|
||||
});
|
||||
this.panel.addButton("Reset", () => {
|
||||
this.resetCanvas()
|
||||
this.saveToNode();
|
||||
});
|
||||
this.panel.addButton("Save", () => this.save());
|
||||
this.panel.addButton("Load", () => this.load());
|
||||
this.panel.addButton("LoadSample", () => this.loadSample());
|
||||
|
||||
const widthLabel = document.createElement("label")
|
||||
widthLabel.innerHTML = "Width"
|
||||
widthLabel.style.fontFamily = "Arial"
|
||||
widthLabel.style.padding = "0 0.5rem";
|
||||
widthLabel.style.color = "#ccc";
|
||||
this.widthInput = document.createElement("input")
|
||||
this.widthInput.style.background = "#1c1c1c";
|
||||
this.widthInput.style.color = "#aaa";
|
||||
this.widthInput.setAttribute("type", "number")
|
||||
this.widthInput.setAttribute("min", "64")
|
||||
this.widthInput.setAttribute("max", "4096")
|
||||
this.widthInput.setAttribute("step", "64")
|
||||
this.widthInput.setAttribute("type", "number")
|
||||
this.widthInput.addEventListener("change", () => {
|
||||
this.resizeCanvas(+this.widthInput.value, +this.heightInput.value);
|
||||
this.saveToNode();
|
||||
})
|
||||
|
||||
const heightLabel = document.createElement("label")
|
||||
heightLabel.innerHTML = "Height"
|
||||
heightLabel.style.fontFamily = "Arial"
|
||||
heightLabel.style.padding = "0 0.5rem";
|
||||
heightLabel.style.color = "#aaa";
|
||||
this.heightInput = document.createElement("input")
|
||||
this.heightInput.style.background = "#1c1c1c";
|
||||
this.heightInput.style.color = "#ccc";
|
||||
this.heightInput.setAttribute("type", "number")
|
||||
this.heightInput.setAttribute("min", "64")
|
||||
this.heightInput.setAttribute("max", "4096")
|
||||
this.heightInput.setAttribute("step", "64")
|
||||
this.heightInput.addEventListener("change", () => {
|
||||
this.resizeCanvas(+this.widthInput.value, +this.heightInput.value);
|
||||
this.saveToNode();
|
||||
})
|
||||
|
||||
this.panel.footer.appendChild(widthLabel);
|
||||
this.panel.footer.appendChild(this.widthInput);
|
||||
this.panel.footer.appendChild(heightLabel);
|
||||
this.panel.footer.appendChild(this.heightInput);
|
||||
|
||||
if (this.node.properties.savedPose) {
|
||||
const error = this.loadJSON(this.node.properties.savedPose);
|
||||
if (error) {
|
||||
console.error("[OpenPose Editor] Failed to restore saved pose JSON", error)
|
||||
this.resizeCanvas(this.canvasWidth, this.canvasHeight)
|
||||
this.setPose(DEFAULT_KEYPOINTS)
|
||||
}
|
||||
this.undo_history.push(JSON.stringify(this.canvas));
|
||||
}
|
||||
else {
|
||||
this.resizeCanvas(this.canvasWidth, this.canvasHeight)
|
||||
this.setPose(DEFAULT_KEYPOINTS)
|
||||
}
|
||||
|
||||
const keyHandler = this.onKeyDown.bind(this);
|
||||
|
||||
document.addEventListener("keydown", keyHandler)
|
||||
this.panel.onClose = () => {
|
||||
document.removeEventListener("keydown", keyHandler)
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDown(e) {
|
||||
if (e.key === "z" && e.ctrlKey) {
|
||||
this.undo()
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
else if (e.key === "y" && e.ctrlKey) {
|
||||
this.redo()
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
addPose(keypoints = undefined){
|
||||
if (keypoints === undefined){
|
||||
keypoints = DEFAULT_KEYPOINTS;
|
||||
}
|
||||
|
||||
const group = new fabric.Group([], {
|
||||
subTargetCheck: true,
|
||||
interactive: true
|
||||
})
|
||||
|
||||
function makeCircle(color, left, top, line1, line2, line3, line4, line5) {
|
||||
var c = new fabric.Circle({
|
||||
left: left,
|
||||
top: top,
|
||||
strokeWidth: 1,
|
||||
radius: 5,
|
||||
fill: color,
|
||||
stroke: color,
|
||||
originX: 'center',
|
||||
originY: 'center',
|
||||
});
|
||||
c.hasControls = c.hasBorders = false;
|
||||
|
||||
c.line1 = line1;
|
||||
c.line2 = line2;
|
||||
c.line3 = line3;
|
||||
c.line4 = line4;
|
||||
c.line5 = line5;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
function makeLine(coords, color) {
|
||||
return new fabric.Line(coords, {
|
||||
fill: color,
|
||||
stroke: color,
|
||||
strokeWidth: 10,
|
||||
selectable: false,
|
||||
evented: false,
|
||||
originX: 'center',
|
||||
originY: 'center',
|
||||
});
|
||||
}
|
||||
|
||||
const lines = []
|
||||
const circles = []
|
||||
|
||||
for (let i = 0; i < connect_keypoints.length; i++){
|
||||
// 接続されるidxを指定 [0, 1]なら0と1つなぐ
|
||||
const item = connect_keypoints[i]
|
||||
const line = makeLine(keypoints[item[0]].concat(keypoints[item[1]]), `rgba(${connect_color[i].join(", ")}, 0.7)`)
|
||||
lines.push(line)
|
||||
this.canvas.add(line)
|
||||
line['id'] = item[0];
|
||||
}
|
||||
|
||||
for (let i = 0; i < keypoints.length; i++){
|
||||
// const list = connect_keypoints.filter(item => item.includes(i));
|
||||
const list = []
|
||||
connect_keypoints.filter((item, idx) => {
|
||||
if(item.includes(i)){
|
||||
list.push(lines[idx])
|
||||
return idx
|
||||
}
|
||||
})
|
||||
const circle = makeCircle(`rgb(${connect_color[i].join(", ")})`, keypoints[i][0], keypoints[i][1], ...list)
|
||||
circle["id"] = i
|
||||
circles.push(circle)
|
||||
// this.canvas.add(circle)
|
||||
group.add(circle);
|
||||
}
|
||||
|
||||
group.lines = lines
|
||||
group.circles = circles
|
||||
|
||||
this.canvas.discardActiveObject();
|
||||
this.canvas.setActiveObject(group);
|
||||
this.canvas.add(group);
|
||||
console.warn(group)
|
||||
// this.canvas.setActiveObject(group)
|
||||
// group.toActiveSelection();
|
||||
this.canvas.requestRenderAll();
|
||||
}
|
||||
|
||||
setPose(keypoints){
|
||||
this.canvas.clear()
|
||||
this.canvas.backgroundColor = "#000"
|
||||
|
||||
const res = [];
|
||||
for (let i = 0; i < keypoints.length; i += 18) {
|
||||
const chunk = keypoints.slice(i, i + 18);
|
||||
res.push(chunk);
|
||||
}
|
||||
|
||||
for (const item of res){
|
||||
this.addPose(item)
|
||||
this.canvas.discardActiveObject();
|
||||
}
|
||||
|
||||
this.saveToNode();
|
||||
}
|
||||
|
||||
calcResolution(width, height){
|
||||
const viewportWidth = window.innerWidth / 2.25;
|
||||
const viewportHeight = window.innerHeight * 0.75;
|
||||
const ratio = Math.min(viewportWidth / width, viewportHeight / height);
|
||||
return {width: width * ratio, height: height * ratio}
|
||||
}
|
||||
|
||||
resizeCanvas(width, height){
|
||||
let resolution = this.calcResolution(width, height)
|
||||
|
||||
this.canvasWidth = width;
|
||||
this.canvasHeight = height;
|
||||
|
||||
this.widthInput.value = `${width}`
|
||||
this.heightInput.value = `${height}`
|
||||
|
||||
this.canvas.setWidth(width);
|
||||
this.canvas.setHeight(height);
|
||||
this.canvasElem.style.width = resolution["width"] + "px"
|
||||
this.canvasElem.style.height = resolution["height"] + "px"
|
||||
this.canvasElem.nextElementSibling.style.width = resolution["width"] + "px"
|
||||
this.canvasElem.nextElementSibling.style.height = resolution["height"] + "px"
|
||||
this.canvasElem.parentElement.style.width = resolution["width"] + "px"
|
||||
this.canvasElem.parentElement.style.height = resolution["height"] + "px"
|
||||
this.canvasElem.parentElement.style.margin = "auto";
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.undo_history.length > 0) {
|
||||
this.lockMode = true;
|
||||
if (this.undo_history.length > 1)
|
||||
this.redo_history.push(this.undo_history.pop());
|
||||
const content = this.undo_history[this.undo_history.length - 1];
|
||||
this.canvas.loadFromJSON(content, () => {
|
||||
this.canvas.renderAll();
|
||||
this.lockMode = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (this.redo_history.length > 0) {
|
||||
this.lockMode = true;
|
||||
const content = this.redo_history.pop();
|
||||
this.undo_history.push(content);
|
||||
this.canvas.loadFromJSON(content, () => {
|
||||
this.canvas.renderAll();
|
||||
this.lockMode = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initCanvas(elem){
|
||||
const canvas = new fabric.Canvas(elem, {
|
||||
backgroundColor: '#000',
|
||||
// selection: false,
|
||||
preserveObjectStacking: true
|
||||
});
|
||||
|
||||
this.undo_history = [];
|
||||
this.redo_history = [];
|
||||
|
||||
const updateLines = (target) => {
|
||||
if ("_objects" in target) {
|
||||
const flipX = target.flipX ? -1 : 1;
|
||||
const flipY = target.flipY ? -1 : 1;
|
||||
this.flipped = flipX * flipY === -1;
|
||||
const showEyes = this.flipped ? !this.visibleEyes : this.visibleEyes;
|
||||
|
||||
if (target.angle === 0) {
|
||||
const rtop = target.top
|
||||
const rleft = target.left
|
||||
for (const item of target._objects){
|
||||
let p = item;
|
||||
p.scaleX = 1;
|
||||
p.scaleY = 1;
|
||||
const top = rtop + p.top * target.scaleY * flipY + target.height * target.scaleY / 2;
|
||||
const left = rleft + p.left * target.scaleX * flipX + (target.width * target.scaleX / 2);
|
||||
p['_top'] = top;
|
||||
p['_left'] = left;
|
||||
if (p["id"] === 0) {
|
||||
p.line1 && p.line1.set({ 'x1': left, 'y1': top });
|
||||
}else{
|
||||
p.line1 && p.line1.set({ 'x2': left, 'y2': top });
|
||||
}
|
||||
if (p['id'] === 14 || p['id'] === 15) {
|
||||
p.radius = showEyes ? 5 : 0;
|
||||
p.strokeWidth = showEyes ? 10 : 0;
|
||||
}
|
||||
p.line2 && p.line2.set({ 'x1': left, 'y1': top });
|
||||
p.line3 && p.line3.set({ 'x1': left, 'y1': top });
|
||||
p.line4 && p.line4.set({ 'x1': left, 'y1': top });
|
||||
p.line5 && p.line5.set({ 'x1': left, 'y1': top });
|
||||
|
||||
}
|
||||
} else {
|
||||
const aCoords = target.aCoords;
|
||||
const center = {'x': (aCoords.tl.x + aCoords.br.x)/2, 'y': (aCoords.tl.y + aCoords.br.y)/2};
|
||||
const rad = target.angle * Math.PI / 180;
|
||||
const sin = Math.sin(rad);
|
||||
const cos = Math.cos(rad);
|
||||
|
||||
for (const item of target._objects){
|
||||
let p = item;
|
||||
const p_top = p.top * target.scaleY * flipY;
|
||||
const p_left = p.left * target.scaleX * flipX;
|
||||
const left = center.x + p_left * cos - p_top * sin;
|
||||
const top = center.y + p_left * sin + p_top * cos;
|
||||
p['_top'] = top;
|
||||
p['_left'] = left;
|
||||
if (p["id"] === 0) {
|
||||
p.line1 && p.line1.set({ 'x1': left, 'y1': top });
|
||||
}else{
|
||||
p.line1 && p.line1.set({ 'x2': left, 'y2': top });
|
||||
}
|
||||
if (p['id'] === 14 || p['id'] === 15) {
|
||||
p.radius = showEyes ? 5 : 0.3;
|
||||
if (p.line1) p.line1.strokeWidth = showEyes ? 10 : 0;
|
||||
if (p.line2) p.line2.strokeWidth = showEyes ? 10 : 0;
|
||||
}
|
||||
p.line2 && p.line2.set({ 'x1': left, 'y1': top });
|
||||
p.line3 && p.line3.set({ 'x1': left, 'y1': top });
|
||||
p.line4 && p.line4.set({ 'x1': left, 'y1': top });
|
||||
p.line5 && p.line5.set({ 'x1': left, 'y1': top });
|
||||
}
|
||||
}
|
||||
target.setCoords();
|
||||
} else {
|
||||
const p = target;
|
||||
const group = p.group;
|
||||
|
||||
const flipX = group.flipX ? -1 : 1;
|
||||
const flipY = group.flipY ? -1 : 1;
|
||||
this.flipped = flipX * flipY === -1;
|
||||
const showEyes = this.flipped ? !this.visibleEyes : this.visibleEyes;
|
||||
|
||||
const aCoords = group.aCoords;
|
||||
const center = {'x': (aCoords.tl.x + aCoords.br.x)/2, 'y': (aCoords.tl.y + aCoords.br.y)/2};
|
||||
const rad = target.angle * Math.PI / 180;
|
||||
const sin = Math.sin(rad);
|
||||
const cos = Math.cos(rad);
|
||||
|
||||
const p_top = p.top * group.scaleY * flipY;
|
||||
const p_left = p.left * group.scaleX * flipX;
|
||||
const left = center.x + p_left * cos - p_top * sin;
|
||||
const top = center.y + p_left * sin + p_top * cos;
|
||||
|
||||
if (p["id"] === 0) {
|
||||
p.line1 && p.line1.set({ 'x1': left, 'y1': top });
|
||||
}else{
|
||||
p.line1 && p.line1.set({ 'x2': left, 'y2': top });
|
||||
}
|
||||
p.line2 && p.line2.set({ 'x1': left, 'y1': top });
|
||||
p.line3 && p.line3.set({ 'x1': left, 'y1': top });
|
||||
p.line4 && p.line4.set({ 'x1': left, 'y1': top });
|
||||
p.line5 && p.line5.set({ 'x1': left, 'y1': top });
|
||||
|
||||
group.setCoords();
|
||||
}
|
||||
canvas.renderAll();
|
||||
}
|
||||
|
||||
canvas.on('object:moving', (e) => {
|
||||
updateLines(e.target);
|
||||
});
|
||||
|
||||
canvas.on('object:scaling', (e) => {
|
||||
updateLines(e.target);
|
||||
canvas.renderAll();
|
||||
});
|
||||
|
||||
canvas.on('object:rotating', (e) => {
|
||||
updateLines(e.target);
|
||||
canvas.renderAll();
|
||||
});
|
||||
|
||||
canvas.on("object:modified", () => {
|
||||
if (this.lockMode) return;
|
||||
this.undo_history.push(JSON.stringify(canvas));
|
||||
this.redo_history.length = 0;
|
||||
this.saveToNode()
|
||||
});
|
||||
|
||||
// const json_observer = new MutationObserver((m) => {
|
||||
// if(gradioApp().querySelector('#tab_openpose_editor').style.display!=='block') return;
|
||||
// try {
|
||||
// const raw = gradioApp().querySelector("#jsonbox").querySelector("textarea").value
|
||||
// if(raw.length!==0) detectImage(raw);
|
||||
// } catch(e){console.log(e)}
|
||||
// })
|
||||
// json_observer.observe(gradioApp().querySelector("#jsonbox"), { "attributes": true })
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
saveToNode() {
|
||||
this.node.setProperty("savedPose", this.serializeJSON());
|
||||
this.uploadCanvasAsFile()
|
||||
}
|
||||
|
||||
async captureCanvasClean() {
|
||||
this.lockMode = true;
|
||||
|
||||
this.canvas.getObjects("image").forEach((img) => {
|
||||
img.opacity = 0;
|
||||
})
|
||||
if (this.canvas.backgroundImage)
|
||||
this.canvas.backgroundImage.opacity = 0
|
||||
this.canvas.discardActiveObject();
|
||||
this.canvas.renderAll()
|
||||
|
||||
const blob = await canvasToBlob(this.canvasElem);
|
||||
|
||||
this.canvas.getObjects("image").forEach((img) => {
|
||||
img.opacity = 1;
|
||||
})
|
||||
if (this.canvas.backgroundImage)
|
||||
this.canvas.backgroundImage.opacity = 0.5
|
||||
this.canvas.renderAll()
|
||||
|
||||
this.lockMode = false;
|
||||
|
||||
return blob
|
||||
}
|
||||
|
||||
async uploadCanvasAsFile() {
|
||||
try {
|
||||
const blob = await this.captureCanvasClean()
|
||||
const filename = `ComfyUI_OpenPose_${this.node.id}.png`;
|
||||
|
||||
const body = new FormData();
|
||||
body.append("image", blob, filename);
|
||||
body.append("overwrite", "true");
|
||||
|
||||
const resp = await fetch("/upload/image", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (resp.status === 200) {
|
||||
const data = await resp.json();
|
||||
await this.node.setImage(data.name)
|
||||
} else {
|
||||
console.error(resp.status + " - " + resp.statusText)
|
||||
alert(resp.status + " - " + resp.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
alert(error);
|
||||
}
|
||||
}
|
||||
|
||||
removePose() {
|
||||
const selection = this.canvas.getActiveObject();
|
||||
if (!selection || !("lines" in selection))
|
||||
return;
|
||||
|
||||
for (const line of selection.lines){
|
||||
this.canvas.remove(line)
|
||||
}
|
||||
|
||||
this.canvas.remove(selection)
|
||||
}
|
||||
|
||||
resetCanvas() {
|
||||
this.canvas.clear()
|
||||
this.canvas.backgroundColor = "#000"
|
||||
}
|
||||
|
||||
load() {
|
||||
this.fileInput.value = null;
|
||||
this.fileInput.click();
|
||||
}
|
||||
|
||||
async onLoad(e) {
|
||||
const file = this.fileInput.files[0];
|
||||
const text = await readFileToText(file);
|
||||
const error = await this.loadJSON(text);
|
||||
if (error != null) {
|
||||
app.ui.dialog.show(error);
|
||||
}
|
||||
else {
|
||||
this.saveToNode();
|
||||
}
|
||||
}
|
||||
|
||||
loadSample() {
|
||||
this.sampleInput.value = null;
|
||||
this.sampleInput.click();
|
||||
}
|
||||
|
||||
async onLoadSample(e) {
|
||||
const file = this.sampleInput.files[0];
|
||||
let url = 'E://ComfyUI_windows_portable//workflows//samples//cloth//4.jpg';
|
||||
// const url = await loadImageAsync(`/view?filename=${name}&type=input&subfolder=${subfolder}`);
|
||||
// if(window.createObjectURL != undefined) {
|
||||
// url = window.createObjectURL(file);
|
||||
// } else if (window.URL != undefined) {
|
||||
// url = window.URL.createObjectURL(file);
|
||||
// } else if (window.webkitURL != undefined) {
|
||||
// url = window.webkitURL.createObjectURL(file);
|
||||
// }
|
||||
// const img = new Image();
|
||||
await (img.src = url);
|
||||
// this.resizeCanvas(img.width, img.height)
|
||||
this.setBackgroundImage(url,this.canvas.renderAll.bind(this),opacity:0.5)
|
||||
// this.urls = [url];
|
||||
// this.backgroundImage = url;
|
||||
// this.backgroundImage.opacity = 0.5;
|
||||
// this.canvas.renderAll.bind(this);
|
||||
this.saveToNode();
|
||||
}
|
||||
|
||||
serializeJSON() {
|
||||
const groups = this.canvas.getObjects().filter(i => i.type === "group");
|
||||
const keypoints = groups.map(g => {
|
||||
const circles = g.getObjects().filter(i => i.type === "circle");
|
||||
return circles.map(c => [c.oCoords.tl.x, c.oCoords.tl.y]);
|
||||
})
|
||||
|
||||
const json = JSON.stringify({
|
||||
"width": this.canvas.width,
|
||||
"height": this.canvas.height,
|
||||
"keypoints": keypoints
|
||||
}, null, 4)
|
||||
return json;
|
||||
}
|
||||
|
||||
save() {
|
||||
const json = this.serializeJSON()
|
||||
const blob = new Blob([json], {
|
||||
type: "application/json"
|
||||
});
|
||||
const filename = "pose-" + Date.now().toString() + ".json"
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
loadJSON(text) {
|
||||
const json = JSON.parse(text);
|
||||
if (json["width"] && json["height"]) {
|
||||
this.resizeCanvas(json["width"], json["height"])
|
||||
} else {
|
||||
return 'width, height is invalid';
|
||||
}
|
||||
this.resetCanvas();
|
||||
const keypoints = json["keypoints"] || []
|
||||
for (const group of keypoints) {
|
||||
if (group.length % 18 === 0) {
|
||||
this.addPose(group)
|
||||
}
|
||||
else {
|
||||
return 'keypoints is invalid'
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "CDL.OpenPoseEditorPlus",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name !== "CDL.OpenPoseEditorPlus") {
|
||||
return
|
||||
}
|
||||
|
||||
fabric.Object.prototype.transparentCorners = false;
|
||||
fabric.Object.prototype.cornerColor = '#108ce6';
|
||||
fabric.Object.prototype.borderColor = '#108ce6';
|
||||
fabric.Object.prototype.cornerSize = 10;
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
||||
|
||||
if (!this.properties) {
|
||||
this.properties = {};
|
||||
this.properties.savedPose = "";
|
||||
}
|
||||
|
||||
this.serialize_widgets = true;
|
||||
|
||||
// Output & widget
|
||||
this.imageWidget = this.widgets.find(w => w.name === "image");
|
||||
this.imageWidget.callback = this.showImage.bind(this);
|
||||
this.imageWidget.disabled = true
|
||||
console.error(this);
|
||||
|
||||
// Non-serialized widgets
|
||||
this.jsonWidget = this.addWidget("text", "", this.properties.savedPose, "savedPose");
|
||||
this.jsonWidget.disabled = true
|
||||
this.jsonWidget.serialize = true
|
||||
|
||||
this.openWidget = this.addWidget("button", "open editor", "image", () => {
|
||||
const graphCanvas = LiteGraph.LGraphCanvas.active_canvas
|
||||
if (graphCanvas == null)
|
||||
return;
|
||||
|
||||
const panel = graphCanvas.createPanel("OpenPose Editor", { closable: true });
|
||||
panel.node = this;
|
||||
panel.classList.add("openpose-editor");
|
||||
|
||||
this.openPosePanel = new OpenPosePanel(panel, this);
|
||||
document.body.appendChild(this.openPosePanel.panel);
|
||||
});
|
||||
this.openWidget.serialize = false;
|
||||
|
||||
// On load if we have a value then render the image
|
||||
// The value isnt set immediately so we need to wait a moment
|
||||
// No change callbacks seem to be fired on initial setting of the value
|
||||
requestAnimationFrame(async () => {
|
||||
if (this.imageWidget.value) {
|
||||
await this.setImage(this.imageWidget.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
nodeType.prototype.showImage = async function(name) {
|
||||
let folder_separator = name.lastIndexOf("/");
|
||||
let subfolder = "";
|
||||
if (folder_separator > -1) {
|
||||
subfolder = name.substring(0, folder_separator);
|
||||
name = name.substring(folder_separator + 1);
|
||||
}
|
||||
const img = await loadImageAsync(`/view?filename=${name}&type=input&subfolder=${subfolder}`);
|
||||
this.imgs = [img];
|
||||
this.setSizeForImage();
|
||||
app.graph.setDirtyCanvas(true);
|
||||
}
|
||||
|
||||
nodeType.prototype.setImage = async function(name) {
|
||||
this.imageWidget.value = name;
|
||||
await this.showImage(name);
|
||||
}
|
||||
|
||||
const onPropertyChanged = nodeType.prototype.onPropertyChanged;
|
||||
nodeType.prototype.onPropertyChanged = function(property, value) {
|
||||
if (property === "savedPose") {
|
||||
this.jsonWidget.value = value;
|
||||
}
|
||||
else {
|
||||
if(onPropertyChanged)
|
||||
onPropertyChanged.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,27 +1,54 @@
|
||||
import os.path
|
||||
from PIL import Image
|
||||
import os
|
||||
import folder_paths
|
||||
from nodes import LoadImage
|
||||
import hashlib
|
||||
import torch
|
||||
import numpy as np
|
||||
|
||||
class openPoseEditorPlus:
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
temp_dir = folder_paths.get_temp_directory()
|
||||
|
||||
class OpenPoseEditorPlus:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required":
|
||||
{"image": ("STRING", { "default": "" })},
|
||||
}
|
||||
if not os.path.isdir(temp_dir):
|
||||
os.makedirs(temp_dir)
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "load_image"
|
||||
temp_dir = folder_paths.get_temp_directory()
|
||||
|
||||
CATEGORY = "image"
|
||||
return {"required":
|
||||
{"image": (sorted(os.listdir(temp_dir)),)},
|
||||
}
|
||||
|
||||
def load_image(self, image):
|
||||
image, mask = LoadImage.load_image(self, image)
|
||||
return (image,)
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "output_pose"
|
||||
|
||||
CATEGORY = "image"
|
||||
|
||||
def output_pose(self, image):
|
||||
image_path = os.path.join(folder_paths.get_temp_directory(), image)
|
||||
# print(f"Create: {image_path}")
|
||||
|
||||
i = Image.open(image_path)
|
||||
image = i.convert("RGB")
|
||||
image = np.array(image).astype(np.float32) / 255.0
|
||||
image = torch.from_numpy(image)[None,]
|
||||
|
||||
return (image,)
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(self, image):
|
||||
image_path = os.path.join(
|
||||
folder_paths.get_temp_directory(), image)
|
||||
# print(f'Change: {image_path}')
|
||||
|
||||
m = hashlib.sha256()
|
||||
with open(image_path, 'rb') as f:
|
||||
m.update(f.read())
|
||||
return m.digest().hex()
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"CDL.OpenPoseEditorPlus": OpenPoseEditorPlus
|
||||
"CDL.OpenPoseEditorPlus": openPoseEditorPlus
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
|
||||
Reference in New Issue
Block a user