欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

数据压缩试验二:TGA 文件转 YUV 文件的 C++ 实现方法

程序员文章站 2022-07-14 22:12:30
...

基础知识 - TGA 文件格式

TGA 文件

Truevision TGA,一般被称作TARGA,是一种由Truevision科技(现在已经被Avid科技收购)公司开发的光栅图像格式。这是一种TARGA和VISTA图形卡的原生数据格式,这两种芯片是IBM第一种支持真彩显示的图形卡。这种系列的图像卡是为了用PC进行专业的图像处理和视频编辑而开发的;因为这个原因,正常TGA格式的图像和NTSC和PAL视频格式中的图像是一致的。[2]通常,在PC中,TGA文件以.tga后缀结尾。TGA文件因为它的简单、容易实现而变得普遍起来。

TGA 文件结构

TGA 文件主要分为两部分,即图像的基本信息块与图像的数据块,具体内容如下表
  数据压缩试验二:TGA 文件转 YUV 文件的 C++ 实现方法
本次试验仅考虑 Image Type = 2,即无压缩的真彩色图像类型

代码实现

将 TGA 文件转换为 RGB 文件

//
//  readTGA.cpp
//  bmp2yuv
//
//  Created by Zachary Chia on 2020/4/9.
//  Copyright © 2020 Zachary Chia. All rights reserved.
//

#include "readTGA2RGB.hpp"
#include <iostream>
#pragma pack(1)

using namespace std;

typedef struct
{
    unsigned char idLength;
    unsigned char colorMapType;
    unsigned char imageType;
    unsigned short firstEntryIndex;
    unsigned short colorMapLength;
    unsigned char colorMapEntrySize;
    unsigned short xOrigin;
    unsigned short yOrigin;
    unsigned short width;
    unsigned short height;
    unsigned char pixelDepth;
    unsigned char imageDescriptor;
}defTgaFileHeader;

#pragma pack()
/*
typedef struct
{
    unsigned char r;
    unsigned char g;
    unsigned char b;
    unsigned char a;
}pixelColor;
*/
void printInfo(defTgaFileHeader* tgaHeader)
{
    cout<<"\nTGA File Header Information:\n";
    printf("ID length:              %d\n", tgaHeader->idLength);
    printf("Colour Map type:        %d\n", tgaHeader->colorMapType);
    printf("Image type code:        %d\n", tgaHeader->imageType);
    printf("First entry index:      %d\n", tgaHeader->firstEntryIndex);
    printf("Colour map length:      %d\n", tgaHeader->colorMapLength);
    printf("Colour map entry size:  %d\n", tgaHeader->colorMapEntrySize);
    printf("X-Origin:               %d\n", tgaHeader->xOrigin);
    printf("Y-Origin:               %d\n", tgaHeader->yOrigin);
    printf("Width:                  %d\n", tgaHeader->width);
    printf("Height:                 %d\n", tgaHeader->height);
    printf("Pixel depth:            %d\n", tgaHeader->pixelDepth);
    printf("Descriptor:             %d\n\n", tgaHeader->imageDescriptor);
}
/*
void getRGB(pixelColor* pixel,int pixelDepth,unsigned char* rgba)
{
    switch(pixelDepth)
    {
        case 4:
            pixel->r = rgba[2];
            pixel->g = rgba[1];
            pixel->b = rgba[0];
            //pixel->a = rgba[3];
        case 3:
            pixel->r = rgba[2];
            pixel->g = rgba[1];
            pixel->b = rgba[0];
        case 2:
            pixel->r = (rgba[1] & 01111100) << 1;
            pixel->g = (rgba[1] & 00000011) << 6 | (rgba[0] & 11100000) >> 2;
            pixel->b = (rgba[0] & 00011111) << 3;
            //pixel->a = (rgba[1] & 10000000);
    }
}
*/
unsigned char* readTGA2RGB(FILE* tgaFile, unsigned char* rgbBuffer, long &imageSize, int &width, int &height)  // transfer tga to rgb file, and only support uncomprossed true-color image
{
    cout<<sizeof(defTgaFileHeader)<<"\n";
    defTgaFileHeader tgaHeader;
    fread(&tgaHeader,sizeof(defTgaFileHeader),1,tgaFile);
    
    /*
    tgaHeader.idLength = fgetc(tgaFile);
    tgaHeader.colorMapType = fgetc(tgaFile);
    tgaHeader.imageType = fgetc(tgaFile);
    fread(&tgaHeader.firstEntryIndex, 2, 1, tgaFile);
    fread(&tgaHeader.colorMapLength, 2, 1, tgaFile);
    tgaHeader.colorMapLength = fgetc(tgaFile);
    fread(&tgaHeader.xOrigin, 2, 1, tgaFile);
    fread(&tgaHeader.yOrigin, 2, 1, tgaFile);
    fread(&tgaHeader.width, 2, 1, tgaFile);
    fread(&tgaHeader.height, 2, 1, tgaFile);
    tgaHeader.pixelDepth = fgetc(tgaFile);
    tgaHeader.imageDescriptor = fgetc(tgaFile);
     */
    
    printInfo(&tgaHeader);  //check the header infomation
    
    if(tgaHeader.imageType != 2) //this function only support uncompressed true-color tga image
    {
        cout<<"Sorry, cannot support this type temporarily.\n\n";
        exit(-1);
    }
    
    int pixelCount; //sum nomber of the image
    pixelCount = tgaHeader.width * tgaHeader.height;
//    pixelColor colorData[pixelCount];   //color array for every pixel
    
    int bytesPerPixel = tgaHeader.pixelDepth / 8;   //calculate the pixel depth by Byte
    
//    unsigned char tempRGBA[bytesPerPixel];  //temporary color array for current pixel
//    for(int i = 0;i < pixelCount; i++)  //loop to get every pixel rgb data
//    {
//        if(fread(tempRGBA,1,bytesPerPixel,tgaFile) != bytesPerPixel)
//        {
//            cout<<"ERROR! the No."<<i+1<<" pixel cannot read in\n\n";
//            exit(-1);
//        }
//        getRGB(&colorData[i],bytesPerPixel,tempRGBA);
//    }
    
    rgbBuffer = (unsigned char*)malloc(pixelCount * 3 * sizeof(unsigned char*));
    
    unsigned char* tempRGBbuffer;
    tempRGBbuffer = (unsigned char*)malloc(pixelCount * 3 * sizeof(unsigned char*));
    
    switch(bytesPerPixel)
    {
        case 4:
        {
            unsigned char* aBuffer;
            aBuffer = (unsigned char*)malloc(pixelCount);
            for(int i = 0; i < pixelCount; i++)
            {
                fread(tempRGBbuffer,1, 3,tgaFile);
                fread(aBuffer,1,1,tgaFile);
            }
            break;
        }
        case 3:
        {
            fread(tempRGBbuffer,1,pixelCount * 3,tgaFile);
            break;
        }
        case 2:
        {
            unsigned char tempRGBA[bytesPerPixel];
            for(int i = 0; i < pixelCount; i++)
            {
                fread(tempRGBA,1, bytesPerPixel,tgaFile);
                tempRGBbuffer[i * 3 + 2] = (tempRGBA[1] & 01111100) << 1;
                tempRGBbuffer[i * 3 + 1] = (tempRGBA[1] & 00000011) << 6 | (tempRGBA[0] & 11100000) >> 2;
                tempRGBbuffer[i * 3] = (tempRGBA[0] & 00011111) << 3;
            }
            break;
        }
        default:
        {
            cout<<"ERROR: unsupport Pixel Depth\n";
            exit(-1);
        }
    }
    for(int i = 0; i < tgaHeader.height; i++)   //loop to write in the rgb data
    {
        for(int j =0; j < tgaHeader.width; j++)
        {
            int rgbPixelNum = (tgaHeader.height - 1 - i) * tgaHeader.width + j; //?
            int tgaPixelNum = i * tgaHeader.width + j;
            rgbBuffer[rgbPixelNum * 3 + 2] = tempRGBbuffer[tgaPixelNum * 3 + 2];
            rgbBuffer[rgbPixelNum * 3 + 1] = tempRGBbuffer[tgaPixelNum * 3 + 1];
            rgbBuffer[rgbPixelNum * 3] = tempRGBbuffer[tgaPixelNum * 3];
        }
    }
    
    //output rgb file
    FILE* rgbFile = NULL;
    if((rgbFile = fopen( "/Users/zachary/Documents/Data Compress Homework/TGA2YUV/middle.rgb" , "wb")) == NULL)
    {
        cout<<"ERROR : rgbFile open !\n";
    }
    else
    {
        cout<<"rgbFile opened\n";
    }
    fwrite(rgbBuffer,pixelCount * 3,1,rgbFile);
    fclose(rgbFile);
    
    //transfer the basic image data to the main function
    width = tgaHeader.width;
    height = tgaHeader.height;
    imageSize = pixelCount;
    return rgbBuffer;
}

在文件的最开始根据 TGA 文件头信息定义了结构体变量用于后续图像信息的读取,在第一版代码的思路中还定义了一个彩色信息的结构体用于后续的循环读取色彩信息,但在第二版的思路中将此段代码连同后续的 getRGB 函数以及循环获取信息的部分一并注释掉换了更简单直接的方法实现色彩信息的读取。
在此段代码中需要注意的是,如果在函数中使用 fread() 函数配合 sizeof() 直接读取文件的文件头会出现错位的情况,为了解决这一问题,可以采用代码中注释部分的方法进行逐个信息的获取,也可以在文件头结构体的前后分别添加#pragma pack(1)#pragma pack()使编译器按照 1 字节对齐后取消自定义对齐方式。同时发现,在定义函数时使用了 unsigned char* 类型进行 rgbBuffer 的传参,但在获取色彩信息后并未回传给 main.cpp,为了解决这个问题不得以将函数直接定义为 unsigned char* 后通过 return(rgbBuffer) 的方式将数据返回给 main.cpp。

将得到的 RGB 文件转为 4:2:0 格式的 YUV 文件

在这里直接调用之前试验中已经完成的 rgb2yuv.cpp 进行,代码如下,不再赘述

//
//  rgb2yuv.cpp
//  yuv2rgb
//
//  Created by Zachary Chia on 2020/3/23.
//  Copyright © 2020 Zachary Chia. All rights reserved.
//
#include <iostream>
#include "rgb2yuv.hpp"

int rgb2yuv(FILE* targetSource, long rgbSize, unsigned char* rgbBuffer, int width, int height)
{
    //定义 4:2:0 色度格式下的 Y、U、V 分量
    unsigned char* yBuffer;
    unsigned char* uBuffer;
    unsigned char* vBuffer;
    yBuffer = (unsigned char*) malloc (rgbSize / 3);
    uBuffer = (unsigned char*) malloc (rgbSize /3 / 4);
    vBuffer = (unsigned char*) malloc (rgbSize /3 / 4);
    
    //定义 4:4:4 色度格式下的 U、V 分量
    unsigned char* uBuffer444 = NULL;
    unsigned char* vBuffer444 = NULL;
    uBuffer444 = (unsigned char *)malloc(rgbSize / 3);
    vBuffer444 = (unsigned char *)malloc(rgbSize / 3);
    int pixelNum = width * height;
    
    //完成 RGB 到 Y、U、V 4:4:4 格式的转换
    for(int i = 0;i < pixelNum; i++)
    {
        unsigned char r = rgbBuffer[3 * i + 2];
        unsigned char g = rgbBuffer[3 * i + 1];
        unsigned char b = rgbBuffer[3 * i];
        //计算 YUV 分量
        yBuffer[i] = (66 * r + 129 * g + 25 * b) / 255 + 16;
        uBuffer444[i] = (-38 * r - 74 * g + 112 * b) / 255 + 128;
        vBuffer444[i] = (112 * r - 94 * g - 18 * b) / 255 + 128;
        
    }
    
    // 4:4:4 To 4:2:0
    int count = 0;
    for (int i = 0; i < height; i = i + 2)
    {
        for(int j = 0; j < width; j = j + 2)
        {
            uBuffer[count] = (uBuffer444[i * width + j] + uBuffer444[i * width + j + 1] + uBuffer444[(i + 1) * width + j] + uBuffer444[(i + 1) * width + j + 1]) / 4;
            vBuffer[count] = (vBuffer444[i * width + j] + vBuffer444[i * width + j + 1] + vBuffer444[(i + 1) * width + j] + vBuffer444[(i + 1) * width + j + 1]) / 4;
            count++;
        }
    }
    
    //write in
    fwrite(yBuffer,sizeof(unsigned char),pixelNum * sizeof(unsigned char),targetSource);
    fwrite(uBuffer,sizeof(unsigned char),pixelNum * sizeof(unsigned char) / 4,targetSource);
    fwrite(vBuffer,sizeof(unsigned char),pixelNum * sizeof(unsigned char) / 4,targetSource);
    
    free(uBuffer444);
    free(vBuffer444);
    free(yBuffer);
    free(uBuffer);
    free(vBuffer);
    return 0;
}

main.cpp

 //
//  main.cpp
//  bmp2yuv
//
//  Created by Zachary Chia on 2020/4/9.
//  Copyright © 2020 Zachary Chia. All rights reserved.
//

#include <iostream>

using namespace std;

unsigned char* readTGA2RGB(FILE* tgaFile, unsigned char* rgbBuffer, long &imageSize, int &width, int &height);
int rgb2yuv(FILE* targetSource, long rgbSize, unsigned char* rgbBuffer, int width, int height);

int main(int argc, const char * argv[])
{
    FILE* tgaFile = NULL;
    FILE* yuvFile = NULL;
    const char* tgaFileAdd = argv[1]; 
    const char* yuvFileAdd = argv[2];
    
    if((tgaFile = fopen( argv[1] , "rb")) == NULL)
        {
            cout<<"ERROR : tgaFile open !\n";
        }
        else
        {
            cout<<"tgaFile opened\n";
        }
        
    if((yuvFile = fopen( argv[2] , "wb+")) == NULL)
        {
            cout<<"ERROR : yuvFile open !\n";
        }
        else
        {
            cout<<"yuvFile opened\n";
        }
    
    long imageSize = 0;
    long rgbSize = 0;
    int width = 0;
    int height = 0;
    unsigned char* rgbBuffer = NULL;
    
    rgbBuffer = readTGA2RGB(tgaFile,rgbBuffer,imageSize,width,height); //?
    rgbSize = imageSize * 3;
    //cout<<sizeof(rgbBuffer)<<"\n";
    rgb2yuv(yuvFile,rgbSize,rgbBuffer,width,height);
    
    cout<<"Done\n";
    
    free(rgbBuffer);
    fclose(yuvFile);
    fclose(tgaFile);
    
    return 0;
}

试验过程中,在完成代码编写后始终无法得到正确的图像,通过多次 debug 利用排除法判断后认为试验代码并无错误,更换试验图片后验证代码正确,进而确定试验前期多次失败的原因来自图片。通过二进制阅读比较试验中的两张图片可以看出初期图片的末尾出现了一段未知字符段,进而确定尽管在文件头读取等部分并无影响,试验初期所用的图片格式并不完全适用于所写代码,仍有改进之处。同时也学习到了在实验前应确定试验数据来源是否可靠或匹配,否则会浪费不必要的试验时间。

试验结果

通过 YUVviewerPlus 分别输出原始 TGA 文件 origin.tga、中间产物 RGB 文件 middle.rgb 以及最终产物 target.yuv 如下:
数据压缩试验二:TGA 文件转 YUV 文件的 C++ 实现方法
数据压缩试验二:TGA 文件转 YUV 文件的 C++ 实现方法
数据压缩试验二:TGA 文件转 YUV 文件的 C++ 实现方法
肉眼并未分辨出明显差异,试验成功

参考资料

*
《TGA图像头文件拾取的字节对齐及#pragma pack的使用》 by brain2004