cv2.arrowedLine
Draw the arrow, then remember BGR means green is (0,255,0)
- source
- pt1
- pt2
- result
An arrow on an image is how you say "this thing moved that way". cv2.arrowedLine draws one from pt1 to pt2 with a triangular head, and it's the primitive under flow-vector overlays, trajectory headers, and the little direction indicators in every QA screenshot you've seen.
It's a drawing wrapper, which means it takes an image in and returns the same image with an arrow on it. The two things that actually require thought: the colour order, and the fact that tipLength is relative to the arrow's length.
How it works
The canvas comes in through source, the arrow is drawn onto it, and the result echoes the input's format. Any of the drawing wrappers in the pack accept an IMAGE, a MASK or a raw NPARRAY as the canvas - you can annotate a mask just as happily as a photo.
The two endpoints arrive as CV_TUPLE, the pack's composite socket for (x, y) values. Two ways to fill them: type the numbers into the widget, or wire them in from the curated CV Tuple node. What you can't do is connect one component of a point - the tuple travels as a whole, which is the point of the type.
Colours are OpenCV scalars written as strings: "(0, 255, 0)" for green, "(0, 0, 255)" for red, "(0, 255, 0, 64)" for a translucent green. The component order is BGR. A bare number broadcasts to all components, so "255" is white, and components past the canvas's channel count are ignored.
tipLength is the head length as a fraction of the arrow's length - the default 0.1 means the head is a tenth of the arrow. That's sane for a long arrow and useless for a short one: a 20-pixel arrow gets a 2-pixel head you can't see. Turn it up when the arrows get short.
shift is fractional bits in the coordinates: at shift = 1, a value of 800 means 400 pixels. It exists so integer APIs can express sub-pixel geometry, and if you set it by accident your arrow lands somewhere else entirely.
Inputs and outputs
source(COMFY_MATCHTYPE_V3) - the canvas; decides the output format.pt1,pt2(CV_TUPLE) - where the arrow starts and points.color(STRING) - the BGR scalar literal.thickness(optional INT, default 1),line_type(optional, defaultLINE_AA),shift(optional INT, default 0),tipLength(optional FLOAT, default 0.1).- result - the canvas with the arrow on it.
What you wire it into
Optical-flow overlays are the obvious case, and the pack also ships curated visualisers that draw whole vector fields at once - CV Draw Flow Vectors for arrows between two point sets, CV Draw Path for ordered trajectories with heading arrows, CV Draw Rays for origin-to-point rays. Reach for those when you have many arrows; reach for this one when you have one or two and want exact control.
The rest is annotation: pointing at a defect in a QA frame, labelling a direction of travel on a map, drawing an axis on a plot (with CV Draw Plot Frame for the grid), or marking a detection's heading.
Install
ComfyUI Manager → comfyui_cv (ComfyUI CV), 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. Python ≥ 3.12 and a recent V3-API ComfyUI; behaviour is curated against OpenCV 5.0.0.93. Keep the contrib wheel - installing a non-contrib opencv-python over it silently strips the contrib submodules.
Where people get burned
Red and blue swapped. The single most common bug with every drawing node in this pack. (0, 0, 255) is red. Type green as (0, 255, 0), not (255, 0, 0).
Invisible arrows. Three causes: coordinates outside the image, an arrow shorter than the head, or the wrong colour for the background (white on white). Check tipLength for the short-arrow case.
A mask canvas that stops being binary. LINE_AA antialiases, which on a MASK canvas leaves values between 0 and 1 along the edges. If the mask feeds a binary consumer, switch line_type to LINE_8 and draw crisp.
The arrow disappearing between nodes. Each drawing node returns a new canvas from the one you gave it - chain them, or draw onto the same source and merge. Wiring two drawing nodes to the same input gives you two canvases with one arrow each.
shift set and forgotten. Your coordinates get divided by 2^shift. Leave it at 0 unless you specifically want fractional-bit coordinates.
Inputs (8)
| Name | Type | Default | Description |
|---|---|---|---|
| source | COMFY_MATCHTYPE_V3 | 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. | |
| pt1 | CV_TUPLE | 0,0 | The point the arrow starts from. One value with 2 components (x, y) - it travels as a whole, so it cannot arrive half-connected. Wire it from 'CV Tuple' or type the components in place. |
| pt2 | CV_TUPLE | 0,0 | The point the arrow points to. One value with 2 components (x, y) - it travels as a whole, so it cannot arrive half-connected. Wire it from 'CV Tuple' or type the components in place. |
| color | STRING | (0, 0, 0, 0) | Line color. cv2 Scalar as a literal, e.g. "(0, 255, 0)" (BGR) or "(0, 255, 0, 64)" (BGRA). A bare number broadcasts to every component, so "255" means (255, 255, 255, 255). Components past the target's channel count are ignored by OpenCV. |
| thicknessopt | INT | 1-2147483648–2147483647 | Line thickness. Preset to the OpenCV default (1). |
| line_typeopt | COMBO | LINE_AA | Type of the line. See #LineTypes |
| shiftopt | INT | 0-2147483648–2147483647 | Number of fractional bits in the point coordinates. Preset to the OpenCV default (0). |
| tipLengthopt | FLOAT | 0.1000-1e+38–1e+38 | The length of the arrow tip in relation to the arrow length Preset to the OpenCV default (0.1). |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| result | COMFY_MATCHTYPE_V3 | Echoes the 'source' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY. |