使用OpenGL渲染YUV数据的方法
本篇文章主要描述如何使用OpenGL ES来渲染i420(YUV420P)和nv21(YUV420SP)
首先准备yuv数据文件,使用ffmpeg对图片进行格式转换
原图大小为800x480:
ffmpeg转化为nv21和i420格式的yuv文件
// convert to nv21
ffmpeg -i test.png -s 800x480 -pix_fmt nv21 test.yuv
// convert to i420
ffmpeg -i test.png -s 800x480 -pix_fmt yuv420p yuv420p.yuv
在OpenGL中,片元着色器最后输出的都是rgba的数据,所以使用OpenGL来渲染YUV数据的关键还是将YUV数据传递给着色器,并在着色器中将YUV转化为RGB
在我们创建一个2D纹理并使用glTexImage2D来填充数据的时候可以指定internalformat
public static native void glTexImage2D(
int target, // 目标纹理,此处必须为GL_TEXTURE_2D
int level, // 执行细节级别,0是最基本的图像级别,n表示第N级贴图细化级别
int internalformat, // 指定纹理中的颜色组件
int width, // 指定纹理图像的宽度,
int height, // 指定纹理图像的高度,
int border, // 指定边框的宽度。必须为0
int format, // 像素数据的颜色格式
int type, // 指定像素数据的数据类型
java.nio.Buffer pixels // 指定内存中指向图像数据的指针
);
internalformat这个参数指定纹理中的颜色组件,可选的值有GL_RGB,GL_RGBA,GL_LUMINANCE,GL_LUMINANCE_ALPHA 等
通常使用的GL_RGBA这种internalformat,它会单独保存R,G,B,A四个数据,而在渲染YUV数据的时候,我们使用GL_LUMINANCE和GL_LUMINANCE_ALPHA
使用GL_LUMINANCE的时候,可以将Y分量存储到像素的各个通道内,这样在着色器中,我们可以通过R,G,B任意一个分量来获取到Y值。U,V分量同理
使用GL_LUMINANCE_ALPHA的时候,首先存储亮度,然后是alpha值,利用这一点可以将U值存储到像素的A通道,V值存储到R,G,B通道
渲染i420
在使用GL渲染i420格式的YUV数据时,需要使用三个2D纹理,每个纹理的颜色组件采用GL_LUMINANCE
private fun textureLuminance(imageData: ByteBuffer, width: Int, height: Int, textureId: Int) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexImage2D(
GLES20.GL_TEXTURE_2D, 0,
GLES20.GL_LUMINANCE, width, height, 0,
GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE, imageData
)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
}
首先把i420数据从文件中读取出来,然后创建3个2D纹理和buffer,并填充数据到buffer中,关键代码如下
// read i420 data
imageBytes = Util.read("yuv420p.yuv", context!!)
yBuffer = ByteBuffer.allocateDirect(imageWidth * imageHeight)
.order(ByteOrder.nativeOrder())
yBuffer.put(imageBytes, 0, imageWidth * imageHeight)
yBuffer.position(0)
// y texture
textureLuminance(yBuffer, imageWidth, imageHeight, yTextureId)
// u, v的流程是一样的,只是填充数据的时候要注意offset和纹理的宽高
uBuffer.put(imageBytes, imageWidth * imageHeight, imageWidth * imageHeight / 4)
textureLuminance(uBuffer, imageWidth / 2, imageHeight / 2, uTextureId)
vBuffer.put(imageBytes, imageWidth * imageHeight * 5 / 4, imageWidth * imageHeight / 4)
textureLuminance(vBuffer, imageWidth / 2, imageHeight / 2, vTextureId)
在渲染的时候,激活三个纹理单元并将纹理传递给着色器即可
在片元着色器中是如何从纹理中拿到Y,U,V分量的数据并且转化为R,G,B的呢?
从纹理中提取Y,U,V分量
// We had put the Y values of each pixel to the R, G, B components by GL_LUMINANCE,
// that's why we're pulling it from the R component, we could also use G or B
y = texture2D(yTexture, vTexCoord).r;
// u, v, same as above
u = texture2D(uTexture, vTexCoord).r;
v = texture2D(vTexture, vTexCoord).r;
YUV与RGB的互转公式
根据公式在片元着色器中进行YUV to RGB的转化
y = 1.164 * (y - 16.0 / 255.0);
u = u - 128.0 / 255.0;
v = v - 128.0 / 255.0;
r = y + 1.596 * v;
g = y - 0.391 * u - 0.813 * v;
b = y + 2.018 * u;
gl_FragColor = vec4(r, g, b, 1.0);
渲染nv21
在使用GL渲染nv21格式的YUV数据时,只需要使用两个2D纹理,Y分量纹理的颜色组件采用GL_LUMINANCE,UV分量纹理的颜色组件采用GL_LUMINANCE_ALPHA
private fun textureLuminanceAlpha(imageData: ByteBuffer, width: Int, height: Int, textureId: Int) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexImage2D(
GLES20.GL_TEXTURE_2D, 0,
GLES20.GL_LUMINANCE_ALPHA, width, height, 0,
GLES20.GL_LUMINANCE_ALPHA,
GLES20.GL_UNSIGNED_BYTE, imageData
)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
}
从文件中读取nv21数据,创建纹理和buffer,填充数据到buffer的流程和渲染i420的步骤是类似的,此处就不再赘述了
和渲染i420的片元着色器中唯一不同的就是获取U分量是从a通道获取
// We had put the U and V values of each pixel to the A and R, G, B components of the
// texture respectively using GL_LUMINANCE_ALPHA. Since U, V bytes are interspread
// in the texture
u = texture2D(uvTexture, vTexCoord).a;
v = texture2D(uvTexture, vTexCoord).r;
DEMO
代码传送门
https://github.com/sifutang/opengl.git
推荐阅读
-
YUV420和YUV422格式的数据采集和存储方法
-
将YUV420图像转换为BGR图像的方法使用OpenCvSharp实现
-
在uyuv转换为planar yuv420的过程中所使用的方法进行记录
-
重新渲染YUV420P格式的OpenGL
-
使用OpenGL渲染YUV数据的方法
-
渲染YUV数据的OpenGL实现
-
10bit YUV(P010)的存储结构和处理-随着计算机处理信息的能力越来越厉害,这种能表现更高动态范围的图像存储格式将会逐渐成为主流,但是现在很多算法都不能直接处理 10bit 的 YUV ,都是先将其转换为 8bit YUV ,然后再进行处理,这实际上是丢弃了 10bit YUV 的图像高动态范围优势。 令人遗憾的是在渲染图像时,目前 OpenGL 也无法直接对 10bit YUV 进行渲染,也是需要先转换为 8bit YUV 。 接下来以一种常见的 10bit YUV (P010) 格式为例,介绍一下 10bit YUV 到 8bit YUV 的转换过程。 P010 最早是微软定义的格式,表示的是 YUV 4:2:0 的采样方式,也就是说 P010 表示的是一类 YUV 格式,它的内存排布方式可能是 NVNVYUYV12 。
-
14-傅里叶变换的代码实现-一、numpy实现傅里叶变换和逆傅里叶变换 1.numpy实现傅里叶变换numpy.fft.fft2实现傅里叶变换,返回一个复数数组(complex ndarray),也就是频谱图像numpy.fft.fftshift将零频率分量移到频谱中心(将左上角的低频区域,移到中心位置) 20*np.log(np.abs(fshift))设置频谱的范围。可以理解为,之前通过傅里叶变换得到复数的数组,是不能通过图像的方法展示出来的,需要转换为灰度图像(映射到[0,255]区间)需要注意的是1> 傅里叶得到低频、高频信息,针对低频、高频处理能够实现不同的目的2> 傅里叶过程是可逆的,图像经过傅里叶变换、逆傅里叶变换后,能够恢复到原始图像3> 在频域对图像进行处理,在频域的处理会反映在逆变换图像上 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\lena.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 f = np.fft.fft2(img) # 移动中心位置 fshift = np.fft.fftshift(f) # 调整值范围 result = 20*np.log(np.abs(fshift)) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(result,cmap=plt.cm.gray) plt.title("result") plt.axis("off") plt.show 傅里叶变换的频谱图像: 2.numpy实现逆傅里叶变换numpy.fft.ifft2实现逆傅里叶变换,返回一个复数数组(complex ndarray)numpy.fft.ifftshiftfftshift函数的逆函数,将中心位置的低频,重新移到左上角iimg = np.abs(逆傅里叶变化结果)设置值的范围,映射到[0,255]区间 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\boat.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 f = np.fft.fft2(img) fshift = np.fft.fftshift(f) # 逆傅里叶变换 ishift = np.fft.ifftshift(fshift) iimg = np.fft.ifft2(ishift) iimg = np.abs(iimg) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(iimg,cmap=plt.cm.gray) plt.title("iimg") plt.axis("off") plt.show 将一副图像,进行傅里叶变换和逆傅里叶变换后,进行对比(一样的) 实例:通过numpy实现高通滤波,保留图像的边缘信息 获取图像的形状rows,cols = img.shape获取图像的中心点crow,ccol = int(rows/2),int(cols/2)将频谱图像的中心区域(低频区域)设置为0(黑色)fshift[crow-30:crow+30,ccol-30:ccol+30] = 0 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\boat.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 f = np.fft.fft2(img) fshift = np.fft.fftshift(f) # 高通滤波 rows,cols = img.shape crow,ccol = int(rows/2),int(cols/2) fshift[crow-30:crow+30,ccol-30:ccol+30] = 0 # 逆傅里叶变换 ishift = np.fft.ifftshift(fshift) iimg = np.fft.ifft2(ishift) iimg = np.abs(iimg) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(iimg,cmap=plt.cm.gray) plt.title("iimg") plt.axis("off") plt.show 使用numpy实现高通滤波的实验结果: 二、opencv实现傅里叶变换和逆傅里叶变换 1.opencv实现傅里叶变换 返回结果 = cv2.dft(原始图像,转换标识)1> 返回结果:是双通道的,第一个通道是结果的实数部分,第二个通道是结果的虚数部分2> 原始图像:输入图像要首先转换成np.float32(img)格式3> 转换标识:flags = cv2.DFT_COMPLEX_OUTPUT,输出一个复数阵列numpy.fft.fftshift将零频率分量移到频谱中心(将左上角的低频区域,移到中心位置)调整频谱的范围,将上面频谱图像的复数数组,转换为可以显示的灰度图像(映射到[0,255]区间)返回值 = 20*np.log(cv2.magnitude(参数1,参数2))1> 参数1:浮点型X坐标值,也就是实部2> 参数2:浮点型Y坐标值,也就是虚部 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\lena.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 dft = cv2.dft(np.float32(img),flags = cv2.DFT_COMPLEX_OUTPUT) # 移动中心位置 dftShift = np.fft.fftshift(dft) # 调整频谱的范围 result = 20*np.log(cv2.magnitude(dftShift[:,:,0],dftShift[:,:,1])) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(result,cmap=plt.cm.gray) plt.title("result") plt.axis("off") plt.show 傅里叶变换的频谱图像: 2.opencv实现逆傅里叶变换返回结果 = cv2.idft(原始数据)1> 返回结果:取决于原始数据的类型和大小2> 原始数据:实数或者复数均可numpy.fft.ifftshiftfftshift函数的逆函数,将中心位置的低频,重新移到左上角调整频谱的范围,映射到[0,255]区间返回值 = cv2.magnitude(参数1,参数2)1> 参数1:浮点型X坐标值,也就是实部2> 参数2:浮点型Y坐标值,也就是虚部 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\lena.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 dft = cv2.dft(np.float32(img),flags = cv2.DFT_COMPLEX_OUTPUT) dftShift = np.fft.fftshift(dft) # 逆傅里叶变换 ishift = np.fft.ifftshift(dftShift) iimg = cv2.idft(ishift) iimg = cv2.magnitude(iimg[:,:,0],iimg[:,:,1]) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(iimg,cmap=plt.cm.gray) plt.title("inverse") plt.axis("off") plt.show 将一副图像,进行傅里叶变换和逆傅里叶变换后,进行对比(一样的) 实例:通过opencv实现低通滤波,模糊一副图像
-
使用ifftshift在Matlab中对数据进行逆快速傅立叶变换的方法
-
Python | 使用 faker 库生成逼真的测试数据的方法-实例:批量生成测试数据