-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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 thin-plate splines warping #7040
base: main
Are you sure you want to change the base?
Conversation
Adds the initial files and code from scikit-image#7001 which itself is the based on this gist https://gist.github.com/zpincus/be1e2293ec0d679b0f6d3e600ac8fccc with a few added modifications.
* Apply PEP08 guidelines * Format signature of warp_images --------- Co-authored-by: Lars Grüter <lagru+github@mailbox.org>
Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org>
…kit-image into dev-tps-warping
Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org>
* Optimize function * Update skimage/transform/_thin_plate_splines.py Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org> * update scipy module reference * Handle divide-by-zero error * Update make_l_matrix and add docstring * Add _coeffs function * Update _calculate_f implementation * Update _make_warp function * Fix shift by 1 pixel bug * Update existing implementation * Remove comment --------- Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org> Co-authored-by: Lars Grüter <lagru+github@mailbox.org>
…kit-image into dev-tps-warping
…to dev-tps-warping
Change parameter names Tp accept a single image as input instead of list of images Add exceptions catch errors Expose _tps_transform function Add example to tps_transform docstring
189af97
to
1994bda
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great progress and work! I've added a few comments to keep you busy. ;)
Co-authored-by: Lars Grüter <lagru+github@mailbox.org>
@@ -0,0 +1,147 @@ | |||
r""" | |||
========================================== | |||
Interpolate images with thin-plate splines |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder how fitting this title is... When I read "Interpolate images (...)," I expect that images with missing or hidden regions would be reconstructed with a given interpolation method, along the lines of the cornea image restoration example. Instead, at first glance, this example looks more like "Deform an image with thin-plate splines" to me. What do you think? /cc @lagru
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might get into semantics but I think interpolation just means something like finding new values based on existing often neighboring data. That's not necessarily reconstruction only but also deformation / remapping, / relocating. I don't find "deform" quite fitting if TPS can also be used to shift or rotate an image. So I'm okay with "interpolate" being the generalized term. Whether this gallery example demonstrates all use cases of "interpolation" is another thing...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example demonstrate interpolating a 2D image over a set src
and dst
points.
@mkcor I update this section based on #7040 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You cannot technically "interpolate images", so how about something like "Image warping using thin-plate splines"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following the convention of writing titles at the imperative mood, I'd suggest either "Warp images using thin-plate splines" or "Use thin-plate splines to warp images" (or "Use thin-plate splines for image warping?").
Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to improve the writing of the gallery example in subsequent PRs.
As suggested by @lagru in yesterday's community call, we could move this new transform into the future
submodule, so we don't commit to specific API choices for now, but users can start using it (and possibly provide feedback). @stefanv what do you think?
@decorouz do you mind if I move this to the |
Please go ahead and move it. |
So we don't commit to specific API choices for now, but users can start using it and possibly provide feedback.
The CI failure is due to a warning while building the docs on Windows. Not sure why that's showing up, but it seems completely unrelated to this. |
Forgot this in the previous commit.
@decorouz, @mkcor I made another pass at the gallery example and docstrings. I've put it through https://languagetool.org and also tried to improve the clearness in a few places, especially the descriptions of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've left only minor suggestions! 🚀
Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org>
This pull request has been mentioned on Image.sc Forum. There might be relevant details there: https://forum.image.sc/t/equivalent-for-matlabs-piecewiselineartransformation2d/51035/12 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -0,0 +1,147 @@ | |||
r""" | |||
========================================== | |||
Interpolate images with thin-plate splines |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You cannot technically "interpolate images", so how about something like "Image warping using thin-plate splines"?
Image deformation implies displacing the pixels of an image relative to one another. | ||
In this example, we deform the (2D) image of an astronaut by using thin-plate splines. | ||
In our image, we define 6 source and target points labeled "1-6": "1-4" are found near | ||
the image corners, "5" near the left smile corner, and "6" in the right eye. | ||
At the "1-4" points, there is no displacement. | ||
Point "5" is displaced upward and point "6" downward. | ||
|
||
We use TPS as a very handy interpolator for image deformation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The phrase interpolation is rather confusing here, since we do two different types of interpolation during image warping. (1) Interpolation of the warp model and (2) interpolation of pixel values. The first is often not even called interpolation, so I'd avoid the term in that context.
|
||
# Fit the thin-plate spline from source (src) to target (dst) points | ||
|
||
warped_img = ski.future.tps_warp(astronaut, src[:, ::-1], dst[:, ::-1], grid_scaling=1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can we not fit TPS within the existing transformation framework?
Estimate model + warp
should be sufficient, and better not to have multiple ways of doing the same thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing TpsTransform()
to warp
already seems to work. Though, I notice that one needs to switch src
and dst
to receive the same results:
import numpy as np
import skimage as ski
image = np.array(
[[9, 9, 9, 9, 9, 9, 9],
[9, 0, 0, 0, 0, 0, 9],
[9, 0, 1, 1, 1, 0, 9],
[9, 0, 1, 2, 1, 0, 9],
[9, 0, 1, 1, 1, 0, 9],
[9, 0, 0, 0, 0, 0, 9],
[9, 9, 9, 9, 9, 9, 9]],
dtype=float,
)
# Zoom in
src = np.array([[2, 2], [2, 4], [4, 4], [4, 2]])
dst = np.array([[1, 1], [1, 5], [5, 5], [5, 1]])
tform = ski.future.TpsTransform()
tform.estimate(src=dst, dst=src) # switched src & dst!
using_warp = ski.transform.warp(image, tform)
using_tps_warp = ski.future.tps_warp(image, src=src, dst=dst)
np.testing.assert_array_equal(using_warp, using_tps_warp)
However, tps_warp
provides grid_scaling
as an optional performance optimization at the cost of accuracy. Would you recommend integrating that into ski.transform.warps
? It's an optimization we took from the reference implementation...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, the need to switch src
and dst
we took from the reference implementation
https://github.com/zpincus/celltool/blob/045920089f88a445fa2483500ef867d267ecef13/celltool/numerics/image_warp.py#L41-L43
# make the reverse transform warping from the to_points to the from_points, because we
# do image interpolation in this reverse fashion
transform = _make_warp(to_points, from_points, x, y)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can make scaling a property of the ThinPlateSpline transform.
return U | ||
|
||
|
||
def tps_warp( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we get rid of this method? warp
should already provide its functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #7040 (comment) above on the parameter grid_scaling
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the same discussion: let's go with warp
instead of adding tpw_warp
. We can look into whether we can integrate the grid_scaling
optimization in warp
itself but it is not that essential to memory optimization (@stefanv can explain that better than me).
I'm not sure when I will get time to look into implementing that right now. @decorouz, feel welcome give it a try if you have time and incentive. :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For each pixel in the output image, we need to find out where in the input image to "fetch" it from. We can do that by computing a coordinate transformation for every pixel. That can be done per pixel (very low memory consumption), but in Python we typically vectorize it, i.e. use a bunch of memory to store the coordinate map (output coord -> input coord).
I haven't looked at this carefully, but I suspect the optimization used here is to generate a smaller coordinate map, and to then trick map_coordinate
into interpolating the coordinates as needed. Less accurated, but smaller memory consumption, and perhaps, due to caching, a little bit faster.
A conventional technique for interpolating surfaces over a set of data points | ||
are thin-plate splines (TPS) [1]_ [2]_. | ||
In an image context, given pairs of source and target control points, TPS can | ||
be used to transform a space, which, in our case, is a 2D image. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this information is too scant.
I'd start with something along the lines of:
To warp an image, we start with a set of source and target coordinates.
The goal is to deform the image such that the source points move to the target locations.
Typically, we only know the target positions for a few, select source points.
To calculate the target positions for all other pixel positions, we need a model.
Various such models exist, such as affine or projective transformations.
At this point, we can refer the reader to https://scikit-image.org/docs/stable/auto_examples/transform/plot_transform_types.html
Most transformations are linear (i.e., they preserve straight lines), but sometimes
we need more flexibility. One model that represents a non-linear transformation,
i.e. one where lines can bend, is thin-plate splines.Thin-plate splines draw on the analogy of a metal sheet, which has inherent rigidity.
Consider our source points: each has to move a certain distance, in both the x and y directions, to land in their corresponding target positions.
First, examine only the x coordinates.
Imagine placing a thin metal plate on top of the image.
Now bend it, such that at each source point, the plate's z offset is the distance,
positive or negative, that that source point has to travel in the x direction
in order to land in its target position.
The plate resists bending, and therefore remains smooth.
We can read offsets for coordinates other than source points from the position of the plate.
The same procedure can be repeated for the y coordinates.This gives us our thin-plate spline model that maps any
(x, y)
coordinate to a target position.
We should also refer the reader to an explanation of how that warping is done (output to input image, using e.g. bilinear interpolation). @jni I recall we wrote up a description of that somewhere; Elegant SciPy? skimage tutorials?
L[:n, -3:] = P | ||
L[-3:, :n] = P.T | ||
V = np.concatenate([dst, np.zeros((d + 1, d))]) | ||
self.spline_mappings = np.dot(np.linalg.inv(L), V) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explicitly inverting matrices is seldom a good idea. Use linalg.solve
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, perhaps better not to reinvent the wheel and use SciPy's RBF to do the work?
Could reduce the code here by quite a bit, if it works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember looking at it in the past, but I don't remember why we didn't follow that approach. Totally possible, that we didn't yet have a good grasp on concepts. @decorouz, do you want to give it a shot?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To add some context from a discussion @stefanv and me just had: linalg.solve
is usually more precise in calculating the inverse, while linalg.inv
is prone to small numeric errors that accumulate during the calculation. (At least that's what I understood 😃)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And faster :)
The short mathematical answer.
The longer mathematical unpacking.
Parameters | ||
---------- | ||
r : (4, N) ndarray | ||
Input array representing the norm distance between interlandmark |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are "norm distance" and "interlandmark"? Consider more common terminology here.
Description
Tracks progress for the TPS Outreachy project.
cc @ana42742, @mkcor
Checklist
./doc/examples
for new features