Skip to content

Commit d936530

Browse files
Fixes and dead code removal (#225)
* Add model directory .gitkeep files * Add model directory .gitkeep files * Add model directory .gitkeep files * Add model directory .gitkeep files * Add model directory .gitkeep files * Add model directory .gitkeep files * Add model directory .gitkeep files * Add model directory .gitkeep files * Add model directory .gitkeep files * Add model directory .gitkeep files * Auto-format by https://ultralytics.com/actions * Add model directory .gitkeep files * Add model directory .gitkeep files * Add model directory .gitkeep files --------- Co-authored-by: UltralyticsAssistant <web@ultralytics.com>
1 parent 76ae65c commit d936530

21 files changed

+277
-553
lines changed

Sources/YOLO/BasePredictor.swift

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public class BasePredictor: Predictor, @unchecked Sendable {
7070
var t3 = CACurrentMediaTime() // FPS start
7171

7272
/// Smoothed frames per second measurement (averaged over recent frames).
73-
var t4 = 0.0 // FPS dt smoothed
73+
var t4 = 1.0 // FPS dt smoothed (non-zero to avoid infinity on first frame)
7474

7575
/// Flag indicating whether the predictor is currently processing an update.
7676
public var isUpdating: Bool = false
@@ -236,7 +236,6 @@ public class BasePredictor: Predictor, @unchecked Sendable {
236236
width: CVPixelBufferGetWidth(pixelBuffer), height: CVPixelBufferGetHeight(pixelBuffer))
237237
currentOnResultsListener = onResultsListener
238238
currentOnInferenceTimeListener = onInferenceTime
239-
// currentOnFpsRateListener = onFpsRate
240239

241240
/// - Tag: MappingOrientation
242241
// The frame is always oriented based on the camera sensor,
@@ -248,8 +247,8 @@ public class BasePredictor: Predictor, @unchecked Sendable {
248247
cvPixelBuffer: pixelBuffer, orientation: imageOrientation, options: [:])
249248
t0 = CACurrentMediaTime() // inference start
250249
do {
251-
if visionRequest != nil {
252-
try handler.perform([visionRequest!])
250+
if let request = visionRequest {
251+
try handler.perform([request])
253252
}
254253
} catch {
255254
print(error)
@@ -270,6 +269,9 @@ public class BasePredictor: Predictor, @unchecked Sendable {
270269
/// - Parameter confidence: The new confidence threshold value (0.0 to 1.0).
271270
func setConfidenceThreshold(confidence: Double) {
272271
confidenceThreshold = confidence
272+
let iou = requiresNMS ? iouThreshold : 1.0
273+
detector?.featureProvider = ThresholdProvider(
274+
iouThreshold: iou, confidenceThreshold: confidenceThreshold)
273275
}
274276

275277
/// The IoU (Intersection over Union) threshold for non-maximum suppression (default: 0.7).
@@ -282,6 +284,9 @@ public class BasePredictor: Predictor, @unchecked Sendable {
282284
/// - Parameter iou: The new IoU threshold value (0.0 to 1.0).
283285
func setIouThreshold(iou: Double) {
284286
iouThreshold = iou
287+
let effectiveIou = requiresNMS ? iouThreshold : 1.0
288+
detector?.featureProvider = ThresholdProvider(
289+
iouThreshold: effectiveIou, confidenceThreshold: confidenceThreshold)
285290
}
286291

287292
/// The maximum number of detections to return in results (default: 30).
@@ -338,8 +343,8 @@ public class BasePredictor: Predictor, @unchecked Sendable {
338343
if let multiArrayConstraint = inputDescription.multiArrayConstraint {
339344
let shape = multiArrayConstraint.shape
340345
if shape.count >= 2 {
341-
let height = shape[0].intValue
342-
let width = shape[1].intValue
346+
let height = shape[shape.count - 2].intValue
347+
let width = shape[shape.count - 1].intValue
343348
return (width: width, height: height)
344349
}
345350
}

Sources/YOLO/BoundingBoxView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public class BoundingBoxView {
6565
/// - color: The color of the bounding box stroke and label background.
6666
/// - alpha: The opacity level for the bounding box stroke and label background.
6767
func show(frame: CGRect, label: String, color: UIColor, alpha: CGFloat) {
68+
CATransaction.begin()
6869
CATransaction.setDisableActions(true) // Disable implicit animations
6970

7071
let path = UIBezierPath(roundedRect: frame, cornerRadius: 6.0) // Rounded rectangle for the bounding box
@@ -86,6 +87,7 @@ public class BoundingBoxView {
8687
let textSize = CGSize(width: textRect.width + 12, height: textRect.height) // Add padding to the text size
8788
let textOrigin = CGPoint(x: frame.origin.x - 2, y: frame.origin.y - textSize.height - 2) // Position above the bounding box
8889
textLayer.frame = CGRect(origin: textOrigin, size: textSize) // Set the text layer frame
90+
CATransaction.commit()
8991
}
9092

9193
/// Hides the bounding box and text layers.

Sources/YOLO/Classifier.swift

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,14 @@
1212
// VNClassificationObservation result types. The implementation extracts both the top prediction
1313
// and the top 5 predictions with their confidence scores, enabling rich user feedback.
1414

15+
import Accelerate
1516
import Foundation
1617
import UIKit
1718
import Vision
1819

1920
/// Specialized predictor for YOLO classification models that identify the subject of an image.
2021
public class Classifier: BasePredictor, @unchecked Sendable {
2122

22-
override func setConfidenceThreshold(confidence: Double) {
23-
confidenceThreshold = confidence
24-
detector?.featureProvider = ThresholdProvider(
25-
iouThreshold: iouThreshold, confidenceThreshold: confidenceThreshold)
26-
}
27-
28-
override func setIouThreshold(iou: Double) {
29-
iouThreshold = iou
30-
detector?.featureProvider = ThresholdProvider(
31-
iouThreshold: iouThreshold, confidenceThreshold: confidenceThreshold)
32-
}
33-
3423
override func processObservations(for request: VNRequest, error: Error?) {
3524
let imageWidth = inputSize.width
3625
let imageHeight = inputSize.height
@@ -43,22 +32,29 @@ public class Classifier: BasePredictor, @unchecked Sendable {
4332
let multiArray = observation.first?.featureValue.multiArrayValue
4433

4534
if let multiArray = multiArray {
46-
// Initialize an array to store the classes
47-
var valuesArray = [Double]()
35+
// Apply softmax to convert raw logits to probabilities
36+
var floatValues = [Float](repeating: 0, count: multiArray.count)
4837
for i in 0..<multiArray.count {
49-
let value = multiArray[i].doubleValue
50-
valuesArray.append(value)
38+
floatValues[i] = multiArray[i].floatValue
39+
}
40+
var softmaxOutput = [Float](repeating: 0, count: floatValues.count)
41+
var count = Int32(floatValues.count)
42+
vvexpf(&softmaxOutput, floatValues, &count)
43+
var sumExp: Float = 0
44+
vDSP_sve(softmaxOutput, 1, &sumExp, vDSP_Length(floatValues.count))
45+
if sumExp > 0 {
46+
vDSP_vsdiv(softmaxOutput, 1, &sumExp, &softmaxOutput, 1, vDSP_Length(floatValues.count))
5147
}
5248

5349
var indexedMap = [Int: Double]()
54-
for (index, value) in valuesArray.enumerated() {
55-
indexedMap[index] = value
50+
for (index, value) in softmaxOutput.enumerated() {
51+
indexedMap[index] = Double(value)
5652
}
5753

5854
let sortedMap = indexedMap.sorted { $0.value > $1.value }
5955

6056
// top1
61-
if let (topIndex, topScore) = sortedMap.first {
57+
if let (topIndex, topScore) = sortedMap.first, topIndex < labels.count {
6258
let top1Label = labels[topIndex]
6359
let top1Conf = Float(topScore)
6460
probs.top1 = top1Label
@@ -70,7 +66,7 @@ public class Classifier: BasePredictor, @unchecked Sendable {
7066
var top5Labels: [String] = []
7167
var top5Confs: [Float] = []
7268

73-
for (index, value) in topObservations {
69+
for (index, value) in topObservations where index < labels.count {
7470
top5Labels.append(labels[index])
7571
top5Confs.append(Float(value))
7672
}
@@ -92,7 +88,7 @@ public class Classifier: BasePredictor, @unchecked Sendable {
9288
top1 = topObservation.identifier
9389
top1Conf = Float(topObservation.confidence)
9490
}
95-
for i in 0...candidateNumber - 1 {
91+
for i in 0..<candidateNumber {
9692
let observation = observations[i]
9793
let label = observation.identifier
9894
let confidence: Float = Float(observation.confidence)
@@ -110,7 +106,6 @@ public class Classifier: BasePredictor, @unchecked Sendable {
110106
self.t3 = CACurrentMediaTime()
111107

112108
self.currentOnInferenceTimeListener?.on(inferenceTime: self.t2 * 1000, fpsRate: 1 / self.t4) // t2 seconds to ms
113-
// self.currentOnFpsRateListener?.on(fpsRate: 1 / self.t4)
114109
let result = YOLOResult(
115110
orig_shape: inputSize, boxes: [], probs: probs, speed: self.t2, fps: 1 / self.t4,
116111
names: labels)
@@ -133,28 +128,33 @@ public class Classifier: BasePredictor, @unchecked Sendable {
133128
do {
134129
try requestHandler.perform([request])
135130
if let observation = request.results as? [VNCoreMLFeatureValueObservation] {
136-
_ = [[String: Any]]()
137-
138131
// Get the MLMultiArray from the observation
139132
let multiArray = observation.first?.featureValue.multiArrayValue
140133

141134
if let multiArray = multiArray {
142-
// Initialize an array to store the classes
143-
var valuesArray = [Double]()
135+
// Apply softmax to convert raw logits to probabilities
136+
var floatValues = [Float](repeating: 0, count: multiArray.count)
144137
for i in 0..<multiArray.count {
145-
let value = multiArray[i].doubleValue
146-
valuesArray.append(value)
138+
floatValues[i] = multiArray[i].floatValue
139+
}
140+
var softmaxOutput = [Float](repeating: 0, count: floatValues.count)
141+
var count = Int32(floatValues.count)
142+
vvexpf(&softmaxOutput, floatValues, &count)
143+
var sumExp: Float = 0
144+
vDSP_sve(softmaxOutput, 1, &sumExp, vDSP_Length(floatValues.count))
145+
if sumExp > 0 {
146+
vDSP_vsdiv(softmaxOutput, 1, &sumExp, &softmaxOutput, 1, vDSP_Length(floatValues.count))
147147
}
148148

149149
var indexedMap = [Int: Double]()
150-
for (index, value) in valuesArray.enumerated() {
151-
indexedMap[index] = value
150+
for (index, value) in softmaxOutput.enumerated() {
151+
indexedMap[index] = Double(value)
152152
}
153153

154154
let sortedMap = indexedMap.sorted { $0.value > $1.value }
155155

156156
// top1
157-
if let (topIndex, topScore) = sortedMap.first {
157+
if let (topIndex, topScore) = sortedMap.first, topIndex < labels.count {
158158
let top1Label = labels[topIndex]
159159
let top1Conf = Float(topScore)
160160
probs.top1 = top1Label
@@ -166,7 +166,7 @@ public class Classifier: BasePredictor, @unchecked Sendable {
166166
var top5Labels: [String] = []
167167
var top5Confs: [Float] = []
168168

169-
for (index, value) in topObservations {
169+
for (index, value) in topObservations where index < labels.count {
170170
top5Labels.append(labels[index])
171171
top5Confs.append(Float(value))
172172
}
@@ -188,7 +188,7 @@ public class Classifier: BasePredictor, @unchecked Sendable {
188188
top1 = topObservation.identifier
189189
top1Conf = Float(topObservation.confidence)
190190
}
191-
for i in 0...candidateNumber - 1 {
191+
for i in 0..<candidateNumber {
192192
let observation = observations[i]
193193
let label = observation.identifier
194194
let confidence: Float = Float(observation.confidence)

Sources/YOLO/NonMaxSuppression.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public func nonMaxSuppression(boxes: [CGRect], scores: [Float], threshold: Float
3333
let otherIdx = sortedIndices[j]
3434
if activeIndices[otherIdx] {
3535
let intersection = boxes[idx].intersection(boxes[otherIdx])
36-
if intersection.area > CGFloat(threshold) * min(boxes[idx].area, boxes[otherIdx].area) {
36+
let union = boxes[idx].area + boxes[otherIdx].area - intersection.area
37+
if union > 0 && intersection.area / union > CGFloat(threshold) {
3738
activeIndices[otherIdx] = false
3839
}
3940
}

Sources/YOLO/ObbDetector.swift

Lines changed: 4 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ public class ObbDetector: BasePredictor, @unchecked Sendable {
4242
obbResults.append(obbResult)
4343
}
4444

45+
self.updateTime()
4546
self.currentOnResultsListener?.on(
4647
result: YOLOResult(
47-
orig_shape: inputSize, boxes: [], obb: obbResults, speed: 0, names: labels))
48-
self.updateTime()
48+
orig_shape: inputSize, boxes: [], obb: obbResults, speed: self.t2, fps: 1 / self.t4,
49+
names: labels))
4950
}
5051
}
5152
}
@@ -99,7 +100,7 @@ public class ObbDetector: BasePredictor, @unchecked Sendable {
99100
return YOLOResult(
100101
orig_shape: inputSize, boxes: [], masks: nil, probs: nil, keypointsList: [],
101102
obb: obbResults, annotatedImage: annotatedImage, speed: self.t2, fps: 1 / self.t4,
102-
originalImage: nil, names: labels)
103+
names: labels)
103104
}
104105
}
105106
} catch {
@@ -108,8 +109,6 @@ public class ObbDetector: BasePredictor, @unchecked Sendable {
108109
return result
109110
}
110111

111-
fileprivate let lockQueue = DispatchQueue(label: "com.example.obbLock")
112-
113112
func postProcessOBB(
114113
feature: MLMultiArray,
115114
confidenceThreshold: Float,
@@ -448,65 +447,6 @@ func polygonArea(_ poly: Polygon) -> CGFloat {
448447
return abs(area) * 0.5
449448
}
450449

451-
/// Compute IoU of two oriented bounding boxes (cx,cy,w,h,angle).
452-
/// Steps:
453-
/// 1. Convert each OBB to polygon
454-
/// 2. Intersect polygons
455-
/// 3. Area(Intersection) / Area(Union)
456-
func obbIoU(_ box1: OBB, _ box2: OBB) -> Float {
457-
let poly1 = box1.toPolygon()
458-
let poly2 = box2.toPolygon()
459-
460-
let area1 = box1.area
461-
let area2 = box2.area
462-
463-
// Intersect polygons
464-
let interPoly = polygonIntersection(subjectPolygon: poly1, clipPolygon: poly2)
465-
let interArea = polygonArea(interPoly)
466-
467-
let unionArea = area1 + area2 - interArea
468-
if unionArea <= 0.0 { return 0.0 }
469-
let iou = Float(interArea / unionArea)
470-
return iou
471-
}
472-
473-
/// Perform NMS for oriented bounding boxes.
474-
/// - Parameters:
475-
/// - boxes: Array of OBB (cx, cy, w, h, angle)
476-
/// - scores: Confidence scores parallel to `boxes`
477-
/// - iouThreshold: Intersection-over-Union threshold
478-
/// - Returns: Array of kept indices
479-
//public func nonMaxSuppressionOBB(
480-
// boxes: [OBB],
481-
// scores: [Float],
482-
// iouThreshold: Float
483-
//) -> [Int] {
484-
// // Sort by descending scores
485-
// let sortedIndices = scores.enumerated()
486-
// .sorted(by: { $0.element > $1.element })
487-
// .map { $0.offset }
488-
//
489-
// var selectedIndices = [Int]()
490-
// var active = [Bool](repeating: true, count: boxes.count)
491-
//
492-
// for i in 0..<sortedIndices.count {
493-
// let idx = sortedIndices[i]
494-
// if !active[idx] { continue } // already suppressed
495-
// selectedIndices.append(idx)
496-
//
497-
// for j in (i+1)..<sortedIndices.count {
498-
// let otherIdx = sortedIndices[j]
499-
// if active[otherIdx] {
500-
// // Compute IoU for oriented boxes
501-
// let iouVal = obbIoU(boxes[idx], boxes[otherIdx])
502-
// if iouVal > iouThreshold {
503-
// active[otherIdx] = false
504-
// }
505-
// }
506-
// }
507-
// }
508-
// return selectedIndices
509-
//}
510450
/// Store cached geometry for faster OBB IoU checks.
511451
public struct OBBInfo {
512452
let box: OBB

Sources/YOLO/ObjectDetector.swift

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,32 +28,6 @@ import Vision
2828
/// - SeeAlso: `Segmenter` for models that produce pixel-level masks for objects.
2929
public class ObjectDetector: BasePredictor, @unchecked Sendable {
3030

31-
/// Sets the confidence threshold and updates the model's feature provider.
32-
///
33-
/// This overridden method ensures that when the confidence threshold is changed,
34-
/// the Vision model's feature provider is also updated to use the new value.
35-
///
36-
/// - Parameter confidence: The new confidence threshold value (0.0 to 1.0).
37-
override func setConfidenceThreshold(confidence: Double) {
38-
confidenceThreshold = confidence
39-
let iou = requiresNMS ? iouThreshold : 1.0
40-
detector?.featureProvider = ThresholdProvider(
41-
iouThreshold: iou, confidenceThreshold: confidenceThreshold)
42-
}
43-
44-
/// Sets the IoU threshold and updates the model's feature provider.
45-
///
46-
/// This overridden method ensures that when the IoU threshold is changed,
47-
/// the Vision model's feature provider is also updated to use the new value.
48-
///
49-
/// - Parameter iou: The new IoU threshold value (0.0 to 1.0).
50-
override func setIouThreshold(iou: Double) {
51-
iouThreshold = iou
52-
let effectiveIou = requiresNMS ? iouThreshold : 1.0
53-
detector?.featureProvider = ThresholdProvider(
54-
iouThreshold: effectiveIou, confidenceThreshold: confidenceThreshold)
55-
}
56-
5731
/// Processes the results from the Vision framework's object detection request.
5832
///
5933
/// This method extracts bounding boxes, class labels, and confidence scores from the
@@ -159,9 +133,9 @@ public class ObjectDetector: BasePredictor, @unchecked Sendable {
159133
} catch {
160134
print(error)
161135
}
162-
_ = Date().timeIntervalSince(start)
136+
let elapsed = Date().timeIntervalSince(start)
163137

164-
var result = YOLOResult(orig_shape: inputSize, boxes: boxes, speed: t1, names: labels)
138+
var result = YOLOResult(orig_shape: inputSize, boxes: boxes, speed: elapsed, names: labels)
165139
let annotatedImage = drawYOLODetections(on: image, result: result)
166140
result.annotatedImage = annotatedImage
167141

@@ -184,7 +158,7 @@ public class ObjectDetector: BasePredictor, @unchecked Sendable {
184158

185159
// Detect format: end2end [1, max_det, 6] vs traditional [1, 4+nc, num_anchors]
186160
guard shape.count == 3 else { return [] }
187-
let isEnd2End = shape[2] <= 6 || shape[2] < shape[1]
161+
let isEnd2End = shape[2] < shape[1]
188162

189163
if isEnd2End {
190164
return processEnd2EndResults(

0 commit comments

Comments
 (0)