Skip to content

Commit 2e05037

Browse files
jtydhr88guill
andauthored
range type (#13322)
Co-authored-by: guill <jacob.e.segal@gmail.com>
1 parent 00d2f40 commit 2e05037

4 files changed

Lines changed: 112 additions & 0 deletions

File tree

comfy_api/input/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
CurveInput,
1010
MonotoneCubicCurve,
1111
LinearCurve,
12+
RangeInput,
1213
)
1314

1415
__all__ = [
@@ -21,4 +22,5 @@
2122
"CurveInput",
2223
"MonotoneCubicCurve",
2324
"LinearCurve",
25+
"RangeInput",
2426
]

comfy_api/latest/_input/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .basic_types import ImageInput, AudioInput, MaskInput, LatentInput
22
from .curve_types import CurvePoint, CurveInput, MonotoneCubicCurve, LinearCurve
3+
from .range_types import RangeInput
34
from .video_types import VideoInput
45

56
__all__ = [
@@ -12,4 +13,5 @@
1213
"CurveInput",
1314
"MonotoneCubicCurve",
1415
"LinearCurve",
16+
"RangeInput",
1517
]
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
import math
5+
import numpy as np
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
class RangeInput:
11+
"""Represents a levels/range adjustment: input range [min, max] with
12+
optional midpoint (gamma control).
13+
14+
Generates a 1D LUT identical to GIMP's levels mapping:
15+
1. Normalize input to [0, 1] using [min, max]
16+
2. Apply gamma correction: pow(value, 1/gamma)
17+
3. Clamp to [0, 1]
18+
19+
The midpoint field is a position in [0, 1] representing where the
20+
midtone falls within [min, max]. It maps to gamma via:
21+
gamma = -log2(midpoint)
22+
So midpoint=0.5 → gamma=1.0 (linear).
23+
"""
24+
25+
def __init__(self, min_val: float, max_val: float, midpoint: float | None = None):
26+
self.min_val = min_val
27+
self.max_val = max_val
28+
self.midpoint = midpoint
29+
30+
@staticmethod
31+
def from_raw(data) -> RangeInput:
32+
if isinstance(data, RangeInput):
33+
return data
34+
if isinstance(data, dict):
35+
return RangeInput(
36+
min_val=float(data.get("min", 0.0)),
37+
max_val=float(data.get("max", 1.0)),
38+
midpoint=float(data["midpoint"]) if data.get("midpoint") is not None else None,
39+
)
40+
raise TypeError(f"Cannot convert {type(data)} to RangeInput")
41+
42+
def to_lut(self, size: int = 256) -> np.ndarray:
43+
"""Generate a float64 lookup table mapping [0, 1] input through this
44+
levels adjustment.
45+
46+
The LUT maps normalized input values (0..1) to output values (0..1),
47+
matching the GIMP levels formula.
48+
"""
49+
xs = np.linspace(0.0, 1.0, size, dtype=np.float64)
50+
51+
in_range = self.max_val - self.min_val
52+
if abs(in_range) < 1e-10:
53+
return np.where(xs >= self.min_val, 1.0, 0.0).astype(np.float64)
54+
55+
# Normalize: map [min, max] → [0, 1]
56+
result = (xs - self.min_val) / in_range
57+
result = np.clip(result, 0.0, 1.0)
58+
59+
# Gamma correction from midpoint
60+
if self.midpoint is not None and self.midpoint > 0 and self.midpoint != 0.5:
61+
gamma = max(-math.log2(self.midpoint), 0.001)
62+
inv_gamma = 1.0 / gamma
63+
mask = result > 0
64+
result[mask] = np.power(result[mask], inv_gamma)
65+
66+
return result
67+
68+
def __repr__(self) -> str:
69+
mid = f", midpoint={self.midpoint}" if self.midpoint is not None else ""
70+
return f"RangeInput(min={self.min_val}, max={self.max_val}{mid})"

comfy_api/latest/_io.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,6 +1266,43 @@ class Histogram(ComfyTypeIO):
12661266
Type = list[int]
12671267

12681268

1269+
@comfytype(io_type="RANGE")
1270+
class Range(ComfyTypeIO):
1271+
from comfy_api.input import RangeInput
1272+
if TYPE_CHECKING:
1273+
Type = RangeInput
1274+
1275+
class Input(WidgetInput):
1276+
def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None,
1277+
socketless: bool=True, default: dict=None,
1278+
display: str=None,
1279+
gradient_stops: list=None,
1280+
show_midpoint: bool=None,
1281+
midpoint_scale: str=None,
1282+
value_min: float=None,
1283+
value_max: float=None,
1284+
advanced: bool=None):
1285+
super().__init__(id, display_name, optional, tooltip, None, default, socketless, None, None, None, None, advanced)
1286+
if default is None:
1287+
self.default = {"min": 0.0, "max": 1.0}
1288+
self.display = display
1289+
self.gradient_stops = gradient_stops
1290+
self.show_midpoint = show_midpoint
1291+
self.midpoint_scale = midpoint_scale
1292+
self.value_min = value_min
1293+
self.value_max = value_max
1294+
1295+
def as_dict(self):
1296+
return super().as_dict() | prune_dict({
1297+
"display": self.display,
1298+
"gradient_stops": self.gradient_stops,
1299+
"show_midpoint": self.show_midpoint,
1300+
"midpoint_scale": self.midpoint_scale,
1301+
"value_min": self.value_min,
1302+
"value_max": self.value_max,
1303+
})
1304+
1305+
12691306
DYNAMIC_INPUT_LOOKUP: dict[str, Callable[[dict[str, Any], dict[str, Any], tuple[str, dict[str, Any]], str, list[str] | None], None]] = {}
12701307
def register_dynamic_input_func(io_type: str, func: Callable[[dict[str, Any], dict[str, Any], tuple[str, dict[str, Any]], str, list[str] | None], None]):
12711308
DYNAMIC_INPUT_LOOKUP[io_type] = func
@@ -2276,5 +2313,6 @@ def as_dict(self):
22762313
"BoundingBox",
22772314
"Curve",
22782315
"Histogram",
2316+
"Range",
22792317
"NodeReplace",
22802318
]

0 commit comments

Comments
 (0)