CV Mesh Split Long Edges
The mesh tweak that stops cv2.rapid from drifting
- pts3d
- tris
- pts3d
- tris
- triangle_count
This one looks like a mesh-utilities node and is actually a fix for a specific, well-characterised failure in cv2.rapid (OpenCV's rigid-object tracker, exposed here as CV Rapid Track). If you're tracking a 3-D model through a video and the pose slowly diverges, the mesh is probably too coarse, and this node is the cure.
Why coarse meshes break RAPID
RAPID places control points along the projected silhouette and searches the image gradient along lines normal to it. extractControlPoints interpolates its 3-D control points linearly between consecutive silhouette vertices, using a blend factor taken from screen-space distance. So a control point sitting between two far-apart vertices is wrong - and wrong by an amount you can predict:
- depth error ≈ L_px² / (16·fx)
- 2-D position error ≈ L_px² / (8·R_px) on a curved silhouette
where L_px is the on-screen spacing of silhouette vertices. The important property: neither error shrinks if you render at a higher resolution. They're geometric, not raster. The author's own measurement on a 3624-triangle vehicle at fx=900 and 8 m: an untouched mesh puts control points up to 23 px off their own 3-D points, and tracking diverges. At L_px ≤ 80 the error is 0.9 px and the same sequence tracks to 3 px, for +14% tracking time and 5960 triangles.
The rule of thumb the node gives you: max_edge = 80 · distance / fx. Below L_px ≈ 20 the raster quantization floor takes over and further splitting buys nothing.
How it works
Repeatedly bisect the longest edge of any triangle over max_edge - one triangle becomes two. That's it, and the efficiency claim is real: only coarse regions grow, so you pay about half the triangles a uniform 1→4 subdivision would cost for the same result.
Inputs: pts3d (Nx3, from CV Mesh From 3D Model), tris (Mx3 int indices - winding preserved), max_edge (in the mesh's own units, default 0.7), and max_rounds (safety stop, default 24; the longest edge roughly halves each round, so 24 is far more than any sane budget needs).
Outputs: pts3d - originals unchanged plus one new vertex per split edge, so existing indices keep their meaning and anything indexed against the input mesh stays valid; tris - the refined mesh; triangle_count - the cost, and it grows fast past your L_px ≈ 20 knee if you set max_edge carelessly.
max_edge = 0 disables splitting and passes the mesh through unchanged. That's the A/B switch: run it once with 0 and once with a real budget and watch the tracker.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
Manager → ComfyUI CV. Restart. Python ≥ 3.12, V3-API ComfyUI, opencv-contrib-python-headless~=5.0.0.93. Nothing else to install, no models - pure geometry.
Putting it in the pipeline
Order matters, and there's one non-obvious consequence: run this before CV Mesh Vertex Normals, because that node's normals are the area-weighted sum of incident face normals and must follow the final topology. Splitting afterwards invalidates them. And the normals themselves should come from triangle winding rather than a plane fit - the author measured a PPF pose 177–179° out with CV Point Cloud Normals on a closed model versus 9.2° with winding-derived normals.
Common issues
You typed a pixel budget into a metre field. max_edge is in mesh units. If your model is in metres and the camera is 8 m away at fx=900, you want ≈ 80·8/900 ≈ 0.71 - the default 0.7 is that number, which is not a coincidence. Anything wildly off (0.0001, or 500) means you skipped the conversion.
Triangle count exploding. Splitting is not free and past the L_px ~20 floor it buys you nothing. Watch triangle_count against the input count, and cap max_rounds on a pathological model.
T-junctions. Bisecting one triangle's edge without splitting its neighbour leaves hanging vertices. The author notes this is harmless for silhouette extraction, since the neighbour's rasterized edge still passes over the new vertex - but if you're exporting this mesh elsewhere, know it's not a watertight subdivision.
Inputs (4)
| Name | Type | Default | Description |
|---|---|---|---|
| pts3d | NPARRAY | Nx3 vertices, from 'CV Mesh From 3D Model'. | |
| tris | NPARRAY | Mx3 int triangle indices. Winding is preserved. | |
| max_edge | FLOAT | 0.700–1000000000 | Longest edge allowed, IN THE MESH'S OWN UNITS. Convert from the on-screen budget with max_edge = L_px * distance / fx (L_px = 80 is the knee) - a 'Math Expression' node keeps that visible in the graph. 0 disables splitting and passes the mesh through unchanged, which is how you A/B the effect. |
| max_roundsopt | INT | 241–64 | Safety stop. Each round splits every triangle still over budget, so the longest edge roughly halves per round and 24 rounds is far more than any sane budget needs. Lower it to cap the triangle count on a pathological model. |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| pts3d | NPARRAY | Nx3 float32 vertices: the originals unchanged, plus one new vertex per split edge. Existing indices keep their meaning, so anything indexed against the input mesh stays valid. |
| tris | NPARRAY | Mx3 int32 triangles of the refined mesh. Bisection leaves T-junctions where a neighbour was not split - harmless for silhouette extraction, since the neighbour's rasterized edge still passes over the new vertex. |
| triangle_count | INT | Triangles after splitting. Watch it against the input count: this is the cost you are paying, and past L_px~20 it grows fast for no accuracy. |