Содержание статьи
Всем привет.
Сегодня я расскажу, как решал задачу по определению качества крупы гречневой через Computer Vision.
Задача состояла в том, что по фото с фотосепаратора (такая штука которая отделяет хорошие зерна от плохих) необходимо определить качество крупы: сколько доброкачественных ядер уходит в некондицию и сколько плохих остается. Размечал и обучал модель я на своем портале picva.ru который доступен всем желающим бесплатно.
Зачем это производству
На производственной линии крупа идет сплошным потоком. Фотосепаратор удаляет зерна, которые считает некондицией (плохими).
Если настройки задрать, в некондицию полетит много хорошей крупы, и предприятие несет потерю продукта. Если ослабить, черные и уже порченые крупинки останутся в продукте. Поэтому в лаборатории периодически фотографируют чистый продукт и некондицию и смотрят реальные проценты. По ним решают, что изменять в работе фотосепаратора.
Что было на входе
Заказчик прислал 24 кадра, по 8 на группу: хорошие, плохие и серые. Снимает линейная камера. Кадр — узкая полоска 2716 на 512, не обычное фото.
На кадре примерно 200–800 зерен. Типичная рамка около 28 на 12 пикселей.
Сверху фото: хорошая партия крупы, дальше плохая, дальше серая.


Нужно было посчитать три класса:
-
хорошие;
-
плохие, черные;
-
серые, пограничные.
Какую крупу считать серой, решала лаборатория. Модель только повторяет ее правило.
Сколько чего насчитали руками
Порция
Крупинок
Хорошие
Серые
Черные
хорошая
2322
86.3%
13.7%
0.0%
плохая
1632
3.7%
22.9%
73.4%
серая
5130
20.8%
70.4%
8.7%
Хорошая крупа в некондиции — прямые потери. Тут его 3.7%. Плохое зерно, крупа в хорошей порции — неправильная настройка фотосепаратора. Черных там не нашлось, серых 13.7%.
Кусок серого кадра после ручной разметки. Желтые — серые, зеленые — хорошие, красные — черные.

Разметка
На picva.ru разметил 5 кадров и сразу обучил модель, YOLOv11. Обучение идет на GPU и пока бесплатно.
Остальные фото размечал уже с помощью этой модели. Сам портал так пока не умеет, поэтому доразметку делал через Cursor. Потом все равно пришлось много править руками: разметил еще 10 кадров, собрал новую модель и так дошел до всех 24.
Иногда думают, что ChatGPT или Claude размечают такое из коробки. На этих кадрах так не вышло: путают хорошее с серым. Я и сам на повторной разметке одних и тех же зерен совпадал с собой примерно на 83%. Это шум разметки, не жесткий потолок модели, но чудес на этой границе ждать не стоит.
Аугментация
24 фото для модели мало, поэтому размножил набор до 500 синтетических кадров. В picva такого пока нет.
Обычный поворот и яркость всего кадра тут не подходят. Съемка на просвет: фон подсвечен сзади и сильно гуляет, а сама крупа почти нет. Если осветлить кадр целиком, модель начнет путать класс с подсветкой.
Оставил такой набор:
-
не менять яркость всего кадра;
-
вырезать зерна и сажать их на новый фон;
-
без поворота, пиксель камеры не квадратный, зерно вытянуто примерно в 2.3 раза;
-
отражения можно;
-
цвет зерна почти не менять;
-
у YOLO выключить цветовой джиттер и mosaic, масштаб оставить маленьким, 0.15.
Модель
Финальную модель учил на этих 500 изображениях, снова через picva.ru. Пробовал v8 и v11, остановился на YOLO26 small, yolo26s, 60 эпох.
Почему вход 1536, а не 640
Кадр шириной 2716, а зерно по высоте около 12 пикселей. На входе 640 зерно сожмется до пары пикселей, и сеть его не увидит. Поставил 1536. Даже так на входе сети зерно остается примерно 15 на 6 пикселей.
Нарезать полоску на куски тоже пробовал: зерно становится крупнее, но обучение от этого портилось. Вернулся к целому кадру. Растягивать полоску по вертикали не стал, вход и так уже большой.
Лимит рамок
У YOLO по умолчанию максимум 300 найденных объектов на кадр, а у меня бывает почти 800 зерен. Поднял потолок. У YOLO26 этот потолок зашивается в ONNX при экспорте. Первый HTML на каждом кадре отдавал ровно 300 зерен и в плотной серой партии терял примерно половину. Пришлось переэкспортировать модель с большим лимитом.
Качество
-
Val: mAP50 77.6%, mAP50-95 46.9%, precision 71.5%, recall 74.0%
-
Test: mAP50 78.3%, mAP50-95 48.1%, precision 71.2%, recall 75.6%
mAP50-95 ниже, потому что зерно мелкое и рамка часто сидит не идеально. Для процентов по классам важнее, что зерно нашлось и класс не перепутан.
Как ошибается модель, лучше видно на картинке. Слева зерно, которое модель не нашла. В середине лишняя рамка там, где зерна слиплись. Справа по разметке серое, а модель сказала «хорошее».

Как отдать заказчику
Инференс можно гонять прямо на сайте. Лаборатории это не подошло: проверять нужно нечасто, но без интернета и без установки Python. Отдельная программа тоже слабый вариант. Она зависит от операционной системы, и на производстве на установщик смотрят как на подозрительный файл.
Поэтому один HTML-файл. Его скачивают, открывают двойным щелчком в Chrome или Edge и загружают пачку фото. Снимки никуда не уходят, считает компьютер в лаборатории.
Так выглядит страница после прогона. Открыт серый кадр, сверху ползунок уверенности. На странице цвета другие, чем на разметке выше: черные синие, хорошие зеленые, серые оранжевые.

Как модель оказалась внутри файла
После обучения модель лежит в ONNX. В браузере ее считает onnxruntime-web 1.20.1, это WebAssembly. Сам wasm-файл движка около 11 МБ.
И модель, и движок вшиты в HTML текстом, base64. Отдельного файла модели рядом нет. Страница, открытая с диска, по правилам браузера обычно не читает соседние файлы. Если оставить модель отдельно, страница либо полезет в интернет, либо просто промолчит.
Движок из коробки хочет скачать свой wasm, обычно с CDN. Я отдаю wasm уже готовыми байтами, у onnxruntime это параметр wasmBinary. Качать больше нечего.
Размер и время
Файл получается 67.4 МБ. Сама модель весит 37 МБ, base64 раздувает объем примерно на треть, плюс движок.
Мерил на ноутбуке Intel i7-13700H, браузер Chromium:
-
страница открывается примерно за 3 секунды, браузер на это время может подвиснуть;
-
один кадр считается около 3.7–3.9 секунды;
-
пачка из 24 кадров — около полутора минут.
Поток один. На странице, открытой как файл, многопоточный WebAssembly браузер не разрешает. Для лаборатории это нормально: им нужна периодическая проверка, а не конвейер.
Ползунок уверенности модель заново не гоняет. Прогон один, с порогом 0.05, а ползунок только прячет слабые рамки. По умолчанию показываю от 0.25. Справа сразу число объектов и доли по классам, и то же самое суммой по всей пачке.
Готовый html-файл с моделью можно также скачать с портала picva.ru (такой функционал уже есть)
Что вышло
Заказчик получил файл, который открывается без интернета и без установки программ. По пачке фото сразу видно доли хороших, серых и черных зерен. Ему этого хватило.
Picva я делаю сам: picva.ru. Там разметка, обучение YOLO на GPU и выгрузка такого HTML.
picva.ru — бесплатный портал для разметки и своих моделей. По смыслу это что-то вроде Roboflow, по коду это форк Label Studio, достаточно сильно измененный и с добавлением возможности обучения моделей. Есть обучение YOLO на GPU, проверка модели на своих фото. Модель можно выгрузить в том числе в onnx формате, запустить прямо на портале или выгрузить одним HTML-файлом как описано выше, который работает без интернета и без Python.
Если есть свой набор фото с дефектами, могу прогнать его через портал и посмотреть, обучается ли модель. Напишите на vopros@picva.ru.
В планах ИИ-помощник, аугментации и помощь с дообучением.
Большое спасибо если дочитали до конца. Буду рад обратной связи.
