在不牺牲质量的情况下压缩 RAW 图像:JPEG XR 简介





0/5 (0投票)
随着数码单反相机在消费市场的普及,许多用户选择使用相机制造商专有的RAW格式拍摄图像。使用JPEG XR压缩RAW图像的能力,可以在极低的图像质量损失下实现高压缩率。
RAW的替代方案
随着数码单反相机在消费市场的普及,许多用户选择使用相机制造商专有的RAW格式拍摄图像,以保留在拍摄为默认JPEG格式时通常会丢失的图像信息。保留尽可能多的信息对于大多数摄影师的工作流程至关重要,这样他们就可以在后处理过程中最大限度地减少质量损失。此外,HDR、移轴和其他摄影技术的兴起,使得在数字图像中保留更大的动态范围变得至关重要。
RAW格式的优势在于,相机拍摄RAW图像时具有比JPEG所能存储的更高的强度分辨率。许多相机现在采用的CCD的每种颜色(或每像素24位(BPP))的比特数都超过8位,但JPEG的使用会在图像被压缩之前丢弃图像强度数据,从而丢失更多信息。RAW格式包含相机采集到的所有强度信息,通常每种颜色有10到14位(或30BPP-42BPP),以及与图像相关的附加信息。
问题在于,RAW格式无法被大多数图像应用程序查看,并且比默认的JPEG格式需要更多的存储空间,这使得它们难以在网络或电子邮件上传输。
JPEG XR(源自微软的HDPhoto,微软最初将其命名为Windows Media Photo)是一种新的压缩ISO标准,支持各种图像类型的有损和无损压缩。这种新的文件格式为摄影师提供了一个绝佳的RAW文件替代方案,它保留了编辑所需的所有信息,同时提供了更高的图像压缩效率。JPEG XR也可以在许多图像查看器和编辑器中查看,包括Adobe Photoshop、Microsoft Windows照片查看器,因此就像标准JPEG图像一样,也可以在Windows Explorer中查看。
将RAW转换为JPEG XR
使用Accusoft Pegasus提供的PICTools成像SDK,可以轻松地将许多专有的相机RAW格式转换为压缩的JPEG XR。本文的其余部分将介绍如何创建一个简单的C++控制台应用程序,该应用程序读取RAW格式的图像文件,并使用PICTools相机RAW提取(CAMERARAWE)操作码从中提取48BPP RGB图像。然后,我们将使用PICTools HDPhoto/JPEG XR操作码(HDPHOTOP)将48BPP图像压缩为JPEG XR图像(包括相机/图像元数据),写入生成的文件,并使用Windows照片查看器查看它。让我们来看看代码。
RAW转JPEG XR 代码
PICTools操作码通过使用输入和输出内存缓冲区来提取和压缩图像,因此第一步是打开RAW文件,分配一块内存,并将文件读取到缓冲区中。然后将缓冲区传递给一个名为CameraRawExtract的函数,该函数将图像提取为48BPP RGB图像,并以有序的数据结构提取元数据。元数据被转换为Tiff/EXIF标签,并与图像一起传递给JPEGXRSave函数。该函数将图像压缩为JPEG XR格式(包括Tiff/EXIF数据),并返回压缩后的图像,然后将其写入新文件。
// Open the camera raw file
ifstream file(szFileName, ios::in|ios::binary|ios::ate);
if (file.is_open())
{
// Read the file into a buffer
DWORD dwInputLen = static_cast<DWORD>(file.tellg());
LPBYTE pbInput = new BYTE[dwInputLen];
file.seekg (0, ios::beg);
file.read (reinterpret_cast<char*>(pbInput), dwInputLen);
file.close();
// Extract a raw image from the file
PIC_PARM picParmJXR;
LPBYTE pRaw = NULL;
DWORD dwRawLen = 0;
if( CameraRawExtract(pbInput, dwInputLen, picParmJXR, pRaw, dwRawLen) )
{
// Relocate TIFF Tags
P2LIST TIFFTags;
if(CreateTIFFTags(picParmJXR, TIFFTags))
{
// Compress the raw image to JXR
LPBYTE pbJXR = NULL;
DWORD dwJXRLen = 0;
if(JPEGXRSave(picParmJXR, pRaw, dwRawLen, TIFFTags, pbJXR, dwJXRLen))
{
// Write the JXR to the output file
ofstream ofile (szOutputFileName, ios::out|ios::binary|ios::trunc);
ofile.write(reinterpret_cast<char*>(pbJXR), dwJXRLen);
ofile.close();
delete[] pbJXR;
}
P2LFree(reinterpret_cast<P2LIST*>(&TIFFTags));
}
P2LFree(reinterpret_cast<P2LIST*>(&picParmJXR.PIC2List));
delete[] pRaw;
}
delete[] pbInput;
}
现在我们有了全局概览,让我们看看在CameraRawExtract、CreateTiffTags和JPEGXRSave函数中发生的有趣工作。
相机RAW提取
一旦文件进入内存,它就被传递给CAMERARAWE操作码来提取图像。所有PICTools操作码都通过一个函数执行,该函数通过PIC_PARM结构进行配置。该结构包含有关要执行的操作、输入缓冲区、元数据缓冲区和输出缓冲区的信息。
BOOL CameraRawExtract(
LPBYTE pbInput, // RAW file buffer
DWORD dwInputLen, // RAW file buffer length
PIC_PARM& picParm, // Operation PIC_PARM Parameters
LPBYTE& pbRaw, // Output image buffer
DWORD& dwRawLen) // Output image buffer length
{
相机RAW操作的配置是通过首先初始化传递给函数的PIC_PARM结构。通过设置Op和ParmVerMinor成员来选择相机RAW提取操作。
// Initialize the operation
memset( &picParm, 0, sizeof(PIC_PARM) );
picParm.ParmSize = sizeof(PIC_PARM);
picParm.ParmVer = CURRENT_PARMVER;
picParm.Op = OP_CAMERARAWE;
picParm.ParmVerMinor = 2;
选择相机RAW操作的输出为RAW图像而不是位图图像,因为位图图像的最大分辨率为24BPP,而我们要超越这个限制。
// Raw supports a larger range of image geometries
picParm.Flags = F_Raw;
然后设置相机RAW成员(CRE)的区域标志,以指示我们希望每个通道使用两个字节,并且图像为RGB图像。最终输出将是48BPP RGB图像。
// Extract the image as 48 BPP (2 Bytes each of Blue, Green, and Red)
picParm.u.CRE.Region.Flags = RF_2Byte | RF_SwapRB;
生成的图像将是一个像素数组,每个像素由6个字节组成:每个红、蓝、绿数据2个字节,采用小端字节序。
Red1 | Red0 | Green1 | Green0 | Blue1 | Blue0 |
因此,对于平均像素尺寸为3000x2000像素的数码单反相机,这个缓冲区大约为36兆字节。
接下来,Get缓冲区被配置为读取传递到函数的RAW文件缓冲区。Get缓冲区是一个循环队列,可以在几种不同的配置中使用。Start和End成员指向输入缓冲区的开始和结束,Front和Rear成员指向要由操作读取的缓冲区中的位置。在这种情况下,它将作为一个包含整个输入数据的单个缓冲区运行,但它可以配置为分块读取文件的一部分,以减少内存开销。Q_EOF标志指示所有图像数据都在缓冲区中,因此操作不需要请求更多数据。
// Set up the Get (input) buffer
picParm.Get.Start = pbInput;
picParm.Get.End = pbInput + dwInputLen;
picParm.Get.Front = picParm.Get.Start;
picParm.Get.Rear = picParm.Get.End;
picParm.Get.QFlags = Q_EOF;
操作本身是一个三部分的过程:初始化、执行和终止。在初始化阶段,操作码解析RAW数据以识别相机,确定输出图像大小并提取任何图像元数据。
// Call REQ_INIT
if ( !PicReq(picParm, REQ_INIT) )
{
P2LFree( reinterpret_cast<P2LIST*>(&picParm.PIC2List) );
return ( FALSE );
}
在初始化阶段,相机RAW操作码会将CRE.Region成员填充为图像几何信息。一旦确定了这些信息,我们就可以为输出图像分配空间。高度是图像的像素高度。步幅是图像的字节宽度。因此,它们的乘积是包含输出图像所需的字节数。
// After init, the image geometry is known, so allocate the output buffer
dwRawLen = picParm.u.CRE.Region.Height * picParm.u.CRE.Region.Stride;
pbRaw = new BYTE[dwRawLen];
if ( NULL == pbRaw )
{
P2LFree( reinterpret_cast<P2LIST*>(&picParm.PIC2List) );
picParm.Status = ERR_OUT_OF_SPACE;
return ( FALSE );
}
请注意,如果上面的分配失败,PIC2List成员将通过调用P2LFree来释放。稍后我们将更详细地讨论这一点,但现在我们只说PIC2List成员包含相机RAW操作从图像中提取的任何元数据,例如拍摄照片时使用的ISO设置或相机制造商。
与之前的Get缓冲区配置类似,Put缓冲区被配置为指向输出缓冲区,该缓冲区将在执行阶段接收提取的图像,但Put缓冲区最初被配置为空(Front == Rear)。
// Set up the Put (output) buffer
picParm.Put.Start = pbRaw;
picParm.Put.End = pbRaw + dwRawLen;
picParm.Put.Front = picParm.Put.Start;
picParm.Put.Rear = picParm.Put.Front;
接下来,从RAW文件中提取图像,并在终止阶段完成操作,释放任何内部分配的资源。
// Extract the image and complete the operation
if ( !PicReq(picParm, REQ_EXEC) || !PicReq(picParm, REQ_TERM) )
{
P2LFree( reinterpret_cast<P2LIST*>(&picParm.PIC2List) );
delete[] pbRaw;
return ( FALSE );
}
return ( TRUE );
}
TIFF/EXIF标签创建
除了图像提取之外,相机RAW操作码还会将元数据提取为TIFF标签。我们将把这些输出标签包含在最终的JPEG XR图像中,但为了符合规范,所有EXIF标签都必须位于EXIF部分。在CreateTIFFTags函数中,我们将遍历从Camera RAW返回的标签,并创建一个新的、格式正确的标签集。
BOOL CreateTIFFTags(PIC_PARM& picParm, P2LIST& newTags)
{
// Initialize result list
newTags.list = NULL;
newTags.len = 0;
newTags.size = 0;
P2LIST数据结构和相关函数为处理Tiff标签提供了一个简单的接口。使用P2LFirstTiffTag可以获取指向第一个Tiff标签的指针。后续调用P2LNextTiffTag将返回列表中的下一个标签,直到到达末尾。
// Iterate the the tiff tags and create a new set of tags
// with EXIF tags in the proper location
P2PktTiffTag* pkt = P2LFirstTiffTag( reinterpret_cast<P2LIST*>(&picParm.PIC2List), 0 );
while(pkt)
{
根据实际标签值,我们将把标签添加到输出标签的原始图像标签部分,或者将其添加到EXIF部分。
switch( pkt->TiffTag )
{
case TAG_ISOSpeedRatings:
case TAG_ShutterSpeedValue:
case TAG_ApertureValue:
case TAG_FocalLength:
case TAG_DNGVersion:
// These EXIF tags need to be relocated to the EXIF location.
if( !P2LAddTiffTag( &newTags, LOC_EXIFIFD,
pkt->TiffTag, pkt->TiffType, pkt->TiffCount, pkt->TiffData ))
{
return FALSE;
}
break;
default:
// Other TIF tags can be added with no location.
if( !P2LAddTiffTag( &newTags, LOC_PRIMARYIMAGEIFD,
pkt->TiffTag, pkt->TiffType, pkt->TiffCount, pkt->TiffData ) )
{
return FALSE;
}
break;
};
pkt = P2LNextTiffTag( (P2LIST*)&picParm.PIC2List, pkt );
}
return TRUE;
}
创建新标签后,会将它们返回以供JPEG XR压缩步骤使用。
JPEG XR (HDPhoto) 压缩
最后一步是压缩RAW图像并包含上一步创建的新TIFF标签。
BOOL JPEGXRSave(
PIC_PARM& picParmRaw, // Raw operation PIC_PARM result
LPBYTE pbRaw, // Raw image buffer
DWORD dwRawLen, // Raw image buffer length
P2LIST& p2list, // Relocated TIFF Tags
LPBYTE& pbOutput, // Output JXR buffer
DWORD& dwOutputLen ) // Output JXR buffer length
{
pbOutput = NULL;
dwOutputLen = 0;
HDPhoto压缩操作的配置是通过首先初始化传递给函数的PIC_PARM结构。通过设置Op和ParmVerMinor成员来选择HDPhoto压缩操作,这与我们之前设置相机RAW操作的方式相同。
// Initialize the operation
PIC_PARM picParmJXR;
memset( &picParmJXR, 0, sizeof(PIC_PARM) );
picParmJXR.ParmSize = sizeof(PIC_PARM);
picParmJXR.ParmVer = CURRENT_PARMVER;
picParmJXR.Op = OP_HDPHOTOP;
picParmJXR.ParmVerMinor = 2;
// Since the input image is 48BPP, we can’t use F_Bmp
picParmJXR.Flags = F_Raw;
相机RAW操作在初始化阶段用图像几何信息填充了CRE.Region。我们将这些信息复制到HDPhoto的Region(HDP.Region),以便HDPhoto操作在开始压缩图像时能够知道高度、宽度、像素深度等信息。
// Copy the output region information from the CameraRaw
// extraction to the HDPhoto input region
picParmJXR.u.HDP.Region = picParmRaw.u.CRE.Region;
我们还将之前创建的TIFF标签复制到PIC2List成员,以便将它们添加到压缩图像中。
// Copy the tiff tags to the output
picParmJXR.PIC2List = reinterpret_cast<char*>(p2list.list);
picParmJXR.PIC2ListLen = p2list.len;
picParmJXR.PIC2ListSize = p2list.size;
由于JXR标准支持48 BPP RGB(而不是48 BPP BGR),我们将设置标志以指示红色和绿色通道需要交换。
// The output image is going to be 48BPP RGB, so swap the Red and Blue
picParmJXR.u.HDP.Region.Flags = RF_SwapRB;
接下来,我们将以与相机RAW操作相同的方式设置Get缓冲区,但这次的区别是输入是48 BPP RGB图像,而不是专有的RAW格式数据。
// Set up the Get (input) buffer
picParmJXR.Get.Start = pbRaw;
picParmJXR.Get.End = pbRaw + dwRawLen;
picParmJXR.Get.Front = picParmJXR.Get.Start;
picParmJXR.Get.Rear = picParmJXR.Get.End;
picParmJXR.Get.QFlags = Q_EOF;
输出图像需要一个缓冲区来写入,因此我们将分配一个大小等于整个图像大小的缓冲区。这个大小是一个不错的选择,因为我们预计压缩后的图像会比原始图像小。
// The compressed image should be smaller than the orignal, so the output
// buffer won't exceed the size of the original image.
dwOutputLen = picParmJXR.u.CRE.Region.Stride * picParmJXR.u.CRE.Region.Height;
// Allocate the output buffer
pbOutput = new BYTE[dwOutputLen];
if ( NULL == pbOutput )
{
return ( FALSE );
}
Put缓冲区被配置为指向新分配的内存,该内存将包含压缩后的图像,就像我们在相机RAW操作中所做的那样。
// Set up the Put (output) buffer
picParmJXR.Put.Start = pbOutput;
picParmJXR.Put.End = pbOutput + dwOutputLen;
picParmJXR.Put.Front = picParmJXR.Put.Start;
picParmJXR.Put.Rear = picParmJXR.Put.Front;
接下来,我们将配置压缩参数。我们希望压缩到ISO标准JPEG XR格式,而不是旧的HDPhoto格式。我们还将选择用于压缩图像的量化值。它范围从1到255,定义了生成的图像质量。较低的值产生较高的质量,而较高的值产生较高的压缩率。值为1表示无损压缩。我们选择128,认为它在48BPP RGB图像的质量和压缩大小之间取得了良好的平衡。
// Enable JPEG-XR output
picParmJXR.Flags2 = PF2_JPEGXR_Format;
picParmJXR.u.HDP.Quantization = 128;
最后,我们将执行压缩。与相机RAW操作不同,我们将一次性完成所有三个步骤(初始化、执行和终止)。
// Compress the image
if ( !PicReq( picParmJXR, REQ_INIT )
|| !PicReq( picParmJXR, REQ_EXEC )
|| !PicReq( picParmJXR, REQ_TERM ) )
{
free( pbOutput );
return ( FALSE );
}
压缩后的图像会比原始图像小,因此我们通过Put缓冲区中的Rear和Front指针之间的差值来确定大小,这些指针由操作码在压缩过程中设置。我们将重新分配内存以释放未使用的部分,并设置返回值。
// Determine the size of the resulting compressed
// image and return the output.
dwOutputLen = static_cast<DWORD>( picParmJXR.Put.Rear - picParmJXR.Put.Front );
return TRUE;
}
质量与RAW相比如何?
使用尼康D60 RAW样本图像(RAW_NIKON_D60.NEF)来比较压缩质量(如下图所示),并且还可以在www.rawsamples.ch上找到许多其他RAW相机格式。
使用量化值为128进行压缩,然后解压缩,通过计算峰值信噪比(PSNR)和均方误差(MSE)将图像与原始RAW图像进行比较。结果显示与原始图像高度相关,如下所示。
通道 | 峰值信噪比 | 均方误差 |
红色 | 46.3 | 1.5 |
绿色 | 47.9 | 1.1 |
蓝色 | 47.7 | 1.1 |
当图像从样本中的相机RAW文件提取为48 BPP RGB图像时,原始图像大小为
3900行 x 2613列 x 3通道 x 2字节/通道 = 61,144,200 字节
生成的压缩文件仅为1,966,666字节,压缩比超过31:1。
以RAW_NIKON_D60.wdp(Windows Media Photo)保存的压缩JPEG XR文件在Windows照片查看器中显示。Windows Explorer的文件属性显示了样本代码中设置的Tiff/EXIF标签。
结论
使用JPEG XR压缩相机RAW图像的能力,可以在极低的图像质量损失下实现高压缩率,同时保留编辑所需的所有RAW数据。Accusoft Pegasus的PICTools SDK使提取和压缩任务变得简单直接,同时支持大量的RAW格式和ISO标准JPEG XR压缩。支持JPEG XR标准的图像查看和编辑应用程序越来越多,使其成为归档图像以及在无需专有RAW格式支持的情况下查看和共享图像的绝佳选择。
JPEG XR确实是RAW的精度与成熟压缩格式的可用性之间的理想折衷。
在www.accusoft.com下载PICTools成像SDK。如需更多信息,请联系我们:sales@accusoft.com或support@accusoft.com。
关于 Accusoft Pegasus
Accusoft Pegasus成立于1991年,当时的企业名称为Pegasus Imaging,总部位于佛罗里达州坦帕市,是成像软件开发工具包(SDK)和图像查看器最大的来源。成像技术解决方案包括条形码、压缩、DICOM、编辑、表单处理、OCR、PDF、扫描、视频和查看。技术支持环境包括Microsoft .NET、ActiveX、Silverlight、AJAX、ASP.NET、Windows Workflow和Java。支持多种32位和64位平台,包括Windows、Windows Mobile、Linux、Sun Solaris、Mac OSX和IBM AIX。请访问www.accusoft.com获取更多信息。
关于作者
在2007年加入Accusoft Pegasus担任高级软件工程师之前,Steve Brooks在MFC、ATL和.NET中开发了仪器控制、成像和类似的科学软件。他曾为微软和NASA提供咨询服务,他 prior employers 包括北卡罗来纳大学教堂山分校、Stingray Software 和 Varian Inc.。Steve获得了阿巴拉契亚州立大学物理学学士学位,并在那里开始使用Turbo C进行开发。