Nodes/ComfyUI CV/cv2.adaptiveThreshold
ComfyUI Node

cv2.adaptiveThreshold

Stop blowing out half the page

By bmad4ever·Created 3 months ago·Updated 14 days ago· 1
cv2.adaptiveThreshold
  • src
  • result
◄maxValue0.0000►
◄adaptiveMethodADAPTIVE_THRESH_GAUSSIAN_C►
◄thresholdTypeTHRESH_BINARY►
◄blockSize0►
◄C0.0000►

Global thresholding picks one number for the whole image, which is fine until your photo has a bright corner and a shadowed one. Then the bright half turns white, the dark half turns black, and you go looking for a better filter. This is the better filter: the threshold is computed per pixel from its local neighbourhood.

Best use in this pack: cleaning up a photo of a document, a receipt, or printed text before you hand it to OCR - or producing a hard mask out of an image with uneven lighting.

How it works

For every pixel, OpenCV computes the mean (or Gaussian-weighted mean) of a blockSize × blockSize neighbourhood, subtracts C, and compares the pixel against that. Darker than the local average minus C → foreground.

That's the whole trick: a shadow gradient stops mattering, because the local mean moves with it. What used to be a global problem becomes a local contrast decision.

One genuinely nice pack-specific detail: src is registered as a single-channel-requiring parameter, so if you link a normal 3-channel IMAGE into it, the wrapper converts it to grayscale for you (BGR→GRAY) rather than making you insert a cvtColor node. You can also feed a MASK, which arrives as a single-channel array already.

Inputs and outputs

  • src (COMFY_MATCHTYPE_V3) - source 8-bit single-channel. IMAGE links work (auto-grayed); a MASK works; NPARRAY must be uint8 and single-channel.
  • maxValue (FLOAT) - the value written where the condition is satisfied. The default is 0, which produces an all-black image. Set it to 255. This is the trap.
  • adaptiveMethod - ADAPTIVE_THRESH_GAUSSIAN_C (default) or ADAPTIVE_THRESH_MEAN_C. Gaussian weights the neighbourhood, so it's less jumpy around gradients and noise; it's slightly slower.
  • thresholdType - THRESH_BINARY or THRESH_BINARY_INV. Print is usually INV if you want dark text as white-on-black for a mask.
  • blockSize (INT) - the neighbourhood size. Odd, > 1. It must be larger than the features you want to keep - too small and the interior of a thick stroke becomes "background".
  • C (FLOAT) - the constant subtracted from the local mean. Small (2–5) keeps fine texture and noise; larger (10–15) drops both.
  • result - echoes src's format: IMAGE in, IMAGE out; MASK in, MASK out.

The pack's own threshold playground uses blockSize = 31, C = 7 for a page of text - a sane starting point to copy.

Parameter intuition

blockSize is a scale control. C is a contrast gate: at C = 0 you get every speckle of dust, because any pixel below its local mean becomes foreground. Turn C up until the noise goes and the strokes survive. GAUSSIAN_C plus C ≈ 10 is the document-scanner default everyone converges on.

Common follow-ups: cv2_morphologyEx (open, then close) to despeckle, or the curated CV Find Contours on the result.

Install

ComfyUI Manager → comfyui_cv (ComfyUI CV), or:

cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"

Restart ComfyUI afterwards. Python ≥ 3.12, recent V3-API ComfyUI, behaviour curated against OpenCV 5.0.0.93. The contrib wheel is the required one; a non-contrib install over it silently removes contrib submodules.

Where people get burned

blockSize left at 0, or set even. OpenCV asserts on it - you'll see an assertion failure in the log rather than a graceful error. Odd numbers only: 3, 5, 7 … 31.

maxValue left at 0. A perfectly valid operation that outputs an all-black image. It's not broken; it's doing what you asked.

blockSize smaller than the strokes. The inside of a letter becomes background and you get hollow outlines. Go bigger.

Expecting colour. The output is a binary single-channel image. If you need a matte, that's fine - but for an IMAGE output it comes back as a picture with 0 or 255.

When a global threshold would have been better. Evenly lit, high-contrast scans don't need local adaptation, and local methods add halo artefacts around big dark blobs. cv2_threshold with the OTSU flag (via CV Threshold Flags) is the right tool there, and it picks the level for you. For a statistically-motivated local alternative, the pack also wraps ximgproc.niBlackThreshold.

Categoryimage/CV/low-level/cv2 A

Inputs (6)

NameTypeDefaultDescription
srcCOMFY_MATCHTYPE_V3Source 8-bit single-channel image. The image output(s) echo this input's format. 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.
maxValueFLOAT0.0000-1e+38–1e+38Non-zero value assigned to the pixels for which the condition is satisfied
adaptiveMethodCOMBOADAPTIVE_THRESH_GAUSSIAN_CAdaptive thresholding algorithm to use, see #AdaptiveThresholdTypes. The #BORDER_REPLICATE | #BORDER_ISOLATED is used to process boundaries.
thresholdTypeCOMBOTHRESH_BINARYThresholding type that must be either #THRESH_BINARY or #THRESH_BINARY_INV, see #ThresholdTypes.
blockSizeINT0-2147483648–2147483647Size of a pixel neighborhood that is used to calculate a threshold value for the pixel: 3, 5, 7, and so on.
CFLOAT0.0000-1e+38–1e+38Constant subtracted from the mean or weighted mean (see the details below). Normally, it is positive but may be zero or negative as well.

Outputs (1)

NameTypeDescription
resultCOMFY_MATCHTYPE_V3Echoes the 'src' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY.