Module code >> skvideo.measure.viideo
Fork me on GitHub

Source code for skvideo.measure.viideo

import numpy as np
import scipy.ndimage
import scipy.special

from ..utils import *

# VIIDEO's own AGGD shape grid (estimateaggdparam.m: gam = 0.2:0.01:5).
# NOTE: this is deliberately NOT skvideo's shared aggd_features grid
# (0.2:0.001:10, which is NIQE's). VIIDEO's features ARE the quantized shape
# parameters, so reproducing the reference requires the reference grid.
_VIIDEO_GAM = np.arange(0.2, 5.0 + 1e-9, 0.01)
_VIIDEO_PREC = (scipy.special.gamma(2.0 / _VIIDEO_GAM) ** 2) / (
    scipy.special.gamma(1.0 / _VIIDEO_GAM) * scipy.special.gamma(3.0 / _VIIDEO_GAM))


def _gaussian2d(n, sigma):
    """fspecial('gaussian', [n n], sigma), normalized to sum 1."""
    ax = np.arange(n, dtype=np.float64) - (n - 1) / 2.0
    g = np.exp(-(ax ** 2) / (2.0 * sigma ** 2))
    k = np.outer(g, g)
    return k / k.sum()


_SHIFTS = [(1, 0), (0, 1), (1, 1), (1, -1)]  # computeVIIDEOscore.m line 87


def _aggd_blocks(P):
    """estimateaggdparam.m over a stack of flattened blocks P (nb, npix), float64.

    Returns (nb, 3) = [alpha, betal, betar]. Faithful to the reference:
      - all-zero block          -> [inf, inf, inf]   (marked invalid)
      - one-sided block (no -ve or no +ve samples) -> betal/betar = NaN
    Both are dropped downstream (the reference deletes nonfinite feature rows).
    Right tail uses strictly > 0 (zeros belong to neither tail).
    """
    absP = np.abs(P)
    zero = absP.sum(axis=1) == 0
    neg = P < 0
    pos = P > 0
    nneg = neg.sum(axis=1)
    npos = pos.sum(axis=1)
    P2 = P * P
    with np.errstate(divide="ignore", invalid="ignore"):
        leftstd = np.sqrt(np.where(nneg > 0, (P2 * neg).sum(axis=1) / nneg, np.nan))
        rightstd = np.sqrt(np.where(npos > 0, (P2 * pos).sum(axis=1) / npos, np.nan))
        gammahat = leftstd / rightstd
        rhat = (absP.mean(axis=1) ** 2) / P2.mean(axis=1)
        rhatnorm = (rhat * (gammahat ** 3 + 1) * (gammahat + 1)) / ((gammahat ** 2 + 1) ** 2)
    d = (_VIIDEO_PREC[None, :] - rhatnorm[:, None]) ** 2
    d = np.where(np.isnan(d), np.inf, d)  # MATLAB min over NaN -> first index
    alpha = _VIIDEO_GAM[np.argmin(d, axis=1)]
    rr = np.sqrt(scipy.special.gamma(1.0 / alpha) / scipy.special.gamma(3.0 / alpha))
    out = np.stack([alpha, leftstd * rr, rightstd * rr], axis=1)
    out[zero] = np.inf
    return out


def _blkproc_aggd(im, bs, ov):
    """blkproc(im, [bs bs], [ov ov], @estimateaggdparam): each distinct bs x bs
    block is extended by an ov-pixel border on ALL sides (zero-padded at image
    edges), giving an (bs+2*ov) square patch. Blocks are always full size - the
    reference never truncates edge blocks.
    """
    H, W = im.shape
    Mb = -(-H // bs)
    Nb = -(-W // bs)
    padded = np.zeros((Mb * bs + 2 * ov, Nb * bs + 2 * ov), dtype=np.float64)
    padded[ov:ov + H, ov:ov + W] = im
    side = bs + 2 * ov
    patches = np.empty((Mb * Nb, side * side), dtype=np.float64)
    n = 0
    for j in range(Nb):
        for i in range(Mb):
            patches[n] = padded[i * bs:i * bs + side, j * bs:j * bs + side].ravel()
            n += 1
    return _aggd_blocks(patches)  # (Mb*Nb, 3)


def _featurevector(y1, y2, bs, ov, window):
    """computeVIIDEOfeaturevector: (nblocks, 28) for one frame pair."""
    cols = []
    X = y1 - y2
    for _scale in range(2):
        mu = scipy.ndimage.correlate(X, window, mode="nearest")
        sigma = np.sqrt(np.abs(
            scipy.ndimage.correlate(X * X, window, mode="nearest") - mu * mu))
        structdis = (X - mu) / (sigma + 1.0)
        f = _blkproc_aggd(structdis, bs, ov)
        cols.append(f[:, 0:1])
        cols.append(((f[:, 1] + f[:, 2]) / 2.0)[:, None])
        for sh in _SHIFTS:
            shifted = np.roll(structdis, sh, axis=(0, 1))
            cols.append(_blkproc_aggd(structdis * shifted, bs, ov))
        X = mu
    return np.concatenate(cols, axis=1)  # (nblocks, 28)


[docs] def viideo_features(videoData, blocksize=(18, 18), blockoverlap=(8, 8), filterlength=7): """Computes VIIDEO features. [#f1]_ [#f2]_ Since this is a referenceless quality algorithm, only 1 video is needed. This function provides the raw features used by the algorithm. Parameters ---------- videoData : ndarray Reference video, ndarray of dimension (T, M, N, C), (T, M, N), (M, N, C), or (M, N), where T is the number of frames, M is the height, N is width, and C is number of channels. blocksize : tuple (2,) blockoverlap: tuple (2,) Returns ------- features : ndarray The individual features of the algorithm, shape (T//2, nblocks, 28). References ---------- .. [#f1] A. Mittal, M. A. Saad and A. C. Bovik, "VIIDEO Software Release", URL: http://live.ece.utexas.edu/research/quality/VIIDEO_release.zip, 2014. .. [#f2] A. Mittal, M. A. Saad and A. C. Bovik, "A 'Completely Blind' Video Integrity Oracle", IEEE Transactions on Image Processing, 2016. """ videoData = vshape(videoData) T, M, N, C = videoData.shape if not (C == 1): raise ValueError("viideo called with video having %d channels. Please supply only the luminance channel." % (C,)) window = _gaussian2d(filterlength, filterlength / 6.0) feats = [] for k in range(T // 2): y1 = videoData[2 * k, :, :, 0].astype(np.float64) y2 = videoData[2 * k + 1, :, :, 0].astype(np.float64) feats.append(_featurevector(y1, y2, blocksize[0], blockoverlap[0], window)) return np.array(feats)
[docs] def viideo_score(videoData, blocksize=(18, 18), blockoverlap=(8, 8), filterlength=7): """Computes VIIDEO score. [#f1]_ [#f2]_ Since this is a referenceless quality algorithm, only 1 video is needed. This function provides the score computed by the algorithm. Higher score = lower quality. This implementation is a faithful port of the LIVE reference ``computeVIIDEOscore.m`` (verified to match it to ~1e-5 on the release demo clips). It does NOT reuse skvideo's shared ``aggd_features`` (which carries NIQE's shape grid); VIIDEO requires its own grid and special-value handling. Parameters ---------- videoData : ndarray Video, ndarray of dimension (T, M, N, C), (T, M, N), (M, N, C), or (M, N). blocksize : tuple (2,) Spatial block size. NOTE: the defaults (18, 18)/(8, 8) are the QCIF demo configuration shipped in the LIVE release (``testscript.m``). The block size used to produce the paper's LIVE VQA results (Table II) was 72; the spatial overlap at that block size is not stated in the paper or release (the demo ratio 8/18 implies ~32, but ~32 vs 36 is not separable within sampling noise). Choose block/overlap to match your intended reference. blockoverlap: tuple (2,) Returns ------- score : float The video quality score. References ---------- .. [#f1] A. Mittal, M. A. Saad and A. C. Bovik, "VIIDEO Software Release", URL: http://live.ece.utexas.edu/research/quality/VIIDEO_release.zip, 2014. .. [#f2] A. Mittal, M. A. Saad and A. C. Bovik, "A 'Completely Blind' Video Integrity Oracle", IEEE Transactions on Image Processing, 2016. """ features = viideo_features(videoData, blocksize=blocksize, blockoverlap=blockoverlap, filterlength=filterlength) npairs = features.shape[0] length = npairs - 1 gap = length // 10 step = max(int(round(gap / 2.0)), 1) scorevect = [] for itr in range(0, length, step): f1_cum = [] f2_cum = [] for t in range(itr, min(itr + gap, length - 1) + 1): low1 = features[t, :, 2:14] low2 = features[t + 1, :, 2:14] high1 = features[t, :, 16:28] high2 = features[t + 1, :, 16:28] vec1 = np.abs(low1 - low2) vec2 = np.abs(high1 - high2) # delete any block (row) with a nonfinite feature in either subband bad = np.any(~np.isfinite(vec1) | ~np.isfinite(vec2), axis=1) if not bad.all(): f1_cum.append(vec1[~bad]) f2_cum.append(vec2[~bad]) if f1_cum: F1 = np.vstack(f1_cum) F2 = np.vstack(f2_cum) C = np.empty(F1.shape[1]) for c in range(F1.shape[1]): a, b = F1[:, c], F2[:, c] if len(a) < 2 or a.std() == 0 or b.std() == 0: C[c] = np.nan # corr of a constant column is undefined else: C[c] = np.corrcoef(a, b)[0, 1] scorevect.append(np.mean(C)) scorevect = np.array(scorevect, dtype=np.float64) change = np.abs(np.roll(scorevect, 1) - scorevect) return float(np.nanmean(scorevect) + np.nanmean(change))