Theo
Theo

Reputation: 4015

AVAssetTrack's preferredTransform sometimes seems to be wrong. How can this be fixed?

I'm using AVAssetExportSession to export videos in an iOS app. To render the videos in their correct orientation, I'm using AVAssetTrack's preferredTransform. For some source videos, this property seems to have a wrong value, and the video appears offset or completely black in the result. How can I fix this?

Upvotes: 8

Views: 2313

Answers (3)

Tom Wilson
Tom Wilson

Reputation: 159

Building on to Theo's excellent answer, I will share a couple other helpful "fixed" variables that I created in the same style as his example, which I believe you will need in order to correctly handle the data.

Also, in my case I found that the "preferred transform" in the right/left/rightMirrored/leftMirrored cases were actually reversed from what worked for me (ie: I had a rightMirrored case but when applying Theo's logic (which seemed correct mathematically) I ended up with an upside down image. I think there is an error in the apple code whereby the preferredTransform shows the rotation that was done to the buffer while saving, rather than the rotation that should be done to restore it to upright.

Also note I am using a custom AVVideoCompositing where I apply the transform directly to the buffer, which may differ from what a built-in compositing option does.

import AVFoundation
import UIKit

extension AVAssetTrack {


// This is the current orientation of the buffer which sits in positive X and Y space.  eg:  For a leftMirrored representation of a portrait 1080w by 1920h image, the buffer has had its X-axis reversed and is rotated to the left, but is still sitting in the top right quadrant in the space of X(0->1920) and Y(0->1080).  This variable is used to calculate the required fixedPreferredTransform and fixedNaturalSize required to handle the buffer appropriately.
var fixedOrientation: UIImage.Orientation {
    
    /*
     // Here is a list of debug transforms showing what each of these terms means.  Useful to check the logic below.
     let id = CGAffineTransform.identity
     let mirror = CGAffineTransform(scaleX: -1.0, y: 1.0)
     let up = id
     let left = id.concatenating(.init(rotationAngle: 1.0 * .pi / 2.0))
     let right = id.concatenating(.init(rotationAngle: -1.0 * .pi / 2.0))
     let down = id.concatenating(.init(rotationAngle: .pi))
     let upMirror = up.concatenating(mirror)
     let leftMirror = left.concatenating(mirror)
     let rightMirror = right.concatenating(mirror)
     let downMirror = down.concatenating(mirror)
     */
    
    switch (preferredTransform.a, preferredTransform.b, preferredTransform.c, preferredTransform.d) {
    case (0.0, 1.0, -1.0, 0.0): return .left
    case (0.0, -1.0, 1.0, 0.0): return .right
    case (1.0, 0.0, 0.0, 1.0): return .up
    case (-1.0, 0.0, 0.0, -1.0): return .down
    case (0.0, 1.0, 1.0, 0.0): return .leftMirrored
    case (0.0, -1.0, -1.0, 0.0): return .rightMirrored
    case (-1.0, 0.0, 0.0, 1.0): return .upMirrored
    case (1.0, 0.0, 0.0, -1.0): return .downMirrored
    default: return .up
    }
}

// When handling the buffer, after it has been unmirrored and set upright using the fixedPreferredTransform variable, it is important to note that the size of the upright buffer might be the transpose (swap X and Y) of the original unmodified buffer, so that subsequent code knows the proper size to deal with.
var fixedNaturalSize: CGSize {
    let naturalSize = naturalSize
    switch fixedOrientation {
    case .up: return naturalSize
    case .down: return naturalSize
    case .left: return .init(width: naturalSize.height, height: naturalSize.width)
    case .right: return .init(width: naturalSize.height, height: naturalSize.width)
    case .upMirrored: return naturalSize
    case .downMirrored: return naturalSize
    case .leftMirrored: return .init(width: naturalSize.height, height: naturalSize.width)
    case .rightMirrored: return .init(width: naturalSize.height, height: naturalSize.width)
    default: return naturalSize
    }
}

// This is the required transform to change the stored buffer (located as defined by the fixedOrientation variable) such that it is upright and un-mirrored, with origin at X,Y = 0,0 and extending into the positive X and Y space (ie: in the upper quadrant).
// Note that the built-in preferredTransform variable does not include shifting of the buffer into the top right quadrant, which is required for subsequent processing.
var fixedPreferredTransform: CGAffineTransform {
    var t = preferredTransform
    switch fixedOrientation {
    case .up:
        t.tx = 0
        t.ty = 0
    case .downMirrored:
        t.tx = 0
        t.ty = fixedNaturalSize.height
    case .upMirrored:
        t.tx = fixedNaturalSize.width
        t.ty = 0
    case .down:
        t.tx = fixedNaturalSize.width
        t.ty = fixedNaturalSize.height
    case .left:
        t = .init(a: 0, b: -1, c: 1, d: 0, tx: 0, ty: 0) // to REVERSE the left turn
        t.tx = 0
        t.ty = fixedNaturalSize.height
    case .right:
        t = .init(a: 0, b: 1, c: -1, d: 0, tx: 0, ty: 0) // to REVERSE the left turn
        t.tx = fixedNaturalSize.width
        t.ty = 0
    case .rightMirrored:
        t = .init(0, 1, 1, 0, 0, 0) // to REVERSE the right turn
        t.tx = 0
        t.ty = 0
    case .leftMirrored:
        t = .init(0, -1, -1, 0, 0, 0) // to REVERSE the right turn
        t.tx = fixedNaturalSize.width
        t.ty = fixedNaturalSize.height
    default:
        break
    }
    return t
}
}

Upvotes: 2

Barak Yoresh
Barak Yoresh

Reputation: 299

I ended up doing something slightly more robust I think, I nullified the transform based on where it would end up:

auto naturalFrame = CGRectMake(0, 0, naturalSize.width, naturalSize.height);
auto preferredFrame = CGRectApplyAffineTransform(naturalFrame, preferredTransform);
preferredTransform.tx -= preferredFrame.origin.x;
preferredTransform.ty -= preferredFrame.origin.y;

Note that you can't just apply the transform on (0, 0) since CGRect.origin takes into account things like flipping.

Upvotes: 1

Theo
Theo

Reputation: 4015

The preferredTransform is a CGAffineTransform. The properties a, b, c, d are concatenations of reflection and rotation matrices, and the properties tx and ty describe a translation. In all cases that I observed with an incorrect preferredTransform, the reflection/rotation part appeared to be correct, and only the translation part contained wrong values. A reliable fix seems to be to inspect a, b, c, d (eight cases in total, each corresponding to a case in UIImageOrientation) and update tx and ty accordingly:

extension AVAssetTrack {
  var fixedPreferredTransform: CGAffineTransform {
    var t = preferredTransform
    switch(t.a, t.b, t.c, t.d) {
    case (1, 0, 0, 1):
      t.tx = 0
      t.ty = 0
    case (1, 0, 0, -1):
      t.tx = 0
      t.ty = naturalSize.height
    case (-1, 0, 0, 1):
      t.tx = naturalSize.width
      t.ty = 0
    case (-1, 0, 0, -1):
      t.tx = naturalSize.width
      t.ty = naturalSize.height
    case (0, -1, 1, 0):
      t.tx = 0
      t.ty = naturalSize.width
    case (0, 1, -1, 0):
      t.tx = naturalSize.height
      t.ty = 0
    case (0, 1, 1, 0):
      t.tx = 0
      t.ty = 0
    case (0, -1, -1, 0):
      t.tx = naturalSize.height
      t.ty = naturalSize.width
    default:
      break
    }
    return t
  }
}

Upvotes: 13

Related Questions