Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
175 changes: 145 additions & 30 deletions Sources/YOLO/Plot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,34 @@ let ultralyticsColors: [UIColor] = [
UIColor(red: 162 / 255, green: 255 / 255, blue: 11 / 255, alpha: 0.6),
]

/// Configuration for annotation rendering parameters
public struct AnnotationConfig {
/// Font size for annotation labels. If nil, uses automatic scaling based on image dimensions.
public let fontSize: CGFloat?
/// Line width for bounding boxes. If nil, uses automatic scaling based on image dimensions.
public let lineWidth: CGFloat?
/// Font weight for annotation labels
public let fontWeight: UIFont.Weight

public init(
fontSize: CGFloat? = nil, lineWidth: CGFloat? = nil, fontWeight: UIFont.Weight = .semibold
) {
self.fontSize = fontSize
self.lineWidth = lineWidth
self.fontWeight = fontWeight
}

/// Default configuration with automatic scaling
public static let `default` = AnnotationConfig()

/// Configuration with custom font size
public static func custom(fontSize: CGFloat, fontWeight: UIFont.Weight = .semibold)
-> AnnotationConfig
{
return AnnotationConfig(fontSize: fontSize, fontWeight: fontWeight)
}
}

let posePalette: [[CGFloat]] = [
[255, 128, 0],
[255, 153, 51],
Expand Down Expand Up @@ -90,7 +118,9 @@ let skeleton = [
[5, 7],
]

public func drawYOLODetections(on ciImage: CIImage, result: YOLOResult) -> UIImage {
public func drawYOLODetections(
on ciImage: CIImage, result: YOLOResult, config: AnnotationConfig = .default
) -> UIImage {
let context = CIContext(options: nil)
guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else {
return UIImage()
Expand All @@ -109,9 +139,9 @@ public func drawYOLODetections(on ciImage: CIImage, result: YOLOResult) -> UIIma
drawContext.draw(cgImage, in: CGRect(origin: .zero, size: imageSize))
drawContext.restoreGState()

// Calculate line width and font size proportionally to image dimensions
let lineWidth = max(width, height) / 200
let fontSize = max(width, height) / 50
// Calculate line width and font size based on configuration
let lineWidth = config.lineWidth ?? CGFloat(max(width, height)) / 200
let fontSize = config.fontSize ?? CGFloat(max(width, height)) / 50

for box in result.boxes {
let colorIndex = box.index % ultralyticsColors.count
Expand All @@ -122,7 +152,7 @@ public func drawYOLODetections(on ciImage: CIImage, result: YOLOResult) -> UIIma
drawContext.stroke(rect)
let confidencePercent = Int(box.conf * 100)
let labelText = "\(box.cls) \(confidencePercent)%"
let font = UIFont.systemFont(ofSize: CGFloat(fontSize), weight: .semibold)
let font = UIFont.systemFont(ofSize: CGFloat(fontSize), weight: config.fontWeight)
let attrs: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: UIColor.white,
Expand Down Expand Up @@ -351,7 +381,9 @@ func composeImageWithMask(
return UIImage(cgImage: composedImage)
}

public func drawYOLOClassifications(on ciImage: CIImage, result: YOLOResult) -> UIImage {
public func drawYOLOClassifications(
on ciImage: CIImage, result: YOLOResult, config: AnnotationConfig = .default
) -> UIImage {
let context = CIContext(options: nil)
guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else {
return UIImage()
Expand All @@ -373,9 +405,9 @@ public func drawYOLOClassifications(on ciImage: CIImage, result: YOLOResult) ->
return UIImage(ciImage: ciImage)
}

// Calculate line width and font size proportionally to image dimensions
let lineWidth = max(width, height) / 200
let fontSize = max(width, height) / 50
// Calculate line width and font size based on configuration
let lineWidth = config.lineWidth ?? CGFloat(max(width, height)) / 200
let fontSize = config.fontSize ?? CGFloat(max(width, height)) / 50
let labelMargin = CGFloat(fontSize / 2)

for (i, candidate) in top5.enumerated() {
Expand All @@ -388,7 +420,7 @@ public func drawYOLOClassifications(on ciImage: CIImage, result: YOLOResult) ->
drawContext.setLineWidth(CGFloat(lineWidth))
let confidencePercent = round((result.probs?.top5Confs[i] ?? 0) * 1000) / 10
let labelText = " \(candidate) \(confidencePercent)% "
let font = UIFont.systemFont(ofSize: CGFloat(fontSize), weight: .semibold)
let font = UIFont.systemFont(ofSize: CGFloat(fontSize), weight: config.fontWeight)
let attrs: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: UIColor.white,
Expand Down Expand Up @@ -681,16 +713,16 @@ class OBBRenderer {
on layer: CALayer,
imageViewSize: CGSize,
originalImageSize: CGSize,
lineWidth: CGFloat = 2.0
config: AnnotationConfig = .default
) {
usedLayerCount = 0

let scaleX = imageViewSize.width
let scaleY = imageViewSize.height

// Calculate line width and font size dynamically based on image dimensions
let dynamicLineWidth = max(imageViewSize.width, imageViewSize.height) / 200
let dynamicFontSize = max(imageViewSize.width, imageViewSize.height) / 50
// Calculate line width and font size based on configuration
let dynamicLineWidth = config.lineWidth ?? max(imageViewSize.width, imageViewSize.height) / 200
let dynamicFontSize = config.fontSize ?? max(imageViewSize.width, imageViewSize.height) / 50

for detection in obbDetections {
let bundle = getLayerBundle(for: layer)
Expand Down Expand Up @@ -721,7 +753,7 @@ class OBBRenderer {
shapeLayer.isHidden = false

let text = detection.cls + String(format: " %.2f", detection.confidence)
let font = UIFont.systemFont(ofSize: dynamicFontSize)
let font = UIFont.systemFont(ofSize: dynamicFontSize, weight: config.fontWeight)

let attributes: [NSAttributedString.Key: Any] = [
.font: font
Expand Down Expand Up @@ -763,7 +795,8 @@ class OBBRenderer {
func drawOBBsOnCIImage(
ciImage: CIImage,
obbDetections: [OBBResult],
targetSize: CGSize? = nil
targetSize: CGSize? = nil,
config: AnnotationConfig = .default
) -> UIImage? {

let context = CIContext(options: nil)
Expand All @@ -773,9 +806,9 @@ func drawOBBsOnCIImage(
return nil
}

// Calculate line width and font size proportionally to image dimensions
let lineWidth: CGFloat = max(extent.width, extent.height) / 200
let fontSize = max(extent.width, extent.height) / 50
// Calculate line width and font size based on configuration
let lineWidth: CGFloat = config.lineWidth ?? CGFloat(max(extent.width, extent.height)) / 200
let fontSize = config.fontSize ?? CGFloat(max(extent.width, extent.height)) / 50
let outputSize = targetSize ?? CGSize(width: extent.width, height: extent.height)

UIGraphicsBeginImageContextWithOptions(outputSize, false, 1.0)
Expand Down Expand Up @@ -810,7 +843,7 @@ func drawOBBsOnCIImage(

let labelText = "\(detection.cls) \(String(format: "%.2f", detection.confidence))%"
let attrs: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: fontSize),
.font: UIFont.systemFont(ofSize: fontSize, weight: config.fontWeight),
.foregroundColor: UIColor.white,
.backgroundColor: color.withAlphaComponent(0.7),
]
Expand Down Expand Up @@ -839,7 +872,8 @@ public func drawYOLOPoseWithBoxes(
boundingBoxes: [Box],
originalImageSize: CGSize,
confThreshold: Float = 0.25,
drawSkeleton: Bool = true
drawSkeleton: Bool = true,
config: AnnotationConfig = .default
) -> UIImage? {
// 1. Convert CIImage to CGImage only once
let context = CIContext(options: nil)
Expand All @@ -853,10 +887,10 @@ public func drawYOLOPoseWithBoxes(
let height = CGFloat(cgImage.height)
let renderedSize = CGSize(width: width, height: height)

// 2. Calculate drawing sizes
// 2. Calculate drawing sizes based on configuration
let circleRadius = max(width, height) / 100
let lineWidth = max(width, height) / 200
let fontSize = max(width, height) / 50
let lineWidth = config.lineWidth ?? CGFloat(max(width, height)) / 200
let fontSize = config.fontSize ?? CGFloat(max(width, height)) / 50

// 3. Create a single rendering context
UIGraphicsBeginImageContextWithOptions(renderedSize, false, 0.0)
Expand Down Expand Up @@ -891,7 +925,7 @@ public func drawYOLOPoseWithBoxes(
// Prepare label
let confidencePercent = Int(box.conf * 100)
let labelText = "\(box.cls) \(confidencePercent)%"
let font = UIFont.systemFont(ofSize: fontSize, weight: .semibold)
let font = UIFont.systemFont(ofSize: fontSize, weight: config.fontWeight)
let attrs: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: UIColor.white,
Expand Down Expand Up @@ -957,7 +991,8 @@ public func drawYOLOSegmentationWithBoxes(
ciImage: CIImage,
boxes: [Box],
maskImage: CGImage?,
originalImageSize: CGSize
originalImageSize: CGSize,
config: AnnotationConfig = .default
) -> UIImage? {
// 1. Convert CIImage to CGImage only once
let context = CIContext(options: nil)
Expand All @@ -971,9 +1006,9 @@ public func drawYOLOSegmentationWithBoxes(
let height = CGFloat(cgImage.height)
let renderedSize = CGSize(width: width, height: height)

// 2. Calculate drawing sizes
let lineWidth = max(width, height) / 200
let fontSize = max(width, height) / 50
// 2. Calculate drawing sizes based on configuration
let lineWidth = config.lineWidth ?? CGFloat(max(width, height)) / 200
let fontSize = config.fontSize ?? CGFloat(max(width, height)) / 50

// 3. Create a single rendering context
UIGraphicsBeginImageContextWithOptions(renderedSize, false, 0.0)
Expand Down Expand Up @@ -1032,7 +1067,7 @@ public func drawYOLOSegmentationWithBoxes(
// Prepare label
let confidencePercent = Int(box.conf * 100)
let labelText = "\(box.cls) \(confidencePercent)%"
let font = UIFont.systemFont(ofSize: fontSize, weight: .semibold)
let font = UIFont.systemFont(ofSize: fontSize, weight: config.fontWeight)
let attrs: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: UIColor.white,
Expand Down Expand Up @@ -1072,3 +1107,83 @@ public func drawYOLOSegmentationWithBoxes(

return finalImage
}

// MARK: - Convenience Functions for Font Size Control

/// Draw YOLO detections with custom font size
public func drawYOLODetections(
on ciImage: CIImage,
result: YOLOResult,
fontSize: CGFloat
) -> UIImage {
let config = AnnotationConfig.custom(fontSize: fontSize)
return drawYOLODetections(on: ciImage, result: result, config: config)
}

/// Draw YOLO classifications with custom font size
public func drawYOLOClassifications(
on ciImage: CIImage,
result: YOLOResult,
fontSize: CGFloat
) -> UIImage {
let config = AnnotationConfig.custom(fontSize: fontSize)
return drawYOLOClassifications(on: ciImage, result: result, config: config)
}

/// Draw YOLO pose with boxes and custom font size
public func drawYOLOPoseWithBoxes(
ciImage: CIImage,
keypointsList: [[(x: Float, y: Float)]],
confsList: [[Float]],
boundingBoxes: [Box],
originalImageSize: CGSize,
confThreshold: Float = 0.25,
drawSkeleton: Bool = true,
fontSize: CGFloat
) -> UIImage? {
let config = AnnotationConfig.custom(fontSize: fontSize)
return drawYOLOPoseWithBoxes(
ciImage: ciImage,
keypointsList: keypointsList,
confsList: confsList,
boundingBoxes: boundingBoxes,
originalImageSize: originalImageSize,
confThreshold: confThreshold,
drawSkeleton: drawSkeleton,
config: config
)
}

/// Draw YOLO segmentation with boxes and custom font size
public func drawYOLOSegmentationWithBoxes(
ciImage: CIImage,
boxes: [Box],
maskImage: CGImage?,
originalImageSize: CGSize,
fontSize: CGFloat
) -> UIImage? {
let config = AnnotationConfig.custom(fontSize: fontSize)
return drawYOLOSegmentationWithBoxes(
ciImage: ciImage,
boxes: boxes,
maskImage: maskImage,
originalImageSize: originalImageSize,
config: config
)
}

/// Draw OBB detections on CIImage with custom font size
public func drawOBBsOnCIImage(
ciImage: CIImage,
obbDetections: [OBBResult],
targetSize: CGSize? = nil,
fontSize: CGFloat
) -> UIImage? {
let config = AnnotationConfig.custom(fontSize: fontSize)
return drawOBBsOnCIImage(
ciImage: ciImage,
obbDetections: obbDetections,
targetSize: targetSize,
config: config
)
}
44 changes: 42 additions & 2 deletions Sources/YOLO/YOLOView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public class YOLOView: UIView, VideoCaptureDelegate {
on: obbLayer,
imageViewSize: self.overlayLayer.frame.size,
originalImageSize: result.orig_shape, // Example
lineWidth: 3
config: annotationConfig
)
}
}
Expand Down Expand Up @@ -122,6 +122,9 @@ public class YOLOView: UIView, VideoCaptureDelegate {
public var activityIndicator = UIActivityIndicatorView()
public var playButton = UIButton()
public var pauseButton = UIButton()

// Font size configuration
public var annotationConfig: AnnotationConfig = .default
public var switchCameraButton = UIButton()
public var shareButton = UIButton()
public var toolbar = UIView()
Expand Down Expand Up @@ -400,6 +403,34 @@ public class YOLOView: UIView, VideoCaptureDelegate {
/// - Returns: The current IoU threshold value.
public func getIouThreshold() -> Double { Double(sliderIoU.value) }

/// Updates the annotation configuration for font size and line width
public func setAnnotationConfig(_ config: AnnotationConfig) {
annotationConfig = config

// Check if this is likely an external display
let viewBounds = self.bounds
let maxDimension = max(viewBounds.width, viewBounds.height)

// Update existing bounding box views with new font size
for boxView in boundingBoxViews {
if maxDimension > 1000 {
// External display - use scaled values
let scaledFontSize = max(24, viewBounds.height * 0.03)
let scaledLineWidth = max(6, viewBounds.height * 0.005)
boxView.setFontSize(scaledFontSize)
boxView.setLineWidth(scaledLineWidth)
} else {
// Regular display - use annotation config
if let fontSize = config.fontSize {
boxView.setFontSize(fontSize)
}
if let lineWidth = config.lineWidth {
boxView.setLineWidth(lineWidth)
}
}
}
}

/// Sets all thresholds at once.
/// - Parameters:
/// - numItems: The maximum number of items to include.
Expand Down Expand Up @@ -429,6 +460,13 @@ public class YOLOView: UIView, VideoCaptureDelegate {
let scaledLineWidth = max(6, viewBounds.height * 0.005)
boxView.setFontSize(scaledFontSize)
boxView.setLineWidth(scaledLineWidth)
} else {
// Use annotation config for font size and line width on regular displays
let fontSize = annotationConfig.fontSize ?? 14.0
let lineWidth = annotationConfig.lineWidth ?? 4.0

boxView.setFontSize(fontSize)
boxView.setLineWidth(lineWidth)
}

boundingBoxViews.append(boxView)
Expand Down Expand Up @@ -729,9 +767,11 @@ public class YOLOView: UIView, VideoCaptureDelegate {
let fontSize: CGFloat

if maxDimension > 1000 {
// External display - use larger font
fontSize = max(36, viewBounds.height * 0.04)
} else {
fontSize = viewBounds.height * 0.035
// Regular display - use annotation config or default scaling
fontSize = annotationConfig.fontSize ?? (viewBounds.height * 0.035)
}

textLayer.font = UIFont.systemFont(ofSize: fontSize, weight: .semibold)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ extension Notification.Name {
static let thresholdDidChange = Notification.Name("ThresholdDidChange")
static let taskDidChange = Notification.Name("TaskDidChange")
static let detectionCountDidUpdate = Notification.Name("DetectionCountDidUpdate")
static let fontSizeDidChange = Notification.Name("FontSizeDidChange")
}
Loading