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 thin-plate splines warping #7040

Open
wants to merge 136 commits into
base: main
Choose a base branch
from

Conversation

decorouz
Copy link
Contributor

Description

Tracks progress for the TPS Outreachy project.

cc @ana42742, @mkcor

Checklist

ana42742 and others added 18 commits June 8, 2023 16:46
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>
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>
@lagru lagru changed the title Dev tps warping Add thin-plate splines warping Jun 27, 2023
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
Copy link
Member

@lagru lagru left a 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. ;)

skimage/transform/_thin_plate_splines.py Outdated Show resolved Hide resolved
skimage/transform/_thin_plate_splines.py Outdated Show resolved Hide resolved
skimage/transform/_thin_plate_splines.py Outdated Show resolved Hide resolved
skimage/transform/_thin_plate_splines.py Outdated Show resolved Hide resolved
skimage/transform/__init__.pyi Outdated Show resolved Hide resolved
skimage/transform/_thin_plate_splines.py Outdated Show resolved Hide resolved
Co-authored-by: Lars Grüter <lagru+github@mailbox.org>
@@ -0,0 +1,147 @@
r"""
==========================================
Interpolate images with thin-plate splines
Copy link
Member

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

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 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...

Copy link
Contributor Author

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)

Copy link
Member

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"?

Copy link
Member

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?").

doc/examples/transform/plot_tps_deformation.py Outdated Show resolved Hide resolved
doc/examples/transform/plot_tps_deformation.py Outdated Show resolved Hide resolved
doc/examples/transform/plot_tps_deformation.py Outdated Show resolved Hide resolved
doc/examples/transform/plot_tps_deformation.py Outdated Show resolved Hide resolved
doc/examples/transform/plot_tps_deformation.py Outdated Show resolved Hide resolved
decorouz and others added 2 commits March 17, 2024 19:27
Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org>
Copy link
Member

@mkcor mkcor left a 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?

@jarrodmillman jarrodmillman removed this from the 0.23 milestone Apr 10, 2024
@lagru
Copy link
Member

lagru commented Apr 11, 2024

@decorouz do you mind if I move this to the skimage.future module or do you want to give it a try? :)

@decorouz
Copy link
Contributor Author

@decorouz do you mind if I move this to the skimage.future module or do you want to give it a try? :)

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.
@lagru
Copy link
Member

lagru commented Apr 17, 2024

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.

@lagru
Copy link
Member

lagru commented Apr 17, 2024

@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 tps_warp's interpolation_order parameter. I think the previous version was incorrect.

@lagru lagru requested a review from mkcor April 17, 2024 11:34
Copy link
Member

@mkcor mkcor left a 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! 🚀

doc/examples/transform/plot_tps_deformation.py Outdated Show resolved Hide resolved
doc/examples/transform/plot_tps_deformation.py Outdated Show resolved Hide resolved
doc/examples/transform/plot_tps_deformation.py Outdated Show resolved Hide resolved
skimage/future/_thin_plate_splines.py Outdated Show resolved Hide resolved
doc/examples/transform/plot_tps_deformation.py Outdated Show resolved Hide resolved
Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org>
@imagesc-bot
Copy link

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

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.

Thanks for all the hard work @decorouz, and @lagru and @mkcor for guidance.

@@ -0,0 +1,147 @@
r"""
==========================================
Interpolate images with thin-plate splines
Copy link
Member

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"?

Comment on lines +25 to +32
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.
Copy link
Member

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)
Copy link
Member

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.

Copy link
Member

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...

Copy link
Contributor Author

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)

Copy link
Member

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(
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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

Copy link
Member

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.

Comment on lines 6 to 9
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.
Copy link
Member

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?

skimage/future/_thin_plate_splines.py Outdated Show resolved Hide resolved
skimage/future/_thin_plate_splines.py Outdated Show resolved Hide resolved
skimage/future/_thin_plate_splines.py Outdated Show resolved Hide resolved
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)
Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

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?

Copy link
Member

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 😃)

Copy link
Member

Choose a reason for hiding this comment

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

Parameters
----------
r : (4, N) ndarray
Input array representing the norm distance between interlandmark
Copy link
Member

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.

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

Successfully merging this pull request may close these issues.

None yet

8 participants