From e22baf996cc783e7091a032bed9bbc911c5e9b8b Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sat, 13 Nov 2021 13:18:53 +0100 Subject: [PATCH 01/22] Rename colorspaces to utils --- src/DitherPunk.jl | 2 +- src/colorspaces.jl | 28 ------------------------ src/utils.jl | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 29 deletions(-) delete mode 100644 src/colorspaces.jl create mode 100644 src/utils.jl diff --git a/src/DitherPunk.jl b/src/DitherPunk.jl index 122cfc5..0b46d4b 100644 --- a/src/DitherPunk.jl +++ b/src/DitherPunk.jl @@ -9,8 +9,8 @@ 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") diff --git a/src/colorspaces.jl b/src/colorspaces.jl deleted file mode 100644 index 3cc22fe..0000000 --- a/src/colorspaces.jl +++ /dev/null @@ -1,28 +0,0 @@ -""" - srgb2linear(u) - -Convert pixel `u` from sRGB to linear color space. -""" -@inline srgb2linear(u::Number) = Colors.invert_srgb_compand(u) -@inline srgb2linear(u::Gray) = typeof(u)(srgb2linear(gray(u))) -@inline srgb2linear(u::Bool) = u - -""" - linear2srgb(u) - -Convert pixel `u` from linear to sRGB color space. -""" -@inline linear2srgb(u::Number) = Colors.srgb_compand(u) -@inline linear2srgb(u::Gray) = typeof(u)(linear2srgb(gray(u))) -@inline linear2srgb(u::Bool) = u - -""" - closest_color(color, cs; metric=DE_2000()) - -Return color in ColorScheme `cs` that is closest to `color` -[according to `colordiff`](http://juliagraphics.github.io/Colors.jl/dev/colordifferences/#Color-Differences) -""" -function closest_color(color::Color, cs::AbstractVector{<:Color}; metric=DE_2000()) - imin = argmin(colordiff.(color, cs; metric=metric)) - return cs[imin] -end diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 0000000..554db32 --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,53 @@ +""" + srgb2linear(u) + +Convert pixel `u` from sRGB to linear color space. +""" +@inline srgb2linear(u::Number) = Colors.invert_srgb_compand(u) +@inline srgb2linear(u::Gray) = typeof(u)(srgb2linear(gray(u))) +@inline srgb2linear(u::Bool) = u + +""" + linear2srgb(u) + +Convert pixel `u` from linear to sRGB color space. +""" +@inline linear2srgb(u::Number) = Colors.srgb_compand(u) +@inline linear2srgb(u::Gray) = typeof(u)(linear2srgb(gray(u))) +@inline linear2srgb(u::Bool) = u + +""" + BinaryIndirectArray(T, img) + +Black and white IndirectArray of gray type `T` with offset indices. +An index of 0 corresponds to black and 1 to white to avoid confusion. +Initialized as a black image. +""" +function BinaryIndirectArray(::Type{T}, img) where {T<:NumberLike} + cs = OffsetVector([T(0), T(1)], 0:1) + return IndirectArray(ones(Int, size(img)...), cs) +end + +""" + perchannelbinarycolors(T) + +Construct color scheme that corresponds to channel-wise binary dithering. +Indexing corresponds to binary respresentation: + +# Example +```julia-repl +julia> perchannelditherpalette(RGB) +8-element Array{RGB{N0f8},1} with eltype RGB{N0f8}: + RGB{N0f8}(0.0,0.0,0.0) + RGB{N0f8}(0.0,0.0,1.0) + RGB{N0f8}(0.0,1.0,0.0) + RGB{N0f8}(0.0,1.0,1.0) + RGB{N0f8}(1.0,0.0,0.0) + RGB{N0f8}(1.0,0.0,1.0) + RGB{N0f8}(1.0,1.0,0.0) + RGB{N0f8}(1.0,1.0,1.0) +``` +""" +function perchannelbinarycolors(::Type{T}) where {T<:Color{<:Any,3}} + return [T(a, b, c) for a in 0:1 for b in 0:1 for c in 0:1] +end From f5bbab60da4718214139889fb7a0ffa39f003950 Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sat, 13 Nov 2021 13:21:46 +0100 Subject: [PATCH 02/22] Use IndirectArrays --- Project.toml | 1 + src/DitherPunk.jl | 1 + src/closest_color.jl | 17 ++++---- src/clustering.jl | 9 ++--- src/colorschemes.jl | 8 ++-- src/dither_api.jl | 89 ++++++++++++++++-------------------------- src/error_diffusion.jl | 40 +++++++++---------- src/ordered.jl | 8 ++-- src/threshold.jl | 14 +++---- 9 files changed, 80 insertions(+), 107 deletions(-) diff --git a/Project.toml b/Project.toml index 305c2c0..5df3c84 100644 --- a/Project.toml +++ b/Project.toml @@ -5,6 +5,7 @@ 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" diff --git a/src/DitherPunk.jl b/src/DitherPunk.jl index 0b46d4b..fb927e9 100644 --- a/src/DitherPunk.jl +++ b/src/DitherPunk.jl @@ -5,6 +5,7 @@ using ImageCore using ImageCore: NumberLike, Pixel, GenericImage, GenericGrayImage, MappedArrays using ImageCore.Colors: DifferenceMetric using Random +using IndirectArrays using OffsetArrays using Requires diff --git a/src/closest_color.jl b/src/closest_color.jl index f147a67..ea93b5a 100644 --- a/src/closest_color.jl +++ b/src/closest_color.jl @@ -3,18 +3,15 @@ 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 img .> 0.5 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)) + # return matrix of indices of closest color + return map((px) -> argmin(colordiff.(px, cs; metric=metric)), img) end diff --git a/src/clustering.jl b/src/clustering.jl index e24ba06..8f9c986 100644 --- a/src/clustering.jl +++ b/src/clustering.jl @@ -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, :) @@ -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 """ diff --git a/src/colorschemes.jl b/src/colorschemes.jl index 9961f95..fadfa2c 100644 --- a/src/colorschemes.jl +++ b/src/colorschemes.jl @@ -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 diff --git a/src/dither_api.jl b/src/dither_api.jl index d8c4efa..71a537c 100644 --- a/src/dither_api.jl +++ b/src/dither_api.jl @@ -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") @@ -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 # @@ -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 ############################# @@ -84,59 +60,62 @@ 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...) .+ 1 + return IndirectArray(index, [T(0), T(1)]) 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 + index = ones(Int, size(img)...) # allocate indices + + for c in 1:3 + channelindex = binarydither(alg, view(channelview(img), c, :, :), kwargs...) + index += 2^(3 - c) * channelindex # reconstruct "decimal" indices 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 diff --git a/src/error_diffusion.jl b/src/error_diffusion.jl index c13829d..46bc75d 100644 --- a/src/error_diffusion.jl +++ b/src/error_diffusion.jl @@ -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] = 1 # apply pixel to dither, which is a BinaryIndirectArray + col = FT1 # round to closest color + else + out[r, c] = 0 + 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) @@ -67,9 +70,8 @@ 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, @@ -77,17 +79,15 @@ function colordither!( # 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 + # 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(img)) img = FT.(img) cs = FT.(cs) filter = eltype(FT).(alg.filter) - h, w = size(img) - fill!(out, zero(eltype(out))) - drs = axes(alg.filter, 1) dcs = axes(alg.filter, 2) @@ -96,9 +96,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, cs; metric=metric)) # find closest color + 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 diff --git a/src/ordered.jl b/src/ordered.jl index bde1f2c..57d9461 100644 --- a/src/ordered.jl +++ b/src/ordered.jl @@ -8,13 +8,11 @@ When applying the algorithm to an image, the threshold matrix is repeatedly tile to match the size of the image. It is then applied as a per-pixel threshold map. Optionally, this final threshold map can be inverted by selecting `invert_map=true`. """ -struct OrderedDither{T<:AbstractMatrix} <: AbstractBinaryDither +struct OrderedDither{T<:AbstractMatrix} <: AbstractDither mat::T end -function binarydither!( - alg::OrderedDither, out::GenericGrayImage, img::GenericGrayImage; invert_map=false -) +function binarydither(alg::OrderedDither, img::GenericGrayImage; invert_map=false) # eagerly promote to the same eltype to make for-loop faster FT = floattype(eltype(img)) if invert_map @@ -23,6 +21,8 @@ function binarydither!( mat = FT.(alg.mat) end + out = Matrix{Int}(undef, size(img)...) + # TODO: add Threads.@threads to this for loop further improves the performances # but it has unidentified memory allocations @inbounds for R in TileIterator(axes(img), size(mat)) diff --git a/src/threshold.jl b/src/threshold.jl index 30e24d5..372ac0c 100644 --- a/src/threshold.jl +++ b/src/threshold.jl @@ -1,4 +1,4 @@ -abstract type AbstractThresholdDither <: AbstractBinaryDither end +abstract type AbstractThresholdDither <: AbstractDither end """ WhiteNoiseThreshold() @@ -7,10 +7,8 @@ Use white noise as a threshold map. """ struct WhiteNoiseThreshold <: AbstractThresholdDither end -function binarydither!(::WhiteNoiseThreshold, out::GenericGrayImage, img::GenericGrayImage) - tmap = rand(eltype(img), size(img)) - out .= img .> tmap - return out +function binarydither(::WhiteNoiseThreshold, img::GenericGrayImage) + return img .> rand(eltype(img), size(img)) end """ @@ -29,8 +27,6 @@ struct ConstantThreshold{T<:Real} <: AbstractThresholdDither end end -function binarydither!(alg::ConstantThreshold, out::GenericGrayImage, img::GenericGrayImage) - tmap = fill(alg.threshold, size(img)) # constant matrix of value threshold - out .= img .> tmap - return out +function binarydither(alg::ConstantThreshold, img::GenericGrayImage) + return img .> fill(eltype(img)(alg.threshold), size(img)) end From 757de6e8909a63095f2ec27352945649f86a27ff Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sat, 13 Nov 2021 13:22:05 +0100 Subject: [PATCH 03/22] Update tests --- .../color_from_gray_FloydSteinberg.txt | 34 +++++++++---------- test/test_color.jl | 7 ++-- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/test/references/color_from_gray_FloydSteinberg.txt b/test/references/color_from_gray_FloydSteinberg.txt index 7c1e785..deea3e7 100644 --- a/test/references/color_from_gray_FloydSteinberg.txt +++ b/test/references/color_from_gray_FloydSteinberg.txt @@ -1,17 +1,17 @@ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ \ No newline at end of file +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ \ No newline at end of file diff --git a/test/test_color.jl b/test/test_color.jl index 2f60812..958b5fd 100644 --- a/test/test_color.jl +++ b/test/test_color.jl @@ -1,5 +1,5 @@ using DitherPunk -using DitherPunk: closest_color, ColorNotImplementedError +using DitherPunk: ColorNotImplementedError using Images using ImageCore using ImageInTerminal @@ -16,9 +16,6 @@ blue = RGB{Float32}(0, 0, 1) cs = [white, yellow, green, orange, red, blue] -# Test helper function -@test closest_color(RGB{Float32}(1, 0.1, 0.1), cs) == red - # Load test image img = testimage("fabio_color_256") img_gray = testimage("fabio_gray_256") @@ -53,7 +50,7 @@ for (name, alg) in algs imshow(img2_gray) end -# Test for argument errors on AbstractBinaryDither algorithms +# Test for argument errors on algorithms that don't support custom color palletes for alg in [Bayer(), WhiteNoiseThreshold(), ConstantThreshold()] @test_throws ColorNotImplementedError dither(img, alg, cs) end From 2277ce431b2042eeacb7425df79f8716d77b7860 Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sun, 14 Nov 2021 13:07:06 +0100 Subject: [PATCH 04/22] Remove unused code --- src/utils.jl | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 554db32..efb7660 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -16,18 +16,6 @@ Convert pixel `u` from linear to sRGB color space. @inline linear2srgb(u::Gray) = typeof(u)(linear2srgb(gray(u))) @inline linear2srgb(u::Bool) = u -""" - BinaryIndirectArray(T, img) - -Black and white IndirectArray of gray type `T` with offset indices. -An index of 0 corresponds to black and 1 to white to avoid confusion. -Initialized as a black image. -""" -function BinaryIndirectArray(::Type{T}, img) where {T<:NumberLike} - cs = OffsetVector([T(0), T(1)], 0:1) - return IndirectArray(ones(Int, size(img)...), cs) -end - """ perchannelbinarycolors(T) From 24c4be179ed781faff6da541141f92a5d8d59805 Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sun, 14 Nov 2021 14:44:41 +0100 Subject: [PATCH 05/22] Try to speed up thresholding algorithms --- src/closest_color.jl | 4 ++-- src/dither_api.jl | 6 +++--- src/threshold.jl | 6 ++++-- src/utils.jl | 9 +++++++++ 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/closest_color.jl b/src/closest_color.jl index ea93b5a..33b1241 100644 --- a/src/closest_color.jl +++ b/src/closest_color.jl @@ -6,12 +6,12 @@ Technically this not a dithering algorithm as the quatization error is not "rand struct ClosestColor <: AbstractDither end function binarydither(::ClosestColor, img::GenericGrayImage) - return img .> 0.5 + return map(px -> px > 0.5 ? INDEX_WHITE : INDEX_BLACK, img) end function colordither( ::ClosestColor, img::GenericImage, cs::AbstractVector{<:Pixel}, metric::DifferenceMetric ) # return matrix of indices of closest color - return map((px) -> argmin(colordiff.(px, cs; metric=metric)), img) + return map(px -> argmin(colordiff.(px, cs; metric=metric)), img) end diff --git a/src/dither_api.jl b/src/dither_api.jl index 71a537c..183529f 100644 --- a/src/dither_api.jl +++ b/src/dither_api.jl @@ -64,8 +64,8 @@ function _dither( ::Type{T}, img::GenericGrayImage, alg::AbstractDither; to_linear=false, kwargs... ) where {T} to_linear && (img = srgb2linear.(img)) - index = binarydither(alg, img; kwargs...) .+ 1 - return IndirectArray(index, [T(0), T(1)]) + 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 @@ -83,7 +83,7 @@ function _dither( for c in 1:3 channelindex = binarydither(alg, view(channelview(img), c, :, :), kwargs...) - index += 2^(3 - c) * channelindex # reconstruct "decimal" indices + index += 2^(3 - c) * (channelindex .- 1) # reconstruct "decimal" indices end return IndirectArray(index, cs) diff --git a/src/threshold.jl b/src/threshold.jl index 372ac0c..7986e7f 100644 --- a/src/threshold.jl +++ b/src/threshold.jl @@ -8,7 +8,8 @@ Use white noise as a threshold map. struct WhiteNoiseThreshold <: AbstractThresholdDither end function binarydither(::WhiteNoiseThreshold, img::GenericGrayImage) - return img .> rand(eltype(img), size(img)) + T = eltype(img) + return map(px -> px > rand(T) ? INDEX_WHITE : INDEX_BLACK, img) end """ @@ -28,5 +29,6 @@ struct ConstantThreshold{T<:Real} <: AbstractThresholdDither end function binarydither(alg::ConstantThreshold, img::GenericGrayImage) - return img .> fill(eltype(img)(alg.threshold), size(img)) + threshold = eltype(img)(alg.threshold) + return map(px -> px > threshold ? INDEX_WHITE : INDEX_BLACK, img) end diff --git a/src/utils.jl b/src/utils.jl index efb7660..452011b 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -16,6 +16,15 @@ Convert pixel `u` from linear to sRGB color space. @inline linear2srgb(u::Gray) = typeof(u)(linear2srgb(gray(u))) @inline linear2srgb(u::Bool) = u +""" + bwcolors(T) + +Construct black & white color scheme of type `T`. +""" +bwcolors(::Type{T}) where {T<:NumberLike} = [T(0), T(1)] +const INDEX_BLACK = Int(1) +const INDEX_WHITE = Int(2) + """ perchannelbinarycolors(T) From 2a1013d0094af3aa1a6348f3b25fd54c92e0cebb Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sun, 14 Nov 2021 14:45:30 +0100 Subject: [PATCH 06/22] Try to speed up error diffusion --- src/error_diffusion.jl | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/error_diffusion.jl b/src/error_diffusion.jl index 46bc75d..dc7b027 100644 --- a/src/error_diffusion.jl +++ b/src/error_diffusion.jl @@ -49,10 +49,10 @@ function binarydither(alg::ErrorDiffusion, img::GenericGrayImage) alg.clamp_error && (px = clamp01(px)) if px > 0.5 - out[r, c] = 1 # apply pixel to dither, which is a BinaryIndirectArray + out[r, c] = INDEX_WHITE col = FT1 # round to closest color else - out[r, c] = 0 + out[r, c] = INDEX_BLACK col = FT0 end @@ -82,11 +82,14 @@ function colordither( out = Matrix{Int}(undef, size(img)...) # allocate matrix of color indices # 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(img)) - img = FT.(img) - cs = FT.(cs) - filter = eltype(FT).(alg.filter) + # 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 + + 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) @@ -96,7 +99,7 @@ function colordither( px = img[r, c] alg.clamp_error && (px = clamp01(px)) - colorindex = argmin(colordiff.(px, cs; metric=metric)) # find closest color + colorindex = argmin(colordiff.(px, labcs; metric=metric)) # find closest color out[r, c] = colorindex # apply pixel to dither, which is an IndirectArray err = px - cs[colorindex] # diffuse "error" to neighborhood in filter From 9ec728e9138c1d72009ca48b7b42f5f2626835ee Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sun, 14 Nov 2021 14:46:18 +0100 Subject: [PATCH 07/22] Try to speed up ordered dithering, remove `TiledIteration` dependency --- Project.toml | 2 -- src/DitherPunk.jl | 1 - src/ordered.jl | 21 +++++++-------------- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/Project.toml b/Project.toml index 5df3c84..f196457 100644 --- a/Project.toml +++ b/Project.toml @@ -9,13 +9,11 @@ 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] diff --git a/src/DitherPunk.jl b/src/DitherPunk.jl index fb927e9..9197080 100644 --- a/src/DitherPunk.jl +++ b/src/DitherPunk.jl @@ -1,6 +1,5 @@ module DitherPunk -using TiledIteration using ImageCore using ImageCore: NumberLike, Pixel, GenericImage, GenericGrayImage, MappedArrays using ImageCore.Colors: DifferenceMetric diff --git a/src/ordered.jl b/src/ordered.jl index 57d9461..e7a4cd6 100644 --- a/src/ordered.jl +++ b/src/ordered.jl @@ -12,6 +12,10 @@ struct OrderedDither{T<:AbstractMatrix} <: AbstractDither mat::T end +@inline function modindex(i::CartesianIndex, size) + return mod1.(Tuple(i), size) +end + function binarydither(alg::OrderedDither, img::GenericGrayImage; invert_map=false) # eagerly promote to the same eltype to make for-loop faster FT = floattype(eltype(img)) @@ -20,22 +24,11 @@ function binarydither(alg::OrderedDither, img::GenericGrayImage; invert_map=fals else mat = FT.(alg.mat) end + matsize = size(mat) out = Matrix{Int}(undef, size(img)...) - - # TODO: add Threads.@threads to this for loop further improves the performances - # but it has unidentified memory allocations - @inbounds for R in TileIterator(axes(img), size(mat)) - mat_size = map(length, R) - if mat_size == size(mat) - # `mappedarray` creates a readonly wrapper with lazy evaluation with `srgb2linear` - # so that original `img` data is not modified. - out[R...] .= @views img[R...] .> mat - else # boundary condition - mat_inds = map(Base.OneTo, mat_size) - out_inds = map(getindex, R, mat_inds) - out[out_inds...] .= @views img[out_inds...] .> mat[mat_inds...] - end + @inbounds for i in CartesianIndices(img) + out[i] = img[i] > mat[modindex(i, matsize)...] ? INDEX_WHITE : INDEX_BLACK end return out end From 55b1c920d86dacb49917f360f855795ace428596 Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sun, 14 Nov 2021 16:01:07 +0100 Subject: [PATCH 08/22] Make things faster --- src/closest_color.jl | 1 + src/dither_api.jl | 11 ++++++++--- src/threshold.jl | 6 ++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/closest_color.jl b/src/closest_color.jl index 33b1241..bb49257 100644 --- a/src/closest_color.jl +++ b/src/closest_color.jl @@ -12,6 +12,7 @@ end function colordither( ::ClosestColor, img::GenericImage, cs::AbstractVector{<:Pixel}, metric::DifferenceMetric ) + cs = Lab{floattype(eltype(eltype(img)))}.(cs) # return matrix of indices of closest color return map(px -> argmin(colordiff.(px, cs; metric=metric)), img) end diff --git a/src/dither_api.jl b/src/dither_api.jl index 183529f..5e0a9f2 100644 --- a/src/dither_api.jl +++ b/src/dither_api.jl @@ -79,13 +79,18 @@ function _dither( to_linear && (@warn "Skipping transformation `to_linear` when dithering color images.") cs = perchannelbinarycolors(T) # color scheme with binary respresentation - index = ones(Int, size(img)...) # allocate indices + # 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 channelindex = binarydither(alg, view(channelview(img), c, :, :), kwargs...) - index += 2^(3 - c) * (channelindex .- 1) # reconstruct "decimal" indices + index += 2^(3 - c) * channelindex end - return IndirectArray(index, cs) end diff --git a/src/threshold.jl b/src/threshold.jl index 7986e7f..b556ac9 100644 --- a/src/threshold.jl +++ b/src/threshold.jl @@ -8,8 +8,7 @@ Use white noise as a threshold map. struct WhiteNoiseThreshold <: AbstractThresholdDither end function binarydither(::WhiteNoiseThreshold, img::GenericGrayImage) - T = eltype(img) - return map(px -> px > rand(T) ? INDEX_WHITE : INDEX_BLACK, img) + return (img .> rand(eltype(img), size(img))) .+ 1 # add one for index b=1, w=2 end """ @@ -29,6 +28,5 @@ struct ConstantThreshold{T<:Real} <: AbstractThresholdDither end function binarydither(alg::ConstantThreshold, img::GenericGrayImage) - threshold = eltype(img)(alg.threshold) - return map(px -> px > threshold ? INDEX_WHITE : INDEX_BLACK, img) + return (img .> alg.threshold) .+ 1 # add one for index b=1, w=2 end From a433a2bfda013b47ad30f811fdd57cf1cf01e8d9 Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sun, 14 Nov 2021 16:16:39 +0100 Subject: [PATCH 09/22] Speed up threshold algorithm --- src/threshold.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/threshold.jl b/src/threshold.jl index b556ac9..4a35f7d 100644 --- a/src/threshold.jl +++ b/src/threshold.jl @@ -28,5 +28,5 @@ struct ConstantThreshold{T<:Real} <: AbstractThresholdDither end function binarydither(alg::ConstantThreshold, img::GenericGrayImage) - return (img .> alg.threshold) .+ 1 # add one for index b=1, w=2 + return map(px -> px > alg.threshold ? INDEX_WHITE : INDEX_BLACK, img) end From b956b14fd7be3c6fc9ec7d7e7a6cb7126162394b Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sun, 14 Nov 2021 17:37:35 +0100 Subject: [PATCH 10/22] Attempt to speed up ordered dithering --- src/ordered.jl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ordered.jl b/src/ordered.jl index e7a4cd6..5afa663 100644 --- a/src/ordered.jl +++ b/src/ordered.jl @@ -12,10 +12,6 @@ struct OrderedDither{T<:AbstractMatrix} <: AbstractDither mat::T end -@inline function modindex(i::CartesianIndex, size) - return mod1.(Tuple(i), size) -end - function binarydither(alg::OrderedDither, img::GenericGrayImage; invert_map=false) # eagerly promote to the same eltype to make for-loop faster FT = floattype(eltype(img)) @@ -28,7 +24,7 @@ function binarydither(alg::OrderedDither, img::GenericGrayImage; invert_map=fals out = Matrix{Int}(undef, size(img)...) @inbounds for i in CartesianIndices(img) - out[i] = img[i] > mat[modindex(i, matsize)...] ? INDEX_WHITE : INDEX_BLACK + out[i] = img[i] > mat[mod1.(Tuple(i), matsize)...] ? INDEX_WHITE : INDEX_BLACK end return out end From d13f3ea4de948ea7b653836f5e2378c2fdf80d84 Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Mon, 15 Nov 2021 18:07:18 +0100 Subject: [PATCH 11/22] Add suggestions from review --- src/closest_color.jl | 5 ++--- src/dither_api.jl | 6 +++--- src/error_diffusion.jl | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/closest_color.jl b/src/closest_color.jl index bb49257..e9549b3 100644 --- a/src/closest_color.jl +++ b/src/closest_color.jl @@ -12,7 +12,6 @@ end function colordither( ::ClosestColor, img::GenericImage, cs::AbstractVector{<:Pixel}, metric::DifferenceMetric ) - cs = Lab{floattype(eltype(eltype(img)))}.(cs) - # return matrix of indices of closest color - return map(px -> argmin(colordiff.(px, cs; metric=metric)), img) + cs = ccolor(Lab, eltype(cs)).(cs) # convert to Lab + return map(px -> argmin(colordiff(px, c; metric=metric) for c in cs), img) end diff --git a/src/dither_api.jl b/src/dither_api.jl index 5e0a9f2..b9b3a1f 100644 --- a/src/dither_api.jl +++ b/src/dither_api.jl @@ -79,6 +79,7 @@ function _dither( to_linear && (@warn "Skipping transformation `to_linear` when dithering color images.") cs = perchannelbinarycolors(T) # color scheme with binary respresentation + cvimg = channelview(img) # 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 @@ -87,9 +88,8 @@ function _dither( # 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 - channelindex = binarydither(alg, view(channelview(img), c, :, :), kwargs...) - index += 2^(3 - c) * channelindex + for c in axes(cvimg, 1) + index += 2^(3 - c) * binarydither(alg, view(cvimg, c, :, :), kwargs...) end return IndirectArray(index, cs) end diff --git a/src/error_diffusion.jl b/src/error_diffusion.jl index dc7b027..f4928e5 100644 --- a/src/error_diffusion.jl +++ b/src/error_diffusion.jl @@ -99,7 +99,7 @@ function colordither( px = img[r, c] alg.clamp_error && (px = clamp01(px)) - colorindex = argmin(colordiff.(px, labcs; metric=metric)) # find closest color + colorindex = argmin(colordiff(px, col; metric=metric) for col in labcs) # find closest color out[r, c] = colorindex # apply pixel to dither, which is an IndirectArray err = px - cs[colorindex] # diffuse "error" to neighborhood in filter From c96227e2011ff3322aac3fdf2d38e11786fb48ae Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sun, 21 Nov 2021 14:31:19 +0100 Subject: [PATCH 12/22] Revert binary dithering to in-place updates --- src/DitherPunk.jl | 5 +- src/api/binary.jl | 91 +++++++++++++++++++++++++++++ src/api/color.jl | 86 ++++++++++++++++++++++++++++ src/closest_color.jl | 4 +- src/clustering.jl | 4 +- src/colorschemes.jl | 8 +-- src/dither_api.jl | 126 ----------------------------------------- src/error_diffusion.jl | 11 ++-- src/ordered.jl | 9 ++- src/threshold.jl | 9 +-- 10 files changed, 206 insertions(+), 147 deletions(-) create mode 100644 src/api/binary.jl create mode 100644 src/api/color.jl delete mode 100644 src/dither_api.jl diff --git a/src/DitherPunk.jl b/src/DitherPunk.jl index 9197080..e0828f4 100644 --- a/src/DitherPunk.jl +++ b/src/DitherPunk.jl @@ -8,9 +8,12 @@ using IndirectArrays using OffsetArrays using Requires +abstract type AbstractDither end + include("compat.jl") include("utils.jl") -include("dither_api.jl") +include("api/binary.jl") +include("api/color.jl") include("threshold.jl") include("ordered.jl") include("ordered_imagemagick.jl") diff --git a/src/api/binary.jl b/src/api/binary.jl new file mode 100644 index 0000000..d501ea7 --- /dev/null +++ b/src/api/binary.jl @@ -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 diff --git a/src/api/color.jl b/src/api/color.jl new file mode 100644 index 0000000..b1177a7 --- /dev/null +++ b/src/api/color.jl @@ -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 diff --git a/src/closest_color.jl b/src/closest_color.jl index e9549b3..eab8548 100644 --- a/src/closest_color.jl +++ b/src/closest_color.jl @@ -5,8 +5,8 @@ Technically this not a dithering algorithm as the quatization error is not "rand """ struct ClosestColor <: AbstractDither end -function binarydither(::ClosestColor, img::GenericGrayImage) - return map(px -> px > 0.5 ? INDEX_WHITE : INDEX_BLACK, img) +function binarydither!(::ClosestColor, out::GenericGrayImage, img::GenericGrayImage) + return out .= img .> 0.5 end function colordither( diff --git a/src/clustering.jl b/src/clustering.jl index 8f9c986..799227c 100644 --- a/src/clustering.jl +++ b/src/clustering.jl @@ -1,7 +1,7 @@ # These functions are only conditionally loaded with Clustering.jl # Code adapted from @cormullion's [ColorSchemeTools](https://github.com/JuliaGraphics/ColorSchemeTools.jl). -function _dither( +function _colordither( ::Type{T}, img, alg, @@ -21,7 +21,7 @@ function _dither( push!(cs, Lab(R.centers[i], R.centers[i + 1], R.centers[i + 2])) end - return _dither(T, img, alg, cs; kwargs...) + return _colordither(T, img, alg, cs; kwargs...) end """ diff --git a/src/colorschemes.jl b/src/colorschemes.jl index fadfa2c..bf921cb 100644 --- a/src/colorschemes.jl +++ b/src/colorschemes.jl @@ -1,9 +1,9 @@ # These functions are only conditionally loaded with ColorSchemes.jl -function _dither(T, img, alg, cs::ColorSchemes.ColorScheme; kwargs...) - return _dither(T, img, alg, cs.colors) +function _colordither(T, img, alg, cs::ColorSchemes.ColorScheme; kwargs...) + return _colordither(T, img, alg, cs.colors) end -function _dither(T, img, alg, csname::Symbol; kwargs...) +function _colordither(T, img, alg, csname::Symbol; kwargs...) cs = ColorSchemes.colorschemes[csname] - return _dither(T, img, alg, cs.colors; kwargs...) + return _colordither(T, img, alg, cs.colors; kwargs...) end diff --git a/src/dither_api.jl b/src/dither_api.jl deleted file mode 100644 index b9b3a1f..0000000 --- a/src/dither_api.jl +++ /dev/null @@ -1,126 +0,0 @@ -abstract type AbstractDither end - -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 # -############## - -""" - 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 - -# If `out` is specified, it will be changed in place... -function dither!(img::GenericImage, alg::AbstractDither, args...; kwargs...) - return img .= _dither(eltype(img), img, alg, args...; kwargs...) -end - -# Otherwise the return type can be chosen... -function dither( - ::Type{T}, img::GenericImage, alg::AbstractDither, args...; kwargs... -) where {T} - 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...) -end - -############################# -# Low-level algorithm calls # -############################# - -# Dispatch to binary dithering on grayscale images -# when no color palette is provided -function _dither( - ::Type{T}, img::GenericGrayImage, alg::AbstractDither; to_linear=false, kwargs... -) where {T} - to_linear && (img = srgb2linear.(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( - ::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.") - - cs = perchannelbinarycolors(T) # color scheme with binary respresentation - cvimg = channelview(img) - - # 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 axes(cvimg, 1) - index += 2^(3 - c) * binarydither(alg, view(cvimg, c, :, :), kwargs...) - end - return IndirectArray(index, cs) -end - -# Dispatch to dithering with custom color palettes on any image type -# when color palette is provided -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.")) - - 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( - ::Type{T}, - img::GenericImage, - alg::AbstractDither, - cs::AbstractVector{<:Color{<:Any,3}}; - metric::DifferenceMetric=DE_2000(), - to_linear=false, -) where {T<:NumberLike} - return _dither(eltype(cs), img, alg, cs; metric=metric, to_linear=to_linear) -end diff --git a/src/error_diffusion.jl b/src/error_diffusion.jl index f4928e5..4df043c 100644 --- a/src/error_diffusion.jl +++ b/src/error_diffusion.jl @@ -27,7 +27,7 @@ struct ErrorDiffusion{T<:AbstractMatrix} <: AbstractDither end ErrorDiffusion(filter; clamp_error=true) = ErrorDiffusion(filter, clamp_error) -function binarydither(alg::ErrorDiffusion, img::GenericGrayImage) +function binarydither!(alg::ErrorDiffusion, out::GenericGrayImage, img::GenericGrayImage) # this function does not yet support OffsetArray require_one_based_indexing(img) @@ -39,9 +39,10 @@ function binarydither(alg::ErrorDiffusion, img::GenericGrayImage) drs = axes(alg.filter, 1) dcs = axes(alg.filter, 2) - FT0, FT1 = FT(0), FT(1) - out = Matrix{Int}(undef, size(img)...) + T = eltype(out) + T0, T1 = T(0), T(1) + FT0, FT1 = FT(0), FT(1) @inbounds for r in axes(img, 1) for c in axes(img, 2) @@ -49,10 +50,10 @@ function binarydither(alg::ErrorDiffusion, img::GenericGrayImage) alg.clamp_error && (px = clamp01(px)) if px > 0.5 - out[r, c] = INDEX_WHITE + out[r, c] = T1 col = FT1 # round to closest color else - out[r, c] = INDEX_BLACK + out[r, c] = T0 col = FT0 end diff --git a/src/ordered.jl b/src/ordered.jl index 5afa663..1bec476 100644 --- a/src/ordered.jl +++ b/src/ordered.jl @@ -12,7 +12,9 @@ struct OrderedDither{T<:AbstractMatrix} <: AbstractDither mat::T end -function binarydither(alg::OrderedDither, img::GenericGrayImage; invert_map=false) +function binarydither!( + alg::OrderedDither, out::GenericGrayImage, img::GenericGrayImage; invert_map=false +) # eagerly promote to the same eltype to make for-loop faster FT = floattype(eltype(img)) if invert_map @@ -22,9 +24,10 @@ function binarydither(alg::OrderedDither, img::GenericGrayImage; invert_map=fals end matsize = size(mat) - out = Matrix{Int}(undef, size(img)...) + T = eltype(out) + black, white = T(0), T(1) @inbounds for i in CartesianIndices(img) - out[i] = img[i] > mat[mod1.(Tuple(i), matsize)...] ? INDEX_WHITE : INDEX_BLACK + out[i] = img[i] > mat[mod1.(Tuple(i), matsize)...] ? white : black end return out end diff --git a/src/threshold.jl b/src/threshold.jl index 4a35f7d..c4b0c36 100644 --- a/src/threshold.jl +++ b/src/threshold.jl @@ -7,8 +7,9 @@ Use white noise as a threshold map. """ struct WhiteNoiseThreshold <: AbstractThresholdDither end -function binarydither(::WhiteNoiseThreshold, img::GenericGrayImage) - return (img .> rand(eltype(img), size(img))) .+ 1 # add one for index b=1, w=2 +function binarydither!(::WhiteNoiseThreshold, out::GenericGrayImage, img::GenericGrayImage) + tmap = rand(eltype(img), size(img)) + return out .= img .> tmap end """ @@ -27,6 +28,6 @@ struct ConstantThreshold{T<:Real} <: AbstractThresholdDither end end -function binarydither(alg::ConstantThreshold, img::GenericGrayImage) - return map(px -> px > alg.threshold ? INDEX_WHITE : INDEX_BLACK, img) +function binarydither!(alg::ConstantThreshold, out::GenericGrayImage, img::GenericGrayImage) + return out .= img .> alg.threshold end From ee8b702c79acff6cb4105b646c7e866cb4a6bca3 Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sun, 21 Nov 2021 20:23:26 +0100 Subject: [PATCH 13/22] Fix compat issues with generator --- src/closest_color.jl | 4 ++-- src/error_diffusion.jl | 2 +- src/utils.jl | 12 ++++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/closest_color.jl b/src/closest_color.jl index eab8548..a4a6ec5 100644 --- a/src/closest_color.jl +++ b/src/closest_color.jl @@ -12,6 +12,6 @@ end function colordither( ::ClosestColor, img::GenericImage, cs::AbstractVector{<:Pixel}, metric::DifferenceMetric ) - cs = ccolor(Lab, eltype(cs)).(cs) # convert to Lab - return map(px -> argmin(colordiff(px, c; metric=metric) for c in cs), img) + cs = Lab.(cs) + return map(px -> _closest_color_idx(px, cs, metric), img) end diff --git a/src/error_diffusion.jl b/src/error_diffusion.jl index 4df043c..8456e5a 100644 --- a/src/error_diffusion.jl +++ b/src/error_diffusion.jl @@ -100,7 +100,7 @@ function colordither( px = img[r, c] alg.clamp_error && (px = clamp01(px)) - colorindex = argmin(colordiff(px, col; metric=metric) for col in labcs) # find closest color + colorindex = _closest_color_idx(px, cs, metric) out[r, c] = colorindex # apply pixel to dither, which is an IndirectArray err = px - cs[colorindex] # diffuse "error" to neighborhood in filter diff --git a/src/utils.jl b/src/utils.jl index 452011b..7f0cdce 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -16,6 +16,18 @@ Convert pixel `u` from linear to sRGB color space. @inline linear2srgb(u::Gray) = typeof(u)(linear2srgb(gray(u))) @inline linear2srgb(u::Bool) = u +""" + +""" + +if VERSION >= v"1.7" + _closest_color_idx(px, cs, metric) = argmin(colordiff(px, c; metric=metric) for c in cs) +else + function _closest_color_idx(px, cs, metric) + return argmin([colordiff(px, c; metric=metric) for c in cs]) + end +end + """ bwcolors(T) From bba6b0e72ff1a2d029815a61e33c31810150a5ed Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sun, 21 Nov 2021 20:46:44 +0100 Subject: [PATCH 14/22] Remove unused code --- src/utils.jl | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 7f0cdce..d4b5b58 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -16,10 +16,6 @@ Convert pixel `u` from linear to sRGB color space. @inline linear2srgb(u::Gray) = typeof(u)(linear2srgb(gray(u))) @inline linear2srgb(u::Bool) = u -""" - -""" - if VERSION >= v"1.7" _closest_color_idx(px, cs, metric) = argmin(colordiff(px, c; metric=metric) for c in cs) else @@ -27,36 +23,3 @@ else return argmin([colordiff(px, c; metric=metric) for c in cs]) end end - -""" - bwcolors(T) - -Construct black & white color scheme of type `T`. -""" -bwcolors(::Type{T}) where {T<:NumberLike} = [T(0), T(1)] -const INDEX_BLACK = Int(1) -const INDEX_WHITE = Int(2) - -""" - perchannelbinarycolors(T) - -Construct color scheme that corresponds to channel-wise binary dithering. -Indexing corresponds to binary respresentation: - -# Example -```julia-repl -julia> perchannelditherpalette(RGB) -8-element Array{RGB{N0f8},1} with eltype RGB{N0f8}: - RGB{N0f8}(0.0,0.0,0.0) - RGB{N0f8}(0.0,0.0,1.0) - RGB{N0f8}(0.0,1.0,0.0) - RGB{N0f8}(0.0,1.0,1.0) - RGB{N0f8}(1.0,0.0,0.0) - RGB{N0f8}(1.0,0.0,1.0) - RGB{N0f8}(1.0,1.0,0.0) - RGB{N0f8}(1.0,1.0,1.0) -``` -""" -function perchannelbinarycolors(::Type{T}) where {T<:Color{<:Any,3}} - return [T(a, b, c) for a in 0:1 for b in 0:1 for c in 0:1] -end From da556a50e248c291dfd2dfd044734b7b26744c0b Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sun, 21 Nov 2021 21:12:20 +0100 Subject: [PATCH 15/22] Small performance tweaks --- src/closest_color.jl | 3 ++- src/error_diffusion.jl | 5 ++--- src/threshold.jl | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/closest_color.jl b/src/closest_color.jl index a4a6ec5..9e6e1c2 100644 --- a/src/closest_color.jl +++ b/src/closest_color.jl @@ -6,7 +6,8 @@ Technically this not a dithering algorithm as the quatization error is not "rand 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( diff --git a/src/error_diffusion.jl b/src/error_diffusion.jl index 8456e5a..8730c8f 100644 --- a/src/error_diffusion.jl +++ b/src/error_diffusion.jl @@ -80,7 +80,7 @@ function colordither( # this function does not yet support OffsetArray require_one_based_indexing(img) - out = Matrix{Int}(undef, size(img)...) # allocate matrix of color indices + out = Matrix{UInt8}(undef, size(img)...) # allocate matrix of color indices # Change from normalized intensities to Float as error will get added! # Eagerly promote to the same type to make loop run faster. @@ -100,7 +100,7 @@ function colordither( px = img[r, c] alg.clamp_error && (px = clamp01(px)) - colorindex = _closest_color_idx(px, cs, metric) + colorindex = _closest_color_idx(px, labcs, metric) out[r, c] = colorindex # apply pixel to dither, which is an IndirectArray err = px - cs[colorindex] # diffuse "error" to neighborhood in filter @@ -113,7 +113,6 @@ function colordither( end end end - return out end diff --git a/src/threshold.jl b/src/threshold.jl index c4b0c36..1be3970 100644 --- a/src/threshold.jl +++ b/src/threshold.jl @@ -29,5 +29,6 @@ struct ConstantThreshold{T<:Real} <: AbstractThresholdDither end function binarydither!(alg::ConstantThreshold, out::GenericGrayImage, img::GenericGrayImage) - return out .= img .> alg.threshold + threshold = eltype(img)(alg.threshold) + return out .= img .> threshold end From 8c6c7e39783517de719d3929038a4e56d768bfcd Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sun, 21 Nov 2021 21:36:18 +0100 Subject: [PATCH 16/22] Revert binary error diffusion --- src/error_diffusion.jl | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/error_diffusion.jl b/src/error_diffusion.jl index 8730c8f..253d89a 100644 --- a/src/error_diffusion.jl +++ b/src/error_diffusion.jl @@ -40,8 +40,6 @@ function binarydither!(alg::ErrorDiffusion, out::GenericGrayImage, img::GenericG drs = axes(alg.filter, 1) dcs = axes(alg.filter, 2) - T = eltype(out) - T0, T1 = T(0), T(1) FT0, FT1 = FT(0), FT(1) @inbounds for r in axes(img, 1) @@ -49,15 +47,10 @@ function binarydither!(alg::ErrorDiffusion, out::GenericGrayImage, img::GenericG px = img[r, c] alg.clamp_error && (px = clamp01(px)) - if px > 0.5 - out[r, c] = T1 - col = FT1 # round to closest color - else - out[r, c] = T0 - col = FT0 - end - + 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 + for dr in drs for dc in dcs if (r + dr) in axes(img, 1) && (c + dc) in axes(img, 2) @@ -80,7 +73,7 @@ function colordither( # this function does not yet support OffsetArray require_one_based_indexing(img) - out = Matrix{UInt8}(undef, size(img)...) # allocate matrix of color indices + index = Matrix{UInt8}(undef, size(img)...) # allocate matrix of color indices # Change from normalized intensities to Float as error will get added! # Eagerly promote to the same type to make loop run faster. @@ -101,7 +94,7 @@ function colordither( alg.clamp_error && (px = clamp01(px)) colorindex = _closest_color_idx(px, labcs, metric) - out[r, c] = colorindex # apply pixel to dither, which is an IndirectArray + index[r, c] = colorindex err = px - cs[colorindex] # diffuse "error" to neighborhood in filter for dr in drs @@ -113,7 +106,7 @@ function colordither( end end end - return out + return index end """ From 44e5c09571b5e114d88b6b31f03ef1a261e9d471 Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sun, 21 Nov 2021 21:59:51 +0100 Subject: [PATCH 17/22] Enable SIMD with lookup tables --- src/ordered.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ordered.jl b/src/ordered.jl index 1bec476..6d888a2 100644 --- a/src/ordered.jl +++ b/src/ordered.jl @@ -22,12 +22,17 @@ function binarydither!( else mat = FT.(alg.mat) end + matsize = size(mat) + rlookup = [mod1(i, matsize[1]) for i in 1:size(img)[1]] + clookup = [mod1(i, matsize[2]) for i in 1:size(img)[2]] T = eltype(out) black, white = T(0), T(1) - @inbounds for i in CartesianIndices(img) - out[i] = img[i] > mat[mod1.(Tuple(i), matsize)...] ? white : black + + @inbounds @simd for i in CartesianIndices(img) + r, c = Tuple(i) + @inbounds out[i] = ifelse(img[i] > mat[rlookup[r], clookup[c]], white, black) end return out end From 729bfce24b0c365b57736cabba56639242b880b5 Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sun, 21 Nov 2021 22:39:22 +0100 Subject: [PATCH 18/22] Fix test warnings Previously got: Warning: Assignment to `d` in soft scope is ambiguous because a global variable by the same name exists: `d` will be treated as a new local. Disambiguate by using `local d` to suppress this warning or `global d` to assign to the existing global variable. --- test/test_fixed_color.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_fixed_color.jl b/test/test_fixed_color.jl index 1e99abf..1bf425b 100644 --- a/test/test_fixed_color.jl +++ b/test/test_fixed_color.jl @@ -16,14 +16,14 @@ for C in [RGB, HSV] for (name, alg) in algs img1 = C.(img) img2 = copy(img1) - d = dither(img2, alg) + local d = dither(img2, alg) @test_reference "references/fixed_color_$(C)_$(name).txt" d @test eltype(d) <: C @test img2 == img1 # image not modified imshow(d) - d = dither!(img2, alg) + local d = dither!(img2, alg) @test eltype(d) <: C @test img2 == d # image updated in-place end From 00fe72d1e10e01987381d66f7cef868115704623 Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Tue, 23 Nov 2021 22:12:38 +0100 Subject: [PATCH 19/22] Update CI for Julia 1.0 --- .github/workflows/CI.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 98ec2d8..bde6cc0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -45,9 +45,11 @@ jobs: run: | using Pkg Pkg.add([ - PackageSpec(name="Images", version="0.23"), + PackageSpec(name="Images", version="0.24"), + 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 From 403a4dcd7c812b7b534ce37df576ec9a3aa73652 Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Tue, 23 Nov 2021 22:14:22 +0100 Subject: [PATCH 20/22] Remove duplicate inbounds call --- src/ordered.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ordered.jl b/src/ordered.jl index 6d888a2..f763e83 100644 --- a/src/ordered.jl +++ b/src/ordered.jl @@ -32,7 +32,7 @@ function binarydither!( @inbounds @simd for i in CartesianIndices(img) r, c = Tuple(i) - @inbounds out[i] = ifelse(img[i] > mat[rlookup[r], clookup[c]], white, black) + out[i] = ifelse(img[i] > mat[rlookup[r], clookup[c]], white, black) end return out end From 80104ab2f220c2d876fa8529c109fa555248abd7 Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Tue, 23 Nov 2021 22:17:01 +0100 Subject: [PATCH 21/22] Attempt to fix CI --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index bde6cc0..76319bb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -45,7 +45,7 @@ jobs: run: | using Pkg Pkg.add([ - PackageSpec(name="Images", version="0.24"), + PackageSpec(name="Images", version="0.23"), PackageSpec(name="IndirectArrays", version="0.5"), PackageSpec(name="ImageCore", version="0.8"), ]) From b88dbcde8abba84c35fc8fb9a2286ff967839452 Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Tue, 23 Nov 2021 22:52:57 +0100 Subject: [PATCH 22/22] Add compat entry for IndirectArrays --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index f196457..2bf5795 100644 --- a/Project.toml +++ b/Project.toml @@ -12,6 +12,7 @@ Requires = "ae029012-a4dd-5104-9daa-d747884805df" [compat] ImageCore = "0.8.1, 0.9" +IndirectArrays = "0.5, 1.0" OffsetArrays = "1" Requires = "1" julia = "1"