POSIX 线程同步——互斥量

POSIX多线程程序设计. 作者 美 David R.Buten

大部分多线程程序需要在线程间共享数据。如果两个线程同时访问共享数据就可能会有问题,因为一个线程可能在另一个线程修改共享数据的过程中使用该数据,并认为共享数据保持未变。

使线程同步最通用和常用的方法就是确保对相同(或相关)数据的内存访问”互斥地”进行,即一次只能允许一个线程写数据,其他线程必须等待。Pthreads使用了一种特殊形式的信号灯——互斥量。互斥量(mutex)是由单词互相(mutual)的首部”mut”和排斥(eexclusion)的首部”ex”组合而成的。

同步不仅仅在修改数据时重要,当线程需要读取其他线程写入的数据时,而且数据写入的顺序也有影响时,同样需要同步。

考虑以下实例:一个线程向数组中某个元素填加新数据,并更新max_index变量以表明该数组元素有效。在另一个处理器上同时运行的处理线程,负责遍历数组并处理每个有效元素。如果处理线程在读取数组元素的新数据:之前先读取了更新后的max_index值,计算就会出错。这可能显得有些不太合理,但是以这种方式工作的内存系统比按照确定顺序访问内存的系统要快得多。互斤量是解决此类问题的通用方法:在访问共享数据的代码段周围加锁互斥量,则一次只能有一个线程进入该代码段。

下图显示了共享互斥量的三个线程的时序图。处于标有”互斥量”的圆形框之上的线段表示相关的线程没有拥有互斥量。处于圆形框中心线之下的线段表示相关的线程拥有互斥量。处于中心线之上的线段表明相关的线程等待拥有互斥量。

最初,互斥量没有被加锁。当线程1试图加锁该互斥量时,由于于没有竞争,线程1立即加锁成功,对应线段也移到中心线之下。然后线程2试图加锁互斥量,由于互斥量已经被锁住,所以线程2被阻塞,对应线段在中心线之上。线程1解锁互斥量,解除线程2的阻塞,使其对互斥量加锁成功。稍后,线程3试图加锁互斥量被阻塞。线程1调用函数pthread_mutex_trylock试着锁住互斥量,而立刻返回EBUSY。线程2解锁互斥量,解除线程3的阻塞,线程3加锁成功。最后,线程3完成工作,解锁互斥量。

1 创建和销毁互斥量

pthread_mutex_t_mutex = PTHREAD_MUTEX_INITTALIZER;
int pthread nutex init(
    pthread_mutex_t *mutex, pthread_mutexattr_t*attr);
int Pthread_mutex_destroy (Pthread_mutex_t*mutex);

程序中的互斥量是用pthread_mutex_t类型的变量来表示的。不能拷贝互斥量变量,因为使用拷贝的互斥量是不确定的。可以拷贝指向互斥量的指针,这样就可以使多个函数或线程共享互斥量来实现同步。

大部分时间你可能在”文件范围”内,即函数体外,声明互斥量为外部或静态存储类型。如果有其他文件使用互斥量,则将其声明为外部类型;如果仅在本文件内使用,则将其声明为静态类型。你应该使用宏 PTHREAD_MUTEEX_INITIALIZER 来声明具有默认属性的静态互斥量,如下面例程mutex_staltic.c所示(你可以编译并运行该程序,不过因为main函数为空,所以不会有任何结果)。

#include <pthread.h>
#include "errors,h"
/*
 * Declare a structure, with a mutex, statically initialized. This
 * is the same as using pthread_mutex_init, withthe default,
 * attributes.
 */
typedef struct my_struct_tag {
    pthread_mutex_t mutex; /* Protects access to value */
    int value; /* Access protected by mutex */
} my_struct_t;

my_struct_t data = {PTHREAD_MUTEX_INITIALIZER, 0};

int main (int argc, char *argv[])
{
    return 0;
}

通常不能静态地初始化一个互斥量,例如当使用malloc动态分配一个包含互斥量的数据结构时。这时,应该使用pthread_mutex_init调用来动态地初始化互斥量,如下面程序mutex_dynamic.c所示。也可以动态地初始化静态声明的互斥量,但必须保证每个互斥量在使用前被初始化,而且只被始化一次。可以在创建任何线程之前初始化它,如通过调用pthread_once。如果需要初始化一个非缺省属性的互斥量,必须使用动态初始化。

#include <pthread.h>
#include "errors.h"
/*
 * Define a structure, with a mutex.
 */
typedef struct my_struct_tag {
    pthread_mutex_t mutex; /* Protects access to value */
    int value; /* Access protected by mutex */
} my_struct_t;

int main (int argc, char *argv[]) {
    my_struct_t *data;
    int status;
    data = malloc (sizeof (my_struct_t));
    if (data == NULL)
        errno_abort ("Allocate structure");
    status = pthread_mutex_init (&data->mutex, NULL);
    if (status != 0)
        err abort (status, "Init mutex");
    status = pthread_mutex_destroy (&data->mutex);
    if (status != 0)
        err_abort (status, "Destroy mutex");
    (void)free (data);
    return status;
}

将互斥量与它要保护的数据明显地联系起来是个不错的注意。如果可能的话,将互斥量和数据定义在一起。例如,在mutex_static.c和mutex_dynamic.c中,互斥量和它要保护的数据就被定义在同一个数据结构中,并通过注释语句记录了这种关系。

当不再需要一个通过pthread_mutex_init调用动态初始化的互斥量时,应该调用pthread_mutex_destroy来释放它。不需要释放一个使用
PTHREAD_MUTEX_INITIALIZER 宏静态初始化的互斥量。当确信没有线程在互斥量上阻塞时,可以立刻释放它。

当知道没有线程在互斥量上阻塞,且互斥量也没有被锁住时,可以安全地释放它。获知此信息的最好方式是在刚刚解锁互斥量的线程内,程序逻辑确保随后不再有线程会加锁该互斥量。当线程在某个堆栈数据结构中锁住互斥量,以从列表中删除该结构并释放内存时,在释放互斥量占有的空间之前先将互斥量解锁和释放是个安全且不错的主意。

2 加锁和解锁互斥量

int pthread_mutex_lock (pthread_mutex_t *mutex) 
int pthread_mutex_trylock (pthread_mutex_t *mutex)
int pthread_mutex_unlock (Pthread_mutex_t *mutex)

在最简单的情况下,使用互斥量是容易的事情。通过调用pthread_mutex_lock或pthread_mutex_trylock锁住互斥量,处理共享数据,然后调用pthread_mutex_unlock解锁互斥量。为确保线程能够读取一组变量的一致的值,需要在任何读写这些变量的代码段周围锁住互斥量。

当调用线程已经锁住互斥量之后,就不能再加锁该互斥量。试图这样做的结果可能是返回错误(EDEADLK),或者可能陷入”自死锁”,使不幸的线程永远等待下去。不能解锁一个已经解锁的互斥量,也不能解锁由其他线程锁住的互斥量。锁住的互斥量是属于加锁线程的。

下面程序alarm_mutex.c是alarm_thread.c的改进版本。它在一个alarmserver线程中处理多个闹铃的请求。

结构体alarm_t现在包含了一个标准UNIX time_t类型的绝对时间,表示从UNIX纪元(1970年1月1日00:00)开始到闹铃时的秒数。这样alarm_t结构体就可以按照闹铃时间排序,而不是按照请求的秒数排序。另外,还有一个link元素将所有请求链接起来。

互斥量alarm_mutex负责协调对闹铃请求列表alarm_list的头节点的访问。互斥量是使用默认属性调用宏 PTHREAD_MUTEX_INITIALIZER 静态初始化的。列首指针初始化为空。

#include <pthread.h>
#include <time.h>
#include "errors.h"
/*
 * The "alarm" structure now contains the tinme t (time since the
 * Epoch, in seconds) for each alarm, so thatthey can be
 * sorted. Storing the requested number of sedconds would not be
 * enough, since the "alarm thread" cannot tel11 how long it has
 * been on the list.
 */
typedef struct alarm_tag {
    struct alarm_tag *link;
    int seconds;
    time_t time; /* seconds from EPOCH */
    char message[64];
} alarm_t;

pthread_mutex_t alarm_mutex = PTHREAD_MUTEX_INITIALIZER;
alarm_t *alarm_list = NULL;

下面讲述函数alarm_thread的代码。该函数作为线程运行,并依次处理列表alarm_list中的每个闹铃请求。线程永不停止,当main函数返回时,线程”蒸发”。这种做法的惟一后果是任何剩余请求都不会被传送,线程没有保留任何能够在进程之外可见的状态。

如果希望程序在退出之前处理所有未完结的闹铃请求,可以简简单地修改程序以达到该目标。当主线程发现列表 alarm_list为空时,需要通过某种方式通知alarm_thread线程终止。例如,可以在主线程中设置一个全局变量alarm_done的值,然后调用pthread_exit而不是调用exit终止。当alarm_thread线程发现列表为空且alarm_done被置位时,它会立即调用pthread_exit,而不是等待下一个请求。

如果列表中没有新的请求,alarm_thread线程需要阻塞自己一小段时间,解锁互斥量,以便主线程能够添加新的闹铃请求。通过将sleep_time置为1秒来作到这点。

如果列表中发现请求,则将它从列表中删除。调用time函数放获得当前时间,并将其与请求时间比较。如果闹铃时间已经过期,则alarm_thread线程将sleep_time置为0;否则,alarm_thread线程计算闹铃时间与当前时间的差,并将sleep_time置为该差值(以秒为单位)。

在线程睡眠或阻塞之前,总是要解锁互斥量。如果互斥量仍被锁住,则主线程就无法向列表中增加请求。这将使程序变成同步工作方式,因为用户必须等到闹铃之后才能做其他事(用户可能输入一个命令,但是必须等到下一闹钟到期时才能获得系统提示)。调用sleep将阻塞alarm_thread线程指定的时间,直到经过该时间后线程才能运行。

调用sched_yield的效果果是将处理器交给另一个等待运行的线程,但是如果没有就绪的线程,则立即返回。在程序中,调用sched_yield意味着:如果有等待处理的用户输入,则主线程运行,处理用户请求;如果用户没有输入请求,则该函数立即返回。

如果alam指针非空,即如果已经从alarm_list列表中处理里了一个闹铃请求,则函数打印消息显示闹铃已到期。然后,线程释放alarm结构,准备处理下一个闹铃请求。

/*
 * The alarm thread's start routine.
 */
void *alarm_thread (void *arg) {
    alarm t *alarm;
    int sleep time;
    time t_now;
    int status;
    /*
    * Loop forever, processing commands. The alarmthread will
    * be disintegrated when the process exits.
    */
    while (1) {
        status = pthread_mutex_lock (&alarm_mutex);
        if (status != 0)
            err_abort (status, "Lock mutex");
        alarm = alarm_list;
        /*
         * If the alarm list is empty, wait for one second. This
         * allows the main thread to run, and readanother
         * command. If the list is not empty, remove tthe first
         * item. Compute the number of seconds to wait-- if the
         * result is less than 0 (the time has passed), t!hen set
         * the sleep time to 0.
         */
        if (alarm == NULL)
            sleep_time = 1;
        else {
            alarm_lişt = alarm->link;
            now = time (NULL);
            if (alarm->time <= now)
                sleep_time = 0;
            else
                sleep_time = alarm->time - now;
#ifdef DEBUG
            printf ("[waiting: %d(%d)\"Bs\"]\n", alarm->timne,
                sleep time, alarm->message);
#endif
        }

        /*
         * Unlock the mutex before waiting, so that thhe main
         * thread can lock it to insert a new alarm request.If
         * the sleep_time is 0, then call sched yield, giving9
         * /the main thread a chance to run if it has been n
         * readied by user input, without delaying the message
         * if there's no input.
         */
        status = pthread_mutex_unlock (salarm_mutex);
        if (status != 0)
            err_abort (status, "Unlock mutex");
        if (sleep_time > 0)
            sleep (sleep_time);
        else
            sched_yield ();
        /*
         * If a timer expired, print the message and free the
         * structure.
         */
        if (alarm != NULL) {
            printf("(td) ts\n", alarm->seconds, alarm->me(ssage)
            free (alarm);
        }
    }
}

最后,我们来讨论alarm_mutex.c的主程序代码。基本结构我们已经开发过的版本相同,包括一个循环、从stdin中读取用户输入的请求并依次处理它们。这一次,没有像alarm.c中那样同步地等待,也没有像alarm_fork.c 或alarm_thread.c 那样为每个请求创建一个异步处理实体进程或线程),而是将所有请求排队,等待服务线程alarm_thread处理。一旦主线程将所有请求排队,它就可以读取下一个请求了。

建立一个服务线程来处理所有请求。返回的线程ID保存在局部变量thread中(尽管我们不使用它)。

与其他的闹铃版本一样读取并处理用户请求。就像在alarm_thread.c中那样,数据保存在malloc分配的堆结构中。

程序将闹铃请求添加到alarm_list列表中,该列表由主线程和alarm_thread线程共享。所以在访问共享数据之前,需要将互斥量alarm_mutex加锁。

由于线程alarm_thread串行地处理列表中的请求,所以没有办法知道从读取用户请求到处理请求的时间间隔。因此,alarm结构中包含了闹铃的绝对时间。绝对时间是通过将用户输入的闹铃时间间隔加上由time调用返回的当前时间获得。

闹铃请求在列表alarm_list中按照闹铃时间先后顺序排序。插入代码遍历列表,直到找到第一个闹铃时间大于或等于新闹铃请求时间的节点,然后将新请求插入到找到的节点前。因为alarm_list是个简单的链表,遍历维护了两个指针:一个next指针指向当前节点,一个last指针指向前一个节点的link指针或者指向列表头指针。

如果没有找到大于或等于当前闹铃时间的节点,则将新请求节点插入列表尾部。当退出遍历时,如果当前节点指针为NULL,则前一节点(或链表头)指向新请求节点。

int main (int argc, char *argv[]) {
    int status;
    char line[128];
    alarm_t *alarm, **last, *next;
    pthread_t thread;

    status = pthread_create {
        &thread, NULL, alarm_thread, NULL);
    if (status != 0)
        err_abort (status, "Create alarm thread");
    while {1) {
        printf ("alarm> ");
        if(fgets(line, sizeof(line), stdin) == NULL) exit(0)
        if (strlen (line)<= 1) continue;
        alarm = (alarm_t*)malloc (sizeof (alarm_t));
        if (alarm == NULL)
            errno_abort ("Allocate alarm");
        /*
         * Parse input line into seconds (%d) and a message
         * (864[^\n]), consisting of up to 64 charadters
         * separated from the seconds by whitespace.
         */
        if (sscanf (line, "%d 864[^\n]",
            &alarm->seconds, alarm->message) < 2) {
            fprintf (stderr, "Bad command\n");
            free (alarm);
        } else {
            status = pthread_mutex_lock (&alarm_mutex);
            if (status != 0)
                err_abort (status, "Lock mutex");
            alarm->time = time (NULL) + alarm->seconds;
            /*
             * Insert the new alarm into the list of alarmis,
             * sorted by expiration time.
             */
            last = &alarm_list;
            next = *last;
            while (next!= NULL) {
                if (next->time >= alarm->time) {
                    alarm->link = next;
                    *last = alarm;
                    break;
                }
                last = &next->link;
                next = next->link;
            }
            /*
             * If we reached the end of the list, insert tthe new
             * alarm there. ("next" is NULL, and "last" pointts
             * to the link field of the last item, or tothe
             * list header).
             */
            if (next == NULL){
                *last = alarm;
                alarm->link = NULL;
            }
#ifdef DEBUG
            printf ("[list: ");
            for (next = alarm_list; next != NULL; next = 1next->link)
                printf ("#d{%d)[\'\'\'] ", next->time,
                    next->time - time (NULL), next->message);
            printf ("]\n");
#endif
            status = pthread_mutex_unlock (&alarm_mutex);
            if (status 1= 0)
                err_abort (status, "Unlock mutex");
        }
    }
}

这个简单的例子存在几个严重的缺点。尽管与alarm_fork.c 和 alarm_thread.c相比,该实例具有占用更少资源的优势,但它的响应性能不够。一旦alarm_thread线程从列表中接收了一个闹铃请求,它就进入睡眠直到闹铃到期。当它发现列表中没有闹铃请求时,也会睡眠1秒,以允许主线程接收新的用户请求。当alarm_thread线程睡眠时,它就不能注意到由主线程添加到请求列表中的任何闹铃请求,直到它从睡眠中返回。

这个问题可以通过不同的方式解决。当然最简单的为方式就是像alarm_thread.c那样为每个闹铃请求建立一个线程。当然这也不坏,因为线程还是比较廉价的。不过还是没有alarm_t数据结构廉价,并且我们更喜欢构造高效的程序,而不仅仅是响应性好的程序。最好的办法是使用条件变量来通知共享数据的状态变化。

3 非阻塞式互斥量锁

当调用pthread_mutex_lock加锁互斥量时,如果此时互斥量已经被锁住,则调用线程将被阻塞。通常这是你希望的结果,但有时你可能希望如果互斥量已被锁住,则执行另外的代码路线,你的程序可能做其他一些有益的工作而不仅仅是等待。为此,Pthreads提供了pthread_mutex_trylock函数,当调用互斥量已被锁住时调用该函数将返回错误代码EBUSY。

使用非阻塞式互斥量加锁函数时,需要确保只有当pthread_mutex_trylock函数调用成功时,才能解锁互斥量。只有拥有互斥量的线程才能解锁它。一个错误的pthread_mutex_trylock函数调用可能返回错误代码,或者可能解锁其他线程依赖的互斥量,这将导致程序产生难以调试的错误。

4 调整互斥量满足工作

互斥量有多大?我可不是指一个pthread_mutex_t类型的结构构占了多少内存。我是使用了一种通俗的、不完全准确的、但是可以被大多数人接受的说法。这种有趣的用法是在有关如何将非线程代码改为线程安全代码的过程中流行起来的。实现线程安全的函数库的一个相对简单的做法是创建一个互斥量,在每次进入函数库时锁住它,在退出库的时候解锁它,这样函数库就变成了一个串行区域,从而防止了线程问的任何冲突。我们就把保护如此大的串行区域的互斥量称为”大”互斥量,并形而上学地认为比那些只保护几行代码的互斥量要明显地大。

进一步扩展,保护两个变量的互斥量比保护一个变量的互斥量要”大”。那么到底互斥量该多大呢?答案只能是:足够大,但不要太大。

当需要保护两个共享变量时,你有两种基本策略:可以为每个个变量指派一个”小”的互斥量,或者为两个变量指派一个”大”的互斥量。哪一种方法更好取决于很多因素。并且,在开发过程中影响因素可能发生改变,这依赖于有多少线程需要共享数据和如何使用共享变量。

以下是主要的设计因素:

  • 互斥量不是免费的,需要时间来加锁和解锁。锁住较少互斥量的程序通常运行得更快。所以,互斥量应该尽量少,够用即可,每个互斥量保护的区域应则尽量大。
  • 互斥量的本质是串行执行。如果很多线程需要频繁地加锁同一个互斥量,则线程的大部分时间就会在等待,这对性能是有害的。如果互斥量保护的数据(或代码)包含彼此无关的片段,则可以将大的互斥量分解解为几个小的互斥量来提高性能。这样,任意时刻需要小互斥量的线程减少,线程等待时间就会减少。所以,互斥量应该足够多(到有意义的地步),每个互斥量保护的区域则应尽量的少。
  • 上述两方面看似互相矛盾,但是这不是我们头一次遇到的情清况。一旦当你解了互斥量的性能后,就能够正确地处理它们。

在复杂的程序中,通常需要一些经验来获得正确的平衡。在大多数情况下,如果你开始使用较大的互斥量,然后当经验或性能数据告诉你哪个地方存在频繁的竞争时,你应改用较小的互斥量,则你的代码通常会更为简单。简单单是好的。除非发现问题,否则不要轻易花费太多时间优化你的代码。

另一方面,如果从一开始就能明晰你的算法必然导致频繁的竞争,则不要过于简单化。开始使用必须的互斥量和数据结构比后来增加它们容易得多。

5 使用多个互斥量

有时,一个互斥量是不够的,特别是当你的代码需要跨越软件体系内部的界限时。例如,当多个线程同时访问一个队列结构时,你需要两个互斥量,一个用来保护队列头,一个用来保护队列元素内的数据。当为多线程建立一个树型结构时,你可能需要为每个节点设置一个互斥量。

同时使用多个互斥量会导致复杂度的增加。最坏的情况就是死锁的发生,即两个线程分别锁住一个互斥量而等待对方的互斥量。

如果可以在独立的数据上使用两个分离的互斥量,那么就应该这样做。这样,通过减少线程必须等待其他线程完成数据操作(甚至是该线程不需要的数据)的时间,你的程序会最终取得成功。如果数据独立,则某个特定函数就不太可能经常需要同时加锁两个互斥量。

当数据不是完全独立的时候,情况就复杂了。如果你的程序中有一个不变量,影响着由两个互斥量保护的数据,即使该不变量很少被改变或引用,你迟早需要编写同时锁住两个互斥量的代码,来确保不变量的完整。如果一个线程锁住互斥量A后,加锁互斥量B;同时另一个线程锁住互斥量B而等待互斥量A,则你的代码就产生了一个经典的死锁现象。

ISP (Intra Subpartition Mode)

内部子分区(ISP)模式是VVC中新引入的工具之一。 它是一种分区机制,旨在对帧内预测块的非平稳特征进行建模。 具体来说,ISP将一个块的亮度分量垂直或水平地分割成K个大小相等的子分区,这些子分区以顺序的方式一一处理。

Layout

帧内子分区 (ISP) 根据块大小将亮度帧内预测块垂直或水平划分为 2 或 4 个子分区。 例如,ISP 的最小块大小为 4×8(或 8×4)。 如果块大小大于 4×8(或 8×4),则相应的块将被划分为 4 个子分区。 值得注意的是,M × 128(M ≤ 64)和 128 × N(N ≤ 64)ISP 块可能会对 64 × 64 VDPU 产生潜在问题。 例如,single-tree 情况下的M×128 CU具有M×128亮度TB和两个相应的M/2×64色度TB。 如果CU使用ISP,则亮度TB将被分为4个M×32TB(只能水平分割),每个TB小于64×64块。 然而,在当前的ISP设计中,色度块没有被划分。 因此,两个色度分量的大小都将大于 32 × 32 块。 类似地,使用 ISP 的 128 × N CU 也可以有类似的情况。 因此,这两种情况对于 64 × 64 解码器管道来说是一个问题。 因此,可以使用 ISP 的 CU 大小限制为最大 64 × 64。图 19 显示了两种可能性的示例。 所有子分区都满足至少有16个样本的条件。

Processing

如果是水平分割,则从上到下逐一处理子分区;如果是垂直分割,则从左到右处理。每个子分区的编码方式与任何其他帧内预测块相同:首先,使用子分区的相邻样本生成预测,并获得相应的残差信号。 后者被变换和量化,变换系数被熵编码并发送到解码器。 最后,得到的重建样本可以用来生成下一个子分区的预测。 此过程一直持续到所有子分区都已编码为止。

然而,对于宽度小于4的子分区,在上述过程中有一个例外。这是因为典型的硬件架构使用光栅扫描模式分配样本,然后以4×1组的方式访问它们。对于这样的设计,1×H和2×H的子分区可能构成一个问题。因此,ISP的最小预测宽度为4个样本,如下所示:不是在TB级别执行每个预测,而是将TB分组在4×H区域中,这些区域仅使用相邻样本作为参考进行一次预测。这保证了可以同时处理每个4×H区域内的TB。例如,一次预测一个垂直分割的4×16 CU,然后并行处理相应的四个1×16 TB。

Prediction

在使用ISP的CU内,所有子部分都应用相同的帧内预测模式,因此只需要对其进行一次编码。ISP帧内预测模式可以从任何传统的帧内预测方式中选择,即Planar、DC和角度方式,其中对于后一种方式,根据CU宽高比执行角度方式向广角方式的转换。PDPC以与非ISP情况相同的方式应用于所有ISP子分区,前提是它们的宽度和高度至少为4。此外,参考样本滤波过程和帧内插值滤波器选择的条件不再存在,并且在ISP模式下,DCT-IF 滤波器总是用于分数位置插值。如果一个块的MRL索引不是0,则ISP编码模式将被推断为0,因此ISP模式信息将不会发送到解码器。

Transforms

为了降低信令成本,ISP总是使用隐式多变换选择(MTS)。因此,编码器不会对每个结果子分区的不同可用变换执行RD测试。ISP模式的变换选择将改为固定的,并根据所使用的帧内模式、处理顺序和块大小进行选择。因此,不需要任何信号。例如,设th和tv是分别为w×h子分区,其中w是宽度h是高度。然后根据以下规则选择变换:

  • 如果w=1或h=1,则分别不存在水平变换或垂直变换。
  • 如果w≥4且h≤16,则th=DST-VII,否则,th=DCT-II
  • 如果h≥4且h≤16,则tw=DST-VII,否则,tw=DCT-II

其次,低频不可分离变换(LFNST)是针对整个ISP块全局用信号发送的,因此,相同的LFNST矩阵用于具有非零编码块标志(CBF)的所有子分区。只有当所有子分区都满足zero-out条件时,LFNST才能用于ISP-CU。非ISP情况下的LFNST转换内核也用于ISP。因此,如果子分区的宽度或高度小于4,则不能将LFNST用于ISP。

Coefficient coding and signalization

子分区的残差信号的系数以与 VVC 中的常规块相同的方式进行熵编码,并进行以下修改:

  • 每个子分区的编码块标志(CBF)的上下文是先前编码的子分区的CBF的值(在第一个子分区的情况下,默认值为0)。
  • 使用ISP的块的至少一个CBF被假定为非零。 因此,如果一个块包含 n 个子分区,并且前 n-1 个子分区的 CBF 为零,则第 n 个 CBF 被推断为1,因此不会显式地用信号通知它。
  • 如果子分区是一行,则最后位置语法元素仅需要发送一个坐标。
  • 令 w 和 h 分别为每个子分区的宽度和高度。 然后,如果 w ≥ 8 且 h ≤ 2,系数子块将具有 16/h × h 的形状。 类似地,如果 w ≤ 2 并且 h ≥ 8,系数子块将具有 w × 16/w 形状。 在所有其他情况下,它们将具有与常规帧内预测块中使用的相同的 4 × 4 形状。 结果,所有系数子块将具有 16 个样本。

关于信令,为每个块添加了两个新的语法元素:第一个是指示当前块是否使用 ISP 的标志(仅适用于不使用 MRL 的非 4×4 亮度块)。 当使用 ISP 时,会发送第二个标志,指定使用哪种类型的分割(水平或垂直)。 此外,这两个新标志使用一个上下文自适应二进制算术编码(CABAC)。 请注意,如果只能进行一次分割(例如,128 × 64 块只能垂直分割),则在解码器侧推断出该标志,因此不会传输该标志。

二值化过程(Binarization process)

VVC标准中使用了4种二值化方案:截断莱斯码(truncated Rice (TR)), 截断二进制码(truncated binary (TB)), k阶指数哥伦布码(k-th order Exp-Golomb (EGk)), 定长码(fixed-length (FL))。

TR码

TR码由前缀和后缀组成,后缀可能不存在。TR码有3个输入,分别是:语法元素值 symbolVal、门限值 cMax 和莱斯参数 cRiceParam。语法元素值 symbolVal 的前缀值 prefixVal 定义如下:

prefixVal = symbolVal >> cRiceParam

如果 prefixVal 小于 cMax >> cRiceParam,则前缀码由 prefixVal 个1和0组成。下表给出了这种一元二值化的码表:

如果 prefixVal 大于等于 cMax >> cRiceParam,则前缀码由 cMax >> cRiceParam 个1组成。

cMax 大于 symbolVal cRiceParam 大于0时,TR码的后缀存在,后缀值推导如下:

suffixVal = symbolVal − ( prefixVal << cRiceParam )

TR 码的后缀通过定长码(FL)为 suffixVal 进行二值化,cMax 值等于 ( 1 << cRiceParam ) − 1。

TB码

TB码的输入有两个,分别为语法元素值 synVal 和门限值 cMax。TB码的推导过程如下:

n = cMax + 1
k = Floor( Log2( n ) )
u = ( 1 << ( k + 1 ) ) − n

如果 synVal 小于 u,则通过定长码(FL)为 synVal 进行二值化,其中 cMax 值等于 ( 1 << k ) − 1。否则,通过定长码(FL)为 (synVal+u) 进行二值化来导出TB码,其中 cMax 值等于(1<<(k+1))−1。

k阶指数哥伦布码

k阶指数哥伦布码包括前缀码和后缀码,输入为语法元素值 symbolVal 和阶数 k,输出的二进制串定义如下,其中函数put(X)是将X放在二进制串的末尾。

absV = Abs( symbolVal )
stopLoop = 0
do
if( absV >= ( 1 << k ) ) {
put( 0 )
absV = absV − ( 1 << k )
k++
} else {
put( 1 )
while( k−− )
put( ( absV >> k ) & 1 )
stopLoop = 1
}
while( !stopLoop )

下表给出了0阶指数哥伦布码的前缀码和后缀码

下表给出了无符号0阶指数哥伦布码,对应语法元素表中的ue(v)

对于有符号的0阶指数哥伦布码,对应语法元素表中的se(v),VVC标准中采用了映射的方法,映射表为:

FL码

FL 码是通过使用符号值 symbolVal 的固定长度位无符号整数二进制串来构造的,其中fixedLength = Ceil( Log2( cMax + 1 ) )。 FL 码的 bin 索引使得 binIdx = 0 与最高有效位相关,并且 binIdx 的值向最低有效位递增。

Geometric Partitioning Mode in Versatile VideoCoding

H. Gao, S. Esenlik, E. Alshina, and E. Steinbach, “Geometric partitioning mode in versatile video coding: Algorithm review and analysis,” IEEE Trans. Circuits Syst. Video Technol., early access, Nov. 24, 2020

对于自然视频内容,矩形块划分对于 MCP (motion compensated prediction)来说不是最佳选择。 视频序列中图片的运动场通常由运动物体的边界来分割,如图2(a)和图2(b)所示。 这是因为对象通常表现出相对于静态背景或其他移动对象的移动,并且自然序列中的对象边界很少遵循矩形块图案。 如图2(c)所示,当使用矩形块来近似移动对象边界时,需要更精细的块划分,这增加了用于用信号通知这些块的划分和预测语法元素的速率开销。 此外,近似的分割边界很少遵循实际的运动场边界。 因此,预测误差更高,这增加了残差信令的比特率。

为了提高划分精度,基于分割的划分方法被集成到现有的视频编码标准结构中。 在基于分割的划分方法中,划分图是通过在编码器处对参考图片中的参考块应用分割算法来获得的。 在解码器处应用相同的分割算法以避免分割图的变换。 当前块被划分图分成多个分区,每个分区或者单独地与其关联的MV执行MCP或者从其他分区导出样本值。 可以向解码器发送额外的MV或者在解码器侧导出额外的MV以指示应用分割算法的参考块的位置。 尽管基于分割的分割方法很好地近似了对象的实际轮廓,但像素精确的分割算法显着增加了计算复杂性并导致硬件实现困难,特别是在解码器处。 此外,基于分割的分割方法无法有效地处理大块或包含由复杂纹理引起的多个边缘的块,例如图2(b)的左侧。

在 HEVC 和 VVC 的开发过程中,几何划分方案及其变体已在文献中提出并在核心实验 (CE) 中进行了研究。 几何划分方案旨在提高划分精度并最小化解码器端的计算复杂度和实现难度。 代替分割算法,使用一组预定义的直线将矩形块几何分割成两个分区,如图2(d)中的示例所示。 每个分区与其运动信息相关联并单独执行MCP。 运动信息和划分信息被发送到解码器。 该块由解码器侧解析的运动和划分信息直接重建。 因此,几何划分方案的解码器复杂度增加较小。

A. Core Algorithm of GPM in VVC

由于VVC中的帧内预测CU可以使用角度和非线性预测模式,非水平和非垂直边缘得到了很好的处理。 因此, GPM 重点关注帧间预测的 CU。 当GPM应用于CU时,该CU被直线划分边界分成两部分。 划分边界的位置在数学上由角度参数 \(\varphi\)和偏移参数 \(\rho\) 定义。 这些参数被量化并组合成GPM划分索引查找表。 当前CU的GPM划分索引被编码到比特流中。 总的来说,VVC 中的 GPM 支持 64 种划分模式,对于尺寸为 \(w\times h={2^k}\times2^l\)的亮度 CU,其中 \(k,l~\in~{3\ldots6}\)。 此外,GPM 在宽高比大于 4:1 或小于 1:4 的 CU 上被禁用,因为窄 CU 很少包含几何划分的图案。

两个 GPM 分区包含用于预测当前 CU 中相应部分的单独运动信息。 GPM 的每个部分只允许使用单向 MCP,因此 GPM 中 MCP 所需的内存大小等于常规双向 MCP 的大小带宽。 为了简化运动信息编码并减少GPM可能的组合,运动信息采用Merge模式进行编码。 GPM Merge候选列表源自传统的Merge候选列表,以确保仅包含单向运动信息。

图4说明了GPM的预测过程。 当前CU的右侧预测部分(大小为w×h)由MV0从P0预测,而左侧部分由MV1从P1预测。 最终的 GPM 预测 PG 是通过使用整数混合矩阵 W0 和 W1 执行混合过程生成的,包含的权重范围为 0 到 8。这可以表示为(1):

\(P_{\mathrm{G}}=(W_0\circ P_0+W_1\circ P_1+4)\gg3\)

其中(2):

\(W_0+W_1=8J_{w,h},\)

其中 Jw,h 是大小为 w × h 的矩阵。 混合矩阵的权重取决于样本位置和分区边界之间的位移。 图 5 显示了一组示例混合矩阵。 混合矩阵推导的计算复杂度极低,因此这些矩阵可以在解码器侧即时生成。

然后从原始信号中减去GPM预测的PG以生成残差。 使用常规 VVC 变换、量化和熵编码引擎将残差变换、量化并编码到比特流中。 在解码器侧,通过将残差添加到 GPM 预测 PG 来重建信号。 当残差可以忽略不计时,GPM 还支持Skip模式。

B. Partitioning Boundary Definition and Quantization

1)划分边界的定义:划分边界定义为几何上位于CU内部的直线。 这条直线的方程用 Hessian 范式表示为(3):

\(x_\mathrm{c}~\cos(\varphi)-y_\mathrm{c}~\sin(\varphi)+\rho=0\)

其中(xc,yc)定义与CU的中心位置相关的连续位置。 在图6(a)所示的示例中,(3)中的角度参数\(\varphi\)描述了从x轴到分割边界法向量的逆时针角度,而(3)中的偏移参数 \(\rho\) 是划分边界从CU中心定义的原点的位移。请注意,y轴被反转以简化分区边界的离散化。

任意位置(xc,yc)相对于分割边界的位移d用于导出混合矩阵W0W1。 根据(3),d的值给出为(4):

\(d(x_\mathrm{c},y_\mathrm{c})=x_\mathrm{c}\mathrm{~}\cos(\varphi)-y_\mathrm{c}\mathrm{~}\sin(\varphi)+\rho.\)

由于位移是有方向的,因此 d 的值可以是正值或负值。 d 的符号表示位置 (xc, yc) 属于哪个分区,而 d 的大小等于 (xc, yc) 到分区边界的距离。

2)角度参数\(\varphi\)的量化:为了实现合理数量的定义的划分边界,必须对角度参数\(\varphi\)和偏移参数\(\rho\) 进行量化。 如图 6(b)所示,角度参数\(\varphi\)被量化为 20 个离散角度 \(\varphi_i\),范围 [0, 2π) 对称划分。 由于对角线或反对角线分割边界在 GPM 中最常用,因此量化的\(\varphi_i\)被设计为固定\(\tan(\varphi_i)\) 值,该值等于应用 GPM 的 CU 的可能长宽比。 例如,如果将 GPM 应用于宽高比 w/h = 1/2 的 CU,则对角线分区边界与由 \(\varphi_i~=~\arctan(1/2)\) 和 \(\rho=0\) 定义的 (3) 线对齐。 在所提出的 GPM 算法中,\(\tan(\varphi_i)\) 被限制为 {0, ±1/4, ±1/2, ±1, ±2, ∞} 中的值。 请注意,不包括产生接近水平划分边界的±4的正切值,因为运动场的接近水平划分不太频繁地用于自然视频内容。

根据基于正切值的角度参数量化,(4) 中的 \(\cos(\varphi_i)\) 被离散化为表 1 所示的 3 位精度查找表。 cosLut[·] 的输入 i 是量化 \(\varphi_i\)的角度索引。 请注意,表 I 中的一些未使用的角度索引 i 值被跳过。 基于三角恒等式,\(sin\varphi_i\)的离散化查找表可以很容易地导出(5):

\(\operatorname{sinLut}[i]=-\operatorname{cosLut}[(i+8)\%32]\)

因此,(4)被重写为(6):

\(d(x_c,y_c)=(x_c\cdot cosLut[i])\gg3+(y_c\cdot cosLut[(i+8)\%32])\gg3+\rho\)

3)偏移参数\(\rho\)的量化:图6(c)示出了偏移参数\(\rho\)量化的示例。 根据CU宽度w和高度h,偏移参数\(\rho\)被量化为\(\rho_j\)。 偏移索引 j = {0 … 3}。 为了避免不同块大小之间分布不均匀的分区边界偏移,首先将 \(\rho_j\)分解为(7):

\(\rho_j=\rho_{x,j}\cos(\varphi_i)-\rho_{y,j}\sin(\varphi_i)\)

或(8):

\(\begin{aligned}\rho_j&=(\rho_{x,j}\cdot\cos\text{Lut}[i])\gg3\\&+(\rho_{y,j}\cdot\cos\text{Lut}[(i+8)\%32])\gg3.\end{aligned}\)

然后使用 ρx,j 和 ρy,j 与 w 和 h 耦合(9):

\(\rho_{x,j}=\begin{cases}0&i\%16=8\text{ or }(i\%16\neq0\text{ and }h\geqslant w)\\\pm j\cdot w/8&\text{otherwise}\end{cases}\)

和(10):

\(\rho_{y,j}=\begin{cases}\pm j\cdot h/8&i\%16=8\text{ or }(i\%16\neq0\text{ and }h\geqslant w)\\0&\mathrm{otherwise},\end{cases}\)

其中 j 是量化偏移 ρj 的偏移索引。 (9)和(10)中的ρx,j和ρy,j的符号表示偏移方向,如果角度索引i < 16,则将其设置为正,否则设置为负。 偏移参数量化后,式(6)改写为(11):

\(\begin{aligned}d(x_{\mathrm{c}},y_{\mathrm{c}})&=((x_{\mathrm{c}}+\rho_{x,j})\cdot cosLut[i])\gg3+((y_{\mathrm{c}}+\rho_{y,j})\cdot cosLut[(i+8)\%32])\gg3.\end{aligned}\)

表 II 中列出的几个冗余量化偏移在所提出的 GPM 算法中被删除。 因此,GPM 划分模式的总数为 \(N_\mathrm{GPM}~=~N_{\varphi}N_{\rho}~-~N_{\varphi}/2~-~2~-~4~=~64\) ,其中 Nψ = 20,Nρ = 4。GPM 划分模式如图 7 所示,按相同的角度索引 i 分组。 图 7 中的虚线划分线展示了未包含在所提出的 GPM 设计中的冗余模式。 支持的 64 种 GPM 分区模式由语法元素 merge_gpm_partition_idx 进行索引。

4)样本位置的离散化:为了避免GPM预测过程中的额外插值,P0和P1的整数位置在(1)中加权。 因此,混合矩阵 W0 和 W1 需要从基于整数位置的 d(m, n) 导出,其中 m, n ∈ Z ≥0,而不是从 d(xc, yc) 导出,其中 xc, yc ∈ R。 原点转移到图6(d)中的左上角,因为样本位置通常指的是VVC中CU的左上角样本。 因此,(11) 的完全离散形式由下式给出(12):

\(d(m,n)=((m+\rho_{x,j})\ll1-w+1)\cdot cosLut[i] +((n+\rho_{y,j})\ll1-h+1)\cdot cosLut[(i+8)\%32]\)

它用于生成混合矩阵 W0 和 W1 。 由于 cosLut[·] 为 3 位精度,且 m 和 n 左移一位,因此 d(m, n) 和 d(xc, yc) 的关系满足(13):

\(d(m,n)=\lfloor16d(x_\mathrm{c},y_\mathrm{c})+0.5\rfloor\)

请注意,在 (12) 中,d(m, n) 无需任何乘法即可实现,因为 cosLut[·] 中的值、ρx,j 中的 w/8 以及 ρy,j 中的 h/8 都是基于移位的值。 (12) 的无乘法设计的结果是 GPM 的编码器和解码器复杂度极低。

C. Blending Matrices of GPM

在提出的GPM算法中,应用了软的混合过程。 也就是说,如图8所示,当样品位于软混合区域内(即在虚线之间)时,使用(0,8)范围内的加权值倾斜; 否则,选择了0或8的完整加权值。 一个混合矩阵中单个样品位置的加权值由坡道函数给出(14):

\(\gamma_{x_c,y_c}=\begin{cases}0&d(x_c,y_c)\leqslant-\tau\\\frac{8}{2\tau}(d(x_c,y_c)+\tau)&-\tau<d(x_c,y_c)<\tau\\8&d(x_c,y_c)\geqslant\tau,&\end{cases}\)

τ定义混合区域的宽度。 在呈现的GPM算法中,选择τ作为两个样品。 基于(13)的τ= 2,(14)被离散为(15):

\(\gamma_{m,n}=\text{Clip}3(0,8,(d(m,n)+32+4)\gg3)\)

其中d(m, n)是根据(12)计算的。 然后,可以通过(2)轻松地计算其他混合矩阵的权重。 图5可视化一组混合矩阵的示例。 请注意,(2)和(15)用于生成Luma分量的混合矩阵,并且基于视频序列的颜色格式,简单地从Luma混合矩阵中进行了色度混合矩阵。

混合矩阵分配基于以下规则:如果角度索引i不在[12,26]的范围内,则将(15)的输出γm,n分配给与m∈{0…w – 1}和n∈{0…h – 1}的混合矩阵W0,而W1由(2)计算。 否则,如果角度索引i属于[12,26],则将γm,n分配给W1,而w0将推到出。 图9显示了解释这些规则的示例。 从常规Merge列表中得出的GPM Merge列表是通过在B1,A1,A1,B0,A0和B2的顺序上确定空间相邻CUS的运动信息来构建的。 熵编码期间,B1索引的code word通常最短。 当预测P0的运动信息由B1预测(即用最短的code word编码)时,GPM中运动信息的信号传输成本被最小化,因为P0的Merge索引在P1 Merge索引之前发出信号。 因此,在图9(a)的情况下,\(i\notin[12,26]\),(15)的γm,n分配给W0,而在图9(b)的情况下,γm,n分配给W1。

D. Motion Information Storage

在VVC中,使用MCP后使用4×4样品粒度将CU的运动信息存储在缓存中。 存储的运动信息用于MV预测和合并列表的构建,或用于推导Deblocking滤波器的边界强度。 在VVC的常规MCP中,实际用于预测当前CU的运动信息被跨越并存储到4×4单元中。 但是,GPM涉及三种类型的运动信息。 也就是说,P0和P1包含其自己的单向MV,而混合区域则通过两者的运动信息进行物理预测。 因此,GPM的运动信息存储是基于分区的自适应设计的。

与混合矩阵派生类似,4×4单元的运动存储类型\(T\)由从本机的中心到划分边界的位移d确定。 在这里,使用(12)中的4×4单元的整数中心位置计算d,即(4M+2,4n+2)用m∈{0…w – 1}和n∈{0…h – 1}。 然后,该4×4单元的运动存储类型\(T\)为(16):

\(T_{4m,4n}=\begin{cases}1-A&d(4m+2,4n+2)\leqslant-32\\2&-32<d(4m+2,4n+2)<32\\A&d(4m+2,4n+2)\geqslant32,&\end{cases}\)

A 表示与混合矩阵对齐对齐的分区分配,由(17):

\(A=\begin{cases}1&i\in[12,26]\\0&otherwise.&\end{cases}\)

图10显示了具有角度索引\(i\notin[12,26]\)和\(i\in[12,26]\)的GPM运动存储图的示例。 t = 0或t = 1表示相应的4×4单元存储了用于预测P0或P1的单向运动信息,而带有t = 2的单元用于对两个单向运动组合的双向运动信息进行排序。 如果P0和P1的运动信息来自同一参考图片列表,则将双向运动信息设置为P1的运动信息。 请注意,GPM的运动存储过程通常与硬件实现中的混合过程并行执行,因此,通常在运动存储过程中重新计算d(4M+2, 4n+2),而不是重复使用d(m, n)来自混合矩阵推导。

E. GPM Merge List Derivation

GPM的运动信息通过Merge模式进行编码。 如前所述,GPM的每个部分仅使用单向运动信息,以避免GPM的MCP的其他内存占用。 但是,常规Merge列表可能包含直接从相邻CUS复制的单向或双向Merge候选。 由于双向Merge候选,常规Merge列表不能直接用作GPM Merge列表。

GPM Merge列表是通过GPM Merge索引的奇偶性从常规Merge列表中派生出来的。也就是说,对于具有GPM Merge索引的偶数值的候选者,使用来自具有对应的常规Merge索引的参考图片列表L0的MV0作为GPM Merge候选者。如果MV0不可用,则使用来自参考图片列表L1的MV1。相反,选择MV1作为GPM Merge索引的奇数值的默认GPM Merge候选,并且如果MV1不可用,则使用MV0。基于索引奇偶校验的方法直接从常规Merge列表中提取GPM Merge候选,而无需修剪,这将实现复杂性降至最低。

表III显示了GPM Merge列表推导的示例。 在表III(a)中所示的常规Merge列表的示例中,索引2和3的Merge候选中包含仅可用MV1的单向MVS,而其他Merge候选可以使用的是双向MVS。 相应的GPM Merge列表如表III(b)所示,MV1用于索引2的候选,因为默认MV0不可用。 GPM Merge候选者的其余部分是根据GPM Merge索引的奇偶校验从常规Merge列表中提取的。

F. Entropy Coding

GPM的几个语法元素使用熵编码被编码到位流中。 在高级语法HLS中,启用标志的GPM和GPM Merge候选者的最大数量在序列参数集SPS中编码。 GPM启用标志用固定长度代码进行编码,而GPM Merge候选者的最大数量相对于常规Merge候选者的最大数量,并用0-阶指数指数Golomb代码进行编码。

图11示出了Merge数据的语法树。 因为 GPM 模式是在Merge数据语法树的底部用信号通知的,所以在 CU 级别不需要 GPM 启用标志。 当获取的 ciip 标志为 0 并且满足 GPM 的应用条件时,GPM 隐式启用。

表IV是GPM的部分语法元素表。使用VVC的基于上下文的自适应二进制算术编码(CABAC)引擎对三个语法元素进行编码,包括GPM分区索引(merge_gpm_partition_idx)、P0的Merge索引(merge_gpm_idx0)和P1的Merge索引(merge_gpm_idx1)。由于merge_gpm_partition_idx是大致均匀分布的,因此使用固定长度代码对其进行二进制化,并使用CABAC的旁路模式(无上下文模型)对其进行编码,以简化编码过程,而merge_gpm_idx0merge_gpm_idx1用0阶截断Rice码进行二进制化,并使用用相同的常规Merge索引初始值初始化的一个单个上下文模型进行编码。注意,所使用的GPM Merge索引GPM_idx0,1通过以下方式从用信号发送的语法元素导出(18):

\(\mathrm{GPM\_idx}_0=merge\_gpm\_idx0\)

和(19):

\(\begin{aligned}\text{GPM_idx}_1&=merge\_gpm\_idx1\\&+(merge\_gpm\_idx1\geqslant\text{GPM_idx}_0)?1:0,\end{aligned}\)

因为两个Merge索引不允许相同。

G. Encoder Search

所提出的 GPM 算法中应用了基于率失真优化 (RDO) 的编码器搜索。 与基于纹理的搜索方法相比,基于RDO的搜索更容易捕获包含相似纹理的几何分离的运动场。 此外,对于不同的视频内容,基于 RDO 的搜索比基于统计的方法更通用。

假设用信号发送的GPM Merge候选的最大数量是六,则每个划分模式总共具有6×5=30个运动信息组合,因为GPM_idx0和GPM_idx1不相同。 由于在 GPM 中设计了 NGPM = 64 划分模式,因此编码器将从 6 × 5 × 64 = 1920 个划分和运动信息的组合 GPM 候选中选择一个。 作为比较,在 VTM 7.0 中使用基于完全搜索的 RDO 对多达 6 × 5 × 2 = 60 个组合的 TPM 候选进行了详尽的测试。 组合的 GPM 候选者的数量对于 RDO 在编码器侧执行完整的混合和变换编码过程来说非常大。 为了降低 GPM 编码器搜索的计算复杂度,使用以下四个阶段确定最佳匹配 GPM 候选者:

Stage1:对于GPM Merge列表的每个单向运动信息候选,执行六个GPM Merge候选上的全CU MCP以获得与当前CU大小相同的六个矩形预测器。 预测变量和原始信号之间的亮度分量的绝对差之和 (SAD) 计算为 SADCU,k,其中索引 k ∈ {0 … 5}。 此阶段排除 GPM Merge候选列表中的相同条目。 如果Merge候选列表中的所有运动信息相同,则中止整个GPM编码器搜索。

Stage2:对于每个 GPM 划分模式,使用硬混合矩阵屏蔽掉由六个矩形预测器的 GPM_idx0 预测的部分(即,仅选择 0 或 8 作为权重值,具体取决于位移 d(m, n)的符号.)。 计算这些部分的亮度 SAD 并表示为 SADP0,k,l,其中 k = 0… 5 且 l = 0…63. 由于使用了硬混合矩阵(无混合区域),因此通过 GPM_idx1 预测的部分的 SAD 可以通过以下方式获得(20):

\(\mathrm{SAD}_{\text{P}1,k,l}=\mathrm{SAD}_{\text{CU},k}-\mathrm{SAD}_{\text{P}0,k,l}.\)

对于索引为 l 的划分模式,组合率失真 (RD) 成本 Jα,β,l 由下式给出(21):

\(J_{\alpha,\beta,l}=\text{SAD}_{\text{P}0,\alpha,l}+\text{SAD}_{\text{P}1,\beta,l}+\lambda(R_\alpha+R_\beta)\)

其中α和β表示预测指标GPM_idx0和GPM_idx1,Rα和Rβ表示相应的估计码率。 α 和 β 都属于 {0…5},但 α = β 的情况被排除在编码器搜索中。 GPM 候选按 Jα,β,l 排序。 此外,排除具有 Jα,β,l > SADCU,k +λRk 的 GPM 候选,其中 Rk 是第 k 个 GPM Merge候选的估计码率。

Stage3:第 2 阶段中最好的 60 个(或更少)组合 GPM 候选者用于进行软混合过程,以在亮度分量中生成 PG。 每个组合候选的 GPM 预测器 PG 和原始信号之间的亮度绝对变换差 (SATD) 之和计算为 SATDPG,l。 RD 成本更新为(22):

\(\begin{aligned}J’_{\alpha,\beta,l}&=\text{SATD}_{\text{PG},l}+\lambda(R_\alpha+R_\beta+R_l)\end{aligned}\)

其中Rl表示划分索引的估计码率。 GPM 候选者再次按 J’α,β,l 排序。 在此阶段,使用先前测试的编码工具(例如常规Merge或Affine)的最低 SATD 成本来提前终止。

Stage4:第 3 阶段中最好的八个(或更少)候选者的相应色度分量是通过软混合过程生成的。 对这些候选应用残差变换编码(如果存在残差)和基于 CABAC 的码率估计,以获得准确的码率成本 RGPM,其中包括运动码率、划分模式和残差编码。 这些候选信号与原始信号之间的三个分量的失真通过平方差之和 (SSD) 来测量,即 SSDPG,l。 最后,总体 RD 成本最低的 GPM 候选者(23):

\(J_{\alpha,\beta,l}^{\prime\prime}=\mathrm{SSD}_{\mathrm{PG},l}+\lambda R_{\mathrm{GPM}}\)

被选为最终的GPM模式。

图 12 显示了全搜索中最常用的 16 种 GPM 模式以及这些模式在所提出的四阶段基搜索中的出现率。 可以看出,基于四阶段的搜索的发生率趋势与全搜索的发生率趋势相似,这表明基于四阶段的搜索可以以相对较低的编码器复杂度有效地保持编码性能。 注意,GPM的编码器搜索算法不限于所提出的基于四阶段的方法。 图 12 中呈现的统计数据可以用作其他编码器实现的指南。

VVC中的 Affine motion compensated prediction

在HEVC中,仅应用平移运动模型来进行运动补偿预测(MCP)。 在现实世界中,运动有很多种,例如放大/缩小、旋转、透视运动和其他不规则运动。 在VVC中,应用基于块的仿射变换运动补偿预测。 如图27所示,块的仿射运动场由两个控制点(4参数)或三个控制点运动矢量(6参数)的运动信息来描述。

对于 4 参数仿射运动模型,块中样本位置 (x, y) 处的运动矢量导出为:

对于 6 参数仿射运动模型,块中样本位置 (x, y) 处的运动矢量导出为:

其中,(mv0x,mv0y)是左上角控制点的运动矢量,(mv1x,mv1y)是右上角控制点运动矢量,以及(mv2x,mv2y)是左下角控制点运动矢量。

为了简化运动补偿预测,应用基于块的仿射变换预测。 为了导出每个 4×4 亮度子块的运动矢量,根据上述方程计算每个子块的中心样本的运动矢量(如图 28 所示),并四舍五入到 1/16 分数精度。 然后应用运动补偿插值滤波器来生成具有导出的运动矢量的每个子块的预测。 色度分量的子块大小也设置为4×4。 4×4 色度子块的 MV 计算为对应 8×8 亮度区域中左上和右下亮度子块 MV 的平均值。

与平移运动帧间预测一样,也有两种仿射运动帧间预测模式:仿射Merge模式和仿射 AMVP 模式。

Affine merge prediction

AF_MERGE模式可以应用于宽度和高度都大于或等于8的CU。在该模式下,当前CU的CPMV是基于空间相邻CU的运动信息生成的。 最多可以有五个 CPMVP 候选者,并且用信号发送索引来指示要用于当前 CU 的候选者。 以下三种类型的 CPMV 候选用于形成仿射合并候选列表:

  • 从相邻 CU 的 CPMV 推断的继承仿射merge候选
  • 使用相邻 CU 的平移 MV 导出的构造仿射merge候选 CPMVP
  • 零 MV

在VVC中,最多有两个继承仿射候选,它们是从相邻块的仿射运动模型导出的,一个来自左相邻CU,一个来自上相邻CU。 候选块如图29所示。对于左侧预测器,扫描顺序为A0->A1,对于上面的预测器,扫描顺序为B0->B1->B2。 仅选择双方的第一个继承候选人。 在两个继承的候选者之间不执行修剪检查。 当识别出相邻仿射CU时,其控制点运动矢量用于导出当前CU的仿射merge列表中的CPMVP候选者。 如图30所示,若将相邻左下块A采用仿射方式编码,则得到包含块A的CU的左上角、右上角和左下角的运动矢量v2、v3和v4。 当块A采用4参数仿射模型编码时,根据v2、v3计算当前CU的两个CPMV。 如果块A采用6参数仿射模型编码,则根据v2、v3和v4计算当前CU的三个CPMV。

构造仿射候选是指通过组合每个控制点的邻近平移运动信息来构造候选。 控制点的运动信息是从图 31 中所示的指定空间邻居和时间邻居导出的。CPMVk (k=1,2,3,4) 表示第 k 个控制点。 对于 CPMV1,检查 B2->B3->A2 块并使用第一个可用块的 MV。 对于 CPMV2,检查 B1->B0 块,对于 CPMV3,检查 A1->A0 块。 对于 TMVP,如果可用,则用作 CPMV4。

获得四个控制点的 MV 后,基于这些运动信息构建仿射merge候选者。 使用以下控制点 MV 组合按顺序构建:

{CPMV1, CPMV2, CPMV3}, {CPMV1, CPMV2, CPMV4}, {CPMV1, CPMV3, CPMV4},
{CPMV2, CPMV3, CPMV4}, { CPMV1, CPMV2}, { CPMV1, CPMV3}

3 个 CPMV 的组合构造了 6 参数仿射合并候选,2 个 CPMV 的组合构造了 4 参数仿射合并候选。 为了避免运动缩放过程,如果控制点的参考索引不同,则丢弃控制点MV的相关组合。

在检查继承的仿射合并候选者和构造的仿射合并候选者之后,如果列表仍然未满,则将零个MV插入到列表的末尾。

Affine AMVP prediction

仿射 AMVP 模式可应用于宽度和高度都大于或等于 16 的 CU。在比特流中用信号发送 CU 级别中的仿射标志以指示是否使用仿射 AMVP 模式,然后用信号发送另一个标志以指示是否使用 4 参数仿射或 6 参数仿射。 在此模式下,当前 CU 的 CPMV 与其预测器 CPMVP 的差异在比特流中用信号表示。 仿射AVMP候选列表大小为2,它是按顺序使用以下四种类型的CPVM候选生成的:

  • 从相邻 CU 的 CPMV 推断的继承仿射 AMVP 候选者
  • 使用相邻 CU 的平移 MV 导出的仿射 AMVP 候选 CPMVP
  • 来自邻近 CU 的平移 MV
  • 零 MV

继承的仿射 AMVP 候选的检查顺序与继承的仿射merge候选的检查顺序相同。 唯一的区别在于,对于 AVMP 候选者,仅考虑与当前块中具有相同参考图片的仿射 CU。 将继承的仿射运动预测器插入候选列表时,不应用修剪过程。

构造的 AMVP 候选是从图 31 中所示的指定空间邻居导出的。使用与仿射merge候选构造中相同的检查顺序。 另外,还检查相邻块的参考图片索引。 使用检查顺序中经过帧间编码且具有与当前 CU 中相同的参考图片的第一个块。 只有一个当当前CU采用4参数仿射模式编码,并且mv0和mv1都可用时,将它们作为一个候选添加到仿射AMVP列表中。 当当前CU采用6参数仿射模式编码并且所有三个CPMV都可用时,它们被添加为仿射AMVP列表中的一个候选者。 否则,构建的 AMVP 候选者将被设置为不可用。

如果插入有效的继承仿射 AMVP 候选和构造 AMVP 候选后,仿射 AMVP 列表候选仍然小于 2,则将依次添加 mv0 、 mv1 和 mv2 作为平移 MV 来预测当前 CU 的所有控制点 MV, 有空的时候。 最后,如果仿射AMVP列表仍未满,则使用零MV来填充该列表。

Affine motion information storage

在 VVC 中,仿射 CU 的 CPMV 存储在单独的缓冲区中。 存储的CPMV仅用于为最近编码的CU以仿射Merge模式和仿射AMVP模式生成继承的CPMVP。 从 CPMV 导出的子块 MV 用于运动补偿、平移 MV 的Merge/AMVP 列表的 MV 导出以及去块。

为了避免附加 CPMV 的图像行缓冲区,来自上面 CTU 的 CU 的仿射运动数据继承与来自正常相邻 CU 的继承的处理方式不同。 如果用于仿射运动数据继承的候选CU位于上述CTU行中,则使用行缓冲器中的左下和右下子块MV而不是CPMV来进行仿射MVP推导。 这样,CPMV仅存储在本地缓冲区中。 如果候选CU是6参数仿射编码,则仿射模型退化为4参数模型。 如图 32 所示,沿着顶部 CTU 边界,CU 的左下和右下子块运动向量用于底部 CTU 中 CU 的仿射继承。

深入理解 Affine-Motion Compensation

1 The six-parameter affine motion model

H. Huang, J. Woods, Y. Zhao, and H. Bai, “Control-point representation and differential coding affine-motion compensation,” IEEE Transactions on Circuits and Systems for Video Technology, vol. 23, no.10, pp.1651–1660, Oct. 2013.

2D 仿射变换被描述为(1-1):

\(\left\{\begin{array}{l}x^{\prime}=a x+b y+e \\ y^{\prime}=c x+d y+f\end{array}\right.\)

其中\((x, y)\)和\(\left(x^{\prime}, y^{\prime}\right)\)分别是当前帧和参考帧中的一对对应位置,\(a 、b、c、d、e\) 和 \(f\) 是仿射参数。 令 \((v_{x}, v_{y})=\) \((x-x^{\prime}, y-y^{\prime})\)为当前帧中位置 \((x, y)\) 的运动,则可以得出(1-2)

\(\left\{\begin{array}{l}
v_{x}=(1-a) x-b y-e \\
v_{y}=(1-c) x-d y-f
\end{array}\right.\)

这称为仿射运动模型(affine motion model)。 与传统的块匹配算法相比,允许参考帧中的匹配块扭曲或变形(warped or deformed),以获得更好的运动补偿预测。 代价是传输更多的运动参数,每块 6 个而不是 2 个。 在视频编码中,运动参数作为辅助信息传输,在较低的总比特率下可能很重要。 因此已经发现,该运动信息的压缩对于提高整体编码性能是至关重要的。 由此产生的运动参数数量的增加可能会抵消仿射运动补偿的优点。因此,运动参数的有效编码对于仿射运动模型变得更加关键。

给定一个大小为S × S的正方形区域\(\chi\),设置坐标系如图2所示。顶部和左侧3个角,即\((0,0),(S,0)\)和\( (0,S)\)可以作为控制点,记为\((x_i,y_i),i=0,1,2\)。设它们的平移运动向量为\(\vec{v_i}=(v_{x_i}, v_{y_i}),i=0,1,2\)。矩阵\(\mathbf{V} =(\vec{v}_0,\vec{v}_1,\vec{v}_2)\)称为 affne运动矩阵。 使 \(\chi\) 变形的位移控制点为 \((x_i^{\prime},y_i^{\prime})=(x_i-v_{x_i},y_i-v_{y_i})\)。用\((v_{x_i},v_{y_i})\) 和 \( (x_i,y_i)\)代入(1-2)中的 \((v_x,v_y)\)和\((x,y)\),我们将有六个方程,其中有六个未知数 \(a,b,c,d,e\) 和 \(f\)。 求解这些方程,我们有(1-3):

\(\left\{\begin{array}{l}a=1-\frac{v_{x_1}-v_{x_0}}S,b=\frac{v_{x_0}-v_{x_2}}S,e=-v_{x_0}\\c=1-\frac{v_{y_1}-v_{y_0}}S,d=\frac{v_{y_0}-v_{y_2}}S,f=-v_{y_0}\end{array}\right.\)

通过 (1-2) 和 (1-3),我们得到(1-4):

\(\left\{\begin{array}{l}v_x=\frac{v_{x_1}-v_{x_0}}Sx+\frac{v_{x_2}-v_{x_0}}Sy+v_{x_0}\\v_y=\frac{v_{y_1}-v_{y_0}}Sx+\frac{v_{y_2}-v_{y_0}}Sy+v_{y_0}\end{array}\right.\)

在解码器端接收仿射运动矩阵V,\(\chi\)上的运动场可由(1-4) 导出。

2 The four-parameter affine motion model

L. Li, H. Li, D. Liu, Z. Li, H. Yang, S. Lin, H. Chen, and F. Wu, “An Efficient Four-Parameter Affine Motion Model for Video Coding,” IEEE Transactions on Circuits and Systems for Video Technology, Apr. 2017.

仿射运动模型可以表征平移、旋转、缩放、错切(translation, rotation, zooming, shear mapping)等。 然而,日常视频中最常见的运动仅包括六种典型摄像机运动(camera track, boom, pan, tilt, zoom, and roll)和典型物体运动(平移、旋转和缩放),其中六模型参数超出必要。 需要说明的是,这里的旋转是指物体在与相机平行的二维平面内旋转。 此外,只有当物体与相机之间的相对距离保持不变或者物体具有平坦表面时,才可以使用仿射运动模型来表征物体缩放。 由于本文关注的是局部仿射模型,因此只要局部块足够小,我们就可以假设局部块具有平坦的表面。 下面,将以典型的物体运动为例来解释所提出的四参数仿射运动模型的物理解释。

事实上,如图1(a)所示,如果只需要对旋转和平移的组合进行表征,则同一像素变换前后的坐标关系可以描述为(2-2):

\(\left\{\begin{array}{l}x’=\cos\theta\cdot x+\sin\theta\cdot y+c\\y’=-\sin\theta\cdot x+\cos\theta\cdot y+f\end{array}\right.\)

其中 θ 是旋转角度。 此外,如图1(b)所示,如果仅表征缩放和平移的组合,则其关系可以描述为(2-3):

\(\left\{\begin{array}{c}x’=\rho\cdot x+c\\y’=\rho\cdot y+f\end{array}\right.\)

其中 ρ 分别是 x 和 y 方向上的缩放因子。

旋转/缩放和平移的组合都需要三个参数来表征。 如果我们将旋转、缩放和平移结合在一起,则需要四个参数,其关系可以描述为(2-4):

\(\left\{\begin{array}{l}x^{\prime}=\rho\cos\theta\cdot x+\rho\sin\theta\cdot y+c\\y^{\prime}=-\rho\sin\theta\cdot x+\rho\cos\theta\cdot y+f\end{array}\right.\)

如果我们将 ρcos θ 和 ρsin θ 替换为 (1 + a) 和 b,则 (2-4) 可以重写为(2-5):

\(\left\{\begin{array}{l}MV_{(x,y)}^h=x^{\prime}-x=ax+by+c\\MV_{(x,y)}^v=y^{\prime}-y=-bx+ay+f\end{array}\right.\)

其中\(MV_{(x,y)}^h\)和\(MV_{(x,y)}^v\) 是位置 (x, y) 的 MV 的水平和垂直分量。 等式(2-5)是采用的四参数仿射运动模型,能够精确表征旋转、缩放和平移的组合。

六参数仿射运动模型与四参数仿射运动模型进行比较,可以明显看出,在四参数仿射运动模型下,每个块计算的参数较少,因此解码复杂度可以稍微降低。 此外,由于稍后将介绍的快速仿射ME算法可以在四参数仿射运动模型下更快地收敛,因此编码复杂度也将降低。 由于标头信息的比特节省,四参数仿射运动模型可以为大多数自然序列带来更好的 R-D 性能。

根据式(2-5),有四个未知模型参数。 除了这四个参数,我们还可以使用两个MV来等效地表示模型,因为使用MV更符合现有的视频编码框架。 可以在当前块的任何位置选择这两个 MV 来表示运动模型。 我们选择当前块的左上和右上位置的MV,因为这两个位置与先前重建的块相邻,并且可以更准确地预测对应的MV。 在如图 2 所示的典型 S × S 块中,如果我们将左上角像素 (0, 0) 的 MV 表示为 MV0,将右上像素 (S − 1, 0) 的 MV 表示为 MV1,则模型参数a、b、c、f可根据式(5)求解如下(2-6):

\(\left\{\begin{array}{ll}a=\frac{MV_1^h-MV_0^h}{S-1}&c=MV_0^h\\b=-\frac{MV_1^v-MV_0^v}{S-1}&f=MV_0^v\end{array}\right.\)

3 Affine Motion Eestimate

下面给出VTM中4参数和6参数Affine ME的迭代步骤:

步骤1:寻找ME过程的初始CPMV。 测试若干个候选的CPMV组合,对于每个候选,通过将CPMV设置为候选MV来执行当前块的AMC,然后计算AMC预测块的SATD成本。 初始CPMV候选设置为等于提供最小 SATD 成本的候选者。

步骤2:从仿射参数Pi推导Pi+1。那么当使用Pi+1进行AMC时当前块的总误差可以计算为(3-3)

\(D=\sum_{(x,y)\in B}\lVert org(x,y)-ref(W(x,y,P_{i+1}))\rVert^2\)

其中对于6参数模型(3-4):

\(\left.W=\left[\begin{array}{c}ax+by+e\\cx+dy+f\end{array}\right.\right]=\left[\begin{array}{cccccc}x&0&y&0&1&0\\0&x&0&y&0&1\end{array}\right]\left[\begin{array}{ccccc}a&c&b&d&e&f\end{array}\right]^T\)

对于6参数模型(3-5):

\(\left.W=\left[\begin{matrix}ax+by+e\\-bx+ay+f\end{matrix}\right.\right]=\left[\begin{matrix}x&y&1&0\\-y&x&0&1\end{matrix}\right]\left[\begin{array}{cccc}a&b&e&f\end{array}\right]^T\)

将(3-3)改写为(3-6):

\(D=\sum_{(x,y)\in B}\lVert org(x,y)-ref(W(x,y,P_i+\Delta P))\rVert^2\)

将\(ref(W(x,y,P_i+\Delta P))\)泰勒展开得(3-7):

\(ref(W(x,y,P_i+\Delta P))\approx ref(W(x,y,P_i))+\frac{\partial ref}{\partial W}\frac{\partial W}{\partial P}\Delta P\)

其中\(\frac{\partial ref}{\partial W}\)为参考图像对位置的导数,即梯度\(\begin{matrix}G=[G_x&G_y]\end{matrix}\); \(\frac{\partial W}{\partial P}\)可以由(3-4)(3-5)推导,因此对于6参数模型(3-8):

\(Q(x,y)=\frac{\partial ref}{\partial W}\frac{\partial W}{\partial P}=\begin{matrix}[xG_x&xG_y&yG_x&yG_y&G_x&G_y]\end{matrix}\)

对于4参数模型(3-9):

\(Q(x,y)=\frac{\partial ref}{\partial W}\frac{\partial W}{\partial P}=\begin{matrix}[xG_x-yG_y&yG_x+xG_y&G_x&G_y]\end{matrix}\)

带入(3-8)或(3-9)到(3-6)中得(3-9):

\(D=\sum_{(x,y)\in B}\lVert E(x,y)-Q(x,y)\Delta P\rVert^2\)

其中\(E(x,y)=org(x,y)-ref(W(x,y,P_i)\)。

求D的最小值即可得到\(\Delta P\),我们对D求导并令导数为0得(3-10):

\(Q^TQ\Delta P=Q^TE\)

其中Q是尺寸为m×6或者m×4的矩阵,E是长度m×1的向量,m是CU内的像素总数。使用高斯消元法可得到\(\Delta P\),则可推导出\(P_{i+1}=P_i+\Delta P\)。

步骤3:使用\(P_{i+1}\)执行当前块的AMC,然后计算AMC预测块的SATD成本C(i+1)。 如果 C(i+1) < C*,则 C*设置为等于 C(i+1)

步骤4:若\(P_{i+1}\)与\(P_i\)不相同且i小于最大迭代次数,则设i=i+1,转步骤2; 否则,终止ME过程。

H.264/AVC 中的 MB-Tree

翻译自 J. Garrett-Glaser, “A novel macroblock-tree algorithm for high-performance optimization of dependent video coding in h. 264/avc,” Tech. Rep., 2009.

x264 有一个复杂的前(lookahead)模块,旨在估计尚未被主编码器模块分析的帧的编码成本。 它使用这些估计来做出各种决策,例如自适应 B 帧、显式加权预测以及带VBV的码率控制的比特分配。 出于速度原因,它在帧的半分辨率版本上运行并仅计算 SATD 残差,不进行量化或重建。

Lookahead 的核心是 x264_slicetype_frame_cost 函数,该函数会被重复调用以计算给定 p0、p1 和 b 值的帧的成本。 p0 是要分析的帧的列表 0(过去)参考帧。 p1 是要分析的帧的列表 1 参考帧。 b 是要分析的帧。 如果p1等于b,则该帧被推断为P帧。 如果p0等于b,则该帧被推断为I帧。 由于 x264_slicetype_frame_cost 可能会作为使用它的算法的一部分在相同的参数上重复调用,因此每个调用的结果都会被缓存以供将来使用。

x264_slicetype_frame_cost 通过为帧中的每个宏块调用 x264_slicetype_mb_cost 进行操作。 由于帧是半分辨率,因此每个“宏块”是 8×8 像素,而不是 16×16。 x264_slicetype_mb_cost 对每个参考帧执行运动搜索。 该运动搜索通常是具有子像素细化的六边形运动搜索。

对于 B 帧,它还检查一些可能的双向模式:类似于 H.264/AVC 的“temporal direct”的模式、零运动矢量以及使用 list0 和 list1 运动搜索产生的运动矢量的模式。 x264_slicetype_mb_cost 还计算近似的intra cost。 所有这些cost都被存储以供将来使用。 这对于宏块树来说很重要,它需要这些信息来进行计算。

The Macroblock-tree algorithm

为了执行上述的信息传播估计,宏块树跟踪每个前瞻帧中每个宏块的传播成本:以 SATD 残差为单位的数值估计,表示未来残差在多大程度上取决于该宏块。 对于前瞻中的所有帧,该值都初始化为零。

宏块树在前瞻中的最后一个minigop 中开始操作,首先对 B 帧进行操作,然后对 P 帧进行操作。 然后,它向后移动到前瞻中的第一帧(要编码的下一帧)。 通过这种方式,宏块树向后工作:它及时向后“传播”依赖关系。 因此,当我们“传播”依赖关系时,我们是在与实际依赖关系本身相反的方向上进行操作:将信息从一个帧移动到其参考帧。

对于每一帧,我们对所有宏块运行传播步骤。 宏块树的传播步骤如下:

  1. 对于当前宏块,我们加载以下变量:
    • intra_cost:该宏块的帧内模式的估计 SATD 成本。
    • inter_cost:该宏块的帧间模式的估计 SATD 成本。 如果该值大于intra_cost,则应将其设置为intra_cost
    • propagate_in:当前宏块的propagate_cost。 对于运行传播的第一个帧,故意将propagate_cost设置为零,因为尚未收集该帧的信息。
  2. 我们计算从该宏块传播到其参考帧中的宏块的信息分数,称为propagate_fraction。 这可以通过公式 1 – intra_cost / inter_cost 来近似。 例如,如果宏块的帧间成本是该宏块的帧内成本的 80%,则我们说该宏块中 20% 的信息源自其参考帧。 这显然是一个非常粗略的近似,但是快速且简单。
  3. 依赖于该宏块的信息总量(total amount)等于(intra_cost+propagate_in)。 这是所有未来依赖性(直到前瞻边缘)和当前宏块的内部成本的总和。 我们将其乘以propagate_fraction,得到应该传播到该宏块的参考帧的近似信息量,propagate_amount
  4. 我们在其参考帧中的宏块之间分割propagate_amount,用于预测当前块。 基于每个宏块用于预测当前宏块的像素数量对分割进行加权。 这可以基于当前宏块的运动矢量来计算。 如果块有两个参考帧(如双向预测的情况),则将传入的值平均分配到两者之间,或者如果启用了加权 B 帧预测,则根据双向预测权重。 然后将propagate_amount 的适当分割部分添加到用于预测的每个宏块的propagate_cost 中。 请注意,如果为了简单起见我们忽略插值滤波器的影响,则每帧中最多有 4 个宏块可用于当前宏块的预测。

该过程的结果是当前帧的参考的propagate_cost值已基于当前帧的内容而增加。 通过对前瞻中的所有帧以相反的顺序重复此操作,我们可以估算出前瞻中每个宏块对前瞻中其余帧的质量的贡献。

最后,将完成步骤应用于我们希望获取最终量化器增量的任何帧中的宏块。 这通常是接下来要编码的几帧。 宏块树的完成步骤使用与传播相同的输入进行操作,但输出 H.264 量化器增量:

\(Macroblock QP Delta = -strength * log2((intra\_cost + propagate\_cost) / intra\_cost)\)

其中strength是从实验中得出的任意因子。 测试表明,对于大多数视频来说,2 是接近最佳的值。 由于 H.264 量化器比例每 6 Qps 精度加倍,这意味着最佳量化器精度似乎与完成步骤中使用的结果值 (1 +propagate_cost/intra_cost) 的立方根大致成比例。

应该注意的是,该算法会导致 QP 增量完全为零的未引用帧,因为没有任何内容传播到它们:

\(Macroblock QP Delta = -strength * log2((intra\_cost + 0) / intra\_cost) = -strength * log2(1) = 0\)

这是有意为之的:根据宏块树的逻辑,所有未引用的宏块都具有相等(因此最小)的值。

Analysis

宏块树在比特分布方面具有许多一致的效果。 其中之一是对 B 帧量化器的影响。 正如简介中提到的,B 帧量化器通常是通过 P 帧量化器的偏移导出的。 宏块树实际上相反:未引用的 B 帧量化器始终相同,而相邻的 P 帧量化器则不同。

其结果实际上是自适应 B 帧量化器偏移。 在高速运动区域,B 帧的量化值往往不会比附近 P 帧的量化值高很多。 在低运动区域,量化器差异要高得多:在某些情况下+4-6 QP 或更多。 值得注意的是,在 x264 中,当宏块树打开时,常规 B 帧量化器偏移将被禁用,因为它们起到相同的作用。

人们可能会类似地假设宏块树可以替代关键帧量化器偏移。 然而,测试表明情况并非如此:宏块树通常会降低整个场景的量化器,而不仅仅是场景中的第一帧。 关键帧量化器偏移对于 PSNR 仍然有用,因此保留在 x264 中。 关键帧量化器偏移的算法推导可能需要某种形式的前瞻量化。

虽然宏块树太复杂而无法实现封闭形式的通用解决方案,但可以从特殊情况输入的解决方案中得出一些见解:全零运动向量、无 B 帧、恒定的intra_cost和恒定的propagate_fraction。 尽管这样的输入完全不切实际,但它提供了对宏块树作为propagate_cost函数进行缩放的方式的深入了解。

令 TREE[N] 为传播 N 帧后的propagate_cost。 令 Y 为常量intra_cost,Z 为常量propagate_fraction,范围为0-1。

\(TREE[0] = 0\)
\(TREE[N] = (TREE[N-1] + Y)*Z\)

对于较大的 N,这可以简单地证明收敛于 Y * (Z/(1-Z))。因此,在这种情况下宏块树的量化器增量按如下比例缩放:

Perceptual considerations

由于宏块树在视频的帧内和帧间之间重新分配比特,因此它特别有可能产生积极和消极的感知后果。 我们在视觉比较过程中注意到这些影响有两个主要类别:运动自适应量化和“pre-echo”。

视频的高运动部分的视觉质量下降是 qcompress 和其他类似算法的感知解释。 因此,与qcompress不同,宏块树不会仅仅因为帧的其他部分在时域上复杂而降低帧的静态部分的质量。 这对于静态背景和重叠图形的情况尤其重要。 从这个意义上说,宏块树是一种注重编码效率的运动自适应量化算法。

“pre-echo”是宏块树设计的自然结果。 宏块的质量取决于它在未来被引用的程度; 如果该宏块很快就会被遮挡(如移动物体的情况)或完全被替换(如场景变化的情况),宏块树将降低其质量。

通常,这种“pre-echo”仅在遮挡/场景变化之前的几帧中可见,并且几乎完全被帧间预测隐藏。 此外,场景变化会导致向后时间掩蔽效应,有助于隐藏此类伪影。 这种向后的时间掩蔽效应持续几十毫秒,足以覆盖一两帧,并且被认为是由人类视觉系统的处理延迟引起的。 这与我们自己对宏块树的非正式视觉测试一致,这也表明此类伪影在视觉上是不可见的。 因此,保存在此类宏块中的比特可以自由地用于视频中的其他地方。

在一种特殊情况下,“pre-echo”是可见的:即强制关键帧。 朴素的宏块树算法将关键帧视为全帧内帧,即使关键帧不是场景变化。 这会导致关键帧之前的几帧质量降低,在关键帧实际上不是场景变化的情况下,这在视觉上可以明显感觉到质量的微小脉冲。

在 x264 中,为了宏块树的目的,通过将强制关键帧视为 P 帧来避免此问题。 请注意,此优化仅在感知优化开启时在 x264 中执行。 忽略这种感知优化可能会对 PSNR 和 SSIM 产生较小的积极影响。

HEVC码流中的NALU

翻译自 High Efficiency Video Coding (HEVC) Algorithms and Architectures

HEVC的“high-level syntax”部分包括适用于位流的一个或多个完整Slice或图像的高级信息的信令。例如,高级语法表示视频的空间分辨率,使用了哪些编码工具,并描述了比特流的随机访问功能。除了语法元素的信号外,与语法元素相关的高级工具解码过程也被认为包含在标准的高级语法部分中。示例高级语法解码过程包括参考图片管理和解码图片的输出。

图2.1显示了一个HEVC编码器和解码器。输入图片被送入编码器,编码器将图片编码成比特流。HEVC位流由称为网络抽象层(NAL)单元的数据单元序列组成,每个单元包含一个整数字节。NALU的前两个字节构成NALU头,而NALU的其余部分包含有效载荷数据。一些NALU携带参数集,其中包含应用于一张或多张完整图片的控制信息,而其他NALU携带单个图片中的编码样本。

NALU由解码器解码以产生解码器输出的解码图像。编码器和解码器都将图片存储在已解码的图片缓冲区(DPB)中。这个缓冲区主要用于存储图片,以便以前编码过的图片可以用来生成预测信号,在编码其他图片时使用。这些存储的图片称为参考图片(reference pictures)。

The NAL Unit Header and the HEVC Bitstream

在HEVC中有两类NALU——视频编码层(VCL) NALU和非VCL NALU。每个VCL NALU携带编码图像数据的一个Slice段,而非VCL NALU包含通常与多个编码图像相关的控制信息。一个编码图像,连同与编码图像相关联的非VCL NALU,称为HEVC访问单元(access unit, AU)。没有要求AU必须包含任何非VCL NALU,并且在一些应用程序(如视频会议)中,大多数AU不包含非VCL NALU。但是,由于每个AU都包含一个编码图像,因此它必须由一个或多个VCL NALU组成——编码图像被分割成的每个Slice对应一个单元。

The NAL Unit Header

图2.2显示了NALU Header的结构,它有两个字节长。所有HEVC NALU Header,对于VCL和非VCL NALU,都从这个两字节的NALU头开始,旨在使解析NALU的主要属性变得容易;它是什么类型,以及它属于什么层和时态子层。

NALU报头的第一个位总是设置为’0’,以防止在遗留的MPEG-2系统环境中产生可能被解释为MPEG-2启动代码的位模式。接下来的六位包含NALU的类型,标识NALU中携带的数据类型。6位表示有64种可能的NAL单位类型值。这些值在VCL和非VCL NALU之间平均分配,因此它们各有32种类型。

下面的6位包含一个层标识符,表示NALU属于哪个层,用于将来的可扩展和分层扩展。虽然第一版HEVC于2013年6月发布,支持时间可扩展性,但它不包括任何其他可扩展或分层编码,因此第一版中所有NALU的层标识符(层ID)始终设置为“00000o”。在以后的HEVC版本中,层ID有望用于识别NAL属于哪个空间可扩展层、质量可扩展层或可扩展多视图层。

NALU Header的最后三位包含NALU的时间标识符,表示七个可能的值,其中一个值是禁止的。HEVC中的每个AU都属于一个时间子层,由时间ID表示。由于每个AU属于一个时间子层,所有属于同一AU的VCL NALU在其NALU头中必须具有相同的时间ID。

图2.3显示了编码视频序列中图片的两个不同示例参考结构,两个时态子层都对应于0和1的时态ID值。Slice类型在图中使用I、P和B表示,箭头显示了图片如何参考其他图片。例如,图2.3a中的图B2是使用bi-prediction的图片,参考图片Io和Pl。

VCL NAL Unit Types

表2.1显示了所有32个VCL NALU类型及其NALU类型(图2.2中的NALType)在NAL单元头中的值。所有VCL相同AU的NALU必须具有相同的NALU类型值,该值定义了AU的类型及其编码图。例如,当一个AU的所有VCL NALU的NALU类型都等于21时,则该AU称为CRA AU,编码图像称为CRA图像。在HEVC中有三种基本类型的图像: intra random access point (IRAP) pictures, leading pictures, and trailing pictures.

IRAP Picture

IRAP图像类型由NAL单元类型16-23组成。这包括IDR, CRA和BLA图片类型以及类型22和23,这些类型目前保留供将来使用。所有IRAP图片必须属于时间子层0,并且不使用任何其他图片的内容作为参考数据进行编码(即仅使用intra编码)。注意,但未标记为IRAP的帧内编码图片允许在比特流中出现。IRAP图片类型用于在比特流中提供可能开始解码的点。因此,IRAP图片本身不允许依赖于比特流中的任何其他图片。

比特流的第一张图片必须是IRAP图片,但在整个比特流中可能有许多其他IRAP图片。IRAP图像还提供了切换比特流的可能性,例如当开始观看TV或从一个TV频道切换到另一个频道时。IRAP图片还可以用于在视频内容中实现时间定位——例如,通过使用视频播放器的控制条来移动视频程序中的当前播放位置。最后,IRAP图片还可以、用于在压缩域中从一个视频流无缝切换到另一个视频流。这被称为比特流切换或拼接,它可以发生在两个直播视频流之间,一个直播流和一个存储的视频文件之间,或者两个存储的视频文件之间。从IRAP图像解码并按输出顺序输出任何后续图像始终是可能的,即使按解码顺序在IRAP图像之前的所有图像都从比特流中丢弃。

当为存储和稍后的播放或广播应用程序编码内容时,IRAP图片通常均匀分布,以在整个比特流中提供相似频率的随机接入点。在实时通信应用中,随机访问功能不是那么重要,或者发送IRAP图片所需的相对大量的比特是一个显著的负担,会增加通信延迟,IRAP图片可能很少发送,或者只有当一些反馈信号表明视频数据已经损坏,需要刷新场景时才发送。

Leading and Trailing Pictures

leading picture 是在解码顺序上跟随特定IRAP图并在输出顺序上先于它的图。trailing picture 是在解码顺序和输出顺序上都遵循特定IRAP图片的图片。图2.4展示了leading picture和trailing picture 的例子。在解码顺序上,leading picture和trailing picture 被认为与最接近的IRAP图像相关联,如图2.4中的I1图像。trailing picture必须使用trailing picture NAL单元类型0-5中的一种。特定IRAP图片的trailing picture不允许依赖于任何leading picture或之前IRAP图片的 trailing picture;相反,它们只能依赖于相关的IRAP图片和同一IRAP图片的其他 trailing picture 图片。此外,IRAP图像的所有leading picture必须在解码顺序中位于与同一IRAP图像相关联的所有trailing picture之前。这意味着关联图片的解码顺序总是:(1)IRAP图片,(2)关联的leading picture(如果有),然后(3)关联的trailing picture(如果有)。

HEVC的trailing picture有三种类型:时间子层访问(TSA)图像、逐级时间子层访问(STSA)图像和普通trailing picture (TRAIL)。

Temporal Sub-layer Access (TSA) Pictures

TSA图像是显示时间子层切换点的trailing picture。TSA图片类型只能用于没有在解码顺序中TSA图片之前的,其时间ID大于或等于TSA图片本身的时间ID,用于预测TSA的图片或用于预测与TSA图片在相同或更高的时间子层中的任何后续(解码顺序)图片。例如,图2.5中的图片P6可以使用TSA图片类型,因为只使用时间子层0中的前一张图片来按照解码顺序预测TSA图片本身和后续图片。

当解码器解码位流中时间子层的子集并且遇到时间子层的TSA图像类型正好在它正在解码的最大时间子层之上时,解码器可以切换到并解码任意数量的附加时间子层。对于图2.5中的示例,仅对TSA 图像的时间子层0进行解码的解码器(1)只解码时域子层0,(2)决定开始解码时域子层1和子层0,或(3)开始解码所有三个子层。对于只转发最低的时间子层的网络节点(例如由于先前的网络拥塞情况),可能会有类似的动作。网络节点可以检查具有时间ID等于1的传入图片的NAL单元类型。这并不需要大量的计算资源,因为NAL单元类型和时间ID可以在NAL单元头中找到,并且很容易解析。当遇到时序子层1的TSA图像时,网络节点可以切换到按照解码顺序转发继TSA图像之后的任何时序子层图像,而不会存在解码器由于没有所有必要的参考图像而无法正确解码的风险。

Step-wise Temporal Sub-layer Access (STSA) Pictures

STSA图片类型与TSA图片类型类似,但它只保证STSA图片本身以及在解码顺序上与它后面的STSA图片具有相同时间ID的图片不参考在解码顺序上与它前面的STSA图片具有相同时间ID的图片。因此,STSA图像可以用来标记比特流中可以切换到具有与STSA图像相同时间ID的子层的位置,而TSA图像可以标记比特流中可以切换到任何更高子层的位置。图2.5中STSA图的一个例子是图P2。这张图片不可能是TSA图片,因为P3参考了P1。但图片P2可以是STSA图片,因为P6没有参考任何子层1的图片,解码顺序在P2之后的任何子层1的图片也没有参考解码顺序在P2之前的任何子层1的图片。TSA和STSA图片的时间ID都必须大于0。

还要注意的是,由于在HEVC中禁止从较高的时间子层预测到较低的时间子层,因此在任何图片上总是有可能向下切换到较低的时间子层,无论图片类型或时间子层。

Ordinary Trailing (TRAIL) Pictures

普通trailing图片用枚举类型TRAIL表示。trailing图片可以属于任何时间子层。它们可以参考关联的IRAP图片和与同一IRAP图片关联的其他trailing图片,但它们不能参考leading图片(或与同一IRAP图片关联的任何其他非trailing图片的图片)。在按解码顺序输出下一张IRAP图片后,它们也不能输出。请注意,所有TSA和STSA图片都可以标记为TRAIL图片,所有TSA图片都可以标记为STSA图片。然而,为了表明比特流中存在的所有可能的时间子层切换点,建议跟踪图片应使用最具限制性的类型。

Instantaneous Decoding Refresh (IDR) Pictures

IDR图像是一个完全刷新解码过程并开始一个新的CVS的帧内图像。这意味着,无论是IDR图像还是按解码顺序在IDR图像之后的任何图像,都不能依赖于按解码顺序在IDR图像之前的任何图像。IDR图像有两种子类型,IDR_W_RADL类型可能有关联的随机访问可解码leading(RADL)图像,IDR_N_LP类型没有任何leading图像。请注意,即使IDR图片没有任何leading图片,编码器也允许(但不推荐)使用IDR_W_RADL类型。但是,禁止将IDR_N_LP类型用于具有先导图像的IDR。使用两种不同的IDR图片类型的原因是为了使系统层能够在随机访问时知道IDR图片是否是要输出的第一个图片。IDR图像的POC值总是等于零。因此,与IDR图像相关联的leading图像(如果有的话)都具有负的POC值。

Clean Random Access (CRA) Pictures

CRA图片是一个帧内图片,与IDR图片不同,它不会刷新解码器,也不会开始一个新的CVS。这使得CRA图像的leading图像以解码顺序依赖于在CRA图像之前的图像。允许这样的leading图像通常使包含CRA图像的序列比包含IDR图像的序列更具压缩效率(约6%)。

CRA图像的随机访问是通过对CRA图像进行解码,其leading图像在解码顺序上不依赖于CRA图像之前的任何图像,以及在解码和输出顺序上都遵循CRA的所有图像。注意,CRA图片不一定有关联的leading图片。

Random Access Decodable Leading (RADL) and Random Access Skipped Leading (RASL) Pictures

leading图像必须使用RADL或RASL NAL单元类型发出信号。RADL和RASL图片可以属于任何时间子层,但不允许被任何trailing图片引用。RADL图片是保证在对关联的IRAP图片进行随机访问时可解码的leading图片。因此,RADL图片只允许引用关联的IRAP图片和同一IRAP图片的其他RADL图片。

当从关联的IRAP图像执行随机访问时,RASL图像是可能无法解码的先导图像。图2.6显示了两张RASL图片,由于图片P2在解码顺序上位于CRA图片之前,因此这两张图片都是不可解码的。由于其在解码顺序上的位置,在CRA图片的位置随机访问不会解码P2图片,因此无法解码这些RASL图片并将其丢弃。尽管对于可解码的leading图,如图2.6中的RADL图,不禁止使用RASL类型,但为了更加网络友好,建议在可能的情况下使用RADL类型。只有其他RASL图片被允许依赖于一个RASL图片。这意味着依赖于RASL图像的每个图像也必须是RASL图像。RADL和RASL图像可以按解码顺序混合,但不能按输出顺序混合。RASL图片在输出顺序上必须在RADL图片之前。

IDR_W_RADL图片的所有前导图片必须是可解码的,并且使用RADL类型。RASL图片不允许与任何IDR图片相关联。CRA图可能同时具有关联的RADL和RASL图,如图2.6所示。允许RASL图片引用在相关IRAP图片的IRAP图片,也可以参考在解码顺序中遵循该IRAP图片的其他图片,但不能引用解码顺序中更早的图片。图2.6中的RASL图片无法引用图片P0。

在HEVC中有三个约束,目的是在执行随机访问时消除图像输出不均匀。其中两个约束依赖于为每张图片设置的变量PicOutputFlag,它指示图片是否输出。当一个被称为pic_output的标志出现在slice header中并且等于0时,或者当当前图片是RASL图片并且关联的IRAP图片是CVS中的第一张图片时,这个变量被设置为0。否则,PicOutputFlag被设置为1。

第一个约束是,任何具有PicOutputFlag等于1的图片,在解码顺序上位于IRAP图片之前,必须在输出顺序上位于IRAP图片之前。图2.7a中的结构不受此约束,因为图像P1在解码顺序上位于CRA之前,但在输出顺序上位于它之后。如果允许这样做,并且对CRA图片进行随机访问,则会丢失图片P1,导致输出不均匀。

第二个约束是,任何具有PicOutputFlag等于1的图像,在解码顺序上位于IRAP图像之前,必须在输出顺序上位于与IRAP图像相关联的任何RADL图像之前。一个引用结构,即如图2.7b所示,因为P1在I2之前,但在输出顺序上在P3之后。如果允许这种引用结构,并且对CRA图片进行随机访问,则缺少P1图片将导致输出不均匀。

第三个约束是所有RASL图片必须在输出顺序上先于任何RADL图片。由于RASL图片在随机访问时被丢弃,而RADL图片没有被丢弃,因此在RADL图片之后显示的任何RASL图片在随机访问时可能会导致输出不均匀。

其他类型用的比较少,这里就不写了。

Primary Transform in VVC

VVC中的变换设计主要包括三个方面:primary transform, secondary transform and transform partitioning.。 N点变换是指可以应用于N点输入向量的一维变换,这是使用大小为N×N的变换矩阵来完成的。

VVC 继承了 HEVC 变换编码的几个设计方面(参考HEVC Core Transform Design),包括:1) 使用定点运算,中间数据表示和算术保持为 16 位; 2) 变换过程可以使用直接矩阵乘法或 一种快速方法,例如部分蝶形; 3)通过将变换基缩放为 64√N 并通过较小调整舍入到最接近的整数来设计变换内核,其中变换基的范数为 1,N 是变换大小; 4)较小的DCT-2是较大的DCT-2的一部分,因此所有DCT-2内核都嵌入在64×64 DCT-2变换内核中。

尽管在 VVC 中可以应用高达 128 × 128 的编码块大小,但变换编码被设计为与虚拟管道数据单元 (VPDU) 实现兼容。 在硬件解码过程中,VPDU是不重叠的64×64块,连续的VPDU由多个管道并行处理。

A. Transform Kernels

在VVC中,除了传统的DCT-2之外,还采用了替代变换类型,包括DST-7和DCT-8。 DST-7和DCT-8的基本函数分别用下面的等式(1)和(2)表示:

其中 N 是变换大小,i = 0, 1,…, N−1 指输出向量的元素索引,j = 0, 1,…, N − 1 指输入向量的元素索引。 对于残差的不均匀分布,DST-7 和 DCT-8 通常比 DCT-2 更有效,因为它们的基函数更符合此类统计数据。 DCT-2的大小范围从4点到64点,DST-7/DCT-8的范围从4点到32点。 值得注意的是,DST-7 和 DCT-8 的变换基是彼此翻转的版本,具有交替的符号变化。

VVC中定义的变换内核由8位有符号整数组成,HEVC中的所有主要变换内核(包括4点DST-7和从4点到32点的DCT-2)保持不变。 VVC 中定义的附加整数变换内核是通过将浮点变换内核缩放 64√N 得出的,其中 N 是变换大小,并在舍入后进一步调整 ±1。 64点DCT-2的调整是以包含HEVC中定义的所有DCT-2内核、支持部分蝶形并且针对更好的正交性优化内核元素的方式执行的。

对DST-7/DCT-8核进行调整是为了确保与DST-7/DCT-8相关的三个显著特征,如图1所示,包括特征 1)某些基中的重复段{b,f,i,l,o};2)一个基中的唯一系数值,3)某些基具有固定模式的元组中系数之间的数学关系,保持在具有优化正交性的整数核中。特别地,对于特征3),支持以下公式:

为了将每个系数的 worst-case 乘法与HEVC对齐,对于64点DCT-2和32点DST-7/DCT-8,分别只保留前32个和16个低频系数,并将高频系数归零,这也在最后一个系数位置编码和系数组扫描中考虑。此外,基于DST-7/DCT-8内核的三个特征,VVC中包含了一种支持双重实现的快速变换方案。通过这种方式,快速算法和直接矩阵乘法产生相同的结果。同时,对于16点DST-7/DCT-8,快速方法实现了约50%的乘法运算减少。

在VVC中,primary transform 被指定为可分离变换。 支持五种不同的变换类型组合,包括传统的(DCT-2、DCT-2)和四种新的 MTS 模式组合,即(DST-7、DST-7)、(DST-7、DCT-8)、 (DCT-8、DST-7)和(DCT-8、DCT-8)。 由于有限的编码增益以及引入额外编码器搜索和额外变换组合的复杂性增加,不支持具有额外信令开销的DCT-2和DST-7(或DCT-8)之间的显式组合。 在VVC中,DST-7和DCT-8可以应用于多种编码工具中的亮度块,包括多重变换选择(MTS)、帧内子分区(ISP)[34]和子块变换(SBT) ,这将在下面与变换类型选择相关的小节中详细介绍。 对于色度编码,在 VVC 的开发过程中也研究了 DST-7/DCT-8 的潜在优势。 然而,由于色度分量通常呈现平滑的纹理,而 DCT-2 就足够了,因此编码增益与复杂性的权衡不太有利。

B. Multiple Transform Selection:

在 VVC 中,MTS有两种变体,称为显式 MTS 和隐式 MTS。 显式MTS可以应用于帧内编码块和帧间编码块,而隐式MTS只能用于帧内编码块。 在显式MTS中,DST-7/DCT-8的选择由变换类型的显式信令指示。 在隐式MTS中,基于编码器和解码器都已知的编码信息来选择变换类型,并且不需要变换类型信令。 在序列参数集(SPS)中,有三个控制 MTS 操作的标志。 第一个用于启用 MTS 本身。 第二个用于在显式或隐式内部 MTS 之间进行选择,最后一个用于启用显式间 MTS。 因此,对于后两个标志,如果启用了 MTS,则需要选择四种 MTS 模式组合。 在显式MTS中,索引mts_idx在编码单元(CU)级语法的末尾处用信号通知以指示水平变换(trTypeHor)和垂直变换(trTypeVer)的变换类型。 mts_idx的取值范围是0到4,变换类型的映射如表1所示:

MTS索引,表示为mts_idx,仅当亮度块的非零系数存在于DC系数之外并且在左上角的16×16系数区域之外未识别出非零系数时才发出信号,因为DST-7/DCT-8仅对最低的16×16频率系数有影响。换句话说,识别超过最低16×16系数的非零系数意味着不应用DST-7/DCT-8。此外,当启用多个工具时,包括ISP、SBT和低频不可分离变换(LFNST),mts_idx不会发出信号,变换类型推断为DCT-2或预定义的变换类型。在表II中,总结了不同工具的组合,包括MTS、LFNST、MIP、ISP和SBT,其中“Y/N”表示行和列中的相关编码工具可以/不能组合,“N/A”表示相关组合不适用。

隐式 MTS 变换类型是根据编码块的形状导出的。 如果块宽度(高度)小于32,则应用DST-7作为水平(垂直)变换。否则,使用DCT-2。 同样的规则也用于导出 ISP 编码块的变换。 图 2 显示了不同块大小的隐式 MTS 推导示例。 VVC 中的隐式 MTS 设计可以被视为帧内预测残差的 HEVC 变换推导的扩展,通过将 DST-7 的适用块大小从 4 × 4 扩展到 16 × 16(含)以及其间的其他矩形块大小 。 除了基于块大小的限制之外,仅当 LFNST 和基于矩阵的帧内预测(MIP)索引设置为零时才能应用隐式变换。

隐式 MTS 的优点总结如下: (1) 尽管与显式 MTS 相比,隐式 MTS 提供的编码增益较少,但它在无需任何编码器搜索的情况下提供了比 DCT-2 显着的编码增益。 此功能对于无法适应复杂率失真搜索的简单编码器设计很有吸引力。 (2)隐式MTS为ISP编码和非ISP帧内编码块提供统一的变换推导规则。 (3) 由于隐式 MTS 中不允许 DST-7 的维数超过 16,因此避免了对高频系数的内置归零操作(仅适用于 32 点 DST-7)。

HEVC Core Transform Design

翻译自论文 “Core Transform Design in the High Efficiency Video Coding (HEVC) Standard”

A. Use of Transforms in Block-Based Video Coding

在基于块的混合视频编码方法中,变换应用于帧间或帧内预测产生的残差信号,如图 1 所示。在编码器处,帧的残差信号被划分大小为\(N×N\)的方形块, 其中\(N=2^M\)和M是一个整数。 然后将每个残差块 (\(U\)) 输入到二维\(N×N\)正向变换。 通过分别对每行和每列应用\(N\)点一维变换,可以将二维变换实现为可分离变换。 然后对得到的变换系数(\(coeff\))进行量化(相当于除以量化步长\(Q_{step}\))以获得量化变换系数(\(level\))。 在解码器处,量化的变换系数随后被反量化(这相当于乘以\(Q_{step}\))。 最后,将二维可分离逆变换应用于反量化变换系数(\(coeff_Q\)),从而产生量化样本的残余块,然后将其添加到帧内或帧间预测样本以获得重建块。

通常,正向变换矩阵和逆变换矩阵是彼此转置的,并且被设计为在没有中间量化和去量化步骤的情况下连接时实现输入残差块的近乎无损重建。

在 HEVC 等视频编码标准中,指定了反量化过程和逆变换,而正向变换和量化过程则由实施者选择(受到比特流的约束)。 然而,在下文中,除非另有说明,我们将根据正向变换矩阵讨论 HEVC 核心变换的设计和属性。 逆变换在HEVC标准中被指定为对应的转置矩阵。

B. Discrete Cosine Transform

应用于输入样本\(u_i\)的\(N\)点 1D DCT 的变换系数\(w_i\)可以表示为:

这里\(i=0,…,N-1\),\(c_{ij}\)为DCT变换矩阵\(C\)的系数,定义如下:

这里\(i,j=0,…,N-1\), \(A = 1\) or \(2^{1/2}\) for \( i = 0 \) or \(i > 0\),DCT的基向量\(c_i\)定义为\(c_i=[c_{i0},…,c_{i(N-1)}]^T, i=0,…,N-1\)

DCT 具有多个被认为对于压缩效率和高效实现都有用的属性:

  1. 基向量是正交的,即\(c^T_ic_j=0\)。 该属性对于通过实现不相关的变换系数来提高压缩效率是理想的。
  2. DCT 的基向量已被证明可以提供良好的能量压缩,这对于压缩效率来说也是理想的。
  3. DCT 的基向量具有相等的范数,即\(c^T_ic_i=1\)。 该属性对于简化量化/去量化过程是有利的。 假设需要量化误差的相等频率加权,则基向量的相等范数消除了对量化/去量化矩阵的需要。
  4. 让\(N=2^M\)。 大小为\(2^M×2^M\) DCT 矩阵的元素是 \(2^{(M+1)}×2^{(M+1)}\) DCT 矩阵元素的子集。更具体地,较小矩阵的基向量等于较大矩阵的偶数基向量的前半部分。 此属性对于降低实施成本很有用,因为相同的乘法器可以重复用于各种变换大小。
  5. DCT矩阵可以通过使用少量的唯一元素来指定。 通过检查 (2) 的元素\(c_{ij}\),可以看出大小为 \(2^M×2^M\)的 DCT 矩阵中唯一元素的数量等于 \(2^M-1\)。
  6. DCT的偶数基向量是对称的,而奇数基向量是反对称的。 此属性对于减少算术运算的数量很有用。
  7. DCT 矩阵的系数具有一定的三角关系,可以减少算术运算的数量,超出利用(反对)对称性质所能实现的数量。

C. Finite Precision DCT Approximations

HEVC 的核心变换矩阵是 DCT 矩阵的有限精度近似。 在视频编码标准中使用有限精度的好处是,实值 DCT 矩阵的近似值在标准中指定,而不是依赖于实现。 这可以避免制造商使用略有不同的浮点表示实现 IDCT 所导致的编码器-解码器不匹配。 另一方面,使用近似矩阵元素的缺点是第 B 节中讨论的 DCT 的一些属性可能不再满足。 更具体地说,在与对矩阵元素使用高位深度相关的计算成本和满足第 B 节的一些条件的程度之间存在权衡。

确定 DCT 矩阵元素的整数近似值的一种直接方法是用某个大数(通常在\(2^5\)和\(2^{16}\)之间)缩放每个矩阵元素,然后舍入到最接近的整数。 然而,这种方法并不一定能产生最佳的压缩性能。 如第 D 节所示,对于给定的矩阵元素位深度,近似 DCT 矩阵元素的不同策略会导致第 B 节的一些属性之间的不同权衡。

D. HEVC Core Transform Design Principles

用于 HEVC 核心变换的 DCT 近似是根据以下原则选择的。 首先,B 部分的属性 4、5 和 6 得到满足,没有任何妥协。 这种选择确保了 DCT 的几个实现友好的方面得以保留。 其次,对于属性 1、2、3 和 7,在用于表示每个矩阵元素的位数和满足每个属性的程度之间存在权衡。

为了测量属性 1、2 和 3 的近似程度,为整数点 DCT 近似定义了以下测量,其缩放矩阵元素等于\(d_{ij}\),基向量\(d_i=[d_{i0},…,d_{i(N-1)}]^T, i=0,…,N-1\)

  1. 正交性度量:\(o_{ij}=d^T_id_j/d^T_0d_0, i≠j\)
  2. 与 DCT 的相似性度量:\(m_{ij}=|αc_{ij}-d_{ij}|/d_{00}\)
  3. 范数度量:\(n_i=|1-d^T_id_j/d^T_0d_0|\)

比例因子\(α\)定义为 \(d_{00}N^{1/2}\)。

经过仔细研究,决定用8位(包括符号位)来表示每个矩阵系数,并选择第一基向量的元素等于64(即 ,\(d_{0j}=64\))。 请注意,与正交 DCT 相比,这会导致 HEVC 变换矩阵的比例因子为\(2^{(6+M/2)}\)。 其余矩阵元素进行手动调整(在属性 4、5 和 6 的约束内),以实现属性 1、2 和 3 之间的良好平衡。手动调整执行如下。 首先,推导出实值缩放 DCT 矩阵元素\(αc_{ij}\) 。

接下来,对于结果矩阵中的每个唯一数字,检查\(αc_{ij}\)区间 [-1.5, 1.5] 周围的每个整数值,并计算 \(o_{ij}\)、 \(m_{ij}\)和 \(n_{ij}\)值。 由于变换矩阵中只有 31 个唯一数字(参见第 E 节),因此可以系统地检查各种排列(尽管不是详尽的)。 选择最终的整数矩阵元素是为了在所有测量 \(o_{ij}\)、 \(m_{ij}\)和 \(n_{ij}\)之间提供良好的折衷。 所得到\(o_{ij}\)、 \(m_{ij}\)和 \(n_{ij}\)的最坏情况值显示在表 I 的第二列中。范数被认为足够接近 1(即范数测量\(n_{ij}\)足够接近 0),以证明不使用非平坦默认值是合理的 HEVC 中的去量化矩阵(即所有变换系数均等缩放)。

为了比较的目的,表I的第三列中列出了将实值DCT矩阵元素与\(2^{(6+M/2)}\)相乘并舍入到最接近的整数时的结果测量。从表中可以看出,虽然HEVC变换的矩阵元素距离缩放后的DCT矩阵元素较远,但它们具有更好的正交性和范数性质。

最后,仅使用 8 位表示,B 节的属性 7(矩阵元素之间的三角关系)不容易保留。 作者不知道 HEVC 核心变换的任何三角特性可用于将算术运算数量减少到低于使用(反)对称特性时所需的数量。

E. Basis Vectors of the HEVC Core Transforms

指定32点正向变换的32×32矩阵的左半部分如图2所示。

右半部分可以通过使用基向量的(反)对称性质(B节的性质6)来导出。HEVC的逆变换矩阵被定义为图中矩阵的转置。32×32矩阵包含多达31个唯一数字,如下所示:

这些唯一数字是正向变换矩阵的第一列的元素1至31。请注意,尽管数字90出现了三次,但这是偶然的,通常不是真的。

此外,较小变换矩阵\(N=4,8,16\)的系数可以从 32×32 变换矩阵的系数导出为:

让\(D_4\)表示 4×4 变换矩阵。 利用式(4)和图2,\(D_4\)可得:

8×8 变换矩阵和16×16 变换矩阵可以类似地从32×32 变换矩阵获得,如图2所示,其中使用不同的颜色来突出显示嵌入的16×16、8×8 和4×4 正向变换矩阵。 此属性允许使用相同的架构来实现不同的变换大小,从而促进不同变换大小之间的硬件共享。

请注意,根据 (3) 的唯一数属性和(反对)对称属性,\(D_4\)也等于:

F. Intermediate Scaling

由于与正交DCT变换相比,HEVC矩阵是按比例\(2^{(6+M/2)}\)缩放的,并且为了通过正向和反向二维变换保持残差块的范数,需要应用额外的比例因子\(S_{T1}\)、\(S_{T2}\)、\(S_{IT1}\)、\(S_{IT2}\),如图3所示。请注意,图3基本上是图1中变换和量化的定点实现。虽然HEVC标准规定了逆变换的比例因子(即\(S_{IT1}\)、\(S_{IT2}\)),但HEVC参考软件也规定了前向变换的相应比例因子(例如\(S_{T1}\)、\(S_{T2}\))。比例因子是在以下限制条件下选择的:

  1. 所有缩放因子应为 2 的幂,以允许缩放以右移的方式实现。
  2. 假设输入残差块的全范围(例如,所有样本都具有最大幅度的DC块),每个变换阶段之后的位深度应等于16位(包括符号位)。 这被认为是准确性和实施成本之间的合理权衡。
  3. 由于HEVC矩阵按比例\(2^{(6+M/2)}\)缩放,2D正向变换和逆变换的级联将导致1D行正向变换、1D列正向变换、1D列逆变换和1D行逆变换中的每一个的按比例\(2^{(6+M/2)}\)缩放。因此,为了通过二维正变换和逆变换保持范数,所有比例因子的乘积应等于\((1/2^{(6+M/2)})^4=2^{-24}2^{-2M}\)。

图4以4×4正向变换为例说明了选择正向变换比例因子的过程。当视频具有B bit位深度时,残差将在需要(B+1)位来表示的范围\([-2^B+1,2^B-1]\)内。 在下面的worst case 位深度分析中,我们将假设残差块的所有样本的最大振幅等于正向变换第一阶段的输入。 我们相信这是一个合理的假设,因为所有基向量都具有几乎相同的范数。

还要注意,在worst case 分析中,我们使用\(-2^B\)而不是\(-2^B+1\)或\(2^B-1\),因为它是2的幂。由于所有比例因子都是2的幂,所以假设输入为\(-2^B\)(仍然适合(B+1)位),则比例因子推导变得更简单。对于这种worst case的输入块,输出样本的最大值将为\(-2^B×N×64\)。这对应于第一基向量(长度为N,所有值均等于64)与由等于\(-2^B\)的值组成的输入向量的点积。因此,对于\(N=2^M\),为了使输出适合于16位(即,\(2^{-15}\)的最大值)内,需要\(1/(2^B×2^M×2^6×2^{-15})\)的缩放。因此,第一变换阶段之后的比例因子被选择为\(S_{T1}=2^{-(B+M-9)}\)。

正向变换的第二阶段由第一变换阶段的结果与\(D^T_4\)相乘组成。正向变换第二阶段的输入是第一阶段的输出,该输出是一个矩阵,第一行中的所有元素都具有 \(2^{-15}\)的值。所有其他元素将为零,如图 4(b) 所示。 与\(D^T_4\)相乘的输出将是一个矩阵,其中仅 DC 值等于 \(2^M×2^6×2^{-15}\),所有其余值等于 0。这意味着第二阶段变换后所需的缩放为 \(S_{T2}=2^{-(M+6)}\),以便输出适合 16 位。

逆变换的第一阶段由正变换的结果与\(D^T_4\)相乘组成。逆变换第一阶段的输入是正变换的输出矩阵,该矩阵是仅 DC 元素等于\(2^{-15}\)的矩阵。 与\(D^T_4\)相乘的输出将是一个矩阵,其第一列元素等于 \(2^6×2^{-15}\)。因此,在逆变换的第一阶段之后,为了使输出适合 16 位,所需的缩放是\(S_{IT1}=2^{-6}\)。

逆变换的第二阶段包括将逆变换的第一阶段的结果与\(D_4\)相乘。 逆变换第二级的输入是逆变换第一级的输出矩阵,该矩阵的第一列元素等于\(2^{-15}\)。与\(D_4\)相乘的输出将是所有元素等于\(2^6×2^{-15}\)的矩阵。 因此,在第二阶段逆变换之后,为了使输出值进入 \([-2^B,2^B-1]\)的原始范围所需的缩放是 \(S_{IT1}=2^{-21-B}\)。

总之,本节中施加的约束会在不同的变换阶段后产生以下比例因子:

  • 在第一个正向变换阶段之后:\(S_{T1}=2^{-(B+M-9)}\)
  • 在第二个正向变换阶段之后:\(S_{T2}=2^{-(M+6)}\)
  • 在第一个逆变换阶段之后:\(S_{IT1}=2^{-6}\)
  • 在第二个逆变换阶段之后:\(S_{IT1}=2^{-21-B}\)
    其中B是输入/输出信号的位深度(例如 8 位), 是\(M=log2(N)\)。

在没有量化/去量化的情况下,比例因子的这种选择可确保在所有变换阶段之后的位深度为 16 位。 然而,量化/反量化过程引入的量化误差可能会将每个逆变换阶段之前的动态范围增加到超过 16 位。 例如,考虑 B = 8 和正向变换的所有输入样本等于 255 的情况。在这种情况下,正向变换的输出将是值等于 \(255<<7 =32640\) 的 DC 系数。对于高 QP 值并使用向上舍入量化器,每个逆变换级的输入很容易超出 [-32768, 32767] 允许的 16 位动态范围。 虽然在去量化器之后clip到 16 位范围被认为是微不足道的,但在第一个逆变换阶段之后被认为是不受欢迎的。 为了允许一定程度的量化误差,同时将两个逆变换级之间的动态范围限制为 16 位,逆变换比例因子的选择最终修改如下:

  • 在第一个逆变换阶段之后:\(S_{IT1}=2^{-7}\)
  • 在第二个逆变换阶段之后:\(S_{IT1}=2^{-20-B}\)

逆变换比例因子的使用如图 5 所示,以 4×4 逆变换为例,假设输入是图 4 的最终输出。

表 II 和表 III 分别总结了与正交 DCT 相比正向变换和反向变换的不同缩放因子。

HEVC规范指定在缩放之前添加的偏移值以进行舍入。 该偏移值等于比例因子除以 2。图 3-5 中未明确显示该偏移。

最后,使用 8 位系数并将中间数据的位深度限制为 16 位的两个有用结果是,所有乘法都可以用具有 16 位或更少的乘法器来表示,并且右移之前的累加器可以用更少的位来实现。 所有变换阶段都超过 32 位。

G. Quantization and De-Quantization

量化由除以量化步长(\(Q_{step}\))组成,逆量化由乘以量化步长组成。类似于H.264/AVC,量化参数(QP)用于确定HEVC中的量化步长。可以取从0到51的52个值。1的增加意味着量化步长增加了大约12%(即\(2^{1/6}\))。增加6导致量化步长增加2倍。除了指定两个连续QP值的步长之间的相对差之外,还需要定义与QP值的范围相关联的绝对步长。这是通过为\(Q_{step}=1\)选择QP=4来完成的。

正交变换的等效量化步长之间的结果关系现在由下式给出:

式(7)也可表示为:

HEVC 量化和反量化基本上是(8)的定点近似。 如图 3 所示,引入了额外的比例因子 \(S_Q\)和 \(S_{IQ}\),以恢复残差块的范数,该范数由于(8)的定点实现中使用的缩放而被修改。

HEVC 中 (8) 的定点近似由下式给出:

这导致

对于量化器输出、level,去量化器在 HEVC 标准中指定为:

这里\(shift1=(M-9+B)\), \(offset_{IQ}=1<<(M-10+B)\).

图3的比例因子\(S_{IQ}\)等于\(2^{-shift1}\),如下所示:当QP=4时,图3中的逆变换和反量化的组合比例在相乘时应产生1的乘积,以通过逆变换和逆量化保持残差块的范数,即

这导致\(S_{IQ}=2^{-(M-9+B)}\)导致\(shift1\)等于右移 \((M-9+B)\) 。 (13) 中的比例因子 D 从表 III 中获得。

对于正向变换的输出样本, coeff,可以按如下方式实现简单的量化方案:

这里\(shift2=29-M-B\),

注意 \(f_{QP\%6} \approx 2^{14}/G_{QP\%6}\)。shift2的值是通过对正向变换和量化器中的组合缩放施加类似的约束来获得的,如(13)中,即 \(S_Q×f_4×2^{(15-B-M)}=1\),这里\(S_Q=2^{-shift2}\).

最后,选择 \(offset_Q\)来实现所需的舍入。

总而言之,选择量化器乘法器\(f_i\)和去量化器乘法器\(g_i\)以满足以下条件

1) 确保\(g_i\)可以用有符号的 8 位数据类型表示。
2) 确保步长从一个QP值到下一个QP值的增加几乎相等(大约 12%)
3) 通过量化和反量化过程确保近似单位增益
4) 为QP=4提供所需的量化步长绝对值。