Skip to content

Commit a3749a2

Browse files
committed
Add earth mover's distance
1 parent f0a303f commit a3749a2

File tree

3 files changed

+91
-2
lines changed

3 files changed

+91
-2
lines changed

src/ImageDistances.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
__precompile__()
2-
31
module ImageDistances
42

53
import Base: size, ==
@@ -9,10 +7,12 @@ import Distances: evaluate, colwise, pairwise
97

108
using Colors
119
using ProgressMeter
10+
using EarthMoversDistance
1211

1312
include("generic.jl")
1413
include("hausdorff.jl")
1514
include("ciede2000.jl")
15+
include("earthmover.jl")
1616

1717
export
1818
# generic types
@@ -24,6 +24,7 @@ export
2424
Hausdorff,
2525
ModifiedHausdorff,
2626
CIEDE2000,
27+
EarthMoverBinned,
2728

2829
# helper functions
2930
hausdorff,

src/earthmover.jl

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using EarthMoversDistance: CSignature, CFlow, FLOW_ARRAY_SIZE
2+
3+
struct EarthMoverBinned{F,H<:AbstractArray,C} <: ImageMetric
4+
binner!::F
5+
histA::H
6+
histB::H
7+
sigA::CSignature
8+
sigB::CSignature
9+
cost::C
10+
cflow::Vector{CFlow}
11+
end
12+
13+
function costlb(ip, jp, edges::AbstractVector{T}) where T
14+
# lower-bound cost
15+
# Adjacent bins have no cost, because you may move by "epsilon" and cross over
16+
# to the next bin
17+
i, j = ip[], jp[]
18+
ii, jj = Int(i), Int(j)
19+
# The final bin is for NaN pixels, which match with zero cost
20+
(abs(ii-jj) <= 1 || ii == length(edges)+2 || jj == length(edges)+2) && return zero(Cfloat)
21+
return Cfloat(ii > jj ? edges[ii-1] - edges[jj] : edges[jj-1] - edges[ii])
22+
end
23+
24+
function EarthMoverBinned(edges::AbstractVector, cost=(i, j)->costlb(i, j, edges))
25+
binner! = graybinner(edges)
26+
len = length(edges)+2
27+
len > FLOW_ARRAY_SIZE && error("edges exceeded maximum allowed size of $(EarthMoversDistance.FLOW_ARRAY_SIZE)")
28+
histA, histB = Vector{Cfloat}(undef, len), Vector{Cfloat}(undef, len)
29+
sigA, sigB = CSignature(histA), CSignature(histB)
30+
costfun = @cfunction $cost Cfloat (Ref{Cfloat}, Ref{Cfloat})
31+
cflow = fill(CFlow(0, 0, 0), FLOW_ARRAY_SIZE)
32+
metric = EarthMoverBinned(binner!, histA, histB, sigA, sigB, costfun, cflow)
33+
# For reasons that are not clear, the first usage errors unless we do the following:
34+
evaluate(metric, [0.0f0], [0.0f0])
35+
return metric
36+
end
37+
38+
function evaluate(em::EarthMoverBinned, imgA::AbstractArray, imgB::AbstractArray)
39+
em.binner!(em.histA, imgA)
40+
em.binner!(em.histB, imgB)
41+
# Do the ccall directly to avoid the allocations
42+
cflowsizeptr = Ref{Cint}(0)
43+
res = ccall((:emd, :emd), # function name and library name
44+
Cfloat, # return type
45+
(Ref{CSignature}, Ref{CSignature}, Ptr{Nothing}, Ref{CFlow}, Ref{Cint}), # argument types
46+
Ref(em.sigA), Ref(em.sigB), em.cost, em.cflow, cflowsizeptr)
47+
return res
48+
end
49+
50+
function graybinner(edges::AbstractVector)
51+
@assert !Base.has_offset_axes(edges)
52+
ord = Base.Order.ForwardOrdering()
53+
@assert issorted(edges, ord)
54+
let ord=ord # julia issue #15276
55+
function graybinner!(hist, A)
56+
fill!(hist, 0)
57+
for a in A
58+
if isnan(a)
59+
hist[end] += oneunit(eltype(hist))
60+
else
61+
hist[searchsortedfirst(edges, a, ord)] += oneunit(eltype(hist))
62+
end
63+
end
64+
return hist
65+
end
66+
end
67+
end

test/runtests.jl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,25 @@ evaluate(d::MyImgDist, imgA, imgB) = π
5353
@test ciede2000(A, B) 0
5454
@test ciede2000(A, B) == ciede2000(B, A)
5555
end
56+
57+
@testset "Earth Mover" begin
58+
edges = 0.0:0.2:1.0
59+
metric = EarthMoverBinned(edges)
60+
imgA = fill(0.1, 5, 5)
61+
imgB = fill(0.3, 5, 5)
62+
@test evaluate(metric, imgA, imgA) == 0
63+
@test evaluate(metric, imgB, imgB) == 0
64+
# Since A and B are on opposite sides of the bin boundary, the cost should be 0
65+
# (i.e., epsilon)
66+
@test evaluate(metric, imgA, imgB) < 1e-8
67+
imgB = fill(0.5, 5, 5)
68+
@test evaluate(metric, imgA, imgB) step(edges)
69+
imgB = [fill(0.1, 5) fill(0.3, 5) fill(0.5, 5) fill(0.7, 5) fill(0.9, 5)]
70+
@test evaluate(metric, imgA, imgB) (5 + 10 + 15) * step(edges) / 25
71+
# Images don't have to be the same size
72+
imgB = [fill(0.1, 10) fill(0.3, 10) fill(0.5, 10) fill(0.7, 10) fill(0.9, 10)]
73+
@test evaluate(metric, imgA, imgB) 5 * step(edges) / 25
74+
imgB = [fill(0.9, 10) fill(0.3, 10) fill(0.7, 10) fill(0.7, 10) fill(0.9, 10)]
75+
@test evaluate(metric, imgA, imgB) 15*(2*step(edges))/25
76+
end
5677
end

0 commit comments

Comments
 (0)