CV Fit Points To Box
Turn metres into pixels without lying about the scale
- points
- extent_from
- points
- scale
- offset_x
- offset_y
Your points are in metres and your canvas is in pixels. This node bridges the two: it scales and centres a 2D point set so it fills a width × height pixel box with a margin, using one uniform scale for both axes so nothing gets stretched. A triangulated cloud projected top-down, a camera trajectory from a pose chain, a click track - all of it becomes drawable.
It's one of the point/plot helpers in bmad4ever's ComfyUI CV pack (bmad4ever/comfyui_cv, a fork of Gerold Meisinger's opencv-comfyui). The pack is ~470 auto-generated cv2.* wrappers plus curated nodes; this one is written by hand and it's pure numpy + python. That matters for one reason: it's failure-tolerant. Empty input passes through as an empty array, a single point (or a zero-extent set) lands at the box centre. No divide-by-zero, no exception, no dead graph halfway down a loop over frames.
Inputs
points is Nx2 or Nx1x2. width, height (300 × 300 by default) should match whatever canvas you're drawing into. margin (20 px) is kept clear inside the box.
Then the two optional inputs that are the reason this node is more than a divide:
extent_from - take the range to fit from these points instead of points. Two tracks fitted separately each fill the box, so the moment you overlay them one is silently rescaled against the other and the picture lies about their relative size. Pass both tracks concatenated here (and to CV Draw Plot Frame) and every set shares one mapping. Tracks in a visual-odometry or Structure-from-Motion example are exactly this situation.
y_axis - which way the second coordinate grows on the canvas. Pixel rows grow downward, so mapping a world axis straight onto them draws it upside down. For a top-down (XZ) map that mirrors the picture, and a left turn comes out looking like a right turn - a bug you'll stare at for a long time because everything else about the frame looks correct. Choose y up (world) whenever the second coordinate is a world axis you intend to read as a map; leave it at y down (pixel rows) when the points are already in pixel space. Pass the same value to CV Draw Plot Frame, or your grid and your data will disagree about which way is up.
Outputs
- points - the mapped points,
Nx1x2float32. - scale - pixels per world unit, from the mapping
pixel = world * scale + offset. Feed it to a scale bar, or to any measurement you want to print. - offset_x, offset_y - the pixel column/row that world zero maps to.
Those three scalars are what makes the mapping auditable. With them you can convert any other point set into the same frame yourself, put a distance annotation on the canvas in real units, or sanity-check a trajectory against known physical dimensions instead of squinting at a plot.
Where it goes
Downstream is the pack's drawing layer: CV Draw Points for markers, CV Draw Path (ordered) if you want direction and end-markers to read, CV Draw Polygon for a closed outline. Pair it with CV Draw Plot Frame for the grid, ticks and scale bar - that node reproduces this mapping exactly, which is the point: a tick and a data point agree only if both went through the same fit. Typical upstream is CV Project To Plane (drop one axis of a 3D cloud) or a pose chain composed by CV Compose Pose (Trajectory).
Install
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
Or search ComfyUI CV in ComfyUI Manager. Python ≥ 3.12, recent ComfyUI with the V3 node API. The example workflows load sample assets from example_inputs/; run workflows/01_install_example_inputs.json once and reload the page or the Load dropdowns stay red. workflows/exercise_visual_odometry.json is where this node earns its keep.
Common issues
Two overlaid tracks, obviously different real-world size, same size on screen. Classic extent_from omission. Fit both through one instance.
The map is mirrored. You wanted y up (world). There is no un-mirroring step later - fix it here, and pass the same setting to the plot frame.
Everything piles up in a corner. A single huge outlier (a bad triangulated point, a pose that diverged for one frame) sets the extent, and everything real collapses into a few pixels. Filter the cloud first - percentile outlier removal exists in the pack for exactly this.
The points are there but nothing draws. Check the canvas size you're drawing into matches width/height. The mapping has no idea how big your image actually is.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| points | NPARRAY | 2D points, Nx2 or Nx1x2 (None/empty -> empty output). | |
| width | INT | 3001–16384 | Target box width in pixels (match your canvas). |
| height | INT | 3001–16384 | Target box height in pixels. |
| margin | INT | 200–4096 | Padding kept clear inside the box, in pixels. |
| extent_fromopt | NPARRAY | Take the range to fit from THESE points instead of 'points'. Two tracks fitted separately each fill the box, so overlaying them silently rescales one against the other - pass both tracks concatenated here (and to 'CV Draw Plot Frame') and every set shares one mapping. Empty = use 'points', as before. | |
| y_axisopt | COMBO | y down (pixel rows) | Which way the second coordinate grows on the canvas. Pixel rows grow DOWNWARD, so mapping a world axis straight onto them draws it upside down - for a top-down (XZ) map that mirrors the picture and a left turn comes out looking like a right turn. Choose 'y up (world)' whenever the second coordinate is a world axis you want read as a map; leave it 'y down' when the points are already in pixel space. Pass the SAME value to 'OpenCV Draw Plot Frame' so the grid and the data agree. |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| points | NPARRAY | Points scaled into the box, Nx1x2 float32. |
| scale | FLOAT | Pixels per world unit, i.e. pixel = world * scale + offset. Feed it to a scale bar or a measurement. |
| offset_x | FLOAT | Pixel column that world x = 0 maps to. |
| offset_y | FLOAT | Pixel row that world y = 0 maps to. |