【转载】JPEG2000

  这是一篇会议论文,由JPEG-2000的官方推荐软件Jasper的作者Michael D. Adams所作的一篇综述性的文章(The JPEG-2000 Still Image Compression Standard)。原文请到www.ece.uvic.ca/˜mdadams去寻找下载。个人认为这篇文章比国内大多数JPEG-2000的教材要好。强烈建议希望从事于JPEG-2000研究的朋友们自己花一、两天时间翻译下。http://blog.csdn.net/KXY_tech/

 

JPEG-2000静止图像压缩标准

 

1简介

       数字图像在世界上应用非常普遍。因此,关于数字图像如何有效显示和存储的标准就非常关键。当今,最为成功并且正在使用的静止图像压缩标准是有联合图像专家组(JPEG)所制订的。这个专家组是由联合技术委员会第29分委员会资助,而这个委员会是由ISO和ITTU-T共同努力建立的。关于JPEG[1-3]和JPEG-LS的标准都是有这个JPEG委员会所制订的。最近几年,JPEG委员会正在努力建立一种新的图像压缩标准:JPEG2000(ISO/IEC15444)。随着JPEG-2000 Part1(i.e., ISO /IEC 15444-1[7])作为一个新的国际标准的发布,他们的努力成果终于变成了现实。

       在这篇论文里面,我们给JPEG-2000 Part-1编码器予详细的技术性介绍,此外还会简要介绍JPEG-2000的标准。这些介绍主要是给有兴趣学习JPEG-2000标准的初学者以指引。尽管这篇文章已经介绍了很多细节,一些其他的细节仍然没有给出。因此,读者如果想对JPEG2000进行开发,应该参考文献[7]。JPEG-2000编码器的实现在JasPer软件[8-10]提供了,作为一个实践的参考给开发者们。读者同样可以在文献[11-13]中找到JPEG2000标准有关的资源。

       这篇文章的结构如下:第二部分给予JPEG2000一个总概。第三部分将详细介绍JPEG2000 Part-1 编码器。在第四章,我们将进行总结。通过我们的文章,读者应该可以对图像编码有个基本的认识。

 

第二章

       JPEG-2000标准支持无损和有损压缩,并且支持单图像分量(如灰度图像)和多图像分量(如彩色图像)。除了基本的图像压缩功能外,还支持其他的功能:1)对图像进行按精度或者按分辨率来渐进显示。2)感兴趣编码,就是对不同的图像区域给予不同的精度。3)对图像的某个区域进行随机访问时,不用对整个码流进行解码。4)提供一种灵活的图像格式,这种格式能够分辨出那些是图像数据信息,那些是码流内部信息。5)具有较好的容错性。由于具有杰出的图像编码性能和众多吸引人的特性,JPEG2000有一个非常大的潜在应用空间。一些可能的应用如下:图像存档、Internet和网页浏览、文件文档、数字照片、医学图像、远程感应、桌面印刷系统。

 

  • A.为什么要使用JPEG2000?

       对于JPEG2000标准的研究和建立从1997年3月的一个准备会议[14]开始的。简历一个新标准有两方面意义。第一,能够解决当今JPEG标准存在的一些缺点。第二,能提供一些JPEG所没有的新功能。新标准将要实现几大目标:1)必须在一个统一的编码框架内同时支持有损和无损压缩。2)必须在较低码率下,提供较好的图像质量,这个质量的标准无论是主观还是客观都必须是很好的。3)支持额外的功能,比如感兴趣区域编码,更灵活的图像格式等。4)避免复杂的计算和避免运算时内存占用过大。毫无疑问,JPEG的极大成功很大部分归功于其免版权费。因此,为了让基本的JPEG2000 编码器的应用免版权费,专家们做了很大的努力。

  • B. 标准的结构

       JPEG2000标准有几大部分组成,如表格I所示。为了方便起见,我们致力于介绍Part1的标准,并把Part1定义为JPEG2000的基础编码器。基础编码器(也可以认为是基本功能),被认为是JPEG2000标准的核。Part2[15]和Part3[16]是对基础编码器的有用和特殊扩展,比如帧内视频信号的压缩。在这篇论文里面,我们将主要讨论基础编码器。而一些扩展,如Part2将简要地介绍。除非明示,以下都是讲的是Part1的内容。

       JPEG2000标准很大程度上是以解码器的角度编写的。这是因为解码器的实现与标准更为接近(如兼容性),而编码器有些部分则没有那么明确的标准要求。因此显然,开发者们需要对什么是必须实现的标准,什么是灵活的有个明确的理解。当然,我们只有在非常必要的情况下,才给出明确的区别。

 

1

 

第三章 JPEG2000编码器

       简单介绍了JPEG-2000标准后,我们现在将一步步地讨论JPEG2000编码器。编码器是基于小波/子带技术的[19,20]。在处理无损和有损压缩时,是基于同样小波变换框架的。同时编码器使用了嵌入式块编码和优化截断(EBCOT)的概念[21-23]。为了更容易更有效地实现有损和无损编码,编码器使用了可逆的整数-整数[24-26]和不可逆的实数-实数的小波变换。为了对小波变换后的数据进行编码,编码器采用了一种称为位平面编码的技术。紧接着的是熵编码,这里采用了一个称为基于上下文的自适应二进制算术编码器[27]-即在JBIG2标准里面使用的MQ编码器。对于压缩后的数据,有两个层次的表示:码流表示结构和JP2文件。码流结构是JPEG标准大体上一致。

       OK,第三章的剩下部分如下:首先,III-A到III-C,将讨论源图像模块和源图像怎么在编码器中表示。随后,III-D是编码器的主体结构。然后,III-E到III-M将详细介绍编码器本身。III-N到III-O将介绍编码器的语法结构。最后,III-P简要介绍标准扩展的Part2。

  • A.源图像模块

       在讨论编码器内部结构之前,我们必须理解编码器所使用的源图像模块。在编码器的角度上看,一个图像能包含大于或等于一的图像分量(最多214个分量)买入图一(a)所示。图一(b)所示,每个分量都是矩形大小的矩阵。每个分量的样本值都是整数,这些样本可以是有符号或无符号的1到38位的整数。样本的符号性质和位数都在分量数据前给出。

 

2

       所有分量都能有个统一的空间扩展信息,但这些分量代表着不同的颜色空间或辅助信息。比如,一个RGB彩色图像有三个图像分量,各表示红、绿、蓝颜色。在最简单的灰度图像中,只有亮度分量。一个图像的不用分量并不需要采样成统一的分辨率。因此,分量之间有不用的尺寸信息。比如,当图像有色度-亮度表示时,亮度样本通常会高于色度样本。

  • B. 参考网格

       给定一个图像,编码器把不同分量的几何图形按表示在一个矩形的网格上,这个网格称为参考网格。参考网格一般如图2所示。网格的大小Xsiz*Ysiz,原点在左上角。以左上角为原点,从(XOsiz,YOsiz)到右下角(Xaix-1,Ysiz-1)是图像区域,即相应的照片区域在这个区域表示。参考网格的大小并不需要扩展到232-1。232-1是编码器所能支持的最大长宽。

3 

       所有的分量都将映射到参考网格的图像区域上。分量并不是在整个参考网格上采样表示,因此需要附加信息来建立这个映射。对于每个分量,各自的水平和垂直采样周期定义为XRsiz和YRsiz 。这两个参数表示在水平和垂直的方向上按XRsiz和YRsiz的倍数来对网格的像素点进行采样。所有落在图像区域的这些像素点组成了我们所说的分量。因此,在我们的坐标系统内,一个分量的大小为:e1 ,而这个分量的左上角原点为e2 。注意,参考网格对于样本对于数据样本的对其因分量的不用而不用。

       在图二中,图像区域的大小是(Xsiz-XOsiz)*(Ysiz-YOsiz)。对于一个给定的图像,Xsiz、Ysiz、XOsiz和YOsiz是可以根据源图像数据的大小自己选取的。因此,有些人就奇怪为什么当Xsiz和Ysiz设置成源图像尺寸时,XOsiz和YOsiz却不是0。事实上,正是用这些巧妙的方法来改变XOsiz和YOsiz参数(在保证图像尺寸大小固定的情况下)。这些改变能改变编码器的运作,将在下面有介绍。这些改变的行为能对图像提供有效的基本操作,比如剪裁、水平、垂直翻转或者一个以90度为倍数的旋转。

  • C.切片

       在一些情况下,分配给编码器的内存对于某些大的图像时显得不够用。在这种情况下,是不能够在使用单一的编码器来编码整幅图像的。为了解决这个问题,编码器允许图像被切成几个小片,每一个小片独立编码。用术语说,就是把一个图像分成相互独立的矩形区域,称为切片。如图3所示,这些切片的格子覆盖在参考网格的上面,每个切片长宽分别为XTsiz和YTsiz。这些切片格子的原点是(XTOsiz,YTOsiz)。切片的大小为XTsiz*YTsiz。各切片的标号由光栅扫描顺序来标记。

4 

       通过把切片的位置从参考网格映射到每个独立分量的独立坐标系统中,就得到了每个分量的各自分切片。比如说,假定一个切片的左上角坐标和右下角的坐标分别为(tx0,ty0)和(tx1-1,ty1-1)。在特定分量的坐标系中,这个切片的左上和右下坐标分别为(tcx0,txy0)。那么它们的关系:

e3 

上面等式对应着图4。对于一个给定分量的切片,称为切片-分量。尽管切片网格根据参考网格的大小来排列,我们必须意识到切片网格不需要根据分量的坐标系来重排列。

  • D.编码器结构

       编码器的总体结构如图5所示。其中图5(a)是编码器,图5(b)是解码器。我们看到,编码器的主要过程包括,1)图像预处理/后处理,2)多分量变换,3)分量内部变换,4)量化/反量化,5)tier1编码,6)tier2编码,7)率控制。解码器的结构其实跟编码器的结构是刚好相反。因此,除率控制之外,都有一个一一对应的编码和解码的功能模块。在解码器中,功能块要么是编码器效果的绝对反效果,要么是大致上的反效果。由于切片是一个接一个地独立编码的,输入图像一次过只能处理一个切片。OK,以下部分将对数据处理进行更详细的介绍。

  • E. 预处理/后处理

       编码器假定输入数据有一个标准的动态范围,并且以0为中心。这些预处理过程就是让这个假设能够实现。假设一个特定分量是P位/每样本,每个样本或者是有符号数,或者是无符号数。因此,输出动态范围是[-2P-1,2P-1-1]或者[0,2P-1]。如果样本是无符号的,动态范围将肯定不是以0为中心的。因此,必须把动态范围调整成标准动态范围,就是用2P-1去减样本值。如果样本值是有符号的,并且以0为中心,则不需要处理。为了保证标准动态范围是以0为中心的,我们可以对编码器做一些简单的假设(比如,在上下文模块中标记溢出位等)。

 

 5

 6

       解码器的后处理过程就是把编码器预处理过程的效果消除。如果样本是无符号的,将会恢复原始样本的动态范围。最后,在有损编码中,将会对数据位进行剪裁来保证样本值不会超出允许的动态范围。

 

  • F. 分量间变换

       对于编码来说,预处理后跟着就是前向分量变换。一个分量变换是对切片-分量进行处理的。这样一个变换是在所有的分量同时进行,目的是减少分量间的相关性,以提高编码效率。

       在JPEG2000编码器中,只提供了两种分量变换:不可逆颜色变换(ICT)和可逆颜色变换(RCT)。ICT是不可逆的,因此是实数-实数的。RCT是可逆的,因此是整数-整数的。两种变换方式都是把图像映射从RGB映射到YCrCb中。这两个变换定义为在一个图像的前三个分量之间进行,假设这三个分量为0,1,2,分别对应红、率、蓝三种颜色空间。由于分量变化自身的特点,将要处理的3个分量必须是被采样成相同的分辨率(即相同大小)。总结上面所述,ICT和RCT仅仅在那些图像有至少3个分量,并且这三个分量必须是有相同分辨率的情况下才使用的。ICT仅在有损压缩下使用。RCT可在有损或者无损下使用。即使在分量变换能使用的情况下,也有不使用的情况。也就是说使不使用分量变换是编码器来判断的。来自各个分量的数据将被独立处理。

       ICT就是典型的RGB到YCrCb颜色空间转换。前向转换定义为:

7 

U0(x,y)、U1(x,y)、U2(x,y)分别代表红、绿、蓝三种颜色。V0(x,y)、V1(x,y)、V2(x,y)分别代表Y、Cr和Cb三种。反向变换如下所示:

 8

       对于ICT来说,RCT就是一个可逆的整数-整数的类似过程[26]:

9 

       U0(x,y)、U1(x,y)、U2(x,y)和V0(x,y)、V1(x,y)、V2(x,y)如上面所定义。反向变换如下所示:

10 

       在解码器端的反向分量变换仅仅是解除前向分量变换的效果。如果编码器使用了多分量变换,则反向变换也必须使用。然而,除非变换是可逆的,否则反向变换仅仅是恢复样本到有限的精度。

 

  • G.分量内变换

       分量变换后紧跟着是分量内变换。在这一个过程中,变换是在分量内部进行的。具体来说,分量内变换就是小波变换。通过小波变换,一个分量将分成多个频带(即子带)。由于这些子带样本具有统计特性,变换后的数据在编码时一般具有更高的效率。

       在编码器中,支持可逆的整数-整数和不可逆的实数-实数小波变换。小波变换的基础模块是1维两通道的完全重构(PR)的均匀极大采样(UMD)的滤波器组(FB),如图6所示。在这里,我们将重点集中在UMDFB[32,33]的提升实现上。这个提升实现能够被编码器应用在可逆的整数-整数和不可逆的实数-实数小波变换。正是因为这样,非常多的其他编码器都使用这提升策略。UMDFB的分析步骤,如图6(a)所示,就是前向小波变换。而综合步骤,如图6(b)所示,是反向小波变换。在以上的图中,Ai(z)、Qi(z)和si分别是滤波器函数、量化因子和(标量)增益。为了得到整数-整数映射,Qi(z)的选择将能产生整数输出的量,si选择整数。对于实数-实数的变换,Qi(z)是不变的,而si也从实数中选择。为了让滤波器能在输入信号的边界中也能实现,必须对原信号进行对称延拓[34,35]。由于我们的图像是2维的,我们需要个2维的UMDFB。其实只要把1维的UMDFB分别应用在水平和垂直方向,就组成了2维的UMDFB。通过使用2维的UMDFB,小波变换将递归地对每个变换级数中的低通信号进行处理。

11 

       假设我们使用(R-1)次小波变换。为了计算前向小波变换,我们把分析端的2维UMDFB以交织的方式应用到切片-分量数据中,从而产生一系列的子带信号。应用1次分析端的2维UMDFB将产生4个子带:1)水平和垂直方向都低通(LL)。2)水平低通、垂直高通子带(LH)。3)水平高通,垂直低通子带(HL)。4)水平和垂直都高通的子带(HH)。(R-1)次小波分解将产生R个分辨率级,标号从0到R-1,分别代表从最粗糙到最高的分辨率。每次分解出来的子带都会由方向(LL、LH、HL、HH)和相应的分辨率等级(0到R-1)来标记。原始输入的切片-分量数据被认为是LLR-1子带。在每个分辨率等级中(除去最低的),LL子带都将会被进一步分解。比如,LLR-1子带被分解将产生LLR-2子带、LHR-2子带HLR-2子带HHR-2子带。在下一次分解中,将使用LLR-2子带,如此继续执行。这个处理过程将重复执行到LL0子带得到为止。子带结构如图7所示。在退化模式(degenerate case),没有小波变换,即R=1,只有LL0一个子带。

12 

       正如上面所说的,小波分解是跟R个不同的分辨率等级联系在一起的。假设一个切片-分量的左上和右下坐标是(tcx0,tcy0)到(tcx1-1,tcy1-1)。在分辨率r中,切片-分量的坐标为(trx0,try0)和(tcx1-1,tcy1-1)。那么它们之间的关系是:

13 

       这里的r是特定的分辨率等级。因此,切片-分量在给定分辨率等级下的大小是:(tcx1- trx0)*( tcy1- try0)。

       分辨率等级的坐标非常重要,同时各子带的坐标也是非常主要。假设我们给定子带的坐标是(tbx0,tby0)和(tbx1-1,tby1-1)。那么计算公式如下:

14 

       其中的r即子带所属的分辨率等级,R是分辨率总级数,tcx0、tcy0、tcx1、tcy1如上面的定义。因此,一个给定的子带大小是(tbx1- tbx0)*( tby1- tby0)。从上面同样可以看出,在LLr子带和下一个分辨率等级,(tbx0,tby0)=(trx0,try0)和(tbx1,tby1)=(tbx1,tby1)。这很容易理解,因为LLr子带即下一个分辨率等级的原始数据。我们将会看到,各个分辨率和子带的坐标对于编码器有重要的意义。

       通过(1)、(6)、(7)式,我们可以看到特定子带左上坐标的样本(tbx0,tby0),由参考网格的XOsiz和YO所决定。在分解的每个级数中,(tbx0,tby0)的极性(即奇数还是偶数),影响着下采样处理过程的输出(因为下采样输出是可变的)。因此,XOsiz和YOsiz在小波变换的计算中有着巧妙、重要的作用。

       在介绍完传统的小波变换框架后,我们现在要介绍特定的两个小波滤波器:5/3和9/7小波变换。5/3小波变换是可逆的,整数-整数和非线性的。详细推导在[24]中,在[36]中显示如何简单地编程线性的小波变换。5/3小波变换的一维UMDFB滤波器的参数如下:

15 

       9/7小波变换是不可逆的实数-实数的变换。在[20]中有推导过程。在FBI的指纹压缩标准中[37]同样是使用9/7小波变换(尽管归一化参数不一样)。9/7小波变换使用的一维UMDFB滤波器有如下参数:

16 

       因为5/3小波变换是可逆的,因此可在无损和有损压缩中使用。而9/7小波变换由于缺乏了可逆的属性,只能在有损压缩中使用。小波变换的典型参数是6(在图像足够大的情况下)。编码器可以对所有的分量进行小波变换,也可以都不进行,或者只进行一部分。这些都由编码器来决定。

       反向的分量内部变换就是消除正向分量内部变换的效果。对一个给定分量在编码中进行了小波变换,那么在解码时也必须有相应的反变换。由于算法精度的有限性,反向处理不能保证原始数据能被精确恢复回来。当然,可逆小波变换除外。

 

  • H.量化/反量化

       切片-分量的数据在经过分量间和分量内的变换之后,就必须对余下的数据进行量化。通过把系数限制在一个比较小的精度范围和想要的图像质量,量化能够极大地压缩数据。量化是造成图像质量下降的两个主要原因之一(另外一个是丢弃编码通道的数据)。

       系数将使用带有死区的标量量化器进行量化。不用的子带将采用不同的量化步长。在数学上,量化的处理如下:

17 

       △就是量化步长,U(x,y)是输入的子带样本,V(x,y)是量化器输出的子带样本。因为量化的使用非常广泛,编码器并不需要严格执行这条公式。然而,大部分的编码器都使用上述的公式。

    基础编码器对于量化有两个不同的操作,分别对应于整数模式和实数模式。在整数模式中,所有的变换本质上都是整数-整数的(RCT、5/3WT),在实数模式,所有变换都是实数-实数的(ICT、9/7WT)。在整数模式中,量化步长将固定为1,因此不需要量化。在这种情况下,有损压缩仍然能够进行,不过率分配将在另一个地方执行(以后将会介绍)。在实数模式(意味着有损编码),量化步长将由率分配来控制。量化步长的选择有多种方案,将在第三章-L中介绍。

    正如我们想象那样,编码器使用的量化步长将通过码流提供给解码器。在编码进行中,我们必须注意到量化步长的值是相对的并且不是固定值。也就是说,我们的量化步长对于每个子带的不同的标准动态范围都是不同的。

    在解码端,反量化的任务是解除量化带来的效果。除非所有量化步长都小于或者等于1,量化的过程都会造成信息的损失。因此反量化仅仅是近似的。量化后的系数能由量化索引号的倍数得到。数学上表示,反量化的处理过程如下面所示:

18 

    △是量化步长,r是一个偏差参数。V(x,y)是作为特定子带的量化索引值的输入,U(x,y)是重构后的子带信号。尽管r的大小在标准中没有给出,大部分的解码器都是使用1/2。

I.Tier-1编码

    量化后,将要进行的是tier-1编码。这是两个编码步骤之一。我们把子带的量化索引号(也就是数据)分区成一个个代码块。代码块是矩形的,并且他们的大小参数有几个限制,主要是:1)代码块的大小必须是2的指数值。2)代码块的样本不能超过4096个。

    假设标称的代码块大小是2xcb*2ycb。在将要被讨论的tier-2编码中,代码块被打包,打包后称为区。因为代码块是不允许超过区的边界的,所以在区的尺寸比较小时,代码块有可能被缩小。在经过这个校准后,假设我们的代码块大小为2xcb'*2ycb',如图8所示。注意这里校准后的代码块是小于或者等于原来标称的代码块大小。我们把子带分成各个覆盖在子带上互不重叠的代码块网格,每个代码块网格的水平和垂直大小分别是2xcb'和2ycb'。如图8,这个网格的原点是子带所在坐标系统的原点(0,0)。代码块的典型大小是64*64(即xcb=6,ycb=6)。

19 

    OK,让我们重新在关注子带的左上角样本(tbx0,tby0)。正如第三章-G所示,(tbx0,tby0)部分地有参考网格的XOsiz和YOsiz参数决定。而(tbx0,tby0)影响到子带所在的代码块边界。因此,XOsiz和YOsiz将对tier-1的处理过程有主要的影响作用(即影响了代码块的边界)。

       在子带被分成代码块后,每一个代码块都进行独立编码。这个编码使用一种叫做位平面编码的技术,在第三章-J中有介绍。对于每个代码块,都产生了嵌入式码流,这些码流由不同的编码通道产生。

       在解码端,位平面编码后的编码通道被输入到tier-1解码器,因此这些编码后的数据就被解码,从而得到数据后装载回子带。也就是说,我们得到了重构的量化索引号。在有损编码中,重构的量化索引仅仅是原来的近似重构。这是因为代码流只是包含了一部分的tie-1后的编码数据。在无损压缩中,重构的编码索引后跟原先的一致,因为所有的编码通道数据都没有被丢弃。

 

  • I. 位平面编码

       Tier1编码的核心之一是位平面编码。在所有的子带都被分成代码块后,每一个代码块都独立进行位平面编码。尽管位平面编码使用的方法类似于嵌入式零树小波编码(EZW)[38]和分区等级树编码(SPIHT)[39],但有两个重要的特点:1)位平面编码没有利用子带间的相关性。2)位平面编码对于每个位平面有三个编码通道,而不是两个。第一个特点,是因为考虑到每一个代码块是完全被一个子带包含的,因此代码块跟其他子带的代码块保持独立。由于没有利用子带间的相关性,我们有效提高了程序的容错能力。第二个特点并不是很大的区别。但使用3个编码通道而不是2个,能有效减少每个编码通道的数据量,从而能够更方便地进行率控制。同样,增加了编码通道的位平面编码能重要数据得到优先编码,从而提高编码效率。

       如上面所示,在每个位平面中有三个编码通道。这三个编码通道是:1)重要性传播通道。2)幅度细化通道。3)清除通道。这三个编码通道对于样本的扫描顺序都如图10那样。代码块被分成水平的多条带,每一条带在垂直方向上有4个样本。如果代码块的高不是4的倍数的话,在最下面的带的长度就会少于4。如图10,带的扫描从顶到底,在一个带内部,样本的扫描是从上到下。

20 

       位平面编码在每一个编码通道中都会产生一系列的标记。这些标记的一部分或者所有都将进去下一步的熵编码。为了进行熵编码,我们采用一种基于上下文的自适应的二进制算术编码--更专业地说,就是JBIG2标准[28]里面提到的MQ编码器。在每个编码通道中,每个编码出来的标记要么是待算术编码的输入要么是原始数据(即这些二进制标记是通过位填充后的原始数据)。算术编码和原始数据的编码输出都要保证在输出中不能出现单一的位(即要填充成字节),这主要是为了提高容错性。

       清除通道出来的数据通常都需要经过算术编码。而在重要性传播通道和幅度细化通道,则有两种可能,这取决于一种叫算术编码旁路模式(也叫Lazy模式)是否开启。如果Lazy模式开启了,我们最高的4个位平面的重要性传播通道和幅度细化通道的标记才进入算术编码,其他的标记以原始数据编码。否则,所有的重要性传播和幅度细化通道标记均要进行算术编码。Lazy模式通过减少进入算术编码的数据减少,能让位平面编码的计算量大幅度降低。当然,这会牺牲编码效率,即数据压缩率。

       如上面所示,编码通道数据能被两种方案之一来处理(即算术编码或原始数据编码)。使用这两种方案之一来进行连续的编码,就产生了数据段。在一个数据段中的所有编码通道的数据能被同一个代码字来标记,或者每一个编码通道都使用一个独立的代码字标记。至于使用这两种模式的哪一种,则取决于我们的终止模式。我们的编码器采用两种终止模式:每通道终止和每数据段终止。在第二种模式,只有数据段的最后一个通道才进行终止操作。而第一种模式,所有的编码通道都会进行终止操作。对所有编码通道进行终止操作能有效提高代码流的容错能力,当然,要牺牲编码效率。

       由于使用了基于上下文的算术编码,我们就要决定什么是上下文。通常来说,上下文就是一个样本的4个或8个邻居的状态信息。如图9所示。

21 

       在我们以下对位平面编码的介绍中,将会集中讨论编码,因为这更容易理解。解码的算法会在编码后直接给出来。

 

J.1 重要性传播通道

       对于每个代码块的第一个位平面是重要性传播通道。这个通道是处理那些样本的重要性信息和符号信息(如有必要),这些样本必须是还没有发现是重要的并且被预测为有可能变成重要。这些样本的扫描过程如图10所示。如果一个样本还没有被发现是重要的,并被预测为有可能是重要的,那么这个样本的重要性就在这个通道中编码成一个二进制标号。如果这个样本的确是重要的,紧接着要对它的符号进行编码,同样编码成一个二进制标号。重要性通道的编码如算法1的伪代码所示。

22 

       在最上面的位平面编码时,所有的样本都被预测成不重要的。除去最上面的位平面,被预测的条件是那个样本的8个邻居至少有一个被发现是重要的。由这种预测的方案可以得出,对于最重要位平面(即最上面那个位平面)的重要性传播通道和幅度细化通道的输出都是0(不需要编码)。

       通过重要性传播通道标记出来的标号或者通过算术编码,或者不通过算术编码。如果要进行算术编码,这个样本的二进制标号随样本的重要性信息是使用9个上下文标号之一。特定上下文标号的选择基于这个样本的8个邻居的重要性信息和该样本所在子带的方向(即LL、LH、HL或者HH)。如果使用了算术编码,样本的符号要编码成真是符号和预测符号的差异值。否则,符号直接进行编码。符号预测时要使用到该样本的4个邻居的符号信息。

 

J.2 幅度细化通道

       第二个通道是幅度细化通道。原始代码块位平面的每一个位经过重要性通道后,剩下的进过幅度细化通道。代码块的扫描顺序同样如图10所示。如果一个样本在之前的重要性传播通道中被发现是重要的,那么该样本的第二位将在这里被编码成一个二进制标号。这个算法的伪指令如算法2所示。

23 

       跟重要性通道一样,幅度细化通道的数据或者要经过算术编码,或者不经过。如果使用了算术编码,每个幅度细化的标号是三种上下文之一。上下文的选择基于我们样本是否已经被细化过了,还有基于8个邻居第的重要性状态。

 

J.3 清除通道

       第三个(即最后一个)编码通道是通道。这个通道是处理那些还没有被发现是重要的样本和预测到要保持不重要的样本的重要性信息和符号信息(如有必要)。

       我们从概念上看,清除通道似乎和重要性通道没什么不同。最主要的不同是清除通道包含的信息是那个被预测成不重要的样本的,而不是像重要性传播通道那样,包含被预测成重要的样本信息。在算法上,也有一个很重要的区别。在清除过程中,有时候会几个样本一齐处理,而不是像重要性传播通道那样一个接着一个来处理。

       OK,让我们再来回忆下代码块中样本的特点,如图10所示。一个代码块被分成很多高为4个样本的带。然后,这些带由上到下逐个扫描,而这些带的列阵是从左向右扫描。为了方便起见,我们把有一个带的列看成垂直扫描。也就是说,每一个垂直矩阵都看成是一个垂直扫描。同时也将会看到,我们的清除通道就是一个垂直扫描的最佳例子(不仅仅是一个一个样本来扫描)。

       清除通道按顺序每次处理一个垂直扫描列,直至样本处理完毕。如果垂直扫描的列中有4个样本通过清除通道(即全部样本),我们就要知道4个样本的所有重要性信息,而且如果4个样本都被预测为不重要的话,我们就要进入一个特殊的模式:集合模式(aggregation mode,就是行程编码)。在这个模式里,要记录在垂直列上的最前面几个不重要样本的个数。因此,在这个垂直列上的重要性信息在这个模式上将不被记录,列中剩下的样本将像重要性传播过程那样来编码。用伪指令来表示这个过程,如算法3所示。

24 

       详细地说,当进入集合模式时,一个垂直列的四个样本被一次扫描。如果四个样本都是不重要的,则四个不重要的样本将一齐编码,这个列的扫描就算是完成了。否则,我们将要对那个重要的样本进行编码,具体地就是用两个位的标号来编码这个扫描列中的头几个不重要的样本的数量。

       这些从清除通道编码出来的标号通常要进入算术编码。在集合模式中,集合标号只有一个单独的上下文,而两位的游程长度的标记都使用同一个固定概率的上下文。这个上下文称为全局上下文。如果没有进入集合模式,重要性编码和符号编码跟重要性传播通道的编码规律一样。

 

  • J. Tier-2 编码

       在编码器中,Tier-1编码完毕紧接着的是Tier-2编码。Tier-2编码的输入是在Tier-1的位平面中各编码通道所输出的数据的集合。在Tier-2中,编码通道的数据将被打包成数据包,这个处理过程称为打包。这些数据包随后输出成最终的代码流。打包其实就是给各编码通道的数据给予一个特定的部分。这个外加的部分能使我们实现各种功能,如率伸展或者分辨率或者精度的渐进恢复。

       一个数据包只不过是一些编码通道数据的集合。每个包都由两部分组成:包头和包体。包头指示了在这个包中包含了哪几个编码通道,而包体就包含了编码通道实际上的数据。在代码流中,包头和包体可以一齐出现,也可以分开出现,这主要是有编码参数来决定的。

       码率的伸展是通过(质量)层来实现。每个切片的编码后数据都被分成L层,这些层的标号从0到L-1。每个编码通道的数据要么被分配到这L层的其中一层,要不就被直接丢弃。那些包含了最重要信息的编码通道数据被放在低层中,而那些包含了更多细节的编码通道数据被放在更高的层中。通过解码,重构出来的图像质量通过每一个层数据的叠加个提升。在有损压缩中,我们会丢弃一些编码通道的数据(即不包含在层内),这是有率控制机制来控制的。在无损压缩中,所有的编码通道都必须包含在层中。如果我们使用了多个层(即L>1),率控制机制就必须决定哪些层包含哪些通道数据。因为有些通道的数据可能被丢弃,Tier-2编码就称为了造成信息损失的第二个原因。

       OK,我们再回忆下第三章-I,每个编码通道即在特定的分量、分辨率、子带和代码块下的。在Tier-2编码中,单一的包其实就是特定分量、分辨率、层、区组成的4维数组。一个包并不需要包含所有的编码通道数据。也就是说,一个包有可能是空包。有时候,空包的出现是必要的,因为一个包是由特定分量-分辨率-层-区组成,如果这些剩下的包没有包含新信息的话,就会出现空包。

       如第三章-G提到的那样,区对于一个子带的代码块的打包是非常重要的。区把特定子带分成了各个区域,是从该子带的上面的LL子带的分区中继承下来(即比该子带高一个分辨率的那个LL子带)。每一个分辨率都可以由一个标称的区的尺寸。区的尺寸必须是2的指数,同样有一些其他限制(最大是215)。对于每个分辨率LL子带会分区。其实就是通过2ppx和2ppy大小的区去分割LL子带,把区组成的标准网格覆盖在LL子带上。如图11所示,这个网格跟LL子带系统的原点对齐。在子带的边界处的区有可能比标称尺寸要小。然后,每一个得到的区将被映射到下一级分辨率的子带(如果有的话)中。这个映射是通过一个坐标转换来实现的:(u,v)=([x/2],[y/2])。(x,y)和(u,v)分别是LL子带和下一级子带的坐标。有了区的分割后,区边界通常跟代码块的边界对齐起来。因此一些区可能是空的。如果标称的代码块大小是2xcb'*2ycb',那么在一个区里面将有2PPx'-xcb'*2PPy'-ycb'个代码块。PPx'和PPy'的大小如下:

PPx'=PPx(r=0)或者PPx'=PPx-1(r>0)

PPy'=PPy(r=0)或者PPy'=PPy-1(r>0)

25 

       由于在不同的区中的编码通道的数据被编码成不用的数据包,使用小尺寸的区会降低每个包的数据量。包中的数据越少,由于比特位错误引起的信息损失就会降低(因为在一定程度上,一个包内比特位的错误并不会引起待解码的其他数据包)。因此,使用更小的区能提高容错性。然而,编程效率会降低,因为需要更多的系统开销来应付更多的包。

       码流支持超过多种的包排列顺序。这些顺序叫做渐进顺序。标准总共定义了5种渐进顺序:1)层-分辨率-分量-位置。2)分辨率-层-分量-位置。3)分辨率-位置-分量-层。4)位置-分量-分辨率-层。5)分量-位置-分辨率-层。关于这些渐进顺序由它们的名字进行区分,位置指的是区的序号,而排序的重点是先列重要的数据,再排不重要的数据。比如,在上述提到的第一种顺序中,包的顺序由层、分辨率、分量、区依次排列。这就等于一个精度的渐进恢复传输。在第二种顺序中,相对于分辨率的渐进传输。第三种要抽象些。用户也可以自己去指定渐进方式,但要增加编码开销。

       在最简单的情况下,在一个特定分量的所有包一个接一个出现在码流中。然而,标准规定允许对于不同切片的交织数据包从而允许更灵活地组织数据。比如,如果我们希望一个切片的数据,在第一层的所有切片都包含了包含了所有的数据包,接着是第二层,等等。

       在解码端,tier-2解码就是把码流中不同的编码通道数据提出出来(即解包),同时把不同的通道数据放回到相应的代码块中。在有损模式,解码器并不能保证所有的通道数据都被解压出来,因为有些通道已经被丢弃了。在无损模式下,则所有的数据都会被解除出来。

       以下,我们将更详细地介绍数据的打包过程。我们以编码器的角度来解释这些过程。解码的算法,能够由编码的算法简单地推导出来。

 

K.1 包头编码

       对应于特定分量、分辨率、层、区的包头如以下编码。首先,一个二进制位编码来指示这个包里面是否包含了编码通道(即包是否空包)。如果包为空,不会有进一步处理,算法终止。否则,我们将以一个给定的顺序来扫描分辨率下的子带。对于每个子带,我们以图12的方式来进行光栅扫描,检查感兴趣的区中的代码块。为了处理某一个代码块,我们要判断这个代码块是否包换了编码通道。如果没有通道数据,则包含信息以类似四叉树的编码方式来编码。否则,一个二进制来指示当前代码块是否有新的编码通道。如果没有新的编码通道,我们就继续处理区中的下一个代码块。如果有了新的编码通道,我们就继续处理当前代码块。如果这是当前代码块第一次包含了编码通道,我们就要对代码块的头几个不重要的位平面个数进行基于四叉数的编码。然后,新的通道数目和这些通道数据的长度都将被编码。为了保证输出是不会出现有比特剩余现象,必须对数据包头进行位填充,这是为了抗误码的需要。包头编码算法如算法4所示。

26 

27 

 

 

K.2 包体编码

       包体编码的算法相对简单。代码块的访问顺序如包头那样。如有包头中有了新的编码通道,这些编码通道的数据将被连接包体中。过程如算法5所示:

28 

 

  • K. 率控制

       在编码器中,率控制通过两种方式来实现:1)选择量化步长。2)在码流中对编码通道选择一些集合。当使用整数编码模式时(如整数-整数的变换),因为量化步长固定为1,只有使用后面一种方法。当实数模式被使用时,或者两种方法都使用,或者只使用其中一种。

       当使用第一种方法时,量化步长会根据率控制来调整。随着量化步长的增加,码率也相对降低,因此造成了相当的失真。尽管这样的率控制机制在概念上比较简单,但也有一个潜在的缺点。每一次量化步长改变时,量化索引都将改变,所以tier-1编码就必须重做。而tier-1编码需要大量的运算量,这种率控制的方法在那些受到运算能力限制的编码器中不那么实际。

       当使用第二种方法时,编码器则根据率控制来丢弃一些编码通道。编码器能计算出每个通道对于码率的贡献,同时能计算出每个通道相关的失真率。使用这些信息,编码器能选择那些能降低失真的通道,直到数据量达到预先要求。这种方法比较灵活,在不用的失真尺度要求下都能轻松使用(比如,平方均值误差、加权均值平方误差等)。

       对于率控制想要有更深的了解,读者可以参考[7]和[21]。

 

  • L. 感兴趣区域编码

       编码器允许一个图像的不同区域以不同的精度编码。这个属性就是ROI(region-of-interest)编码。为了实现ROI编码,以下将要介绍一种比较简单和灵活的方法。

       当把一些系数综合成一副图像时,每一个系数都是对于一个特定区域的重构做贡献。因此,一个ROI编码的实现方法是找出这些ROI区域的系数,同时在解码这些系数时让这些系数比其他系数具有更高精度。事实上,这些关于ROI的基本假设都被JPEG-2000编码器所使用了。

       当一个图像被编码成ROI时,有一些系数就被认为比其他系数更重要。这些更重要的系数叫做ROI系数,而其余的称为背景系数。必须注意到,在量化索引号和变换系数间有一一对应的关系。我们进一步定义关于ROI的量化索引和背景系数的量化索引,还有ROI量化步长和背景系数的量化步长。好了,在定义完这些术语后,我们现在可以介绍ROI编码是如何应用在编码框架上的了。

       ROI编码主要是影响tier-1编码。在编码端,在量化索引进入不同的子带的位平面编码时,ROI量化索引将被放大2的指数倍(即左移)。放大是为了保证ROI索引的所有比特位都比背景索引的有可能是非零的样本更重要。结果,所有关于ROI量化索引的信息都先于背景索引被编码记录下来。通过这种方法,ROI在重构时就会比背景拥有更高的精度。

       在对量化索引进行位平面编码时,编码器会遍历所有子带的所有背景量化索引,以寻找这些索引的最大值。假设最大值的比特面是N-1。所有的ROI索引同时左移了N位,接着的位平面编码过程就像没有ROI那样的执行。ROI的偏移量N会被记录在码流中。

       通过解码,在位平面N或者以上的非零的量化索引会收集到ROI集中。随着位平面解码和重构量化步长的执行,所有的ROI索引值都会被右移回N个比特。这就是消除了编码时做得的影响。

       ROI集可以是一个图像特定区域的变换系数或者是影响这些区域的子集。ROI编码技术有很多吸引的特性。首先,ROI区域可以被随意裁剪和拆分。其次,并不需要对ROI集内的样本特殊标记,因为在解码时可以通过ROI偏移量和量化索引的幅值来得到。

       更多关于ROI的信息,读者可以参考[40,41]。

 

N.代码流

       为了对图像的编码后数据做一个规范,标准将采用两个级别的语法。最低级的语法结构被称为代码流。代码流一系列的标记和伴随数据所组成的。

 29

       代码流的基本结构是标记段。如图13所示,一个标记段有三个部分组成:类型、长度和参数域。类型(或者叫标记)域指定了该标记段的类型。长度域指定了该标记段的字节长度。参数域提供了标记段的附加信息。注意并不是所有的标记段都由长度和参数域。这些域的存在与否有参数类型决定。每个标记段指定了一种特定信息。

       一个代码流就是一系列的标记段和附加数据组成(即包),如图14所示。代码流包括了主文件头、切片文件头、切片主体、主尾部记录。一些重要的标记段如表II所示。在主文件头的标记段中参数作为整个码流的默认参数。然而,这些默认设置,可以在特定的切片头里指定新的参数所覆盖。

       所有的标记段、包头、包体都是以8位为倍数的长度。因此,所有标记段都是字节对齐的,码流本身也是字节的整数倍。

30 

 31

O.文件格式

       一个代码流提供了解码图像时所需要的最基本信息(即有足够的信息来推断出这些样本值)。当一些简单的应用中,这些信息已经是足够了,但一些额外的应用程序则需要一些额外的数据。比如,为了正确显示一个解码的图像,通常需要知道图像的附加特征,如图像的颜色空间和透明度属性。同样,在某些情况下,我们需要知道图像的其他信息(如版权、产地等)。为了以上的数据能被显示出来,编码器需要一个额外的语法结构。这个语法结构就是文件格式。文件格式通常包含了图像数据和关于图像的附加信息。尽管这个文件格式是可选的,但毫无疑问它会被广泛应用到很多程序中,尤其是基于计算机的应用程序。

32 

       文件格式的基本模块被称为框。如图15所示,一个框通常包含了以下几部分:LBox,Tbox,XLbox和DBox。LBox是这个框的字节长度。TBox是框的类型(即框所包含信息的性质)。XLBox是一个延伸长度指示,在一个框的长度太大以至于LBox超出最大范围时,就要使用到XLBox。如果LBox为1,则XLBox存在并且包含了这个框的真实长度。否则XLBox不存在。DBox包含了该类型框所包含的具体数据。有一些框在其数据域可能包含了其他框。在术语上,一个在DBox包含了其他框的框被称为超级框。几种重要的框如表III所示。

33

       一个文件就是一系列的框组成。由于有一些类型的框包含了其他框,文件则形成了一个天然的等级结构。文件结构如图16所示。JPEG-2000的signature box首先出现,指示着这个文件能被正确的格式化。File type box通常紧跟随后,标记该字节流的文件格式。尽管余下的框在出现顺序上有一些限制,但还是有不少的灵活性。Header box包含了其他一些框。Image header box标记了图像的基本特性(尺寸、分量数等)。Bits per component box标记了分量的精度和符号。 Color specification box标记了分量的颜色空间(为了显示)和分量该映射到哪些空间(即各分量的对应关系和颜色/透明度平面)。每一个文件必须包含至少一个连续的code stream box(有时候为了增加一些简单的动画效果,可以采用多个代码流框)。每一个连续的代码流框作为一个码流作为框的数据。即图像数据被嵌入到文件中。除了上面所说的框的类型外,还有一些其他类型的框,比如指定图像获取和显示分辨率的框、指示调色板的框、知识产权框、厂家/应用程序特有的数据框。

 34

       尽管存储在文件格式级的一些信息是多余的(也同样在码流级中给出了),但这个多余性允许在没有代码流语法结构的时候对文件进行一些操作。这个文件格式名为"jp2",标记那些包含了JP2文件格式的数据。更多的文件格式信息,读者可以访问[42]。

 

P.拓展

       尽管基础编码器功能已经比较全面,一些应用能从额外的延拓中得到实现。最后,标准[15]的Part2定义了对基础编码器的几个延拓。一些延拓如下定义:1)独立区域,是一个能分区成随意形状和随意覆盖的切片。2)额外的分量变换(如多维小波/子带变换)。3)额外的分量内变换(如基于随意滤波器和分解树的子带变换,在水平和垂直方向使用不同的滤波器)。4)重叠处理。5)额外的量化方法,如trellis coded量化[43,44]。6)拓展的ROI支持(如随意形状和随意位移机制的ROI)。7)扩展的文件格式,用来支持额外的颜色空间和混合文档。

 

第四章 结论

       在这篇论文内,我们在比较高的层次上介绍了JPEG-2000标准,同时比较详细地介绍了JPEG-2000编码器。有着极其出色编码性能和众多引人注目的特点,JPEG-2000无疑会在数年间被广泛应用。

 

附录

    JasPer是一个关于图像编码和操作的软件集合(即由库和应用程序组成的软件)。这个软件由C语言便写成。值得注意的是,JasPer软件提供了JPEG-2000 Part-1编码器的实现。对于那些希望使用JPEG-2000标准的人来说,JasPer软件提供了一个免费的JPEG-2000编码器的实现。作为一个官方指定的JPEG-2000 Part-1编码器,这个软件同样被包含在JPEG-2000 Part-5标准中。JasPer软件可以在JasPer项目的主页(http://www.ece.uvic.ca/˜mdadams/jasper)下载。也可以在JPEG的网站下载(http://www.jpeg.org/software)。更多的信息请参考[8-10]。

鸣谢

    作者必须感谢Image Power公司,他们对Jasper的开发有强力的支持。同时,作者必须对Faouzi Kossentini博士表示衷心的感谢,因为Faouzi Kossentini博士以往的协作,作者才能继续研究开发JPEG-2000。(因此,写下了这篇文章)。

复制去Google翻译 翻译结果
 

转载于:https://www.cnblogs.com/Qianqian-Dong/p/4165880.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Changes from version 2.2.2 to version 2.2.3 ------------------------------------------- * Extremely minor changes to avoid mixed use of formatted and unformatted calls to "ostream" objects. These appear to excite a bug in GCC version 3.0. The only file affected is "params.cpp" in "coresys/parameters". Changes from version 2.2.1 to version 2.2.2 ------------------------------------------- Note: none of these have any impact whatsoever on executable code or DLL's. * Renamed the "core" directory as "coresys". A trivial change and my appologies for those whom this might adversely affect. However, the use of the name "core" was causing some people difficulties, since it is identical to the name of a Unix core dump file. * Made the Linux MMX-optimized functions "extern C" instead of "extern", so as to avoid problems caused by different name mangling conventions between various versions of gcc. * Eliminated multi-line comments from assembler files so as to avoid problems created by earlier versions of the gnu assembler. Changes from version 2.2 to version 2.2.1 ----------------------------------------- * Replaced the C++ I/O routines used for image and compressed data transfers with ANSI C I/O functions. This was motivated by the fact that the new-style ANSI C++ I/O package is unbelievably slow, at least as implemented in Microsoft's Visual C++ compiler. The change has no impact whatsoever on the Kakadu core system; it affects only the implementation of a few application level objects -- the core system abstracts all such I/O considerations through interface classes which are implemented by applications. Everything now runs a little faster than it did in version 2.1 and quite a bit faster than it did in the first release of version 2.2. * Made provision for compiling under versions of GCC prior to version 3.0. To use this, you should define GCC_VERSION_LESS_THAN_3. Changes from version 2.1 to version 2.2 --------------------------------------- * Extensive support for ROI (Region of Interest) specification at encode time (see "kakadu.pdf" for more on this). * Migrated from the old-style C++ iostream package to the new standard iostream package -- minimal use of "using namespace std" and never used in common header files, so this should enhance namespace protection and portability. * Added AT&T style versions of the small amount of Pentium assembly code to enable compilation of a high speed version under GCC as well as MSVC. * Some minor bug fixes. Changes from version 2.0.2 to version 2.1 ----------------------------------------- * Extensive support for working with the JP2 file format. The "kdu_show" application demonstrates the capabilities required of a conformant JP2 reader: palette mapping; interpolation of components to a common resolution; application of registration offsets in the CRG marker segment; and colour conversion to an appropriate rendering space (sRGB here). The "kdu_region_decompressor" object provides extensive support for general purpose interactive rendering applications, performing all of the above tasks in a platform independent manner. * It is now possible to directly control rate-distortion slope thresholds used in the construction of quality layers. This capability may also be used to significantly increase compression speed, if a suitable threshold is known, since the encoder then incrementally predicts the point at which there is no point in coding further coding passes. * A number of improvements to the "kdu_show" application, including the ability to arbitrarily zoom into images. * A number of minor bug fixes, including one important bug reported by Aaron Deever of Kodak, and a bug which occasionally manifested itself in the incremental rate prediction heuristic (reported by Jim Andrew of CISRA). * Improved documentation. Changes from version 2.0.1 to version 2.02 ------------------------------------------ * A PDF document (documentation.pdf) has been prepared to guide the new user into some of the more important aspects of the Kakadu system. The first draft is included here. * A very simple compression example and a very simple decompression example have been added to assist developers in familiarizing themselves with the Kakadu system -- the existing demo apps provide perhaps too much functionality to be quickly understood. * A full BIBO (Bounded Input Bounded Output) numerical analysis of the DWT and other processing steps is used to establish the best utilization of limited precision sample data representations. The new version should not be able to fall prey to numerical overflow or underflow difficulties under any circumstances (this could just have been possible with the last version). It also provides slightly higher accuracy. * The automatic heuristic for generating quality layer rate targets has been substantially improved. * A number of minor bugs/limitations were corrected, although these had not manifested themselves in any real examples. Changes from version 2.0 to version 2.01 ---------------------------------------- * One line change in each of "kdu_expand.cpp" and "kdu_compress.cpp" to correct a minor rare bug in these demo applications. * Minor changes in "kdu_show.cpp" to correct a rare bug and improve the user interface in regard to image rotation. * Four lines added to each of "encoder.cpp" and "decoder.cpp" to fix a minor memory leak.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值