Subpixel Accuracy
dictk.translation.locate's
own docstring has said this from the start: "Integer-pixel precision
only; subpixel refinement is out of scope for now." Simple Stretch
Revisited found the
concrete case where that limitation actually bites. At VIC-2D's own
point density — 5 pixels apart — most points' true stretched position
isn't an integer at all. locate can only ever report a whole pixel,
so it's necessarily wrong by some amount for those points. That's not
a tracking failure. It's the wrong question being asked.
What locate Actually Rounds Away
skimage.registration.phase_cross_correlation, the FFT-based
correlation locate runs, doesn't only find whole-pixel peaks. Its own
upsample_factor parameter refines that peak to within 1 / upsample_factor of a pixel. locate never uses it — every result gets
truncated to the nearest whole pixel with int() before it's returned.
dictk.translation.locate_subpixel
and its batch counterpart,
dictk.grid.locate_subpixel,
are new, separate functions — not a parameter added to locate itself,
so locate's own return type never changes shape based on an argument.
They pass upsample_factor straight through, and return the refined
position directly, undiscarded, as a
dictk.image.SubpixelCoordinate
— the same (x, y) shape as PixelCoordinate, but float, not int.
Measuring the Difference
Reusing Simple Stretch Revisited's own scenario — astronaut0.png,
factor_x = 1.02 — at VIC-2D's own 5-pixel spacing across the whole
image, 2862 points:
from dictk.grid import generate, locate, locate_subpixel
points = generate(
origin=PixelCoordinate(x=18, y=16),
count_x=53,
count_y=54,
spacing_x=5,
spacing_y=5,
)
true_x = [point.x * factor_x for point in points]
integer_found = locate(
reference_image=reference_image,
current_image=current_image,
reference_points=points,
kernel_margin_width=20,
kernel_margin_height=20,
search_margin_width=48,
search_margin_height=52,
)
subpixel_found = locate_subpixel(
reference_image=reference_image,
current_image=current_image,
reference_points=points,
kernel_margin_width=20,
kernel_margin_height=20,
search_margin_width=48,
search_margin_height=52,
upsample_factor=10,
)
| Method | Mean abs error (px) | Max abs error (px) |
|---|---|---|
locate() (integer, truncated) | 0.2627 | 0.5600 |
locate_subpixel(upsample_factor=1) | 0.2627 | 0.5600 |
locate_subpixel(upsample_factor=10) | 0.0940 | 0.2600 |
locate_subpixel(upsample_factor=100) | 0.0885 | 0.2600 |
locate() lands on the exact expected integer pixel for 1479/2862 points (52%) — the same interpolation confound Simple Stretch Revisited found and worked around, not a new one.
upsample_factor=1 matches locate's own error exactly — no
refinement requested, none applied. upsample_factor=10 cuts the mean
error by roughly a third. upsample_factor=100 barely improves on
10 — diminishing returns past that point, for this scenario.
Why This Isn't "Fixing" the Exact-Match Problem
upsample_factor does not make locate's own truncated answer more
often correct. If the true target is 64.26, no amount of refinement
turns that into a whole number — locate_subpixel reports something
close to 64.26 itself, not 64 or 65 more reliably. Simple
Stretch Revisited
solved a different problem: it kept every point's restricted to
values where the true target genuinely is an integer, so locate
could report it exactly. This page accepts that most targets, at this
density, aren't integers at all, and asks how close tracking gets to
the real one instead. Two different, both legitimate, answers to the
same density problem.
This closes Path Forward's own
Postponed subpixel-accuracy item — real displacements don't land on
exact pixels, and now dictk has a way to track them without
pretending otherwise. High Point
Density picks this up next, at a density
Simple Stretch Revisited's own integer-safety constraint couldn't
reach.