Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Log #37

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open

Log #37

Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
43c8f8b
Add human feature.
john-rocky May 21, 2024
1c72a52
Add human feature.
john-rocky May 21, 2024
0cef67b
Edit porstprocessing.
john-rocky May 22, 2024
a7fcb2b
Merge branch 'main' into human
glenn-jocher May 29, 2024
51b6226
Merge branch 'main' into human
john-rocky Jun 3, 2024
09c6447
simple swift tracking
john-rocky Jun 4, 2024
cbfd8c5
hide a tracking toggle in detect mode
john-rocky Jun 4, 2024
b73291e
Fix feature value order.
john-rocky Jun 4, 2024
22eec3d
updated human features every frame
john-rocky Jun 5, 2024
16c668f
add landscape mode
john-rocky Jun 14, 2024
0299227
Merge branch 'main' into landscapemode
glenn-jocher Jun 16, 2024
8c19ab0
Merge branch 'main' into landscapemode
glenn-jocher Jun 20, 2024
d020aba
fix zoom label position.
john-rocky Jun 23, 2024
a245109
Detection log for developer mode
john-rocky Jun 26, 2024
1d11612
Auto-format by https://ultralytics.com/actions
UltralyticsAssistant Jun 26, 2024
f2a0cf7
Delete app sandbox log files after sharing
john-rocky Jun 26, 2024
aaa8e77
Merge remote-tracking branch 'refs/remotes/origin/log'
john-rocky Jun 26, 2024
9a88943
Merge branch 'main' into log
john-rocky Jun 26, 2024
5339c8d
correct the item name in detection mode
john-rocky Jun 26, 2024
41d820a
Merge remote-tracking branch 'refs/remotes/origin/log'
john-rocky Jun 26, 2024
46ec93e
Merge branch 'main' into log
glenn-jocher Aug 11, 2024
8530c07
Auto-format by https://ultralytics.com/actions
UltralyticsAssistant Aug 11, 2024
66d2e9d
Update format.yml
glenn-jocher Aug 11, 2024
dab00d4
Auto-format by https://ultralytics.com/actions
UltralyticsAssistant Aug 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/format.yml
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ on:

jobs:
format:
runs-on: ubuntu-latest
runs-on: macos-latest
steps:
- name: Run Ultralytics Formatting
uses: ultralytics/actions@main
@@ -23,6 +23,7 @@ jobs:
python: true # format Python code and docstrings
markdown: true # format Markdown
prettier: true # format YAML
swift: true # format Swift
spelling: false # check spelling
links: false # check broken links
summary: true # print PR summary with GPT4 (requires 'openai_api_key' or 'openai_azure_api_key' and 'openai_azure_endpoint')
49 changes: 23 additions & 26 deletions YOLO.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 63;
objects = {

/* Begin PBXBuildFile section */
@@ -13,14 +13,13 @@
636EFCAF21E62DD300DE43BC /* VideoCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 636EFCA221E62DD300DE43BC /* VideoCapture.swift */; };
636EFCB321E62DD300DE43BC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 636EFCA721E62DD300DE43BC /* AppDelegate.swift */; };
636EFCB921E62E3900DE43BC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 636EFCB821E62E3900DE43BC /* Assets.xcassets */; };
6381D2182B7817C200ABA4E8 /* yolov8l.mlpackage in Sources */ = {isa = PBXBuildFile; fileRef = 6381D2132B7817C200ABA4E8 /* yolov8l.mlpackage */; };
6381D2192B7817C200ABA4E8 /* yolov8x.mlpackage in Sources */ = {isa = PBXBuildFile; fileRef = 6381D2142B7817C200ABA4E8 /* yolov8x.mlpackage */; };
6381D21A2B7817C200ABA4E8 /* yolov8s.mlpackage in Sources */ = {isa = PBXBuildFile; fileRef = 6381D2152B7817C200ABA4E8 /* yolov8s.mlpackage */; };
6381D21B2B7817C200ABA4E8 /* yolov8m.mlpackage in Sources */ = {isa = PBXBuildFile; fileRef = 6381D2162B7817C200ABA4E8 /* yolov8m.mlpackage */; };
6381D21C2B7817C200ABA4E8 /* yolov8n.mlpackage in Sources */ = {isa = PBXBuildFile; fileRef = 6381D2172B7817C200ABA4E8 /* yolov8n.mlpackage */; };
63CF371F2514455300E2DEA1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6323C44D22186177008AE681 /* LaunchScreen.storyboard */; };
63CF37202514455300E2DEA1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6323C44F22186177008AE681 /* Main.storyboard */; };
63CF37212514455300E2DEA1 /* ultralytics_yolo_logotype.png in Resources */ = {isa = PBXBuildFile; fileRef = 6323C45122186177008AE681 /* ultralytics_yolo_logotype.png */; };
730E72CD2BFC43BF000E1F45 /* PostProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 730E72CC2BFC43BF000E1F45 /* PostProcessing.swift */; };
73A4E7752C0EA36D00218E8F /* HumanModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73A4E7742C0EA36D00218E8F /* HumanModel.swift */; };
73A4E7772C0EA37300218E8F /* TrackingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73A4E7762C0EA37300218E8F /* TrackingModel.swift */; };
73FE95772C2B5A2D00C6C806 /* SaveResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73FE95762C2B5A2D00C6C806 /* SaveResults.swift */; };
8EDAA33950796844333D60A7 /* BoundingBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDAA633C1F2B50286D16008 /* BoundingBoxView.swift */; };
/* End PBXBuildFile section */

@@ -35,12 +34,11 @@
636EFCA221E62DD300DE43BC /* VideoCapture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCapture.swift; sourceTree = "<group>"; };
636EFCA721E62DD300DE43BC /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
636EFCB821E62E3900DE43BC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
6381D2132B7817C200ABA4E8 /* yolov8l.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolov8l.mlpackage; sourceTree = "<group>"; };
6381D2142B7817C200ABA4E8 /* yolov8x.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolov8x.mlpackage; sourceTree = "<group>"; };
6381D2152B7817C200ABA4E8 /* yolov8s.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolov8s.mlpackage; sourceTree = "<group>"; };
6381D2162B7817C200ABA4E8 /* yolov8m.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolov8m.mlpackage; sourceTree = "<group>"; };
6381D2172B7817C200ABA4E8 /* yolov8n.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolov8n.mlpackage; sourceTree = "<group>"; };
63B8B0A821E62A890026FBC3 /* .gitignore */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .gitignore; sourceTree = "<group>"; };
730E72CC2BFC43BF000E1F45 /* PostProcessing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostProcessing.swift; sourceTree = "<group>"; };
73A4E7742C0EA36D00218E8F /* HumanModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HumanModel.swift; sourceTree = "<group>"; };
73A4E7762C0EA37300218E8F /* TrackingModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingModel.swift; sourceTree = "<group>"; };
73FE95762C2B5A2D00C6C806 /* SaveResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveResults.swift; sourceTree = "<group>"; };
7BCB411721C3096100BFC4D0 /* YOLO.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = YOLO.app; sourceTree = BUILT_PRODUCTS_DIR; };
8EDAA633C1F2B50286D16008 /* BoundingBoxView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoundingBoxView.swift; sourceTree = "<group>"; };
8EDAAA4507D2D23D7FAB827F /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
@@ -60,8 +58,12 @@
636166E72514438D0054FA7E /* Utilities */ = {
isa = PBXGroup;
children = (
73A4E7762C0EA37300218E8F /* TrackingModel.swift */,
73A4E7742C0EA36D00218E8F /* HumanModel.swift */,
730E72CC2BFC43BF000E1F45 /* PostProcessing.swift */,
636166E9251443B20054FA7E /* ThresholdProvider.swift */,
8EDAA633C1F2B50286D16008 /* BoundingBoxView.swift */,
73FE95762C2B5A2D00C6C806 /* SaveResults.swift */,
);
path = Utilities;
sourceTree = "<group>";
@@ -87,11 +89,6 @@
63A946D8271800E20001C3ED /* Models */ = {
isa = PBXGroup;
children = (
6381D2132B7817C200ABA4E8 /* yolov8l.mlpackage */,
6381D2162B7817C200ABA4E8 /* yolov8m.mlpackage */,
6381D2172B7817C200ABA4E8 /* yolov8n.mlpackage */,
6381D2152B7817C200ABA4E8 /* yolov8s.mlpackage */,
6381D2142B7817C200ABA4E8 /* yolov8x.mlpackage */,
);
path = Models;
sourceTree = "<group>";
@@ -160,6 +157,7 @@
Base,
);
mainGroup = 7BCB410E21C3096100BFC4D0;
minimizedProjectReferenceProxies = 1;
productRefGroup = 7BCB411821C3096100BFC4D0 /* Products */;
projectDirPath = "";
projectRoot = "";
@@ -210,16 +208,15 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6381D21B2B7817C200ABA4E8 /* yolov8m.mlpackage in Sources */,
6381D21C2B7817C200ABA4E8 /* yolov8n.mlpackage in Sources */,
730E72CD2BFC43BF000E1F45 /* PostProcessing.swift in Sources */,
636EFCAF21E62DD300DE43BC /* VideoCapture.swift in Sources */,
636166EA251443B20054FA7E /* ThresholdProvider.swift in Sources */,
6381D2182B7817C200ABA4E8 /* yolov8l.mlpackage in Sources */,
6381D21A2B7817C200ABA4E8 /* yolov8s.mlpackage in Sources */,
6381D2192B7817C200ABA4E8 /* yolov8x.mlpackage in Sources */,
636EFCB321E62DD300DE43BC /* AppDelegate.swift in Sources */,
73A4E7772C0EA37300218E8F /* TrackingModel.swift in Sources */,
636EFCAA21E62DD300DE43BC /* ViewController.swift in Sources */,
8EDAA33950796844333D60A7 /* BoundingBoxView.swift in Sources */,
73A4E7752C0EA36D00218E8F /* HumanModel.swift in Sources */,
73FE95772C2B5A2D00C6C806 /* SaveResults.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -350,8 +347,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 0;
DEVELOPMENT_TEAM = 3MR4P6CL3X;
CURRENT_PROJECT_VERSION = 2.2;
DEVELOPMENT_TEAM = MFN25KNUGJ;
INFOPLIST_FILE = YOLO/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Ultralytics YOLO";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
@@ -378,8 +375,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 0;
DEVELOPMENT_TEAM = 3MR4P6CL3X;
CURRENT_PROJECT_VERSION = 2.2;
DEVELOPMENT_TEAM = MFN25KNUGJ;
INFOPLIST_FILE = YOLO/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Ultralytics YOLO";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
@@ -389,7 +386,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 8.2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.ultralytics.iDetection;
PRODUCT_BUNDLE_IDENTIFIER = com.YoloiOSApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
78 changes: 78 additions & 0 deletions YOLO.xcodeproj/xcshareddata/xcschemes/YOLO.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7BCB411621C3096100BFC4D0"
BuildableName = "YOLO.app"
BlueprintName = "YOLO"
ReferencedContainer = "container:YOLO.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7BCB411621C3096100BFC4D0"
BuildableName = "YOLO.app"
BlueprintName = "YOLO"
ReferencedContainer = "container:YOLO.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7BCB411621C3096100BFC4D0"
BuildableName = "YOLO.app"
BlueprintName = "YOLO"
ReferencedContainer = "container:YOLO.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
78 changes: 45 additions & 33 deletions YOLO/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -17,49 +17,61 @@ import UIKit
/// The main application delegate, handling global app behavior and configuration.
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var window: UIWindow?

/// Called when the app finishes launching, used here to set global app settings.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Disable screen dimming and auto-lock to keep the app active during long operations.
UIApplication.shared.isIdleTimerDisabled = true
/// Called when the app finishes launching, used here to set global app settings.
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Disable screen dimming and auto-lock to keep the app active during long operations.
UIApplication.shared.isIdleTimerDisabled = true

// Enable battery monitoring to allow the app to adapt its behavior based on battery level.
UIDevice.current.isBatteryMonitoringEnabled = true
// Enable battery monitoring to allow the app to adapt its behavior based on battery level.
UIDevice.current.isBatteryMonitoringEnabled = true

// Store the app version and build version in UserDefaults for easy access elsewhere in the app.
if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
let buildVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
UserDefaults.standard.set("\(appVersion) (\(buildVersion))", forKey: "app_version")
}
// Store the app version and build version in UserDefaults for easy access elsewhere in the app.
if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
let buildVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
{
UserDefaults.standard.set("\(appVersion) (\(buildVersion))", forKey: "app_version")
}

// Store the device's UUID in UserDefaults for identification purposes.
if let uuid = UIDevice.current.identifierForVendor?.uuidString {
UserDefaults.standard.set(uuid, forKey: "uuid")
}
// Store the device's UUID in UserDefaults for identification purposes.
if let uuid = UIDevice.current.identifierForVendor?.uuidString {
UserDefaults.standard.set(uuid, forKey: "uuid")
}

// Ensure UserDefaults changes are immediately saved.
UserDefaults.standard.synchronize()
// Ensure UserDefaults changes are immediately saved.
UserDefaults.standard.synchronize()

return true
}
return true
}

func applicationDidBecomeActive(_ application: UIApplication) {
NotificationCenter.default.post(name: .settingsChanged, object: nil)
}
}

extension Notification.Name {
static let settingsChanged = Notification.Name("settingsChanged")
}

/// Extension to CALayer to add functionality for generating screenshots of any layer.
extension CALayer {
var screenShot: UIImage? {
// Begin a new image context, using the device's screen scale to ensure high-resolution output.
UIGraphicsBeginImageContextWithOptions(frame.size, false, UIScreen.main.scale)
defer {
UIGraphicsEndImageContext()
} // Ensure the image context is cleaned up correctly.
var screenShot: UIImage? {
// Begin a new image context, using the device's screen scale to ensure high-resolution output.
UIGraphicsBeginImageContextWithOptions(frame.size, false, UIScreen.main.scale)
defer {
UIGraphicsEndImageContext()
} // Ensure the image context is cleaned up correctly.

if let context = UIGraphicsGetCurrentContext() {
// Render the layer into the current context.
render(in: context)
// Attempt to generate an image from the current context.
return UIGraphicsGetImageFromCurrentImageContext()
}
return nil // Return nil if the operation fails.
if let context = UIGraphicsGetCurrentContext() {
// Render the layer into the current context.
render(in: context)
// Attempt to generate an image from the current context.
return UIGraphicsGetImageFromCurrentImageContext()
}
return nil // Return nil if the operation fails.
}
}
8 changes: 5 additions & 3 deletions YOLO/Assets.xcassets/Focus.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
{
"images": [
{
"filename": "ultralytics_square_focus_image.png",
"idiom": "universal",
"scale": "1x"
},
{
"filename": "ultralytics_square_focus_image 1.png",
"idiom": "universal",
"scale": "2x"
},
{
"filename": "ultralytics_square_focus_image 2.png",
"idiom": "universal",
"filename": "ultralytics_square_focus_image.png",
"scale": "3x"
}
],
"info": {
"version": 1,
"author": "xcode"
"author": "xcode",
"version": 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion YOLO/Info.plist
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>24</string>
<string>386</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
@@ -52,6 +52,8 @@
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
4 changes: 2 additions & 2 deletions YOLO/LaunchScreen.storyboard
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina5_9" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
229 changes: 182 additions & 47 deletions YOLO/Main.storyboard

Large diffs are not rendered by default.

133 changes: 81 additions & 52 deletions YOLO/Utilities/BoundingBoxView.swift
Original file line number Diff line number Diff line change
@@ -14,66 +14,95 @@ import UIKit

/// Manages the visualization of bounding boxes and associated labels for object detection results.
class BoundingBoxView {
/// The layer that draws the bounding box around a detected object.
let shapeLayer: CAShapeLayer
/// The layer that draws the bounding box around a detected object.
let shapeLayer: CAShapeLayer

/// The layer that displays the label and confidence score for the detected object.
let textLayer: CATextLayer
/// The layer that displays the label and confidence score for the detected object.
let textLayer: CATextLayer

/// Initializes a new BoundingBoxView with configured shape and text layers.
init() {
shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor // No fill to only show the bounding outline
shapeLayer.lineWidth = 4 // Set the stroke line width
shapeLayer.isHidden = true // Initially hidden; shown when a detection occurs
/// The layer that displays the inner text within the bounding box.
let innerTextLayer: CATextLayer

textLayer = CATextLayer()
textLayer.isHidden = true // Initially hidden; shown with label when a detection occurs
textLayer.contentsScale = UIScreen.main.scale // Ensure the text is sharp on retina displays
textLayer.fontSize = 14 // Set font size for the label text
textLayer.font = UIFont(name: "Avenir", size: textLayer.fontSize) // Use Avenir font for labels
textLayer.alignmentMode = .center // Center-align the text within the layer
}
/// Initializes a new BoundingBoxView with configured shape and text layers.
init() {
shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor // No fill to only show the bounding outline
shapeLayer.lineWidth = 4 // Set the stroke line width
shapeLayer.isHidden = true // Initially hidden; shown when a detection occurs

/// Adds the bounding box and text layers to a specified parent layer.
/// - Parameter parent: The CALayer to which the bounding box and text layers will be added.
func addToLayer(_ parent: CALayer) {
parent.addSublayer(shapeLayer)
parent.addSublayer(textLayer)
}
textLayer = CATextLayer()
textLayer.isHidden = true // Initially hidden; shown with label when a detection occurs
textLayer.contentsScale = UIScreen.main.scale // Ensure the text is sharp on retina displays
textLayer.fontSize = 14 // Set font size for the label text
textLayer.font = UIFont(name: "Avenir", size: textLayer.fontSize) // Use Avenir font for labels
textLayer.alignmentMode = .center // Center-align the text within the layer

/// Updates the bounding box and label to be visible with specified properties.
/// - Parameters:
/// - frame: The CGRect frame defining the bounding box's size and position.
/// - label: The text label to display (e.g., object class and confidence).
/// - color: The color of the bounding box stroke and label background.
/// - alpha: The opacity level for the bounding box stroke and label background.
func show(frame: CGRect, label: String, color: UIColor, alpha: CGFloat) {
CATransaction.setDisableActions(true) // Disable implicit animations
innerTextLayer = CATextLayer()
innerTextLayer.isHidden = true // Initially hidden; shown with label when a detection occurs
innerTextLayer.contentsScale = UIScreen.main.scale // Ensure the text is sharp on retina displays
innerTextLayer.fontSize = 12 // Set font size for the inner text
innerTextLayer.font = UIFont(name: "Avenir", size: innerTextLayer.fontSize) // Use Avenir font for inner text
innerTextLayer.alignmentMode = .left // Left-align the text within the layer
innerTextLayer.isWrapped = true // Wrap the text to fit within the layer
}

let path = UIBezierPath(roundedRect: frame, cornerRadius: 6.0) // Rounded rectangle for the bounding box
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = color.withAlphaComponent(alpha).cgColor // Apply color and alpha to the stroke
shapeLayer.isHidden = false // Make the shape layer visible
/// Adds the bounding box, text, and inner text layers to a specified parent layer.
/// - Parameter parent: The CALayer to which the bounding box, text, and inner text layers will be added.
func addToLayer(_ parent: CALayer) {
parent.addSublayer(shapeLayer)
parent.addSublayer(textLayer)
parent.addSublayer(innerTextLayer)
}

textLayer.string = label // Set the label text
textLayer.backgroundColor = color.withAlphaComponent(alpha).cgColor // Apply color and alpha to the background
textLayer.isHidden = false // Make the text layer visible
textLayer.foregroundColor = UIColor.white.withAlphaComponent(alpha).cgColor // Set text color
/// Updates the bounding box, label, and inner text to be visible with specified properties.
/// - Parameters:
/// - frame: The CGRect frame defining the bounding box's size and position.
/// - label: The text label to display (e.g., object class and confidence).
/// - color: The color of the bounding box stroke and label background.
/// - alpha: The opacity level for the bounding box stroke and label background.
/// - innerTexts: The text to display inside the bounding box.
func show(frame: CGRect, label: String, color: UIColor, alpha: CGFloat, innerTexts: String) {
CATransaction.setDisableActions(true) // Disable implicit animations

// Calculate the text size and position based on the label content
let attributes = [NSAttributedString.Key.font: textLayer.font as Any]
let textRect = label.boundingRect(with: CGSize(width: 400, height: 100),
options: .truncatesLastVisibleLine,
attributes: attributes, context: nil)
let textSize = CGSize(width: textRect.width + 12, height: textRect.height) // Add padding to the text size
let textOrigin = CGPoint(x: frame.origin.x - 2, y: frame.origin.y - textSize.height - 2) // Position above the bounding box
textLayer.frame = CGRect(origin: textOrigin, size: textSize) // Set the text layer frame
}
let path = UIBezierPath(roundedRect: frame, cornerRadius: 6.0) // Rounded rectangle for the bounding box
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = color.withAlphaComponent(alpha).cgColor // Apply color and alpha to the stroke
shapeLayer.isHidden = false // Make the shape layer visible

textLayer.string = label // Set the label text
textLayer.backgroundColor = color.withAlphaComponent(alpha).cgColor // Apply color and alpha to the background
textLayer.isHidden = false // Make the text layer visible
textLayer.foregroundColor = UIColor.white.withAlphaComponent(alpha).cgColor // Set text color

/// Hides the bounding box and text layers.
func hide() {
shapeLayer.isHidden = true
textLayer.isHidden = true
// Calculate the text size and position based on the label content
let attributes = [NSAttributedString.Key.font: textLayer.font as Any]
let textRect = label.boundingRect(
with: CGSize(width: 400, height: 100),
options: .truncatesLastVisibleLine,
attributes: attributes, context: nil)
let textSize = CGSize(width: textRect.width + 12, height: textRect.height) // Add padding to the text size
let textOrigin = CGPoint(x: frame.origin.x - 2, y: frame.origin.y - textSize.height - 2) // Position above the bounding box
textLayer.frame = CGRect(origin: textOrigin, size: textSize) // Set the text layer frame

if !innerTexts.isEmpty {
innerTextLayer.string = innerTexts // Set the inner text
innerTextLayer.backgroundColor = UIColor.clear.cgColor // No background color
innerTextLayer.isHidden = false // Make the inner text layer visible
innerTextLayer.foregroundColor = UIColor.red.cgColor // Set text color
innerTextLayer.frame = CGRect(
x: frame.origin.x + 4, y: frame.origin.y + 4, width: frame.width / 2 - 8,
height: frame.height - 8)
// Set the inner text layer frame
} else {
innerTextLayer.isHidden = true // Hide the inner text layer if innerTexts is empty
}

}

/// Hides the bounding box, text, and inner text layers.
func hide() {
shapeLayer.isHidden = true
textLayer.isHidden = true
innerTextLayer.isHidden = true
}
}
150 changes: 150 additions & 0 deletions YOLO/Utilities/HumanModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Ultralytics YOLO 🚀 - AGPL-3.0 License
//
// HumanModel for Ultralytics YOLO App
// This struct is designed to turn the inference results of the YOLOv8-Human model into a manageable DataModel of human feature values ​​in the Ultralytics YOLO app. When in tracking mode, this struct averages the feature values ​​of a given individual across frames to a stable value.
// This struct automatically analyzes the boxes, scores, and feature values ​​provided to the update function to create a human model.// Licensed under AGPL-3.0. For commercial use, refer to Ultralytics licensing: https://ultralytics.com/license
// Access the source code: https://github.com/ultralytics/yolo-ios-app

import Foundation
import UIKit

let updateFrequency: Int = 120

struct Person {
var index: Int
var box: CGRect = .zero

var score: Float = 0
var weight: Float = 0
var height: Float = 0

var age: Int = 0

var gender: String = "female"
var genderConfidence: Float = 0
var race: String = "asian"
var raceConfidence: Float = 0

var listCount: Int = 0
var scoreRawList: [Float] = []
var weightRawList: [Float] = []
var heightRawList: [Float] = []
var ageRawList: [Float] = []
var maleRawList: [Float] = []
var femaleRawList: [Float] = []
var asianRawList: [Float] = []
var whiteRawList: [Float] = []
var middleEasternRawList: [Float] = []
var indianRawList: [Float] = []
var latinoRawList: [Float] = []
var blackRawList: [Float] = []

var trackedBox: CGRect?
var color: UIColor

var unDetectedCounter: Int = 0
var stable = false

init(index: Int) {
self.index = index
self.color = UIColor(
red: CGFloat.random(in: 0...1),
green: CGFloat.random(in: 0...1),
blue: CGFloat.random(in: 0...1),
alpha: 0.6)
}

mutating func update(box: CGRect, score: Float, features: [Float]) {
self.box = box
if scoreRawList.count >= updateFrequency {
scoreRawList.removeFirst()
weightRawList.removeFirst()
heightRawList.removeFirst()
ageRawList.removeFirst()
maleRawList.removeFirst()
femaleRawList.removeFirst()
asianRawList.removeFirst()
whiteRawList.removeFirst()
middleEasternRawList.removeFirst()
indianRawList.removeFirst()
latinoRawList.removeFirst()
blackRawList.removeFirst()
}

self.scoreRawList.append(score)
self.weightRawList.append(features[0])
self.heightRawList.append(features[1])
self.ageRawList.append(features[2])
self.femaleRawList.append(features[3])
self.maleRawList.append(features[4])
self.asianRawList.append(features[5])
self.whiteRawList.append(features[6])
self.middleEasternRawList.append(features[7])
self.indianRawList.append(features[8])
self.latinoRawList.append(features[9])
self.blackRawList.append(features[10])
calcurateFeatures()

self.unDetectedCounter = 0
}

private mutating func calcurateFeatures() {

self.score = average(of: scoreRawList)
self.weight = average(of: weightRawList)
self.height = average(of: heightRawList)
self.age = Int(round(average(of: ageRawList)))
let femaleAverage = average(of: femaleRawList)
let maleAverage = average(of: maleRawList)
let genderCandidates = [femaleAverage, maleAverage]
var genderMaxIndex = 0
var genderMaxValue = genderCandidates[0]

for (genderIndex, genderValue) in genderCandidates.dropFirst().enumerated() {
if genderValue > genderMaxValue {
genderMaxValue = genderValue
genderMaxIndex = genderIndex + 1
}
}

self.gender = genders[genderMaxIndex]
self.genderConfidence = genderMaxValue

let asianAverage = average(of: asianRawList)
let whiteAverage = average(of: whiteRawList)
let middleEasternAverage = average(of: middleEasternRawList)
let indianAverage = average(of: indianRawList)
let latinoAverage = average(of: latinoRawList)
let blackAverage = average(of: blackRawList)

let raceCandidates = [
asianAverage, whiteAverage, middleEasternAverage, indianAverage, latinoAverage, blackAverage,
]
var raceMaxIndex = 0
var raceMaxValue = raceCandidates[0]

for (raceIndex, raceValue) in raceCandidates.dropFirst().enumerated() {
if raceValue > raceMaxValue {
raceMaxValue = raceValue
raceMaxIndex = raceIndex + 1
}
}
self.race = races[raceMaxIndex]
self.raceConfidence = raceMaxValue
}

func average(of numbers: [Float]) -> Float {
guard !numbers.isEmpty else {
return 0
}
var sum: Float = 0
for number in numbers {
sum += number
}
return sum / Float(numbers.count)
}

}

let genders = ["female", "male"]
let races = ["asian", "white", "middle eastern", "indian", "latino", "black"]
103 changes: 103 additions & 0 deletions YOLO/Utilities/PostProcessing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Ultralytics YOLO 🚀 - AGPL-3.0 License
//
// PostProcessing for Ultralytics YOLO App
// This feature is designed to post-process the output of a YOLOv8 model within the Ultralytics YOLO app to extract high-confidence objects.
// Output high confidence boxes and their corresponding feature values using Non max suppression.
// Licensed under AGPL-3.0. For commercial use, refer to Ultralytics licensing: https://ultralytics.com/license
// Access the source code: https://github.com/ultralytics/yolo-ios-app

import CoreML
import Foundation
import Vision

func nonMaxSuppression(boxes: [CGRect], scores: [Float], threshold: Float) -> [Int] {
let sortedIndices = scores.enumerated().sorted { $0.element > $1.element }.map { $0.offset }
var selectedIndices = [Int]()
var activeIndices = [Bool](repeating: true, count: boxes.count)

for i in 0..<sortedIndices.count {
let idx = sortedIndices[i]
if activeIndices[idx] {
selectedIndices.append(idx)
for j in i + 1..<sortedIndices.count {
let otherIdx = sortedIndices[j]
if activeIndices[otherIdx] {
let intersection = boxes[idx].intersection(boxes[otherIdx])
if intersection.area > CGFloat(threshold) * min(boxes[idx].area, boxes[otherIdx].area) {
activeIndices[otherIdx] = false
}
}
}
}
}
return selectedIndices
}

// Human model's output [1,15,8400] to [(Box, Confidence, HumanFeatures)]

func PostProcessHuman(prediction: MLMultiArray, confidenceThreshold: Float, iouThreshold: Float)
-> [(CGRect, Float, [Float])]
{
let numAnchors = prediction.shape[2].intValue
var boxes = [CGRect]()
var scores = [Float]()
var features = [[Float]]()
let featurePointer = UnsafeMutablePointer<Float>(OpaquePointer(prediction.dataPointer))
let lock = DispatchQueue(label: "com.example.lock")

DispatchQueue.concurrentPerform(iterations: numAnchors) { j in
let confIndex = 4 * numAnchors + j
let confidence = featurePointer[confIndex]
if confidence > confidenceThreshold {
let x = featurePointer[j]
let y = featurePointer[numAnchors + j]
let width = featurePointer[2 * numAnchors + j]
let height = featurePointer[3 * numAnchors + j]

let boxWidth = CGFloat(width)
let boxHeight = CGFloat(height)
let boxX = CGFloat(x - width / 2)
let boxY = CGFloat(y - height / 2)

let boundingBox = CGRect(x: boxX, y: boxY, width: boxWidth, height: boxHeight)

var boxFeatures = [Float](repeating: 0, count: 11)
for k in 0..<11 {
let key = (5 + k) * numAnchors + j
boxFeatures[k] = featurePointer[key]
}

lock.sync {
boxes.append(boundingBox)
scores.append(confidence)
features.append(boxFeatures)
}
}
}

let selectedIndices = nonMaxSuppression(boxes: boxes, scores: scores, threshold: iouThreshold)
var selectedBoxesAndFeatures = [(CGRect, Float, [Float])]()

for idx in selectedIndices {
selectedBoxesAndFeatures.append((boxes[idx], scores[idx], features[idx]))
}
print(selectedBoxesAndFeatures)
return selectedBoxesAndFeatures
}

func toPerson(boxesAndScoresAndFeatures: [(CGRect, Float, [Float])]) -> [Person] {
var persons = [Person]()
for detectedHuman in boxesAndScoresAndFeatures {
var person = Person(index: -1)
person.update(box: detectedHuman.0, score: detectedHuman.1, features: detectedHuman.2)
person.color = .red
persons.append(person)
}
return persons
}

extension CGRect {
var area: CGFloat {
return width * height
}
}
50 changes: 50 additions & 0 deletions YOLO/Utilities/SaveResults.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// SaveResults.swift
// YOLO
//
// Created by 間嶋大輔 on 2024/06/26.
// Copyright © 2024 Ultralytics. All rights reserved.
//

import Foundation

let detectionHeader =
"sec_day, free_space, batteryLevel ,class,confidence,box_x, box_y, box_w, box_h\n"
let humanHeader =
"sec_day, free_space, battery_level ,id, confidence, box_x, box_y, box_w, box_h, weight, height, age, gender, gender_confidence, race, race_confidence \n"

func saveDetectionResultsToCSV(detectionResults: [String], task: Task) -> URL? {
var header = ""
var taskName = ""
switch task {
case .detect:
header = detectionHeader
taskName = "detection"

case .human:
header = humanHeader
taskName = "human"
}
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMdd_HH:mm:ss"
let dateString = formatter.string(from: Date())
let fileName = taskName + "_results_\(dateString).csv"

let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent(fileName)

var csvText = header

for result in detectionResults {
csvText.append(contentsOf: result)
}

do {
try csvText.write(to: path, atomically: true, encoding: .utf8)
print("CSV file saved at: \(path)")
return path
} catch {
print("Failed to save CSV file: \(error)")
return nil
}
}
44 changes: 22 additions & 22 deletions YOLO/Utilities/ThresholdProvider.swift
Original file line number Diff line number Diff line change
@@ -14,29 +14,29 @@ import CoreML

/// Provides custom IoU and confidence thresholds for adjusting model predictions.
class ThresholdProvider: MLFeatureProvider {
/// Stores IoU and confidence thresholds as MLFeatureValue objects.
var values: [String: MLFeatureValue]
/// Stores IoU and confidence thresholds as MLFeatureValue objects.
var values: [String: MLFeatureValue]

/// The set of feature names provided by this provider.
var featureNames: Set<String> {
return Set(values.keys)
}
/// The set of feature names provided by this provider.
var featureNames: Set<String> {
return Set(values.keys)
}

/// Initializes the provider with specified IoU and confidence thresholds.
/// - Parameters:
/// - iouThreshold: The IoU threshold for determining object overlap.
/// - confidenceThreshold: The minimum confidence for considering a detection valid.
init(iouThreshold: Double = 0.45, confidenceThreshold: Double = 0.25) {
values = [
"iouThreshold": MLFeatureValue(double: iouThreshold),
"confidenceThreshold": MLFeatureValue(double: confidenceThreshold)
]
}
/// Initializes the provider with specified IoU and confidence thresholds.
/// - Parameters:
/// - iouThreshold: The IoU threshold for determining object overlap.
/// - confidenceThreshold: The minimum confidence for considering a detection valid.
init(iouThreshold: Double = 0.45, confidenceThreshold: Double = 0.25) {
values = [
"iouThreshold": MLFeatureValue(double: iouThreshold),
"confidenceThreshold": MLFeatureValue(double: confidenceThreshold),
]
}

/// Returns the feature value for the given feature name.
/// - Parameter featureName: The name of the feature.
/// - Returns: The MLFeatureValue object corresponding to the feature name.
func featureValue(for featureName: String) -> MLFeatureValue? {
return values[featureName]
}
/// Returns the feature value for the given feature name.
/// - Parameter featureName: The name of the feature.
/// - Returns: The MLFeatureValue object corresponding to the feature name.
func featureValue(for featureName: String) -> MLFeatureValue? {
return values[featureName]
}
}
128 changes: 128 additions & 0 deletions YOLO/Utilities/TrackingModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Ultralytics YOLO 🚀 - AGPL-3.0 License
//
// HumanModel for Ultralytics YOLO App

// This class is designed to track and identify the same person across frames using the inference results of the YOLOv8-Human model in the Ultralytics YOLO app.
// The tack function is a simple tracking algorithm that tracks boxes of the same person based on box overlap across frames.
// Access the source code: https://github.com/ultralytics/yolo-ios-app

import Accelerate
import Foundation
import Vision

class TrackingModel {
var persons = [Person]()
var personIndex: Int = 0
var recent: [(CGRect, Float, [Float])] = []

func track(boxesAndScoresAndFeatures: [(CGRect, Float, [Float])]) -> [Person] {

if persons.isEmpty {
for detectedHuman in boxesAndScoresAndFeatures {
var person = Person(index: personIndex)
person.update(box: detectedHuman.0, score: detectedHuman.1, features: detectedHuman.2)
personIndex += 1
persons.append(person)

}
return persons
}

var unDetectedPersonIndexes: [Int] = []
var usedDetectedIndex: Set<Int> = Set()

for (pi, person) in persons.enumerated() {
var bestIOU: CGFloat = 0
var bestIndex = 0

for (i, detected) in boxesAndScoresAndFeatures.enumerated() {
let IoU = overlapPercentage(rect1: person.box, rect2: detected.0)
if IoU > bestIOU {
bestIOU = IoU
bestIndex = i
}
}
if bestIOU >= 50 {
let detectedPerson = boxesAndScoresAndFeatures[bestIndex]
persons[pi].update(
box: detectedPerson.0, score: detectedPerson.1, features: detectedPerson.2)
usedDetectedIndex.insert(bestIndex)
} else {
unDetectedPersonIndexes.append(pi)
}
}

let sortedIndices = unDetectedPersonIndexes.sorted(by: >)
for index in sortedIndices {
persons[index].unDetectedCounter += 1
}

for (index, det) in boxesAndScoresAndFeatures.enumerated() {
if !usedDetectedIndex.contains(index) {
var person = Person(index: personIndex)
person.update(box: det.0, score: det.1, features: det.2)
personIndex += 1
persons.append(person)
}
}

persons = removeOverlappingRects(persons: persons)

var personsToShow: [Person] = []
var removePersonIndexes: [Int] = []
for (pindex, person) in persons.enumerated() {
if person.unDetectedCounter == 0 {
personsToShow.append(person)
} else if person.unDetectedCounter >= 15 {
removePersonIndexes.append(pindex)
}
}
let sortedRemoveIndices = removePersonIndexes.sorted(by: >)
for index in sortedRemoveIndices {
persons.remove(at: index)
}

return personsToShow

}
}

func overlapPercentage(rect1: CGRect, rect2: CGRect) -> CGFloat {
let intersection = rect1.intersection(rect2)

if intersection.isNull {
return 0.0
}

let intersectionArea = intersection.width * intersection.height

let rect1Area = rect1.width * rect1.height

let overlapPercentage = (intersectionArea / rect1Area) * 100

return overlapPercentage
}

func removeOverlappingRects(persons: [Person], threshold: CGFloat = 90.0) -> [Person] {
var filteredPersons = persons
var index = 0

while index < filteredPersons.count {
var shouldRemove = false
for j in (index + 1)..<filteredPersons.count {
let percentage = overlapPercentage(
rect1: filteredPersons[index].box, rect2: filteredPersons[j].box)
if percentage >= threshold {
shouldRemove = true
break
}
}
if shouldRemove {
filteredPersons.remove(at: index)
} else {
index += 1
}
}

return filteredPersons
}
232 changes: 137 additions & 95 deletions YOLO/VideoCapture.swift
Original file line number Diff line number Diff line change
@@ -11,123 +11,165 @@
// the capture session. It also provides methods to start and stop video capture and delivers captured frames
// to a delegate implementing the VideoCaptureDelegate protocol.


import AVFoundation
import CoreVideo
import UIKit

// Defines the protocol for handling video frame capture events.
public protocol VideoCaptureDelegate: AnyObject {
func videoCapture(_ capture: VideoCapture, didCaptureVideoFrame: CMSampleBuffer)
func videoCapture(_ capture: VideoCapture, didCaptureVideoFrame: CMSampleBuffer)
}

// Identifies the best available camera device based on user preferences and device capabilities.
func bestCaptureDevice() -> AVCaptureDevice {
if UserDefaults.standard.bool(forKey: "use_telephoto"), let device = AVCaptureDevice.default(.builtInTelephotoCamera, for: .video, position: .back) {
return device
} else if let device = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) {
return device
} else if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
return device
} else {
fatalError("Expected back camera device is not available.")
}
if UserDefaults.standard.bool(forKey: "use_telephoto"),
let device = AVCaptureDevice.default(.builtInTelephotoCamera, for: .video, position: .back)
{
return device
} else if let device = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) {
return device
} else if let device = AVCaptureDevice.default(
.builtInWideAngleCamera, for: .video, position: .back)
{
return device
} else {
fatalError("Expected back camera device is not available.")
}
}

public class VideoCapture: NSObject {
public var previewLayer: AVCaptureVideoPreviewLayer?
public weak var delegate: VideoCaptureDelegate?

let captureDevice = bestCaptureDevice()
let captureSession = AVCaptureSession()
let videoOutput = AVCaptureVideoDataOutput()
var cameraOutput = AVCapturePhotoOutput()
let queue = DispatchQueue(label: "camera-queue")

// Configures the camera and capture session with optional session presets.
public func setUp(sessionPreset: AVCaptureSession.Preset = .hd1280x720, completion: @escaping (Bool) -> Void) {
queue.async {
let success = self.setUpCamera(sessionPreset: sessionPreset)
DispatchQueue.main.async {
completion(success)
}
}
public var previewLayer: AVCaptureVideoPreviewLayer?
public weak var delegate: VideoCaptureDelegate?

let captureDevice = bestCaptureDevice()
let captureSession = AVCaptureSession()
let videoOutput = AVCaptureVideoDataOutput()
var cameraOutput = AVCapturePhotoOutput()
let queue = DispatchQueue(label: "camera-queue")

// Configures the camera and capture session with optional session presets.
public func setUp(
sessionPreset: AVCaptureSession.Preset = .hd1280x720, completion: @escaping (Bool) -> Void
) {
queue.async {
let success = self.setUpCamera(sessionPreset: sessionPreset)
DispatchQueue.main.async {
completion(success)
}
}
}

// Internal method to configure camera inputs, outputs, and session properties.
private func setUpCamera(sessionPreset: AVCaptureSession.Preset) -> Bool {
captureSession.beginConfiguration()
captureSession.sessionPreset = sessionPreset

// Internal method to configure camera inputs, outputs, and session properties.
private func setUpCamera(sessionPreset: AVCaptureSession.Preset) -> Bool {
captureSession.beginConfiguration()
captureSession.sessionPreset = sessionPreset

guard let videoInput = try? AVCaptureDeviceInput(device: captureDevice) else {
return false
}

if captureSession.canAddInput(videoInput) {
captureSession.addInput(videoInput)
}

let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.videoGravity = .resizeAspectFill
previewLayer.connection?.videoOrientation = .portrait
self.previewLayer = previewLayer

let settings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_32BGRA)
]

videoOutput.videoSettings = settings
videoOutput.alwaysDiscardsLateVideoFrames = true
videoOutput.setSampleBufferDelegate(self, queue: queue)
if captureSession.canAddOutput(videoOutput) {
captureSession.addOutput(videoOutput)
}

if captureSession.canAddOutput(cameraOutput) {
captureSession.addOutput(cameraOutput)
}

videoOutput.connection(with: .video)?.videoOrientation = .portrait

do {
try captureDevice.lockForConfiguration()
captureDevice.focusMode = .continuousAutoFocus
captureDevice.focusPointOfInterest = CGPoint(x: 0.5, y: 0.5)
captureDevice.exposureMode = .continuousAutoExposure
captureDevice.unlockForConfiguration()
} catch {
print("Unable to configure the capture device.")
return false
}

captureSession.commitConfiguration()
return true
guard let videoInput = try? AVCaptureDeviceInput(device: captureDevice) else {
return false
}

// Starts the video capture session.
public func start() {
if !captureSession.isRunning {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.captureSession.startRunning()
}
}
if captureSession.canAddInput(videoInput) {
captureSession.addInput(videoInput)
}

// Stops the video capture session.
public func stop() {
if captureSession.isRunning {
captureSession.stopRunning()
}
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.videoGravity = .resizeAspectFill
previewLayer.connection?.videoOrientation = .portrait
self.previewLayer = previewLayer

let settings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_32BGRA)
]

videoOutput.videoSettings = settings
videoOutput.alwaysDiscardsLateVideoFrames = true
videoOutput.setSampleBufferDelegate(self, queue: queue)
if captureSession.canAddOutput(videoOutput) {
captureSession.addOutput(videoOutput)
}
}

// Extension to handle AVCaptureVideoDataOutputSampleBufferDelegate events.
extension VideoCapture: AVCaptureVideoDataOutputSampleBufferDelegate {
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
delegate?.videoCapture(self, didCaptureVideoFrame: sampleBuffer)
if captureSession.canAddOutput(cameraOutput) {
captureSession.addOutput(cameraOutput)
}
switch UIDevice.current.orientation {
case .portrait:
videoOutput.connection(with: .video)?.videoOrientation = .portrait
case .portraitUpsideDown:
videoOutput.connection(with: .video)?.videoOrientation = .portraitUpsideDown
case .landscapeRight:
videoOutput.connection(with: .video)?.videoOrientation = .landscapeLeft
case .landscapeLeft:
videoOutput.connection(with: .video)?.videoOrientation = .landscapeRight
default:
videoOutput.connection(with: .video)?.videoOrientation = .portrait
}

public func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
// Optionally handle dropped frames, e.g., due to full buffer.
if let connection = videoOutput.connection(with: .video) {
self.previewLayer?.connection?.videoOrientation = connection.videoOrientation
}
do {
try captureDevice.lockForConfiguration()
captureDevice.focusMode = .continuousAutoFocus
captureDevice.focusPointOfInterest = CGPoint(x: 0.5, y: 0.5)
captureDevice.exposureMode = .continuousAutoExposure
captureDevice.unlockForConfiguration()
} catch {
print("Unable to configure the capture device.")
return false
}

captureSession.commitConfiguration()
return true
}

// Starts the video capture session.
public func start() {
if !captureSession.isRunning {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.captureSession.startRunning()
}
}
}

// Stops the video capture session.
public func stop() {
if captureSession.isRunning {
captureSession.stopRunning()
}
}

func updateVideoOrientation() {
guard let connection = videoOutput.connection(with: .video) else { return }
switch UIDevice.current.orientation {
case .portrait:
connection.videoOrientation = .portrait
case .portraitUpsideDown:
connection.videoOrientation = .portraitUpsideDown
case .landscapeRight:
connection.videoOrientation = .landscapeLeft
case .landscapeLeft:
connection.videoOrientation = .landscapeRight
default:
return
}
self.previewLayer?.connection?.videoOrientation = connection.videoOrientation
}

}

// Extension to handle AVCaptureVideoDataOutputSampleBufferDelegate events.
extension VideoCapture: AVCaptureVideoDataOutputSampleBufferDelegate {
public func captureOutput(
_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
delegate?.videoCapture(self, didCaptureVideoFrame: sampleBuffer)
}

public func captureOutput(
_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
// Optionally handle dropped frames, e.g., due to full buffer.
}
}
1,331 changes: 834 additions & 497 deletions YOLO/ViewController.swift

Large diffs are not rendered by default.