cv2.bitwise_xor
The mask op that means 'in one, or the other, but not both'
- src1
- src2
- mask
- result
Of the four bitwise ops, XOR is the one people have to think about. AND is intersection, OR is union, and XOR is symmetric difference: a pixel survives if exactly one of the two inputs has it set. That single property is why this node exists in a ComfyUI graph - it's how you say "changed" rather than "present".
It comes from comfyui_cv, a pack that exposes ~470 raw cv2.* functions as nodes, so this is the OpenCV call with ComfyUI sockets bolted on, not a curated composition of it. Which is the honest framing: what you get is cv2.bitwise_xor, faithfully, plus the pack's socket machinery.
Where XOR actually earns its place
Two masks, and you want the pixels that belong to one region or the other but not the overlap - that's XOR, and no amount of AND/OR gets you there without a subtract in between. In a detection-and-detail loop (the detect → crop → re-render → paste-back pattern) that's the difference between "mask everything either detector found" and "mask only what the two detectors disagree about".
The second use is change detection. XOR two frames and any bit that moved shows up as non-zero; identical pixels cancel to exactly 0. On binary masks (which is how masks arrive here - 0/1) this behaves exactly like a logical XOR. On actual photos it does bit-level arithmetic, not a perceptual difference, which is why it looks like near-black noise: two similar photographs differ in their low bits, so XOR leaves a sparse spray of values 1–40 and returns black everywhere the bits agree.
For continuous image differencing you want cv2.absdiff or cv2.subtract from the same pack. Reach for XOR when the data is genuinely binary, or when you want the low-bit noise - XORing a keyed pattern back out of an image is the classic that survives XOR-with-a-pad.
How it works, and the three inputs
The node writes src1 ^ src2 element-wise. src1 is the format-deciding input: it's a match type, so an IMAGE link comes back as IMAGE, a MASK as MASK, an NPARRAY as NPARRAY. src2 is independent - it accepts NPARRAY, IMAGE or MASK.
src1(required) - first operand, and the one that sets the output's socket type.src2(required) - second operand. Same size and type assrc1or OpenCV raises.mask(optional) - a CV_8U / CV_8S / CV_Bool single-channel array. Where it's non-zero, the element gets written; elsewhere it isn't. Leave it unconnected and the whole array is combined.
Output is one socket, result. Wire it to any mask consumer - a mask preview, a detailer's mask input, or CV Array → Mask if you were operating on raw arrays.
Two behaviours worth knowing before you debug anything. First, IMAGE inputs are converted to uint8 and quantized - ComfyUI's 0–1 float tensor round-trips through 0–255, so two frames that differ subtly can come out bit-identical and XOR to solid black. Second, arithmetic-style batch handling: when both image inputs are IMAGE batches of the same size, the whole batch flows through; a mismatch means only frame 0 is used. If your graph feeds a 4-frame MASK into one socket and a 100-frame MASK into the other, you'll get one frame of output and wonder why.
Install
Standard custom-node business - ComfyUI Manager, search "comfyui_cv", or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
Then restart. The pack needs Python ≥ 3.12, a recent ComfyUI on the V3 node API, and opencv-contrib-python-headless (it pins ~=5.0.0.93, and behaviour is curated against that version):
pip install "opencv-contrib-python-headless~=5.0.0.93"
No models, nothing to download for this node.
Common issues
The result is solid black. Either your two inputs are byte-identical after the uint8 round trip, or the values genuinely cancel - XOR of a byte with itself is 0. Check with two masks first; if that works, the problem is quantization on the IMAGE path, and absdiff is the node you wanted.
"Sizes of input arguments do not match." src1 and src2 have different dimensions. XOR has no broadcasting here; use cv2.broadcast or resize one side.
The mask does nothing. It has to be single-channel 8-bit (or CV_Bool). A 3-channel mask array from an image conversion is silently the wrong thing - run it through Mask → CV Array instead of Image → CV Array.
The node isn't in the menu at all. Low-level wrappers are generated at import time from whatever your installed OpenCV build exposes, so a missing entry means the pack failed to load or your cv2 is wrong - see the pack's tools/repair_opencv_contrib.py, which diagnoses (--check) and fixes (--apply) a non-contrib wheel having empty-contrib'd your site-packages/cv2.
Inputs (3)
| Name | Type | Default | Description |
|---|---|---|---|
| src1 | COMFY_MATCHTYPE_V3 | first input array or a scalar. 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. | |
| src2 | NPARRAY,IMAGE,MASK | second input array or a scalar. 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. | |
| maskopt | NPARRAY,IMAGE,MASK | optional operation mask, CV_8U, CV_8S or CV_Bool single channel array, that specifies elements of the output array to be changed. 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. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| result | COMFY_MATCHTYPE_V3 | Echoes the 'src1' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY. |