Nodes/ComfyUI CV/CV Fit Points To Box
ComfyUI Node

CV Fit Points To Box

Turn metres into pixels without lying about the scale

By bmad4ever·Created 3 months ago·Updated 14 days ago· 1
CV Fit Points To Box
  • points
  • extent_from
  • points
  • scale
  • offset_x
  • offset_y
◄width300►
◄height300►
◄margin20►
◄y_axisy down (pixel rows)►

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, Nx1x2 float32.
  • 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.

Categoryimage/CV/points

Inputs (6)

NameTypeDefaultDescription
pointsNPARRAY2D points, Nx2 or Nx1x2 (None/empty -> empty output).
widthINT3001–16384Target box width in pixels (match your canvas).
heightINT3001–16384Target box height in pixels.
marginINT200–4096Padding kept clear inside the box, in pixels.
extent_fromoptNPARRAYTake 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_axisoptCOMBOy 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)

NameTypeDescription
pointsNPARRAYPoints scaled into the box, Nx1x2 float32.
scaleFLOATPixels per world unit, i.e. pixel = world * scale + offset. Feed it to a scale bar or a measurement.
offset_xFLOATPixel column that world x = 0 maps to.
offset_yFLOATPixel row that world y = 0 maps to.