Реализация AI-трекинга объектов в видеопотоке мобильного приложения
Трекинг объектов — отдельная задача от детекции. Детектор говорит «здесь машина» на каждом кадре независимо. Трекер говорит «это та же машина #7, что была на прошлом кадре слева». Потеря идентичности объекта — типичная ошибка при наивном подходе: объект вышел за край кадра и вернулся — трекер присвоил ему новый ID.
Классификация задач трекинга
SOT (Single Object Tracking) — трекинг одного выбранного объекта. Пользователь тапает на объект → приложение следит за ним. Применение: спортивные трансляции, слежение за конкретным человеком в кадре. Алгоритмы: SiamFC, OSTrack, STARK.
MOT (Multi-Object Tracking) — одновременный трекинг всех объектов нужного класса. Применение: подсчёт посетителей, контроль трафика, производственные конвейеры. Алгоритмы: SORT, ByteTrack, StrongSORT, OC-SORT.
MOT: связка детектор + трекер
Стандартный pipeline для мобильных:
// 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 state
)
}
}
}
maxAge = 10 — трек живёт 10 кадров без детекции (объект за препятствием). При 30 FPS это 333 мс — достаточно для большинства случаев кратких окклюзий.
ByteTrack: лучше SORT при окклюзиях
SORT использует только детекции с confidence > порога. 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 confidence
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)
// Bounding box
drawRect(color = color, topLeft = rect.topLeft,
size = rect.size, style = Stroke(width = 3f))
// ID badge
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 недели.







