cv2.EMD
The distance metric you don't need until you really do
- signature1
- signature2
- retval
- lowerBound
- flow
If you arrived here hoping cv2.EMD measures how different two images are, take the exit now. EMD is the earth mover's distance - the classic Wasserstein-1 metric - and it compares two distributions, not two pictures. OpenCV's own documentation for it is one line long and that is a fair reflection of how often anyone reaches for it. But it is the correct tool for a specific job, and this wrapper exposes the whole function, so here is the job.
What it actually computes
Think of each input as a pile of dirt: a set of positions, each with a weight. EMD is the minimum total cost of shovelling one pile into the shape of the other, where the cost of moving a unit of dirt is measured with distType. That is the whole idea, and it is why it shows up in histogram comparison, palette analysis and any "how far apart are these two distributions" question.
That structure is the thing to internalise, because it dictates the input format. A signature is a 2-D matrix of shape N x (dim + 1): the first dim columns are the position of each pile and the last column is its weight. Two signatures must have the same dim, and the rows are independent - nothing needs to line up between signature1 and signature2. That is the difference from every point-matching node in this pack, which all want correspondences.
The inputs and outputs that matter
signature1 and signature2 are both NPARRAY only - the tooltip spells it out ("A data array (points / matrix), NOT an image"). You cannot wire an IMAGE into them. Build them with Parse Matrix (a whitespace/delimiter-separated text block is the easiest way to hand-write a signature), or CV Numbers To Array followed by CV Reshape Array if you are assembling one from computed floats. distType defaults to DIST_L2; DIST_L1 and DIST_C are the alternatives, and picking one is about what you consider a unit of movement, not about accuracy.
lowerBound is a string field for a Python literal, and it is the interesting one: leave it blank for OpenCV's default, or pass a number to tell the solver "I already know the answer is at least this much, stop early if you can". That is genuinely useful in a filter chain where you only care whether two things are close - one thread of the solver's work disappears.
On the way out you get three sockets. retval is the distance itself - the number you came for. The second output is confusingly also named lowerBound and is OpenCV's own computed bound on the result, which is normally not what you want. flow is the NPARRAY transport plan: for each pile in signature1, how much of it was moved to each pile of signature2. It is a real object you can inspect with CV Array To Numbers or Preview CV Array, and the only reason to care is debugging - if retval looks wrong, the flow tells you why.
Where this fits in a ComfyUI graph
Honestly: it usually does not. The one workflow-shaped use is as a metric, not a filter. Compare a reference image's colour distribution to a generated one and you get a scalar saying "these palettes are 0.7 apart", which you can log per-batch, threshold, or feed into a comparison of two samplers. It is a QA number, and the community layer of ComfyUI has almost nothing like it - the post-processing node ecosystem is dominated by doing operations (see post-processing.md) rather than scoring them, which is precisely why a raw wrapper keeps being the only route to something like this.
If you need a histogram first, that is work you do by hand here: there is no calcHist-style convenience step in this batch of nodes, so a matrix you compute or paste in is the input.
Installing it
It rides along with the pack - every cv2.* wrapper installs at once:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
Or search comfyui_cv in ComfyUI Manager and restart. The pack needs Python ≥ 3.12 and a recent ComfyUI on the V3 node API. You find the node under image/CV/low-level/cv2 E.
Traps
Non-2-D or float64 signatures are the usual failure - OpenCV wants a float matrix, so an int array from a literal is a coin flip. Equal dim on both sides is mandatory. And forgetting the weight column silently changes the problem: an N x dim matrix gets parsed as dim-1 position columns plus whatever is in the last column, so if you have no weights, append a column of 1s and mean it. When a shape error appears, CV Inspect CV Data will tell you exactly what the array looks like before you blame the maths.
Inputs (4)
| Name | Type | Default | Description |
|---|---|---|---|
| signature1 | NPARRAY | - - - A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here. | |
| signature2 | NPARRAY | - - - A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here. | |
| distType | COMBO | DIST_L2 | - - - |
| lowerBoundopt | STRING | - - - Optional - leave blank to use the OpenCV default. Accepts a Python literal, e.g. 3, 1.5, true, or (3, 3). |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| retval | FLOAT | — |
| lowerBound | FLOAT | — |
| flow | NPARRAY | — |