---
title: metric
---

<div class="python-reference">
<a id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric-classes"></a>
<a id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric-functions"></a>
<a id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric--required-sample-keys"></a>
<a id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric--optional-sample-keys"></a>
<a id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric"></a>
<p>Compare optical flow extracted from a generated video against optical
flow synthesized analytically from per-frame actions.</p>
<p>The reference flow is <em>not</em> observed from a ground-truth video — it's
predicted from the action stream via a third-person camera-kinematics
model (Longuet-Higgins linearization + off-pivot translation correction;
no depth). Observed flow comes from the same <code>ptlflow</code> model used by
<code>gt_optical_flow</code>, and the two are compared with the identical metric
set, so scores are directly comparable across the two metrics.</p>
<h2>Required sample keys</h2>
<p><code>video</code>
    <code>(B, T, C, H, W)</code> float in <code>[0, 1]</code>.
<code>actions</code>
    <code>dict</code> (or list-of-dicts of length B) with two <code>np.ndarray</code> keys:</p>
<pre><code>* ``keyboard`` of shape ``(T, 6)`` — ``[W, S, A, D, turn_left, turn_right]``
* ``mouse`` of shape ``(T, 2)`` — ``[pitch, yaw]``
</code></pre>
<p><code>calibration</code>
    Either a path to a <code>ThirdPersonCalibration</code> JSON file, or a dict
    of fitted parameters. May also be set once at construction time via
    <code>calibration_path=</code> and reused across samples.</p>
<h2>Optional sample keys</h2>
<p><code>mouse_pitch_sign</code>
    <code>+1</code> (default) or <code>-1</code> if the dataset's mouse-pitch sign is
    flipped (mhuo's data carries this in metadata).</p>
<h2 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric</code></h2>
<pre class="doc-signature"><code>BaseMetric()</code></pre>
<p>Abstract base class for all eval metrics.</p>
<p>Two execution shapes:</p>
<ul>
<li>
<p><strong>Per-sample</strong> (<code>is_set_metric=False</code>, default) — implement
  :meth:<code>compute</code>. The Evaluator calls it once per input sample and
  returns one :class:<code>MetricResult</code> per sample.</p>
</li>
<li>
<p><strong>Set-vs-set</strong> (<code>is_set_metric=True</code>) — implement
  :meth:<code>accumulate</code> (called once per sample to buffer features)
  and :meth:<code>finalize</code> (called once after all samples to compute
  the corpus-level result). Use :meth:<code>reset</code> to clear buffers and
  :meth:<code>merge_from</code> to fold multi-GPU per-worker state together.</p>
</li>
</ul>
<p>Optionally override :meth:<code>setup</code> to eagerly load models. Metrics
that chunk along the time dim for memory hardcode their own chunk
size in <code>__init__</code> (see <code>optical_flow</code> for the canonical
example). Eval always processes one video per
:meth:<code>Evaluator.evaluate</code> call; <code>compute</code> / <code>accumulate</code>
receive a single sample, not a batch.</p>

<h3 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric.to"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric.to</code></h3>
<pre class="doc-signature"><code>to(device: str | torch.device) -&gt; BaseMetric</code></pre>
<p>Move metric (and its internal models) to <em>device</em>.</p>
<h3 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric.setup"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric.setup</code></h3>
<pre class="doc-signature"><code>setup() -&gt; None</code></pre>
<p>Eagerly load models. Called once by :class:<code>EvalWorker</code>.</p>
<p>Default is a no-op; metrics with no eager state (pixel math,
closed-form ops) inherit this. Override only if your metric
needs to load weights.</p>
<h3 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric.compute"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric.compute</code></h3>
<pre class="doc-signature"><code>compute(sample: dict) -&gt; MetricResult</code></pre>
<p>Per-sample metrics: compute the score for one sample.</p>
<p><code>sample["video"]</code> is <code>(T, C, H, W)</code> float in <code>[0, 1]</code>.
<code>sample["reference"]</code> (if used) has the same shape. Return
<code>self._skip(sample, reason)</code> for missing inputs.</p>
<h3 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric.reset"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric.reset</code></h3>
<pre class="doc-signature"><code>reset() -&gt; None</code></pre>
<p>Clear accumulator state at the start of each evaluate() call.</p>
<h3 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric.accumulate"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric.accumulate</code></h3>
<pre class="doc-signature"><code>accumulate(sample: dict) -&gt; None</code></pre>
<p>Buffer per-sample features for a corpus-level metric.</p>
<h3 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric.finalize"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric.finalize</code></h3>
<pre class="doc-signature"><code>finalize() -&gt; MetricResult</code></pre>
<p>Compute the corpus-level result from buffered state.</p>
<h3 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric.merge_from"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.BaseMetric.merge_from</code></h3>
<pre class="doc-signature"><code>merge_from(other: BaseMetric) -&gt; None</code></pre>
<p>Multi-GPU: fold another worker's accumulator state into this one.</p>
<h2 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.aggregate_temporal"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.aggregate_temporal</code></h2>
<pre class="doc-signature"><code>aggregate_temporal(per_frame: list&#91;dict&#91;str, float&#93;&#93;) -&gt; dict&#91;str, float | int | None&#93;</code></pre>
<p>Aggregate per-frame metric dicts into mean/std/max/auc/onset summaries.</p>
<p>Port of mhuo's compute_temporal_metrics.</p>
<h2 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.compute_frame_metrics"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.compute_frame_metrics</code></h2>
<pre class="doc-signature"><code>compute_frame_metrics(flow_gt: np.ndarray, flow_gen: np.ndarray, grid_size: int = 8, min_mag: float = 0.5, max_mag_pct: float = 80.0) -&gt; dict&#91;str, float&#93;</code></pre>
<p>Per-frame comparison metrics between two HxWx2 flow fields.</p>
<p>Port of mhuo's compute_frame_metrics — see ptlflow_validation.py.</p>
<h2 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.extract_video_flows"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.extract_video_flows</code></h2>
<pre class="doc-signature"><code>extract_video_flows(model, video: torch.Tensor, *, chunk: int, device: torch.device) -&gt; list&#91;np.ndarray&#93;</code></pre>
<p>Run <em>model</em> on every consecutive frame pair in <em>video</em>.</p>
<p>Returns a list of HxWx2 flow arrays of length <code>T - 1</code>.</p>
<h2 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.load_ptlflow_model"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.load_ptlflow_model</code></h2>
<pre class="doc-signature"><code>load_ptlflow_model(model_name: str, ckpt: str, device: torch.device)</code></pre>
<p>Load a <code>ptlflow</code> model on <em>device</em> in eval mode.</p>
<h2 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.ThirdPersonCalibration"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.ThirdPersonCalibration</code></h2>
<pre class="doc-signature"><code>ThirdPersonCalibration(alpha_yaw: float, alpha_pitch: float, alpha_turn: float, beta_fwd: float, beta_strafe: float, focal_length: float, r_z: float = 0.0, r_y: float = 0.0, init_pitch: float = 0.0, notes: str = &#x27;&#x27;, fit_metadata: dict = dict())</code></pre>
<p>Fitted parameters for a third-person rig.</p>
<p><code>r_y</code> defaults to 0 (avatar at camera height). <code>focal_length</code> is
in pixels. <code>init_pitch</code> is the camera's rest pitch (radians, negative
means tilted down — typical over-the-shoulder framing). User mouse-pitch
input is integrated on top of this baseline.</p>

<h3 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.ThirdPersonCalibration.to_json"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.ThirdPersonCalibration.to_json</code></h3>
<pre class="doc-signature"><code>to_json(path: str | Path) -&gt; None</code></pre>

<h3 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.ThirdPersonCalibration.from_dict"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.ThirdPersonCalibration.from_dict</code></h3>
<pre class="doc-signature"><code>from_dict(d: dict) -&gt; ThirdPersonCalibration</code></pre>

<h2 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.ThirdPersonFlowGenerator"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.ThirdPersonFlowGenerator</code></h2>
<pre class="doc-signature"><code>ThirdPersonFlowGenerator(calibration: ThirdPersonCalibration, frame_shape: tuple&#91;int, int&#93;, mouse_pitch_sign: int = +1)</code></pre>
<p>Vectorized 3P synthetic-flow generator. No depth.</p>
<h2>Parameters</h2>
<p>calibration : ThirdPersonCalibration
frame_shape : (H, W)
mouse_pitch_sign : +1 or -1 — sample-level flag from metadata
    (<code>mouse_pitch_flipped: true</code> in mhuo's data ⇒ -1).</p>

<h3 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.ThirdPersonFlowGenerator.generate_flow"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.ThirdPersonFlowGenerator.generate_flow</code></h3>
<pre class="doc-signature"><code>generate_flow(keyboard: np.ndarray, mouse: np.ndarray) -&gt; np.ndarray</code></pre>
<p>Synthesize HxWx2 flow for one frame's action.</p>
<h3 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.ThirdPersonFlowGenerator.generate_flow_sequence"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.ThirdPersonFlowGenerator.generate_flow_sequence</code></h3>
<pre class="doc-signature"><code>generate_flow_sequence(actions: dict, n_pairs: int | None = None) -&gt; list&#91;np.ndarray&#93;</code></pre>
<p>Generate flow for each consecutive frame pair.</p>
<p>Returns <code>n</code> flows where <code>n = n_pairs</code> if supplied, else
<code>len(keyboard) - 1</code>.</p>
<h2 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.load_calibration"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.load_calibration</code></h2>
<pre class="doc-signature"><code>load_calibration(path: str | Path) -&gt; ThirdPersonCalibration</code></pre>

<h2 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.register"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.register</code></h2>
<pre class="doc-signature"><code>register(name: str)</code></pre>
<p>Decorator to register a metric class.</p>
<p>Usage::</p>
<pre><code>@register("ssim")
class SSIMMetric(BaseMetric):
    ...
</code></pre>
<h2 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.MetricResult"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.MetricResult</code></h2>
<pre class="doc-signature"><code>MetricResult(name: str, score: float | None, details: dict&#91;str, Any&#93; = dict())</code></pre>
<p>Standard result container returned by all metrics.</p>
<p><code>score</code> is <code>None</code> when the metric was skipped (e.g. missing
required input).  Check <code>details["skipped"]</code> for the reason.</p>

<h2 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.SyntheticOpticalFlowMetric"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.SyntheticOpticalFlowMetric</code></h2>
<pre class="doc-signature"><code>SyntheticOpticalFlowMetric(model_name: str = &#x27;dpflow&#x27;, ckpt: str = &#x27;things&#x27;, calibration_path: str | Path | None = None, min_mag: float = 0.5, max_mag_pct: float = 80.0, grid_size: int = 8)</code></pre>
<p>Action-driven synthetic flow vs. video-extracted observed flow.</p>
<p>Pass <code>calibration_path</code> at construction to bind the calibration
once across all samples; otherwise supply <code>sample["calibration"]</code>
per call. Missing actions or calibration produce a skipped result
(<code>score=None</code>) rather than raising.</p>

<details><summary>Source</summary><pre><code>@register(&quot;optical_flow.synthetic_optical_flow&quot;)
class SyntheticOpticalFlowMetric(BaseMetric):
    &quot;&quot;&quot;Action-driven synthetic flow vs. video-extracted observed flow.

    Pass ``calibration_path`` at construction to bind the calibration
    once across all samples; otherwise supply ``sample&#91;&quot;calibration&quot;&#93;``
    per call. Missing actions or calibration produce a skipped result
    (``score=None``) rather than raising.
    &quot;&quot;&quot;

    name = &quot;optical_flow.synthetic_optical_flow&quot;
    requires_reference = False
    higher_is_better = False
    needs_gpu = True
    backbone = &quot;optical_flow&quot;
    dependencies = &#91;&quot;ptlflow&quot;&#93;

    def __init__(
        self,
        model_name: str = &quot;dpflow&quot;,
        ckpt: str = &quot;things&quot;,
        calibration_path: str | Path | None = None,
        min_mag: float = 0.5,
        max_mag_pct: float = 80.0,
        grid_size: int = 8,
    ) -&gt; None:
        super().__init__()
        self.model_name = model_name
        self.ckpt = ckpt
        self.min_mag = min_mag
        self.max_mag_pct = max_mag_pct
        self.grid_size = grid_size
        self._calibration: ThirdPersonCalibration | None = (_resolve_calibration(calibration_path)
                                                            if calibration_path else None)
        self._model = None
        # One frame-pair per DPFlow forward; see ``gt_optical_flow``.
        self._chunk_size = 1

    def to(self, device: str | torch.device) -&gt; SyntheticOpticalFlowMetric:
        super().to(device)
        if self._model is not None:
            self._model = self._model.to(self.device)
        return self

    def setup(self) -&gt; None:
        if self._model is not None:
            return
        self._model = load_ptlflow_model(self.model_name, self.ckpt, self.device)

    def compute(self, sample: dict) -&gt; MetricResult:
        if self._model is None:
            self.setup()

        actions = sample.get(&quot;actions&quot;)
        if actions is None:
            return self._skip(sample, &quot;missing &#x27;actions&#x27; (keyboard + mouse)&quot;)

        cal_obj = sample.get(&quot;calibration&quot;)
        cal = self._calibration if cal_obj is None else _resolve_calibration(cal_obj)
        if cal is None:
            return self._skip(
                sample,
                &quot;missing &#x27;calibration&#x27; (pass calibration_path= at construction &quot;
                &quot;or sample&#91;&#x27;calibration&#x27;&#93; per call)&quot;,
            )

        video = sample&#91;&quot;video&quot;&#93;.float()  # (T, C, H, W)
        T, _, H, W = video.shape
        if T &lt; 2:
            raise ValueError(&quot;Need at least 2 frames to compute optical flow&quot;)
        n_pairs = T - 1

        mouse_pitch_sign = int(sample.get(&quot;mouse_pitch_sign&quot;, 1))
        chunk = self._chunk_size or 16

        observed = extract_video_flows(
            self._model,
            video,
            chunk=chunk,
            device=self.device,
        )
        predictor = ThirdPersonFlowGenerator(
            calibration=cal,
            frame_shape=(H, W),
            mouse_pitch_sign=mouse_pitch_sign,
        )
        predicted = predictor.generate_flow_sequence(actions, n_pairs=n_pairs)

        n = min(len(observed), len(predicted))
        per_frame = &#91;
            compute_frame_metrics(
                predicted&#91;i&#93;,
                observed&#91;i&#93;,
                grid_size=self.grid_size,
                min_mag=self.min_mag,
                max_mag_pct=self.max_mag_pct,
            ) for i in range(n)
        &#93;
        summary = aggregate_temporal(per_frame)
        score = summary.get(&quot;pixel_epe_mean_mean&quot;)
        details = dict(summary)
        details&#91;&quot;per_frame_metrics&quot;&#93; = per_frame
        return MetricResult(
            name=self.name,
            score=float(score) if score is not None else None,
            details=details,
        )</code></pre></details>
<h3 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.SyntheticOpticalFlowMetric.to"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.SyntheticOpticalFlowMetric.to</code></h3>
<pre class="doc-signature"><code>to(device: str | torch.device) -&gt; SyntheticOpticalFlowMetric</code></pre>

<details><summary>Source</summary><pre><code>def to(self, device: str | torch.device) -&gt; SyntheticOpticalFlowMetric:
    super().to(device)
    if self._model is not None:
        self._model = self._model.to(self.device)
    return self</code></pre></details>
<h3 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.SyntheticOpticalFlowMetric.setup"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.SyntheticOpticalFlowMetric.setup</code></h3>
<pre class="doc-signature"><code>setup() -&gt; None</code></pre>

<details><summary>Source</summary><pre><code>def setup(self) -&gt; None:
    if self._model is not None:
        return
    self._model = load_ptlflow_model(self.model_name, self.ckpt, self.device)</code></pre></details>
<h3 id="fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.SyntheticOpticalFlowMetric.compute"><code>fastvideo.eval.metrics.optical_flow.synthetic_optical_flow.metric.SyntheticOpticalFlowMetric.compute</code></h3>
<pre class="doc-signature"><code>compute(sample: dict) -&gt; MetricResult</code></pre>

<details><summary>Source</summary><pre><code>def compute(self, sample: dict) -&gt; MetricResult:
    if self._model is None:
        self.setup()

    actions = sample.get(&quot;actions&quot;)
    if actions is None:
        return self._skip(sample, &quot;missing &#x27;actions&#x27; (keyboard + mouse)&quot;)

    cal_obj = sample.get(&quot;calibration&quot;)
    cal = self._calibration if cal_obj is None else _resolve_calibration(cal_obj)
    if cal is None:
        return self._skip(
            sample,
            &quot;missing &#x27;calibration&#x27; (pass calibration_path= at construction &quot;
            &quot;or sample&#91;&#x27;calibration&#x27;&#93; per call)&quot;,
        )

    video = sample&#91;&quot;video&quot;&#93;.float()  # (T, C, H, W)
    T, _, H, W = video.shape
    if T &lt; 2:
        raise ValueError(&quot;Need at least 2 frames to compute optical flow&quot;)
    n_pairs = T - 1

    mouse_pitch_sign = int(sample.get(&quot;mouse_pitch_sign&quot;, 1))
    chunk = self._chunk_size or 16

    observed = extract_video_flows(
        self._model,
        video,
        chunk=chunk,
        device=self.device,
    )
    predictor = ThirdPersonFlowGenerator(
        calibration=cal,
        frame_shape=(H, W),
        mouse_pitch_sign=mouse_pitch_sign,
    )
    predicted = predictor.generate_flow_sequence(actions, n_pairs=n_pairs)

    n = min(len(observed), len(predicted))
    per_frame = &#91;
        compute_frame_metrics(
            predicted&#91;i&#93;,
            observed&#91;i&#93;,
            grid_size=self.grid_size,
            min_mag=self.min_mag,
            max_mag_pct=self.max_mag_pct,
        ) for i in range(n)
    &#93;
    summary = aggregate_temporal(per_frame)
    score = summary.get(&quot;pixel_epe_mean_mean&quot;)
    details = dict(summary)
    details&#91;&quot;per_frame_metrics&quot;&#93; = per_frame
    return MetricResult(
        name=self.name,
        score=float(score) if score is not None else None,
        details=details,
    )</code></pre></details>
</div>
