Skip to content

kalman

sleap_nn.tracking.mot.kalman

Kalman filters for the MOT trackers (numpy only).

Three constant-velocity box filters, one per parameterization used in the literature:

  • :class:KalmanFilterXYSR -- SORT / OC-SORT: z = [cx, cy, s=area, r=w/h], 7-D state, fixed noise.
  • :class:KalmanFilterXYAH -- DeepSORT / StrongSORT / ByteTrack: z = [cx, cy, a=w/h, h], 8-D state, noise scaled by box height.
  • :class:KalmanFilterXYWH -- BoT-SORT / Deep OC-SORT: z = [cx, cy, w, h], 8-D state, noise scaled by width (x, w) and height (y, h).

Filters are stateless configuration objects; each track owns its (mean, cov). All filters share the interface of :class:_KalmanBase, accept and return boxes as xyxy, clamp degenerate sizes to >= 1 px, keep covariances symmetric and never produce NaN/inf. The OC-SORT freeze / observation-centric re-update (ORU) helpers work with every filter.

The measurement model of every filter observes the first four state entries (H = [I_4 | 0]) and the motion model adds each velocity once per step. The noise constants follow the published trackers (cited per class); the updates use the Joseph form of the covariance correction, which is algebraically identical to the references' P - K S K^T but keeps P positive semi-definite under rounding.

Classes:

Name Description
KalmanFilterXYAH

[cx, cy, a, h] constant-velocity filter (DeepSORT, StrongSORT, ByteTrack).

KalmanFilterXYSR

[cx, cy, s, r] filter with 7-D state [cx, cy, s, r, vx, vy, vs] (SORT, OC-SORT).

KalmanFilterXYWH

[cx, cy, w, h] constant-velocity filter (BoT-SORT, Deep OC-SORT).

Functions:

Name Description
make_kalman_filter

Return a filter by name: "xywh", "xyah" or "xysr".

oru_replay

OC-SORT observation-centric re-update (ORU).

KalmanFilterXYAH

Bases: _KalmanBase

[cx, cy, a, h] constant-velocity filter (DeepSORT, StrongSORT, ByteTrack).

DeepSORT constants with p = 1/20, v = 1/160: P0 std [2p h, 2p h, 1e-2, 2p h, 10v h, 10v h, 1e-5, 10v h], Q std [p h, p h, 1e-2, p h, v h, v h, 1e-5, v h], R std [p h, p h, 1e-1, p h].

P0 uses the first measurement's height, Q and R the current state's (clamped to >= 1 px). Prediction zeroes va (vh) when a + va <= 0 (h + vh <= 0); this only fires for degenerate boxes.

Methods:

Name Description
xyxy_from_mean

Convert state mean(s) to xyxy box(es) (h and a*h >= 1 px).

z_from_xyxy

Convert xyxy box(es) to [cx, cy, w/h, h] (sides >= 1 px).

Source code in sleap_nn/tracking/mot/kalman.py
class KalmanFilterXYAH(_KalmanBase):
    """``[cx, cy, a, h]`` constant-velocity filter (DeepSORT, StrongSORT, ByteTrack).

    DeepSORT constants with ``p = 1/20``, ``v = 1/160``:
    ``P0`` std ``[2p h, 2p h, 1e-2, 2p h, 10v h, 10v h, 1e-5, 10v h]``,
    ``Q`` std ``[p h, p h, 1e-2, p h, v h, v h, 1e-5, v h]``,
    ``R`` std ``[p h, p h, 1e-1, p h]``.

    ``P0`` uses the first measurement's height, ``Q`` and ``R`` the current
    state's (clamped to >= 1 px). Prediction zeroes ``va`` (``vh``) when
    ``a + va <= 0`` (``h + vh <= 0``); this only fires for degenerate boxes.
    """

    ndim = 8
    _velocity_pairs = ((0, 4), (1, 5), (2, 6), (3, 7))
    _size_velocity_pairs = ((2, 6), (3, 7))

    def z_from_xyxy(self, xyxy: np.ndarray) -> np.ndarray:
        """Convert ``xyxy`` box(es) to ``[cx, cy, w/h, h]`` (sides >= 1 px).

        Args:
            xyxy: A ``(4,)`` box or ``(..., 4)`` boxes.

        Returns:
            The measurement(s), same leading shape as ``xyxy``.
        """
        c = _cxcywh_from_xyxy(xyxy)
        c[..., 2] = c[..., 2] / c[..., 3]
        return c

    def xyxy_from_mean(self, mean: np.ndarray) -> np.ndarray:
        """Convert state mean(s) to ``xyxy`` box(es) (``h`` and ``a*h`` >= 1 px).

        Args:
            mean: A ``(8,)`` state mean or ``(..., 8)`` means.

        Returns:
            The box(es) ``(..., 4)``.
        """
        c = np.array(np.asarray(mean, dtype=np.float64)[..., :4], copy=True)
        h = np.maximum(c[..., 3], _MIN_SIZE)
        c[..., 2] = np.maximum(c[..., 2] * h, _MIN_SIZE)
        c[..., 3] = h
        return _xyxy_from_cxcywh(c)

    def _initial_var(self, z: np.ndarray) -> np.ndarray:
        """Initial variances from the first measurement's height."""
        h = max(z[3], _MIN_SIZE)
        p, v = 2 * _STD_WEIGHT_POSITION * h, 10 * _STD_WEIGHT_VELOCITY * h
        return np.square([p, p, 1e-2, p, v, v, 1e-5, v])

    def _process_var(self, means: np.ndarray) -> np.ndarray:
        """Process-noise variances from each state's height."""
        h = np.maximum(means[:, 3], _MIN_SIZE)
        p, v = _STD_WEIGHT_POSITION * h, _STD_WEIGHT_VELOCITY * h
        a_pos, a_vel = np.full_like(h, 1e-2), np.full_like(h, 1e-5)
        return np.square(np.stack([p, p, a_pos, p, v, v, a_vel, v], axis=1))

    def _measurement_var(self, mean: np.ndarray) -> np.ndarray:
        """Measurement-noise variances from the state's height."""
        p = _STD_WEIGHT_POSITION * max(mean[3], _MIN_SIZE)
        return np.square([p, p, 1e-1, p])

xyxy_from_mean(mean)

Convert state mean(s) to xyxy box(es) (h and a*h >= 1 px).

Parameters:

Name Type Description Default
mean ndarray

A (8,) state mean or (..., 8) means.

required

Returns:

Type Description
ndarray

The box(es) (..., 4).

Source code in sleap_nn/tracking/mot/kalman.py
def xyxy_from_mean(self, mean: np.ndarray) -> np.ndarray:
    """Convert state mean(s) to ``xyxy`` box(es) (``h`` and ``a*h`` >= 1 px).

    Args:
        mean: A ``(8,)`` state mean or ``(..., 8)`` means.

    Returns:
        The box(es) ``(..., 4)``.
    """
    c = np.array(np.asarray(mean, dtype=np.float64)[..., :4], copy=True)
    h = np.maximum(c[..., 3], _MIN_SIZE)
    c[..., 2] = np.maximum(c[..., 2] * h, _MIN_SIZE)
    c[..., 3] = h
    return _xyxy_from_cxcywh(c)

z_from_xyxy(xyxy)

Convert xyxy box(es) to [cx, cy, w/h, h] (sides >= 1 px).

Parameters:

Name Type Description Default
xyxy ndarray

A (4,) box or (..., 4) boxes.

required

Returns:

Type Description
ndarray

The measurement(s), same leading shape as xyxy.

Source code in sleap_nn/tracking/mot/kalman.py
def z_from_xyxy(self, xyxy: np.ndarray) -> np.ndarray:
    """Convert ``xyxy`` box(es) to ``[cx, cy, w/h, h]`` (sides >= 1 px).

    Args:
        xyxy: A ``(4,)`` box or ``(..., 4)`` boxes.

    Returns:
        The measurement(s), same leading shape as ``xyxy``.
    """
    c = _cxcywh_from_xyxy(xyxy)
    c[..., 2] = c[..., 2] / c[..., 3]
    return c

KalmanFilterXYSR

Bases: _KalmanBase

[cx, cy, s, r] filter with 7-D state [cx, cy, s, r, vx, vy, vs] (SORT, OC-SORT).

SORT constants: R = diag(1, 1, 10, 10), P0 = diag(10, 10, 10, 10, 1e4, 1e4, 1e4), Q = diag(1, 1, 1, 1, 0.01, 0.01, 1e-4). Prediction zeroes vs when it would make the area non-positive.

Methods:

Name Description
xyxy_from_mean

Convert state mean(s) to xyxy box(es) (sides >= 1 px).

z_from_xyxy

Convert xyxy box(es) to [cx, cy, w*h, w/h] (sides >= 1 px).

Source code in sleap_nn/tracking/mot/kalman.py
class KalmanFilterXYSR(_KalmanBase):
    """``[cx, cy, s, r]`` filter with 7-D state ``[cx, cy, s, r, vx, vy, vs]`` (SORT, OC-SORT).

    SORT constants: ``R = diag(1, 1, 10, 10)``,
    ``P0 = diag(10, 10, 10, 10, 1e4, 1e4, 1e4)``,
    ``Q = diag(1, 1, 1, 1, 0.01, 0.01, 1e-4)``. Prediction zeroes ``vs`` when it
    would make the area non-positive.
    """

    ndim = 7
    _velocity_pairs = ((0, 4), (1, 5), (2, 6))
    _size_velocity_pairs = ((2, 6),)

    _P0_VAR = np.array([10.0, 10.0, 10.0, 10.0, 1e4, 1e4, 1e4])
    _Q_VAR = np.array([1.0, 1.0, 1.0, 1.0, 1e-2, 1e-2, 1e-4])
    _R_VAR = np.array([1.0, 1.0, 10.0, 10.0])

    def z_from_xyxy(self, xyxy: np.ndarray) -> np.ndarray:
        """Convert ``xyxy`` box(es) to ``[cx, cy, w*h, w/h]`` (sides >= 1 px).

        Args:
            xyxy: A ``(4,)`` box or ``(..., 4)`` boxes.

        Returns:
            The measurement(s), same leading shape as ``xyxy``.
        """
        c = _cxcywh_from_xyxy(xyxy)
        w, h = c[..., 2].copy(), c[..., 3].copy()
        c[..., 2] = w * h
        c[..., 3] = w / h
        return c

    def xyxy_from_mean(self, mean: np.ndarray) -> np.ndarray:
        """Convert state mean(s) to ``xyxy`` box(es) (sides >= 1 px).

        Args:
            mean: A ``(7,)`` state mean or ``(..., 7)`` means.

        Returns:
            The box(es) ``(..., 4)``.
        """
        m = np.asarray(mean, dtype=np.float64)
        s = np.maximum(m[..., 2], _MIN_SIZE)
        r = np.maximum(m[..., 3], _MIN_RATIO)
        w = np.maximum(np.sqrt(s * r), _MIN_SIZE)
        h = np.maximum(np.sqrt(s / r), _MIN_SIZE)
        return _xyxy_from_cxcywh(np.stack([m[..., 0], m[..., 1], w, h], axis=-1))

    def _initial_var(self, z: np.ndarray) -> np.ndarray:
        """Fixed SORT initial variances."""
        return self._P0_VAR.copy()

    def _process_var(self, means: np.ndarray) -> np.ndarray:
        """Fixed SORT process-noise variances."""
        return np.broadcast_to(self._Q_VAR, (len(means), self.ndim))

    def _measurement_var(self, mean: np.ndarray) -> np.ndarray:
        """Fixed SORT measurement-noise variances."""
        return self._R_VAR.copy()

xyxy_from_mean(mean)

Convert state mean(s) to xyxy box(es) (sides >= 1 px).

Parameters:

Name Type Description Default
mean ndarray

A (7,) state mean or (..., 7) means.

required

Returns:

Type Description
ndarray

The box(es) (..., 4).

Source code in sleap_nn/tracking/mot/kalman.py
def xyxy_from_mean(self, mean: np.ndarray) -> np.ndarray:
    """Convert state mean(s) to ``xyxy`` box(es) (sides >= 1 px).

    Args:
        mean: A ``(7,)`` state mean or ``(..., 7)`` means.

    Returns:
        The box(es) ``(..., 4)``.
    """
    m = np.asarray(mean, dtype=np.float64)
    s = np.maximum(m[..., 2], _MIN_SIZE)
    r = np.maximum(m[..., 3], _MIN_RATIO)
    w = np.maximum(np.sqrt(s * r), _MIN_SIZE)
    h = np.maximum(np.sqrt(s / r), _MIN_SIZE)
    return _xyxy_from_cxcywh(np.stack([m[..., 0], m[..., 1], w, h], axis=-1))

z_from_xyxy(xyxy)

Convert xyxy box(es) to [cx, cy, w*h, w/h] (sides >= 1 px).

Parameters:

Name Type Description Default
xyxy ndarray

A (4,) box or (..., 4) boxes.

required

Returns:

Type Description
ndarray

The measurement(s), same leading shape as xyxy.

Source code in sleap_nn/tracking/mot/kalman.py
def z_from_xyxy(self, xyxy: np.ndarray) -> np.ndarray:
    """Convert ``xyxy`` box(es) to ``[cx, cy, w*h, w/h]`` (sides >= 1 px).

    Args:
        xyxy: A ``(4,)`` box or ``(..., 4)`` boxes.

    Returns:
        The measurement(s), same leading shape as ``xyxy``.
    """
    c = _cxcywh_from_xyxy(xyxy)
    w, h = c[..., 2].copy(), c[..., 3].copy()
    c[..., 2] = w * h
    c[..., 3] = w / h
    return c

KalmanFilterXYWH

Bases: _KalmanBase

[cx, cy, w, h] constant-velocity filter (BoT-SORT, Deep OC-SORT).

Noise standard deviations: position 1/20 and velocity 1/160 of the box width (x and w terms) or height (y and h terms). P0 std [2p w, 2p h, 2p w, 2p h, 10v w, 10v h, 10v w, 10v h] (sizes of the first measurement), Q std [p w, p h, p w, p h, v w, v h, v w, v h] and R std [p w, p h, p w, p h] (sizes of the current state), with p = 1/20, v = 1/160 and sizes clamped to >= 1 px. Like Deep OC-SORT, prediction zeroes vw (vh) when w + vw <= 0 (h + vh <= 0).

Methods:

Name Description
xyxy_from_mean

Convert state mean(s) to xyxy box(es) (sides >= 1 px).

z_from_xyxy

Convert xyxy box(es) to [cx, cy, w, h] (sides >= 1 px).

Source code in sleap_nn/tracking/mot/kalman.py
class KalmanFilterXYWH(_KalmanBase):
    """``[cx, cy, w, h]`` constant-velocity filter (BoT-SORT, Deep OC-SORT).

    Noise standard deviations: position ``1/20`` and velocity ``1/160`` of the
    box width (x and w terms) or height (y and h terms).
    ``P0`` std ``[2p w, 2p h, 2p w, 2p h, 10v w, 10v h, 10v w, 10v h]`` (sizes of
    the first measurement), ``Q`` std ``[p w, p h, p w, p h, v w, v h, v w, v h]``
    and ``R`` std ``[p w, p h, p w, p h]`` (sizes of the current state), with
    ``p = 1/20``, ``v = 1/160`` and sizes clamped to >= 1 px. Like Deep OC-SORT,
    prediction zeroes ``vw`` (``vh``) when ``w + vw <= 0`` (``h + vh <= 0``).
    """

    ndim = 8
    _velocity_pairs = ((0, 4), (1, 5), (2, 6), (3, 7))
    _size_velocity_pairs = ((2, 6), (3, 7))

    def z_from_xyxy(self, xyxy: np.ndarray) -> np.ndarray:
        """Convert ``xyxy`` box(es) to ``[cx, cy, w, h]`` (sides >= 1 px).

        Args:
            xyxy: A ``(4,)`` box or ``(..., 4)`` boxes.

        Returns:
            The measurement(s), same leading shape as ``xyxy``.
        """
        return _cxcywh_from_xyxy(xyxy)

    def xyxy_from_mean(self, mean: np.ndarray) -> np.ndarray:
        """Convert state mean(s) to ``xyxy`` box(es) (sides >= 1 px).

        Args:
            mean: A ``(8,)`` state mean or ``(..., 8)`` means.

        Returns:
            The box(es) ``(..., 4)``.
        """
        c = np.array(np.asarray(mean, dtype=np.float64)[..., :4], copy=True)
        c[..., 2:4] = np.maximum(c[..., 2:4], _MIN_SIZE)
        return _xyxy_from_cxcywh(c)

    def _initial_var(self, z: np.ndarray) -> np.ndarray:
        """Initial variances from the first measurement's width and height."""
        w, h = max(z[2], _MIN_SIZE), max(z[3], _MIN_SIZE)
        p, v = 2 * _STD_WEIGHT_POSITION, 10 * _STD_WEIGHT_VELOCITY
        return np.square([p * w, p * h, p * w, p * h, v * w, v * h, v * w, v * h])

    def _process_var(self, means: np.ndarray) -> np.ndarray:
        """Process-noise variances from each state's width and height."""
        w = np.maximum(means[:, 2], _MIN_SIZE)
        h = np.maximum(means[:, 3], _MIN_SIZE)
        whwh = np.stack([w, h, w, h], axis=1)
        return np.square(
            np.concatenate(
                [_STD_WEIGHT_POSITION * whwh, _STD_WEIGHT_VELOCITY * whwh], axis=1
            )
        )

    def _measurement_var(self, mean: np.ndarray) -> np.ndarray:
        """Measurement-noise variances from the state's width and height."""
        w, h = max(mean[2], _MIN_SIZE), max(mean[3], _MIN_SIZE)
        return np.square(_STD_WEIGHT_POSITION * np.array([w, h, w, h]))

xyxy_from_mean(mean)

Convert state mean(s) to xyxy box(es) (sides >= 1 px).

Parameters:

Name Type Description Default
mean ndarray

A (8,) state mean or (..., 8) means.

required

Returns:

Type Description
ndarray

The box(es) (..., 4).

Source code in sleap_nn/tracking/mot/kalman.py
def xyxy_from_mean(self, mean: np.ndarray) -> np.ndarray:
    """Convert state mean(s) to ``xyxy`` box(es) (sides >= 1 px).

    Args:
        mean: A ``(8,)`` state mean or ``(..., 8)`` means.

    Returns:
        The box(es) ``(..., 4)``.
    """
    c = np.array(np.asarray(mean, dtype=np.float64)[..., :4], copy=True)
    c[..., 2:4] = np.maximum(c[..., 2:4], _MIN_SIZE)
    return _xyxy_from_cxcywh(c)

z_from_xyxy(xyxy)

Convert xyxy box(es) to [cx, cy, w, h] (sides >= 1 px).

Parameters:

Name Type Description Default
xyxy ndarray

A (4,) box or (..., 4) boxes.

required

Returns:

Type Description
ndarray

The measurement(s), same leading shape as xyxy.

Source code in sleap_nn/tracking/mot/kalman.py
def z_from_xyxy(self, xyxy: np.ndarray) -> np.ndarray:
    """Convert ``xyxy`` box(es) to ``[cx, cy, w, h]`` (sides >= 1 px).

    Args:
        xyxy: A ``(4,)`` box or ``(..., 4)`` boxes.

    Returns:
        The measurement(s), same leading shape as ``xyxy``.
    """
    return _cxcywh_from_xyxy(xyxy)

make_kalman_filter(kind, nsa=False)

Return a filter by name: "xywh", "xyah" or "xysr".

Parameters:

Name Type Description Default
kind str

Filter name (case-insensitive). "auto" must be resolved by the caller (each tracker has its own default).

required
nsa bool

Enable StrongSORT's NSA measurement-noise scaling.

False

Returns:

Type Description
_KalmanBase

A new filter instance.

Raises:

Type Description
ValueError

If kind is not a known filter.

Source code in sleap_nn/tracking/mot/kalman.py
def make_kalman_filter(kind: str, nsa: bool = False) -> _KalmanBase:
    """Return a filter by name: ``"xywh"``, ``"xyah"`` or ``"xysr"``.

    Args:
        kind: Filter name (case-insensitive). ``"auto"`` must be resolved by the
            caller (each tracker has its own default).
        nsa: Enable StrongSORT's NSA measurement-noise scaling.

    Returns:
        A new filter instance.

    Raises:
        ValueError: If ``kind`` is not a known filter.
    """
    key = str(kind).strip().lower()
    if key not in _KALMAN_FILTERS:
        raise ValueError(
            f"Unknown Kalman filter {kind!r}; expected one of "
            f"{', '.join(repr(k) for k in _KALMAN_FILTERS)}."
        )
    return _KALMAN_FILTERS[key](nsa=nsa)

oru_replay(kf, frozen_mean, frozen_cov, last_obs_xyxy, new_obs_xyxy, n_gap, conf=None, frozen_steps=1)

OC-SORT observation-centric re-update (ORU).

Restore the state frozen at a track's first miss and replay n_gap steps of predict + update with virtual observations linearly interpolated (in cx, cy, w, h) from last_obs_xyxy to new_obs_xyxy; the last virtual observation is new_obs_xyxy itself. The caller then applies the real update (the reference applies the real observation twice; kept for fidelity).

Virtual observation j (1 <= j <= n_gap) sits j / n_gap of the way from the last observation to the new one. The replay updates the state at every step from frozen_steps on and predicts once between consecutive steps, so with the reference freeze (frozen_steps=1: the state was saved right after the first missed step's prediction) it runs n_gap updates and n_gap - 1 predictions and ends at the new observation's step.

Parameters:

Name Type Description Default
kf _KalmanBase

The track's filter.

required
frozen_mean ndarray

State mean saved at the first miss (after that frame's update).

required
frozen_cov ndarray

State covariance saved at the first miss.

required
last_obs_xyxy ndarray

Last real observation before the gap.

required
new_obs_xyxy ndarray

The re-matched observation.

required
n_gap int

Steps between the last observation and the new one (>= 1).

required
conf Optional[float]

Detection confidence for NSA filters.

None
frozen_steps int

Steps the frozen state had been predicted past last_obs_xyxy when it was saved (clipped to [0, n_gap]): 1 (default, the reference) when it was saved after the first missed step's prediction; k > 1 when that first miss came after a k-step frame gap (virtual observations at earlier steps are skipped); 0 when it is the posterior of the last update (one extra prediction is run first).

1

Returns:

Type Description
Tuple[ndarray, ndarray]

(mean, cov) after the replay (ready for the real update).

Source code in sleap_nn/tracking/mot/kalman.py
def oru_replay(
    kf: _KalmanBase,
    frozen_mean: np.ndarray,
    frozen_cov: np.ndarray,
    last_obs_xyxy: np.ndarray,
    new_obs_xyxy: np.ndarray,
    n_gap: int,
    conf: Optional[float] = None,
    frozen_steps: int = 1,
) -> Tuple[np.ndarray, np.ndarray]:
    """OC-SORT observation-centric re-update (ORU).

    Restore the state frozen at a track's first miss and replay ``n_gap`` steps of
    predict + update with virtual observations linearly interpolated (in
    ``cx, cy, w, h``) from ``last_obs_xyxy`` to ``new_obs_xyxy``; the last virtual
    observation is ``new_obs_xyxy`` itself. The caller then applies the real
    update (the reference applies the real observation twice; kept for fidelity).

    Virtual observation ``j`` (``1 <= j <= n_gap``) sits ``j / n_gap`` of the way
    from the last observation to the new one. The replay updates the state at
    every step from ``frozen_steps`` on and predicts once between consecutive
    steps, so with the reference freeze (``frozen_steps=1``: the state was saved
    right after the first missed step's prediction) it runs ``n_gap`` updates and
    ``n_gap - 1`` predictions and ends at the new observation's step.

    Args:
        kf: The track's filter.
        frozen_mean: State mean saved at the first miss (after that frame's update).
        frozen_cov: State covariance saved at the first miss.
        last_obs_xyxy: Last real observation before the gap.
        new_obs_xyxy: The re-matched observation.
        n_gap: Steps between the last observation and the new one (>= 1).
        conf: Detection confidence for NSA filters.
        frozen_steps: Steps the frozen state had been predicted past
            ``last_obs_xyxy`` when it was saved (clipped to ``[0, n_gap]``):
            1 (default, the reference) when it was saved after the first missed
            step's prediction; ``k > 1`` when that first miss came after a
            ``k``-step frame gap (virtual observations at earlier steps are
            skipped); 0 when it is the posterior of the last update (one extra
            prediction is run first).

    Returns:
        ``(mean, cov)`` after the replay (ready for the real update).
    """
    n_gap = max(int(n_gap), 1)
    frozen_steps = min(max(int(frozen_steps), 0), n_gap)
    mean = np.array(frozen_mean, dtype=np.float64, copy=True)
    cov = np.array(frozen_cov, dtype=np.float64, copy=True)

    new_obs = np.asarray(new_obs_xyxy, dtype=np.float64).reshape(4)
    start = _cxcywh_from_xyxy(np.asarray(last_obs_xyxy, dtype=np.float64).reshape(4))
    end = _cxcywh_from_xyxy(new_obs)
    if not (np.all(np.isfinite(start)) and np.all(np.isfinite(end))):
        # No virtual trajectory without both endpoints: coast to the new step.
        logger.debug("ORU replay without finite endpoints; coasting instead.")
        for _ in range(n_gap - frozen_steps):
            mean, cov = kf.predict(mean, cov)
        return mean, cov

    if frozen_steps == 0:
        mean, cov = kf.predict(mean, cov)
    first = max(frozen_steps, 1)
    for j in range(first, n_gap + 1):
        if j > first:
            mean, cov = kf.predict(mean, cov)
        if j == n_gap:
            # The last virtual observation is the new observation itself.
            virtual_xyxy = new_obs
        else:
            frac = j / n_gap
            virtual_xyxy = _xyxy_from_cxcywh((1.0 - frac) * start + frac * end)
        mean, cov = kf.update(mean, cov, virtual_xyxy, conf)
    return mean, cov