圖片和視頻編輯之旋轉角度問題

在作圖片和視頻編輯時,不可避免的是旋轉角度問題,這裏僅記錄下相關處理策略。html

圖片旋轉角度

獲取圖片旋轉角度

通常狀況下,Camera拍攝的圖片和視頻都存在旋轉角度問題,真正渲染時,須要進行旋轉操做。在Android平臺上能夠經過ExifInterface類獲取JPEG的EXIF信息,其中就包括了旋轉角度,以下所示:java

// 圖片旋轉角度,該角度表示在正常圖片的基礎上逆時針旋轉的角度
var degree = 0
val exifInterface = ExifInterface(path)
val orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
when (orientation) {
    ExifInterface.ORIENTATION_ROTATE_90 -> degree = 90
    ExifInterface.ORIENTATION_ROTATE_180 -> degree = 180
    ExifInterface.ORIENTATION_ROTATE_270 -> degree = 270
}
複製代碼

除此以外,簡單列舉幾個經過ExifInterface獲取的元數據:json

  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信息,能夠經過如下工具進行查看:canvas

EXIF(EXchangeable Image File Format)是專門爲數碼相機照片設定的可交換圖像文件格式,能夠記錄數碼照片的屬性信息和拍攝數據。app

經過Mac的顯示檢查器(打開圖片->工具->顯示檢查器)也能夠查看圖片的EXIF信息,以下所示。所以咱們須要順時針旋轉90度,以修正圖片,因此最終上屏的Size是3456*4608工具

處理圖片旋轉角度

上述獲取了圖片的逆時針旋轉角度degree,因此咱們須要順時針旋轉相同的角度,以修正圖片旋轉問題,而Matrix的旋轉API正好就是順時針旋轉,因此處理邏輯以下所示:post

val matrix = Matrix()
val originWidth = originBitmap.width
val originHeight = originBitmap.height
val originRectF = RectF(0f, 0f, originWidth.toFloat(), originHeight.toFloat())
val tempRectF = RectF()
// 首先圍繞圖片中心點旋轉上面獲取的degree(由於degree是逆時針旋轉角度,因此這裏須要順時針旋轉進行修正,而Matrix的setRotate正好是順時針旋轉)
matrix.setRotate(degree, originWidth / 2f, originHeight / 2f)
matrix.mapRect(tempRectF, originRectF)
// 修正旋轉致使的位移
matrix.postTranslate(0 - tempRectF.left, 0 - tempRectF.top)
複製代碼

所以,經過上述得到的matrix,去canvas.drawBitmap,就修正了圖片的旋轉問題。整個流程,能夠經過下面的示意圖描述: spa

處理圖片旋轉問題

上述degree表示在正常圖片的基礎上逆時針旋轉的角度,即爲了修正圖片,咱們須要順時針旋轉相同的角度。code

視頻旋轉角度

獲取視頻旋轉角度

val mediaPlayerWrapper = MediaMetadataRetriever()
mediaPlayerWrapper.setDataSource(inputPath)
// 視頻旋轉角度,該角度表示在正常視頻的基礎上逆時針旋轉的角度(與圖片旋轉角度含義一致) 
val rotation = mediaPlayerWrapper.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
// 配合上述旋轉角度的視頻寬度
val width = mediaPlayerWrapper.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
// 配合上述旋轉角度的視頻高度
val height = mediaPlayerWrapper.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
// 視頻時長
val duration = mediaPlayerWrapper.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
複製代碼

上述經過MediaMetadataRetriever獲取的rotation表示在正常視頻的基礎上逆時針旋轉的角度(與圖片旋轉角度degree含義一致)。orm

處理視頻旋轉角度

通常狀況下,播放器在硬解後拿到的OES Texture就是逆時針旋轉rotation以後紋理,因此咱們只要對紋理座標順時針旋轉相同角度就能夠了。

在對紋理座標進行旋轉時,有一個重要的差別點:屏幕紋理座標系和FBO紋理座標系的座標原點(0,0)在Y軸上是相反的,以下所示:

  • 屏幕紋理座標系
  • FBO紋理座標系

因此,渲染到FBO與屏幕時,須要使用不一樣的紋理座標。

假設頂點座標是

float pos[] = {
    -1.0f, 1.0f,
    -1.0f, -1.0f,
    1.0f, 1.0f,
    1.0f, -1.0f,
    };
複製代碼

如果渲染到FBO時,旋轉角度和紋理座標的對應關係以下所示:

val rotation = 90
val textureCoord = when (rotation) {
    90 -> floatArrayOf(
        1.0f, 1.0f,
        0.0f, 1.0f,
        1.0f, 0.0f,
        0.0f, 0.0f
        )
    180 -> floatArrayOf(
        1.0f, 0.0f,
        1.0f, 1.0f,
        0.0f, 0.0f,
        0.0f, 1.0f
        )
    270 -> floatArrayOf(
        0.0f, 0.0f,
        1.0f, 0.0f,
        0.0f, 1.0f,
        1.0f, 1.0f
        )
    // 0
    else -> floatArrayOf(
        0.0f, 1.0f,
        0.0f, 0.0f,
        1.0f, 1.0f,
        1.0f, 0.0f
        )
}
複製代碼

相反的,如果渲染到屏幕時,旋轉角度和紋理座標的對應關係以下所示:

val rotation = 90
val textureCoord = when (rotation) {
    90 -> floatArrayOf(
        0.0f, 1.0f,
        1.0f, 1.0f,
        0.0f, 0.0f,
        1.0f, 0.0f
        )
    180 -> floatArrayOf(
        1.0f, 1.0f,
        1.0f, 0.0f,
        0.0f, 1.0f,
        0.0f, 0.0f
        )
    270 -> floatArrayOf(
        1.0f, 0.0f,
        0.0f, 0.0f,
        1.0f, 1.0f,
        0.0f, 1.0f
        )
    // 0
    else -> floatArrayOf(
        0.0f, 0.0f,
        0.0f, 1.0f,
        1.0f, 0.0f,
        1.0f, 1.0f
        )
}
複製代碼

整個流程,能夠經過下面的示意圖描述:

視頻旋轉問題

上述rotation表示在正常視頻的基礎上逆時針旋轉的角度,即爲了修正視頻,咱們須要針對紋理座標順時針轉相同的角度(與處理圖片旋轉角度的方式一致)。

GLES20.glReadPixels

衆所周知,咱們能夠經過GLES20.glReadPixels從指定的Read Buffer中獲取一幀圖像。可是實踐中發現,當從FBO和屏幕Buffer(FBO0)中分別讀取幀數據時,獲得的圖像是鏡像關係,緣由就是咱們上面說起的***屏幕紋理座標系和FBO紋理座標系在Y軸上是反序的***。

首先看下如何讀取並保存幀數據,以下所示:

// 一幀圖像的Size
val buffer = ByteBuffer.allocateDirect(width * height * 4)
buffer.order(ByteOrder.LITTLE_ENDIAN)
GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer)
buffer.rewind()

// outputFile表示存儲的目標文件
val bos = BufferedOutputStream(FileOutputStream(outputFile))
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
// 填充像素數據
bmp.copyPixelsFromBuffer(buffer)
bmp.compress(Bitmap.CompressFormat.JPEG, 100, bos)
bmp.recycle()
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是反序的。

參考文章

相關文章
相關標籤/搜索