Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
3 changes: 1 addition & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ version = "2.0.1"

[deps]
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
IndirectArrays = "9b13fd28-a010-5f03-acff-a1bbcff69959"
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
TiledIteration = "06e1c1a7-607b-532d-9fad-de7d9aa2abac"

[compat]
ImageCore = "0.8.1, 0.9"
OffsetArrays = "1"
Requires = "1"
TiledIteration = "0.3"
julia = "1"

[extras]
Expand Down
4 changes: 2 additions & 2 deletions src/DitherPunk.jl
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
module DitherPunk

using TiledIteration
using ImageCore
using ImageCore: NumberLike, Pixel, GenericImage, GenericGrayImage, MappedArrays
using ImageCore.Colors: DifferenceMetric
using Random
using IndirectArrays
using OffsetArrays
using Requires

include("compat.jl")
include("utils.jl")
include("dither_api.jl")
include("colorspaces.jl")
include("threshold.jl")
include("ordered.jl")
include("ordered_imagemagick.jl")
Expand Down
18 changes: 8 additions & 10 deletions src/closest_color.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,16 @@ Simplest form of image quantization by turning each pixel to the closest one
in the provided color palette `cs`.
Technically this not a dithering algorithm as the quatization error is not "randomized".
"""
struct ClosestColor <: AbstractCustomColorDither end
struct ClosestColor <: AbstractDither end

function binarydither!(::ClosestColor, out::GenericGrayImage, img::GenericGrayImage)
return out .= img .> 0.5
function binarydither(::ClosestColor, img::GenericGrayImage)
return map(px -> px > 0.5 ? INDEX_WHITE : INDEX_BLACK, img)
end

function colordither!(
::ClosestColor,
out::GenericImage,
img::GenericImage,
cs::AbstractVector{<:Pixel},
metric::DifferenceMetric,
function colordither(
::ClosestColor, img::GenericImage, cs::AbstractVector{<:Pixel}, metric::DifferenceMetric
)
return out .= eltype(out).(map((px) -> closest_color(px, cs; metric=metric), img))
cs = Lab{floattype(eltype(eltype(img)))}.(cs)
Comment thread
adrhill marked this conversation as resolved.
Outdated
# return matrix of indices of closest color
return map(px -> argmin(colordiff.(px, cs; metric=metric)), img)
end
9 changes: 4 additions & 5 deletions src/clustering.jl
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
# These functions are only conditionally loaded with Clustering.jl
# Code adapted from @cormullion's [ColorSchemeTools](https://github.com/JuliaGraphics/ColorSchemeTools.jl).

function _dither!(
out,
function _dither(
::Type{T},
img,
alg,
ncolors::Int;
maxiter=Clustering._kmeans_default_maxiter,
tol=Clustering._kmeans_default_tol,
kwargs...,
)
T = eltype(img)
) where {T}

# Cluster in Lab color space
data = reshape(channelview(Lab.(img)), 3, :)
Expand All @@ -22,7 +21,7 @@ function _dither!(
push!(cs, Lab(R.centers[i], R.centers[i + 1], R.centers[i + 2]))
end

return _dither!(out, img, alg, T.(cs); kwargs...)
return _dither(T, img, alg, cs; kwargs...)
end

"""
Expand Down
8 changes: 4 additions & 4 deletions src/colorschemes.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# These functions are only conditionally loaded with ColorSchemes.jl
function _dither!(out, img, alg, cs::ColorSchemes.ColorScheme; kwargs...)
return _dither!(out, img, alg, cs.colors)
function _dither(T, img, alg, cs::ColorSchemes.ColorScheme; kwargs...)
return _dither(T, img, alg, cs.colors)
end

function _dither!(out, img, alg, csname::Symbol; kwargs...)
function _dither(T, img, alg, csname::Symbol; kwargs...)
cs = ColorSchemes.colorschemes[csname]
return _dither!(out, img, alg, cs.colors; kwargs...)
return _dither(T, img, alg, cs.colors; kwargs...)
end
28 changes: 0 additions & 28 deletions src/colorspaces.jl

This file was deleted.

94 changes: 39 additions & 55 deletions src/dither_api.jl
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
abstract type AbstractDither end

# Algorithms for which the user can define a custom output color palette.
# If no palette is specified, algorithms of this type will default to a binary color palette
# and act similarly to algorithms of type AbstractBinaryDither.
abstract type AbstractCustomColorDither <: AbstractDither end

# Algorithms which strictly do binary dithering:
abstract type AbstractBinaryDither <: AbstractDither end

struct ColorNotImplementedError <: Exception
algname::String
ColorNotImplementedError(alg::AbstractDither) = new("$alg")
Expand All @@ -17,7 +9,7 @@ function Base.showerror(io::IO, e::ColorNotImplementedError)
io, e.algname, " algorithm currently doesn't support custom color palettes."
)
end
colordither!(alg, out, img, cs, metric) = throw(ColorNotImplementedError(alg))
colordither(alg, img, cs, metric) = throw(ColorNotImplementedError(alg))

##############
# Public API #
Expand All @@ -44,38 +36,22 @@ If no return type is specified, `dither` will default to the type of the input i
dither

# If `out` is specified, it will be changed in place...
function dither!(
out::GenericImage, img::GenericImage, alg::AbstractDither, args...; kwargs...
)
if size(out) != size(img)
throw(
ArgumentError(
"out and img should have the same shape, instead they are $(size(out)) and $(size(img))",
),
)
end
return _dither!(out, img, alg, args...; kwargs...)
end

# ...otherwise `img` will be changed in place.
function dither!(img::GenericImage, alg::AbstractDither, args...; kwargs...)
tmp = copy(img)
return _dither!(img, tmp, alg, args...; kwargs...)
return img .= _dither(eltype(img), img, alg, args...; kwargs...)
end

# The return type can be chosen...
# Otherwise the return type can be chosen...
function dither(
::Type{T}, img::GenericImage, alg::AbstractDither, args...; kwargs...
) where {T}
out = similar(Array{T}, axes(img))
return _dither!(out, img, alg, args...; kwargs...)
return _dither(T, img, alg, args...; kwargs...)
end

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

#############################
Expand All @@ -84,59 +60,67 @@ end

# Dispatch to binary dithering on grayscale images
# when no color palette is provided
function _dither!(
out::GenericGrayImage, img::GenericGrayImage, alg::AbstractDither; to_linear=false
)
function _dither(
::Type{T}, img::GenericGrayImage, alg::AbstractDither; to_linear=false, kwargs...
) where {T}
to_linear && (img = srgb2linear.(img))
return binarydither!(alg, out, img)
index = binarydither(alg, img; kwargs...)
return IndirectArray(index, bwcolors(T))
end

# Dispatch to per-channel dithering on color images
# when no color palette is provided
function _dither!(
out::GenericImage{<:Color{<:Real,3},2},
# Dispatch to per-channel dithering on color images when no color palette is provided
function _dither(
::Type{T},
img::GenericImage{<:Color{<:Real,3},2},
alg::AbstractDither;
to_linear=false,
)
kwargs...,
) where {T}
to_linear && (@warn "Skipping transformation `to_linear` when dithering color images.")

cvout = channelview(out)
cvimg = channelview(img)
for c in axes(cvout, 1)
# Note: the input `out` will be modified
# since the dithering algorithms modify the view of the channelview of `out`.
binarydither!(alg, view(cvout, c, :, :), view(cvimg, c, :, :))
cs = perchannelbinarycolors(T) # color scheme with binary respresentation

# We want to reconstruct indices 1..8 from binary colorscheme indices 1,2.
# Let's assume three channels r, g, b. Using `perchannelbinarycolors`, the color scheme
# can be reconstructed as:
# 2^2 * (r-1) + 2^1 * (g-1) + 2^0 * (b-1) + 1
# We can skip subtracting 1 from each channel by doing:
# 4*r + 2*g + b - 6
index = fill(Int(-6), size(img)...)
for c in 1:3
Comment thread
adrhill marked this conversation as resolved.
Outdated
channelindex = binarydither(alg, view(channelview(img), c, :, :), kwargs...)
Comment thread
adrhill marked this conversation as resolved.
Outdated
index += 2^(3 - c) * channelindex
end
return out
return IndirectArray(index, cs)
end

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

index = colordither(alg, img, cs, metric)
return IndirectArray(index, T.(cs))
end

# 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 _dither!(
out::GenericGrayImage,
img::GenericGrayImage,
function _dither(
::Type{T},
img::GenericImage,
alg::AbstractDither,
cs::AbstractVector{<:Color{<:Any,3}};
metric::DifferenceMetric=DE_2000(),
to_linear=false,
)
T = eltype(cs)
return _dither!(T.(out), T.(img), alg, cs; metric=metric, to_linear=to_linear)
) where {T<:NumberLike}
return _dither(eltype(cs), img, alg, cs; metric=metric, to_linear=to_linear)
end
49 changes: 26 additions & 23 deletions src/error_diffusion.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,39 +21,42 @@ julia> cs = ColorSchemes.PuOr_7.colors; # using ColorSchemes.jl for color palett
julia> dither!(img, alg, cs);
```
"""
struct ErrorDiffusion{T<:AbstractMatrix} <: AbstractCustomColorDither
struct ErrorDiffusion{T<:AbstractMatrix} <: AbstractDither
filter::T
clamp_error::Bool
end
ErrorDiffusion(filter; clamp_error=true) = ErrorDiffusion(filter, clamp_error)

function binarydither!(alg::ErrorDiffusion, out::GenericGrayImage, img::GenericGrayImage)
function binarydither(alg::ErrorDiffusion, img::GenericGrayImage)
# this function does not yet support OffsetArray
require_one_based_indexing(img)

# Change from normalized intensities to Float as error will get added!
FT = floattype(eltype(out))

# eagerly promote to the same type to make loop run faster
FT = floattype(eltype(img))
img = FT.(img)
filter = eltype(FT).(alg.filter)

h, w = size(img)
fill!(out, zero(eltype(out)))

drs = axes(alg.filter, 1)
dcs = axes(alg.filter, 2)
FT0, FT1 = FT(0), FT(1)

out = Matrix{Int}(undef, size(img)...)

@inbounds for r in axes(img, 1)
for c in axes(img, 2)
px = img[r, c]
alg.clamp_error && (px = clamp01(px))

px >= 0.5 ? (col = FT1) : (col = FT0) # round to closest color
out[r, c] = col # apply pixel to dither
err = px - col # diffuse "error" to neighborhood in filter
if px > 0.5
out[r, c] = INDEX_WHITE
col = FT1 # round to closest color
else
out[r, c] = INDEX_BLACK
col = FT0
end

err = px - col # diffuse "error" to neighborhood in filter
for dr in drs
for dc in dcs
if (r + dr) in axes(img, 1) && (c + dc) in axes(img, 2)
Expand All @@ -67,26 +70,26 @@ function binarydither!(alg::ErrorDiffusion, out::GenericGrayImage, img::GenericG
return out
end

function colordither!(
function colordither(
alg::ErrorDiffusion,
out::GenericImage,
img::GenericImage,
cs::AbstractVector{<:Pixel},
metric::DifferenceMetric,
)
# this function does not yet support OffsetArray
require_one_based_indexing(img)

# Change from normalized intensities to Float as error will get added!
FT = floattype(eltype(out))
out = Matrix{Int}(undef, size(img)...) # allocate matrix of color indices

# eagerly promote to the same type to make loop run faster
img = FT.(img)
cs = FT.(cs)
filter = eltype(FT).(alg.filter)
# Change from normalized intensities to Float as error will get added!
# Eagerly promote to the same type to make loop run faster.
FT = floattype(eltype(eltype(img))) # type of Float
CT = floattype(eltype(img)) # type of colorant

h, w = size(img)
fill!(out, zero(eltype(out)))
img = CT.(img)
cs = CT.(cs)
labcs = Lab{FT}.(cs) # otherwise each call to colordiff converts cs to Lab
filter = FT.(alg.filter)

drs = axes(alg.filter, 1)
dcs = axes(alg.filter, 2)
Expand All @@ -96,9 +99,9 @@ function colordither!(
px = img[r, c]
alg.clamp_error && (px = clamp01(px))

col = closest_color(px, cs; metric=metric) # round to closest color
out[r, c] = col # apply pixel to dither
err = px - col # diffuse "error" to neighborhood in filter
colorindex = argmin(colordiff.(px, labcs; metric=metric)) # find closest color
Comment thread
adrhill marked this conversation as resolved.
Outdated
out[r, c] = colorindex # apply pixel to dither, which is an IndirectArray
err = px - cs[colorindex] # diffuse "error" to neighborhood in filter

for dr in drs
for dc in dcs
Comment thread
adrhill marked this conversation as resolved.
Expand Down
Loading