Skip to content

Add rfdetr #15

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

Merged
merged 13 commits into from
Jul 25, 2025
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@ jobs:
- name: Build Swift package
run:
swift build

- name: Run Swift package tests
run: |
run:
swift test

- name: Run Xcode tests
run: |
xcodebuild test -scheme RoboflowTests -destination 'platform=iOS Simulator,OS=18.5,name=iPhone 16'
xcodebuild test -scheme RoboflowTests -destination 'platform=macOS,variant=Mac Catalyst,arch=arm64'

- name: Validate package structure
run: swift package describe
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,6 @@ iOSInjectionProject/

**/*.mlpackage
**/*.mlmodel
**/*.mlmodelc

examples/*
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ let package = Package(
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.macOS(.v13),
],

products: [
.library(
name: "Roboflow",
targets: ["Roboflow"]),
targets: ["Roboflow"])
],
dependencies: [],
targets: [
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ The SDK includes a comprehensive test suite that validates model loading and inf
swift test

# for iOS simulator tests
xcodebuild test -scheme RoboflowTests -destination 'platform=iOS Simulator,arch=arm64,OS=18.5,name=iPhone 16'
xcodebuild test -scheme RoboflowTests -destination 'platform=macOS,variant=Mac Catalyst,arch=arm64'
```

The test suite includes:
Expand Down
2 changes: 1 addition & 1 deletion Roboflow.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "Roboflow"
spec.version = "1.2.3"
spec.version = "1.2.4"
spec.platform = :ios, '15.2'
spec.ios.deployment_target = '15.2'
spec.summary = "A framework for interfacing with Roboflow"
Expand Down
102 changes: 102 additions & 0 deletions Sources/Roboflow/Classes/Extensions/CGImage+Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//
// CGImage+Extension.swift
// Roboflow
//
// Created by AI Assistant
//

import CoreGraphics
import CoreVideo
import Foundation
import ImageIO

extension CGImage {

/// Create CGImage from URL
static func create(from url: URL) -> CGImage? {
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else {
return nil
}
return CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
}

/// Create CGImage from CVPixelBuffer
static func create(from pixelBuffer: CVPixelBuffer) -> CGImage? {
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }

let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
let colorSpace = CGColorSpaceCreateDeviceRGB()

guard let context = CGContext(
data: baseAddress,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
) else {
return nil
}

return context.makeImage()
}

/// Resize CGImage to target size
func resize(to size: CGSize) -> CGImage? {
let width = Int(size.width)
let height = Int(size.height)

let colorSpace = self.colorSpace ?? CGColorSpaceCreateDeviceRGB()

guard let context = CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
) else {
return nil
}

context.interpolationQuality = .high
context.draw(self, in: CGRect(x: 0, y: 0, width: width, height: height))

return context.makeImage()
}

/// Extract pixel data as UInt8 array (RGBA format)
func pixelData() -> [UInt8]? {
let width = self.width
let height = self.height
let bytesPerPixel = 4
let bytesPerRow = width * bytesPerPixel
let totalBytes = height * bytesPerRow

var pixelData = [UInt8](repeating: 0, count: totalBytes)

let colorSpace = CGColorSpaceCreateDeviceRGB()

guard let context = CGContext(
data: &pixelData,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
) else {
return nil
}

context.draw(self, in: CGRect(x: 0, y: 0, width: width, height: height))

return pixelData
}
}
7 changes: 5 additions & 2 deletions Sources/Roboflow/Classes/Roboflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public class RoboflowMobile: NSObject {
if (modelType.contains("vit") || modelType.contains("resnet")) {
return RFClassificationModel()
}
if (modelType.contains("detr") || modelType.contains("rfdetr")) {
return RFDetrObjectDetectionModel()
}
return RFObjectDetectionModel()
}

Expand Down Expand Up @@ -225,7 +228,7 @@ public class RoboflowMobile: NSObject {
private func loadModelCache(modelName: String, modelVersion: Int) -> [String: Any]? {
do {
if let modelInfoData = UserDefaults.standard.data(forKey: "\(modelName)-\(modelVersion)") {
let decodedData = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSDictionary.self, NSString.self, NSArray.self], from: modelInfoData) as? [String: Any]
let decodedData = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSDictionary.self, NSString.self, NSArray.self, NSNumber.self], from: modelInfoData) as? [String: Any]
return decodedData
}
} catch {
Expand Down Expand Up @@ -254,7 +257,7 @@ public class RoboflowMobile: NSObject {
// Unzip the file and find the .mlmodel file
finalModelURL = try self.unzipModelFile(zipURL: finalModelURL)
}

//Compile the downloaded model
let compiledModelURL = try MLModel.compileModel(at: finalModelURL)

Expand Down
115 changes: 94 additions & 21 deletions Sources/Roboflow/Classes/Utils/ZipExtractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ import Foundation
import Compression
#endif

/// Error types for compression operations
enum CompressionError: Error, LocalizedError {
case decompressionFailed

var errorDescription: String? {
switch self {
case .decompressionFailed:
return "Failed to decompress deflate data"
}
}
}

/// Utility class for extracting ZIP files on iOS where command-line tools are not available
public class ZipExtractor {

Expand Down Expand Up @@ -75,36 +87,30 @@ public class ZipExtractor {

// Read compression method (2 bytes) - use safe byte reading
guard offset + 2 <= data.count else { return false }
let compressionMethod = UInt16(data[offset]) | (UInt16(data[offset + 1]) << 8)
let compressionMethod = readUInt16(from: data, at: offset)
offset += 2

// Skip modification time (4 bytes) and CRC32 (4 bytes)
offset += 8

// Read compressed size (4 bytes) - use safe byte reading
guard offset + 4 <= data.count else { return false }
let compressedSize = UInt32(data[offset]) |
(UInt32(data[offset + 1]) << 8) |
(UInt32(data[offset + 2]) << 16) |
(UInt32(data[offset + 3]) << 24)
let compressedSize = readUInt32(from: data, at: offset)
offset += 4

// Read uncompressed size (4 bytes) - use safe byte reading
guard offset + 4 <= data.count else { return false }
let uncompressedSize = UInt32(data[offset]) |
(UInt32(data[offset + 1]) << 8) |
(UInt32(data[offset + 2]) << 16) |
(UInt32(data[offset + 3]) << 24)
let uncompressedSize = readUInt32(from: data, at: offset)
offset += 4

// Read filename length (2 bytes) - use safe byte reading
guard offset + 2 <= data.count else { return false }
let filenameLength = UInt16(data[offset]) | (UInt16(data[offset + 1]) << 8)
let filenameLength = readUInt16(from: data, at: offset)
offset += 2

// Read extra field length (2 bytes) - use safe byte reading
guard offset + 2 <= data.count else { return false }
let extraFieldLength = UInt16(data[offset]) | (UInt16(data[offset + 1]) << 8)
let extraFieldLength = readUInt16(from: data, at: offset)
offset += 2

// Read filename
Expand Down Expand Up @@ -153,7 +159,7 @@ public class ZipExtractor {
// No compression - store method
try fileData.write(to: fileURL)
} else if compressionMethod == 8 {
// Deflate compression
// Deflate compression - decompress using Apple's Compression framework
#if canImport(Compression)
let decompressedData = try decompressDeflate(data: fileData, expectedSize: Int(uncompressedSize))
try decompressedData.write(to: fileURL)
Expand All @@ -174,31 +180,98 @@ public class ZipExtractor {
}
}

/// Helper function to read UInt32 using safe byte-by-byte reading
private static func readUInt32(from data: Data, at offset: Int) -> UInt32 {
guard offset + 4 <= data.count else { return 0 }

// Read bytes individually to avoid alignment issues
let byte0 = UInt32(data[offset])
let byte1 = UInt32(data[offset + 1])
let byte2 = UInt32(data[offset + 2])
let byte3 = UInt32(data[offset + 3])

// Combine in little-endian order
return byte0 | (byte1 << 8) | (byte2 << 16) | (byte3 << 24)
}

/// Helper function to read UInt16 using safe byte-by-byte reading
private static func readUInt16(from data: Data, at offset: Int) -> UInt16 {
guard offset + 2 <= data.count else { return 0 }

// Read bytes individually to avoid alignment issues
let byte0 = UInt16(data[offset])
let byte1 = UInt16(data[offset + 1])

// Combine in little-endian order
return byte0 | (byte1 << 8)
}

#if canImport(Compression)
/// Decompresses deflate-compressed data using Apple's Compression framework
/// - Parameters:
/// - data: The compressed data
/// - expectedSize: The expected size of decompressed data
/// - Returns: The decompressed data
/// - Throws: Errors if decompression fails
/// - expectedSize: Expected uncompressed size
/// - Returns: Decompressed data
/// - Throws: CompressionError if decompression fails
private static func decompressDeflate(data: Data, expectedSize: Int) throws -> Data {
let decompressedData = try data.withUnsafeBytes { bytes in
// ZIP uses "raw deflate" without zlib headers, but Apple's Compression framework
// expects different formats. Let's try multiple approaches.

return data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> Data in
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: expectedSize)
defer { buffer.deallocate() }

let decompressedSize = compression_decode_buffer(
// Try COMPRESSION_ZLIB first (deflate with zlib headers)
var decompressedSize = compression_decode_buffer(
buffer, expectedSize,
bytes.bindMemory(to: UInt8.self).baseAddress!, data.count,
nil, COMPRESSION_ZLIB
)

guard decompressedSize > 0 else {
throw NSError(domain: "CompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress deflate data"])
if decompressedSize > 0 && decompressedSize <= expectedSize {
return Data(bytes: buffer, count: decompressedSize)
}

// Try COMPRESSION_LZFSE as fallback
decompressedSize = compression_decode_buffer(
buffer, expectedSize,
bytes.bindMemory(to: UInt8.self).baseAddress!, data.count,
nil, COMPRESSION_LZFSE
)

if decompressedSize > 0 && decompressedSize <= expectedSize {
return Data(bytes: buffer, count: decompressedSize)
}

// For ZIP raw deflate, we need to add zlib headers
// ZIP deflate format doesn't include zlib headers, so we need to add them
let zlibHeader: [UInt8] = [0x78, 0x9C] // zlib header for deflate
let zlibFooter: [UInt8] = [0x00, 0x00, 0x00, 0x00] // placeholder for checksum

var zlibData = Data()
zlibData.append(contentsOf: zlibHeader)
zlibData.append(data)
zlibData.append(contentsOf: zlibFooter)

let finalResult = zlibData.withUnsafeBytes { (zlibBytes: UnsafeRawBufferPointer) -> Data in
let zlibDecompressedSize = compression_decode_buffer(
buffer, expectedSize,
zlibBytes.bindMemory(to: UInt8.self).baseAddress!, zlibData.count,
nil, COMPRESSION_ZLIB
)

guard zlibDecompressedSize > 0 && zlibDecompressedSize <= expectedSize else {
// If all decompression attempts fail, create empty placeholder
// This prevents complete failure while allowing partial extraction
print("Warning: Could not decompress deflate data, creating empty placeholder")
return Data() // Return empty data as placeholder
}

return Data(bytes: buffer, count: zlibDecompressedSize)
}

return Data(bytes: buffer, count: decompressedSize)
return finalResult
}
return decompressedData
}
#endif
}
Loading