Adding reference images to ARKit from the app - ios

I was learning to use ARKIT and I was wondering if there is a way to add reference images(images to be recognised) from within the app(based on the user's choice). As per the documentation, this can be done by adding the reference images to the Assets(during the development phase) which limits the usability of the app. I was wondering if there is a way where we can download/add these images based on the user's choice and use these images as reference image(within the app).

If you have a look at the documentation for: ARReferenceImage you will note that there are two methods of generating ARReferenceImages manually:
init(CGImage, orientation: CGImagePropertyOrientation, physicalWidth: CGFloat)
init(CVPixelBuffer, orientation: CGImagePropertyOrientation, physicalWidth: CGFloat)
The one that you will need if you are downloading from a Server is the first one, which requires the use of a CGImage.
So any image(s) which you download will need to be converted using this method.
To download images from a Server first you will need to use a URLSession to download these to a location on your device e.g. the Documents Directory.
A simple example of this would look like so:
/// Downloads An Image From A Remote URL
func downloadImageTask(){
//1. Get The URL Of The Image
guard let url = URL(string: "http://www.blackmirrorz.tech/images/BlackMirrorz/blackMirrorzLogo.png") else { return }
//2. Create The Download Session
let downloadSession = URLSession(configuration: URLSession.shared.configuration, delegate: self, delegateQueue: nil)
//3. Create The Download Task & Run It
let downloadTask = downloadSession.downloadTask(with: url)
downloadTask.resume()
}
}
Having created the URLSession you would then need to register for the URLSessionDownloadDelegate and the following method:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
Whereby the location parameter refers to the:
A file URL for the temporary file. Because the file is temporary, you
must either open the file for reading or move it to a permanent
location in your app’s sandbox container directory before returning
from this delegate method.
As such your callback might look like so:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
//1. Create The Filename
let fileURL = getDocumentsDirectory().appendingPathComponent("image.png")
//2. Copy It To The Documents Directory
do {
try FileManager.default.copyItem(at: location, to: fileURL)
print("Successfuly Saved File \(fileURL)")
} catch {
print("Error Saving: \(error)")
}
}
Whereby I use the following function to get the users Documents Directory:
/// Returns The Documents Directory
///
/// - Returns: URL
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return documentsDirectory
}
Now we have downloaded the images we would then create a function to retrieve these and return a Set of ARReferenceImage which is needed by ARWorldTrackingConfiguration.
/// Creates A Set Of ARReferenceImages From All PNG Content In The Documents Directory
///
/// - Returns: Set<ARReferenceImage>
func loadedImagesFromDirectoryContents() -> Set<ARReferenceImage>?{
var index = 0
var customReferenceSet = Set<ARReferenceImage>()
let documentsDirectory = getDocumentsDirectory()
do {
let directoryContents = try FileManager.default.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil, options: [])
let filteredContents = directoryContents.filter{ $0.pathExtension == "png" }
filteredContents.forEach { (url) in
do{
//1. Create A Data Object From Our URL
let imageData = try Data(contentsOf: url)
guard let image = UIImage(data: imageData) else { return }
//2. Convert The UIImage To A CGImage
guard let cgImage = image.cgImage else { return }
//3. Get The Width Of The Image
let imageWidth = CGFloat(cgImage.width)
//4. Create A Custom AR Reference Image With A Unique Name
let customARReferenceImage = ARReferenceImage(cgImage, orientation: CGImagePropertyOrientation.up, physicalWidth: imageWidth)
customARReferenceImage.name = "MyCustomARImage\(index)"
//4. Insert The Reference Image Into Our Set
customReferenceSet.insert(customARReferenceImage)
print("ARReference Image == \(customARReferenceImage)")
index += 1
}catch{
print("Error Generating Images == \(error)")
}
}
} catch {
print("Error Reading Directory Contents == \(error)")
}
//5. Return The Set
return customReferenceSet
}
So to put this last function into place you would do the following:
let detectionImages = loadedImagesFromDirectoryContents()
configuration.detectionImages = detectionImages
augmentedRealitySession.run(configuration, options: [.resetTracking, .removeExistingAnchors])
Hope it helps...

Related

ErrorDomain Code=260 When attempting to read back a locally-stored image file

I'm making an application with one Core Data entity, Park, which is decoded from the data returned from an API request and stores the image url and local file/download location (if it has been downloaded) for each image as attributes. I created a computed property that returns a dictionary of ImageInfoObjects (which is a struct that basically just bundles the information together) based on the stored attributes. The first time I run the app, everything works fine but when I close the app and run it again it gives me the error "the file couldn't be opened because there is no such file". So I know there must be an issue with the way I'm storing the file paths in Core Data, or the way I'm reading them back to display the images. Any help would be appreciated. Code snippets below.
The method which catches the error:
func displayPhoto(_ object: ImageInfoObject, imageView: UIImageView) {
guard let location = object.downloadLocation else { return }
do {
let imageData = try Data(contentsOf: location)
let image = UIImage(data: imageData)
DispatchQueue.main.async {
imageView.image = image
}
} catch (let error) {
print(error)
}
}
The URLSessionDownloadDelegate method which is called once each image is finished downloading:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let fileManager = FileManager.default
guard let documentsPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first,
let sourceURL = downloadTask.originalRequest?.url,
let download = self.photoDownloads[sourceURL] else {
fatalError()
}
let lastPathComponent = sourceURL.lastPathComponent
let destinationURL = documentsPath.appendingPathComponent(lastPathComponent)
do {
if fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.removeItem(at: destinationURL)
}
try fileManager.copyItem(at: location, to: destinationURL)
let index = download.imageInfoObject.index
let newImageInfoObject = ImageInfoObject(url: sourceURL, index: index, downloadLocation: destinationURL)
self.park?.photoInfoObjects[index] = newImageInfoObject
switch newImageInfoObject.index {
case 1:
displayPhoto(newImageInfoObject, imageView: self.photo1View)
case 2:
displayPhoto(newImageInfoObject, imageView: self.photo2View)
default:
break
}
try context?.save()
} catch {
print(error)
}
}
The computed property that stores and retrieves the paths from the CoreData entity. The NSManaged attributes are self.photo1_url, self.photo1_location, self.photo2_url, and self.photo2_location.
public var photoInfoObjects: Dictionary<Int, ImageInfoObject> {
get {
var dictionary: Dictionary<Int, ImageInfoObject> = [:]
var object: ImageInfoObject
if let photo1_url = self.photo1_url,
let url = URL(string: photo1_url) {
if let photo1_location = self.photo1_location {
let location = URL(fileURLWithPath: photo1_location)
object = ImageInfoObject(url: url, index: 1, downloadLocation: location)
object.isDownloaded = true
} else {
object = ImageInfoObject(url: url, index: 1)
}
dictionary[1] = object
}
var object2: ImageInfoObject
if let photo2_url = self.photo2_url,
let url = URL(string: photo2_url) {
if let photo2_location = self.photo2_location {
let location = URL(fileURLWithPath: photo2_location)
object2 = ImageInfoObject(url: url, index: 2, downloadLocation: location)
object2.isDownloaded = true
} else {
object2 = ImageInfoObject(url: url, index: 2)
}
dictionary[2] = object2
}
return dictionary
}
set {
self.photo1_url = newValue[1]?.url.absoluteString
self.photo1_location = newValue[1]?.downloadLocation?.path
self.photo2_url = newValue[2]?.url.absoluteString
self.photo2_location = newValue[2]?.downloadLocation?.path
}
}

.obj file from server URL doesn't work

I need to import 3D model from server URL but it's not working properly.
Here is my code:
guard let path = modelPath, !path.isEmpty else {
fatalError("Failed to find model file path.")
}
guard let modelURL = URL(string: path) else {
fatalError("Failed to find model URL.")
}
let asset = MDLAsset(url:modelURL)
guard let object = asset.object(at: 0) as? MDLMesh else {
fatalError("Failed to get mesh from asset.")
}
...crash here at object.
MDLAsset(url:) does not handle downloading models from a server, it's only for URLs that point to local storage.
You will have to download it yourself (using URLSession or a framework like Alamofire).
Example using URLSession:
Download task will return temporary location for the file that will be deleted after the callback closure return so if you need to reuse the file you will have to re-save it somewhere.
The tempLocation file will have an extension of .tmp, which MDLAsset will not be able to process. Even if you don't need to persist the file, I didn't come up with a better way than to re-save it with the needed extension (.obj that is).
let fileManager = FileManager.default
let localModelName = "model.obj"
let serverModelURL = URL(...)
let localModelURL = fileManager
.urls(for: .documentDirectory, in: .userDomainMask[0]
.appendingPathComponent(localModelName)
let session = URLSession(configuration: .default)
let task = session.downloadTask(with: modelURL) { tempLocation, response, error in
guard let tempLocation = tempLocation else {
// handle error
return
}
do {
// FileManager's copyItem throws an error if the file exist
// so we check and remove previously downloaded file
// That's just for testing purposes, you probably wouldn't want to download
// the same model multiple times instead of just persisting it
if fileManager.fileExists(atPath: localModelURL.path) {
try fileManager.removeItem(at: localModelURL)
}
try fileManager.copyItem(at: tempLocation, to: localModelURL)
} catch {
// handle error
}
let asset = MDLAsset(url: localURL)
guard let object = asset.object(at: 0) as? MDLMesh else {
fatalError("Failed to get mesh from asset.")
}
}
task.resume() // don't forget to call resume to start downloading
I think that .obj objects also need at least an .mlt file and probably an .jpg file for texture, check if you have an error because these files missing

UIImageView, Load UIImage from remote URL

This problems it's driving me crazy...
I have this string url:
"verona-api.municipiumstaging.it/system/images/image/image/22/app_1920_1280_4.jpg"
and I have to load this image in my imageView.
this is my code :
do {
let url = URL(fileURLWithPath: "http://verona-api.municipiumstaging.it/system/images/image/image/22/app_1920_1280_4.jpg")
let data = try Data(contentsOf: url)
self.imageView.image = UIImage(data: data)
}
catch{
print(error)
}
This throw the exception :
No such file or directory.
But if I search this url with a browser I can see the image correctly!
You are using wrong method to create URL. Try URLWithString instead of fileURLWithPath. fileURLWithPath is used to get image from local file path not from internet url.
or
do {
let url = URL(string: "http://verona-api.municipiumstaging.it/system/images/image/image/22/app_1920_1280_4.jpg")
let data = try Data(contentsOf: url)
self.imageView.image = UIImage(data: data)
}
catch{
print(error)
}
The method fileURLWithPath opens file from file system. The file address is prepended with file://. You can print the url string.
From Apple documentation about + (NSURL *)fileURLWithPath:(NSString *)path;
The path that the NSURL object will represent. path should be a valid
system path, and must not be an empty path. If path begins with a
tilde, it must first be expanded with stringByExpandingTildeInPath. If
path is a relative path, it is treated as being relative to the
current working directory.
Here is one of a few possible solutions:
let imageName = "http://verona-api.municipiumstaging.it/system/images/image/image/22/app_1920_1280_4.jpg"
func loadImage(with address: String) {
// Perform on background thread
DispatchQueue.global().async {
// Create url from string address
guard let url = URL(string: address) else {
return
}
// Create data from url (You can handle exeption with try-catch)
guard let data = try? Data(contentsOf: url) else {
return
}
// Create image from data
guard let image = UIImage(data: data) else {
return
}
// Perform on UI thread
DispatchQueue.main.async {
let imageView = UIImageView(image: image)
/* Do some stuff with your imageView */
}
}
}
loadImage(with: imageName)
It's best practice if you just send a completion handler to perform on main thread to loadImage(with:).
Here the url is not of the local system but of the server.
let url = URL(fileURLWithPath: "http://verona-api.municipiumstaging.it/system/images/image/image/22/app_1920_1280_4.jpg")
Here the url created is of file which is locally on the device.
Create url like this:-
url = URL(string: "http://verona-api.municipiumstaging.it/system/images/image/image/22/app_1920_1280_4.jpg")
Use below code snippet to loading an image into imageview
func imageDownloading() {
DispatchQueue.global().async {
let url = URL(string: "http://verona-api.municipiumstaging.it/system/images/image/image/22/app_1920_1280_4.jpg")!
do {
let data = try Data(contentsOf: url)
DispatchQueue.main.async {
self.imageView.image = UIImage(data: data)
}
} catch {
print(error.localizedDescription)
}
}
}

swift: nsurlsession downloading files

There is a tableViewController with 5 cells. When I click on a cell, the download starts, if the file is not found.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row > 0 {
let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
let documentDirectoryPath:String = path[0]
let fileManager = FileManager()
let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/file%d.pdf",indexPath.row))
if fileManager.fileExists(atPath: destinationURLForFile.path){ self)
}else{
var downloadTask: URLSessionDownloadTask!
index = indexPath.row
let url = URL(string: "http://publications.gbdirect.co.uk/c_book/thecbook.pdf")!
downloadTask = backgroundSession.downloadTask(with: url)
downloadTask.resume()
}}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL){
let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
let documentDirectoryPath:String = path[0]
let fileManager = FileManager()
let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/file%d.pdf",index))
do {
try fileManager.moveItem(at: location, to: destinationURLForFile)
}catch{
print("An error occurred while moving file to destination url")
}
}
The problem is that downloading one of the files stops if I download 2 files at a time. How to fix it?
A couple of thoughts:
A single, numeric index property is obviously insufficient to keep track of the multiple downloads that might be in progress. You need some structure to keep track of the correlation between downloads and their eventual file names in the Documents folder. It might be:
struct Download {
enum Status {
case notStarted
case started
case finished
case failed(Error?)
}
/// URL of resource on web server
let remoteURL: URL
/// URL of file in Documents folder
let localURL: URL
/// The status of the download
var status: Status
}
Now that you have a type to keep track of the state of an individual download, create an array of those Download objects:
var downloads = [Download]()
You might populate that in viewDidLoad, or something like that:
override func viewDidLoad() {
super.viewDidLoad()
// create the `Download` objects, e.g. I'll create one here
let remoteURL = URL(string: "http://publications.gbdirect.co.uk/c_book/thecbook.pdf")!
let fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("file0.pdf")
let status: Download.Status
if try! fileURL.checkResourceIsReachable() {
status = .finished
} else {
status = .notStarted
}
downloads.append(Download(remoteURL: remoteURL, localURL: fileURL, status: status))
// since you're dealing with background session (e.g. tasks may have been previously
// scheduled), let's iterate through any pending tasks, updating status accordingly
backgroundSession.getAllTasks { tasks in
for task in tasks {
guard let index = self.downloads.index(where: { $0.remoteURL == task.originalRequest?.url }) else {
print("cannot find download for \(task.originalRequest?.url)")
return
}
self.downloads[index].status = .started
}
}
}
When the download is done, you can now just look up that download in our array of downloads in order to determine the file URL:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL){
guard let index = downloads.index(where: { $0.remoteURL == downloadTask.originalRequest?.url }) else {
print("cannot find download for \(downloadTask.originalRequest?.url)")
return
}
do {
try FileManager.default.moveItem(at: location, to: downloads[index].localURL)
downloads[index].status = .finished
} catch {
print("An error occurred while moving file to destination url: \(error.localizedDescription)")
downloads[index].status = .failed(error)
}
}
It's worth noting that the logic that says "if the file doesn't exist, then start download" is, most likely, insufficient. Sure, if the file exists, then the download is done. But what if a download has been started already, but hasn't yet finished? You probably do not want to start a new download if a previously initiated download simply hasn't yet finished.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row > 0 {
let index = indexPath.row - 1 // for some reason you're looking at indexes greater than zero, so let's adjust our index for a zero-based index within our array
switch downloads[index].status {
case .notStarted:
let downloadTask = backgroundSession.downloadTask(with: downloads[index].remoteURL)
downloads[index].status = .started
downloadTask.resume()
default:
break
}
}
}
Now, I don't want to get too lost in the details of these code snippets above, but rather I want to make sure you grok the basic concept, namely that you can't have a single numeric index, but rather you need some collection (an array or dictionary) to keep track of all of the various downloads that may be in progress at any given time.
You cannot download two files at a time if you are using single variables (index and downloadTask). Whenever the user selects the second cell, a new value for index is set, so using that value in urlSession:downloadTask:didFinishDownloadingTo: is a mistake when it is being called by the first task.
You need to keep that values in a collection, for example an array of tuples, keeping the index, the task and any other info about the file, for example the file path.

iOS offline HLS file size

In iOS 10, Apple added offline HLS. In the documentation, they mention:
Important: Downloaded HLS assets are stored on disk in a private
bundle format. This bundle format may change over time, and developers
should not attempt to access or store files within the bundle
directly, but should instead use AVFoundation and other iOS APIs to
interact with downloaded assets.
It appears the access to information about these files is limited. I'm trying to find the size of the stored file. Here is what I do. After download finishes, I save the relative path
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
//Save path
video?.downloadPath = location.relativePath
}
later I reconstruct the file path as follows
if let assetPath = workout.downloadPath {
let baseURL = URL(fileURLWithPath: NSHomeDirectory())
let assetURL = baseURL.appendingPathComponent(assetPath)
This works:
try FileManager.default.removeItem(at: assetURL)
This does not and returns an error that the file doesn't exist:
let att = try FileManager.default.attributesOfItem(atPath: assetURL.absoluteString)
I can load in the video asset as follows and play it offline with:
let avAsset = AVURLAsset(url: assetURL)
But this returns me an empty array:
let tracks = avAsset.tracks(withMediaType: AVMediaTypeVideo)
Once again I'm just trying to get the file size of an offline HLS asset. It appears the other answers on SO for getting a file size using FileManager don't work for these nor do the answers for getting the size of a loaded AVAsset. Thanks in advance.
Swift 5.3 Solution
Here is how to calculate offline HLS (.movpkg) File Size:
/// Calculates HLS File Size.
/// - Parameter directoryPath: file directory path.
/// - Returns: Human Redable File Size.
func getHLSFileSize(at directoryPath: String) -> String? {
var result: String? = nil
let properties: [URLResourceKey] = [.isRegularFileKey,
.totalFileAllocatedSizeKey,
/*.fileAllocatedSizeKey*/]
guard let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: directoryPath),
includingPropertiesForKeys: properties,
options: .skipsHiddenFiles,
errorHandler: nil) else {
return nil
}
let urls: [URL] = enumerator
.compactMap { $0 as? URL }
.filter { $0.absoluteString.contains(".frag") }
let regularFileResources: [URLResourceValues] = urls
.compactMap { try? $0.resourceValues(forKeys: Set(properties)) }
.filter { $0.isRegularFile == true }
let sizes: [Int64] = regularFileResources
.compactMap { $0.totalFileAllocatedSize! /* ?? $0.fileAllocatedSize */ }
.compactMap { Int64($0) }
let size = sizes.reduce(0, +)
result = ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .file)
return result
}
Usage
if let url = URL(string: localFileLocation),
let size = self.getHLSFileSize(at: url.path) {
result = String(size)
}
The only way is to sum all files sizes inside a folder where your downloaded content is stored.
- (NSUInteger)hlsFileSize:(NSURL *)fileURL {
NSUInteger size = 0;
let enumerator = [NSFileManager.defaultManager enumeratorAtURL:fileURL includingPropertiesForKeys:nil options:0 errorHandler:nil];
for (NSURL *url in enumerator) {
NSError *error = nil;
// Get values
let resourceValues = [url resourceValuesForKeys:#[NSURLIsRegularFileKey, NSURLFileAllocatedSizeKey, NSURLNameKey] error:&error];
// Skip unregular files
let isRegularFile = [resourceValues[NSURLIsRegularFileKey] boolValue];
if (!isRegularFile) {
continue;
}
let fileAllocatedSize = [resourceValues[NSURLFileAllocatedSizeKey] unsignedLongLongValue];
size += fileAllocatedSize;
}
return size;
}
Try this instead:
let att = try FileManager.default.attributesOfItem(atPath: assetURL.path)

Resources