2

I am building an app that captures facetracking data from the iPhone TrueDepth camera.

I need to write this data to files so I can use it as the basis for another app.

Within the app, the data is saved into four separate arrays, one containing ARFaceGeometry objects, and the other three with transform coordinates as simd_float4x4 matrices.

I am converting the arrays into Data objects using archivedData(withRootObject: requiringSecureCoding:) then calling write(to:) on them to create the files.

The file containing the ARFaceGeometry data is written and read back in correctly. But the three simd_float4x4 arrays aren't being written, even though the code for doing so is identical. Along with my print logs, the error being given is 'unrecognized selector sent to instance'.

Properties:

var faceGeometryCapture = [ARFaceGeometry]()
var faceTransformCapture = [simd_float4x4]()
var leftEyeTransformCapture = [simd_float4x4]()
var rightEyeTransformCapture = [simd_float4x4]()

var faceGeometryCaptureFilePath: URL!
var faceTransformCaptureFilePath: URL!
var leftEyeTransformCaptureFilePath: URL!
var rightEyeTransformCaptureFilePath: URL!

Code for establishing file URLs:

let fileManager = FileManager.default
let dirPaths = fileManager.urls(for: .documentDirectory,
                        in: .userDomainMask)
        
faceGeometryCaptureFilePath = dirPaths[0].appendingPathComponent("face-geometries.txt")
faceTransformCaptureFilePath = dirPaths[0].appendingPathComponent("face-transforms.txt")
leftEyeTransformCaptureFilePath = dirPaths[0].appendingPathComponent("left-eye-transforms.txt")
rightEyeTransformCaptureFilePath = dirPaths[0].appendingPathComponent("right-eye-transforms.txt")

Code for writing the data to files:

do {
    let data = try NSKeyedArchiver.archivedData(withRootObject: faceGeometryCapture, requiringSecureCoding: false)
    try data.write(to: faceGeometryCaptureFilePath)
    } catch { print("Error writing face geometries to file") }
do {
    let data = try NSKeyedArchiver.archivedData(withRootObject: faceTransformCapture, requiringSecureCoding: false)
    try data.write(to: faceTransformCaptureFilePath)
    } catch { print("Error writing face transforms to file") }
do {
    let data = try NSKeyedArchiver.archivedData(withRootObject: leftEyeTransformCapture, requiringSecureCoding: false)
    try data.write(to: leftEyeTransformCaptureFilePath)
    } catch { print("Error writing left eye transforms to file") }
do {
    let data = try NSKeyedArchiver.archivedData(withRootObject: rightEyeTransformCapture, requiringSecureCoding: false)
    try data.write(to: rightEyeTransformCaptureFilePath)
    } catch { print("Error writing right eye transforms to file") }

I'm guessing it's the simd_float4x4 struct that is causing the issue, as this is the only difference between working and not working. Can anyone confirm and suggest a solution?

Thanks in advance.

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
MightyMeta
  • 599
  • 4
  • 14
  • As the name implies `archivedData(withRootObject:` expects an *object* (a class) which conforms to `NSCoding`. The `simd` matrices are structs, they cannot adopt the protocol. – vadian Aug 30 '20 at 20:17

1 Answers1

10

As already mentioned in comments structures can't conform to NSCoding but you can make simd_float4x4 conform to Codable and persist its data:

extension simd_float4x4: Codable {
    public init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        try self.init(container.decode([SIMD4<Float>].self))
    }
    public func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try container.encode([columns.0,columns.1, columns.2, columns.3])
    }
}

Playground testing:

do {
    let vector = simd_float4x4(2.7)  // simd_float4x4([[2.7, 0.0, 0.0, 0.0], [0.0, 2.7, 0.0, 0.0], [0.0, 0.0, 2.7, 0.0], [0.0, 0.0, 0.0, 2.7]])
    let data = try JSONEncoder().encode(vector)  // 111 bytes
    let json = String(data: data, encoding: .utf8)
    print(json ?? "")  // [[[2.7000000476837158,0,0,0],[0,2.7000000476837158,0,0],[0,0,2.7000000476837158,0],[0,0,0,2.7000000476837158]]]\n"
    let decoded = try JSONDecoder().decode(simd_float4x4.self, from: data)
    print(decoded)     // "simd_float4x4([[2.7, 0.0, 0.0, 0.0], [0.0, 2.7, 0.0, 0.0], [0.0, 0.0, 2.7, 0.0], [0.0, 0.0, 0.0, 2.7]])\n"
    decoded == vector  // true
} catch {
    print(error)
}

edit/update:

Another option is to save its raw bytes. It will use only 64 bytes:

extension simd_float4x4: ContiguousBytes {
    public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
        try Swift.withUnsafeBytes(of: self) { try body($0) }
    }
}

extension ContiguousBytes {
    init<T: ContiguousBytes>(_ bytes: T) {
        self = bytes.withUnsafeBytes { $0.load(as: Self.self) }
    }
    var bytes: [UInt8] { withUnsafeBytes { .init($0) } }
    var data: Data { withUnsafeBytes { .init($0) } }
    func object<T>() -> T { withUnsafeBytes { $0.load(as: T.self) } }
    func objects<T>() -> [T] { withUnsafeBytes { .init($0.bindMemory(to: T.self)) } }
    var simdFloat4x4: simd_float4x4 { object() }
    var simdFloat4x4Collection: [simd_float4x4] { objects() }
}

extension Array where Element: ContiguousBytes {
    var bytes: [UInt8] { withUnsafeBytes { .init($0) } }
    var data: Data { withUnsafeBytes { .init($0) } }
}

let vector1 = simd_float4x4(.init(2, 1, 1, 1), .init(1, 2, 1, 1), .init(1, 1, 2, 1), .init(1, 1, 1, 2))
let vector2 = simd_float4x4(.init(3, 1, 1, 1), .init(1, 3, 1, 1), .init(1, 1, 3, 1), .init(1, 1, 1, 3))
let data = [vector1,vector2].data          // 128 bytes
let loaded = data.simdFloat4x4Collection
print(loaded)    // "[simd_float4x4([[2.0, 1.0, 1.0, 1.0], [1.0, 2.0, 1.0, 1.0], [1.0, 1.0, 2.0, 1.0], [1.0, 1.0, 1.0, 2.0]]), simd_float4x4([[3.0, 1.0, 1.0, 1.0], [1.0, 3.0, 1.0, 1.0], [1.0, 1.0, 3.0, 1.0], [1.0, 1.0, 1.0, 3.0]])]\n"
loaded[0] == vector1  // true
loaded[1] == vector2  // true
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Thanks for this Leo. Have added in the extension, but still getting the same errors as before. Do I need to ditch NSKeyedArchiver and use JSONEncoder instead, before saving the data to a file? – MightyMeta Aug 30 '20 at 23:06
  • You are welcome. Forget about `NSKeyedArchiver` when dealing with structures. Just encode it as shown above using `JSONEncoder`. – Leo Dabus Aug 30 '20 at 23:33
  • Yes, got it to work. Thank you! Is there a way of getting the ARFaceGeometry objects to encode using JSONEncoder too? The file being created by NSKeyedArchiver is several MB, which seems huge, whereas the .json files are a few KB. Plus the KeyedArchiver data is not human-readable, which I like about the .json files - it means I can edit them manually if needed. The debugger is saying that ARFaceGeometry also needs to conform to Encodable. Is this possible? – – MightyMeta Aug 31 '20 at 00:36
  • @MightyMeta AFAIK this will be a bit more complicated (Don't even know if it is possible). All `ARFaceGeometry` properties are get only and there is no initializers that would accept them. In other words. You would be able to encode but not decode the object. `extension ARFaceGeometry: Encodable {` `public func encode(to encoder: Encoder) throws {` `var container = encoder.unkeyedContainer()` `try container.encode(vertices)` `try container.encode(textureCoordinates)` `try container.encode(triangleCount)` `try container.encode(triangleIndices)` `}` `}` – Leo Dabus Aug 31 '20 at 01:21
  • Ok, I’ll stick with nskeyedarchiver for that then, since it does work. Thanks again for your help! – MightyMeta Aug 31 '20 at 07:56