Cross Correlation (CC)
Cross-correlation can be used to find where point in the reference_image
can be found in the current_image.
There are many different implementations of cross-correlation. We discuss the varied implementations in Correlation Criteria. For now, it is sufficient to know only that cross-correlation is used to locate a point in a current image given a known location of that same point in a reference image. The current focus is to make the subordinate concepts underlying cross-correlation be well-defined and well-illustrated.
Let the kernel (also called a subset, filter, or
convolution matrix) be a rectangular region of reference_image centered on
, the vector that locates point from origin in the reference_image.
The kernel is a small, distinctive patch of the reference image
content that we want to locate within a subsequent image.
In the needle in a haystack idiom, the kernel is the needle, and the
haystack is current_image. To keep the search tractable, we don't
search the entire haystack — we constrain it to a search area (also
called the area of interest (AOI), search window, or scanning zone).
The search area is a subimage of the current_image, centered on a search_center, which is a guess of roughly where ended up, not the answer
itself.
While there are techniques derived from macro deformation metrics that can
provide a good first guess for the search_center, for simplicity, and since
the deformations are small, we reuse itself as the search_center
in this example.
Because the kernel and search area are themselves subimages of a larger image (see Subimage Generation), each subimage has its own local frame:
- Let be the reference frame of the kernel subimage.
- Let be the reference frame of the search area subimage.
reference_image, p0, current_image, and p1 are the same as in
Single Point Motion:
from dictk.image import read, translate, PixelCoordinate
reference_image = read(path="checkerboard0.png")
p0 = PixelCoordinate(x=100, y=75)
dx, dy = -6, 8
current_image = translate(arr=reference_image, dx=dx, dy=dy)
p1 = PixelCoordinate(x=p0.x + dx, y=p0.y + dy)
Kernel
From reference_image, extract the kernel surrounding ,
with a 25-pixel margin on every side (50x50 total):
from dictk.plot import subimage_comparison_plot
kernel_margin = 25
kernel_origin = PixelCoordinate(x=p0.x - kernel_margin, y=p0.y - kernel_margin)
subimage_comparison_plot(
image=reference_image,
origin=kernel_origin,
width=2 * kernel_margin,
height=2 * kernel_margin,
point=p0,
point_color="orange",
point_label="$P$",
subimage_label="kernel",
color="green",
origin_label="$K$",
source_origin_label="$O$",
figsize=(6.4, 4.8),
path="single_point_motion_kernel.png",
)
Saved: single_point_motion_kernel.png
reference_image centered on , with origin pixels (green dot); point itself is the orange dot at . Right: the extracted kernel, in its own local reference frame ; the same point (orange dot) is now at pixels.The kernel has its own local coordinate system , with origin at its top-left corner. Point 's position is the same in both frames, just expressed relative to a different origin:
Since the kernel is centered on with a 25-pixel margin,
pixels. Point always
sits at (kernel_margin_width, kernel_margin_height) within the kernel's
own frame, regardless of where the kernel came from in reference_image.
Search Area
From current_image, extract the search area surrounding
search_center (here, again, since it is currently our best guess), with a 50-pixel margin
on every side (100x100 total):
search_margin = 50
search_center = p0
search_origin = PixelCoordinate(
x=search_center.x - search_margin, y=search_center.y - search_margin
)
subimage_comparison_plot(
image=current_image,
origin=search_origin,
width=2 * search_margin,
height=2 * search_margin,
subimage_label="search area",
origin_label="$S$",
source_origin_label="$O$",
figsize=(6.4, 4.8),
path="single_point_motion_search.png",
)
Saved: single_point_motion_search.png
current_image centered on search_center, with origin pixels (red dot); the source image's own origin is labeled , the search area's origin is labeled . Right: the extracted search area on its own, in its own local reference frame , with origin .The search area likewise has its own local frame , origin
at its top-left corner. The location of in the current_image is given by:
The goal of the DIC process is to locate by solving for the quantity . The location of in the search area's local frame is the single unknown; all other vectors are known.
Solution
The insight into the solution is to further decompose into the sum of two additional vectors:
The second term, , is a known constant. The first term, , is unknown and can be calculated using cross-correlation. When the kernel and search area subimages align, their cross-correlation is maximized. We find the maximum cross-correlation to determine and thus calculate .
This is exactly what
dictk.translation.locate computes
internally — via
skimage.registration.phase_cross_correlation
for .
The locate function returns directly (one does not assemble the
vector chain manually).
phase_cross_correlation is a Fourier-domain computation — every locate
call in this book takes that route under the hood, rather than sliding
the kernel across the search area one position at a time. Correlation
Criteria examines that
Fourier-domain implementation in greater depth, alongside the
spatial-domain CC, NCC, ZCC, and ZNCC criteria it complements.
Locating the Point
from dictk.translation import locate
found = locate(
reference_image=reference_image,
current_image=current_image,
reference_point=p0,
search_center=search_center,
kernel_margin_width=kernel_margin,
kernel_margin_height=kernel_margin,
search_margin_width=search_margin,
search_margin_height=search_margin,
)
print(f"found = {found}")
print(f"displacement = ({found.x - p0.x}, {found.y - p0.y})")
found = PixelCoordinate(x=94, y=83)
displacement = (-6, 8)
found matches the ground-truth pixels from
earlier, recovering the known displacement
pixels using only the two images and — exactly the
information available for a real (not synthetically generated) image
pair.
Visualizing the Solution
For illustration, we can back out — the
one quantity locate finds via cross-correlation, everything else here
being known geometry — from found and the boxed equation above, and
draw the full chain on
current_image:
from dictk.plot import point_plot, ArrowAnnotation, BoxAnnotation, PointAnnotation
r_sk = PixelCoordinate(
x=found.x - search_origin.x - kernel_margin,
y=found.y - search_origin.y - kernel_margin,
)
kernel_found_origin = PixelCoordinate(
x=search_origin.x + r_sk.x, y=search_origin.y + r_sk.y
)
image_height, image_width = current_image.shape
point_plot(
image=current_image,
boxes=[
BoxAnnotation(
origin=PixelCoordinate(x=0, y=0),
width=image_width,
height=image_height,
color="blue",
label="source image",
),
BoxAnnotation(
origin=search_origin,
width=2 * search_margin,
height=2 * search_margin,
color="red",
label="search area",
),
BoxAnnotation(
origin=kernel_found_origin,
width=2 * kernel_margin,
height=2 * kernel_margin,
color="green",
label="kernel",
),
],
points=[
PointAnnotation(position=PixelCoordinate(x=0, y=0), label="$O$", color="blue"),
PointAnnotation(position=search_origin, label="$S$", color="red"),
PointAnnotation(position=kernel_found_origin, label="$K$", color="green"),
PointAnnotation(position=found, label="$P$", color="black"),
],
arrows=[
ArrowAnnotation(
tail=PixelCoordinate(x=0, y=0),
head=found,
color="cyan",
label=r"$\boldsymbol{r}_{OP'/\mathcal{F}}$",
),
ArrowAnnotation(
tail=PixelCoordinate(x=0, y=0),
head=search_origin,
color="blue",
label=r"$\boldsymbol{r}_{OS/\mathcal{F}}$: search area origin",
),
ArrowAnnotation(
tail=search_origin,
head=kernel_found_origin,
color="red",
label=r"$\boldsymbol{r}_{SK/\mathcal{S}}$: kernel found in search area",
),
ArrowAnnotation(
tail=kernel_found_origin,
head=found,
color="green",
label=r"$\boldsymbol{r}_{KP/\mathcal{K}}$: point within kernel",
),
],
figsize=(6.4, 4.8),
path="single_point_motion_solution_vectors.png",
)
Saved: single_point_motion_solution_vectors.png
current_image — with the source image (blue box), search area (red box), and the kernel as found within it (green box) shown behind the arrows, each origin labeled: , , , and the found point .| vector, value | description |
|---|---|
| + | origin of the search area (blue arrow) |
| + | kernel located within the search area, from cross-correlation (red arrow) |
| = | point's fixed position within the kernel (green arrow) |
current position , matching found above (cyan arrow) |
NOTE: Cross-correlation may be conceptualized as the sliding dot product of pixel values from the kernel with pixel values from the search area. In this discussion we have described sliding the kernel across a stationary search area. The reverse, sliding the search area across a stationary kernel, is conceptually different but mathematically identical. Both approaches yield the same result: , which locates the kernel frame in the search area frame.
Next: Correlation Criteria defines the four
cross-correlation formulas and explains the Fourier-domain route locate
actually takes, and Correlation
Visualization visualizes each of them on
this same kernel and search area.