Nodes/ComfyUI CV/cv2.xphoto.oilPainting (1/2)
ComfyUI Node

cv2.xphoto.oilPainting (1/2)

The overload where you pick the colour space the brush mixes in

By bmad4ever·Created 3 months ago·Updated 14 days ago· 1
cv2.xphoto.oilPainting (1/2)
  • src
  • result
◄size0►
◄dynRatio0►
◄codeCOLOR_BGR2Lab►

Digital "oil painting" is a wonderfully dumb algorithm that happens to work: for every pixel, look at a square neighbourhood, quantise the colours into a small number of bins, and replace the pixel with whichever bin is most common. Flat regions turn into single brush strokes. Edges turn into wobbly painterly boundaries. That's the whole thing, no model, no VRAM, milliseconds-to-seconds depending on how big a brush you ask for.

Why two nodes with the same name

OpenCV ships cv2.xphoto.oilPainting as two C++ overloads - one taking a colour-space conversion code, one not - and the pack's registry is generated from the type stubs, so each overload became its own node. You'll see them in the node list as cv2.xphoto.oilPainting (1/2) and (2/2), which is the pack being honest that they're near-duplicates. This one, (1/2), is the four-argument version: src, size, dynRatio, code.

Why you'd reach for it

Three real reasons, honestly ranked:

  1. Stylising a photo or a frame deterministically. Same input, same output, forever. A painterly LoRA is more convincing, but it's a re-generation - it changes the content. This changes only the rendering.
  2. As a pre-pass for something else. Flattening an image into paint-like blobs before edge detection, contour work or segmentation removes high-frequency noise and leaves bigger, cleaner shapes. Cheap trick, real effect.
  3. Building a round-trip demo. The pack uses it this way: stylise, then de-stylise or measure, and check the pipeline behaves. If you're learning the low-level half of the pack, this is a good node to learn it on because the input is one image and the output is one image.

Where it's not the right tool: anything that needs to look like a real painting to a human who cares. Oil paint has directional strokes that follow form; this has isotropic blobs. It reads as "filter" at 100%, and always will.

How it works, and what code really does

The image is converted into the colour space you choose, then binned - divided by dynRatio - and each pixel becomes the most common bin value in its neighbourhood. The pack's own note on the dropdown is the useful part: Lab (the default) and HSV separate hue from lightness, which is what gives a painterly look, because the algorithm then decides "same paint" based on colour rather than brightness. GRAY bins on brightness only, which is the fastest and the flattest. The other choices (COLOR_BGR2HLS, COLOR_BGR2LUV, COLOR_BGR2YCrCb, COLOR_BGR2XYZ, COLOR_BGR2YUV) are variations on "which three numbers is a colour", and they're worth a comparison run if you're chasing a specific look. The histogram is taken on the first plane only - so the space you pick is really about which single channel dominates the mode.

The inputs that matter

  • src - three-channel or single-channel image (CV_8UC3 or CV_8UC1); NPARRAY, IMAGE or MASK. It's promoted to three-channel if needed, because binning a colour space is meaningless on a mask.
  • size - the neighbourhood half-size: the window is (2*size+1)². The default is 0, which is a 1×1 window, i.e. the image unchanged. This is the "the node does nothing" report. Try 2–7: 5 is a broad, obvious brush; 7+ on a large image is a real wait.
  • dynRatio - the intensity quantisation step. Values are divided by it, so it's a divisor and the shipped default of 0 is not a usable setting - keep it at 1 or above. 1 keeps full precision (subtle), 2–4 gives coarser bins and flatter, more poster-like strokes.
  • code - the colour space combo, COLOR_BGR2Lab by default.

Output: result, echoing src's format. Low-level wrapper, so an IMAGE in still means an NPARRAY out - convert with CV Array → Image.

Install

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

Then restart, or install via Manager → ComfyUI CV. Requires opencv-contrib-python-headless~=5.0.0.93 - xphoto is contrib-only, so without a contrib build neither this node nor its twin is registered at all. Python 3.12+ and a V3-API ComfyUI round it out.

Traps

  • size = 0 does nothing; dynRatio = 0 is not a sane divisor. Both ship at 0 because that's what the stub says, not because it's a good default.
  • It's slow, and it grows quadratically in size. Sweep parameters on a 512-pixel crop, then run the winner at full size.
  • Colour shifts are part of the deal. Converting to Lab and back is lossy at 8 bits, and binning makes it worse. If you're stylising a plate that has to colour-match something else, expect to correct afterwards.
  • Edges get wobbly, not sharp. That's the algorithm, not a bug in the wrapper - painterly boundaries come from the mode jumping between bins. If you need clean edges, this is the wrong filter.
Categoryimage/CV/low-level/xphoto

Inputs (4)

NameTypeDefaultDescription
srcCOMFY_MATCHTYPE_V3Input three-channel or one channel image (either CV_8UC3 or CV_8UC1) 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.
sizeINT0-2147483648–2147483647neighbouring size is 2-size+1
dynRatioINT0-2147483648–2147483647image is divided by dynRatio before histogram processing
codeCOMBOCOLOR_BGR2Labcolor space conversion code(see ColorConversionCodes). Histogram will used only first plane

Outputs (1)

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