Nodes/ComfyUI CV/cv2.Canny (1/2)
ComfyUI Node

cv2.Canny (1/2)

The edge map every 'canny' ControlNet was trained on

By bmad4ever·Created 3 months ago·Updated 14 days ago· 1
cv2.Canny (1/2)
  • image
  • result
◄threshold10.0000►
◄threshold20.0000►
◄apertureSize3►
◄L2gradientfalse►

Canny is the edge detector that gave its name to an entire conditioning path. When a workflow says "canny ControlNet", this is the image being produced: a thin, binary, high-contrast outline of the structure in a shot. It's been the default preprocessor since ControlNet shipped, and it remains the one people reach for on architecture, machinery, and anything with clean contours - hard edges, no ambiguity about what it's detecting.

OpenCV's Canny has two overloads, and the pack exposes both as separate nodes because the registry is generated per signature. This is 1/2, the normal one: image in, edges out. It's part of comfyui_cv, a pack that wraps ~470 cv2.* functions for ComfyUI.

Do you need it?

Honest answer: ComfyUI core already ships a Canny node, and if all you want is an edge map for a ControlNet you can use that one and never think about this pack. You're here for one of three reasons - you're already working inside comfyui_cv and want to stay in its socket types, you want the apertureSize and L2gradient controls the core node doesn't expose, or you want the second overload (2/2) to run Canny on derivatives you computed yourself.

The pack's own curated alternative is worth knowing too: CV Detect Lines (Hough) does Canny plus a probabilistic Hough transform in one node if lines, not edges, are what you're after.

How it works, and the only two numbers that matter

Canny is three stages. Sobel derivatives (apertureSize is the kernel - 3, 5 or 7) give the gradient; non-maximum suppression thins that to one-pixel ridges; then hysteresis decides which ridges are real. Hysteresis is the clever bit: pixels above the high threshold are committed, pixels between the low and high thresholds are kept only if they connect to something committed. That's why Canny gives you continuous contours instead of noise confetti.

  • image (required) - 8-bit input, and the format-deciding socket: IMAGE in → IMAGE out, MASK in → MASK out, NPARRAY stays NPARRAY. An IMAGE link gets converted to uint8 BGR and only frame 0 of a batch is used.
  • threshold1 (required, default 0) - the low threshold. OpenCV's naming, not mine.
  • threshold2 (required, default 0) - the high threshold.
  • apertureSize (optional, default 3) - Sobel aperture. Larger means a smoother, more noise-tolerant gradient.
  • L2gradient (optional, default false) - true uses sqrt(dx² + dy²) for magnitude instead of |dx| + |dy|. More accurate, marginally slower.

Both thresholds default to 0, which is not a usable configuration - set them. 100 / 200 is the standard starting point, and keeping the high threshold roughly twice the low one is the conventional ratio. Too low and you get noise edges everywhere; too high and whole contours vanish and the ControlNet has nothing structural to hold onto.

One output, result, echoing image's format. The 0/255 binary result is an ordinary IMAGE when you fed an IMAGE, so it drops straight into a ControlNet Apply node - no conversion step, which is the whole reason the pack made it type-preserving.

Install

cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv

Restart, or Manager → search "comfyui_cv". Python ≥ 3.12, a recent ComfyUI on the V3 node API, and:

pip install "opencv-contrib-python-headless~=5.0.0.93"

That's the version behaviour is curated against. No models - Canny is arithmetic.

Common issues

Solid black output. Thresholds too high, or both left at 0 on a flat image. Drop to 100/200 and check you're feeding something with actual contrast.

Confetti. Thresholds too low, or sensor noise in the source. Raise the low threshold, or blur slightly first (cv2.blur or cv2.GaussianBlur from the same pack) - Canny amplifies whatever noise survives the gradient stage, and a pre-blur is the cheapest fix there is.

Edges are doubled or thick. That's apertureSize too small on a soft or upscaled image. Try 5.

A smooth gradient disappears entirely. Non-maximum suppression only leaves ridges, so a scene that's all soft shading and no hard boundaries produces almost nothing. That's the documented limitation of this preprocessor, and the reason softer edge models exist for organic subjects.

It won't connect to a ControlNet. Check what you linked in. If you fed a MASK, the output is a MASK, and ControlNet wants an IMAGE - put the edge map through a mask-to-image step or feed the IMAGE path instead.

Categoryimage/CV/low-level/cv2 C

Inputs (5)

NameTypeDefaultDescription
imageCOMFY_MATCHTYPE_V38-bit input image. The image output(s) echo this input's format. Accepts a ComfyUI IMAGE/MASK directly (frame 0 of a batch) or an NPARRAY. Arithmetic ops (add, multiply, etc.) process the full IMAGE batch when both inputs have the same batch size.
threshold1FLOAT0.0000-1e+38–1e+38first threshold for the hysteresis procedure.
threshold2FLOAT0.0000-1e+38–1e+38second threshold for the hysteresis procedure.
apertureSizeoptINT3-2147483648–2147483647aperture size for the Sobel operator. Preset to the OpenCV default (3).
L2gradientoptBOOLEANfalsea flag, indicating whether a more accurate $L_2$ norm $=\sqrt{(dI/dx)^2 + (dI/dy)^2}$ should be used to calculate the image gradient magnitude ( L2gradient=true ), or whether the default $L_1$ norm $=|dI/dx|+|dI/dy|$ is enough ( L2gradient=false ). Preset to the OpenCV default (False).

Outputs (1)

NameTypeDescription
resultCOMFY_MATCHTYPE_V3Echoes the 'image' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY.