Skip to content

Commit

Permalink
Optimize distinguishable_colors (#506)
Browse files Browse the repository at this point in the history
This changes the type used for internal operations from `Lab{Float64}` to `Lab{Float32}`.
This also removes the unnecessary sRGB gamma operations and `deleteat!`.
  • Loading branch information
kimikage authored Aug 2, 2021
1 parent f71d7a1 commit f7fddbd
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 41 deletions.
72 changes: 43 additions & 29 deletions src/colormaps.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ include("maps_data.jl")
# color scale generation
# ----------------------

@noinline function _generate_lab(l::Float32, c::Float32, h::Float32)
lab = convert(Lab{Float32}, LCHab{Float32}(l, c, h))
rgb = xyz_to_linear_rgb(convert(XYZ{Float32}, lab))
convert(Lab{Float32}, linear_rgb_to_xyz(correct_gamut(rgb)))
end

"""
colors = distinguishable_colors(n, seed=RGB{N0f8}[];
dropseed=false,
Expand All @@ -12,9 +18,9 @@ include("maps_data.jl")
cchoices=range(0, stop=100, length=15),
hchoices=range(0, stop=342, length=20))
Generate n maximally distinguishable colors.
Generate `n` maximally distinguishable colors.
This uses a greedy brute-force approach to choose n colors that are maximally
This uses a greedy brute-force approach to choose `n` colors that are maximally
distinguishable. Given seed color(s), and a set of possible hue, chroma, and
lightness values (in LCHab space), it repeatedly chooses the next color as the
one that maximizes the minimum pairwise distance to any of the colors already
Expand All @@ -39,62 +45,70 @@ in the palette.
Returns a `Vector` of colors of length `n`, of the type specified in `seed`.
"""
function distinguishable_colors(n::Integer,
seed::AbstractVector{T};
dropseed = false,
@nospecialize(seed::AbstractVector{<:Color});
dropseed::Bool = false,
transform::Function = identity,
lchoices::AbstractVector = range(0, stop=100, length=15),
cchoices::AbstractVector = range(0, stop=100, length=15),
hchoices::AbstractVector = range(0, stop=342, length=20)) where T<:Color

lchoices::AbstractVector{<:Real} = range(0.0f0, stop=100.0f0, length=15),
cchoices::AbstractVector{<:Real} = range(0.0f0, stop=100.0f0, length=15),
hchoices::AbstractVector{<:Real} = range(0.0f0, stop=342.0f0, length=20))
if n <= length(seed) && !dropseed
return seed[1:n]
end

# Candidate colors
N = length(lchoices)*length(cchoices)*length(hchoices)
candidate = Vector{Lab{Float64}}(undef, N)
N = length(lchoices) * length(cchoices) * length(hchoices)
candidate = Vector{Lab{Float32}}(undef, N)
j = 0
for h in hchoices, c in cchoices, l in lchoices
rgb = convert(RGB, LCHab(l, c, h))
candidate[j+=1] = convert(LCHab, rgb)
@inbounds candidate[j+=1] = _generate_lab(Float32(l), Float32(c), Float32(h))
end

_distinguishable_colors(Int(n), seed, dropseed, transform, candidate)
end

function _distinguishable_colors(n::Int,
@nospecialize(seed::AbstractVector{<:Color}),
dropseed::Bool,
@nospecialize(transform::Function),
candidate::Vector{Lab{Float32}})

N = length(candidate)
colors = Vector{eltype(seed)}(undef, n)

# Transformed colors
candidate_t = Vector{Lab{Float64}}(undef, N)
for i = 1:N
candidate_t[i] = transform(candidate[i])
if transform === identity
candidate_t = candidate
else
candidate_t = convert.(Lab{Float32}, transform.(candidate))::Vector{Lab{Float32}}
end

# Start with the seed colors
n += dropseed ? length(seed) : 0
colors = Vector{T}(undef, n)
copyto!(colors, seed)
# Start with the seed colors, unless `dropseed`
dropseed || copyto!(colors, seed)

# Minimum distances of the current color to each previously selected color.
ds = fill(Inf, N)
for i = 1:length(seed)
ts = convert(Lab{Float64}, transform(seed[i]))::Lab{Float64}
ds = fill(Inf32, N)
@inbounds for s in seed
ts = convert(Lab{Float32}, transform(s))::Lab{Float32}
for k = 1:N
ds[k] = min(ds[k], colordiff(ts, candidate_t[k]))
ds[k] = @fastmath min(ds[k], colordiff(ts, candidate_t[k]))
end
end

for i in length(seed)+1:n
n1 = dropseed ? 1 : length(seed) + 1
@inbounds for i in n1:n
j = argmax(ds)
colors[i] = candidate[j]
tc = candidate_t[j]
ds[j] = 0.0f0
for k = 1:N
d = colordiff(tc, candidate_t[k])
ds[k] = min(ds[k], d)
ds[k] == 0.0f0 && continue # already selected
ds[k] = @fastmath min(ds[k], colordiff(tc, candidate_t[k]))
end
end

dropseed && deleteat!(colors, 1:length(seed))

return colors
end


distinguishable_colors(n::Integer, seed::Color; kwargs...) = distinguishable_colors(n, [seed]; kwargs...)
distinguishable_colors(n::Integer; kwargs...) = distinguishable_colors(n, Vector{RGB{N0f8}}(); kwargs...)

Expand Down
6 changes: 5 additions & 1 deletion src/conversions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,15 @@ end
const M_RGB2XYZ = Mat3x3([0.4124564390896921 0.357576077643909 0.18043748326639894
0.21267285140562248 0.715152155287818 0.07217499330655958
0.019333895582329317 0.119192025881303 0.9503040785363677 ])

function linear_rgb_to_xyz(c::AbstractRGB)
@mul3x3 XYZ M_RGB2XYZ red(c) green(c) blue(c)
end
function cnvt(::Type{XYZ{T}}, c::AbstractRGB) where T
r = invert_srgb_compand(red(c))
g = invert_srgb_compand(green(c))
b = invert_srgb_compand(blue(c))
return @mul3x3 XYZ{T} M_RGB2XYZ r g b
@mul3x3 XYZ{T} M_RGB2XYZ r g b
end


Expand Down
31 changes: 20 additions & 11 deletions test/colormaps.jl
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
using Test, Colors

@testset "Colormaps" begin
col = distinguishable_colors(10)
@test isconcretetype(eltype(col))
local mindiff
mindiff = Inf
for i = 1:10
for j = i+1:10
mindiff = min(mindiff, colordiff(col[i], col[j]))
@testset "distinguishable_colors" begin
cols = distinguishable_colors(10)
@test isconcretetype(eltype(cols))

mindiff = Inf
for i = 1:10, j = i+1:10
mindiff = min(mindiff, colordiff(cols[i], cols[j]))
end
end
@test mindiff > 8
@test mindiff > 8

cols = distinguishable_colors(1)
@test colordiff(distinguishable_colors(1, cols; dropseed=true)[1], cols[1]) > 50
seed = distinguishable_colors(1)
@test colordiff(distinguishable_colors(1, seed)[1], seed[1]) == 0.0
@test colordiff(distinguishable_colors(1, seed; dropseed=true)[1], seed[1]) > 50

cols_i = distinguishable_colors(4, LCHab(60, 50, 40), transform=identity,
lchoices=[40, 60], cchoices=[50], hchoices=[40, 140])
cols_p = distinguishable_colors(4, LCHab(60, 50, 40), transform=protanopic,
lchoices=[40, 60], cchoices=[50], hchoices=[40, 140])
@test cols_i[2] LCHab(40, 50, 140) atol=1f-3 # green
@test cols_p[2] LCHab(40, 50, 40) atol=1f-3 # red
end

@test length(colormap("RdBu", 100)) == 100

Expand Down

0 comments on commit f7fddbd

Please sign in to comment.