diff --git a/Project.toml b/Project.toml index 5dd2e581..59f1c863 100644 --- a/Project.toml +++ b/Project.toml @@ -18,6 +18,7 @@ MLDataPattern = "9920b226-0b2a-5f5f-9153-9aa70a013f8b" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" [compat] ComputationalResources = "0.3" @@ -37,6 +38,7 @@ julia = "1" [extras] ImageDistances = "51556ac3-7006-55f5-8cb3-34580c88182d" ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" +ImageQualityIndexes = "2996bd0c-7a13-11e9-2da2-2f5ce47296a9" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" ReferenceTests = "324d217c-45ce-50fc-942e-d289b448e8cf" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" @@ -44,4 +46,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990" [targets] -test = ["ImageDistances", "ImageMagick", "Random", "ReferenceTests", "Statistics", "TestImages", "Test"] +test = ["ImageDistances", "ImageMagick", "ImageQualityIndexes", "Random", "ReferenceTests", "Statistics", "TestImages", "Test"] diff --git a/docs/operations/blur/config.json b/docs/operations/blur/config.json new file mode 100644 index 00000000..5d2444fa --- /dev/null +++ b/docs/operations/blur/config.json @@ -0,0 +1,6 @@ +{ + "title": "Blurring", + "order": [ + "gaussianblur.jl" + ] +} diff --git a/docs/operations/blur/gaussianblur.jl b/docs/operations/blur/gaussianblur.jl new file mode 100644 index 00000000..80c4299d --- /dev/null +++ b/docs/operations/blur/gaussianblur.jl @@ -0,0 +1,34 @@ +# --- +# title: GaussianBlur +# cover: gaussianblur.gif +# description: blur the input image using a gaussian kernel +# --- + +# [`GaussianBlur`](@ref) can be used to blur the input image using a gaussian +# kernel with a specified kernel size and standard deviation. + +using Augmentor +using ImageShow, ImageCore + +img_in = testpattern(RGB, ratio=0.5) + +mosaicview( + img_in, + augment(img_in, GaussianBlur(3)), + augment(img_in, GaussianBlur(5, 2.5)); + fillvalue=colorant"white", nrow=1, npad=10 +) + +# ## References + +#md # ```@docs +#md # GaussianBlur +#md # ``` + + +## save covers #src +using ImageMagick #src +using FileIO #src +include(joinpath("..", "assets", "utilities.jl")) #src +cover = make_gif(testpattern(RGB, ratio=0.5), GaussianBlur(5), 2) #src +ImageMagick.save("gaussianblur.gif", cover; fps=1) #src diff --git a/docs/operations/color/colorjitter.jl b/docs/operations/color/colorjitter.jl new file mode 100644 index 00000000..e8c938d5 --- /dev/null +++ b/docs/operations/color/colorjitter.jl @@ -0,0 +1,32 @@ +# --- +# title: ColorJitter +# cover: colorjitter.gif +# description: Adjust contrast and brightness of an image +# --- + +# [`ColorJitter`](@ref) can be used to adjust the contrast and brightness of an input image. + +using Augmentor +using ImageShow, ImageCore + +img_in = testpattern(RGB, ratio=0.5) + +mosaicview( + img_in, + augment(img_in, ColorJitter(1.2, 0.3)), + augment(img_in, ColorJitter(0.75, -0.2)); + fillvalue=colorant"white", nrow=1, npad=10 +) + +# ## References + +#md # ```@docs +#md # ColorJitter +#md # ``` + +## save covers #src +using ImageMagick #src +using FileIO #src +include(joinpath("..", "assets", "utilities.jl")) #src +cover = make_gif(testpattern(RGB, ratio=0.5), ColorJitter(), 4) #src +ImageMagick.save("colorjitter.gif", cover; fps=1) #src diff --git a/docs/operations/color/config.json b/docs/operations/color/config.json new file mode 100644 index 00000000..9705db1f --- /dev/null +++ b/docs/operations/color/config.json @@ -0,0 +1,7 @@ +{ + "title": "Color adjustments", + "order": [ + "colorjitter.jl" + ], + "description": "" +} diff --git a/docs/operations/config.json b/docs/operations/config.json index 3acad879..5413633f 100644 --- a/docs/operations/config.json +++ b/docs/operations/config.json @@ -2,8 +2,10 @@ "order": [ "affine", "distortions", + "color", + "blur", "size", "misc" ] } - \ No newline at end of file + diff --git a/src/Augmentor.jl b/src/Augmentor.jl index c2974a5a..80813cf9 100644 --- a/src/Augmentor.jl +++ b/src/Augmentor.jl @@ -69,6 +69,9 @@ export ElasticDistortion, + ColorJitter, + GaussianBlur, + augment, augment!, augmentbatch!, @@ -95,6 +98,8 @@ include("operations/resize.jl") include("operations/scale.jl") include("operations/zoom.jl") include("operations/either.jl") +include("operations/color.jl") +include("operations/blur.jl") include("distortionfields.jl") include("distortedview.jl") diff --git a/src/operations/blur.jl b/src/operations/blur.jl new file mode 100644 index 00000000..04df9b74 --- /dev/null +++ b/src/operations/blur.jl @@ -0,0 +1,76 @@ +import ImageFiltering: imfilter, KernelFactors.gaussian + +""" + GaussianBlur <: ImageOperation + +Description +-------------- + +Blurs an image using a Gaussian filter. + +Usage +-------------- + + GaussianBlur(k, [σ]) + +Arguments +-------------- + +- **`k`** : `Integer` or `AbstractVector` of `Integer` that denote + the kernel size. It must be an odd positive number. +- **`σ`** : Optional. `Real` or `AbstractVector` of `Real` that denote the + standard deviation. It must be a positive number. + Defaults to `0.3 * ((k - 1) / 2 - 1) + 0.8`. + +Examples +-------------- + +``` +using Augmentor +img = testpattern() + +# use exactly k=3 and σ=1.0 +augment(img, GaussianBlur(3, 1.0)) + +# pick k and σ randomly from the specified ranges +augment(img, GaussianBlur(3:2:7, 1.0:0.1:2.0)) +``` +""" +struct GaussianBlur{K <: AbstractVector, S <: AbstractVector} <: ImageOperation + k::K + σ::S + + function GaussianBlur(k::K, σ::S) where {K <: AbstractVector{<:Integer}, + S <: AbstractVector{<:Real}} + minimum(k) > 0 || throw(ArgumentError("Kernel size must be positive: $(k)")) + minimum(σ) > 0 || throw(ArgumentError("σ must be positive: $(σ)")) + new{K, S}(k, σ) + end +end + +# The default value for σ is taken from Albumentations +GaussianBlur(k) = GaussianBlur(k, 0.3 * ((k - 1) / 2 - 1) + 0.8) +GaussianBlur(k, σ) = GaussianBlur(vectorize(k), vectorize(σ)) + +randparam(op::GaussianBlur, img) = (safe_rand(op.k), safe_rand(op.σ)) + +@inline supports_eager(::Type{<:GaussianBlur}) = true + +function applyeager(op::GaussianBlur, img::AbstractArray, (k, σ)) + n = ndims(img) + kernel = gaussian(ntuple(_->σ, n), ntuple(_->k, n)) + return imfilter(img, kernel) +end + +function showconstruction(io::IO, op::GaussianBlur) + print(io, typeof(op).name.name, '(', op.k, ", ", op.σ,')') +end + +function Base.show(io::IO, op::GaussianBlur) + if get(io, :compact, false) + print(io, "GaussianBlur with k=$(op.k) and σ=$(op.σ)") + else + print(io, "Augmentor.") + showconstruction(io, op) + end +end diff --git a/src/operations/color.jl b/src/operations/color.jl new file mode 100644 index 00000000..a9529240 --- /dev/null +++ b/src/operations/color.jl @@ -0,0 +1,119 @@ +import ImageCore: clamp01, gamutmax +import Statistics: mean + +""" + ColorJitter <: ImageOperation + +Description +-------------- + +Adjusts the brightness and contrast of an image according to the formula +`α * image[i] + β * M`, where `M` is either `mean(image)` or the maximum +intensity value. + +Usage +-------------- + + ColorJitter() + ColorJitter(α, β; [usemax]) + +Arguments +-------------- + +- **`α`** : `Real` or `AbstractVector` of `Real` that denote the coefficient(s) + for contrast adjustment. Defaults to `0.8:0.1:1.2`. +- **`β`** : `Real` or `AbstractVector` of `Real` that denote the coefficient(s) + for brightness adjustment. Defaults to `-0.2:0.1:0.2`. +- **`usemax::Bool`**: Optional. If `true`, the brightness will be adjusted by + the maximum intensity value; otherwise, the image mean will be used. + Defaults to `true`. + +Examples +-------------- + +``` +using Augmentor +img = testpattern() + +# use exactly 1.2 for contrast, and one of 0.5 and 0.8 for brightness +augment(img, ColorJitter(1.2, [0.5, 0.8])) + +# pick the coefficients randomly from the specified ranges +augment(img, ColorJitter(0.8:0.1:2.0, 0.5:0.1:1.1)) +``` +""" +struct ColorJitter{A<:AbstractVector, B<:AbstractVector} <: ImageOperation + α::A + β::B + usemax::Bool + + function ColorJitter(α::A, β::B, usemax) where {A<:AbstractVector{<:Real}, + B<:AbstractVector{<:Real}} + length(α) > 0 || throw(ArgumentError("Range $(α) is empty")) + length(β) > 0 || throw(ArgumentError("Range $(β) is empty")) + new{A, B}(α, β, usemax) + end +end + +# The default values for α and β are taken from Albumentations +ColorJitter() = ColorJitter(0.8:0.1:1.2, -0.2:0.1:0.2, true) +ColorJitter(α, β; usemax=true) = ColorJitter(vectorize(α), vectorize(β), usemax) + +randparam(op::ColorJitter, img) = (safe_rand(op.α), safe_rand(op.β)) + +@inline supports_eager(::Type{<:ColorJitter}) = true +@inline supports_lazy(::Type{<:ColorJitter}) = true + +function applyeager(op::ColorJitter, img::AbstractArray, (α, β)) + M = _get_M(op, img) + return _map_pix.(α, β, M, img) +end + +function applylazy(op::ColorJitter, img::AbstractArray, (α, β)) + M = _get_M(op, img) + return ColorJitterView(img, α, β, M) +end + +function showconstruction(io::IO, op::ColorJitter) + print(io, typeof(op).name.name, '(', op.α, ", ", op.β, ", ", op.usemax, ')') +end + +function Base.show(io::IO, op::ColorJitter) + if get(io, :compact, false) + maxmsg = op.usemax ? "max. intensity" : "mean value"; + print(io, "Color jitter with coffecients α=$(op.α) and β=$(op.β) (w.r.t. $(maxmsg))") + else + print(io, "Augmentor.") + showconstruction(io, op) + end +end + +_map_pix(α, β, M, pix) = clamp01(α * pix + β * M) + +function _get_M(op::ColorJitter, img) + T = eltype(img) + if op.usemax + return T(gamutmax(T)...) + else + return convert(T, mean(img)) + end +end + +# This wraps an image so that its pixels appear as after the contrast and +# brightness adjustment. This is done in the `getindex` method. +struct ColorJitterView{T, P<:AbstractMatrix{T}, R} <: AbstractArray{T, 2} + orig::P + α::R + β::R + M::T + + function ColorJitterView(img::P, α::R, β::R, M) where {P <: AbstractMatrix, + R <: Real} + new{eltype(P), P, R}(img, α, β, M) + end +end + +Base.parent(A::ColorJitterView) = A.parent +Base.size(A::ColorJitterView) = size(A.orig) +Base.axes(A::ColorJitterView) = axes(A.orig) +Base.@propagate_inbounds Base.getindex(A::ColorJitterView, i, j) = _map_pix(A.α, A.β, A.M, A.orig[i, j]) diff --git a/test/operations/tst_blur.jl b/test/operations/tst_blur.jl new file mode 100644 index 00000000..7adeb826 --- /dev/null +++ b/test/operations/tst_blur.jl @@ -0,0 +1,35 @@ +@testset "GaussianBlur" begin + @testset "constructor" begin + @test_throws ArgumentError GaussianBlur(0) + @test_throws ArgumentError GaussianBlur(3, 0) + @test_throws ArgumentError GaussianBlur([1, 3], 0) + @test_throws ArgumentError GaussianBlur([1, 0], 1) + @test_throws ArgumentError GaussianBlur(3, -1:1) + end + @testset "randparam" begin + img = testpattern() + ks = [3, 1:2:5, [1, 3, 5]] + σs = [1, 1:0.1:2, [1, 2]] + for k in ks, σ in σs + op = GaussianBlur(k, σ) + p = @inferred Augmentor.randparam(op, img) + @test p[1] in k + @test p[2] in σ + end + end + @testset "eager" begin + @test Augmentor.supports_eager(GaussianBlur) + + k = 3 + σ = 3.2 + + imgs = [testpattern(), camera] + + for img in imgs + img = convert.(RGB, img) + ref = imfilter(img, KernelFactors.gaussian((σ, σ), (k, k))) + res = Augmentor.applyeager(GaussianBlur(k, σ), img) + @test assess_psnr(res, ref) > 25 + end + end +end diff --git a/test/operations/tst_color.jl b/test/operations/tst_color.jl new file mode 100644 index 00000000..5e77b4c5 --- /dev/null +++ b/test/operations/tst_color.jl @@ -0,0 +1,77 @@ +@testset "ColorJitter" begin + @testset "constructor" begin + @test_throws ArgumentError ColorJitter(1., 3.0:2.0) + @test_throws ArgumentError ColorJitter(5.0:3.0, 1.) + end + @testset "randparam" begin + img = testpattern() + αs = [3.0, 1.0:0.2:5.0, [1.0, 3.0, 5.0]] + βs = [1.0, 1.0:0.1:2.0, [1.0, 2.0]] + for α in αs, β in βs + op = ColorJitter(α, β) + p = @inferred Augmentor.randparam(op, img) + @test p[1] in α + @test p[2] in β + end + end + @testset "_get_M" begin + imgs = [testpattern()]#, camera] + for img in imgs + T = eltype(img) + op = ColorJitter(1., 0., usemax=true) + M = @inferred Augmentor._get_M(op, img) + @test M == T(gamutmax(T)...) + op = ColorJitter(1., 0., usemax=false) + M = @inferred Augmentor._get_M(op, img) + @test M == convert(T, mean(img)) + end + end + @testset "eager" begin + @test Augmentor.supports_eager(ColorJitter) + + α = 0.2 + β = 0.1 + + imgs = [testpattern(), camera] + + # M = mean value + for img in imgs + img = convert.(color_type(eltype(img)), img) + ref = clamp01.(α .* img .+ β * mean(img)) + res = Augmentor.applyeager(ColorJitter(α, β, usemax=false), img) + @test assess_psnr(res, ref) > 25 + end + # M = maximum value + for img in imgs + img = convert.(color_type(eltype(img)), img) + T = eltype(img) + ref = clamp01.(α .* img .+ β * T(gamutmax(T)...)) + res = Augmentor.applyeager(ColorJitter(α, β, usemax=true), img) + @test assess_psnr(res, ref) > 25 + end + end + @testset "lazy" begin + @test Augmentor.supports_lazy(ColorJitter) + + α = 0.2 + β = 0.1 + + imgs = [testpattern(), camera] + + # M = mean value + for img in imgs + img = convert.(color_type(eltype(img)), img) + ref = clamp01.(α .* img .+ β * mean(img)) + res = Augmentor.applylazy(ColorJitter(α, β, usemax=false), img) + @test assess_psnr(res, ref) > 25 + end + # M = maximum value + for img in imgs + img = convert.(color_type(eltype(img)), img) + T = eltype(img) + ref = clamp01.(α .* img .+ β * T(gamutmax(T)...)) + res = Augmentor.applylazy(ColorJitter(α, β, usemax=true), img) + @test assess_psnr(res, ref) > 25 + end + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 6636bbb7..05f80e1b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,7 +4,7 @@ using ImageTransformations, CoordinateTransformations, Interpolations using MLDataPattern using OffsetArrays, StaticArrays, IdentityRanges, ComputationalResources using ImageCore.MappedArrays -using ReferenceTests, Test, TestImages, ImageDistances, Statistics +using ReferenceTests, Test, TestImages, ImageDistances, ImageQualityIndexes, Statistics if isdefined(OffsetArrays, :IdOffsetRange) OffsetRange = OffsetArrays.IdOffsetRange @@ -52,6 +52,8 @@ tests = [ "operations/tst_zoom.jl", "operations/tst_distortions.jl", "operations/tst_either.jl", + "operations/tst_blur.jl", + "operations/tst_color.jl", "tst_operations.jl", "tst_pipeline.jl", "tst_augment.jl",