True RND или что делать с обученной моделью (опыт чайника)

от автора

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

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

Я выбрал двадцатигранные кости, хотя это не принципиально.

Подключаем aduino к драйверу и соленоиду тормоза, Далее arduino слушает команды на rs232, отключает тормоз и включает двигатель либо наоборот.

скетч
int drive = 11;                  int brake = 10;                  void setup() {   Serial.begin(9600);   Serial.setTimeout(5); }  void loop()  {     if (Serial.available())   {     int val = Serial.parseInt();       if (val == 123) {       digitalWrite(brake, LOW);       digitalWrite(drive, HIGH);        }     if (val == 234) {       digitalWrite(brake, HIGH);       digitalWrite(drive, LOW);        }     }   }

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

Делаем разметку. Для этого я накидал программку, которая по координатам трех точек строит окружность, находит координаты ее центра. Далее кликаем мышкой в углы костей, и сохраняем вместе с именем файла в csv. Но после разметки 700 картинок я понял, надо что то менять.

Зайдем с другой стороны. На размеченных картинках вырезаем круглые области с костями, сохраняем их в png, так как нам не нужно все что вне окружности, сразу раскладываем по папкам в соответствии с выпавшими цифрами. Делаем несколько фоток без костей. Далее просто размещаем в случайных местах на фоновой картинке 3 случайные кости. Тут нужно учесть что кости не могут пересекаться и должны находится внутри стакана.

Таким образом создаем 100000 картинок, сохраняя разметку.

Не забываем про главную формулу ML: shit in = shit out

Поэтому оценим получившийся датасет при помощи простой модельки на базе Xception.

baseline
base_model = Xception(weights='imagenet', include_top=False, input_shape = [480, 640, 3]) base_model.trainable = True #Устанавливаем новую «голову» (head): x = base_model.output x = GlobalAveragePooling2D()(x)  #Pooling слой x = BatchNormalization()(x) #добавим Batch нормализацию x = Dense(256, activation='relu')(x) # полносвязный слой с активацией relu x = Dropout(0.25)(x) # полносвязный слой с вероятность отключения нейронов в слое output = Dense(6,  name=out_key)(x)  model = Model(inputs=base_model.input, outputs=output)

На выходе модели 6 чисел, соответствующих координатам центров костей. Проверил на реальных картинках, процентов 80 распозналось как то так:

остальные как то так:

Вывод: синтетический датасет вполне пригоден для обучения. Далее обучим Yolo3. За основу возьмем эту реализацию. Реализаций много, но тех которые работают из коробки мало.

Результат: мы молодцы, обучили классную модель, которая классно находит кости и… все. А что с ней делать дальше? Как ее «установить» своей бабушке?

Нужно дружить ее, например с C# и делать нормальное приложение с юзерфрендли интерфейсом. Есть несколько вариантов чтобы подружить модель с C#. Рассмотрим ONNX. Итак, конвертируем модель, в в формат onnx. Далее смотрим в гугле или ютубе туториал например этот. Пробуем повторить и… код не работает. Но работоспособность кода запечетлена на видео! Смотрим очень внимательно и устанавливаем именно те версии библиотек. Теперь работает.

Но модель ничего не видит. Предполагаем что C# скармливает картинку сетке не так как Python. Проверим.

Для этого сделаем маленькую сетку, которая будет принимать на вход картинку 3*3 а на выход просто выдавать 27 цифр соответствующих цветам пикселей.

тестовая модель
input = Input(shape=[IMG_SIZE, IMG_SIZE, IMG_CHANNELS], name='image') output = Flatten()(input) model = tf.keras.models.Model(input, output)

Подадим ей на вход синюю картинку в Python и C#, сравним результаты:

Видим что в отличие от Python`а C# извлекает сначала все байты одного цвета, потом второго и третьего.

Укажем в экстракторе пикселей что не надо так, а заодно укажем правильный порядок цветов.

код
... .Append(context.Transforms.ExtractPixels(outputColumnName: "image",                                          orderOfExtraction: ImagePixelExtractingEstimator.ColorsOrder.ABGR,                                          colorsToExtract: ImagePixelExtractingEstimator.ColorBits.Rgb,                                           interleavePixelColors: true                                          ))

Ну вот теперь, модель видит все как положено. Вернемся к версии библиотек. Если верить тому что тут написано микрософт решила убрать поддержку Bitmap, потому что эта сущность есть только в виндовс. В замен предлагают использовать MLImage. Обожаю когда авторы меняют интерфейсы. Давайте попробуем. И когда мы передаем модельке картинку загруженную из файла: MLImage.CreateFromFile(String) то проблем действительно нет.

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

Кадры с вебкамеры Emgu.CV извлекает в обьекты тыпа Mat. По сути это просто матрица, в нашем случае байтов. MLImage можно создать из линейного массива байтов: CreateFromPixels(Int32, Int32, MLPixelFormat, ReadOnlySpan<Byte>).

Вытягиваем наш Mat и пробуем создать MLImage.

код
Mat m = new Mat(); webcam.Retrieve(m); Bitmap img = m.ToImage<Bgr, byte>().ToBitmap(); byte[] barray = new byte[img.Width*img.Height*3]; m.CopyTo(barray); MLImage image = MLImage.CreateFromPixels(img.Width, img.Height, MLPixelFormat.Bgra32, barray);

Bitmap здесь создается только для вывода в pictureBox. Запускаем и модель ничего не видит. Снова смотрим как передаются данные, и проблема в том что формат пикселя у MLImage всегда содержит альфа слой, а Mat с камеры приходит без него. Добавляем альфу:

код
Mat m = new Mat(); webcam.Retrieve(m); Bitmap img = m.ToImage<Bgr, byte>().ToBitmap(); CvInvoke.CvtColor(m, m, ColorConversion.Bgr2Bgra); byte[] barray = new byte[img.Width*img.Height*4]; m.CopyTo(barray); MLImage image = MLImage.CreateFromPixels(img.Width, img.Height, MLPixelFormat.Bgra32, barray);

и получаем то к чему стремились:

PS. Большая часть кода в проекте взята из указанных источников почти без изменений, либо с незначительной подгонкой, здесь я указал лишь не очевидные моменты. Если будут интересны подробности, пишите.


ссылка на оригинал статьи https://habr.com/ru/post/720800/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *