Adding batch image processing mode to the RS Image Adjustments node
This commit is contained in:
File diff suppressed because one or more lines are too long
+46
-8
@@ -2,7 +2,7 @@
|
||||
# Copyright 2025-2026 Raykosan (RaykoStudio)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use th is file except in compliance with the License.
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
@@ -50,6 +50,7 @@ class LRUCache:
|
||||
PENDING_ADJUSTMENTS = LRUCache(max_size=50)
|
||||
PREVIEW_CACHE = LRUCache(max_size=20)
|
||||
LUT_CACHE = LRUCache(max_size=10)
|
||||
BATCH_PARAMS = {} # Глобальное хранилище для параметров батча {node_id: adjustments}
|
||||
|
||||
def tensor2pil(tensor):
|
||||
arr = (tensor.cpu().numpy() * 255.0).astype(np.uint8)
|
||||
@@ -380,9 +381,9 @@ def apply_selective_color(img_bgr, color_name, cyan, magenta, yellow, black):
|
||||
|
||||
c, m, y, k = cyan / 100.0, magenta / 100.0, yellow / 100.0, black / 100.0
|
||||
|
||||
img_rgb[:, :, 0] += mask * (-c - k) # Red: Cyan уменьшает Red, Black уменьшает все
|
||||
img_rgb[:, :, 1] += mask * (-m - k) # Green: Magenta уменьшает Green
|
||||
img_rgb[:, :, 2] += mask * (-y - k) # Blue: Yellow уменьшает Blue
|
||||
img_rgb[:, :, 0] += mask * (-c - k)
|
||||
img_rgb[:, :, 1] += mask * (-m - k)
|
||||
img_rgb[:, :, 2] += mask * (-y - k)
|
||||
|
||||
img_rgb = np.clip(img_rgb, 0, 1)
|
||||
result_bgr = (cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR) * 255).astype(np.uint8)
|
||||
@@ -496,10 +497,15 @@ async def adjustments_handler(request):
|
||||
data = await request.json()
|
||||
node_id = str(data.get("id"))
|
||||
adjustments = data.get("adjustments", {})
|
||||
is_batch = data.get("is_batch", False)
|
||||
|
||||
if node_id is None or adjustments is None:
|
||||
return web.json_response({"error": "Missing data"}, status=400)
|
||||
|
||||
# Если включен Batch - сохраняем параметры глобально
|
||||
if is_batch:
|
||||
BATCH_PARAMS[node_id] = adjustments
|
||||
|
||||
PENDING_ADJUSTMENTS.put(node_id, {
|
||||
"status": "completed",
|
||||
"adjustments": adjustments
|
||||
@@ -519,6 +525,8 @@ async def cancel_handler(request):
|
||||
node_id = str(data.get("node_id"))
|
||||
if node_id:
|
||||
PENDING_ADJUSTMENTS.put(node_id, {"status": "cancelled"})
|
||||
# Очищаем batch-параметры при отмене
|
||||
BATCH_PARAMS.pop(node_id, None)
|
||||
temp_dir = folder_paths.get_temp_directory()
|
||||
cleanup_temp_files(node_id, temp_dir)
|
||||
return web.json_response({"status": "cancelled"})
|
||||
@@ -653,12 +661,15 @@ class RS_ImageAdjustments:
|
||||
def apply_adjustments(self, image, brightness, contrast, sharpen, hue, saturation, unique_id=None, advanced_params=None):
|
||||
unique_id = str(unique_id) if unique_id is not None else "unknown"
|
||||
|
||||
# Создаем безопасный fallback-тензор на случай ошибок/отмены
|
||||
safe_fallback = torch.zeros((1, 512, 512, 3))
|
||||
|
||||
if image.numel() == 0:
|
||||
return (image,)
|
||||
return (safe_fallback,)
|
||||
|
||||
h, w = image.shape[1], image.shape[2]
|
||||
if h < 8 or w < 8:
|
||||
return (image,)
|
||||
return (safe_fallback,)
|
||||
|
||||
PENDING_ADJUSTMENTS.cache.pop(unique_id, None)
|
||||
|
||||
@@ -673,6 +684,32 @@ class RS_ImageAdjustments:
|
||||
|
||||
cleanup_temp_files(unique_id, td, exclude_file=bfn)
|
||||
|
||||
# === BATCH MODE CHECK ===
|
||||
# Проверяем ДО входа в режим ожидания
|
||||
if unique_id in BATCH_PARAMS:
|
||||
print(f"[RS Adjustments] Node {unique_id}: Applying saved BATCH parameters...")
|
||||
adjustments = BATCH_PARAMS[unique_id].copy()
|
||||
|
||||
# Гарантируем наличие базовых параметров
|
||||
adjustments["brightness"] = adjustments.get("brightness", brightness)
|
||||
adjustments["contrast"] = adjustments.get("contrast", contrast)
|
||||
adjustments["sharpen"] = adjustments.get("sharpen", sharpen)
|
||||
adjustments["hue"] = adjustments.get("hue", hue)
|
||||
adjustments["saturation"] = adjustments.get("saturation", saturation)
|
||||
|
||||
img_bgr = cv2.cvtColor(np.array(bg_pil), cv2.COLOR_RGB2BGR)
|
||||
result_bgr = process_image(img_bgr, adjustments)
|
||||
|
||||
result_pil = Image.fromarray(cv2.cvtColor(result_bgr, cv2.COLOR_BGR2RGB))
|
||||
cleanup_temp_files(unique_id, td)
|
||||
|
||||
# Удаляем batch-параметры ТОЛЬКО после успешной обработки
|
||||
# Это гарантирует, что следующий кадр уже не будет batch
|
||||
BATCH_PARAMS.pop(unique_id, None)
|
||||
|
||||
return (pil2tensor(result_pil),)
|
||||
|
||||
# === ОБЫЧНЫЙ РЕЖИМ ===
|
||||
PENDING_ADJUSTMENTS.put(unique_id, {"status": "pending"})
|
||||
|
||||
time.sleep(0.5)
|
||||
@@ -702,15 +739,16 @@ class RS_ImageAdjustments:
|
||||
if PENDING_ADJUSTMENTS.cache[unique_id].get("status") == "cancelled":
|
||||
print(f"[RS Image Adjustments] Cancelled by user for node {unique_id}")
|
||||
PENDING_ADJUSTMENTS.cache.pop(unique_id, None)
|
||||
BATCH_PARAMS.pop(unique_id, None) # Очищаем batch при отмене
|
||||
cleanup_temp_files(unique_id, td)
|
||||
return (image,)
|
||||
return (safe_fallback,)
|
||||
|
||||
print(f"[RS Image Adjustments] Input received for node {unique_id}")
|
||||
|
||||
data = PENDING_ADJUSTMENTS.cache.pop(unique_id, None)
|
||||
if data is None:
|
||||
cleanup_temp_files(unique_id, td)
|
||||
return (image,)
|
||||
return (safe_fallback,)
|
||||
|
||||
adjustments = data.get("adjustments", {})
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
|
||||
REQUIRED_MODULES: Set[str] = set()
|
||||
|
||||
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
|
||||
__version__ = "0.36.2"
|
||||
__version__ = "0.36.3"
|
||||
|
||||
def load_modules():
|
||||
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ComfyUI_RaykoStudio",
|
||||
"version": "0.36.2",
|
||||
"version": "0.36.3",
|
||||
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
|
||||
"author": "Raykosan",
|
||||
"license": "Apache License 2.0",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "ComfyUI_RaykoStudio"
|
||||
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
|
||||
version = "0.36.2"
|
||||
version = "0.36.3"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["torch", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"]
|
||||
|
||||
|
||||
+120
-25
@@ -182,6 +182,15 @@ app.registerExtension({
|
||||
this._lastHeavyRenderTime = 0;
|
||||
this._isHeavyRenderPending = false;
|
||||
|
||||
// BATCH MODE VARIABLES
|
||||
this.isBatchMode = false;
|
||||
this._batchParams = null;
|
||||
this.btnBatchHover = false;
|
||||
this._queueWasRunning = false;
|
||||
this._batchResetTimer = null;
|
||||
|
||||
console.log(`[RS Adjustments] Node ${this.id} created. Registering status listener.`);
|
||||
|
||||
this.displayWidth = 420;
|
||||
this.displayHeight = 420;
|
||||
this.canvasPixelSize = 420;
|
||||
@@ -284,6 +293,34 @@ app.registerExtension({
|
||||
api.addEventListener("interrupted", () => {
|
||||
this._cleanup();
|
||||
});
|
||||
|
||||
// STATUS LISTENER FOR BATCH RESET (moved inside onNodeCreated for correct 'this')
|
||||
api.addEventListener("status", (event) => {
|
||||
const remaining = event.detail?.exec_info?.queue_remaining;
|
||||
console.log(`[RS Adjustments] Status event received. queue_remaining=${remaining}, this.id=${this.id}, isBatchMode=${this.isBatchMode}`);
|
||||
|
||||
if (remaining > 0) {
|
||||
this._queueWasRunning = true;
|
||||
if (this._batchResetTimer) {
|
||||
clearTimeout(this._batchResetTimer);
|
||||
this._batchResetTimer = null;
|
||||
}
|
||||
} else if (remaining === 0 && this._queueWasRunning) {
|
||||
if (!this._batchResetTimer) {
|
||||
console.log(`[RS Adjustments] Node ${this.id}: Queue finished. Starting batch reset timer (3s)...`);
|
||||
this._batchResetTimer = setTimeout(() => {
|
||||
console.log(`[RS Adjustments] Node ${this.id}: Timer fired! Resetting batch. isBatchMode was: ${this.isBatchMode}`);
|
||||
this.isBatchMode = false;
|
||||
this._batchParams = null;
|
||||
this.setDirtyCanvas(true);
|
||||
|
||||
this._batchResetTimer = null;
|
||||
this._queueWasRunning = false;
|
||||
console.log(`[RS Adjustments] Node ${this.id}: Batch reset complete. isBatchMode is now: ${this.isBatchMode}`);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
nodeType.prototype._hideNativeWidgets = function() {
|
||||
@@ -495,11 +532,11 @@ app.registerExtension({
|
||||
return b;
|
||||
};
|
||||
|
||||
const btnNormalMode = makeBtn(" NORMAL MODE", "#2196F3", () => { this._toggleAdvancedMode(); });
|
||||
const btnNormalMode = makeBtn("🔵 NORMAL MODE", "#2196F3", () => { this._toggleAdvancedMode(); });
|
||||
buttonContainer.appendChild(btnNormalMode);
|
||||
const btnApply = makeBtn("✔️ APPLY", "#4CAF50", () => { this._sendAdjustments(); this._toggleAdvancedMode(); });
|
||||
buttonContainer.appendChild(btnApply);
|
||||
const btnCancel = makeBtn(" CANCEL", "#dc3545", () => { this._cancelEditing(); this._toggleAdvancedMode(); });
|
||||
const btnCancel = makeBtn("❌ CANCEL", "#dc3545", () => { this._cancelEditing(); this._toggleAdvancedMode(); });
|
||||
buttonContainer.appendChild(btnCancel);
|
||||
|
||||
const bottomRow = document.createElement('div');
|
||||
@@ -629,7 +666,7 @@ app.registerExtension({
|
||||
};
|
||||
|
||||
const resetBtn = document.createElement('button');
|
||||
resetBtn.textContent = '🔄️';
|
||||
resetBtn.textContent = '🔄';
|
||||
resetBtn.style.cssText = 'width:28px;height:28px;background:#252525;color:#888;border:1px solid #444;border-radius:4px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:0.15s;flex-shrink:0;';
|
||||
resetBtn.onmouseenter = () => { resetBtn.style.background = '#2a2a2a'; resetBtn.style.color = '#fff'; resetBtn.style.borderColor = '#4CAF50'; };
|
||||
resetBtn.onmouseleave = () => { resetBtn.style.background = '#252525'; resetBtn.style.color = '#888'; resetBtn.style.borderColor = '#444'; };
|
||||
@@ -739,7 +776,7 @@ app.registerExtension({
|
||||
browseBtn.onmouseleave = () => { browseBtn.style.background = '#252525'; browseBtn.style.color = '#aaa'; browseBtn.style.borderColor = '#444'; };
|
||||
browseBtn.onclick = (e) => { e.stopPropagation(); fileInput.click(); };
|
||||
const resetBtn = document.createElement('button');
|
||||
resetBtn.textContent = '🔄';
|
||||
resetBtn.textContent = '';
|
||||
resetBtn.style.cssText = 'width:28px;height:28px;background:#252525;color:#888;border:1px solid #444;border-radius:4px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:0.15s;flex-shrink:0;';
|
||||
resetBtn.onmouseenter = () => { resetBtn.style.background = '#2a2a2a'; resetBtn.style.color = '#fff'; resetBtn.style.borderColor = '#4CAF50'; };
|
||||
resetBtn.onmouseleave = () => { resetBtn.style.background = '#252525'; resetBtn.style.color = '#888'; resetBtn.style.borderColor = '#444'; };
|
||||
@@ -1074,6 +1111,17 @@ app.registerExtension({
|
||||
};
|
||||
|
||||
nodeType.prototype._openDeferredEditor = function() {
|
||||
// BATCH MODE: SKIP EDITOR AND AUTO-APPLY SAVED PARAMS
|
||||
if (this.isBatchMode && this._batchParams) {
|
||||
this.adjustments = JSON.parse(JSON.stringify(this._batchParams));
|
||||
this._syncWidgetsFromAdjustments();
|
||||
this.previewImage = null;
|
||||
this.setDirtyCanvas(true);
|
||||
this._scheduleHeavyRender();
|
||||
setTimeout(() => this._sendAdjustments(), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.pendingEditorData) return;
|
||||
this.backgroundImage = null;
|
||||
this.previewImage = null;
|
||||
@@ -1250,7 +1298,7 @@ app.registerExtension({
|
||||
this.currentRenderAbortController.abort();
|
||||
this.currentRenderAbortController = null;
|
||||
}
|
||||
const payload = { id: String(this.id), adjustments: this.adjustments };
|
||||
const payload = { id: String(this.id), adjustments: this.adjustments, is_batch: this.isBatchMode };
|
||||
try {
|
||||
await api.fetchApi("/rayko/rs_adjustments", {
|
||||
method: "POST",
|
||||
@@ -1540,29 +1588,48 @@ app.registerExtension({
|
||||
}
|
||||
});
|
||||
|
||||
// THREE BUTTON ROW: BATCH | APPLY | CANCEL
|
||||
const buttonsY = slidersY + 5 * (sliderH + gapSlider) + 10;
|
||||
const btnH2 = 30;
|
||||
const btnGap2 = 10;
|
||||
const btnW2 = (this.size[0] - 35) / 2;
|
||||
const btnGap2 = 8;
|
||||
const btnW3 = (this.size[0] - 46) / 3;
|
||||
|
||||
ctx.fillStyle = this.btnApplyHover ? "#444" : "#2a2a2a";
|
||||
this._roundRect(ctx, 15, buttonsY, btnW2, btnH2, 6);
|
||||
// BATCH BUTTON
|
||||
const batchBtnX = 15;
|
||||
const isBatchActive = this.isBatchMode;
|
||||
ctx.fillStyle = isBatchActive ? "#2196F3" : (this.btnBatchHover ? "#3a3a3a" : "#2a2a2a");
|
||||
ctx.strokeStyle = isBatchActive ? "#2196F3" : "#666";
|
||||
ctx.lineWidth = isBatchActive ? 2 : 1;
|
||||
this._roundRect(ctx, batchBtnX, buttonsY, btnW3, btnH2, 6);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "#4CAF50";
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "#4CAF50";
|
||||
ctx.fillStyle = isBatchActive ? "#fff" : "#aaa";
|
||||
ctx.font = "bold 11px Arial";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "alphabetic";
|
||||
ctx.fillText("✔️ APPLY", 15 + btnW2 / 2, buttonsY + btnH2 / 2 + 4);
|
||||
ctx.fillText(isBatchActive ? "⚪ BATCH ON" : "️🔘 BATCH OFF", batchBtnX + btnW3/2, buttonsY + btnH2/2 + 4);
|
||||
|
||||
// APPLY BUTTON
|
||||
const applyBtnX = batchBtnX + btnW3 + btnGap2;
|
||||
ctx.fillStyle = this.btnApplyHover ? "#444" : "#2a2a2a";
|
||||
this._roundRect(ctx, applyBtnX, buttonsY, btnW3, btnH2, 6);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "#4CAF50";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "#4CAF50";
|
||||
ctx.fillText("✔️ APPLY", applyBtnX + btnW3/2, buttonsY + btnH2/2 + 4);
|
||||
|
||||
// CANCEL BUTTON
|
||||
const cancelBtnX = applyBtnX + btnW3 + btnGap2;
|
||||
ctx.fillStyle = this.btnCancelHover ? "#444" : "#2a2a2a";
|
||||
this._roundRect(ctx, 15 + btnW2 + btnGap2, buttonsY, btnW2, btnH2, 6);
|
||||
this._roundRect(ctx, cancelBtnX, buttonsY, btnW3, btnH2, 6);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "#dc3545";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "#dc3545";
|
||||
ctx.fillText(" CANCEL", 15 + btnW2 + btnGap2 + btnW2 / 2, buttonsY + btnH2 / 2 + 4);
|
||||
ctx.fillText("❌ CANCEL", cancelBtnX + btnW3/2, buttonsY + btnH2/2 + 4);
|
||||
};
|
||||
|
||||
nodeType.prototype.onMouseDown = function(event, pos) {
|
||||
@@ -1624,16 +1691,37 @@ app.registerExtension({
|
||||
|
||||
if (pos && this.slidersY !== undefined) {
|
||||
const btnH2 = 30;
|
||||
const btnGap2 = 10;
|
||||
const btnW2 = (this.size[0] - 35) / 2;
|
||||
const btnGap2 = 8;
|
||||
const btnW3 = (this.size[0] - 46) / 3;
|
||||
const buttonsY = this.slidersY + 5 * 38 + 10;
|
||||
const y1 = buttonsY;
|
||||
|
||||
if (pos[0] >= 15 && pos[0] <= 15 + btnW2 && pos[1] >= y1 && pos[1] <= y1 + btnH2) {
|
||||
// BATCH CLICK
|
||||
const batchBtnX = 15;
|
||||
if (pos[0] >= batchBtnX && pos[0] <= batchBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2) {
|
||||
this.isBatchMode = !this.isBatchMode;
|
||||
console.log(`[RS Adjustments] Node ${this.id}: Batch toggled to ${this.isBatchMode}`);
|
||||
if (!this.isBatchMode) {
|
||||
this._batchParams = null;
|
||||
}
|
||||
this.setDirtyCanvas(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// APPLY CLICK
|
||||
const applyBtnX = batchBtnX + btnW3 + btnGap2;
|
||||
if (pos[0] >= applyBtnX && pos[0] <= applyBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2) {
|
||||
if (this.isBatchMode) {
|
||||
this._batchParams = JSON.parse(JSON.stringify(this.adjustments));
|
||||
}
|
||||
this._sendAdjustments();
|
||||
return true;
|
||||
}
|
||||
if (pos[0] >= 15 + btnW2 + btnGap2 && pos[0] <= 15 + btnW2 + btnGap2 + btnW2 && pos[1] >= y1 && pos[1] <= y1 + btnH2) {
|
||||
|
||||
// CANCEL CLICK
|
||||
const cancelBtnX = applyBtnX + btnW3 + btnGap2;
|
||||
if (pos[0] >= cancelBtnX && pos[0] <= cancelBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2) {
|
||||
this.isBatchMode = false;
|
||||
this._batchParams = null;
|
||||
this._cancelEditing();
|
||||
return true;
|
||||
}
|
||||
@@ -1696,17 +1784,24 @@ app.registerExtension({
|
||||
|
||||
if (pos && this.slidersY !== undefined) {
|
||||
const btnH2 = 30;
|
||||
const btnGap2 = 10;
|
||||
const btnW2 = (this.size[0] - 35) / 2;
|
||||
const btnGap2 = 8;
|
||||
const btnW3 = (this.size[0] - 46) / 3;
|
||||
const buttonsY = this.slidersY + 5 * 38 + 10;
|
||||
const y1 = buttonsY;
|
||||
|
||||
const prevBatch = this.btnBatchHover;
|
||||
const batchBtnX = 15;
|
||||
this.btnBatchHover = pos[0] >= batchBtnX && pos[0] <= batchBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2;
|
||||
|
||||
const prevApply = this.btnApplyHover;
|
||||
const prevCancel = this.btnCancelHover;
|
||||
this.btnApplyHover = pos[0] >= 15 && pos[0] <= 15 + btnW2 && pos[1] >= y1 && pos[1] <= y1 + btnH2;
|
||||
this.btnCancelHover = pos[0] >= 15 + btnW2 + btnGap2 && pos[0] <= 15 + btnW2 + btnGap2 + btnW2 && pos[1] >= y1 && pos[1] <= y1 + btnH2;
|
||||
const applyBtnX = batchBtnX + btnW3 + btnGap2;
|
||||
const cancelBtnX = applyBtnX + btnW3 + btnGap2;
|
||||
this.btnApplyHover = pos[0] >= applyBtnX && pos[0] <= applyBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2;
|
||||
this.btnCancelHover = pos[0] >= cancelBtnX && pos[0] <= cancelBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2;
|
||||
|
||||
if (prevApply !== this.btnApplyHover || prevCancel !== this.btnCancelHover) this.setDirtyCanvas(true);
|
||||
if (prevBatch !== this.btnBatchHover || prevApply !== this.btnApplyHover || prevCancel !== this.btnCancelHover) {
|
||||
this.setDirtyCanvas(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user