Всем привет! На связи Владимир Бойко и Александр Лахонин, мы занимаемся продуктом «Умная камера» в Центре технологий искусственного интеллекта Т-Банка. В статье рассказываем, как в суперсжатые сроки реализовали распознавание номеров телефонов on-device на iOS. Результаты работы мы представили 40 тысячам гостей на стенде Т-Банка нашего продукта на ИТ-Пикнике 2024 — ежегодном фестивале для айтишников. 

Мы расскажем о технических достижениях и вызовах, с которыми столкнулись, поделимся решениями, которые разработали специально для мероприятия, а еще теми, что уже встроены в наши приложения и успешно используются

Какая задача перед нами стояла

В июле нам нужно было сделать стенд Умной камеры в зоне Т-Банка, где компания представляла свои продукты и технологии ИИ на ИТ-Пикнике. У нас было три недели и пять человек в команде, чтобы придумать и воплотить свою идею. Мы постарались сделать приложение, которое было бы интересным и взрослым, и детям.

Мы решили показать гостям, что компьютерное зрение работает точнее и быстрее, чем человек. Умная камера от Т-Банка умеет определять разные объекты — от рукописных номеров телефонов до кошек и собак. Специально для стенда мы придумали интерактивную игру из двух раундов, в которой пользователи соревновались между собой и с Умной камерой. Суть игры — распознать номера телефонов за меньшее время. 

В первом раунде пользователям нужно было ввести руками номер телефона с экрана телевизора перед ними. Во втором — отсканировать Умной камерой напечатанный номер телефона с того же самого телевизора. Побеждал тот пользователь, у которого время по итогам двух раундов было меньше. В конце отображали лидерборд с топ-3 лучшими результатами за все время. 

Таким составом выступали на стенде: два разработчика, продакт, техлид и ML-эксперт из области компьютерного зрения
Таким составом выступали на стенде: два разработчика, продакт, техлид и ML-эксперт из области компьютерного зрения

Написали с нуля два приложения: клиентское и серверное. Хотели сделать игру максимально автономной. По опыту прошлого ИТ-Пикника из-за большого количества посетителей на месте могли быть серьезные проблемы с доступом в интернет. Это повлияло на выбор технологий во время реализации игры.


Клиентское приложение состояло из шести экранов:

  • ввод никнейма;

  • ожидание второго игрока;

  • ввод номера телефона;

  • промежуточные результаты;

  • камера;

  • финальный экран с результатами и местом.

Окна клиента
Окна клиента

Серверное приложение — основной центр управления. С него управляли двумя клиентами, и на нем же была логика сетевого взаимодействия: синхронизация между раундами, таймеры, подсчет результатов и записи в БД, валидации никнеймов. Вторая часть приложения транслировалась на экран телевизора — там были инструкции, лидерборд, таймеры и сами номера.

Экран инструкций
Экран инструкций
Промежуточные результаты
Промежуточные результаты
Таймер
Таймер
Экран номера телефона
Экран номера телефона
Лидерборд
Лидерборд

Стек технологий

Нам не нужно было поддерживать большой парк устройств и можно было использовать самую актуальную iOS, мы могли применять самые современные технологии и подходы. Поэтому решили использовать:

  • SwiftUI + Combine для верстки.

  • SwiftData для хранения данных.

  • Swift Concurrency для решения задач многопоточности.

  • Связку TensorFlowLite + Accelerate + Metal для определения номеров. В качестве альтернативы и для замеров качества нативных решений — Vision.

  • Multipeer Connectivity для реализации сетевого соединения. Позже мы перешли на использование WebSocket.

Оба приложения написаны на SwiftUI, потому что на нем простые экраны создавать быстрее и проще. У нас не было специфичных анимаций или сильно кастомных элементов. Combine использовали, потому что он хорошо интегрируется со SwiftUI.

Серверное приложение писалось под macOS, и там тоже не возникло проблем с версткой. Плюс ко всему, на macOS нет UIKit, поэтому, используя SwiftUI, мы решали проблему переиспользуемости некоторых UI компонентов на обеих платформах.

В клиентском приложении мы предусмотрели дебаг-меню, которое упростило тестирование и управление. В этом меню можно изучать логи событий, открыть нужный экран, протестировать ML-модели и изменить настройки камеры.

Окно дебаг-меню в клиентском приложении
Окно дебаг-меню в клиентском приложении

В серверном приложении мы тоже предусмотрели дебаг-меню, которое обогатили логами событий и значениями базы данных. На вкладке «Управление клиентами» в реальном времени можно было посмотреть состояние подключенных устройств и открыть любой экран, чтобы протестировать его или вообще перезапустить весь сценарий.

Дебаг-меню в серверном приложении
Дебаг-меню в серверном приложении

Добавили возможность ручного управления обоими клиентами — в каких-то экстренных случаях мы вешали на весь экран заставку о проведении работ и уходили на маленький перерыв. Но таких перерывов было очень-очень мало.  

Проблемы, решения и технологии

Хранение данных. Нам необходимо было логировать результаты пользователей во время соревнования: время камеры, время человека, общее и никнеймы. Для этого написали локальную БД на серверном приложении с помощью SwiftData, поскольку, по документации и примерам от Apple, с ней намного проще работать, чем с той же CoreData. Все построено на макросах и заводится с пол-оборота. Так и было. 

Соединение клиентов с сервером. Для реализации соединения между клиентом и сервером с самого начала решили использовать Multipeer Connectivity. Если судить по описанию, технология отличная и идеально подходит для наших нужд. Она не требует локальной сети, все работает из коробки и обеспечивает простое подключение. 

Multipeer Connectivity позволяет соединяться двум и более устройствам, которые не подключены к одной локальной сети (нет явного IP-адреса), и для поиска устройств используется Bluetooth или Wi-Fi.

Общая схема взаимодействия клиентов и сервера, где сервер соединен по проводу с телевизором, а клиенты держат связь через Wi-Fi
Общая схема взаимодействия клиентов и сервера, где сервер соединен по проводу с телевизором, а клиенты держат связь через Wi-Fi

В процессе тестов выяснилось, что Multipeer Connectivity недостаточно надежна в плане поддержания соединения. В любой момент соединение могло пропасть: клиент отображал сервер в сети, в то время как сервер не видел клиента. Из-за сжатых сроков у нас не получилось уделить достаточно времени отладке, а еще мы нашли подтверждение тому, что эта технология нам не подойдет. Тогда мы приняли решение пойти в сторону проверенного и известного решения — WebSocket.

Нам понадобился маршрутизатор для раздачи локальной сети, он никуда не был подключен, кроме розетки. На маршрутизаторе мы установили статический IP-адрес для сервера и жестко прописали его в клиентском приложении. 

Все работало как нужно, никаких сбоев не было. Практически…

Во время тестов выяснилось, что большинство пользователей по привычке нажимали кнопку блокировки телефона и это автоматически разрывало соединение. Решение заключалось в автоматическом восстановлении соединения с сервером методом AppDelegate. И вот тогда все заработало как часы.

Настройка распознавания номеров. Первое, о чем мы подумали: есть крутой нативный фреймворк от Apple — Vision, с хорошей документацией, простой интеграцией и понятными возможностями. Мы очень легко и просто завели первую версию, которая, на первый взгляд, быстро и точно все сканировала. 

Позже столкнулись с тем, что, если рядом с номером будет какой-нибудь текст, мы его тоже распознаем. Вроде бы не такая большая проблема — добавим регулярку проверяющую, что в строке 11 цифр. А если в номере будет «+» и «("")», такой номер не пройдет. 

Чуть усложнили регулярное выражение. И вроде бы теперь точно все идеально, но периодически мы стали замечать, что некоторые номера, особенно рукописные, не проходят регулярные выражения. 

Начали выяснять и поняли, что Vision не понимает контекст и что буква о очень похожа на 0, а цифра 1 — на i и l. Нам удалось найти все или почти все кейсы схожих символов и цифр, и получился метод, который уже кратно повышал точность, но разные интересные кейсы все равно продолжали и продолжали возникать. 

Мы попытались заменить возможные неверные интерпретации Vision-символов на цифры. Предполагаем, что максимальное число замен будет три. Если их будет больше, вероятно, это не номер, который нас интересует.

Разница между двумя режимами распознавания
Разница между двумя режимами распознавания

Подробнее про разницу между fast и accurate можно почитать на Recognizing Text in Images.

Функция по замене символов в выходном результате:

extension Character {
    func getSimilarCharacterIfNotIn(allowedChars: String) -> Character? {
        let conversionTable = [
            "s": "5",
            "S": "5",
            "o": "0",
            "Q": "0",
            "O": "0",
            "i": "1",
            "l": "1",
            "I": "1",
            "B": "8",
            "в": "8",
            "b": "8",
            "з": "3",
            "о": "0",
            "О": "0",
            ":": "8"
        ]
        let maxSubstitutions = 3
        var current = String(self)
        var counter = 0
        while !allowedChars.contains(current) && counter < maxSubstitutions {
            if let altChar = conversionTable[current] {
                current = altChar
                counter += 1
            } else {
                break
            }
        }
         
        return current.first
    }
}

Мы не на шутку задумались: а ведь у нас есть своя модель, которая в проде сканирует тысячи номеров телефонов в день и работает очень хорошо. Но есть маленькое но: эта модель развернута на бекенде и нам нужно перенести ее в наше приложение. 

Приключение на 15 минут, подумали мы...

Работа с нашими моделями. Мы решили сделать ставку на собственные модели. Они работали с распознаванием данных быстрее и точнее. Этот подход позволил нам избежать большинства проблем, связанных с избыточной информацией и медленной обработкой, которые возникали при использовании Vision.

Наши коллеги из Computer Vision сделали специальные модели под мобильные устройства, натренированные на поиск телефонов на изображении. В итоге у нас получился пайплайн из трех моделей плюс наша логика обработки изображений в переходных состояниях. 

Кроппер — модель для получения координат изображения с текстом из начального изображения.Сегментор — модель для получения масок из изображения от кроппера с интересующими нас областями. Маски представлены наборами координат.OCR (Object/optical character recognition) — модель для получения текста из изображения
Кроппер — модель для получения координат изображения с текстом из начального изображения.Сегментор — модель для получения масок из изображения от кроппера с интересующими нас областями. Маски представлены наборами координат.OCR (Object/optical character recognition) — модель для получения текста из изображения

Кроппер находит на начальном изображении интересующий нас прямоугольник. Их может быть несколько, если на изображении присутствуют несколько областей с текстом.


На выходе получаем координаты для вырезки области.

Работа кроппера
Работа кроппера

После кроппера изображение, вырезанное перспективно по выданным имкоординатам, отправлялось в сегментер, который выдавал маску интересующего нас текста. 

Работа сегменера: маска интересующей нас области
Работа сегменера: маска интересующей нас области

Потом изображение снова обрезалось и отправлялось в OCR, которая отдавала уже текст

Работа OCR-модели
Работа OCR-модели

Полученный пайплайн получился гибким. Для поиска других артефактов, будь то банковские карты или что-то другое, достаточно будет заменить или дообучить сегментер под поиск чего-то, кроме телефонов.

Для запуска моделей нам потребовался TensorFlowLite. Поскольку его интерфейсы принимают на вход Data, необходимо преобразовать из CIImage в Data с учетом желаемых размеров, количества битов для представления канала на один пиксель, количества битов для представления самого пикселя и дополнительных флагов. Для этих целей мы воспользовались Accelerate и его типами — он позволял проводить оптимизированные высокопроизводительные векторные вычисления. Ниже приведена часть функции по преобразованию типов с его помощью:

// Используем vImage_CGImageFormat для формата исходного изображения
var format = vImage_CGImageFormat(bitsPerComponent: 8,
                                          bitsPerPixel: 32,
                                          colorSpace: nil,
                                          bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipLast.rawValue),
                                          version: 0,
                                          decode: nil,
                                          renderingIntent: .defaultIntent)


        // Инициализируем vImage_Buffer для исходного изображения
        var sourceBuffer = vImage_Buffer()
        defer {
            // Не забываем освободить память  
            sourceBuffer.data?.deallocate()
        }
        // Инициализируем буффер с помошью CGImage, проверяя на ошибки
        var error = vImageBuffer_InitWithCGImage(&sourceBuffer, &format, nil, cgImage, vImage_Flags(kvImageNoFlags))
        guard error == kvImageNoError else {
            print("Error initializing vImage buffer: \(error)")
            return nil
        }
        // Определяем количество байтов на пиксель и считаем размер строки для буффера
        let bytesPerPixel = 4 // Формат ARGB (1 байт для каждого канала
        let destBytesPerRow = destWidth * bytesPerPixel
        // Выделяем память под буффер
        let destData = UnsafeMutablePointer<UInt8>.allocate(capacity: destHeight * destBytesPerRow)
        defer {
            // Не забываем освободить память
            destData.deallocate()
        }
        // Создаем буффер с аллоцированными данными
        var destBuffer = vImage_Buffer(data: destData,
                                       height: vImagePixelCount(destHeight),
                                       width: vImagePixelCount(destWidth),
                                       rowBytes: destBytesPerRow)
        // Масштабируем исходное изображение под размеры конечного буффера, проверяем на ошибки
        error = vImageScale_ARGB8888(&sourceBuffer, &destBuffer, nil, vImage_Flags(kvImageHighQualityResampling))
        guard error == kvImageNoError else {
            print("Error scaling image: \(error)")
            return nil
        }
        // Выделяем память под RGB data (3 канала на пиксель)
        let rgbData = UnsafeMutablePointer<UInt8>.allocate(capacity: destHeight * destWidth * 3)
        defer {
            // Не забываем освободить память
            rgbData.deallocate()
        }
        // Конвертируем ARGB-данные в RGB-формат, копируя только RGB-каналы
        for y in 0..<destHeight {
            for x in 0..<destWidth {
                let sourceIndex = y * destBytesPerRow + x * 4 // Каждый пиксель равен 4 байтам(ARGB)— исходное изображение
                let destIndex = (y * destWidth + x) * 3 // Каждый пиксель равен 3 байтам(RGB)— конечное изображение
                rgbData[destIndex] = destData[sourceIndex] // Красный канал
                rgbData[destIndex + 1] = destData[sourceIndex + 1] // Зеленый канал


                rgbData[destIndex + 2] = destData[sourceIndex + 2] // Синий канал
            }
        }
        // Создаем объект Data из rgbData для дальнейшего процессинга
        let data = Data(bytes: rgbData, count: destHeight * destWidth * 3)

Изначальное изображение часто не имеет ровного горизонтального текста, что требует применения перспективной трансформации — стандартной практики в области компьютерного зрения.

Нам требовалась перспективная трансформация для получения горизонтально выровненного изображения с текстом. Сначала мы попробовали perspectiveTransform, но он выдал не тот результат, который нам нужен.

Пример работы CIPerspectiveTransform
Пример работы CIPerspectiveTransform

Тогда мы применили perspectiveCorrection и получили нужное преобразование, с которым могли идти дальше.

Пример работы CIPerspectiveCorrection
Пример работы CIPerspectiveCorrection

После отработки алгоритмов и поиска связанных компонентов мы заново обрезаем изображение от сегментера и отправляем полученный результат в OCR для чтения текста.

Для выполнения расчетов на GPU мы использовали Metal, чтобы ускорить процесс. Это позволило оптимизировать алгоритм поиска связанных компонентов. Подобная задача типичная в компьютерном зрении, и обычно для ее решения используют фреймворки наподобие OpenCV. 

Мы предпочли реализовать собственное решение. Написали шейдер с двумя проходами для вычислений, что существенно улучшило производительность распознавания текста. 

Шейдер — это отдельная программа, которая исполняется на GPU. Есть несколько типов разных шейдеров в зависимости от предназначения. Наиболее часто они используются в 3D-графике, в нашем случае мы использовали специальный тип шейдера (compute), который позволяет обрабатывать каждый пиксель изображения параллельно.

Оставлю ссылку для тех, кто хочет подробнее прочитать про двойной подход для поиска связанных компонентов.

Первое прохождение в шейдере: 

kernel void firstPass(device uint *labels [[buffer(0)]],
                      device float *image [[buffer(1)]],
                      constant uint &width [[buffer(2)]],
                      constant uint &height [[buffer(3)]],
                      device atomic_uint &pixelsCount [[buffer(4)]],
                      device uint *labelsRes [[buffer(5)]],
                      uint2 gid [[thread_position_in_grid]]) {
    uint x = gid.x;
    uint y = gid.y;
 
    if (x >= width || y >= height) return;
 
    uint index = y * width + x;
 
    uint newIndex;
    if (index % 4 == 1) {
        newIndex = index / 4;
    } else {
        return;
    }
 
    uint size2 = 256 * 256;
 
    uint newIndex2;
    if (index % 256 == 0) {
        newIndex2 = newIndex / 256;
    } else {
        newIndex2 = (newIndex % 256) * (size2 / 256) + newIndex / 256;
    }
    if (image[index] < 0.5) {
        labels[newIndex2] = 0;
        return;
    }
     
    uint pixelIndex = atomic_fetch_add_explicit(&pixelsCount, 1, memory_order_relaxed);
 
    labelsRes[pixelIndex] = newIndex2 + 1;
    labels[newIndex2] = newIndex2 + 1;
}

Вот так выглядела работа написанного нами Connected-component labling Kernel — сущность для вызова shader и выноса работы на GPU:

func run(image: inout [Float32]) ->[Int: LabelInfo] {
        // Определяем размеры для входного изображения и полного изображения
        let width = 256
        let height = 256
        var widthFull = 256 * 2
        var heightFull = 256 * 2
        // Инициализируем массивы для хранения данных меток и результатов.
        var labels = [UInt32](repeating: 0, count: width * height) // Метки для каждого пикселя
        var labelsRes = [UInt32](repeating: 0, count: width * height) // Результирующие метки после обработки
        var pixelsCount: UInt32 = 0 // Счетчик ненулевых пикселей
        // Создаем буферы для обработки на GPU с использованием Metal
        let imageBuffer = stack.device.makeBuffer(bytes: &image, length: image.count * MemoryLayout<Float32>.stride, options: .storageModeShared)
        let labelsBuffer = stack.device.makeBuffer(bytes: &labels, length: labels.count * MemoryLayout<UInt32>.stride, options: .storageModeShared)
        let labelsResBuffer = stack.device.makeBuffer(bytes: &labelsRes, length: labelsRes.count * MemoryLayout<UInt32>.stride, options: .storageModeShared)
        let pixelsCountBuffer = stack.device.makeBuffer(bytes: &pixelsCount, length: MemoryLayout<UInt32>.stride, options: .storageModeShared)
        // Буферы для хранения размеров изображения
        let widthBuffer = stack.device.makeBuffer(bytes: &widthFull, length: MemoryLayout<UInt32>.stride, options: .storageModeShared)
        let heightBuffer = stack.device.makeBuffer(bytes: &heightFull, length: MemoryLayout<UInt32>.stride, options: .storageModeShared)
        // Создаем commandBuffer и commandEncoder вычислений для выполнения задач на GPU
        let commandBuffer = stack.commandQueue.makeCommandBuffer()!
        let commandEncoder = commandBuffer.makeComputeCommandEncoder()!
        // Устанавливаем состояние вычислительного пайплайна и связываем буферы с кодировщиком
commandEncoder.setComputePipelineState(firstPassPipelineState)
        commandEncoder.setBuffer(labelsBuffer, offset: 0, index: 0) // Привязываем буфер меток
        commandEncoder.setBuffer(imageBuffer, offset: 0, index: 1) // Привязываем буфер изображения
        commandEncoder.setBuffer(widthBuffer, offset: 0, index: 2) // Привязываем буфер ширины
        commandEncoder.setBuffer(heightBuffer, offset: 0, index: 3) // Привязываем буфер высоты
 
        commandEncoder.setBuffer(pixelsCountBuffer, offset: 0, index: 4) // Привязываем буфер количества пикселей
        commandEncoder.setBuffer(labelsResBuffer, offset: 0, index: 5) // Привязываем буфер результирующих меток


        // Определяем размер сетки и размер группы потоков для отправки вычислительных задач
        let gridSize = MTLSize(width: widthFull, height: heightFull, depth: 1)
        let threadGroupSize = MTLSize(width: 8, height: 8, depth: 1)
        // Отправляем потоки на выполнение
        commandEncoder.dispatchThreads(gridSize, threadsPerThreadgroup: threadGroupSize)
        // Завершаем кодирование команд и отправляем командный буфер для выполнения
        commandEncoder.endEncoding()
        commandBuffer.commit()


        // Ждем завершения всех команд перед продолжением
        commandBuffer.waitUntilCompleted()


        // Читаем результаты из буферов GPU в локальные переменные
        let resultPointer = labelsBuffer?.contents().bindMemory(to: UInt32.self, capacity: width * height)
        let result = Array(UnsafeBufferPointer(start: resultPointer, count: width * height))
        let pixelsCountPointer = pixelsCountBuffer?.contents().bindMemory(to: UInt32.self, capacity: 1)
        let pixelsCountResult = pixelsCountPointer!.pointee
 
        let labelsGPUResultPointer = labelsResBuffer?.contents().bindMemory(to: UInt32.self, capacity: Int(pixelsCountResult))
        let labelsGPUResult = Array(UnsafeBufferPointer(start: labelsGPUResultPointer, count: Int(pixelsCountResult)))


        // Фильтруем нулевые значения и создаем массив помеченных пикселей
        var array = labelsGPUResult.sorted(by: <).compactMap {
            $0 == 0 ? nil : LabeledPixel(value: Int($0))
        }
        // Создаем словарь для хранения информации о метках
        var labelsDict: [Int: LabelInfo] = [:]
        // Выполняем проход по CPU для дальнейшей обработки помеченных пикселей и заполнения словаря
        cpuPass(la: &array, labelsDict: &labelsDict)
 
        return labelsDict // Возвращаем словарь с информацией о метках
    }

Иногда возникает ситуация, когда координаты, полученные от кропера, оказываются отрицательными. Это указывает на то, что изображение расположено близко к границе исходного изображения, например, в левом верхнем углу. В таких случаях необходимо дополнить изображение черными областями на величину, равную отрицательной координате плюс один пиксель. Это позволяет системе корректно работать и обеспечивать стабильный результат. 

Результаты и проблемы

Результаты получились очень крутыми. Мы логировали ошибки, и в итоге модель ни разу не ошиблась, в то время как люди ошибались часто. Если говорить про время распознавания, наша Умная камера превзошла все наши ожидания: в среднем она оказалась быстрее человека в 7,5 раза!

Камера

Человек

Среднее время

0,84

6,26

Самое длительное время

5,73

73,09

Самое быстрое время

0,0069

2,59

Умная камера по сравнению с человеком
Умная камера по сравнению с человеком

Отдельного внимания заслуживает проблема нагрева телефонов. При превышении определенного температурного порога система перестает выделять необходимые нам ресурсы. В результате вместо изображения с камеры можно получить черный экран или системное предупреждение. Из-за моделей onDevice и постоянной работы устройства будут перегреваться, поэтому мы добавили мониторинг температуры.

Устройства пришлось заменить около четырех раз из-за уведомлений о перегреве за первый час работы стенда. Это стало неожиданностью, поскольку во время тестов мы с таким не сталкивались. Мы предположили, что проблема в «антикражных датчиках» — магните, прикрепляемом к задней части телефона, и зарядном кабеле. Кажется, что проблема была как раз в постоянной зарядке телефона, из-за которой он нагревался.

В итоге мы решили отказаться от этой связки с датчиками. Это оказалось верным решением, поскольку устройства практически перестали перегреваться. Одна пара телефонов проработала без перерыва почти четыре часа.

Примерно через 2 минуты активной игры телефон начинает нагреваться. На это влияет несколько факторов, а именно: — постоянно поддерживаемый сокет для соединения с сервером;— работа onDevice моделей во время работы камеры;—телефон постоянно с включенным экраном.Поэтому нам приходилось постоянно следить за состоянием девайсов.
Примерно через 2 минуты активной игры телефон начинает нагреваться. На это влияет несколько факторов, а именно:
— постоянно поддерживаемый сокет для соединения с сервером;
— работа onDevice моделей во время работы камеры;
‑телефон постоянно с включенным экраном.
Поэтому нам приходилось постоянно следить за состоянием девайсов.

Итоги и выводы

За время работы стенда нас посетило около 800 участников. Записей в БД насчитали 916, некоторые участники стояли в очереди несколько раз и вводили разные никнеймы. 

CSAT (customer satisfaction score) по итогам опроса получился 4,7 из 5 — это очень хорошие показатели, которыми мы довольны.

Мы сделали такие выводы: 

  • SwiftUI позволяет тратить меньше времени на разработку простых экранов и переиспользовать их на MacOS.

  • SwiftData — простой, приятный и удобный фреймворк для MVP или небольших проектов. Но требует глубокой проработки перед использованием на больших проектах.

  • Vision — неплохой фреймворк компьютерного зрения от Apple, но требует много доработок, если вы хотите заточить его под определенную задачу. И если вы хотите сделать не просто неплохое, а лучшее решение, точно стоит задуматься о создании и использовании более узконаправленного решения.

  • On-Device ML и вычислениям точно быть, и мы в в Центре искусственного интеллекта Т-Банка (AI-Центре) активно развиваем это направление. Но всегда стоит осторожно взвешивать все плюсы и минусы On-Device- и Server-Side-подходов. Когда на стенде использовались мощные устройства, у нас точно могли быть проблемы с интернет-соединением, и нам важно, чтобы все быстро работало. У нас не было ограничений в размере приложения и так далее, нам точно имело смысл делать решение on-device. Наше решение получилось быстрее и точнее Vision, и мы будем дальше использовать эти наработки для создания еще более качественных продуктов.

Комментарии (1)


  1. DvoiNic
    20.01.2025 11:07

    "умная камера" в зоне

    ...навевает какие-то совершенно неприятные ассоциации.