cv2.cornerSubPix
Turning pixel corners into sub-pixel corners
- image
- corners
- winSize
- zeroZone
- nparray
cv2.cornerSubPix does not find corners. It takes corners you already found - integer pixel positions from goodFeaturesToTrack, a chessboard detector, a click, anything - and refines each one to sub-pixel accuracy by iterating over a small window around it. It's the step between "there's a corner somewhere near (412, 208)" and "(412.37, 207.62)".
You reach for it whenever the exact position matters more than the detection: camera calibration, stereo rectification, pose estimation, photogrammetry, or aligning an AI render to a reference plate. For a pure ComfyUI image pipeline you probably don't need it; that's worth saying out loud, because refining corners is one of those steps that looks like progress and changes nothing visible.
The ComfyUI CV node is cv2.cornerSubPix, category image/CV/low-level/cv2 C - a raw wrapper from bmad4ever's pack, so the inputs are cv2's, including one that's worth reading carefully.
How it works
For each initial corner, cv2 looks in a winSize window around it and minimises the error between the image's gradient field and the model of a corner - mathematically, it solves for the point where the gradient-weighted sum of first-order image derivatives vanishes. It iterates: adjust the position, re-evaluate, repeat until either the movement drops below an epsilon or you hit a maximum iteration count. The algorithm also accepts a zeroZone, a dead region in the middle of the search window that's excluded from the sum, which exists to avoid a singular autocorrelation matrix when the corner is perfect.
The output is the same point list, moved. Nothing else about the corners changes - count, order and correspondence all survive, which is exactly what makes it safe to insert in the middle of a pipeline.
The inputs and outputs that matter
image is the image - single-channel 8-bit or float, and if you wire a normal color IMAGE the pack grayscales it for you. It must be the same image you detected the corners in; refining coordinates against a resized or filtered version just moves them confidently to the wrong place.
corners is an NPARRAY - a data array of points, not an image. The node's socket only accepts an NPARRAY link, which is deliberate: this is the thing cv2.goodFeaturesToTrack emits, and cv2.findChessboardCorners output also lands here through the array bridges. You cannot type corners into a widget.
winSize is half the search window, as a two-component CV_TUPLE - the default (0, 0) is useless, and (5, 5) means an 11×11 search window. Wire it from CV Tuple or type the pair in place. zeroZone is also a CV_TUPLE, defaulting to (-1, -1), which means "don't exclude anything". The termination criteria are split into three widgets - because a cv2 TermCriteria has no widget form: criteria_type ("max count or epsilon (whichever first)" / "max count only" / "epsilon only"), criteria_max_count (30) and criteria_epsilon (0.001). Those defaults are good; the one to raise is criteria_max_count on badly-focused images.
One output, nparray: the refined points, same layout as the input. From there the usual destinations are CV Triangulate Points, cv2.solvePnP, or just CV Draw Points / CV Annotate Points to see what moved.
The usual chain
cv2.goodFeaturesToTrack → nparray (points)
↓
cv2.cornerSubPix(image, corners, winSize=(5,5), zeroZone=(-1,-1))
↓
CV Draw Points (to eyeball it) or pose/triangulation nodes
Feed cv2.goodFeaturesToTrack a maxCorners in the 50–500 range with qualityLevel around 0.01 and you get a sane seed set; cornerSubPix then does the precise half of the job.
Installing it
Manager → search the pack title (ComfyUI CV) → install → restart. Manual:
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 and a V3-API ComfyUI. Heads-up on that pin: it pulls numpy 2.x, and this ecosystem has a well-documented fight over it - insightface 0.7.3 wants numpy 1.x, opencv wants 2.x, and the people who hit it lose an afternoon or two. If your FaceID/IPAdapter nodes died the day you installed anything opencv-shaped, that's the collision, not this node.
Common issues and troubleshooting
"(-215) ... assertion failed" on the corners array. The points aren't in the layout cv2 expects (single-precision floats, N×1×2 or N×2). If you built the point list by hand, cast it - CV Cast Array to float32 - and check the shape with Inspect CV Data.
Corners moved a lot, or not at all. winSize too large and the refinement latched onto a neighbouring structure; too small and there was nothing to fit. Stay near (5, 5), and keep winSize comfortably smaller than the distance between corners.
No nparray output downstream because you fed IMAGE corners. The corners socket only takes NPARRAY. Corners from a keypoint-based node (CV Detect Corners) come out as KEYPOINTS, not points - use the raw cv2.goodFeaturesToTrack wrapper for the point-array route, or convert through the points/array bridge nodes.
A batch of images. The wrapper works on one frame; if you feed an IMAGE batch, remember that the corners you refined belong to a specific frame. Loop or unstack with CV Unstack Batch rather than assuming frame 0 is representative.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| image | NPARRAY,IMAGE,MASK | Input single-channel, 8-bit or float image. 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. | |
| corners | NPARRAY | Initial coordinates of the input corners and refined coordinates provided for output. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here. | |
| winSize | CV_TUPLE | 0,0 | Half of the side length of the search window. For example, if winSize=Size(5,5) , then a $(5*2+1) \times (5*2+1) = 11 \times 11$ search window is used. One value with 2 components (w, h) - it travels as a whole, so it cannot arrive half-connected. Wire it from 'CV Tuple' or type the components in place. |
| zeroZone | CV_TUPLE | -1,-1 | Half of the size of the dead region in the middle of the search zone over which the summation in the formula below is not done. It is used sometimes to avoid possible singularities of the autocorrelation matrix. The value of (-1,-1) indicates that there is no such a size. One value with 2 components (w, h) - it travels as a whole, so it cannot arrive half-connected. Wire it from 'CV Tuple' or type the components in place. |
| criteria_type | COMBO | max count or epsilon (whichever first) | When to stop iterating: after max_count iterations, when the change drops below epsilon, or whichever comes first. |
| criteria_max_count | INT | 301–2147483647 | Maximum iterations (ignored when 'epsilon only'). |
| criteria_epsilon | FLOAT | 0.000–1e+38 | Target accuracy / smallest change worth continuing for (ignored when 'max count only'). |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| nparray | NPARRAY | — |