Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] fixed point perspective for imresize, zoom and imrotate #142

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/ImageTransformations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ export
warpedview,
InvWarpedView,
invwarpedview,
imrotate
imrotate,
zoom

include("autorange.jl")
include("interpolations.jl")
include("warp.jl")
include("warpedview.jl")
include("invwarpedview.jl")
include("resizing.jl")
include("zoom.jl")
include("compat.jl")
include("deprecated.jl")

Expand Down
58 changes: 51 additions & 7 deletions src/resizing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,23 @@ function restrict(img::Union{WarpedView, InvWarpedView}, dims::Integer)
restrict(OffsetArray(collect(img), axes(img)), dims)
end

# imresize
###########
# imresize/imresize!
#
# `imresize` API:
# - `imresize(original; ratio, [method])`
# - `imresize(original, inds; [method])`
# - `imresize(original, sz; [method])`
# The output type:
# - If `inds` method get triggered, the output will unconditionally be `OffsetArray` type.
# - Otherwise, it should preserve the original input array type.
#
# `imresize!` API:
# - `imresize!(resized, original::AbstractArray; [method])`
# - `imresize!(resized, original::AbstractInterpolation)`
###########


imresize(original::AbstractArray, dim1::T, dimN::T...; kwargs...) where T<:Union{Integer,AbstractUnitRange} = imresize(original, (dim1,dimN...); kwargs...)
function imresize(original::AbstractArray; ratio, kwargs...)
all(ratio .> 0) || throw(ArgumentError("ratio $ratio should be positive"))
Expand All @@ -26,16 +42,18 @@ function imresize(original::AbstractArray, itp::Union{Interpolations.Degree,Inte
end

odims(original, i, short_size::Tuple{Integer,Vararg{Integer}}) = size(original, i)
odims(original, i, short_size::Tuple{}) = axes(original, i)
odims(original, i, short_size::Tuple{}) = size(original, i)
odims(original, i, short_size) = oftype(first(short_size), axes(original, i))

"""
imresize(img, sz; [method]) -> imgr
imresize(img, inds; [method]) -> imgr
imresize(img, inds; [method]) -> imgr::OffsetArray
imresize(img; ratio, [method]) -> imgr

upsample/downsample the image `img` to a given size `sz` or axes `inds` using interpolations. If
`ratio` is provided, the output size is then `ceil(Int, size(img).*ratio)`.
upsample/downsample the image `img` to a given size `sz` or axes `inds` using interpolations.

If `ratio` is provided, the output size is then `ceil(Int, size(img).*ratio)`. If axes information is
provided by passing `inds`, then the output array is `OffsetArray`.

!!! tip
This interpolates the values at sub-pixel locations. If you are shrinking the image, you risk
Expand Down Expand Up @@ -104,19 +122,42 @@ function imresize(original::AbstractArray{T,N}, new_size::Dims{N}; kwargs...) wh
if axes(dest) == inds
copyto!(dest, original)
else
# Non 1-based case as a fallback solution
# The OffsetArray case is specially handled to also output OffsetArray
copyto!(dest, CartesianIndices(axes(dest)), original, CartesianIndices(inds))
end
else
imresize!(similar(original, Tnew, new_size), original; kwargs...)
end
end

function imresize(original::OffsetArray{T,N}, new_size::Dims{N}; kwargs...) where {T,N}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OffsetArray is not necessarily the only array type that doesn't have indexing starting with 1; there are several other examples. What's the reason for the special method? It looks almost identical to the above.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only difference here is that the above method returns an Array while this new method for OffsetArray unconditionally returns an OffsetArray.

I also have the same concerns and I'm not very satisfied with this version very much, but it seems there's no other way to work around the type instability if we put them into one method that returns either OffsetArray or Array.

Tnew = imresize_type(first(original))
inds = axes(original)
new_inds = map((ax, n)->first(ax):first(ax)+n-1, axes(original), new_size)
if map(length, inds) == new_size
dest = similar(original, Tnew, new_inds)
if axes(dest) == new_inds
# a trivial case of OffsetArray
copyto!(parent(dest), original)
else
copyto!(dest, CartesianIndices(axes(dest)), original, CartesianIndices(inds))
end
else
dest = imresize(original, new_inds; kwargs...)
end
return dest
end

function imresize(original::AbstractArray{T,N}, new_inds::Indices{N}; kwargs...) where {T,N}
Tnew = imresize_type(first(original))
# The pirated `similar` method from OffsetArrays will be triggered
# thus for type stability, we unconditionally output an `OffsetArray`.
origin = OffsetArrays.Origin(map(first, new_inds))
if axes(original) == new_inds
copyto!(similar(original, Tnew), original)
OffsetArray(copyto!(similar(original, Tnew), original), origin)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than manually create an OffsetArray, how about use similar(original, Tnew, new_inds)? That will create an OffsetArray if new_inds[i] isa UnitRange rather than a Base.OneTo.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this won't be type stable due to the if axes(original) == new_inds branch.

With this change:

-         OffsetArray(copyto!(similar(original, Tnew), original), origin)
+         copyto!(similar(original, Tnew, new_inds), original)
julia> img = testimage("cameraman");

julia> @inferred imresize(img, axes(img))
ERROR: return type Matrix{Gray{N0f8}} does not match inferred return type Union{Matrix{Gray{N0f8}}, OffsetMatrix{Gray{N0f8}, Matrix{Gray{N0f8}}}}

else
imresize!(similar(original, Tnew, new_inds), original; kwargs...)
OffsetArray(imresize!(similar(original, Tnew, new_inds), original; kwargs...), origin)
end
end

Expand All @@ -140,6 +181,9 @@ function imresize!(resized::AbstractArray{T,N}, original::AbstractArray{S,N}; me
imresize!(resized, itp)
end

# If we use the `warp` API then we need to build the backward coordinate map function
# as a closure, which would unavoidably introduce the overhead. (Although Julia compiler
# might optimize it away)
function imresize!(resized::AbstractArray{T,N}, original::AbstractInterpolation{S,N}) where {T,S,N}
# Define the equivalent of an affine transformation for mapping
# locations in `resized` to the corresponding position in
Expand Down
36 changes: 36 additions & 0 deletions src/zoom.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
zoom(img; ratio, [fixed_point], kwargs...)

Zoom in/out.
"""
function zoom(img; ratio, kwargs...)
all(ratio .> 0) || throw(ArgumentError("ratio $ratio should be positive"))
new_size = ceil.(Int, size(img) .* ratio) # use ceil to avoid 0
_zoom(img, new_size; kwargs...)
end

# TODO:
# before we make this a part of API, we need to figure out how should we interpret
# the axes information.
function _zoom(img, size_or_axes; fixed_point=OffsetArrays.center(img), kwargs...)
# zoom introduces out-of-domain points, so we need to build extrapolation

# Because `first(CartesianIndices(R)) == oneunit(first(R))`, we need to
# preserve the axes information if `size_or_axes` is actually an CartesianIndices `R`
Rdst = if size_or_axes isa AbstractArray{<:CartesianIndex}
size_or_axes
else
CartesianIndices(size_or_axes)
end
Rsrc = CartesianIndices(img)
tform = zoom_coordinate_map(Rdst, Rsrc, fixed_point)
@assert tform(SVector(fixed_point)) == SVector(fixed_point)

warp(img, tform, Rsrc.indices)
end

function zoom_coordinate_map(Rdst, Rsrc, c)
k = SVector((size(Rsrc) .- 1) ./(size(Rdst) .- 1))
b = @. (1-k)*c
return x->@. k*x + b
end
79 changes: 57 additions & 22 deletions test/resizing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,62 @@ end
testtype = (Float32, Float64, N0f8, N0f16)

@testset "Interface" begin
function test_imresize_interface(img, outsz, args...; kargs...)
img2 = @test_broken @inferred imresize(img, args...; kargs...) # FIXME: @inferred failed
img2 = @test_nowarn imresize(img, args...; kargs...)
@test size(img2) == outsz
@test eltype(img2) == eltype(img)
function test_imresize_interface(src_img, outsz, outinds, args...; kwargs...)
out = @inferred imresize(src_img, args...; kwargs...)
@test size(out) == outsz
@test axes(out) == outinds
@test eltype(out) == eltype(src_img)
return out
end
for C in testcolor, T in testtype
img = rand(C{T},10,10)

test_imresize_interface(img, (5,5), (5,5))
test_imresize_interface(img, (5,5), (1:5,1:5)) # FIXME: @inferred failed
test_imresize_interface(img, (5,5), 5,5)
test_imresize_interface(img, (5,5), 1:5,1:5) # FIXME: @inferred failed
test_imresize_interface(img, (5,5), ratio = 0.5)
test_imresize_interface(img, (20,20), ratio = 2)
test_imresize_interface(img, (20,20), ratio = (2, 2))
test_imresize_interface(img, (20,10), ratio = (2, 1))
test_imresize_interface(img, (10,10), ())
test_imresize_interface(img, (5,10), 5)
test_imresize_interface(img, (5,10), (5,))
test_imresize_interface(img, (5,10), 1:5) # FIXME: @inferred failed
test_imresize_interface(img, (5,10), (1:5,)) # FIXME: @inferred failed
@test test_imresize_interface(img, (10,10), (1:10, 1:10), ()) isa Array

@test test_imresize_interface(img, (5,5), (1:5, 1:5), (5,5)) isa Array
@test test_imresize_interface(img, (5,5), (1:5, 1:5), 5, 5) isa Array
@test test_imresize_interface(img, (5,10), (1:5, 1:10), 5) isa Array
@test test_imresize_interface(img, (5,10), (1:5, 1:10), (5,)) isa Array

@test test_imresize_interface(img, (5,5), (1:5, 1:5), ratio = 0.5) isa Array
@test test_imresize_interface(img, (20,20), (1:20, 1:20), ratio = 2) isa Array
@test test_imresize_interface(img, (20,20), (1:20, 1:20), ratio = (2, 2)) isa Array
@test test_imresize_interface(img, (20,10), (1:20, 1:10), ratio = (2, 1)) isa Array

# indices method always return OffsetArray
@test test_imresize_interface(img, (5,5), (1:5, 1:5), (1:5,1:5)) isa OffsetArray
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also add

@test test_imresize_interface(img, (5,5), (1:5, 1:5), (Base.OneTo(5),Base.OneTo(5))) isa Array

@test test_imresize_interface(img, (5,5), (1:5, 1:5), 1:5,1:5) isa OffsetArray
@test test_imresize_interface(img, (5,10), (1:5, 1:10), 1:5) isa OffsetArray
@test test_imresize_interface(img, (5,10), (1:5, 1:10), (1:5,)) isa OffsetArray

@test_throws MethodError imresize(img,5.0,5.0)
@test_throws MethodError imresize(img,(5.0,5.0))
@test_throws MethodError imresize(img,(5, 5.0))
@test_throws MethodError imresize(img,[5,5])
@test_throws UndefKeywordError imresize(img)
@test_throws DimensionMismatch imresize(img,(5,5,5))
@test_throws ArgumentError imresize(img, ratio = -0.5)
@test_throws ArgumentError imresize(img, ratio = (-0.5, 1))
@test_throws DimensionMismatch imresize(img, ratio=(5,5,5))
@test_throws DimensionMismatch imresize(img, (5,5,1))
end

for C in testcolor, T in testtype
img = OffsetArray(rand(C{T},10,10), -5, -4)

@test test_imresize_interface(img, (5,5), (-4:0, -3:1), (5,5)) isa OffsetArray
@test test_imresize_interface(img, (5,5), (-4:0, -3:1), 5, 5) isa OffsetArray
@test test_imresize_interface(img, (5,5), (1:5, 1:5), (1:5,1:5)) isa OffsetArray
@test test_imresize_interface(img, (5,5), (1:5, 1:5), 1:5,1:5) isa OffsetArray
@test test_imresize_interface(img, (5,5), (-4:0, -3:1), ratio = 0.5) isa OffsetArray
@test test_imresize_interface(img, (20,20), (-4:15, -3:16), ratio = 2) isa OffsetArray
@test test_imresize_interface(img, (20,20), (-4:15, -3:16), ratio = (2, 2)) isa OffsetArray
@test test_imresize_interface(img, (20,10), (-4:15, -3:6), ratio = (2, 1)) isa OffsetArray
@test test_imresize_interface(img, (10,10), (-4:5, -3:6), ()) isa OffsetArray
@test test_imresize_interface(img, (5,10), (-4:0, -3:6), 5) isa OffsetArray
@test test_imresize_interface(img, (5,10), (-4:0, -3:6), (5,)) isa OffsetArray
@test test_imresize_interface(img, (5,10), (1:5, -3:6), 1:5) isa OffsetArray
@test test_imresize_interface(img, (5,10), (1:5, -3:6), (1:5,)) isa OffsetArray

@test_throws MethodError imresize(img,5.0,5.0)
@test_throws MethodError imresize(img,(5.0,5.0))
Expand Down Expand Up @@ -105,7 +139,7 @@ end
@test !(R === A)
Ao = OffsetArray(A, -2:2, 0:4)
R = imresize(Ao, (5,5))
@test axes(R) === axes(A)
@test axes(R) === axes(Ao)
R = imresize(Ao, axes(Ao))
@test axes(R) === axes(Ao)
@test !(R === A)
Expand Down Expand Up @@ -134,9 +168,10 @@ end
@test_throws ArgumentError imresize(img, Linear())

#consisency checks
@test imresize(img, (128, 128), method=Linear()) == imresize(OffsetArray(img, -1, -1), (128, 128), method=Linear())
@test imresize(img, (128, 128), method=BSpline(Linear())) == imresize(OffsetArray(img, -1, -1), (128, 128), method=BSpline(Linear()))
@test imresize(img, (128, 128), method=Lanczos4OpenCV()) == imresize(OffsetArray(img, -1, -1), (128, 128), method=Lanczos4OpenCV())
imgo = OffsetArray(img, -1, -1)
@test imresize(img, (128, 128), method=Linear()) == OffsetArrays.no_offset_view(imresize(imgo, (128, 128), method=Linear()))
@test imresize(img, (128, 128), method=BSpline(Linear())) == OffsetArrays.no_offset_view(imresize(imgo, (128, 128), method=BSpline(Linear())))
@test imresize(img, (128, 128), method=Lanczos4OpenCV()) == OffsetArrays.no_offset_view(imresize(imgo, (128, 128), method=Lanczos4OpenCV()))

out = imresize(img, (0:127, 0:127), method=Linear())
@test axes(out) == (0:127, 0:127)
Expand Down
6 changes: 4 additions & 2 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using CoordinateTransformations, Rotations, TestImages, ImageCore, StaticArrays, OffsetArrays, Interpolations, LinearAlgebra
using Test, ReferenceTests
using Test
using OffsetArrays: IdentityUnitRange # compat for Julia <1.1

refambs = detect_ambiguities(CoordinateTransformations, Base, Core)
using ImageTransformations
ambs = detect_ambiguities(ImageTransformations, CoordinateTransformations, Base, Core)
@test isempty(setdiff(ambs, refambs))
@test length(setdiff(ambs, refambs)) == 4 # FIXME

using ReferenceTests # this package requires ImageTransformations

# helper function to compare NaN
nearlysame(x, y) = x ≈ y || (isnan(x) & isnan(y))
Expand Down