参考资料:
vp9提供了一种新的划分称为segment,tile也好,hevc的slice也好,都是画面内按顺序的区域划分,但segment不是,所以segment的标记是对每个处理块标记一个id,画面最多可被划分为八个segment。每个segment可拥有四种能力:
-SEG_LVL_ALT_Q:设定自己的qp,应用场景如实现ROI,或者进行码率控制等,后面会再提到因为vp9不对每个block设定qp,所以画面内的画质调制啊码率控制啊就可以通过这个功能来实现。
-SEG_LVL_ALT_LF:设定自己的loop filter强度,应用场景如画面中有一部分水面等特别容易blocky的部分,就可以仅对这样的部分增强delocking filter,而不影响其他的部分。
-SEG_LVL_REF_FRAME:设定自己的参考帧,应用场景如视频会议中背景一般是不变的,就可以设定背景区域都去参考固定的参考帧。
-SEG_LVL_SKIP:segment skip(即不传送coeff),应用场景如背景等画面中静止部分,就可以全部skip节省码流,特别是在低码率的情况下比较有帮助。
所以这种segment的划分并不是为了并行或容错,更多的是为了满足不同应用场景下对画质调试的需求。到qp/loop filter部分会具体提到,hevc的这些部分都是可以对每个处理块来控制的,所以可以实现精细调整。只不过二者实现方式不同,也各有优势吧。
VP9 引入了一个更加先进的分段( segmentation)的概念,使编码器更加高效。 一帧图像中的每个 16×16 的宏块都有一个段标识( segment_id)来区分所属的段。 位于同一个段的宏块在参考帧、量化参数、滤波强度等参数上的处理是相同的, 这样能更好的避免冗余和应对不同的应用需要。例如在一帧中的背景区域可以采 用较小的量化参数以达到较好的编码质量,这样在编码后续帧时,背景区域通过 帧间预测可以达到更精确的预测效果,从而可以节约出更多的码率用于编码前景。
结合代码的理解
1、segment 是vp9中将一帧有相同参考帧、量化参数、滤波强度等参数的宏块进行统一管理的方式,segment最多有8个,每个segment 用segment id进行统一管理更新。每个segment的数据可以在帧级别进行单独更新,如果某一帧没有更新segment的数据,那么他用的就是前一帧保存使用的。除了I帧 全I块的帧,以及跟之前完全没关系的帧,这些的帧的seg 值都重置到默认。
2、segment map 也就是segment id 和参数直接的存储关系,可以通过两种方式进行获取,一种是时域编码,也就是通过上一帧的segmap预测当前的segmap,一种是直接编码,直接在码流中传递segment的信息,这个是通过temporal_update 这个码流元素来区别,为1时采用时域编码方式,否则直接传递。这种在帧级别就定义,也就是一整帧采用的是同一种方式。
3、update_map 是否更新segment map, 为1更新,否则不更新,这个也是在帧级别进行定义的。
4、c last_seg_map cur_seg_map: last_seg_map:上一帧的seg_map, 跟随着解码一直同步进行更新的。 cur_seg_map: 如果没有更新就用上一帧的。
代码中有关segment id的管理
1、 初始context buffer的申请
在每一帧启动解码之前 会去分配一些需要的buffer,在vp9_alloc_context_buffers中
if (cm->seg_map_alloc_size < cm->mi_rows * cm->mi_cols) {
// Create the segmentation map structure and set to 0.
free_seg_map(cm);
if (alloc_seg_map(cm, cm->mi_rows * cm->mi_cols))
goto fail;
}
如果当前cm->mi_rows*cm->mi_cols的大小大于上一帧的,那么释放掉cm->seg_map_array[i],重新申请cm->mi_rows*cm->mi_cols 大小的seg_map_array。其中数组大小总共为2, 其中current_frame_seg_map默认为0, last_frame_seg_map为1。
2、 I帧 和 错误隔离相关的处理
if (frame_is_intra_only(cm) || cm->error_resilient_mode)
vp9_setup_past_independence(cm);
其中vp9_setup_past_independence 的处理
if (cm->last_frame_seg_map && !cm->frame_parallel_decode)
memset(cm->last_frame_seg_map, 0, (cm->mi_rows * cm->mi_cols));
if (cm->current_frame_seg_map)
memset(cm->current_frame_seg_map, 0, (cm->mi_rows * cm->mi_cols));
可以看到在I帧或者是错误隔离的模式时,会重新初始化context、segment id 相关的结构体。segment 相关的是将last 和current 重新初始化为0。 其中>error_resilient_mode
容错模式允许独立于先前帧进行当前帧的语法元素的解码
3、 buffer的乒乓
在1 中 申请了两个buffer来存储当前和上一帧的。 在解码结束之后。
if (!cm->show_existing_frame) {
cm->last_show_frame = cm->show_frame;
cm->prev_frame = cm->cur_frame;
if (cm->seg.enabled && !pbi->frame_parallel_decode)
vp9_swap_current_and_last_seg_map(cm);
}
vp9_swap_current_and_last_seg_map中将当前帧指向last, 将last指向当前帧。
4、segmen id的使用和更新
在read_inter_frame_mode_info 和read_intra_segment_id中会把相关的segmentid从码流中读取处理。
read_intra_segment_id: 中seg->enabled 为0 的时候, segment id返回0, seg->update_map 为 0 时,将上一帧的last segment id 复制到当前的 curent segment id 里面。
但上述的两个标志都为1的时候,从码流中读出 并赋值到current_seg_map里面。
read_inter_segment_id: 中seg->enabled 为0 的时候, segment id返回0。否则 dec_get_segment_id 将last_seg_map的segment id 取出来(是遍历整个宏块 然后取最小的segmentid 来使用) 放到predicted_segment_id。 这个时候并没有去更新current_seg_map。当seg->update_map 根据不同的预测方式 来更新当前的current_seg_map。
segment id 读上来之后,就会根据这个segment id 去读取想对应的帧间 帧内预测的一些参数来进行解码。
5、整体的流程
第一帧:
首先申请存储segmap的数组,分别存放last seg map和 current seg map。 然后根据码流中的enable和update_map 的标志位 进行current seg map 的更新,当enable 和update_map 都是1 的时候,current_seg_map 更新成码流中的值。 等一帧解码后,判断seg enable 的标志 为1 的情况下, 交换current 和 last的位置,也就是当前帧的current seg map要做为下一帧的last seg map。
第二帧:
首先判断segmap 的空间要不要重新申请了,如果说图像宽高变大了。那么空间要重新申请, 这个时候上一帧的last_seg_map 就被清空了。没有last_seg_map 可以用。 然后判断是不是I帧和错误相关的,是的话,上一帧的last_seg_map 也要清空了。 都不是的情况下,会判断seg enable 和 seg update_map的标志,如果seg enable 为0,segid 为0, 如果update_map为0 ,segment id 用的是last seg map 中的id。否则用码流中current id 并更新current seg map。
在libvpx 中, 只要seg是enable的时候,都会swap currnet_seg_map 和 last_seg_map,即使 当前currnet_seg_map没有更新,curent_seg_map没有更新的时候 current_seg_map 是等于last_seg_map的。
在实际实现的时候,如果说当前curent_seg_map 没有更新,那么不会去swap current 和 last。只有清零 或者current seg map更新的时候才会去swap current 和last。