前言
提一下这篇文章的需求:将USB相机获取到的YUV数据进行旋转,然后转为QImage进行显示。原本程序中是有旋转的代码,但不知道为什么,旋转出来的图片会花屏。关于花屏的问题,后面会稍微阐述一下。所以,经过查询各种资料,终于将这个问题给解决了,所以,自己也稍微总结一下解决问题的过程,以及最终的结果。
正文
一、环境
基本上这个的旋转跟你机器的环境也没多大关系,主要还是跟你YUV数据的格式有关系。
我所使用的环境是ARM+Qt+Win10
二、解决过程
1、解决思路
首先,我们解决问题,一定要明确自己解决问题的思路,才能不浪费时间。
起初对图片旋转的问题,没有一个清晰的解题思路,只知道不断的尝试各种旋转算法,导致浪费了部分时间,网上单独查询到的资料基本都是NV21或是NV12的图片旋转算法,所以,大部分都不可用。
最终梳理了一下,采取了以下流程解决了图片旋转的问题:
1、先将采集出的YUV数据保存成文件->
2、使用YUV Viwer进行显示,确认属于哪种格式的YUV数据。->
3、在网上搜索对应格式的YUV数据的旋转方式->
4、将YUV经过上一步的YUV数据旋转算法旋转->
5、保存旋转后的YUV数据、确保旋转后的YUV数据时没问题->
6、然后将YUV转QImage ,这里需注意,若旋转90度或270度,生成QImage时要注意宽高对调,否则会产生花屏。
我也大概按照这样的思路,来阐述我解决问题的思路。
2、如何保存采集到的YUV数据
以下有两种保存方式:
第一:把文件名称、YUV数据、YUV数据的宽高都传进去。
void CUsbVideo::_Yuv420pSaveFile(QString _sFileName, unsigned char *_inFile, int _iWidth, int _iHeight)
{
#if 1
FILE *pFileOut;
// _sFileName = QString("/ics/File/test.yuv");
std::string fileName = _sFileName.toStdString();
if (NULL == (pFileOut = fopen(fileName.c_str(), "wb")))
{
LOG_INFO << "File output can not open";
fclose(pFileOut);
return;
}
int iBufLen = 0;
iBufLen = _iWidth * _iHeight * 3 /2;//注意,这里YUV数据的大小一般是3/2 具体原因可以网上查下。
fwrite(_inFile, iBufLen*sizeof(unsigned char), 1, pFileOut);
fclose(pFileOut);
#endif
}
关于上面的fopen 也有一些知识:
以上的w和r分别代表写和读,但是这个是只写和只读,那要同时进行读写怎么办呢?没办法,那再来一个参数吧,于是+就出现了,于是r+、w+、a+就出现了,对应二进制文件就是rb+、rw+、ab+了。
1、采用r、rb、r+、rb+方式打开文件的时候,文件必须已经存在,否则会出错
2、采用w、wb、w+、wb+方式打开文件的时候,如果文件不存在,则新建该文件;如果文件已经存在,则删除该文件再重新建立
3、采用a、ab、a+、ab+方式打开文件,文件必须已经存在,否则出错。
3、使用YUV Viwer进行显示
将保存下的数据使用YUV Viewer进行显示。
YUView 2.13 这个版本的也不错
我是用上面这个
还有下面这个:
YUV-Viewer工具的下载
这两个工具好像不太一样,可以都下来看一下。
还有一个软件叫7yuv
使用软件去显示保存下来的Yuv文件,确定对应的类型。
4、YUV420 I420的旋转方式
我这边主要用到的是I420的yuv数据旋转方式,网上很多查询到的是NV21或是NV12的数据类型的旋转算法。都不太适用,都会造成花屏。
底下,直接贴算法了,包含了四种,90度,180度,270度,镜像:
//输入:_pYUV 输出:_pYUV180
void CUsbVideo::_Yuv420I420Rotate180(unsigned char* _pYUV, unsigned char* _pYUV180)
{
int width = m_iWidth;
int height = m_iHeight;
uint8_t* src_y = _pYUV;
uint8_t* src_u = _pYUV + width * height;
uint8_t* src_v = _pYUV + width * height + width * height / 4;
uint8_t* dst_y = _pYUV180;
uint8_t* dst_u = _pYUV180 + width * height;
uint8_t* dst_v = _pYUV180 + width * height + width * height / 4;
int index = 0;
for (int i = width * height-1; i >= 0 ; --i)
{
dst_y[index++] = src_y[i];
}
int uv_index = 0;
for (int i = width / 2 * height / 2; i >= 0; i--)
{
dst_u[uv_index] = src_u[i];
dst_v[uv_index] = src_v[i];
uv_index++;
}
}
void CUsbVideo::_Yuv420I420Rotate270(unsigned char* _pYUV, unsigned char* _pYUV270)
{
int width = m_iWidth;
int height = m_iHeight;
uint8_t* src_y = _pYUV;
uint8_t* src_u = _pYUV + width * height;
uint8_t* src_v = _pYUV + width * height + width * height / 4;
uint8_t* dst_y = _pYUV270;
uint8_t* dst_u = _pYUV270 + width * height;
uint8_t* dst_v = _pYUV270 + width * height + width * height / 4;
int index = 0;
for (int i = width-1; i >= 0; --i)
{
for (int j = 0; j < height; ++j) {
dst_y[index++] = src_y[j * width+i];
}
}
int uv_index = 0;
for (int i = width / 2-1; i >= 0; i--) {
for (int j = 0; j< height / 2; j++)
{
dst_u[uv_index] = src_u[width / 2 * j+i];
dst_v[uv_index] = src_v[width / 2 * j+ i];
uv_index++;
}
}
}
void CUsbVideo::_Yuv420I420Rotate90(unsigned char* _pYUV, unsigned char* _pYUV90)
{
int width = m_iWidth;
int height = m_iHeight;
uint8_t* src_y = _pYUV;
uint8_t* src_u = _pYUV+ width* height;
uint8_t* src_v = _pYUV + width * height + width * height/4;
uint8_t* dst_y = _pYUV90 ;
uint8_t* dst_u = _pYUV90 + width * height;
uint8_t* dst_v = _pYUV90 + width * height + width * height / 4;
int index = 0;
for (int i = 0; i < width; ++i) {
for (int j = height - 1; j >= 0; --j) {
dst_y[index++] = src_y[j * width + i];
}
}
int uv_index = 0;
for (int i = 0; i < width / 2; i++) {
for (int j = height / 2 - 1; j >= 0; j--)
{
dst_u[uv_index] = src_u[width / 2 * j + i];
dst_v[uv_index] = src_v[width / 2 * j + i];
uv_index++;
}
}
}
void CUsbVideo::_Yuv420I420RotateMirror(unsigned char* _pYUV, unsigned char* _pYUVMirror)
{
int width = m_iWidth;
int height = m_iHeight;
uint8_t* src_y = _pYUV;
uint8_t* src_u = _pYUV + width * height;
uint8_t* src_v = _pYUV + width * height + width * height / 4;
uint8_t* dst_y = _pYUVMirror;
uint8_t* dst_u = _pYUVMirror + width * height;
uint8_t* dst_v = _pYUVMirror + width * height + width * height / 4;
for (int j = 0; j < height; ++j) {
for (int i = 0; i < width; i++)
{
dst_y[j * width + width-1-i] = src_y[j * width + i];
}
}
for (int j = 0; j < height / 2; ++j) {
for (int i = 0; i < width / 2; i++)
{
dst_u[j * width / 2 + width / 2-1 - i] = src_u[j * width / 2 + i];
dst_v[j * width / 2 + width / 2-1 - i] = src_v[j * width / 2 + i];
}
}
}
调用方式:
char* m_pData; ///<图像数据 YUV数据
m_pConverData = new unsigned char[m_iWidth * m_iHeight * 3/2];
_Yuv420I420Rotate90((unsigned char*)m_pData, (unsigned char*)m_pConverData);
5、将旋转后的YUV数据转为QImage进行显示
void CQueryVideoDialog::SLOT_NewImage(const QByteArray &_oYuv420, int _iWidth, int _iHeight)
{
QMutexLocker oLocker(&m_mutex);
//注意这里的宽高一定要进行对调,也就是创建QImage的宽高一定要进行对调,否则,就会产生花屏
m_iWidth = _iHeight;
m_iHeight = _iWidth;
if (m_pRGBData == nullptr)
{
m_pRGBData = new uchar[_iWidth * _iHeight * 3];
m_oImage = std::move(QImage(m_pRGBData, m_iWidth, m_iHeight, QImage::Format_RGB888));
}
m_iIndex++;
CPixelFormatConverter::YUV420ToRGB24((uchar*)_oYuv420.data(), m_iWidth, m_iHeight, &m_pRGBData);
m_pwgtCamera->SLOT_NewFrame(m_oImage);
if (!m_oWatcher.isFinished())
{
return;
}
QImage oImg = m_oImage.copy();
if (oImg.isNull())
{
LOG_INFO << "--> CQueryVideoDialog::SLOT_NewImage oImg is Null";
return;
}
auto fn = [this, oImg]() {
QVector<QRect> vtFaceRectPos;
auto oRet = gKldFaceDevice::instance()->DetectFace(oImg, vtFaceRectPos);
if (oRet.first && false == m_bStop)
{
QMetaObject::invokeMethod(m_pTimer, "start");
m_bStop = true;
}
QMetaObject::invokeMethod(this, "_ShowFaceRect",Q_ARG(QVector<QRect>, vtFaceRectPos));
};
m_oWatcher.setFuture(QtConcurrent::run(fn));
}
int CPixelFormatConverter::YUV420ToRGB24(const unsigned char* data, int width, int height,unsigned char** pOutData)
{
int iRet = 0;
int dstStride[4];
av_image_fill_linesizes(dstStride, AV_PIX_FMT_RGB24, width);
AVFrame* pFrameYuv = av_frame_alloc();
av_image_fill_linesizes(pFrameYuv->linesize, AV_PIX_FMT_YUV420P, width);
av_image_fill_arrays(pFrameYuv->data, pFrameYuv->linesize, data, AV_PIX_FMT_YUV420P, width, height, 1);
SwsContext * pSwsContext = nullptr;
pSwsContext = sws_getCachedContext(pSwsContext, width, height, AV_PIX_FMT_YUV420P, width, height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
iRet = sws_scale(pSwsContext, pFrameYuv->data, pFrameYuv->linesize, 0, height, pOutData, dstStride);
sws_freeContext(pSwsContext);
av_frame_free(&pFrameYuv);
return iRet >= 0 ? 0 : -1;
}
主要遇到的花屏的问题都是因为宽高的问题,宽高没有适当的对调,当对YUV数据进行90度和270度旋转的时候,一定要对宽高进行对调
。
基本上我这边使用的旋转的主题的流程就都在上面了,下面就稍微介绍一下我在解决问题的过程中,所记录下来的一些知识,也稍微扩展一下知识面。
三、知识扩展
1、YUV 的相关知识
1、YUV数据的相关格式可以看这篇文章,总结的很好:详解 YUV 格式(I420/YUV420/NV12/NV12/YUV422)
2、我的I420的图像旋转主要就是参考这篇文章:视音频数据处理入门:I420 图像90° 、180°、270°旋转和左右镜像翻转
3、主要参考这篇文章:c++ 读取 yuv 图片_从YUV到RGB 主流的YUV420的格式:
这些主流YUV格式中,YUV420更常见(因为更节省空间啊),而从YUV420这种采样格式引申出来,叠加上不同的存储格式,就又可以细分为很多。Gemfield没有精力介绍这么多,你可以参考:http://www.fourcc.org/yuv.php 。
当YUV420采样格式叠加上planar 储存格式后,可以产生出YUV420P和YUV420SP格式。YUV420P是Y之后U之后V来储存,相当于RGB中的chw了,又可以细分为I420(又叫YU12)和YV12格式;而YUV420SP是Y之后UV交替…,又可以细分为NV12和NV21格式。这四种细分格式如下所示:
- NV12 ,FourCC为0x3231564E ,1个像素12 bit, 8-bit Y plane,Y通道平面结束后是交叉进行的 U/V 通道平面,U/V使用2x2的采样率(各是Y的四分之一)。
- NV21 ,FourCC为0x3132564E ,1个像素12 bit, 和NV12一样——除了U/V交叉的时候是先V再U,也就是U/V和V/U的区别;这个是Android上摄像头图像的标准;
- I420 (也叫YU12),FourCC为0x30323449 ,1个像素12 bit,8 bit Y 通道平面结束后,是U通道平面,最后是V通道平面;
- YV12 ,FourCC为0x32315659,1个像素12 bit,8 bit Y 通道平面结束后,是V通道平面,最后是U通道平面;
4、YUV420的图片旋转:
1、Y表示亮度(Luminance),也就是灰度值。 "U"和"V"表示的则是色度(Chrominance),描述影像色彩及其饱和度,用于指定像素的颜色。
2、YUV的四个结论:
三个结论先记好:
1.YUV 4:4:4采样,每一个Y对应一组UV分量。
2.YUV 4:2:2采样,每两个Y共用一组UV分量。
3.YUV 4:2:0采样,每四个Y共用一组UV分量。3、特别提醒旋转90和270后宽高要记得对调,不然会花屏
5、花屏问题该如何解决:音视频开发之视频花屏问题汇总分析
除了需要注意的,旋转的时候,一定要注意宽高对调。其他的问题,可以从上面找找答案。
2、YUV420sp 图片旋转90度
网上应该可以找到更好的,这个是我找到的,这里只是做一个记录,只有一个90度的:
void CUsbVideo::_Yuv420spRotate90(unsigned char* _pYUV, unsigned char* _pYUV90)
{
int width = m_iHeight;
int height = m_iWidth;
int count = 0;
int uvHeight = height >> 1;
int imgSize = width * height;
// byte[] des = new byte[imgSize * 3 >> 1];
//copy y
for (int j = width - 1; j >= 0; j--) {
for (int i = 0; i < height; i++) {
_pYUV90[count++] = _pYUV[width * i + j];
}
}
//u,v
for (int j = width - 1; j > 0; j -= 2) {
for (int i = 0; i < uvHeight; i++) {
_pYUV90[count++] = _pYUV[imgSize + width * i + j - 1];
_pYUV90[count++] = _pYUV[imgSize + width * i + j];
}
}
}
3、YUV NV21或NV12的旋转
这个代码没有测试过,不保真,麻烦自己测试下,有啥问题,概不负责。
void CUsbVideo::_Yuv420Rotate180(unsigned char* _pYUV, unsigned char* _pYUV180)
{
//旋转180:将右下角的点作为第一个点,从右往左,从下往上取点
//Y 宽:[0,w-1] 高:[0,h-1]
int idx = 0;
for (int i = m_iHeight-1; i >=0; i--){
for (int j = m_iWidth-1 ; j >= 0; j--){
_pYUV180[idx++] = *(_pYUV+(i*m_iWidth+j));
}
}
uint8_t* uheader = _pYUV + m_iWidth*m_iHeight;
uint8_t* vheader = uheader + m_iWidth*m_iHeight/4;
int iVSize = m_iWidth*m_iHeight/4;
//U
for (int i = m_iHeight/2 - 1; i >= 0; i--){
for (int j = m_iWidth/2-1 ; j >= 0; j--){
_pYUV180[idx] = *(uheader + (i*m_iWidth / 2 + j));
_pYUV180[idx++ + iVSize] = *(vheader + (i*m_iWidth / 2 + j));
}
}
}
void CUsbVideo::_Yuv420RotateFlip(unsigned char* _pYUV, unsigned char* _pYUVFlip)
{
int idx = 0;
//水平翻转:将右上角的点作为第一个点,从右往左,从上往下取点
//Y 宽:[0,w-1] 高:[0,h-1]
for (int i = 0; i<m_iHeight; i++){
for (int j = m_iWidth - 1; j >= 0; j--){
_pYUVFlip[idx++] = *(_pYUV + (i*m_iWidth + j));
}
}
uint8_t* uheader = _pYUV + m_iWidth*m_iHeight;
uint8_t* vheader = uheader + m_iWidth*m_iHeight / 4;
int iVSize = m_iWidth*m_iHeight / 4;
//U
for (int i = 0; i < m_iHeight / 2 ; i++){
for (int j = m_iWidth / 2 - 1; j >= 0; j--){
_pYUVFlip[idx] = *(uheader + (i*m_iWidth / 2 + j));
_pYUVFlip[idx++ + iVSize] = *(vheader + (i*m_iWidth / 2 + j));
}
}
}
void CUsbVideo::_Yuv420Rotate270(unsigned char* _pYUV, unsigned char* _pYUV270)
{
// Rotate the Y luma
int width = m_iHeight;
int height = m_iWidth;
int count = 0;
int uvHeight = height >> 1;
//copy y
for (int j = width - 1; j >= 0; j--) {
for (int i = 0; i < height; i++) {
_pYUV270[count++] = _pYUV[width * i + j];
}
}
//u,v
uint8_t* uheader = _pYUV + m_iWidth * m_iHeight;
uint8_t* vheader = uheader + m_iWidth * m_iHeight / 4;
int iVSize = m_iWidth*m_iHeight / 4;
width = width / 2;
for (int j = width - 1; j > 0; j--) {
for (int i = 0; i < uvHeight; i++) {
_pYUV270[count] = uheader[width * i + j];
_pYUV270[count++ + iVSize] = vheader[+ width * i + j];
}
}
}
void CUsbVideo::_Yuv420Rotate90(unsigned char* _pYUV, unsigned char* _pYUV90)
{
// Rotate the Y luma
int width = m_iHeight;
int height = m_iWidth;
int count = 0;
int uvHeight = height >> 1;
//copy y
for (int j = 0; j < width; j++) {
for (int i = 0; i < height; i++) {
_pYUV90[count++] = _pYUV[width * i + j];
}
}
//u,v
uint8_t* uheader = _pYUV + m_iWidth * m_iHeight;
uint8_t* vheader = uheader + m_iWidth * m_iHeight / 4;
int iVSize = m_iWidth*m_iHeight / 4;
width = width / 2;
for (int j = 0; j < width; j++) {
for (int i = 0; i < uvHeight; i++) {
_pYUV90[count] = uheader[width * i + j];
_pYUV90[count++ + iVSize] = vheader[+ width * i + j];
}
}
}
4、使用OpenCV进行图像翻转
Mat angleRectify(Mat img, float angle)
{
cout << "--> angleRectify:" << img.rows << img.cols;
Mat retMat = Mat::zeros(img.cols, img.rows, CV_8UC3);
float anglePI = (float)(angle * CV_PI / 180);
int xSm, ySm;
for (int i = 0; i < retMat.rows; i++)
for (int j = 0; j < retMat.cols; j++)
{
xSm = (int)((i - retMat.rows / 2) * cos(anglePI) - (j - retMat.cols / 2) * sin(anglePI) + 0.5);
ySm = (int)((i - retMat.rows / 2) * sin(anglePI) + (j - retMat.cols / 2) * cos(anglePI) + 0.5);
xSm += img.rows / 2;
ySm += img.cols / 2;
if (xSm >= img.rows || ySm >= img.cols || xSm <= 0 || ySm <= 0) {
retMat.at<Vec3b>(i, j) = Vec3b(0, 0);
}
else {
retMat.at<Vec3b>(i, j) = img.at<Vec3b>(xSm, ySm);
}
}
return retMat;
}
Mat dstImg2 = angleRectify(srcImg, 270);
imshow("src", srcImg);