[关闭]
@ltlovezh 2019-01-18T14:46:02.000000Z 字数 4268 阅读 2009

图片和视频编辑之旋转角度问题

OpenGL 编辑


在做图片和视频编辑时,不可避免的是旋转角度问题,这里仅记录下相关处理策略。

图片旋转角度

获取图片旋转角度

一般情况下,Camera拍摄的图片和视频都存在旋转角度问题,真正渲染时,需要进行旋转操作。在Android平台上可以通过ExifInterface类获取JPEG的EXIF信息,其中就包括了旋转角度,如下所示:

  1. // 图片旋转角度,该角度表示在正常图片的基础上逆时针旋转的角度
  2. var degree = 0
  3. val exifInterface = ExifInterface(path)
  4. val orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
  5. when (orientation) {
  6. ExifInterface.ORIENTATION_ROTATE_90 -> degree = 90
  7. ExifInterface.ORIENTATION_ROTATE_180 -> degree = 180
  8. ExifInterface.ORIENTATION_ROTATE_270 -> degree = 270
  9. }

除此之外,简单列举几个通过ExifInterface获取的元数据:

  1. ExifInterface.TAG_ORIENTATION :逆时针的旋转角度
  2. ExifInterface.TAG_IMAGE_WIDTH :图片宽度(处理旋转角度之前的宽度,顺时针旋转扶正后的高度)
  3. ExifInterface.TAG_IMAGE_LENGTH :图片高度(处理旋转角度之前的高度,顺时针旋转扶正后的宽度)
  4. ExifInterface.TAG_DATETIME :拍摄时间,例如:2018:01:15 15:13:50
  5. ExifInterface.TAG_MODEL :设备型号,例如:ONEPLUS A6010
  6. ExifInterface.TAG_MAKE :设备生产厂商,例如:OnePlus

更详细的EXIF信息,可以通过以下工具进行查看:

EXIF(EXchangeable Image File Format)是专门为数码相机照片设定的可交换图像文件格式,可以记录数码照片的属性信息和拍摄数据。

通过Mac的显示检查器(打开图片->工具->显示检查器)也可以查看图片的EXIF信息,如下所示。因此我们需要顺时针旋转90度,以修正图片,所以最终上屏的Size是3456*4608

处理图片旋转角度

上述获取了图片的逆时针旋转角度degree,所以我们需要顺时针旋转相同的角度,以修正图片旋转问题,而Matrix的旋转API正好就是顺时针旋转,所以处理逻辑如下所示:

  1. val matrix = Matrix()
  2. val originWidth = originBitmap.width
  3. val originHeight = originBitmap.height
  4. val originRectF = RectF(0f, 0f, originWidth.toFloat(), originHeight.toFloat())
  5. val tempRectF = RectF()
  6. // 首先围绕图片中心点旋转上面获取的degree(因为degree是逆时针旋转角度,所以这里需要顺时针旋转进行修正,而Matrix的setRotate正好是顺时针旋转)
  7. matrix.setRotate(degree, originWidth / 2f, originHeight / 2f)
  8. matrix.mapRect(tempRectF, originRectF)
  9. // 修正旋转导致的位移
  10. matrix.postTranslate(0 - tempRectF.left, 0 - tempRectF.top)

因此,通过上述获得的matrix,去canvas.drawBitmap,就修正了图片的旋转问题。整个流程,可以通过下面的示意图描述:
处理图片旋转问题

上述degree表示在正常图片的基础上逆时针旋转的角度,即为了修正图片,我们需要顺时针旋转相同的角度。

视频旋转角度

获取视频旋转角度

  1. val mediaPlayerWrapper = MediaMetadataRetriever()
  2. mediaPlayerWrapper.setDataSource(inputPath)
  3. // 视频旋转角度,该角度表示在正常视频的基础上逆时针旋转的角度(与图片旋转角度含义一致)
  4. val rotation = mediaPlayerWrapper.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
  5. // 配合上述旋转角度的视频宽度
  6. val width = mediaPlayerWrapper.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
  7. // 配合上述旋转角度的视频高度
  8. val height = mediaPlayerWrapper.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
  9. // 视频时长
  10. val duration = mediaPlayerWrapper.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)

上述通过MediaMetadataRetriever获取的rotation表示在正常视频的基础上逆时针旋转的角度(与图片旋转角度degree含义一致)。

处理视频旋转角度

一般情况下,播放器在硬解后拿到的OES Texture就是逆时针旋转rotation之后纹理,所以我们只要对纹理坐标顺时针旋转相同角度就可以了。

在对纹理坐标进行旋转时,有一个重要的差异点:屏幕纹理坐标系和FBO纹理坐标系的坐标原点(0,0)在Y轴上是相反的,如下所示:

所以,渲染到FBO与屏幕时,需要使用不同的纹理坐标。

假设顶点坐标是

  1. float pos[] = {
  2. -1.0f, 1.0f,
  3. -1.0f, -1.0f,
  4. 1.0f, 1.0f,
  5. 1.0f, -1.0f,
  6. };

若是渲染到FBO时,旋转角度和纹理坐标的对应关系如下所示:

  1. val rotation = 90
  2. val textureCoord = when (rotation) {
  3. 90 -> floatArrayOf(
  4. 1.0f, 1.0f,
  5. 0.0f, 1.0f,
  6. 1.0f, 0.0f,
  7. 0.0f, 0.0f
  8. )
  9. 180 -> floatArrayOf(
  10. 1.0f, 0.0f,
  11. 1.0f, 1.0f,
  12. 0.0f, 0.0f,
  13. 0.0f, 1.0f
  14. )
  15. 270 -> floatArrayOf(
  16. 0.0f, 0.0f,
  17. 1.0f, 0.0f,
  18. 0.0f, 1.0f,
  19. 1.0f, 1.0f
  20. )
  21. // 0
  22. else -> floatArrayOf(
  23. 0.0f, 1.0f,
  24. 0.0f, 0.0f,
  25. 1.0f, 1.0f,
  26. 1.0f, 0.0f
  27. )
  28. }

相反的,若是渲染到屏幕时,旋转角度和纹理坐标的对应关系如下所示:

  1. val rotation = 90
  2. val textureCoord = when (rotation) {
  3. 90 -> floatArrayOf(
  4. 0.0f, 1.0f,
  5. 1.0f, 1.0f,
  6. 0.0f, 0.0f,
  7. 1.0f, 0.0f
  8. )
  9. 180 -> floatArrayOf(
  10. 1.0f, 1.0f,
  11. 1.0f, 0.0f,
  12. 0.0f, 1.0f,
  13. 0.0f, 0.0f
  14. )
  15. 270 -> floatArrayOf(
  16. 1.0f, 0.0f,
  17. 0.0f, 0.0f,
  18. 1.0f, 1.0f,
  19. 0.0f, 1.0f
  20. )
  21. // 0
  22. else -> floatArrayOf(
  23. 0.0f, 0.0f,
  24. 0.0f, 1.0f,
  25. 1.0f, 0.0f,
  26. 1.0f, 1.0f
  27. )
  28. }

整个流程,可以通过下面的示意图描述:
视频旋转问题

上述rotation表示在正常视频的基础上逆时针旋转的角度,即为了修正视频,我们需要针对纹理坐标顺时针转相同的角度(与处理图片旋转角度的方式一致)。

GLES20.glReadPixels

众所周知,我们可以通过GLES20.glReadPixels从指定的Read Buffer中获取一帧图像。但是实践中发现,当从FBO和屏幕Buffer(FBO0)中分别读取帧数据时,得到的图像是镜像关系,原因就是我们上面提及的屏幕纹理坐标系和FBO纹理坐标系在Y轴上是反序的

首先看下如何读取并保存帧数据,如下所示:

  1. // 一帧图像的Size
  2. val buffer = ByteBuffer.allocateDirect(width * height * 4)
  3. buffer.order(ByteOrder.LITTLE_ENDIAN)
  4. GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer)
  5. buffer.rewind()
  6. // outputFile表示存储的目标文件
  7. val bos = BufferedOutputStream(FileOutputStream(outputFile))
  8. val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
  9. // 填充像素数据
  10. bmp.copyPixelsFromBuffer(buffer)
  11. bmp.compress(Bitmap.CompressFormat.JPEG, 100, bos)
  12. bmp.recycle()
  13. bos.close()

然后看下真实的案例:

  1. 首先,把一张纹理绘制到FBO(按照纹理坐标原点在左下角),此时FBO中的纹理数据应该是正常的(非反序)。
  2. 其次,通过GLES20.glReadPixels从FBO中读取并保存帧数据,得到图片1
  3. 然后,把FBO对应的纹理绘制到屏幕(按照纹理坐标原点在左上角),此时屏幕上的纹理数据也应该是正常的(非反序)。
  4. 最后,通过GLES20.glReadPixels从屏幕(FBO0)中读取并保存帧数据,得到图片2
  5. 截取屏幕图像得到图片0,与图像1和图像2进行对比

最后我们来看下几张图片的对比:
首先是最终上屏的图片0,如下所示:

然后是从FBO读取并保存的图片1,如下所示:

最后是从屏幕(FBO0)读取并保存的图片2,如下所示:

可见,从FBO中读取的图片1是正常的,而从屏幕(FBO0)读取的图片2是反序的。

参考文章

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注