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

Streamline API for warp and WarpedView #24

Merged
merged 18 commits into from
May 4, 2017
Merged
Show file tree
Hide file tree
Changes from 9 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
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ notifications:
# uncomment the following lines to override the default test script
script:
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
- julia -e 'Pkg.clone(pwd()); Pkg.build("ImageTransformations"); Pkg.test("ImageTransformations"; coverage=VERSION >= v"0.6.0-alpha")'
- julia --color=yes -e 'Pkg.clone(pwd()); Pkg.build("ImageTransformations"); Pkg.test("ImageTransformations"; coverage=VERSION >= v"0.6.0-alpha")'
after_success:
# push coverage results to Codecov
- julia -e 'if VERSION >= v"0.6.0-alpha" cd(Pkg.dir("ImageTransformations")); Pkg.add("Coverage"); using Coverage; Codecov.submit(Codecov.process_folder()); end'
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ build_script:
Pkg.clone(pwd(), \"ImageTransformations\"); Pkg.build(\"ImageTransformations\")"

test_script:
- C:\projects\julia\bin\julia -e "Pkg.test(\"ImageTransformations\")"
- C:\projects\julia\bin\julia --color=yes -e "Pkg.test(\"ImageTransformations\")"
8 changes: 6 additions & 2 deletions src/ImageTransformations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ export
imresize,
center,
warp,
WarpedView
InvWarpedView,
invwarpedview

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

@inline _getindex(A, v::StaticVector) = A[convert(Tuple, v)...]

center{T,N}(img::AbstractArray{T,N}) = SVector{N}(map(_center, indices(img)))
_center(ind::AbstractUnitRange) = (first(ind)+last(ind))/2
Expand Down
49 changes: 49 additions & 0 deletions src/interpolations.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# FIXME: upstream https://github.com/JuliaGraphics/ColorVectorSpace.jl/issues/75
@inline _nan(::Type{HSV{Float16}}) = HSV{Float16}(NaN16,NaN16,NaN16)
@inline _nan(::Type{HSV{Float32}}) = HSV{Float32}(NaN32,NaN32,NaN32)
@inline _nan(::Type{HSV{Float64}}) = HSV{Float64}(NaN,NaN,NaN)
@inline _nan{T}(::Type{T}) = nan(T)

# The default values used by extrapolation for off-domain points
@compat const FillType = Union{Number,Colorant,Flat,Periodic,Reflect}
@compat const FloatLike{T<:AbstractFloat} = Union{T,AbstractGray{T}}
@compat const FloatColorant{T<:AbstractFloat} = Colorant{T}
@inline _default_fill{T<:FloatLike}(::Type{T}) = convert(T, NaN)
@inline _default_fill{T<:FloatColorant}(::Type{T}) = _nan(T)
@inline _default_fill{T}(::Type{T}) = zero(T)

box_extrapolation(etp::AbstractExtrapolation) = etp
Copy link
Member

Choose a reason for hiding this comment

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

What does the box_ signify?

Copy link
Member Author

Choose a reason for hiding this comment

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

Just that the image is "boxed" or "decorated" by an extrapolation. It's supposed to signify that this will be a view.

Can you think of a better name?

Copy link
Member

@timholy timholy May 4, 2017

Choose a reason for hiding this comment

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

I was tempted by wrap_extrapolation, but I think that could be confusing: people might think it's a typo of warp! So let's stay with box. (EDIT: originally I had a typo in the word typo. Amusing!)


function box_extrapolation{T}(itp::AbstractInterpolation{T}, fill::FillType = _default_fill(T))
etp = extrapolate(itp, fill)
box_extrapolation(etp)
end

function box_extrapolation{T,N,D<:Union{Linear,Constant}}(parent::AbstractArray{T,N}, degree::D = Linear(), args...)
itp = Interpolations.BSplineInterpolation{T,N,typeof(parent),BSpline{D},OnGrid,0}(parent)
box_extrapolation(itp, args...)
end

function box_extrapolation(parent::AbstractArray, fill::FillType)
box_extrapolation(parent, Linear(), fill)
end

function box_extrapolation(itp::AbstractInterpolation, degree::Union{Linear,Constant}, args...)
throw(ArgumentError("Boxing an interpolation in another interpolation is discouraged. Did you specify the parameter \"$degree\" on purpose?"))
Copy link
Member

Choose a reason for hiding this comment

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

A very nice touch. 👍

end

function box_extrapolation(itp::AbstractExtrapolation, fill::FillType)
throw(ArgumentError("Boxing an extrapolation in another extrapolation is discouraged. Did you specify the parameter \"$fill\" on purpose?"))
end

# This is type-piracy, but necessary if we want Interpolations to be
# independent of OffsetArrays.
function AxisAlgorithms.A_ldiv_B_md!(dest::OffsetArray, F, src::OffsetArray, dim::Integer, b::AbstractVector)
indsdim = indices(parent(src), dim)
indsF = indices(F)[2]
if indsF == indsdim
AxisAlgorithms.A_ldiv_B_md!(parent(dest), F, parent(src), dim, b)
return dest
end
throw(DimensionMismatch("indices $(indices(parent(src))) do not match $(indices(F))"))
end
74 changes: 74 additions & 0 deletions src/invwarpedview.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
immutable InvWarpedView{T,N,A<:AbstractArray,F1<:Transformation,I,F2<:Transformation,E<:AbstractExtrapolation} <: AbstractArray{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.

At some point we might want to define a WarpedView and implement InvWarpedView as a wrapper type around it (one that provides extra functionality), but for now this seems fine.

Copy link
Member Author

Choose a reason for hiding this comment

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

good idea. I will think on that

parent::A
transform::F1
indices::I
inverse::F2
extrapolation::E

function (::Type{InvWarpedView{T,N,TA,F,I}}){T,N,TA<:AbstractArray,F<:Transformation,I<:Tuple}(
parent::TA,
tform::F,
indices::I)
@assert eltype(parent) == T
etp = box_extrapolation(parent)
tinv = inv(tform)
new{T,N,TA,F,I,typeof(tinv),typeof(etp)}(parent, tform, indices, tinv, etp)
end
end

function InvWarpedView(inner::InvWarpedView, outer_tform::Transformation)
tform = compose(outer_tform, inner.transform)
A = parent(inner)
inds = autorange(A, tform)
InvWarpedView(A, tform, inds)
end

function InvWarpedView{T,N,F<:Transformation,I<:Tuple}(
A::AbstractArray{T,N},
tform::F,
inds::I = autorange(A, tform))
InvWarpedView{T,N,typeof(A),F,I}(A, tform, inds)
end

Base.parent(A::InvWarpedView) = A.parent
@inline Base.indices(A::InvWarpedView) = A.indices

@compat Compat.IndexStyle{T<:InvWarpedView}(::Type{T}) = IndexCartesian()
@inline Base.getindex{T,N}(A::InvWarpedView{T,N}, I::Vararg{Int,N}) =
_getindex(A.extrapolation, A.inverse(SVector(I)))

Base.size(A::InvWarpedView) = OffsetArrays.errmsg(A)
Base.size(A::InvWarpedView, d) = OffsetArrays.errmsg(A)

function ShowItLikeYouBuildIt.showarg(io::IO, A::InvWarpedView)
print(io, "InvWarpedView(")
showarg(io, parent(A))
print(io, ", ")
print(io, A.transform)
print(io, ')')
end

Base.summary(A::InvWarpedView) = summary_build(A)

"""
TODO
Copy link
Member

Choose a reason for hiding this comment

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

Obviously, this will be important to get right.

"""
@inline invwarpedview(A::AbstractArray, tform::Transformation, args...) =
InvWarpedView(A, tform, args...)

function invwarpedview{T}(
A::AbstractArray{T},
tform::Transformation,
degree::Union{Linear,Constant},
fill::FillType = _default_fill(T),
args...)
invwarpedview(box_extrapolation(A, degree, fill), tform, args...)
end

function invwarpedview(
A::AbstractArray,
tform::Transformation,
fill::FillType,
args...)
invwarpedview(A, tform, Linear(), fill, args...)
end
185 changes: 129 additions & 56 deletions src/warp.jl
Original file line number Diff line number Diff line change
@@ -1,51 +1,35 @@
# None of these types are provided by this package, so the following line seems unclean
# @inline Base.getindex(A::AbstractExtrapolation, v::StaticVector) = A[convert(Tuple, v)...]
# furthermore it would be ambiguous with getindex(::Extrapolation, xs...) after https://github.com/tlycken/Interpolations.jl/pull/141
@inline _getindex(A, v::StaticVector) = A[convert(Tuple, v)...]

warp(img::AbstractArray, args...) = warp(interpolate(img, BSpline(Linear()), OnGrid()), args...)

@inline _dst_type{T<:Colorant,S}(::Type{T}, ::Type{S}) = ccolor(T, S)
@inline _dst_type{T<:Number,S}(::Type{T}, ::Type{S}) = T

function warp{T,S}(::Type{T}, img::AbstractArray{S}, args...)
TCol = _dst_type(T,S)
TNorm = eltype(TCol)
apad, pad = Interpolations.prefilter(TNorm, TCol, img, typeof(BSpline(Linear())), typeof(OnGrid()))
itp = Interpolations.BSplineInterpolation(TNorm, apad, BSpline(Linear()), OnGrid(), pad)
warp(itp, args...)
end

@compat const FloatLike{T<:AbstractFloat} = Union{T,AbstractGray{T}}
@compat const FloatColorant{T<:AbstractFloat} = Colorant{T}

# The default values used by extrapolation for off-domain points
@inline _default_fill{T<:FloatLike}(::Type{T}) = convert(T, NaN)
@inline _default_fill{T<:FloatColorant}(::Type{T}) = nan(T)
@inline _default_fill{T}(::Type{T}) = zero(T)

warp{T}(img::AbstractInterpolation{T}, tform, fill=_default_fill(T)) = warp(extrapolate(img, fill), tform)

"""
warp(img, tform) -> imgw
warp(img, tform, [indices], [degree = Linear()], [fill = NaN]) -> imgw

Transform the coordinates of `img`, returning a new `imgw` satisfying
`imgw[x] = img[tform(x)]`. `tform` should be defined using
CoordinateTransformations.jl.
`imgw[I] = img[tform(I)]`. This approach is known as backward
mode warping. The transformation `tform` should be defined using
[CoordinateTransformations.jl](https://github.com/FugroRoames/CoordinateTransformations.jl).
Copy link
Member

Choose a reason for hiding this comment

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

Not strictly required that it come from CoordinateTransformations, is it? Anything that can take an SVector input should work?

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'll rewrite it to be more neutral


# Interpolation scheme

At off-grid points, `imgw` is calculated by interpolation. The default
is linear interpolation, used when `img` is a plain array, and `NaN`
values are used to indicate locations for which `tform(x)` was outside
the bounds of the input `img`. For more control over the interpolation
scheme---and how beyond-the-edge points are handled---pass it in as an
`AbstractExtrapolation` from Interpolations.jl.
At off-grid points, `imgw` is calculated by interpolation. The
Copy link
Member

@c42f c42f Apr 27, 2017

Choose a reason for hiding this comment

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

I suggest abandoning the idea that "on grid points" [edit: or, conversely off-grid points] are particularly meaningful. This is only an artifact of certain interpolation schemes.

From a camera, a pixel value is not a gridded point sample of some underlying continuous image. (Neither is it a little square "pixel box" we've all grown so used to looking at - pixel boxes are just the zeroth order b-spline interpolant.) In reality, the image is the incoming light field convolved with the point spread function of the optics, and integrated over the physical CCD/cmos sensor pixel. The pixels are then a discrete low dimensional representation of the underlying light field.

All of which is to say... I'm not sure what! Perhaps just that we shouldn't confuse the standard (ie, cheap) interpolation schemes with the interpretation of the pixels values.

Copy link
Member Author

Choose a reason for hiding this comment

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

Alternatives:

  • "At off-index points ..."

  • "At points between indices ..."

Other ideas?

Copy link
Member

Choose a reason for hiding this comment

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

It sounds like I might be missing something about the way images are treated in this package, or about the way that interpolations works in conjunction with this.

Normally (ie, in all other software packages I know about), cubic spline image resampling technically gives an approximating spline rather than an interpolating one. Is that not the case here?

Copy link
Member

Choose a reason for hiding this comment

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

For quadratic and cubic, Interpolations solves a tridiagonal/Woodbury system for the coefficients to ensure that they are interpolating.

Copy link
Member

Choose a reason for hiding this comment

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

To address @c42f's point more completely, yes, agreed 100% with your physical model of the process. The issue is that without more information there isn't, to my knowledge, a great way to do a lot better than interpolation. So personally I'm fine with talking about a "grid," though I'd also be fine with a slightly more verbose statement "if tform(I) does not have integer coordinate values, the image value is computed by interpolation."

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for clearing that up. I'm also not sure what more should be done, other than some kind of interpolating or approximating spline. (Or at least, that anything else which could be done is far too fancy to be done behind the user's back.) This makes more sense of @Evizero's worry about using higher-than-linear orders inside warp.

I must admit it's not quite clear to me that an interpolating spline is the right choice in general over the much simpler and - at least somewhat - more efficient approximating splines for image resampling. In the alternative kernel-based view of reconstructing a continuous image from discrete pixel values, there's a convenient family of bicubic kernels parameterizing the apparent sharpness of the resulting reconstruction. This formulation makes the reconstruction entirely spatially local, and there's no need to run any kind of solver before sampling in an arbitrary location.

Perhaps all I'm quibbling about is that the docs give the idea that the computation is different on vs off the integer lattice. It shouldn't be, other than as an optimization in the case that the reconstruction is done with an actual interpolating spline.

Copy link
Member

Choose a reason for hiding this comment

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

there's a convenient family of bicubic kernels parameterizing the apparent sharpness of the resulting reconstruction

@c42f, a reference would be awesome.

the docs give the idea that the computation is different on vs off the integer lattice. It shouldn't be

That's absolutely right. Do you have a good way to phrase this? I wondered if that's what you were worried about, but I couldn't quickly come up with a concise way of expressing that.

Copy link
Member

Choose a reason for hiding this comment

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

It's mentioned in passing on the wikipedia page about bicubic interpolation, but it's not great. I finally found a better reference in some old code of mine:

Filter kernels for bicubic reconstruction filters are discussed in detail in
"Reconstruction filters in computer graphics", D. Mitchell and A. Netravali,
Computer Graphics, vol. 22 no. 4, 1988.

Mitchell and Netravali identify two desirable properties for signal
reconstruction:

  • We want the image and its first derivative to be continuous
  • We want constant areas of an image to stay constant after interpolation

This narrows down the possible filters to a two parameter space; parameters
B and C in the paper. In addition, restricting to the line 2C + B = 1
ensures quadratic convergence as the pixel size is reduced; we use a
qualitative parameter "sharpness = 1-B" below.

For a reasonably sharp filter, try sharpness = 1; this is the classic
Catmull-Rom spline. For an image sharpness enhancing filter, try
sharpness = 2.

A somewhat blurry result (but without any ringing) will be obtained using
the cubic B-spline, which corresponds to sharpness = 0.

The code itself was just an implementation of the piecewise formulae from the paper.

Copy link
Member

Choose a reason for hiding this comment

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

Do you have a good way to phrase this

Not exactly :-) How about

# Reconstruction scheme

During warping, values for `img` must be reconstructed at arbitrary
locations `tform(I)` which do not lie on to the lattice of pixels. How
this reconstruction is done depends on the type of `img` and the
optional parameter `degree`.

When `img` is a plain array, b-spline interpolation is used with
`degree=Linear()` for linear interpolation or `degree=Constant()` for
nearest neighbor interpolation.  In the case ...

Obviously I'm trying to avoid the word "interpolation", as there's quite reasonable non-interpolating reconstructions.

Copy link
Member

Choose a reason for hiding this comment

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

Musing a bit more about reconstructions and the desiderata from the Mitchell paper -

  • The Catmull-Rom kernel referred to above gives an interpolating spline, IIRC. Other values of sharpness give approximating splines.
  • Natural images are composed of discontinuous geometric edges and smooth lighting variations. Reconstructing such an image with a smooth spline is practical and looks reasonably good, but can't ever be quite the right thing for the non-smooth parts of an image. (Ultimately I guess the "right thing" is something more like a statistical model such as https://arxiv.org/abs/1702.00783 :-) Not that I'm remotely suggesting doing this, but it's interesting to have in mind as a point of comparison with what we're actually doing.)

degree of the b-spline can be specified with the optional
parameter `degree`, which can take the values `Linear()` or
`Constant()`.
Copy link
Member

Choose a reason for hiding this comment

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

This raises an interesting issue: since warp doesn't have a "view" pledge to keep, could it support Quadratic or Cubic directly?

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 was aiming for for consistency with the view.

Regardless of consistency, I do think that we shouldn't hide such allocating actions from a user. It should be a choice. It isn't that much more effort to pass interpolate(img, ...) to the constructor.

The options presented are special in that they are quite conservative in the memory allocation regard.

Copy link
Member

Choose a reason for hiding this comment

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

I agree with Tim here, I think it would be very natural to support other interpolation types. (Caveat: I don't think I currently have a good overview of what you're building in this package as a whole.)

Copy link
Member

Choose a reason for hiding this comment

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

Having had the long discussion about "off-grid points" I think I now understand the context for your reservations about higher orders, and the view pledge which Tim is referring to.

But I also think we could do a cubic spline view just fine, as long as we take a convolution kernel-style viewpoint of the reconstruction (interpolation/approximation) process: in this variant, there's no matrix solver to run after modifying the underlying image, you only need the pixel values from a local 4x4 image patch around the sample point, and the fractional coordinates of the sample point away from the integer lattice.

Copy link
Member

Choose a reason for hiding this comment

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

Sure. But we don't have that implemented yet, so let's finish this so that it works for what we have.

I can go either way (restrict to Constant and Linear, or support higher orders).

Copy link
Member

Choose a reason for hiding this comment

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

Sure, there's lots of great stuff here, I don't mean to hold up getting it merged.


The b-spline interpolation is used when `img` is a plain array
and `img[tform(I)]` is inbound. In the case `tform(I)` maps
to indices outside the original `img`, the value or extrapolation
Copy link
Member

Choose a reason for hiding this comment

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

might be clearer to say "those locations are set to a value fill (which defaults...)" and delete the material after the closing parenthesis.

scheme denoted by the optional parameter `fill` (which defaults
to `NaN` if the element type supports it, and `0` otherwise) is
used to indicate locations for which `tform(I)` was outside the
bounds of the input `img`.

For more control over the interpolation scheme --- and how
beyond-the-edge points are handled --- pass it in as an
Copy link
Member

Choose a reason for hiding this comment

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

Maybe "pass img in as..."?

`AbstractInterpolation` or `AbstractExtrapolation` from
[Interpolations.jl](https://github.com/JuliaMath/Interpolations.jl).
Copy link
Member

@c42f c42f Apr 27, 2017

Choose a reason for hiding this comment

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

It's interesting how these packages fit together.

It might be worth noting that interpolations isn't the only piece of the puzzle regarding image sampling. In particular, it's never going to deal with proper antialiasing which can be quite important when the warping is severe or you're downsampling by a large factor.

For severe warping and large downsampling things can get quite interesting, as you need full anisotropic filtering, usually coupled with image pyramiding to make it efficient. Arguably, warp could do these things with appropriate options turned on.

Copy link
Member

Choose a reason for hiding this comment

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

(Obviously, this is a side comment - nothing to do with any changes you should be making here in this PR.)

Copy link
Member Author

Choose a reason for hiding this comment

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

We currently have the same kind of problem with imresize

see #7 (comment)


# The meaning of the coordinates

The output array `imgw` has indices that would result from applying
`tform` to the indices of `img`. This can be very handy for keeping
`inv(tform)` to the indices of `img`. This can be very handy for keeping
track of how pixels in `imgw` line up with pixels in `img`.

If you just want a plain array, you can "strip" the custom indices
Expand All @@ -65,7 +49,7 @@ julia> indices(img)
julia> tfm = recenter(RotMatrix(pi/4), center(img))
AffineMap([0.707107 -0.707107; 0.707107 0.707107], [347.01,-68.7554])

julia> imgw = warp(img, tfm);
julia> imgw = warp(img, inv(tfm));
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be better to pass tfm directly. You could change the angle to -pi/4 above.


julia> indices(imgw)
(-196:709,-68:837)
Expand All @@ -76,7 +60,7 @@ julia> img0 = OffsetArray(img, -30:481, -384:383); # origin near top of image
julia> rot = LinearMap(RotMatrix(pi/4))
LinearMap([0.707107 -0.707107; 0.707107 0.707107])

julia> imgw = warp(img0, rot);
julia> imgw = warp(img0, inv(rot));

julia> indices(imgw)
(-293:612,-293:611)
Expand All @@ -87,28 +71,117 @@ julia> indices(imgr)
(Base.OneTo(906),Base.OneTo(905))
```
"""
function warp(img::AbstractExtrapolation, tform)
inds = autorange(img, tform)
out = OffsetArray(Array{eltype(img)}(map(length, inds)), inds)
function warp_new{T}(img::AbstractExtrapolation{T}, tform, inds::Tuple = autorange(img, inv(tform)))
Copy link
Member

Choose a reason for hiding this comment

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

Someday this will have to become tform::InvertibleTransformation or similar.

Copy link
Member

Choose a reason for hiding this comment

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

Presumably invertibility is more of a trait? I can't see it fitting neatly into the type hierarchy.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, yes, agreed.

out = OffsetArray(Array{T}(map(length, inds)), inds)
warp!(out, img, tform)
end

# this function was never exported, so no need to deprecate
Copy link
Member

Choose a reason for hiding this comment

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

Agreed.

function warp!(out, img::AbstractExtrapolation, tform)
tinv = inv(tform)
for I in CartesianRange(indices(out))
out[I] = _getindex(img, tinv(SVector(I.I)))
@inbounds for I in CartesianRange(indices(out))
out[I] = _getindex(img, tform(SVector(I.I)))
end
out
end

# This is type-piracy, but necessary if we want Interpolations to be
# independent of OffsetArrays.
function AxisAlgorithms.A_ldiv_B_md!(dest::OffsetArray, F, src::OffsetArray, dim::Integer, b::AbstractVector)
indsdim = indices(parent(src), dim)
indsF = indices(F)[2]
if indsF == indsdim
AxisAlgorithms.A_ldiv_B_md!(parent(dest), F, parent(src), dim, b)
return dest
end
throw(DimensionMismatch("indices $(indices(parent(src))) do not match $(indices(F))"))
function warp_new(img::AbstractArray, tform, inds::Tuple, args...)
etp = box_extrapolation(img, args...)
warp_new(etp, tform, inds)
end

function warp_new(img::AbstractArray, tform, args...)
etp = box_extrapolation(img, args...)
warp_new(etp, tform)
end

# # after deprecation period:
# @deprecate warp_new(img::AbstractArray, tform, args...) warp(img, tform, args...)
# @deprecate warp_old(img::AbstractArray, tform, args...) warp(img, tform, args...)

"""
warp(img, tform, [indices], [degree = Linear()], [fill = NaN]) -> imgw

!!! warning "Deprecation Warning!"

This method with the signature `warp(img, tform, args...)` is
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure we need to document the warp_old in as much detail, perhaps only enough to get people to switch:

    `warp` is transitioning to a different interpretation of the transformation, and you are using
    the old version.
    Please see https://github.com/JuliaImages/ImageTransformations.jl for information about
    how to switch to the new version.

Or we can specify how one switches in the docstring (e.g., to help people who run into this at a time when they don't happen to have internet access, e.g., while flying).

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point

deprecated in favour of the cleaner interpretation `warp(img,
inv(tform), args...)`. Set `const warp =
ImageTransformations.warp_new` right after package import to
change to the new behaviour right away.

Transform the coordinates of `img`, returning a new `imgw`
satisfying `imgw[I] = img[inv(tform(I))]`. This approach is known
as backward mode warping. The transformation `tform` should be
defined using
[CoordinateTransformations.jl](https://github.com/FugroRoames/CoordinateTransformations.jl).

# Interpolation scheme

At off-grid points, `imgw` is calculated by interpolation. The
degree of the b-spline can be specified with the optional
parameter `degree`, which can take the values `Linear()` or
`Constant()`.

The b-spline interpolation is used when `img` is a plain array
and `img[inv(tform(I))]` is inbound. In the case `inv(tform(I))`
maps to indices outside the original `img`, the value or
extrapolation scheme denoted by the optional parameter `fill`
(which defaults to `NaN` if the element type supports it, and `0`
otherwise) is used to indicate locations for which
`inv(tform(I))` was outside the bounds of the input `img`.

For more control over the interpolation scheme --- and how
beyond-the-edge points are handled --- pass it in as an
`AbstractInterpolation` or `AbstractExtrapolation` from
[Interpolations.jl](https://github.com/JuliaMath/Interpolations.jl).

# The meaning of the coordinates

The output array `imgw` has indices that would result from applying
`tform` to the indices of `img`. This can be very handy for keeping
track of how pixels in `imgw` line up with pixels in `img`.

If you just want a plain array, you can "strip" the custom indices
with `parent(imgw)`.

# Examples: a 2d rotation (see JuliaImages documentation for pictures)

```jldoctest
julia> using Images, CoordinateTransformations, TestImages, OffsetArrays

julia> img = testimage("lighthouse");

julia> indices(img)
(Base.OneTo(512),Base.OneTo(768))

# Rotate around the center of `img`
julia> tfm = recenter(RotMatrix(pi/4), center(img))
AffineMap([0.707107 -0.707107; 0.707107 0.707107], [347.01,-68.7554])

julia> imgw = warp(img, tfm);

julia> indices(imgw)
(-196:709,-68:837)

# Alternatively, specify the origin in the image itself
julia> img0 = OffsetArray(img, -30:481, -384:383); # origin near top of image

julia> rot = LinearMap(RotMatrix(pi/4))
LinearMap([0.707107 -0.707107; 0.707107 0.707107])

julia> imgw = warp(img0, rot);

julia> indices(imgw)
(-293:612,-293:611)

julia> imgr = parent(imgw);

julia> indices(imgr)
(Base.OneTo(906),Base.OneTo(905))
```
"""
@generated function warp_old(img::AbstractArray, tform, args...)
Copy link
Member

Choose a reason for hiding this comment

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

Does this need to be a @generated function? Can't it just call warp_new directly?

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 reason for @generated is to only present the warning once.

warn("'warp(img, tform)' is deprecated in favour of the cleaner interpretation 'warp(img, inv(tform))'. Set 'const warp = ImageTransformations.warp_new' right after package import to change to the new behaviour right away.")
Copy link
Member

Choose a reason for hiding this comment

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

Shall we use the Base.depwarn mechanism? And perhaps rather than call it "cleaner" (which is a value judgement), just refer people to the docstring and/or the web site?

The web site can provide more justification, e.g., like my post in #25 (comment) except probably using embedded pictures.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure what you are referring to with the comment on embedded pictures.

:(warp_new(img, inv(tform), args...))
end
const warp = warp_old
Loading