Add Presets

This commit is contained in:
ialhabbal
2026-05-01 07:43:14 +03:00
parent 00a6931681
commit 6c1f2641cc
6 changed files with 423 additions and 37 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
__pycache__/
Archive/
desktop.ini
node.zip
node.zip
.ruf_cache
+62 -4
View File
@@ -2,7 +2,7 @@
A ComfyUI node that turns clean AI-generated portraits and photos into images that look like they were shot on real film, edited in a darkroom, or simply lived-in and human. It combines classic photo effects (compression artifacts, grain, vignette, color grading) with a full suite of face skin effects that break the plastic, over-smooth look common in AI art.
![PhotoLab](https://raw.githubusercontent.com/ialhabbal/PhotoLab/main/media/PhotoLab_.png)
<img width="1684" height="780" alt="workflow" src="https://github.com/user-attachments/assets/83f5b134-5b3d-48b8-a81a-a964d14e374c" />
---
@@ -16,11 +16,14 @@ The node has a lot of inputs, but most of them default to `0` (disabled). A good
| `grain_strength` | 12 | Light film grain |
| `color_grade` | Faded | Lifts blacks like old film |
| `color_grade_strength` | 40 | Keeps it subtle |
| `mask_mode` | Face Only | Confine skin effects to the face when a mask is connected |
| `skin_texture_strength` | 35 | Breaks the plastic-skin look |
| `pores_strength` | 30 | Adds visible pore detail |
| `sss_strength` | 18 | Warm inner glow under skin |
| `skin_redness_strength` | 20 | Natural cheek/nose flush |
> **New to the node?** Click one of the built-in **preset buttons** at the bottom of the node to instantly load a starting point, then tweak from there.
---
## All Settings Explained
@@ -77,7 +80,7 @@ How strong the blur is. Has no effect if `blur_type` is None.
---
### Lighting Match
### Lighting Match & Mask Mode
These settings let you transfer the lighting mood from one image onto another — useful for making a generated portrait match the lighting of a reference photo.
@@ -95,6 +98,19 @@ Connect any image here to use as the lighting reference. Only active when `light
---
**`mask_mode`** *(Face Only / Inverted / Disabled, default Face Only)*
Controls how the connected `face_mask` is applied, **and acts as a master on/off switch for all skin effects below it**.
| Value | Behaviour |
|---|---|
| **Face Only** | All skin effects are confined to the white (masked) region — great for keeping effects on the face only when a mask is connected |
| **Inverted** | Skin effects are applied *outside* the mask — useful for adding texture to the body or neck while leaving the face untouched |
| **Disabled** | **All skin effect widgets are completely skipped.** Only the global effects above (colour grade, blur, grain, vignette, compression) run. Use this when you want a pure film/grade look with no skin processing |
> When `mask_mode` is **Disabled**, none of the skin sliders below it have any effect regardless of their values. This is the fastest way to use the node as a global-only effects unit without zeroing every skin slider manually.
---
### Face Skin Effects
These effects are designed specifically to fix the over-smooth, plastic look of AI-generated faces. They work best when combined with a **face mask** input so they only apply to skin and not the background or hair.
@@ -194,12 +210,53 @@ Simulates the natural oil/sebum shine of the T-zone (forehead, nose bridge, chin
Controls the random pattern for all procedural skin effects (pores, freckles, blemishes, acne, peach fuzz, shine). The same seed always produces the same pattern on the same image. Change this number to get a completely different arrangement while keeping all your strength settings. Useful when generating multiple images of the same character.
**`face_mask`** *(optional mask input)*
Connect a face mask here to restrict all skin effects to the face only, leaving the background, hair, and clothing untouched. Any ComfyUI mask node works — face detection segmenters (like BiSeNet), hand-painted masks, or SAM segments. The mask edges are automatically feathered so the transition is seamless.
Connect a face mask here to spatially control where skin effects apply. Behaviour depends on `mask_mode`:
- **Face Only** — effects apply inside the mask (face skin only)
- **Inverted** — effects apply outside the mask (body / neck skin)
- **Disabled** — this input is ignored entirely; all skin effects are skipped
Any ComfyUI mask node works — face detection segmenters (like BiSeNet), hand-painted masks, or SAM segments. The mask edges are automatically feathered for a seamless blend.
When a mask is connected, it is also passed through as the second output (`face_mask`) so you can reuse it in other nodes without needing a second mask node.
---
## Presets
The node includes a built-in preset system directly on the node panel, split into two collapsible sections. Click any section header (▼/▶) to expand or collapse it. The active preset is highlighted with a **●** marker so you always know which one is loaded.
### Global Presets
Affect colour grade, grain, blur, compression, and vignette. Skin effects are intentionally left unchanged so these can be freely combined with any face preset.
| Preset | Description |
|---|---|
| 📷 Film Snapshot | Faded warm tones, grain, soft vignette — classic point-and-shoot feel |
| 🎞 Darkroom B&W | Desaturated, high-contrast with heavy grain — analogue darkroom aesthetic |
| ❄️ Cool Editorial | Crisp cool tones, minimal grain — modern editorial / fashion look |
| 🟤 Sepia Vintage | Full sepia with soft vignette and light compression artifacts |
| 🌅 Golden Hour | Rich warm saturation boost — sunset / golden hour atmosphere |
| 📼 Lo-Fi Degraded | Aggressive compression + pixelation + grain — VHS / lo-fi look |
| 🌫 Dreamy Soft Focus | Soft-focus blur with faded lift — hazy, ethereal portrait look |
### Face Presets
Affect skin effect widgets only. `mask_mode` is set to **Face Only** so effects are automatically confined to the face when a mask is connected.
| Preset | Description |
|---|---|
| ✨ Natural Skin | Subtle texture and SSS — realistic, non-plastic skin |
| 🔬 High-Detail Skin | Macro-level pores, texture, fuzz — great for close-up hero shots |
| 🌸 Freckled & Rosy | Light freckles with rosy cheeks — fair to medium skin types |
| 🩸 Acne Breakout | Moderate acne with blemishes — realistic skin condition portrayal |
| 🌟 Oily T-Zone | Sebum shine on forehead, nose and chin — natural oily-skin sheen |
| 👴 Aged Complexion | Heavy texture, blemishes and visible pores — mature / aged skin |
### My Presets (User-Saved)
Click **+ Save Current as Preset** to snapshot all current widget values under a custom name. Saved presets are stored in your browser's `localStorage` and persist across sessions. To remove a preset click the **✕ Delete** button that appears beneath it.
> Global and Face presets only change their own group of settings — click one from each section to combine them.
---
## Outputs
| Output | Type | Description |
@@ -258,7 +315,8 @@ pores_strength: 20
## Notes
- Skin effects apply to the **entire image** by default. Connect a `face_mask` to restrict them to the face.
- **`mask_mode`** is a master switch for all skin effects. Set it to **Disabled** to skip every skin slider and run the node as a global-only effects unit. Set to **Inverted** to apply skin effects outside the mask instead of inside.
- Skin effects apply to the **entire image** when no `face_mask` is connected and `mask_mode` is not Disabled. Connect a `face_mask` to restrict them to the face.
- Effects are applied in a fixed order: lighting match → color grade → pixelate → skin effects → grain → blur → vignette → JPEG compression. The JPEG pass happens last so compression interacts with all effects.
- The `skin_seed` affects pores, freckles, blemishes, acne, peach fuzz, and sebum shine. It does **not** affect film grain (use a different `grain_strength` value for variation there).
- Skin tone is automatically detected from the centre of the image and used to tune freckle colour, SSS tint, and peach fuzz colour. No manual skin tone setting is needed.
+322 -17
View File
@@ -2,11 +2,19 @@
* photo_lab.js
*
* ComfyUI frontend extension for the PhotoLab node.
* Adds a "↺ Reset to Defaults" button widget inside the node.
*
* Features
* --------
* - ↺ Reset to Defaults
* - Built-in Global and Face preset buttons (collapsible sections)
* - User presets saved to localStorage — persist across sessions
* · Each user preset has its own Apply button + a separate ✕ Delete button
* · Active preset is highlighted with a ● marker
*/
import { app } from "/scripts/app.js";
// ─── Default widget values ────────────────────────────────────────────────────
const WIDGET_DEFAULTS = {
quality: 70,
passes: 1,
@@ -20,6 +28,7 @@ const WIDGET_DEFAULTS = {
blur_strength: 0,
lighting_match_mode: "Disabled",
lighting_match_strength: 1.0,
mask_mode: "Face Only",
pores_strength: 0,
blemishes_strength: 0,
acne_strength: 0,
@@ -37,28 +46,324 @@ const WIDGET_DEFAULTS = {
skin_texture_opacity: 100,
};
// ─── Built-in presets ─────────────────────────────────────────────────────────
// Each preset lists only the keys it changes; everything else is left untouched.
const GLOBAL_PRESETS = [
{
label: "📷 Film Snapshot",
desc: "Faded warm tones, grain, soft vignette — classic point-and-shoot feel.",
values: { quality:65, passes:2, grain_strength:18, vignette_strength:22,
color_grade:"Warm", color_grade_strength:55, saturation:80,
blur_type:"None", blur_strength:0 },
},
{
label: "🎞 Darkroom B&W",
desc: "Desaturated, high-contrast with heavy grain — analogue darkroom aesthetic.",
values: { quality:80, passes:1, grain_strength:30, vignette_strength:35,
color_grade:"Faded", color_grade_strength:70, saturation:0,
blur_type:"None", blur_strength:0 },
},
{
label: "❄️ Cool Editorial",
desc: "Crisp cool tones, minimal grain — modern editorial / fashion look.",
values: { quality:90, passes:1, grain_strength:6, vignette_strength:10,
color_grade:"Cool", color_grade_strength:45, saturation:88,
blur_type:"None", blur_strength:0 },
},
{
label: "🟤 Sepia Vintage",
desc: "Full sepia with soft vignette and light compression artifacts.",
values: { quality:65, passes:2, grain_strength:14, vignette_strength:28,
color_grade:"Sepia", color_grade_strength:90, saturation:60,
blur_type:"None", blur_strength:0 },
},
{
label: "🌅 Golden Hour",
desc: "Rich warm saturation boost — sunset / golden hour atmosphere.",
values: { quality:85, passes:1, grain_strength:8, vignette_strength:18,
color_grade:"Warm", color_grade_strength:75, saturation:120,
blur_type:"None", blur_strength:0 },
},
{
label: "📼 Lo-Fi Degraded",
desc: "Aggressive compression + pixelation + grain — VHS / lo-fi look.",
values: { quality:30, passes:4, pixelate_strength:12, grain_strength:35,
vignette_strength:40, color_grade:"Faded", color_grade_strength:60,
saturation:65, blur_type:"None", blur_strength:0 },
},
{
label: "🌫 Dreamy Soft Focus",
desc: "Soft-focus blur with faded lift — hazy, ethereal portrait look.",
values: { quality:82, passes:1, grain_strength:10, vignette_strength:20,
color_grade:"Faded", color_grade_strength:40, saturation:75,
blur_type:"Soft Focus", blur_strength:28 },
},
];
const FACE_PRESETS = [
{
label: "✨ Natural Skin",
desc: "Subtle texture and SSS — realistic, non-plastic skin.",
values: { mask_mode:"Face Only", skin_texture_strength:25, skin_texture_opacity:85,
pores_strength:20, pores_opacity:70, sss_strength:12,
skin_redness_strength:8, peach_fuzz_strength:10 },
},
{
label: "🔬 High-Detail Skin",
desc: "Macro-level pores, texture, fuzz — great for close-up hero shots.",
values: { mask_mode:"Face Only", skin_texture_strength:45, skin_texture_opacity:100,
pores_strength:55, pores_opacity:90, sss_strength:20,
skin_redness_strength:15, peach_fuzz_strength:30, sebum_shine_strength:18 },
},
{
label: "🌸 Freckled & Rosy",
desc: "Light freckles with rosy cheeks — fair to medium skin types.",
values: { mask_mode:"Face Only", freckles_strength:35, freckles_opacity:80,
skin_redness_strength:28, sss_strength:14,
skin_texture_strength:18, skin_texture_opacity:75, peach_fuzz_strength:12 },
},
{
label: "🩸 Acne Breakout",
desc: "Moderate acne with blemishes — realistic skin condition portrayal.",
values: { mask_mode:"Face Only", acne_strength:40, acne_opacity:90,
blemishes_strength:30, blemishes_opacity:75, skin_redness_strength:35,
pores_strength:30, pores_opacity:80, skin_texture_strength:20 },
},
{
label: "🌟 Oily T-Zone",
desc: "Sebum shine on forehead, nose and chin — natural oily-skin sheen.",
values: { mask_mode:"Face Only", sebum_shine_strength:40, skin_redness_strength:12,
sss_strength:18, pores_strength:25, pores_opacity:65, skin_texture_strength:22 },
},
{
label: "👴 Aged Complexion",
desc: "Heavy texture, blemishes and visible pores — mature / aged skin.",
values: { mask_mode:"Face Only", skin_texture_strength:60, skin_texture_opacity:100,
pores_strength:65, pores_opacity:100, blemishes_strength:45,
blemishes_opacity:80, skin_redness_strength:30, sss_strength:10, peach_fuzz_strength:8 },
},
];
// ─── localStorage key ─────────────────────────────────────────────────────────
const LS_KEY = "PhotoLab_userPresets_v1";
function loadUserPresets() { try { return JSON.parse(localStorage.getItem(LS_KEY) || "[]"); } catch { return []; } }
function saveUserPresets(a) { localStorage.setItem(LS_KEY, JSON.stringify(a)); }
// ─── Apply / capture helpers ──────────────────────────────────────────────────
function applyPreset(node, values) {
for (const w of node.widgets) {
if (Object.prototype.hasOwnProperty.call(values, w.name)) {
w.value = values[w.name];
w.callback?.(values[w.name], app.canvas, node);
}
}
app.graph.setDirtyCanvas(true, true);
}
function captureValues(node) {
const out = {};
for (const w of node.widgets) {
if (w.type !== "button") out[w.name] = w.value;
}
return out;
}
// ─── Active-preset highlight state ────────────────────────────────────────────
//
// We track which button widget is currently "active" so we can prefix its
// label with ● and clear the previous one when a new preset is selected.
//
// Since ComfyUI button widgets render their `name` as the visible label,
// we store the original name alongside a flag and swap the name on activate /
// deactivate.
let _activePresetWidget = null; // the currently highlighted button widget
let _activePresetLabel = ""; // its original label (without ●)
function activatePreset(node, btnWidget, originalLabel, values) {
// Clear previous highlight
if (_activePresetWidget && _activePresetWidget !== btnWidget) {
_activePresetWidget.name = _activePresetLabel;
}
// Set new highlight
_activePresetWidget = btnWidget;
_activePresetLabel = originalLabel;
btnWidget.name = `● ${originalLabel}`;
applyPreset(node, values);
app.graph.setDirtyCanvas(true, true);
}
function clearActiveHighlight() {
if (_activePresetWidget) {
_activePresetWidget.name = _activePresetLabel;
_activePresetWidget = null;
_activePresetLabel = "";
}
}
// ─── Section builder ──────────────────────────────────────────────────────────
//
// Adds a collapsible section to the node with one button widget per preset.
// The toggle header button hides/shows the row buttons by setting w.hidden.
//
// @param tag string — unique identifier used to tag widgets
// @param title string — section header text
// @param presets array — [{label, desc, values}]
// @param startOpen bool
//
// Returns an array of the added button widgets (not the header).
function addSection(node, tag, title, presets, startOpen) {
let open = startOpen;
const header = node.addWidget("button",
`${open ? "▼" : "▶"} ${title}`, null,
() => {
open = !open;
header.name = `${open ? "▼" : "▶"} ${title}`;
for (const w of node.widgets) {
if (w._plTag === tag) w.hidden = !open;
}
node.setSize(node.computeSize());
app.graph.setDirtyCanvas(true, true);
},
{ serialize: false }
);
header._plHeader = true;
const btns = [];
for (const p of presets) {
const btn = node.addWidget("button", p.label, null,
() => activatePreset(node, btn, p.label, p.values),
{ serialize: false, tooltip: p.desc || "" }
);
btn._plTag = tag;
btn.hidden = !open;
btns.push(btn);
}
return btns;
}
// ─── Extension ────────────────────────────────────────────────────────────────
app.registerExtension({
name: "PhotoLab.ResetDefaults",
name: "PhotoLab.PresetsV3",
async nodeCreated(node) {
if (node.comfyClass !== "PhotoLab" && node.type !== "PhotoLab") return;
node.addWidget(
"button",
"↺ Reset to Defaults",
null,
() => {
for (const widget of node.widgets) {
const def = WIDGET_DEFAULTS[widget.name];
if (def === undefined) continue;
widget.value = def;
widget.callback?.(def, app.canvas, node);
}
app.graph.setDirtyCanvas(true, true);
},
{ serialize: false }
);
// Guard: ComfyUI fires nodeCreated both on first creation and on
// workflow load/deserialisation. Without this check every widget is
// added twice, producing two full sets of buttons.
if (node._plWidgetsAdded) return;
node._plWidgetsAdded = true;
// ── Reset to defaults ──────────────────────────────────────────────
node.addWidget("button", "↺ Reset to Defaults", null, () => {
clearActiveHighlight();
for (const w of node.widgets) {
const def = WIDGET_DEFAULTS[w.name];
if (def !== undefined) { w.value = def; w.callback?.(def, app.canvas, node); }
}
app.graph.setDirtyCanvas(true, true);
}, { serialize: false });
// ── Global presets ─────────────────────────────────────────────────
addSection(node, "global", "Global Presets", GLOBAL_PRESETS, true);
// ── Face presets ───────────────────────────────────────────────────
addSection(node, "face", "Face Presets", FACE_PRESETS, false);
// ── User presets ───────────────────────────────────────────────────
// Layout per user preset:
// [ Apply — preset label (full width) ]
// [ ✕ Delete (compact, below apply) ]
//
// Keeping apply and delete as completely separate widgets with
// unambiguous click targets avoids the unreliable pos hit-test.
//
// The section is fully rebuilt after every save or delete so indices
// stay consistent.
function rebuildUser() {
// Remove all widgets that belong to the user section
node.widgets = node.widgets.filter(w => !w._plUser);
const all = loadUserPresets();
// ── Section toggle header ──────────────────────────────────────
let open = true;
const header = node.addWidget("button",
`▼ My Presets (${all.length})`, null,
() => {
open = !open;
header.name = `${open ? "▼" : "▶"} My Presets (${all.length})`;
for (const w of node.widgets) {
if (w._plUser && !w._plUserHeader) w.hidden = !open;
}
node.setSize(node.computeSize());
app.graph.setDirtyCanvas(true, true);
},
{ serialize: false }
);
header._plUser = true;
header._plUserHeader = true;
// ── Save current settings as a new preset ──────────────────────
const saveBtn = node.addWidget("button", "+ Save Current as Preset", null,
() => {
const name = prompt(
"Name your preset:",
`My Preset ${loadUserPresets().length + 1}`
);
if (!name) return;
const fresh = loadUserPresets();
fresh.push({ label: `★ ${name}`, desc: "User preset", values: captureValues(node) });
saveUserPresets(fresh);
rebuildUser();
},
{ serialize: false }
);
saveBtn._plUser = true;
// ── One Apply + one Delete widget per user preset ──────────────
// Apply and Delete are separate sequential widgets.
// ComfyUI renders them on separate rows, which gives each a full
// unambiguous click target — no pos coordinate math needed.
for (let i = 0; i < all.length; i++) {
const p = all[i];
// Apply button — full label, activates highlight
const applyBtn = node.addWidget("button", p.label, null,
() => activatePreset(node, applyBtn, p.label, p.values),
{ serialize: false, tooltip: "Click to apply this preset" }
);
applyBtn._plUser = true;
// Delete button — compact, indented label
const delBtn = node.addWidget("button", ` ✕ Delete "${p.label}"`, null,
() => {
// If this preset was active, clear the highlight
if (_activePresetWidget === applyBtn) clearActiveHighlight();
const fresh = loadUserPresets();
fresh.splice(i, 1);
saveUserPresets(fresh);
rebuildUser();
},
{ serialize: false, tooltip: "Delete this preset" }
);
delBtn._plUser = true;
}
node.setSize(node.computeSize());
app.graph.setDirtyCanvas(true, true);
}
rebuildUser();
node.setSize(node.computeSize());
},
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

+36 -14
View File
@@ -96,6 +96,16 @@ class PhotoLab:
"step": 0.05,
"tooltip": "Blend between original (0.0) and fully matched lighting (1.0)"
}),
"mask_mode": (["Face Only", "Inverted", "Disabled"], {
"default": "Face Only",
"tooltip": (
"Controls the face_mask and ALL skin effects below this widget. "
"'Face Only' — skin effects apply only inside the connected mask. "
"'Inverted' — skin effects apply only outside the mask (e.g. body skin). "
"'Disabled' — ALL skin effect widgets below are completely skipped; "
"only global effects above (color grade, blur, grain, vignette, compression) run."
)
}),
"pores_strength": ("INT", {
"default": 0,
"min": 0,
@@ -260,11 +270,10 @@ class PhotoLab:
}),
"face_mask": ("MASK", {
"tooltip": (
"Optional grayscale mask (0 = no effect, 1 = full effect) that confines all five "
"face skin effects (pores, blemishes, acne, freckles, skin texture) to the masked "
"region only. Connect any mask node — e.g. a face-detection segmenter or a hand-"
"painted mask — to restrict skin effects to the face and leave the background "
"untouched. When omitted, skin effects apply to the entire image as before."
"Optional grayscale mask (0 = no effect, 1 = full effect) used to spatially "
"restrict skin effects. Behaviour set by mask_mode: 'Face Only' keeps effects "
"inside the mask; 'Inverted' keeps them outside; 'Disabled' skips all skin "
"effects entirely regardless of this input."
)
}),
},
@@ -277,8 +286,10 @@ class PhotoLab:
DESCRIPTION = (
"Apply photo lab effects including JPG compression artifacts, film grain, vignette, blur, and color grading. "
"Optionally matches the lighting and shadows of a reference image. "
"Skin effects (pores, freckles, blemishes, acne, texture, SSS, peach fuzz, redness, shine) are "
"confined to an optional face mask, which is also passed through as a second output. "
"Skin effects (pores, freckles, blemishes, acne, texture, SSS, peach fuzz, redness, shine) sit below "
"the mask_mode widget and are fully skipped when mask_mode is 'Disabled', spatially confined to a "
"connected face mask when 'Face Only', or applied outside the mask when 'Inverted'. "
"The mask is also passed through as a second output. "
"A seed input controls all procedural skin effect patterns for reproducibility."
)
@@ -286,6 +297,7 @@ class PhotoLab:
vignette_strength=0, color_grade="Faded", color_grade_strength=50,
saturation=70, blur_type="None", blur_strength=0,
lighting_match_mode="Disabled", lighting_match_strength=1.0,
mask_mode="Face Only",
pores_strength=0, blemishes_strength=0, acne_strength=0,
freckles_strength=0, skin_texture_strength=0,
sss_strength=0, peach_fuzz_strength=0,
@@ -348,8 +360,10 @@ class PhotoLab:
if lighting_match_mode != "Disabled" and reference_image is not None:
ref_np = reference_image[0].cpu().numpy().astype(np.float32)
# Determine whether any skin effect is active (avoids mask work when unused)
any_skin_effect = any([
# Skin effects are entirely skipped when mask_mode is "Disabled".
# This lets the node act as a pure global-effects node without needing
# to zero out every individual skin slider.
any_skin_effect = mask_mode != "Disabled" and any([
skin_texture_strength > 0, pores_strength > 0, freckles_strength > 0,
blemishes_strength > 0, acne_strength > 0, sss_strength > 0,
peach_fuzz_strength > 0, skin_redness_strength > 0, sebum_shine_strength > 0,
@@ -451,11 +465,13 @@ class PhotoLab:
seed=skin_seed + 6
)
# If a face mask was supplied, composite: result = skin*mask + original*(1-mask)
# This restricts all skin effects exclusively to the masked region.
# Composite skin effects back through the mask.
# mask_mode "Disabled" never reaches here (any_skin_effect is False).
# "Face Only": apply inside mask. "Inverted": apply outside mask.
if face_mask is not None:
img_pil = self._composite_with_mask(
img_before_skin, img_pil, face_mask, i
img_before_skin, img_pil, face_mask, i,
invert=(mask_mode == "Inverted")
)
if grain_strength > 0:
@@ -607,10 +623,10 @@ class PhotoLab:
elif l_mean > 38: return 5
else: return 6
def _composite_with_mask(self, original_pil, processed_pil, face_mask, batch_index):
def _composite_with_mask(self, original_pil, processed_pil, face_mask, batch_index, invert=False):
"""
Alpha-composite the skin-processed image over the original using the
face mask, so skin effects only appear inside the masked region.
face mask, so skin effects only appear inside (or outside) the masked region.
Args:
original_pil : PIL.Image — image before any skin effects
@@ -618,6 +634,8 @@ class PhotoLab:
face_mask : torch.Tensor — ComfyUI MASK, shape [B, H, W] or [B, 1, H, W],
values in [0, 1] where 1 = fully apply skin effect
batch_index : int — which batch element to use from face_mask
invert : bool — flip the mask before compositing so skin effects
apply *outside* the masked region (mask_mode "Inverted")
Returns:
PIL.Image composited result
@@ -643,6 +661,10 @@ class PhotoLab:
mask_np = gaussian_filter(mask_np, sigma=1.5)
mask_np = np.clip(mask_np, 0.0, 1.0)
# Flip for "Inverted" mode: apply skin effects outside the masked region
if invert:
mask_np = 1.0 - mask_np
# --- composite: out = processed * mask + original * (1 - mask) -----
orig_arr = np.array(original_pil, dtype=np.float32)
proc_arr = np.array(processed_pil, dtype=np.float32)
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "PhotoLab"
description = "ComfyUI node that applies effects to generated images like JPG compression, pixelation, color grading, film grain, vignette, blur, and optional lighting matching with a reference image, and many more"
version = "2.0.0"
version = "2.5.0"
license = { file = "LICENSE" }
dependencies = [
"numpy",