using Colors using StatsBase using FixedPointNumbers: N0f8, Normed using Memoize StatsBase.pairwise(metric::Colors.DifferenceMetric, colours::Vector{<:Colorant}) = pairwise((x, y) -> colordiff(x, y; metric), colours) function colourdistances(metric::Colors.DifferenceMetric, colours::Vector{<:Colorant}) distmat = pairwise(metric, colours) Dict(c => sort([distmat[i, j] => colours[j] for j in setdiff(axes(distmat, 1), i)], by=first) for (i, c) in enumerate(colours)) end function deconstruct(colour::C) where {C <: Colorant} fnames = fieldnames(C) getfield.(colour, fnames) end deconstruct(colour::RGB{N0f8}) = Float64.(getfield.(colour, (:r, :g, :b))) function construct(C::Type{<:Colorant}, fields::Tuple) eval(Expr(:new, C, fields...)) end construct(::Type{RGB{N0f8}}, fields::NTuple{3, Float64}) = RGB{N0f8}(fields...) @memoize nearestcolour(metric::Colors.DifferenceMetric, options::Vector{<:Colorant}, colour::Colorant) = options[argmin(o -> colordiff(options[o], colour; metric), axes(options, 1))] """ Examine various mixings of colours `a` and `b` in an attempt to confirm whether there are no other colours from `options` that lie directly between `a` and `b` according to `metric`. This is done by bisecting the mix factor and checking if any other colours are reported as nearest within `tol` of the crossover point. """ function bisectadjacency(metric::Colors.DifferenceMetric, a::C, b::C, options::Vector{<:Colorant}; tol=1e-6) where {C <: Colorant} af, bf, = deconstruct.((a, b)) lastmixfactor, mixfactor, mixstep = 1.0, 0.5, 0.25 while abs(lastmixfactor - mixfactor) > tol abf = @. mixfactor * af + (1 - mixfactor) * bf ab = construct(C, abf) near_ab = nearestcolour(metric, options, ab) if near_ab == a lastmixfactor, mixfactor = mixfactor, mixfactor + mixstep elseif near_ab == b lastmixfactor, mixfactor = mixfactor, mixfactor - mixstep else return false end mixstep /= 2 end return true end function adjacentcolours(metric::Colors.DifferenceMetric, colours::Vector{C}) where {C <: Colorant} distlist = colourdistances(metric, colours) |> collect adjacencylist = Dict{C, Vector{Pair{Float64, C}}}() for (c, others) in distlist adjacencylist[c] = filter(oth -> let o = last(oth) if haskey(adjacencylist, o) c ∈ last.(adjacencylist[o]) else bisectadjacency(metric, c, o, colours) end end, others) end adjacencylist end @memoize function growcolours(metric::Colors.DifferenceMetric, colours::Vector{<:Colorant}, refmat::Matrix{<:Colorant}) cmat = Matrix{Union{Colorant, Missing}}(fill(missing, size(refmat))) cadj = adjacentcolours(metric, colours) for i in CartesianIndices(cmat) if ismissing(cmat[i]) # @info "@ $(i.I)" cnearest = nearestcolour(metric, colours, refmat[i]) cmat[i] = cnearest if length(cadj[cnearest]) > 0 safethreshold = minimum(first.(cadj[cnearest]))/2 growcolour!(cmat, metric, refmat, cnearest, safethreshold, i) end end end Matrix{Colorant}(cmat) end function growcolour!(cmat::Matrix{Union{Colorant, Missing}}, metric::Colors.DifferenceMetric, refmat::Matrix{<:Colorant}, cnearest::Colorant, safethreshold::Float64, initalpos::CartesianIndex{2}) seeds = [initalpos] while !isempty(seeds) pos = pop!(seeds) surrounding = Ref(pos) .+ CartesianIndex{2}.([(0, 1), (0, -1), (1, 0), (-1, 0)]) filter!(s -> all((1,1) .<= s.I .<= size(refmat)), surrounding) for spos in surrounding if ismissing(cmat[spos]) && colordiff(cnearest, refmat[spos]; metric) < safethreshold cmat[spos] = cnearest push!(seeds, spos) end end end end function colour_grid_hsl(xs, ys, saturation=1) [HSL(h, saturation, l) for h in range(0, 360, length=xs), l in range(0, 1, length=ys)] end # function growcolours!(cmat::Matrix, metric::Colors.DifferenceMetric, # adjc::Dict{Colorant, Vector{Pair{Float64, Colorant}}}, refmat::Matrix, # pos::CartesianIndex{2}, # lrwrap::Bool=false, udwrap::Bool=false) # end