cv2.Canny (2/2)
Run the hysteresis on derivatives you computed yourself
- dx
- dy
- result
Canny, minus the gradient stage. You hand it dx and dy - 16-bit signed derivative images - and it does the rest: non-maximum suppression and hysteresis, the same edge map, but from numbers you supplied. OpenCV has exposed both overloads forever; the pack generates a node per signature, so here they are as (1/2) and (2/2), with distinct class ids because they're distinct functions.
This is one of ~470 raw cv2.* wrappers in comfyui_cv. It's a niche node and I'll say so plainly: most people want (1/2), which takes an image. You want this one in a specific situation.
When this is the right node
You already have derivatives. If a graph has already run Sobel for its own reasons, running Canny's internal Sobel again is duplicated work - and worse, you can't reuse the same derivatives for anything else. Feeding them in explicitly means one gradient computation, two consumers.
You want a different derivative operator. Sobel is the default inside Canny. Scharr is more rotationally faithful at 3×3, and there are filtered or masked gradients that Canny itself has no way to express. Compute whatever you like, hand it over, get edges.
You want to tune the gradient stage. scale and delta on the Sobel call, per-axis handling, a ksize of 7 - all of that disappears inside (1/2), where the only control is apertureSize.
Inputs
dx(required) - the 16-bit x derivative:CV_16SC1, orCV_16SC3if you're working on colour. The type matters, and this is the node's real trap - see below. It's the format-deciding socket, so the outputresultechoes whatever you linked here.dy(required) - the y derivative, same size and type.threshold1,threshold2(required, default0) - low and high hysteresis thresholds. Same meaning, same advice:100/200to start, keep the ratio around 2:1.L2gradient(optional, defaultfalse) - magnitude assqrt(dx² + dy²)instead of|dx| + |dy|.
One output, result, the binary edge map.
The obvious recipe, using the pack's own wrappers:
cv2.Sobel(src, ddepth=CV_16S, dx=1, dy=0, ksize=3) -> dx
cv2.Sobel(src, ddepth=CV_16S, dx=0, dy=1, ksize=3) -> dy
both -> cv2.Canny (2/2)
Note that Sobel's output is a plain NPARRAY in this pack rather than a type-preserving image - deliberately, because it's a depth changer and normalising a float score map into an image would destroy it. So both connections here are array-to-array, which is exactly what this node expects.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
Restart, or ComfyUI 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"
No models, no downloads - it's arithmetic over two arrays.
Common issues
cv2 asserts on the input type. ddepth on your Sobel call is the culprit 9 times out of 10. It must be CV_16S - the node's tooltip is explicit that it wants 16-bit signed derivatives. An 8-bit Sobel clips every gradient above 255, and a float Sobel is the wrong type outright.
The edges are wrong or half-missing. You ran convertScaleAbs (or any absolute-value / normalising step) before this node. Canny needs the signed derivatives - the sign is how it knows which way the gradient points, and the magnitude is how it decides what's an edge. Absolute values throw that away. Feed the raw Sobel output.
Edges are doubled. Noise survived into the derivative. Blur the source before you take the Sobel - pre-blur, then Sobel, then this node.
Thresholds seem to behave differently than in (1/2). They don't, but the numbers you're comparing are on a different scale. If your Sobel used a scale factor, every gradient - and therefore the meaning of "200" - scaled with it. Normalise first, or re-tune.
Nothing structural survives. Same limitation as the other overload, and it's inherent to Canny rather than to this node: non-maximum suppression discards smooth gradients, so soft-edged subject matter produces very little. For organic images, softer edge conditioning exists for exactly this reason.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| dx | COMFY_MATCHTYPE_V3 | 16-bit x derivative of input image (CV_16SC1 or CV_16SC3). 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. | |
| dy | NPARRAY,IMAGE,MASK | 16-bit y derivative of input image (same type as dx). 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. | |
| threshold1 | FLOAT | 0.0000-1e+38–1e+38 | first threshold for the hysteresis procedure. |
| threshold2 | FLOAT | 0.0000-1e+38–1e+38 | second threshold for the hysteresis procedure. |
| L2gradientopt | BOOLEAN | false | a 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)
| Name | Type | Description |
|---|---|---|
| result | COMFY_MATCHTYPE_V3 | Echoes the 'dx' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY. |