Nodes/Jovi_GLSL/GRAYSCALE (JOV_GL)
ComfyUI Node

GRAYSCALE (JOV_GL)

Grayscale with the weights you control — NTSC weights by default, Rec. 709 if you want

By Amorano·Created 2 years ago·Updated about a year ago· 20
GRAYSCALE (JOV_GL)
  • image
  • convert
  • RGBA
  • RGB
  • MASK
FRAGMENT// name: GRAYSCALE // desc: Convert input to grayscale // category: COLOR // default grayscale using NTSC conversion weights uniform sampler2D image; // | MASK, RGB or RGBA uniform vec3 convert; // 0.299, 0.587, 0.114; 0; 1; 0.01 | Scalar for each channel void mainImage( out vec4 fragColor, vec2 fragCoord ) { vec2 uv = fragCoord / iResolution.xy; vec4 color = texture(image, uv); vec3 gray = vec3(dot(color.rgb, convert)); fragColor = vec4(gray, color.a); }

Desaturating an image is the easiest thing in the world right up until you care what the result looks like. Average the channels and you get muddy grays where the image looked colorful. The reason is that plain averaging weights red, green, and blue equally, when human vision doesn't - green is brightest to us, blue the dimmest. This node is the "do it right" version, with a single VEC3 input that tells it how much each channel contributes. The default is the classic NTSC set - 0.299, 0.587, 0.114 - and you can swap in whatever weights your pipeline needs.

How it works

For each pixel it computes dot(rgb, convert) - the RGB vector dotted against your weight vector - and writes the resulting scalar to all three channels, preserving the input's alpha. It's a one-line fragment shader, which is the whole beauty of it: GPU-fast, deterministic, and trivially easy to reason about. There are no modes, no sliders, no curve controls. Just weights.

The convert VEC3 defaults to NTSC weights, and the field's tooltip says "Scalar for each channel." If you want Rec. 709 luma instead - the standard for modern video - use 0.2126, 0.7152, 0.0722. The difference is subtle on most images and obvious on saturated reds and blues.

The inputs

  • image - RGB, RGBA, or MASK input. If you feed it a mask you get a gray mask back.
  • convert - the three channel weights, default (0.299, 0.587, 0.114).

Outputs are the pack's trio: RGBA, RGB, and MASK. The MASK output is quietly the most useful one - a well-weighted grayscale is a great luma mask for compositing, so this node doubles as a "luminance selection" tool.

Install

One install covers the pack. Via ComfyUI Manager, search Jovi_GLSL; or:

cd ComfyUI/custom_nodes
git clone https://github.com/Amorano/Jovi_GLSL.git
cd Jovi_GLSL
pip install -r requirements.txt

Restart ComfyUI. Dependencies: PyOpenGL, glfw, opencv-contrib-python, cozy_comfyui from git. Needs an OpenGL context at runtime. No models, no keys. Under JOVI_GLSL 🌈 → COLOR.

Gotchas

Weights must sum to roughly 1.0 or your grayscale will get brighter or darker than the source - set all three to 1 and you've accidentally made a highlight detector. If your "grayscale" looks off, that's the first thing to check. And one nice property worth using: keep the alpha and this node is a perfectly good way to strip color from an RGBA image without losing your mask.

CategoryJOV_GL 🌈/COLOR

Inputs (3)

NameTypeDefaultDescription
imageoptIMAGEMASK, RGB or RGBA
convertoptVEC30.299,0.587,0.114Scalar for each channel
FRAGMENToptSTRING// name: GRAYSCALE // desc: Convert input to grayscale // category: COLOR // default grayscale using NTSC conversion weights uniform sampler2D image; // | MASK, RGB or RGBA uniform vec3 convert; // 0.299, 0.587, 0.114; 0; 1; 0.01 | Scalar for each channel void mainImage( out vec4 fragColor, vec2 fragCoord ) { vec2 uv = fragCoord / iResolution.xy; vec4 color = texture(image, uv); vec3 gray = vec3(dot(color.rgb, convert)); fragColor = vec4(gray, color.a); }

Outputs (3)

NameTypeDescription
RGBAIMAGEFull channel [RGBA] image. If there is an alpha, the image will be masked out with it when using this output.
RGBIMAGEThree channel [RGB] image. There will be no alpha.
MASKMASKSingle channel mask output.