Clipspace SAM Detector Feature added

This commit is contained in:
Dr.Lt.Data
2023-05-21 00:37:16 +09:00
parent a614949fbf
commit ef8c008277
7 changed files with 765 additions and 41 deletions
+20 -1
View File
@@ -4,7 +4,7 @@ This custom node helps to conveniently enhance images through Detector, Detailer
## Custom nodes pack for ComfyUI
# Features
# Custom Nodes
* SAMLoader - Load SAM model
* MMDetDetectorProvider - Load MMDet model to provide BBOX_DETECTOR, SEGM_DETECTOR
* ONNXDetectorProvider - Load ONNX model to provide SEGM_DETECTOR
@@ -62,6 +62,10 @@ This takes latent as input and outputs latent as the result.
* PreviewBridge - This custom node can be used with a bridge when using the MaskEditor feature of Clipspace.
# Feature
* SAM Detector (Clipspace) - When you right-click on a node that has 'MASK' and 'IMAGE' outputs, a context menu will open. From this menu, you can either open a dialog to create a SAM Mask using 'Open in SAM Detector', or copy the content (likely mask data) using 'Copy (Clipspace)' and generate a mask using 'Impact SAM Detector' from the clipspace menu, and then paste it using 'Paste (Clipspace)'.
# Depercated
* The following nodes have been kept only for compatibility with existing workflows, and are no longer supported. Please replace them with new nodes.
* MMDetLoader -> MMDetDetectorProvider
@@ -148,6 +152,21 @@ This takes latent as input and outputs latent as the result.
![combination-example-original](https://github.com/ltdrdata/ComfyUI-extension-tutorials/raw/Main/ComfyUI-Impact-Pack/images/upscale-original.png) ![combination-example-refined](https://github.com/ltdrdata/ComfyUI-extension-tutorials/raw/Main/ComfyUI-Impact-Pack/images/upscale-3x.png)
#### 5. SAM Detector (Clipspace)
* When you right-click on the node that outputs 'MASK' and 'IMAGE', a menu called "Open in SAM Detector" appears, as shown in the following picture. Clicking on the menu opens a dialog in SAM's functionality, allowing you to generate a segment mask.
![samdetector-menu](https://github.com/ltdrdata/ComfyUI-extension-tutorials/raw/Main/ComfyUI-Impact-Pack/images/SAMDetector-menu.png)
* By clicking the left mouse button on a coordinate, a positive prompt in blue color is entered, indicating the area that should be included. Clicking the right mouse button on a coordinate enters a negative prompt in red color, indicating the area that should be excluded. Positive prompts represent the areas that should be included, while negative prompts represent the areas that should be excluded.
* You can remove the points that were added by using the "undo" button. After selecting the points, pressing the "detect" button generates the mask. Additionally, you can adjust the fidelity slider to determine the extent to which the mask belongs to the confidence region.
![samdetector-dialog](https://github.com/ltdrdata/ComfyUI-extension-tutorials/raw/Main/ComfyUI-Impact-Pack/images/SAMDetector-dialog.png)
* If you opened the dialog through "Open in SAM Detector" from the node, you can directly apply the changes by clicking the "Save to node" button. However, if you opened the dialog through the "clipspace" menu, you can save it to clipspace by clicking the "Save" button.
![samdetector-result](https://github.com/ltdrdata/ComfyUI-extension-tutorials/raw/Main/ComfyUI-Impact-Pack/images/SAMDetector-result.png)
* When you execute using the reflected mask in the node, you can observe that the image and mask are displayed separately.
# Others Tutorials
* [ComfyUI-extension-tutorials/ComfyUI-Impact-Pack](https://github.com/ltdrdata/ComfyUI-extension-tutorials/tree/Main/ComfyUI-Impact-Pack) - You can find various tutorials and workflows on this page.
+4
View File
@@ -46,8 +46,12 @@ def setup_js():
js_dest_path = os.path.join(comfy_path, "web", "extensions", "impact-pack")
if not os.path.exists(js_dest_path):
os.makedirs(js_dest_path)
js_src_path = os.path.join(impact_path, "js", "impact-pack.js")
shutil.copy(js_src_path, js_dest_path)
js_src_path = os.path.join(impact_path, "js", "impact-sam-editor.js")
shutil.copy(js_src_path, js_dest_path)
setup_js()
+1 -1
View File
@@ -1,7 +1,7 @@
import configparser
import os
version = "V2.5.1"
version = "V2.6"
dependency_version = 1
+114
View File
@@ -1,8 +1,18 @@
import os
import threading
from aiohttp import web
import server
import folder_paths
import impact_core as core
import impact_pack
from segment_anything import SamPredictor, sam_model_registry
import numpy as np
import nodes
import torch
from PIL import Image
import io
@server.PromptServer.instance.routes.post("/upload/temp")
async def upload_image(request):
@@ -33,3 +43,107 @@ async def upload_image(request):
return web.json_response({"name": filename})
else:
return web.Response(status=400)
sam_predictor = None
default_sam_model_name = os.path.join(impact_pack.model_path, "sams", "sam_vit_b_01ec64.pth")
sam_lock = threading.Condition()
last_prepare_data = None
@server.PromptServer.instance.routes.post("/sam/prepare")
async def load_sam_model(request):
global sam_predictor
global last_prepare_data
data = await request.json()
with sam_lock:
if last_prepare_data is not None and last_prepare_data == data:
# already loaded: skip -- prevent redundant loading
return web.Response(status=200)
last_prepare_data = data
model_name = os.path.join(impact_pack.model_path, "sams", data['sam_model_name'])
print(f"ComfyUI-Impact-Pack: Loading SAM model '{impact_pack.model_path}'")
filename, image_dir = folder_paths.annotated_filepath(data["filename"])
if image_dir is None:
typ = data['type'] if data['type'] != '' else 'output'
image_dir = folder_paths.get_directory_by_type(typ)
if image_dir is None:
return web.Response(status=400)
if 'vit_h' in model_name:
model_kind = 'vit_h'
elif 'vit_l' in model_name:
model_kind = 'vit_l'
else:
model_kind = 'vit_b'
sam_model = sam_model_registry[model_kind](checkpoint=model_name)
sam_predictor = SamPredictor(sam_model)
image_path = os.path.join(image_dir, filename)
image = nodes.LoadImage().load_image(image_path)[0]
image = np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
sam_predictor.set_image(image, "RGB")
@server.PromptServer.instance.routes.post("/sam/release")
async def release_sam(request):
global sam_predictor
with sam_lock:
sam_predictor = None
print(f"ComfyUI-Impact-Pack: unloading SAM model")
@server.PromptServer.instance.routes.post("/sam/detect")
async def sam_detect(request):
global sam_predictor
with sam_lock:
if sam_predictor is not None:
data = await request.json()
positive_points = data['positive_points']
negative_points = data['negative_points']
threshold = data['threshold']
points = []
plabs = []
for p in positive_points:
points.append(p)
plabs.append(1)
for p in negative_points:
points.append(p)
plabs.append(0)
detected_masks = core.sam_predict(sam_predictor, points, plabs, None, threshold)
mask = core.combine_masks2(detected_masks)
if mask is None:
return web.Response(status=400)
image = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3)
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i[0], 0, 255).astype(np.uint8))
img_buffer = io.BytesIO()
img.save(img_buffer, format='png')
headers = {'Content-Type': 'image/png'}
return web.Response(body=img_buffer.getvalue(), headers=headers)
else:
return web.Response(status=400)
-2
View File
@@ -265,9 +265,7 @@ function executeHandler(event) {
if(event.detail.output.aux){
const id = event.detail.node;
if(input_tracking.hasOwnProperty(id)) {
console.log(input_tracking[id]);
if(input_tracking.hasOwnProperty(id) && input_tracking[id][0] != event.detail.output.aux[0]) {
console.log(`dirty(${id}) = ${input_tracking[id][0]} != ${event.detail.output.aux[0]}`);
input_dirty[id] = true;
}
else{
+626
View File
@@ -0,0 +1,626 @@
import { app } from "/scripts/app.js";
import { ComfyDialog, $el } from "/scripts/ui.js";
import { ComfyApp } from "/scripts/app.js";
import { ClipspaceDialog } from "/extensions/core/clipspace.js";
function addMenuHandler(nodeType, cb) {
const getOpts = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function () {
const r = getOpts.apply(this, arguments);
cb.apply(this, arguments);
return r;
};
}
// Helper function to convert a data URL to a Blob object
function dataURLToBlob(dataURL) {
const parts = dataURL.split(';base64,');
const contentType = parts[0].split(':')[1];
const byteString = atob(parts[1]);
const arrayBuffer = new ArrayBuffer(byteString.length);
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
uint8Array[i] = byteString.charCodeAt(i);
}
return new Blob([arrayBuffer], { type: contentType });
}
function loadedImageToBlob(image) {
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
const dataURL = canvas.toDataURL('image/png', 1);
const blob = dataURLToBlob(dataURL);
return blob;
}
async function uploadMask(filepath, formData) {
await fetch('/upload/mask', {
method: 'POST',
body: formData
}).then(response => {}).catch(error => {
console.error('Error:', error);
});
ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']] = new Image();
ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src = `view?filename=${filepath.filename}&type=${filepath.type}`;
if(ComfyApp.clipspace.images)
ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']] = filepath;
ClipspaceDialog.invalidatePreview();
}
class ImpactSamEditorDialog extends ComfyDialog {
static instance = null;
static getInstance() {
if(!ImpactSamEditorDialog.instance) {
ImpactSamEditorDialog.instance = new ImpactSamEditorDialog();
}
return ImpactSamEditorDialog.instance;
}
constructor() {
super();
this.element = $el("div.comfy-modal", { parent: document.body },
[ $el("div.comfy-modal-content",
[...this.createButtons()]),
]);
}
createButtons() {
return [];
}
createButton(name, callback) {
var button = document.createElement("button");
button.innerText = name;
button.addEventListener("click", callback);
return button;
}
createLeftButton(name, callback) {
var button = this.createButton(name, callback);
button.style.cssFloat = "left";
button.style.marginRight = "4px";
return button;
}
createRightButton(name, callback) {
var button = this.createButton(name, callback);
button.style.cssFloat = "right";
button.style.marginLeft = "4px";
return button;
}
createLeftSlider(self, name, callback) {
const divElement = document.createElement('div');
divElement.id = "sam-fidelity-slider";
divElement.style.cssFloat = "left";
divElement.style.fontFamily = "sans-serif";
divElement.style.marginRight = "4px";
divElement.style.color = "var(--input-text)";
divElement.style.backgroundColor = "var(--comfy-input-bg)";
divElement.style.borderRadius = "8px";
divElement.style.borderColor = "var(--border-color)";
divElement.style.borderStyle = "solid";
divElement.style.fontSize = "15px";
divElement.style.height = "21px";
divElement.style.padding = "1px 6px";
divElement.style.display = "flex";
divElement.style.position = "relative";
divElement.style.top = "2px";
self.fidelity_slider_input = document.createElement('input');
self.fidelity_slider_input.setAttribute('type', 'range');
self.fidelity_slider_input.setAttribute('min', '0');
self.fidelity_slider_input.setAttribute('max', '100');
self.fidelity_slider_input.setAttribute('value', '70');
const labelElement = document.createElement("label");
labelElement.textContent = name;
divElement.appendChild(labelElement);
divElement.appendChild(self.fidelity_slider_input);
self.fidelity_slider_input.addEventListener("change", callback);
return divElement;
}
async detect_and_invalidate_mask_canvas(self) {
const mask_img = await self.detect(self);
const canvas = self.maskCtx.canvas;
const ctx = self.maskCtx;
ctx.clearRect(0, 0, canvas.width, canvas.height);
await new Promise((resolve, reject) => {
self.mask_image = new Image();
self.mask_image.onload = function() {
ctx.drawImage(self.mask_image, 0, 0, canvas.width, canvas.height);
resolve();
};
self.mask_image.onerror = reject;
self.mask_image.src = mask_img.src;
});
}
setlayout(imgCanvas, maskCanvas, pointsCanvas) {
const self = this;
// If it is specified as relative, using it only as a hidden placeholder for padding is recommended
// to prevent anomalies where it exceeds a certain size and goes outside of the window.
var placeholder = document.createElement("div");
placeholder.style.position = "relative";
placeholder.style.height = "50px";
var bottom_panel = document.createElement("div");
bottom_panel.style.position = "absolute";
bottom_panel.style.bottom = "0px";
bottom_panel.style.left = "20px";
bottom_panel.style.right = "20px";
bottom_panel.style.height = "50px";
var brush = document.createElement("div");
brush.id = "sam-brush";
brush.style.backgroundColor = "blue";
brush.style.outline = "2px solid pink";
brush.style.borderRadius = "50%";
brush.style.MozBorderRadius = "50%";
brush.style.WebkitBorderRadius = "50%";
brush.style.position = "absolute";
brush.style.zIndex = 100;
brush.style.pointerEvents = "none";
this.brush = brush;
this.element.appendChild(imgCanvas);
this.element.appendChild(maskCanvas);
this.element.appendChild(pointsCanvas);
this.element.appendChild(placeholder); // must below z-index than bottom_panel to avoid covering button
this.element.appendChild(bottom_panel);
document.body.appendChild(brush);
this.brush_size = 5;
var fidelity_slider = this.createLeftSlider(self, "Fidelity", (event) => {
self.fidelity = event.target.value;
});
var clearButton = this.createLeftButton("Clear", () => {
self.maskCtx.clearRect(0, 0, self.maskCanvas.width, self.maskCanvas.height);
self.pointsCtx.clearRect(0, 0, self.pointsCanvas.width, self.pointsCanvas.height);
self.prompt_points = [];
self.invalidatePointsCanvas(self);
});
var detectButton = this.createLeftButton("Detect", () => self.detect_and_invalidate_mask_canvas(self));
var cancelButton = this.createRightButton("Cancel", () => {
document.removeEventListener("mouseup", ImpactSamEditorDialog.handleMouseUp);
document.removeEventListener("keydown", ImpactSamEditorDialog.handleKeyDown);
self.close();
});
self.saveButton = this.createRightButton("Save", () => {
document.removeEventListener("mouseup", ImpactSamEditorDialog.handleMouseUp);
document.removeEventListener("keydown", ImpactSamEditorDialog.handleKeyDown);
self.save(self);
});
var undoButton = this.createLeftButton("Undo", () => {
if(self.prompt_points.length > 0) {
self.prompt_points.pop();
self.pointsCtx.clearRect(0, 0, self.pointsCanvas.width, self.pointsCanvas.height);
self.invalidatePointsCanvas(self);
}
});
bottom_panel.appendChild(clearButton);
bottom_panel.appendChild(detectButton);
bottom_panel.appendChild(self.saveButton);
bottom_panel.appendChild(cancelButton);
bottom_panel.appendChild(fidelity_slider);
bottom_panel.appendChild(undoButton);
imgCanvas.style.position = "relative";
imgCanvas.style.top = "200";
imgCanvas.style.left = "0";
maskCanvas.style.position = "absolute";
maskCanvas.style.opacity = 0.5;
pointsCanvas.style.position = "absolute";
}
show() {
this.mask_image = null;
self.prompt_points = [];
this.message_box = $el("p", ["Please wait a moment while the SAM model and the image are being loaded."]);
this.element.appendChild(this.message_box);
if(self.imgCtx) {
self.imgCtx.clearRect(0, 0, self.imageCanvas.width, self.imageCanvas.height);
}
const target_image_path = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src;
this.load_sam(target_image_path);
if(!this.is_layout_created) {
// layout
const imgCanvas = document.createElement('canvas');
const maskCanvas = document.createElement('canvas');
const pointsCanvas = document.createElement('canvas');
imgCanvas.id = "imageCanvas";
maskCanvas.id = "maskCanvas";
pointsCanvas.id = "pointsCanvas";
this.setlayout(imgCanvas, maskCanvas, pointsCanvas);
// prepare content
this.imgCanvas = imgCanvas;
this.maskCanvas = maskCanvas;
this.pointsCanvas = pointsCanvas;
this.maskCtx = maskCanvas.getContext('2d');
this.pointsCtx = pointsCanvas.getContext('2d');
this.is_layout_created = true;
// replacement of onClose hook since close is not real close
const self = this;
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
if(self.last_display_style && self.last_display_style != 'none' && self.element.style.display == 'none') {
ComfyApp.onClipspaceEditorClosed();
}
self.last_display_style = self.element.style.display;
}
});
});
const config = { attributes: true };
observer.observe(this.element, config);
}
this.setImages(target_image_path, this.imgCanvas, this.pointsCanvas);
if(ComfyApp.clipspace_return_node) {
this.saveButton.innerText = "Save to node";
}
else {
this.saveButton.innerText = "Save";
}
this.saveButton.disabled = true;
this.element.style.display = "block";
this.element.style.zIndex = 8888; // NOTE: alert dialog must be high priority.
}
updateBrushPreview(self, event) {
event.preventDefault();
const centerX = event.pageX;
const centerY = event.pageY;
const brush = self.brush;
brush.style.width = self.brush_size * 2 + "px";
brush.style.height = self.brush_size * 2 + "px";
brush.style.left = (centerX - self.brush_size) + "px";
brush.style.top = (centerY - self.brush_size) + "px";
}
setImages(target_image_path, imgCanvas, pointsCanvas) {
const imgCtx = imgCanvas.getContext('2d');
const maskCtx = this.maskCtx;
const maskCanvas = this.maskCanvas;
const self = this;
// image load
const orig_image = new Image();
window.addEventListener("resize", () => {
// repositioning
imgCanvas.width = window.innerWidth - 250;
imgCanvas.height = window.innerHeight - 200;
// redraw image
let drawWidth = orig_image.width;
let drawHeight = orig_image.height;
if (orig_image.width > imgCanvas.width) {
drawWidth = imgCanvas.width;
drawHeight = (drawWidth / orig_image.width) * orig_image.height;
}
if (drawHeight > imgCanvas.height) {
drawHeight = imgCanvas.height;
drawWidth = (drawHeight / orig_image.height) * orig_image.width;
}
imgCtx.drawImage(orig_image, 0, 0, drawWidth, drawHeight);
// update mask
pointsCanvas.width = drawWidth;
pointsCanvas.height = drawHeight;
pointsCanvas.style.top = imgCanvas.offsetTop + "px";
pointsCanvas.style.left = imgCanvas.offsetLeft + "px";
maskCanvas.width = drawWidth;
maskCanvas.height = drawHeight;
maskCanvas.style.top = imgCanvas.offsetTop + "px";
maskCanvas.style.left = imgCanvas.offsetLeft + "px";
self.invalidateMaskCanvas(self);
self.invalidatePointsCanvas(self);
});
// original image load
orig_image.onload = () => self.onLoaded(self);
const rgb_url = new URL(target_image_path);
rgb_url.searchParams.delete('channel');
rgb_url.searchParams.set('channel', 'rgb');
orig_image.src = rgb_url;
self.image = orig_image;
}
onLoaded(self) {
if(self.message_box) {
self.element.removeChild(self.message_box);
self.message_box = null;
}
window.dispatchEvent(new Event('resize'));
self.setEventHandler(pointsCanvas);
self.saveButton.disabled = false;
}
setEventHandler(targetCanvas) {
targetCanvas.addEventListener("contextmenu", (event) => {
event.preventDefault();
});
const self = this;
targetCanvas.addEventListener('pointermove', (event) => this.updateBrushPreview(self,event));
targetCanvas.addEventListener('pointerdown', (event) => this.handlePointerDown(self,event));
targetCanvas.addEventListener('pointerover', (event) => { this.brush.style.display = "block"; });
targetCanvas.addEventListener('pointerleave', (event) => { this.brush.style.display = "none"; });
document.addEventListener('keydown', ImpactSamEditorDialog.handleKeyDown);
}
static handleKeyDown(event) {
const self = ImpactSamEditorDialog.instance;
if (event.key === '=') { // positive
brush.style.backgroundColor = "blue";
brush.style.outline = "2px solid pink";
self.is_positive_mode = true;
} else if (event.key === '-') { // negative
brush.style.backgroundColor = "red";
brush.style.outline = "2px solid skyblue";
self.is_positive_mode = false;
}
}
is_positive_mode = true;
prompt_points = [];
fidelity = 70;
invalidatePointsCanvas(self) {
const ctx = self.pointsCtx;
for (const i in self.prompt_points) {
const [is_positive, x, y] = self.prompt_points[i];
const scaledX = x * ctx.canvas.width / self.image.width;
const scaledY = y * ctx.canvas.height / self.image.height;
if(is_positive)
ctx.fillStyle = "blue";
else
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(scaledX, scaledY, 3, 0, 3 * Math.PI);
ctx.fill();
}
}줘
invalidateMaskCanvas(self) {
if(self.mask_image) {
self.maskCtx.clearRect(0, 0, self.maskCanvas.width, self.maskCanvas.height);
self.maskCtx.drawImage(self.mask_image, 0, 0, self.maskCanvas.width, self.maskCanvas.height);
}
}
async load_sam(url) {
const parsedUrl = new URL(url);
const searchParams = new URLSearchParams(parsedUrl.search);
const filename = searchParams.get("filename") || "";
const fileType = searchParams.get("type") || "";
const subfolder = searchParams.get("subfolder") || "";
const data = {
sam_model_name: "sam_vit_b_01ec64.pth",
filename: filename,
type: fileType,
subfolder: subfolder
};
fetch('/sam/prepare', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
async detect(self) {
const positive_points = [];
const negative_points = [];
for(const i in self.prompt_points) {
const [is_positive, x, y] = self.prompt_points[i];
const point = [x,y];
if(is_positive)
positive_points.push(point);
else
negative_points.push(point);
}
const data = {
positive_points: positive_points,
negative_points: negative_points,
threshold: self.fidelity/100
};
const response = await fetch('/sam/detect', {
method: 'POST',
headers: { 'Content-Type': 'image/png' },
body: JSON.stringify(data)
});
const blob = await response.blob();
const url = URL.createObjectURL(blob);
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = reject;
image.src = url;
});
}
handlePointerDown(self, event) {
if ([0, 2, 5].includes(event.button)) {
event.preventDefault();
const x = event.offsetX || event.targetTouches[0].clientX - maskRect.left;
const y = event.offsetY || event.targetTouches[0].clientY - maskRect.top;
const originalX = x * self.image.width / self.pointsCanvas.width;
const originalY = y * self.image.height / self.pointsCanvas.height;
var point = null;
if (event.button == 0) {
// positive
point = [true, originalX, originalY];
} else {
// negative
point = [false, originalX, originalY];
}
self.prompt_points.push(point);
self.invalidatePointsCanvas(self);
}
}
async save(self) {
if(!self.mask_image) {
this.close();
return;
}
const save_canvas = document.createElement('canvas');
const save_ctx = save_canvas.getContext('2d', {willReadFrequently:true});
save_canvas.width = self.mask_image.width;
save_canvas.height = self.mask_image.height;
save_ctx.drawImage(self.mask_image, 0, 0, save_canvas.width, save_canvas.height);
const save_data = save_ctx.getImageData(0, 0, save_canvas.width, save_canvas.height);
// refine mask image
for (let i = 0; i < save_data.data.length; i += 4) {
if(save_data.data[i]) {
save_data.data[i+3] = 0;
}
else {
save_data.data[i+3] = 255;
}
save_data.data[i] = 0;
save_data.data[i+1] = 0;
save_data.data[i+2] = 0;
}
save_ctx.globalCompositeOperation = 'source-over';
save_ctx.putImageData(save_data, 0, 0);
const formData = new FormData();
const filename = "clipspace-mask-" + performance.now() + ".png";
const item =
{
"filename": filename,
"subfolder": "",
"type": "temp",
};
if(ComfyApp.clipspace.images)
ComfyApp.clipspace.images[0] = item;
if(ComfyApp.clipspace.widgets) {
const index = ComfyApp.clipspace.widgets.findIndex(obj => obj.name === 'image');
if(index >= 0)
ComfyApp.clipspace.widgets[index].value = item;
}
const dataURL = save_canvas.toDataURL();
const blob = dataURLToBlob(dataURL);
const original_blob = loadedImageToBlob(this.image);
formData.append('image', blob, filename);
formData.append('original_image', original_blob);
formData.append('type', "temp");
await uploadMask(item, formData);
ComfyApp.onClipspaceEditorSave();
this.close();
}
}
app.registerExtension({
name: "Comfy.Impact.SAMEditor",
init(app) {
const callback =
function () {
let dlg = ImpactSamEditorDialog.getInstance();
dlg.show();
};
const context_predicate = () => ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0
ClipspaceDialog.registerButton("Impact SAM Detector", context_predicate, callback);
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.output.includes("MASK") && nodeData.output.includes("IMAGE")) {
addMenuHandler(nodeType, function (_, options) {
options.unshift({
content: "Open in SAM Detector",
callback: () => {
ComfyApp.copyToClipspace(this);
ComfyApp.clipspace_return_node = this;
let dlg = ImpactSamEditorDialog.getInstance();
dlg.show();
},
});
});
}
}
});
-37
View File
@@ -1,37 +0,0 @@
main_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
sys.path.append(os.path.dirname(__file__))
sys.path.append(main_dir)
import server
from aiohttp import web
@server.PromptServer.instance.routes.post("/upload/temp")
async def upload_image(request):
upload_dir = folder_paths.get_temp_directory()
if not os.path.exists(upload_dir):
os.makedirs(upload_dir)
post = await request.post()
image = post.get("image")
if image and image.file:
filename = image.filename
if not filename:
return web.Response(status=400)
split = os.path.splitext(filename)
i = 1
while os.path.exists(os.path.join(upload_dir, filename)):
filename = f"{split[0]} ({i}){split[1]}"
i += 1
filepath = os.path.join(upload_dir, filename)
with open(filepath, "wb") as f:
f.write(image.file.read())
return web.json_response({"name": filename})
else:
return web.Response(status=400)