Refactor transform initialization#7754
Conversation
Standardize and refactor initialization of transform classes by adding new _MatrixTransform base class, and formalizing rules for dimensionality. Add and test `identity` class constructor. Deprecate use of scalar ``scale`` parameter with dimensionality=3. In due course, perhaps warn on use of `dimensionality` when calling class constructor with other arguments, to avoid confusion, and prefer ``identity`` class constructor.
Maybe a result of platform linalg etc differences.
(To my eye) Simplify logic in creating homogenous transform matrix for center and normalize points routine. Also work round ruff's rather extreme reformatting of an array literal.
Move application of homogenous matrix application to its own function. Use instead of `_apply_mat` method.
This also shows that we are not testing this properly.
Check some error messages. Cover case of not-identity rotation.
Test AffineTransform constructor against 2D algorithm in docstring.
Confirm that order of application of transforms is as specified in the docstring. It is. But - oh dear - that's a different order than the parameter position. We'll need to address that at some point.
da87c6d to
d0fdbdf
Compare
Test rigid and scale + rigid transforms. Document transformation order.
As discussed on Signal. Also, reorder parameters for AffineTransform, that did not match the order of application.
Test that transforms with implicit parameters must be passed as keyword arguments.
|
As discussed on Signal, I've made all implicit parameters keyword only, because there was a mismatch between parameter order and application order in Specifically, the order in which transformations are applied differs from the parameter order in AffineTransform. As noted in the docstring, transformations applied are in order scale then shear then rotation then translation, as confirmed by new test, but the parameter order is scale, rotation, shear, translation. That is very confusing, and should be fixed. The PR here goes straight to keyword-only for all classes with implicit parameters, for consistency. A search on Github for e.g. |
Simplify code somewhat, and extend tests.
|
I checked the tests for this PR against the main branch. The differences are: For
rather than raising a
|
jni
left a comment
There was a problem hiding this comment.
Hi @matthew-brett! I've left some minor comments here and there. I'm not sure about some of the decisions — it might be worth moving discussion from Signal to our public Zulip for visibility — but happy to be convinced!
skimage/transform/_geometric.py
Outdated
| translation = np.asarray(translation) | ||
| if rotation.shape != (3, 3): | ||
| raise ValueError("Invalid shape of rotation matrix") | ||
| if abs(np.linalg.det(rotation) - 1) > 1e-6: |
There was a problem hiding this comment.
Can we make 1e-6 a parameter "abs_tolerance" of the method with a default value? Or it could be a private attribute of the class. Either way, I don't like seeing them scattered across various if-statements.
There was a problem hiding this comment.
Can we make 1e-6 a parameter "abs_tolerance" of the method with a default value? Or it could be a private attribute of the class. Either way, I don't like seeing them scattered across various if-statements.
Done - that was in the code before refactor.
skimage/transform/_geometric.py
Outdated
| points_h = np.vstack([points.T, np.ones(n)]) | ||
|
|
||
| new_points_h = (matrix @ points_h).T | ||
| def _apply_homogenous(matrix, points): |
There was a problem hiding this comment.
I've typically seen "homogeneous" rather than "homogenous", e.g. https://en.wikipedia.org/wiki/Homogeneous_coordinates
skimage/transform/_geometric.py
Outdated
| Parameters | ||
| ---------- | ||
| matrix : (D+1, D+1) array_like | ||
| The transformation matrix to obtain the new points. |
There was a problem hiding this comment.
I would add a note here that any object with an __array__ method will work, as long as the array shape matches.
There was a problem hiding this comment.
Is there a policy for how to say that in a docstring? I am used to array-like for that - as in anything that can become an array with np.array(param) - but perhaps that is not your tradition.
There was a problem hiding this comment.
Hmm, good point! 😅 I am used to thinking as array-likes being:
- numpy arrays
- lists and tuples (including nested lists)
- other arrays, such as dask arrays
I guess what I was trying to say is that I wouldn't think of skimage.transform.AffineTransform as an "array-like" without further prodding. So I think "array-like" in the type section is fine, but I would add a sentence to the description like:
Note that any object with an `__array__` method [1]_ that returns a matrix
with the correct dimensions can be used as input here. This includes all
subclasses of `skimage.transform.ProjectiveTransform`, for example.
[...]
References
----------
.. [1]: https://numpy.org/doc/stable/user/basics.interoperability.html#using-arbitrary-objects-in-numpy
There was a problem hiding this comment.
Ah yes - I see what you mean - I've added your change. Do you think this should go everywhere we accept an array-like for a transformation matrix? Such as the class constructors?
|
|
||
| def __nice__(self): | ||
| """common 'paramstr' used by __str__ and __repr__""" | ||
| if not hasattr(self, 'params'): |
There was a problem hiding this comment.
Personally, I prefer classes to have all their expected attributes even if they can be null, rather than not have the attribute. ie, I would make sure that self.params is at least set to None. (I'd consider using dataclasses in this refactor.)
Since params is a public attribute, I'd consider this a breaking change...
There was a problem hiding this comment.
The only purpose of this line is to avoid confusing warnings when object construction fails - and the only situation in which the object would lack self.params is if object construction fails. So I don't think this code could break anything out there; it would only improve the repr displayed when the object generates an error during construction.
There was a problem hiding this comment.
avoid confusing warnings when object construction fails
under what circumstances can construction fail but still return an instance?
There was a problem hiding this comment.
under what circumstances can construction fail but still return an instance?
This was happening in the tests, when the constructor failed, and the error message displayed a repr of the half-initialized object. In that case you'd get two errors, one displaying the repr - not relevant to the problem - and the other for the actual problem in initialization. Because I found that distracting, I made this change to prevent the first error. But I wouldn't die on a hill to keep it - it shouldn't get triggered in ordinary practice.
There was a problem hiding this comment.
@matthew-brett what do you think about setting params=None first thing in the constructor so we don't have to hasattr here?
There was a problem hiding this comment.
I would rather avoid putting params=None at the top of all the __init__s, because people are more likely to read that code, and be confused by it - when it is only solving a problem they would not normally see. So my preference order would be:
- The current fix in the
__repr__/__nice__function. - No fix at all, just let the double error happen in the rare case where initialisation fails and we generate a
__repr__of the result. params=Nonein all__init__s.
| # With scalar scale and 3D, we get a deprecationwarning. This is to | ||
| # generalize the rule that dimensionality should be implied by | ||
| # input parameters, when given. |
There was a problem hiding this comment.
Has there been much discussion about this rule? I'm not sure how I feel about it, and could be convinced that it's good, but AffineTransform(scale=2, dimensionality=3) seems like an ok thing to me a priori?
There was a problem hiding this comment.
I personally slightly prefer a more explicit API such as
AffineTransform(scale=(2, 2, 2))
that can do away with an additional parameter dimensionality=. That simplifies input validation and documentation.
There was a problem hiding this comment.
I think I added it when making transforms nD-compatible because it was possible before to do AffineTransform().estimate(...), so we want to still be able to do that for higher dimensions, as in AffineTransform(dimensionality=3).estimate(...). If we get rid of dimensionality, this would become (presumably) AffineTransform.identity(ndim=3).estimate(...), which is clunky.
We could instead make estimate a staticmethod, since it doesn't use starting parameters anyway. But that's a breaking change. Perhaps something to make an issue for skimage2?
There was a problem hiding this comment.
I presume that AffineTransform().estimate(...) was always a 2D identity transform? If so, there is no change if we remove dimensionality from the constructor.
One could argue about which of AffineTransform(dimensionality=3).estimate(...) or AffineTransform.identity(3).estimate(...) is more clunky - I personally find the second easier to read, because I have to translate the first into "Make an identity transform of dimension 3" in my head. But the problem with the dimensionality= constructor call is that, in order to have this convenience in using the constructor for the identity, we have a constructor where one argument is nearly always redundant or incorrect, if any other parameters are specified.
There was a problem hiding this comment.
We could instead make
estimatea staticmethod, since it doesn't use starting parameters anyway. But that's a breaking change. Perhaps something to make an issue for skimage2?
Are you suggesting that we have the constructor with no input arguments generate a self.params=None not-initialized object, and then, if estimate finds such an object, it works out what identity transform to initialize with?
We could make a constructor such that it would be possible to tform = AffineTransform.from_estimate(src, dst).
Of course that would just boil down to a slight polishing of:
@classmethod
def from_estimate(cls, src, dst):
obj = cls.identity(src.shape[1])
if not obj.estimate(src, dst):
raise SomeError('Estimation failed')
return obj
That doesn't seem unreasonable. And it would make it less common to need to construct the identity transform explicitly.
There was a problem hiding this comment.
Well, for 2D Transform().estimate is already working and the way we teach it, so why not just make that work with other dimensions as well?
It's a bit unreasonable to expect a transform to function before either initializing it with parameters, or estimating those parameters.
I think getting rid of dimensions is fine, as is introducing identity.
But, ok, I'll let @jni take another look. I'm pretty sleep deprived atm.
There was a problem hiding this comment.
Chiming in late to the conversation, but I'd also prefer making use of classmethods. Both in a vacuum and with the current API Transform.from_estimate seems like a good idea for already stated reasons. Transform.estimate() feels unnecessarily confusing, and I'd be fine with deprecating Transform().estimate(). It's always been slightly bugging me that the API allows for an "empty" transform and a readonly instance (think frozen dataclass) seems like a very good fit to me.
There was a problem hiding this comment.
I am still puzzled by the staticmethod idea. Given that the callable produces a class, and needs to know its own class, surely it should be classmethod.
Sorry, you are right. Technically it needs to know its own class but, looking at the code, literally every class has its own estimate method, so it could be hardcoded, which is where my brain was going. But I think that is not as nice as using cls(params), so indeed classmethod is the way to go.
It's too fancy and magical (see advice above). It will make even experienced developers do a double take, and try and work out what is going on.
This would only be a temporary state, because instance.estimate() would be deprecated. So I don't think it's a big deal.
I don't think estimate is the right name for a classmethod (see below) that returns a new transform instance. I think it needs to be from_something. I personally find from_correspondence ugly, and still prefer from_estimation - it doesn't worry me that you pass points rather than an estimation, that will be obvious from inspecting the signature, and not surprising.
I acknowledge that from_something is the most common pattern for classmethod naming, but it's certainly not a hard and fast rule, and for me tf = AffineTransform.estimate(src, dst) is the most natural incantation by a long shot. I think @stefanv also prefers it but I'll let him speak for himself when he's awake. 😂 And again, I am in no way advocating to keep both the instance and the class methods around, I'm thinking about what to do in the future. Having the instance method turns out to make no sense so imho keeping it is out of the question.
I find AffineTransform.from_estimation awkward, and would potentially even prefer registration.from_points(src, dst, tf_class=AffineTransform).
Incidentally I don't see why identity(n) gets to avoid the from prefix. 😉
I'm on board with identity and I'm on board with dropping dimensionality.
There was a problem hiding this comment.
@jni - can you live with estimated as the class constructor name? If so, I think we're very close ...
There was a problem hiding this comment.
I can live with from_estimate as the class constructor name.
| # Not so if we specify some other input giving dimensionality. | ||
| tf = SimilarityTransform(scale=4, translation=(0, 0, 0)) |
There was a problem hiding this comment.
And this feels super weird... imho either we allow scalars, or we don't, I don't see why the dimensionality needs to be "implied"?
There was a problem hiding this comment.
I concur, either we deprecate implicit broadcasting of scalars to the appropriate shape everywhere (my preference), or we keep dimensionality= around.
There was a problem hiding this comment.
My reasoning for this change was that I found it hard to grok the meaning of e.g dimensionality=3 in a constructor call like SimilarityTransform(np.eye(4), dimensionality=3), or SimilarityTransform(rotation=(0.1, 0.2), dimensionality=3). That is - it seemed to be, nearly always, either redundant, or to require an error on construction, when the dimensionality does not match the inputs.
On further reflection, it became clear that the dimensionality=3 parameter was almost invariably used to construct an identity transform - as in SimilarityTransform(dimensionality=3). So this leads us to the odd situation where, for a huge majority of calls (actually, I think all but one in the test suite) dimensionality, when present, is redundant, a source of error, or intended to construct an identity transform.
Put another way, in the vast majority of cases, when passing any non-default argument to the constructor, dimensionality is implied by the arguments, and dimensionality is redundant, or should cause an error because it does not match.
I thought this was confusing and unsatisfying as an API, so I planned to, in due course, remove the dimensionality parameter entirely from the constructor API, and ask users to write SimilarityTransform.identity(3) instead, for the vast majority of cases where dimensionality should be specified.
This left only one valid use-case for non-identity transforms, non-default dimensionality, and that was the scalar scale case you've referred to. As I say - I think that was used only once in the whole test suite. Of course, it will only be useful specifically in the 3 or greater dimension case - that is - the only place where dimensionality makes sense, for not-default input arguments is specifically e.g. SimilarityTransform(scale=4, dimensionality=3).
So it seemed better to me, from the point of view of the API, to plan, in due course, to remove dimensionality from the constructor, because it is nearly always redundant, and for the only case where it was not redundant, ask users to specify the dimensionality by SimilarityTransform(scale=(4, 4, 4)) instead. This, along with the .identity constructor, allows us to remove a huge number of redundant uses of dimensionality, with very small loss of convenience, for this scalar scale case.
There was a problem hiding this comment.
Or to put it yet another way - I think the current class constructor API is a fusion of two different constructors, which the current API fuses into one constructor:
# Transform from parameters.
SimilarityTransform(rotations=(0.1, 0.2, 0.3))
# Identity transform.
SimilarityTransform(dimensionality=3)leading to confusing calls like:
# In user code, but also occurs in the scikit-image codebase.
SimilarityTransform(rotations=(0.1, 0.2, 0.3), dimensionality=3)I think this should be, in due course:
# Transform from parameters.
SimilarityTransform(rotations=(0.1, 0.2, 0.3))
# This will raise an error, because `dimensionality` will not be an argument to constructor.
SimilarityTransform(rotations=(0.1, 0.2, 0.3), dimensionality=3)
# Identity transform.
SimilarityTransform.identity(3)Therefore, it separates the two APIs.
There was a problem hiding this comment.
Thanks for the rationale @matthew-brett. See this comment re APIs — I think AffineTransform(dimensionality=3).estimate(src, dst) reads better than AffineTransform.identity(3).estimate(src, dst). However, even better is AffineTransform.estimate(src, dst), with the dimensionality implicit in the shapes of src and dst. Therefore, I think forge ahead with your proposal, but with an eye towards making estimation a static method. Thoughts?
Responding to Juan's comments.
Responding to JNI's comments.
Co-authored-by: Marianne Corvellec <[email protected]>
Co-authored-by: Marianne Corvellec <[email protected]>
From @mkcor review.
From @mkcor review.
Move from NotImplemented to abstract class method.
|
@matthew-brett on a side note, are you aware that you can apply multiple suggestions as a batch? |
skimage/transform/_geometric.py
Outdated
| Fallback number of dimensions for transform when no other paremeters | ||
| are specified. Otherwise ignored, and we infer dimensionality from the |
There was a problem hiding this comment.
| Fallback number of dimensions for transform when no other paremeters | |
| are specified. Otherwise ignored, and we infer dimensionality from the | |
| Fallback number of dimensions for transform when no other parameter | |
| is specified. Otherwise ignored, and we infer dimensionality from the |
typo + singular (thinking, there's not even a single other parameter)
Co-authored-by: Marianne Corvellec <[email protected]>
|
Have I got to everything here? |
|
It's really your call - I am a newcomer here - but given you already have
so many deprecations - is it really worth doing here? I suspect, if
there's only one instance in the whole of Github that will run into this,
as there appears to be, that you're adding this deprecation for a tiny
number of affected users, and those users will in any case get an
informative error - so while I see that it's technically not what you have
been doing in terms of policy - to me this one seems not worth the extra
overhead for the user or the developers...
…On Tue, Apr 1, 2025 at 6:10 PM Lars Grüter ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In skimage/transform/_geometric.py
<#7754 (comment)>
:
> def __init__(
- self, rotation=None, translation=None, matrix=None, *, dimensionality=2
+ self, *, rotation=None, translation=None, matrix=None, dimensionality=None
While I agree, with the guess that few users will be impacted, so far
that's not been our guiding principle for deciding on deprecation cycles. I
also wouldn't categorize the mismatch between argument order and
application order a bug. And in this case – concerning
EssentialMatrixTransform – the order stays the same anyway. Though,
aligning the order has merit IMO.
We have used the criteria, that users won't just silently fail with
different result, to decide on how we can do a deprecation, *not if we
need to do a deprecation*. E.g. does it require a namespace change
(skimage2 / new function) or can we do it with a temporary decorator.
And in light of the many deprecations we are already piling on our users
in preparation of skimage2, I'd rather be as nice as possible, than skip a
deprecation I could probably implement in 20 min. I know we are trying to
be time economical here. So in the interest of resolving this and moving
forward, can I just add the necessary decorators and FutureWarnings?
—
Reply to this email directly, view it on GitHub
<#7754 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAQQHEYVBO2WXGJ7LRBEI32XLB7HAVCNFSM6AAAAABY6GLEA6VHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHMZDOMZTGQZTAOJSHA>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
How about merging this one - and doing another PR with the deprecation
changes?
I should also say - I don't feel super-strongly either way.
@stefanv - perhaps you can be a tie-breaker?
|
Co-authored-by: Marianne Corvellec <[email protected]>
mkcor
left a comment
There was a problem hiding this comment.
I have left only minor stylistic suggestions/questions. Thank you!
| ), | ||
| ) | ||
| def test_affine_params(pts, params): | ||
| # Test AffineTransform against docstring algorithm. |
matthew-brett
left a comment
There was a problem hiding this comment.
Thanks - applied - I believe.
Co-authored-by: Marianne Corvellec <[email protected]>
Co-authored-by: Marianne Corvellec <[email protected]>
Co-authored-by: Marianne Corvellec <[email protected]>
Co-authored-by: Marianne Corvellec <[email protected]>
|
Since @stefanv is currently afk, I'm going to merge this one, and @matthew-brett could you please open an issue to check on the deprecation before releasing 0.26? |
* Refactor transform initialization Standardize and refactor initialization of transform classes by adding new _MatrixTransform base class, and formalizing rules for dimensionality. Add and test `identity` class constructor. Deprecate use of scalar ``scale`` parameter with dimensionality=3. In due course, perhaps warn on use of `dimensionality` when calling class constructor with other arguments, to avoid confusion, and prefer ``identity`` class constructor. * Allow for negative zero in doctest output Maybe a result of platform linalg etc differences. * Refector center_and_normalize points matrix (To my eye) Simplify logic in creating homogenous transform matrix for center and normalize points routine. Also work round ruff's rather extreme reformatting of an array literal. * Refactor homogenous matrix application to function Move application of homogenous matrix application to its own function. Use instead of `_apply_mat` method. * Rotation was missing from EssentialMatrixTransform This also shows that we are not testing this properly. * Add test of rotations and EssentialMatrixTransform Check some error messages. Cover case of not-identity rotation. * Add test against AffineTransform docstring Test AffineTransform constructor against 2D algorithm in docstring. * Add test of AffineTransform application order Confirm that order of application of transforms is as specified in the docstring. It is. But - oh dear - that's a different order than the parameter position. We'll need to address that at some point. * Extend test of parameter application order Test rigid and scale + rigid transforms. Document transformation order. * Make implicit parameters keyword only As discussed on Signal. Also, reorder parameters for AffineTransform, that did not match the order of application. * Docstrings for identity transform. * Add test for kw-only parameters Test that transforms with implicit parameters must be passed as keyword arguments. * Rewrite identity docstrings * Refactor assembly of rotation matrix * Refactor init checking Simplify code somewhat, and extend tests. * Extend tests, adapt inits to docstring * Move calculation after check for efficiency * Drop unnecessary check for PolynomialTransform * Move tolerances to private class attribute Responding to Juan's comments. * Fix spelling of homogeneous Responding to JNI's comments. * Convert DeprecatonWarning to FutureWarning From JNI review. * Expand documentation of array_like for function From review by JNI. * Add and use _as_h helper routine ``_as_h`` function adds ones to right of (n, d) points array. * Update skimage/transform/_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Update skimage/transform/_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Formalize tests on broadcasting. * Raise for identity on base class NotImplementedError - inheritors must define the identity. * Update skimage/transform/_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Update skimage/transform/_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Rename homogeneous coordinate helper function From review by @lagru and @Joi. * Single backticks for parameter in error message From @mkcor review. * Rework `dimensionality` docstring. From @mkcor review. * Convert base class `identity` to abstract method Move from NotImplemented to abstract class method. * Update skimage/transform/_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Fix docstring typo in skimage/transform/_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Update skimage/transform/tests/test_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Update skimage/transform/tests/test_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Update skimage/transform/tests/test_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Update skimage/transform/tests/test_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> --------- Co-authored-by: Marianne Corvellec <[email protected]> Co-authored-by: Juan Nunez-Iglesias <[email protected]>
* Refactor transform initialization Standardize and refactor initialization of transform classes by adding new _MatrixTransform base class, and formalizing rules for dimensionality. Add and test `identity` class constructor. Deprecate use of scalar ``scale`` parameter with dimensionality=3. In due course, perhaps warn on use of `dimensionality` when calling class constructor with other arguments, to avoid confusion, and prefer ``identity`` class constructor. * Allow for negative zero in doctest output Maybe a result of platform linalg etc differences. * Refector center_and_normalize points matrix (To my eye) Simplify logic in creating homogenous transform matrix for center and normalize points routine. Also work round ruff's rather extreme reformatting of an array literal. * Refactor homogenous matrix application to function Move application of homogenous matrix application to its own function. Use instead of `_apply_mat` method. * Rotation was missing from EssentialMatrixTransform This also shows that we are not testing this properly. * Add test of rotations and EssentialMatrixTransform Check some error messages. Cover case of not-identity rotation. * Add test against AffineTransform docstring Test AffineTransform constructor against 2D algorithm in docstring. * Add test of AffineTransform application order Confirm that order of application of transforms is as specified in the docstring. It is. But - oh dear - that's a different order than the parameter position. We'll need to address that at some point. * Extend test of parameter application order Test rigid and scale + rigid transforms. Document transformation order. * Make implicit parameters keyword only As discussed on Signal. Also, reorder parameters for AffineTransform, that did not match the order of application. * Docstrings for identity transform. * Add test for kw-only parameters Test that transforms with implicit parameters must be passed as keyword arguments. * Rewrite identity docstrings * Refactor assembly of rotation matrix * Refactor init checking Simplify code somewhat, and extend tests. * Extend tests, adapt inits to docstring * Move calculation after check for efficiency * Drop unnecessary check for PolynomialTransform * Move tolerances to private class attribute Responding to Juan's comments. * Fix spelling of homogeneous Responding to JNI's comments. * Convert DeprecatonWarning to FutureWarning From JNI review. * Expand documentation of array_like for function From review by JNI. * Add and use _as_h helper routine ``_as_h`` function adds ones to right of (n, d) points array. * Update skimage/transform/_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Update skimage/transform/_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Formalize tests on broadcasting. * Raise for identity on base class NotImplementedError - inheritors must define the identity. * Update skimage/transform/_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Update skimage/transform/_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Rename homogeneous coordinate helper function From review by @lagru and @Joi. * Single backticks for parameter in error message From @mkcor review. * Rework `dimensionality` docstring. From @mkcor review. * Convert base class `identity` to abstract method Move from NotImplemented to abstract class method. * Update skimage/transform/_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Fix docstring typo in skimage/transform/_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Update skimage/transform/tests/test_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Update skimage/transform/tests/test_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Update skimage/transform/tests/test_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> * Update skimage/transform/tests/test_geometric.py Co-authored-by: Marianne Corvellec <[email protected]> --------- Co-authored-by: Marianne Corvellec <[email protected]> Co-authored-by: Juan Nunez-Iglesias <[email protected]>
## Description This is the promised follow-on PR from the discussion more or less starting at #7754 (comment). To recap briefly: There is a wart in the current API, in that the `estimate` method of Transforms ignores any stored state of the initialized Transform, and re-estimates. So, for example: ```python tf = SimilarityTransform(rotation=(0.2, 0.3)) tf.estimate(src, dst) ``` wil discard the initial state with the specified rotations. Another problem is that one generally has to first initialize the transform, most sensibly as an identity transform, and then estimate, thus: ```python tf = SimilarityTransform.identity(3) tf.estimate(src, dst) ``` However, the argument to the identity transform is redundant, because we can infer the dimensionality of the points (3) from `src` and `dst` point arrays. So, this PR deprecates the `estimate` method, in favor of a class constructor, like this: ```python tf = SimilarityTransform.from_estimate(src, pts) ``` ### For discussion * We floated various names for the class constructor. `from_estimate` gained favor from me (MB) (I proposed it) and @stefanv. @jni leaned towards some fancy footwork to allow `estimate` to be *both* the original estimation method, *and* the class constructor, using the [trick here](https://stackoverflow.com/questions/20533349/python-regular-method-and-static-method-with-same-name/28766809#28766809). I [thought that was going to be confusing](#7754 (comment)). We can rehearse that argument more. * In writing the tests, I (MB) discovered an error in the check for a degenerate tranform, [discussed here](#7770). I used an obvious but quite possibly wrong workaround (mentioned in the linked issue). I would be happy to be corrected. * One wrinkle is that the `from_estimate` API returns `None` for failed estimation. This is generally just as informative as the `estimate` method, except in the case of `PiecewiseAffineTransform`, where the estimation involves multiple steps, and resulting affines. The `estimate` method collects an affine for each step, recording an all-NaN affine for steps that fail estimation, so you can track, from the resulting `tform.affines` attribute, where the estimation failed. The returned `None` does not give you that granular information. Is there a use for that information? How should it reach the user? * Should we also adapt the API-a-like classes `LineModelND`, `CircleModel`, `EllipseModel` at https://github.com/scikit-image/scikit-image/blob/main/skimage/measure/fit.py#L27 to have a `from_estimate` class method (instead of an `esimate` method)? * Similarly, should we also adapt the `ThinPlateSpline` class (https://github.com/scikit-image/scikit-image/blob/main/skimage/transform/_thin_plate_splines.py#L7)? (I think so). ## Release note For maintainers and optionally contributors, please refer to the [instructions](https://scikit-image.org/docs/stable/development/contribute.html#documenting-changes) on how to document this PR for the release notes. ```release-note In `skimage.measure`, add a new class method and constructor `from_estimate` for `LineModelND`, `CircleModel`, and `EllipseModel`. This replaces the old more un-intuitive API that used the now deprecated `estimate` method, which required initalizing a model with undefined parameters before calling `estimate`. ``` ```release-note In `skimage.transform`, add a new class method and constructor `from_estimate` for `AffineTransform`, `EssentialMatrixTransform`, `EuclideanTransform`, FundamentalMatrixTransform`, `PiecewiseAffineTransform`, `PolynomialTransform`, `ProjectiveTransform`, `SimilarityTransform`, and `ThinPlateSplineTransform`. This replaces the old more un-intuitive API that used the now deprecated `estimate` method, which required initalizing an undefined transform before calling `estimate`. ``` --------- Co-authored-by: Lars Grüter <[email protected]> Co-authored-by: Stefan van der Walt <[email protected]>
|
I tried to make the release notes a bit more end user friendly in the context of our release notes. Will be updated in #7967. |
Standardize and refactor initialization of transform classes, formalize rules for specifying dimensionality.
API break: implicit (scale, shear, rotation, translation) parameters are now all keyword-only, to avoid confusion from differences in previous argument ordering and application order of the transforms.
Add and test
identityclass constructor.Deprecate use of scalar
scaleparameter with dimensionality=3.In due course, perhaps warn on use of
dimensionalitywhen calling class constructor with other arguments, to avoid confusion, and preferidentityclass constructor.Release note