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 qd_translate and qd_affine methods for QuadDIRECT registration #81

Closed
wants to merge 1 commit into from

Conversation

Cody-G
Copy link
Contributor

@Cody-G Cody-G commented Jul 6, 2018

Also added new tests and refactored existing ones. You'll notice that some new qd_affine tests are commented out because they incorporate randomness and they fail sometimes. The qd_translate and qd_rigid tests with randomness pass reliably so I kept those. We may be able to switch the affine ones back on in the future with some newer version of QuadDIRECT.

@Cody-G Cody-G mentioned this pull request Jul 6, 2018
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.

This is looking really good. Thanks for enhancing the functionality so dramatically.

Two thoughts:

  • I suspect @ChantalJuntao has another pull request coming. If it's close, we might want to merge that first (since this is a pretty dramatic reorganization and that's tough for newcomers to cope with from the standpoint of git etc)
  • I suspect it might be worthwhile poking a bit more at why the affine transformation test fails. I am sure some of the limitations are in QuadDIRECT (and that won't be fixed until I have its replacement ready), but some may also be in subtle issues here. Before merging I might want to play with this a bit and see if I can gain more insight.

error()
end
#################### Utilities ##########################
function inds_intersect(img1, img2)
Copy link
Member

Choose a reason for hiding this comment

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

You can also write this more compactly as intersect.(indices(A), indices(B)). Might not need a function for that? But I'm not against having one.

end
newmov = warp(moving, tfm)
inds = inds_intersect(newmov, fixed)
return newmov[inds...], view(fixed, inds...)
Copy link
Member

Choose a reason for hiding this comment

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

I noticed this before, but didn't ask. Now I'm curious, why use newmov[inds...] but view(fixed, inds...)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wanted to use views for both, but I remember I was getting an error. Now I can't remember what the error was (it's been a while) but I'll try changing it back and see.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I found the error again and opened issue #83

Sorry for not reporting it sooner, I think I was in a hurry at the time I encountered it and then forgot.

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 opening the issue and finding a minimal reproducer. I'll try to get that one figured out myself.

error()
function warp_and_intersect(moving, fixed, tfm)
if tfm == IdentityTransformation()
return moving, fixed
Copy link
Member

Choose a reason for hiding this comment

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

For full consistency here, before returning we should check that indices(moving) == indices(fixed); if that's not true, we should still intersect and subset.

Or, if having "extra pixels" in one of the images might be useful, then we shouldn't trim at all. Either way, it might be best to make only the warp part conditional on tfm and keep the various branches more parallel.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I take your first point, and as for the extra pixels I can imagine them being useful sometimes. I can think of at least one case where that would require other changes: we're using mismatch0 in some places and that doesn't work when images aren't of equal size. I'll see how easy it looks, and if it's not so easy maybe I'll take the "always intersect" approach for this PR if that sounds good to you.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I still like the idea of keeping the "extra pixels" around, but I think that will be easier to do after #83 is addressed so I'll continue intersecting for now.

mms = mismatch(fixed, moving, mxshift; normalization=normalization)
best_i = indmin_mismatch(mms, thresh)
return best_i.I, ratio(mms[best_i], 0.0, Inf)
end

#returns new minbounds and maxbounds with range sizes change by fac
function scalebounds(minb, maxb, fac::Float64)
Copy link
Member

Choose a reason for hiding this comment

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

fac::Real? Float64 seems unnecessarily specific.

#(Includes rotations, scaling, shear, etc)
#The space is centered on the identity matrix
function default_linmap_bounds(img::AbstractArray{T,N}) where {T, N}
d=0.05; nd=0.05; #not sure yet whether off-diagonals should be treated differently
Copy link
Member

Choose a reason for hiding this comment

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

I suspect these constants should be a keyword argument. Otherwise "reasonable default" might depend on how much movement one gets with this prep.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense! Initially I wanted to try to phrase these in more understandable terms, letting the user specify maximum rotation, scaling, shear, etc but I found it difficult to combine those constraints. For now I guess I'll go with diagonal and off-diagonal kwargs.

return ratio(mm, thresh, Inf)
end

#sets splits based on lower and upper bounds
Copy link
Member

Choose a reason for hiding this comment

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

👍 . I should bake this into QuadDIRECT (or rather, its successor) when I get to it.

`kwargs...` can also include any other keyword argument that can be passed to `QuadDIRECT.analyze`.
It's recommended that you pass your own stopping criteria when possible (i.e. `rtol`, `atol`, and/or `fvalue`).

If you have a good initial guess at the solution, pass it with the `tfm0` kwarg to jump-start the search.
Copy link
Member

Choose a reason for hiding this comment

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

tfm0->initial_tfm

Therefore if you try to use the transform to warp an image of a size other than `size(moving)` then the result will not be as expected.
If you want to keep the rotation centered you must call `recenter(tfm, newctr)` where `newctr` is the displacement of the new center from the old center.
"""
function qd_affine(fixed, moving, mxshift, linmins, linmaxs, SD=eye(ndims(fixed)); thresh=0.1*sum(abs2.(fixed[.!(isnan.(fixed))])), initial_tfm = IdentityTransformation(), kwargs...)
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 worth noting that affine doesn't technically require SD, since an affine transformation can accommodate the compensating transformation. I suspect we should still use it as a hint, however, because the intializations may be more "rigid-like" with it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see your point, and as you say I think it's a good hint to have so that we can make the most of the user's intuitions about real-world bounds on movement.

# a different set of errors than the tests above)
@testset "QuadDIRECT tests with standard images" begin
img = testimage("cameraman");
img = Float64.(img)
Copy link
Member

Choose a reason for hiding this comment

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

Is this Float64 necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah It used to be before I fixed the overflow in mismatch0, I can change it back.

Copy link
Contributor Author

@Cody-G Cody-G Jul 7, 2018

Choose a reason for hiding this comment

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

Wow so this was more complicated than I thought. After removing the Float64 conversion some tests failed to reach the fvalue criteria that we had in place. At first this seemed like a problem because those tests were passing with the same criteria before this PR. But after a lot of checks I became convinced that it was just a lucky accident that the tests in question passed with the 8-bit cameraman image before. What I realized is that with low precision sometimes the "coarse" step can achieve a lower objective value than the fine step. I'm pretty sure this is how that can happen:

The coarse step of qd_rigid involves two warps (a rotation and then a pixel-aligned shift) before computing the mismatch while the fine step only does a single warp (combined rotation and shift). Remember that neither method can reduce the mismatch to 0 because the "moving" image was created by interpolating the fixed image. It seems that with low-precision images sometimes the algorithm can find a pair of transforms in the coarse step that exhibit favorable rounding errors and achieve a lower mismatch than the single combined transform. In the cases where I've seen this happen the coarse step is already very close to the best answer so the fine step doesn't need to do anything. But it can make setting stopping criteria very confusing. After that rabbit hole my feeling is that it's simplest to just convert the images to higher precision before doing the registration.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm. The tests should be deterministic, though? There isn't any randomness I can see, and I don't think analyze has anything non-deterministic about it. (I could be forgetting, however.)

I wonder if we might be seeing overflow in warp? (i.e., Interpolations?) So that very subtly different ways of computing things yield quite different results?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is deterministic. If I run this PR without converting to Float64 I always get the same result. The reason it's inconsistent with Chantal's PR is that I've since changed some things about how the function works. The change may be due to my correcting the order of more of the compositions, though I haven't tracked it down to a single line. But I do know that the mismatch value shown in the log of her Travis run is lower than expected even when applying the perfect inverse transform to moving, consistent with my idea that the test passed with 8-bit images only by a happy accident.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And all of this could also be consistent with overflow within warp, good idea!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I got it! The problem is with warp, but it's not overflow. Since we can't represent NaN with fixed point numbers warp imputes zeros when sampling outside of the image. This can change the normalizing term in the mismatch calculation dramatically, especially for small images. Here's an example:

using BlockRegistration, RegisterMismatch, FixedPointNumbers, CoordinateTransformations

a = fill(N0f16(0.5), 5,5)
b = Float64.(a)

tfm = Translation(0.5,0.5)

wa, fa = RegisterOptimize.warp_and_intersect(a, a, tfm)
wb, fb = RegisterOptimize.warp_and_intersect(b, b, tfm)
@show ratio(mismatch0(wa, fa), 0) #nonzero
@show ratio(mismatch0(wb, fb), 0) #zero

There may be a nice Julian way to fix this. Maybe by defining an efficient element type that wraps a fixed point number and can handle missing values? Or maybe the new missing value from Julia 0.7? I haven't played with that yet but my guess is that performance may suffer. Maybe easiest would be just to never try to warp an array of fixed point numbers, forcing conversion beforehand?

Copy link
Member

Choose a reason for hiding this comment

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

Great analysis! Yes, this makes it much clearer, and a good example of how important NaN-poisoning is. (Note that most transformation frameworks, e.g., ANTs and matlab, fill with zeros.) Now I have no objection to coercing it. Maybe better it's to use float.(img), though; the latter will even work for RGB, I think, whereas Float64.(img) will not.

I'm filing an issue (#82) to remind myself to go through BlockRegistration more generally and see whether there are other places we should be doing this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great, and I think I will use float(img) rather than float.(img) because the former is smart enough not to allocate a new array when it already has element type Float64

inds = intersect.(indices(img), indices(img2))
fixed = img[inds...]
moving = img2[inds...]
return fixed, moving
Copy link
Member

Choose a reason for hiding this comment

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

weird indentation here and below

@Cody-G
Copy link
Contributor Author

Cody-G commented Jul 11, 2018

Continued in #85

@Cody-G Cody-G closed this Jul 11, 2018
@Cody-G Cody-G deleted the cjg/qd_trans_aff branch August 23, 2018 22:32
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.

2 participants