Results, saving and reloading#

The displacement array#

get_displacements() returns, and stores on the method object as .displacements, an array of shape (n_points, n_frames, 2) in pixels, relative to the reference frame:

displacements = lk.get_displacements()

displacements[i, :, 0]     # displacement history of point i, row (y) direction
displacements[i, :, 1]     # displacement history of point i, column (x) direction

DIC additionally exposes .warp_params of shape (n_points, n_frames, n_param), from which strain and rotation are read directly — see Displacement identification methods.

Points that could not be tracked#

A point placed on a uniform region, or on a single straight edge with no gradient along it, cannot be tracked. Since 1.4.0 this no longer aborts the analysis: the point is set to NaN from the frame at which it was lost, every other point is computed normally, and a warning is issued.

Which points failed, and why, is recorded in failed_points:

displacements = lk.get_displacements()

lk.failed_points          # {point_index: {'frame': ..., 'status': ...}}

status distinguishes a singular gradient matrix (a flat or edge-only subset — the point never had enough information to track) from a diverged iteration (the optimizer ran away to a non-finite value).

The practical consequence downstream is that results may contain NaN:

import numpy as np

ok = ~np.isnan(displacements).any(axis=(1, 2))     # points that tracked all the way
amplitude = np.nanmax(np.abs(displacements), axis=1)

Warning

The detection is best effort. It catches points whose displacement becomes non-finite or larger than the image, but a point can return physically implausible values well below that bound. A result without ``NaN`` is not proof that every point tracked correctly. Check the displacements against what the structure can plausibly do.

Where results are saved#

get_displacements() saves automatically (pass autosave=False to suppress it). Results go into a directory next to the recording, one sub-directory per run:

measurement.cih
measurement_pyidi_analysis/
    analysis_001/
        points.pkl          # the points that were tracked
        results.pkl         # the displacements
        directions.pkl      # only for DirectionalLucasKanade
        warp_params.pkl     # only for DIC
        settings.json       # every configure() argument, plus source and date
    analysis_002/
        ...

settings.json is what makes a run reproducible: it records the input file, the method, the creation date, the video dimensions, and the full configuration. This is why every configure() parameter must be stored as an attribute of the same name.

Reloading a saved analysis#

from pyidi import load_analysis

video, idi, settings = load_analysis('measurement_pyidi_analysis/analysis_001')

displacements = idi.displacements
points = idi.points

load_analysis returns three things: a fresh VideoReader, the reconstructed method object with its points, directions and results restored, and the settings dictionary.

# the recording has moved since the analysis was run
video, idi, settings = load_analysis(path, input_file='new/location/measurement.cih')

# only the points and settings, without reading the results back
video, idi, settings = load_analysis(path, load_results=False)

Resuming an interrupted analysis#

Long analyses checkpoint as they go, into a temp_file directory beside the recording. If a run is interrupted — a crash, a full disk, a closed laptop — it can pick up from the last completed time point instead of starting over:

lk.configure(resume_analysis=True)
displacements = lk.get_displacements()

The checkpoint is only reused if the settings match the interrupted run; if they do not, the analysis restarts from the beginning. The temporary files are removed on successful completion.

Note

failed_points is rebuilt from scratch on resume rather than being restored from the checkpoint. A point that was already lost before the interruption stays lost — the previous displacement is checked for NaN/inf before it is used — but the recorded frame number refers to the resumed run.

Viewing the results#

ResultViewer animates the identified displacements over the recording (requires the [qt] extra):

from pyidi import ResultViewer

viewer = ResultViewer(
    video,
    displacements=displacements,
    points=points,
    fps=30,                # playback rate
    magnification=10,      # visual amplification of the displacement
    point_size=10,
    colormap='cool',
)

The window opens on construction and blocks until it is closed. points uses the same (row, column) convention as everywhere else, and displacements is the array as returned by get_displacements(). A 2-D (n_points, 2) array is also accepted and animated as a mode shape.

If the analysis was run through the napari GUI, the method object is at gui.method and the results at gui.method.displacements.