Skip to content

Commit f9e23ea

Browse files
shoumikhinfacebook-github-bot
authored andcommitted
iOS demo app. (#557)
Summary: Pull Request resolved: #557 allow-non-ascii-filenames Reviewed By: cccclai Differential Revision: D49800418 fbshipit-source-id: 0061c0066230a18eee0324e59eebff8c9c574d5f
1 parent 145b624 commit f9e23ea

File tree

18 files changed

+1747
-0
lines changed

18 files changed

+1747
-0
lines changed

examples/ios_demo_apps/ExecuTorchDemo/ExecuTorchDemo.xcodeproj/project.pbxproj

Lines changed: 859 additions & 0 deletions
Large diffs are not rendered by default.
Loading
Loading
Loading
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import SwiftUI
10+
11+
@main
12+
struct App: SwiftUI.App {
13+
var body: some Scene {
14+
WindowGroup {
15+
ContentView()
16+
}
17+
}
18+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import AVFoundation
10+
import SwiftUI
11+
12+
enum CameraControllerError: Error {
13+
case authorization(String)
14+
case capture(String)
15+
case setup(String)
16+
}
17+
18+
class CameraController: NSObject, ObservableObject, AVCapturePhotoCaptureDelegate {
19+
let captureSession = AVCaptureSession()
20+
private var photoOutput = AVCapturePhotoOutput()
21+
private var timer: Timer?
22+
private var callback: ((Result<UIImage, Error>) -> Void)?
23+
24+
func startCapturing(withTimeInterval interval: TimeInterval,
25+
callback: @escaping (Result<UIImage, Error>) -> Void) {
26+
authorize { error in
27+
if let error {
28+
DispatchQueue.main.async {
29+
callback(.failure(error))
30+
}
31+
return
32+
}
33+
self.setup { error in
34+
if let error {
35+
DispatchQueue.main.async {
36+
callback(.failure(error))
37+
}
38+
return
39+
}
40+
self.captureSession.startRunning()
41+
DispatchQueue.main.async {
42+
self.callback = callback
43+
self.timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
44+
self.photoOutput.capturePhoto(with: AVCapturePhotoSettings(), delegate: self)
45+
}
46+
}
47+
}
48+
}
49+
}
50+
51+
private func authorize(_ completion: @escaping (Error?) -> Void) {
52+
switch AVCaptureDevice.authorizationStatus(for: .video) {
53+
case .authorized:
54+
DispatchQueue.global(qos: .userInitiated).async {
55+
completion(nil)
56+
}
57+
case .notDetermined:
58+
AVCaptureDevice.requestAccess(for: .video) { granted in
59+
DispatchQueue.global(qos: .userInitiated).async {
60+
if granted {
61+
completion(nil)
62+
} else {
63+
completion(CameraControllerError.authorization("Camera access denied"))
64+
}
65+
}
66+
}
67+
default:
68+
DispatchQueue.global(qos: .userInitiated).async {
69+
completion(CameraControllerError.authorization("Camera access denied"))
70+
}
71+
}
72+
}
73+
74+
private func setup(_ callback: (Error?) -> Void) {
75+
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video)
76+
else {
77+
callback(CameraControllerError.setup("Cannot get video capture device"))
78+
return
79+
}
80+
let videoInput: AVCaptureDeviceInput
81+
do {
82+
videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
83+
} catch {
84+
callback(CameraControllerError.setup("Cannot set up video input: \(error)"))
85+
return
86+
}
87+
if captureSession.canAddInput(videoInput) {
88+
captureSession.addInput(videoInput)
89+
} else {
90+
callback(CameraControllerError.setup("Cannot add video input"))
91+
return
92+
}
93+
if captureSession.canAddOutput(photoOutput) {
94+
captureSession.addOutput(photoOutput)
95+
} else {
96+
callback(CameraControllerError.setup("Cannot add photo output"))
97+
return
98+
}
99+
callback(nil)
100+
}
101+
102+
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
103+
guard let callback = self.callback else {
104+
print("No image capturing callback set")
105+
return
106+
}
107+
if let error {
108+
callback(.failure(CameraControllerError.capture("Image capture error: \(error)")))
109+
}
110+
guard let imageData = photo.fileDataRepresentation(),
111+
let image = UIImage(data: imageData),
112+
let cgImage = image.cgImage
113+
else {
114+
callback(.failure(CameraControllerError.capture("Couldn't get image data")))
115+
return
116+
}
117+
var orientation = UIImage.Orientation.up
118+
switch UIDevice.current.orientation {
119+
case .portrait:
120+
orientation = .right
121+
case .portraitUpsideDown:
122+
orientation = .left
123+
case .landscapeRight:
124+
orientation = .down
125+
default:
126+
break
127+
}
128+
callback(.success(UIImage(cgImage: cgImage, scale: image.scale, orientation: orientation)))
129+
}
130+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import AVFoundation
10+
import SwiftUI
11+
12+
struct CameraPreview: UIViewRepresentable {
13+
let captureSession: AVCaptureSession
14+
15+
func makeUIView(context: Context) -> UIView {
16+
let view = CameraView(frame: UIScreen.main.bounds)
17+
view.videoPreviewLayer?.session = captureSession
18+
return view
19+
}
20+
21+
func updateUIView(_ uiView: UIView, context: Context) {
22+
if let view = uiView as? CameraView {
23+
view.videoPreviewLayer?.frame = uiView.bounds
24+
}
25+
}
26+
}
27+
28+
final class CameraView: UIView {
29+
override class var layerClass: AnyClass {
30+
return AVCaptureVideoPreviewLayer.self
31+
}
32+
33+
var videoPreviewLayer: AVCaptureVideoPreviewLayer? {
34+
return layer as? AVCaptureVideoPreviewLayer
35+
}
36+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import ImageClassification
10+
import MobileNetClassifier
11+
import SwiftUI
12+
13+
enum Mode: String, CaseIterable {
14+
case xnnpack = "XNNPACK"
15+
case benchmark = "Benchmark"
16+
case coreML = "Core ML"
17+
}
18+
19+
class ClassificationController: ObservableObject {
20+
@AppStorage("mode") var mode: Mode = .benchmark
21+
@Published var classifications: [Classification] = []
22+
@Published var elapsedTime: TimeInterval = 0.0
23+
@Published var isRunning = false
24+
25+
private let queue = DispatchQueue(label: "org.pytorch.executorch.demo", qos: .userInitiated)
26+
private var classifier: ImageClassification?
27+
private var currentMode: Mode = .benchmark
28+
29+
func classify(_ image: UIImage) {
30+
guard !isRunning else {
31+
print("Dropping frame")
32+
return
33+
}
34+
isRunning = true
35+
36+
if currentMode != mode {
37+
currentMode = mode
38+
classifier = nil
39+
}
40+
queue.async {
41+
var classifications: [Classification] = []
42+
var elapsedTime: TimeInterval = -1
43+
do {
44+
if self.classifier == nil {
45+
self.classifier = try self.createClassifier(for: self.currentMode)
46+
}
47+
let startTime = CFAbsoluteTimeGetCurrent()
48+
classifications = try self.classifier?.classify(image: image) ?? []
49+
elapsedTime = (CFAbsoluteTimeGetCurrent() - startTime) * 1000
50+
} catch {
51+
print("Error classifying image: \(error)")
52+
}
53+
DispatchQueue.main.async {
54+
self.classifications = classifications
55+
self.elapsedTime = elapsedTime
56+
self.isRunning = false
57+
}
58+
}
59+
}
60+
61+
private func createClassifier(for mode: Mode) throws -> ImageClassification? {
62+
let modelFileName: String
63+
switch mode {
64+
case .xnnpack:
65+
modelFileName = "mv3_xnnpack_fp32"
66+
case .benchmark:
67+
modelFileName = "mv3"
68+
case .coreML:
69+
modelFileName = "mv3_coreml"
70+
}
71+
guard let modelFilePath = Bundle.main.path(forResource: modelFileName, ofType: "pte"),
72+
let labelsFilePath = Bundle.main.path(forResource: "imagenet_classes", ofType: "txt")
73+
else { return nil }
74+
return try MobileNetClassifier(modelFilePath: modelFilePath, labelsFilePath: labelsFilePath)
75+
}
76+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import AVFoundation
10+
import SwiftUI
11+
12+
struct ContentView: View {
13+
@StateObject private var cameraController = CameraController()
14+
@StateObject private var classificationController = ClassificationController()
15+
16+
var body: some View {
17+
ZStack {
18+
cameraPreview
19+
controlPanel
20+
}
21+
}
22+
23+
private var cameraPreview: some View {
24+
CameraPreview(captureSession: cameraController.captureSession)
25+
.aspectRatio(contentMode: .fill)
26+
.edgesIgnoringSafeArea(.all)
27+
.onAppear(perform: startCapturing)
28+
.onDisappear(perform: stopCapturing)
29+
}
30+
31+
private var controlPanel: some View {
32+
VStack(spacing: 0) {
33+
TopBar(title: "ExecuTorch Demo")
34+
ClassificationLabelView(controller: classificationController)
35+
Spacer()
36+
ClassificationTimeView(controller: classificationController)
37+
ModeSelector(controller: classificationController)
38+
}
39+
}
40+
41+
private func startCapturing() {
42+
UIApplication.shared.isIdleTimerDisabled = true
43+
cameraController.startCapturing(withTimeInterval: 1.0) { result in
44+
switch result {
45+
case .success(let image):
46+
self.classificationController.classify(image)
47+
case .failure(let error):
48+
self.handleError(error)
49+
}
50+
}
51+
}
52+
53+
private func stopCapturing() {
54+
UIApplication.shared.isIdleTimerDisabled = false
55+
}
56+
57+
private func handleError(_ error: Error) {
58+
stopCapturing()
59+
print(error)
60+
}
61+
}
62+
63+
struct ContentView_Previews: PreviewProvider {
64+
static var previews: some View {
65+
ContentView()
66+
}
67+
}

0 commit comments

Comments
 (0)