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

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

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

83827b4e22ffc23b755262ff7d8ba4a8.jpg

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

Подключаем 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 команды бросить кости, а затем сохраняет кадр с камеры. Получаюем такие картинки:

42cca7f214b38188a7e863023686b259.jpg

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

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

61da1a9c3493b5074c8d5a15d1612bba.jpg

Таким образом создаем 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 распозналось как то так:

78f53e45e04ec0e3a370a91e5de633f1.jpg

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

b0eca3ba402eaf8a2f20d9bf6ab8821a.jpg

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

8dcc80827c13a9e73a7f5e26177ee58b.jpg

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

Нужно дружить ее, например с 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#, сравним результаты:

5a61d331e0cca4b9a475869dcf71b084.png

Видим что в отличие от 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).

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

код

Mat m = new Mat();
webcam.Retrieve(m);
Bitmap img = m.ToImage().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().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);

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

a8d529283a122c2d479622bdf677837f.png

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

© Habrahabr.ru