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

Add sub-pixel corner detection capability #682 #685

Merged
merged 1 commit into from
Dec 17, 2017

Conversation

zygmuntszpak
Copy link
Member

Introduces the function imcorner_subpixel as well as several requisite
support functions to facilitate the ability to detect corners to
sub-pixel precision.

The imcorner_subpixel uses the existing imcorner functionality to obtain
initial corner response values and a binary corner indicator matrix. The
corner response values and the indicator matrix are used in a univariate
quadratic interpolation scheme to determine the corner coordinates to
sub-pixel precision.

The new imcorner_subpixel function returns a length-3 NTuple of type
HomogeneousPoint2D, which represents the homogeneous coordinates of a
planar point.

@@ -18,6 +18,7 @@ using Base: depwarn
using Base.Order: Ordering, ForwardOrdering, ReverseOrdering

using Compat
using StaticArrays
Copy link
Member

Choose a reason for hiding this comment

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

minor comment: this needs a corresponding entry in the REQUIRE file

Copy link
Member Author

@zygmuntszpak zygmuntszpak Dec 2, 2017

Choose a reason for hiding this comment

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

Thanks, I will amend the commit and add the entry in the REQUIRE file. I will also add some more tests as per the codecov suggestions. I'm not sure what to do about the failing tests on the win32 build. It seems to be a matter of precision because the approximately equal symbol is failing. Do you have any suggestions for that?

@codecov
Copy link

codecov bot commented Dec 2, 2017

Codecov Report

Merging #685 into master will increase coverage by 0.4%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master     #685     +/-   ##
=========================================
+ Coverage   88.77%   89.18%   +0.4%     
=========================================
  Files          13       13             
  Lines        1167     1211     +44     
=========================================
+ Hits         1036     1080     +44     
  Misses        131      131
Impacted Files Coverage Δ
src/corner.jl 89.56% <100%> (+4.75%) ⬆️
src/Images.jl 91.66% <100%> (+16.66%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b6f85f6...d611b92. Read the comment docs.

@zygmuntszpak
Copy link
Member Author

zygmuntszpak commented Dec 2, 2017

Looking at the failed build for Win32 and the debug info I printed I no longer think it is failing because of precision. Could it be something more fundamental in Base? The stack trace seems to suggest there is something wrong with the statement all(ids[i].coords .≈ corner_pts[i].coords) in general, and not that the approximately true statement doesn't hold.

The problem was that the length of ids[i].coords does not match the length of corner_pts[i].coords. This is because the function corner_pts = imcorner_subpixel(img, Percentile(96), method = harris) returns a different number of corners on Win32 and Win64. It seems that the result of Percentile is slightly different on Win32 and Win64 when using a value of 96. I've set the value to 98 and on my home machine the Win32 and Win64 builds now both return the same number of points. Since I am relying on imcorner I expect that imcorner will also return different results for different percentiles on Win32 and Win64.

 Expression: all(ids[i].coords .≈ corner_pts[i].coords)
Stacktrace:
 [1] macro expansion at C:\Users\appveyor\.julia\v0.6\Images\test\corner.jl:110 [inlined]
 [2] macro expansion at .\test.jl:860 [inlined]
 [3] macro expansion at C:\Users\appveyor\.julia\v0.6\Images\test\corner.jl:33 [inlined]
 [4] macro expansion at .\test.jl:860 [inlined]
 [5] anonymous at .\<missing>:?

src/Images.jl Outdated
# divide each coordinate by the last coordinate everytime we attempt to index
# into a matrix which could induce a performance hit. Note also that we return
# a tuple of (y,x) since matrices are indexed according to row and then column.
Base.to_indices(A::AbstractArray, p::Tuple{<: HomogeneousPoint2D}) = round(Int, p[1].coords[2]), round(Int, p[1].coords[1])
Copy link
Member

Choose a reason for hiding this comment

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

Just to double check: p is limited to a 1-element tuple, but this is on purpose right?

It may be interesting to note (but no need to change it here) that Tuple get special treatment in Julia. For example they are a covariant type. This means that you could actually write p::Tuple{HomogeneousPoint2D} because for any T <: HomogeneousPoint2D it holds that Tuple{T} <: Tuple{HomogeneousPoint2D}

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, p is limited to a 1-element tuple. SimonDanish explained why in one of his replies to my question

I have quoted him below:

That's indeed confusing and is the aftermath of indexing a multi dimensional array with a single element. to_indices takes a tuple because it gets called called like this in the abstract array code:

#note, that indices isn't splatted, so it turns into a tuple:
getindex(A::AbstractArray, indices...) = getindex(A, to_indices(indices)...)
so calling getindex(A, Point(1.5, 1.5)) will call to_indices(A, (Point(1.5, 1.5),)).

Thanks for your remark about covariant types, I will do some more reading on the subject.

Copy link
Member

Choose a reason for hiding this comment

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

This is one of these painful decisions. From a performance standpoint I completely understand why you've done it, OTOH it breaks the core equivalence-class property of HomogeneousPoint. How bad is the hit if you add @assert p.coords[end] == 1? @assert statements get skipped if you start julia with julia -O3.

See [`corner2subpixel`](@ref) for more details of the interpolation scheme.

"""
function imcorner_subpixel(img::AbstractArray; method::Function = harris, args...)
Copy link
Member

Choose a reason for hiding this comment

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

@timholy Do (or should) we have a convention for naming catch-all optional positional or named arguments?

Personally I adapted the convention of using args... for positional arguments and kw... for keyword arguments.

Copy link
Member Author

Choose a reason for hiding this comment

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

In this instance I just copied and pasted the current definition of imcorner, so whatever change you suggest I will then also apply to the existing imcorner code.

Copy link
Member

Choose a reason for hiding this comment

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

Images developed over a long enough period of time that there isn't as much consistency as would be desirable. I also tend to use args... for positional, and kw... or kwargs... for keywords.

Copy link
Member Author

Choose a reason for hiding this comment

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

Since I have followed the current convention of imcorner, how about we leave it as args for the purpose of this commit? We could raise a new issue for systematically checking the Image package for consistent usage of args and kw? I just noticed that there appear to be several places where kwargs should have been used, such as gradcovs.

In the meantime, I will address the other issues that Evizero has pointed out and amend the current commit.

src/corner.jl Outdated
nrows,ncolumns = size(corner_indicator)
row, col, _ = findnz(corner_indicator)
ncorners = length(row)
corners = fill(Images.HomogeneousPoint2D((0.0,0.0,0.0)),ncorners)
Copy link
Member

Choose a reason for hiding this comment

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

Let's remove the Images. prefix here, since we are within the Images module

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for spotting that, I thought I had systematically removed any reference to Images.

src/corner.jl Outdated
```
Refines integer corner coordinates to sub-pixel precision.

The function takes as input a matrix reresenting corner responses
Copy link
Member

Choose a reason for hiding this comment

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

typo representing

src/corner.jl Outdated
\end{bmatrix}
```
the coefficients of the quadratic polynomial can be found by solving the
system of equations `b = Ax`. The result is given by `x = inv(A)b`.
Copy link
Member

Choose a reason for hiding this comment

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

Since you used the math environment in the previous paragraph I think its worth considering doing it here for b = Ax and below as well (the two backticks environment instead of one)

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, no worries. Do you know how I might visualise what the LaTeX markup will look like? My version of Juno does not support Latex preview, and I didn't see any such functionality in Visual Studio Code. I'm not sure how to build the documentation so that I can actually see the LaTeX markup.

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 access to Jupyter? I think it does the appropriate rendering for docstrings

test/corner.jl Outdated
@@ -29,6 +29,118 @@ using Base.Test, Images, Colors, FixedPointNumbers
for id in ids expected_corners[id] = 0 end
@test sum(corners .!= expected_corners) < 3
end

@testset "Corners Sub-pixel API" begin
Copy link
Member

Choose a reason for hiding this comment

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

These are tabs I think. Could you change this to use spaces instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

How can you tell that they are tabs not spaces? I was using tab, but I thought that the editors (VS Code and Juno) were set up to automatically convert tabs to spaces.

Copy link
Member

Choose a reason for hiding this comment

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

Just by selecting the text with the mouse basically. You'll see that you can just select one giant white space. In contrast if using spaces it would select a bunch of small whitespaces

@Evizero
Copy link
Member

Evizero commented Dec 3, 2017

This looks like a nicely made addition. I left a couple of minor and style related comments. I am afraid I don't know much about the subject itself. Maybe someone else here could review the functionality.

@timholy
Copy link
Member

timholy commented Dec 3, 2017

I'll review this, but first I'd really like to thank @Evizero. In recent months my administrative responsibilities at work have increased enormously, to the point where it's hard for me to put much time into development. Having help with some of the review/bugfix/merge responsibilities will allow me to focus on higher-level issues, and collectively we'll ensure that JuliaImages keeps thriving!

@Evizero
Copy link
Member

Evizero commented Dec 3, 2017

happy to help where my skillset allows it. You were a great tutor to me when I started working with Images.jl, so I'd like to give back how I can (though my knowledge about image processing is very spotty)

Copy link
Member

@timholy timholy left a comment

Choose a reason for hiding this comment

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

Love it, this is great! Thanks for being so conscientious about the docstrings and tests.

src/Images.jl Outdated
@@ -64,6 +66,33 @@ Indicate that `x` should be interpreted as a [percentile](https://en.wikipedia.o
"""
struct Percentile{T} <: Real p::T end


"""
HomogeneousPoint2D(x::NTuple{3, T})
Copy link
Member

Choose a reason for hiding this comment

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

Would it be worth calling this

struct HomogeneousPoint{T<:AbstractFloat,N}
    coords::NTuple{N,T}
end

The concept is a general one, even if its first application (imcorner) is currently limited to 2d.

src/Images.jl Outdated
# divide each coordinate by the last coordinate everytime we attempt to index
# into a matrix which could induce a performance hit. Note also that we return
# a tuple of (y,x) since matrices are indexed according to row and then column.
Base.to_indices(A::AbstractArray, p::Tuple{<: HomogeneousPoint2D}) = round(Int, p[1].coords[2]), round(Int, p[1].coords[1])
Copy link
Member

Choose a reason for hiding this comment

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

This is one of these painful decisions. From a performance standpoint I completely understand why you've done it, OTOH it breaks the core equivalence-class property of HomogeneousPoint. How bad is the hit if you add @assert p.coords[end] == 1? @assert statements get skipped if you start julia with julia -O3.

src/corner.jl Outdated
"""
```
corners = corner2subpixel(responses::Matrix{Float64},corner_indicator::Array{Bool,2})
-> Vector{HomogeneousPoint2D{3,Float64}}
Copy link
Member

Choose a reason for hiding this comment

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

I see you considered having an extra parameter to HomogeneousPoint already 🙂. Whatever gets finally decided, make sure this docstring is in sync.

src/corner.jl Outdated
The function takes as input a matrix reresenting corner responses
and a boolean indicator matrix denoting the integer coordinates of a corner
in the image. The output is a vector of type `HomogeneousPoint2D`
(an `NTuple{3, Float64}`) storing the sub-pixel coordinates of the corners.
Copy link
Member

Choose a reason for hiding this comment

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

change to [`HomogeneousPoint`](@ref) and drop the statement in parentheses

src/corner.jl Outdated
invA = @SMatrix [0.5 -1.0 0.5; -0.5 0.0 0.5; 0.0 1.0 -0.0]
for k = 1:ncorners
# Corners on the perimeter of the image will not be interpolated.
if row[k] == 1 || row[k] == nrows || col[k] == 1 || col[k] == ncolumns
Copy link
Member

Choose a reason for hiding this comment

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

imcorner works even for non-1 arrays:

img = testimage("cam")
imgo = OffsetArray(img, (-2, 5))
imcorner(imgo)

It would be better to compare to the first/last values of indices(corner_indicator).

src/corner.jl Outdated
Corners on the boundary of the image are not refined to sub-pixel precision.

"""
function corner2subpixel(responses::Matrix{Float64}, corner_indicator::Array{Bool,2})
Copy link
Member

Choose a reason for hiding this comment

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

I would favor more general choices like corner_indicator::AbstractMatrix{Bool}. See below (re indices) to see how other types might arise.

src/corner.jl Outdated

"""
function unsafe_neighbourhood_4(matrix::Matrix{Float64},r::Int,c::Int)
@views center = matrix[r,c]
Copy link
Member

Choose a reason for hiding this comment

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

I don't think you need the @views here, you're just extracting a single value from the array.

Copy link
Member Author

Choose a reason for hiding this comment

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

In light of your comment regarding OffsetArray, I presume I have to change matrix::Matrix{Float64} to matrix::Union{OffsetArrays.OffsetArray{Float64,2,Array{Float64,2}}, Matrix{Float64}}?

Similarly, responses::Matrix{Float64} would need to become responses::Union{OffsetArrays.OffsetArray{Float64,2,Array{Float64,2}}, Matrix{Float64}}?

Copy link
Member

Choose a reason for hiding this comment

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

Why put a type on at all? (There is no performance advantage to specifying the type.) The only thing that adding the type does here is ensure that a MethodError gets thrown by an inappropriate call to this function, rather than some error thrown by, e.g., the indexing operations inside the body of this function.

It wouldn't be crazy to restrict it to two-dimensional AbstractArrays, though:

unsafe_neighbourhood_4(matrix::AbstractMatrix, r::Int, c::Int)

test/corner.jl Outdated

ids = map(HomogeneousPoint2D,
[(4.244432486132958, 4.244432486132958, 1.0),
(4.244432486132958, 16.755567513867042, 1.0),
Copy link
Member

Choose a reason for hiding this comment

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

indentation issues

test/corner.jl Outdated
@@ -188,4 +300,4 @@ using Base.Test, Images, Colors, FixedPointNumbers

end

nothing
nothing
Copy link
Member

Choose a reason for hiding this comment

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

Add blank line at end

@zygmuntszpak
Copy link
Member Author

The latest pull-request addresses all the comments and suggestions. If I have left anything out I did so unintentionally.

@zygmuntszpak
Copy link
Member Author

I fixed a typo I spotted in one if the docstrings,

Copy link
Member

@timholy timholy left a comment

Choose a reason for hiding this comment

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

Just the docstring change and this is ready to go.

src/Images.jl Outdated
# the corresponding Cartesian coordinates.
Base.to_indices(A::AbstractArray, p::Tuple{<: HomogeneousPoint}) = homogeneous_point_to_indices(A, p)

function homogeneous_point_to_indices(A::AbstractArray, p::Tuple{<: HomogeneousPoint})
Copy link
Member

Choose a reason for hiding this comment

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

This might be prettier if you extract the HomogeneousPoint in to_indices and just pass that:

Base.to_indices(A::AbstractArray, p::Tuple{<: HomogeneousPoint}) = homogeneous_point_to_indices(p[1])

because then you don't have to write p[1] everywhere in homogeneous_point_to_indices.

A more typical "Julian" style would be to avoid the branch and provide two methods:

function homogeneous_point_to_indices(p::HomogeneousPoint{T,3}) where T
    if p.coords[end] == 1
    ...
end
function homogeneous_point_to_indices(p::HomogeneousPoint)
    if p.coords[end] == 1
    ...
end

However, the compiler should be smart enough that it will evaluate your if (length(p[1]).coords) == 3 at compile time so it avoids introducing another branch (in other words, in terms of generated code yours will be the same as mine).

All these are optional, just wanted you to know.

src/corner.jl Outdated
"""
```
corners = corner2subpixel(responses::AbstractMatrix,corner_indicator::AbstractMatrix{Bool})
-> Vector{HomogeneousPoint{3,Float64}}
Copy link
Member

Choose a reason for hiding this comment

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

Parameters are swapped (should be Float64,3).

Introduces the function imcorner_subpixel as well as several requisite
support functions to facilitate the ability to detect corners to
sub-pixel precision.

The imcorner_subpixel uses the existing imcorner functionality to obtain
initial corner response values and a binary corner indicator matrix. The
corner response values and the indicator matrix are used in a univariate
quadratic interpolation scheme to determine the corner coordinates to
sub-pixel precision.

The new imcorner_subpixel function returns a length-3 NTuple of type
HomogeneousPoint, which represents the homogeneous coordinates of a
planar point.
@zygmuntszpak
Copy link
Member Author

Done. Thank you for all the excellent advice.

@timholy timholy merged commit 18590c7 into JuliaImages:master Dec 17, 2017
@timholy
Copy link
Member

timholy commented Dec 17, 2017

Sorry I forgot to hit merge! Thanks again!

@zygmuntszpak zygmuntszpak deleted the imcorner_subpixel branch December 17, 2017 11:44
@zygmuntszpak
Copy link
Member Author

@timholy If I want this to appear when I do Pkg.update("Images") do I have to wait for you to tag a release? Do you have a policy for tagging releases?

@timholy
Copy link
Member

timholy commented Dec 17, 2017

Just tagged (will have to wait for the METADATA.jl PR to be merged). Thanks for the reminder!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants