Підрахунок об'єктів через AI з кадру камери у мобільних додатках
Підрахунок об'єктів через камеру видається простим, але приховує кілька нетривіальних проблем: перекриваючись об'єкти, об'єкти різних масштабів у одному кадрі, і головна пастка — подвійний підрахунок при русі камери. Промисловий склад, стадо тварин, монети на столі — кожен сценарій має свої особливості.
Два підходи: детекція проти оцінки щільності
Підрахунок на основі детекції — YOLOv8 або RT-DETR детектує кожен об'єкт; підрахунок = кількість детекцій. Працює при низькій щільності (до 50–100 об'єктів на кадр) коли об'єкти не перекриваються сильно.
Оцінка карти щільності — CNN передбачає карту щільності; підрахунок = інтеграл карти. Використовується для високої щільності: натовпи, зерно у бункері, клітини під мікроскопом. CSRNet, DMCount, BL-model — актуальні архітектури.
// iOS: вибір методу на основі очікуваної щільності
enum CountingStrategy {
case detection(model: VNCoreMLModel) // < 100 об'єктів
case densityMap(model: VNCoreMLModel) // > 100 об'єктів на кадр
case hybrid // адаптивний вибір
}
class AdaptiveObjectCounter {
func selectStrategy(for objectClass: CountableObject) -> CountingStrategy {
switch objectClass {
case .vehicle, .person_sparse:
return .detection(model: vehicleDetector)
case .crowd, .grain, .cell:
return .densityMap(model: densityEstimator)
case .product_shelf:
return .hybrid
}
}
}
Детекція на основі: реалізація з дедублікацією
class DetectionCounter {
func count(in sampleBuffer: CMSampleBuffer,
targetClass: String) async throws -> CountResult {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
throw CounterError.invalidFrame
}
let request = VNCoreMLRequest(model: detectionModel)
request.imageCropAndScaleOption = .scaleFill
try VNImageRequestHandler(cvPixelBuffer: pixelBuffer).perform([request])
let observations = (request.results as? [VNRecognizedObjectObservation]) ?? []
// Фільтрація за класом та впевненістю
let targetObjects = observations.filter { obs in
obs.labels.first?.identifier == targetClass &&
obs.confidence >= 0.4
}
// NMS для видалення дублюючих обмежуючих прямокутників
let deduplicated = applyNMS(targetObjects, iouThreshold: 0.45)
return CountResult(
count: deduplicated.count,
detections: deduplicated,
confidence: deduplicated.map { $0.confidence }.average()
)
}
private func applyNMS(_ observations: [VNRecognizedObjectObservation],
iouThreshold: Float) -> [VNRecognizedObjectObservation] {
// Сортування за впевненістю (спадання)
let sorted = observations.sorted { $0.confidence > $1.confidence }
var kept: [VNRecognizedObjectObservation] = []
for obs in sorted {
let overlapping = kept.contains { existingObs in
iou(obs.boundingBox, existingObs.boundingBox) > iouThreshold
}
if !overlapping { kept.append(obs) }
}
return kept
}
}
Vision framework не застосовує NMS автоматично з VNCoreMLRequest—це потрібно робити вручну, інакше об'єкти на межах кроп вважаються двічі.
Карта щільності для високої щільності
// Android: оцінка карти щільності через TFLite
class DensityMapCounter(context: Context) {
private val interpreter: Interpreter by lazy {
val model = FileUtil.loadMappedFile(context, "csrnet_lite.tflite")
Interpreter(model, Interpreter.Options().apply {
addDelegate(GpuDelegate())
numThreads = 4
})
}
fun estimate(bitmap: Bitmap): Int {
// Розмір вводу моделі — зазвичай 512×512 або кратний 16
val resized = Bitmap.createScaledBitmap(bitmap, 512, 512, true)
val inputBuffer = TensorImage.fromBitmap(resized).buffer
// Вихідний тензор — карта щільності того ж дозволу
val outputBuffer = TensorBuffer.createFixedSize(
intArrayOf(1, 512, 512, 1), DataType.FLOAT32
)
interpreter.run(inputBuffer, outputBuffer.buffer)
// Сума по всім пікселям карти щільності = оцінюваний підрахунок
val densitySum = outputBuffer.floatArray.sum()
// Масштабування: сума відповідає підрахунку об'єктів
return densitySum.roundToInt()
}
}
Підрахунок при русі камери: трекинг
Якщо користувач плавно панує камерою (склад, аудиторія), потрібен трекинг щоб уникнути подвійного підрахунку одних і тих же об'єктів:
class TrackingObjectCounter {
private var tracker = ByteTracker() // BYTE алгоритм трекингу
private var countedIds: Set<Int> = [] // унікальні ID у сесії
func processFrame(_ detections: [Detection]) -> TrackingCountResult {
let tracks = tracker.update(detections: detections)
// Нові ID = нові об'єкти, які входять у кадр
let newIds = tracks.map { $0.trackId }.filter { !countedIds.contains($0) }
countedIds.formUnion(newIds)
return TrackingCountResult(
currentFrameCount: tracks.count, // у кадрі зараз
totalUniqueCount: countedIds.count // всього у сесії
)
}
}
ByteTracker — один з найкращих алгоритмів трекингу для цієї задачі, стійкий до перекриття.
Кошторис за часом
Підрахунок на основі детекції з готовою моделлю (одного класу об'єктів) та UI лічильника займає 3–5 днів. Адаптивна система з детекцією + карта щільності, трекингом при русі камери, кількома класами об'єктів та підтримкою iOS + Android вимагає 1–2 тижнів.







