Nodes/ComfyUI CV/CV Draw Plot Frame
ComfyUI Node

CV Draw Plot Frame

Axes under your trajectory, so 3 metres doesn't look like 300

By bmad4ever·Created 3 months ago·Updated 14 days ago· 1
CV Draw Plot Frame
  • source
  • extent_from
  • image
◄x_min0.00►
◄x_max100.00►
◄y_min0.00►
◄y_max100.00►
◄margin20►
◄grid_x6►
◄grid_y6►
◄y_axisy down (pixel rows)►
◄unitsm►
◄x_labelx►
◄y_labelz►
◄scale_barbottom-left►
◄color(200, 200, 200)►
◄grid_color(70, 70, 70)►

Why you'd reach for this

Draw a camera trajectory onto a blank canvas and you get a squiggle. Is that a three-metre wobble from a loose tripod, or a 300-metre walk around a building? The picture is identical. The numbers exist, but they're in an array nobody reads.

This node supplies the missing half of a plot: a border, a grid, ticks labelled in world units, axis names and a scale bar. Put it under your drawn path or point cloud and the squiggle becomes a figure with a size.

Critically, it doesn't measure the data - it's fed the same world extent your mapper was, and reproduces that mapping exactly. That's what stops the amusing failure where the tick marked 50 m lands a centimetre away from the point that's at 50 m.

How it works

The geometry is shared with CV Fit Points To Box - one function used by both nodes, so they cannot disagree about where a world coordinate lands, including which way up it lands. That shared logic also explains the y_axis control's existence: pixel rows grow downward, so mapping a world axis straight onto rows draws your top-down map mirrored, and a left turn comes out looking like a right turn. y down (pixel rows) is the default because points that came through Fit Points To Box are already in pixel space; pick y up (world) only if you flipped them yourself.

Tick values are rounded to 1/2/5 × 10ᵏ so the labels read as round numbers - you ask for roughly six grid lines and get whatever step makes sense across the visible span. Grid lines that fall outside the plot box are skipped rather than clamped. units is a suffix on every tick label and the scale bar. The scale bar is the one annotation that still says how big the picture is after someone crops or rescales it, which is why it's on by default in the bottom-left.

Because nothing here reads the data, it also works as an empty graticule: render the frame, then draw points or a path on top. That ordering is the intended one - draw the frame first.

margin must match the margin you gave CV Fit Points To Box. The tooltip is blunt about it, and it's the one field that silently ruins the alignment if you set it on one node and not the other.

Inputs and outputs that matter

  • source - the canvas to draw on, e.g. a constant image the size you want, or the image you're overlaying. Its size defines the plot box.
  • extent_from (optional) - Nx2 world points whose bounding box sets the plotted range. Pass the same array you fed Fit Points To Box; if you're overlaying two tracks, pass both concatenated. With it connected, the min/max widgets are ignored.
  • x_min/x_max/y_min/y_max - the manual alternative when extent_from is empty.
  • margin - must match Fit Points To Box.
  • grid_x / grid_y - how many lines to aim for; 0 turns a direction off.
  • units, x_label, y_label - m, px, mm; axis names, drawn bottom-right and top-left. For a top-down (XZ) trajectory of a drive, the defaults x/z are already right.
  • scale_bar - a labelled bar of known world length, default bottom-left (none / bottom-left / bottom-right). It's the one annotation that still says how big the picture is after someone crops or rescales it, so leave it on.
  • color / grid_color - ticks, border and scale bar vs the grid itself. The grid should stay well below the data's contrast.
  • image - the only output.

Install

cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"

Python ≥ 3.12, current ComfyUI (V3 node API), or install ComfyUI CV from ComfyUI Manager. CPU drawing; the geometry came from upstream whatever produced your world points.

Common issues

  • Ticks don't line up with the data. margin differs between this node and Fit Points To Box, or you fed the frame a different point set than you fed the mapper (one track instead of both).
  • Everything drawn mirrored / left turns look like right turns. y_axis left on the default while your second coordinate is a world axis. Switch to y up (world). The KB's notes on reconstructing geometry from frames are the same story in a different context: sign and axis conventions are where these pipelines quietly go wrong.
  • Grid drowns the data. grid_color too bright, or too many lines. Aim for 4–8.
  • Labels unreadable. The frame is sized by its canvas - give it more pixels, or fewer ticks.
  • Nothing visible on a MASK source. BGR literal on a one-channel canvas keeps only blue; use a single value, and expect a grey frame.
  • Contrib trap. A plain opencv-python install over the contrib wheel empties the shared contrib submodules and contrib-backed nodes disappear. tools/repair_opencv_contrib.py --check then --apply.
Categoryimage/CV/points

Inputs (16)

NameTypeDefaultDescription
sourceCOMFY_MATCHTYPE_V3Canvas to draw on (a copy is made) - e.g. 'OpenCV Constant Like'. Its size sets the plot box. 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.
x_minFLOAT0.00-1000000000–1000000000World range, used only when 'extent_from' is empty.
x_maxFLOAT100.00-1000000000–1000000000—
y_minFLOAT0.00-1000000000–1000000000—
y_maxFLOAT100.00-1000000000–1000000000—
marginINT200–4096Must MATCH the margin given to 'CV Fit Points To Box', or the grid and the data disagree.
grid_xINT60–64Roughly how many vertical grid lines to aim for; the actual step is rounded to a 1 / 2 / 5 x 10^k value so the labels stay round numbers. 0 = no grid.
grid_yINT60–64Same for horizontal grid lines.
y_axisCOMBOy down (pixel rows)Which way the second coordinate grows. Points that came through 'Fit Points To Box' are already in PIXEL rows (y grows downward) - that is the default. Choose 'y up (world)' only if you flipped them yourself.
unitsSTRINGmUnit suffix on the tick labels and the scale bar ('m', 'px', 'mm'...). Empty for bare numbers.
x_labelSTRINGxAxis name drawn at the bottom right. Empty = none.
y_labelSTRINGzAxis name drawn at the top left. Empty = none. For a top-down ('XZ') view of a trajectory this is z.
scale_barCOMBObottom-leftA labelled bar of known world length. It is the one annotation that still says how big the picture is after someone crops or rescales the image.
colorSTRING(200, 200, 200)Colour of the ticks, labels, border and scale bar.
grid_colorSTRING(70, 70, 70)Colour of the grid lines - keep it well below the data's contrast so the grid stays behind it.
extent_fromoptNPARRAYWorld points, Nx2, whose bounding box defines the plotted range - pass the same array you gave 'Fit Points To Box' (both tracks concatenated, if you are overlaying two). When absent the explicit min/max below are used.

Outputs (1)

NameTypeDescription
imageCOMFY_MATCHTYPE_V3Same format as the image input.