diff --git a/js/CombinePointsDialog.js b/js/CombinePointsDialog.js new file mode 100644 index 0000000..bf38618 --- /dev/null +++ b/js/CombinePointsDialog.js @@ -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" + ) + ) + ) + ) + ); +} diff --git a/js/Container.js b/js/Container.js index 8130d57..dcad520 100644 --- a/js/Container.js +++ b/js/Container.js @@ -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(), diff --git a/js/index.js b/js/index.js index d188493..b6c7d47 100644 --- a/js/index.js +++ b/js/index.js @@ -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; diff --git a/js/state.js b/js/state.js index 131edb9..b52e384 100644 --- a/js/state.js +++ b/js/state.js @@ -63,7 +63,6 @@ export const imagePrompts = van.state([]); export const allImagePrompts = van.state([{}]); - /** @type {State>} */ 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} */ +export const combinePointsNode = van.state(); +export const samPrompts = van.state({}); diff --git a/js/tw-styles.css b/js/tw-styles.css index 63baa6d..618e44c 100644 --- a/js/tw-styles.css +++ b/js/tw-styles.css @@ -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; diff --git a/sam/combine_points.py b/sam/combine_points.py new file mode 100644 index 0000000..c86dba1 --- /dev/null +++ b/sam/combine_points.py @@ -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"} diff --git a/sam/extract_boundary_points.py b/sam/extract_boundary_points.py new file mode 100644 index 0000000..eba315e --- /dev/null +++ b/sam/extract_boundary_points.py @@ -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"}