Nodes/Jovi_GLSL/SOBEL (JOV_GL)
ComfyUI Node

SOBEL (JOV_GL)

Sobel edge detection as a node — the poor man's Canny, minus the thresholds

By Amorano·Created 2 years ago·Updated about a year ago· 20
SOBEL (JOV_GL)
  • image
  • RGBA
  • RGB
  • MASK
FRAGMENT// name: SOBEL // desc: Finds the edges of an image // category: FILTER uniform sampler2D image; // | RGB(A) image void mainImage( out vec4 fragColor, in vec2 fragCoord ) { float x = 1.0 / iResolution.x; float y = 1.0 / iResolution.y; vec2 uv = fragCoord / iResolution.xy; vec4 horizEdge = vec4( 0.0 ); horizEdge -= texture( image, vec2( uv.x - x, uv.y - y ) ) * 1.0; horizEdge -= texture( image, vec2( uv.x - x, uv.y ) ) * 2.0; horizEdge -= texture( image, vec2( uv.x - x, uv.y + y ) ) * 1.0; horizEdge += texture( image, vec2( uv.x + x, uv.y - y ) ) * 1.0; horizEdge += texture( image, vec2( uv.x + x, uv.y ) ) * 2.0; horizEdge += texture( image, vec2( uv.x + x, uv.y + y ) ) * 1.0; vec4 vertEdge = vec4( 0.0 ); vertEdge -= texture( image, vec2( uv.x - x, uv.y - y ) ) * 1.0; vertEdge -= texture( image, vec2( uv.x , uv.y - y ) ) * 2.0; vertEdge -= texture( image, vec2( uv.x + x, uv.y - y ) ) * 1.0; vertEdge += texture( image, vec2( uv.x - x, uv.y + y ) ) * 1.0; vertEdge += texture( image, vec2( uv.x , uv.y + y ) ) * 2.0; vertEdge += texture( image, vec2( uv.x + x, uv.y + y ) ) * 1.0; vec3 edge = sqrt((horizEdge.rgb * horizEdge.rgb) + (vertEdge.rgb * vertEdge.rgb)); fragColor = vec4( edge, texture( image, uv ).a ); }

If you've done any ControlNet work you know edge detection: feed it the right edge map and the model follows your structure instead of improvising it. Canny gets all the attention, but plain Sobel is the honest workhorse - it finds edges by measuring how fast brightness changes, and it does it with zero parameters to tune. This node is exactly that: a classic 3×3 Sobel operator, running on the GPU, no thresholds, no sliders. Slap it on an image and you get a readable edge map you can feed straight into an edge-conditioning workflow or use as a mask.

How it works

For each pixel it runs two 3×3 convolutions - one for horizontal edges, one for vertical - using the standard Sobel kernels, and combines them into a per-pixel edge strength. Because it's a convolution, it does real gradient math rather than "detect contrast by eye." The shader handles both directions in one pass, which is where the GPU does the heavy lifting. There's no thresholding, so you get the full gradient response - strong edges bright, weak edges dimmer - and it's up to you or your downstream nodes to decide where the cut is.

The input

  • image - RGB, RGBA, or MASK. That's it. No parameters, no tuning, which is either liberating or a dealbreaker depending on how you feel about Canny's threshold fiddling.

Outputs are the pack standard: RGBA, RGB, and MASK. The MASK output is the natural one for edge maps - a clean grayscale edge selection you can feed into ControlNet-style conditioning or use as a compositing mask.

Install

One-time pack install. 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. Standard pack deps (PyOpenGL, glfw, opencv-contrib-python, cozy_comfyui from git), needs an OpenGL context. No models, no keys. Under **JOVI_GLSL 🌈 → FILTER`.

Where people get tripped up

The honest comparison is with Canny: Canny's thresholds filter out texture noise, Sobel doesn't, so busy images come out with a lot of low-level fuzz. If you want cleaner edges, run a blur upstream or threshold the result downstream - but for line-heavy, graphic material (architecture, line art, mechanical subjects) Sobel's unfiltered response is often better for conditioning, because it keeps structure Canny would throw away. The lack of parameters is a feature here, not a bug.

CategoryJOV_GL 🌈/FILTER

Inputs (2)

NameTypeDefaultDescription
imageoptIMAGERGB(A) image
FRAGMENToptSTRING// name: SOBEL // desc: Finds the edges of an image // category: FILTER uniform sampler2D image; // | RGB(A) image void mainImage( out vec4 fragColor, in vec2 fragCoord ) { float x = 1.0 / iResolution.x; float y = 1.0 / iResolution.y; vec2 uv = fragCoord / iResolution.xy; vec4 horizEdge = vec4( 0.0 ); horizEdge -= texture( image, vec2( uv.x - x, uv.y - y ) ) * 1.0; horizEdge -= texture( image, vec2( uv.x - x, uv.y ) ) * 2.0; horizEdge -= texture( image, vec2( uv.x - x, uv.y + y ) ) * 1.0; horizEdge += texture( image, vec2( uv.x + x, uv.y - y ) ) * 1.0; horizEdge += texture( image, vec2( uv.x + x, uv.y ) ) * 2.0; horizEdge += texture( image, vec2( uv.x + x, uv.y + y ) ) * 1.0; vec4 vertEdge = vec4( 0.0 ); vertEdge -= texture( image, vec2( uv.x - x, uv.y - y ) ) * 1.0; vertEdge -= texture( image, vec2( uv.x , uv.y - y ) ) * 2.0; vertEdge -= texture( image, vec2( uv.x + x, uv.y - y ) ) * 1.0; vertEdge += texture( image, vec2( uv.x - x, uv.y + y ) ) * 1.0; vertEdge += texture( image, vec2( uv.x , uv.y + y ) ) * 2.0; vertEdge += texture( image, vec2( uv.x + x, uv.y + y ) ) * 1.0; vec3 edge = sqrt((horizEdge.rgb * horizEdge.rgb) + (vertEdge.rgb * vertEdge.rgb)); fragColor = vec4( edge, texture( image, uv ).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.