diff --git a/Project.toml b/Project.toml index 883cf60..2efa103 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ImageTransformations" uuid = "02fcd773-0e25-5acc-982a-7f6622650795" -version = "0.8.0" +version = "0.8.1" [deps] AxisAlgorithms = "13072b0f-2c55-5437-9ae7-d433b7a33950" @@ -17,14 +17,14 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] AxisAlgorithms = "1.0" +ColorTypes = "0.7.3, 0.8" ColorVectorSpace = "0.2, 0.3, 0.4, 0.5, 0.6, 0.7" Colors = "0.7, 0.8, 0.9" -ColorTypes = "0.7.3, 0.8" CoordinateTransformations = "0.5" FixedPointNumbers = "0.5, 0.6" +IdentityRanges = "0.3" ImageCore = "0.7, 0.8" Interpolations = "0.9, 0.10, 0.11, 0.12" -IdentityRanges = "0.3" OffsetArrays = "0.10, 0.11" StaticArrays = "0.10, 0.11, 0.12" julia = "1" diff --git a/src/warp.jl b/src/warp.jl index 3183aab..35d647f 100644 --- a/src/warp.jl +++ b/src/warp.jl @@ -112,7 +112,7 @@ By default, rotated image `imgr` will not be cropped. Bilinear interpolation wil ```julia julia> img = testimage("cameraman") -# rotate with bilinear interpolation but without cropping +# rotate with bilinear interpolation but without cropping julia> imrotate(img, π/4) # rotate with bilinear interpolation and with cropping @@ -124,8 +124,28 @@ julia> imrotate(img, π/4, Constant()) See also [`warp`](@ref). """ -function imrotate(img::AbstractArray{T}, θ::Real, args...) where T - θ = floor(mod(θ,2pi)*typemax(Int16))/typemax(Int16) # periodic discretezation - tform = recenter(RotMatrix{2}(θ), center(img)) - warp(img, tform, args...) +function imrotate(img::AbstractMatrix{T}, θ::Real, args...) where T + # 1. discretize periodic for numerical stability to make sure + # imrotate(img, θ+2pi) == imrotate(img, θ) + # 2. typemax(Int16) is 32767, we choose 32760 to make sure the + # discretization result of pi/2 is exactly pi/2 (or 90°) + max_num_angles = 32760 + θ = round(Int, 180*floor(mod(θ, 2pi)/pi*max_num_angles)/max_num_angles) + tform = recenter(RotMatrix{2}(θ/180*pi), center(img)) + if θ == 0 + return img + elseif θ == 90 + idx = StepRange.(axes(img)) + perm_img = PermutedDimsArray(img, (2, 1)) + return view(perm_img, idx[2], reverse(idx[1])) + elseif θ == 180 + idx = map(i->1:1:length(i), axes(img)) + return view(img, reverse(idx[1]), reverse(idx[2])) + elseif θ == 270 + idx = StepRange.(axes(img)) + perm_img = PermutedDimsArray(img, (2, 1)) + return view(perm_img, reverse(idx[2]), idx[1]) + else + return warp(img, tform, args...) + end end diff --git a/test/warp.jl b/test/warp.jl index 162d46e..a4e0420 100644 --- a/test/warp.jl +++ b/test/warp.jl @@ -427,15 +427,26 @@ ref_img_pyramid_grid = Float64[ @test_nowarn imrotate(img, π/4, Linear()) @test_nowarn imrotate(img, π/4, axes(img)) @test_nowarn imrotate(img, π/4, axes(img), Constant()) - @test isequal(channelview(imrotate(img,π/4)), channelview(imrotate(img, π/4, Linear()))) # TODO: if we remove channelview the test will break for Float + @test isequal(channelview(imrotate(img, π/4)), channelview(imrotate(img, π/4, Linear()))) # NaN != NaN end + + # check special rotation degrees + img = rand(Gray{N0f8}, 100, 50) + @test size(imrotate(img, pi/2)) == (50, 100) + @test size(imrotate(img, pi)) == (100, 50) + @test size(imrotate(img, 3pi/2)) == (50, 100) + @test size(imrotate(img, 2pi)) == (100, 50) + rotated_img = imrotate(imrotate(imrotate(imrotate(img, pi/2), pi/2), pi/2), pi/2) + @test rotated_img == img + rotated_img = imrotate(imrotate(img, pi), pi) + @test rotated_img == img end @testset "numerical" begin for T in test_types img = Gray{T}.(graybar) for θ in range(0,stop=2π,length = 100) - @test isequal(channelview(imrotate(img,θ)), channelview(imrotate(img,θ+2π))) # TODO: if we remove channelview the test will break for Float + @test isequal(channelview(imrotate(img,θ)), channelview(imrotate(img,θ+2π))) # NaN != NaN end end end