CV Draw Plot Frame
Axes under your trajectory, so 3 metres doesn't look like 300
- source
- extent_from
- image
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) -
Nx2world points whose bounding box sets the plotted range. Pass the same array you fedFit 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_fromis 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 defaultsx/zare 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.
margindiffers between this node andFit 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_axisleft on the default while your second coordinate is a world axis. Switch toy 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_colortoo 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-pythoninstall over the contrib wheel empties the shared contrib submodules and contrib-backed nodes disappear.tools/repair_opencv_contrib.py --checkthen--apply.
Inputs (16)
| Name | Type | Default | Description |
|---|---|---|---|
| source | COMFY_MATCHTYPE_V3 | Canvas 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_min | FLOAT | 0.00-1000000000–1000000000 | World range, used only when 'extent_from' is empty. |
| x_max | FLOAT | 100.00-1000000000–1000000000 | — |
| y_min | FLOAT | 0.00-1000000000–1000000000 | — |
| y_max | FLOAT | 100.00-1000000000–1000000000 | — |
| margin | INT | 200–4096 | Must MATCH the margin given to 'CV Fit Points To Box', or the grid and the data disagree. |
| grid_x | INT | 60–64 | Roughly 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_y | INT | 60–64 | Same for horizontal grid lines. |
| y_axis | COMBO | y 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. |
| units | STRING | m | Unit suffix on the tick labels and the scale bar ('m', 'px', 'mm'...). Empty for bare numbers. |
| x_label | STRING | x | Axis name drawn at the bottom right. Empty = none. |
| y_label | STRING | z | Axis name drawn at the top left. Empty = none. For a top-down ('XZ') view of a trajectory this is z. |
| scale_bar | COMBO | bottom-left | A 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. |
| color | STRING | (200, 200, 200) | Colour of the ticks, labels, border and scale bar. |
| grid_color | STRING | (70, 70, 70) | Colour of the grid lines - keep it well below the data's contrast so the grid stays behind it. |
| extent_fromopt | NPARRAY | World 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)
| Name | Type | Description |
|---|---|---|
| image | COMFY_MATCHTYPE_V3 | Same format as the image input. |