From 35068f304b26938f6f8a452942054d9659b5cebb Mon Sep 17 00:00:00 2001 From: Adrian Hill Date: Sat, 14 Sep 2024 16:56:39 +0200 Subject: [PATCH] Refactor `OrderedDithering` struct (#104) * Refactor ordered dithering structs * Allow any `AbstractMatrix` * Refactor tests on grayscale gradient --- Project.toml | 2 +- docs/literate/sdf_halftoning.jl | 6 +- src/ordered.jl | 120 +++++++++--------- src/ordered_imagemagick.jl | 128 +++++++++---------- test/gradient_image.jl | 8 ++ test/runtests.jl | 8 -- test/test_braille.jl | 7 ++ test/test_gradient.jl | 212 ++++++++++++++++++-------------- 8 files changed, 262 insertions(+), 229 deletions(-) create mode 100644 test/gradient_image.jl diff --git a/Project.toml b/Project.toml index 0759838..ff36a15 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "DitherPunk" uuid = "b8f752a5-abd5-43b6-a55b-e75efda20de0" authors = ["Adrian Hill"] -version = "3.1.0" +version = "4.0.0-DEV" [deps] ColorQuantization = "652893fb-f6a0-4a00-a44a-7fb8fac69e01" diff --git a/docs/literate/sdf_halftoning.jl b/docs/literate/sdf_halftoning.jl index 580011f..f8644fd 100644 --- a/docs/literate/sdf_halftoning.jl +++ b/docs/literate/sdf_halftoning.jl @@ -33,10 +33,10 @@ function sdf2halftone(sdf, n) rg = range(-1, 1; length=n) A = [sdf(x, y) for y in rg, x in rg] p = sortperm(reshape(-A, :)) - B = Vector{Int}(undef, n^2) + B = Matrix{Int}(undef, n, n) B[p] .= 1:(n^2) - return OrderedDither(reshape(B, size(A)...)//(n^2 + 1)) -end; + return OrderedDither(B) +end # Let's try it on a test image: img = testimage("fabio_gray_512") diff --git a/src/ordered.jl b/src/ordered.jl index 7c51fd6..b95e961 100644 --- a/src/ordered.jl +++ b/src/ordered.jl @@ -1,36 +1,45 @@ """ - OrderedDither(mat::AbstractMatrix) + OrderedDither(mat::AbstractMatrix, [max::Integer]) Generalized ordered dithering algorithm using a threshold map. -Takes a normalized threshold matrix `mat`. +Takes an unnormalized threshold matrix `mat` and optionally a normalization quotient `max` (defaults to `length(mat)+1`). When applying the algorithm to an image, the threshold matrix is repeatedly tiled 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{<:Rational},R<:Real} <: AbstractDither - mat::T +struct OrderedDither{I<:Integer,M<:AbstractMatrix{<:I},R<:Real} <: AbstractDither + mat::M + max::I color_error_multiplier::R -end -function OrderedDither(mat; invert_map=false, color_error_multiplier=0.5) - if invert_map - return OrderedDither(1 .- mat, color_error_multiplier) + + function OrderedDither( + mat::M; + max::I=convert(I, length(mat) + 1), + invert_map=false, + color_error_multiplier::R=0.5, + ) where {I<:Integer,M<:AbstractMatrix{<:I},R<:Real} + require_one_based_indexing(mat) + if invert_map + mat = max .- mat + end + return new{I,M,R}(mat, max, color_error_multiplier) end - return OrderedDither(mat, color_error_multiplier) end function binarydither!(alg::OrderedDither, out::GenericGrayImage, img::GenericGrayImage) # eagerly promote to the same eltype to make for-loop faster FT = floattype(eltype(img)) - mat = FT.(alg.mat) + mat = FT.(alg.mat / alg.max) T = eltype(out) black, white = T(0), T(1) # Precompute lookup tables for modulo indexing of threshold matrix - 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]] + hm, wm = size(mat) + hi, wi = size(img) + rlookup = [mod1(i, hm) for i in 1:hi] + clookup = [mod1(i, wm) for i in 1:wi] @inbounds @simd for i in CartesianIndices(img) r, c = Tuple(i) @@ -53,11 +62,12 @@ function colordither( cs_xyz = XYZ.(cs) # Precompute lookup tables for modulo indexing of threshold matrix - mat = numerator.(alg.mat) + mat = alg.mat nmax = maximum(mat) - 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]] + hm, wm = size(mat) + hi, wi = size(img) + rlookup = [mod1(i, hm) for i in 1:hi] + clookup = [mod1(i, wm) for i in 1:wi] # Allocate matrices candidates = Array{Int}(undef, nmax) @@ -109,7 +119,7 @@ which defaults to `1`. """ function Bayer(level=1; kwargs...) bayer = bayer_matrix(level) .+ 1 - return OrderedDither(bayer//(2^(2 * level + 2) + 1); kwargs...) + return OrderedDither(bayer; kwargs...) end """ @@ -136,15 +146,14 @@ Clustered dots ordered dithering. Uses ``6 \\times 6`` threshold matrix `CLUSTERED_DOTS_MAT`. """ ClusteredDots(; kwargs...) = OrderedDither(CLUSTERED_DOTS_MAT; kwargs...) -const CLUSTERED_DOTS_MAT = - [ - 35 30 18 22 31 36 - 29 15 10 17 21 32 - 14 9 5 6 16 20 - 13 4 1 2 11 19 - 28 8 3 7 24 25 - 34 27 12 23 26 33 - ]//37 +const CLUSTERED_DOTS_MAT = [ + 35 30 18 22 31 36 + 29 15 10 17 21 32 + 14 9 5 6 16 20 + 13 4 1 2 11 19 + 28 8 3 7 24 25 + 34 27 12 23 26 33 +] """ CentralWhitePoint() @@ -153,15 +162,14 @@ Central white point ordered dithering. Uses ``6 \\times 6`` threshold matrix `CENTRAL_WHITE_POINT_MAT`. """ CentralWhitePoint(; kwargs...) = OrderedDither(CENTRAL_WHITE_POINT_MAT; kwargs...) -const CENTRAL_WHITE_POINT_MAT = - [ - 35 26 22 18 30 34 - 31 14 10 6 13 25 - 19 7 2 1 9 21 - 23 11 3 4 5 17 - 27 15 8 12 16 29 - 36 32 20 24 28 33 - ]//37 +const CENTRAL_WHITE_POINT_MAT = [ + 35 26 22 18 30 34 + 31 14 10 6 13 25 + 19 7 2 1 9 21 + 23 11 3 4 5 17 + 27 15 8 12 16 29 + 36 32 20 24 28 33 +] """ BalancedCenteredPoint() @@ -170,15 +178,14 @@ Balanced centered point ordered dithering. Uses ``6 \\times 6`` threshold matrix `BALANCED_CENTERED_POINT_MAT`. """ BalancedCenteredPoint(; kwargs...) = OrderedDither(BALANCED_CENTERED_POINT_MAT; kwargs...) -const BALANCED_CENTERED_POINT_MAT = - [ - 31 23 17 22 34 36 - 25 12 8 10 27 29 - 14 6 1 3 15 20 - 16 4 2 5 13 19 - 28 9 7 11 26 30 - 33 21 18 24 32 35 - ]//37 +const BALANCED_CENTERED_POINT_MAT = [ + 31 23 17 22 34 36 + 25 12 8 10 27 29 + 14 6 1 3 15 20 + 16 4 2 5 13 19 + 28 9 7 11 26 30 + 33 21 18 24 32 35 +] """ Rhombus() @@ -186,15 +193,14 @@ const BALANCED_CENTERED_POINT_MAT = Diagonal ordered matrix with balanced centered points. Uses ``8 \\times 8`` threshold matrix `RHOMBUS_MAT`. """ -Rhombus(; kwargs...) = OrderedDither(RHOMBUS_MAT; kwargs...) -const RHOMBUS_MAT = - [ - 14 10 6 13 19 23 27 20 - 7 2 1 9 26 31 32 24 - 11 3 4 5 22 30 29 28 - 15 8 12 16 18 25 21 17 - 19 23 27 20 14 10 6 13 - 26 31 32 24 7 2 1 9 - 22 30 29 28 11 3 4 5 - 18 25 21 17 15 8 12 16 - ]//33 +Rhombus(; kwargs...) = OrderedDither(RHOMBUS_MAT; max=33, kwargs...) +const RHOMBUS_MAT = [ + 14 10 6 13 19 23 27 20 + 7 2 1 9 26 31 32 24 + 11 3 4 5 22 30 29 28 + 15 8 12 16 18 25 21 17 + 19 23 27 20 14 10 6 13 + 26 31 32 24 7 2 1 9 + 22 30 29 28 11 3 4 5 + 18 25 21 17 15 8 12 16 +] diff --git a/src/ordered_imagemagick.jl b/src/ordered_imagemagick.jl index 3f74061..d619a83 100644 --- a/src/ordered_imagemagick.jl +++ b/src/ordered_imagemagick.jl @@ -18,11 +18,11 @@ ImageMagick's Checkerboard 2x2 dither """ -IM_checks(; kwargs...) = OrderedDither(CHECKS; kwargs...) +IM_checks(; kwargs...) = OrderedDither(CHECKS; max=3, kwargs...) const CHECKS = [ 1 2 2 1 -]//3 +] ## ImageMagic's Halftones - Angled 45 degrees # Initially added to ImageMagick by Glenn Randers-Pehrson, IM v6.2.8-6, @@ -33,47 +33,45 @@ const CHECKS = [ ImageMagick's Halftone 4x4 - Angled 45 degrees """ -IM_h4x4a(; kwargs...) = OrderedDither(H4X4A; kwargs...) +IM_h4x4a(; kwargs...) = OrderedDither(H4X4A; max=9, kwargs...) const H4X4A = [ 4 2 7 5 3 1 8 6 7 5 4 2 8 6 3 1 -]//9 +] """ IM_h6x6a() ImageMagick's Halftone 6x6 - Angled 45 degrees """ -IM_h6x6a(; kwargs...) = OrderedDither(H6X6A; kwargs...) -const H6X6A = - [ - 14 13 10 8 2 3 - 16 18 12 7 1 4 - 15 17 11 9 6 5 - 8 2 3 14 13 10 - 7 1 4 16 18 12 - 9 6 5 15 17 11 - ]//19 +IM_h6x6a(; kwargs...) = OrderedDither(H6X6A; max=19, kwargs...) +const H6X6A = [ + 14 13 10 8 2 3 + 16 18 12 7 1 4 + 15 17 11 9 6 5 + 8 2 3 14 13 10 + 7 1 4 16 18 12 + 9 6 5 15 17 11 +] """ IM_h8x8a() ImageMagick's Halftone 8x8 - Angled 45 degrees """ -IM_h8x8a(; kwargs...) = OrderedDither(H8X8A; kwargs...) -const H8X8A = - [ - 13 7 8 14 17 21 22 18 - 6 1 3 9 28 31 29 23 - 5 2 4 10 27 32 30 24 - 16 12 11 15 20 26 25 19 - 17 21 22 18 13 7 8 14 - 28 31 29 23 6 1 3 9 - 27 32 30 24 5 2 4 10 - 20 26 25 19 16 12 11 15 - ]//33 +IM_h8x8a(; kwargs...) = OrderedDither(H8X8A; max=33, kwargs...) +const H8X8A = [ + 13 7 8 14 17 21 22 18 + 6 1 3 9 28 31 29 23 + 5 2 4 10 27 32 30 24 + 16 12 11 15 20 26 25 19 + 17 21 22 18 13 7 8 14 + 28 31 29 23 6 1 3 9 + 27 32 30 24 5 2 4 10 + 20 26 25 19 16 12 11 15 +] # ImageMagic's Halftones - Orthogonally Aligned, or Un-angled # Initially added by Anthony Thyssen, IM v6.2.9-5 using techniques from @@ -91,7 +89,7 @@ const H4X4O = [ 12 16 14 8 10 15 6 2 5 9 3 1 -]//17 +] """ IM_h6x6o() @@ -99,15 +97,14 @@ const H4X4O = [ ImageMagick's Halftone 6x6 - Orthogonally Aligned """ IM_h6x6o(; kwargs...) = OrderedDither(H6X6O; kwargs...) -const H6X6O = - [ - 7 17 27 14 9 4 - 21 29 33 31 18 11 - 24 32 36 34 25 22 - 19 30 35 28 20 10 - 8 15 26 16 6 2 - 5 13 23 12 3 1 - ]//37 +const H6X6O = [ + 7 17 27 14 9 4 + 21 29 33 31 18 11 + 24 32 36 34 25 22 + 19 30 35 28 20 10 + 8 15 26 16 6 2 + 5 13 23 12 3 1 +] """ IM_h8x8o() @@ -115,17 +112,16 @@ const H6X6O = ImageMagick's Halftone 8x8 - Orthogonally Aligned """ IM_h8x8o(; kwargs...) = OrderedDither(H8X8O; kwargs...) -const H8X8O = - [ - 7 21 33 43 36 19 9 4 - 16 27 51 55 49 29 14 11 - 31 47 57 61 59 45 35 23 - 41 53 60 64 62 52 40 38 - 37 44 58 63 56 46 30 22 - 15 28 48 54 50 26 17 10 - 8 18 34 42 32 20 6 2 - 5 13 25 39 24 12 3 1 - ]//65 +const H8X8O = [ + 7 21 33 43 36 19 9 4 + 16 27 51 55 49 29 14 11 + 31 47 57 61 59 45 35 23 + 41 53 60 64 62 52 40 38 + 37 44 58 63 56 46 30 22 + 15 28 48 54 50 26 17 10 + 8 18 34 42 32 20 6 2 + 5 13 25 39 24 12 3 1 +] ## ImageMagic's Halftones - Orthogonally Expanding Circle Patterns # Added by Glenn Randers-Pehrson, 4 Nov 2010, ImageMagick 6.6.5-6 @@ -141,7 +137,7 @@ const C5X5 = [ 6 21 25 24 12 7 18 22 23 11 2 8 9 10 3 -]//26 +] """ IM_c6x6() @@ -149,15 +145,14 @@ const C5X5 = [ ImageMagick's Halftone 6x6 - Orthogonally Expanding Circle Patterns """ IM_c6x6(; kwargs...) = OrderedDither(C6X6; kwargs...) -const C6X6 = - [ - 1 5 14 13 12 4 - 6 22 28 27 21 11 - 15 29 35 34 26 20 - 16 30 36 33 25 19 - 7 23 31 32 24 10 - 2 8 17 18 9 3 - ]//37 +const C6X6 = [ + 1 5 14 13 12 4 + 6 22 28 27 21 11 + 15 29 35 34 26 20 + 16 30 36 33 25 19 + 7 23 31 32 24 10 + 2 8 17 18 9 3 +] """ IM_c7x7() @@ -165,13 +160,12 @@ const C6X6 = ImageMagick's Halftone 7x7 - Orthogonally Expanding Circle Patterns """ IM_c7x7(; kwargs...) = OrderedDither(C7X7; kwargs...) -const C7X7 = - [ - 3 9 18 28 17 8 2 - 10 24 33 39 32 23 7 - 19 34 44 48 43 31 16 - 25 40 45 49 47 38 27 - 20 35 41 46 42 29 15 - 11 21 36 37 28 22 6 - 4 12 13 26 14 5 1 - ]//50 +const C7X7 = [ + 3 9 18 28 17 8 2 + 10 24 33 39 32 23 7 + 19 34 44 48 43 31 16 + 25 40 45 49 47 38 27 + 20 35 41 46 42 29 15 + 11 21 36 37 28 22 6 + 4 12 13 26 14 5 1 +] diff --git a/test/gradient_image.jl b/test/gradient_image.jl new file mode 100644 index 0000000..ad41ae3 --- /dev/null +++ b/test/gradient_image.jl @@ -0,0 +1,8 @@ +using DitherPunk: srgb2linear + +function gradient_image(height, width) + row = reshape(range(0; stop=1, length=width), 1, width) + grad = Gray.(vcat(repeat(row, height))) # Linear gradient + img = srgb2linear.(grad) # For printing, compensate for SRGB colorspace + return grad, img +end diff --git a/test/runtests.jl b/test/runtests.jl index cc09333..ae5b8c5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,14 +1,6 @@ using DitherPunk using Test using Aqua -using ImageBase: Gray - -function gradient_image(height, width) - row = reshape(range(0; stop=1, length=width), 1, width) - grad = Gray.(vcat(repeat(row, height))) # Linear gradient - img = srgb2linear.(grad) # For printing, compensate for SRGB colorspace - return grad, img -end @testset "DitherPunk.jl" begin @testset "Aqua.jl" begin diff --git a/test/test_braille.jl b/test/test_braille.jl index c32dc4d..1d89640 100644 --- a/test/test_braille.jl +++ b/test/test_braille.jl @@ -1,3 +1,10 @@ +using DitherPunk +using ImageBase: RGB, Gray +using Test +using ReferenceTests + +include("gradient_image.jl") + w = 200 h = 4 * 4 # multiple of 4 for unicode braille print img, srgb = gradient_image(h, w) diff --git a/test/test_gradient.jl b/test/test_gradient.jl index 8a355fa..80c6150 100644 --- a/test/test_gradient.jl +++ b/test/test_gradient.jl @@ -1,34 +1,44 @@ using DitherPunk -using ReferenceTests +using DitherPunk: DEFAULT_METHOD, AbstractDither +using ImageBase: Gray using OffsetArrays +using Test +using ReferenceTests + +include("gradient_image.jl") + img, srgb = gradient_image(16, 200) +isoutputrandom(::AbstractDither) = false +isoutputrandom(::WhiteNoiseThreshold) = true + ## Run reference tests for deterministic algorithms # using Dict for Julia 1.0 compatibility -algs_deterministic = Dict( +const ALGS = Dict( # threshold methods - "ConstantThreshold" => ConstantThreshold(), - "ClosestColor" => ClosestColor(), + "ConstantThreshold" => @inferred(ConstantThreshold()), + "ClosestColor" => @inferred(ClosestColor()), + "WhiteNoiseThreshold" => @inferred(WhiteNoiseThreshold()), # ordered dithering - "Bayer" => Bayer(), - "Bayer_l2" => Bayer(2), - "Bayer_l3" => Bayer(3), - "Bayer_l4" => Bayer(4), - "ClusteredDots" => ClusteredDots(), - "CentralWhitePoint" => CentralWhitePoint(), - "BalancedCenteredPoint" => BalancedCenteredPoint(), - "Rhombus" => Rhombus(), - "IM_checks" => IM_checks(), - "IM_h4x4a" => IM_h4x4a(), - "IM_h6x6a" => IM_h6x6a(), - "IM_h8x8a" => IM_h8x8a(), - "IM_h4x4o" => IM_h4x4o(), - "IM_h6x6o" => IM_h6x6o(), - "IM_h8x8o" => IM_h8x8o(), - "IM_c5x5" => IM_c5x5(), - "IM_c6x6" => IM_c6x6(), - "IM_c7x7" => IM_c7x7(), + "Bayer" => @inferred(Bayer()), + "Bayer_l2" => @inferred(Bayer(2)), + "Bayer_l3" => @inferred(Bayer(3)), + "Bayer_l4" => @inferred(Bayer(4)), + "ClusteredDots" => @inferred(ClusteredDots()), + "CentralWhitePoint" => @inferred(CentralWhitePoint()), + "BalancedCenteredPoint" => @inferred(BalancedCenteredPoint()), + "Rhombus" => @inferred(Rhombus()), + "IM_checks" => @inferred(IM_checks()), + "IM_h4x4a" => @inferred(IM_h4x4a()), + "IM_h6x6a" => @inferred(IM_h6x6a()), + "IM_h8x8a" => @inferred(IM_h8x8a()), + "IM_h4x4o" => @inferred(IM_h4x4o()), + "IM_h6x6o" => @inferred(IM_h6x6o()), + "IM_h8x8o" => @inferred(IM_h8x8o()), + "IM_c5x5" => @inferred(IM_c5x5()), + "IM_c6x6" => @inferred(IM_c6x6()), + "IM_c7x7" => @inferred(IM_c7x7()), # error error_diffusion "SimpleErrorDiffusion" => @inferred(SimpleErrorDiffusion()), "FloydSteinberg" => @inferred(FloydSteinberg()), @@ -44,86 +54,102 @@ algs_deterministic = Dict( "ShiauFan2" => @inferred(ShiauFan2()), "FalseFloydSteinberg" => @inferred(DitherPunk.FalseFloydSteinberg()), # Keyword arguments - "Bayer_invert_map" => Bayer(; invert_map=true), + "Bayer_invert_map" => @inferred(Bayer(; invert_map=true)), ) -for (name, alg) in algs_deterministic - local img2 = copy(img) - local d = @inferred dither(img2, alg) - @test_reference "references/gradient/$(name).txt" braille(d; to_string=true) - @test eltype(d) == eltype(img) - @test img2 == img # image not modified -end +# Test setting output type +@testset verbose = true "Binary dithering methods" begin + @testset "$(name)" for (name, alg) in ALGS + _img = copy(img) + dref = @inferred dither(_img, alg) + @test eltype(dref) == eltype(img) + @test _img == img # image not modified -# Test error diffusion kwarg `clamp_error`: -d = @inferred dither(img, FloydSteinberg(); clamp_error=false) -@test_reference "references/gradient/FloydSteinberg_clamp_error.txt" braille( - d; to_string=true -) -@test eltype(d) == eltype(img) + if !isoutputrandom(alg) + @testset "Reference tests" begin + @test_reference "references/gradient/$(name).txt" braille( + dref; to_string=true + ) + end + end -## Algorithms with random output are currently only tested visually -algs_random = Dict( - # threshold methods - "white_noise_dithering" => WhiteNoiseThreshold(), -) + @testset "Output type selection: $T" for T in (Float16, Float32, Bool) + d2 = @inferred dither(Gray{T}, _img, alg) + @test eltype(d2) == Gray{T} + @test d2 ≈ dref + @test _img == img # image not modified + end -for (name, alg) in algs_random - local img2 = copy(img) - local d = @inferred dither(Gray{Bool}, img2, alg) - @test eltype(d) == Gray{Bool} - @test img2 == img # image not modified -end + # Inplace modify output image + @testset "In-place updates" begin + @testset "2-arg" begin + out = zeros(Bool, size(_img)...) + d_inplace = @inferred dither!(out, _img, alg) + @test eltype(out) == Bool + @test out == dref # image updated in-place + @test d_inplace === out + @test _img == img # image not modified + end -## Test to_linear -img2 = copy(img) -d = @inferred dither(img2, Bayer(); to_linear=true) -@test_reference "references/gradient/Bayer_linear.txt" braille(d; to_string=true) -alg = FloydSteinberg() -dl1 = @inferred dither(img2, alg; to_linear=true) -dl2 = @inferred dither(img2; to_linear=true) -@test dl1 == dl2 + @testset "3-arg" begin + # Inplace modify image + imgcopy = deepcopy(img) + d4 = @inferred dither!(imgcopy, alg) + @test d4 == dref + @test imgcopy === d4 # image updated in-place + @test eltype(d4) == eltype(_img) + end + end + end +end ## Test API -d = @inferred dither(img2, alg) -ddef = @inferred dither(img2) -@test d == ddef +@testset "Default algorithm" begin + _img = copy(img) -# Test setting output type -d2 = @inferred dither(Gray{Float16}, img2, alg) -@test eltype(d2) == Gray{Float16} -@test d2 == d -@test img2 == img # image not modified -d2def = @inferred dither(Gray{Float16}, img2) -@test d2 == d2def - -# Inplace modify output image -out = zeros(Bool, size(img2)...) -d3 = @inferred dither!(out, img2, alg) -@test out == d # image updated in-place -@test d3 == d -@test eltype(out) == Bool -@test eltype(d3) == Bool -@test img2 == img # image not modified -outdef = zeros(Bool, size(img2)...) -d3def = @inferred dither!(outdef, img2) -@test out == outdef -@test d3 == d3def - -# Inplace modify image -img2def = deepcopy(img2) -d4 = @inferred dither!(img2, alg) -@test d4 == d -@test img2 == d # image updated in-place -@test eltype(d4) == eltype(img) -@test eltype(img2) == eltype(img) -d4def = @inferred dither!(img2def) -@test d4 == d4def -@test img2 == img2def + d1 = @inferred dither(_img, DEFAULT_METHOD) + d1_default = @inferred dither(_img) + @test d1_default == d1 + + d2 = @inferred dither(Gray{Float16}, _img, DEFAULT_METHOD) + d2_default = @inferred dither(Gray{Float16}, _img) + @test d2_default == d2 + + out = zeros(Bool, size(_img)...) + d3_default = @inferred dither!(out, _img) + @test out == d1 + @test d3_default === out + + imgcopy = deepcopy(_img) + d4_default = @inferred dither!(imgcopy) + @test d4_default == d1 + @test d4_default === imgcopy +end + +# Test error diffusion kwarg `clamp_error`: +@testset "clamp_error" begin + d = @inferred dither(img, FloydSteinberg(); clamp_error=false) + @test_reference "references/gradient/FloydSteinberg_clamp_error.txt" braille( + d; to_string=true + ) + @test eltype(d) == eltype(img) +end + +## Test to_linear +@testset "to_linear" begin + _img = copy(img) + d = @inferred dither(_img, Bayer(); to_linear=true) + @test_reference "references/gradient/Bayer_linear.txt" braille(d; to_string=true) + dl1 = @inferred dither(_img, DEFAULT_METHOD; to_linear=true) + dl2 = @inferred dither(_img; to_linear=true) + @test dl1 == dl2 +end ## Test error messages -@test_throws DomainError ConstantThreshold(; threshold=-0.5) +@testset "Error messages" begin + @test_throws DomainError ConstantThreshold(; threshold=-0.5) -img_zero_based = OffsetMatrix(rand(Float32, 10, 10), 0:9, 0:9) -@test_throws ArgumentError dither(img_zero_based, FloydSteinberg()) -@test_throws ArgumentError dither(img_zero_based) + img_zero_based = OffsetMatrix(rand(Float32, 10, 10), 0:9, 0:9) + @test_throws ArgumentError dither(img_zero_based, FloydSteinberg()) + @test_throws ArgumentError dither(img_zero_based) +end