feat: extract boundary points and combine points node

This commit is contained in:
Radionic
2023-12-29 18:10:25 +08:00
parent c65b730020
commit f373433dca
7 changed files with 466 additions and 22 deletions
+137
View File
@@ -0,0 +1,137 @@
import { combinePointsNode, samPrompts } from "./state.js";
import { van } from "./van.js";
const { div, dialog, form, button, h3, input, span } = van.tags;
van.derive(() => {
if (
combinePointsNode.val != undefined &&
combinePointsNode.val.type === "Combine Points"
) {
const inputNames =
combinePointsNode.val.inputs?.map((x) => x.name).slice(1) || [];
const record = Object.keys(samPrompts.val);
const missingDiff = record.filter((x) => !inputNames.includes(x));
if (missingDiff.length > 0) {
missingDiff.forEach((x) => {
combinePointsNode.val.addInput(x, "POINTS");
});
combinePointsNode.val.graph.change();
}
}
});
export function CombinePointsDialog() {
const showAddLayer = van.state(false);
return div(
{
class: () =>
"absolute z-[100] top-0 left-0 flex justify-center w-full h-full ",
},
() =>
dialog(
{ id: "combine_points_dialog", class: "modal" },
div(
{ class: "modal-box text-base-content" },
form(
{
class: "gap-2 flex flex-col",
method: "dialog",
onsubmit: (e) => {
e.preventDefault();
combine_points_dialog.close();
},
},
button(
{
type: "button",
class: "btn btn-sm btn-circle btn-ghost absolute right-2 top-2",
onclick: (e) => {
e.stopPropagation();
combine_points_dialog.close();
},
},
"✕"
),
h3({ class: "font-bold text-lg text-base-content" }, "Edit points"),
() =>
div(
{ class: "flex flex-col gap-2 mb-2" },
...Object.keys(samPrompts.val).map((key) => {
return span(key);
})
),
() =>
showAddLayer.val
? div(
{
class:
"flex flex-row justify-center items-center border rounded-md pr-2",
},
input({
type: "text",
placeholder: "Type here",
id: "layerName",
class:
"input input-ghost w-full focus:ring-0 focus:border-none focus:outline-none",
autofocus: true,
}),
button(
{
onclick: (e) => {
e.stopPropagation();
e.preventDefault();
showAddLayer.val = false;
},
},
span({
class: "iconify text-2xl",
"data-icon": "iconoir:cancel",
})
),
button(
{
onclick: (e) => {
e.stopPropagation();
e.preventDefault();
showAddLayer.val = false;
const inputText =
document.getElementById("layerName").value;
samPrompts.val = {
...samPrompts.val,
[inputText]: [],
};
},
},
span({
class: "iconify text-2xl",
"data-icon": "mdi:tick",
})
)
)
: button(
{
class: "btn btn-outline btn",
onclick: (e) => {
e.stopPropagation();
e.preventDefault();
showAddLayer.val = true;
},
},
"Add new layer"
),
button(
{
type: "submit",
class: "btn btn-sm btn-ghost place-self-end",
},
"Confirm"
)
)
)
)
);
}
+11 -9
View File
@@ -1,18 +1,20 @@
import { LayerEditor } from './LayerEditor.js';
import { ShapeFlowEditor } from './ShapeFlowEditor.js';
import { van } from './van.js';
import { AvatarPreview } from './AvatarPreview.js';
import { Loading } from './Loading.js';
import { Alert } from './Alert.js';
import { AppHeader } from './AppHeader.js';
import { LayerEditor } from "./LayerEditor.js";
import { ShapeFlowEditor } from "./ShapeFlowEditor.js";
import { van } from "./van.js";
import { AvatarPreview } from "./AvatarPreview.js";
import { Loading } from "./Loading.js";
import { Alert } from "./Alert.js";
import { AppHeader } from "./AppHeader.js";
import { CombinePointsDialog } from "./CombinePointsDialog.js";
const { button, iframe, div, img } = van.tags;
export function Container() {
return div(
{
class: 'fixed left-0 top-0 w-full h-full z-[1000] pointer-events-none',
id: 'avatech-editor',
class: "fixed left-0 top-0 w-full h-full z-[1000] pointer-events-none",
id: "avatech-editor",
},
CombinePointsDialog(),
ShapeFlowEditor(),
LayerEditor(),
AvatarPreview(),
+35 -4
View File
@@ -5,6 +5,7 @@ import {
imageUrl,
imagePrompts,
targetNode,
combinePointsNode,
fileName,
embeddings,
imagePromptsMulti,
@@ -389,6 +390,16 @@ const ext = {
widget: btn,
};
},
COMBINE_POINTS(node, inputName, inputData, app) {
const btn = node.addWidget("button", "Edit points", "", () => {
console.log("Edit points");
combine_points_dialog.showModal();
combinePointsNode.val = node;
});
return {
widget: btn,
};
},
BLENDSHAPES_CONFIG(node, inputName, inputData, app) {
const btn = node.addWidget("button", "Edit Shape Flow", "", () => {
targetNode.val = node;
@@ -675,6 +686,18 @@ const ext = {
nodeData.input.required.sam = ["SAM_PROMPTS"];
// nodeData.input.required.upload = ['IMAGEUPLOAD'];
// nodeData.input.required.prompts_points = ["IMAGEUPLOAD"];
addMenuHandler(nodeType, function (_, options) {
options.unshift({
content: "Open In Points Editor (Local)",
callback: () => {
showMyImageEditor(this);
},
});
});
break;
case "Combine Points":
nodeData.input.required.sam = ["COMBINE_POINTS"];
addMenuHandler(nodeType, function (_, options) {
options.unshift({
content: "Open In Points Editor (Local)",
@@ -746,11 +769,19 @@ function injectUIComponentToComfyuimenu() {
if (!filename.toLowerCase().endsWith(".json")) {
filename += ".json";
}
app.graphToPrompt().then(p=>{
console.log('fkfk');
app.graphToPrompt().then((p) => {
let json = JSON.stringify(p.output, null, 2); // convert the data to a JSON string
json = json.replace(/"seed": (\d+)/g, `"seed": "SEED"`).replace(/"image": "(?!.*mask.*\.png).*"/g, '"image": "reference_image_avatech"').replace(/"embedding_id": ".*"/g, '"embedding_id": "embedding_id_avatech"');
const blob = new Blob([json], {type: "application/json"});
json = json
.replace(/"seed": (\d+)/g, `"seed": "SEED"`)
.replace(
/"image": "(?!.*mask.*\.png).*"/g,
'"image": "reference_image_avatech"'
)
.replace(
/"embedding_id": ".*"/g,
'"embedding_id": "embedding_id_avatech"'
);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
a.href = url;
a.download = filename;
+4 -1
View File
@@ -63,7 +63,6 @@ export const imagePrompts = van.state([]);
export const allImagePrompts = van.state([{}]);
/** @type {State<Record<string, Point[]>>} */
export const imagePromptsMulti = van.state({});
@@ -76,3 +75,7 @@ export const targetNode = van.state();
export const imageSize = van.state({ width: 0, height: 0, samScale: 0 });
export const embeddings = van.state();
export const embeddingID = van.state("Test");
/** @type {State<LGraphNode>} */
export const combinePointsNode = van.state();
export const samPrompts = van.state({});
+187 -8
View File
@@ -995,6 +995,64 @@ html{
}
}
.btn-outline:hover{
--tw-border-opacity: 1;
border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));
--tw-bg-opacity: 1;
background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));
--tw-text-opacity: 1;
color: var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity)));
}
.btn-outline.btn-primary:hover{
--tw-text-opacity: 1;
color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black);
}
.btn-outline.btn-secondary:hover{
--tw-text-opacity: 1;
color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)));
background-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black);
}
.btn-outline.btn-accent:hover{
--tw-text-opacity: 1;
color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)));
background-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black);
}
.btn-outline.btn-success:hover{
--tw-text-opacity: 1;
color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)));
background-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black);
}
.btn-outline.btn-info:hover{
--tw-text-opacity: 1;
color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));
background-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black);
}
.btn-outline.btn-warning:hover{
--tw-text-opacity: 1;
color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)));
background-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black);
}
.btn-outline.btn-error:hover{
--tw-text-opacity: 1;
color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));
background-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black);
}
.btn-disabled:hover,
.btn[disabled]:hover,
.btn:disabled:hover{
@@ -1382,6 +1440,109 @@ html{
background-color: var(--fallback-bc,oklch(var(--bc)/0.2));
}
.btn-outline{
border-color: currentColor;
background-color: transparent;
--tw-text-opacity: 1;
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.btn-outline.btn-active{
--tw-border-opacity: 1;
border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));
--tw-bg-opacity: 1;
background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));
--tw-text-opacity: 1;
color: var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity)));
}
.btn-outline.btn-primary{
--tw-text-opacity: 1;
color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)));
}
.btn-outline.btn-primary.btn-active{
--tw-text-opacity: 1;
color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black);
}
.btn-outline.btn-secondary{
--tw-text-opacity: 1;
color: var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)));
}
.btn-outline.btn-secondary.btn-active{
--tw-text-opacity: 1;
color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)));
background-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black);
}
.btn-outline.btn-accent{
--tw-text-opacity: 1;
color: var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity)));
}
.btn-outline.btn-accent.btn-active{
--tw-text-opacity: 1;
color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)));
background-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black);
}
.btn-outline.btn-success{
--tw-text-opacity: 1;
color: var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)));
}
.btn-outline.btn-success.btn-active{
--tw-text-opacity: 1;
color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)));
background-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black);
}
.btn-outline.btn-info{
--tw-text-opacity: 1;
color: var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)));
}
.btn-outline.btn-info.btn-active{
--tw-text-opacity: 1;
color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));
background-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black);
}
.btn-outline.btn-warning{
--tw-text-opacity: 1;
color: var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)));
}
.btn-outline.btn-warning.btn-active{
--tw-text-opacity: 1;
color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)));
background-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black);
}
.btn-outline.btn-error{
--tw-text-opacity: 1;
color: var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)));
}
.btn-outline.btn-error.btn-active{
--tw-text-opacity: 1;
color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));
background-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black);
}
.btn.btn-disabled,
.btn[disabled],
.btn:disabled{
@@ -1554,6 +1715,18 @@ details.collapse summary::-webkit-details-marker{
outline-color: var(--fallback-bc,oklch(var(--bc)/0.2));
}
.input-ghost{
--tw-bg-opacity: 0.05;
}
.input-ghost:focus,
.input-ghost:focus-within{
--tw-bg-opacity: 1;
--tw-text-opacity: 1;
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
box-shadow: none;
}
.input-disabled,
.input:disabled,
.input[disabled]{
@@ -2174,10 +2347,6 @@ details.collapse summary::-webkit-details-marker{
left: 50%;
}
.left-28{
left: 7rem;
}
.left-4{
left: 1rem;
}
@@ -2268,10 +2437,6 @@ details.collapse summary::-webkit-details-marker{
margin-top: 0.5rem;
}
.mt-4{
margin-top: 1rem;
}
.mt-8{
margin-top: 2rem;
}
@@ -2702,6 +2867,10 @@ details.collapse summary::-webkit-details-marker{
padding-bottom: 0.5rem;
}
.pr-2{
padding-right: 0.5rem;
}
.text-start{
text-align: start;
}
@@ -3026,11 +3195,21 @@ img[src] {
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.focus\:border-none:focus{
border-style: none;
}
.focus\:outline-none:focus{
outline: 2px solid transparent;
outline-offset: 2px;
}
.focus\:ring-0:focus{
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
@media (min-width: 640px){
.sm\:flex{
display: flex;
+26
View File
@@ -0,0 +1,26 @@
class CombinePoints:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
},
}
RETURN_TYPES = ("SAM_PROMPTS",)
FUNCTION = "run"
CATEGORY = "image"
# OUTPUT_NODE = True
def run(self, *args, **kwargs):
print("args", args)
print("kwargs", kwargs)
return ([])
NODE_CLASS_MAPPINGS = {"Combine Points": CombinePoints}
NODE_DISPLAY_NAME_MAPPINGS = {"Combine Points": "Combine Points"}
+66
View File
@@ -0,0 +1,66 @@
import cv2
import numpy as np
import torch
class ExtractBoundaryPoints:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"n_points": ("INT", {"default": -1, "min": -1, "max": 100}),
},
}
RETURN_TYPES = ("POINTS", "IMAGE")
FUNCTION = "run"
CATEGORY = "image"
def find_main_contour(self, image, n_points):
image = np.copy(image[0].numpy())
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
gray = (gray * 255).astype(np.uint8)
# Find contours
contours, _ = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) == 0:
raise Exception(
"No contours found. Please ensure that the image has the correct segments (e.g. when you click on the mouth, it should display a proper blue area over the mouth region)."
)
# Get the largest contour
areas = [cv2.contourArea(contour) for contour in contours]
max_area_index = areas.index(max(areas))
largest_contour = contours[max_area_index]
if n_points > 0:
divided_by = int(largest_contour.shape[0] / n_points)
divided_by = min(divided_by, largest_contour.shape[0])
largest_contour = largest_contour[::divided_by].astype(int)
contours = [largest_contour]
if not image.flags["C_CONTIGUOUS"]:
image = np.ascontiguousarray(image)
cv2.drawContours(image, contours, -1, (0, 255, 0), 3)
points = []
for point in largest_contour:
points.append(
{"x": point[0][0], "y": point[0][1], "label": 1, "isAuto": True}
)
return image, points
def run(self, image, n_points):
contour_image, points = self.find_main_contour(image, n_points)
contour_image = torch.from_numpy(np.expand_dims(contour_image, axis=0))
print(points)
return (points, contour_image)
NODE_CLASS_MAPPINGS = {"Extract Boundary Points": ExtractBoundaryPoints}
NODE_DISPLAY_NAME_MAPPINGS = {"Extract Boundary Points": "Extract Boundary Points"}