Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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
kernel placement in the reference image with point P marked by an orange dot, and the extracted kernel itself with point P marked by an orange dot
Left: the kernel (green box), a 50x50 region of 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
search area placement in the current image, and the extracted search area itself
Left: the search area (red box), a 100x100 region of 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
chained vector solution: cyan shortcut arrow from origin directly to the found point, blue arrow from origin to search area origin, red arrow to the located kernel, green arrow to the found point, with a blue source-image box, a red search-area box, and a green kernel box drawn behind the arrows, and O, S, K, P labels drawn on top
The current configuration (tip of the cyan and green arrows) as the vector chain (blue) (red) (green), equal to the direct shortcut (cyan), drawn on 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, valuedescription
+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.