CV Edge Drawing
Edges that are already chains, plus the ellipses nothing else finds
- image
- edge_map
- gradient
- segments
- lines
- ellipses
- segment_count
- line_count
- ellipse_count
Canny gives you pixels that are probably an edge. Edge Drawing gives you the edge as an ordered path - and that difference is the entire reason this node exists. Instead of thresholding a gradient map and leaving you to reconnect the dots, it finds anchor pixels at gradient maxima and walks the ridge between them, so what comes out is one-pixel-wide and already chained.
What you get out of it
Five data outputs, and they're a small toolkit rather than one result:
edge_map- a uint8(H, W)mask, 255 on the traced edges, one pixel wide by construction. It drops into anything that takes a Canny result, includingCV Array -> Mask.gradient- the uint16 magnitude map ED computed internally. Preview it withPreview CV Arrayin normalize mode when you want to know why an edge was missed.segments- the chains themselves, asCV_CONTOURS(one orderedNx1x2int32 run each). This is the clever bit: an ED chain is exactly what the pack's contour nodes already eat, so arc length, approx-poly, drawing and the shape nodes all work on it. They're open curves, so area is meaningless - length isn't.lines-Nx4float32(x1, y1, x2, y2)straight fits, same layout HoughLinesP and LSD produce, soCV Draw Segmentsplugs straight in.ellipses-Nx5float32(cx, cy, semi_axis_a, semi_axis_b, angle_deg), with circles coming back asa == b, angle 0. This is EDCircles, and it's the one thing the other line detectors in the pack cannot do. Draw them withCV Draw Ellipses.
Plus segment_count, line_count and ellipse_count for branching. Nothing found is a valid result: every output comes back empty, never None.
Why the CV class matters
cv2.ximgproc.createEdgeDrawing is a class - a factory returning a stateful object - and the pack's ~470 auto-generated wrappers only reach top-level functions. So this node is hand-written, and it runs all three stages on one handle over a single detectEdges pass. The source comment explains why the three results aren't three nodes: the line and ellipse calls are cheap next to the edge pass (measured at roughly 15 ms of edges against 4 ms of lines and 10 ms of ellipses on a 1500×1050 photo), and splitting them would mean pushing a live C++ object across the graph - the hazard this whole contrib module was written to avoid.
The knobs that matter
image- feed the original image, not an edge map. ED computes its own gradient.gradient_threshold(default 20) - the one knob that decides how much low-contrast detail gets traced. Lower is more edges and more noise.min_path_length(default 10) - drops short chains. This is your declutter forsegments.sigma(default 1) - Gaussian blur before the gradient. Raise on noisy input; 0 disables it.anchor_threshold(default 0) - 0 anchors every local maximum, which is cv2's default; raising it thins the result to the strongest edges.operator- Prewitt (cv2's default), Sobel, Scharr, or LSD, which switches the line stage to the LSD-style detector.pf_mode- parameter-free mode: edges are validated by the Helmholtz principle instead of the gradient threshold, so the result stops depending on the knobs above. Slower, and the usual choice when you're after ellipses. When someone says this node found a circle that HoughCircles missed, this is nearly always why.
The remaining optional inputs (nfa_validation, sum_flag, line_fit_error, max_distance_between_two_lines, max_error_threshold, min_line_length) are the fit tolerances - leave them alone until you have a reason.
Install
# ComfyUI Manager → search "ComfyUI CV" → install → restart
# or manually:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
cd comfyui_cv && pip install -r requirements.txt
Needs Python ≥ 3.12 and a recent, V3-API ComfyUI. The single dependency is opencv-contrib-python-headless~=5.0.0.93 - and for this node the contrib part is load-bearing, since Edge Drawing lives in the ximgproc contrib module. Installing a plain opencv-python wheel alongside silently empties the contrib submodules (all four OpenCV distributions share one site-packages/cv2), and the node disappears from the menu. That's what tools/repair_opencv_contrib.py is for:
python tools/repair_opencv_contrib.py --check
python tools/repair_opencv_contrib.py --apply
Tuning notes
Start at the defaults and look at edge_map. If your object's outline is broken into fragments, lower gradient_threshold and raise sigma; the chain walker needs a continuous ridge to follow. If segments is thousands of entries that mean nothing, raise min_path_length - that's the filter designed for exactly that. And expect ellipse_count = 0 on a scene with no round objects; it's a normal answer, not a broken node.
One framing note: this pack is the OpenCV half of ComfyUI - deterministic image maths that doesn't know or care which diffusion model you're running - and its author says outright that it was written with heavy LLM assistance and isn't production-grade. This particular node is one of the more interesting pieces in it, because the underlying algorithm genuinely does something the rest of the pack can't.
Inputs (14)
| Name | Type | Default | Description |
|---|---|---|---|
| image | NPARRAY,IMAGE | Image to trace (converted to grayscale internally). Feed the ORIGINAL image, not an edge map - ED computes its own gradient. 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. | |
| operator | COMBO | Prewitt | Gradient operator. Prewitt (cv2's default) and Sobel are 3x3; Scharr is the more rotation-accurate 3x3 kernel; 'LSD' switches the whole line stage to the LSD-style detector. |
| gradient_threshold | INT | 201–255 | Pixels with a gradient below this can never be part of an edge. This is the ONE knob that decides how much low-contrast detail is traced; lower = more edges and more noise. |
| anchor_threshold | INT | 00–255 | How much a pixel must exceed its neighbours along the gradient to become an ANCHOR (a seed the tracing starts from). 0 anchors every local maximum, which is cv2's default; raising it thins the result to the strongest edges. |
| scan_interval | INT | 11–32 | Anchors are only looked for every Nth row/column. 1 scans everything; higher is faster and misses short edges. |
| min_path_length | INT | 100–10000 | Edge chains shorter than this many pixels are dropped - the declutter knob for the SEGMENTS output. |
| sigma | FLOAT | 1.00–10 | Gaussian blur applied before the gradient. Raise it on noisy images; 0 disables the smoothing. |
| min_line_lengthopt | INT | -1-1–10000 | Shortest line the line stage will fit, in pixels. -1 (cv2's default) derives it from the image size. |
| pf_modeopt | BOOLEAN | false | Parameter-Free mode: ED validates every edge with the Helmholtz principle instead of the gradient threshold, so the result stops depending on the knobs above. Slower, and the usual choice when you are after ELLIPSES. |
| nfa_validationopt | BOOLEAN | true | Validate fitted lines and ellipses with the number-of-false-alarms test. Off returns more, less trustworthy fits. |
| sum_flagopt | BOOLEAN | true | Gradient magnitude as |gx| + |gy| (on, cv2's default) instead of the exact hypot. |
| line_fit_erroropt | FLOAT | 1.00–10 | Maximum RMS error, in pixels, for a piece of chain to be accepted as one straight line. |
| max_distance_between_two_linesopt | FLOAT | 6.00–100 | Two collinear line pieces farther apart than this are not merged (used by the ellipse fitting too). |
| max_error_thresholdopt | FLOAT | 1.30–10 | Maximum fit error accepted when circular arcs are joined into a circle or an ellipse. |
Outputs (8)
| Name | Type | Description |
|---|---|---|
| edge_map | NPARRAY | uint8 (H,W) mask, 255 on the traced edges - one-pixel-wide by construction. Feed it to 'CV Array -> Mask' or to any node that takes a Canny result. |
| gradient | NPARRAY | uint16 (H,W) gradient magnitude ED computed - preview it with 'Preview CV Array' (normalize). |
| segments | CV_CONTOURS | The edge CHAINS as CV_CONTOURS (one ordered Nx1x2 int32 point run each) - draw them with 'OpenCV Draw Contours' or measure them with the contour nodes. These are open curves, so area is meaningless; length is not. |
| lines | NPARRAY | Nx4 float32 (x1, y1, x2, y2) straight fits - same layout as HoughLinesP / LSD / FLD, so 'CV Draw Segments' plugs straight in. |
| ellipses | NPARRAY | Nx5 float32 (cx, cy, semi_axis_a, semi_axis_b, angle_deg) - circles come back with a == b and angle 0. Draw them with 'CV Draw Ellipses'. |
| segment_count | INT | How many edge chains were traced. |
| line_count | INT | How many straight lines were fitted. |
| ellipse_count | INT | How many circles + ellipses were fitted - branch on it with if/else, 0 is a normal outcome on a scene with no round objects. |