Skip to content
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

Add color picker abstraction #106

Draft
wants to merge 19 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ ColorQuantization = "652893fb-f6a0-4a00-a44a-7fb8fac69e01"
ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4"
ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f"
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93"
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
IndirectArrays = "9b13fd28-a010-5f03-acff-a1bbcff69959"
UnicodeGraphics = "ebadf6b4-db70-5817-83da-4a19ad584e34"
Expand All @@ -17,6 +18,7 @@ ColorQuantization = "0.1"
ColorSchemes = "3"
ColorTypes = "0.11"
Colors = "0.12"
FixedPointNumbers = "0.8"
ImageCore = "0.10"
IndirectArrays = "1"
UnicodeGraphics = "0.2"
Expand Down
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ makedocs(;
],
linkcheck=true,
checkdocs=:exports,
warnonly=[:missing_docs],
)

deploydocs(; repo="github.com/JuliaImages/DitherPunk.jl")
39 changes: 29 additions & 10 deletions src/DitherPunk.jl
Original file line number Diff line number Diff line change
@@ -1,28 +1,42 @@
module DitherPunk

using Base: require_one_based_indexing
using ColorTypes: ColorTypes, AbstractGray, Color, Colorant, Gray, HSV, Lab, XYZ, gray
using Colors: DifferenceMetric, colordiff, DE_2000, invert_srgb_compand
using ImageCore: channelview, floattype, clamp01
using FixedPointNumbers: N0f8, floattype
using ColorTypes: ColorTypes, AbstractGray, Color, Colorant
using ColorTypes: RGB, HSV, Lab, XYZ, Gray, gray
using Colors:
DifferenceMetric,
EuclideanDifferenceMetric,
DE_2000,
DE_94,
DE_JPC79,
DE_CMC,
DE_BFD,
colordiff,
invert_srgb_compand
using ImageCore: channelview, clamp01
using IndirectArrays: IndirectArray
import Colors: _colordiff # extended in colordiff.jl

using ColorSchemes: ColorScheme
using ColorQuantization: quantize, AbstractColorQuantizer, KMeansQuantization
using UnicodeGraphics: uprint, ustring

abstract type AbstractDither end

const BinaryGray = AbstractGray{Bool}
const NumberLike = Union{Number,AbstractGray}
const BinaryLike = Union{Bool,BinaryGray}
const Pixel = Union{Number,Colorant}
const ColorLike = Union{Number,Colorant}
const GrayLike = Union{Number,AbstractGray}
const BinaryLike = Union{Bool,AbstractGray{Bool}}

const GenericBinaryImage{T<:BinaryLike} = Union{BitMatrix,AbstractArray{T,2}}
const GenericGrayImage{T<:NumberLike} = AbstractArray{T,2}
const GenericImage{T<:Pixel,N} = AbstractArray{T,N}
const ColorVector{T<:ColorLike} = AbstractArray{T,1}
const GenericImage{T<:ColorLike} = AbstractArray{T,2}
const GrayImage{T<:GrayLike} = AbstractArray{T,2}
const BinaryImage{T<:BinaryLike} = AbstractArray{T,2}

include("colorschemes.jl")
include("utils.jl")
include("colordiff.jl")
include("color_picker.jl")
include("api/binary.jl")
include("api/color.jl")
include("threshold.jl")
Expand All @@ -48,6 +62,11 @@ export SimpleErrorDiffusion, FloydSteinberg, JarvisJudice, Stucki, Burkes
export Sierra, TwoRowSierra, SierraLite, Atkinson, Fan93, ShiauFan, ShiauFan2
# Closest color
export ClosestColor
# Closest color lookup
export AbstractColorPicker
export RuntimeColorPicker
export LookupColorPicker
export FastEuclideanMetric
# Other utilities
export upscale
export braille
Expand Down
12 changes: 4 additions & 8 deletions src/api/binary.jl
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function dither(::Type{T}, img::GenericImage, alg::AbstractDither; kwargs...) wh
end

# ...and defaults to the type of the input image.
function dither(img::GenericImage{T,N}, alg::AbstractDither; kwargs...) where {T<:Pixel,N}
function dither(img::GenericImage{T}, alg::AbstractDither; kwargs...) where {T<:ColorLike}
return dither(T, img, alg; kwargs...)
end

Expand All @@ -53,20 +53,16 @@ end
# Dispatch to binary dithering on grayscale images
# when no color palette is provided
function _binarydither!(
out::GenericGrayImage,
img::GenericGrayImage,
alg::AbstractDither;
to_linear=false,
kwargs...,
out::GrayImage, img::GrayImage, alg::AbstractDither; to_linear=false, kwargs...
)
to_linear && (img = srgb2linear.(img))
return binarydither!(alg, out, img; kwargs...)
end

# Dispatch to per-channel dithering on color images when no color palette is provided
function _binarydither!(
out::GenericImage{T,2},
img::GenericImage{T,2},
out::GenericImage{T},
img::GenericImage{T},
alg::AbstractDither;
to_linear=false,
kwargs...,
Expand Down
83 changes: 53 additions & 30 deletions src/api/color.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
const DEFAULT_METRIC = FastEuclideanMetric()
DefaultColorPicker(colors) = RuntimeColorPicker(colors, DEFAULT_METRIC)

# Binary dithering and color dithering can be distinguished by the extra argument `arg`,
# which is either
# - a color scheme (array of colors)
Expand All @@ -6,22 +9,9 @@
#
# All functions in this file end up calling `colordither` and return IndirectArrays.

struct ColorNotImplementedError <: Exception
algname::String
ColorNotImplementedError(alg::AbstractDither) = new("$alg")
end
function Base.showerror(io::IO, e::ColorNotImplementedError)
return print(
io, e.algname, " algorithm currently doesn't support custom color palettes."
)
end
colordither(alg, img, cs, metric) = throw(ColorNotImplementedError(alg))

const DEFAULT_METRIC = DE_2000()

##############
#============#
# Public API #
##############
#============#

# If `out` is specified, it will be changed in place...
function dither!(out::GenericImage, img::GenericImage, alg::AbstractDither, arg; kwargs...)
Expand All @@ -40,47 +30,80 @@ end

# ...and defaults to the type of the input image.
function dither(
img::GenericImage{T,N}, alg::AbstractDither, arg; kwargs...
) where {T<:Pixel,N}
img::GenericImage{T}, alg::AbstractDither, arg; kwargs...
) where {T<:ColorLike}
return _colordither(T, img, alg, arg; kwargs...)
end

#############################
#===========================#
# Low-level algorithm calls #
#############################
#===========================#

# Dispatch to dithering with custom color palettes on any image type
# when color palette is provided
function _colordither(
::Type{T},
img::GenericImage,
alg::AbstractDither,
cs::AbstractVector{<:Pixel};
metric::DifferenceMetric=DEFAULT_METRIC,
colorscheme::AbstractVector{<:ColorLike};
colorpicker::AbstractColorPicker=DefaultColorPicker(colorscheme),
to_linear=false,
kwargs...,
) where {T}
to_linear && (@warn "Skipping transformation `to_linear` when dithering in color.")
length(cs) >= 2 ||
throw(DomainError(length(cs), "Color scheme for dither needs >= 2 colors."))
length(colorscheme) >= 2 || throw(
DomainError(length(colorscheme), "Color scheme for dither needs >= 2 colors.")
)

index = colordither(alg, img, cs, metric; kwargs...)
_cs::Vector{T} = T.(cs)
return IndirectArray(index, _cs)
# Allocate output: matrix of indices onto the colorscheme
out = similar(img, Int)
# Eagerly promote to the optimal color space to make loop run faster
CS = colorspace(colorpicker)
img = convert.(CS, img)
cs_alg = convert.(CS, colorscheme)
# Call method
index = colordither!(out, alg, img, cs_alg, colorpicker; kwargs...)
# Assemble IndirectArray with correct colorant-type
cs_out = convert.(T, colorscheme)
return IndirectArray(index, cs_out)
end

# TODO: deprecate
# A special case occurs when a grayscale output image is to be dithered in colors.
# Since this is not possible, instead the return image will be of type of the color scheme.
function _colordither(
::Type{T},
img::GenericImage,
alg::AbstractDither,
cs::AbstractVector{<:Color{<:Any,3}};
metric::DifferenceMetric=DEFAULT_METRIC,
colorscheme::AbstractVector{<:Color{<:Any,3}};
colorpicker::AbstractColorPicker=DefaultColorPicker(colorscheme),
to_linear=false,
kwargs...,
) where {T<:NumberLike}
) where {T<:GrayLike}
return _colordither(
eltype(cs), img, alg, cs; metric=metric, to_linear=to_linear, kwargs...
eltype(colorscheme),
img,
alg,
colorscheme;
colorpicker=colorpicker,
to_linear=to_linear,
kwargs...,
)
end

#================#
# Error handling #
#================#

struct ColorNotImplementedError <: Exception
algname::String
ColorNotImplementedError(alg::AbstractDither) = new("$alg")
end
function Base.showerror(io::IO, e::ColorNotImplementedError)
return print(
io, e.algname, " algorithm currently doesn't support custom color palettes."
)
end
function colordither!(out, alg, img, colorscheme, colorpicker; kwargs...)
throw(ColorNotImplementedError(alg))
end
4 changes: 2 additions & 2 deletions src/braille.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function braille(img::GenericImage, alg::AbstractDither; kwargs...)
return braille(img, alg; kwargs...)
end
function braille(
img::GenericGrayImage,
img::GrayImage,
alg::AbstractDither;
invert::Bool=false,
to_string::Bool=false,
Expand All @@ -38,7 +38,7 @@ end
# Enable direct printing of Binary images:
braille(img::AbstractMatrix{Bool}; kwargs...) = _braille(img; kwargs...)
braille(img::BitMatrix; kwargs...) = _braille(img; kwargs...)
function braille(img::AbstractMatrix{<:BinaryGray}; kwargs...)
function braille(img::AbstractMatrix{<:AbstractGray{Bool}}; kwargs...)
return _braille(channelview(img); kwargs...)
end

Expand Down
15 changes: 9 additions & 6 deletions src/closest_color.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ Technically this not a dithering algorithm as the quatization error is not "rand
"""
struct ClosestColor <: AbstractDither end

function binarydither!(::ClosestColor, out::GenericGrayImage, img::GenericGrayImage)
function binarydither!(::ClosestColor, out::GrayImage, img::GrayImage)
threshold = eltype(img)(0.5)
return out .= img .> threshold
end

function colordither(
::ClosestColor, img::GenericImage, cs::AbstractVector{<:Pixel}, metric::DifferenceMetric
)::Matrix{Int}
cs_lab = Lab.(cs)
return map(px -> _closest_color_idx(px, cs_lab, metric), img)
function colordither!(
out::Matrix{Int},
::ClosestColor,
img::GenericImage{C},
cs::AbstractVector{C},
colorpicker::AbstractColorPicker{C},
) where {C<:ColorLike}
return map!(colorpicker, out, img)
end
90 changes: 90 additions & 0 deletions src/color_picker.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""
AbstractColorPicker{C}

Abstract supertype of all color pickers.
The parametric type `C` indicates the color space in which the closest color is computed.
"""
abstract type AbstractColorPicker{C<:ColorLike} end
colorspace(::AbstractColorPicker{C}) where {C} = C

function (picker::AbstractColorPicker{C})(color::C) where {C<:ColorLike}
closest_color_index(picker, color)
end

"""
RuntimeColorPicker(colorscheme)
RuntimeColorPicker(colorscheme, metric)

Select closest color in `colorscheme` during runtime.
Used by default if `dither` is called without a color picker.
"""
struct RuntimeColorPicker{C<:ColorLike,M<:DifferenceMetric} <: AbstractColorPicker{C}
metric::M
colorscheme::Vector{C}

function RuntimeColorPicker(colorscheme, metric::M) where {M<:DifferenceMetric}
C = colorspace(metric)
colorscheme = convert.(C, colorscheme)
return new{C,M}(metric, colorscheme)
end
end

RuntimeColorPicker(cs; metric=DEFAULT_METRIC) = RuntimeColorPicker(cs, metric)

# Performance can be gained by converting colors to the colorspace the picker operates in:

function closest_color_index(p::RuntimeColorPicker{C}, c::C) where {C<:ColorLike}
return closest_color_index_runtime(c, p.colorscheme, p.metric)
end

if VERSION >= v"1.7"
function closest_color_index_runtime(px, cs, metr)
return argmin(colordiff(px, c; metric=metr) for c in cs)
end
else
function closest_color_index_runtime(px, cs, metr)
return argmin([colordiff(px, c; metric=metr) for c in cs])
end
end

#===================#
# LookupColorPicker #
#===================#

const LUT_COLORSPACE = RGB{N0f8}
const LUT_INDEXTYPE = UInt16

"""
LookupColorPicker(colorscheme)
LookupColorPicker(colorscheme, metric)

Compute a look-up table of closest colors on the `$LUT_COLORSPACE` color cube.
"""
struct LookupColorPicker <: AbstractColorPicker{LUT_COLORSPACE}
lut::Array{LUT_INDEXTYPE,3} # look-up table

function LookupColorPicker(lut::Array{LUT_INDEXTYPE,3})
size(lut) != (256, 256, 256) &&
error("Look-up table has to be of size `(256, 256, 256)`, got $(size(lut)).")
return new(lut)
end
end

# Construct LUT from colorscheme and color difference metric
function LookupColorPicker(
colorscheme::ColorVector; metric::DifferenceMetric=DEFAULT_METRIC
)
lut = Array{LUT_INDEXTYPE}(undef, 256, 256, 256)
@inbounds @simd for I in CartesianIndices(lut)
r, g, b = I.I
px = ints_to_rgb_n0f8(r - 1, g - 1, b - 1)
lut[I] = closest_color_index_runtime(px, colorscheme, metric)
end
return LookupColorPicker(lut)
end

# Use LUT to look up index of closest color
function closest_color_index(picker::LookupColorPicker, c::LUT_COLORSPACE)
ir, ig, ib = rgb_n0f8_to_ints(c) .+ 0x01
return @inbounds picker.lut[ir, ig, ib]
end
Loading
Loading