cv2.Scharr
The 3×3 Gradient That Doesn't Lie About Edge Angles
- src
- nparray
If you need a 3×3 gradient, cv2.Scharr is usually the one to pick over cv2.Sobel. Same idea, same interface, better kernel. Sobel's classic [1, 2, 1] smoothing approximates a derivative with noticeable directional bias - gradient angles come out a few degrees off, and if you're deriving edge orientation (not just edge strength) that error is the whole problem. Scharr's [3, 10, 3] weights are the rotation-optimal 3×3, which is why it's the default choice in a lot of older OpenCV pipelines.
Mechanically it's literally "Sobel with ksize = -1": OpenCV substitutes the fixed Scharr kernel and ignores any size you might want. That's why this wrapper has no ksize input at all - the node signature is honestly smaller than Sobel's, and it means there is nothing to tune. You get src, ddepth, dx, dy, plus the advanced trio scale, delta and borderType.
The inputs you actually set
src- an IMAGE or MASK straight in (no bridge node needed), an NPARRAY, and it will process a full batch frame-by-frame rather than only frame 0.ddepth- the depth of the output. Default is same as input, and that default is a trap: a derivative has negative values, so on a uint8 image every negative comes back truncated to zero and you get a one-sided, misleading gradient. Set CV_32F. That's the single most important setting on this node.dx/dy- the derivative orders, and Scharr is strict: exactly one of them must be 1 and the other 0.dx=1, dy=0gives the horizontal derivative;dx=0, dy=1the vertical. Any other combination raises.scale/delta/borderType(advanced) - a multiplier and offset applied before storing, and the pixel-extrapolation mode at the frame edge.scaleis how you keep magnitudes in a sane range;deltais a bias, mostly useful for visualisation.
Output is a single nparray. Gradients change the data type, so the socket can't echo your IMAGE the way the blur nodes do - that's by design, not a bug.
What you do with it
Two Scharr nodes (dx and dy) plus cv2.cartToPolar is the standard recipe: magnitude and angle, one array each. Preview the magnitude with Preview CV Array in normalize mode (min–max stretched, which is the only way a gradient looks like anything), and turn the angle field into colour with CV Flow To Color or render it as arrows via CV Draw Flow Grid. The pack ships exactly this as a subgraph - CV Sobel 2D.json - so you can drop in the blueprint and swap Sobel for Scharr.
Worth saying plainly, given how many people arrive at gradient nodes from the ControlNet side: a raw Scharr/Sobel magnitude map is not the best edge conditioning. ControlNet's own preprocessors (Canny, lineart, softedge/HED) are trained for the job and produce the thin, legible maps the models respond to; a first-derivative map is noisy and thickness-dependent. Scharr is for measuring - gradient direction, flow fields, structure tensors, edge orientation histograms - and for the front end of a hand-built pipeline.
Install
Manager → search ComfyUI CV → install, or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
Restart ComfyUI. Requirements are Python ≥ 3.12 and a recent ComfyUI with the V3 node API. No model files - nothing in this node touches the network or the GPU.
Where people get burned
Truncated derivatives. Covered above, but it's the number-one confusion: ddepth left on same as input with a uint8 source produces a plausible-looking image with all the negative half discarded. If your gradient looks like it only catches bright edges, that's this.
Expecting an IMAGE out. Link an IMAGE in, get an NPARRAY out. To get back to a viewable IMAGE, use CV Array → Image (which handles float and converts to uint8) or, better for inspection, Preview CV Array - it renders floats, normalizes, and shows channel quadrants without destroying values.
Contrib wheel collisions. Installing a non-contrib OpenCV wheel over the contrib one wipes the shared site-packages/cv2 and makes contrib-derived nodes disappear silently. tools/repair_opencv_contrib.py --check tells you if that's what happened.
Version drift. The pack is built and curated against opencv-contrib-python-headless~=5.0.0.93, and it says other versions may behave differently. Worth knowing before you file a bug on a 4.x install.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| src | NPARRAY,IMAGE,MASK | input image. 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. | |
| ddepth | COMBO | same as input | output image depth, see "combinations" |
| dx | INT | 0-2147483648–2147483647 | order of the derivative x. |
| dy | INT | 0-2147483648–2147483647 | order of the derivative y. |
| scaleopt | FLOAT | 1.0000-1e+38–1e+38 | optional scale factor for the computed derivative values; by default, no scaling is applied (see #getDerivKernels for details). Preset to the OpenCV default (1.0). |
| deltaopt | FLOAT | 0.0000-1e+38–1e+38 | optional delta value that is added to the results prior to storing them in dst. Preset to the OpenCV default (0.0). |
| borderTypeopt | COMBO | BORDER_DEFAULT | pixel extrapolation method, see #BorderTypes. #BORDER_WRAP is not supported. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| nparray | NPARRAY | — |