---
title: metric
---

<div class="python-reference">
<a id="fastvideo.eval.metrics.vbench.dynamic_degree.metric-classes"></a>
<a id="fastvideo.eval.metrics.vbench.dynamic_degree.metric-functions"></a>
<a id="fastvideo.eval.metrics.vbench.dynamic_degree.metric"></a>
<p>VBench Dynamic Degree — RAFT optical flow motion detection.</p>
<p>For each consecutive frame pair, computes optical flow via RAFT and takes
the mean of the top 5% flow magnitudes.  If enough pairs exceed an
adaptive threshold, the video is classified as dynamic (1.0) vs static (0.0).</p>
<h2 id="fastvideo.eval.metrics.vbench.dynamic_degree.metric.BaseMetric"><code>fastvideo.eval.metrics.vbench.dynamic_degree.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.vbench.dynamic_degree.metric.BaseMetric.to"><code>fastvideo.eval.metrics.vbench.dynamic_degree.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.vbench.dynamic_degree.metric.BaseMetric.setup"><code>fastvideo.eval.metrics.vbench.dynamic_degree.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.vbench.dynamic_degree.metric.BaseMetric.compute"><code>fastvideo.eval.metrics.vbench.dynamic_degree.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.vbench.dynamic_degree.metric.BaseMetric.reset"><code>fastvideo.eval.metrics.vbench.dynamic_degree.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.vbench.dynamic_degree.metric.BaseMetric.accumulate"><code>fastvideo.eval.metrics.vbench.dynamic_degree.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.vbench.dynamic_degree.metric.BaseMetric.finalize"><code>fastvideo.eval.metrics.vbench.dynamic_degree.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.vbench.dynamic_degree.metric.BaseMetric.merge_from"><code>fastvideo.eval.metrics.vbench.dynamic_degree.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.vbench.dynamic_degree.metric.register"><code>fastvideo.eval.metrics.vbench.dynamic_degree.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.vbench.dynamic_degree.metric.MetricResult"><code>fastvideo.eval.metrics.vbench.dynamic_degree.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.vbench.dynamic_degree.metric.DynamicDegreeMetric"><code>fastvideo.eval.metrics.vbench.dynamic_degree.metric.DynamicDegreeMetric</code></h2>
<pre class="doc-signature"><code>DynamicDegreeMetric()</code></pre>


<details><summary>Source</summary><pre><code>@register(&quot;vbench.dynamic_degree&quot;)
class DynamicDegreeMetric(BaseMetric):

    name = &quot;vbench.dynamic_degree&quot;
    requires_reference = False
    higher_is_better = True
    needs_gpu = True
    dependencies = &#91;&quot;easydict&quot;&#93;

    def __init__(self) -&gt; None:
        super().__init__()
        self._model: Any = None
        self._chunk_size = 16

    def to(self, device):
        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
        from easydict import EasyDict
        from vbench.third_party.RAFT.core.raft import RAFT

        args = EasyDict(small=False, mixed_precision=False, alternate_corr=False, dropout=0.0)
        model = torch.nn.DataParallel(RAFT(args))

        from fastvideo.eval.models import ensure_checkpoint
        ckpt_path = ensure_checkpoint(
            &quot;raft-things.pth&quot;,
            source=&quot;sbalani/raft-things&quot;,
            filename=&quot;raft-things.pth&quot;,
        )
        model.load_state_dict(torch.load(ckpt_path, map_location=&quot;cpu&quot;))
        model = model.module
        model.to(self.device)
        model.eval()
        self._model = model

    def _get_score(self, flow: torch.Tensor) -&gt; float:
        &quot;&quot;&quot;Top-5% mean flow magnitude (matching VBench dynamic_degree.get_score).&quot;&quot;&quot;
        flo = flow.permute(1, 2, 0).cpu().numpy()
        rad = np.sqrt(flo&#91;..., 0&#93;**2 + flo&#91;..., 1&#93;**2)
        h, w = rad.shape
        cut = max(1, int(h * w * 0.05))
        rad_flat = rad.flatten()
        return float(np.mean(np.sort(rad_flat)&#91;-cut:&#93;))

    @torch.no_grad()
    def compute(self, sample: dict) -&gt; MetricResult:
        from vbench.third_party.RAFT.core.utils_core.utils import InputPadder

        video = sample&#91;&quot;video&quot;&#93;  # (T, C, H, W) &#91;0, 1&#93;
        T, _, H, W = video.shape

        # fps controls the temporal sampling stride for optical flow.
        # vbench computes flow at 8fps (interval = round(fps/8)). The metric
        # cannot auto-derive fps from a tensor, so a missing fps would silently
        # use a wrong stride and produce a wrong score. Skip explicitly.
        if &quot;fps&quot; not in sample:
            return self._skip(sample, &quot;missing &#x27;fps&#x27; (required to set the &quot;
                              &quot;8fps optical-flow sampling stride)&quot;)
        fps = float(sample&#91;&quot;fps&quot;&#93;)
        interval = max(1, round(fps / 8.0))

        video_255 = video * 255.0
        chunk = self._chunk_size or 16

        # Cap chunk so the RAFT correlation volume doesn&#x27;t overflow int32.
        # RAFT downsamples 8x in the feature encoder; CorrBlock&#x27;s tensor is
        # shape (B*H1*W1, 1, H2, W2) with H1=H2=H/8, W1=W2=W/8. Its element
        # count is B*(H/8)^2*(W/8)^2 — F.avg_pool2d&#x27;s index space starts to
        # overflow int32 around 2^31. Safety factor 2x.
        h_red = max(1, H // 8)
        w_red = max(1, W // 8)
        max_chunk = max(1, (1 &lt;&lt; 30) // (h_red * h_red * w_red * w_red))
        chunk = min(chunk, max_chunk)

        indices = list(range(0, T, interval))
        n = len(indices)
        all_img1 = &#91;video_255&#91;indices&#91;i&#93;&#93; for i in range(n - 1)&#93;
        all_img2 = &#91;video_255&#91;indices&#91;i + 1&#93;&#93; for i in range(n - 1)&#93;

        scores: list&#91;float&#93; = &#91;&#93;
        for start in range(0, len(all_img1), chunk):
            end = min(start + chunk, len(all_img1))
            img1_batch = torch.stack(all_img1&#91;start:end&#93;).to(self.device)
            img2_batch = torch.stack(all_img2&#91;start:end&#93;).to(self.device)
            padder = InputPadder(img1_batch.shape)
            img1p, img2p = padder.pad(img1_batch, img2_batch)
            _, flow = self._model(img1p, img2p, iters=20, test_mode=True)
            for i in range(flow.shape&#91;0&#93;):
                scores.append(self._get_score(flow&#91;i&#93;))

        scale = min(H, W)
        thres = 6.0 * (scale / 256.0)
        count_needed = round(4 * (n / 16.0))
        count_above = sum(1 for s in scores if s &gt; thres)
        is_dynamic = 1.0 if count_above &gt;= count_needed else 0.0

        return MetricResult(
            name=self.name,
            score=is_dynamic,
            details=&#123;
                &quot;per_pair_magnitude&quot;: scores,
                &quot;threshold&quot;: thres,
                &quot;count_above&quot;: count_above,
                &quot;count_needed&quot;: count_needed,
                &quot;fps&quot;: fps,
                &quot;interval&quot;: interval,
                &quot;n_frames_used&quot;: n
            &#125;,
        )</code></pre></details>
<h3 id="fastvideo.eval.metrics.vbench.dynamic_degree.metric.DynamicDegreeMetric.to"><code>fastvideo.eval.metrics.vbench.dynamic_degree.metric.DynamicDegreeMetric.to</code></h3>
<pre class="doc-signature"><code>to(device)</code></pre>

<details><summary>Source</summary><pre><code>def to(self, device):
    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.vbench.dynamic_degree.metric.DynamicDegreeMetric.setup"><code>fastvideo.eval.metrics.vbench.dynamic_degree.metric.DynamicDegreeMetric.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
    from easydict import EasyDict
    from vbench.third_party.RAFT.core.raft import RAFT

    args = EasyDict(small=False, mixed_precision=False, alternate_corr=False, dropout=0.0)
    model = torch.nn.DataParallel(RAFT(args))

    from fastvideo.eval.models import ensure_checkpoint
    ckpt_path = ensure_checkpoint(
        &quot;raft-things.pth&quot;,
        source=&quot;sbalani/raft-things&quot;,
        filename=&quot;raft-things.pth&quot;,
    )
    model.load_state_dict(torch.load(ckpt_path, map_location=&quot;cpu&quot;))
    model = model.module
    model.to(self.device)
    model.eval()
    self._model = model</code></pre></details>
<h3 id="fastvideo.eval.metrics.vbench.dynamic_degree.metric.DynamicDegreeMetric.compute"><code>fastvideo.eval.metrics.vbench.dynamic_degree.metric.DynamicDegreeMetric.compute</code></h3>
<pre class="doc-signature"><code>compute(sample: dict) -&gt; MetricResult</code></pre>

<details><summary>Source</summary><pre><code>@torch.no_grad()
def compute(self, sample: dict) -&gt; MetricResult:
    from vbench.third_party.RAFT.core.utils_core.utils import InputPadder

    video = sample&#91;&quot;video&quot;&#93;  # (T, C, H, W) &#91;0, 1&#93;
    T, _, H, W = video.shape

    # fps controls the temporal sampling stride for optical flow.
    # vbench computes flow at 8fps (interval = round(fps/8)). The metric
    # cannot auto-derive fps from a tensor, so a missing fps would silently
    # use a wrong stride and produce a wrong score. Skip explicitly.
    if &quot;fps&quot; not in sample:
        return self._skip(sample, &quot;missing &#x27;fps&#x27; (required to set the &quot;
                          &quot;8fps optical-flow sampling stride)&quot;)
    fps = float(sample&#91;&quot;fps&quot;&#93;)
    interval = max(1, round(fps / 8.0))

    video_255 = video * 255.0
    chunk = self._chunk_size or 16

    # Cap chunk so the RAFT correlation volume doesn&#x27;t overflow int32.
    # RAFT downsamples 8x in the feature encoder; CorrBlock&#x27;s tensor is
    # shape (B*H1*W1, 1, H2, W2) with H1=H2=H/8, W1=W2=W/8. Its element
    # count is B*(H/8)^2*(W/8)^2 — F.avg_pool2d&#x27;s index space starts to
    # overflow int32 around 2^31. Safety factor 2x.
    h_red = max(1, H // 8)
    w_red = max(1, W // 8)
    max_chunk = max(1, (1 &lt;&lt; 30) // (h_red * h_red * w_red * w_red))
    chunk = min(chunk, max_chunk)

    indices = list(range(0, T, interval))
    n = len(indices)
    all_img1 = &#91;video_255&#91;indices&#91;i&#93;&#93; for i in range(n - 1)&#93;
    all_img2 = &#91;video_255&#91;indices&#91;i + 1&#93;&#93; for i in range(n - 1)&#93;

    scores: list&#91;float&#93; = &#91;&#93;
    for start in range(0, len(all_img1), chunk):
        end = min(start + chunk, len(all_img1))
        img1_batch = torch.stack(all_img1&#91;start:end&#93;).to(self.device)
        img2_batch = torch.stack(all_img2&#91;start:end&#93;).to(self.device)
        padder = InputPadder(img1_batch.shape)
        img1p, img2p = padder.pad(img1_batch, img2_batch)
        _, flow = self._model(img1p, img2p, iters=20, test_mode=True)
        for i in range(flow.shape&#91;0&#93;):
            scores.append(self._get_score(flow&#91;i&#93;))

    scale = min(H, W)
    thres = 6.0 * (scale / 256.0)
    count_needed = round(4 * (n / 16.0))
    count_above = sum(1 for s in scores if s &gt; thres)
    is_dynamic = 1.0 if count_above &gt;= count_needed else 0.0

    return MetricResult(
        name=self.name,
        score=is_dynamic,
        details=&#123;
            &quot;per_pair_magnitude&quot;: scores,
            &quot;threshold&quot;: thres,
            &quot;count_above&quot;: count_above,
            &quot;count_needed&quot;: count_needed,
            &quot;fps&quot;: fps,
            &quot;interval&quot;: interval,
            &quot;n_frames_used&quot;: n
        &#125;,
    )</code></pre></details>
</div>
