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 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 .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