AV1解码器模型

Stella981
• 阅读 854

这篇文章可以作为AV1规范中与解码器型号和级别有关的部分的简介,本文的其余部分描述了一些AV1基本概念,AV1解码器模型,并提供了开发它时做出决策的原因。有关解码器模型的更多详细信息,请阅读AV1规范。

文 / Andrey Norkin

原文链接: https://norkin.org/research/av1\_decoder\_model/

为什么编解码器需要解码器模型

大多数现代视频编解码器都具有某种形式的解码器模型。在MPEG-2中,它被称为视频缓冲验证器(VBV);在H.264 / AVC和HEVC / H.265中,它可以称为假设参考解码器(HRD)。解码器模型提高了互操作性。解码器模型允许确认一个比特流是否可以被一个特定的解码器解码。这些模型还可以向解码器提供关于何时开始解码帧以能够及时显示它的指令。

通常来说,视频解码器声明支持某个配置文件和级别。配置文件可以指定有关比特深度和色度二次采样的视频格式,以及解码器需要支持的以解码比特流的一组编码工具。级别描述了视频比特流的定量特征,例如分辨率,帧速率和比特率。对于视频编解码器生态系统而言至关重要的一点是,表明支持某个级别的解码器是否能够解码符合该级别要求的任何比特流,并且内容提供商和编码器制造商可以检查其生成的流是否符合这些要求。

为了实现这些目标,由开放媒体联盟(AOM)开发的AV1规范定义了与配置文件和级别系统耦合的解码器模型。AV1解码器模型包括平滑/位流缓冲区,解码过程以及对解码后的帧缓冲区的操作。这篇文章可以作为AV1规范中与解码器型号和级别有关的部分的简介。本文的其余部分描述了一些AV1基本概念,AV1解码器模型,并提供了开发它时做出决策的原因。有关解码器模型的更多详细信息,请阅读AV1规范。

AV1比特流的高级结构

在更高级别上,AV1结构以开放比特流单元(OBU)打包。每个OBU都有一个标头,该标头提供标识其有效负载的信息(请参见图1)。可以在AV1视频比特流中出现的OBU类型的示例是序列头OBU,帧头OBU,元数据OBU,时间定界符OBU和图块组OBU。帧OBU由打包到一个OBU中的帧头和图块组OBU组成,以提供一种通用结构的更有效表示,其中帧头数据后紧跟着帧或图块组数据。

根据语法元素show_existing_frame的值,AV1帧头可以分为两种主要类型。

AV1解码器模型

show_existing_frame等于0的帧头是需要解码的常规帧。show_existing_frame等于1的帧头指定了在该帧头中指定的显示时间显示先前解码的帧(由frame_to_show_map_idx表示)的命令。当解码顺序与显示顺序不同时,该机制有助于帧重新排序。

另一个AV1概念是时间单元(TU),它由时间定界符OBU和在此之后且在下一个时间定界符OBU之前的所有OBU组成。TU始终遵循递增的显示顺序。如果未使用可伸缩性,则TU仅包含一个显示帧,即show_existing_frame等于1或show_frame等于1的帧。如果使用了可伸缩性,则TU中来自不同可伸缩层的所有显示帧都对应于相同的呈现时间。  一个TU也可以包含show_frame标志等于0的帧。此类帧会被解码但不会立即显示。它们用于支持如上所述的帧重排序。类似地,也可以发送覆盖帧,该覆盖帧会对先前解码的帧(称为替代参考帧(ARF))与源帧之间的差异进行编码。AV1比特流的这一方面类似于VP9编解码器中的超帧。

在图2中显示出了将比特流划分为时间单元的示例。在该图中,帧编号按照显示顺序编号。比特流使用具有三个时间层的4帧的双向层级预测结构。show_frame等于0的帧显示为青色框,show_frame等于1的帧显示为深绿色框。FrameHdr 2是show_existing_frame标志等 于1的帧头,该帧指向先前解码的Frame 2。

AV1解码器模型

平滑缓冲

平滑缓冲器是AV1解码器的一部分,用于存储AV1比特流,直到压缩数据被解码器解码完毕为止。缓冲区由所谓的“漏桶”模型构成。漏桶的类比和编码器的操作有关,在压缩器中,压缩帧被分块转储到缓冲区中,并且数据以恒定速率连续离开缓冲区。解码器缓冲区是编码器之一的对应部分。注意,平滑缓冲器是解码器内部的。通常来说,解码系统会在更高级别上具有其他缓冲区,这些缓冲区不在AV1规范的范围内。从解码器模型的角度来看,可以将较高级别的缓冲区视为传输通道中造成总延迟的一部分。例如,从解码器的角度来看,与自适应流式传输有关的缓冲将被视为传输通道的一部分,在本文中不再讨论。而且,可能经常出现预先准备编码的比特流,而这会使延迟相当长。但是,对于模型而言,这样的长延迟通常不是问题,因为它在方程式中被抵消了。

平滑缓冲区可确保解码器具有足够的内部存储器来存储到达(或读取)的位流的数据。当解码器需要时,它还确保下一帧的压缩数据在缓冲区中。平滑缓冲器的大小限制了瞬时比特率的变化,并限制了帧数据消耗的时序。

AV1解码器模型仅支持可变比特率(VBR)操作模式,而不支持恒定比特率(CBR)模式。解码器模型的VBR模式是一种抽象模式,其中速率在最大级别比特率和零之间交替。听起来可能有限制。但是,此模型足以确保在最坏的情况下确保比特流与解码器功能匹配。

平滑缓冲区充满度随时间变化的示意图如图3所示。时钟从与帧0有关的第一个比特的到达开始。斜线的斜率与比特到达的速率相对应。Removal [i]对应于从缓冲区中删除帧i的数据并开始解码帧i的时刻。注意,可能会有一段时间没有新的比特到达,例如Removal [1]之后的时间。这与编码器没有要发送的位(即编码器缓冲区为空)的时间段匹配。

AV1解码器模型

帧i的removal [i]是根据两种解码模式之一来定义的。在解码调度模式下,这些值在比特流中用信号发送。在资源可用性模式下,根据解码器操作导出Removal [i]。解码的开始,即Removal [0],由两种模式中的变量decoder_buffer_delay确定。

在时间Removal [i]时从解码缓冲区中删除的比特属于可解码帧组(DFG)i,即与帧i − 1相关的最后一个OBU的末尾与与帧i相关的最后一个OBU的末尾之间的所有OBU 。DFG中的OBU可以包括序列头OBU,帧和图块组OBU,帧头OBU和元数据OBU。

DFG i的第一位到达平滑缓冲区由FirstBitArrival [i]确定,该值如下所示:

AV1解码器模型

关于后一个表达式中coder_buffer_delay和decoder_buffer_delay之间关系以及其他有用的信息可以从 Ribas-Corbera et al, 2003 中找到很好的解释。该模型假设一个编码器具有一个以恒定速率发送比特的平滑缓冲器,并且一个解码器带有一个以该比特率接收比特的平滑缓冲器。通常来说,encoder_buffer_delay和decoder_buffer_delay的作用是确定帧的编码和解码之间的延迟,因此限制了比特流存储在解码器缓冲区中的“窗口”(通过网络/信道进行的传输是排除在外的)。由于缓冲区大小设置为比特流在最大级别比特率下的1秒,因此建议不要将这两个变量的总和超过90 000,这相当于时钟频率的1秒。

当low_delay_mode标志等于1时,解码器在低延迟模式下运行,在该模式下,帧数据在预定的移除时间可能还不在缓冲区,在这种情况下,移除时间会延迟,直到数据到达缓冲区。

除非处于低延迟模式,否则平滑缓冲区不应下溢。平滑缓冲区也不应溢出。这些限制适用于所有一致的比特流。

解码帧缓冲区

帧缓冲器用于存储解码后的帧,以便可以将它们用于帧间预测或之后的显示。AV1定义了一个缓冲池,该缓冲池代表帧缓冲区的存储区域。AV1帧缓冲区的管理示意图如图4所示。AV1规范要求解码器支持10个物理帧缓冲区。帧缓冲器的时隙应能够以对应级别的最大分辨率存储帧。虚拟缓冲器索引(VBI)用于指向图片间预测中的帧。VBI可以在帧缓冲池中存储8个帧索引。并且允许不同的VBI条目指向同一缓冲区。空的VBI条目值为-1。当前帧缓冲区索引(cfbi)将索引存储到正在解码当前帧的帧缓冲区。注意,有一个“额外的”物理帧缓冲区,可用于保存帧以用于显示。

AV1解码器模型

数组DecoderRefCount和PlayerRefCount(图4中的前两行)分别跟踪解码和显示过程是否仍需要帧缓冲区。DecoderRefCount跟踪对VBI中的帧缓冲区的引用数,并由语法元素refresh_frame_flags更新,而当帧在上次演示时已显示时,PlayerRefCount设置为0。空帧缓冲区和相应的计数器在图4中显示为白色方块。

帧缓冲器对视频帧的解码和表示施加了限制,从而限制了编码器可以使用哪些预测结构和帧的重新排序。通常来说,10个帧缓冲区允许支持相当复杂的预测结构。解码器模型在应显示该帧时会验证该帧是否可用,并且在应解码一帧时在缓冲池中有一个空闲位置。

解码过程

AV1解码器模型的解码过程将对平滑缓冲区和解码器帧缓冲区的操作联系在一起。特别地,解码器模型确定何时开始帧解码以及从平滑缓冲器中移除帧比特,这立即使平滑缓冲器的饱和度降低了相应的量。解码器模型还会计算解码何时完成,并将解码后的帧添加到帧缓冲区。它还确定何时为显示输出帧并将其从缓冲区中移除。

AV1的一个特点是广泛使用替代参考帧(ARF),即用作预测参考但从未显示过的帧。此外,AV1在主配置文件中支持参考图片的缩放和可伸缩性。这意味着该模型应适应帧解码所需的不同时间,并支持不同的帧解码和显示速率。请注意,即使H.264和HEVC允许显示不可显示的图片,但这并不是这些编解码器的典型用法,而在AV1中,这是一种典型的使用情况,需要解码器模型很好地支持。

AV1解码器模型

图5中展示了使用ARF进行编码的示例。该图显示了sub-GOP大小为4的双向层级结构编码。可显示的帧显示为灰色矩形。不显示的替代参考帧(ARF),用白色矩形表示。通常,该帧是在相同时间位置的帧的滤波版本,这为帧间预测带来了优势。由于对ARF进行了低通滤波,因此可以使用ARF作为预测因子对覆盖帧(图5中的OL)进行编码。覆盖帧会添加高频和纹理信息。

为了支持替代参考帧和不同分辨率的帧,AV1解码器模型引入了以下功能:

l在解码器中使用不同数量的时间单位并显示时钟节拍的可能性。注意,图5中的显示时钟节拍(DispCT)和解码时钟节拍(DecCT)具有不同的长度,因为解码和显示速率不同。解码器和显示刻度均使用相同的时标,并且时钟已同步 l帧不会立即解码,并且根据帧分辨率和其他因素,可以有不同的时间 可以看到,图5中的解码和显示时间轴使用了不同的时钟节拍。 显然,在显示帧之前需要完成每个帧的解码。 为了确保将来有可用的帧,编码器可以使用initial_display_delay_minus_1,该参数对应已解码的帧数减去在显示第一帧之前帧缓冲区中应可用的帧数。 此参数相对于解码偏移了显示过程。 如果未发信号,则将initial_display_delay_minus_1的值推断为BUFFER_POOL_MAX_SIZE −1。 总的显示延迟包括coder_buffer_delay,它与图3中的变量相同,是从第一个比特到达到开始解码帧0之间的时间,即Removal [0]。

解码帧i所需的时间确定为:

TimeToDecode [i] = lumaSamples [i]÷MaxDecodeRate,

其中,MaxDecodeRate以样本/秒为单位进行测量,并由每个解码器级别指定。依次为帧内预测帧计算lumaSamples,如下所示:

lumaSamples [i] = UpscaledWidth [i] * FrameHeight [i]。

UpscaledWidth是使用可选的超分辨率工具后的帧的宽度。对于帧间预测帧,在参考图片重采样的情况下,考虑到来自分辨率更高的帧的可能运动补偿,可以确定此数量,如下所示

lumaSamples [i] = max_frame_width * max_frame_height。

在可伸缩比特流中,将lumaSamples确定为当前可伸缩层的最大宽度和高度的乘积。

除了知道帧解码需要花费多长时间之外,解码器模型还需要确定何时开始解码以及从平滑缓冲区中删除压缩帧,即Removal [i]。关于如何计算Removal [i],AV1具有两种不同的模式。这两种模式是以下描述的资源可用性模式和解码调度模式。

资源可用性模式

在资源可用性模式中,如果在解码的帧缓冲区中有可用的空闲位置,则在完成前一帧解码之后立即解码一帧。否则,在一个位置释放后对帧进行解码。如果比特流低于解码器的最大级别限制,则逐帧解码这些帧,直到它们填满所有可用的帧缓冲区,此后解码速度会减慢。然后,仅在解码的帧缓冲区释放后,才进行下一帧的解码。帧0的删除时间由decoder_buffer_delay确定:

Removal[ 0 ] = decoder_buffer_delay ÷ 90 000

要使用资源可用性模式,应在比特流中设置以下参数:Timing_info_present_flag = 1,decoder_model_info_present_flag = 0,并且equal_picture_interval =1。标志equal_picture_interval等于1表示使用了恒定的帧速率,并且不发送显示时间。而是从帧速率和initial_display_delay_minus_1得出显示时间。解码定时Removal [i]由解码的帧缓冲器可用时的时刻来决定,并且也不发信号通知。一些解码器模型参数在资源可用性模式下采用默认值,例如,encoder_buffer_delay = 20 000,decoder_buffer_delay = 70 000,low_delay_mode_flag = 0。

解码调度模式

在解码调度模式下,除了帧显示时间之外,还在视频比特流中用信号发送解码时间Removal [i]。该模型灵活地定义了何时从平滑缓冲区中删除帧并对其进行解码,以及何时显示该帧。除了使用恒定的帧速率外,该模型还可以通过显式发送帧表示时间来支持变化的帧速率。除此之外,解码器时钟节拍DecCT以及decoder_buffer_delay,encoder_buffer_delay和ScheduledRemovalTiming [i]也以这种解码模式发送信号。

在这种模式下,帧i的计划删除时间如下所示。

ScheduledRemovalTiming [0] = encoder_buffer_delay÷90 000。

ScheduledRemovalTiming [i] = ScheduledRemovalTiming [PrevRap] + buffer_removal_time [i] * DecCT,

其中PrevRap是先前的随机访问点(RAP)。如果帧i对应于RAP,但不是比特流中的第一帧,则PrevRAP对应于先前的RAP。这里的随机访问点是指比特流中的一个位置,可以从中解码该比特流。它通常对应于一个关键帧,并且应包含所有开始解码位流所需的信息,包括序列头。

除非解码器在低延迟模式下运行,否则删除时间与计划的删除时间一致

Removal [i] = ScheduledRemovalTiming [i]。

为了支持可伸缩性,解码器模型针对每个工作点(OP)单独发出信号。工作点与某个可伸缩层的解码及其解码所需的较低可伸缩层有关。比特流中较高的工作点可能需要使用符合较高级别的解码器。

解码器模型的两种模式之间的差异

可以注意到,解码调度模式下的解码器操作是资源可用性模式下的解码器操作的超集。编码器应该有可能用信号通知在资源可用性模式中可能已经导出的相同Removal [i]。解码时间表模式也可以用于控制帧解码时间表。图6示出了当比特流需求低于最大等级能力时的情形。在资源可用性模式下,将帧依次解码,并且当帧缓冲区中没有剩余空闲时隙时,解码速度会变慢。在解码调度模式下,可以以恒定速度解码比特流。注意,当解码器接近其最大能力工作时(例如,比特流接近于等级限制的分辨率和帧率),两种模式下的解码器 操作是相似的。

AV1解码器模型

另外,可以使用解码调度模式来更好地控制平滑缓冲区的饱和度(见图7)。该图说明了平滑缓冲区充满度如何随时间变化,取决于参数coder_buffer_delay和decoder_buffer_delay的值。该图使用1920×1080的视频,每秒24帧,编码为4.0级AV1比特流。选择符合8帧分层预测结构的帧大小;该示例已构建,并不代表任何特定的 视频编码。最大的平滑缓冲区容量由水平虚线显示。

AV1解码器模型

图7(a)显示了encoder_buffer_delay = 20 000,decoder_buffer_delay = 70 000时随时间变化的缓冲区充满度,它们等于资源可用性模式中使用的默认值。

AV1解码器模型

通过减少coder_buffer_delay,可以更早开始解码,这在图7(b)中通过使用encoder_buffer_delay和decoder_buffer_delay均等于45 000进行了演示。请注意,encoder_buffer_delay与decoder_buffer_delay的总和等于90 000,这对应于1秒,即平滑缓冲区可以保持的最大级别比特率下的比特流持续时间。

AV1解码器模型

通过使用参数coder_buffer_delay = 10000,decoder_buffer_delay = 45000,也可以将缓冲区充满度保持在较低水平,如图7(c)所示。

显示时间

AV1的显示时间通过frame_presentation_time语法元素发出信号。实际的显示时间还取决于InitialPresentationDelay,其计算方式如下:

PresentationTime [0] = InitialPresentationDelay,

PresentationTime [j] = PresentationTime [PrevPresent] + frame_presentation_time [j] * DispCT,

其中,如果前一个RAP是关键帧RAP,则PrevPresent对应于与最后一个关键帧随机接入点(RAP)关联的索引;如果前一个RAP是延迟RAP,则PrevPresent对应于延迟恢复点(即对应于前向关键帧/open-GOP)。延迟的恢复点对应于open-GOP中的关键帧的显示时间。

InitialPresentationDelay依次确定如下:

InitialPresentationDelay =Removal[initial_display_delay_minus_1] + TimeToDecode [initial_display_delay_minus_1]。

换句话说,InitialPresentationDelay是帧缓冲区中存在initial_display_delay_minus_1 + 1个解码帧的时间。

当equal_picture_interval等于1时,使用恒定帧率模式,并且大于0的帧j的显示时间推导如下:

PresentationTime [j] = PresentationTime [j − 1] +(num_ticks_per_picture_minus_1 + 1)* DispCT,

其中PresentationTime [j-1]指的是显示顺序中的前一帧。如上导出PresentationTime [0]。

解码器模型信令

解码器模型参数主要在序列和帧级别上发出信号。序列标头可以包括Timing_info()结构,该结构包含显示时序信息。基本的解码器模型信息位于decoder_model_info()结构中。除此之外,还可以在序列头中用信号发送一个或多个操作点(OP),以实现可伸缩的比特流。每个OP对应于解码该OP所必需的解码器级别,并且可以可选地被分配一组解码器模型参数。

Timing_info()结构包含时间刻度和显示刻度号num_units_in_display_tick中的时间单位数,而coder_model_info()结构包含解码器刻度号num_units_in_decoding_tick中的单位数以及其他解码器模型语法元素的长度。这两个语法元素将DispCT和DecCT变量的持续时间定义为:

DispCT = num_units_in_display_tick÷time_scale,

DecCT = num_units_in_decoding_tick÷time_scale。

operating_parameters_info()结构包含用于操作点的 encoder_buffer_delay 和decoder_buffer_delay 以及低延迟模式标志。如果使用解码器模型,则可以在帧头中为选定的工作点发信号通知以解码时钟节拍为单位的buffer_removal_time。帧头中的temporal_point_info()结构包含frame_presentation_time语法元素,该元素以显示时钟节拍表示信号的显示时间。

AV1等级

在撰写本文时,AV1规范定义了2.0到6.3级,该级别大致涵盖了将视频从426×240 @ 30fps解码到7680×4320 @ 120fps所需的解码器功能。解码器模型将比特流和解码器一致性统一到了一定水平。AV1级别声明支持某种帧分辨率(一帧中的样本数),解码以及显示的采样率。与解码器模型相关的其他级别参数包括最大比特率和帧头速率。级别可以属于两个级别(主级别和高级级别)之一,其中高级级别具有比主级别更高的最大比特率,并且面向专业和特殊应用。

最大比特率直接定义了平滑缓冲区的大小,该平滑缓冲区应能够以最大级别的比特率保持最多1秒的压缩流。由于对一致的比特流不允许缓冲区上溢或下溢,因此这对峰值比特率施加了限制。除此之外,还规定了帧的最小压缩率。

声称符合某个级别的比特流,如果通过解码器模型,则不应违反约束。顺便说一句,相应的解码器应能够解码相同或更低级别的任何顺应性比特流,只要该比特流符合AV1规范(包括通过相应级别的解码器模型测试)即可。

可以在此Wikipedia链接上找到AV1级别的表,尽管通常推荐的来源是AV1规范。

进一步阅读

这篇文章介绍了AV1解码器模型。它还提供了一些有关在开发时进行的设计选择的背景。在 AV1规范 (https://aomediacodec.github.io/av1-spec/av1-spec.pdf)中可以找到AV1解码器模型的完整描述和更多细节。


AV1解码器模型

LiveVideoStackCon 2020 北京

2020年10月31日-11月1日

点击【阅读原文】了解更多详细信息

本文分享自微信公众号 - LiveVideoStack(livevideostack)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
3年前
AV1:为互联网提供开放、免费的视频编解码工具
!(https://oscimg.oschina.net/oscnet/ea2afc18e53a4b838f29d25b713ff5a3.jpg)从学术研究到进入工业界,ZoeLiu一直在算法和音视频领域,目前在谷歌编解码团队为编解码器AV1做开发支持。Zoe畅谈了评定编解码器的标准,以及AV1的最新进度。本文是『下一代编码器』
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这