一、前言
最近需要yuv文件作为素材,但是在网上没有找到对应格式的。只有420p格式的,我需要422p和444p,本来想的是自己写一个420p转422p的函数,但是那样素材就太少了,只能下载已知,想到以前使用过的opencv库,决定自己将图像文件转为yuv文件,这样素材就丰富了。由于习惯使用Qt编程,所以下列的代码都是用Qt,图片素材我使用的都是jpg格式的图片,所以这里的图片也都是用JPG文件。
二、open CV库的安装
关于这个,请看我以前写的一篇文章,里面有提到:
QT QPixmap或者QImage加载图片程序异常结束问题(code: 0xc0000602: ,)本文讲述了,当图片 - 掘金 (juejin.cn)
三、yuv文件格式解析
关于YUV数据格式分类,可以看下面这篇文章,是我看过的几篇文章中讲解的最清晰的了:
图像原始格式 YUV444 YUV422 YUV420 详细解析-CSDN博客
四、图片转YUV422p
根据上面的介绍,假设图片为1*4,图片数据格式为YYYYUUVV,将mat数据按像素解析,422p的格式取出填入yuv文件即可,函数封装如下:
C++
bool jpegFileToYUV422pFile(QString jFileName, QString yuvFileName)
{
// 读取JPEG图像
cv::Mat image = cv::imread(jFileName.toStdString());
if (image.empty()) {
qDebug() << "无法读取图片";
return false;
}
// 调整图像宽度为偶数,以便处理 YUV422 格式
if (image.cols % 2 != 0) {
cv::resize(image, image, cv::Size(image.cols - 1, image.rows));
}
// 转换为YUV格式
cv::Mat yuvImage;
cv::cvtColor(image, yuvImage, cv::COLOR_BGR2YUV);
QFile file(yuvFileName);
if (!file.open(QIODevice::WriteOnly)) {
qDebug() << "无法打开文件" << yuvFileName;
return false;
}
int width = yuvImage.cols;
int height = yuvImage.rows;
qDebug() << width << height << yuvImage.total() * yuvImage.elemSize();
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; ++j) {
cv::Vec3b yuvPixel = yuvImage.at<cv::Vec3b>(i, j);
unsigned char Y = yuvPixel[0]; // Y分量
file.write((const char*)&Y, 1);
}
}
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; j += 2) {
cv::Vec3b yuvPixel = yuvImage.at<cv::Vec3b>(i, j);
unsigned char U = yuvPixel[1]; // U分量
file.write((const char*)&U, 1);
}
}
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; j += 2) {
cv::Vec3b yuvPixel = yuvImage.at<cv::Vec3b>(i, j);
unsigned char V = yuvPixel[2]; // V分量
file.write((const char*)&V, 1);
}
}
file.close();
qDebug() << "文件保存完成:" << yuvFileName;
return true;
}
五、YUV422p转图片
还是一样,假设图片为1*4,图片数据格式为YYYYUUVV,然后读出数据解析即可得到Y、U、V各个分量的数据,然后填充在Mat中,函数如下:
C++
bool yuv422pFileToJpegFile(QString yuvFileName, QString jFileName, int width, int height)
{
FILE *yuvFile = fopen(yuvFileName.toStdString().c_str(), "rb");
if (!yuvFile) {
perror("Failed to open YUV file");
return false;
}
// YUV422p: Y plane size = width * height, U and V plane size = (width/2) * height
size_t ySize = width * height;
size_t uvSize = (width / 2) * height;
size_t frameSize = width * height * 2; // Since YUV422p uses 2 bytes per pixel
unsigned char *yuvData = (unsigned char*)malloc(frameSize);
unsigned char *pYBuffer = (unsigned char*)malloc(ySize);
unsigned char *pUBuffer = (unsigned char*)malloc(uvSize);
unsigned char *pVBuffer = (unsigned char*)malloc(uvSize);
if (!yuvData || !pYBuffer || !pUBuffer || !pVBuffer) {
perror("Failed to allocate memory for YUV data");
fclose(yuvFile);
return false;
}
// Read the YUV data from the file
size_t readSize = fread(yuvData, 1, frameSize, yuvFile);
if (readSize != frameSize) {
perror("Failed to read complete YUV frame");
free(yuvData);
free(pYBuffer);
free(pUBuffer);
free(pVBuffer);
fclose(yuvFile);
return false;
}
memcpy(pYBuffer, yuvData, ySize);
memcpy(pUBuffer, yuvData + ySize, uvSize);
memcpy(pVBuffer, yuvData + ySize + uvSize, uvSize);
free(yuvData);
fclose(yuvFile);
// 创建一个存放YUV422的Mat
cv::Mat yuvImage(height, width, CV_8UC2); // 2通道的 YUV422p 格式
// 填充YUV422p数据
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; j += 2) {
int yIndex1 = i * width + j;
int yIndex2 = yIndex1 + 1;
int uvIndex = (i * (width / 2)) + (j / 2);
// Y 分量
yuvImage.at<cv::Vec2b>(i, j)[0] = pYBuffer[yIndex1]; // 第一个像素的Y
yuvImage.at<cv::Vec2b>(i, j+1)[0] = pYBuffer[yIndex2]; // 第二个像素的Y
// U、V 分量交错存放
yuvImage.at<cv::Vec2b>(i, j)[1] = pUBuffer[uvIndex]; // U分量
yuvImage.at<cv::Vec2b>(i, j+1)[1] = pVBuffer[uvIndex]; // V分量
}
}
free(pYBuffer);
free(pUBuffer);
free(pVBuffer);
// 将YUV422p转换为BGR格式
cv::Mat bgrImage;
cv::cvtColor(yuvImage, bgrImage, cv::/*COLOR_YUV2BGR_Y422*/COLOR_YUV2BGR_YUYV); // YUV422 to BGR
// 将BGR格式保存为JPEG
if (cv::imwrite(jFileName.toStdString(), bgrImage)) {
// qDebug() << "图片保存失败!";
// return false;
}
return true;
}
六、cv::Mat 的介绍
如上述,每个像素都有三个分量值,分别是Y、U、V分量,如果为图片文件,需要cv::Mat转换成功后才可正确拿取,代码介绍:
C++
// cv::Mat image 转换为 YUV 格式,方便拿取YUV数据
cv::Mat yuvImage;
cv::cvtColor(image, yuvImage, cv::COLOR_BGR2YUV);
// 拿取其第i行,j列的数据,i取值0->height,j取值0->width
cv::Vec3b yuvPixel = yuvImage.at<cv::Vec3b>(i, j);
unsigned char Y = yuvPixel[0]; // Y分量
unsigned char U = yuvPixel[1]; // U分量
unsigned char V = yuvPixel[2]; // V分量
如果是YUV文件,需要初始化对应的cv::Mat对象,再将其数据填充,使用422p为例,代码解释如下:
C++
// 创建一个存放YUV422的Mat
cv::Mat yuvImage(height, width, CV_8UC2); // 2通道的 YUV422p 格式
//同样使用i行j列为例
// Y 分量
yuvImage.at<cv::Vec2b>(i, j)[0] = pYBuffer[yIndex1]; // 第一个像素的Y
yuvImage.at<cv::Vec2b>(i, j+1)[0] = pYBuffer[yIndex2]; // 第二个像素的Y
// 422p格式的数据在CV_8UC2图片中保存为,U、V 分量交错存放
yuvImage.at<cv::Vec2b>(i, j)[1] = pUBuffer[uvIndex]; // U分量
yuvImage.at<cv::Vec2b>(i, j+1)[1] = pVBuffer[uvIndex]; // V分量
七、结语
还有很多其他格式的YUV数据互相转换为图片文件,例如444p、420p、422YUYV、NV12等等,就不一一列举了,清楚YUV对应的数据结构后,按照对应格式解析、填充即可。关于编程语言和图片格式,也可以使用其他的,只要Mat对象正常初始化即可。
有问题欢迎留言。