Filtering Depth Data on iOS 12 appears to be rotated

1.4k views Asked by At

I am having an issue where the Depth Data for the .builtInDualCamera appears to be rotated 90 degrees when isFilteringEnabled = true

Here is my code:

fileprivate let session = AVCaptureSession()

fileprivate let meta = AVCaptureMetadataOutput()
fileprivate let video = AVCaptureVideoDataOutput()
fileprivate let depth = AVCaptureDepthDataOutput()

fileprivate let camera: AVCaptureDevice
fileprivate let input: AVCaptureDeviceInput

fileprivate let synchronizer: AVCaptureDataOutputSynchronizer

init(delegate: CaptureSessionDelegate?) throws {
    self.delegate = delegate
    session.sessionPreset = .vga640x480

    // Setup Camera Input
    let discovery = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInDualCamera], mediaType: .video, position: .unspecified)
    if let device = discovery.devices.first {
        camera = device
    } else {
        throw SessionError.CameraNotAvailable("Unable to load camera")
    }

    input = try AVCaptureDeviceInput(device: camera)
    session.addInput(input)

    // Setup Metadata Output (Face)
    session.addOutput(meta)
    if meta.availableMetadataObjectTypes.contains(AVMetadataObject.ObjectType.face) {
        meta.metadataObjectTypes = [ AVMetadataObject.ObjectType.face ]
    } else {
        print("Can't Setup Metadata: \(meta.availableMetadataObjectTypes)")
    }

    // Setup Video Output
    video.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
    session.addOutput(video)
    video.connection(with: .video)?.videoOrientation = .portrait

    // ****** THE ISSUE IS WITH THIS BLOCK HERE ******
    // Setup Depth Output
    depth.isFilteringEnabled = true
    session.addOutput(depth)
    depth.connection(with: .depthData)?.videoOrientation = .portrait

    // Setup Synchronizer
    synchronizer = AVCaptureDataOutputSynchronizer(dataOutputs: [depth, video, meta])


    let outputRect = CGRect(x: 0, y: 0, width: 1, height: 1)
    let videoRect = video.outputRectConverted(fromMetadataOutputRect: outputRect)
    let depthRect = depth.outputRectConverted(fromMetadataOutputRect: outputRect)

    // Ratio of the Depth to Video
    scale = max(videoRect.width, videoRect.height) / max(depthRect.width, depthRect.height)

    // Set Camera to the framerate of the Depth Data Collection
    try camera.lockForConfiguration()
    if let fps = camera.activeDepthDataFormat?.videoSupportedFrameRateRanges.first?.minFrameDuration {
        camera.activeVideoMinFrameDuration = fps
    }
    camera.unlockForConfiguration()

    super.init()
    synchronizer.setDelegate(self, queue: syncQueue)
}

func dataOutputSynchronizer(_ synchronizer: AVCaptureDataOutputSynchronizer, didOutput data: AVCaptureSynchronizedDataCollection) {
    guard let delegate = self.delegate else {
        return
    }

    // Check to see if all the data is actually here
    guard
        let videoSync = data.synchronizedData(for: video) as? AVCaptureSynchronizedSampleBufferData,
        !videoSync.sampleBufferWasDropped,
        let depthSync = data.synchronizedData(for: depth) as? AVCaptureSynchronizedDepthData,
        !depthSync.depthDataWasDropped
    else {
        return
    }

    // It's OK if the face isn't found.
    let face: AVMetadataFaceObject?
    if let metaSync = data.synchronizedData(for: meta) as? AVCaptureSynchronizedMetadataObjectData {
            face = (metaSync.metadataObjects.first { $0 is AVMetadataFaceObject }) as? AVMetadataFaceObject
    } else {
            face = nil
    }

    // Convert Buffers to CIImage
    let videoImage = convertVideoImage(fromBuffer: videoSync.sampleBuffer)
    let depthImage = convertDepthImage(fromData: depthSync.depthData, andFace: face)

    // Call Delegate
    delegate.captureImages(video: videoImage, depth: depthImage, face: face)
}

fileprivate func convertVideoImage(fromBuffer sampleBuffer: CMSampleBuffer) -> CIImage {
    // Convert from "CoreMovie?" to CIImage - fairly straight-forward
    let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
    let image = CIImage(cvPixelBuffer: pixelBuffer!)
    return image
}

fileprivate func convertDepthImage(fromData depthData: AVDepthData, andFace face: AVMetadataFaceObject?) -> CIImage {

    var convertedDepth: AVDepthData

    // Convert 16-bif floats up to 32
    if depthData.depthDataType != kCVPixelFormatType_DisparityFloat32 {
        convertedDepth = depthData.converting(toDepthDataType: kCVPixelFormatType_DisparityFloat32)
    } else {
        convertedDepth = depthData
    }

    // Pixel buffer comes straight from depthData
    let pixelBuffer = convertedDepth.depthDataMap

    let image = CIImage(cvPixelBuffer: pixelBuffer)
    return image
}

The original Video Looks like this: (For reference)

Original Video Image

When the values are:

// Setup Depth Output
depth.isFilteringEnabled = false
depth.connection(with: .depthData)?.videoOrientation = .portrait

The Image looks like this: (you can see the closer jacket is white, the farther jacket is grey, and the distance is dark grey - as expected)

Filtering=False, Orientation=Portrait

When the values are:

// Setup Depth Output
depth.isFilteringEnabled = true
depth.connection(with: .depthData)?.videoOrientation = .portrait

The image looks like this: (You can see the color values appear to be in the right places, but the shapes in the smoothing filter appear to be rotated)

Filtering=True, Orientation=Portrait

When the values are:

// Setup Depth Output
depth.isFilteringEnabled = true
depth.connection(with: .depthData)?.videoOrientation = .landscapeRight

The image looks like this: (Both the colors and the shapes appear to be horizontal)

Filtering=True, Orientation=Landscape_Right

Am I doing something wrong to get these incorrect values?

I have tried re-ordering the code

// Setup Depth Output
depth.connection(with: .depthData)?.videoOrientation = .portrait
depth.isFilteringEnabled = true

But that does nothing.

I think this is an issue related to iOS 12, because I remember this working just fine under iOS 11 (although I don't have any images saved to prove it)

Any Help is appreciated, thanks!

1

There are 1 answers

3
App Dev Guy On

Unlike the suggestion to review other answers on rotating the image after creation, which I found did not work, in the AVDepthData documentation, there is a method available that does the orientation correction for you.

The method is called: depthDataByApplyingExifOrientation: which returns an instance of AVDepthData with the orientation applied, ie. you can create your image in the correct orientation you desire by passing in the parameter of your choice.

This is my helper method that returns a UIImage with the orientation fix.

- (UIImage *)createDepthMapImageFromCapturePhoto:(AVCapturePhoto *)photo {
    // AVCapturePhoto which has depthData - in swift you should confirm this exists
    AVDepthData *frontDepthData = [photo depthData];
    // Overwrite the instance with the correct orientation applied.
    frontDepthData = [frontDepthData depthDataByApplyingExifOrientation:kCGImagePropertyOrientationRight];
    // Create the CIImage from the depth data using the available method.
    CIImage *ciDepthImage = [CIImage imageWithDepthData:frontDepthData];
    // Create CIContext which enables converting CIImage to CGImage
    CIContext *context = [[CIContext alloc] init];
    // Create the CGImage
    CGImageRef img = [context createCGImage:ciDepthImage fromRect:[ciDepthImage extent]];
    // Create the final image.
    UIImage *depthImage = [UIImage imageWithCGImage:img];
    // Return the depth image.
    return depthImage;
}