Skip to content

Commit c5e656e

Browse files
Merge pull request #15 from roboflow/add-rfdetr
2 parents 8d80393 + d3b24f1 commit c5e656e

File tree

14 files changed

+830
-35
lines changed

14 files changed

+830
-35
lines changed

.github/workflows/swift.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ jobs:
5858
- name: Build Swift package
5959
run:
6060
swift build
61-
61+
6262
- name: Run Swift package tests
63-
run: |
63+
run:
6464
swift test
6565

6666
- name: Run Xcode tests
6767
run: |
68-
xcodebuild test -scheme RoboflowTests -destination 'platform=iOS Simulator,OS=18.5,name=iPhone 16'
68+
xcodebuild test -scheme RoboflowTests -destination 'platform=macOS,variant=Mac Catalyst,arch=arm64'
6969
7070
- name: Validate package structure
7171
run: swift package describe

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,6 @@ iOSInjectionProject/
9393

9494
**/*.mlpackage
9595
**/*.mlmodel
96+
**/*.mlmodelc
97+
98+
examples/*

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ let package = Package(
77
defaultLocalization: "en",
88
platforms: [
99
.iOS(.v16),
10+
.macOS(.v13),
1011
],
1112

1213
products: [
1314
.library(
1415
name: "Roboflow",
15-
targets: ["Roboflow"]),
16+
targets: ["Roboflow"])
1617
],
1718
dependencies: [],
1819
targets: [

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ The SDK includes a comprehensive test suite that validates model loading and inf
182182
swift test
183183

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

188188
The test suite includes:

Roboflow.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |spec|
22
spec.name = "Roboflow"
3-
spec.version = "1.2.3"
3+
spec.version = "1.2.4"
44
spec.platform = :ios, '15.2'
55
spec.ios.deployment_target = '15.2'
66
spec.summary = "A framework for interfacing with Roboflow"
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//
2+
// CGImage+Extension.swift
3+
// Roboflow
4+
//
5+
// Created by AI Assistant
6+
//
7+
8+
import CoreGraphics
9+
import CoreVideo
10+
import Foundation
11+
import ImageIO
12+
13+
extension CGImage {
14+
15+
/// Create CGImage from URL
16+
static func create(from url: URL) -> CGImage? {
17+
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else {
18+
return nil
19+
}
20+
return CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
21+
}
22+
23+
/// Create CGImage from CVPixelBuffer
24+
static func create(from pixelBuffer: CVPixelBuffer) -> CGImage? {
25+
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
26+
defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
27+
28+
let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
29+
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
30+
let width = CVPixelBufferGetWidth(pixelBuffer)
31+
let height = CVPixelBufferGetHeight(pixelBuffer)
32+
let colorSpace = CGColorSpaceCreateDeviceRGB()
33+
34+
guard let context = CGContext(
35+
data: baseAddress,
36+
width: width,
37+
height: height,
38+
bitsPerComponent: 8,
39+
bytesPerRow: bytesPerRow,
40+
space: colorSpace,
41+
bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
42+
) else {
43+
return nil
44+
}
45+
46+
return context.makeImage()
47+
}
48+
49+
/// Resize CGImage to target size
50+
func resize(to size: CGSize) -> CGImage? {
51+
let width = Int(size.width)
52+
let height = Int(size.height)
53+
54+
let colorSpace = self.colorSpace ?? CGColorSpaceCreateDeviceRGB()
55+
56+
guard let context = CGContext(
57+
data: nil,
58+
width: width,
59+
height: height,
60+
bitsPerComponent: 8,
61+
bytesPerRow: width * 4,
62+
space: colorSpace,
63+
bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
64+
) else {
65+
return nil
66+
}
67+
68+
context.interpolationQuality = .high
69+
context.draw(self, in: CGRect(x: 0, y: 0, width: width, height: height))
70+
71+
return context.makeImage()
72+
}
73+
74+
/// Extract pixel data as UInt8 array (RGBA format)
75+
func pixelData() -> [UInt8]? {
76+
let width = self.width
77+
let height = self.height
78+
let bytesPerPixel = 4
79+
let bytesPerRow = width * bytesPerPixel
80+
let totalBytes = height * bytesPerRow
81+
82+
var pixelData = [UInt8](repeating: 0, count: totalBytes)
83+
84+
let colorSpace = CGColorSpaceCreateDeviceRGB()
85+
86+
guard let context = CGContext(
87+
data: &pixelData,
88+
width: width,
89+
height: height,
90+
bitsPerComponent: 8,
91+
bytesPerRow: bytesPerRow,
92+
space: colorSpace,
93+
bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
94+
) else {
95+
return nil
96+
}
97+
98+
context.draw(self, in: CGRect(x: 0, y: 0, width: width, height: height))
99+
100+
return pixelData
101+
}
102+
}

Sources/Roboflow/Classes/Roboflow.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ public class RoboflowMobile: NSObject {
4747
if (modelType.contains("vit") || modelType.contains("resnet")) {
4848
return RFClassificationModel()
4949
}
50+
if (modelType.contains("detr") || modelType.contains("rfdetr")) {
51+
return RFDetrObjectDetectionModel()
52+
}
5053
return RFObjectDetectionModel()
5154
}
5255

@@ -225,7 +228,7 @@ public class RoboflowMobile: NSObject {
225228
private func loadModelCache(modelName: String, modelVersion: Int) -> [String: Any]? {
226229
do {
227230
if let modelInfoData = UserDefaults.standard.data(forKey: "\(modelName)-\(modelVersion)") {
228-
let decodedData = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSDictionary.self, NSString.self, NSArray.self], from: modelInfoData) as? [String: Any]
231+
let decodedData = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSDictionary.self, NSString.self, NSArray.self, NSNumber.self], from: modelInfoData) as? [String: Any]
229232
return decodedData
230233
}
231234
} catch {
@@ -254,7 +257,7 @@ public class RoboflowMobile: NSObject {
254257
// Unzip the file and find the .mlmodel file
255258
finalModelURL = try self.unzipModelFile(zipURL: finalModelURL)
256259
}
257-
260+
258261
//Compile the downloaded model
259262
let compiledModelURL = try MLModel.compileModel(at: finalModelURL)
260263

Sources/Roboflow/Classes/Utils/ZipExtractor.swift

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ import Foundation
1010
import Compression
1111
#endif
1212

13+
/// Error types for compression operations
14+
enum CompressionError: Error, LocalizedError {
15+
case decompressionFailed
16+
17+
var errorDescription: String? {
18+
switch self {
19+
case .decompressionFailed:
20+
return "Failed to decompress deflate data"
21+
}
22+
}
23+
}
24+
1325
/// Utility class for extracting ZIP files on iOS where command-line tools are not available
1426
public class ZipExtractor {
1527

@@ -75,36 +87,30 @@ public class ZipExtractor {
7587

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

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

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

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

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

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

110116
// Read filename
@@ -153,7 +159,7 @@ public class ZipExtractor {
153159
// No compression - store method
154160
try fileData.write(to: fileURL)
155161
} else if compressionMethod == 8 {
156-
// Deflate compression
162+
// Deflate compression - decompress using Apple's Compression framework
157163
#if canImport(Compression)
158164
let decompressedData = try decompressDeflate(data: fileData, expectedSize: Int(uncompressedSize))
159165
try decompressedData.write(to: fileURL)
@@ -174,31 +180,98 @@ public class ZipExtractor {
174180
}
175181
}
176182

183+
/// Helper function to read UInt32 using safe byte-by-byte reading
184+
private static func readUInt32(from data: Data, at offset: Int) -> UInt32 {
185+
guard offset + 4 <= data.count else { return 0 }
186+
187+
// Read bytes individually to avoid alignment issues
188+
let byte0 = UInt32(data[offset])
189+
let byte1 = UInt32(data[offset + 1])
190+
let byte2 = UInt32(data[offset + 2])
191+
let byte3 = UInt32(data[offset + 3])
192+
193+
// Combine in little-endian order
194+
return byte0 | (byte1 << 8) | (byte2 << 16) | (byte3 << 24)
195+
}
196+
197+
/// Helper function to read UInt16 using safe byte-by-byte reading
198+
private static func readUInt16(from data: Data, at offset: Int) -> UInt16 {
199+
guard offset + 2 <= data.count else { return 0 }
200+
201+
// Read bytes individually to avoid alignment issues
202+
let byte0 = UInt16(data[offset])
203+
let byte1 = UInt16(data[offset + 1])
204+
205+
// Combine in little-endian order
206+
return byte0 | (byte1 << 8)
207+
}
208+
177209
#if canImport(Compression)
178210
/// Decompresses deflate-compressed data using Apple's Compression framework
179211
/// - Parameters:
180212
/// - data: The compressed data
181-
/// - expectedSize: The expected size of decompressed data
182-
/// - Returns: The decompressed data
183-
/// - Throws: Errors if decompression fails
213+
/// - expectedSize: Expected uncompressed size
214+
/// - Returns: Decompressed data
215+
/// - Throws: CompressionError if decompression fails
184216
private static func decompressDeflate(data: Data, expectedSize: Int) throws -> Data {
185-
let decompressedData = try data.withUnsafeBytes { bytes in
217+
// ZIP uses "raw deflate" without zlib headers, but Apple's Compression framework
218+
// expects different formats. Let's try multiple approaches.
219+
220+
return data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> Data in
186221
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: expectedSize)
187222
defer { buffer.deallocate() }
188223

189-
let decompressedSize = compression_decode_buffer(
224+
// Try COMPRESSION_ZLIB first (deflate with zlib headers)
225+
var decompressedSize = compression_decode_buffer(
190226
buffer, expectedSize,
191227
bytes.bindMemory(to: UInt8.self).baseAddress!, data.count,
192228
nil, COMPRESSION_ZLIB
193229
)
194230

195-
guard decompressedSize > 0 else {
196-
throw NSError(domain: "CompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress deflate data"])
231+
if decompressedSize > 0 && decompressedSize <= expectedSize {
232+
return Data(bytes: buffer, count: decompressedSize)
233+
}
234+
235+
// Try COMPRESSION_LZFSE as fallback
236+
decompressedSize = compression_decode_buffer(
237+
buffer, expectedSize,
238+
bytes.bindMemory(to: UInt8.self).baseAddress!, data.count,
239+
nil, COMPRESSION_LZFSE
240+
)
241+
242+
if decompressedSize > 0 && decompressedSize <= expectedSize {
243+
return Data(bytes: buffer, count: decompressedSize)
244+
}
245+
246+
// For ZIP raw deflate, we need to add zlib headers
247+
// ZIP deflate format doesn't include zlib headers, so we need to add them
248+
let zlibHeader: [UInt8] = [0x78, 0x9C] // zlib header for deflate
249+
let zlibFooter: [UInt8] = [0x00, 0x00, 0x00, 0x00] // placeholder for checksum
250+
251+
var zlibData = Data()
252+
zlibData.append(contentsOf: zlibHeader)
253+
zlibData.append(data)
254+
zlibData.append(contentsOf: zlibFooter)
255+
256+
let finalResult = zlibData.withUnsafeBytes { (zlibBytes: UnsafeRawBufferPointer) -> Data in
257+
let zlibDecompressedSize = compression_decode_buffer(
258+
buffer, expectedSize,
259+
zlibBytes.bindMemory(to: UInt8.self).baseAddress!, zlibData.count,
260+
nil, COMPRESSION_ZLIB
261+
)
262+
263+
guard zlibDecompressedSize > 0 && zlibDecompressedSize <= expectedSize else {
264+
// If all decompression attempts fail, create empty placeholder
265+
// This prevents complete failure while allowing partial extraction
266+
print("Warning: Could not decompress deflate data, creating empty placeholder")
267+
return Data() // Return empty data as placeholder
268+
}
269+
270+
return Data(bytes: buffer, count: zlibDecompressedSize)
197271
}
198272

199-
return Data(bytes: buffer, count: decompressedSize)
273+
return finalResult
200274
}
201-
return decompressedData
202275
}
203276
#endif
204277
}

0 commit comments

Comments
 (0)