Трекинг об'єктів AI у відеопотоках для мобільних додатків
Трекинг об'єктів — окремої задача від детекції. Детектор каже «тут машина» на кожному кадрі незалежно. Трекер каже «це та сама машина #7, яка була на минулому кадрі зліва». Втрата ідентичності об'єкту — типова помилка при наївному підході: об'єкт вийшов за межі кадру і повернувся — трекер присвоїв йому новий ID.
Класифікація задач трекингу
SOT (Single Object Tracking) — трекинг одного вибраного об'єкту. Користувач тапає на об'єкт → додаток його відстежує. Застосування: спортивні трансляції, відстеження конкретної людини у кадрі. Алгоритми: SiamFC, OSTrack, STARK.
MOT (Multi-Object Tracking) — одночасний трекинг всіх об'єктів цільового класу. Застосування: підрахунок відвідувачів, контроль трафіку, виробничі конвеєри. Алгоритми: SORT, ByteTrack, StrongSORT, OC-SORT.
MOT: конвеєр детектора + трекера
Стандартний конвеєр для мобільних:
// iOS: YOLOv8 детекція + SORT трекинг
class MultiObjectTracker {
private let detector: YOLOv8Detector
private let tracker: SORTTracker
// SORT параметри—важливо налаштувати під вашу задачу
init(targetClass: String,
maxAge: Int = 10, // кадри без детекції перед видаленням треку
minHits: Int = 3, // кадри детекції для підтвердження треку
iouThreshold: Float = 0.3) {
self.detector = YOLOv8Detector(targetClass: targetClass)
self.tracker = SORTTracker(maxAge: maxAge,
minHits: minHits,
iouThreshold: iouThreshold)
}
func processFrame(_ pixelBuffer: CVPixelBuffer) async -> [TrackedObject] {
// 1. Детекція на поточному кадрі
let detections = await detector.detect(pixelBuffer)
// 2. Оновлення трекера
let tracks = tracker.update(detections: detections.map { det in
Detection(bbox: det.boundingBox, confidence: det.confidence)
})
// 3. Конвертація у TrackedObject
return tracks.map { track in
TrackedObject(
id: track.trackId,
boundingBox: track.bbox,
isConfirmed: track.hitStreak >= tracker.minHits,
velocity: track.kalmanFilter.velocity // зі стану Kalman
)
}
}
}
maxAge = 10 означає, що трек живе 10 кадрів без детекції (об'єкт за перешкодою). При 30 FPS це 333 мс—достатньо для більшості коротких перекриттів.
ByteTrack: краще за SORT при перекритті
SORT використовує лише детекції з високою впевненістю. ByteTrack використовує ВСІ детекції — включно з низько впевненими — для пов'язування з існуючими треками. Це різко знижує втрату треку під час перекриття:
// Android: ByteTrack пов'язування
class ByteTracker(
private val trackThresh: Float = 0.5f,
private val highThresh: Float = 0.6f,
private val matchThresh: Float = 0.8f
) {
private val trackedStracks = mutableListOf<STrack>()
private val lostStracks = mutableListOf<STrack>()
fun update(detections: List<Detection>): List<STrack> {
// Розділення детекцій на high/low впевненість
val highDetections = detections.filter { it.confidence >= highThresh }
val lowDetections = detections.filter { it.confidence in trackThresh..<highThresh }
// 1. Пов'язування high-confidence з активними треками
val (matches1, unmatched_tracks1, unmatched_dets1) =
linearAssignment(trackedStracks, highDetections, matchThresh)
// 2. Пов'язування low-confidence з непов'язаними треками з кроку 1
val (matches2, _, _) =
linearAssignment(unmatched_tracks1, lowDetections, 0.5f)
// 3. Ініціалізація нових треків для непов'язаних high-conf детекцій
val newTracks = unmatched_dets1.map { STrack(it) }
return (matches1 + matches2).map { it.track } + newTracks
}
}
SOT: тап-для-трекингу
// iOS: користувач вибирає об'єкт тапом, додаток його відстежує
class SingleObjectTracker {
// Використовуємо Vision VNTrackObjectRequest
private var trackingRequest: VNTrackObjectRequest?
func initializeTracking(at point: CGPoint, in frame: CVPixelBuffer) {
let observation = VNDetectedObjectObservation(
boundingBox: CGRect(center: point, size: CGSize(width: 0.1, height: 0.1))
)
trackingRequest = VNTrackObjectRequest(
detectedObjectObservation: observation
) { [weak self] request, _ in
guard let obs = request.results?.first as? VNDetectedObjectObservation else { return }
self?.delegate?.didUpdateTracking(boundingBox: obs.boundingBox,
confidence: obs.confidence)
}
trackingRequest?.trackingLevel = .accurate // vs .fast
}
func trackInFrame(_ pixelBuffer: CVPixelBuffer) {
guard let request = trackingRequest else { return }
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer)
try? handler.perform([request])
}
}
trackingLevel = .accurate використовує важчий трекер (CorrelateBased vs Optical Flow). Різниця: .fast — 50+ FPS, втрачає трек при швидких рухах. .accurate — 20–30 FPS, більш стійкий до швидких об'єктів. Виберіть на основі вашої задачі.
Рендеринг треків
@Composable
fun TrackingOverlay(
tracks: List<TrackedObject>,
imageSize: Size,
modifier: Modifier = Modifier
) {
val colors = remember { generateTrackColors(maxTracks = 100) }
Canvas(modifier = modifier) {
tracks.forEach { track ->
val color = colors[track.id % colors.size]
val rect = track.boundingBox.toScreenRect(imageSize, size)
// Обмежуючий прямокутник
drawRect(color = color, topLeft = rect.topLeft,
size = rect.size, style = Stroke(width = 3f))
// ID бейдж
drawIntoCanvas { canvas ->
canvas.nativeCanvas.drawText(
"ID: ${track.id}",
rect.left + 4f,
rect.top + 20f,
Paint().apply { this.color = color.toArgb(); textSize = 32f }
)
}
// Вектор швидкості (опціонально)
if (track.velocity != null) {
drawLine(
color = color.copy(alpha = 0.6f),
start = rect.center,
end = rect.center + track.velocity.toOffset(scale = 20f),
strokeWidth = 2f
)
}
}
}
}
Кошторис за часом
SOT (Vision VNTrackObjectRequest) з тапом для вибору об'єкту займає 2–3 дні. MOT з YOLOv8 + ByteTrack, рендерингом треків, кількома класами об'єктів та підтримкою iOS + Android вимагає 1–2 тижнів.







