diff --git a/README.md b/README.md index 8ed70ce..46ef444 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Ultralytics Actions](https://github.com/ultralytics/yolo-ios-app/actions/workflows/format.yml/badge.svg)](https://github.com/ultralytics/yolo-ios-app/actions/workflows/format.yml) Discord Ultralytics Forums Ultralytics Reddit -Welcome to the [Ultralytics YOLO iOS App](https://apps.apple.com/us/app/idetection/id1452689527) GitHub repository! 📖 Leveraging Ultralytics' advanced [YOLOv8 object detection models](https://github.com/ultralytics/ultralytics), this app transforms your iOS device into an intelligent detection tool. Explore our guide to get started with the Ultralytics YOLO iOS App and discover the world in a new and exciting way. +Welcome to the [Ultralytics YOLO iOS App](https://apps.apple.com/us/app/idetection/id1452689527) GitHub repository! 📖 Leveraging Ultralytics' advanced [YOLO11 object detection models](https://github.com/ultralytics/ultralytics), this app transforms your iOS device into an intelligent detection tool. Explore our guide to get started with the Ultralytics YOLO iOS App and discover the world in a new and exciting way.
Ultralytics YOLO iOS App previews @@ -60,17 +60,17 @@ Ensure you have the following before you start: In Xcode, go to the project's target settings and choose your Apple Developer account under the "Signing & Capabilities" tab. -3. **Add YOLOv8 Models to the Project:** +3. **Add YOLO11 Models to the Project:** - Export CoreML INT8 models using the `ultralytics` Python package (with `pip install ultralytics`), or download them from our [GitHub release assets](https://github.com/ultralytics/yolo-ios-app/releases). You should have 5 YOLOv8 models in total. Place these in the `YOLO/Models` directory as seen in the Xcode screenshot below. + Export CoreML INT8 models using the `ultralytics` Python package (with `pip install ultralytics`), or download them from our [GitHub release assets](https://github.com/ultralytics/yolo-ios-app/releases). You should have 5 YOLO11 models in total. Place these in the `YOLO/Models` directory as seen in the Xcode screenshot below. ```python from ultralytics import YOLO - # Loop through all YOLOv8 model sizes + # Loop through all YOLO11 model sizes for size in ("n", "s", "m", "l", "x"): - # Load a YOLOv8 PyTorch model - model = YOLO(f"yolov8{size}.pt") + # Load a YOLO11 PyTorch model + model = YOLO(f"yolo11{size}.pt") # Export the PyTorch model to CoreML INT8 format with NMS layers model.export(format="coreml", int8=True, nms=True, imgsz=[640, 384]) @@ -89,7 +89,7 @@ Ensure you have the following before you start: The Ultralytics YOLO iOS App is designed to be intuitive: - **Real-Time Detection:** Launch the app and aim your camera at objects to detect them instantly. -- **Multiple AI Models:** Select from a range of Ultralytics YOLOv8 models, from YOLOv8n 'nano' to YOLOv8x 'x-large'. +- **Multiple AI Models:** Select from a range of Ultralytics YOLO11 models, from YOLO11n 'nano' to YOLO11x 'x-large'. ## 💡 Contribute diff --git a/YOLO.xcodeproj/project.pbxproj b/YOLO.xcodeproj/project.pbxproj index 68916e9..7ee32c7 100644 --- a/YOLO.xcodeproj/project.pbxproj +++ b/YOLO.xcodeproj/project.pbxproj @@ -13,16 +13,17 @@ 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 */; }; + 73E3C7FE2CE3AF1A00E2D85C /* yolo11m.mlpackage in Sources */ = {isa = PBXBuildFile; fileRef = 73E3C7FA2CE3AF1A00E2D85C /* yolo11m.mlpackage */; }; + 73E3C7FF2CE3AF1A00E2D85C /* yolo11l.mlpackage in Sources */ = {isa = PBXBuildFile; fileRef = 73E3C7F92CE3AF1A00E2D85C /* yolo11l.mlpackage */; }; + 73E3C8002CE3AF1A00E2D85C /* yolo11x.mlpackage in Sources */ = {isa = PBXBuildFile; fileRef = 73E3C7FD2CE3AF1A00E2D85C /* yolo11x.mlpackage */; }; + 73E3C8012CE3AF1A00E2D85C /* yolo11s.mlpackage in Sources */ = {isa = PBXBuildFile; fileRef = 73E3C7FC2CE3AF1A00E2D85C /* yolo11s.mlpackage */; }; + 73E3C8022CE3AF1A00E2D85C /* yolo11n.mlpackage in Sources */ = {isa = PBXBuildFile; fileRef = 73E3C7FB2CE3AF1A00E2D85C /* yolo11n.mlpackage */; }; 8EDAA33950796844333D60A7 /* BoundingBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDAA633C1F2B50286D16008 /* BoundingBoxView.swift */; }; /* End PBXBuildFile section */ + /* Begin PBXFileReference section */ 6323C44D22186177008AE681 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 6323C44F22186177008AE681 /* Main.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; @@ -34,12 +35,12 @@ 636EFCA221E62DD300DE43BC /* VideoCapture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCapture.swift; sourceTree = ""; }; 636EFCA721E62DD300DE43BC /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 636EFCB821E62E3900DE43BC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 6381D2132B7817C200ABA4E8 /* yolov8l.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolov8l.mlpackage; sourceTree = ""; }; - 6381D2142B7817C200ABA4E8 /* yolov8x.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolov8x.mlpackage; sourceTree = ""; }; - 6381D2152B7817C200ABA4E8 /* yolov8s.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolov8s.mlpackage; sourceTree = ""; }; - 6381D2162B7817C200ABA4E8 /* yolov8m.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolov8m.mlpackage; sourceTree = ""; }; - 6381D2172B7817C200ABA4E8 /* yolov8n.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolov8n.mlpackage; sourceTree = ""; }; 63B8B0A821E62A890026FBC3 /* .gitignore */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; + 73E3C7F92CE3AF1A00E2D85C /* yolo11l.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolo11l.mlpackage; sourceTree = ""; }; + 73E3C7FA2CE3AF1A00E2D85C /* yolo11m.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolo11m.mlpackage; sourceTree = ""; }; + 73E3C7FB2CE3AF1A00E2D85C /* yolo11n.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolo11n.mlpackage; sourceTree = ""; }; + 73E3C7FC2CE3AF1A00E2D85C /* yolo11s.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolo11s.mlpackage; sourceTree = ""; }; + 73E3C7FD2CE3AF1A00E2D85C /* yolo11x.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = yolo11x.mlpackage; sourceTree = ""; }; 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 = ""; }; 8EDAAA4507D2D23D7FAB827F /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -86,11 +87,11 @@ 63A946D8271800E20001C3ED /* Models */ = { isa = PBXGroup; children = ( - 6381D2132B7817C200ABA4E8 /* yolov8l.mlpackage */, - 6381D2162B7817C200ABA4E8 /* yolov8m.mlpackage */, - 6381D2172B7817C200ABA4E8 /* yolov8n.mlpackage */, - 6381D2152B7817C200ABA4E8 /* yolov8s.mlpackage */, - 6381D2142B7817C200ABA4E8 /* yolov8x.mlpackage */, + 73E3C7F92CE3AF1A00E2D85C /* yolo11l.mlpackage */, + 73E3C7FA2CE3AF1A00E2D85C /* yolo11m.mlpackage */, + 73E3C7FB2CE3AF1A00E2D85C /* yolo11n.mlpackage */, + 73E3C7FC2CE3AF1A00E2D85C /* yolo11s.mlpackage */, + 73E3C7FD2CE3AF1A00E2D85C /* yolo11x.mlpackage */, ); path = Models; sourceTree = ""; @@ -209,14 +210,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6381D21B2B7817C200ABA4E8 /* yolov8m.mlpackage in Sources */, - 6381D21C2B7817C200ABA4E8 /* yolov8n.mlpackage 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 */, + 73E3C7FE2CE3AF1A00E2D85C /* yolo11m.mlpackage in Sources */, + 73E3C7FF2CE3AF1A00E2D85C /* yolo11l.mlpackage in Sources */, + 73E3C8002CE3AF1A00E2D85C /* yolo11x.mlpackage in Sources */, + 73E3C8012CE3AF1A00E2D85C /* yolo11s.mlpackage in Sources */, + 73E3C8022CE3AF1A00E2D85C /* yolo11n.mlpackage in Sources */, 636EFCAA21E62DD300DE43BC /* ViewController.swift in Sources */, 8EDAA33950796844333D60A7 /* BoundingBoxView.swift in Sources */, ); @@ -354,12 +355,12 @@ INFOPLIST_FILE = YOLO/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Ultralytics YOLO"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 8.3.0; PRODUCT_BUNDLE_IDENTIFIER = com.ultralytics.iDetection; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -382,12 +383,12 @@ INFOPLIST_FILE = YOLO/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Ultralytics YOLO"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 8.3.0; PRODUCT_BUNDLE_IDENTIFIER = com.ultralytics.iDetection; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/YOLO/Info.plist b/YOLO/Info.plist index a7022ec..e41ead4 100644 --- a/YOLO/Info.plist +++ b/YOLO/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 25 + 55 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/YOLO/Main.storyboard b/YOLO/Main.storyboard index 549bc72..b558fc8 100644 --- a/YOLO/Main.storyboard +++ b/YOLO/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -41,11 +41,11 @@ - - - - - + + + + + @@ -139,7 +139,7 @@ - + @@ -331,10 +331,10 @@ - + - + diff --git a/YOLO/ViewController.swift b/YOLO/ViewController.swift index 840d91d..25f352d 100644 --- a/YOLO/ViewController.swift +++ b/YOLO/ViewController.swift @@ -1,7 +1,7 @@ // Ultralytics YOLO 🚀 - AGPL-3.0 License // // Main View Controller for Ultralytics YOLO App -// This file is part of the Ultralytics YOLO app, enabling real-time object detection using YOLOv8 models on iOS devices. +// This file is part of the Ultralytics YOLO app, enabling real-time object detection using YOLO11 models on iOS devices. // 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 // @@ -17,7 +17,7 @@ import CoreMedia import UIKit import Vision -var mlModel = try! yolov8m(configuration: .init()).model +var mlModel = try! yolo11m(configuration: .init()).model class ViewController: UIViewController { @IBOutlet var videoPreview: UIView! @@ -55,6 +55,9 @@ class ViewController: UIViewController { var t3 = CACurrentMediaTime() // FPS start var t4 = 0.0 // FPS dt smoothed // var cameraOutput: AVCapturePhotoOutput! + var longSide: CGFloat = 3 + var shortSide: CGFloat = 4 + var frameSizeCaptured = false // Developer mode let developerMode = UserDefaults.standard.bool(forKey: "developer_mode") // developer mode selected in settings @@ -127,6 +130,7 @@ class ViewController: UIViewController { @objc func orientationDidChange() { videoCapture.updateVideoOrientation() + // frameSizeCaptured = false } @IBAction func vibrate(_ sender: Any) { @@ -140,20 +144,20 @@ class ViewController: UIViewController { /// Switch model switch segmentedControl.selectedSegmentIndex { case 0: - self.labelName.text = "YOLOv8n" - mlModel = try! yolov8n(configuration: .init()).model + self.labelName.text = "YOLO11n" + mlModel = try! yolo11n(configuration: .init()).model case 1: - self.labelName.text = "YOLOv8s" - mlModel = try! yolov8s(configuration: .init()).model + self.labelName.text = "YOLO11s" + mlModel = try! yolo11s(configuration: .init()).model case 2: - self.labelName.text = "YOLOv8m" - mlModel = try! yolov8m(configuration: .init()).model + self.labelName.text = "YOLO11m" + mlModel = try! yolo11m(configuration: .init()).model case 3: - self.labelName.text = "YOLOv8l" - mlModel = try! yolov8l(configuration: .init()).model + self.labelName.text = "YOLO11l" + mlModel = try! yolo11l(configuration: .init()).model case 4: - self.labelName.text = "YOLOv8x" - mlModel = try! yolov8x(configuration: .init()).model + self.labelName.text = "YOLO11x" + mlModel = try! yolo11x(configuration: .init()).model default: break } @@ -222,7 +226,7 @@ class ViewController: UIViewController { } func setLabels() { - self.labelName.text = "YOLOv8m" + self.labelName.text = "YOLO11m" self.labelVersion.text = "Version " + UserDefaults.standard.string(forKey: "app_version")! } @@ -279,6 +283,28 @@ class ViewController: UIViewController { let maxBoundingBoxViews = 100 var boundingBoxViews = [BoundingBoxView]() var colors: [String: UIColor] = [:] + let ultralyticsColorsolors: [UIColor] = [ + UIColor(red: 4 / 255, green: 42 / 255, blue: 255 / 255, alpha: 0.6), // #042AFF + UIColor(red: 11 / 255, green: 219 / 255, blue: 235 / 255, alpha: 0.6), // #0BDBEB + UIColor(red: 243 / 255, green: 243 / 255, blue: 243 / 255, alpha: 0.6), // #F3F3F3 + UIColor(red: 0 / 255, green: 223 / 255, blue: 183 / 255, alpha: 0.6), // #00DFB7 + UIColor(red: 17 / 255, green: 31 / 255, blue: 104 / 255, alpha: 0.6), // #111F68 + UIColor(red: 255 / 255, green: 111 / 255, blue: 221 / 255, alpha: 0.6), // #FF6FDD + UIColor(red: 255 / 255, green: 68 / 255, blue: 79 / 255, alpha: 0.6), // #FF444F + UIColor(red: 204 / 255, green: 237 / 255, blue: 0 / 255, alpha: 0.6), // #CCED00 + UIColor(red: 0 / 255, green: 243 / 255, blue: 68 / 255, alpha: 0.6), // #00F344 + UIColor(red: 189 / 255, green: 0 / 255, blue: 255 / 255, alpha: 0.6), // #BD00FF + UIColor(red: 0 / 255, green: 180 / 255, blue: 255 / 255, alpha: 0.6), // #00B4FF + UIColor(red: 221 / 255, green: 0 / 255, blue: 186 / 255, alpha: 0.6), // #DD00BA + UIColor(red: 0 / 255, green: 255 / 255, blue: 255 / 255, alpha: 0.6), // #00FFFF + UIColor(red: 38 / 255, green: 192 / 255, blue: 0 / 255, alpha: 0.6), // #26C000 + UIColor(red: 1 / 255, green: 255 / 255, blue: 179 / 255, alpha: 0.6), // #01FFB3 + UIColor(red: 125 / 255, green: 36 / 255, blue: 255 / 255, alpha: 0.6), // #7D24FF + UIColor(red: 123 / 255, green: 0 / 255, blue: 104 / 255, alpha: 0.6), // #7B0068 + UIColor(red: 255 / 255, green: 27 / 255, blue: 108 / 255, alpha: 0.6), // #FF1B6C + UIColor(red: 252 / 255, green: 109 / 255, blue: 47 / 255, alpha: 0.6), // #FC6D2F + UIColor(red: 162 / 255, green: 255 / 255, blue: 11 / 255, alpha: 0.6), // #A2FF0B + ] func setUpBoundingBoxViews() { // Ensure all bounding box views are initialized up to the maximum allowed. @@ -292,14 +318,15 @@ class ViewController: UIViewController { } // Assign random colors to the classes. + var count = 0 for label in classLabels { - if colors[label] == nil { // if key not in dict - colors[label] = UIColor( - red: CGFloat.random(in: 0...1), - green: CGFloat.random(in: 0...1), - blue: CGFloat.random(in: 0...1), - alpha: 0.6) + let color = ultralyticsColorsolors[count] + count += 1 + if count > 19 { + count = 0 } + colors[label] = color + } } @@ -330,7 +357,13 @@ class ViewController: UIViewController { func predict(sampleBuffer: CMSampleBuffer) { if currentBuffer == nil, let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { currentBuffer = pixelBuffer - + if !frameSizeCaptured { + let frameWidth = CGFloat(CVPixelBufferGetWidth(pixelBuffer)) + let frameHeight = CGFloat(CVPixelBufferGetHeight(pixelBuffer)) + longSide = max(frameWidth, frameHeight) + shortSide = min(frameWidth, frameHeight) + frameSizeCaptured = true + } /// - Tag: MappingOrientation // The frame is always oriented based on the camera sensor, // so in most cases Vision needs to rotate it for the model to work as expected. @@ -449,18 +482,7 @@ class ViewController: UIViewController { } func show(predictions: [VNRecognizedObjectObservation]) { - let width = videoPreview.bounds.width // 375 pix - let height = videoPreview.bounds.height // 812 pix var str = "" - - // ratio = videoPreview AR divided by sessionPreset AR - var ratio: CGFloat = 1.0 - if videoCapture.captureSession.sessionPreset == .photo { - ratio = (height / width) / (4.0 / 3.0) // .photo - } else { - ratio = (height / width) / (16.0 / 9.0) // .hd4K3840x2160, .hd1920x1080, .hd1280x720 etc. - } - // date let date = Date() let calendar = Calendar.current @@ -473,89 +495,139 @@ class ViewController: UIViewController { self.labelSlider.text = String(predictions.count) + " items (max " + String(Int(slider.value)) + ")" - for i in 0..= 1 { // iPhone ratio = 1.218 - let offset = (1 - ratio) * (0.5 - rect.minX) - let transform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: offset, y: -1) - rect = rect.applying(transform) - rect.size.width *= ratio - } else { // iPad ratio = 0.75 - let offset = (ratio - 1) * (0.5 - rect.maxY) - let transform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: offset - 1) - rect = rect.applying(transform) - ratio = (height / width) / (3.0 / 4.0) - rect.size.height /= ratio - } + if UIDevice.current.orientation == .portrait { - // Scale normalized to pixels [375, 812] [width, height] - rect = VNImageRectForNormalizedRect(rect, Int(width), Int(height)) - - // The labels array is a list of VNClassificationObservation objects, - // with the highest scoring class first in the list. - let bestClass = prediction.labels[0].identifier - let confidence = prediction.labels[0].confidence - // print(confidence, rect) // debug (confidence, xywh) with xywh origin top left (pixels) - let label = String(format: "%@ %.1f", bestClass, confidence * 100) - let alpha = CGFloat((confidence - 0.2) / (1.0 - 0.2) * 0.9) - // Show the bounding box. - boundingBoxViews[i].show( - frame: rect, - label: label, - color: colors[bestClass] ?? UIColor.white, - alpha: alpha) // alpha 0 (transparent) to 1 (opaque) for conf threshold 0.2 to 1.0) - - if developerMode { - // Write - if save_detections { - str += String( - format: "%.3f %.3f %.3f %@ %.2f %.1f %.1f %.1f %.1f\n", - sec_day, freeSpace(), UIDevice.current.batteryLevel, bestClass, confidence, - rect.origin.x, rect.origin.y, rect.size.width, rect.size.height) + // ratio = videoPreview AR divided by sessionPreset AR + var ratio: CGFloat = 1.0 + if videoCapture.captureSession.sessionPreset == .photo { + ratio = (height / width) / (4.0 / 3.0) // .photo + } else { + ratio = (height / width) / (16.0 / 9.0) // .hd4K3840x2160, .hd1920x1080, .hd1280x720 etc. + } + + for i in 0..= 1 { // iPhone ratio = 1.218 + let offset = (1 - ratio) * (0.5 - rect.minX) + let transform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: offset, y: -1) + rect = rect.applying(transform) + rect.size.width *= ratio + } else { // iPad ratio = 0.75 + let offset = (ratio - 1) * (0.5 - rect.maxY) + let transform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: offset - 1) + rect = rect.applying(transform) + ratio = (height / width) / (3.0 / 4.0) + rect.size.height /= ratio + } + + // Scale normalized to pixels [375, 812] [width, height] + rect = VNImageRectForNormalizedRect(rect, Int(width), Int(height)) + + // The labels array is a list of VNClassificationObservation objects, + // with the highest scoring class first in the list. + let bestClass = prediction.labels[0].identifier + let confidence = prediction.labels[0].confidence + // print(confidence, rect) // debug (confidence, xywh) with xywh origin top left (pixels) + let label = String(format: "%@ %.1f", bestClass, confidence * 100) + let alpha = CGFloat((confidence - 0.2) / (1.0 - 0.2) * 0.9) + // Show the bounding box. + boundingBoxViews[i].show( + frame: rect, + label: label, + color: colors[bestClass] ?? UIColor.white, + alpha: alpha) // alpha 0 (transparent) to 1 (opaque) for conf threshold 0.2 to 1.0) + + if developerMode { + // Write + if save_detections { + str += String( + format: "%.3f %.3f %.3f %@ %.2f %.1f %.1f %.1f %.1f\n", + sec_day, freeSpace(), UIDevice.current.batteryLevel, bestClass, confidence, + rect.origin.x, rect.origin.y, rect.size.width, rect.size.height) + } + } + } else { + boundingBoxViews[i].hide() } + } + } else { + let frameAspectRatio = longSide / shortSide + let viewAspectRatio = width / height + var scaleX: CGFloat = 1.0 + var scaleY: CGFloat = 1.0 + var offsetX: CGFloat = 0.0 + var offsetY: CGFloat = 0.0 + + if frameAspectRatio > viewAspectRatio { + scaleY = height / shortSide + scaleX = scaleY + offsetX = (longSide * scaleX - width) / 2 } else { - boundingBoxViews[i].hide() + scaleX = width / longSide + scaleY = scaleX + offsetY = (shortSide * scaleY - height) / 2 } - } + for i in 0..