Nodes/ComfyUI CV/cv2.mulSpectrums
ComfyUI Node

cv2.mulSpectrums

Multiply in frequency space (this is how convolution gets fast)

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

This is the one node in the pack that does nothing you can look at. It multiplies two Fourier spectra together, element-wise, with complex-number arithmetic. The reason to care: convolution - blurring, sharpening, deblurring, template matching, filtering out periodic noise - is a multiplication in the frequency domain, and that's how OpenCV's Wiener-filter and notch-filter style pipelines actually work. The pack ships both as subgraphs (CV Wiener Filter (deblur), CV Notch Reject Filter (periodic noise)), and each one has a cv2_mulSpectrums sitting in the middle.

The other reason to care is the KB's post-processing doc's standing complaint: motion deblur and grain-adjacent operations are patchy in ComfyUI and people leave for a compositor. The raw Fourier primitives are here; the ergonomics are not.

The packed-complex thing

OpenCV never gives you a "complex array" type. A DFT of a real image with DFT_COMPLEX_OUTPUT comes back as a 2-channel float array where channel 0 is the real part and channel 1 is the imaginary part, interleaved. cv2.mulSpectrums expects exactly that packing on both inputs and produces the same packing on the way out. That's why both a and b are typed NPARRAY only - a data array, in the pack's own wording, not an image. Feeding it a picture is a type error waiting to happen.

Then conjB: set it true and the second spectrum is conjugated before the multiply. That single boolean is the difference between convolution and correlation - and cross-correlation is what you want for matched filtering and template matching, since it's the one that peaks when the two signals align without a flip. This is the knob beginners miss; if your Wiener/deblur result looks like a mirrored mess, check conjB.

flags is a two-option dropdown: whole 2-D spectra (value 0, the normal case - multiply the two 2D spectra as one image) or each row is an independent 1-D spectrum (DFT_ROWS). The second form matters if you built the spectra row-by-row with DFT_ROWS too; the two must agree or you're multiplying things that don't correspond.

Inputs, output, and the required follow-up

Inputs: a and b, both NPARRAY, same size and type. Optional conjB. Output: one NPARRAY on the nparray socket, still packed complex - not previewable as an image, so you can't just eyeball it.

The inverse happens next: cv2_idft with DFT_SCALE | DFT_REAL_OUTPUT - scale, because the inverse transform is otherwise unnormalized by a factor of the pixel count, and real-output, because you want an image and not another complex array. The pack provides CV DFT Flags for authoring those bit combinations instead of typing the constants. Only after that do you have a picture.

The other half of using this correctly is context: multiplying spectra computes a circular convolution, so anything that wraps around the border folds back into the opposite side. Real deblurring pads both the image and the kernel first (cv2_copyMakeBorder, sized with cv2_getOptimalDFTSize) so the wrap lands in the padding. The bundled Wiener subgraph does that boundary dance on both inputs, and it's the part worth copying even if you don't copy the rest.

Install

ComfyUI Manager → search ComfyUI CV → install → restart, or:

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, a ComfyUI new enough for the V3 node API. Core cv2 - no contrib submodule, no models.

Realistic expectations

  • Both spectra must match in size and type. Different padding sizes are the usual mistake, and the failure is either an assertion or a subtly wrong image.
  • Frequency-domain filtering is fiddly by nature. The order of dft → mulSpectrums → idft is easy; choosing the filter, tuning it, and not ringing is the actual work. Start from the bundled subgraphs.
  • You will not beat cv2_filter2D for a small kernel. Spatial convolution wins below a handful of taps. FFT multiplication only pays off when the kernel is large or you're doing something spatial filtering can't express.
  • If these nodes are missing from your menu, check CV Build Information - the low-level wrappers are generated against your installed cv2.
Categoryimage/CV/low-level/cv2 M

Inputs (4)

NameTypeDefaultDescription
aNPARRAYfirst input array. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here.
bNPARRAYsecond input array of the same size and type as src1 . A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here.
flagsCOMBOwhole 2-D spectraoperation flags; currently, the only supported flag is cv::DFT_ROWS, which indicates that each row of src1 and src2 is an independent 1D Fourier spectrum. If you do not want to use this flag, then simply add a `0` as value.
conjBoptBOOLEANfalseoptional flag that conjugates the second input array before the multiplication (true) or not (false). Preset to the OpenCV default (False).

Outputs (1)

NameTypeDescription
nparrayNPARRAY—