Optimise colour approximations
This commit is contained in:
parent
a1a0974127
commit
58fda5eab8
|
@ -1,5 +1,7 @@
|
||||||
[deps]
|
[deps]
|
||||||
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
|
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
|
||||||
|
FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93"
|
||||||
GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a"
|
GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a"
|
||||||
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
|
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
|
||||||
|
Memoize = "c03570c3-d221-55d1-a50c-7939bbd78826"
|
||||||
Observables = "510215fc-4207-5dde-b226-833fc4488ee2"
|
Observables = "510215fc-4207-5dde-b226-833fc4488ee2"
|
||||||
|
|
|
@ -2,70 +2,18 @@ module ColourApproximations
|
||||||
|
|
||||||
using Colors
|
using Colors
|
||||||
using GLMakie
|
using GLMakie
|
||||||
using Makie.GeometryBasics: Rect, HyperRectangle
|
|
||||||
using Observables
|
using Observables
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
|
|
||||||
const colours3bit = # xterm colours
|
include("coloursets.jl")
|
||||||
Colorant[colorant"rgb(0, 0, 0)",
|
include("pairsurrounding.jl")
|
||||||
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)
|
|
||||||
|
|
||||||
const coloursets =
|
const coloursets =
|
||||||
[Symbol("3-bit") => colours3bit,
|
[Symbol("3-bit") => colours3bit,
|
||||||
Symbol("4-bit") => colours4bit,
|
Symbol("4-bit") => colours4bit,
|
||||||
Symbol("8-bit") => colours8bit]
|
Symbol("8-bit") => colours8bit,
|
||||||
|
Symbol("24-bit") => Colorant[]]
|
||||||
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
|
|
||||||
|
|
||||||
# Extra colour differences
|
# Extra colour differences
|
||||||
|
|
||||||
|
@ -78,11 +26,11 @@ colour_distance_metrics =
|
||||||
"CIEΔE94" => DE_94(),
|
"CIEΔE94" => DE_94(),
|
||||||
"BFD" => DE_BFD(),
|
"BFD" => DE_BFD(),
|
||||||
"CMC" => DE_CMC(),
|
"CMC" => DE_CMC(),
|
||||||
"JPC79" => DE_JPC79(),
|
# "JPC79" => DE_JPC79(),
|
||||||
"l2 LAB" => DE_AB(),
|
"l2 LAB" => DE_AB(),
|
||||||
"l2 DIN99" => DE_DIN99(),
|
"l2 DIN99" => DE_DIN99(),
|
||||||
"l2 DIN99d" => DE_DIN99d(),
|
# "l2 DIN99d" => DE_DIN99d(),
|
||||||
"l2 DIN99o" => DE_DIN99o(),
|
# "l2 DIN99o" => DE_DIN99o(),
|
||||||
"l2 RGB" => DE_RGB(),
|
"l2 RGB" => DE_RGB(),
|
||||||
"l2 HSL" => DE_HSL()]
|
"l2 HSL" => DE_HSL()]
|
||||||
|
|
||||||
|
@ -90,8 +38,11 @@ colour_distance_metrics =
|
||||||
|
|
||||||
saturation = Observable(1.0)
|
saturation = Observable(1.0)
|
||||||
|
|
||||||
x_granularity = Observable(256)
|
y_granularity = Observable(256)
|
||||||
y_granularity = Observable(128)
|
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)))
|
diffmetric = Observable{Colors.DifferenceMetric}(last(first(colour_distance_metrics)))
|
||||||
|
|
||||||
|
@ -99,44 +50,33 @@ diffmetric = Observable{Colors.DifferenceMetric}(last(first(colour_distance_metr
|
||||||
|
|
||||||
fig = Figure()
|
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])
|
ax = Axis(fig[2,1])
|
||||||
hidedecorations!(ax)
|
hidedecorations!(ax)
|
||||||
hidespines!(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()
|
controlrow2 = fig[3,1] = GridLayout()
|
||||||
|
|
||||||
saturation_slider = Slider(controlrow2[1,2], range = 0:0.01:1, startvalue=saturation[])
|
saturation_slider = Slider(controlrow2[1,2], range = 0:0.01:1, startvalue=saturation[])
|
||||||
Label(controlrow2[1,1], text="Saturation")
|
Label(controlrow2[1,1], text="Saturation")
|
||||||
colourset_slider = Slider(controlrow2[1,4], range=1:length(coloursets))
|
colourset_slider = Slider(controlrow2[1,4], range=1:length(coloursets))
|
||||||
Label(controlrow2[1,3], text=@lift(coloursets[$(colourset_slider.value)] |> first |> string))
|
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, 1, Auto(true, 0.0))
|
||||||
colsize!(controlrow2, 2, Auto(false, 0.8))
|
colsize!(controlrow2, 2, Auto(false, 0.8))
|
||||||
colsize!(controlrow2, 3, Auto(true, 0.0))
|
colsize!(controlrow2, 3, Auto(true, 0.0))
|
||||||
colsize!(controlrow2, 4, Fixed(100))
|
colsize!(controlrow2, 4, Makie.Fixed(100))
|
||||||
colsize!(controlrow2, 5, Auto(true, 0.0))
|
|
||||||
colsize!(controlrow2, 6, Fixed(60))
|
|
||||||
|
|
||||||
# Plotting
|
# Plotting
|
||||||
|
|
||||||
|
@ -146,37 +86,32 @@ map!(diffmetric, metric_slider.value) do v
|
||||||
colour_distance_metrics[v] |> last
|
colour_distance_metrics[v] |> last
|
||||||
end
|
end
|
||||||
|
|
||||||
roundfun = Observable{Function}(identity)
|
colour_grid_unprocessed = map(y_granularity, x_granularity, saturation) do x, y, s
|
||||||
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
|
|
||||||
[HSL(h, s, l)
|
[HSL(h, s, l)
|
||||||
for h in range(0, 360, length=x_granularity[])
|
for h in range(0, 360, length=x),
|
||||||
for l in range(0, 1, length=y_granularity[])]
|
l in range(0, 1, length=y)]
|
||||||
end
|
end
|
||||||
|
|
||||||
colour_list = Observable(Vector{Colorant}())
|
colour_grid = Observable(Matrix{Colorant}(undef, 0, 0))
|
||||||
map!(colour_list, colour_list_unprocessed, roundfun, round_colours.active) do clist, cround, croundp
|
map!(colour_grid, colour_grid_unprocessed, diffmetric, colourset_slider.value) #=
|
||||||
if croundp cround.(clist) else clist end
|
=# do cmat, metric, cset
|
||||||
|
if cset == length(coloursets)
|
||||||
|
cmat
|
||||||
|
else
|
||||||
|
growcolours(metric, last(coloursets[cset]), cmat)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# The main event
|
# The main event
|
||||||
|
|
||||||
p = poly!(ax, rect_list, color = colour_list)
|
image!(ax, colour_grid)
|
||||||
|
|
||||||
on(rect_list, priority=1) do
|
on(y_granularity, priority=-1) do _
|
||||||
delete!(ax, p)
|
reset_limits!(ax)
|
||||||
p = poly!(ax, rect_list, color = colour_list)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
fig
|
on(x_granularity, priority=-1) do _
|
||||||
|
reset_limits!(ax)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -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)
|
|
@ -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
|
Loading…
Reference in New Issue