YUV是图片、相机、视频中会用到的一种图像格式,Y(Luminance)表示亮度,U(Chrominance)表示色度,V(Chroma)表示浓度。
YUV根据Y、U、V在水平、垂直方向的采样率之比又分为yuv444、yuv422和yuv420,不同的格式存储方式也略有不同,本文我们以yuv420为例。
yuv420格式中,Y和UV在水平和垂直方向采样率之比都是2:1,这意味着yuv420一定会损失部分色彩信息。
Y、U、V分量的存储空间占用比为4:1\:1,存储顺序则是按像素从左到右横向扫描,先存储所有Y分量,再存储所有U分量,最后存储所有V分量,每个分量占一个字节,也就是8bit。
如果有一个4x4的yuv420p图片,其24B存储空间的大致分布将如下所示:
| Y | Y | Y | Y |
|---|---|---|---|
| Y | Y | Y | Y |
| Y | Y | Y | Y |
| Y | Y | Y | Y |
| V | V | V | V |
| U | U | U | U |
Y分量占据了16个字节,U、V分量分别占据4个字节。并不需要知道这些UV是如何反映到每个像素上的,知道大致存储结构就已经可以对图像进行消色操作了。
如果我们只获取Y分量,那么我们就能得到一副黑白画面,如果除Y分量还额外获取UV分量,就能得到彩色画面。这也是为什么电视信号能同时兼容彩电和黑白电视,因为电视信号就是用这种存储方式传输画面。
要转换yuv格式和查看yuv图片,需要安装ffmpeg。
yuv格式并没有文件头,无法从文件内容中获知图片尺寸信息,必须人为指定。
因此建议在命名yuv文件时,在文件名中注明图片尺寸,以便后续使用。
#安装ffmpeg
$ sudo apt install ffmpeg
#将图片尺寸为384x182的TestPic.png转换为yuv420格式的图片out_384x182.yuv
$ ffmpeg -i TestPic.png -s 384x182 -pix_fmt yuv420p out_384x182.yuv
#查看尺寸为384x182的yuv格式图片out_384x182.yuv
$ ffplay -f rawvideo -video_size 384x182 out_384x182.yuv
通过C++的文件读写操作,可以很轻易的按字节操作yuv文件。
用 0x80 (-128)覆盖UV分量即可消除对应的色彩信息。
#include<iostream>
#include<iomanip>
#include<fstream>
#include<thread>
#include<string.h>
#include<string>
#include<dirent.h>
#include<vector>
#include<utility>
#include<chrono>
#include<cstdlib>
#include<unistd.h>
#include <android/log.h>
#include <utils/CallStack.h>
//<utils/CallStack.h> refers <log/log.h>, in <log/log.h> defined LOG_TAG to NULL
#undef LOG_TAG
#define LOG_TAG "wangtianlin1"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG ,__VA_ARGS__)
const uint WIDTH = 1844;
const uint HEIGTH = 4000;
const uint BUFFER_CAPACITY = 256;
const std::string INPUT_FOLDER = "input//";
const std::string OUTPUT_FOLDER = "output//";
const uint8_t FILE_TYPE_NUM = 8;
android::CallStack g_callStack;
void dump_stack_android(void)
{
g_callStack.update();
g_callStack.log(LOG_TAG);
}
class DataQueue{
public:
DataQueue(){
this->head = nullptr;
this->tail = nullptr;
this->readDone = false;
this->writeDone = false;
}
//Add a node at queue tail.
void Product(char* inputData, uint inputLength){
if (!tail){
this->tail = new DataNode(inputData, inputLength);
this->head = this->tail;
}else{
this->tail->_next = new DataNode(inputData, inputLength);
this->tail = this->tail->_next;
}
}
//Get a node at queue head and delete it, if head is null, keep waitting.
std::pair<char*, uint> Consume(){
//Consumer is allowed to wait.
while(!this->head);
std::pair<char*, uint> result((char*)this->head->_data, this->head->_length);
DataNode* temp = (DataNode*)this->head;
//Another wait.
while(!this->head->_next && !this->readDone);
/*///////////////////////////////////////////////////////////////////////////////////////////////
//
// Three situations : (next) || (done) || (next && done)
// next: strill reading.
// done: read finished and data is empty. Writting should stop at this situation.
// next && done: read finished but data is not empty.
//
///////////////////////////////////////////////////////////////////////////////////////////////*/
if(this->head->_next){
this->head = temp->_next;
}else{
//read done and write done.
this->writeDone = true;
LOGD("Child Thread - Write Finished.\n");
dump_stack_android();
}
delete(temp);
return result;
}
void SetReadDone(){
this->readDone = true;
}
bool IsWriteDone(){
return (bool)this->writeDone;
}
private:
struct DataNode{
char* volatile _data;
volatile uint _length;
DataNode* volatile _next;
DataNode(char* inputData, uint inputLength){
_data = inputData;
_length = inputLength;
_next = nullptr;
}
};
DataNode* volatile head;
DataNode* volatile tail;
volatile bool readDone;
volatile bool writeDone;
};
DataQueue* g_DQ = new DataQueue();
//Open INPUT_FOLDER and create a string vector by all files
void CreateFileNameVector(std::vector<std::string> &inputStringVector){
DIR* dir;
struct dirent* ptr;
std::string path = "input//";
if(NULL == (dir = opendir(path.c_str()))){
LOGI("Main Thread - No input file found!\n");
exit(1);
return;
}
while((ptr = readdir(dir)) != NULL){
if(FILE_TYPE_NUM == ptr->d_type){
std::string temp = ptr->d_name;
inputStringVector.push_back(temp);
}
}
return;
}
//Read BUFFER_CAPACITY Byte once from yuv file and deliver it to writer thread, skip -
//color informations(UV).
void PicDataRead(void){
const uint ReadByteNum = WIDTH * HEIGTH;
std::vector<std::string> fileNames;
CreateFileNameVector(fileNames);
for(const std::string ¤tFileName : fileNames){
std::ifstream picReader(INPUT_FOLDER + currentFileName, std::ifstream::binary);
char* _temp = new char[currentFileName.size()];
strcpy(_temp, currentFileName.c_str());
picReader.seekg(0, picReader.end);
const uint PicSize = picReader.tellg();
picReader.seekg(0, picReader.beg);
g_DQ->Product(_temp, PicSize - ReadByteNum);
LOGD("Main Thread - File %s reading...\n", currentFileName.c_str());
for(uint readCounter = 0; readCounter < ReadByteNum;){
uint _bufferLeft = ReadByteNum - readCounter;
uint _currentBufferSize = BUFFER_CAPACITY;
if(_bufferLeft < BUFFER_CAPACITY){
_currentBufferSize = _bufferLeft;
}
char* inputData = new char[_currentBufferSize];
picReader.read(inputData, _currentBufferSize);
g_DQ->Product(inputData, _currentBufferSize);
readCounter += _currentBufferSize;
}
picReader.close();
LOGD("Main Thread - File %s reading done!\n", currentFileName.c_str());
}
g_DQ->SetReadDone();
LOGD("Main Thread - Read Finished!\n");
return;
}
//Write information received to file, and fill color information with 0x80.
void PicDataWrite(void){
const uint WriteByteNum = WIDTH * HEIGTH;
char* grayBuffer = new char[BUFFER_CAPACITY];
memset(grayBuffer,0x80,BUFFER_CAPACITY);
while(true){
LOGD("Child Thread - Getting file name and bufferLeftNum...\n");
std::pair<char*, uint> resultReceiver = g_DQ->Consume();
std::string currentFileName(resultReceiver.first);
const uint bufferLeftNum = resultReceiver.second;
LOGD("Child Thread - File name: %s\n", currentFileName.c_str());
delete[](resultReceiver.first);
std::ofstream picOutput(OUTPUT_FOLDER + currentFileName, std::ofstream::binary);
LOGD("Child Thread - File %s writing...\n", currentFileName.c_str());
for(uint writeCounter = 0; writeCounter < WriteByteNum;){
resultReceiver = g_DQ->Consume();
picOutput.write(resultReceiver.first, resultReceiver.second);
delete[](resultReceiver.first);
writeCounter += resultReceiver.second;
}
for(uint writeCounter = 0; writeCounter < bufferLeftNum;){
uint _bufferLeft = bufferLeftNum - writeCounter;
uint _currentBufferSize = BUFFER_CAPACITY;
if(_bufferLeft < BUFFER_CAPACITY){
_currentBufferSize = _bufferLeft;
}
picOutput.write(grayBuffer, _currentBufferSize);
writeCounter += _currentBufferSize;
}
picOutput.close();
LOGD("Child Thread - File %s writing done!\n", currentFileName.c_str());
if(g_DQ->IsWriteDone()){
LOGD("Child Thread - All file processed!\n");
break;
}
}
delete[](grayBuffer);
return;
}
int main(void){
const std::chrono::system_clock::time_point startTime = std::chrono::system_clock::now();
LOGD("Main Thread - #######Start#######\n");
std::thread t_writer(PicDataWrite);
PicDataRead();
LOGD("Main Thread - Waitting child thread...\n");
t_writer.join();
const std::chrono::system_clock::time_point endTime = std::chrono::system_clock::now();
const auto duration = std::chrono::duration_cast<std::chrono::microseconds>(endTime - startTime).count();
LOGD("Main Thread - All time consumed %.3f ms\n", duration * 1e-3);
LOGD("Main Thread - ####### End #######\n");
}