open-standards/wai/tools/wai_int_video_real_encode.py
Transaction Science 326a1b13b6 wai: half-pel motion compensation for int_motion video + real-footage conformance
Real captured video pans a non-integer number of pixels per frame, so
whole-pel block motion compensation always leaves a sub-pixel residual.
Add half-pel MC to wai.video.int_motion: an mv_precision field (1=whole,
default; 2=half) selects sub-pixel block prediction by integer bilinear
interpolation — (a+b+1)>>1 on an axis, (a+b+c+d+2)>>2 on the diagonal —
in pure i64. No float: the clip stays byte-exact (frame-state-equivalence,
a per-frame portable hash) while the residual, and the bitstream, shrink.

- decoder: int_video::predict() gated on mv_precision, wired into both
  decode() and decode_pframes(); existing whole-pel vectors decode
  unchanged (absent field => prec=1).
- encoder: tools/wai_int_video_real_encode.py drives the codec from REAL
  footage (a sub-pixel Ken-Burns pan over a captured photograph), encodes
  the clip both whole-pel and half-pel with a residual-histogram-fit CDF,
  and reports the delta. Two-stage block search: whole-pel SAD then a
  ±half-pel bilinear refine.
- conformance: tests/vectors/int_video/video_real_* — a 96x96x8 half-pel
  clip from real footage; int_video_halfpel.rs never-skips. The Rust
  predict() reproduces the Python encoder's integer bilinear output
  byte-for-byte (32% under whole-pel at the same reproducible frames).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 11:10:48 -04:00

207 lines
9.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Encode a clip from REAL captured footage with HALF-PEL integer motion
compensation, and measure what half-pel buys over whole-pel.
Real footage has *sub-pixel* motion: a camera pan rarely moves an exact integer
number of pixels per frame. Whole-pixel block MC can only ever snap to the
nearest integer shift, so the leftover half-pixel shows up as residual energy
(blur ghosts at edges). Half-pel MC predicts at the half-grid by integer
bilinear interpolation — `(a+b+1)>>1` on an axis, `(a+b+c+d+2)>>2` on the
diagonal — which is the EXACT integer op `wai::int_video::predict()` mirrors on
the decode side, so the clip stays byte-exact (frame-state-equivalence) while the
residual (and the bitstream) shrinks.
The "camera": a sub-pixel Ken-Burns pan over a real photograph (bilinear sampled
— float is fine here, this is capture; only the DECODE is integer). We then encode
the SAME clip twice — whole-pel (mv_precision=1) and half-pel (mv_precision=2) —
and print the rANS byte delta. The half-pel bitstream + its per-frame/clip hashes
are written out; the Rust decoder reproduces them bit-for-bit from the committed
`mv_precision` field.
ffmpeg -i "<a real photo>.heic" /tmp/real_full.png # capture source
python tools/wai_int_video_real_encode.py /tmp/real_full.png <out-dir> [prefix] [size] [nf]
"""
import sys, os, json, base64
import numpy as np
from compressai.ans import RansEncoder
BLOCK = 16 # motion block size
SEARCH = 4 # whole-pixel MV search range (±), in WHOLE pixels
Q = 5 # residual quantization step (fine enough to preserve the sub-pixel gain)
RMAX = (255 + Q - 1) // Q
def fnv1a(buf):
h = 14695981039346656037
for b in buf:
h = ((h ^ b) * 1099511628211) & 0xFFFFFFFFFFFFFFFF
return h
def fit_cdf(syms):
"""Build a precision-16 CDF over residual symbols [-RMAX, RMAX] FIT to the
actual residual histogram (with a floor so every symbol stays decodable, plus
the escape slot). The bitstream carries it, so the decoder uses the identical
model — a data-fit CDF just makes the byte count reflect true residual entropy
instead of a mismatched constant, so the whole-vs-half comparison is honest."""
n = 2 * RMAX + 1
hist = np.bincount(np.asarray(syms, np.int64) + RMAX, minlength=n).astype(np.float64)
total = 1 << 16
f = np.maximum((hist / hist.sum() * (total - n)).astype(np.int64), 1) # floor 1 each
f[f.argmax()] += (total - 1) - int(f.sum()) # reals sum to total-1
cdf = [0]
for v in f:
cdf.append(cdf[-1] + int(v))
cdf.append(total) # escape slot
return cdf
def bilinear_sample(src, oy, ox, h, w):
"""Sample an h×w window from float src at sub-pixel top-left (oy,ox) — the
CAMERA. Bilinear, rounded to u8. (Float lives only on the capture side.)"""
H, W, _ = src.shape
ys = oy + np.arange(h)
xs = ox + np.arange(w)
y0 = np.floor(ys).astype(np.int64); x0 = np.floor(xs).astype(np.int64)
wy = (ys - y0)[:, None, None]; wx = (xs - x0)[None, :, None]
y0c = np.clip(y0, 0, H - 1); y1c = np.clip(y0 + 1, 0, H - 1)
x0c = np.clip(x0, 0, W - 1); x1c = np.clip(x0 + 1, 0, W - 1)
a = src[np.ix_(y0c, x0c)]; b = src[np.ix_(y0c, x1c)]
c = src[np.ix_(y1c, x0c)]; d = src[np.ix_(y1c, x1c)]
top = a * (1 - wx) + b * wx
bot = c * (1 - wx) + d * wx
return np.clip(np.round(top * (1 - wy) + bot * wy), 0, 255).astype(np.int64)
def predict_block(ref, by, bx, bh, bw, dy, dx, prec, H, W):
"""The EXACT integer op `int_video::predict()` runs, vectorized over a block.
`dy,dx` are in whole pels when prec==1, half pels when prec==2. Each tap is
clamped independently (matching the decoder's per-access clamp)."""
if prec == 1:
ys = np.clip(np.arange(by, by + bh) + dy, 0, H - 1)
xs = np.clip(np.arange(bx, bx + bw) + dx, 0, W - 1)
return ref[np.ix_(ys, xs)]
iy = np.arange(by, by + bh) + (dy >> 1) # arithmetic shift == floor/2 (neg-safe)
ix = np.arange(bx, bx + bw) + (dx >> 1)
fy, fx = dy & 1, dx & 1 # 0/1 even for negatives (two's complement)
def g(yo, xo):
return ref[np.ix_(np.clip(iy + yo, 0, H - 1), np.clip(ix + xo, 0, W - 1))]
if fy == 0 and fx == 0:
return g(0, 0)
if fy == 0:
return (g(0, 0) + g(0, 1) + 1) >> 1
if fx == 0:
return (g(0, 0) + g(1, 0) + 1) >> 1
return (g(0, 0) + g(0, 1) + g(1, 0) + g(1, 1) + 2) >> 2
def search_mvs(cur, ref, prec, H, W):
"""Two-stage block search. Stage 1: whole-pel SAD over [-SEARCH,SEARCH]^2.
Stage 2 (prec==2 only): refine ±1 half-pel around 2×(best whole-pel). SAD sums
over the 3 channels (one MV per block). Returns MVs in `prec`-th-pel units."""
mvs = []
for by in range(0, H, BLOCK):
bh = min(BLOCK, H - by)
for bx in range(0, W, BLOCK):
bw = min(BLOCK, W - bx)
cb = cur[by:by + bh, bx:bx + bw]
best, bmv = None, (0, 0)
for dy in range(-SEARCH, SEARCH + 1):
for dx in range(-SEARCH, SEARCH + 1):
pred = predict_block(ref, by, bx, bh, bw, dy, dx, 1, H, W)
sad = int(np.abs(cb - pred).sum())
if best is None or sad < best:
best, bmv = sad, (dy, dx)
if prec == 1:
mvs.append([bmv[0], bmv[1]])
continue
cy, cx = 2 * bmv[0], 2 * bmv[1] # center in half-pel units
best2, bhalf = None, (cy, cx)
for hy in (cy - 1, cy, cy + 1):
for hx in (cx - 1, cx, cx + 1):
pred = predict_block(ref, by, bx, bh, bw, hy, hx, 2, H, W)
sad = int(np.abs(cb - pred).sum())
if best2 is None or sad < best2:
best2, bhalf = sad, (hy, hx)
mvs.append([bhalf[0], bhalf[1]])
return mvs
def encode(frames, prec, H, W):
"""Encode the clip at the given MV precision; return (rANS bytes, mvs, hashes,
cdf, offset). Residuals (hence reconstruction/hashes) are independent of the
entropy model, so we gather all symbols first, then fit the CDF to them."""
offset = -RMAX
syms, mvs_all, recon, hashes = [], [], [], []
for f in range(len(frames)):
if f == 0:
pred = np.full((H, W, 3), 128, np.int64) # I-frame: predict from grey
mvs = []
else:
mvs = search_mvs(frames[f], recon[-1], prec, H, W)
pred = mc_full(recon[-1], mvs, prec, H, W)
rq = np.round((frames[f] - pred) / Q).astype(np.int64).clip(-RMAX, RMAX)
syms.extend(rq.reshape(-1).tolist())
rec = np.clip(pred + rq * Q, 0, 255).astype(np.int64)
recon.append(rec)
mvs_all.append(mvs)
hashes.append(format(fnv1a(np.ascontiguousarray(rec.astype(np.uint8)).tobytes()), '016x'))
cdf = fit_cdf(syms)
bits = RansEncoder().encode_with_indexes(syms, [0] * len(syms), [cdf], [len(cdf)], [offset])
return bytes(bits), mvs_all, hashes, cdf, offset
def mc_full(ref, mvs, prec, H, W):
"""Apply per-block MVs over a full 3-channel reference (decoder-mirroring)."""
nbx = (W + BLOCK - 1) // BLOCK
pred = np.empty((H, W, 3), np.int64)
for by in range(0, H, BLOCK):
bh = min(BLOCK, H - by)
for bx in range(0, W, BLOCK):
bw = min(BLOCK, W - bx)
dy, dx = mvs[(by // BLOCK) * nbx + (bx // BLOCK)]
for c in range(3):
pred[by:by + bh, bx:bx + bw, c] = predict_block(ref[..., c], by, bx, bh, bw, dy, dx, prec, H, W)
return pred
def main(src_png, outdir, prefix='video_real', size=96, nf=8):
os.makedirs(outdir, exist_ok=True)
from PIL import Image
full = np.asarray(Image.open(src_png).convert('RGB')).astype(np.float64)
Hs, Ws, _ = full.shape
H = W = size
# A detailed, textured region (avoid flat sky) + a sub-pixel Ken-Burns pan.
oy0, ox0 = Hs * 0.55, Ws * 0.55 # start window top-left (real terrain detail)
vy, vx = 1.3, 0.8 # px/frame — deliberately NON-integer
frames = [bilinear_sample(full, oy0 + vy * f, ox0 + vx * f, H, W) for f in range(nf)]
out = {}
for prec, tag in ((1, 'whole'), (2, 'half')):
bits, mvs_all, hashes, cdf, offset = encode(frames, prec, H, W)
clip_hash = format(fnv1a(b''.join(bytes.fromhex(h) for h in hashes)), '016x')
out[tag] = (bits, mvs_all, hashes, cdf, offset, clip_hash)
wb, hb = len(out['whole'][0]), len(out['half'][0])
print(f'real footage {nf}x {H}x{W} (sub-pixel pan v=({vy},{vx}) px/frame):')
print(f' whole-pel rANS: {wb} bytes')
print(f' half-pel rANS: {hb} bytes -> {100*(wb-hb)/wb:+.1f}% ({wb-hb:+d} bytes)')
# Ship the HALF-PEL clip (smaller, and the point of the exercise).
bits, mvs_all, hashes, cdf, offset, clip_hash = out['half']
json.dump({'block': BLOCK, 'q': Q, 'rmax': RMAX, 'offset': offset, 'cdf': cdf,
'n_frames': nf, 'h': H, 'w': W, 'mv_precision': 2,
'mvs': mvs_all, 'rans_b64': base64.b64encode(bits).decode()},
open(f'{outdir}/{prefix}_bitstream.json', 'w'))
json.dump({'n_frames': nf, 'h': H, 'w': W, 'frame_hashes': hashes, 'clip_hash': clip_hash,
'whole_pel_bytes': wb, 'half_pel_bytes': hb}, # so the demo can state the real saving
open(f'{outdir}/{prefix}.json', 'w'))
print(f' half-pel clip_hash {clip_hash} (I-frame {hashes[0][:8]}) -> {prefix}_bitstream.json')
if __name__ == '__main__':
main(sys.argv[1], sys.argv[2],
sys.argv[3] if len(sys.argv) > 3 else 'video_real',
int(sys.argv[4]) if len(sys.argv) > 4 else 96,
int(sys.argv[5]) if len(sys.argv) > 5 else 8)