Port volleyball analyzer to iPhone App Store via Swift + CoreML
The only viable path for this ML stack is Swift native + CoreML. Python wrappers (BeeWare, Kivy) cannot run PyTorch/OpenCV on iOS. React Native cannot handle frame-by-frame video at 30fps.
weights/yolov8m_volleyball_v2.pt or yolov8s_volleyball_v2.pt# Direct export (skip ONNX intermediate — more reliable)
from ultralytics import YOLO
model = YOLO("weights/yolov8s_volleyball_v2.pt")
model.export(format="coreml", nms=False, imgsz=640)
# Produces yolov8s_volleyball_v2.mlpackage
Important: Export with nms=False — the NMS head doesn't convert cleanly. Implement NMS in Swift (~30 lines).
import coremltools as ct
import coremltools.optimize.coreml as cto
mlmodel = ct.models.MLModel("yolov8s_volleyball_v2.mlpackage")
config = cto.OptimizationConfig(
global_config=cto.OpLinearQuantizerConfig(mode="linear_symmetric")
)
mlmodel_q = cto.linear_quantize_weights(mlmodel, config)
mlmodel_q.save("YOLOVolleyball_INT8.mlpackage")
Use existing src/training/onnx_export.py to get ONNX, then:
mlmodel = ct.convert("tracknetv2.onnx",
inputs=[ct.ImageType(name="input", shape=(1, 9, 288, 512))],
minimum_deployment_target=ct.target.iOS16)
mlmodel.save("TrackNetV2.mlpackage")
| Device | Model | imgsz | Precision | FPS |
|---|---|---|---|---|
| iPhone 14 | YOLOv8s | 640 | INT8 | 40-55 |
| iPhone 14 | YOLOv8m | 640 | INT8 | 25-32 |
| iPhone 15 Pro | YOLOv8m | 640 | INT8 | 35-45 |
Critical: Drop imgsz from 1280 to 640 for mobile. 1280 is ~80ms/frame (below real-time).
| Python Source | Swift Replacement |
|---|---|
src/ball_tracking/kalman_filter.py | Swift + Accelerate (~100 lines) |
src/ball_tracking/volleyball_detector.py | Swift + vImage (4-gate logic) |
src/ball_tracking/hybrid_tracker.py | Swift orchestrator |
src/pose/pose_tracker.py | VNDetectHumanBodyPoseRequest (built-in, zero extra models) |
src/analysis/speed_estimator.py | Swift translation (pure math) |
config.yaml | UserDefaults / JSON settings store |
let model = try VNCoreMLModel(for: YOLOVolleyball().model)
let request = VNCoreMLRequest(model: model) { request, error in
// Parse detections, run through Kalman filter
}
request.imageCropAndScaleOption = .scaleFit
// AVCaptureVideoDataOutputSampleBufferDelegate:
func captureOutput(_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer, ...) {
let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer)
try? handler.perform([request])
}
Apple's VNDetectHumanBodyPoseRequest provides all 17 landmarks used in pose_tracker.py (shoulders, elbows, wrists, hips, knees, ankles) with zero additional models.
byte_track.py — redundant for single-ball tracking, Kalman filter handles itcv2.createBackgroundSubtractorMOG2 — use frame differencing via vImage insteadaugment=True) — not available in CoreML, compensate with lower confidence thresholdconfig.yaml thresholdsNSCameraUsageDescription — camera accessNSPhotoLibraryUsageDescription — if processing saved videosPrivacyInfo.xcprivacy — required since iOS 1711-16 weeks solo with iOS experience. Add 4-6 weeks if Swift is new.