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

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


  1. kichrot
    00.00.0000 00:00
    -12

    ... (опыт чайника) ...

    ...

    ... генерации по настоящему случайного пароля ...

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

    Риторический вопрос автору статьи: Что такое "по-настоящему случайный пароль"??? :)

    Видимо автор статьи считает, что некая "абсолютная случайность", это отсутствие любой закономерности и детерминизма, т.е. это абсолютный произвол. :)

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

    СЛУЧАЙНОСТЬ – философская категория, выражающая один из предельных видов (классов) взаимосвязей и взаимоотношений в мире, характеризующийся отсутствием прямых закономерных связей в поведении и функционировании объектов и систем.

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

    Например, генератор последовательности натуральных чисел от нуля до бесконечности, для человека незнакомого с арифметикой и принятой системой обозначения цифр, выдает последовательность случайных знаков, "по-настоящему случайных". :)


    1. dabrahabra
      00.00.0000 00:00
      +3

      Для вас может показаться случайностью, но разработчики знают, что в большинстве ЯП и C# в том числе, базовый генератор случайных чисел на самом деле псевдослучайный.


    1. ARad
      00.00.0000 00:00
      +2

      На данный момент некоторые квантовые процессы считаются абсолютно случайными, даже в теории.


  1. MrGrod
    00.00.0000 00:00
    +1

    Осталось встроить в корпус ПК как "заводское решение" и вызывать каждый раз при вызове random.random :D


  1. ZiggiPop
    00.00.0000 00:00
    +5

    Когда-то в детстве, когда с увлечением читал книги Мартина Гарднера, Якова Перельмана и Петра Маковецкого, решил заморочиться и пять тысяч раз подбросил пару старых зариков из нардов. Надо сказать, в упор не помню, что именно меня сподвигнуло в этих книгах на такую "проверку". В результате обнаружил, что один из кубиков выбрасывает пятерку примерно чуть меньше, чем в 18% бросков, а двойку — не более 14, а второй вообще выбрасывает шестерку более чем в 20%! В одном кубике была небольшая едва заметная капелька воздуха, а у второго был сколот угол. С тех пор к абсолютной "случайности" бросков кубика отношусь немного скептично.

    Любопытно, что поначалу распределение было примерно по 16-17 процентов, как и должно было быть, но резко начало "сползать" после 3 тысяч бросков. Думаю, что сейчас бы было интересно проверять качество кубиков на вашей шайтан-машине )


  1. ARad
    00.00.0000 00:00
    -2

    Вы же понимаете что на самом деле у вас не получатся идеально случайные числа.

    Современные процессоры поддерживают инструкцию генерации случайного числа https://en.wikipedia.org/wiki/RDRAND.

    Также можно использовать сервисы в интернете, например https://qrng.anu.edu.au/.


  1. Tarakanator
    00.00.0000 00:00

    ок, машина вам выдала абсолютно случайные X,Y,Z.
    Как вы составляете из них число?
    XYZ? XZY? ZYX? как вы выбираете вариант?


    1. EgorovM
      00.00.0000 00:00

      выбираем случайное из этих трех, получается рандом в квадрате!


      1. Tarakanator
        00.00.0000 00:00

        Если бы мы могли выбрать случайное число, то эту установку бы не городили.


        1. EgorovM
          00.00.0000 00:00

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


          1. Tarakanator
            00.00.0000 00:00

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


            1. rutexd
              00.00.0000 00:00

              Да.

              Во втором броске псевдослучайно читаем числа справа налево или слева направо, потом в зависимости от того какое число вышло - применяем такой же алгоритм для первого броска. Например 213 312 321 итд. Это уже почти что квантовый рандом)))


    1. ovegio
      00.00.0000 00:00

      Судя по скриншоту, просто сортируем по x-координате


    1. Kwent
      00.00.0000 00:00

      Убираем эту неопределенность любым детерминированным алгоритмом (сортировка по координатам, сумма, умножение), получаем нормальное случайное число, вроде как


    1. ykx3hr Автор
      00.00.0000 00:00

      На самом деле ни как, Yolo сама выбирает последовательность объектов которые она обнаружила. Целью этого проекта и не было генерировать что-то идеальное, я просто взял идею которая была когда то давно озвучена здесь же на habr`е и построил для себя некий пайплайн, от обученной модели к ее применению в "повседневной жизни". Результатами решил поделится.