From 58fda5eab8417fdf429521b35374375a62f6d010 Mon Sep 17 00:00:00 2001 From: TEC Date: Wed, 10 Aug 2022 19:25:44 +0800 Subject: [PATCH] Optimise colour approximations --- Project.toml | 2 + approximations.jl | 149 +++++++++++++-------------------------------- coloursets.jl | 33 ++++++++++ pairsurrounding.jl | 126 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 203 insertions(+), 107 deletions(-) create mode 100644 coloursets.jl create mode 100644 pairsurrounding.jl diff --git a/Project.toml b/Project.toml index 59eba06..6d312ef 100644 --- a/Project.toml +++ b/Project.toml @@ -1,5 +1,7 @@ [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Memoize = "c03570c3-d221-55d1-a50c-7939bbd78826" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" diff --git a/approximations.jl b/approximations.jl index 7b773c5..c7c7bb6 100644 --- a/approximations.jl +++ b/approximations.jl @@ -2,70 +2,18 @@ module ColourApproximations using Colors using GLMakie -using Makie.GeometryBasics: Rect, HyperRectangle using Observables # Utilities -const colours3bit = # xterm colours - Colorant[colorant"rgb(0, 0, 0)", - colorant"rgb(205, 0, 0)", - colorant"rgb(0, 205, 0)", - colorant"rgb(205, 205, 0)", - colorant"rgb(0, 0, 238)", - colorant"rgb(205, 0, 205)", - colorant"rgb(0, 205, 205)", - colorant"rgb(229, 229, 229)"] - -const colours4bit = # xterm again - vcat(colours3bit, - Colorant[colorant"rgb(127, 127, 127)", - colorant"rgb(255, 0, 0)", - colorant"rgb(0, 252, 0)", - colorant"rgb(255, 255, 0)", - colorant"rgb(0, 0, 252)", - colorant"rgb(255, 0, 255)", - colorant"rgb(0, 255, 255)", - colorant"rgb(255, 255, 255)"]) - -const colour6cube = - Colorant[RGB(r, g, b) - for r in range(0, 1, length=6) - for g in range(0, 1, length=6) - for b in range(0, 1, length=6)] - -const colour24greys = Colorant[RGB(w, w, w) for w in range(0, 1, length=24)] - -const colours8bit = vcat(colours4bit, colour6cube, colour24greys) +include("coloursets.jl") +include("pairsurrounding.jl") const coloursets = [Symbol("3-bit") => colours3bit, Symbol("4-bit") => colours4bit, - Symbol("8-bit") => colours8bit] - -nearestcolour(metric::Colors.DifferenceMetric, options::Vector{<:Colorant}, colour::Colorant) = - options[argmin(o -> colordiff(options[o], colour; metric), axes(options, 1))] - -const nearcolourcache = - Dict{Colors.DifferenceMetric, Dict{Symbol, Dict{Colorant, Colorant}}}() - -function nearestcolour(metric::Colors.DifferenceMetric, options::Symbol) - if !haskey(nearcolourcache, metric) - nearcolourcache[metric] = Dict{Symbol, Dict{Colorant, Colorant}}() - end - if !haskey(nearcolourcache[metric], options) - nearcolourcache[metric][options] = Dict{Colorant, Colorant}() - end - relevantcache = nearcolourcache[metric][options] - colourset = coloursets[findfirst(s -> first(s) == options, coloursets)] |> last - function (c::Colorant) - cached = get(relevantcache, c, nothing) - if isnothing(cached) - cached = relevantcache[c] = nearestcolour(metric, colourset, c) - end - cached - end -end + Symbol("8-bit") => colours8bit, + Symbol("24-bit") => Colorant[]] # Extra colour differences @@ -78,11 +26,11 @@ colour_distance_metrics = "CIEΔE94" => DE_94(), "BFD" => DE_BFD(), "CMC" => DE_CMC(), - "JPC79" => DE_JPC79(), + # "JPC79" => DE_JPC79(), "l2 LAB" => DE_AB(), "l2 DIN99" => DE_DIN99(), - "l2 DIN99d" => DE_DIN99d(), - "l2 DIN99o" => DE_DIN99o(), + # "l2 DIN99d" => DE_DIN99d(), + # "l2 DIN99o" => DE_DIN99o(), "l2 RGB" => DE_RGB(), "l2 HSL" => DE_HSL()] @@ -90,8 +38,11 @@ colour_distance_metrics = saturation = Observable(1.0) -x_granularity = Observable(256) -y_granularity = Observable(128) +y_granularity = Observable(256) +x_granularity = Observable(128) + +const x_grads = round.(Int, 2 .^(sort(vcat(6:10, (6:10) .+ log2(1.5))))) +const y_grads = round.(Int, 2 .^(sort(vcat(5:10, (5:9) .+ log2(1.5))))) diffmetric = Observable{Colors.DifferenceMetric}(last(first(colour_distance_metrics))) @@ -99,44 +50,33 @@ diffmetric = Observable{Colors.DifferenceMetric}(last(first(colour_distance_metr fig = Figure() -controlrow1 = fig[1,1] = GridLayout() - -xgrad_slider = Slider(controlrow1[1,2], range=32:32:1024, startvalue=x_granularity[]) -Label(controlrow1[1,1], text="X divisions") -ygrad_slider = Slider(controlrow1[1,4], range=16:16:512, startvalue=y_granularity[]) -Label(controlrow1[1,3], text="Y divisions") -metric_slider = Slider(controlrow1[1,6], range=1:length(colour_distance_metrics)) -Label(controlrow1[1,5], text=@lift(colour_distance_metrics[$(metric_slider.value)] |> first)) - -connect!(x_granularity, xgrad_slider.value) -connect!(y_granularity, ygrad_slider.value) - -# colsize!(controlrow1, 1, Auto(true, 0.0)) -# colsize!(controlrow1, 2, Fixed(120)) -# colsize!(controlrow1, 3, Auto(true, 0.0)) -# colsize!(controlrow1, 4, Fixed(120)) -# colsize!(controlrow1, 5, Auto(true, 0.0)) -# colsize!(controlrow1, 6, Fixed(160)) - ax = Axis(fig[2,1]) hidedecorations!(ax) hidespines!(ax) +controlrow1 = fig[1,1] = GridLayout() + +xgrad_slider = Slider(controlrow1[1,2], range=y_grads, startvalue=y_granularity[]) +Label(controlrow1[1,1], text="Y divisions") +ygrad_slider = Slider(controlrow1[1,4], range=x_grads, startvalue=x_granularity[]) +Label(controlrow1[1,3], text="X divisions") +metric_slider = Slider(controlrow1[1,6], range=1:length(colour_distance_metrics)) +Label(controlrow1[1,5], text=@lift(colour_distance_metrics[$(metric_slider.value)] |> first)) + +connect!(y_granularity, xgrad_slider.value) +connect!(x_granularity, ygrad_slider.value) + controlrow2 = fig[3,1] = GridLayout() saturation_slider = Slider(controlrow2[1,2], range = 0:0.01:1, startvalue=saturation[]) Label(controlrow2[1,1], text="Saturation") colourset_slider = Slider(controlrow2[1,4], range=1:length(coloursets)) Label(controlrow2[1,3], text=@lift(coloursets[$(colourset_slider.value)] |> first |> string)) -round_colours = Toggle(controlrow2[1,6]) -Label(controlrow2[1,5], text="Round") colsize!(controlrow2, 1, Auto(true, 0.0)) colsize!(controlrow2, 2, Auto(false, 0.8)) colsize!(controlrow2, 3, Auto(true, 0.0)) -colsize!(controlrow2, 4, Fixed(100)) -colsize!(controlrow2, 5, Auto(true, 0.0)) -colsize!(controlrow2, 6, Fixed(60)) +colsize!(controlrow2, 4, Makie.Fixed(100)) # Plotting @@ -146,37 +86,32 @@ map!(diffmetric, metric_slider.value) do v colour_distance_metrics[v] |> last end -roundfun = Observable{Function}(identity) -map!(roundfun, diffmetric, colourset_slider.value) do metric, cset - nearestcolour(metric, coloursets[cset] |> first) -end - -rect_list = Observable(Vector{HyperRectangle}()) - -rect_list = map(x_granularity, y_granularity) do x, y - [Rect(i, j, 1, 1) for i in 1:x for j in 1:y] -end - -colour_list_unprocessed = map(saturation) do s +colour_grid_unprocessed = map(y_granularity, x_granularity, saturation) do x, y, s [HSL(h, s, l) - for h in range(0, 360, length=x_granularity[]) - for l in range(0, 1, length=y_granularity[])] + for h in range(0, 360, length=x), + l in range(0, 1, length=y)] end -colour_list = Observable(Vector{Colorant}()) -map!(colour_list, colour_list_unprocessed, roundfun, round_colours.active) do clist, cround, croundp - if croundp cround.(clist) else clist end +colour_grid = Observable(Matrix{Colorant}(undef, 0, 0)) +map!(colour_grid, colour_grid_unprocessed, diffmetric, colourset_slider.value) #= +=# do cmat, metric, cset + if cset == length(coloursets) + cmat + else + growcolours(metric, last(coloursets[cset]), cmat) + end end # The main event -p = poly!(ax, rect_list, color = colour_list) +image!(ax, colour_grid) -on(rect_list, priority=1) do - delete!(ax, p) - p = poly!(ax, rect_list, color = colour_list) +on(y_granularity, priority=-1) do _ + reset_limits!(ax) end -fig +on(x_granularity, priority=-1) do _ + reset_limits!(ax) +end end diff --git a/coloursets.jl b/coloursets.jl new file mode 100644 index 0000000..e29d882 --- /dev/null +++ b/coloursets.jl @@ -0,0 +1,33 @@ +using Colors +using FixedPointNumbers + +const colours3bit = # xterm colours + [colorant"rgb(0, 0, 0)", + colorant"rgb(205, 0, 0)", + colorant"rgb(0, 205, 0)", + colorant"rgb(205, 205, 0)", + colorant"rgb(0, 0, 238)", + colorant"rgb(205, 0, 205)", + colorant"rgb(0, 205, 205)", + colorant"rgb(229, 229, 229)"] + +const colours4bit = # xterm again + vcat(colours3bit, + [colorant"rgb(127, 127, 127)", + colorant"rgb(255, 0, 0)", + colorant"rgb(0, 252, 0)", + colorant"rgb(255, 255, 0)", + colorant"rgb(0, 0, 252)", + colorant"rgb(255, 0, 255)", + colorant"rgb(0, 255, 255)", + colorant"rgb(255, 255, 255)"]) + +const colour6cube = + [RGB{N0f8}(r, g, b) + for r in range(0, 1, length=6) + for g in range(0, 1, length=6) + for b in range(0, 1, length=6)] + +const colour24greys = [RGB{N0f8}(w, w, w) for w in range(0, 1, length=24)] + +const colours8bit = vcat(colours4bit, colour6cube, colour24greys) diff --git a/pairsurrounding.jl b/pairsurrounding.jl new file mode 100644 index 0000000..f2aa425 --- /dev/null +++ b/pairsurrounding.jl @@ -0,0 +1,126 @@ +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, + pos::CartesianIndex{2}) + # @info "Growing $(pos.I)" + surrounding = Ref(pos) .+ CartesianIndex{2}.([(0, 1), (0, -1), (1, 0), (-1, 0)]) + filter!(s -> all((1,1) .<= s.I .<= size(refmat)), surrounding) + reseed = Vector{CartesianIndex{2}}() + for spos in surrounding + # @info " @ $(spos.I)" + if ismissing(cmat[spos]) && colordiff(cnearest, refmat[spos]; metric) < safethreshold + cmat[spos] = cnearest + push!(reseed, spos) + end + end + for spos in reseed + growcolour!(cmat, metric, refmat, cnearest, safethreshold, spos) + 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