Skip to content

Commit

Permalink
Use IndirectArrays (#47)
Browse files Browse the repository at this point in the history
* Use IndirectArrays for color dithering
* Rewrite of ordered dithering, removing `TiledIteration` dependency
* Performance enhancements
  • Loading branch information
adrhill authored Nov 25, 2021
1 parent a7d6ff1 commit f2f4d43
Show file tree
Hide file tree
Showing 16 changed files with 261 additions and 239 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ jobs:
using Pkg
Pkg.add([
PackageSpec(name="Images", version="0.23"),
PackageSpec(name="IndirectArrays", version="0.5"),
PackageSpec(name="ImageCore", version="0.8"),
])
shell: julia --project=. --startup=no --color=yes {0}
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
Expand Down
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ 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"
IndirectArrays = "0.5, 1.0"
OffsetArrays = "1"
Requires = "1"
TiledIteration = "0.3"
julia = "1"

[extras]
Expand Down
9 changes: 6 additions & 3 deletions src/DitherPunk.jl
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
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

abstract type AbstractDither end

include("compat.jl")
include("dither_api.jl")
include("colorspaces.jl")
include("utils.jl")
include("api/binary.jl")
include("api/color.jl")
include("threshold.jl")
include("ordered.jl")
include("ordered_imagemagick.jl")
Expand Down
91 changes: 91 additions & 0 deletions src/api/binary.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# All functions in this file end up calling `binarydither!` and don't return IndirectArrays.

"""
dither!([out,] img, alg::AbstractDither, args...; kwargs...)
Dither image `img` using algorithm `alg`.
# Output
If `out` is specified, it will be changed in place. Otherwise `img` will be changed in place.
"""
dither!

"""
dither([T::Type,] img, alg::AbstractDither, args...; kwargs...)
Dither image `img` using algorithm `alg`.
# Output
If no return type is specified, `dither` will default to the type of the input image.
"""
dither

##############
# Public API #
##############

# If `out` is specified, it will be changed in place...
function dither!(out::GenericImage, img::GenericImage, alg::AbstractDither; 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 _binarydither!(out, img, alg; kwargs...)
end

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

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

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

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

# 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...,
) where {T}
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},
alg::AbstractDither;
to_linear=false,
kwargs...,
) where {T<:Color{<:Real,3}}
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 binarydither! modifies the view of the channelview of `out`.
binarydither!(alg, view(cvout, c, :, :), view(cvimg, c, :, :))
end
return out
end
86 changes: 86 additions & 0 deletions src/api/color.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Binary dithering and color dithering can be distinguished by the extra argument `arg`,
# which is either
# - a color scheme (array of colors)
# - a ColorSchemes.jl symbol
# - the number of colors specified for clustering
#
# 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))

##############
# Public API #
##############

# If `out` is specified, it will be changed in place...
function dither!(out::GenericImage, img::GenericImage, alg::AbstractDither, arg; 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 out .= _colordither(eltype(out), img, alg, arg; kwargs...)
end

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

# The return type can be chosen...
function dither(::Type{T}, img::GenericImage, alg::AbstractDither, arg; kwargs...) where {T}
return _colordither(T, img, alg, arg; kwargs...)
end

# ...and defaults to the type of the input image.
function dither(
img::GenericImage{T,N}, alg::AbstractDither, arg; kwargs...
) where {T<:Pixel,N}
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=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."))

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 _colordither(
::Type{T},
img::GenericImage,
alg::AbstractDither,
cs::AbstractVector{<:Color{<:Any,3}};
metric::DifferenceMetric=DE_2000(),
to_linear=false,
) where {T<:NumberLike}
return _colordither(eltype(cs), img, alg, cs; metric=metric, to_linear=to_linear)
end
16 changes: 7 additions & 9 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
threshold = eltype(img)(0.5)
return out .= img .> threshold
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.(cs)
return map(px -> _closest_color_idx(px, cs, 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 _colordither(
::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 _colordither(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 _colordither(T, img, alg, cs::ColorSchemes.ColorScheme; kwargs...)
return _colordither(T, img, alg, cs.colors)
end

function _dither!(out, img, alg, csname::Symbol; kwargs...)
function _colordither(T, img, alg, csname::Symbol; kwargs...)
cs = ColorSchemes.colorschemes[csname]
return _dither!(out, img, alg, cs.colors; kwargs...)
return _colordither(T, img, alg, cs.colors; kwargs...)
end
Loading

0 comments on commit f2f4d43

Please sign in to comment.