TGA转换为YUV的C++实现方法
TGA(或TARGA)格式,是Truevision公司开发的一种用于描述位图图像的格式,它能够表示从黑白、索引颜色到RGB颜色的位图,支持Α通道,并支持多种压缩方法,兼具了体积小和效果清晰的特点,成为了CG领域影视动画的常用序列输出格式。
本文通过C++,实现了将TGA格式图像转换为YUV格式。暂时只讨论Image Type Code为2(无压缩、无调色板的RGB图像)和10(游程编码的RGB图像)的情况,且均支持Targa 16、Targa 24和Targa 32图像的转换。
一. TGA文件结构简介
下面先介绍无压缩TGA文件的文件结构:
名称 | 偏移量(Byte) | 长度(Byte) | 说明 |
---|---|---|---|
ID Length | 0 | 1 | 图像信息字段的长度:为0时表示不存在图像信息字段 |
Colour Map Type | 1 | 1 | 调色板类型: 0:无调色板 1:有调色板 |
Image Type Code | 2 | 1 | 图像类型码: 0:无图像数据 1:无压缩、有调色板的图像 2:无压缩、无调色板的RGB图像 3:无压缩黑白图像 9:有调色板的游程编码图像 10:游程编码的RGB图像 11:压缩的黑白图像 32:使用Huffman、Delta、游程编码压缩编码的有调色板的图像 33:使用Huffman、Delta、游程编码压缩编码的有调色板的图像,4趟四叉树类型处理 |
Colour Map Offset | 3 | 1 | 颜色表入口索引 |
Colour Map Length | 5 | 2 | 颜色表总entry数 |
Colour Map Depth | 7 | 2 | 每个颜色表entry的位数: 16:Targa 16 24:Targa 24 32:Targa 32 |
X Origin | 8 | 1 | 图像左下角的x坐标 |
Y Origin | 10 | 2 | 图像左下角的y坐标 |
Width | 12 | 2 | 图像宽 |
Height | 14 | 2 | 图像高 |
Bits Per Pixel | 16 | 2 | 图像每像素占用的位数 |
Image Descriptor | 17 | 1 | 图像描述符字节: bits 3-0——每像素对应的属性位的位数: Targa 16, 该值为 0 或 1;Targa 24,该值为 0;Targa 32,该值为 8 bit 4——保留,必须为 0 bit 5——屏幕起始位置标志: 0:原点在左下角1;1:原点在左上角;对于truevision图像必须为 0 bits 7-6——交叉数据存储标志: 00:无交叉 01:两路奇/偶交叉 10:四路交叉 11:保留 |
Image Identification Field | 18 | 可变 | 该字段长度由ID Length字段指定。一般被忽略。如果需要存储更多信息,可以放在图像数据之后 |
Colour Map Data | 可变 | 可变 | 若Colour Map Type为 0,则该字段不存在。每个调色板entry的长度可为4(RGBA)、3(RGB)或2字节(第1字节为GGBBBBB,第2字节为ARRRRRGG) |
Image Data Field | 可变 | 可变 | 注意,TGA文件采用小端存储方式,RGB分量以B、G、R的顺序存储,图像数据是从左下角开始存储的 |
二. 基本原理
1. RLE(Run Length Encoding,游程编码)算法的简单介绍
游程编码算法的基本原理其实非常简单,例如我们在微信聊天时,经常会发送这样一句话:
emmmmmmmmmmmmmm
你可能会很自然地想到,后面14个“m”是完全重复的,那么将这一字符串编为“1e14m”就可以很大程度上压缩数据量,这就是RLE的基本原理。
(但是一般来说,这种压缩编码方法的压缩比并不高,且压缩比随图形复杂程度的上升而下降。)
如前所述,在Image Type Code为10的TGA图像中,Image Data Field中的图像数据就采用了RLE算法。其中,数据被分为若干个Packet(数据包),其中共有两类:
- Run-Length Packet:存储了连续的不重复RGB(A)值的像素信息;
- Raw Packet:存储了具有连续的重复RGB(A)值的像素信息。
数据包的结构为:
名称 | 长度 | 说明 |
---|---|---|
Packet Header | 1字节 | 第1位为Packet Header ID:0表示为Raw Packet,1表示为RL Packet; 后7位:对于Raw Packet,表示该包中的像素数-1;对于RL Packet,表示游程(Run Size)-1 |
Packet Data | 可变 | 对于Raw Packet,该字段中依次存储了包中不同的1—128个像素的RGB(A)值; 对于RL Packet,该字段中存储了1—128个相同的像素的RGB(A)值 |
以两个Targa 24的数据包为例,我们只要将0x01 B0 G0 R0 B1 G1 R1
的数据包还原为0xB0 G0 R0 B1 G1 R1
、0x83 B G R
的数据包还原为0xB G R B G R B G R
即可(其中,“B G R
”不表示十六进制数)。
2. RGB与YUV色彩空间转换的基本原理
三. 思路
- 准备工作:打开所需要的文件;
- 为TGA文件建立文件头的结构体,将文件头的内容读入;
- 根据文件头信息,计算偏移量,找到图像数据的起始点;
- 读入图像数据,对RLE压缩的图像进行解码,并根据Bits Per Pixel的不同,分离R、G、B分量;
- 将图像上下颠倒,即改为RGB文件的存储方式
- 使用
rgb2yuv
函数,即完成了向YUV文件的转换
四. 源代码
declarations.h
#pragma once
#include <iostream>
typedef struct
{
char idLength = 0; // Length of identification field (length = 1B, offset = 0B)
char colourMapType = 0; // Colour map type (length = 1B, offset = 1B): 0 for no colour map included, 1 for colour map included
char imageTypeCode = 0; // Image type code (length = 1B, offset = 2B): 2 for uncompressed & unmapped RGB image, 10 for run length encoded & unmapped RGB image
/* Colour map specification (all set to 0 if colour map type is 0) */
short colourMapOffset = 0; // Colour map origin (length = 2B, offset = 3B): index of first colour map entry
short colourMapLength = 0; // Colour map length (length = 2B, offset = 5B): number of colour map entries
char colourMapDepth = 0; // Colour map depth (length = 1B, offfset = 7B): number of bits in each entry. 16 for the Targa 16, 24 for the Targa 24, 32 for the Targa 32
/* Image specification */
short x_origin = 0; // X coordinate of the lower left corner (length = 2B, offset = 8B)
short y_origin = 0; // Y coordinate of the lower left corner (length = 2B, offset = 10B)
short width = 0; // Width of image (length = 2B, offset = 12B)
short height = 0; // Height of image (length = 2B, offset = 14B)
char bitsPerPixel = 0; // Number of bits in each pixel (length = 1B, offset = 16B)
char imageDescriptor = 0; // Image descripter byte (length = 1B, offset = 17B)
} HEADER;
typedef struct
{
unsigned char r, g, b, a;
} PIXEL;
void ReadTgaHeader(HEADER* tgaHdPtr, FILE* tgaPtr);
void ReadColourData(HEADER* tgaHdPtr, PIXEL* colourData, FILE* tgaPtr);
void Trans2RgbFormat(PIXEL* colourData, unsigned char* rgbBuff, HEADER* tgaHdPtr);
void rgb2yuv(FILE* yuvPtr, int width, int height, unsigned char* rgbBuff);
ReadTgaHeader.cpp
#include <iostream>
#include "declarations.h"
using namespace std;
void ReadTgaHeader(HEADER* tgaHdPtr, FILE* tgaPtr)
{
/* Read data in TGA file header fields */
tgaHdPtr->idLength = fgetc(tgaPtr);
tgaHdPtr->colourMapType = fgetc(tgaPtr);
tgaHdPtr->imageTypeCode = fgetc(tgaPtr);
fread(&tgaHdPtr->colourMapOffset, 2, 1, tgaPtr);
fread(&tgaHdPtr->colourMapLength, 2, 1, tgaPtr);
tgaHdPtr->colourMapDepth = fgetc(tgaPtr);
fread(&tgaHdPtr->x_origin, 2, 1, tgaPtr);
fread(&tgaHdPtr->y_origin, 2, 1, tgaPtr);
fread(&tgaHdPtr->width, 2, 1, tgaPtr);
fread(&tgaHdPtr->height, 2, 1, tgaPtr);
tgaHdPtr->bitsPerPixel = fgetc(tgaPtr);
tgaHdPtr->imageDescriptor = fgetc(tgaPtr);
/* Display header info on screen */
cout << "\nTGA file header information:\n";
printf("ID length: %d\n", tgaHdPtr->idLength);
printf("Colour Map type: %d\n", tgaHdPtr->colourMapType);
printf("Image type code: %d\n", tgaHdPtr->imageTypeCode);
printf("Colour map offset: %d\n", tgaHdPtr->colourMapOffset);
printf("Colour map length: %d\n", tgaHdPtr->colourMapLength);
printf("Colour map depth: %d\n", tgaHdPtr->colourMapDepth);
printf("X origin: %d\n", tgaHdPtr->x_origin);
printf("Y origin: %d\n", tgaHdPtr->y_origin);
printf("Width: %d\n", tgaHdPtr->width);
printf("Height: %d\n", tgaHdPtr->height);
printf("Image pixel size: %d\n", tgaHdPtr->bitsPerPixel);
printf("Descriptor: %d\n\n", tgaHdPtr->imageDescriptor);
}
ReadColourData.cpp
#include "declarations.h"
void SeparateRGBA(PIXEL* pixel, unsigned char* rgba, int bytesPerPixel)
{
switch (bytesPerPixel)
{
case 4:
pixel->r = rgba[2];
pixel->g = rgba[1];
pixel->b = rgba[0];
pixel->a = rgba[3];
break;
case 3:
pixel->r = rgba[2];
pixel->g = rgba[1];
pixel->b = rgba[0];
pixel->a = 255;
break;
case 2:
pixel->r = (rgba[1] & 0b01111100) << 1;
pixel->g = ((rgba[1] & 0b00000011) << 6) | ((rgba[0] & 0b11100000) >> 2);
pixel->b = (rgba[0] & 0b00011111) << 3;
pixel->a = (rgba[1] & 0b10000000);
break;
default:
break;
}
}
void ReadColourData(HEADER* tgaHdPtr, PIXEL* colourData, FILE* tgaPtr)
{
int bytesPerPx = tgaHdPtr->bitsPerPixel / 8; // Bytes per pixel
unsigned char tempRGBA[4]; // Temporary buffer for the RGBA data of 1 pixel
int n = 0; // nth pixel
while (n < tgaHdPtr->width * tgaHdPtr->height)
{
switch (tgaHdPtr->imageTypeCode)
{
/* Uncompressed, unmapped RGB image */
case 2:
{
/* Read the colour data of 1 pixel */
if (fread(tempRGBA, 1, bytesPerPx, tgaPtr) != bytesPerPx)
{
printf("ERROR!!! Unexpected end of file at pixel %d.\n", n);
exit(-1);
}
SeparateRGBA(&(colourData[n]), tempRGBA, bytesPerPx);
//printf("%-4x%-4x%-4x\n", colourData[n].b, colourData[n].g, colourData[n].r); // Check
n++;
break;
}
/* Run length encoded, unmapped RGB image */
case 10:
{
unsigned char tempPktHeader; // Temporary buffer for the packet header
int pktHdID = 0; // Determines if it's an RL packet or a raw packet
int pktRunSize = 0; // Run size (the number of pixels in this packet)
/* Read the packet header */
if (fread(&tempPktHeader, 1, 1, tgaPtr) != 1)
{
printf("ERROR!!! Unexpected end of file at pixel %d.\n", n);
exit(-1);
}
pktHdID = (tempPktHeader & 0x80) >> 7;
pktRunSize = (tempPktHeader & 0x7F) + 1;
/* Raw packet */
if (pktHdID == 0)
{
for (int i = 0; i < pktRunSize; i++)
{
if (fread(tempRGBA, 1, bytesPerPx, tgaPtr) != bytesPerPx)
{
printf("ERROR!!! Unexpected end of file at pixel %d.\n", n);
exit(-1);
}
SeparateRGBA(&(colourData[n]), tempRGBA, bytesPerPx);
n++;
}
}
/* RL packet */
else if (pktHdID == 1)
{
if (fread(tempRGBA, 1, bytesPerPx, tgaPtr) != bytesPerPx)
{
printf("ERROR!!! Unexpected end of file at pixel %d.\n", n);
exit(-1);
}
for (int i = 0; i < pktRunSize; i++)
{
SeparateRGBA(&(colourData[n]), tempRGBA, bytesPerPx);
n++;
}
}
else
{
printf("ERROR!!! Unexpected invalid value of packet header ID\n");
exit(-1);
}
break;
}
default:
break;
}
}
}
Trans2RgbFormat
#include "declarations.h"
void Trans2RgbFormat(PIXEL* colourData, unsigned char* rgbBuff, HEADER* tgaHdPtr)
{
/* Write RGB data in .rgb format into rgbBuff */
int w = tgaHdPtr->width;
int h = tgaHdPtr->height;
for (int i = 0; i < h; i++) // i for row of image
{
for (int j = 0; j < w; j++) // j for column of image
{
int rgbPxNum = (h - 1 - i) * w + j; // Pixel number in RGB file
int tgaPxNum = i * w + j; // Pixel number in TGA file
rgbBuff[3 * rgbPxNum + 2] = colourData[tgaPxNum].r;
rgbBuff[3 * rgbPxNum + 1] = colourData[tgaPxNum].g;
rgbBuff[3 * rgbPxNum] = colourData[tgaPxNum].b;
}
}
}
rgb2yuv.cpp
#include <iostream>
#include "declarations.h"
int rgb66[256], rgb129[256], rgb25[256];
int rgb38[256], rgb74[256], rgb112[256];
int rgb94[256], rgb18[256];
void rgbLookupTable()
{
for (int i = 0; i < 256; i++)
{
rgb66[i] = 66 * i;
rgb129[i] = 129 * i;
rgb25[i] = 25 * i;
rgb38[i] = 38 * i;
rgb74[i] = 74 * i;
rgb112[i] = 112 * i;
rgb94[i] = 94 * i;
rgb18[i] = 18 * i;
}
}
void rgb2yuv(FILE* yuvPtr, int width, int height, unsigned char* rgbBuff)
{
int pxCount = width * height;
unsigned char* yBuff = new unsigned char[pxCount]; // Buffer for Y component
unsigned char* uBuff = new unsigned char[pxCount / 4]; // Buffer for U component
unsigned char* vBuff = new unsigned char[pxCount / 4]; // Buffer for V component
unsigned char* uBuff444 = new unsigned char[pxCount]; // Buffer for U component in 4:4:4 format
unsigned char* vBuff444 = new unsigned char[pxCount]; // Buffer for V component in 4:4:4 format
// RGB to YUV (4:4:4)
for (int i = 0; i < pxCount; i++) // i for pixel number
{
unsigned char r = rgbBuff[3 * i + 2]; // R component of the ith pixel
unsigned char g = rgbBuff[3 * i + 1]; // G component of the ith pixel
unsigned char b = rgbBuff[3 * i]; // B component of the ith pixel
rgbLookupTable();
yBuff[i] = ((rgb66[r] + rgb129[g] + rgb25[b]) >> 8) + 16;
uBuff444[i] = ((-rgb38[r] - rgb74[g] + rgb112[b]) >> 8) + 128;
vBuff444[i] = ((rgb112[r] - rgb94[g] - rgb18[b]) >> 8) + 128;
}
// 4:4:4 to 4:2:0
for (int i = 0; i < height; i += 2)
{
for (int j = 0; j < width; j += 2)
{
uBuff[i / 2 * width / 2 + j / 2] = uBuff444[i * width + j];
vBuff[i / 2 * width / 2 + j / 2] = vBuff444[i * width + j];
}
}
delete[]uBuff444;
delete[]vBuff444;
fwrite(yBuff, sizeof(unsigned char), pxCount, yuvPtr);
fwrite(uBuff, sizeof(unsigned char), pxCount / 4, yuvPtr);
fwrite(vBuff, sizeof(unsigned char), pxCount / 4, yuvPtr);
}
main.cpp
#include "stdio.h"
#include <iostream>
#include "stdlib.h"
#include "math.h"
#include "declarations.h"
using namespace std;
int main(int argc, char* argv[])
{
/* Declarations */
FILE* tgaFilePtr, * yuvFilePtr;
//FILE* rgbFilePtr;
HEADER hd; // Structure variable for TGA file header
int w, h, pxCount;
const char* tgaFileName = "snow32RLE.tga";
const char* yuvFileName = "snow32RLE.yuv";
//const char* rgbFileName = "snow16.rgb";
PIXEL* rgbaData = NULL; // Entire RGBA data of TGA file; used for future funtions
unsigned char* rgbBuffer = NULL; // RGB data of TGA file (in .rgb format); extracted from rgbaData; used for tga2yuv
int offset = 0;
/* Open the files */
if (fopen_s(&tgaFilePtr, tgaFileName, "rb") == 0)
{
cout << "Successfully opened \"" << tgaFileName << "\".\n";
}
else
{
cout << "Failed to open \"" << tgaFileName << "\".\n";
exit(-1);
}
if (fopen_s(&yuvFilePtr, yuvFileName, "wb") == 0)
{
cout << "Successfully opened \"" << yuvFileName << "\".\n";
}
else
{
cout << "Failed to open \"" << yuvFileName << "\".\n";
exit(-1);
}
//if (fopen_s(&rgbFilePtr, rgbFileName, "wb") == 0)
//{
// cout << "Successfully opened \"" << rgbFileName << "\".\n";
//}
//else
//{
// cout << "Failed to open \"" << rgbFileName << "\".\n";
// exit(-1);
//}
/* Read and display the header fields */
ReadTgaHeader(&hd, tgaFilePtr);
w = hd.height;
h = hd.width;
pxCount = w * h;
/* Space allocation */
rgbaData = new PIXEL[hd.width * hd.height];
memset(rgbaData, 0, hd.height * hd.width); // Initialisation
rgbBuffer = new unsigned char[hd.width * hd.height * 3];
memset(rgbBuffer, 0, hd.height * hd.width * 3); // Initialisation
/* Developed function check & invalidation check */
if (hd.imageTypeCode != 2 && hd.imageTypeCode != 10)
{
cout << "Can only handle image type 2 (uncompressed, unmapped RGB) & image type 10 (run length encoded, unmapped RGB).\nOther options being developed.\n";
exit(-1);
}
if (hd.bitsPerPixel != 16 && hd.bitsPerPixel != 24 && hd.bitsPerPixel != 32)
{
cout << "Invalid value of image pixel size!\nCan only handle pixel depths of 16, 24, and 32.\n";
exit(-1);
}
if (hd.colourMapType != 0 && hd.colourMapType != 1)
{
cout << "Invalid value of colour map type!\nCan only handle colour map types of 0 and 1.\n";
exit(-1);
}
/* Skip over unnecessary chunks */
offset += hd.idLength;
offset += hd.colourMapType * hd.colourMapLength * hd.colourMapDepth;
cout << offset << " byte(s) skipped over.\n\n";
fseek(tgaFilePtr, offset, SEEK_CUR); // Skip 'offset' bytes from the end of header
/* Read the image RGB (A, if exists) data */
ReadColourData(&hd, rgbaData, tgaFilePtr);
/* Transform .tga formatted RGB data into .rgb format */
Trans2RgbFormat(rgbaData, rgbBuffer, &hd);
//fwrite(rgbBuffer, 3, pxCount, rgbFilePtr);
/* Transform RGB into YUV */
rgb2yuv(yuvFilePtr, w, h, rgbBuffer);
delete[]rgbaData;
delete[]rgbBuffer;
//fclose(rgbFilePtr);
fclose(tgaFilePtr);
fclose(yuvFilePtr);
}
五. 运行结果
实验所用的测试图像为“snow.jpeg”通过Adobe Photoshop转换而来的“snow16.tga”“snow24.tga”和“snow32.tga”。原始图像如下:
将转换的图像用YUV Viewer Plus打开,结果如下:
转换基本成功。