Nodes/ComfyUI CV/cv2.divSpectrums
ComfyUI Node

cv2.divSpectrums

Frequency-domain division, i.e. deconvolution by hand

By bmad4ever·Created 3 months ago·Updated 14 days ago· 1
cv2.divSpectrums
  • a
  • b
  • nparray
◄flagswhole 2-D spectra►
◄conjBfalse►

Complex division of two Fourier spectra, element by element. Two 2-channel (real, imaginary) arrays in, one out. It looks like a curiosity until you notice what it is: if spectrum A is your image and spectrum B is your blur kernel, then A/B is deconvolution. That's the whole Wiener-filter family, running as one node.

What the two inputs are

Both a and b are typed NPARRAY, and the tooltips are blunt about it: "a data array (points / matrix), NOT an image - only an NPARRAY link is accepted here." You cannot wire an IMAGE into this node. You get there with cv2.dft set to DFT_COMPLEX_OUTPUT, which turns a single-channel float image into a 2-channel (re, im) spectrum. Two spectra of the same size, and you're in business.

That detail is the main practical difference between this node and the pack's other wrappers: the arithmetic nodes happily take IMAGE and echo it back, while the spectrum family speaks only NPARRAY.

flags is the same choice cv2.mulSpectrums offers - "whole 2-D spectra" (0, the default) or "each row is an independent 1-D spectrum (DFT_ROWS)". If you transformed the whole image in one dft, leave it.

conjB divides by the conjugate of b instead of b. That flips the operation from division into correlation with the kernel - which is what you actually want when your real goal is "match the image against this pattern in frequency space" rather than "undo this filter".

Output: one nparray, still 2-channel float. To get an image back, hand it to cv2.idft with DFT_SCALE | DFT_REAL_OUTPUT.

Why you'd do this at all

Because a very small kernel spread over a lot of pixels is cheap in frequency space and expensive in the spatial domain, and because a Wiener-style deblur is expressible as arithmetic on spectra. The pack's own Fourier playground shows the neighbouring version of the recipe - the out-of-focus and motion deblur filters that shape a PSF spectrum and combine it with the image spectrum, then inverse-transform. This node is the pure division step in that family: Hw = Re(H) / (Re(H)² + 1/SNR) is the same idea with a denominator that's been regularized so you don't divide by near-zero noise.

The honest framing: "sharpen" in the spatial domain is unsharp masking - blur a copy, subtract, add back scaled. The KB's docs/knowledge/post-processing.md covers that; it's the right tool for 95% of images. Frequency division is the tool for undoing a known blur - out-of-focus, motion smear - which unsharp masking cannot do because it doesn't know the kernel's shape.

Installing comfyui_cv

From bmad4ever/comfyui_cv: about 470 auto-generated raw cv2.* wrappers plus curated nodes, a GPL-3.0 fork of Gerold Meisinger's opencv-comfyui, curated against OpenCV 5.0.0.93. Manager: search ComfyUI CV. Or:

cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv

Restart. Requires Python ≥ 3.12 and a recent ComfyUI (V3 node API), one dependency: opencv-contrib-python-headless~=5.0.0.93. The contrib wheel matters; a non-contrib opencv-python installed afterwards silently empties the contrib submodules and removes their nodes. tools/repair_opencv_contrib.py --check / --apply handles that.

Common issues

"(-215) ... type == CV_32FC2". One of your inputs isn't a 2-channel float spectrum. Re-run cv2.dft with DFT_COMPLEX_OUTPUT, and make sure you didn't inverse-transform it already.

"Sizes of input arguments do not match". Both spectra must be the same dimensions - which in practice means zero-padding both the image and the PSF to the same canvas before the DFT. cv2.getOptimalDFTSize plus a padded copy is the standard dance.

Ringing and wrap-around stripes at the frame edges. The DFT assumes your image tiles infinitely. Taper the borders before transforming (a border copy, or the copyMakeBorder trick) or the deblur will manufacture bright edges that aren't in the data.

Garbage where the kernel has no energy. Dividing by a spectrum near zero amplifies noise into structure. That is exactly the failure the Wiener denominator fixes; a bare division like this one is only safe when you control the kernel's zeros.

Can't see the output. It's two channels of float. Preview CV Array, or idft it first.

Categoryimage/CV/low-level/cv2 D

Inputs (4)

NameTypeDefaultDescription
aNPARRAY - - - A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here.
bNPARRAY - - - A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here.
flagsCOMBOwhole 2-D spectra - - -
conjBoptBOOLEANfalse - - - Preset to the OpenCV default (False).

Outputs (1)

NameTypeDescription
nparrayNPARRAY—