Nodes/ComfyUI CV/CV Edge Drawing
ComfyUI Node

CV Edge Drawing

Edges that are already chains, plus the ellipses nothing else finds

By bmad4ever·Created 4 months ago·Updated 15 days ago· 1
CV Edge Drawing
  • image
  • edge_map
  • gradient
  • segments
  • lines
  • ellipses
  • segment_count
  • line_count
  • ellipse_count
◄operatorPrewitt►
◄gradient_threshold20►
◄anchor_threshold0►
◄scan_interval1►
◄min_path_length10►
◄sigma1.0►
◄min_line_length-1►
◄pf_modefalse►
◄nfa_validationtrue►
◄sum_flagtrue►
◄line_fit_error1.0►
◄max_distance_between_two_lines6.0►
◄max_error_threshold1.3►

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, including CV Array -> Mask.
  • gradient - the uint16 magnitude map ED computed internally. Preview it with Preview CV Array in normalize mode when you want to know why an edge was missed.
  • segments - the chains themselves, as CV_CONTOURS (one ordered Nx1x2 int32 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 - Nx4 float32 (x1, y1, x2, y2) straight fits, same layout HoughLinesP and LSD produce, so CV Draw Segments plugs straight in.
  • ellipses - Nx5 float32 (cx, cy, semi_axis_a, semi_axis_b, angle_deg), with circles coming back as a == b, angle 0. This is EDCircles, and it's the one thing the other line detectors in the pack cannot do. Draw them with CV 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 for segments.
  • 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.

Categoryimage/CV/contrib

Inputs (14)

NameTypeDefaultDescription
imageNPARRAY,IMAGEImage 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.
operatorCOMBOPrewittGradient 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_thresholdINT201–255Pixels 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_thresholdINT00–255How 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_intervalINT11–32Anchors are only looked for every Nth row/column. 1 scans everything; higher is faster and misses short edges.
min_path_lengthINT100–10000Edge chains shorter than this many pixels are dropped - the declutter knob for the SEGMENTS output.
sigmaFLOAT1.00–10Gaussian blur applied before the gradient. Raise it on noisy images; 0 disables the smoothing.
min_line_lengthoptINT-1-1–10000Shortest line the line stage will fit, in pixels. -1 (cv2's default) derives it from the image size.
pf_modeoptBOOLEANfalseParameter-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_validationoptBOOLEANtrueValidate fitted lines and ellipses with the number-of-false-alarms test. Off returns more, less trustworthy fits.
sum_flagoptBOOLEANtrueGradient magnitude as |gx| + |gy| (on, cv2's default) instead of the exact hypot.
line_fit_erroroptFLOAT1.00–10Maximum RMS error, in pixels, for a piece of chain to be accepted as one straight line.
max_distance_between_two_linesoptFLOAT6.00–100Two collinear line pieces farther apart than this are not merged (used by the ellipse fitting too).
max_error_thresholdoptFLOAT1.30–10Maximum fit error accepted when circular arcs are joined into a circle or an ellipse.

Outputs (8)

NameTypeDescription
edge_mapNPARRAYuint8 (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.
gradientNPARRAYuint16 (H,W) gradient magnitude ED computed - preview it with 'Preview CV Array' (normalize).
segmentsCV_CONTOURSThe 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.
linesNPARRAYNx4 float32 (x1, y1, x2, y2) straight fits - same layout as HoughLinesP / LSD / FLD, so 'CV Draw Segments' plugs straight in.
ellipsesNPARRAYNx5 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_countINTHow many edge chains were traced.
line_countINTHow many straight lines were fitted.
ellipse_countINTHow many circles + ellipses were fitted - branch on it with if/else, 0 is a normal outcome on a scene with no round objects.