-
Notifications
You must be signed in to change notification settings - Fork 2
Initial functionality #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 5 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
ae63141
Initial functionality of K-means clustering
adrhill 8c58d70
Add `UniformQuantization`
adrhill 9f38003
Always return vector of colors
adrhill 4a72d35
Add `rng` kwarg from Clustering `v0.14.3`
adrhill bf9d86a
Add reference tests
adrhill 5fa0fa6
Add Julia 1.8 to CI matrix
adrhill 58a6416
Faster `UniformQuantization`
adrhill 81066d4
Discretize to center of cube instead of grid
adrhill 07ea010
Fix comment in tests
adrhill 5c0a260
Abstract types out of functions
adrhill 196e507
Add separate implementation for FixedPoint numbers
adrhill 079e673
Formatting fixes
adrhill 5eeaac1
Update default colorspace to `RGB{Float32}`
adrhill 06851ac
Update references
adrhill d5ed701
Cleanup code
adrhill File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
*.jl.*.cov | ||
*.jl.cov | ||
*.jl.mem | ||
/Manifest.toml | ||
Manifest.toml | ||
/docs/build/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,5 +3,15 @@ uuid = "652893fb-f6a0-4a00-a44a-7fb8fac69e01" | |
authors = ["Adrian Hill <[email protected]>"] | ||
version = "0.1.0" | ||
|
||
[deps] | ||
Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5" | ||
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" | ||
ImageBase = "c817782e-172a-44cc-b673-b171935fbb9e" | ||
LazyModules = "8cdb02fc-e678-4876-92c5-9defec4f444e" | ||
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" | ||
|
||
[compat] | ||
Clustering = "0.14.3" | ||
Colors = "0.12" | ||
LazyModules = "0.3" | ||
julia = "1.6" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,21 @@ | ||
module ColorQuantization | ||
|
||
# Write your package code here. | ||
using Colors | ||
using ImageBase: channelview, colorview, floattype, restrict | ||
using Random: AbstractRNG, GLOBAL_RNG | ||
using LazyModules: @lazy | ||
#! format: off | ||
@lazy import Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5" | ||
#! format: on | ||
|
||
abstract type AbstractColorQuantizer end | ||
|
||
include("api.jl") | ||
include("utils.jl") | ||
include("uniform.jl") | ||
include("clustering.jl") # lazily loaded | ||
|
||
export AbstractColorQuantizer, quantize | ||
export UniformQuantization, KMeansQuantization | ||
|
||
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
""" | ||
quantize([T,] cs, alg) | ||
|
||
Apply color quantization algorithm `alg` to an iterable collection of Colorants, | ||
e.g. an image or any `AbstractArray`. | ||
The return type `T` can be specified and defaults to the element type of `cs`. | ||
""" | ||
function quantize(cs::AbstractArray{T}, alg::AbstractColorQuantizer) where {T<:Colorant} | ||
return quantize(T, cs, alg) | ||
end | ||
|
||
function quantize( | ||
::Type{T}, cs::AbstractArray{<:Colorant}, alg::AbstractColorQuantizer | ||
) where {T} | ||
return convert.(T, alg(cs)[:]) | ||
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# The following code is lazily loaded from Clustering.jl using LazyModules.jl | ||
# Code adapted from @cormullion's ColorSchemeTools (https://github.com/JuliaGraphics/ColorSchemeTools.jl) | ||
|
||
# The following type definition is taken from from Clustering.jl for the kwarg `init`: | ||
const ClusteringInitType = Union{ | ||
Symbol,Clustering.SeedingAlgorithm,AbstractVector{<:Integer} | ||
} | ||
|
||
const KMEANS_DEFAULT_COLORSPACE = RGB{Float32} | ||
|
||
""" | ||
KMeansQuantization([T=RGB,] ncolors) | ||
|
||
Quantize colors by applying the K-means method, where `ncolors` corresponds to | ||
the amount of clusters and output colors. | ||
|
||
The colorspace `T` in which K-means are computed defaults to `RGB`. | ||
|
||
## Optional arguments | ||
The following keyword arguments from Clustering.jl can be specified: | ||
- `init`: specifies how cluster seeds are initialized | ||
- `maxiter`: maximum number of iterations | ||
- `tol`: minimal allowed change of the objective during convergence. | ||
The algorithm is considered to be converged when the change of objective value between | ||
consecutive iterations drops below `tol`. | ||
|
||
The default values are carried over from are imported from Clustering.jl. | ||
For more details, refer to the [documentation](https://juliastats.org/Clustering.jl/stable/) | ||
of Clustering.jl. | ||
""" | ||
struct KMeansQuantization{T<:Colorant,I<:ClusteringInitType,R<:AbstractRNG} <: | ||
AbstractColorQuantizer | ||
ncolors::Int | ||
maxiter::Int | ||
tol::Float64 | ||
init::I | ||
rng::R | ||
|
||
function KMeansQuantization( | ||
T::Type{<:Colorant}, | ||
ncolors::Integer; | ||
init=Clustering._kmeans_default_init, | ||
maxiter=Clustering._kmeans_default_maxiter, | ||
tol=Clustering._kmeans_default_tol, | ||
rng=GLOBAL_RNG, | ||
) | ||
ncolors ≥ 2 || | ||
throw(ArgumentError("K-means clustering requires ncolors ≥ 2, got $(ncolors).")) | ||
return new{T,typeof(init),typeof(rng)}(ncolors, maxiter, tol, init, rng) | ||
end | ||
end | ||
function KMeansQuantization(ncolors::Integer; kwargs...) | ||
return KMeansQuantization(KMEANS_DEFAULT_COLORSPACE, ncolors; kwargs...) | ||
end | ||
|
||
function (alg::KMeansQuantization{T})(cs::AbstractArray{<:Colorant}) where {T} | ||
# Clustering on the downsampled image already generates good enough colormap estimation. | ||
# This significantly reduces the algorithmic complexity. | ||
cs = _restrict_to(cs, alg.ncolors * 100) | ||
return _kmeans(alg, convert.(T, cs)) | ||
end | ||
|
||
function _kmeans(alg::KMeansQuantization, cs::AbstractArray{<:Colorant{T,N}}) where {T,N} | ||
data = reshape(channelview(cs), N, :) | ||
R = Clustering.kmeans( | ||
data, alg.ncolors; maxiter=alg.maxiter, tol=alg.tol, init=alg.init, rng=alg.rng | ||
) | ||
return colorview(eltype(cs), R.centers) | ||
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
""" | ||
UniformQuantization(n::Int) | ||
|
||
Quantize colors in RGB color space by dividing each dimension of the ``[0, 1]³`` | ||
RGB color cube into `n` equidistant steps. This results in a grid with ``(n+1)³`` points. | ||
Each color in `cs` is then quantized to the closest point on the grid. Only unique colors | ||
are returned. The amount of output colors is therefore bounded by ``(n+1)³``. | ||
""" | ||
struct UniformQuantization <: AbstractColorQuantizer | ||
n::Int | ||
|
||
function UniformQuantization(n) | ||
return n < 1 ? throw(ArgumentError("n has to be ≥ 1, got $(n).")) : new(n) | ||
end | ||
end | ||
|
||
(alg::UniformQuantization)(cs::AbstractArray{<:Colorant}) = alg(convert.(RGB{Float32}, cs)) | ||
function (alg::UniformQuantization)(cs::AbstractArray{T}) where {T<:RGB{<:AbstractFloat}} | ||
return colorview(T, unique(round.(channelview(cs[:]) * alg.n); dims=2) / alg.n) | ||
end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Reduce the size of img until there are at most n elements left | ||
function _restrict_to(img, n) | ||
length(img) <= n && return img | ||
out = restrict(img) | ||
while length(out) > n | ||
out = restrict(out) | ||
end | ||
return out | ||
end |
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,13 @@ | ||
[deps] | ||
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" | ||
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" | ||
ReferenceTests = "324d217c-45ce-50fc-942e-d289b448e8cf" | ||
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" | ||
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" | ||
TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990" | ||
|
||
[compat] | ||
Colors = "0.12" | ||
ReferenceTests = "0.10" | ||
StableRNGs = "1" | ||
TestImages = "1" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
[0m[38;5;52m█[38;5;108m█[38;5;59m█[38;5;131m█[38;5;143m█[38;5;101m█[38;5;88m█[38;5;137m█[0m |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
[0m[38;5;101m█[38;5;143m█[38;5;52m█[38;5;130m█[38;5;108m█[38;5;137m█[38;5;107m█[38;5;58m█[0m |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
[0m[38;5;52m█[38;5;52m█[38;5;125m█[38;5;137m█[38;5;143m█[38;5;101m█[38;5;65m█[38;5;94m█[38;5;124m█[38;5;137m█[38;5;107m█[38;5;132m█[38;5;137m█[38;5;101m█[38;5;127m█[38;5;96m█[38;5;102m█[38;5;175m█[38;5;209m█[38;5;102m█[38;5;102m█[38;5;97m█[38;5;91m█[38;5;138m█[38;5;18m█[38;5;60m█[38;5;66m█[0m |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
[0m[38;5;52m█[38;5;52m█[38;5;125m█[38;5;137m█[38;5;143m█[38;5;101m█[38;5;65m█[38;5;94m█[38;5;124m█[38;5;137m█[38;5;107m█[38;5;132m█[38;5;137m█[38;5;101m█[38;5;127m█[38;5;96m█[38;5;102m█[38;5;175m█[38;5;209m█[38;5;102m█[38;5;102m█[38;5;97m█[38;5;91m█[38;5;138m█[38;5;18m█[38;5;60m█[38;5;66m█[0m |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,37 @@ | ||
using ColorQuantization | ||
using Test | ||
using TestImages, ReferenceTests | ||
using Colors | ||
using Random, StableRNGs | ||
|
||
rng = StableRNG(123) | ||
Random.seed!(rng, 34568) | ||
|
||
img = testimage("peppers") | ||
|
||
algs_deterministic = Dict( | ||
"UniformQuantization4" => UniformQuantization(4), | ||
"KMeansQuantization8" => KMeansQuantization(8; rng=rng), | ||
) | ||
|
||
@testset "ColorQuantization.jl" begin | ||
# Write your tests here. | ||
# Reference tests on deterministic methods | ||
@testset "Reference tests" begin | ||
for (name, alg) in algs_deterministic | ||
@testset "$name" begin | ||
cs = quantize(img, alg) | ||
@test eltype(cs) == eltype(img) | ||
@test_reference "references/$(name).txt" cs | ||
|
||
cs = quantize(HSV{Float16}, img, alg) | ||
@test eltype(cs) == HSV{Float16} | ||
@test_reference "references/$(name)_HSV.txt" cs | ||
end | ||
end | ||
end | ||
|
||
@testset "Error messages" begin | ||
@test_throws ArgumentError UniformQuantization(0) | ||
@test_throws ArgumentError KMeansQuantization(0) | ||
end | ||
end |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is okay. But I get something faster by making it a plain lookup table (The key idea is to pass
T,N
to the compiler so that we can do the computation in the compilation stage using generated function):and I get
In the meantime, the current version is:
This trick is only applicable for fixed point numbers. Do you think it's worth the change?
For float-point numbers, a "round" process is needed to fully utilize the lookup table.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I now have both in the code for maximum flexibility w.r.t. input data types. However the benchmarks are a bit more complicated:
And building the look-up table for
N0f32
freezes my Julia session.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We certainly cannot make it a generic implementation here -- too risky here whether we can delegate so much computation to compiler. We only need those really matters in practice.
For generic types, a runtime lookup table (using Dict{Type,Vector} ) can be used instead (but whether it's faster than the naive version is unclear to me)