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

Use IndirectArrays #47

Merged
merged 22 commits into from
Nov 25, 2021
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)
adrhill marked this conversation as resolved.
Show resolved Hide resolved
# 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
adrhill marked this conversation as resolved.
Show resolved Hide resolved
channelindex = binarydither(alg, view(channelview(img), c, :, :), kwargs...)
adrhill marked this conversation as resolved.
Show resolved Hide resolved
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
adrhill marked this conversation as resolved.
Show resolved Hide resolved
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
adrhill marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Loading