ARKit – How to export OBJ from iPhone/iPad with LiDAR? - swift

How can I export the ARMeshGeometry generated by the new SceneReconstruction API on the latest iPad Pro to an .obj file?
Here's SceneReconstruction documentation.

Starting with Apple's Visualising Scene Scemantics sample app, you can retrieve the ARMeshGeometry object from the first anchor in the frame.
The easiest approach to exporting the data is to first convert it to an MDLMesh:
extension ARMeshGeometry {
func toMDLMesh(device: MTLDevice) -> MDLMesh {
let allocator = MTKMeshBufferAllocator(device: device);
let data = Data.init(bytes: vertices.buffer.contents(), count: vertices.stride * vertices.count);
let vertexBuffer = allocator.newBuffer(with: data, type: .vertex);
let indexData = Data.init(bytes: faces.buffer.contents(), count: faces.bytesPerIndex * faces.count * faces.indexCountPerPrimitive);
let indexBuffer = allocator.newBuffer(with: indexData, type: .index);
let submesh = MDLSubmesh(indexBuffer: indexBuffer,
indexCount: faces.count * faces.indexCountPerPrimitive,
indexType: .uInt32,
geometryType: .triangles,
material: nil);
let vertexDescriptor = MDLVertexDescriptor();
vertexDescriptor.attributes[0] = MDLVertexAttribute(name: MDLVertexAttributePosition,
format: .float3,
offset: 0,
bufferIndex: 0);
vertexDescriptor.layouts[0] = MDLVertexBufferLayout(stride: vertices.stride);
return MDLMesh(vertexBuffer: vertexBuffer,
vertexCount: vertices.count,
descriptor: vertexDescriptor,
submeshes: [submesh]);
}
}
Once you have the MDLMesh, exporting to an OBJ file is a breeze:
#IBAction func exportMesh(_ button: UIButton) {
let meshAnchors = arView.session.currentFrame?.anchors.compactMap({ $0 as? ARMeshAnchor });
DispatchQueue.global().async {
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0];
let filename = directory.appendingPathComponent("MyFirstMesh.obj");
guard let device = MTLCreateSystemDefaultDevice() else {
print("metal device could not be created");
return;
};
let asset = MDLAsset();
for anchor in meshAnchors! {
let mdlMesh = anchor.geometry.toMDLMesh(device: device);
asset.add(mdlMesh);
}
do {
try asset.export(to: filename);
} catch {
print("failed to write to file");
}
}
}

The answer of the #swiftcoder works great. But in the case of several anchors you need to convert the vertices coordinates to the world coordinate system based on the anchor transform. In the opposite case all meshes will be placed at zero position and you will have a mess.
The updated code looks like this:
extension ARMeshGeometry {
func toMDLMesh(device: MTLDevice, transform: simd_float4x4) -> MDLMesh {
let allocator = MTKMeshBufferAllocator(device: device)
let data = Data.init(bytes: transformedVertexBuffer(transform), count: vertices.stride * vertices.count)
let vertexBuffer = allocator.newBuffer(with: data, type: .vertex)
let indexData = Data.init(bytes: faces.buffer.contents(), count: faces.bytesPerIndex * faces.count * faces.indexCountPerPrimitive)
let indexBuffer = allocator.newBuffer(with: indexData, type: .index)
let submesh = MDLSubmesh(indexBuffer: indexBuffer,
indexCount: faces.count * faces.indexCountPerPrimitive,
indexType: .uInt32,
geometryType: .triangles,
material: nil)
let vertexDescriptor = MDLVertexDescriptor()
vertexDescriptor.attributes[0] = MDLVertexAttribute(name: MDLVertexAttributePosition,
format: .float3,
offset: 0,
bufferIndex: 0)
vertexDescriptor.layouts[0] = MDLVertexBufferLayout(stride: vertices.stride)
return MDLMesh(vertexBuffer: vertexBuffer,
vertexCount: vertices.count,
descriptor: vertexDescriptor,
submeshes: [submesh])
}
func transformedVertexBuffer(_ transform: simd_float4x4) -> [Float] {
var result = [Float]()
for index in 0..<vertices.count {
let vertexPointer = vertices.buffer.contents().advanced(by: vertices.offset + vertices.stride * index)
let vertex = vertexPointer.assumingMemoryBound(to: (Float, Float, Float).self).pointee
var vertextTransform = matrix_identity_float4x4
vertextTransform.columns.3 = SIMD4<Float>(vertex.0, vertex.1, vertex.2, 1)
let position = (transform * vertextTransform).position
result.append(position.x)
result.append(position.y)
result.append(position.z)
}
return result
}
}
extension simd_float4x4 {
var position: SIMD3<Float> {
return SIMD3<Float>(columns.3.x, columns.3.y, columns.3.z)
}
}
extension Array where Element == ARMeshAnchor {
func save(to fileURL: URL, device: MTLDevice) throws {
let asset = MDLAsset()
self.forEach {
let mesh = $0.geometry.toMDLMesh(device: device, transform: $0.transform)
asset.add(mesh)
}
try asset.export(to: fileURL)
}
}
I am not an expert in ModelIO and maybe there is more simple way to transform vertex buffer :) But this code works for me.

Exporting LiDAR-reconstructed geometry
This code allows you save LiDAR's geometry as USD and send it to Mac computer via AirDrop. You can export not only .usd but also .usda, .usdc, .obj, .stl, .abc, and .ply file formats.
Additionally you can use SceneKit's write(to:options:delegate:progressHandler:) method to save a .usdz version of file.
import RealityKit
import ARKit
import MetalKit
import ModelIO
#IBOutlet var arView: ARView!
var saveButton: UIButton!
let rect = CGRect(x: 50, y: 50, width: 100, height: 50)
override func viewDidLoad() {
super.viewDidLoad()
let tui = UIControl.Event.touchUpInside
saveButton = UIButton(frame: rect)
saveButton.setTitle("Save", for: [])
saveButton.addTarget(self, action: #selector(saveButtonTapped), for: tui)
self.view.addSubview(saveButton)
}
#objc func saveButtonTapped(sender: UIButton) {
print("Saving is executing...")
guard let frame = arView.session.currentFrame
else { fatalError("Can't get ARFrame") }
guard let device = MTLCreateSystemDefaultDevice()
else { fatalError("Can't create MTLDevice") }
let allocator = MTKMeshBufferAllocator(device: device)
let asset = MDLAsset(bufferAllocator: allocator)
let meshAnchors = frame.anchors.compactMap { $0 as? ARMeshAnchor }
for ma in meshAnchors {
let geometry = ma.geometry
let vertices = geometry.vertices
let faces = geometry.faces
let vertexPointer = vertices.buffer.contents()
let facePointer = faces.buffer.contents()
for vtxIndex in 0 ..< vertices.count {
let vertex = geometry.vertex(at: UInt32(vtxIndex))
var vertexLocalTransform = matrix_identity_float4x4
vertexLocalTransform.columns.3 = SIMD4<Float>(x: vertex.0,
y: vertex.1,
z: vertex.2,
w: 1.0)
let vertexWorldTransform = (ma.transform * vertexLocalTransform).position
let vertexOffset = vertices.offset + vertices.stride * vtxIndex
let componentStride = vertices.stride / 3
vertexPointer.storeBytes(of: vertexWorldTransform.x,
toByteOffset: vertexOffset,
as: Float.self)
vertexPointer.storeBytes(of: vertexWorldTransform.y,
toByteOffset: vertexOffset + componentStride,
as: Float.self)
vertexPointer.storeBytes(of: vertexWorldTransform.z,
toByteOffset: vertexOffset + (2 * componentStride),
as: Float.self)
}
let byteCountVertices = vertices.count * vertices.stride
let byteCountFaces = faces.count * faces.indexCountPerPrimitive * faces.bytesPerIndex
let vertexBuffer = allocator.newBuffer(with: Data(bytesNoCopy: vertexPointer,
count: byteCountVertices,
deallocator: .none), type: .vertex)
let indexBuffer = allocator.newBuffer(with: Data(bytesNoCopy: facePointer,
count: byteCountFaces,
deallocator: .none), type: .index)
let indexCount = faces.count * faces.indexCountPerPrimitive
let material = MDLMaterial(name: "material",
scatteringFunction: MDLPhysicallyPlausibleScatteringFunction())
let submesh = MDLSubmesh(indexBuffer: indexBuffer,
indexCount: indexCount,
indexType: .uInt32,
geometryType: .triangles,
material: material)
let vertexFormat = MTKModelIOVertexFormatFromMetal(vertices.format)
let vertexDescriptor = MDLVertexDescriptor()
vertexDescriptor.attributes[0] = MDLVertexAttribute(name: MDLVertexAttributePosition,
format: vertexFormat,
offset: 0,
bufferIndex: 0)
vertexDescriptor.layouts[0] = MDLVertexBufferLayout(stride: ma.geometry.vertices.stride)
let mesh = MDLMesh(vertexBuffer: vertexBuffer,
vertexCount: ma.geometry.vertices.count,
descriptor: vertexDescriptor,
submeshes: [submesh])
asset.add(mesh)
}
let filePath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first!
let usd: URL = filePath.appendingPathComponent("model.usd")
if MDLAsset.canExportFileExtension("usd") {
do {
try asset.export(to: usd)
let controller = UIActivityViewController(activityItems: [usd],
applicationActivities: nil)
controller.popoverPresentationController?.sourceView = sender
self.present(controller, animated: true, completion: nil)
} catch let error {
fatalError(error.localizedDescription)
}
} else {
fatalError("Can't export USD")
}
}
Tap Save button, and in Activity View Controller choose More and send ready-to-use model to Mac's Downloads folder via AirDrop.
P.S.
Here you can find an extra info on capturing real-world texture.

I'm using Lidar and beginner in scan 3d, how to export to files with swift code:
GLTF (Share in AR for Android devices)
GLB
STL (Un-textured file used in 3d printing)
Point Cloud (PCD PLY PTS XYZ LAS e57)
All Data (Includes captured images)
DAE (Compatible with Sketchfab)
FBX
thanks you

Related

Accessing Recently Stored Files (The file “insert file” couldn’t be opened because there is no such file)

In my video recording app, I am trying to implement a merge video function which takes in an array of URLs which were recently recorded in the app.
extension AVMutableComposition {
func mergeVideo(_ urls: [URL], completion: #escaping (_ url: URL?, _ error: Error?) -> Void) {
guard let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
completion(nil, nil)
return
}
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .short
let date = dateFormatter.string(from: Date())
let outputURL = documentDirectory.appendingPathComponent("mergedVideo_\(date).mp4")
// If there is only one video, we dont to touch it to save export time.
if let url = urls.first, urls.count == 1 {
do {
try FileManager().copyItem(at: url, to: outputURL)
completion(outputURL, nil)
} catch let error {
completion(nil, error)
}
return
}
let maxRenderSize = CGSize(width: 1280.0, height: 720.0)
var currentTime = CMTime.zero
var renderSize = CGSize.zero
// Create empty Layer Instructions, that we will be passing to Video Composition and finally to Exporter.
var instructions = [AVMutableVideoCompositionInstruction]()
urls.enumerated().forEach { index, url in
let asset = AVAsset(url: url)
print(asset)
let assetTrack = asset.tracks.first!
// Create instruction for a video and append it to array.
let instruction = AVMutableComposition.instruction(assetTrack, asset: asset, time: currentTime, duration: assetTrack.timeRange.duration, maxRenderSize: maxRenderSize)
instructions.append(instruction.videoCompositionInstruction)
// Set render size (orientation) according first videro.
if index == 0 {
renderSize = instruction.isPortrait ? CGSize(width: maxRenderSize.height, height: maxRenderSize.width) : CGSize(width: maxRenderSize.width, height: maxRenderSize.height)
}
do {
let timeRange = CMTimeRangeMake(start: .zero, duration: assetTrack.timeRange.duration)
// Insert video to Mutable Composition at right time.
try insertTimeRange(timeRange, of: asset, at: currentTime)
currentTime = CMTimeAdd(currentTime, assetTrack.timeRange.duration)
} catch let error {
completion(nil, error)
}
}
// Create Video Composition and pass Layer Instructions to it.
let videoComposition = AVMutableVideoComposition()
videoComposition.instructions = instructions
// Do not forget to set frame duration and render size. It will crash if you dont.
videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
videoComposition.renderSize = renderSize
guard let exporter = AVAssetExportSession(asset: self, presetName: AVAssetExportPresetHighestQuality) else {
completion(nil, nil)
return
}
exporter.outputURL = outputURL
exporter.outputFileType = .mp4
// Pass Video Composition to the Exporter.
exporter.videoComposition = videoComposition
exporter.shouldOptimizeForNetworkUse = true
exporter.exportAsynchronously {
DispatchQueue.main.async {
completion(exporter.outputURL, nil)
}
}
}
static func instruction(_ assetTrack: AVAssetTrack, asset: AVAsset, time: CMTime, duration: CMTime, maxRenderSize: CGSize)
-> (videoCompositionInstruction: AVMutableVideoCompositionInstruction, isPortrait: Bool) {
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: assetTrack)
// Find out orientation from preferred transform.
let assetInfo = orientationFromTransform(assetTrack.preferredTransform)
// Calculate scale ratio according orientation.
var scaleRatio = maxRenderSize.width / assetTrack.naturalSize.width
if assetInfo.isPortrait {
scaleRatio = maxRenderSize.height / assetTrack.naturalSize.height
}
// Set correct transform.
var transform = CGAffineTransform(scaleX: scaleRatio, y: scaleRatio)
transform = assetTrack.preferredTransform.concatenating(transform)
layerInstruction.setTransform(transform, at: .zero)
// Create Composition Instruction and pass Layer Instruction to it.
let videoCompositionInstruction = AVMutableVideoCompositionInstruction()
videoCompositionInstruction.timeRange = CMTimeRangeMake(start: time, duration: duration)
videoCompositionInstruction.layerInstructions = [layerInstruction]
return (videoCompositionInstruction, assetInfo.isPortrait)
}
static func orientationFromTransform(_ transform: CGAffineTransform) -> (orientation: UIImage.Orientation, isPortrait: Bool) {
var assetOrientation = UIImage.Orientation.up
var isPortrait = false
switch [transform.a, transform.b, transform.c, transform.d] {
case [0.0, 1.0, -1.0, 0.0]:
assetOrientation = .right
isPortrait = true
case [0.0, -1.0, 1.0, 0.0]:
assetOrientation = .left
isPortrait = true
case [1.0, 0.0, 0.0, 1.0]:
assetOrientation = .up
case [-1.0, 0.0, 0.0, -1.0]:
assetOrientation = .down
default:
break
}
return (assetOrientation, isPortrait)
}
}
However, when I try to call the mergeVideo function, I get the error of trying to unwrap an optional value found nil. For instance, in my log I print the values in the array of URLS. I can have an array that looks like this [file:///private/var/mobile/Containers/Data/Application/FD6FFB6E-36A8-49A9-8892-15BEAC0BA817/tmp/846F105D-A2F7-4F0A-A512-643B0407B962.mp4] but get the error The file “846F105D-A2F7-4F0A-A512-643B0407B962.mp4” couldn’t be opened because there is no such file. Any suggestions on how to properly access this recently stored files or any suggestions?

The File "xxx" couldn’t be opened because there is no such file from directory

In my video recording app, I record a video and save it to the photo library. The ultimate goal is to take the recently taken videos and merge them with this merge function.
extension AVMutableComposition {
func mergeVideo(_ urls: [URL], completion: #escaping (_ url: URL?, _ error: Error?) -> Void) {
guard let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
completion(nil, nil)
return
}
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .short
let date = dateFormatter.string(from: Date())
let outputURL = documentDirectory.appendingPathComponent("mergedVideo_\(date).mp4")
// If there is only one video, we dont to touch it to save export time.
if let url = urls.first, urls.count == 1 {
do {
try FileManager().copyItem(at: url, to: outputURL)
completion(outputURL, nil)
} catch let error {
completion(nil, error)
}
return
}
let maxRenderSize = CGSize(width: 1280.0, height: 720.0)
var currentTime = CMTime.zero
var renderSize = CGSize.zero
// Create empty Layer Instructions, that we will be passing to Video Composition and finally to Exporter.
var instructions = [AVMutableVideoCompositionInstruction]()
urls.enumerated().forEach { index, url in
let asset = AVAsset(url: url)
print(asset)
let assetTrack = asset.tracks.first!
// Create instruction for a video and append it to array.
let instruction = AVMutableComposition.instruction(assetTrack, asset: asset, time: currentTime, duration: assetTrack.timeRange.duration, maxRenderSize: maxRenderSize)
instructions.append(instruction.videoCompositionInstruction)
// Set render size (orientation) according first videro.
if index == 0 {
renderSize = instruction.isPortrait ? CGSize(width: maxRenderSize.height, height: maxRenderSize.width) : CGSize(width: maxRenderSize.width, height: maxRenderSize.height)
}
do {
let timeRange = CMTimeRangeMake(start: .zero, duration: assetTrack.timeRange.duration)
// Insert video to Mutable Composition at right time.
try insertTimeRange(timeRange, of: asset, at: currentTime)
currentTime = CMTimeAdd(currentTime, assetTrack.timeRange.duration)
} catch let error {
completion(nil, error)
}
}
// Create Video Composition and pass Layer Instructions to it.
let videoComposition = AVMutableVideoComposition()
videoComposition.instructions = instructions
// Do not forget to set frame duration and render size. It will crash if you dont.
videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
videoComposition.renderSize = renderSize
guard let exporter = AVAssetExportSession(asset: self, presetName: AVAssetExportPresetHighestQuality) else {
completion(nil, nil)
return
}
exporter.outputURL = outputURL
exporter.outputFileType = .mp4
// Pass Video Composition to the Exporter.
exporter.videoComposition = videoComposition
exporter.shouldOptimizeForNetworkUse = true
exporter.exportAsynchronously {
DispatchQueue.main.async {
completion(exporter.outputURL, nil)
}
}
}
static func instruction(_ assetTrack: AVAssetTrack, asset: AVAsset, time: CMTime, duration: CMTime, maxRenderSize: CGSize)
-> (videoCompositionInstruction: AVMutableVideoCompositionInstruction, isPortrait: Bool) {
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: assetTrack)
// Find out orientation from preferred transform.
let assetInfo = orientationFromTransform(assetTrack.preferredTransform)
// Calculate scale ratio according orientation.
var scaleRatio = maxRenderSize.width / assetTrack.naturalSize.width
if assetInfo.isPortrait {
scaleRatio = maxRenderSize.height / assetTrack.naturalSize.height
}
// Set correct transform.
var transform = CGAffineTransform(scaleX: scaleRatio, y: scaleRatio)
transform = assetTrack.preferredTransform.concatenating(transform)
layerInstruction.setTransform(transform, at: .zero)
// Create Composition Instruction and pass Layer Instruction to it.
let videoCompositionInstruction = AVMutableVideoCompositionInstruction()
videoCompositionInstruction.timeRange = CMTimeRangeMake(start: time, duration: duration)
videoCompositionInstruction.layerInstructions = [layerInstruction]
return (videoCompositionInstruction, assetInfo.isPortrait)
}
static func orientationFromTransform(_ transform: CGAffineTransform) -> (orientation: UIImage.Orientation, isPortrait: Bool) {
var assetOrientation = UIImage.Orientation.up
var isPortrait = false
switch [transform.a, transform.b, transform.c, transform.d] {
case [0.0, 1.0, -1.0, 0.0]:
assetOrientation = .right
isPortrait = true
case [0.0, -1.0, 1.0, 0.0]:
assetOrientation = .left
isPortrait = true
case [1.0, 0.0, 0.0, 1.0]:
assetOrientation = .up
case [-1.0, 0.0, 0.0, -1.0]:
assetOrientation = .down
default:
break
}
return (assetOrientation, isPortrait)
}
}
After calling this function, if the 'urls' array has only 1 item then I get an error saying "The File (insert filename) couldn't be opened because there is no such file." Otherwise, if the array has more than 1 item than the app crashes due to a force unwrapping optional value found nil. This is how I'm formatting the url and saving them to the app directory
func tempURL() -> URL? {
let directory = NSTemporaryDirectory() as NSString
if directory != "" {
let path = directory.appendingPathComponent(NSUUID().uuidString + ".mp4")
return URL(fileURLWithPath: path)
}
return nil
}
Any ideas on what's the issue or how to fix this?
assetTrack = asset.tracks.first!
.first! is a force-unwrapped optional so try doing something like
assetTrack = asset.tracks.first ?? asset.tracks.someValue

Tensor (.tflite) Model inference returning nil using Firebase SDK on Swift

Preface:
My ML (specifically NN() knowledge is very limited and i'm really only getting more and more familiar as time goes on.
Essentially, I have a model that accepts input [1, H, W, 3] (1 image, height, width, 3 channels) and SHOULD output [1, H, W, 2] (1 image, height, width, 2 channels). The idea is that with that, I'll be able to grab image data from the output with 1 of the channels in order to then convert it to an actual image which should essentially display indication AND sort of highlighting if a certain "something" existed in the input image using that 1 color channel (or the other color channel).
The model author is actively working on the model so it's nothing close to a perfect model.
So, with that:
I was initially using the tensorflowlite SDK to do everything but I found that official documentation, examples, and open source work wasn't even close to comparable with Firebase SDK. Plus, the actual project (currently testing this in a test environment) already uses Firebase SDK. Anyway, i was able to get some form of output, but I wasn't normalizing the image properly so the output wasn't as expected but at least there was SOMETHING.
Using this guide on Firebase, I am trying to run an inference on a tflite model.
From the below code you'll see that I have TensorFlowLite as a dependency but i'm not actually ACTIVELY using it. I have a function that uses it but the function isn't called.
So essentially you can ignore: parseOutputTensor, coordinateToIndex, and enum: Constants
Theories:
My modelInputs aren't set up properly.
I'm not correctly looking at the output
I'm not resizing and processing the image correctly before I use it to set the input data for inference
I don't know what I"m doing and i'm way off. D:
Below is my code:
import UIKit
import Firebase
import AVFoundation
import TensorFlowLite
class ViewController: UIViewController {
var captureSesssion : AVCaptureSession!
var cameraOutput : AVCapturePhotoOutput!
var previewLayer : AVCaptureVideoPreviewLayer!
#objc let device = AVCaptureDevice.default(for: .video)!
private var previousInferenceTimeMs: TimeInterval = Date.distantPast.timeIntervalSince1970 * 1000
private let delayBetweenInferencesMs: Double = 1000
#IBOutlet var imageView: UIImageView!
private var button1 : UIButton = {
var button = UIButton()
button.setTitle("button lol", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(buttonClicked), for: .touchDown)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
startCamera()
view.addSubview(button1)
view.bringSubviewToFront(button1)
button1.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
button1.titleLabel?.font = UIFont(name: "Helvetica", size: 25)
button1.widthAnchor.constraint(equalToConstant: view.frame.width/3).isActive = true
button1.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
#objc func buttonClicked() {
cameraPressed()
}
private func configureLocalModel() -> CustomLocalModel {
guard let modelPath = Bundle.main.path(forResource: "modelName", ofType: "tflite") else { fatalError("Couldn't find the modelPath") }
return CustomLocalModel(modelPath: modelPath)
}
private func createInterpreter(customLocalModel: CustomLocalModel) -> ModelInterpreter{
return ModelInterpreter.modelInterpreter(localModel: customLocalModel)
}
private func setModelInputOutput() -> ModelInputOutputOptions? {
var ioOptions : ModelInputOutputOptions
do {
ioOptions = ModelInputOutputOptions()
try ioOptions.setInputFormat(index: 0, type: .float32, dimensions: [1, 512, 512, 3])
try ioOptions.setOutputFormat(index: 0, type: .float32, dimensions: [1, 512, 512, 2])
} catch let error as NSError {
print("Failed to set input or output format with error: \(error.localizedDescription)")
}
return ioOptions
}
private func inputDataForInference(theImage: CGImage) -> ModelInputs?{
let image: CGImage = theImage
guard let context = CGContext(
data: nil,
width: image.width, height: image.height,
bitsPerComponent: 8, bytesPerRow: image.width * 4,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue
) else { fatalError("Context issues") }
context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))
guard let imageData = context.data else { fatalError("Context issues") }
let inputs : ModelInputs
var inputData = Data()
do {
for row in 0 ..< 512 {
for col in 0 ..< 512 {
let offset = 4 * (col * context.width + row)
// (Ignore offset 0, the unused alpha channel)
let red = imageData.load(fromByteOffset: offset+1, as: UInt8.self)
let green = imageData.load(fromByteOffset: offset+2, as: UInt8.self)
let blue = imageData.load(fromByteOffset: offset+3, as: UInt8.self)
// Normalize channel values to [0.0, 1.0]. This requirement varies
// by model. For example, some models might require values to be
// normalized to the range [-1.0, 1.0] instead, and others might
// require fixed-point values or the original bytes.
var normalizedRed = Float32(red) / 255.0
var normalizedGreen = Float32(green) / 255.0
var normalizedBlue = Float32(blue) / 255.0
// Append normalized values to Data object in RGB order.
let elementSize = MemoryLayout.size(ofValue: normalizedRed)
var bytes = [UInt8](repeating: 0, count: elementSize)
memcpy(&bytes, &normalizedRed, elementSize)
inputData.append(&bytes, count: elementSize)
memcpy(&bytes, &normalizedGreen, elementSize)
inputData.append(&bytes, count: elementSize)
memcpy(&bytes, &normalizedBlue, elementSize)
inputData.append(&bytes, count: elementSize)
}
}
inputs = ModelInputs()
try inputs.addInput(inputData)
} catch let error {
print("Failed to add input: \(error)")
}
return inputs
}
private func runInterpreter(interpreter: ModelInterpreter, inputs: ModelInputs, ioOptions: ModelInputOutputOptions){
interpreter.run(inputs: inputs, options: ioOptions) { outputs, error in
guard error == nil, let outputs = outputs else { fatalError("interpreter run error is nil or outputs is nil") }
let output = try? outputs.output(index: 0) as? [[NSNumber]]
print()
print("output?[0]: \(output?[0])")
print("output?.count: \(output?.count)")
print("output?.description: \(output?.description)")
}
}
private func gotImage(cgImage: CGImage){
let configuredModel = configureLocalModel()
let interpreter = createInterpreter(customLocalModel: configuredModel)
guard let modelioOptions = setModelInputOutput() else { fatalError("modelioOptions got image error") }
guard let modelInputs = inputDataForInference(theImage: cgImage) else { fatalError("modelInputs got image error") }
runInterpreter(interpreter: interpreter, inputs: modelInputs, ioOptions: modelioOptions)
}
private func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage {
let newSize = CGSize(width: targetSize.width, height: targetSize.height)
// This is the rect that we've calculated out and this is what is actually used below
let rect = CGRect(x: 0, y: 0, width: targetSize.width, height: targetSize.height)
// Actually do the resizing to the rect using the ImageContext stuff
UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
image.draw(in: rect)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
}
extension ViewController: AVCapturePhotoCaptureDelegate{
func startCamera(){
captureSesssion = AVCaptureSession()
previewLayer = AVCaptureVideoPreviewLayer(session: captureSesssion)
captureSesssion.sessionPreset = AVCaptureSession.Preset.photo;
cameraOutput = AVCapturePhotoOutput()
previewLayer.frame = CGRect(x: view.frame.origin.x, y: view.frame.origin.y, width: view.frame.width, height: view.frame.height)
previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
do {
try device.lockForConfiguration()
} catch {
return
}
device.focusMode = .continuousAutoFocus
device.unlockForConfiguration()
print("startcamera")
if let input = try? AVCaptureDeviceInput(device: device) {
if captureSesssion.canAddInput(input) {
captureSesssion.addInput(input)
if captureSesssion.canAddOutput(cameraOutput) {
captureSesssion.addOutput(cameraOutput)
view.layer.addSublayer(previewLayer)
captureSesssion.startRunning()
}
} else {
print("issue here : captureSesssion.canAddInput")
_ = UIAlertController(title: "Your camera doesn't seem to be working :(", message: "Please make sure your camera works", preferredStyle: .alert)
}
} else {
fatalError("TBPVC -> startCamera() : AVCaptureDeviceInput Error")
}
}
func cameraPressed(){
let outputFormat = [kCVPixelBufferPixelFormatTypeKey as String: kCMPixelFormat_32BGRA]
let settings = AVCapturePhotoSettings(format: outputFormat)
cameraOutput.capturePhoto(with: settings, delegate: self)
}
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
print("got image")
// guard let cgImageFromPhoto = photo.cgImageRepresentation()?.takeRetainedValue() else { fatalError("cgImageRepresentation()?.takeRetainedValue error") }
guard let imageData = photo.fileDataRepresentation() else {
fatalError("Error while generating image from photo capture data.")
}
guard let uiImage = UIImage(data: imageData) else {
fatalError("Unable to generate UIImage from image data.")
}
let tempImage = resizeImage(image: uiImage, targetSize: CGSize(width: 512, height: 512))
// generate a corresponding CGImage
guard let tempCgImage = tempImage.cgImage else {
fatalError("Error generating CGImage")
}
gotImage(cgImage: tempCgImage)
}
#objc func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
if let error = error {
let ac = UIAlertController(title: "Save error", message: error.localizedDescription, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default))
present(ac, animated: true)
} else {
let ac = UIAlertController(title: "Saved!", message: "Your altered image has been saved to your photos.", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default))
present(ac, animated: true)
}
}
}

AVAudioEngine, using 3D Audio

I would like to use AVAudioEngine for a 3D audio effect, where the sound source circles the user head. The source appears to move from left to right but I've been unable to figure out how to make it circle the users head. The audio source must be mono or it cant work.
I dont understand AVAudioMake3DVectorOrientation and AVAudioMake3DAngularOrientation.
I thought my math was correct but I suspect that if it were, I would have gotten the results I was looking for.
This is bare bones, so there isn't much error checking.
Would someone provide guidance to get me on track?
Thank you,
W.
import AVFoundation
class ThreeDAudio {
var _angleIndx = 0.0
var _engine : AVAudioEngine!
var _player : AVAudioPlayerNode!
var _environment : AVAudioEnvironmentNode!
var _circleTimer : Timer!
func initTimer()
{
_circleTimer = Timer.scheduledTimer(timeInterval: 0.10, target: self,
selector: #selector(ThreeDAudio.updatePosition),userInfo: nil, repeats: true)
}
#objc func updatePosition()
{
let centerX = 0.0
let centerY = 0.0
let radius = 10.0
let degToRads = Double.pi / 180.0
let angle = _angleIndx * degToRads
let x = centerX + sin(angle) * radius
let y = centerY + cos(angle) * radius
let z = 0.0
let posInSpace = AVAudioMake3DPoint(Float(x), Float(y), Float(z))
_angleIndx += 1.0
_player.position = posInSpace
print("angle: \(_angleIndx) , \(posInSpace)")
if(_angleIndx == 360.0) { _circleTimer.invalidate() }
}
func getBufferFromFileInBundle(fileName: String, fileType: String) -> AVAudioPCMBuffer?
{
// audio MUST be a monoaural source or it cant work in 3D
var file:AVAudioFile
var audioBuffer : AVAudioPCMBuffer? = nil
let path = Bundle.main.path(forResource: fileName, ofType: fileType)!
do{
file = try AVAudioFile(forReading: URL(fileURLWithPath:path))
audioBuffer = AVAudioPCMBuffer(pcmFormat:(file.processingFormat), frameCapacity: AVAudioFrameCount(file.length))
try file.read(into: audioBuffer!)
} catch let error as NSError {
print("Error AVAudioFile:\(error)")
}
return audioBuffer
}
func outputFormat() -> AVAudioFormat
{
let outputFormat = _engine.outputNode.outputFormat(forBus: 0)
let nChannels = outputFormat.channelCount // testing, will always be 2 channels
let sampleRate = outputFormat.sampleRate
return AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: nChannels)
}
func setupEngine(_ audioBuffer: AVAudioPCMBuffer )
{
_engine = AVAudioEngine()
_player = AVAudioPlayerNode()
_environment = AVAudioEnvironmentNode()
_player.renderingAlgorithm = .HRTF
_engine.attach(_player)
_engine.attach(_environment)
_engine.connect(_player, to: _environment, format: audioBuffer.format)
_engine.connect(_environment, to: _engine.outputNode, format: outputFormat())
_environment.listenerPosition = AVAudioMake3DPoint(0.0, 0.0, 0.0);
_environment.listenerVectorOrientation = AVAudioMake3DVectorOrientation(AVAudioMake3DVector(0, 0, -1), AVAudioMake3DVector(0, 0, 0))
_environment.listenerAngularOrientation = AVAudioMake3DAngularOrientation(0.0,0.0, 0.0)
do{
try _engine.start()
} catch let error as NSError {
print("Error start:\(error)")
}
}
func startAudioTest()
{
var thisFile = (name: "", fileType: "")
thisFile = (name: "sound_voices", fileType: "wav")
thisFile = (name: "Bouncing-Ball-MONO", fileType: "aiff")
let audioBuffer = getBufferFromFileInBundle(fileName: thisFile.name, fileType: thisFile.fileType )
if ( audioBuffer != nil )
{
setupEngine( audioBuffer! )
initTimer()
_player.scheduleBuffer(audioBuffer!, at: nil, options: .loops, completionHandler: nil)
_player.play()
}
}
}

SCNNode and Firebase

I have 15 box and I add an image on each boxnode as first material. How can I load the images from Firebase and add an Image to each node. I already know how to load images from Firebase, I want to know the best way I can add an image on each 15 nodes, Thanks.
Here's the view controller:
class newsVC: UIViewController, presentVCProtocol {
#IBOutlet var scnView: SCNView!
var newsScene = NewsScene(create: true)
var screenSize: CGRect!
var screenWidth: CGFloat!
var screenHeight: CGFloat!
var posts = [ArtModel]()
var post: ArtModel!
var arts = [ArtModel]()
static var imageCache: NSCache<NSString, UIImage> = NSCache()
var type: String!
override func viewDidLoad() {
super.viewDidLoad()
let scnView = self.scnView!
let scene = newsScene
scnView.scene = scene
scnView.autoenablesDefaultLighting = true
scnView.backgroundColor = UIColor.white
DataService.ds.REF_USERS.child((FIRAuth.auth()?.currentUser?.uid)!).child("arts").observe(.value) { (snapshot: FIRDataSnapshot) in
self.posts = []
if let snapshot = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshot {
if let postDict = snap.value as? Dictionary<String, AnyObject> {
let key = snap.key
let post = ArtModel(key: key, artData: postDict)
self.posts.insert(post, at: 0)
}
}
}
}
}
func configureView(_ post: ArtModel, img: UIImage? = nil, imageView: FXImageView? = nil) {
self.post = post
if img != nil {
self.newsScene.setup(image: img!)
print("IMAGE:\(img)")
} else {
let ref = FIRStorage.storage().reference(forURL: post.imgUrl)
ref.data(withMaxSize: 2 * 1024 * 1024, completion: { (data, error) in
if error != nil {
print("JESS: Unable to download image from Firebase storage")
print("Unable to download image: \(error?.localizedDescription)")
} else {
print("JESS: Image downloaded from Firebase storage")
if let imgData = data {
if let img = UIImage(data: imgData) {
self.newsScene.setup(image: img)
print("IMAGE:\(img)")
ProfileVC.imageCache.setObject(img, forKey: post.imgUrl as NSString)
}
}
}
})
}
}
}
Here's the NewsScene:
import SceneKit
import CoreMotion
import FirebaseAuth
import FirebaseStorage
import FirebaseDatabase
class NewsScene: SCNScene {
var geometry = SCNBox()
var boxnode = SCNNode()
var art: ArtModel!
var artImage = UIImage()
var index = IndexPath()
var geo = SCNBox()
var cameranode = SCNNode()
convenience init(create: Bool) {
self.init()
setup(image: artImage)
}
func setup(image: UIImage) {
self.artImage = image
typealias BoxDims = (width: CGFloat, height: CGFloat,
length: CGFloat, chamferRadius: CGFloat)
let box1Dim = BoxDims(CGFloat(0.8), CGFloat(0.8), CGFloat(0.10), CGFloat(0.01))
let box2Dim = BoxDims(CGFloat(0.7), CGFloat(0.7), CGFloat(0.10), CGFloat(0.01))
let box3Dim = BoxDims(CGFloat(0.8), CGFloat(0.6), CGFloat(0.10), CGFloat(0.01))
let box4Dim = BoxDims(CGFloat(0.8), CGFloat(0.9), CGFloat(0.10), CGFloat(0.01))
let box5Dim = BoxDims(CGFloat(0.9), CGFloat(1.0), CGFloat(0.10), CGFloat(0.01))
let box6Dim = BoxDims(CGFloat(0.4), CGFloat(0.5), CGFloat(0.10), CGFloat(0.01))
let box7Dim = BoxDims(CGFloat(0.9), CGFloat(0.7), CGFloat(0.10), CGFloat(0.01))
let box8Dim = BoxDims(CGFloat(0.7), CGFloat(0.8), CGFloat(0.10), CGFloat(0.01))
let box9Dim = BoxDims(CGFloat(0.9), CGFloat(0.9), CGFloat(0.10), CGFloat(0.01))
let box10Dim = BoxDims(CGFloat(0.6), CGFloat(0.6), CGFloat(0.10), CGFloat(0.01))
let box11Dim = BoxDims(CGFloat(0.7), CGFloat(0.7), CGFloat(0.10), CGFloat(0.01))
let box12Dim = BoxDims(CGFloat(0.8), CGFloat(0.8), CGFloat(0.10), CGFloat(0.01))
let box13Dim = BoxDims(CGFloat(0.6), CGFloat(0.8), CGFloat(0.10), CGFloat(0.01))
let box14Dim = BoxDims(CGFloat(0.6), CGFloat(0.6), CGFloat(0.10), CGFloat(0.01))
let box15Dim = BoxDims(CGFloat(0.9), CGFloat(0.9), CGFloat(0.10), CGFloat(0.01))
let allBoxDims = [box1Dim, box2Dim, box3Dim, box4Dim, box5Dim, box6Dim, box7Dim, box8Dim,box9Dim,box10Dim,box11Dim,box12Dim,box13Dim,box14Dim,box15Dim ]
let offset: Int = 50
var boxCounter: Int = 0
for xIndex: Int in 0...2 {
for yIndex: Int in 0...4 {
// create a geometry
let boxDim = allBoxDims[boxCounter]
geo = SCNBox(width: boxDim.width, height: boxDim.height, length: boxDim.length, chamferRadius: boxDim.chamferRadius)
let material = SCNMaterial()
material.diffuse.contents = image
geo.firstMaterial = material
boxCounter = boxCounter + 1
boxnode = SCNNode(geometry: geo)
boxnode.position.x = Float(xIndex - offset)
boxnode.position.y = Float(yIndex - offset)
self.rootNode.addChildNode(boxnode)
}
}
}
func deviceDidMove(motion: CMDeviceMotion?, error: NSError?) {
if let motion = motion {
boxnode.orientation = motion.gaze(atOrientation: UIApplication.shared.statusBarOrientation)
if error != nil {
print("DEVICEDIDMOVE: \(error?.localizedDescription)")
}
}
}
}