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. Большая часть кода в проекте взята из указанных источников почти без изменений, либо с незначительной подгонкой, здесь я указал лишь не очевидные моменты. Если будут интересны подробности, пишите.