Adding batch image processing mode to the RS Image Adjustments node

This commit is contained in:
Raykosan
2026-08-07 02:35:25 +03:00
parent e1008d59ab
commit 54ef5f393b
7 changed files with 170 additions and 36 deletions
File diff suppressed because one or more lines are too long
+46 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
};