Skip to content

Commit c3fe280

Browse files
authored
Initial functionality (#1)
Introduces `quantize(img, alg)` and the following algorithms: * `UniformQuantization` * `KMeansQuantization` via Clustering.jl
1 parent f0be8f8 commit c3fe280

15 files changed

+245
-37
lines changed

.github/workflows/CI.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
matrix:
2020
version:
2121
- '1.6'
22+
- '1'
2223
- 'nightly'
2324
os:
2425
- ubuntu-latest

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
*.jl.*.cov
22
*.jl.cov
33
*.jl.mem
4-
/Manifest.toml
4+
Manifest.toml
55
/docs/build/

Project.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,15 @@ uuid = "652893fb-f6a0-4a00-a44a-7fb8fac69e01"
33
authors = ["Adrian Hill <[email protected]>"]
44
version = "0.1.0"
55

6+
[deps]
7+
Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5"
8+
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
9+
ImageBase = "c817782e-172a-44cc-b673-b171935fbb9e"
10+
LazyModules = "8cdb02fc-e678-4876-92c5-9defec4f444e"
11+
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
12+
613
[compat]
14+
Clustering = "0.14.3"
15+
Colors = "0.12"
16+
LazyModules = "0.3"
717
julia = "1.6"

src/ColorQuantization.jl

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
module ColorQuantization
22

3-
# Write your package code here.
3+
using Colors
4+
using ImageBase: FixedPoint, floattype, FixedPointNumbers.rawtype
5+
using ImageBase: channelview, colorview, restrict
6+
using Random: AbstractRNG, GLOBAL_RNG
7+
using LazyModules: @lazy
8+
#! format: off
9+
@lazy import Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5"
10+
#! format: on
11+
12+
abstract type AbstractColorQuantizer end
13+
14+
include("api.jl")
15+
include("utils.jl")
16+
include("uniform.jl")
17+
include("clustering.jl") # lazily loaded
18+
19+
export AbstractColorQuantizer, quantize
20+
export UniformQuantization, KMeansQuantization
421

522
end

src/api.jl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
quantize([T,] cs, alg)
3+
4+
Apply color quantization algorithm `alg` to an iterable collection of Colorants,
5+
e.g. an image or any `AbstractArray`.
6+
The return type `T` can be specified and defaults to the element type of `cs`.
7+
"""
8+
function quantize(cs::AbstractArray{T}, alg::AbstractColorQuantizer) where {T<:Colorant}
9+
return quantize(T, cs, alg)
10+
end
11+
12+
function quantize(
13+
::Type{T}, cs::AbstractArray{<:Colorant}, alg::AbstractColorQuantizer
14+
) where {T}
15+
return convert.(T, alg(cs)[:])
16+
end

src/clustering.jl

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# The following code is lazily loaded from Clustering.jl using LazyModules.jl
2+
# Code adapted from @cormullion's ColorSchemeTools (https://github.com/JuliaGraphics/ColorSchemeTools.jl)
3+
4+
# The following type definition is taken from from Clustering.jl for the kwarg `init`:
5+
const ClusteringInitType = Union{
6+
Symbol,Clustering.SeedingAlgorithm,AbstractVector{<:Integer}
7+
}
8+
9+
const KMEANS_DEFAULT_COLORSPACE = RGB{Float32}
10+
11+
"""
12+
KMeansQuantization([T=RGB,] ncolors)
13+
14+
Quantize colors by applying the K-means method, where `ncolors` corresponds to
15+
the amount of clusters and output colors.
16+
17+
The colorspace `T` in which K-means are computed defaults to `RGB`.
18+
19+
## Optional arguments
20+
The following keyword arguments from Clustering.jl can be specified:
21+
- `init`: specifies how cluster seeds are initialized
22+
- `maxiter`: maximum number of iterations
23+
- `tol`: minimal allowed change of the objective during convergence.
24+
The algorithm is considered to be converged when the change of objective value between
25+
consecutive iterations drops below `tol`.
26+
27+
The default values are carried over from are imported from Clustering.jl.
28+
For more details, refer to the [documentation](https://juliastats.org/Clustering.jl/stable/)
29+
of Clustering.jl.
30+
"""
31+
struct KMeansQuantization{T<:Colorant,I<:ClusteringInitType,R<:AbstractRNG} <:
32+
AbstractColorQuantizer
33+
ncolors::Int
34+
maxiter::Int
35+
tol::Float64
36+
init::I
37+
rng::R
38+
39+
function KMeansQuantization(
40+
T::Type{<:Colorant},
41+
ncolors::Integer;
42+
init=Clustering._kmeans_default_init,
43+
maxiter=Clustering._kmeans_default_maxiter,
44+
tol=Clustering._kmeans_default_tol,
45+
rng=GLOBAL_RNG,
46+
)
47+
ncolors 2 ||
48+
throw(ArgumentError("K-means clustering requires ncolors ≥ 2, got $(ncolors)."))
49+
return new{T,typeof(init),typeof(rng)}(ncolors, maxiter, tol, init, rng)
50+
end
51+
end
52+
function KMeansQuantization(ncolors::Integer; kwargs...)
53+
return KMeansQuantization(KMEANS_DEFAULT_COLORSPACE, ncolors; kwargs...)
54+
end
55+
56+
function (alg::KMeansQuantization{T})(cs::AbstractArray{<:Colorant}) where {T}
57+
# Clustering on the downsampled image already generates good enough colormap estimation.
58+
# This significantly reduces the algorithmic complexity.
59+
cs = _restrict_to(cs, alg.ncolors * 100)
60+
return _kmeans(alg, convert.(T, cs))
61+
end
62+
63+
function _kmeans(alg::KMeansQuantization, cs::AbstractArray{<:Colorant{T,N}}) where {T,N}
64+
data = reshape(channelview(cs), N, :)
65+
R = Clustering.kmeans(
66+
data, alg.ncolors; maxiter=alg.maxiter, tol=alg.tol, init=alg.init, rng=alg.rng
67+
)
68+
return colorview(eltype(cs), R.centers)
69+
end

src/uniform.jl

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
UniformQuantization(n::Int)
3+
4+
Quantize colors in RGB color space by dividing each dimension of the ``[0, 1]³``
5+
RGB color cube into `n` equidistant steps for a total of `n³` cubes of equal size.
6+
Each color in `cs` is then quantized to the center of the cube it is in.
7+
Only unique colors are returned. The amount of output colors is therefore bounded by `n³`.
8+
"""
9+
struct UniformQuantization{N} <: AbstractColorQuantizer
10+
function UniformQuantization(n::Integer)
11+
return n < 1 ? throw(ArgumentError("n has to be ≥ 1, got $(n).")) : new{n}()
12+
end
13+
end
14+
15+
# UniformQuantization is applied in RGB colorspace:
16+
const UQ_RGB = RGB{Float32}
17+
@inline (alg::UniformQuantization)(cs::AbstractArray) = alg(convert.(UQ_RGB, cs))
18+
@inline (alg::UniformQuantization)(c::Colorant) = alg(convert(UQ_RGB, c))
19+
20+
@inline (alg::UniformQuantization)(cs::AbstractArray{<:RGB}) = unique(alg.(cs))
21+
@inline (alg::UniformQuantization)(c::RGB) = mapc(alg, c)
22+
23+
@inline (alg::UniformQuantization)(x::Real) = _uq_round(alg, x)
24+
@inline (alg::UniformQuantization)(x::FixedPoint) = _uq_lookup(alg, x)
25+
26+
# For general real numbers, use round
27+
@inline @generated function _uq_round(::UniformQuantization{N}, x::T) where {N,T<:Real}
28+
return quote
29+
x < $(T(1 / N)) && return $(T(1 / (2 * N)))
30+
x $(T((N - 1) / N)) && return $(T((2 * N - 1) / (2 * N)))
31+
return (round(x * $N - $(T(0.5))) + $(T(0.5))) / $N
32+
end
33+
end
34+
35+
# For fixed-point numbers, use the internal integer to index onto a lookup table
36+
@inline function _uq_lookup(alg::UniformQuantization{N}, x::T) where {N,T<:FixedPoint}
37+
@inbounds _uq_lookup_table(alg, T)[x.i + 1]
38+
end
39+
# the lookup table is generated at compile time
40+
@generated function _uq_lookup_table(
41+
::UniformQuantization{N}, ::Type{T}
42+
) where {N,T<:FixedPoint}
43+
RT = rawtype(T)
44+
tmax = typemax(RT)
45+
table = Vector{T}(undef, tmax + 1)
46+
for raw_x in zero(RT):tmax
47+
x = reinterpret(T, raw_x)
48+
table[raw_x + 1] = _uq_round(UniformQuantization(N), x)
49+
end
50+
return table
51+
end

src/utils.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Reduce the size of img until there are at most n elements left
2+
function _restrict_to(img, n)
3+
length(img) <= n && return img
4+
out = restrict(img)
5+
while length(out) > n
6+
out = restrict(out)
7+
end
8+
return out
9+
end

test/Manifest.toml

Lines changed: 0 additions & 34 deletions
This file was deleted.

test/Project.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,13 @@
11
[deps]
2+
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
3+
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
4+
ReferenceTests = "324d217c-45ce-50fc-942e-d289b448e8cf"
5+
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
26
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
7+
TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990"
8+
9+
[compat]
10+
Colors = "0.12"
11+
ReferenceTests = "0.10"
12+
StableRNGs = "1"
13+
TestImages = "1"

0 commit comments

Comments
 (0)