├── .DS_Store ├── .gitattributes ├── LICENSE.md ├── Podfile ├── Podfile.lock ├── Pods ├── .DS_Store ├── Manifest.lock ├── Pods.xcodeproj │ ├── project.pbxproj │ └── xcuserdata │ │ ├── kennethblomqvist.xcuserdatad │ │ └── xcschemes │ │ │ ├── Pods-StrayScanner.xcscheme │ │ │ └── xcschememanagement.plist │ │ └── sihatafnan.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── Target Support Files │ └── Pods-StrayScanner │ ├── Pods-StrayScanner-Info.plist │ ├── Pods-StrayScanner-acknowledgements.markdown │ ├── Pods-StrayScanner-acknowledgements.plist │ ├── Pods-StrayScanner-dummy.m │ ├── Pods-StrayScanner-frameworks-Debug-input-files.xcfilelist │ ├── Pods-StrayScanner-frameworks-Debug-output-files.xcfilelist │ ├── Pods-StrayScanner-frameworks-Release-input-files.xcfilelist │ ├── Pods-StrayScanner-frameworks-Release-output-files.xcfilelist │ ├── Pods-StrayScanner-frameworks.sh │ ├── Pods-StrayScanner-umbrella.h │ ├── Pods-StrayScanner.debug.xcconfig │ ├── Pods-StrayScanner.modulemap │ └── Pods-StrayScanner.release.xcconfig ├── README.md ├── StrayScanner.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── StrayScanner.xcscheme └── xcuserdata │ ├── kennethblomqvist.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ └── xcschememanagement.plist │ └── sihatafnan.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── StrayScanner.xcworkspace ├── contents.xcworkspacedata ├── xcshareddata │ └── IDEWorkspaceChecks.plist └── xcuserdata │ ├── kennethblomqvist.xcuserdatad │ └── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── sihatafnan.xcuserdatad │ └── UserInterfaceState.xcuserstate ├── StrayScanner ├── .DS_Store ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── 1024.png │ │ ├── 120-1.png │ │ ├── 120.png │ │ ├── 152.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 20.png │ │ ├── 29.png │ │ ├── 40-1.png │ │ ├── 40-2.png │ │ ├── 40.png │ │ ├── 58-1.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 76.png │ │ ├── 80-1.png │ │ ├── 80.png │ │ ├── 87.png │ │ └── Contents.json │ └── Contents.json ├── BridgeHeader.h ├── CCode │ ├── PngEncoder.h │ ├── PngEncoder.mm │ ├── StrayScannerTests-Bridging-Header.h │ ├── lodepng.cpp │ └── lodepng.h ├── Controllers │ └── RecordSessionViewController.swift ├── Helpers │ ├── AppDaemon.swift │ ├── CameraRenderer.swift │ ├── ConfidenceEncoder.swift │ ├── DatasetEncoder.swift │ ├── DepthEncoder.swift │ ├── IMUEncoder.swift │ ├── OdometryEncoder.swift │ └── VideoEncoder.swift ├── Info-debug.plist ├── Info.plist ├── Models │ ├── Recording+CoreDataClass.swift │ └── Recording+CoreDataProperties.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Resources │ └── Colors.xcassets │ │ ├── BackgroundColor.colorset │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── DangerColor.colorset │ │ └── Contents.json │ │ ├── DarkColor.colorset │ │ └── Contents.json │ │ ├── LightColor.colorset │ │ └── Contents.json │ │ ├── MediumGrey.colorset │ │ └── Contents.json │ │ └── TextColor.colorset │ │ └── Contents.json ├── SceneDelegate.swift ├── Shaders │ ├── ShaderTypes.h │ └── Shaders.metal ├── Stray_Scanner.xcdatamodeld │ ├── .xccurrentversion │ └── Stray_Scanner.xcdatamodel │ │ └── contents ├── UI │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ └── RecordSessionView.xib └── Views │ ├── InformationView.swift │ ├── NewSession.swift │ ├── RecordButton.swift │ ├── SessionDetail.swift │ ├── SessionList.swift │ └── SessionRow.swift ├── StrayScannerTests ├── Info.plist ├── StrayScannerTests.swift └── Stray_ScannerTests.swift ├── docs ├── export.md ├── format.md └── readme.md ├── images ├── euclid.jpg ├── screenshot.jpg ├── stray-logo.png └── stray-logo.webp └── scripts └── integrate.py /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strayrobots/scanner/e0e252221048b80d288dc1f264df775aacbe4304/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.png filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2023 Stray Robots, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '14.2' 2 | use_frameworks! 3 | 4 | target 'StrayScanner' do 5 | end 6 | 7 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODFILE CHECKSUM: 81e5e599b317c26a8c925b2314ec3b051ecd5716 2 | 3 | COCOAPODS: 1.10.0 4 | -------------------------------------------------------------------------------- /Pods/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strayrobots/scanner/e0e252221048b80d288dc1f264df775aacbe4304/Pods/.DS_Store -------------------------------------------------------------------------------- /Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODFILE CHECKSUM: 81e5e599b317c26a8c925b2314ec3b051ecd5716 2 | 3 | COCOAPODS: 1.10.0 4 | -------------------------------------------------------------------------------- /Pods/Pods.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 24866C67E744750B8654D97CC98DEDE4 /* Pods-StrayScanner-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = F5EADC50C7DC33CC7852198F1C3D3B6D /* Pods-StrayScanner-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 29C455039B03CD1151B9833ADBADBF7F /* Pods-StrayScanner-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = BE5BD95B0E516855540792A1E3546D32 /* Pods-StrayScanner-dummy.m */; }; 12 | 3D7F2503125A36CA443106A64A53045E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73010CC983E3809BECEE5348DA1BB8C6 /* Foundation.framework */; }; 13 | /* End PBXBuildFile section */ 14 | 15 | /* Begin PBXFileReference section */ 16 | 3900A08661BE3B233425DD518129A775 /* Pods-StrayScanner-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-StrayScanner-acknowledgements.plist"; sourceTree = ""; }; 17 | 390C81CF744904DA5B88D0DB0F4898F4 /* Pods-StrayScanner.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-StrayScanner.modulemap"; sourceTree = ""; }; 18 | 73010CC983E3809BECEE5348DA1BB8C6 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; 19 | 7615ED2B77940E30C0BE39EAA5529253 /* Pods-StrayScanner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-StrayScanner.debug.xcconfig"; sourceTree = ""; }; 20 | 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 21 | A2E0313D6F8EA35F541E317027642860 /* Pods-StrayScanner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-StrayScanner.release.xcconfig"; sourceTree = ""; }; 22 | A3A97D92386F7D53DEB58F88452AC70B /* Pods_StrayScanner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_StrayScanner.framework; path = "Pods-StrayScanner.framework"; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | B1451AA3C604961AB6937B12E280A522 /* Pods-StrayScanner-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-StrayScanner-acknowledgements.markdown"; sourceTree = ""; }; 24 | BE5BD95B0E516855540792A1E3546D32 /* Pods-StrayScanner-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-StrayScanner-dummy.m"; sourceTree = ""; }; 25 | E7B762E232C5643F8DB1C2245BAA8587 /* Pods-StrayScanner-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-StrayScanner-Info.plist"; sourceTree = ""; }; 26 | F5EADC50C7DC33CC7852198F1C3D3B6D /* Pods-StrayScanner-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-StrayScanner-umbrella.h"; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 48B4A7C032CE61480ACF82BB45AE9303 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | 3D7F2503125A36CA443106A64A53045E /* Foundation.framework in Frameworks */, 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | 1107C1B99E04F0247618C26BEE3679D4 /* Products */ = { 42 | isa = PBXGroup; 43 | children = ( 44 | A3A97D92386F7D53DEB58F88452AC70B /* Pods_StrayScanner.framework */, 45 | ); 46 | name = Products; 47 | sourceTree = ""; 48 | }; 49 | 578452D2E740E91742655AC8F1636D1F /* iOS */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 73010CC983E3809BECEE5348DA1BB8C6 /* Foundation.framework */, 53 | ); 54 | name = iOS; 55 | sourceTree = ""; 56 | }; 57 | C7B665B913A3774D5CA0063A5CD3E332 /* Targets Support Files */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | E6994EFEFDB706E50F7EE21D591E6A1E /* Pods-StrayScanner */, 61 | ); 62 | name = "Targets Support Files"; 63 | sourceTree = ""; 64 | }; 65 | CF1408CF629C7361332E53B88F7BD30C = { 66 | isa = PBXGroup; 67 | children = ( 68 | 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, 69 | D210D550F4EA176C3123ED886F8F87F5 /* Frameworks */, 70 | 1107C1B99E04F0247618C26BEE3679D4 /* Products */, 71 | C7B665B913A3774D5CA0063A5CD3E332 /* Targets Support Files */, 72 | ); 73 | sourceTree = ""; 74 | }; 75 | D210D550F4EA176C3123ED886F8F87F5 /* Frameworks */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 578452D2E740E91742655AC8F1636D1F /* iOS */, 79 | ); 80 | name = Frameworks; 81 | sourceTree = ""; 82 | }; 83 | E6994EFEFDB706E50F7EE21D591E6A1E /* Pods-StrayScanner */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 390C81CF744904DA5B88D0DB0F4898F4 /* Pods-StrayScanner.modulemap */, 87 | B1451AA3C604961AB6937B12E280A522 /* Pods-StrayScanner-acknowledgements.markdown */, 88 | 3900A08661BE3B233425DD518129A775 /* Pods-StrayScanner-acknowledgements.plist */, 89 | BE5BD95B0E516855540792A1E3546D32 /* Pods-StrayScanner-dummy.m */, 90 | E7B762E232C5643F8DB1C2245BAA8587 /* Pods-StrayScanner-Info.plist */, 91 | F5EADC50C7DC33CC7852198F1C3D3B6D /* Pods-StrayScanner-umbrella.h */, 92 | 7615ED2B77940E30C0BE39EAA5529253 /* Pods-StrayScanner.debug.xcconfig */, 93 | A2E0313D6F8EA35F541E317027642860 /* Pods-StrayScanner.release.xcconfig */, 94 | ); 95 | name = "Pods-StrayScanner"; 96 | path = "Target Support Files/Pods-StrayScanner"; 97 | sourceTree = ""; 98 | }; 99 | /* End PBXGroup section */ 100 | 101 | /* Begin PBXHeadersBuildPhase section */ 102 | 7D7AAF7C89F3463C4BFD67121B1DE7C4 /* Headers */ = { 103 | isa = PBXHeadersBuildPhase; 104 | buildActionMask = 2147483647; 105 | files = ( 106 | 24866C67E744750B8654D97CC98DEDE4 /* Pods-StrayScanner-umbrella.h in Headers */, 107 | ); 108 | runOnlyForDeploymentPostprocessing = 0; 109 | }; 110 | /* End PBXHeadersBuildPhase section */ 111 | 112 | /* Begin PBXNativeTarget section */ 113 | 95EB939FB4C0994A4A3C34ADA174C9EF /* Pods-StrayScanner */ = { 114 | isa = PBXNativeTarget; 115 | buildConfigurationList = 460ECBBCCE3D9BE36632C08CAAA8600C /* Build configuration list for PBXNativeTarget "Pods-StrayScanner" */; 116 | buildPhases = ( 117 | 7D7AAF7C89F3463C4BFD67121B1DE7C4 /* Headers */, 118 | CB16C54DEAA8C86CEE5AEE6D85993C43 /* Sources */, 119 | 48B4A7C032CE61480ACF82BB45AE9303 /* Frameworks */, 120 | A3B7CAA6AE9E96DAB7DD2B8AB9458767 /* Resources */, 121 | ); 122 | buildRules = ( 123 | ); 124 | dependencies = ( 125 | ); 126 | name = "Pods-StrayScanner"; 127 | productName = "Pods-StrayScanner"; 128 | productReference = A3A97D92386F7D53DEB58F88452AC70B /* Pods_StrayScanner.framework */; 129 | productType = "com.apple.product-type.framework"; 130 | }; 131 | /* End PBXNativeTarget section */ 132 | 133 | /* Begin PBXProject section */ 134 | BFDFE7DC352907FC980B868725387E98 /* Project object */ = { 135 | isa = PBXProject; 136 | attributes = { 137 | LastSwiftUpdateCheck = 1100; 138 | LastUpgradeCheck = 1100; 139 | }; 140 | buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; 141 | compatibilityVersion = "Xcode 9.3"; 142 | developmentRegion = en; 143 | hasScannedForEncodings = 0; 144 | knownRegions = ( 145 | en, 146 | Base, 147 | ); 148 | mainGroup = CF1408CF629C7361332E53B88F7BD30C; 149 | productRefGroup = 1107C1B99E04F0247618C26BEE3679D4 /* Products */; 150 | projectDirPath = ""; 151 | projectRoot = ""; 152 | targets = ( 153 | 95EB939FB4C0994A4A3C34ADA174C9EF /* Pods-StrayScanner */, 154 | ); 155 | }; 156 | /* End PBXProject section */ 157 | 158 | /* Begin PBXResourcesBuildPhase section */ 159 | A3B7CAA6AE9E96DAB7DD2B8AB9458767 /* Resources */ = { 160 | isa = PBXResourcesBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | /* End PBXResourcesBuildPhase section */ 167 | 168 | /* Begin PBXSourcesBuildPhase section */ 169 | CB16C54DEAA8C86CEE5AEE6D85993C43 /* Sources */ = { 170 | isa = PBXSourcesBuildPhase; 171 | buildActionMask = 2147483647; 172 | files = ( 173 | 29C455039B03CD1151B9833ADBADBF7F /* Pods-StrayScanner-dummy.m in Sources */, 174 | ); 175 | runOnlyForDeploymentPostprocessing = 0; 176 | }; 177 | /* End PBXSourcesBuildPhase section */ 178 | 179 | /* Begin XCBuildConfiguration section */ 180 | 2C94712AD7BB503189FD70ADAF3A496B /* Release */ = { 181 | isa = XCBuildConfiguration; 182 | baseConfigurationReference = A2E0313D6F8EA35F541E317027642860 /* Pods-StrayScanner.release.xcconfig */; 183 | buildSettings = { 184 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; 185 | CLANG_ENABLE_OBJC_WEAK = NO; 186 | "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; 187 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 188 | "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; 189 | CURRENT_PROJECT_VERSION = 1; 190 | DEFINES_MODULE = YES; 191 | DYLIB_COMPATIBILITY_VERSION = 1; 192 | DYLIB_CURRENT_VERSION = 1; 193 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 194 | INFOPLIST_FILE = "Target Support Files/Pods-StrayScanner/Pods-StrayScanner-Info.plist"; 195 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 196 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 197 | LD_RUNPATH_SEARCH_PATHS = ( 198 | "$(inherited)", 199 | "@executable_path/Frameworks", 200 | "@loader_path/Frameworks", 201 | ); 202 | MACH_O_TYPE = staticlib; 203 | MODULEMAP_FILE = "Target Support Files/Pods-StrayScanner/Pods-StrayScanner.modulemap"; 204 | OTHER_LDFLAGS = ""; 205 | OTHER_LIBTOOLFLAGS = ""; 206 | PODS_ROOT = "$(SRCROOT)"; 207 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; 208 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 209 | SDKROOT = iphoneos; 210 | SKIP_INSTALL = YES; 211 | TARGETED_DEVICE_FAMILY = "1,2"; 212 | VALIDATE_PRODUCT = YES; 213 | VERSIONING_SYSTEM = "apple-generic"; 214 | VERSION_INFO_PREFIX = ""; 215 | }; 216 | name = Release; 217 | }; 218 | 54E8C58F86114E9C07D950FA5D4D8131 /* Debug */ = { 219 | isa = XCBuildConfiguration; 220 | baseConfigurationReference = 7615ED2B77940E30C0BE39EAA5529253 /* Pods-StrayScanner.debug.xcconfig */; 221 | buildSettings = { 222 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; 223 | CLANG_ENABLE_OBJC_WEAK = NO; 224 | "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; 225 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 226 | "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; 227 | CURRENT_PROJECT_VERSION = 1; 228 | DEFINES_MODULE = YES; 229 | DYLIB_COMPATIBILITY_VERSION = 1; 230 | DYLIB_CURRENT_VERSION = 1; 231 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 232 | INFOPLIST_FILE = "Target Support Files/Pods-StrayScanner/Pods-StrayScanner-Info.plist"; 233 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 234 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 235 | LD_RUNPATH_SEARCH_PATHS = ( 236 | "$(inherited)", 237 | "@executable_path/Frameworks", 238 | "@loader_path/Frameworks", 239 | ); 240 | MACH_O_TYPE = staticlib; 241 | MODULEMAP_FILE = "Target Support Files/Pods-StrayScanner/Pods-StrayScanner.modulemap"; 242 | OTHER_LDFLAGS = ""; 243 | OTHER_LIBTOOLFLAGS = ""; 244 | PODS_ROOT = "$(SRCROOT)"; 245 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; 246 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 247 | SDKROOT = iphoneos; 248 | SKIP_INSTALL = YES; 249 | TARGETED_DEVICE_FAMILY = "1,2"; 250 | VERSIONING_SYSTEM = "apple-generic"; 251 | VERSION_INFO_PREFIX = ""; 252 | }; 253 | name = Debug; 254 | }; 255 | 5B82FA55F6A12DBC8196D07B0C1EEB40 /* Release */ = { 256 | isa = XCBuildConfiguration; 257 | buildSettings = { 258 | ALWAYS_SEARCH_USER_PATHS = NO; 259 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 260 | CLANG_ANALYZER_NONNULL = YES; 261 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 262 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 263 | CLANG_CXX_LIBRARY = "libc++"; 264 | CLANG_ENABLE_MODULES = YES; 265 | CLANG_ENABLE_OBJC_ARC = YES; 266 | CLANG_ENABLE_OBJC_WEAK = YES; 267 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 268 | CLANG_WARN_BOOL_CONVERSION = YES; 269 | CLANG_WARN_COMMA = YES; 270 | CLANG_WARN_CONSTANT_CONVERSION = YES; 271 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 272 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 273 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 274 | CLANG_WARN_EMPTY_BODY = YES; 275 | CLANG_WARN_ENUM_CONVERSION = YES; 276 | CLANG_WARN_INFINITE_RECURSION = YES; 277 | CLANG_WARN_INT_CONVERSION = YES; 278 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 279 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 280 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 281 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 282 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 283 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 284 | CLANG_WARN_STRICT_PROTOTYPES = YES; 285 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 286 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 287 | CLANG_WARN_UNREACHABLE_CODE = YES; 288 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 289 | COPY_PHASE_STRIP = NO; 290 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 291 | ENABLE_NS_ASSERTIONS = NO; 292 | ENABLE_STRICT_OBJC_MSGSEND = YES; 293 | GCC_C_LANGUAGE_STANDARD = gnu11; 294 | GCC_NO_COMMON_BLOCKS = YES; 295 | GCC_PREPROCESSOR_DEFINITIONS = ( 296 | "POD_CONFIGURATION_RELEASE=1", 297 | "$(inherited)", 298 | ); 299 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 300 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 301 | GCC_WARN_UNDECLARED_SELECTOR = YES; 302 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 303 | GCC_WARN_UNUSED_FUNCTION = YES; 304 | GCC_WARN_UNUSED_VARIABLE = YES; 305 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 306 | MTL_ENABLE_DEBUG_INFO = NO; 307 | MTL_FAST_MATH = YES; 308 | PRODUCT_NAME = "$(TARGET_NAME)"; 309 | STRIP_INSTALLED_PRODUCT = NO; 310 | SWIFT_COMPILATION_MODE = wholemodule; 311 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 312 | SWIFT_VERSION = 5.0; 313 | SYMROOT = "${SRCROOT}/../build"; 314 | }; 315 | name = Release; 316 | }; 317 | 6C6178559BD9074C26E073DF5769DB42 /* Debug */ = { 318 | isa = XCBuildConfiguration; 319 | buildSettings = { 320 | ALWAYS_SEARCH_USER_PATHS = NO; 321 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 322 | CLANG_ANALYZER_NONNULL = YES; 323 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 324 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 325 | CLANG_CXX_LIBRARY = "libc++"; 326 | CLANG_ENABLE_MODULES = YES; 327 | CLANG_ENABLE_OBJC_ARC = YES; 328 | CLANG_ENABLE_OBJC_WEAK = YES; 329 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 330 | CLANG_WARN_BOOL_CONVERSION = YES; 331 | CLANG_WARN_COMMA = YES; 332 | CLANG_WARN_CONSTANT_CONVERSION = YES; 333 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 334 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 335 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 336 | CLANG_WARN_EMPTY_BODY = YES; 337 | CLANG_WARN_ENUM_CONVERSION = YES; 338 | CLANG_WARN_INFINITE_RECURSION = YES; 339 | CLANG_WARN_INT_CONVERSION = YES; 340 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 341 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 342 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 343 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 344 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 345 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 346 | CLANG_WARN_STRICT_PROTOTYPES = YES; 347 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 348 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 349 | CLANG_WARN_UNREACHABLE_CODE = YES; 350 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 351 | COPY_PHASE_STRIP = NO; 352 | DEBUG_INFORMATION_FORMAT = dwarf; 353 | ENABLE_STRICT_OBJC_MSGSEND = YES; 354 | ENABLE_TESTABILITY = YES; 355 | GCC_C_LANGUAGE_STANDARD = gnu11; 356 | GCC_DYNAMIC_NO_PIC = NO; 357 | GCC_NO_COMMON_BLOCKS = YES; 358 | GCC_OPTIMIZATION_LEVEL = 0; 359 | GCC_PREPROCESSOR_DEFINITIONS = ( 360 | "POD_CONFIGURATION_DEBUG=1", 361 | "DEBUG=1", 362 | "$(inherited)", 363 | ); 364 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 365 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 366 | GCC_WARN_UNDECLARED_SELECTOR = YES; 367 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 368 | GCC_WARN_UNUSED_FUNCTION = YES; 369 | GCC_WARN_UNUSED_VARIABLE = YES; 370 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 371 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 372 | MTL_FAST_MATH = YES; 373 | ONLY_ACTIVE_ARCH = YES; 374 | PRODUCT_NAME = "$(TARGET_NAME)"; 375 | STRIP_INSTALLED_PRODUCT = NO; 376 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 377 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 378 | SWIFT_VERSION = 5.0; 379 | SYMROOT = "${SRCROOT}/../build"; 380 | }; 381 | name = Debug; 382 | }; 383 | /* End XCBuildConfiguration section */ 384 | 385 | /* Begin XCConfigurationList section */ 386 | 460ECBBCCE3D9BE36632C08CAAA8600C /* Build configuration list for PBXNativeTarget "Pods-StrayScanner" */ = { 387 | isa = XCConfigurationList; 388 | buildConfigurations = ( 389 | 54E8C58F86114E9C07D950FA5D4D8131 /* Debug */, 390 | 2C94712AD7BB503189FD70ADAF3A496B /* Release */, 391 | ); 392 | defaultConfigurationIsVisible = 0; 393 | defaultConfigurationName = Release; 394 | }; 395 | 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { 396 | isa = XCConfigurationList; 397 | buildConfigurations = ( 398 | 6C6178559BD9074C26E073DF5769DB42 /* Debug */, 399 | 5B82FA55F6A12DBC8196D07B0C1EEB40 /* Release */, 400 | ); 401 | defaultConfigurationIsVisible = 0; 402 | defaultConfigurationName = Release; 403 | }; 404 | /* End XCConfigurationList section */ 405 | }; 406 | rootObject = BFDFE7DC352907FC980B868725387E98 /* Project object */; 407 | } 408 | -------------------------------------------------------------------------------- /Pods/Pods.xcodeproj/xcuserdata/kennethblomqvist.xcuserdatad/xcschemes/Pods-StrayScanner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 53 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Pods/Pods.xcodeproj/xcuserdata/kennethblomqvist.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Pods-StrayScanner.xcscheme 8 | 9 | isShown 10 | 11 | orderHint 12 | 0 13 | 14 | 15 | SuppressBuildableAutocreation 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Pods/Pods.xcodeproj/xcuserdata/sihatafnan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Pods-StrayScanner.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-StrayScanner/Pods-StrayScanner-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-StrayScanner/Pods-StrayScanner-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | Generated by CocoaPods - https://cocoapods.org 4 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-StrayScanner/Pods-StrayScanner-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Generated by CocoaPods - https://cocoapods.org 18 | Title 19 | 20 | Type 21 | PSGroupSpecifier 22 | 23 | 24 | StringsTable 25 | Acknowledgements 26 | Title 27 | Acknowledgements 28 | 29 | 30 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-StrayScanner/Pods-StrayScanner-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_StrayScanner : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_StrayScanner 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-StrayScanner/Pods-StrayScanner-frameworks-Debug-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-StrayScanner/Pods-StrayScanner-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/RecordButton/RecordButton.framework -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-StrayScanner/Pods-StrayScanner-frameworks-Debug-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RecordButton.framework -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-StrayScanner/Pods-StrayScanner-frameworks-Release-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-StrayScanner/Pods-StrayScanner-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/RecordButton/RecordButton.framework -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-StrayScanner/Pods-StrayScanner-frameworks-Release-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RecordButton.framework -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-StrayScanner/Pods-StrayScanner-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | function on_error { 7 | echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" 8 | } 9 | trap 'on_error $LINENO' ERR 10 | 11 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 12 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 13 | # frameworks to, so exit 0 (signalling the script phase was successful). 14 | exit 0 15 | fi 16 | 17 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 18 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 19 | 20 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 21 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 22 | BCSYMBOLMAP_DIR="BCSymbolMaps" 23 | 24 | 25 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 26 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 27 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 28 | 29 | # Copies and strips a vendored framework 30 | install_framework() 31 | { 32 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 33 | local source="${BUILT_PRODUCTS_DIR}/$1" 34 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 35 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 36 | elif [ -r "$1" ]; then 37 | local source="$1" 38 | fi 39 | 40 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 41 | 42 | if [ -L "${source}" ]; then 43 | echo "Symlinked..." 44 | source="$(readlink "${source}")" 45 | fi 46 | 47 | if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then 48 | # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied 49 | find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do 50 | echo "Installing $f" 51 | install_bcsymbolmap "$f" "$destination" 52 | rm "$f" 53 | done 54 | rmdir "${source}/${BCSYMBOLMAP_DIR}" 55 | fi 56 | 57 | # Use filter instead of exclude so missing patterns don't throw errors. 58 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 59 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 60 | 61 | local basename 62 | basename="$(basename -s .framework "$1")" 63 | binary="${destination}/${basename}.framework/${basename}" 64 | 65 | if ! [ -r "$binary" ]; then 66 | binary="${destination}/${basename}" 67 | elif [ -L "${binary}" ]; then 68 | echo "Destination binary is symlinked..." 69 | dirname="$(dirname "${binary}")" 70 | binary="${dirname}/$(readlink "${binary}")" 71 | fi 72 | 73 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 74 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 75 | strip_invalid_archs "$binary" 76 | fi 77 | 78 | # Resign the code if required by the build settings to avoid unstable apps 79 | code_sign_if_enabled "${destination}/$(basename "$1")" 80 | 81 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 82 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 83 | local swift_runtime_libs 84 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) 85 | for lib in $swift_runtime_libs; do 86 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 87 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 88 | code_sign_if_enabled "${destination}/${lib}" 89 | done 90 | fi 91 | } 92 | # Copies and strips a vendored dSYM 93 | install_dsym() { 94 | local source="$1" 95 | warn_missing_arch=${2:-true} 96 | if [ -r "$source" ]; then 97 | # Copy the dSYM into the targets temp dir. 98 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 99 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 100 | 101 | local basename 102 | basename="$(basename -s .dSYM "$source")" 103 | binary_name="$(ls "$source/Contents/Resources/DWARF")" 104 | binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" 105 | 106 | # Strip invalid architectures from the dSYM. 107 | if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then 108 | strip_invalid_archs "$binary" "$warn_missing_arch" 109 | fi 110 | if [[ $STRIP_BINARY_RETVAL == 0 ]]; then 111 | # Move the stripped file into its final destination. 112 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 113 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 114 | else 115 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 116 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" 117 | fi 118 | fi 119 | } 120 | 121 | # Used as a return value for each invocation of `strip_invalid_archs` function. 122 | STRIP_BINARY_RETVAL=0 123 | 124 | # Strip invalid architectures 125 | strip_invalid_archs() { 126 | binary="$1" 127 | warn_missing_arch=${2:-true} 128 | # Get architectures for current target binary 129 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 130 | # Intersect them with the architectures we are building for 131 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 132 | # If there are no archs supported by this binary then warn the user 133 | if [[ -z "$intersected_archs" ]]; then 134 | if [[ "$warn_missing_arch" == "true" ]]; then 135 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 136 | fi 137 | STRIP_BINARY_RETVAL=1 138 | return 139 | fi 140 | stripped="" 141 | for arch in $binary_archs; do 142 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 143 | # Strip non-valid architectures in-place 144 | lipo -remove "$arch" -output "$binary" "$binary" 145 | stripped="$stripped $arch" 146 | fi 147 | done 148 | if [[ "$stripped" ]]; then 149 | echo "Stripped $binary of architectures:$stripped" 150 | fi 151 | STRIP_BINARY_RETVAL=0 152 | } 153 | 154 | # Copies the bcsymbolmap files of a vendored framework 155 | install_bcsymbolmap() { 156 | local bcsymbolmap_path="$1" 157 | local destination="${BUILT_PRODUCTS_DIR}" 158 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" 159 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" 160 | } 161 | 162 | # Signs a framework with the provided identity 163 | code_sign_if_enabled() { 164 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 165 | # Use the current code_sign_identity 166 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 167 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 168 | 169 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 170 | code_sign_cmd="$code_sign_cmd &" 171 | fi 172 | echo "$code_sign_cmd" 173 | eval "$code_sign_cmd" 174 | fi 175 | } 176 | 177 | if [[ "$CONFIGURATION" == "Debug" ]]; then 178 | install_framework "${BUILT_PRODUCTS_DIR}/RecordButton/RecordButton.framework" 179 | fi 180 | if [[ "$CONFIGURATION" == "Release" ]]; then 181 | install_framework "${BUILT_PRODUCTS_DIR}/RecordButton/RecordButton.framework" 182 | fi 183 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 184 | wait 185 | fi 186 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-StrayScanner/Pods-StrayScanner-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_StrayScannerVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_StrayScannerVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-StrayScanner/Pods-StrayScanner.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | PODS_BUILD_DIR = ${BUILD_DIR} 4 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 5 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 6 | PODS_ROOT = ${SRCROOT}/Pods 7 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 8 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 9 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-StrayScanner/Pods-StrayScanner.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_StrayScanner { 2 | umbrella header "Pods-StrayScanner-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-StrayScanner/Pods-StrayScanner.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | PODS_BUILD_DIR = ${BUILD_DIR} 4 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 5 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 6 | PODS_ROOT = ${SRCROOT}/Pods 7 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 8 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Stray Scanner App 3 | 4 | The goal of this app, is to allow easy access to raw RGB-D video datasets recorded using LiDAR enabled iOS devices. The app can be downloaded for free [on the iOS App Store](https://apps.apple.com/us/app/stray-scanner/id1557051662). 5 | 6 | Stray Scanner RGB-D scanning view 7 | 8 | ## Usage 9 | 10 | Documentation can be found under the [docs](docs/) directory. The document [docs/format.md](docs/format.md) describes the data format and conventions used in the app. 11 | 12 | ## Related projects 13 | 14 | - [StrayVisualizer](https://github.com/kekeblom/StrayVisualizer): a project to visualize the raw data collected by the app 15 | - [MATLAB Scripts](https://github.com/PyojinKim/StrayScannerVisualizer): a set of tools to work with the data format in Matlab 16 | - [Autolabel](https://github.com/ethz-asl/autolabel): an interactive volumetric labeling tool 17 | 18 | ## Contributing 19 | 20 | Contributions are welcome. To contribute, simply open a pull request. 21 | 22 | Some ideas for improvements include: 23 | - Better scan viewer 24 | - Better ways to organize scans, e.g. through tags 25 | - Recording other modalities, such as audio, Wifi or GPS 26 | - Direct upload to cloud services or S3 27 | 28 | -------------------------------------------------------------------------------- /StrayScanner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /StrayScanner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /StrayScanner.xcodeproj/xcshareddata/xcschemes/StrayScanner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /StrayScanner.xcodeproj/xcuserdata/kennethblomqvist.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /StrayScanner.xcodeproj/xcuserdata/kennethblomqvist.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | StrayScanner copy.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 2 11 | 12 | StrayScanner debug.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 2 16 | 17 | StrayScanner.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 1 21 | 22 | 23 | SuppressBuildableAutocreation 24 | 25 | 385999B825616F2A00F3F681 26 | 27 | primary 28 | 29 | 30 | 385999D125616F3100F3F681 31 | 32 | primary 33 | 34 | 35 | 3863FE5A25FCBB0E00C1DA4F 36 | 37 | primary 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /StrayScanner.xcodeproj/xcuserdata/sihatafnan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | StrayScanner debug.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 2 11 | 12 | StrayScanner.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /StrayScanner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /StrayScanner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /StrayScanner.xcworkspace/xcuserdata/kennethblomqvist.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /StrayScanner.xcworkspace/xcuserdata/sihatafnan.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strayrobots/scanner/e0e252221048b80d288dc1f264df775aacbe4304/StrayScanner.xcworkspace/xcuserdata/sihatafnan.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /StrayScanner/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strayrobots/scanner/e0e252221048b80d288dc1f264df775aacbe4304/StrayScanner/.DS_Store -------------------------------------------------------------------------------- /StrayScanner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Stray Scanner 4 | // 5 | // Created by Kenneth Blomqvist on 11/15/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreData 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | public var appDaemon: AppDaemon? = Optional.none 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | appDaemon = AppDaemon(appDelegate: self) 19 | return true 20 | } 21 | 22 | // MARK: UISceneSession Lifecycle 23 | 24 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 25 | // Called when a new scene session is being created. 26 | // Use this method to select a configuration to create the new scene with. 27 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 28 | } 29 | 30 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 31 | // Called when the user discards a scene session. 32 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 33 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 34 | } 35 | 36 | // MARK: - Core Data stack 37 | 38 | lazy var persistentContainer: NSPersistentContainer = { 39 | /* 40 | The persistent container for the application. This implementation 41 | creates and returns a container, having loaded the store for the 42 | application to it. This property is optional since there are legitimate 43 | error conditions that could cause the creation of the store to fail. 44 | */ 45 | let container = NSPersistentContainer(name: "Stray_Scanner") 46 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 47 | if let error = error as NSError? { 48 | // Replace this implementation with code to handle the error appropriately. 49 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 50 | 51 | /* 52 | Typical reasons for an error here include: 53 | * The parent directory does not exist, cannot be created, or disallows writing. 54 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 55 | * The device is out of space. 56 | * The store could not be migrated to the current model version. 57 | Check the error message to determine what the actual problem was. 58 | */ 59 | fatalError("Unresolved error \(error), \(error.userInfo)") 60 | } 61 | }) 62 | return container 63 | }() 64 | 65 | // MARK: - Core Data Saving support 66 | 67 | func saveContext () { 68 | let context = persistentContainer.viewContext 69 | if context.hasChanges { 70 | do { 71 | try context.save() 72 | } catch { 73 | // Replace this implementation with code to handle the error appropriately. 74 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 75 | let nserror = error as NSError 76 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 77 | } 78 | } 79 | } 80 | 81 | } 82 | 83 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:50657ef7a67b6a67b842b83992ce93ec18064c3cbd59acf14561f02561f531e2 3 | size 6926 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/120-1.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f00591fd157e15d5e194fa6fc498a4085a885cc8cd028acf182cbf02126f52d9 3 | size 2724 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f00591fd157e15d5e194fa6fc498a4085a885cc8cd028acf182cbf02126f52d9 3 | size 2724 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:114b5576869f4d9a322f47d520061d972e4cd465253992fe0fd37a94802aa9ed 3 | size 3466 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e62d62b49eff5aa005a4fa29205aa8e716f31b09d164f8e8c72cb06bbe1f4836 3 | size 3717 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:49ada2c6cfcd67e7f8e531e01f511fe0a1bf5eee04d6d5f9a4187cddd6b9e1e5 3 | size 3991 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c704c9d4f9613fa7c9de965bd704fe88bd308d19619877db0bc35cf9c35637e9 3 | size 524 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:56f546abfc1fb354aa67efc358eddd448d34cdaeb7d625af73d2d504438f4782 3 | size 738 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/40-1.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e5d8696f191b57b74a4c76ec5ed069cb29dad69764ef5e96e08aed80a898a009 3 | size 1006 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/40-2.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e5d8696f191b57b74a4c76ec5ed069cb29dad69764ef5e96e08aed80a898a009 3 | size 1006 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e5d8696f191b57b74a4c76ec5ed069cb29dad69764ef5e96e08aed80a898a009 3 | size 1006 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/58-1.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ad76f892e3504b213bfe9b50efbb0551daebfd5d15c783b118b8672722fa86dd 3 | size 1352 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ad76f892e3504b213bfe9b50efbb0551daebfd5d15c783b118b8672722fa86dd 3 | size 1352 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:05886f78011cd51dac979c4f35ee10b7ab3a9f4c64caa5e4f515cb3670b4d5e2 3 | size 1420 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:692523f5f52e77aa94ba857525eee594e6bde0443c54e8c7118f12a8a10e5a9d 3 | size 1752 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/80-1.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5ed1e407a0cb76644e482e697043c9202bbd7c1e1f4c1f9cefb1719814059d45 3 | size 1859 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5ed1e407a0cb76644e482e697043c9202bbd7c1e1f4c1f9cefb1719814059d45 3 | size 1859 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a35ff0b0e8436ff0630b4ae07d8579efb74d01cc1b1235115eb234963b9eb05d 3 | size 2035 4 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "58.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "87.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "80.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "120-1.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "180.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "20.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "40-1.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "29.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "58-1.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "40-2.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "80-1.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "76.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "152.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "167.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "1024.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | } 111 | ], 112 | "info" : { 113 | "author" : "xcode", 114 | "version" : 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /StrayScanner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /StrayScanner/BridgeHeader.h: -------------------------------------------------------------------------------- 1 | // 2 | // BridgeHeader.h 3 | // Stray Scanner 4 | // 5 | // Created by Kenneth Blomqvist on 11/29/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | 9 | #ifndef BridgeHeader_h 10 | #define BridgeHeader_h 11 | #include "ShaderTypes.h" 12 | #include "PngEncoder.h" 13 | 14 | #endif /* BridgeHeader_h */ 15 | -------------------------------------------------------------------------------- /StrayScanner/CCode/PngEncoder.h: -------------------------------------------------------------------------------- 1 | // 2 | // PngWriter.h 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 2/15/22. 6 | // Copyright © 2022 Stray Robots. All rights reserved. 7 | // 8 | 9 | #ifndef PngEncoder_h 10 | #define PngEncoder_h 11 | #include 12 | 13 | @interface PngEncoder : NSObject 14 | 15 | - (instancetype) initWithDepth:(float *)content width:(int)width height:(int)height; 16 | - (NSData*) fileContents; 17 | 18 | @end 19 | #endif /* PngWriter_h */ 20 | -------------------------------------------------------------------------------- /StrayScanner/CCode/PngEncoder.mm: -------------------------------------------------------------------------------- 1 | // 2 | // PngWriter.m 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 2/15/22. 6 | // Copyright © 2022 Stray Robots. All rights reserved. 7 | // 8 | 9 | #import 10 | #define LODEPNG_NO_COMPILE_DECODER 1 11 | #define LODEPNG_NO_COMPILE_DISK 1 12 | #import "PngEncoder.h" 13 | #import "lodepng.h" 14 | #include 15 | 16 | @implementation PngEncoder { 17 | std::vector png; 18 | std::vector inputImage; 19 | int height; 20 | int width; 21 | } 22 | - (instancetype) initWithDepth:(float*) content width:(int) w height:(int) h { 23 | self = [super init]; 24 | width = w; 25 | height = h; 26 | inputImage.resize(width * height * 2); 27 | unsigned char* outData = inputImage.data(); 28 | for (int y=0; y < height; y++) { 29 | for (int x=0; x < width; x++) { 30 | float value = content[y * width + x]; 31 | uint16_t converted = uint16_t(std::round(value * 1000.0f)); 32 | unsigned int index = (y * width + x) * 2; 33 | outData[index] = converted >> 8; 34 | outData[index+1] = converted & 0xFF; 35 | } 36 | } 37 | return self; 38 | } 39 | 40 | - (NSData*) fileContents { 41 | lodepng::encode(png, inputImage, width, height, LCT_GREY, 16); 42 | NSData* outData = [[NSData alloc] initWithBytes:(void*)png.data() length:png.size()]; 43 | return outData; 44 | } 45 | 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /StrayScanner/CCode/StrayScannerTests-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /StrayScanner/Controllers/RecordSessionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordSessionViewController.swift 3 | // Stray Scanner 4 | // 5 | // Created by Kenneth Blomqvist on 11/28/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import Metal 12 | import ARKit 13 | import CoreData 14 | import CoreMotion 15 | 16 | let FpsDividers: [Int] = [1, 2, 4, 12, 60] 17 | let AvailableFpsSettings: [Int] = FpsDividers.map { Int(60 / $0) } 18 | let FpsUserDefaultsKey: String = "FPS" 19 | 20 | class MetalView : UIView { 21 | override class var layerClass: AnyClass { 22 | get { 23 | return CAMetalLayer.self 24 | } 25 | } 26 | override var layer: CAMetalLayer { 27 | return super.layer as! CAMetalLayer 28 | } 29 | } 30 | 31 | class RecordSessionViewController : UIViewController, ARSessionDelegate { 32 | private var unsupported: Bool = false 33 | private var arConfiguration: ARWorldTrackingConfiguration? 34 | private let session = ARSession() 35 | private let motionManager = CMMotionManager() 36 | private var renderer: CameraRenderer? 37 | private var updateLabelTimer: Timer? 38 | private var startedRecording: Date? 39 | private var dataContext: NSManagedObjectContext! 40 | private var datasetEncoder: DatasetEncoder? 41 | private let imuOperationQueue = OperationQueue() 42 | private var chosenFpsSetting: Int = 0 43 | @IBOutlet private var rgbView: MetalView! 44 | @IBOutlet private var depthView: MetalView! 45 | @IBOutlet private var recordButton: RecordButton! 46 | @IBOutlet private var timeLabel: UILabel! 47 | @IBOutlet weak var fpsButton: UIButton! 48 | var dismissFunction: Optional<() -> Void> = Optional.none 49 | 50 | func setDismissFunction(_ fn: Optional<() -> Void>) { 51 | self.dismissFunction = fn 52 | } 53 | override func viewWillAppear(_ animated: Bool) { 54 | self.chosenFpsSetting = UserDefaults.standard.integer(forKey: FpsUserDefaultsKey) 55 | updateFpsSetting() 56 | } 57 | 58 | override func viewDidLoad() { 59 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } 60 | self.dataContext = appDelegate.persistentContainer.newBackgroundContext() 61 | self.renderer = CameraRenderer(rgbLayer: rgbView.layer, depthLayer: depthView.layer) 62 | 63 | depthView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(viewTapped))) 64 | rgbView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(viewTapped))) 65 | 66 | setViewProperties() 67 | session.delegate = self 68 | 69 | recordButton.setCallback { (recording: Bool) in 70 | self.toggleRecording(recording) 71 | } 72 | fpsButton.layer.masksToBounds = true 73 | fpsButton.layer.cornerRadius = 12.0 74 | 75 | imuOperationQueue.qualityOfService = .userInitiated 76 | } 77 | 78 | override func viewDidDisappear(_ animated: Bool) { 79 | session.pause(); 80 | } 81 | 82 | override func viewWillDisappear(_ animated: Bool) { 83 | updateLabelTimer?.invalidate() 84 | datasetEncoder = nil 85 | } 86 | 87 | override func viewDidAppear(_ animated: Bool) { 88 | startSession() 89 | } 90 | 91 | private func startSession() { 92 | let config = ARWorldTrackingConfiguration() 93 | arConfiguration = config 94 | if !ARWorldTrackingConfiguration.isSupported || !ARWorldTrackingConfiguration.supportsFrameSemantics(.sceneDepth) { 95 | print("AR is not supported.") 96 | unsupported = true 97 | } else { 98 | config.frameSemantics.insert(.sceneDepth) 99 | session.run(config) 100 | } 101 | } 102 | 103 | private func startRawIMU() { 104 | if self.motionManager.isAccelerometerAvailable { 105 | self.motionManager.accelerometerUpdateInterval = 1.0 / 1200.0 // Set update rate 106 | self.motionManager.startAccelerometerUpdates(to: imuOperationQueue) { (data, error) in 107 | guard let data = data else { 108 | if let error = error { 109 | print("Error retrieving accelerometer data: \(error.localizedDescription)") 110 | } 111 | return 112 | } 113 | self.datasetEncoder?.addRawAccelerometer(data: data) 114 | } 115 | } else { 116 | print("Accelerometer not available on this device.") 117 | } 118 | 119 | if self.motionManager.isGyroAvailable { 120 | self.motionManager.gyroUpdateInterval = 1.0 / 1200.0 // Set update rate 121 | self.motionManager.startGyroUpdates(to: imuOperationQueue) { (data, error) in 122 | guard let data = data else { 123 | if let error = error { 124 | print("Error retrieving gyroscope data: \(error.localizedDescription)") 125 | } 126 | return 127 | } 128 | self.datasetEncoder?.addRawGyroscope(data: data) 129 | } 130 | } else { 131 | print("Gyroscope not available on this device.") 132 | } 133 | } 134 | 135 | private func stopRawIMU() { 136 | if self.motionManager.isAccelerometerActive { 137 | self.motionManager.stopAccelerometerUpdates() 138 | print("Stopped accelerometer updates.") 139 | } 140 | if self.motionManager.isGyroActive { 141 | self.motionManager.stopGyroUpdates() 142 | print("Stopped gyroscope updates.") 143 | } 144 | } 145 | 146 | private func toggleRecording(_ recording: Bool) { 147 | if unsupported { 148 | showUnsupportedAlert() 149 | return 150 | } 151 | if recording && self.startedRecording == nil { 152 | startRecording() 153 | } else if self.startedRecording != nil && !recording { 154 | stopRecording() 155 | } else { 156 | print("This should not happen. We are either not recording and want to stop, or we are recording and want to start.") 157 | } 158 | } 159 | 160 | private func startRecording() { 161 | self.startedRecording = Date() 162 | updateTime() 163 | updateLabelTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in 164 | self.updateTime() 165 | } 166 | startRawIMU() 167 | datasetEncoder = DatasetEncoder(arConfiguration: arConfiguration!, fpsDivider: FpsDividers[chosenFpsSetting]) 168 | startRawIMU() 169 | } 170 | 171 | private func stopRecording() { 172 | guard let started = self.startedRecording else { 173 | print("Hasn't started recording. Something is wrong.") 174 | return 175 | } 176 | startedRecording = nil 177 | updateLabelTimer?.invalidate() 178 | updateLabelTimer = nil 179 | // Stop IMU updates 180 | stopRawIMU() 181 | datasetEncoder?.wrapUp() 182 | if let encoder = datasetEncoder { 183 | switch encoder.status { 184 | case .allGood: 185 | saveRecording(started, encoder) 186 | case .videoEncodingError: 187 | showError() 188 | case .directoryCreationError: 189 | showError() 190 | } 191 | } else { 192 | print("No dataset encoder. Something is wrong.") 193 | } 194 | self.dismissFunction?() 195 | } 196 | 197 | private func saveRecording(_ started: Date, _ encoder: DatasetEncoder) { 198 | let sessionCount = countSessions() 199 | 200 | let duration = Date().timeIntervalSince(started) 201 | let entity = NSEntityDescription.entity(forEntityName: "Recording", in: self.dataContext)! 202 | let recording: Recording = Recording(entity: entity, insertInto: self.dataContext) 203 | recording.setValue(datasetEncoder!.id, forKey: "id") 204 | recording.setValue(duration, forKey: "duration") 205 | recording.setValue(started, forKey: "createdAt") 206 | recording.setValue("Recording \(sessionCount)", forKey: "name") 207 | recording.setValue(datasetEncoder!.rgbFilePath.relativeString, forKey: "rgbFilePath") 208 | recording.setValue(datasetEncoder!.depthFilePath.relativeString, forKey: "depthFilePath") 209 | do { 210 | try self.dataContext.save() 211 | } catch let error as NSError { 212 | print("Could not save recording. \(error), \(error.userInfo)") 213 | } 214 | } 215 | 216 | private func showError() { 217 | let controller = UIAlertController(title: "Error", 218 | message: "Something went wrong when encoding video. This should not have happened. You might want to file a bug report.", 219 | preferredStyle: .alert) 220 | controller.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Default Action"), style: .default, handler: { _ in 221 | self.dismiss(animated: true, completion: nil) 222 | })) 223 | self.present(controller, animated: true, completion: nil) 224 | } 225 | 226 | private func updateTime() { 227 | guard let started = self.startedRecording else { return } 228 | let seconds = Date().timeIntervalSince(started) 229 | let minutes: Int = Int(floor(seconds / 60).truncatingRemainder(dividingBy: 60)) 230 | let hours: Int = Int(floor(seconds / 3600)) 231 | let roundSeconds: Int = Int(floor(seconds.truncatingRemainder(dividingBy: 60))) 232 | self.timeLabel.text = String(format: "%02d:%02d:%02d", hours, minutes, roundSeconds) 233 | } 234 | 235 | @objc func viewTapped() { 236 | switch renderer!.renderMode { 237 | case .depth: 238 | renderer!.renderMode = RenderMode.rgb 239 | rgbView.isHidden = false 240 | depthView.isHidden = true 241 | case .rgb: 242 | renderer!.renderMode = RenderMode.depth 243 | depthView.isHidden = false 244 | rgbView.isHidden = true 245 | } 246 | } 247 | 248 | @IBAction func fpsButtonTapped() { 249 | chosenFpsSetting = (chosenFpsSetting + 1) % AvailableFpsSettings.count 250 | updateFpsSetting() 251 | UserDefaults.standard.set(chosenFpsSetting, forKey: FpsUserDefaultsKey) 252 | } 253 | 254 | func session(_ session: ARSession, didUpdate frame: ARFrame) { 255 | self.renderer!.render(frame: frame) 256 | if startedRecording != nil { 257 | if let encoder = datasetEncoder { 258 | encoder.add(frame: frame) 259 | } else { 260 | print("There is no video encoder. That can't be good.") 261 | } 262 | } 263 | } 264 | 265 | private func setViewProperties() { 266 | self.view.backgroundColor = UIColor(named: "BackgroundColor") 267 | } 268 | 269 | private func updateFpsSetting() { 270 | let fps = AvailableFpsSettings[chosenFpsSetting] 271 | let buttonLabel: String = "\(fps) fps" 272 | fpsButton.setTitle(buttonLabel, for: UIControl.State.normal) 273 | } 274 | 275 | private func showUnsupportedAlert() { 276 | let alert = UIAlertController(title: "Unsupported device", message: "This device doesn't seem to have the required level of ARKit support.", preferredStyle: .alert) 277 | alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in 278 | self.dismissFunction?() 279 | })) 280 | self.present(alert, animated: true) 281 | } 282 | 283 | private func countSessions() -> Int { 284 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return 0 } 285 | let request = NSFetchRequest(entityName: "Recording") 286 | do { 287 | let fetched: [NSManagedObject] = try appDelegate.persistentContainer.viewContext.fetch(request) 288 | return fetched.count 289 | } catch let error { 290 | print("Could not fetch sessions for counting. \(error.localizedDescription)") 291 | } 292 | return 0 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /StrayScanner/Helpers/AppDaemon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDaemon.swift 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 1/17/21. 6 | // Copyright © 2021 Stray Robots. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | import UIKit 12 | 13 | class AppDaemon { 14 | private let dataContext: NSManagedObjectContext? 15 | 16 | init(appDelegate: AppDelegate) { 17 | dataContext = appDelegate.persistentContainer.viewContext 18 | } 19 | 20 | public func removeDeletedEntries() { 21 | let request = NSFetchRequest(entityName: "Recording") 22 | do { 23 | let fetched: [NSManagedObject] = try dataContext?.fetch(request) ?? [] 24 | let sessions = fetched.map { session in 25 | return session as! Recording 26 | } 27 | sessions.forEach { session in 28 | if let path = session.absoluteRgbPath() { 29 | let enclosingFolder = path.deletingLastPathComponent() 30 | if !FileManager.default.fileExists(atPath: enclosingFolder.path) { 31 | // This means that the files have been removed from the device. 32 | // For example through finder/iTunes. 33 | // This is the main mechanism through which datasets are exported. 34 | print("The dataset folder has been removed.") 35 | DispatchQueue.main.async { 36 | self.removeEntry(session) 37 | } 38 | } 39 | } else { 40 | print("Session \(session) does not have an rgb file.") 41 | } 42 | } 43 | 44 | NotificationCenter.default.post(name: NSNotification.Name("sessionsChanged"), object: nil) 45 | } catch let error as NSError { 46 | print("Something went wrong. Error: \(error), \(error.userInfo)") 47 | } 48 | 49 | } 50 | 51 | private func removeEntry(_ session: Recording) { 52 | session.deleteFiles() 53 | self.dataContext?.delete(session) 54 | do { 55 | try self.dataContext?.save() 56 | } catch let error as NSError { 57 | print("Could not delete recording. \(error), \(error.userInfo)") 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /StrayScanner/Helpers/CameraRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraRenderer.swift 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 12/20/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Metal 11 | import ARKit 12 | 13 | let vertexData: [Float] = [ 14 | -1.0, -1.0, 1.0, 1.0, 15 | 1.0, -1.0, 1.0, 0.0, 16 | -1.0, 1.0, 0.0, 1.0, 17 | 1.0, 1.0, 0.0, 0.0 18 | ] 19 | 20 | enum RenderMode { 21 | case depth 22 | case rgb 23 | } 24 | 25 | class CameraRenderer { 26 | private var device: MTLDevice! 27 | private let rgbLayer: CAMetalLayer 28 | private let depthLayer: CAMetalLayer 29 | private var vertexBuffer: MTLBuffer! 30 | private var rgbPipelineState: MTLRenderPipelineState! 31 | private var depthPipelineState: MTLRenderPipelineState! 32 | private var commandQueue: MTLCommandQueue! 33 | private var rgbTextureY: MTLTexture! 34 | private var rgbTextureCbCr: MTLTexture! 35 | private var depthTexture: MTLTexture! 36 | private lazy var textureCache: CVMetalTextureCache = makeTextureCache() 37 | 38 | public var renderMode: RenderMode = RenderMode.depth 39 | 40 | init(rgbLayer: CAMetalLayer, depthLayer: CAMetalLayer) { 41 | self.rgbLayer = rgbLayer 42 | self.depthLayer = depthLayer 43 | initMetal() 44 | } 45 | 46 | func render(frame: ARFrame) { 47 | switch (renderMode) { 48 | case .depth: 49 | updateDepthTexture(frame: frame) 50 | renderDepth() 51 | case .rgb: 52 | updateRGBTexture(frame: frame) 53 | renderRGB() 54 | } 55 | } 56 | 57 | private func renderRGB() { 58 | guard let drawable = rgbLayer.nextDrawable() else { return } 59 | let renderPassDescriptor = MTLRenderPassDescriptor() 60 | renderPassDescriptor.colorAttachments[0].texture = drawable.texture 61 | renderPassDescriptor.colorAttachments[0].loadAction = .clear 62 | renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.1, green: 0.1, blue: 0.1, alpha: 1.0) 63 | 64 | let commandBuffer = commandQueue.makeCommandBuffer()! 65 | let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! 66 | renderEncoder.pushDebugGroup("RGB") 67 | renderEncoder.setCullMode(.none) 68 | renderEncoder.label = "CustomCameraViewEncoder" 69 | renderEncoder.setRenderPipelineState(rgbPipelineState) 70 | renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) 71 | renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 1) 72 | renderEncoder.setFragmentTexture(rgbTextureY, index: 0) 73 | renderEncoder.setFragmentTexture(rgbTextureCbCr, index: 1) 74 | renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) 75 | renderEncoder.popDebugGroup() 76 | renderEncoder.endEncoding() 77 | 78 | commandBuffer.present(drawable) 79 | commandBuffer.commit() 80 | } 81 | 82 | private func renderDepth() { 83 | guard let drawable = depthLayer.nextDrawable() else { return } 84 | 85 | let renderPassDescriptor = MTLRenderPassDescriptor() 86 | renderPassDescriptor.colorAttachments[0].texture = drawable.texture 87 | renderPassDescriptor.colorAttachments[0].loadAction = .clear 88 | renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.1, green: 0.1, blue: 0.1, alpha: 1.0) 89 | 90 | let commandBuffer = commandQueue.makeCommandBuffer()! 91 | let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! 92 | renderEncoder.pushDebugGroup("Depth") 93 | renderEncoder.setCullMode(.none) 94 | renderEncoder.label = "DepthEncoder" 95 | renderEncoder.setRenderPipelineState(depthPipelineState) 96 | renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) 97 | renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 1) 98 | renderEncoder.setFragmentTexture(depthTexture, index: 0) 99 | renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) 100 | renderEncoder.popDebugGroup() 101 | renderEncoder.endEncoding() 102 | 103 | commandBuffer.present(drawable) 104 | commandBuffer.commit() 105 | } 106 | 107 | private func initMetal() { 108 | device = MTLCreateSystemDefaultDevice() 109 | rgbLayer.device = device 110 | rgbLayer.pixelFormat = .bgra8Unorm 111 | rgbLayer.framebufferOnly = true 112 | 113 | depthLayer.device = device 114 | depthLayer.pixelFormat = .bgra8Unorm 115 | depthLayer.framebufferOnly = true 116 | 117 | let dataSize = vertexData.count * MemoryLayout.size 118 | vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: []) 119 | 120 | let imagePlaneVertexDescriptor = MTLVertexDescriptor() 121 | // Vertices. 122 | imagePlaneVertexDescriptor.attributes[0].format = .float2 123 | imagePlaneVertexDescriptor.attributes[0].offset = 0 124 | imagePlaneVertexDescriptor.attributes[0].bufferIndex = 0 125 | 126 | // Texture coordinates. 127 | imagePlaneVertexDescriptor.attributes[1].format = .float2 128 | imagePlaneVertexDescriptor.attributes[1].offset = 8 129 | imagePlaneVertexDescriptor.attributes[1].bufferIndex = 0 130 | 131 | // Buffer layout. 132 | imagePlaneVertexDescriptor.layouts[0].stride = 16 133 | imagePlaneVertexDescriptor.layouts[0].stepRate = 1 134 | imagePlaneVertexDescriptor.layouts[0].stepFunction = .perVertex 135 | 136 | let defaultLibrary = device.makeDefaultLibrary()! 137 | 138 | let pipeline = MTLRenderPipelineDescriptor() 139 | pipeline.label = "CustomCameraView" 140 | let vertexFunction = defaultLibrary.makeFunction(name: "drawRectangle") 141 | pipeline.vertexFunction = vertexFunction 142 | pipeline.fragmentFunction = defaultLibrary.makeFunction(name: "displayTexture") 143 | pipeline.vertexDescriptor = imagePlaneVertexDescriptor 144 | pipeline.colorAttachments[0].pixelFormat = .bgra8Unorm 145 | pipeline.sampleCount = 1 146 | 147 | let depthPipeline = MTLRenderPipelineDescriptor() 148 | depthPipeline.label = "DepthView" 149 | depthPipeline.vertexFunction = vertexFunction 150 | depthPipeline.fragmentFunction = defaultLibrary.makeFunction(name: "depthFragment") 151 | depthPipeline.vertexDescriptor = imagePlaneVertexDescriptor 152 | depthPipeline.colorAttachments[0].pixelFormat = depthLayer.pixelFormat 153 | depthPipeline.sampleCount = 1 154 | 155 | rgbPipelineState = try! device.makeRenderPipelineState(descriptor: pipeline) 156 | depthPipelineState = try! device.makeRenderPipelineState(descriptor: depthPipeline) 157 | 158 | commandQueue = device.makeCommandQueue() 159 | } 160 | 161 | private func updateRGBTexture(frame: ARFrame) { 162 | let colorImage = frame.capturedImage 163 | rgbTextureY = createTexture(fromPixelBuffer: colorImage, pixelFormat: .r8Unorm, planeIndex: 0)! 164 | rgbTextureCbCr = createTexture(fromPixelBuffer: colorImage, pixelFormat: .rg8Unorm, planeIndex: 1)! 165 | } 166 | 167 | private func updateDepthTexture(frame: ARFrame) { 168 | guard let sceneDepth = frame.sceneDepth else { return } 169 | let depthImage = sceneDepth.depthMap 170 | depthTexture = createTexture(fromPixelBuffer: depthImage, pixelFormat: .r32Float, planeIndex: 0) 171 | } 172 | 173 | private func createTexture(fromPixelBuffer pixelBuffer: CVPixelBuffer, pixelFormat: MTLPixelFormat, planeIndex: Int) -> MTLTexture? { 174 | var mtlTexture: MTLTexture? = nil 175 | let width = CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex) 176 | let height = CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex) 177 | 178 | var texture: CVMetalTexture? = nil 179 | let status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, pixelBuffer, nil, pixelFormat, width, height, planeIndex, &texture) 180 | if status == kCVReturnSuccess { 181 | mtlTexture = CVMetalTextureGetTexture(texture!) 182 | } 183 | 184 | return mtlTexture 185 | } 186 | 187 | private func makeTextureCache() -> CVMetalTextureCache { 188 | var cache: CVMetalTextureCache! 189 | CVMetalTextureCacheCreate(nil, nil, device, nil, &cache) 190 | return cache 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /StrayScanner/Helpers/ConfidenceEncoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfidenceEncoder.swift 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 3/13/21. 6 | // Copyright © 2021 Stray Robots. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreImage 11 | 12 | class ConfidenceEncoder { 13 | enum Status { 14 | case allGood 15 | case encodingError 16 | } 17 | private let baseDirectory: URL 18 | private let ciContext: CIContext 19 | private var previousFrame: Int = -1 20 | public var status: Status = Status.allGood 21 | 22 | init(outDirectory: URL) { 23 | self.baseDirectory = outDirectory 24 | self.ciContext = CIContext() 25 | do { 26 | try FileManager.default.createDirectory(at: outDirectory.absoluteURL, withIntermediateDirectories: true, attributes: nil) 27 | } catch let error { 28 | print("Could not create confidence folder. \(error.localizedDescription)") 29 | status = Status.encodingError 30 | } 31 | } 32 | 33 | func encodeFrame(frame: CVPixelBuffer, frameNumber: Int) { 34 | assert((previousFrame+1) == frameNumber, "Confidence skipped a frame. \(previousFrame+1) != \(frameNumber)") 35 | previousFrame = frameNumber 36 | let filename = String(format: "%06d", frameNumber) 37 | let image = CIImage(cvPixelBuffer: frame) 38 | assert(CVPixelBufferGetPixelFormatType(frame) == kCVPixelFormatType_OneComponent8) 39 | let framePath = self.baseDirectory.absoluteURL.appendingPathComponent(filename, isDirectory: false).appendingPathExtension("png") 40 | 41 | if let colorSpace = CGColorSpace(name: CGColorSpace.extendedGray) { 42 | do { 43 | try self.ciContext.writePNGRepresentation(of: image, to: framePath, format: CIFormat.L8, colorSpace: colorSpace) 44 | } catch let error { 45 | print("Could not save confidence value. \(error.localizedDescription)") 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /StrayScanner/Helpers/DatasetEncoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatasetEncoder.swift 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 1/2/21. 6 | // Copyright © 2021 Stray Robots. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ARKit 11 | import CryptoKit 12 | import CoreMotion 13 | 14 | class DatasetEncoder { 15 | enum Status { 16 | case allGood 17 | case videoEncodingError 18 | case directoryCreationError 19 | } 20 | private let rgbEncoder: VideoEncoder 21 | private let depthEncoder: DepthEncoder 22 | private let confidenceEncoder: ConfidenceEncoder 23 | private let datasetDirectory: URL 24 | private let odometryEncoder: OdometryEncoder 25 | private let imuEncoder: IMUEncoder 26 | private var lastFrame: ARFrame? 27 | private var dispatchGroup = DispatchGroup() 28 | private var currentFrame: Int = -1 29 | private var savedFrames: Int = 0 30 | private let frameInterval: Int // Only save every frameInterval-th frame. 31 | public let id: UUID 32 | public let rgbFilePath: URL // Relative to app document directory. 33 | public let depthFilePath: URL // Relative to app document directory. 34 | public let cameraMatrixPath: URL 35 | public let odometryPath: URL 36 | public let imuPath: URL 37 | public var status = Status.allGood 38 | private let queue: DispatchQueue 39 | 40 | private var latestAccelerometerData: (timestamp: Double, data: simd_double3)? 41 | private var latestGyroscopeData: (timestamp: Double, data: simd_double3)? 42 | 43 | 44 | init(arConfiguration: ARWorldTrackingConfiguration, fpsDivider: Int = 1) { 45 | self.frameInterval = fpsDivider 46 | self.queue = DispatchQueue(label: "encoderQueue") 47 | 48 | let width = arConfiguration.videoFormat.imageResolution.width 49 | let height = arConfiguration.videoFormat.imageResolution.height 50 | var theId: UUID = UUID() 51 | datasetDirectory = DatasetEncoder.createDirectory(id: &theId) 52 | self.id = theId 53 | self.rgbFilePath = datasetDirectory.appendingPathComponent("rgb.mp4") 54 | self.rgbEncoder = VideoEncoder(file: self.rgbFilePath, width: width, height: height) 55 | self.depthFilePath = datasetDirectory.appendingPathComponent("depth", isDirectory: true) 56 | self.depthEncoder = DepthEncoder(outDirectory: self.depthFilePath) 57 | let confidenceFilePath = datasetDirectory.appendingPathComponent("confidence", isDirectory: true) 58 | self.confidenceEncoder = ConfidenceEncoder(outDirectory: confidenceFilePath) 59 | self.cameraMatrixPath = datasetDirectory.appendingPathComponent("camera_matrix.csv", isDirectory: false) 60 | self.odometryPath = datasetDirectory.appendingPathComponent("odometry.csv", isDirectory: false) 61 | self.odometryEncoder = OdometryEncoder(url: self.odometryPath) 62 | self.imuPath = datasetDirectory.appendingPathComponent("imu.csv", isDirectory: false) 63 | self.imuEncoder = IMUEncoder(url: self.imuPath) 64 | } 65 | 66 | func add(frame: ARFrame) { 67 | let totalFrames: Int = currentFrame 68 | let frameNumber: Int = savedFrames 69 | currentFrame = currentFrame + 1 70 | if (currentFrame % frameInterval != 0) { 71 | return 72 | } 73 | dispatchGroup.enter() 74 | queue.async { 75 | if let sceneDepth = frame.sceneDepth { 76 | self.depthEncoder.encodeFrame(frame: sceneDepth.depthMap, frameNumber: frameNumber) 77 | if let confidence = sceneDepth.confidenceMap { 78 | self.confidenceEncoder.encodeFrame(frame: confidence, frameNumber: frameNumber) 79 | } else { 80 | print("warning: confidence map missing.") 81 | } 82 | } else { 83 | print("warning: scene depth missing.") 84 | } 85 | self.rgbEncoder.add(frame: VideoEncoderInput(buffer: frame.capturedImage, time: frame.timestamp), currentFrame: totalFrames) 86 | self.odometryEncoder.add(frame: frame, currentFrame: frameNumber) 87 | self.lastFrame = frame 88 | self.dispatchGroup.leave() 89 | } 90 | savedFrames = savedFrames + 1 91 | } 92 | 93 | func addRawAccelerometer(data: CMAccelerometerData) { 94 | let acceleration = simd_double3(data.acceleration.x, data.acceleration.y, data.acceleration.z) 95 | latestAccelerometerData = (timestamp: data.timestamp, data: acceleration) 96 | tryWritingIMUData() 97 | } 98 | 99 | func addRawGyroscope(data: CMGyroData) { 100 | let rotationRate = simd_double3(data.rotationRate.x, data.rotationRate.y, data.rotationRate.z) 101 | latestGyroscopeData = (timestamp: data.timestamp, data: rotationRate) 102 | tryWritingIMUData() 103 | } 104 | 105 | private func tryWritingIMUData() { 106 | guard 107 | let accelerometer = latestAccelerometerData, 108 | let gyroscope = latestGyroscopeData 109 | else { 110 | return 111 | } 112 | 113 | // Write the row to the CSV with the most recent timestamp 114 | let timestamp = max(accelerometer.timestamp, gyroscope.timestamp) 115 | imuEncoder.add( 116 | timestamp: timestamp, 117 | linear: accelerometer.data, 118 | angular: gyroscope.data 119 | ) 120 | 121 | // Clear the buffers after writing 122 | latestAccelerometerData = nil 123 | latestGyroscopeData = nil 124 | } 125 | 126 | func wrapUp() { 127 | dispatchGroup.wait() 128 | self.rgbEncoder.finishEncoding() 129 | self.imuEncoder.done() 130 | self.odometryEncoder.done() 131 | writeIntrinsics() 132 | switch self.rgbEncoder.status { 133 | case .allGood: 134 | status = .allGood 135 | case .error: 136 | status = .videoEncodingError 137 | } 138 | switch self.depthEncoder.status { 139 | case .allGood: 140 | status = .allGood 141 | case .frameEncodingError: 142 | status = .videoEncodingError 143 | print("Something went wrong encoding depth.") 144 | } 145 | switch self.confidenceEncoder.status { 146 | case .allGood: 147 | status = .allGood 148 | case .encodingError: 149 | status = .videoEncodingError 150 | print("Something went wrong encoding confidence values.") 151 | } 152 | } 153 | 154 | private func writeIntrinsics() { 155 | if let cameraMatrix = lastFrame?.camera.intrinsics { 156 | let rows = cameraMatrix.transpose.columns 157 | var csv: [String] = [] 158 | for row in [rows.0, rows.1, rows.2] { 159 | let csvLine = "\(row.x), \(row.y), \(row.z)" 160 | csv.append(csvLine) 161 | } 162 | let contents = csv.joined(separator: "\n") 163 | do { 164 | try contents.write(to: self.cameraMatrixPath, atomically: true, encoding: String.Encoding.utf8) 165 | } catch let error { 166 | print("Could not write camera matrix. \(error.localizedDescription)") 167 | } 168 | } 169 | } 170 | 171 | static private func createDirectory(id: inout UUID) -> URL { 172 | let directoryId = hashUUID(id: id) 173 | let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! 174 | var directory = URL(fileURLWithPath: directoryId, relativeTo: url) 175 | if FileManager.default.fileExists(atPath: directory.absoluteString) { 176 | // Just in case the first 5 characters clash, try again. 177 | id = UUID() 178 | directory = DatasetEncoder.createDirectory(id: &id) 179 | } 180 | do { 181 | try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil) 182 | } catch let error as NSError { 183 | print("Error creating directory. \(error), \(error.userInfo)") 184 | } 185 | return directory 186 | } 187 | 188 | static private func hashUUID(id: UUID) -> String { 189 | var hasher: SHA256 = SHA256() 190 | hasher.update(data: id.uuidString.data(using: .ascii)!) 191 | let digest = hasher.finalize() 192 | var string = "" 193 | digest.makeIterator().prefix(5).forEach { (byte: UInt8) in 194 | string += String(format: "%02x", byte) 195 | } 196 | print("Hash: \(string)") 197 | return string 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /StrayScanner/Helpers/DepthEncoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DepthEncoder.swift 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 1/24/21. 6 | // Copyright © 2021 Stray Robots. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreImage 11 | import UIKit 12 | 13 | class DepthEncoder { 14 | enum Status { 15 | case allGood 16 | case frameEncodingError 17 | } 18 | private let baseDirectory: URL 19 | public var status: Status = Status.allGood 20 | 21 | init(outDirectory: URL) { 22 | self.baseDirectory = outDirectory 23 | do { 24 | try FileManager.default.createDirectory(at: outDirectory.absoluteURL, withIntermediateDirectories: true, attributes: nil) 25 | } catch let error { 26 | print("Could not create folder. \(error.localizedDescription)") 27 | status = Status.frameEncodingError 28 | } 29 | } 30 | 31 | func encodeFrame(frame: CVPixelBuffer, frameNumber: Int) { 32 | let filename = String(format: "%06d", frameNumber) 33 | let encoder = self.convert(frame: frame) 34 | let data = encoder.fileContents() 35 | let framePath = self.baseDirectory.absoluteURL.appendingPathComponent(filename, isDirectory: false).appendingPathExtension("png") 36 | do { 37 | try data?.write(to: framePath) 38 | } catch let error { 39 | print("Could not save depth image \(frameNumber). \(error.localizedDescription)") 40 | } 41 | } 42 | 43 | private func convert(frame: CVPixelBuffer) -> PngEncoder { 44 | assert(CVPixelBufferGetPixelFormatType(frame) == kCVPixelFormatType_DepthFloat32) 45 | let height = CVPixelBufferGetHeight(frame) 46 | let width = CVPixelBufferGetWidth(frame) 47 | CVPixelBufferLockBaseAddress(frame, CVPixelBufferLockFlags.readOnly) 48 | let inBase = CVPixelBufferGetBaseAddress(frame) 49 | let inPixelData = inBase!.assumingMemoryBound(to: Float32.self) 50 | 51 | let out = PngEncoder.init(depth: inPixelData, width: Int32(width), height: Int32(height))! 52 | CVPixelBufferUnlockBaseAddress(frame, CVPixelBufferLockFlags(rawValue: 0)) 53 | return out 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /StrayScanner/Helpers/IMUEncoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OdometryEncoder.swift 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 1/31/21. 6 | // Copyright © 2021 Stray Robots. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ARKit 11 | 12 | class IMUEncoder { 13 | let path: URL 14 | let fileHandle: FileHandle 15 | 16 | init(url: URL) { 17 | self.path = url 18 | FileManager.default.createFile(atPath: self.path.absoluteString, contents:Data("".utf8), attributes: nil) 19 | do { 20 | try "".write(to: self.path, atomically: true, encoding: .utf8) 21 | self.fileHandle = try FileHandle(forWritingTo: self.path) 22 | let heading: String = "timestamp, a_x, a_y, a_z, alpha_x, alpha_y, alpha_z\n" 23 | self.fileHandle.write(heading.data(using: .utf8)!) 24 | } catch let error { 25 | print("Can't create file \(self.path.absoluteString). \(error.localizedDescription)") 26 | preconditionFailure("Can't open imu file for writing.") 27 | } 28 | } 29 | 30 | func add(timestamp: Double, linear: simd_double3, angular: simd_double3) { 31 | let line = "\(timestamp), \(linear.x), \(linear.y), \(linear.z), \(angular.x), \(angular.y), \(angular.z)\n" 32 | self.fileHandle.write(line.data(using: .utf8)!) 33 | } 34 | 35 | func done() { 36 | do { 37 | try self.fileHandle.close() 38 | } catch let error { 39 | print("Closing imu \(self.path.absoluteString) file handle failed. \(error.localizedDescription)") 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /StrayScanner/Helpers/OdometryEncoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OdometryEncoder.swift 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 1/31/21. 6 | // Copyright © 2021 Stray Robots. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Accelerate 11 | import ARKit 12 | 13 | class OdometryEncoder { 14 | let path: URL 15 | let q_AC = simd_quatf(ix: 1.0, iy: 0.0, iz: 0.0, r: 0.0) 16 | var transforms: [simd_float4x4] = [] 17 | let fileHandle: FileHandle 18 | 19 | init(url: URL) { 20 | self.path = url 21 | do { 22 | try "".write(to: self.path, atomically: true, encoding: .utf8) 23 | self.fileHandle = try FileHandle(forWritingTo: self.path) 24 | self.fileHandle.write("timestamp, frame, x, y, z, qx, qy, qz, qw\n".data(using: .utf8)!) 25 | } catch let error { 26 | print("Can't create file \(self.path.absoluteString). \(error.localizedDescription)") 27 | preconditionFailure("Can't open odometry file for writing.") 28 | } 29 | 30 | } 31 | 32 | func add(frame: ARFrame, currentFrame: Int) { 33 | let transform = frame.camera.transform 34 | transforms.append(transform) 35 | let xyz: vector_float3 = getTranslation(T: transform) 36 | let q_WA = simd_quatf(transform) 37 | let q: vector_float4 = (q_WA * q_AC).vector 38 | let frameNumber = String(format: "%06d", currentFrame) 39 | let line = "\(frame.timestamp), \(frameNumber), \(xyz.x), \(xyz.y), \(xyz.z), \(q.x), \(q.y), \(q.z), \(q.w)\n" 40 | self.fileHandle.write(line.data(using: .utf8)!) 41 | } 42 | 43 | func done() { 44 | do { 45 | try self.fileHandle.close() 46 | } catch let error { 47 | print("Can't close odometry file \(self.path.absoluteString). \(error.localizedDescription)") 48 | } 49 | } 50 | 51 | private func getTranslation(T: simd_float4x4) -> vector_float3 { 52 | let t = T[3] 53 | return vector_float3(t.x, t.y, t.z) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /StrayScanner/Helpers/VideoEncoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoEncoder.swift 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 12/30/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ARKit 11 | 12 | struct VideoEncoderInput { 13 | let buffer: CVPixelBuffer 14 | let time: TimeInterval // Relative to boot time. 15 | } 16 | 17 | class VideoEncoder { 18 | enum EncodingStatus { 19 | case allGood 20 | case error 21 | } 22 | 23 | private var videoWriter: AVAssetWriter? 24 | private var videoWriterInput: AVAssetWriterInput? 25 | private var videoAdapter: AVAssetWriterInputPixelBufferAdaptor? 26 | private let timeScale = CMTimeScale(60) 27 | public let width: CGFloat 28 | public let height: CGFloat 29 | private let systemBootedAt: TimeInterval 30 | private var done: Bool = false 31 | private var previousFrame: Int = -1 32 | public var filePath: URL 33 | public var status: EncodingStatus = EncodingStatus.allGood 34 | 35 | init(file: URL, width: CGFloat, height: CGFloat) { 36 | self.systemBootedAt = ProcessInfo.processInfo.systemUptime 37 | self.filePath = file 38 | self.width = width 39 | self.height = height 40 | initializeFile() 41 | } 42 | 43 | func finishEncoding() { 44 | self.doneRecording() 45 | } 46 | 47 | func add(frame: VideoEncoderInput, currentFrame: Int) { 48 | previousFrame = currentFrame 49 | while !videoWriterInput!.isReadyForMoreMediaData { 50 | print("Sleeping.") 51 | Thread.sleep(until: Date() + TimeInterval(0.01)) 52 | } 53 | encode(frame: frame, frameNumber: currentFrame) 54 | } 55 | 56 | private func initializeFile() { 57 | do { 58 | videoWriter = try AVAssetWriter(outputURL: self.filePath, fileType: .mp4) 59 | let settings: [String : Any] = [ 60 | AVVideoCodecKey: AVVideoCodecType.hevc, 61 | AVVideoWidthKey: self.width, 62 | AVVideoHeightKey: self.height 63 | ] 64 | let input = AVAssetWriterInput(mediaType: .video, outputSettings: settings) 65 | input.expectsMediaDataInRealTime = true 66 | input.mediaTimeScale = timeScale 67 | input.performsMultiPassEncodingIfSupported = false 68 | videoAdapter = createVideoAdapter(input) 69 | if videoWriter!.canAdd(input) { 70 | videoWriter!.add(input) 71 | videoWriterInput = input 72 | videoWriter!.startWriting() 73 | videoWriter!.startSession(atSourceTime: .zero) 74 | } else { 75 | print("Can't create writer.") 76 | } 77 | } catch let error as NSError { 78 | print("Creating AVAssetWriter failed. \(error), \(error.userInfo)") 79 | } 80 | } 81 | 82 | private func encode(frame: VideoEncoderInput, frameNumber: Int) { 83 | let image: CVPixelBuffer = frame.buffer 84 | let time = CMTime(value: Int64(frameNumber), timescale: timeScale) 85 | let success = videoAdapter!.append(image, withPresentationTime: time) 86 | if !success { 87 | print("Pixel buffer could not be appended. \(videoWriter!.error!.localizedDescription)") 88 | } 89 | } 90 | 91 | private func doneRecording() { 92 | if videoWriter?.status == .failed { 93 | let error = videoWriter!.error 94 | print("Something went wrong when writing video. \(error!.localizedDescription)") 95 | self.status = .error 96 | } else { 97 | videoWriterInput?.markAsFinished() 98 | videoWriter?.finishWriting { [weak self] in 99 | self?.videoWriter = nil 100 | self?.videoWriterInput = nil 101 | self?.videoAdapter = nil 102 | } 103 | } 104 | } 105 | 106 | private func createVideoAdapter(_ input: AVAssetWriterInput) -> AVAssetWriterInputPixelBufferAdaptor { 107 | return AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: input, sourcePixelBufferAttributes: nil) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /StrayScanner/Info-debug.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Stray Scanner Debug 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSRequiresIPhoneOS 24 | 25 | LSSupportsOpeningDocumentsInPlace 26 | 27 | NSCameraUsageDescription 28 | For recording datasets. 29 | UIApplicationSceneManifest 30 | 31 | UIApplicationSupportsMultipleScenes 32 | 33 | UISceneConfigurations 34 | 35 | UIWindowSceneSessionRoleApplication 36 | 37 | 38 | UISceneConfigurationName 39 | Default Configuration 40 | UISceneDelegateClassName 41 | $(PRODUCT_MODULE_NAME).SceneDelegate 42 | 43 | 44 | 45 | 46 | UIFileSharingEnabled 47 | 48 | UILaunchStoryboardName 49 | LaunchScreen 50 | UIRequiredDeviceCapabilities 51 | 52 | metal 53 | video-camera 54 | arkit 55 | armv7 56 | 57 | UIStatusBarStyle 58 | UIStatusBarStyleLightContent 59 | UISupportedInterfaceOrientations 60 | 61 | UIInterfaceOrientationPortrait 62 | 63 | UISupportedInterfaceOrientations~ipad 64 | 65 | UIInterfaceOrientationPortrait 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /StrayScanner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Stray Scanner 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSRequiresIPhoneOS 24 | 25 | LSSupportsOpeningDocumentsInPlace 26 | 27 | NSCameraUsageDescription 28 | For recording datasets. 29 | UIApplicationSceneManifest 30 | 31 | UIApplicationSupportsMultipleScenes 32 | 33 | UISceneConfigurations 34 | 35 | UIWindowSceneSessionRoleApplication 36 | 37 | 38 | UISceneConfigurationName 39 | Default Configuration 40 | UISceneDelegateClassName 41 | $(PRODUCT_MODULE_NAME).SceneDelegate 42 | 43 | 44 | 45 | 46 | UIFileSharingEnabled 47 | 48 | UILaunchStoryboardName 49 | LaunchScreen 50 | UIRequiredDeviceCapabilities 51 | 52 | metal 53 | video-camera 54 | arkit 55 | armv7 56 | 57 | UIStatusBarStyle 58 | UIStatusBarStyleLightContent 59 | UISupportedInterfaceOrientations 60 | 61 | UIInterfaceOrientationPortrait 62 | 63 | UISupportedInterfaceOrientations~ipad 64 | 65 | UIInterfaceOrientationPortrait 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /StrayScanner/Models/Recording+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Recording+CoreDataClass.swift 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 12/29/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | // 9 | 10 | import Foundation 11 | import CoreData 12 | 13 | @objc(Recording) 14 | public class Recording: NSManagedObject { 15 | 16 | func deleteFiles() { 17 | deleteFile(directoryPath()) 18 | } 19 | 20 | private func deleteFile(_ path: URL?) { 21 | if let filePath = path { 22 | if FileManager.default.fileExists(atPath: filePath.path) { 23 | do { 24 | try FileManager.default.removeItem(atPath: filePath.path) 25 | print("Deleted file \(filePath.absoluteString)") 26 | } catch let error as NSError { 27 | print("Could not delete file \(filePath.absoluteString). \(error), \(error.userInfo)") 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /StrayScanner/Models/Recording+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Recording+CoreDataProperties.swift 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 12/30/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | // 9 | 10 | import Foundation 11 | import CoreData 12 | 13 | 14 | extension Recording { 15 | 16 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 17 | return NSFetchRequest(entityName: "Recording") 18 | } 19 | 20 | @NSManaged public var createdAt: Date? 21 | @NSManaged public var duration: Double 22 | @NSManaged public var name: String? 23 | @NSManaged public var id: UUID? 24 | @NSManaged public var rgbFilePath: String? 25 | @NSManaged public var depthFilePath: String? 26 | 27 | func directoryPath() -> URL? { 28 | if let path = self.rgbFilePath { 29 | let rgb = URL(fileURLWithPath: path, relativeTo: pathsRelativeTo()) 30 | return rgb.deletingLastPathComponent() 31 | } 32 | return Optional.none 33 | } 34 | 35 | func absoluteRgbPath() -> URL? { 36 | if let path = self.rgbFilePath { 37 | return URL(fileURLWithPath: path, relativeTo: pathsRelativeTo()) 38 | } 39 | return Optional.none 40 | } 41 | 42 | func absoluteDepthPath() -> URL? { 43 | if let path = self.depthFilePath { 44 | return URL(fileURLWithPath: path, relativeTo: pathsRelativeTo()) 45 | } 46 | return Optional.none 47 | } 48 | 49 | private func pathsRelativeTo() -> URL { 50 | return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! 51 | } 52 | } 53 | 54 | extension Recording : Identifiable { 55 | 56 | } 57 | -------------------------------------------------------------------------------- /StrayScanner/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /StrayScanner/Resources/Colors.xcassets/BackgroundColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "229", 9 | "green" : "240", 10 | "red" : "246" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "26", 27 | "green" : "15", 28 | "red" : "9" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /StrayScanner/Resources/Colors.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /StrayScanner/Resources/Colors.xcassets/DangerColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.189", 9 | "green" : "0.350", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.189", 27 | "green" : "0.350", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /StrayScanner/Resources/Colors.xcassets/DarkColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.100", 9 | "green" : "0.100", 10 | "red" : "0.100" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /StrayScanner/Resources/Colors.xcassets/LightColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.100", 27 | "green" : "0.100", 28 | "red" : "0.100" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /StrayScanner/Resources/Colors.xcassets/MediumGrey.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.400", 9 | "green" : "0.400", 10 | "red" : "0.400" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.600", 27 | "green" : "0.600", 28 | "red" : "0.600" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /StrayScanner/Resources/Colors.xcassets/TextColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.100", 9 | "green" : "0.100", 10 | "red" : "0.100" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /StrayScanner/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Stray Scanner 4 | // 5 | // Created by Kenneth Blomqvist on 11/15/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | 21 | // Get the managed object context from the shared persistent container. 22 | let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext 23 | 24 | // Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath. 25 | // Add `@Environment(\.managedObjectContext)` in the views that will need the context. 26 | let sessionList = SessionList().environment(\.managedObjectContext, context) 27 | 28 | // Use a UIHostingController as window root view controller. 29 | if let windowScene = scene as? UIWindowScene { 30 | let window = UIWindow(windowScene: windowScene) 31 | window.rootViewController = UIHostingController(rootView: sessionList) 32 | self.window = window 33 | window.makeKeyAndVisible() 34 | } 35 | } 36 | 37 | func sceneDidDisconnect(_ scene: UIScene) { 38 | // Called as the scene is being released by the system. 39 | // This occurs shortly after the scene enters the background, or when its session is discarded. 40 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 41 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 42 | } 43 | 44 | func sceneDidBecomeActive(_ scene: UIScene) { 45 | // Called when the scene has moved from an inactive state to an active state. 46 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 47 | } 48 | 49 | func sceneWillResignActive(_ scene: UIScene) { 50 | // Called when the scene will move from an active state to an inactive state. 51 | // This may occur due to temporary interruptions (ex. an incoming phone call). 52 | } 53 | 54 | func sceneWillEnterForeground(_ scene: UIScene) { 55 | // Called as the scene transitions from the background to the foreground. 56 | // Use this method to undo the changes made on entering the background. 57 | } 58 | 59 | func sceneDidEnterBackground(_ scene: UIScene) { 60 | // Called as the scene transitions from the foreground to the background. 61 | // Use this method to save data, release shared resources, and store enough scene-specific state information 62 | // to restore the scene back to its current state. 63 | 64 | // Save changes in the application's managed object context when the application transitions to the background. 65 | (UIApplication.shared.delegate as? AppDelegate)?.saveContext() 66 | } 67 | 68 | 69 | } 70 | 71 | -------------------------------------------------------------------------------- /StrayScanner/Shaders/ShaderTypes.h: -------------------------------------------------------------------------------- 1 | // 2 | // ShaderTypes.h 3 | // Stray Scanner 4 | // 5 | // Created by Kenneth Blomqvist on 11/29/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | 9 | #ifndef ShaderTypes_h 10 | #define ShaderTypes_h 11 | 12 | enum TextureIndices { 13 | kTextureIndexY = 0, 14 | kTextureIndexCbCr = 1, 15 | }; 16 | 17 | #endif /* ShaderTypes_h */ 18 | -------------------------------------------------------------------------------- /StrayScanner/Shaders/Shaders.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Shaders.metal 3 | // Stray Scanner 4 | // 5 | // Created by Kenneth Blomqvist on 11/28/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | 9 | #include 10 | #import "ShaderTypes.h" 11 | 12 | using namespace metal; 13 | 14 | constant float MinDepth = 0.05; 15 | constant float InvMaxDepth = 1.0 / 10.0; 16 | constant float InvE3 = 1.0 / (M_E_F * M_E_F * M_E_F); 17 | 18 | struct VertexIn { 19 | float2 vertexCoordinates [[attribute(0)]]; // Position index 0. 20 | float2 textureCoordinates [[attribute(1)]]; // Texture coordinate index 1. 21 | }; 22 | 23 | struct VertexOut { 24 | float4 vertexCoordinate [[position]]; 25 | float2 textureCoordinate; 26 | }; 27 | 28 | struct ImageColorOut { 29 | float4 color; 30 | float2 textureCoordinate; 31 | }; 32 | 33 | vertex VertexOut drawRectangle(const device VertexIn* vertexIn[[ buffer(0) ]], 34 | uint vertexIndex [[ vertex_id ]]) { 35 | VertexOut out; 36 | VertexIn v = vertexIn[vertexIndex]; 37 | out.vertexCoordinate = float4(v.vertexCoordinates, 0.0, 1.0); 38 | out.textureCoordinate = v.textureCoordinates; 39 | return out; 40 | } 41 | 42 | fragment half4 displayTexture(VertexOut in [[stage_in]], 43 | texture2d capturedImageTextureY [[ texture(kTextureIndexY) ]], 44 | texture2d capturedImageTextureCbCr [[ texture(kTextureIndexCbCr) ]]) { 45 | 46 | constexpr sampler colorSampler(address::clamp_to_edge, filter::linear); 47 | 48 | half4 ycbcr = half4(half(capturedImageTextureY.sample(colorSampler, in.textureCoordinate).r), 49 | half2(capturedImageTextureCbCr.sample(colorSampler, in.textureCoordinate).rg), 1.0); 50 | 51 | const half4x4 ycbcrToRGBTransform = half4x4( 52 | half4(+1.0000f, +1.0000f, +1.0000f, +0.0000f), 53 | half4(+0.0000f, -0.3441f, +1.7720f, +0.0000f), 54 | half4(+1.4020f, -0.7141f, +0.0000f, +0.0000f), 55 | half4(-0.7010f, +0.5291f, -0.8860f, +1.0000f) 56 | ); 57 | 58 | return ycbcrToRGBTransform * ycbcr; 59 | } 60 | 61 | fragment half4 depthFragment(VertexOut in [[stage_in]], depth2d depthFrame [[ texture(0) ]]) { 62 | constexpr sampler depthSampler(address::clamp_to_edge, filter::bicubic); 63 | float depth = depthFrame.sample(depthSampler, in.textureCoordinate); 64 | 65 | depth = max(depth - MinDepth, 0.0); 66 | depth = min(depth * InvMaxDepth, M_E_F - InvE3); 67 | half clamped = 0.1 + (1.0 - (log(InvE3 + depth) + 3.0) / 4.0) * 0.8; 68 | return half4(clamped, clamped, clamped, 1.0); 69 | } 70 | -------------------------------------------------------------------------------- /StrayScanner/Stray_Scanner.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | Stray_Scanner.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /StrayScanner/Stray_Scanner.xcdatamodeld/Stray_Scanner.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /StrayScanner/UI/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /StrayScanner/UI/RecordSessionView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /StrayScanner/Views/InformationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InformationView.swift 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 2/28/21. 6 | // Copyright © 2021 Stray Robots. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct InformationView: View { 12 | let paddingLeftRight: CGFloat = 15 13 | let paddingTextTop: CGFloat = 10 14 | var body: some View { 15 | ZStack { 16 | Color("BackgroundColor").ignoresSafeArea() 17 | ScrollView { 18 | VStack(alignment: .leading) { 19 | Text("About This App").font(.title) 20 | .fontWeight(.bold) 21 | Group { 22 | bodyText(""" 23 | This app lets you record video and depth datasets using the camera and LIDAR scanner. 24 | """) 25 | 26 | heading("Transfering Datasets To Your Desktop Computer") 27 | 28 | bodyText(""" 29 | The recorded datasets can be exported by connecting your device to it with the lightning cable. 30 | 31 | On Mac, you can access the files through Finder. In the sidebar, select your device. Under the "Files" tab, you should see an entry for Stray Scanner. Expand it, then drag the folders to the desired location. There is one folder per dataset, each named after a random alphanumerical hash. 32 | 33 | On Windows, you can access the files through iTunes. 34 | 35 | Alternatively, you can access the data in the Files app under "Browse > On My iPhone > Stray Scanner" and export them to another app or move them to your iCloud drive. 36 | """) 37 | } 38 | Group { 39 | heading("Source Code") 40 | bodyText("This app is open source. You can find the source code behind the link below. The Github project also contains documentation and links to other related projects.") 41 | 42 | bodyText("To report bugs, please open an issue on the Github project. Contributions are more than welcome.") 43 | 44 | link(text: "Github project", destination: 45 | "https://github.com/StrayRobots/scanner") 46 | 47 | } 48 | Group { 49 | heading("Disclaimer") 50 | bodyText("This application is provided as is.\nIn no event shall the authors or copyright holders be liable for any claim, damages or other liability arising from using, or in connection with using the software.") 51 | Spacer() 52 | } 53 | }.padding(.all, paddingLeftRight) 54 | } 55 | .frame(minWidth: 0, maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 56 | 57 | } 58 | } 59 | 60 | private func heading(_ text: String) -> some View { 61 | Text(text) 62 | .font(.title3) 63 | .fontWeight(.bold) 64 | .padding(.top, 20) 65 | } 66 | 67 | private func bodyText(_ text: String) -> some View { 68 | Text(text) 69 | .font(.body) 70 | .multilineTextAlignment(.leading) 71 | .lineSpacing(1.25) 72 | .padding(.top, paddingTextTop) 73 | } 74 | 75 | private func link(text: String, destination: String) -> some View { 76 | Text(text) 77 | .font(.body) 78 | .foregroundColor(Color.blue) 79 | .padding(.top, paddingTextTop) 80 | .onTapGesture { 81 | let url = URL.init(string: destination) 82 | guard let destinationUrl = url else { return } 83 | UIApplication.shared.open(destinationUrl) 84 | } 85 | } 86 | } 87 | 88 | struct InformationView_Previews: PreviewProvider { 89 | static var previews: some View { 90 | InformationView() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /StrayScanner/Views/NewSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewSession.swift 3 | // Stray Scanner 4 | // 5 | // Created by Kenneth Blomqvist on 11/28/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct NavigationConfigurator: UIViewControllerRepresentable { 12 | var configure: (UINavigationController) -> Void = { _ in } 13 | 14 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIViewController { 15 | UIViewController() 16 | } 17 | func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext) { 18 | if let nc = uiViewController.navigationController { 19 | self.configure(nc) 20 | } 21 | } 22 | } 23 | 24 | struct RecordSessionManager: UIViewControllerRepresentable { 25 | @Environment(\.presentationMode) var presentationMode: Binding 26 | 27 | func makeUIViewController(context: Context) -> some UIViewController { 28 | let viewController = RecordSessionViewController(nibName: "RecordSessionView", bundle: nil) 29 | viewController.setDismissFunction { 30 | presentationMode.wrappedValue.dismiss() 31 | viewController.setDismissFunction(Optional.none) 32 | } 33 | return viewController 34 | } 35 | 36 | func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {} 37 | } 38 | 39 | struct NewSessionView : View { 40 | 41 | var body: some View { 42 | RecordSessionManager() 43 | .padding(.vertical, 0.0) 44 | .navigationBarTitle("Recording") 45 | .navigationBarTitleDisplayMode(.inline) 46 | .edgesIgnoringSafeArea(.all) 47 | .background(NavigationConfigurator { nc in 48 | nc.navigationBar.barTintColor = UIColor(named: "BackgroundColor") 49 | }) 50 | 51 | } 52 | } 53 | 54 | struct NewSessionView_Previews: PreviewProvider { 55 | static var previews: some View { 56 | NewSessionView() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /StrayScanner/Views/RecordButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordButton.swift 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 12/27/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | @IBDesignable 13 | class RecordButton : UIView { 14 | private let circleStroke: CGFloat = 5.0 15 | private let animationDuration = 0.1 16 | private var recording: Bool = false 17 | private var disk: CALayer! 18 | private var callback: Optional<(Bool) -> Void> = Optional.none 19 | 20 | override public init(frame: CGRect) { 21 | super.init(frame: frame) 22 | self.backgroundColor = UIColor.clear 23 | } 24 | 25 | override func layoutSubviews() { 26 | setup() 27 | } 28 | 29 | required public init?(coder aCoder: NSCoder) { 30 | super.init(coder: aCoder) 31 | self.backgroundColor = UIColor.clear 32 | } 33 | 34 | func setCallback(callback: @escaping (Bool) -> Void) { 35 | self.callback = Optional.some(callback) 36 | } 37 | 38 | @objc func buttonPressed() { 39 | self.animateButton() 40 | self.recording = !self.recording 41 | self.callback?(self.recording) 42 | } 43 | 44 | private func setup() { 45 | drawInner() 46 | drawEdge() 47 | self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(buttonPressed))) 48 | } 49 | 50 | private func drawEdge() { 51 | let circleLayer = CAShapeLayer() 52 | let circleRadius: CGFloat = self.bounds.height * 0.5 - circleStroke; 53 | let x: CGFloat = self.bounds.size.width / 2 54 | let y: CGFloat = self.bounds.size.height / 2 55 | let path = CGMutablePath() 56 | path.addArc(center: CGPoint(x: x, y: y), radius: circleRadius, startAngle: CGFloat(0), endAngle: CGFloat(Float.pi * 2.0), clockwise: false) 57 | circleLayer.path = path 58 | circleLayer.fillColor = UIColor.clear.cgColor 59 | circleLayer.strokeColor = UIColor(named: "DarkColor")!.cgColor 60 | circleLayer.lineWidth = circleStroke 61 | circleLayer.opacity = 1.0 62 | self.layer.addSublayer(circleLayer) 63 | } 64 | 65 | private func drawInner() { 66 | disk = CALayer() 67 | let diameter = self.bounds.height - circleStroke * 4.0 68 | let radius = diameter * 0.5 69 | let x: CGFloat = self.bounds.width * 0.5 70 | let y: CGFloat = self.bounds.height * 0.5 71 | let rect = CGRect(x: 0, y: 0, width: diameter, height: diameter) 72 | disk.backgroundColor = UIColor.red.cgColor 73 | disk.position.x = x 74 | disk.position.y = y 75 | disk.bounds = rect 76 | disk.cornerRadius = radius 77 | disk.opacity = 1.0 78 | disk.transform = CATransform3DIdentity 79 | self.layer.addSublayer(disk) 80 | } 81 | 82 | private func animateButton() { 83 | // Called before the flag is flipped. 84 | if self.recording { 85 | // Finished recording. 86 | self.animateToIdle() 87 | } else { 88 | self.animateToRecording() 89 | } 90 | } 91 | 92 | private func animateToRecording() { 93 | let squaringAnimation = CABasicAnimation(keyPath: "cornerRadius") 94 | squaringAnimation.fromValue = cornerRadius(recording: false) 95 | squaringAnimation.toValue = cornerRadius(recording: true) 96 | squaringAnimation.isRemovedOnCompletion = false 97 | squaringAnimation.fillMode = CAMediaTimingFillMode.forwards 98 | 99 | let scaleAnimation = CABasicAnimation(keyPath: "transform.scale") 100 | scaleAnimation.fromValue = 1.0 101 | scaleAnimation.toValue = 0.5 102 | scaleAnimation.isRemovedOnCompletion = false 103 | scaleAnimation.fillMode = CAMediaTimingFillMode.forwards 104 | 105 | let animationGroup = CAAnimationGroup() 106 | animationGroup.animations = [squaringAnimation, scaleAnimation] 107 | animationGroup.isRemovedOnCompletion = false 108 | animationGroup.fillMode = CAMediaTimingFillMode.forwards 109 | animationGroup.duration = animationDuration 110 | disk.add(animationGroup, forKey: "cornerRadius") 111 | } 112 | 113 | private func animateToIdle() { 114 | let roundingAnimation = CABasicAnimation(keyPath: "cornerRadius") 115 | roundingAnimation.fromValue = cornerRadius(recording: true) 116 | roundingAnimation.toValue = cornerRadius(recording: false) 117 | roundingAnimation.fillMode = CAMediaTimingFillMode.forwards 118 | roundingAnimation.isRemovedOnCompletion = false 119 | 120 | let scaleAnimation = CABasicAnimation(keyPath: "transform.scale") 121 | scaleAnimation.fromValue = 0.5 122 | scaleAnimation.toValue = 1.0 123 | scaleAnimation.isRemovedOnCompletion = false 124 | scaleAnimation.fillMode = CAMediaTimingFillMode.forwards 125 | 126 | let animationGroup = CAAnimationGroup() 127 | animationGroup.animations = [roundingAnimation, scaleAnimation] 128 | animationGroup.isRemovedOnCompletion = false 129 | animationGroup.fillMode = CAMediaTimingFillMode.forwards 130 | animationGroup.duration = animationDuration 131 | disk.add(animationGroup, forKey: "cornerRadius") 132 | } 133 | 134 | @objc override func prepareForInterfaceBuilder() { 135 | super.prepareForInterfaceBuilder() 136 | drawInner() 137 | drawEdge() 138 | self.backgroundColor = UIColor.clear 139 | } 140 | 141 | private func cornerRadius(recording: Bool) -> CGFloat { 142 | if recording { 143 | return 10.0 144 | } else { 145 | return (self.bounds.height - circleStroke * 4.0) * 0.5 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /StrayScanner/Views/SessionDetail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionDetailView.swift 3 | // StrayScanner 4 | // 5 | // Created by Kenneth Blomqvist on 12/30/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AVKit 11 | import CoreData 12 | 13 | class SessionDetailViewModel: ObservableObject { 14 | private var dataContext: NSManagedObjectContext? 15 | 16 | init() { 17 | let appDelegate = UIApplication.shared.delegate as? AppDelegate 18 | self.dataContext = appDelegate?.persistentContainer.viewContext 19 | } 20 | 21 | func title(recording: Recording) -> String { 22 | let dateFormatter = DateFormatter() 23 | dateFormatter.dateStyle = .long 24 | dateFormatter.timeStyle = .short 25 | 26 | if let created = recording.createdAt { 27 | return dateFormatter.string(from: created) 28 | } else { 29 | return recording.name ?? "Recording" 30 | } 31 | } 32 | 33 | func delete(recording: Recording) { 34 | recording.deleteFiles() 35 | self.dataContext?.delete(recording) 36 | do { 37 | try self.dataContext?.save() 38 | } catch let error as NSError { 39 | print("Could not save recording. \(error), \(error.userInfo)") 40 | } 41 | } 42 | } 43 | 44 | struct SessionDetailView: View { 45 | @ObservedObject var viewModel = SessionDetailViewModel() 46 | var recording: Recording 47 | @Environment(\.presentationMode) var presentationMode: Binding 48 | 49 | let defaultUrl = URL(fileURLWithPath: "") 50 | 51 | var body: some View { 52 | let width = UIScreen.main.bounds.size.width 53 | let height = width * 0.75 54 | ZStack { 55 | Color("BackgroundColor") 56 | .edgesIgnoringSafeArea(.all) 57 | VStack { 58 | let player = AVPlayer(url: recording.absoluteRgbPath() ?? defaultUrl) 59 | VideoPlayer(player: player) 60 | .frame(width: width, height: height) 61 | .padding(.horizontal, 0.0) 62 | Button(action: deleteItem) { 63 | Text("Delete").foregroundColor(Color("DangerColor")) 64 | } 65 | } 66 | .navigationBarTitle(viewModel.title(recording: recording)) 67 | .background(Color("BackgroundColor")) 68 | } 69 | } 70 | 71 | func deleteItem() { 72 | viewModel.delete(recording: recording) 73 | self.presentationMode.wrappedValue.dismiss() 74 | } 75 | } 76 | 77 | 78 | 79 | struct SessionDetailView_Previews: PreviewProvider { 80 | static var recording: Recording = { () -> Recording in 81 | let rec = Recording() 82 | rec.id = UUID() 83 | rec.name = "Placeholder name" 84 | rec.createdAt = Date() 85 | rec.duration = 30.0 86 | return rec 87 | }() 88 | 89 | static var previews: some View { 90 | SessionDetailView(recording: recording) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /StrayScanner/Views/SessionList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionList.swift 3 | // Stray Scanner 4 | // 5 | // Created by Kenneth Blomqvist on 11/15/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import CoreData 11 | 12 | class SessionListViewModel: ObservableObject { 13 | private var dataContext: NSManagedObjectContext? 14 | @Published var sessions: [Recording] = [] 15 | 16 | init() { 17 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } 18 | dataContext = appDelegate.persistentContainer.viewContext 19 | self.sessions = [] 20 | NotificationCenter.default.addObserver(self, selector: #selector(sessionsChanged), name: NSNotification.Name("sessionsChanged"), object: nil) 21 | } 22 | 23 | deinit { 24 | NotificationCenter.default.removeObserver(self) 25 | } 26 | 27 | func fetchSessions() { 28 | let request = NSFetchRequest(entityName: "Recording") 29 | do { 30 | let fetched: [NSManagedObject] = try dataContext?.fetch(request) ?? [] 31 | sessions = fetched.map { session in 32 | return session as! Recording 33 | } 34 | 35 | } catch let error as NSError { 36 | print("Something went wrong. Error: \(error), \(error.userInfo)") 37 | } 38 | } 39 | 40 | @objc func sessionsChanged() { 41 | fetchSessions() 42 | } 43 | 44 | } 45 | 46 | struct SessionList: View { 47 | @ObservedObject var viewModel = SessionListViewModel() 48 | @State private var showingInfo = false 49 | 50 | init() { 51 | UITableView.appearance().backgroundColor = UIColor(named: "BackgroundColor") 52 | } 53 | 54 | var body: some View { 55 | ZStack { 56 | Color.black 57 | .ignoresSafeArea() 58 | NavigationView { 59 | VStack(alignment: .leading) { 60 | HStack { 61 | Text("Recordings") 62 | .foregroundColor(Color("TextColor")) 63 | .font(.largeTitle) 64 | .fontWeight(.bold) 65 | .multilineTextAlignment(.center) 66 | .padding([.top, .leading], 15.0) 67 | Spacer() 68 | Button(action: { 69 | showingInfo.toggle() 70 | }, label: { 71 | Image(systemName: "info.circle") 72 | .resizable() 73 | .frame(width: 25, height: 25, alignment: .center) 74 | .padding(.top, 17) 75 | .padding(.trailing, 20) 76 | .foregroundColor(Color("TextColor")) 77 | }).sheet(isPresented: $showingInfo) { 78 | InformationView() 79 | } 80 | } 81 | 82 | if !viewModel.sessions.isEmpty { 83 | List { 84 | ForEach(Array(viewModel.sessions.enumerated()), id: \.element) { i, recording in 85 | NavigationLink(destination: SessionDetailView(recording: recording)) { 86 | SessionRow(session: recording) 87 | } 88 | } 89 | } 90 | Spacer() 91 | } else { 92 | Spacer() 93 | Text("No recorded sessions. Record one, and it will appear here.") 94 | .font(.body) 95 | .multilineTextAlignment(.center) 96 | .padding(.horizontal, 50.0) 97 | } 98 | HStack { 99 | Spacer() 100 | NavigationLink(destination: NewSessionView(), label: { 101 | Text("Record new session") 102 | .font(.title3) 103 | .padding(20) 104 | .background(Color("TextColor")) 105 | .foregroundColor(Color("LightColor")) 106 | .cornerRadius(35) 107 | .padding(20) 108 | }) 109 | Spacer() 110 | } 111 | if (viewModel.sessions.isEmpty) { 112 | Spacer() 113 | } 114 | } 115 | .navigationBarHidden(true) 116 | .background(Color("BackgroundColor").ignoresSafeArea()) 117 | .onAppear { 118 | DispatchQueue.main.async { 119 | viewModel.fetchSessions() 120 | } 121 | FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).forEach({ url in 122 | let relative = url.relativeString 123 | print("relative url: \(relative)") 124 | }) 125 | let delegate = UIApplication.shared.delegate as! AppDelegate 126 | delegate.appDaemon?.removeDeletedEntries() 127 | } 128 | } 129 | .background(Color("BackgroundColor").edgesIgnoringSafeArea(.all)) 130 | } 131 | } 132 | } 133 | 134 | struct SessionList_Previews: PreviewProvider { 135 | static var previews: some View { 136 | SessionList() 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /StrayScanner/Views/SessionRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionRow.swift 3 | // Stray Scanner 4 | // 5 | // Created by Kenneth Blomqvist on 11/15/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct SessionRow: View { 12 | var session: Recording 13 | 14 | var body: some View { 15 | let duration = String(format: "%ds", Int(round(session.duration))) 16 | HStack { 17 | VStack(alignment: .leading) { 18 | Text(sessionTitle()) 19 | .multilineTextAlignment(.leading) 20 | .padding(.leading, 0.0) 21 | Text("\(duration)") 22 | .font(.caption) 23 | .multilineTextAlignment(.leading) 24 | .padding(.leading, 0.0) 25 | } 26 | Spacer() 27 | } 28 | } 29 | 30 | private func sessionTitle() -> String { 31 | let dateFormatter = DateFormatter() 32 | dateFormatter.dateStyle = .long 33 | dateFormatter.timeStyle = .short 34 | 35 | if let created = session.createdAt { 36 | return dateFormatter.string(from: created) 37 | } else { 38 | return "Session" 39 | } 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /StrayScannerTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /StrayScannerTests/StrayScannerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StrayScannerTests.swift 3 | // StrayScannerTests 4 | // 5 | // Created by Kenneth Blomqvist on 2/27/21. 6 | // Copyright © 2021 Stray Robots. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class StrayScannerTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testNpyArrayCreate() throws { 22 | var frame: CVPixelBuffer? = nil 23 | let options = [ kCVPixelBufferCGImageCompatibilityKey as String: true, kCVPixelBufferCGBitmapContextCompatibilityKey as String: true ] 24 | let height: Int32 = 25 25 | let width: Int32 = 50 26 | CVPixelBufferCreate(kCFAllocatorDefault, Int(width), Int(height), kCVPixelFormatType_24RGB, options as CFDictionary, &frame) 27 | CVPixelBufferLockBaseAddress(frame!, CVPixelBufferLockFlags(rawValue: 1)) 28 | let baseAddress = CVPixelBufferGetBaseAddress(frame!)! 29 | let frameData: UnsafeMutablePointer = baseAddress.assumingMemoryBound(to: UInt16.self) 30 | 31 | for i in 0...height { 32 | for j in 0...width { 33 | let index: Int = Int(i * width + j) 34 | frameData[index] = UInt16(index) 35 | } 36 | } 37 | let npyArray: NPYArrayWrapper = NPYArrayWrapper(array: frameData, width: width, height: height)! 38 | let shape = npyArray.shape()! 39 | 40 | XCTAssert(shape[0] as! Int == 25) 41 | XCTAssert(shape[1] as! Int == 50) 42 | 43 | let data = npyArray.contents()! 44 | XCTAssert(data[0] == 0) 45 | XCTAssert(data[1] == 1) 46 | XCTAssert(data[Int(width * height - 1)] == width * height - 1) 47 | 48 | CVPixelBufferUnlockBaseAddress(frame!, CVPixelBufferLockFlags(rawValue: 0)) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /StrayScannerTests/Stray_ScannerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stray_ScannerTests.swift 3 | // Stray ScannerTests 4 | // 5 | // Created by Kenneth Blomqvist on 11/15/20. 6 | // Copyright © 2020 Stray Robots. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import StrayScanner 11 | 12 | class Stray_ScannerTests: XCTestCase { 13 | 14 | override func setUpWithError() throws { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() throws { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | XCTAssert(2 == 2) 26 | } 27 | 28 | func testPerformanceExample() throws { 29 | // This is an example of a performance test case. 30 | self.measure { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /docs/export.md: -------------------------------------------------------------------------------- 1 | # Exporting Data 2 | 3 | There are two ways of exporting the data from the device. The first way is to connect your phone to a computer with a lightning cable. The other option is through the iOS Files app. 4 | 5 | ## Exporting Using Cable 6 | 7 | To access data collected using Stray Scanner, connect your iPhone or iPad to your computer using a lightning cable. Open Finder.app. Select your device from the sidebar. Click on the "Files" tab beneath your device description. Under "Stray Scanner", you should see one directory per dataset you have collected. Drag these to wherever you want to place them. 8 | 9 | ![How to access Stray Scanner data](/images/euclid.jpg) 10 | In this image, you can see the two datasets "ac1ed2228f" and "c26b6838a9". These are the folders you should drag to your desired destination. 11 | 12 | On Windows, a similar process can be followed, but the device is accessed through iTunes. 13 | 14 | ## Exporting Through the Files App 15 | 16 | In the Files app, under "Browse > On My iPhone > Stray Scanner" you can see a folder for each recorded dataset. You can export a folder by moving it to your iCloud drive or share it with some other app. 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/format.md: -------------------------------------------------------------------------------- 1 | 2 | # Data Specification 3 | 4 | This document describes the data format recorded by the app. 5 | 6 | The collected datasets are each contained in a folder, named after a random hash, for example `71de12f9`. A dataset folder has the following directory structure: 7 | 8 | ``` 9 | camera_matrix.csv 10 | odometry.csv 11 | imu.csv 12 | depth/ 13 | - 000000.png 14 | - 000001.png 15 | - ... 16 | confidence/ 17 | - 000000.png 18 | - 000001.png 19 | - ... 20 | rgb.mp4 21 | ``` 22 | 23 | `rgb.mp4` is an HEVC encoded video, which contains the recorded data from the iPhone's camera. 24 | 25 | The `depth/` directory contains the depth maps. One `.png` file per rgb frame. Each of these is a 16 bit grayscale png image. They have a height of 192 elements and width of 256 elements. The values are the measured depth in millimeters, for that pixel position. In [OpenCV](https://docs.opencv.org/4.5.5/), these can be read with `cv2.imread(depth_frame_path, -1)`. 26 | 27 | The `confidence/` directory contains confidence maps corresponding to each depth map. They are grayscale png files encoding 192 x 256 element matrices. The values are either 0, 1 or 2. A higher value means a higher confidence. 28 | 29 | The `camera_matrix.csv` is a 3 x 3 matrix containing the [camera intrinsic parameters](https://en.wikipedia.org/wiki/Camera_resectioning#Intrinsic_parameters). 30 | 31 | The `odometry.csv` file contains the camera positions for each frame. The first line is a header. The meaning of the fields are: 32 | 33 | | Field |
Meaning
| 34 | |---|---| 35 | | timestamp | Timestamp in seconds | 36 | | frame | Frame number to which this pose corresponds to e.g. `000005` | 37 | | x | x coordinate in meters from when the session was started | 38 | | y | y coordinate in meters from when the session was started | 39 | | z | z coordinate in meters from when the session was started | 40 | | qx | x component of quaternion representing camera pose rotation | 41 | | qy | y component of quaternion representing camera pose rotation | 42 | | qz | z component of quaternion representing camera pose rotation | 43 | | qw | w component of quaternion representing camera pose rotation | 44 | 45 | The `imu.csv` file contains timestamps, linear acceleration readings and angular rotation readings. The first line is a header. The meaning of the fields are: 46 | 47 | | Field |
Meaning
| 48 | |---|---| 49 | | timestamp | Timestamp in seconds | 50 | | a\_x | Acceleration in m/s^2 in x direction | 51 | | a\_y | Acceleration in m/s^2 in y direction | 52 | | a\_z | Acceleration in m/s^2 in z direction | 53 | | alpha\_x | Rotation in rad/s around the x-axis | 54 | | alpha\_y | Rotation in rad/s around the y-axis | 55 | | alpha\_z | Rotation in rad/s around the z-axis | 56 | 57 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | ![Stray Toolkit](/images/stray-logo.png) 2 | # Stray Scanner 3 | 4 | Stray Scanner is an iOS app for collecting RGB-D datasets. It can be downloaded from the [App Store](https://apps.apple.com/us/app/stray-scanner/id1557051662). 5 | 6 | The recorded datasets contain: 7 | - color images 8 | - depth frames from the LiDAR sensor 9 | - depth confidence maps 10 | - camera position estimates for each frame 11 | - camera calibration matrix 12 | - IMU measurements 13 | 14 | The datasets can be exported using the instructions found [here](/docs/export.md). 15 | 16 | The data format is described in [this document](/docs/format.md). 17 | 18 | 19 | -------------------------------------------------------------------------------- /images/euclid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strayrobots/scanner/e0e252221048b80d288dc1f264df775aacbe4304/images/euclid.jpg -------------------------------------------------------------------------------- /images/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strayrobots/scanner/e0e252221048b80d288dc1f264df775aacbe4304/images/screenshot.jpg -------------------------------------------------------------------------------- /images/stray-logo.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:050f4eb071aa64ab33f1b3d908cb4ab921f58b6ee2ea12e9539353e26f05838d 3 | size 11208 4 | -------------------------------------------------------------------------------- /images/stray-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strayrobots/scanner/e0e252221048b80d288dc1f264df775aacbe4304/images/stray-logo.webp -------------------------------------------------------------------------------- /scripts/integrate.py: -------------------------------------------------------------------------------- 1 | import os 2 | import open3d as o3d 3 | import numpy as np 4 | from scipy.spatial.transform import Rotation 5 | from argparse import ArgumentParser 6 | from PIL import Image 7 | import cv2 8 | import skvideo.io 9 | 10 | DEPTH_WIDTH = 256 11 | DEPTH_HEIGHT = 192 12 | 13 | def read_args(): 14 | parser = ArgumentParser() 15 | parser.add_argument('path', type=str, help="Path to StrayScanner folder to process.") 16 | parser.add_argument('--frames', action='store_true', help="Visualize coordinate frames.") 17 | parser.add_argument('--point-clouds', action='store_true', help="Visualize point clouds one-by-one.") 18 | parser.add_argument('--integrate', action='store_true', help="Visualize point clouds one-by-one.") 19 | parser.add_argument('--every', type=int, default=50) 20 | return parser.parse_args() 21 | 22 | def _resize_camera_matrix(camera_matrix, scale_x, scale_y): 23 | fx = camera_matrix[0, 0] 24 | fy = camera_matrix[1, 1] 25 | cx = camera_matrix[0, 2] 26 | cy = camera_matrix[1, 2] 27 | return np.array([[fx * scale_x, 0.0, cx * scale_x], 28 | [0., fy * scale_y, cy * scale_y], 29 | [0., 0., 1.0]]) 30 | 31 | def read_data(flags): 32 | intrinsics = np.loadtxt(os.path.join(flags.path, 'camera_matrix.csv'), delimiter=',') 33 | odometry = np.loadtxt(os.path.join(flags.path, 'odometry.csv'), delimiter=',') 34 | poses = [] 35 | # T_AC = np.eye(4) 36 | # T_AC[:3, :3] = Rotation.from_rotvec([0.0, 0.0, np.deg2rad(180)]).as_matrix() @ Rotation.from_rotvec([0.0, np.deg2rad(180), 0.0]).as_matrix() 37 | 38 | for line in odometry: 39 | # x, y, z, qx, qy, qz, qw 40 | position = line[:3] 41 | quaternion = line[3:] 42 | T_WC = np.eye(4) 43 | T_WC[:3, :3] = Rotation.from_quat(quaternion).as_matrix() 44 | T_WC[:3, 3] = position 45 | poses.append(T_WC) 46 | return { 'poses': poses, 'intrinsics': intrinsics } 47 | 48 | def load_depth(path): 49 | depth_mm = np.load(path) 50 | depth_m = depth_mm.astype(np.float32) / 1000.0 51 | return o3d.geometry.Image(depth_m) 52 | 53 | def main(): 54 | flags = read_args() 55 | 56 | data = read_data(flags) 57 | geometries = [] 58 | if flags.frames: 59 | geometries += show_frames(flags, data) 60 | if flags.point_clouds: 61 | geometries += point_clouds(flags, data) 62 | if flags.integrate: 63 | geometries += integrate(flags, data) 64 | o3d.visualization.draw_geometries(geometries) 65 | 66 | def get_intrinsics(intrinsics): 67 | intrinsics_scaled = _resize_camera_matrix(intrinsics, DEPTH_WIDTH / 1920, DEPTH_HEIGHT / 1280) 68 | return o3d.camera.PinholeCameraIntrinsic(width=DEPTH_WIDTH, height=DEPTH_HEIGHT, fx=intrinsics_scaled[0, 0], 69 | fy=intrinsics_scaled[1, 1], cx=intrinsics_scaled[0, 2], cy=intrinsics_scaled[1, 2]) 70 | 71 | def show_frames(flags, data): 72 | frames = [o3d.geometry.TriangleMesh.create_coordinate_frame().scale(0.25, np.zeros(3))] 73 | for i, T_WC in enumerate(data['poses']): 74 | if not i % flags.every == 0: 75 | continue 76 | print(f"Frame {i}", end="\r") 77 | mesh = o3d.geometry.TriangleMesh.create_coordinate_frame().scale(0.1, np.zeros(3)) 78 | frames.append(mesh.transform(T_WC)) 79 | return frames 80 | 81 | def point_clouds(flags, data): 82 | pcs = [] 83 | intrinsics = get_intrinsics(data['intrinsics']) 84 | pc = o3d.geometry.PointCloud() 85 | meshes = [] 86 | for i, T_WC in enumerate(data['poses']): 87 | if i % flags.every != 0: 88 | continue 89 | print(f"Point cloud {i}", end="\r") 90 | T_CW = np.linalg.inv(T_WC) 91 | depth = load_depth(os.path.join(flags.path, 'depth', f'{i:06}.npy')) 92 | X = o3d.geometry.PointCloud.create_from_depth_image(depth, intrinsics, extrinsic=T_CW, depth_scale=1.0) 93 | pc += X.uniform_down_sample(every_k_points=10) 94 | return [pc] 95 | 96 | def integrate(flags, data): 97 | volume = o3d.pipelines.integration.ScalableTSDFVolume( 98 | voxel_length=4.0 / 1024.0, 99 | sdf_trunc=0.05, 100 | color_type=o3d.pipelines.integration.TSDFVolumeColorType.RGB8) 101 | 102 | intrinsics = get_intrinsics(data['intrinsics']) 103 | 104 | rgb_path = os.path.join(flags.path, 'rgb.mp4') 105 | video = skvideo.io.vreader(rgb_path) 106 | for i, (T_WC, rgb) in enumerate(zip(data['poses'], video)): 107 | print(f"Integrating frame {i:06}", end='\r') 108 | depth_path = os.path.join(flags.path, 'depth', f'{i:06}.npy') 109 | depth = load_depth(depth_path) 110 | rgb = Image.fromarray(rgb) 111 | rgb = rgb.resize((DEPTH_WIDTH, DEPTH_HEIGHT)) 112 | rgb = np.array(rgb) 113 | rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth( 114 | o3d.geometry.Image(rgb), depth, 115 | depth_scale=1.0, depth_trunc=4.0, convert_rgb_to_intensity=False) 116 | 117 | volume.integrate(rgbd, intrinsics, np.linalg.inv(T_WC)) 118 | mesh = volume.extract_triangle_mesh() 119 | mesh.compute_vertex_normals() 120 | return [mesh] 121 | 122 | if __name__ == "__main__": 123 | main() 124 | 125 | --------------------------------------------------------------------------------