65.9K
CodeProject 正在变化。 阅读更多。
Home

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

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2010 年 12 月 2 日

CPOL

11分钟阅读

viewsIcon

32152

随着数码单反相机在消费市场的普及,许多用户选择使用相机制造商专有的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。

image001.jpg image002.jpg

以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.comsupport@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进行开发。

© . All rights reserved.