Skip to content

Add SPIFF correction#799

Open
putnokiabel wants to merge 11 commits into
soft-matter:masterfrom
putnokiabel:spiff
Open

Add SPIFF correction#799
putnokiabel wants to merge 11 commits into
soft-matter:masterfrom
putnokiabel:spiff

Conversation

@putnokiabel

@putnokiabel putnokiabel commented Jan 26, 2026

Copy link
Copy Markdown

Closes #695. Closes #413.

Feature

Based on https://www.nature.com/articles/s41598-017-14166-6?WT.feed_name=subjects_optical-manipulation-and-tweezers#Sec9 .
Use SPIFF to remove pixel-locking bias and improve sub-pixel accuracy of located features.
In the test cases shown (see test_spiff.py), you can see an order of magnitude better accuracy when the SPIFF correction is applied.

Sample results

Verify by adding the following line in test_spiff.py and running the tests:
image

2D
error before: 0.1300
error after SPIFF: 0.01998

3D
error before SPIFF: 0.2486
error after SPIFF: 0.0227

Note on documentation and further integration

I did not invest time into updating the documentation and further integrating it into the basic features yet, as I wanted to see what the maintainers think of the feature first.

Since it seems like SPIFF improves accuracy pretty much universally, I'd suggest the following (but feel free to ignore or suggest something else):

  • Include an apply_spiff argument in tp.locate() and tp.batch() (could be enabled or disabled by default) and have those functions apply the SPIFF correction so the user doesn't have to know or think about it necessarily.
  • Add documentation on using the new apply_spiff argument as well as the underlying apply_spiff_correction function.
  • Update documentation (and walkthrough example) on subpixel bias to mention that using SPIFF is preferred to increasing the location radius (see the paper mentioned in the apply_spiff_correction method for more details on this).

@putnokiabel putnokiabel marked this pull request as ready for review January 26, 2026 22:13
@putnokiabel

Copy link
Copy Markdown
Author

@nkeim
Hi, I'm new here!
Is there a process for contributing to trackpy (other than just submitting a PR and waiting)?

@nkeim

nkeim commented Feb 5, 2026 via email

Copy link
Copy Markdown
Contributor

@nkeim nkeim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks great! See comments.

I'm open to adding a spiff option to locate and batch. We'd have to be deliberate about the behavior when there aren't enough features.

  • Perhaps the values could be True, False, and None (or 'auto')? Only True would raise a warning if there were too few features.
  • I'd prefer to leave it turned off by default in the next trackpy release. We can add it to the walkthrough to let people experiment with it. But you're right that making it the default in a later release could make sense — the performance penalty is small, and it's (currently) hard to imagine cases where the correction would be unwelcome.

Comment thread trackpy/tests/test_spiff.py Outdated
Comment thread trackpy/spiff.py Outdated
Comment thread trackpy/spiff.py Outdated
(as opposed to applying this function for each individual frame).
If f contains less than 100 features, f is returned as-is, due to lack of data.
"""
if len(f) < 100:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How did you settle on 100?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It's not based on data or experiments, just intuition. I extracted it to a variable and commented on this to make the assumption explicit (and invite further optimization). I also reduced the requirement from 100 to 50.

My reasoning behind the number is:

  • 50-100+ features seems clearly enough to make a decent correction (at least to an extent that the result is better than without a correction).
  • 5 features seems clearly not enough, to an extent that applying the correction might make the results worse.
  • 10-20 features would probably, on average, make results slightly better, but might make results worse in a few cases
  • Prefer going for a conservative requirement (higher minimum number of features) so that we know we practically never make the results worse by applying a correction

To optimize this even further, instead of optimizing the minimum number of features, I'd sooner go for something more elaborate, like:
Not just considering the number of features, but also the distribution of the sub-pixel values across features. That way, we could lower the minimum number of features (e.g. from 50 to 10), if some basic assumptions are met, for example:

  • the subpixel values are somewhat symmetric across the center of the pixel.
  • There is some minimum spread (or minimum standard deviation) to the subpixel values, e.g. if we only have 10 features, and some of the subpixel values are very close to each other or even equal, we are perhaps better off not doing the correction.

I'd prefer to keep further optimization of the minimum feature requirement outside the scope of this PR (and go for a conservative limit for now) to see how well the correction actually works for the users of trackpy (across different usecases), then optimize the feature requirement further in a future PR. I could create separate Github issues for further optimizing the SPIFF correction.

@nkeim

nkeim commented May 14, 2026

Copy link
Copy Markdown
Contributor

I thought of one reason not to make SPIFF automatic: it can mask a poor choice of locate parameters. SPIFF works best when the corrections are small—and it does not correct other data like mass and eccentricity.

At least until we think of something better, it's probably best to let users decide when the raw coordinates are good enough, and then apply the correction before linking—and after applying cuts on eccentricity, etc. to remove spurious particles.

@putnokiabel

Copy link
Copy Markdown
Author

Thank you @nkeim for the review!
I applied your code suggestions, and added a spiff option to locate and batch (with True, False and 'auto' options, defaulting to False for now).
I reduced the minimum feature requirement from 100 -> 50, this is subject to further optimization.

@putnokiabel putnokiabel requested a review from nkeim May 15, 2026 13:05
Comment thread trackpy/api.py Outdated
Comment thread trackpy/feature.py Outdated
Comment thread trackpy/feature.py Outdated
``'auto'``, the correction is applied silently when there are
enough features, and skipped otherwise. Pooling features across
many frames is the recommended way to use SPIFF. Note that this
argument is not compatible with ``output``.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good catch. But, if output is enabled, why not apply spiff to individual frames? As an example, my group typically uses streaming not because there are too many frames, but because each frame has O(10^5) features.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point! I updated this to apply spiff to individual frames when output is enabled.

Future work:
We could split apply_spiff_correction into two methods, one of which creates the SPIFF function based on the data, and the other actually applies the SPIFF function to data.
This way we could apply SPIFF frame-by-frame but update the SPIFF function with additional data every frame so that the correction gets more and more accurate as there as more frames.

@nkeim

nkeim commented May 18, 2026

Copy link
Copy Markdown
Contributor

@putnokiabel I just merged #803 so that the CI tests pass. If you can, please rebase your branch onto upstream/master so we can run the checks.

Now that we can see it implemented, I want to check our reasoning one last time. locate and batch already have too many options :). By adding spiff as an option, we are treating it as a kind of post-processing that could be done as a separate step, but that many users will want to do routinely. I think the only other examples are minmass, maxsize, and topn, so that is a very selective group!

The argument against is what I stated above: while you're trying to figure out diameter etc., SPIFF removes an important signal that you should be optimizing. Would it be appropriate to caution users in the docstring? Should we just make a point of discussing this in the walkthrough?

The argument for is that you typically optimize your parameters on a single frame, and then you run feature-finding on all frames. So you'd only use spiff=True on the subsequent invocations of locate(). And, spiff is a big convenience for batch() with streaming, because otherwise you have to write a custom after_locate() function.

So, at this point I think we're making the right choice, but adding kwargs is serious business, so I wanted to lay out the reasoning and make a little room for debate!

@putnokiabel

Copy link
Copy Markdown
Author

@nkeim
I understand the concern about adding this as a (default) kwarg, leading to users not optimizing diameter as the issue is masked to some extent.

I do think that the way of optimizing diameter based on subpixel bias that's recommended in the docs should likely be updated / nuanced if we merge this (or at least when we enable SPIFF correction by default).
The logic behind optimizing diameter based on subpixel bias (as I understand) is that you take a larger-than-necessary diameter in order to fix sub-pixel bias by including neighbouring "background" pixels.
The issue here is that the "background" pixels have noise and may even have other features/particles (in a dense system where features/particles are quite close to each other and may even come into contact).
The paper I used (https://www.nature.com/articles/s41598-017-14166-6?WT.feed_name=subjects_optical-manipulation-and-tweezers#Sec2) discusses this in the Results section, the outcome is especially visible on Figure 1 and Figure 3.
In Figure 3 you see that diameter optimization has a huge effect on tracking error without SPIFF (R=1 has a significantly higher error rate than R=2), but has a much smaller impact with SPIFF (R=1 has a slightly higher error rate than R=2 but the difference is barely noticeable, and in either case much smaller than both versions without SPIFF).
In Figure 1 and 2 you see the limitation in increasing diameter in order to "fix" sub-pixel bias. A higher diameter leads to less sub-pixel bias, but becomes less accurate because:

  • nearby particles may be interpreted as a single particle (figure 1), and
  • even when nearby particles are interpreted as separate, the higher diameter means that the algorithm will show the particles as being closer to each other than they actually are (figure 2).

As I understand it, in the ideal case, we don't optimize diameter to reduce subpixel bias, but determine the optimal diameter by other means (leading to a smaller diameter and more subpixel bias) after which the subpixel bias is corrected by SPIFF.
In other words, especially in dense systems (where there are particles close to each other), if there is no significant subpixel bias before applying SPIFF, the diameter is actually too high and should likely be reduced.

I mentioned the diameter should likely be determined by other means. I'm not sure on what other means would be best at the moment.
One way would be "visually" (look at a few particles, and determine their diameter in pixels): this may not be super accurate (especially if the particles are not completely uniform in shape & size) as there is an element of particle choice and subjective measurement, but it allows for an informed tradeoff (e.g. in denser systems you might choose a slightly lower diameter to prevent neighboring particles from affecting tracking, whereas in a sparser system you might choose a slightly higher diameter for more accurate measurements, since the chances of particles being very close to each other is small).
Alternatively, one could run an optimization function (e.g. from scipy.optimize) that picks the diameter by minimizing the error function based on a prior assumption on the feature shape / the feature's pixel intensity distribution (e.g. it would need to optimize differently if the features are a circle that's completely filled with a sharp outline, if feature intensity has a Gaussian distribution, or a different distribution).
I can totally see this as a useful tool that trackpy could offer, where users would go through this process:

  1. Call trackpy.optimize_diameter(frames, diameter_estimate, min_diameter=diameter_estimate/2, max_diameter=diameter_estimate*2, shape='gaussian') (or similar)
  2. Do a visual check of the diameter (trackpy could provide a function that randomly displays n particles with a ring of a certain diameter around them)
  3. Run track or batch with the chosen diameter.

That said, diameter optimization is a somewhat unrelated issue, that, for now, could be addressed with updating documentation / warning users to first optimize parameters before enabling SPIFF. (especially since the importance of diameter optimization seems to slightly get less when spiff is enabled)

I do believe the benefit of adding this as a (disabled-by-default) argument outweighs the drawback of some users not optimizing the diameter as well, but happy to change it if you think otherwise!
And agree on it being a post-processing step that users could separately themselves, but it being a useful added argument since it seems like something most users would want to do most of the time.

I could either:

  • Update the docstrings to mention the need to do parameter optimization before enabling spiff
  • Update the docs (I'd need some pointers on this as I'm not sure where/how docs are updated, I haven't seen them in this repo?). Though this is somewhat more sensitive and it might be better for someone with more experience and domain knowledge to update them.
  • Remove the parameter from locate and batch (in which case SPIFF would need to be mentioned in the docs in order for users to find out about it).

What do you think?

@nkeim

nkeim commented May 22, 2026

Copy link
Copy Markdown
Contributor

Thanks! It sounds like this should be an update to the walkthrough notebook in the trackpy-examples repo. There just needs to be different guidance about choosing diameter. I'm still in favor of doing the first locate pass without spiff, just to keep things simple for novices and avoid the impression that SPIFF is a magical black box. We should save for later the (brief) explanation of SPIFF and the importance of having enough features. And we can cite the journal articles for further reading.

I think this PR is ready to merge once the walkthrough changes are drafted!

One last thing: Could the journal article citations please be in the docstring of apply_spiff_correction? That way they are surfaced in the API reference docs. Those are helpful papers!

@putnokiabel

Copy link
Copy Markdown
Author

@nkeim
Sounds good! I've updated the docstring to include a proper citation.

I also created soft-matter/trackpy-examples#66 with an updated walkthrough.

Do you think a method to optimize the diameter automatically (like I mentioned in my previous comment) would be a useful addition to trackpy? In that case I'll create an issue (and file a separate PR for this).

@nkeim nkeim self-requested a review June 12, 2026 20:58

@nkeim nkeim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I have this one last change to suggest. We might also consider renaming apply_spiff_correction to be shorter, in keeping with most other API function names in trackpy. Do you have an idea? spiff_correct? If not, it can be a separate PR :).

Comment thread trackpy/feature.py Outdated
@putnokiabel

Copy link
Copy Markdown
Author

@nkeim good point! I updated the function name to just apply_spiff (both here and in the walkthrough) which matches the function parameters and is semantically more correct (since the last "f" stands for function, so applying a function is sufficient context).

Alternatively it could be called correct_pixel_locking which is more intuitive / self-explanatory for people who are not familiar with SPIFF, but on the other hand is again a longer name.

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.

Feature request: Implementing SPIFF to improve subpixel accuracy new approach for correcting errors in particle localization

2 participants