CV Local Linear Fit (per pixel)
A regression at every pixel, and the matte that falls out of it
- target
- predictor
- slope
- intercept
- predictor_sigma
- residual
This is one of the handful of nodes in the pack that isn't a wrapper around an OpenCV function at all - the README calls it out by name as a genuine exception. It fits target ≈ slope * predictor + intercept inside a window around every single pixel, and returns the fit as image-sized maps. Its global counterpart in the pack fits one gain/bias pair for the whole frame; this one fits a gain per pixel. And the reason that's interesting is that it's the algebra underneath guided filtering, flat-field correction, and clean-plate matting.
The inputs worth understanding
target is the thing being explained (the y) and predictor is what explains it (the x) - same size and channel count. radius (default 5) sets the window: the fit at each pixel uses a (2·radius+1) square around it. Bigger windows average out noise but assert that the relationship holds over a wider area, which for matting means the alpha edge gets smeared. Keep it just large enough to contain some predictor texture - that phrase is the whole tuning rule.
scope is pooled versus per channel. Pooled fits one slope per pixel using all channels as evidence, and the node's own tooltip argues for it: it's far more stable, and it's the right choice when the slope is a physical scalar like 1−alpha. Per channel fits B, G and R independently and can capture a colour shift. If you're doing photometric work rather than colour work, stay pooled.
window chooses box (uniform) or gaussian (sigma = radius/2) averaging; gaussian makes smoother maps with less blocky edges. ridge is optional and adds to the predictor variance before dividing, in squared input units (~1.0 for the noise of an 8-bit image) - it pulls the slope toward 0 where the predictor barely varies instead of letting noise decide. Zero by default.
Why the matting thing works
A shot of a subject over a known background obeys C = (1−a)·B + a·F. Regress the shot (target) on the clean plate (predictor) and the slope is 1−a and the intercept is a·F, wherever alpha and the foreground colour are roughly constant across the window. That's a matte as a by-product of a regression - no model, no download, and it's why this node is filed next to the pack's flat-field and photometric nodes rather than under segmentation.
The outputs tell you whether to trust it. slope is float32 [H,W] pooled or [H,W,C] per channel, and it's 0 where the predictor is flat. intercept is the local mean of the target minus slope times the local mean of the predictor - the target where the predictor would be zero, which for flat-field work is the shading offset. predictor_sigma is how much the predictor varies inside the window: "the confidence of the slope, in input units," where a flat region reads ~0 and its slope means nothing. residual is the local standard deviation of the target around the fitted line - large where a single line can't describe the window, e.g. an alpha edge crossing it. That's your edge map, for free.
A calm design choice repeated for emphasis: it never raises on flat data. Where there's nothing to fit, slope is 0 and the intercept holds the local mean, and you're expected to consult predictor_sigma to find those pixels. That means "all zeros" is a legitimate answer rather than an error, and a graph that treats zero slope as a failure is a graph that will fail.
Install
ComfyUI Manager, search comfyui_cv (bmad4ever/comfyui_cv), or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
Restart ComfyUI. Needs Python ≥ 3.12 and a recent ComfyUI on the V3 node API - this pack declares everything as io.ComfyNode with io.Schema and ships no NODE_CLASS_MAPPINGS, so it won't load on an old build. Dependency:
pip install "opencv-contrib-python-headless~=5.0.0.93"
Pinned, and it must be contrib: all four OpenCV wheels share one site-packages/cv2, so a non-contrib install on top of a contrib one silently guts the contrib submodules and the contrib nodes disappear from the menu without an error. The pack ships tools/repair_opencv_contrib.py (--check, --apply).
Reality check
This is the fiddliest node in the pack's curated set, and it's worth being honest that clean-plate matting is a technique that needs a plate - it's not a background remover and it won't work on a photo without a matching background reference. Where it does shine: flat-field/shading estimation, guided-filter-style smoothing, and any local "how does A track B" question where you want the relationship per pixel rather than per image.
And the caveat from the README that applies to the whole pack: heavy LLM assistance in development, examples tuned to specific datasets, no planned updates, and its own recommendation against production use without independent review. For a node whose algorithm is the report-worthy exception, read the source before you trust the number.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| target | NPARRAY | The array being explained (the 'y' of the fit). [H,W] or [H,W,C], any dtype; the maths runs in float32. | |
| predictor | NPARRAY | The array explaining it (the 'x'). Same size and channel count as the target. | |
| radius | INT | 51–200 | Half-size of the window, in pixels: the fit at each pixel uses a (2*radius+1) square around it. Bigger windows average out noise but assume the relationship holds over a wider area - for matting that means the alpha edge gets smeared, so keep it just large enough to contain some predictor texture. |
| scope | COMBO | pooled (one slope for all channels) | Pooled fits ONE slope per pixel using every channel as evidence (the right choice when the slope is a physical scalar, e.g. 1-alpha in matting, and it is far more stable). Per channel fits B, G and R independently, which also captures a colour shift. |
| window | COMBO | box (uniform) | Shape of the averaging window. Box weights every pixel in the square equally; gaussian (sigma = radius/2) weights the centre more, which makes the maps smoother and their edges less blocky. |
| ridgeopt | FLOAT | 00–1000000 | Added to the predictor variance before dividing, in SQUARED input units (so ~1.0 for the sensor noise of an 8-bit image). It pulls the slope toward 0 where the predictor barely varies instead of letting noise decide it. |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| slope | NPARRAY | float32 [H,W] (pooled) or [H,W,C] (per channel). 0 where the predictor is flat. |
| intercept | NPARRAY | float32, same shape as the inputs: local_mean(target) - slope * local_mean(predictor), i.e. the target's value where the predictor would be 0. |
| predictor_sigma | NPARRAY | float32 [H,W] (pooled: sqrt of the summed per-channel variance) or [H,W,C]. How much the predictor varies inside the window - the confidence of the slope, in input units. A flat region reads ~0 and its slope means nothing. |
| residual | NPARRAY | float32: local standard deviation of the target AROUND the fitted line, in input units. Large where a single line cannot describe the window (e.g. an alpha edge crossing it). |