Preview CV Array
See the array, including the NaN pixels
- nparray
- image
What it's for
Raw cv2 output isn't a picture. It's a float32 array where a disparity map might range from 0.3 to 91.7, an optical-flow field has two signed channels that cancel each other out, a latent has sixteen channels nobody has ever looked at, and a distance transform has inf in the corners. Funnel any of that into ComfyUI's normal Preview Image and you get black, or white, or a type error.
Preview CV Array renders it anyway. It's the see-it-half of the debugging pair, and unlike most preview nodes it was clearly written by someone who has been confused by their own arrays at 2am: it handles scaling, sign, outliers and non-finite values explicitly, and it tells you which range it used.
The render modes
- image - shows uint8 or float values as-is. 1, 3 and 4 channels. For data that's already in display range.
- normalize - min-max stretches to 0..255. This is the one you want for gradients, disparity maps, score maps, anything where the interesting variation is inside a narrow band.
- heatmap - normalize, then apply the
colormappalette. DefaultCOLORMAP_VIRIDIS; the author's own note that JET "misreads as structure" is exactly right and worth taking seriously, because the rainbow map will have you chasing bands that aren't in your data.
Multi-channel arrays in normalize/heatmap mode get drawn as a 2×2 quadrant view with one shared value scale - so you can compare channels without the per-channel autoscaling lying to you. More than four channels and you need a different approach.
The controls worth understanding
reduce_channels - collapses multi-channel data to one 2-D map. KEEP_ALL leaves it alone; REDUCE_SUM/AVG/MAX/MIN/SUM2 are cv2.reduce's own reductions; MAGNITUDE_L2 is sqrt(sum of squares). That last one is the correct collapse for signed fields - an optical-flow field averaged across channels cancels to near zero and looks like an empty grey box, while its L2 magnitude shows you the motion. It's also the only way to preview more than four channels, which means it's the only way to look at a LATENT at all.
range_mode - per frame (auto) is the default and it's a trap in disguise: each frame stretches on its own min..max, which makes the frames of a batch incomparable and makes a near-static sequence flicker as the range jumps around. Switch to batch for one shared range, or manual with vmin/vmax when you know what the numbers should be. (If your manual range is inverted the node falls back to automatic rather than failing.)
clip_percent - a robust stretch: ignore this percentage of values at each end when computing min..max. 2.0 is a good default. Without it, one hot pixel or one huge distance-transform corner squashes everything else to black - the single most common "my preview is useless" cause.
color_scale - draws a labelled value bar beside the preview, with the actual min..max. It keeps a legible height even for a one-pixel array, and the label column is sized to the batch's widest labels so frames stay the same width.
Non-finite pixels are marked, not hidden
nan and inf - routine in distance transforms, optical flow and score maps - are excluded from the value range and painted in a colour the active colormap cannot produce. Pure red for viridis and the grey ramp; magenta or cyan for maps that contain red, like JET, HOT and AUTUMN. That's deliberate: an undocumented marker is a mystery, an obviously-wrong-coloured patch is a warning. Inspect CV Data will tell you how many there were.
A batched input previews every frame, and the node also returns the rendering as an IMAGE, so a preview can be chained into the rest of the graph if you actually want the visual.
Install
ComfyUI Manager, search ComfyUI CV. Or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
Restart afterwards. Needs Python ≥ 3.12, a recent ComfyUI (V3 node API) and the contrib OpenCV wheel:
pip install "opencv-contrib-python-headless~=5.0.0.93"
Where people get burned
Per-frame ranges make comparisons meaningless. Two frames that look the same brightness may have completely different data ranges. If you're judging anything across a batch, set range_mode to batch or manual first.
image mode on float data. A disparity map or gradient in image mode goes black. That's not a broken node, it's a scale mismatch - normalize it.
Signed data previewed as quadrants. Two channels of optical flow as quadrants tell you almost nothing; MAGNITUDE_L2 tells you where the motion is. Flip the mental model: for signed fields, magnitude is the picture.
A big red patch is not data. It's the non-finite marker. Go find out where your inf came from.
The repo's 03_debug_visualization_playground.json has these wired up already if you'd rather poke at a working example than build one.
Inputs (9)
| Name | Type | Default | Description |
|---|---|---|---|
| nparray | NPARRAY,IMAGE,MASK | 2-D or 3-D ndarray to render. image mode supports 1/3/4 channels; normalize/heatmap support 1/2/3/4 channels and draw multi-channel inputs as quadrants. More channels than that need 'reduce_channels'. A batched IMAGE/MASK/LATENT or a 4-D [B,H,W,C] array renders every frame. For other shapes use 'Inspect CV Data' instead. | |
| render | COMBO | image | image: show uint8/float values as-is. normalize: min-max stretch to 0-255 (for gradients/disparity/score maps). heatmap: normalize then apply the 'colormap' palette. |
| color_scaleopt | BOOLEAN | false | Draw a vertical colour/value scale bar with min..max labels next to the preview. The labels are the array's value range ('normalize'/'heatmap' map that range to the bar exactly). On a batch each frame gets its own range unless 'range_mode' says otherwise, and the label column is widened to the batch's widest labels so the frames stay the same width (the surplus is black). |
| reduce_channelsopt | COMBO | KEEP_ALL | Collapse a multi-channel array to ONE 2-D map before rendering, instead of drawing the channels as quadrants. KEEP_ALL leaves the array alone. REDUCE_SUM/AVG/MAX/MIN/SUM2 are cv2.reduce's reductions across the channel axis. MAGNITUDE_L2 is sqrt(sum of squares) - use it for SIGNED fields (optical flow, gradients), where AVG and SUM cancel to near zero. This is also the only way to preview an array with more than 4 channels, such as a LATENT. The reduction runs in float32, so SUM cannot wrap. |
| colormapopt | COMBO | COLORMAP_VIRIDIS | Palette for 'heatmap' (ignored by the other render modes). VIRIDIS/INFERNO are perceptually uniform and the right default for data; JET is the classic rainbow and misreads as structure. The colour bar and the NaN marker both follow this choice. |
| range_modeopt | COMBO | per frame (auto) | Which value range 'normalize'/'heatmap' stretch to 0-255 (ignored by 'image'). per frame: each frame uses its own min..max - the default, but it makes the frames of a batch incomparable and a near-static sequence flicker. batch: one min..max shared by every frame. manual: the fixed 'vmin'..'vmax' below, with out-of-range values clamped. |
| vminopt | FLOAT | 0.00 | Bottom of the value range, used only when 'range_mode' is manual. If vmin >= vmax the node falls back to the automatic range rather than failing. |
| vmaxopt | FLOAT | 1.00 | Top of the value range, used only when 'range_mode' is manual. |
| clip_percentopt | FLOAT | 0.00–49 | Robust stretch for the automatic range modes: ignore this percentage of values at EACH end when picking min..max, so a single outlier (a hot pixel, a huge distanceTransform corner) cannot flatten the rest of the map to black. 2.0 is a good starting point. 0 = plain min..max. Ignored when 'range_mode' is manual. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |