From 0825e075280d003df683bf4c1a00d50207be0ed5 Mon Sep 17 00:00:00 2001 From: Tuan Mai A <62716934+st-tuanmai@users.noreply.github.com> Date: Wed, 27 Mar 2024 00:54:19 +0700 Subject: [PATCH] [WIP] ios Image segmenter sample (#292) * demo image segmenter * add render video * video segment * test render image * fix image show black * rotate image * change name, send image width to shader * fix UI * suport lanscape orientation * remove unnecessary code * test using deeplabv3 * update to 0.10.12 * add change model function * add chose delegate * Add download deeplab v3 model ro RunScripts file --- .../ImageSegmenter.xcodeproj/project.pbxproj | 859 ++++++++++++++++++ .../xcschemes/ImageSegmenter.xcscheme | 101 ++ .../ios/ImageSegmenter/AppDelegate.swift | 31 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 14 + .../AppIcon.appiconset/ic_launcher_1024.png | Bin 0 -> 105970 bytes .../Assets.xcassets/Contents.json | 6 + .../ic_add.imageset/Contents.json | 21 + .../ic_add.imageset/ic_add.png | Bin 0 -> 1532 bytes .../ic_camera.imageset/Contents.json | 21 + .../ic_camera.imageset/ic_camera.png | Bin 0 -> 2675 bytes .../ic_expand_down.imageset/Contents.json | 21 + .../ic_expand_down.png | Bin 0 -> 464 bytes .../ic_expand_up.imageset/Contents.json | 21 + .../ic_expand_up.imageset/ic_expand_up.png | Bin 0 -> 451 bytes .../ic_library.imageset/Contents.json | 21 + .../ic_library.imageset/ic_library.png | Bin 0 -> 1321 bytes .../ic_mediapipe.imageset/Contents.json | 21 + .../ic_launcher_adaptive_fore.png | Bin 0 -> 32505 bytes .../Base.lproj/LaunchScreen.storyboard | 25 + .../ImageSegmenter/Base.lproj/Main.storyboard | 434 +++++++++ .../Configurations/DefaultConstants.swift | 60 ++ .../InferenceConfigurationManager.swift | 41 + .../ios/ImageSegmenter/Info.plist | 25 + .../ios/ImageSegmenter/SceneDelegate.swift | 47 + .../Services/CameraFeedService.swift | 364 ++++++++ .../Services/ImageSegmenterService.swift | 191 ++++ .../Services/SegmentedImageRenderer.swift | 567 ++++++++++++ .../Units/MergeColorShaders.metal | 43 + .../Units/RenderToMtlViewShaders.metal | 33 + .../BottomSheetViewController.swift | 111 +++ .../ViewContoller/CameraViewController.swift | 258 ++++++ .../MediaLibraryViewController.swift | 369 ++++++++ .../ViewContoller/RootViewController.swift | 240 +++++ .../Views/PreviewMetalView.swift | 362 ++++++++ .../ImageSegmenterTests.swift | 36 + .../ImageSegmenterUITests.swift | 41 + .../ImageSegmenterUITestsLaunchTests.swift | 1 + examples/image_segmentation/ios/Podfile | 20 + examples/image_segmentation/ios/Podfile.lock | 20 + .../ios/RunScripts/download_models.sh | 33 + 41 files changed, 4469 insertions(+) create mode 100644 examples/image_segmentation/ios/ImageSegmenter.xcodeproj/project.pbxproj create mode 100644 examples/image_segmentation/ios/ImageSegmenter.xcodeproj/xcshareddata/xcschemes/ImageSegmenter.xcscheme create mode 100644 examples/image_segmentation/ios/ImageSegmenter/AppDelegate.swift create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/AppIcon.appiconset/ic_launcher_1024.png create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/Contents.json create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_add.imageset/Contents.json create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_add.imageset/ic_add.png create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_camera.imageset/Contents.json create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_camera.imageset/ic_camera.png create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_expand_down.imageset/Contents.json create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_expand_down.imageset/ic_expand_down.png create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_expand_up.imageset/Contents.json create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_expand_up.imageset/ic_expand_up.png create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_library.imageset/Contents.json create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_library.imageset/ic_library.png create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_mediapipe.imageset/Contents.json create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_mediapipe.imageset/ic_launcher_adaptive_fore.png create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Base.lproj/LaunchScreen.storyboard create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Base.lproj/Main.storyboard create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Configurations/DefaultConstants.swift create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Configurations/InferenceConfigurationManager.swift create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Info.plist create mode 100644 examples/image_segmentation/ios/ImageSegmenter/SceneDelegate.swift create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Services/CameraFeedService.swift create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Services/ImageSegmenterService.swift create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Services/SegmentedImageRenderer.swift create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Units/MergeColorShaders.metal create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Units/RenderToMtlViewShaders.metal create mode 100644 examples/image_segmentation/ios/ImageSegmenter/ViewContoller/BottomSheetViewController.swift create mode 100644 examples/image_segmentation/ios/ImageSegmenter/ViewContoller/CameraViewController.swift create mode 100644 examples/image_segmentation/ios/ImageSegmenter/ViewContoller/MediaLibraryViewController.swift create mode 100644 examples/image_segmentation/ios/ImageSegmenter/ViewContoller/RootViewController.swift create mode 100644 examples/image_segmentation/ios/ImageSegmenter/Views/PreviewMetalView.swift create mode 100644 examples/image_segmentation/ios/ImageSegmenterTests/ImageSegmenterTests.swift create mode 100644 examples/image_segmentation/ios/ImageSegmenterUITests/ImageSegmenterUITests.swift create mode 100644 examples/image_segmentation/ios/ImageSegmenterUITests/ImageSegmenterUITestsLaunchTests.swift create mode 100644 examples/image_segmentation/ios/Podfile create mode 100644 examples/image_segmentation/ios/Podfile.lock create mode 100755 examples/image_segmentation/ios/RunScripts/download_models.sh diff --git a/examples/image_segmentation/ios/ImageSegmenter.xcodeproj/project.pbxproj b/examples/image_segmentation/ios/ImageSegmenter.xcodeproj/project.pbxproj new file mode 100644 index 00000000..31d8be80 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter.xcodeproj/project.pbxproj @@ -0,0 +1,859 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 05F80D26699F307C3C6E05AB /* Pods_ImageSegmenterTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A89C5A44F11923E2CD6086BB /* Pods_ImageSegmenterTests.framework */; }; + 1FB4DB731718CFD48F6CCA1F /* Pods_ImageSegmenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB81BA1A32936BF6829D8CD6 /* Pods_ImageSegmenter.framework */; }; + BF2D8E652B830C7B0050C3B6 /* deeplab_v3.tflite in Resources */ = {isa = PBXBuildFile; fileRef = BF2D8E642B830C7A0050C3B6 /* deeplab_v3.tflite */; }; + BF6EBB4F2B1EB5BE00CD13FB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB4E2B1EB5BE00CD13FB /* AppDelegate.swift */; }; + BF6EBB512B1EB5BE00CD13FB /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB502B1EB5BE00CD13FB /* SceneDelegate.swift */; }; + BF6EBB562B1EB5BE00CD13FB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF6EBB542B1EB5BE00CD13FB /* Main.storyboard */; }; + BF6EBB582B1EB5BF00CD13FB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF6EBB572B1EB5BF00CD13FB /* Assets.xcassets */; }; + BF6EBB5B2B1EB5BF00CD13FB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF6EBB592B1EB5BF00CD13FB /* LaunchScreen.storyboard */; }; + BF6EBB662B1EB5BF00CD13FB /* ImageSegmenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB652B1EB5BF00CD13FB /* ImageSegmenterTests.swift */; }; + BF6EBB702B1EB5BF00CD13FB /* ImageSegmenterUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB6F2B1EB5BF00CD13FB /* ImageSegmenterUITests.swift */; }; + BF6EBB722B1EB5BF00CD13FB /* ImageSegmenterUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB712B1EB5BF00CD13FB /* ImageSegmenterUITestsLaunchTests.swift */; }; + BF6EBB912B1EB75B00CD13FB /* ImageSegmenterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB822B1EB75B00CD13FB /* ImageSegmenterService.swift */; }; + BF6EBB922B1EB75B00CD13FB /* CameraFeedService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB832B1EB75B00CD13FB /* CameraFeedService.swift */; }; + BF6EBB932B1EB75B00CD13FB /* MergeColorShaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB842B1EB75B00CD13FB /* MergeColorShaders.metal */; }; + BF6EBB952B1EB75B00CD13FB /* InferenceConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB882B1EB75B00CD13FB /* InferenceConfigurationManager.swift */; }; + BF6EBB962B1EB75B00CD13FB /* DefaultConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB892B1EB75B00CD13FB /* DefaultConstants.swift */; }; + BF6EBB972B1EB75B00CD13FB /* MediaLibraryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB8B2B1EB75B00CD13FB /* MediaLibraryViewController.swift */; }; + BF6EBB982B1EB75B00CD13FB /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB8C2B1EB75B00CD13FB /* RootViewController.swift */; }; + BF6EBB992B1EB75B00CD13FB /* BottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB8D2B1EB75B00CD13FB /* BottomSheetViewController.swift */; }; + BF6EBB9A2B1EB75B00CD13FB /* CameraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB8E2B1EB75B00CD13FB /* CameraViewController.swift */; }; + BF6EBB9C2B200D9400CD13FB /* PreviewMetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBB9B2B200D9400CD13FB /* PreviewMetalView.swift */; }; + BF6EBBA22B20531800CD13FB /* SegmentedImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBBA12B20531800CD13FB /* SegmentedImageRenderer.swift */; }; + BF6EBBA62B2070F000CD13FB /* RenderToMtlViewShaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = BF6EBBA52B2070F000CD13FB /* RenderToMtlViewShaders.metal */; }; + BF6EBBA82B20712600CD13FB /* selfie_segmenter.tflite in Resources */ = {isa = PBXBuildFile; fileRef = BF6EBBA72B20712600CD13FB /* selfie_segmenter.tflite */; }; + FBCBDD23FAD8125B5F819751 /* libPods-ImageSegmenterUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D95B4C9DB808B140005D1F61 /* libPods-ImageSegmenterUITests.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + BF6EBB622B1EB5BF00CD13FB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BF6EBB432B1EB5BE00CD13FB /* Project object */; + proxyType = 1; + remoteGlobalIDString = BF6EBB4A2B1EB5BE00CD13FB; + remoteInfo = ImageSegmenter; + }; + BF6EBB6C2B1EB5BF00CD13FB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BF6EBB432B1EB5BE00CD13FB /* Project object */; + proxyType = 1; + remoteGlobalIDString = BF6EBB4A2B1EB5BE00CD13FB; + remoteInfo = ImageSegmenter; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 00FE4AC67A3BC7929C07F88A /* Pods-ImageSegmenterUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageSegmenterUITests.release.xcconfig"; path = "Target Support Files/Pods-ImageSegmenterUITests/Pods-ImageSegmenterUITests.release.xcconfig"; sourceTree = ""; }; + 082C9F953835AD674CDB4C7F /* Pods-ImageSegmenterUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageSegmenterUITests.debug.xcconfig"; path = "Target Support Files/Pods-ImageSegmenterUITests/Pods-ImageSegmenterUITests.debug.xcconfig"; sourceTree = ""; }; + 1F44B4095124C60C8C521374 /* Pods-ImageSegmenter.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageSegmenter.release.xcconfig"; path = "Target Support Files/Pods-ImageSegmenter/Pods-ImageSegmenter.release.xcconfig"; sourceTree = ""; }; + 3DD5C13A41F6755F985364BD /* Pods-ImageSegmenter.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageSegmenter.debug.xcconfig"; path = "Target Support Files/Pods-ImageSegmenter/Pods-ImageSegmenter.debug.xcconfig"; sourceTree = ""; }; + 718FBD8F849AF3A5F7CD51A4 /* Pods-ImageSegmenter-ImageSegmenterUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageSegmenter-ImageSegmenterUITests.debug.xcconfig"; path = "Target Support Files/Pods-ImageSegmenter-ImageSegmenterUITests/Pods-ImageSegmenter-ImageSegmenterUITests.debug.xcconfig"; sourceTree = ""; }; + 7FEBF7670100412F1C68BD87 /* Pods-ImageSegmenterTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageSegmenterTests.debug.xcconfig"; path = "Target Support Files/Pods-ImageSegmenterTests/Pods-ImageSegmenterTests.debug.xcconfig"; sourceTree = ""; }; + 98B7F48961448745D60EAF71 /* Pods-ImageSegmenter-ImageSegmenterUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageSegmenter-ImageSegmenterUITests.release.xcconfig"; path = "Target Support Files/Pods-ImageSegmenter-ImageSegmenterUITests/Pods-ImageSegmenter-ImageSegmenterUITests.release.xcconfig"; sourceTree = ""; }; + A89C5A44F11923E2CD6086BB /* Pods_ImageSegmenterTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ImageSegmenterTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BF2D8E642B830C7A0050C3B6 /* deeplab_v3.tflite */ = {isa = PBXFileReference; lastKnownFileType = file; path = deeplab_v3.tflite; sourceTree = ""; }; + BF6EBB4B2B1EB5BE00CD13FB /* ImageSegmenter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ImageSegmenter.app; sourceTree = BUILT_PRODUCTS_DIR; }; + BF6EBB4E2B1EB5BE00CD13FB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + BF6EBB502B1EB5BE00CD13FB /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + BF6EBB552B1EB5BE00CD13FB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + BF6EBB572B1EB5BF00CD13FB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + BF6EBB5A2B1EB5BF00CD13FB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + BF6EBB5C2B1EB5BF00CD13FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BF6EBB612B1EB5BF00CD13FB /* ImageSegmenterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ImageSegmenterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BF6EBB652B1EB5BF00CD13FB /* ImageSegmenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSegmenterTests.swift; sourceTree = ""; }; + BF6EBB6B2B1EB5BF00CD13FB /* ImageSegmenterUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ImageSegmenterUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BF6EBB6F2B1EB5BF00CD13FB /* ImageSegmenterUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSegmenterUITests.swift; sourceTree = ""; }; + BF6EBB712B1EB5BF00CD13FB /* ImageSegmenterUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSegmenterUITestsLaunchTests.swift; sourceTree = ""; }; + BF6EBB822B1EB75B00CD13FB /* ImageSegmenterService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageSegmenterService.swift; sourceTree = ""; }; + BF6EBB832B1EB75B00CD13FB /* CameraFeedService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraFeedService.swift; sourceTree = ""; }; + BF6EBB842B1EB75B00CD13FB /* MergeColorShaders.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = MergeColorShaders.metal; sourceTree = ""; }; + BF6EBB882B1EB75B00CD13FB /* InferenceConfigurationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InferenceConfigurationManager.swift; sourceTree = ""; }; + BF6EBB892B1EB75B00CD13FB /* DefaultConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultConstants.swift; sourceTree = ""; }; + BF6EBB8B2B1EB75B00CD13FB /* MediaLibraryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaLibraryViewController.swift; sourceTree = ""; }; + BF6EBB8C2B1EB75B00CD13FB /* RootViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = ""; }; + BF6EBB8D2B1EB75B00CD13FB /* BottomSheetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomSheetViewController.swift; sourceTree = ""; }; + BF6EBB8E2B1EB75B00CD13FB /* CameraViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraViewController.swift; sourceTree = ""; }; + BF6EBB9B2B200D9400CD13FB /* PreviewMetalView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewMetalView.swift; sourceTree = ""; }; + BF6EBBA12B20531800CD13FB /* SegmentedImageRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedImageRenderer.swift; sourceTree = ""; }; + BF6EBBA52B2070F000CD13FB /* RenderToMtlViewShaders.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = RenderToMtlViewShaders.metal; sourceTree = ""; }; + BF6EBBA72B20712600CD13FB /* selfie_segmenter.tflite */ = {isa = PBXFileReference; lastKnownFileType = file; path = selfie_segmenter.tflite; sourceTree = ""; }; + CB81BA1A32936BF6829D8CD6 /* Pods_ImageSegmenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ImageSegmenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D95B4C9DB808B140005D1F61 /* libPods-ImageSegmenterUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ImageSegmenterUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + E17D861DD2F87160038B0515 /* Pods-ImageSegmenterTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageSegmenterTests.release.xcconfig"; path = "Target Support Files/Pods-ImageSegmenterTests/Pods-ImageSegmenterTests.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + BF6EBB482B1EB5BE00CD13FB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FB4DB731718CFD48F6CCA1F /* Pods_ImageSegmenter.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BF6EBB5E2B1EB5BF00CD13FB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 05F80D26699F307C3C6E05AB /* Pods_ImageSegmenterTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BF6EBB682B1EB5BF00CD13FB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FBCBDD23FAD8125B5F819751 /* libPods-ImageSegmenterUITests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 429F4DEBB7DA7662C280952A /* Pods */ = { + isa = PBXGroup; + children = ( + 3DD5C13A41F6755F985364BD /* Pods-ImageSegmenter.debug.xcconfig */, + 1F44B4095124C60C8C521374 /* Pods-ImageSegmenter.release.xcconfig */, + 718FBD8F849AF3A5F7CD51A4 /* Pods-ImageSegmenter-ImageSegmenterUITests.debug.xcconfig */, + 98B7F48961448745D60EAF71 /* Pods-ImageSegmenter-ImageSegmenterUITests.release.xcconfig */, + 7FEBF7670100412F1C68BD87 /* Pods-ImageSegmenterTests.debug.xcconfig */, + E17D861DD2F87160038B0515 /* Pods-ImageSegmenterTests.release.xcconfig */, + 082C9F953835AD674CDB4C7F /* Pods-ImageSegmenterUITests.debug.xcconfig */, + 00FE4AC67A3BC7929C07F88A /* Pods-ImageSegmenterUITests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + BF6EBB422B1EB5BE00CD13FB = { + isa = PBXGroup; + children = ( + BF6EBB4D2B1EB5BE00CD13FB /* ImageSegmenter */, + BF6EBB642B1EB5BF00CD13FB /* ImageSegmenterTests */, + BF6EBB6E2B1EB5BF00CD13FB /* ImageSegmenterUITests */, + BF6EBB4C2B1EB5BE00CD13FB /* Products */, + 429F4DEBB7DA7662C280952A /* Pods */, + CA201CA7C3CA7FC43CD98251 /* Frameworks */, + ); + sourceTree = ""; + }; + BF6EBB4C2B1EB5BE00CD13FB /* Products */ = { + isa = PBXGroup; + children = ( + BF6EBB4B2B1EB5BE00CD13FB /* ImageSegmenter.app */, + BF6EBB612B1EB5BF00CD13FB /* ImageSegmenterTests.xctest */, + BF6EBB6B2B1EB5BF00CD13FB /* ImageSegmenterUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + BF6EBB4D2B1EB5BE00CD13FB /* ImageSegmenter */ = { + isa = PBXGroup; + children = ( + BF6EBB872B1EB75B00CD13FB /* Configurations */, + BF6EBB812B1EB75B00CD13FB /* Services */, + BF6EBB852B1EB75B00CD13FB /* Units */, + BF6EBB8A2B1EB75B00CD13FB /* ViewContoller */, + BF6EBB7E2B1EB75B00CD13FB /* Views */, + BF6EBB4E2B1EB5BE00CD13FB /* AppDelegate.swift */, + BF6EBB502B1EB5BE00CD13FB /* SceneDelegate.swift */, + BF6EBB542B1EB5BE00CD13FB /* Main.storyboard */, + BF6EBBA72B20712600CD13FB /* selfie_segmenter.tflite */, + BF2D8E642B830C7A0050C3B6 /* deeplab_v3.tflite */, + BF6EBB572B1EB5BF00CD13FB /* Assets.xcassets */, + BF6EBB592B1EB5BF00CD13FB /* LaunchScreen.storyboard */, + BF6EBB5C2B1EB5BF00CD13FB /* Info.plist */, + ); + path = ImageSegmenter; + sourceTree = ""; + }; + BF6EBB642B1EB5BF00CD13FB /* ImageSegmenterTests */ = { + isa = PBXGroup; + children = ( + BF6EBB652B1EB5BF00CD13FB /* ImageSegmenterTests.swift */, + ); + path = ImageSegmenterTests; + sourceTree = ""; + }; + BF6EBB6E2B1EB5BF00CD13FB /* ImageSegmenterUITests */ = { + isa = PBXGroup; + children = ( + BF6EBB6F2B1EB5BF00CD13FB /* ImageSegmenterUITests.swift */, + BF6EBB712B1EB5BF00CD13FB /* ImageSegmenterUITestsLaunchTests.swift */, + ); + path = ImageSegmenterUITests; + sourceTree = ""; + }; + BF6EBB7E2B1EB75B00CD13FB /* Views */ = { + isa = PBXGroup; + children = ( + BF6EBB9B2B200D9400CD13FB /* PreviewMetalView.swift */, + ); + path = Views; + sourceTree = ""; + }; + BF6EBB812B1EB75B00CD13FB /* Services */ = { + isa = PBXGroup; + children = ( + BF6EBB822B1EB75B00CD13FB /* ImageSegmenterService.swift */, + BF6EBB832B1EB75B00CD13FB /* CameraFeedService.swift */, + BF6EBBA12B20531800CD13FB /* SegmentedImageRenderer.swift */, + ); + path = Services; + sourceTree = ""; + }; + BF6EBB852B1EB75B00CD13FB /* Units */ = { + isa = PBXGroup; + children = ( + BF6EBB842B1EB75B00CD13FB /* MergeColorShaders.metal */, + BF6EBBA52B2070F000CD13FB /* RenderToMtlViewShaders.metal */, + ); + path = Units; + sourceTree = ""; + }; + BF6EBB872B1EB75B00CD13FB /* Configurations */ = { + isa = PBXGroup; + children = ( + BF6EBB882B1EB75B00CD13FB /* InferenceConfigurationManager.swift */, + BF6EBB892B1EB75B00CD13FB /* DefaultConstants.swift */, + ); + path = Configurations; + sourceTree = ""; + }; + BF6EBB8A2B1EB75B00CD13FB /* ViewContoller */ = { + isa = PBXGroup; + children = ( + BF6EBB8B2B1EB75B00CD13FB /* MediaLibraryViewController.swift */, + BF6EBB8C2B1EB75B00CD13FB /* RootViewController.swift */, + BF6EBB8D2B1EB75B00CD13FB /* BottomSheetViewController.swift */, + BF6EBB8E2B1EB75B00CD13FB /* CameraViewController.swift */, + ); + path = ViewContoller; + sourceTree = ""; + }; + CA201CA7C3CA7FC43CD98251 /* Frameworks */ = { + isa = PBXGroup; + children = ( + CB81BA1A32936BF6829D8CD6 /* Pods_ImageSegmenter.framework */, + A89C5A44F11923E2CD6086BB /* Pods_ImageSegmenterTests.framework */, + D95B4C9DB808B140005D1F61 /* libPods-ImageSegmenterUITests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + BF6EBB4A2B1EB5BE00CD13FB /* ImageSegmenter */ = { + isa = PBXNativeTarget; + buildConfigurationList = BF6EBB752B1EB5BF00CD13FB /* Build configuration list for PBXNativeTarget "ImageSegmenter" */; + buildPhases = ( + 16CD3DB75953570593C92185 /* [CP] Check Pods Manifest.lock */, + BF6EBBAB2B20749E00CD13FB /* Download model */, + BF6EBB472B1EB5BE00CD13FB /* Sources */, + BF6EBB482B1EB5BE00CD13FB /* Frameworks */, + BF6EBB492B1EB5BE00CD13FB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ImageSegmenter; + productName = ImageSegmenter; + productReference = BF6EBB4B2B1EB5BE00CD13FB /* ImageSegmenter.app */; + productType = "com.apple.product-type.application"; + }; + BF6EBB602B1EB5BF00CD13FB /* ImageSegmenterTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = BF6EBB782B1EB5BF00CD13FB /* Build configuration list for PBXNativeTarget "ImageSegmenterTests" */; + buildPhases = ( + 78AF909D807F31032449F052 /* [CP] Check Pods Manifest.lock */, + BF6EBB5D2B1EB5BF00CD13FB /* Sources */, + BF6EBB5E2B1EB5BF00CD13FB /* Frameworks */, + BF6EBB5F2B1EB5BF00CD13FB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BF6EBB632B1EB5BF00CD13FB /* PBXTargetDependency */, + ); + name = ImageSegmenterTests; + productName = ImageSegmenterTests; + productReference = BF6EBB612B1EB5BF00CD13FB /* ImageSegmenterTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + BF6EBB6A2B1EB5BF00CD13FB /* ImageSegmenterUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = BF6EBB7B2B1EB5BF00CD13FB /* Build configuration list for PBXNativeTarget "ImageSegmenterUITests" */; + buildPhases = ( + 16FC60A11FF43663A314A8A9 /* [CP] Check Pods Manifest.lock */, + BF6EBB672B1EB5BF00CD13FB /* Sources */, + BF6EBB682B1EB5BF00CD13FB /* Frameworks */, + BF6EBB692B1EB5BF00CD13FB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BF6EBB6D2B1EB5BF00CD13FB /* PBXTargetDependency */, + ); + name = ImageSegmenterUITests; + productName = ImageSegmenterUITests; + productReference = BF6EBB6B2B1EB5BF00CD13FB /* ImageSegmenterUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BF6EBB432B1EB5BE00CD13FB /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + BF6EBB4A2B1EB5BE00CD13FB = { + CreatedOnToolsVersion = 15.0; + }; + BF6EBB602B1EB5BF00CD13FB = { + CreatedOnToolsVersion = 15.0; + TestTargetID = BF6EBB4A2B1EB5BE00CD13FB; + }; + BF6EBB6A2B1EB5BF00CD13FB = { + CreatedOnToolsVersion = 15.0; + TestTargetID = BF6EBB4A2B1EB5BE00CD13FB; + }; + }; + }; + buildConfigurationList = BF6EBB462B1EB5BE00CD13FB /* Build configuration list for PBXProject "ImageSegmenter" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = BF6EBB422B1EB5BE00CD13FB; + productRefGroup = BF6EBB4C2B1EB5BE00CD13FB /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + BF6EBB4A2B1EB5BE00CD13FB /* ImageSegmenter */, + BF6EBB602B1EB5BF00CD13FB /* ImageSegmenterTests */, + BF6EBB6A2B1EB5BF00CD13FB /* ImageSegmenterUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + BF6EBB492B1EB5BE00CD13FB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BF6EBB5B2B1EB5BF00CD13FB /* LaunchScreen.storyboard in Resources */, + BF6EBB582B1EB5BF00CD13FB /* Assets.xcassets in Resources */, + BF2D8E652B830C7B0050C3B6 /* deeplab_v3.tflite in Resources */, + BF6EBBA82B20712600CD13FB /* selfie_segmenter.tflite in Resources */, + BF6EBB562B1EB5BE00CD13FB /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BF6EBB5F2B1EB5BF00CD13FB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BF6EBB692B1EB5BF00CD13FB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 16CD3DB75953570593C92185 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ImageSegmenter-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 16FC60A11FF43663A314A8A9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ImageSegmenterUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 78AF909D807F31032449F052 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ImageSegmenterTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + BF6EBBAB2B20749E00CD13FB /* Download model */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Download model"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/RunScripts/download_models.sh\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + BF6EBB472B1EB5BE00CD13FB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BF6EBB982B1EB75B00CD13FB /* RootViewController.swift in Sources */, + BF6EBBA62B2070F000CD13FB /* RenderToMtlViewShaders.metal in Sources */, + BF6EBBA22B20531800CD13FB /* SegmentedImageRenderer.swift in Sources */, + BF6EBB912B1EB75B00CD13FB /* ImageSegmenterService.swift in Sources */, + BF6EBB4F2B1EB5BE00CD13FB /* AppDelegate.swift in Sources */, + BF6EBB922B1EB75B00CD13FB /* CameraFeedService.swift in Sources */, + BF6EBB952B1EB75B00CD13FB /* InferenceConfigurationManager.swift in Sources */, + BF6EBB972B1EB75B00CD13FB /* MediaLibraryViewController.swift in Sources */, + BF6EBB962B1EB75B00CD13FB /* DefaultConstants.swift in Sources */, + BF6EBB9C2B200D9400CD13FB /* PreviewMetalView.swift in Sources */, + BF6EBB512B1EB5BE00CD13FB /* SceneDelegate.swift in Sources */, + BF6EBB9A2B1EB75B00CD13FB /* CameraViewController.swift in Sources */, + BF6EBB932B1EB75B00CD13FB /* MergeColorShaders.metal in Sources */, + BF6EBB992B1EB75B00CD13FB /* BottomSheetViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BF6EBB5D2B1EB5BF00CD13FB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BF6EBB662B1EB5BF00CD13FB /* ImageSegmenterTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BF6EBB672B1EB5BF00CD13FB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BF6EBB702B1EB5BF00CD13FB /* ImageSegmenterUITests.swift in Sources */, + BF6EBB722B1EB5BF00CD13FB /* ImageSegmenterUITestsLaunchTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + BF6EBB632B1EB5BF00CD13FB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BF6EBB4A2B1EB5BE00CD13FB /* ImageSegmenter */; + targetProxy = BF6EBB622B1EB5BF00CD13FB /* PBXContainerItemProxy */; + }; + BF6EBB6D2B1EB5BF00CD13FB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BF6EBB4A2B1EB5BE00CD13FB /* ImageSegmenter */; + targetProxy = BF6EBB6C2B1EB5BF00CD13FB /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + BF6EBB542B1EB5BE00CD13FB /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BF6EBB552B1EB5BE00CD13FB /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + BF6EBB592B1EB5BF00CD13FB /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BF6EBB5A2B1EB5BF00CD13FB /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + BF6EBB732B1EB5BF00CD13FB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + BF6EBB742B1EB5BF00CD13FB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + BF6EBB762B1EB5BF00CD13FB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3DD5C13A41F6755F985364BD /* Pods-ImageSegmenter.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_APP_SANDBOX = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ImageSegmenter/Info.plist; + INFOPLIST_KEY_NSCameraUsageDescription = "AVCamFilter uses the camera to take photos and video."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "AVCamFilter saves captured photos and videos to your photo library."; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.google.mediapipe.examples.imagesegmenter.ImageSegmenter; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + BF6EBB772B1EB5BF00CD13FB /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1F44B4095124C60C8C521374 /* Pods-ImageSegmenter.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_APP_SANDBOX = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ImageSegmenter/Info.plist; + INFOPLIST_KEY_NSCameraUsageDescription = "AVCamFilter uses the camera to take photos and video."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "AVCamFilter saves captured photos and videos to your photo library."; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.google.mediapipe.examples.imagesegmenter.ImageSegmenter; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + BF6EBB792B1EB5BF00CD13FB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7FEBF7670100412F1C68BD87 /* Pods-ImageSegmenterTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = H83UK2M7VU; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.google.mediapipe.examples.imagesegmenter.ImageSegmenterTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ImageSegmenter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ImageSegmenter"; + }; + name = Debug; + }; + BF6EBB7A2B1EB5BF00CD13FB /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E17D861DD2F87160038B0515 /* Pods-ImageSegmenterTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = H83UK2M7VU; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.google.mediapipe.examples.imagesegmenter.ImageSegmenterTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ImageSegmenter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ImageSegmenter"; + }; + name = Release; + }; + BF6EBB7C2B1EB5BF00CD13FB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 082C9F953835AD674CDB4C7F /* Pods-ImageSegmenterUITests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = H83UK2M7VU; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.google.mediapipe.examples.imagesegmenter.ImageSegmenterUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = ImageSegmenter; + }; + name = Debug; + }; + BF6EBB7D2B1EB5BF00CD13FB /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 00FE4AC67A3BC7929C07F88A /* Pods-ImageSegmenterUITests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = H83UK2M7VU; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.google.mediapipe.examples.imagesegmenter.ImageSegmenterUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = ImageSegmenter; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + BF6EBB462B1EB5BE00CD13FB /* Build configuration list for PBXProject "ImageSegmenter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BF6EBB732B1EB5BF00CD13FB /* Debug */, + BF6EBB742B1EB5BF00CD13FB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BF6EBB752B1EB5BF00CD13FB /* Build configuration list for PBXNativeTarget "ImageSegmenter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BF6EBB762B1EB5BF00CD13FB /* Debug */, + BF6EBB772B1EB5BF00CD13FB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BF6EBB782B1EB5BF00CD13FB /* Build configuration list for PBXNativeTarget "ImageSegmenterTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BF6EBB792B1EB5BF00CD13FB /* Debug */, + BF6EBB7A2B1EB5BF00CD13FB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BF6EBB7B2B1EB5BF00CD13FB /* Build configuration list for PBXNativeTarget "ImageSegmenterUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BF6EBB7C2B1EB5BF00CD13FB /* Debug */, + BF6EBB7D2B1EB5BF00CD13FB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BF6EBB432B1EB5BE00CD13FB /* Project object */; +} diff --git a/examples/image_segmentation/ios/ImageSegmenter.xcodeproj/xcshareddata/xcschemes/ImageSegmenter.xcscheme b/examples/image_segmentation/ios/ImageSegmenter.xcodeproj/xcshareddata/xcschemes/ImageSegmenter.xcscheme new file mode 100644 index 00000000..7ef61810 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter.xcodeproj/xcshareddata/xcschemes/ImageSegmenter.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/image_segmentation/ios/ImageSegmenter/AppDelegate.swift b/examples/image_segmentation/ios/ImageSegmenter/AppDelegate.swift new file mode 100644 index 00000000..66dd007d --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/AppDelegate.swift @@ -0,0 +1,31 @@ + + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..e730c9ca --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "ic_launcher_1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/AppIcon.appiconset/ic_launcher_1024.png b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/AppIcon.appiconset/ic_launcher_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..bcda9bb5c437c5e748b11c9111d1cfc38564e96c GIT binary patch literal 105970 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 E027qCr~m)} literal 0 HcmV?d00001 diff --git a/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/Contents.json b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_add.imageset/Contents.json b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_add.imageset/Contents.json new file mode 100644 index 00000000..283b7ca6 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_add.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_add.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_add.imageset/ic_add.png b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_add.imageset/ic_add.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca202482a423139f02566c1597602a31c862ad3 GIT binary patch literal 1532 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 literal 0 HcmV?d00001 diff --git a/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_expand_down.imageset/Contents.json b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_expand_down.imageset/Contents.json new file mode 100644 index 00000000..291ebf91 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_expand_down.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_expand_down.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_expand_down.imageset/ic_expand_down.png b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_expand_down.imageset/ic_expand_down.png new file mode 100644 index 0000000000000000000000000000000000000000..0443030e44b674d38d854e1f48926109dfa17f73 GIT binary patch literal 464 zcmeAS@N?(olHy`uVBq!ia0vp^DImbhwB~a25a@up>_g0nvt{TFny>mb_(A4BNvcYYzRGAIx-WnM7$(K#jpGr3F6{ zt{ipvcyL8`)1&4a zfq_?yk7Z~`F(!+M^tN2I?s7ZwLey0``Agd(4vD=BG=$o$yOff4EYdJ)lkPh3!BJ4c zPJ`(%yKBOa1spu`B8?B5l?-YESlal}C41hU`dsqw{BQLs{nEF+w{Lr%{yg)rIp2gj zTg}Gx7j`>p0!g{h6-i}CO>8~>Vg0YMC)`u`r|ABi5nHGkK#0HcV( M)78&qol`;+0Efu0(*OVf literal 0 HcmV?d00001 diff --git a/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_expand_up.imageset/Contents.json b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_expand_up.imageset/Contents.json new file mode 100644 index 00000000..6eda6253 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_expand_up.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_expand_up.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_expand_up.imageset/ic_expand_up.png b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_expand_up.imageset/ic_expand_up.png new file mode 100644 index 0000000000000000000000000000000000000000..d02518603c28118c4be9f5d2ce493131da664f6a GIT binary patch literal 451 zcmeAS@N?(olHy`uVBq!ia0vp^DImZ!8JAzA*(~Hb#gnG2k>&pP_8#_|tpT3| zy~H$GfyO}q$M(DP6t#Q(DSNEnCiMuY5K|NR9Ba?crx3f z?z!p`8QgQ!Ju$~q|`;Xr*sx6Lu_2tIhrSHG{pDJGY`09DF zT6x!=oq=gbD!_D~z~YsUcbeZb{;&DlmIW4K4VQL?HyqqMFF(4*2_)v}>gTe~DWM4f D;;yoK literal 0 HcmV?d00001 diff --git a/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_library.imageset/Contents.json b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_library.imageset/Contents.json new file mode 100644 index 00000000..5abf40ed --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_library.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_library.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_library.imageset/ic_library.png b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_library.imageset/ic_library.png new file mode 100644 index 0000000000000000000000000000000000000000..7845f28d94be60a43ef8543570e4811b47943f7b GIT binary patch literal 1321 zcmeAS@N?(olHy`uVBq!ia0vp^DImh~%Nx3oBdzTD*h#lB@r$B#y(MGrQ2DE)Yltflee zg}1y{e9ao~%%pF3JOx!EmRLRa(Qpa~2=MG$5OGnT|Z`=F&@990gz~p{MPOVeZSWjaR1kn*DZQUYpsi&cw0MRM7F?Y{87J zrHV?;T*}QH8}~S+7bs>OGF-x|});)fg9rpW9n^5;(jrU96e&Y}P%r6&f zEf8S-ksbT@V7$ln>dlNcljZf;Zl7nHCLh4Ni19gB-2diIfuar7FZpr{?+GjY`=aWw zcf!5JQc6)r4(l~m=ePa%+?=fc1#jU|@V=8wKdKmRNB!uI@&gBkxW|Lu}g)_%h9pjPUGw7*Bx z$veH9|JJg-SfhTie$Aux`Qbeho{AHF<%$$f8XM{D5_o32Vt3JOwm&CkXQcoBePsrx zHBEq(QCJ$nO#)`xej-lseFTTj<(sTY-7 z-IcG`1J&CYZAFQxNd=+%85^`2p$#U!Vq)%gqFpW64@ zjK|6^{#=incevGtn)h<4A(IaIb-b?dXZo)+D}JB1Y0tKjBK_BKw;mN+8kwzo+jcvB zOH0VgebNUate#Gsv}4hxO+4Q>KQDI;ir~ID&F;?HU+d=HeDylcGBInfu)6+^?UVMN zf7+tI&1qH0{jHOiZI|-ymeVZG(~1i__RLOn$p(={y8B|c{#rL%a{IefL(Qn~r?`Xn zO$aHAuXAdM3-y^1<+1$pwY{rO+*okN@J)-xy!o4>zV=M{y2^NkQIN}t4X@)^EtAc= zin=&7J6~Q@ZdRR|)1hl>78yh)JIx_#eu#KmkLz6FZozwqo zwqd%8PBZYJgMJ-3Do#9)r5An`REl5X^V5mzpT}lPMKS*lkx!>COpwWs?J$s0YLSGX zgRLG#xB55|*{l^eP2@1_$#1#l-XYR|CBrp7wj<=rUp4Kjg{m$b#t_uR&3TU@jpcp+ z1KtF;5C1q~7f3#v&9PBf&qb^1(bw~ROBXF)s8xk1Sp-Cw|FJ6ET(^8-@5-ORvX8;j L)z4*}Q$iB}OJZl) literal 0 HcmV?d00001 diff --git a/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_mediapipe.imageset/Contents.json b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_mediapipe.imageset/Contents.json new file mode 100644 index 00000000..01f67d24 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_mediapipe.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_launcher_adaptive_fore.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_mediapipe.imageset/ic_launcher_adaptive_fore.png b/examples/image_segmentation/ios/ImageSegmenter/Assets.xcassets/ic_mediapipe.imageset/ic_launcher_adaptive_fore.png new file mode 100644 index 0000000000000000000000000000000000000000..5d5d7d140dabc4a2cae481c6e5f68df510ea783d GIT binary patch literal 32505 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 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/image_segmentation/ios/ImageSegmenter/Base.lproj/Main.storyboard b/examples/image_segmentation/ios/ImageSegmenter/Base.lproj/Main.storyboard new file mode 100644 index 00000000..db10bcc1 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Base.lproj/Main.storyboard @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/image_segmentation/ios/ImageSegmenter/Configurations/DefaultConstants.swift b/examples/image_segmentation/ios/ImageSegmenter/Configurations/DefaultConstants.swift new file mode 100644 index 00000000..7b90197a --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Configurations/DefaultConstants.swift @@ -0,0 +1,60 @@ +// Copyright 2023 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import UIKit +import MediaPipeTasksVision + +// MARK: Define default constants +struct DefaultConstants { + static let model: Model = .deeplabV3 + static let delegate: Delegate = .CPU +} + +// MARK: Model +enum Model: Int, CaseIterable { + case selfieSegmenter + case deeplabV3 + + var name: String { + switch self { + case .selfieSegmenter: + return "Selfie segmenter" + case .deeplabV3: + return "Deeplab V3" + } + } + + var modelPath: String? { + switch self { + case .selfieSegmenter: + return Bundle.main.path( + forResource: "selfie_segmenter", ofType: "tflite") + case .deeplabV3: + return Bundle.main.path( + forResource: "deeplab_v3", ofType: "tflite") + } + } + + init?(name: String) { + switch name { + case "Selfie segmenter": + self.init(rawValue: 0) + case "Deeplab V3": + self.init(rawValue: 1) + default: + return nil + } + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Configurations/InferenceConfigurationManager.swift b/examples/image_segmentation/ios/ImageSegmenter/Configurations/InferenceConfigurationManager.swift new file mode 100644 index 00000000..8cebab1a --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Configurations/InferenceConfigurationManager.swift @@ -0,0 +1,41 @@ +// Copyright 2023 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import MediaPipeTasksVision + +/** + * Singleton storing the configs needed to initialize an MediaPipe Tasks object and run inference. + * Controllers can observe the `InferenceConfigurationManager.notificationName` for any changes made by the user. + */ + +class InferenceConfigurationManager: NSObject { + + var model: Model = DefaultConstants.model { + didSet { postConfigChangedNotification() } + } + + var delegate: Delegate = DefaultConstants.delegate { + didSet { postConfigChangedNotification() } + } + + static let sharedInstance = InferenceConfigurationManager() + + static let notificationName = Notification.Name.init(rawValue: "com.google.mediapipe.inferenceConfigChanged") + + private func postConfigChangedNotification() { + NotificationCenter.default + .post(name: InferenceConfigurationManager.notificationName, object: nil) + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Info.plist b/examples/image_segmentation/ios/ImageSegmenter/Info.plist new file mode 100644 index 00000000..dd3c9afd --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/examples/image_segmentation/ios/ImageSegmenter/SceneDelegate.swift b/examples/image_segmentation/ios/ImageSegmenter/SceneDelegate.swift new file mode 100644 index 00000000..6a0d0f1d --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/SceneDelegate.swift @@ -0,0 +1,47 @@ + + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/examples/image_segmentation/ios/ImageSegmenter/Services/CameraFeedService.swift b/examples/image_segmentation/ios/ImageSegmenter/Services/CameraFeedService.swift new file mode 100644 index 00000000..9b409e7c --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Services/CameraFeedService.swift @@ -0,0 +1,364 @@ +// Copyright 2023 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +import AVFoundation + +// MARK: CameraFeedServiceDelegate Declaration +protocol CameraFeedServiceDelegate: AnyObject { + + /** + This method delivers the pixel buffer of the current frame seen by the device's camera. + */ + func didOutput(sampleBuffer: CMSampleBuffer, orientation: UIImage.Orientation) + + /** + This method initimates that a session runtime error occured. + */ + func didEncounterSessionRuntimeError() + + /** + This method initimates that the session was interrupted. + */ + func sessionWasInterrupted(canResumeManually resumeManually: Bool) + + /** + This method initimates that the session interruption has ended. + */ + func sessionInterruptionEnded() + +} + +/** + This class manages all camera related functionality + */ +class CameraFeedService: NSObject { + /** + This enum holds the state of the camera initialization. + */ + enum CameraConfigurationStatus { + case success + case failed + case permissionDenied + } + + // MARK: Public Instance Variables + var videoResolution: CGSize { + get { + guard let size = imageBufferSize else { + return CGSize.zero + } + let minDimension = min(size.width, size.height) + let maxDimension = max(size.width, size.height) + switch UIDevice.current.orientation { + case .portrait: + return CGSize(width: minDimension, height: maxDimension) + case .landscapeLeft: + fallthrough + case .landscapeRight: + return CGSize(width: maxDimension, height: minDimension) + default: + return CGSize(width: minDimension, height: maxDimension) + } + } + } + + let videoGravity = AVLayerVideoGravity.resizeAspectFill + + // MARK: Instance Variables + private let session: AVCaptureSession = AVCaptureSession() + private let sessionQueue = DispatchQueue(label: "com.google.mediapipe.CameraFeedService.sessionQueue") + private let cameraPosition: AVCaptureDevice.Position = .front + + private var cameraConfigurationStatus: CameraConfigurationStatus = .failed + private lazy var videoDataOutput = AVCaptureVideoDataOutput() + private var isSessionRunning = false + private var imageBufferSize: CGSize? + + + // MARK: CameraFeedServiceDelegate + weak var delegate: CameraFeedServiceDelegate? + + // MARK: Initializer + override init() { + super.init() + + // Initializes the session + session.sessionPreset = .high + + attemptToConfigureSession() + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: Session Start and End methods + + /** + This method starts an AVCaptureSession based on whether the camera configuration was successful. + */ + + func startLiveCameraSession(_ completion: @escaping(_ cameraConfiguration: CameraConfigurationStatus) -> Void) { + sessionQueue.async { + switch self.cameraConfigurationStatus { + case .success: + self.addObservers() + self.startSession() + default: + break + } + completion(self.cameraConfigurationStatus) + } + } + + /** + This method stops a running an AVCaptureSession. + */ + func stopSession() { + self.removeObservers() + sessionQueue.async { + if self.session.isRunning { + self.session.stopRunning() + self.isSessionRunning = self.session.isRunning + } + } + + } + + /** + This method resumes an interrupted AVCaptureSession. + */ + func resumeInterruptedSession(withCompletion completion: @escaping (Bool) -> ()) { + sessionQueue.async { + self.startSession() + + DispatchQueue.main.async { + completion(self.isSessionRunning) + } + } + } + + /** + This method starts the AVCaptureSession + **/ + private func startSession() { + self.session.startRunning() + self.isSessionRunning = self.session.isRunning + } + + // MARK: Session Configuration Methods. + /** + This method requests for camera permissions and handles the configuration of the session and stores the result of configuration. + */ + private func attemptToConfigureSession() { + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: + self.cameraConfigurationStatus = .success + case .notDetermined: + self.sessionQueue.suspend() + self.requestCameraAccess(completion: { (granted) in + self.sessionQueue.resume() + }) + case .denied: + self.cameraConfigurationStatus = .permissionDenied + default: + break + } + + self.sessionQueue.async { + self.configureSession() + } + } + + /** + This method requests for camera permissions. + */ + private func requestCameraAccess(completion: @escaping (Bool) -> ()) { + AVCaptureDevice.requestAccess(for: .video) { (granted) in + if !granted { + self.cameraConfigurationStatus = .permissionDenied + } + else { + self.cameraConfigurationStatus = .success + } + completion(granted) + } + } + + + /** + This method handles all the steps to configure an AVCaptureSession. + */ + private func configureSession() { + + guard cameraConfigurationStatus == .success else { + return + } + session.beginConfiguration() + + // Tries to add an AVCaptureDeviceInput. + guard addVideoDeviceInput() == true else { + self.session.commitConfiguration() + self.cameraConfigurationStatus = .failed + return + } + + // Tries to add an AVCaptureVideoDataOutput. + guard addVideoDataOutput() else { + self.session.commitConfiguration() + self.cameraConfigurationStatus = .failed + return + } + + session.commitConfiguration() + self.cameraConfigurationStatus = .success + } + + /** + This method tries to an AVCaptureDeviceInput to the current AVCaptureSession. + */ + private func addVideoDeviceInput() -> Bool { + + /**Tries to get the default back camera. + */ + guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: cameraPosition) else { + return false + } + + do { + let videoDeviceInput = try AVCaptureDeviceInput(device: camera) + if session.canAddInput(videoDeviceInput) { + session.addInput(videoDeviceInput) + return true + } + else { + return false + } + } + catch { + fatalError("Cannot create video device input") + } + } + + /** + This method tries to an AVCaptureVideoDataOutput to the current AVCaptureSession. + */ + private func addVideoDataOutput() -> Bool { + + let sampleBufferQueue = DispatchQueue(label: "sampleBufferQueue") + videoDataOutput.setSampleBufferDelegate(self, queue: sampleBufferQueue) + videoDataOutput.alwaysDiscardsLateVideoFrames = true + videoDataOutput.videoSettings = [ String(kCVPixelBufferPixelFormatTypeKey) : kCMPixelFormat_32BGRA] + + if session.canAddOutput(videoDataOutput) { + session.addOutput(videoDataOutput) + videoDataOutput.connection(with: .video)?.videoOrientation = .portrait + if videoDataOutput.connection(with: .video)?.isVideoOrientationSupported == true + && cameraPosition == .front { + videoDataOutput.connection(with: .video)?.isVideoMirrored = true + } + return true + } + return false + } + + // MARK: Notification Observer Handling + private func addObservers() { + NotificationCenter.default.addObserver(self, selector: #selector(CameraFeedService.sessionRuntimeErrorOccured(notification:)), name: NSNotification.Name.AVCaptureSessionRuntimeError, object: session) + NotificationCenter.default.addObserver(self, selector: #selector(CameraFeedService.sessionWasInterrupted(notification:)), name: NSNotification.Name.AVCaptureSessionWasInterrupted, object: session) + NotificationCenter.default.addObserver(self, selector: #selector(CameraFeedService.sessionInterruptionEnded), name: NSNotification.Name.AVCaptureSessionInterruptionEnded, object: session) + } + + private func removeObservers() { + NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVCaptureSessionRuntimeError, object: session) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVCaptureSessionWasInterrupted, object: session) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVCaptureSessionInterruptionEnded, object: session) + } + + // MARK: Notification Observers + @objc func sessionWasInterrupted(notification: Notification) { + + if let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as AnyObject?, + let reasonIntegerValue = userInfoValue.integerValue, + let reason = AVCaptureSession.InterruptionReason(rawValue: reasonIntegerValue) { + print("Capture session was interrupted with reason \(reason)") + + var canResumeManually = false + if reason == .videoDeviceInUseByAnotherClient { + canResumeManually = true + } else if reason == .videoDeviceNotAvailableWithMultipleForegroundApps { + canResumeManually = false + } + + self.delegate?.sessionWasInterrupted(canResumeManually: canResumeManually) + + } + } + + @objc func sessionInterruptionEnded(notification: Notification) { + self.delegate?.sessionInterruptionEnded() + } + + @objc func sessionRuntimeErrorOccured(notification: Notification) { + guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else { + return + } + + print("Capture session runtime error: \(error)") + + guard error.code == .mediaServicesWereReset else { + self.delegate?.didEncounterSessionRuntimeError() + return + } + + sessionQueue.async { + if self.isSessionRunning { + self.startSession() + } else { + DispatchQueue.main.async { + self.delegate?.didEncounterSessionRuntimeError() + } + } + } + } +} + +/** + AVCaptureVideoDataOutputSampleBufferDelegate + */ +extension CameraFeedService: AVCaptureVideoDataOutputSampleBufferDelegate { + + /** This method delegates the CVPixelBuffer of the frame seen by the camera currently. + */ + func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + delegate?.didOutput(sampleBuffer: sampleBuffer, orientation: .upMirrored) + } +} + +// MARK: UIImage.Orientation Extension +extension UIImage.Orientation { + static func from(deviceOrientation: UIDeviceOrientation) -> UIImage.Orientation { + switch deviceOrientation { + case .portrait: + return .up + case .landscapeLeft: + return .left + case .landscapeRight: + return .right + default: + return .up + } + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Services/ImageSegmenterService.swift b/examples/image_segmentation/ios/ImageSegmenter/Services/ImageSegmenterService.swift new file mode 100644 index 00000000..62b1e054 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Services/ImageSegmenterService.swift @@ -0,0 +1,191 @@ +// Copyright 2023 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +import MediaPipeTasksVision +import AVFoundation + +/** + This protocol must be adopted by any class that wants to get the segmention results of the image segmenter in live stream mode. + */ +protocol ImageSegmenterServiceLiveStreamDelegate: AnyObject { + func imageSegmenterService(_ imageSegmenterService: ImageSegmenterService, + didFinishSegmention result: ResultBundle?, + error: Error?) +} + +/** + This protocol must be adopted by any class that wants to take appropriate actions during different stages of image segmenter on videos. + */ +//protocol ImageSegmenterServiceVideoDelegate: AnyObject { +// func imageSegmenterService(_ imageSegmenterService: ImageSegmenterService, +// didFinishSegmentionOnVideoFrame index: Int) +// func imageSegmenterService(_ imageSegmenterService: ImageSegmenterService, +// willBeginSegmention totalframeCount: Int) +//} + + +// Initializes and calls the MediaPipe APIs for segmention. +class ImageSegmenterService: NSObject { + + weak var liveStreamDelegate: ImageSegmenterServiceLiveStreamDelegate? + // weak var videoDelegate: ImageSegmenterServiceVideoDelegate? + + var imageSegmenter: ImageSegmenter? + private(set) var runningMode = RunningMode.image + var modelPath: String + var delegate: Delegate + + // MARK: - Custom Initializer + private init?(modelPath: String?, + runningMode:RunningMode, + delegate: Delegate) { + guard let modelPath = modelPath else { return nil } + self.modelPath = modelPath + self.runningMode = runningMode + self.delegate = delegate + super.init() + + createImageSegmenter() + } + + private func createImageSegmenter() { + let imageSegmenterOptions = ImageSegmenterOptions() + imageSegmenterOptions.runningMode = runningMode + imageSegmenterOptions.shouldOutputCategoryMask = true + imageSegmenterOptions.baseOptions.modelAssetPath = modelPath + imageSegmenterOptions.baseOptions.delegate = self.delegate + if runningMode == .liveStream { + imageSegmenterOptions.imageSegmenterLiveStreamDelegate = self + } + do { + imageSegmenter = try ImageSegmenter(options: imageSegmenterOptions) + } + catch { + print(error) + } + } + + // MARK: - Static Initializers + static func videoImageSegmenterService( + modelPath: String?, + delegate: Delegate) -> ImageSegmenterService? { + let imageSegmenterService = ImageSegmenterService( + modelPath: modelPath, + runningMode: .video, + delegate: delegate) + return imageSegmenterService + } + + static func liveStreamImageSegmenterService( + modelPath: String?, + liveStreamDelegate: ImageSegmenterServiceLiveStreamDelegate?, + delegate: Delegate) -> ImageSegmenterService? { + let imageSegmenterService = ImageSegmenterService( + modelPath: modelPath, + runningMode: .liveStream, + delegate: delegate) + imageSegmenterService?.liveStreamDelegate = liveStreamDelegate + + return imageSegmenterService + } + + static func stillImageSegmenterService( + modelPath: String?, + delegate: Delegate) -> ImageSegmenterService? { + let imageSegmenterService = ImageSegmenterService( + modelPath: modelPath, + runningMode: .image, + delegate: delegate) + + return imageSegmenterService + } + + // MARK: - Segmention Methods for Different Modes + /** + This method return ImageSegmenterResult and infrenceTime when receive an image + **/ + func segment(image: UIImage) -> ResultBundle? { + guard let cgImage = image.fixedOrientation() else { return nil } + let fixImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .up) + guard let mpImage = try? MPImage(uiImage: fixImage) else { + return nil + } + do { + let startDate = Date() + let result = try imageSegmenter?.segment(image: mpImage) + let inferenceTime = Date().timeIntervalSince(startDate) * 1000 + return ResultBundle(inferenceTime: inferenceTime, imageSegmenterResults: [result]) + } catch { + print(error) + return nil + } + } + + func segmentAsync( + sampleBuffer: CMSampleBuffer, + orientation: UIImage.Orientation, + timeStamps: Int) { + guard let image = try? MPImage(sampleBuffer: sampleBuffer, orientation: orientation) else { + return + } + do { + try imageSegmenter?.segmentAsync(image: image, timestampInMilliseconds: timeStamps) + } catch { + print(error) + } + } + + func segment( + videoFrame: CGImage, + orientation: UIImage.Orientation, + timeStamps: Int) + -> ResultBundle? { + do { + let mpImage = try MPImage(uiImage: UIImage(cgImage: videoFrame)) + let startDate = Date() + let result = try imageSegmenter?.segment(videoFrame: mpImage, timestampInMilliseconds: timeStamps) + let inferenceTime = Date().timeIntervalSince(startDate) * 1000 + return ResultBundle(inferenceTime: inferenceTime, imageSegmenterResults: [result]) + } catch { + print(error) + return nil + } + } +} + +// MARK: - ImageSegmenterLiveStreamDelegate Methods +extension ImageSegmenterService: ImageSegmenterLiveStreamDelegate { + func imageSegmenter(_ imageSegmenter: ImageSegmenter, didFinishSegmentation result: ImageSegmenterResult?, timestampInMilliseconds: Int, error: Error?) { + let resultBundle = ResultBundle( + inferenceTime: Date().timeIntervalSince1970 * 1000 - Double(timestampInMilliseconds), + imageSegmenterResults: [result]) + liveStreamDelegate?.imageSegmenterService( + self, + didFinishSegmention: resultBundle, + error: error) + } +} + +/// A result from the `ImageSegmenterService`. +struct ResultBundle { + let inferenceTime: Double + let imageSegmenterResults: [ImageSegmenterResult?] + var size: CGSize = .zero +} + +struct VideoFrame { + let pixelBuffer: CVPixelBuffer + let formatDescription: CMFormatDescription +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Services/SegmentedImageRenderer.swift b/examples/image_segmentation/ios/ImageSegmenter/Services/SegmentedImageRenderer.swift new file mode 100644 index 00000000..99419ec9 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Services/SegmentedImageRenderer.swift @@ -0,0 +1,567 @@ + +import CoreMedia +import CoreVideo +import Metal +import UIKit +import MetalPerformanceShaders +import MetalKit + +class SegmentedImageRenderer { + + var description: String = "Metal" + + var isPrepared = false + + private(set) var outputFormatDescription: CMFormatDescription? + + private var outputPixelBufferPool: CVPixelBufferPool? + + private let metalDevice = MTLCreateSystemDefaultDevice()! + + private var computePipelineState: MTLComputePipelineState? + + var inputTexture2: MTLTexture? + + private var textureCache: CVMetalTextureCache! + let context: CIContext + + let textureLoader: MTKTextureLoader + + private lazy var commandQueue: MTLCommandQueue? = { + return self.metalDevice.makeCommandQueue() + }() + + required init() { + let defaultLibrary = metalDevice.makeDefaultLibrary()! + let kernelFunction = defaultLibrary.makeFunction(name: "mergeColor") + do { + computePipelineState = try metalDevice.makeComputePipelineState(function: kernelFunction!) + } catch { + print("Could not create pipeline state: \(error)") + } + context = CIContext(mtlDevice: metalDevice) + textureLoader = MTKTextureLoader(device: metalDevice) + } + + func prepare(with imageSize: CGSize, + outputRetainedBufferCountHint: Int) { + reset() + + (outputPixelBufferPool, _, outputFormatDescription) = allocateOutputBufferPool(with: imageSize, + outputRetainedBufferCountHint: outputRetainedBufferCountHint) + if outputPixelBufferPool == nil { + return + } + + var metalTextureCache: CVMetalTextureCache? + if CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, metalDevice, nil, &metalTextureCache) != kCVReturnSuccess { + assertionFailure("Unable to allocate texture cache") + } else { + textureCache = metalTextureCache + } + + isPrepared = true + } + + func prepare(with formatDescription: CMFormatDescription, + outputRetainedBufferCountHint: Int, + needChangeWidthHeight: Bool = false) { + reset() + + (outputPixelBufferPool, _, outputFormatDescription) = allocateOutputBufferPool(with: formatDescription, + outputRetainedBufferCountHint: outputRetainedBufferCountHint, needChangeWidthHeight: needChangeWidthHeight) + if outputPixelBufferPool == nil { + return + } + + var metalTextureCache: CVMetalTextureCache? + if CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, metalDevice, nil, &metalTextureCache) != kCVReturnSuccess { + assertionFailure("Unable to allocate texture cache") + } else { + textureCache = metalTextureCache + } + + isPrepared = true + } + + func reset() { + outputPixelBufferPool = nil + outputFormatDescription = nil + textureCache = nil + isPrepared = false + } + + func render(image: UIImage, categoryMasks: UnsafePointer?) -> UIImage? { + guard let categoryMasks = categoryMasks else { + print("confidenceMasks not found") + return nil + } + + var newPixelBuffer: CVPixelBuffer? + CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, outputPixelBufferPool!, &newPixelBuffer) + guard let outputPixelBuffer = newPixelBuffer else { + print("Allocation failure: Could not get pixel buffer from pool. (\(self.description))") + return nil + } + guard let outputTexture = makeTextureFromCVPixelBuffer(pixelBuffer: outputPixelBuffer, textureFormat: .bgra8Unorm) else { + return nil + } + + var inputTexture: MTLTexture! + do { + guard let cgImage = image.fixedOrientation() else { return nil } + inputTexture = try textureLoader.newTexture(cgImage: cgImage) + } catch { + print(error) + return nil + } + + let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: inputTexture.width, height: inputTexture.height, mipmapped: false) + textureDescriptor.usage = .unknown + + // Set up command queue, buffer, and encoder. + guard let commandQueue = commandQueue, + let commandBuffer = commandQueue.makeCommandBuffer(), + let commandEncoder = commandBuffer.makeComputeCommandEncoder() else { + print("Failed to create a Metal command queue.") + CVMetalTextureCacheFlush(textureCache!, 0) + return nil + } + + commandEncoder.label = "Render Image" + commandEncoder.setComputePipelineState(computePipelineState!) + commandEncoder.setTexture(inputTexture, index: 0) + commandEncoder.setTexture(outputTexture, index: 1) + let buffer = metalDevice.makeBuffer(bytes: categoryMasks, length: inputTexture.width * inputTexture.height * MemoryLayout.size)! + commandEncoder.setBuffer(buffer, offset: 0, index: 0) + var imageWidth: Int = Int(inputTexture.width) + commandEncoder.setBytes(&imageWidth, length: MemoryLayout.size, index: 1) + + // Set up the thread groups. + let width = computePipelineState!.threadExecutionWidth + let height = computePipelineState!.maxTotalThreadsPerThreadgroup / width + let threadsPerThreadgroup = MTLSizeMake(width, height, 1) + let threadgroupsPerGrid = MTLSize(width: (inputTexture.width + width - 1) / width, + height: (inputTexture.height + height - 1) / height, + depth: 1) + print(threadgroupsPerGrid) + commandEncoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup) + + commandEncoder.endEncoding() + commandBuffer.commit() + commandBuffer.waitUntilCompleted() + guard let ciImage = CIImage(mtlTexture: outputTexture) else { return nil } + let newCiimage = ciImage.transformed(by: ciImage.orientationTransform(for: .downMirrored)) + return UIImage(ciImage: newCiimage) + + } + + /** + This method merge frame of video with backgroud image using segment data and return an pixel buffer + **/ + func render(ciImage: CIImage, segmentDatas: UnsafePointer?) -> CVPixelBuffer? { + + guard let segmentDatas = segmentDatas, isPrepared else { + print("segmentDatas not found") + return nil + } + + var newPixelBuffer: CVPixelBuffer? + CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, outputPixelBufferPool!, &newPixelBuffer) + guard let outputPixelBuffer = newPixelBuffer else { + print("Allocation failure: Could not get pixel buffer from pool. (\(self.description))") + return nil + } + guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else { fatalError("error") } + print(cgImage.width, cgImage.height) + + guard let outputTexture = makeTextureFromCVPixelBuffer(pixelBuffer: outputPixelBuffer, textureFormat: .bgra8Unorm) else { + return nil + } + var inputTexture: MTLTexture! + do { + inputTexture = try textureLoader.newTexture(cgImage: cgImage) + } catch { + print(error) + return nil + } + let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: inputTexture.width, height: inputTexture.height, mipmapped: false) + textureDescriptor.usage = .unknown + let inputScaleTexture = metalDevice.makeTexture(descriptor: textureDescriptor) + + resizeTexture(sourceTexture: inputTexture2!, desTexture: inputScaleTexture!, targetSize: MTLSize(width: inputTexture.width, height: inputTexture.height, depth: 3), resizeMode: .scaleToFill) + + // Set up command queue, buffer, and encoder. + guard let commandQueue = commandQueue, + let commandBuffer = commandQueue.makeCommandBuffer(), + let commandEncoder = commandBuffer.makeComputeCommandEncoder() else { + print("Failed to create a Metal command queue.") + CVMetalTextureCacheFlush(textureCache!, 0) + return nil + } + + commandEncoder.label = "Demo Metal" + commandEncoder.setComputePipelineState(computePipelineState!) + commandEncoder.setTexture(inputTexture, index: 0) + commandEncoder.setTexture(inputScaleTexture, index: 1) + commandEncoder.setTexture(outputTexture, index: 2) + let buffer = metalDevice.makeBuffer(bytes: segmentDatas, length: inputTexture.width * inputTexture.height * MemoryLayout.size)! + commandEncoder.setBuffer(buffer, offset: 0, index: 0) + var imageWidth: Int = Int(inputTexture.width) + commandEncoder.setBytes(&imageWidth, length: MemoryLayout.size, index: 1) + + // Set up the thread groups. + let width = computePipelineState!.threadExecutionWidth + let height = computePipelineState!.maxTotalThreadsPerThreadgroup / width + let threadsPerThreadgroup = MTLSizeMake(width, height, 1) + let threadgroupsPerGrid = MTLSize(width: (inputTexture.width + width - 1) / width, + height: (inputTexture.height + height - 1) / height, + depth: 1) + commandEncoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup) + + commandEncoder.endEncoding() + commandBuffer.commit() + commandBuffer.waitUntilCompleted() + return outputPixelBuffer + } + + func render(pixelBuffer: CVPixelBuffer, segmentDatas: UnsafePointer?) -> CVPixelBuffer? { + guard let segmentDatas = segmentDatas, isPrepared else { + print("segmentDatas not found") + return nil + } + + var newPixelBuffer: CVPixelBuffer? + CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, outputPixelBufferPool!, &newPixelBuffer) + guard let outputPixelBuffer = newPixelBuffer else { + print("Allocation failure: Could not get pixel buffer from pool. (\(self.description))") + return nil + } + guard let inputTexture = makeTextureFromCVPixelBuffer(pixelBuffer: pixelBuffer, textureFormat: .bgra8Unorm), + let outputTexture = makeTextureFromCVPixelBuffer(pixelBuffer: outputPixelBuffer, textureFormat: .bgra8Unorm) else { + return nil + } + + let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: inputTexture.width, height: inputTexture.height, mipmapped: false) + textureDescriptor.usage = .unknown + + // Set up command queue, buffer, and encoder. + guard let commandQueue = commandQueue, + let commandBuffer = commandQueue.makeCommandBuffer(), + let commandEncoder = commandBuffer.makeComputeCommandEncoder() else { + print("Failed to create a Metal command queue.") + CVMetalTextureCacheFlush(textureCache!, 0) + return nil + } + + commandEncoder.label = "Demo Metal" + commandEncoder.setComputePipelineState(computePipelineState!) + commandEncoder.setTexture(inputTexture, index: 0) + commandEncoder.setTexture(outputTexture, index: 1) + let buffer = metalDevice.makeBuffer(bytes: segmentDatas, length: inputTexture.width * inputTexture.height * MemoryLayout.size)! + commandEncoder.setBuffer(buffer, offset: 0, index: 0) + var imageWidth: Int = Int(inputTexture.width) + commandEncoder.setBytes(&imageWidth, length: MemoryLayout.size, index: 1) + + // Set up the thread groups. + let width = computePipelineState!.threadExecutionWidth + let height = computePipelineState!.maxTotalThreadsPerThreadgroup / width + let threadsPerThreadgroup = MTLSizeMake(width, height, 1) + let threadgroupsPerGrid = MTLSize(width: (inputTexture.width + width - 1) / width, + height: (inputTexture.height + height - 1) / height, + depth: 1) + commandEncoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup) + + commandEncoder.endEncoding() + commandBuffer.commit() + return outputPixelBuffer + } + + func getCGImmage(ciImage: CIImage) -> CGImage { + guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else { fatalError("error") } + return cgImage + } + + func resizeTexture(sourceTexture: MTLTexture, desTexture: MTLTexture, targetSize:MTLSize, resizeMode: UIView.ContentMode) { + guard let queue = self.commandQueue, + let commandBuffer = queue.makeCommandBuffer() else { + print("FrameMixer resizeTexture command buffer create failed") + return + } + + let device = queue.device; + + // Scale texture + let sourceWidth = sourceTexture.width + let sourceHeight = sourceTexture.height + let widthRatio: Double = Double(targetSize.width) / Double(sourceWidth) + let heightRatio: Double = Double(targetSize.height) / Double(sourceHeight) + var scaleX: Double = 0; + var scaleY: Double = 0; + var translateX: Double = 0; + var translateY: Double = 0; + if resizeMode == .scaleToFill { + //ScaleFill + scaleX = Double(targetSize.width) / Double(sourceWidth) + scaleY = Double(targetSize.height) / Double(sourceHeight) + + } else if resizeMode == .scaleAspectFit { + //AspectFit + if heightRatio > widthRatio { + scaleX = Double(targetSize.width) / Double(sourceWidth) + scaleY = scaleX + let currentHeight = Double(sourceHeight) * scaleY + translateY = (Double(targetSize.height) - currentHeight) * 0.5 + } else { + scaleY = Double(targetSize.height) / Double(sourceHeight) + scaleX = scaleY + let currentWidth = Double(sourceWidth) * scaleX + translateX = (Double(targetSize.width) - currentWidth) * 0.5 + } + } else if resizeMode == .scaleAspectFill { + //AspectFill + if heightRatio > widthRatio { + scaleY = Double(targetSize.height) / Double(sourceHeight) + scaleX = scaleY + let currentWidth = Double(sourceWidth) * scaleX + translateX = (Double(targetSize.width) - currentWidth) * 0.5 + + } else { + scaleX = Double(targetSize.width) / Double(sourceWidth) + scaleY = scaleX + let currentHeight = Double(sourceHeight) * scaleY + translateY = (Double(targetSize.height) - currentHeight) * 0.5 + } + } + var transform = MPSScaleTransform(scaleX: scaleX, scaleY: scaleY, translateX: translateX, translateY: translateY) + if #available(iOS 11.0, *) { + let scale = MPSImageBilinearScale.init(device: device) + withUnsafePointer(to: &transform) { (transformPtr: UnsafePointer) -> () in + scale.scaleTransform = transformPtr + scale.encode(commandBuffer: commandBuffer, sourceTexture: sourceTexture, destinationTexture: desTexture) + } + } else { + print("Frame mixer resizeTexture failed, only support iOS 11.0") + } + + commandBuffer.commit() + commandBuffer.waitUntilCompleted() + } + + func makeTextureFromCVPixelBuffer(pixelBuffer: CVPixelBuffer, textureFormat: MTLPixelFormat) -> MTLTexture? { + let width = CVPixelBufferGetWidth(pixelBuffer) + let height = CVPixelBufferGetHeight(pixelBuffer) + + // Create a Metal texture from the image buffer. + var cvTextureOut: CVMetalTexture? + CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, nil, textureFormat, width, height, 0, &cvTextureOut) + + guard let cvTexture = cvTextureOut, let texture = CVMetalTextureGetTexture(cvTexture) else { + CVMetalTextureCacheFlush(textureCache, 0) + return nil + } + return texture + } +} + +func allocateOutputBufferPool(with imageSize: CGSize, outputRetainedBufferCountHint: Int) ->( + outputBufferPool: CVPixelBufferPool?, + outputColorSpace: CGColorSpace?, + outputFormatDescription: CMFormatDescription?) { + + let width = imageSize.width + let height = imageSize.height + let pixelBufferAttributes: [String: Any] = [ + kCVPixelBufferPixelFormatTypeKey as String: UInt(kCVPixelFormatType_32BGRA), + kCVPixelBufferWidthKey as String: Int(width), + kCVPixelBufferHeightKey as String: Int(height), + kCVPixelBufferIOSurfacePropertiesKey as String: [:] + ] + + // Get pixel buffer attributes and color space from the input format description. + let cgColorSpace = CGColorSpaceCreateDeviceRGB() + + // Create a pixel buffer pool with the same pixel attributes as the input format description. + let poolAttributes = [kCVPixelBufferPoolMinimumBufferCountKey as String: outputRetainedBufferCountHint] + var cvPixelBufferPool: CVPixelBufferPool? + CVPixelBufferPoolCreate(kCFAllocatorDefault, poolAttributes as NSDictionary?, pixelBufferAttributes as NSDictionary?, &cvPixelBufferPool) + guard let pixelBufferPool = cvPixelBufferPool else { + assertionFailure("Allocation failure: Could not allocate pixel buffer pool.") + return (nil, nil, nil) + } + + preallocateBuffers(pool: pixelBufferPool, allocationThreshold: outputRetainedBufferCountHint) + + // Get the output format description. + var pixelBuffer: CVPixelBuffer? + var outputFormatDescription: CMFormatDescription? + let auxAttributes = [kCVPixelBufferPoolAllocationThresholdKey as String: outputRetainedBufferCountHint] as NSDictionary + CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, pixelBufferPool, auxAttributes, &pixelBuffer) + if let pixelBuffer = pixelBuffer { + CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, + imageBuffer: pixelBuffer, + formatDescriptionOut: &outputFormatDescription) + } + pixelBuffer = nil + + return (pixelBufferPool, cgColorSpace, outputFormatDescription) + } + +func allocateOutputBufferPool(with inputFormatDescription: CMFormatDescription, outputRetainedBufferCountHint: Int, needChangeWidthHeight: Bool) ->( + outputBufferPool: CVPixelBufferPool?, + outputColorSpace: CGColorSpace?, + outputFormatDescription: CMFormatDescription?) { + + let inputMediaSubType = CMFormatDescriptionGetMediaSubType(inputFormatDescription) + if inputMediaSubType != kCVPixelFormatType_32BGRA { + assertionFailure("Invalid input pixel buffer type \(inputMediaSubType)") + return (nil, nil, nil) + } + + let inputDimensions = CMVideoFormatDescriptionGetDimensions(inputFormatDescription) + var width = inputDimensions.width + var height = inputDimensions.height + if needChangeWidthHeight { + width = height + height = inputDimensions.width + } + var pixelBufferAttributes: [String: Any] = [ + kCVPixelBufferPixelFormatTypeKey as String: UInt(inputMediaSubType), + kCVPixelBufferWidthKey as String: Int(width), + kCVPixelBufferHeightKey as String: Int(height), + kCVPixelBufferIOSurfacePropertiesKey as String: [:] + ] + + // Get pixel buffer attributes and color space from the input format description. + var cgColorSpace = CGColorSpaceCreateDeviceRGB() + if let inputFormatDescriptionExtension = CMFormatDescriptionGetExtensions(inputFormatDescription) as Dictionary? { + let colorPrimaries = inputFormatDescriptionExtension[kCVImageBufferColorPrimariesKey] + + if let colorPrimaries = colorPrimaries { + var colorSpaceProperties: [String: AnyObject] = [kCVImageBufferColorPrimariesKey as String: colorPrimaries] + + if let yCbCrMatrix = inputFormatDescriptionExtension[kCVImageBufferYCbCrMatrixKey] { + colorSpaceProperties[kCVImageBufferYCbCrMatrixKey as String] = yCbCrMatrix + } + + if let transferFunction = inputFormatDescriptionExtension[kCVImageBufferTransferFunctionKey] { + colorSpaceProperties[kCVImageBufferTransferFunctionKey as String] = transferFunction + } + + pixelBufferAttributes[kCVBufferPropagatedAttachmentsKey as String] = colorSpaceProperties + } + + if let cvColorspace = inputFormatDescriptionExtension[kCVImageBufferCGColorSpaceKey] { + cgColorSpace = cvColorspace as! CGColorSpace + } else if (colorPrimaries as? String) == (kCVImageBufferColorPrimaries_P3_D65 as String) { + cgColorSpace = CGColorSpace(name: CGColorSpace.displayP3)! + } + } + + // Create a pixel buffer pool with the same pixel attributes as the input format description. + let poolAttributes = [kCVPixelBufferPoolMinimumBufferCountKey as String: outputRetainedBufferCountHint] + var cvPixelBufferPool: CVPixelBufferPool? + CVPixelBufferPoolCreate(kCFAllocatorDefault, poolAttributes as NSDictionary?, pixelBufferAttributes as NSDictionary?, &cvPixelBufferPool) + guard let pixelBufferPool = cvPixelBufferPool else { + assertionFailure("Allocation failure: Could not allocate pixel buffer pool.") + return (nil, nil, nil) + } + + preallocateBuffers(pool: pixelBufferPool, allocationThreshold: outputRetainedBufferCountHint) + + // Get the output format description. + var pixelBuffer: CVPixelBuffer? + var outputFormatDescription: CMFormatDescription? + let auxAttributes = [kCVPixelBufferPoolAllocationThresholdKey as String: outputRetainedBufferCountHint] as NSDictionary + CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, pixelBufferPool, auxAttributes, &pixelBuffer) + if let pixelBuffer = pixelBuffer { + CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, + imageBuffer: pixelBuffer, + formatDescriptionOut: &outputFormatDescription) + } + pixelBuffer = nil + + return (pixelBufferPool, cgColorSpace, outputFormatDescription) + } + +/// - Tag: AllocateRenderBuffers +private func preallocateBuffers(pool: CVPixelBufferPool, allocationThreshold: Int) { + var pixelBuffers = [CVPixelBuffer]() + var error: CVReturn = kCVReturnSuccess + let auxAttributes = [kCVPixelBufferPoolAllocationThresholdKey as String: allocationThreshold] as NSDictionary + var pixelBuffer: CVPixelBuffer? + while error == kCVReturnSuccess { + error = CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, pool, auxAttributes, &pixelBuffer) + if let pixelBuffer = pixelBuffer { + pixelBuffers.append(pixelBuffer) + } + pixelBuffer = nil + } + pixelBuffers.removeAll() +} + +extension UIImage { + + + func fixedOrientation() -> CGImage? { + + guard let cgImage = self.cgImage else { + //CGImage is not available + return nil + } + + guard imageOrientation != UIImage.Orientation.up else { + //This is default orientation, don't need to do anything + return cgImage.copy() + } + + guard let colorSpace = cgImage.colorSpace, let ctx = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: cgImage.bitsPerComponent, bytesPerRow: 0, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { + return nil //Not able to create CGContext + } + + var transform: CGAffineTransform = CGAffineTransform.identity + + switch imageOrientation { + case .down, .downMirrored: + transform = transform.translatedBy(x: size.width, y: size.height) + transform = transform.rotated(by: CGFloat.pi) + break + case .left, .leftMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.rotated(by: CGFloat.pi / 2.0) + break + case .right, .rightMirrored: + transform = transform.translatedBy(x: 0, y: size.height) + transform = transform.rotated(by: CGFloat.pi / -2.0) + break + default: + break + } + + //Flip image one more time if needed to, this is to prevent flipped image + switch imageOrientation { + case .upMirrored, .downMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + break + case .leftMirrored, .rightMirrored: + transform = transform.translatedBy(x: size.height, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + default: + break + } + + ctx.concatenate(transform) + + switch imageOrientation { + case .left, .leftMirrored, .right, .rightMirrored: + ctx.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.height, height: size.width)) + default: + ctx.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) + break + } + + guard let newCGImage = ctx.makeImage() else { return nil } + return newCGImage + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Units/MergeColorShaders.metal b/examples/image_segmentation/ios/ImageSegmenter/Units/MergeColorShaders.metal new file mode 100644 index 00000000..edaa8f2d --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Units/MergeColorShaders.metal @@ -0,0 +1,43 @@ + +#include +using namespace metal; + +constant half4 legendColors[] = { + {1.0000, 0.7725, 0.0000, 1}, //Vivid Yellow + {0.5020, 0.2431, 0.4588, 1}, //Strong Purple + {1.0000, 0.4078, 0.0000, 1}, //Vivid Orange + {0.6510, 0.7412, 0.8431, 1}, //Very Light Blue + {0.7569, 0.0000, 0.1255, 1}, //Vivid Red + {0.8078, 0.6353, 0.3843, 1}, //Grayish Yellow + {0.5059, 0.4392, 0.4000, 1}, //Medium Gray + {0.0000, 0.4902, 0.2039, 1}, //Vivid Green + {0.9647, 0.4627, 0.5569, 1}, //Strong Purplish Pink + {0.0000, 0.3255, 0.5412, 1}, //Strong Blue + {1.0000, 0.4392, 0.3608, 1}, //Strong Yellowish Pink + {0.3255, 0.2157, 0.4392, 1}, //Strong Violet + {1.0000, 0.5569, 0.0000, 1}, //Vivid Orange Yellow + {0.7020, 0.1569, 0.3176, 1}, //Strong Purplish Red + {0.9569, 0.7843, 0.0000, 1}, //Vivid Greenish Yellow + {0.4980, 0.0941, 0.0510, 1}, //Strong Reddish Brown + {0.5765, 0.6667, 0.0000, 1}, //Vivid Yellowish Green + {0.3490, 0.2000, 0.0824, 1}, //Deep Yellowish Brown + {0.9451, 0.2275, 0.0745, 1}, //Vivid Reddish Orange + {0.1373, 0.1725, 0.0863, 1}, //Dark Olive Green + {0.0000, 0.6314, 0.7608, 1}, //Vivid Blue +}; + +half4 mergeColor(half4 pixelData, int categoryIndex) { + half4 color = legendColors[categoryIndex]; + return pixelData*0.5 + color*0.5; +} + +kernel void mergeColor(texture2d inTexture [[ texture (0) ]], + texture2d outTexture [[ texture (1) ]], + device uint8_t* data_in [[ buffer(0) ]], + constant int& width [[buffer(1)]], + uint2 gid [[ thread_position_in_grid ]]) { + half4 pixelData = inTexture.read(gid).rgba; + uint8_t categoryIndex = data_in[gid.y * width + gid.x]; + half4 outputPixelData = mergeColor(pixelData, categoryIndex); + outTexture.write(outputPixelData, gid); +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Units/RenderToMtlViewShaders.metal b/examples/image_segmentation/ios/ImageSegmenter/Units/RenderToMtlViewShaders.metal new file mode 100644 index 00000000..fa0bc4de --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Units/RenderToMtlViewShaders.metal @@ -0,0 +1,33 @@ + + +#include +using namespace metal; + + +// Vertex input/output structure for passing results from vertex shader to fragment shader +struct VertexIO +{ + float4 position [[position]]; + float2 textureCoord [[user(texturecoord)]]; +}; + +// Vertex shader for a textured quad +vertex VertexIO vertexPassThrough(const device packed_float4 *pPosition [[ buffer(0) ]], + const device packed_float2 *pTexCoords [[ buffer(1) ]], + uint vid [[ vertex_id ]]) +{ + VertexIO outVertex; + + outVertex.position = pPosition[vid]; + outVertex.textureCoord = pTexCoords[vid]; + + return outVertex; +} + +// Fragment shader for a textured quad +fragment half4 fragmentPassThrough(VertexIO inputFragment [[ stage_in ]], + texture2d inputTexture [[ texture(0) ]], + sampler samplr [[ sampler(0) ]]) +{ + return inputTexture.sample(samplr, inputFragment.textureCoord); +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/ViewContoller/BottomSheetViewController.swift b/examples/image_segmentation/ios/ImageSegmenter/ViewContoller/BottomSheetViewController.swift new file mode 100644 index 00000000..29fa9b76 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/ViewContoller/BottomSheetViewController.swift @@ -0,0 +1,111 @@ +// Copyright 2023 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +import MediaPipeTasksVision + +protocol BottomSheetViewControllerDelegate: AnyObject { + /** + This method is called when the user opens or closes the bottom sheet. + **/ + func viewController( + _ viewController: BottomSheetViewController, + didSwitchBottomSheetViewState isOpen: Bool) +} + +/** The view controller is responsible for presenting the controls to change the meta data for the image segmenter and updating the singleton`` DetectorMetadata`` on user input. + */ +class BottomSheetViewController: UIViewController { + + enum Action { + case changeModel(Model) + case changeBottomSheetViewBottomSpace(Bool) + } + + // MARK: Delegates + weak var delegate: BottomSheetViewControllerDelegate? + + // MARK: Storyboards Connections + @IBOutlet weak var inferenceTimeNameLabel: UILabel! + @IBOutlet weak var inferenceTimeLabel: UILabel! + @IBOutlet weak var choseModelButton: UIButton! + @IBOutlet weak var toggleBottomSheetButton: UIButton! + @IBOutlet weak var delegateButton: UIButton! + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + + // MARK: - Private function + private func setupUI() { + // Choose model option + let selectedModelAction = {(action: UIAction) in + self.updateModel(modelTitle: action.title) + } + // Model acctions + let actions: [UIAction] = Model.allCases.compactMap { model in + return UIAction( + title: model.name, + state: (InferenceConfigurationManager.sharedInstance.model == model) ? .on : .off, + handler: selectedModelAction + ) + } + choseModelButton.menu = UIMenu(children: actions) + choseModelButton.showsMenuAsPrimaryAction = true + choseModelButton.changesSelectionAsPrimaryAction = true + + let selectedDelegateAction = {(action: UIAction) in + self.updateDelegate(title: action.title) + } + + let delegates: [Delegate] = [.CPU, .GPU] + let delegateActions: [UIAction] = delegates.compactMap { delegate in + return UIAction( + title: delegate == .CPU ? "CPU" : "GPU", + state: (InferenceConfigurationManager.sharedInstance.delegate == delegate) ? .on : .off, + handler: selectedDelegateAction + ) + } + + delegateButton.menu = UIMenu(children: delegateActions) + delegateButton.showsMenuAsPrimaryAction = true + delegateButton.changesSelectionAsPrimaryAction = true + + } + + private func updateModel(modelTitle: String) { + guard let model = Model(name: modelTitle) else { + return + } + InferenceConfigurationManager.sharedInstance.model = model + } + + private func updateDelegate(title: String) { + InferenceConfigurationManager.sharedInstance.delegate = title == "GPU" ? .GPU : .CPU + } + + // MARK: - Public Functions + func update(inferenceTimeString: String) { + inferenceTimeLabel.text = inferenceTimeString + } + + // MARK: IBAction + @IBAction func expandButtonTouchUpInside(_ sender: UIButton) { + sender.isSelected.toggle() + inferenceTimeLabel.isHidden = !sender.isSelected + inferenceTimeNameLabel.isHidden = !sender.isSelected + delegate?.viewController(self, didSwitchBottomSheetViewState: sender.isSelected) + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/ViewContoller/CameraViewController.swift b/examples/image_segmentation/ios/ImageSegmenter/ViewContoller/CameraViewController.swift new file mode 100644 index 00000000..dd52635a --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/ViewContoller/CameraViewController.swift @@ -0,0 +1,258 @@ +// Copyright 2023 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import AVFoundation +import MediaPipeTasksVision +import UIKit +import Metal +import MetalKit +import MetalPerformanceShaders + +/** + * The view controller is responsible for performing segmention on incoming frames from the live camera and presenting the frames with the + * new backgrourd to the user. + */ +class CameraViewController: UIViewController { + private struct Constants { + static let edgeOffset: CGFloat = 2.0 + } + + weak var inferenceResultDeliveryDelegate: InferenceResultDeliveryDelegate? + + @IBOutlet weak var previewView: PreviewMetalView! + @IBOutlet weak var cameraUnavailableLabel: UILabel! + @IBOutlet weak var resumeButton: UIButton! + + private var videoPixelBuffer: CVImageBuffer! + private var formatDescription: CMFormatDescription! + private var isSessionRunning = false + private var isObserving = false + private let backgroundQueue = DispatchQueue(label: "com.google.mediapipe.cameraController.backgroundQueue") + + // MARK: Controllers that manage functionality + // Handles all the camera related functionality + private lazy var cameraFeedService = CameraFeedService() + private let render = SegmentedImageRenderer() + + private let imageSegmenterServiceQueue = DispatchQueue( + label: "com.google.mediapipe.cameraController.imageSegmenterServiceQueue", + attributes: .concurrent) + + // Queuing reads and writes to imageSegmenterService using the Apple recommended way + // as they can be read and written from multiple threads and can result in race conditions. + private var _imageSegmenterService: ImageSegmenterService? + private var imageSegmenterService: ImageSegmenterService? { + get { + imageSegmenterServiceQueue.sync { + return self._imageSegmenterService + } + } + set { + imageSegmenterServiceQueue.async(flags: .barrier) { + self._imageSegmenterService = newValue + } + } + } + +#if !targetEnvironment(simulator) + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + initializeImageSegmenterServiceOnSessionResumption() + cameraFeedService.startLiveCameraSession {[weak self] cameraConfiguration in + DispatchQueue.main.async { + switch cameraConfiguration { + case .failed: + self?.presentVideoConfigurationErrorAlert() + case .permissionDenied: + self?.presentCameraPermissionsDeniedAlert() + default: + break + } + } + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + cameraFeedService.stopSession() + clearImageSegmenterServiceOnSessionInterruption() + } + + override func viewDidLoad() { + super.viewDidLoad() + cameraFeedService.delegate = self + // Do any additional setup after loading the view. + } + +#endif + + // Resume camera session when click button resume + @IBAction func onClickResume(_ sender: Any) { + cameraFeedService.resumeInterruptedSession {[weak self] isSessionRunning in + if isSessionRunning { + self?.resumeButton.isHidden = true + self?.cameraUnavailableLabel.isHidden = true + self?.initializeImageSegmenterServiceOnSessionResumption() + } + } + } + + private func presentCameraPermissionsDeniedAlert() { + let alertController = UIAlertController( + title: "Camera Permissions Denied", + message: + "Camera permissions have been denied for this app. You can change this by going to Settings", + preferredStyle: .alert) + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + let settingsAction = UIAlertAction(title: "Settings", style: .default) { (action) in + UIApplication.shared.open( + URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) + } + alertController.addAction(cancelAction) + alertController.addAction(settingsAction) + + present(alertController, animated: true, completion: nil) + } + + private func presentVideoConfigurationErrorAlert() { + let alert = UIAlertController( + title: "Camera Configuration Failed", + message: "There was an error while configuring camera.", + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + + self.present(alert, animated: true) + } + + private func initializeImageSegmenterServiceOnSessionResumption() { + clearAndInitializeImageSegmenterService() + startObserveConfigChanges() + } + + @objc private func clearAndInitializeImageSegmenterService() { + imageSegmenterService = nil + imageSegmenterService = ImageSegmenterService + .liveStreamImageSegmenterService( + modelPath: InferenceConfigurationManager.sharedInstance.model.modelPath, + liveStreamDelegate: self, + delegate: InferenceConfigurationManager.sharedInstance.delegate) + } + + private func clearImageSegmenterServiceOnSessionInterruption() { + stopObserveConfigChanges() + imageSegmenterService = nil + } + + private func startObserveConfigChanges() { + NotificationCenter.default + .addObserver(self, + selector: #selector(clearAndInitializeImageSegmenterService), + name: InferenceConfigurationManager.notificationName, + object: nil) + isObserving = true + } + + private func stopObserveConfigChanges() { + if isObserving { + NotificationCenter.default + .removeObserver(self, + name:InferenceConfigurationManager.notificationName, + object: nil) + } + isObserving = false + } +} + +extension CameraViewController: CameraFeedServiceDelegate { + + func didOutput(sampleBuffer: CMSampleBuffer, orientation: UIImage.Orientation) { + let currentTimeMs = Date().timeIntervalSince1970 * 1000 + guard let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), + let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) else { + return + } + + self.videoPixelBuffer = videoPixelBuffer + self.formatDescription = formatDescription + + backgroundQueue.async { [weak self] in + self?.imageSegmenterService?.segmentAsync( + sampleBuffer: sampleBuffer, + orientation: orientation, + timeStamps: Int(currentTimeMs)) + } + } + + // MARK: Session Handling Alerts + func sessionWasInterrupted(canResumeManually resumeManually: Bool) { + // Updates the UI when session is interupted. + if resumeManually { + resumeButton.isHidden = false + } else { + cameraUnavailableLabel.isHidden = false + } + clearImageSegmenterServiceOnSessionInterruption() + } + + func sessionInterruptionEnded() { + // Updates UI once session interruption has ended. + cameraUnavailableLabel.isHidden = true + resumeButton.isHidden = true + initializeImageSegmenterServiceOnSessionResumption() + } + + func didEncounterSessionRuntimeError() { + // Handles session run time error by updating the UI and providing a button if session can be + // manually resumed. + resumeButton.isHidden = false + clearImageSegmenterServiceOnSessionInterruption() + } +} + +// MARK: ImageSegmenterServiceLiveStreamDelegate +extension CameraViewController: ImageSegmenterServiceLiveStreamDelegate { + + func imageSegmenterService(_ imageSegmenterService: ImageSegmenterService, didFinishSegmention result: ResultBundle?, error: Error?) { + guard let imageSegmenterResult = result?.imageSegmenterResults.first as? ImageSegmenterResult, + let confidenceMasks = imageSegmenterResult.categoryMask else { return } + let confidenceMask = confidenceMasks.uint8Data +// let bytesArray = UnsafeBufferPointer(start: confidenceMask, count: confidenceMasks.width * confidenceMasks.height).map{$0} +// print(bytesArray[0], bytesArray[1]) + + if !render.isPrepared { + render.prepare(with: formatDescription, outputRetainedBufferCountHint: 3) + } + + let outputPixelBuffer = render.render(pixelBuffer: videoPixelBuffer, segmentDatas: confidenceMask) + previewView.pixelBuffer = outputPixelBuffer + inferenceResultDeliveryDelegate?.didPerformInference(result: result) + } +} + +// MARK: - AVLayerVideoGravity Extension +extension AVLayerVideoGravity { + var contentMode: UIView.ContentMode { + switch self { + case .resizeAspectFill: + return .scaleAspectFill + case .resizeAspect: + return .scaleAspectFit + case .resize: + return .scaleToFill + default: + return .scaleAspectFill + } + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/ViewContoller/MediaLibraryViewController.swift b/examples/image_segmentation/ios/ImageSegmenter/ViewContoller/MediaLibraryViewController.swift new file mode 100644 index 00000000..ae261052 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/ViewContoller/MediaLibraryViewController.swift @@ -0,0 +1,369 @@ +// Copyright 2023 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import AVKit +import MediaPipeTasksVision +import UIKit +import Metal +import MetalKit +import MetalPerformanceShaders + +/** + * The view controller is responsible for performing segmention on videos or images selected by the user from the device media library and + * presenting them with the new backgrourd of the image to the user. + */ +class MediaLibraryViewController: UIViewController { + + // MARK: Constants + private struct Constants { + static let edgeOffset: CGFloat = 2.0 + static let inferenceTimeIntervalInMilliseconds = 200.0 + static let milliSeconds = 1000.0 + static let savedPhotosNotAvailableText = "Saved photos album is not available." + static let mediaEmptyText = + "Click + to add an image or a video to begin running the image sengmentation." + static let pickFromGalleryButtonInset: CGFloat = 10.0 + } + // MARK: Face Segmenter Service + weak var inferenceResultDeliveryDelegate: InferenceResultDeliveryDelegate? + + // MARK: Controllers that manage functionality + private lazy var pickerController = UIImagePickerController() + private var playerViewController: AVPlayerViewController? + + private let backgroundQueue = DispatchQueue(label: "com.google.mediapipe.cameraController.backgroundQueue") + private let imageSegmenterServiceQueue = DispatchQueue( + label: "com.google.mediapipe.cameraController.imageSegmenterServiceQueue", + attributes: .concurrent) + + // MARK: Face Segmenter Service + private var imageSegmenterService: ImageSegmenterService? + private let render = SegmentedImageRenderer() + + // MARK: Storyboards Connections + @IBOutlet weak var pickFromGalleryButton: UIButton! + @IBOutlet weak var progressView: UIProgressView! + @IBOutlet weak var imageEmptyLabel: UILabel! + @IBOutlet weak var pickedImageView: UIImageView! + @IBOutlet weak var pickFromGalleryButtonBottomSpace: NSLayoutConstraint! + @IBOutlet weak var previewView: PreviewMetalView! + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + redrawBoundingBoxesForCurrentDeviceOrientation() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + guard UIImagePickerController.isSourceTypeAvailable(.savedPhotosAlbum) else { + pickFromGalleryButton.isEnabled = false + self.imageEmptyLabel.text = Constants.savedPhotosNotAvailableText + return + } + pickFromGalleryButton.isEnabled = true + self.imageEmptyLabel.text = Constants.mediaEmptyText + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + clearPlayerView() + imageSegmenterService = nil + } + + @IBAction func onClickPickFromGallery(_ sender: Any) { + configurePickerController() + present(pickerController, animated: true) + } + + private func configurePickerController() { + pickerController.delegate = self + pickerController.sourceType = .savedPhotosAlbum + pickerController.mediaTypes = [UTType.image.identifier, UTType.movie.identifier] + pickerController.allowsEditing = false + } + + private func addPlayerViewControllerAsChild() { + guard let playerViewController = playerViewController else { + return + } + playerViewController.view.translatesAutoresizingMaskIntoConstraints = false + + self.addChild(playerViewController) + self.view.addSubview(playerViewController.view) + self.view.bringSubviewToFront(self.pickFromGalleryButton) + NSLayoutConstraint.activate([ + playerViewController.view.leadingAnchor.constraint( + equalTo: view.leadingAnchor, constant: 0.0), + playerViewController.view.trailingAnchor.constraint( + equalTo: view.trailingAnchor, constant: 0.0), + playerViewController.view.topAnchor.constraint( + equalTo: view.topAnchor, constant: 0.0), + playerViewController.view.bottomAnchor.constraint( + equalTo: view.bottomAnchor, constant: 0.0) + ]) + playerViewController.didMove(toParent: self) + } + + private func removePlayerViewController() { + defer { + playerViewController?.view.removeFromSuperview() + playerViewController?.willMove(toParent: nil) + playerViewController?.removeFromParent() + } + + playerViewController?.player?.pause() + playerViewController?.player = nil + } + + private func openMediaLibrary() { + configurePickerController() + present(pickerController, animated: true) + } + + private func clearPlayerView() { + imageEmptyLabel.isHidden = false + removePlayerViewController() + } + + private func showProgressView() { + guard let progressSuperview = progressView.superview?.superview else { + return + } + progressSuperview.isHidden = false + progressView.progress = 0.0 + progressView.observedProgress = nil + self.view.bringSubviewToFront(progressSuperview) + } + + private func hideProgressView() { + guard let progressSuperview = progressView.superview?.superview else { + return + } + self.view.sendSubviewToBack(progressSuperview) + self.progressView.superview?.superview?.isHidden = true + } + + func layoutUIElements(withInferenceViewHeight height: CGFloat) { + pickFromGalleryButtonBottomSpace.constant = + height + Constants.pickFromGalleryButtonInset + view.layoutSubviews() + } + + func redrawBoundingBoxesForCurrentDeviceOrientation() { + guard let imageSegmenterService = imageSegmenterService, + imageSegmenterService.runningMode == .image || + self.playerViewController?.player?.timeControlStatus == .paused else { + return + } + } + + deinit { + playerViewController?.player?.removeTimeObserver(self) + } +} + +extension MediaLibraryViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true) + } + + private func playVideoAnDetect(asset: AVAsset) { + let videoDescription = getVideoFormatDescription(from: asset) + guard let formatDescription = videoDescription.description else { return } + if playerViewController == nil { + let playerViewController = AVPlayerViewController() + self.playerViewController = playerViewController + } + + let playerItem = AVPlayerItem(asset: asset) + + if let player = playerViewController?.player { + player.replaceCurrentItem(with: playerItem) + } + else { + playerViewController?.player = AVPlayer(playerItem: playerItem) + } + + playerViewController?.showsPlaybackControls = false + addPlayerViewControllerAsChild() + guard let player = playerViewController?.player, let playerItem = player.currentItem else { return } + let timeRange = CMTimeRange(start: .zero, duration: asset.duration) + var datas: [UnsafePointer] = [] + let videoComposition = AVMutableVideoComposition(asset: asset) { [weak self] request in + guard let self = self else { return } + backgroundQueue.async { + if !self.render.isPrepared { + self.render.prepare(with: formatDescription, outputRetainedBufferCountHint: 3, needChangeWidthHeight: videoDescription.needChangeWidthHeight) + } + let sourceImage = request.sourceImage + let time = Float((request.compositionTime - timeRange.start).seconds) + let cgimage = self.render.getCGImmage(ciImage: sourceImage) + guard let resultBundle = self.imageSegmenterService?.segment(videoFrame: cgimage, orientation: .up, timeStamps: Int(time * 1000)) else { + request.finish(with: sourceImage, context: nil) + return + } + self.inferenceResultDeliveryDelegate?.didPerformInference(result: resultBundle) + guard let result = resultBundle.imageSegmenterResults.first, let result = result else { return } + let marks = result.confidenceMasks + let _mark = marks![0] + let float32Data = _mark.float32Data + datas.append(float32Data) + guard let outputPixelBuffer = self.render.render(ciImage: sourceImage, segmentDatas: datas.removeFirst()) else { + request.finish(with: sourceImage, context: nil) + return + } + let outputImage = CIImage(cvImageBuffer: outputPixelBuffer) + request.finish(with: outputImage, context: nil) + } + } + + playerItem.videoComposition = videoComposition + playerViewController?.player?.play() + } + + func imagePickerController( + _ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + clearPlayerView() + pickedImageView.image = nil + + picker.dismiss(animated: true) + + guard let mediaType = info[.mediaType] as? String else { + return + } + render.reset() + + switch mediaType { + case UTType.movie.identifier: + guard let mediaURL = info[.mediaURL] as? URL else { + imageEmptyLabel.isHidden = false + return + } + clearAndInitializeImageSegmenterService(runningMode: .video) + let asset = AVAsset(url: mediaURL) + playVideoAnDetect(asset: asset) + + case UTType.image.identifier: + guard let image = info[.originalImage] as? UIImage else { + imageEmptyLabel.isHidden = false + break + } + imageEmptyLabel.isHidden = true + + showProgressView() + + clearAndInitializeImageSegmenterService(runningMode: .image) + print(image.size) + + DispatchQueue.global(qos: .userInteractive).async { [weak self] in + guard let self = self, + let resultBundle = self.imageSegmenterService?.segment(image: image), + let imageSegmenterResult = resultBundle.imageSegmenterResults.first, + let imageSegmenterResult = imageSegmenterResult else { + DispatchQueue.main.async { + self?.hideProgressView() + } + return + } + + DispatchQueue.main.async { + self.hideProgressView() + self.render.prepare(with: image.size, outputRetainedBufferCountHint: 3) + self.inferenceResultDeliveryDelegate?.didPerformInference(result: resultBundle) + let mark = imageSegmenterResult.categoryMask + let uint8Data = mark?.uint8Data + let newImage = self.render.render(image: image, categoryMasks: uint8Data) + self.pickedImageView.image = newImage + } + } + default: + break + } + } + + private func imageGenerator(with videoAsset: AVAsset) -> AVAssetImageGenerator { + let generator = AVAssetImageGenerator(asset: videoAsset) + generator.requestedTimeToleranceBefore = CMTimeMake(value: 1, timescale: 25) + generator.requestedTimeToleranceAfter = CMTimeMake(value: 1, timescale: 25) + generator.appliesPreferredTrackTransform = true + + return generator + } + + func clearAndInitializeImageSegmenterService(runningMode: RunningMode) { + imageSegmenterService = nil + switch runningMode { + case .image: + imageSegmenterService = ImageSegmenterService.stillImageSegmenterService( + modelPath: InferenceConfigurationManager.sharedInstance.model.modelPath, + delegate: InferenceConfigurationManager.sharedInstance.delegate) + case .video: + imageSegmenterService = ImageSegmenterService.videoImageSegmenterService( + modelPath: InferenceConfigurationManager.sharedInstance.model.modelPath, + delegate: InferenceConfigurationManager.sharedInstance.delegate) + default: + break; + } + } + + func getVideoFormatDescription(from asset: AVAsset) -> (description: CMFormatDescription?, needChangeWidthHeight: Bool) { + // Get the video track from the asset + guard let videoTrack = asset.tracks(withMediaType: AVMediaType.video).first else { + print("No video track found in the asset.") + return (nil, false) + } + let naturalSize = videoTrack.naturalSize + let size = videoTrack.naturalSize.applying(videoTrack.preferredTransform) + let newSize = CGSize(width: abs(size.width), height: abs(size.height)) + + // Create an asset reader + do { + let assetReader = try AVAssetReader(asset: asset) + + // Create an asset reader track output + let outputSettings: [String: Any] = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] + let trackOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: outputSettings) + assetReader.add(trackOutput) + + // Start the asset reader + guard assetReader.startReading() else { + print("Failed to start asset reader.") + return (nil, false) + } + defer { + assetReader.cancelReading() + } + // Read a sample to get the format description + if let sampleBuffer = trackOutput.copyNextSampleBuffer() { + if let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) { + return (formatDescription, naturalSize.width == newSize.height) + } else { + print("Failed to get format description from sample buffer.") + } + } else { + print("Failed to read a sample buffer.") + } + } catch { + print("Error creating asset reader: \(error)") + } + return (nil, false) + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/ViewContoller/RootViewController.swift b/examples/image_segmentation/ios/ImageSegmenter/ViewContoller/RootViewController.swift new file mode 100644 index 00000000..890a5148 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/ViewContoller/RootViewController.swift @@ -0,0 +1,240 @@ +// Copyright 2023 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit + +protocol InferenceResultDeliveryDelegate: AnyObject { + func didPerformInference(result: ResultBundle?) +} + +/** The view controller is responsible for presenting and handling the tabbed controls for switching between the live camera feed and + * media library view controllers. It also handles the presentation of the inferenceVC + */ +class RootViewController: UIViewController { + + // MARK: Storyboards Connections + @IBOutlet weak var tabBarContainerView: UIView! + @IBOutlet weak var runningModeTabbar: UITabBar! + @IBOutlet weak var bottomSheetViewBottomSpace: NSLayoutConstraint! + @IBOutlet weak var bottomViewHeightConstraint: NSLayoutConstraint! + + // MARK: Constants + private struct Constants { + static let inferenceBottomHeight = 212.0 + static let expandButtonHeight = 41.0 + static let expandButtonTopSpace = 10.0 + static let mediaLibraryViewControllerStoryBoardId = "MEDIA_LIBRARY_VIEW_CONTROLLER" + static let cameraViewControllerStoryBoardId = "CAMERA_VIEW_CONTROLLER" + static let storyBoardName = "Main" + static let inferenceVCEmbedSegueName = "EMBED" + static let tabBarItemsCount = 2 + } + + // MARK: Controllers that manage functionality + private var inferenceViewController: BottomSheetViewController? + private var cameraViewController: CameraViewController? + private var mediaLibraryViewController: MediaLibraryViewController? + + // MARK: Private Instance Variables + private var totalBottomSheetHeight: CGFloat { + guard let isOpen = inferenceViewController?.toggleBottomSheetButton.isSelected else { + return 0.0 + } + + return isOpen ? Constants.inferenceBottomHeight - self.view.safeAreaInsets.bottom + : Constants.expandButtonHeight + Constants.expandButtonTopSpace + } + + // MARK: View Handling Methods + override func viewDidLoad() { + super.viewDidLoad() + + runningModeTabbar.selectedItem = runningModeTabbar.items?.first + runningModeTabbar.delegate = self + instantiateCameraViewController() + switchTo(childViewController: cameraViewController, fromViewController: nil) + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + guard inferenceViewController?.toggleBottomSheetButton.isSelected == true else { + bottomSheetViewBottomSpace.constant = -Constants.inferenceBottomHeight + + Constants.expandButtonHeight + + self.view.safeAreaInsets.bottom + + Constants.expandButtonTopSpace + return + } + + bottomSheetViewBottomSpace.constant = 0.0 + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + // MARK: Storyboard Segue Handlers + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) + if segue.identifier == Constants.inferenceVCEmbedSegueName { + inferenceViewController = segue.destination as? BottomSheetViewController + inferenceViewController?.delegate = self + bottomViewHeightConstraint.constant = Constants.inferenceBottomHeight + view.layoutSubviews() + } + } + + // MARK: Private Methods + private func instantiateCameraViewController() { + guard cameraViewController == nil else { + return + } + + guard let viewController = UIStoryboard( + name: Constants.storyBoardName, bundle: .main) + .instantiateViewController( + withIdentifier: Constants.cameraViewControllerStoryBoardId) as? CameraViewController else { + return + } + + viewController.inferenceResultDeliveryDelegate = self + + cameraViewController = viewController + } + + private func instantiateMediaLibraryViewController() { + guard mediaLibraryViewController == nil else { + return + } + guard let viewController = UIStoryboard(name: Constants.storyBoardName, bundle: .main) + .instantiateViewController( + withIdentifier: Constants.mediaLibraryViewControllerStoryBoardId) + as? MediaLibraryViewController else { + return + } + + viewController.inferenceResultDeliveryDelegate = self + mediaLibraryViewController = viewController + } + + private func updateMediaLibraryControllerUI() { + guard let mediaLibraryViewController = mediaLibraryViewController else { + return + } + + mediaLibraryViewController.layoutUIElements( + withInferenceViewHeight: self.totalBottomSheetHeight) + } +} + +// MARK: UITabBarDelegate +extension RootViewController: UITabBarDelegate { + func switchTo( + childViewController: UIViewController?, + fromViewController: UIViewController?) { + fromViewController?.willMove(toParent: nil) + fromViewController?.view.removeFromSuperview() + fromViewController?.removeFromParent() + + guard let childViewController = childViewController else { + return + } + + addChild(childViewController) + childViewController.view.translatesAutoresizingMaskIntoConstraints = false + tabBarContainerView.addSubview(childViewController.view) + NSLayoutConstraint.activate( + [ + childViewController.view.leadingAnchor.constraint( + equalTo: tabBarContainerView.leadingAnchor, + constant: 0.0), + childViewController.view.trailingAnchor.constraint( + equalTo: tabBarContainerView.trailingAnchor, + constant: 0.0), + childViewController.view.topAnchor.constraint( + equalTo: tabBarContainerView.topAnchor, + constant: 0.0), + childViewController.view.bottomAnchor.constraint( + equalTo: tabBarContainerView.bottomAnchor, + constant: 0.0) + ] + ) + childViewController.didMove(toParent: self) + } + + func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { + guard let tabBarItems = tabBar.items, tabBarItems.count == Constants.tabBarItemsCount else { + return + } + + var fromViewController: UIViewController? + var toViewController: UIViewController? + + switch item { + case tabBarItems[0]: + fromViewController = mediaLibraryViewController + toViewController = cameraViewController + case tabBarItems[1]: + instantiateMediaLibraryViewController() + fromViewController = cameraViewController + toViewController = mediaLibraryViewController + default: + break + } + + switchTo( + childViewController: toViewController, + fromViewController: fromViewController) + self.updateMediaLibraryControllerUI() + } +} + +// MARK: InferenceResultDeliveryDelegate Methods +extension RootViewController: InferenceResultDeliveryDelegate { + func didPerformInference(result: ResultBundle?) { + var inferenceTimeString = "" + + if let inferenceTime = result?.inferenceTime { + inferenceTimeString = String(format: "%.2fms", inferenceTime) + } + DispatchQueue.main.async { + self.inferenceViewController?.update(inferenceTimeString: inferenceTimeString) + } + } +} + +// MARK: BottomSheetViewControllerDelegate Methods +extension RootViewController: BottomSheetViewControllerDelegate { + func viewController( + _ viewController: BottomSheetViewController, + didSwitchBottomSheetViewState isOpen: Bool) { + if isOpen == true { + bottomSheetViewBottomSpace.constant = 0.0 + } + else { + bottomSheetViewBottomSpace.constant = -Constants.inferenceBottomHeight + + Constants.expandButtonHeight + + self.view.safeAreaInsets.bottom + + Constants.expandButtonTopSpace + } + + UIView.animate(withDuration: 0.3) {[weak self] in + guard let weakSelf = self else { + return + } + weakSelf.view.layoutSubviews() + weakSelf.updateMediaLibraryControllerUI() + } + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenter/Views/PreviewMetalView.swift b/examples/image_segmentation/ios/ImageSegmenter/Views/PreviewMetalView.swift new file mode 100644 index 00000000..71d923d2 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenter/Views/PreviewMetalView.swift @@ -0,0 +1,362 @@ +// Copyright 2023 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import CoreMedia +import Metal +import MetalKit + +class PreviewMetalView: MTKView { + + enum Rotation: Int { + case rotate0Degrees + case rotate90Degrees + case rotate180Degrees + case rotate270Degrees + } + + private func curentDeviceRotation() -> Rotation { + switch UIDevice.current.orientation { + case .landscapeLeft: + return .rotate270Degrees + case .landscapeRight: + return .rotate90Degrees + default: + return .rotate0Degrees + } + } + + var mirroring = false { + didSet { + syncQueue.sync { + internalMirroring = mirroring + } + } + } + + private var internalMirroring: Bool = false + + var pixelBuffer: CVPixelBuffer? { + didSet { + syncQueue.sync { + internalPixelBuffer = pixelBuffer + } + } + } + + private var internalPixelBuffer: CVPixelBuffer? + + private let syncQueue = DispatchQueue(label: "Preview View Sync Queue", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem) + + private var textureCache: CVMetalTextureCache? + + private var textureWidth: Int = 0 + + private var textureHeight: Int = 0 + + private var textureMirroring = false + + private var textureRotation: Rotation = .rotate0Degrees + + private var sampler: MTLSamplerState! + + private var renderPipelineState: MTLRenderPipelineState! + + private var commandQueue: MTLCommandQueue? + + private var vertexCoordBuffer: MTLBuffer! + + private var textCoordBuffer: MTLBuffer! + + private var internalBounds: CGRect! + + private var textureTranform: CGAffineTransform? + + func texturePointForView(point: CGPoint) -> CGPoint? { + var result: CGPoint? + guard let transform = textureTranform else { + return result + } + let transformPoint = point.applying(transform) + + if CGRect(origin: .zero, size: CGSize(width: textureWidth, height: textureHeight)).contains(transformPoint) { + result = transformPoint + } else { + print("Invalid point \(point) result point \(transformPoint)") + } + + return result + } + + func viewPointForTexture(point: CGPoint) -> CGPoint? { + var result: CGPoint? + guard let transform = textureTranform?.inverted() else { + return result + } + let transformPoint = point.applying(transform) + + if internalBounds.contains(transformPoint) { + result = transformPoint + } else { + print("Invalid point \(point) result point \(transformPoint)") + } + + return result + } + + func flushTextureCache() { + textureCache = nil + } + + private func setupTransform(width: Int, height: Int, mirroring: Bool, rotation: Rotation) { + var scaleX: Float = 1.0 + var scaleY: Float = 1.0 + var resizeAspect: Float = 1.0 + + internalBounds = self.bounds + textureWidth = width + textureHeight = height + textureMirroring = mirroring + textureRotation = rotation + + if textureWidth > 0 && textureHeight > 0 { + switch textureRotation { + case .rotate0Degrees, .rotate180Degrees: + scaleX = Float(internalBounds.width / CGFloat(textureWidth)) + scaleY = Float(internalBounds.height / CGFloat(textureHeight)) + + case .rotate90Degrees, .rotate270Degrees: + scaleX = Float(internalBounds.width / CGFloat(textureHeight)) + scaleY = Float(internalBounds.height / CGFloat(textureWidth)) + } + } + // Resize aspect ratio. + resizeAspect = min(scaleX, scaleY) + if scaleX > scaleY { + scaleY = scaleX / scaleY + scaleX = 1.0 + } else { + scaleX = scaleY / scaleX + scaleY = 1.0 + } + + if textureMirroring { + scaleX *= -1.0 + } + + // Vertex coordinate takes the gravity into account. + let vertexData: [Float] = [ + -scaleX, -scaleY, 0.0, 1.0, + scaleX, -scaleY, 0.0, 1.0, + -scaleX, scaleY, 0.0, 1.0, + scaleX, scaleY, 0.0, 1.0 + ] + vertexCoordBuffer = device!.makeBuffer(bytes: vertexData, length: vertexData.count * MemoryLayout.size, options: []) + + // Texture coordinate takes the rotation into account. + var textData: [Float] + switch textureRotation { + case .rotate0Degrees: + textData = [ + 0.0, 1.0, + 1.0, 1.0, + 0.0, 0.0, + 1.0, 0.0 + ] + + case .rotate180Degrees: + textData = [ + 1.0, 0.0, + 0.0, 0.0, + 1.0, 1.0, + 0.0, 1.0 + ] + + case .rotate90Degrees: + textData = [ + 1.0, 1.0, + 1.0, 0.0, + 0.0, 1.0, + 0.0, 0.0 + ] + + case .rotate270Degrees: + textData = [ + 0.0, 0.0, + 0.0, 1.0, + 1.0, 0.0, + 1.0, 1.0 + ] + } + textCoordBuffer = device?.makeBuffer(bytes: textData, length: textData.count * MemoryLayout.size, options: []) + + // Calculate the transform from texture coordinates to view coordinates + var transform = CGAffineTransform.identity + if textureMirroring { + transform = transform.concatenating(CGAffineTransform(scaleX: -1, y: 1)) + transform = transform.concatenating(CGAffineTransform(translationX: CGFloat(textureWidth), y: 0)) + } + + switch textureRotation { + case .rotate0Degrees: + transform = transform.concatenating(CGAffineTransform(rotationAngle: CGFloat(0))) + + case .rotate180Degrees: + transform = transform.concatenating(CGAffineTransform(rotationAngle: CGFloat(Double.pi))) + transform = transform.concatenating(CGAffineTransform(translationX: CGFloat(textureWidth), y: CGFloat(textureHeight))) + + case .rotate90Degrees: + transform = transform.concatenating(CGAffineTransform(rotationAngle: CGFloat(Double.pi) / 2)) + transform = transform.concatenating(CGAffineTransform(translationX: CGFloat(textureHeight), y: 0)) + + case .rotate270Degrees: + transform = transform.concatenating(CGAffineTransform(rotationAngle: 3 * CGFloat(Double.pi) / 2)) + transform = transform.concatenating(CGAffineTransform(translationX: 0, y: CGFloat(textureWidth))) + } + + transform = transform.concatenating(CGAffineTransform(scaleX: CGFloat(resizeAspect), y: CGFloat(resizeAspect))) + let tranformRect = CGRect(origin: .zero, size: CGSize(width: textureWidth, height: textureHeight)).applying(transform) + let xShift = (internalBounds.size.width - tranformRect.size.width) / 2 + let yShift = (internalBounds.size.height - tranformRect.size.height) / 2 + transform = transform.concatenating(CGAffineTransform(translationX: xShift, y: yShift)) + textureTranform = transform.inverted() + } + + required init(coder: NSCoder) { + super.init(coder: coder) + + device = MTLCreateSystemDefaultDevice() + + configureMetal() + + createTextureCache() + + colorPixelFormat = .bgra8Unorm + } + + func configureMetal() { + let defaultLibrary = device!.makeDefaultLibrary()! + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + pipelineDescriptor.vertexFunction = defaultLibrary.makeFunction(name: "vertexPassThrough") + pipelineDescriptor.fragmentFunction = defaultLibrary.makeFunction(name: "fragmentPassThrough") + + // To determine how textures are sampled, create a sampler descriptor to query for a sampler state from the device. + let samplerDescriptor = MTLSamplerDescriptor() + samplerDescriptor.sAddressMode = .clampToEdge + samplerDescriptor.tAddressMode = .clampToEdge + samplerDescriptor.minFilter = .linear + samplerDescriptor.magFilter = .linear + sampler = device!.makeSamplerState(descriptor: samplerDescriptor) + + do { + renderPipelineState = try device!.makeRenderPipelineState(descriptor: pipelineDescriptor) + } catch { + fatalError("Unable to create preview Metal view pipeline state. (\(error))") + } + + commandQueue = device!.makeCommandQueue() + } + + func createTextureCache() { + var newTextureCache: CVMetalTextureCache? + if CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device!, nil, &newTextureCache) == kCVReturnSuccess { + textureCache = newTextureCache + } else { + assertionFailure("Unable to allocate texture cache") + } + } + + /// - Tag: DrawMetalTexture + override func draw(_ rect: CGRect) { + var pixelBuffer: CVPixelBuffer? + var mirroring = false + var rotation: Rotation = .rotate0Degrees + + syncQueue.sync { + pixelBuffer = internalPixelBuffer + mirroring = internalMirroring + rotation = curentDeviceRotation() + } + + guard let drawable = currentDrawable, + let currentRenderPassDescriptor = currentRenderPassDescriptor, + let previewPixelBuffer = pixelBuffer else { + return + } + + // Create a Metal texture from the image buffer. + let width = CVPixelBufferGetWidth(previewPixelBuffer) + let height = CVPixelBufferGetHeight(previewPixelBuffer) + + if textureCache == nil { + createTextureCache() + } + var cvTextureOut: CVMetalTexture? + CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, + textureCache!, + previewPixelBuffer, + nil, + .bgra8Unorm, + width, + height, + 0, + &cvTextureOut) + guard let cvTexture = cvTextureOut, let texture = CVMetalTextureGetTexture(cvTexture) else { + print("Failed to create preview texture") + + CVMetalTextureCacheFlush(textureCache!, 0) + return + } + + if texture.width != textureWidth || + texture.height != textureHeight || + self.bounds != internalBounds || + mirroring != textureMirroring || + rotation != textureRotation { + setupTransform(width: texture.width, height: texture.height, mirroring: mirroring, rotation: rotation) + } + + // Set up command buffer and encoder + guard let commandQueue = commandQueue else { + print("Failed to create Metal command queue") + CVMetalTextureCacheFlush(textureCache!, 0) + return + } + + guard let commandBuffer = commandQueue.makeCommandBuffer() else { + print("Failed to create Metal command buffer") + CVMetalTextureCacheFlush(textureCache!, 0) + return + } + + guard let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: currentRenderPassDescriptor) else { + print("Failed to create Metal command encoder") + CVMetalTextureCacheFlush(textureCache!, 0) + return + } + + commandEncoder.label = "Preview display" + commandEncoder.setRenderPipelineState(renderPipelineState!) + commandEncoder.setVertexBuffer(vertexCoordBuffer, offset: 0, index: 0) + commandEncoder.setVertexBuffer(textCoordBuffer, offset: 0, index: 1) + commandEncoder.setFragmentTexture(texture, index: 0) + commandEncoder.setFragmentSamplerState(sampler, index: 0) + commandEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) + commandEncoder.endEncoding() + + // Draw to the screen. + commandBuffer.present(drawable) + commandBuffer.commit() + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenterTests/ImageSegmenterTests.swift b/examples/image_segmentation/ios/ImageSegmenterTests/ImageSegmenterTests.swift new file mode 100644 index 00000000..b94ac850 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenterTests/ImageSegmenterTests.swift @@ -0,0 +1,36 @@ +// +// ImageSegmenterTests.swift +// ImageSegmenterTests +// +// Created by MBA0077 on 12/5/23. +// + +import XCTest +@testable import ImageSegmenter + +final class ImageSegmenterTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/examples/image_segmentation/ios/ImageSegmenterUITests/ImageSegmenterUITests.swift b/examples/image_segmentation/ios/ImageSegmenterUITests/ImageSegmenterUITests.swift new file mode 100644 index 00000000..ccf7c1cd --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenterUITests/ImageSegmenterUITests.swift @@ -0,0 +1,41 @@ +// +// ImageSegmenterUITests.swift +// ImageSegmenterUITests +// +// Created by MBA0077 on 12/5/23. +// + +import XCTest + +final class ImageSegmenterUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/examples/image_segmentation/ios/ImageSegmenterUITests/ImageSegmenterUITestsLaunchTests.swift b/examples/image_segmentation/ios/ImageSegmenterUITests/ImageSegmenterUITestsLaunchTests.swift new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/image_segmentation/ios/ImageSegmenterUITests/ImageSegmenterUITestsLaunchTests.swift @@ -0,0 +1 @@ + diff --git a/examples/image_segmentation/ios/Podfile b/examples/image_segmentation/ios/Podfile new file mode 100644 index 00000000..fff4ee4b --- /dev/null +++ b/examples/image_segmentation/ios/Podfile @@ -0,0 +1,20 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'ImageSegmenter' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + pod 'MediaPipeTasksVision' + + target 'ImageSegmenterTests' do + inherit! :search_paths + # Pods for testing + end +end + +target 'ImageSegmenterUITests' do + inherit! :search_paths + # Pods for testing +end + diff --git a/examples/image_segmentation/ios/Podfile.lock b/examples/image_segmentation/ios/Podfile.lock new file mode 100644 index 00000000..3a56d67e --- /dev/null +++ b/examples/image_segmentation/ios/Podfile.lock @@ -0,0 +1,20 @@ +PODS: + - MediaPipeTasksCommon (0.10.12) + - MediaPipeTasksVision (0.10.12): + - MediaPipeTasksCommon (= 0.10.12) + +DEPENDENCIES: + - MediaPipeTasksVision + +SPEC REPOS: + trunk: + - MediaPipeTasksCommon + - MediaPipeTasksVision + +SPEC CHECKSUMS: + MediaPipeTasksCommon: 254e6bff77804b262f6ecf180477142ea551e802 + MediaPipeTasksVision: 78d5c47cd7996b4d815bacba0a52dbf01458dfaf + +PODFILE CHECKSUM: 5da73db60b6d26e627c8f1906095fe364c7d0a7f + +COCOAPODS: 1.14.3 diff --git a/examples/image_segmentation/ios/RunScripts/download_models.sh b/examples/image_segmentation/ios/RunScripts/download_models.sh new file mode 100755 index 00000000..bac13440 --- /dev/null +++ b/examples/image_segmentation/ios/RunScripts/download_models.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Copyright 2023 The MediaPipe Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# Download selfie_segmenter.tflite from the internet if it's not exist. +MODEL_FILE=./ImageSegmenter/selfie_segmenter.tflite +if test -f "$MODEL_FILE"; then + echo "INFO: selfie_segmenter.tflite existed. Skip downloading and use the local task." +else + curl -o ${MODEL_FILE} https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite + echo "INFO: Downloaded selfie_segmenter.tflite to $MODEL_FILE ." +fi + +# Download deeplab_v3.tflite from the internet if it's not exist. +MODEL_FILE=./ImageSegmenter/deeplab_v3.tflite +if test -f "$MODEL_FILE"; then + echo "INFO: deeplab_v3.tflite existed. Skip downloading and use the local task." +else + curl -o ${MODEL_FILE} https://storage.googleapis.com/mediapipe-models/image_segmenter/deeplab_v3/float32/latest/deeplab_v3.tflite + echo "INFO: Downloaded deeplab_v3.tflite to $MODEL_FILE ." +fi