Pyramid vector quantization (PVQ) is a method used in audio and video codecs to quantize and transmit unit vectors, i.e. vectors whose magnitudes are known to the decoder but whose directions are unknown. PVQ may also be used as part of a gain/shape quantization scheme, whereby the magnitude and direction of a vector are quantized separately from each other. PVQ was initially described in 1986 in the paper "A Pyramid Vector Quantizer" by Thomas R. Fischer.[1]
One caveat of PVQ is that it operates under the taxicab distance (L1-norm). Conversion to/from the more familiar Euclidean distance (L2-norm) is possible via vector projection, though results in a less uniform distribution of quantization points (the poles of the Euclidean n-sphere become denser than non-poles).[2] No efficient algorithm for the ideal (i.e., uniform) vector quantization of the Euclidean n-sphere is known as of 2010.[3] This non-uniformity can be reduced by applying deformation like coordinate-wise power before projection, reducing mean-squared quantization error by ~10%.[4]
PVQ is used in the CELT audio codec (inherited into Opus) and the Daala video codec.
As a form of vector quantization, PVQ defines a codebook of quantization points, each of which is assigned an integer codeword from 0 to -1. The goal of the encoder is to find the codeword of the closest vector, which the decoder must decode back into a vector.
The PVQ codebook consists of all -dimensional points
\vec{p}
S(N,K)=\left\{\vec{p}\inZN:\left\|\vec{p}\right\|1=K\right\}
where
\left\|\vec{p}\right\|1
\vec{p}
As it stands, the set tesselates the surface of an -dimensional pyramid. If desired, we may reshape it into a sphere by "projecting" the points onto the sphere, i.e. by normalizing them:
S | ||||
|
where
\left\|\vec{p}\right\|2
\vec{p}
\vec{v}
Suppose we wish to quantize three-dimensional unit vectors using the parameter =2. Our codebook becomes:
|
|
(0.707 =
\sqrt{2}/2
Now, suppose we wish to transmit the unit vector <0.592, -0.720, 0.362> (rounded here to 3 decimal places, for clarity). According to our codebook, the closest point we can pick is codeword 13 (<0.707, -0.707, 0.000>), located approximately 0.381 units away from our original point.
Increasing the parameter results in a larger codebook, which typically increases the reconstruction accuracy. For example, based on the Python code below, =5 (codebook size: 102) yields an error of only 0.097 units, and =20 (codebook size: 1602) yields an error of only 0.042 units.
class PVQEntry(NamedTuple): codeword: int point: Tuple[int, ...] normalizedPoint: Tuple[float, ...]
def create_pvq_codebook(n: int, k: int) -> List[PVQEntry]: """ Naive algorithm to generate an n-dimensional PVQ codebook with k pulses. Runtime complexity: O(k**n) """ ret = [] for p in itertools.product(range(-k, k + 1), repeat=n): if sum(abs(x) for x in p)
return ret
def search_pvq_codebook(codebook: List[PVQEntry], p: Tuple[float, ...]) -> Tuple[PVQEntry, float]: """ Naive algorithm to search the PVQ codebook. Returns the point in the codebook that's "closest" to p, according to the Euclidean distance.) """ ret = None min_dist = None for entry in codebook: q = entry.normalizedPoint dist = math.sqrt(sum((q[j] - p[j]) ** 2 for j in range(len(p)))) if min_dist is None or dist < min_dist: ret = entry min_dist = dist
return ret, min_dist
def example(p: Tuple[float, ...], k: int) -> None: n = len(p) codebook = create_pvq_codebook(n, k) print("Number of codebook entries: " + str(len(codebook))) entry, dist = search_pvq_codebook(codebook, p) print("Best entry: " + str(entry)) print("Distance: " + str(dist))
phi = 1.2theta = 5.4x = math.sin(phi) * math.cos(theta)y = math.sin(phi) * math.sin(theta)z = math.cos(phi)p = (x, y, z)example(p, 2)example(p, 5)example(p, 20)
The PVQ codebook can be searched in
O(KN)
O(KN)
O(K+N)
The codebook size obeys the recurrence
V(N,K)=V(N-1,K)+V(N,K-1)+V(N-1,K-1)
with
V(N,0)=1
N\ge0
V(0,K)=0
K\ne0
A closed-form solution is given by[6]
V(N,K)=2N ⋅ {}2F1(1-K,1-N;2;2).
where
{}2F1