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 nD support to several geometric transform classes #5188

Merged
merged 27 commits into from
Mar 13, 2021

Conversation

jni
Copy link
Member

@jni jni commented Jan 18, 2021

This is the second PR with more gradual changes from #3544. I've pulled out the
automatic conversion of 1D parameter vectors to transforms. Viewed in
isolation, that was definitely way too magical given our values. I've left the
implementation in as a private function for future reference. Eventually, I'd
like to add a class method, from_parameter_vector, that would be
well-documented for each class, but I think that should come in a future PR.

I've done my best with the Euler rotation matrix code, but that would benefit
the most from review. Since we don't have a strict xyz/zyx axis order, it's
basically ambiguous whether XYZ or ZYX Euler angles are implemented (if it's
either of those 😂). CC @stefanv @grlee77 @matthew-brett

Description

Checklist

For reviewers

  • Check that the PR title is short, concise, and will make sense 1 year
    later.
  • Check that new functions are imported in corresponding __init__.py.
  • Check that new features, API changes, and deprecations are mentioned in
    doc/release/release_dev.rst.

@pep8speaks
Copy link

pep8speaks commented Jan 18, 2021

Hello @jni! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

Line 75:13: E126 continuation line over-indented for hanging indent
Line 76:13: E123 closing bracket does not match indentation of opening bracket's line
Line 78:13: E126 continuation line over-indented for hanging indent
Line 78:30: E231 missing whitespace after ','
Line 79:13: E123 closing bracket does not match indentation of opening bracket's line
Line 592:42: E226 missing whitespace around arithmetic operator
Line 699:32: E226 missing whitespace around arithmetic operator
Line 704:19: E226 missing whitespace around arithmetic operator
Line 704:29: E226 missing whitespace around arithmetic operator
Line 704:32: E226 missing whitespace around arithmetic operator
Line 704:40: E226 missing whitespace around arithmetic operator
Line 704:43: E226 missing whitespace around arithmetic operator
Line 704:53: E226 missing whitespace around arithmetic operator
Line 704:56: E226 missing whitespace around arithmetic operator
Line 705:19: E226 missing whitespace around arithmetic operator
Line 705:29: E226 missing whitespace around arithmetic operator
Line 705:32: E226 missing whitespace around arithmetic operator
Line 705:40: E226 missing whitespace around arithmetic operator
Line 705:43: E226 missing whitespace around arithmetic operator
Line 706:19: E226 missing whitespace around arithmetic operator
Line 706:29: E226 missing whitespace around arithmetic operator
Line 706:32: E226 missing whitespace around arithmetic operator
Line 706:38: E226 missing whitespace around arithmetic operator
Line 707:19: E226 missing whitespace around arithmetic operator
Line 707:29: E226 missing whitespace around arithmetic operator
Line 707:32: E226 missing whitespace around arithmetic operator
Line 708:19: E226 missing whitespace around arithmetic operator
Line 708:29: E226 missing whitespace around arithmetic operator
Line 708:32: E226 missing whitespace around arithmetic operator
Line 708:38: E226 missing whitespace around arithmetic operator
Line 708:64: E226 missing whitespace around arithmetic operator
Line 721:24: E226 missing whitespace around arithmetic operator
Line 721:29: E226 missing whitespace around arithmetic operator
Line 1066:23: E241 multiple spaces after ','
Line 1185:41: E241 multiple spaces after ','
Line 1186:22: E201 whitespace after '['
Line 1186:41: E241 multiple spaces after ','

Line 499:45: E202 whitespace before ']'
Line 499:47: E231 missing whitespace after ','

Comment last updated at 2021-03-11 11:12:33 UTC

@jni
Copy link
Member Author

jni commented Jan 18, 2021

The CI failure appears to be an unrelated hang.

matrix = np.eye(dimensionality + 1)
else:
dimensionality = matrix.shape[0] - 1
if matrix.shape != (dimensionality + 1, dimensionality + 1):
Copy link
Member

@rfezzani rfezzani Jan 18, 2021

Choose a reason for hiding this comment

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

Shouldn't dimensionality be deduced from matrix.shape if matrix is not None?
I mean, when matrix is provided, we can assume that the user knows the dimensionality, we don't need to force him to give it as argument if dimensionality != 2...
I think dimensionality should be None by default...

Copy link
Member

@rfezzani rfezzani Jan 18, 2021

Choose a reason for hiding this comment

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

Well, sorry, I need one more coffee this morning 😅...

BTW, this situation can never happen since dimensionality is overwritten when matrix is not None.

Copy link
Member

Choose a reason for hiding this comment

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

Waouh, I need a bucket of coffee...

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 actually checked the diff cover when developing tests here so I'm fairly confident that all lines are reachable. 😃

Copy link
Member

@stefanv stefanv left a comment

Choose a reason for hiding this comment

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

The Guardian of the Dimensions Returns 🛡️!

I appreciate you making this an incremental step; it would benefit from some implementation notes. But the implementation itself looks sound.

Thank you!

skimage/transform/_geometric.py Show resolved Hide resolved
nparam = v.size
# solve for d in: d * (d - 1) = nparam
d = (1 + np.sqrt(1 + 4 * nparam)) / 2 - 1
dimensionality = int(d)
Copy link
Member

Choose a reason for hiding this comment

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

for nparam == 9, solution is, e.g., 3.999 — is int then the right call?

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, there's no such thing as 9 parameters for the affine... It's 6 (2D), 12 (3D), 20 (4D), ... And anyway you've made a mistake typing stuff in somewhere because I get 4.0 for n=20 and 3.88748 for 19?

But, it's totally true that one should never trust floating point arithmetic to be exact. I'll use np.round here.

v, (dimensionality, dimensionality + 1)
)
matrix = np.concatenate(
(part_matrix, np.eye(dimensionality + 1)[-1:]), axis=0
Copy link
Member

Choose a reason for hiding this comment

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

Easier to just pop the one into place?

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 alternative code is:

matrix = np.concatenate(
    (part_matrix, np.zeros(dimensionality + 1), axis=0
)
matrix[-1, -1] = 1

So, you get an extra line of code, for something maybe marginally clearer? I agree that what I'm after really is the vector commonly called e_i. But I don't think that exists in NumPy. =)

Copy link
Member

Choose a reason for hiding this comment

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

It actually exists: what you are looking for is np.eye(1, dimensionality + 1, dimensionality) 😉

>>> np.eye(1, 4, 3)
array([[0., 0., 0., 1.]])

centered = points - centroid
rms = np.sqrt(np.sum(centered ** 2) / n)

if rms == 0:
Copy link
Member

Choose a reason for hiding this comment

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

I guess this point could come pretty close to zero by itself, so is it worth doing the explicit check, or should we just let the algorithm run its course?

Copy link
Member

Choose a reason for hiding this comment

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

Or should we define a tolerance parameter for numerical stability?

if rms < tol:
    ...

Copy link
Member

Choose a reason for hiding this comment

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

tol is hard to specify, so I'd say "no, if the algorithm produces nans that pass through anyway" or "yes, if @jni says we need to force the output to have a certain context". Either way, there may be better ways to check than looking at the RMS---like looking at the condition number of the matrix being solved.

Copy link
Member

@rfezzani rfezzani Jan 21, 2021

Choose a reason for hiding this comment

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

OK, after reading more closely, I think that @jni wants to avoid these try... except... blocks here and here.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, after reading more closely, I think that @jni wants to avoid these try... except... blocks

bingo! 😊 And those are only triggered when actual 0 is reached. I'll add a comment.

skimage/transform/_geometric.py Show resolved Hide resolved
skimage/transform/_geometric.py Show resolved Hide resolved
@jni
Copy link
Member Author

jni commented Jan 22, 2021

Ok @stefanv I think I've addressed all your questions! Thanks for the review! 🎉

Comment on lines +1070 to +1069
# We need the axes other than the rotation axis, in the right order:
# 0 -> (1, 2); 1 -> (0, 2); 2 -> (0, 1).
axes = sorted({0, 1, 2} - {axis})
# We then embed the 2-axis rotation matrix into the full matrix.
# (1, 2) -> R[1:3:1, 1:3:1] = R2, (0, 2) -> R[0:3:2, 0:3:2] = R2, etc.
sl = slice(axes[0], axes[1] + 1, axes[1] - axes[0])
Copy link
Member

Choose a reason for hiding this comment

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

Give me more magic

Suggested change
# We need the axes other than the rotation axis, in the right order:
# 0 -> (1, 2); 1 -> (0, 2); 2 -> (0, 1).
axes = sorted({0, 1, 2} - {axis})
# We then embed the 2-axis rotation matrix into the full matrix.
# (1, 2) -> R[1:3:1, 1:3:1] = R2, (0, 2) -> R[0:3:2, 0:3:2] = R2, etc.
sl = slice(axes[0], axes[1] + 1, axes[1] - axes[0])
args = [None, None, None]
args[-axis] = 1 + ( axis > 0)
sl = slice(*args)

😈

Copy link
Contributor

Choose a reason for hiding this comment

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

That does look correct, but is pretty obscure (thus the devil emoji, I suppose!).

sorted axes could also be created with a generator expression:

# Get the (sorted) axes of the plane in which rotation occurs
ax1, ax2 = (ax for ax in range(3) if ax != axis)
# We then embed the 2-axis rotation matrix into the full matrix. 
sl = slice(ax1, ax2 + 1, ax2 - ax1)

Copy link
Member Author

Choose a reason for hiding this comment

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

Imma keep my code + comments, thanks both. 😂

The Euler rotation matrix.
"""
if axes is None:
axes = list(range(3))
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
axes = list(range(3))
axes = tuple(range(3))

It doesn't look like the list is modified later, so may as well use a tuple

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

actually, we only iterate over axes later, so I'm just gonna leave it as range()!

Copy link
Contributor

@grlee77 grlee77 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 pretty good to me. I have left some minor comments

Comment on lines 1264 to 1259
Implemented only for 2D and 3D. For 3D, this is given in XZX Euler
angles.
Copy link
Contributor

Choose a reason for hiding this comment

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

XZX is not right here is it? It looks like we use axes in the order 0, 1, 2 in _euler_matrix which should be XYZ (assuming axis=0 means X?). XZX is apparently one of 12 possible conventions, but it didn't seem to be the one used here.

Another form for 3D that I find fairly intuitive is where the user specifies a unit vector corresponding to the [axis of rotation and then a single rotation angle about that vector. There is an explicit formula for that form.

Copy link
Member Author

Choose a reason for hiding this comment

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

XZX is not right here is it?

Ah, yes, XZX is left over from an older version of the code. Will fix.

Another form for 3D that I find fairly intuitive is where the user specifies a unit vector corresponding to the axis of rotation and then a single rotation angle about that vector.

Could change to that if people think that's better...

Copy link
Member Author

Choose a reason for hiding this comment

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

@grlee77 since the 3D Euler angles and the 3D vector + rotation are unambiguously distinguishable, I'm going to leave it as 3D Euler angles for now (fixed the docstring), and we can revisit in the future if someone clamors for alternate parameterizations.

Base automatically changed from master to main February 18, 2021 18:23
@jni jni mentioned this pull request Mar 11, 2021
5 tasks
This reverts commit 9630425.

This change turned out to be incorrect because of the missing
`norm_factor`.
@jni
Copy link
Member Author

jni commented Mar 11, 2021

@scikit-image/core I believe I've addressed all the review comments — please have another look! 🎉

Copy link
Member

@rfezzani rfezzani left a comment

Choose a reason for hiding this comment

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

Looks good to me. Thank you @jni 🎉

@jni
Copy link
Member Author

jni commented Mar 12, 2021

Note, the optional deps failure is from #5266 and unrelated to this PR.

@stefanv stefanv merged commit bc57d2c into scikit-image:main Mar 13, 2021
@stefanv
Copy link
Member

stefanv commented Mar 13, 2021

Nice work. Thank you @jni!

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.

None yet

5 participants