Сложные решения рассказ дзен часть 23

Сегодня мы хотим поделиться опытом решения задачи детекции дефектов на снимках промышленных объектов методами современного компьютерного зрения. наш рассказ будет

eokbi9r9h8yzmaq6 crg3qdd ma

Сегодня мы хотим поделиться опытом решения задачи детекции дефектов на снимках промышленных объектов методами современного компьютерного зрения.

Наш рассказ будет состоять из нескольких частей:

  • “Постановка задачи и Данные”, в которой мы будем смотреть на ржавые отопительные котлы и лопнувшие трубы, наслаждаться разметкой и аугментацией данных, а также будем вращать и шатать трубы чтобы сделать данные разнообразнее;
  • “Выбор архитектуры”, в которой мы сядем на два стула попытаемся выбрать между скоростью и точностью;
  • “Фреймворки для обучения”, в которой мы будем погружаться в Darknet и заглянем в MMLab и покажем как сделать итоговое решение воспроизводимым и удобным для тестов.

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

Заметка от партнера IT-центра МАИ и организатора магистерской программы “VR/AR & AI” — компании PHYGITALISM.

xhz4tbiynt0azwhgw6fd 5rwvw
Рис 1. Схематичное изображение рассматриваемого проекта.

Машинное обучение (machine learning / ML) в общем и компьютерное зрение (computer vision / CV) в частности находят сегодня все больше применений в решение задач из промышленной области (пример). Начиная от задач нахождения бракованных деталей на конвейере и заканчивая управлением беспилотным транспортом — везде используются глубокие архитектуры, позволяющие детектировать многочисленные объекты разных категорий, предсказывать пространственное расположение объектов друг относительно друга и многое другое.

Сегодня мы рассмотрим кейс (проект Defects detector CV) по созданию прототипа программного обеспечения (ПО), которое использует нейронные сети, для того, чтобы на фото или видеопотоке детектировать объекты заданных категорий. В качестве предметной области, в данном проекте, выступила дефектоскопия промышленных труб.

art1m9ofczvbn9ta lbl17n9nj4
Рис 2.Схематичное изображение парового котла, аналогичного тому, что рассматривался в проекте.

Кратко опишем предметную постановку задачи:

В системах теплового обогрева городов и промышленных предприятий используются специальные паровые котлы (см. пример на рис. 2), внутренность которых пронизана многочисленными трубами, которые в процессе эксплуатации могут получать различные повреждения (разного рода трещины, разрывы, деформации, коррозию и многое другое). Для того, чтобы своевременно устранять повреждения и поддерживать работоспособность котлов, периодически их работа останавливается, из них выкачиваются все жидкости, вскрывается проход во внутреннее пространство котла, которое представляет из себя ряд связанных пространственных отсеков без освещения, способных вместить внутрь себя трехэтажный дом. После производят очистку поверхности труб, выстраивают промышленные леса, чтобы у специалистов по дефектоскопии была возможность получить прямой доступ к трубам, которые они будут осматривать и проверять с помощью специальной аппаратуры. После того как все дефекты обнаружены, производится замена или ремонт поврежденных участков, после чего необходимо разобрать леса, законсервировать котел и снова его запустить.

Зачастую, большую часть времени отнимает сбор и демонтаж промышленных лесов и визуальный осмотр труб специалистами. Для того, чтобы сократить время на выполнение этих этапов было предложено использовать пилотируемый дрон с системой освещения, фото и видеосъемки, который смог бы быстро облететь внутреннее пространство котла (в том числе, небольшие размеры дрона позволяют попасть во внутрь труднодоступных узких шахт, куда человеку сложно проникнуть), собрать данные для обработки, и с помощью алгоритмов компьютерного зрения, выделить все обнаруженные дефекты.

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

Мы постараемся осветить все основные этапы при разработке проекта, связанного с машинным

  • обучением;
  • сбор данных;
  • разметка данных;
  • дополнение данных (аугментация);
  • обучение модели;
  • оценка качества работы модели;
  • подготовка к внедрению.

В первую очередь поговорим о самом главном с точки зрения машинного обучения объекте — о данных. Откуда их взять, в каком формате хранить обрабатывать, как из меньшего количества данных получить больше информации и пр.

ejy78n6rpmjxz67chm24bg2fm e
Рис. 3 Пример объекта обучающей выборки — фото, сделанное при осмотре внутренности остановленного котла.

Данные в нашей задачи представлены классическим для CV образом — в виде цветных RGB снимков. На каждом снимки может как присутствовать сразу несколько дефектов, так и не присутствовать вовсе.

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

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

Виды задач распознавания образов на изображениях


Рис.4 Пример разметки для задачи детекции объектов на изображениях из датасета MS COCO. Иллюстрация из репозитория detectron2.

Сложность задачи в области компьютерного зрения отчасти определяется формой предсказания. Отметим сразу, что задача распознавания образов на изображения логически подразделяется на две подзадачи:

  • выделения области интереса;
  • классификация объекта в области интереса.

В зависимости от формы области интереса, задачи детекции делят на детектирование в виде ограничивающих прямоугольников (bounding boxes), в виде многоугольников, в виде битовых масок, в виде точек интереса и пр.

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

Помимо формы предсказания, также необходимо было определить формат датасета (способ организации данных и аннотаций). Формат датасета обычно определяется группами исследователей и разработчиков, которые создают значимые датасеты или архитектуры. В нашем случае, датасеты рассматривались в форматах MS COCO и YOLO.

Подробнее про задачи детекции объектов на изображениях и основные подходы при конструировании архитектур глубокого обучения для решения этой задачи можно прочитать здесь.

Про устройство формата MS COCO и формы предсказаний можно узнать здесь.

Разметка данных

0wepcm7jq8i2s5vetbeuvkrdzc0
Рис. 5 Демонстрация работы CVAT для разметки дефекта трубы в виде полигональной маски.

Для того, чтобы разметить сырые данные существует несколько основных способов. Во-первых, можно воспользоваться услугами сервисов облачной распределенной разметки данных, вроде Amazon Mechanical Turk или Яндекс.Толока, во-вторых, если данные приходят не из реального мира, а генерируются искусственно, то разметку можно генерировать вместе с данными, ну и самым доступным способом является использование специализированного ПО для разметки данных.

В нашей задаче, мы воспользовались вторым и третьим способами. Про наш генератор синтетических данных мы расскажем отдельно во второй части. В качестве ПО для разметки мы выбрали Intel CVAT (заметка про инструмент на Хабре), который был развернут на сервере заказчика.

Некоторые альтернативные инструменты разметки:

  • Supervisely
  • labelImg
  • VGG Image Annotator

Усиление обобщающей способности

В случае, когда данных для обучения мало и необходимо добиться хорошей обобщающей способности от алгоритма, обычно прибегают к разного рода “трюкам” — манипуляциям с данными.


Рис. 5.1 Изображение из сообщества “Memes on Machine Learning for Young Ladies”. Там же можно увидеть пример того как не нужно делать аугментации.

Такие трюки могут быть связаны как с процессом обучения (использование беггинга или дропаута), так и с процессом подготовки данных (аугментация данных). При решении нашей задачи, чтобы расширить исходный датасет небольшого размера, мы прибегли к аугментации данных — различные преобразования исходных данных, такие как добавление шума к изображению, различные повороты и растяжения, обесцвечивание и многое другое. Для обеих архитектур были подготовлены одинаковые датасеты с аугментированными изображениями, хотя стоит отметить, что у обоих фреймворков есть встроенные возможности автоматической аугментации в процессе обучения (заложено в возможностях фреймворков).

Подробнее про примеры аугментации данных в задачах компьютерного зрения можно прочитать здесь и здесь.

Метрики качества

Для того, чтобы измерять качество работы алгоритма после процесса обучения на тестовой выборке в задачах детекции объектов на изображениях традиционно используют метрику mean average precision (mAP), рассчитанную для каждого класса по отдельности и усредненную для всех классов. Значение этой метрики рассчитываются при разных уровнях характеристики Intersection over Union (IoU). Здесь мы не будем подробно останавливаться на разъяснении устройства этих функций, всем заинтересованным предлагаем пройти по ссылкам ниже на статьи и заметки, которые помогут освоится в данном вопросе, но все же поясним некоторые основные моменты оценки качества работы алгоритмов в нашей задаче.

Для оценки результатов были выбраны следующие метрики:

  • mAP (mean average precision) — среднее значение точности по всем классам (поскольку у термина могут быть разные трактовки, рекомендуем ознакомится с различными вариантами здесь).
  • AP (средняя точность) — средняя точность по каждому отдельному классу.
  • Precision Recall кривая.
  • Число случаев когда дефект был обнаружен и он на самом деле был (TP).
  • Число случаев когда дефект был обнаружен, но его не было на самом деле (FP) т. е. ложное срабатывание.

Расчет метрик производился следующим образом. Так как целевым объектом для оценки был ограничивающий прямоугольник (bounding box), то интерес для оценки представляет три свойства:

  • Насколько модель уверена в предсказании. Чем выше уверенность, тем более надёжным будет результат. В идеале все правильные предсказания должны быть с высокой степенью уверенности.
  • Правильно ли предсказан класс объекта.
  • Насколько предсказанный ограничивающий прямоугольник совпадает с правильным.

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

Рассмотрим пример вычисления метрик на основе примера из рис. ниже. Для этого определяется величина IoU, которая равна отношению площади пересечения прямоугольников (серый прямоугольник) к площади их объединения. Она принимает значения в отрезке [0;1]. Можно выбрать определённый порог и считать, что при превышении этого порога прямоугольники совпадают.

Определение TP:
Если IoU больше определенного порога и метки классов совпадают, то предсказание считается правильным.

Определение FP:
Если IoU меньше определенного порога и метки классов совпадают, то предсказание считается ложным.

На основании этих показателей рассчитывается точность и полнота. Если кратко, то точность показывает насколько хорошо модель предсказывает дефекты определенного класса из тех которые были обнаружены вообще. Чем больше значение, тем меньше ошибок совершается. Значение в отрезке от [0;1].


Рис. 6 Пример рассчитанной Precision-Recall кривой для одного из классов дефектов для архитектуры DetectoRS.

Полнота показывает способность модели найти все дефекты определенного класса. Чем выше значение тем больше будет обнаруживаться дефектов, но не все могут быть точно предсказаны Эти величины взаимосвязаны. Как правило, чем больше точность, тем меньше полнота и наоборот. Т. к. предсказания зависят от некоторого порога принятия решения, то строят графики точности-полноты при разных значениях порога принятия решения для его оптимального определения и оценки возможностей модели.

oe9 zokruz28gnoj1bqju 0guaa
Рис. 7 Сравнение технических метрик для выбранных архитектур для процесса обучения.

Подробную статью про метрики качества и функции ошибок в задачах компьютерного зрения можно найти здесь. Про метрики в задачах машинного обучения в целом смотрите эту замечательную заметку на хабре.

Заметку с примерами и объяснениями всей терминологии про метрики качества в CV можно посмотреть здесь.

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


*Рис. 8 Сравнение технических метрик для выбранных архитектур для процесса использования.* тестирование проводилось на видеокарте RTX 2080 Ti, тестирование проводилось на CPU AMD Ryzen 7 2700X Eight-Core Processor.*

Деление данных на обучающую и тестовую выборку

Для тестирования алгоритмов и оценки качества их работы часто нужно поделить данные на несколько частей. Одна часть используется при обучении, а другая для проверки обобщающей способности и подсчета метрик. Обычно это не представляет проблемы, но в нашем случае ситуация сложнее, т. к. каждое изображение (объект) может содержать несколько классов и в разном количестве. Изображение атомарно и мы не можем его как-то разделить логически, чтобы число примеров каждого класса поделилось в нужном соотношении. Тут стоит заметить, что фактически можно разделить изображение на множество отдельных частей, в которых есть только один класс, но это приведёт к уничтожению изначальной структуры изображений.

Рассмотрим пример, изображенный на рис. 9 — случай, когда каждый объект принадлежит одному классу:

qehih8w8 ptdctouewihz9ml
Рис. 9 Деление данных на тестовую и обучающую выборку.

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

ykpzefj6kivmcty0sp7j abkepa
Рис. 10 Каждый объект может содержать иметь несколько классов.

Для решения проблемы разделения такого вида данных на подвыборки была использована библиотека scikit-multilearn и метод iterative_train_test_split. На вход подавалась матрица из нулей и единиц. Каждая строка обозначала изображение. Единицы стояли в столбцах с номерами соответствующими номерам классов. Если применить функцию к объектам на рис. выше, то получится следующее разделение:


Рис. 11 Результат разделения на тестовую и обучающую выборку.

Конечно, здесь не учитывается количество повторений классов в рамках одного изображения (объекта), поэтому деление может быть несбалансированным.

На основе результатов анализа известных архитектур собранных на paperwithcode (на момент 3 квартала 2020 года) для детектирования дефектов были выбраны две архитектуры:

  • YOLOv4
  • DetectoRS

ndeni8c kb lapmfq1bayfd3swk
Рис. 12 Сравнение архитектур на бенчмарке MS COCO object detection с сайта papesrwithcode (3 квартал 2020 года).


Рис.13 Сравнение архитектур на бенчмарке MS COCO, полученное авторами архитектуры YOLOv4.

YOLOv4 попала в этот список из-за своей скорости работы. Мы хотели посмотреть на результаты разных моделей и выбрать итоговый вариант в зависимости от требований. Потенциально могла быть потребность в обработке видео и для этого случая планировалось использовать YOLOv4. Более точная, но медленная работа ожидалась от DetectoRS.

Больше информации о том, как конструируются современные глубокие архитектуры для задач детекции объектов на изображениях можно прочитать в этой статье или в этой заметке.

DetectoRS

Данная архитектура базируется на использовании специального типа сверток (Switchable Atrouse Convolution / SAC) и в верхнем уровне устроена в виде рекурсивной пирамиды (Recursive Feature Pyramid / RFP), объединяющий локальные и глобальные признаки.

g2ruihl554zu7vnpqmq4yhj1ndg

Рис. 14 Основные нововведения, используемые авторами архитектуры DetectoRS: (a) — рекурсивная пирамида признаковых описаний, используемая для объединения глобальных признаков на изображении; (b) — переключательные свертки типа “atrouse” для работы с локальными признаками.

Реализация данной модели присутствует внутри фреймворка MMDetection.
Об устройстве архитектуры можно прочитать здесь.

YOLOv4

Эта модель делает свои предсказания на основе анализа изображения, разбивая его на квадратную сетку как на рис. ниже:


Рис. 15 Принцип работы из оригинальной статьи про YOLO.

Видео с объяснением работы можно найти здесь.

YOLOv4 это дальнейшее улучшение (четвертая версия) оригинальной архитектуры как с точки зрения устройства так и процесса обучения. В данной статье про YOLOv4 исследуются как различные техники аугментации изображений влияют на конечный результат.

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

Для разработки под Python, двумя наиболее популярными фреймворками являются MMdetection (open source) и Detectron2 (Facebook research). Для разработки под C, существует фреймворк Darknet (open source). Подробнее про то, как использовать данные фреймворки можно прочитать в заметках один, два, три.

MMDetection

Поскольку архитектура DetectoRS имела единственную реализацию на момент работы над проектом и эта реализация была написана на фреймворке MMDetection, имплементация архитектуры была основана на использовании данного фреймворка.

Устройство фреймворка

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

  • подготовить веса при тренированной модели в формате .pth или прописать самостоятельно инициализацию новых весов;
  • написать конфигурационный файл в виде Python скрипта со всеми настройками для сети и описание хода обучения, валидации и пр.;
  • выбрать способ логирования процесса обучения (доступна запись в текстовый файл и логирование с помощью tensorboard);
  • организовать данные в файловой системе согласно выбранному типу разметки (доступны MS COCO, Pascal VOC и поддерживается возможность внедрение пользовательских форматов);
  • написать основный скрипт, собирающий воедино все перечисленные выше составные части.

После тестов локально на компьютере, на котором была установлена и развернута среда для работы с MMDetection, аналогично YOLOv4, сборка была перенесена внутрь NVidia Docker контейнера.

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

Наш конфигурационный файл для обучения на данных с трубами для 9 классов дефектов:

# Обучения на 9 классах без масок

model = dict(
    type='CascadeRCNN',
    pretrained='torchvision://resnet50',
    backbone=dict(
        type='DetectoRS_ResNet',
        depth=50,
        num_stages=4,
        out_indices=(0, 1, 2, 3),
        frozen_stages=1,
        norm_cfg=dict(type='BN', requires_grad=True),
        norm_eval=True,
        style='pytorch',
        conv_cfg=dict(type='ConvAWS'),
        sac=dict(type='SAC', use_deform=True),
        stage_with_sac=(False, True, True, True),
        output_img=True),
    neck=dict(
        type='RFP',
        in_channels=[256, 512, 1024, 2048],
        out_channels=256,
        num_outs=5,
        rfp_steps=2,
        aspp_out_channels=64,
        aspp_dilations=(1, 3, 6, 1),
        rfp_backbone=dict(
            rfp_inplanes=256,
            type='DetectoRS_ResNet',
            depth=50,
            num_stages=4,
            out_indices=(0, 1, 2, 3),
            frozen_stages=1,
            norm_cfg=dict(type='BN', requires_grad=True),
            norm_eval=True,
            conv_cfg=dict(type='ConvAWS'),
            sac=dict(type='SAC', use_deform=True),
            stage_with_sac=(False, True, True, True),
            pretrained='torchvision://resnet50',
            style='pytorch')),
    rpn_head=dict(
        type='RPNHead',
        in_channels=256,
        feat_channels=256,
        anchor_generator=dict(
            type='AnchorGenerator',
            scales=[8],
            ratios=[0.5, 1.0, 2.0],
            strides=[4, 8, 16, 32, 64]),
        bbox_coder=dict(
            type='DeltaXYWHBBoxCoder',
            target_means=[0.0, 0.0, 0.0, 0.0],
            target_stds=[1.0, 1.0, 1.0, 1.0]),
        loss_cls=dict(
            type='CrossEntropyLoss', use_sigmoid=True, loss_weight=1.0),
        loss_bbox=dict(
            type='SmoothL1Loss', beta=0.1111111111111111, loss_weight=1.0)),
    roi_head=dict(
        type='CascadeRoIHead',
        num_stages=3,
        stage_loss_weights=[1, 0.5, 0.25],
        bbox_roi_extractor=dict(
            type='SingleRoIExtractor',
            roi_layer=dict(type='RoIAlign', out_size=7, sample_num=0),
            out_channels=256,
            featmap_strides=[4, 8, 16, 32]),
        bbox_head=[
            dict(
                type='Shared2FCBBoxHead',
                in_channels=256,
                fc_out_channels=1024,
                roi_feat_size=7,
                num_classes=9,
                bbox_coder=dict(
                    type='DeltaXYWHBBoxCoder',
                    target_means=[0.0, 0.0, 0.0, 0.0],
                    target_stds=[0.1, 0.1, 0.2, 0.2]),
                reg_class_agnostic=True,
                loss_cls=dict(
                    type='CrossEntropyLoss',
                    use_sigmoid=False,
                    loss_weight=1.0),
                loss_bbox=dict(type='SmoothL1Loss', beta=1.0,
                               loss_weight=1.0)),
            dict(
                type='Shared2FCBBoxHead',
                in_channels=256,
                fc_out_channels=1024,
                roi_feat_size=7,
                num_classes=9,
                bbox_coder=dict(
                    type='DeltaXYWHBBoxCoder',
                    target_means=[0.0, 0.0, 0.0, 0.0],
                    target_stds=[0.05, 0.05, 0.1, 0.1]),
                reg_class_agnostic=True,
                loss_cls=dict(
                    type='CrossEntropyLoss',
                    use_sigmoid=False,
                    loss_weight=1.0),
                loss_bbox=dict(type='SmoothL1Loss', beta=1.0,
                               loss_weight=1.0)),
            dict(
                type='Shared2FCBBoxHead',
                in_channels=256,
                fc_out_channels=1024,
                roi_feat_size=7,
                num_classes=9,
                bbox_coder=dict(
                    type='DeltaXYWHBBoxCoder',
                    target_means=[0.0, 0.0, 0.0, 0.0],
                    target_stds=[0.033, 0.033, 0.067, 0.067]),
                reg_class_agnostic=True,
                loss_cls=dict(
                    type='CrossEntropyLoss',
                    use_sigmoid=False,
                    loss_weight=1.0),
                loss_bbox=dict(type='SmoothL1Loss', beta=1.0, loss_weight=1.0))
        ],
        train_cfg=[
            dict(
                assigner=dict(
                    type='MaxIoUAssigner',
                    pos_iou_thr=0.5,
                    neg_iou_thr=0.5,
                    min_pos_iou=0.5,
                    match_low_quality=False,
                    ignore_iof_thr=-1),
                sampler=dict(
                    type='RandomSampler',
                    num=512,
                    pos_fraction=0.25,
                    neg_pos_ub=-1,
                    add_gt_as_proposals=True),
                pos_weight=-1,
                debug=False),
            dict(
                assigner=dict(
                    type='MaxIoUAssigner',
                    pos_iou_thr=0.6,
                    neg_iou_thr=0.6,
                    min_pos_iou=0.6,
                    match_low_quality=False,
                    ignore_iof_thr=-1),
                sampler=dict(
                    type='RandomSampler',
                    num=512,
                    pos_fraction=0.25,
                    neg_pos_ub=-1,
                    add_gt_as_proposals=True),
                pos_weight=-1,
                debug=False),
            dict(
                assigner=dict(
                    type='MaxIoUAssigner',
                    pos_iou_thr=0.7,
                    neg_iou_thr=0.7,
                    min_pos_iou=0.7,
                    match_low_quality=False,
                    ignore_iof_thr=-1),
                sampler=dict(
                    type='RandomSampler',
                    num=512,
                    pos_fraction=0.25,
                    neg_pos_ub=-1,
                    add_gt_as_proposals=True),
                pos_weight=-1,
                debug=False)
        ],
        test_cfg=dict(
            score_thr=0.05, nms=dict(type='nms', iou_thr=0.5),
            max_per_img=100)))
train_cfg = dict(
    rpn=dict(
        assigner=dict(
            type='MaxIoUAssigner',
            pos_iou_thr=0.7,
            neg_iou_thr=0.3,
            min_pos_iou=0.3,
            match_low_quality=True,
            ignore_iof_thr=-1),
        sampler=dict(
            type='RandomSampler',
            num=256,
            pos_fraction=0.5,
            neg_pos_ub=-1,
            add_gt_as_proposals=False),
        allowed_border=0,
        pos_weight=-1,
        debug=False),
    rpn_proposal=dict(
        nms_across_levels=False,
        nms_pre=2000,
        nms_post=2000,
        max_num=2000,
        nms_thr=0.7,
        min_bbox_size=0),
    rcnn=[
        dict(
            assigner=dict(
                type='MaxIoUAssigner',
                pos_iou_thr=0.5,
                neg_iou_thr=0.5,
                min_pos_iou=0.5,
                match_low_quality=False,
                ignore_iof_thr=-1),
            sampler=dict(
                type='RandomSampler',
                num=512,
                pos_fraction=0.25,
                neg_pos_ub=-1,
                add_gt_as_proposals=True),
            pos_weight=-1,
            debug=False),
        dict(
            assigner=dict(
                type='MaxIoUAssigner',
                pos_iou_thr=0.6,
                neg_iou_thr=0.6,
                min_pos_iou=0.6,
                match_low_quality=False,
                ignore_iof_thr=-1),
            sampler=dict(
                type='RandomSampler',
                num=512,
                pos_fraction=0.25,
                neg_pos_ub=-1,
                add_gt_as_proposals=True),
            pos_weight=-1,
            debug=False),
        dict(
            assigner=dict(
                type='MaxIoUAssigner',
                pos_iou_thr=0.7,
                neg_iou_thr=0.7,
                min_pos_iou=0.7,
                match_low_quality=False,
                ignore_iof_thr=-1),
            sampler=dict(
                type='RandomSampler',
                num=512,
                pos_fraction=0.25,
                neg_pos_ub=-1,
                add_gt_as_proposals=True),
            pos_weight=-1,
            debug=False)
    ])
test_cfg = dict(
    rpn=dict(
        nms_across_levels=False,
        nms_pre=1000,
        nms_post=1000,
        max_num=1000,
        nms_thr=0.7,
        min_bbox_size=0),
    rcnn=dict(
        score_thr=0.05, nms=dict(type='nms', iou_thr=0.5), max_per_img=100))
dataset_type = 'CocoDataset'
data_root = 'data/coco/'
classes = ('ПРМУ_поперечная трещина на изгибе', 'ПРМУ_выход трубы из ряда',
           'ПРМУ_Крип', 'ПРМУ_свищи', 'ПРМУ_разрыв трубы',
           'ПРМУ_поперечная трещина в околошовной зоне',
           'ПРМУ_трещина в основном металле', 'ПРМУ_продольные трещины',
           'ПРМУ_Цвета побежалости')
img_norm_cfg = dict(
    mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True)
train_pipeline = [
    dict(type='LoadImageFromFile'),
    dict(type='LoadAnnotations', with_bbox=True),
    dict(type='Resize', img_scale=(1280, 720), keep_ratio=True),
    dict(type='RandomFlip', flip_ratio=0.5),
    dict(
        type='Normalize',
        mean=[123.675, 116.28, 103.53],
        std=[58.395, 57.12, 57.375],
        to_rgb=True),
    dict(type='Pad', size_divisor=32),
    dict(type='DefaultFormatBundle'),
    dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels'])
]
test_pipeline = [
    dict(type='LoadImageFromFile'),
    dict(
        type='MultiScaleFlipAug',
        img_scale=(1280, 720),
        flip=False,
        transforms=[
            dict(type='Resize', keep_ratio=True),
            dict(type='RandomFlip'),
            dict(
                type='Normalize',
                mean=[123.675, 116.28, 103.53],
                std=[58.395, 57.12, 57.375],
                to_rgb=True),
            dict(type='Pad', size_divisor=32),
            dict(type='ImageToTensor', keys=['img']),
            dict(type='Collect', keys=['img'])
        ])
]
data = dict(
    samples_per_gpu=2,
    workers_per_gpu=1,
    train=dict(
        type='CocoDataset',
        classes=('ПРМУ_поперечная трещина на изгибе',
                 'ПРМУ_выход трубы из ряда', 'ПРМУ_Крип', 'ПРМУ_свищи',
                 'ПРМУ_разрыв трубы',
                 'ПРМУ_поперечная трещина в околошовной зоне',
                 'ПРМУ_трещина в основном металле', 'ПРМУ_продольные трещины',
                 'ПРМУ_Цвета побежалости'),
        ann_file='data/coco/annotations/instances_train.json',
        img_prefix='data/coco/train/',
        pipeline=[
            dict(type='LoadImageFromFile'),
            dict(type='LoadAnnotations', with_bbox=True),
            dict(type='Resize', img_scale=(1280, 720), keep_ratio=True),
            dict(type='RandomFlip', flip_ratio=0.5),
            dict(
                type='Normalize',
                mean=[123.675, 116.28, 103.53],
                std=[58.395, 57.12, 57.375],
                to_rgb=True),
            dict(type='Pad', size_divisor=32),
            dict(type='DefaultFormatBundle'),
            dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels'])
        ]),
    val=dict(
        type='CocoDataset',
        classes=('ПРМУ_поперечная трещина на изгибе',
                 'ПРМУ_выход трубы из ряда', 'ПРМУ_Крип', 'ПРМУ_свищи',
                 'ПРМУ_разрыв трубы',
                 'ПРМУ_поперечная трещина в околошовной зоне',
                 'ПРМУ_трещина в основном металле', 'ПРМУ_продольные трещины',
                 'ПРМУ_Цвета побежалости'),
        ann_file='data/coco/annotations/instances_val.json',
        img_prefix='data/coco/val/',
        pipeline=[
            dict(type='LoadImageFromFile'),
            dict(
                type='MultiScaleFlipAug',
                img_scale=(1280, 720),
                flip=False,
                transforms=[
                    dict(type='Resize', keep_ratio=True),
                    dict(type='RandomFlip'),
                    dict(
                        type='Normalize',
                        mean=[123.675, 116.28, 103.53],
                        std=[58.395, 57.12, 57.375],
                        to_rgb=True),
                    dict(type='Pad', size_divisor=32),
                    dict(type='ImageToTensor', keys=['img']),
                    dict(type='Collect', keys=['img'])
                ])
        ]),
    test=dict(
        type='CocoDataset',
        classes=('ПРМУ_поперечная трещина на изгибе',
                 'ПРМУ_выход трубы из ряда', 'ПРМУ_Крип', 'ПРМУ_свищи',
                 'ПРМУ_разрыв трубы',
                 'ПРМУ_поперечная трещина в околошовной зоне',
                 'ПРМУ_трещина в основном металле', 'ПРМУ_продольные трещины',
                 'ПРМУ_Цвета побежалости'),
        ann_file='data/coco/annotations/instances_val.json',
        img_prefix='data/coco/val/',
        pipeline=[
            dict(type='LoadImageFromFile'),
            dict(
                type='MultiScaleFlipAug',
                img_scale=(1280, 720),
                flip=False,
                transforms=[
                    dict(type='Resize', keep_ratio=True),
                    dict(type='RandomFlip'),
                    dict(
                        type='Normalize',
                        mean=[123.675, 116.28, 103.53],
                        std=[58.395, 57.12, 57.375],
                        to_rgb=True),
                    dict(type='Pad', size_divisor=32),
                    dict(type='ImageToTensor', keys=['img']),
                    dict(type='Collect', keys=['img'])
                ])
        ]))
evaluation = dict(interval=1, metric='bbox')
optimizer = dict(type='SGD', lr=0.0001, momentum=0.9, weight_decay=0.0001)
optimizer_config = dict(grad_clip=None, type='OptimizerHook')
lr_config = dict(
    policy='step',
    warmup=None,
    warmup_iters=500,
    warmup_ratio=0.001,
    step=[8, 11],
    type='StepLrUpdaterHook')
total_epochs = 12
checkpoint_config = dict(interval=-1, type='CheckpointHook')
log_config = dict(
    interval=10,
    hooks=[dict(type='TextLoggerHook'),
           dict(type='TensorboardLoggerHook')])
dist_params = dict(backend='nccl')
log_level = 'INFO'
load_from = './checkpoints/detectors_cascade_rcnn_r50_1x_coco-32a10ba0.pth'
resume_from = None
workflow = [('train', 1)]
work_dir = './logs'
seed = 0
gpu_ids = range(0, 1)

Обучение модели

Обучение модели занимало порядка 2 часов на RTX 2080 Ti. Прогресс можно было отслеживать с помощью запущенного tensorboard. Ниже приведены графики эволюции метрик и функции ошибки в процессе обучения на реальных данных.

mhg5vdp2qkme4foee3ib7elde1m
Рис. 16 Зависимость mAP от числа итераций обучения для архитектуры DetectoRS для датасета дефектов труб по 9 классам на валидационном датасете.


Рис. 17 Зависимость функции потерь (multiclass cross entropy) от числа итераций обучения для архитектуры DetectoRS для датасета дефектов труб по 9 классам.

Подготовка модели для использования

После того, как сеть была обучена, ее конфигурационный файл и веса, были помещены внутрь отдельного NVidia Docker контейнера вместе с самим фреймворком и необходимыми зависимостями, для того, чтобы использовать готовую архитектуру внутри прототипа ПО для детекции дефектов.

Стоит отметить, что, несмотря на то, что данная архитектура обучается в несколько раз быстрее чем YOLOv4, она занимает в 2 раза больше памяти (500 MB для DetectoRS против 250 MB для YOLOv4 для хранения весов модели) и работает на порядок медленнее (1 с. для DetectoRS против 10 мс. для YOLOv4).

Малое время обучения DetectoRS отчасти объясняется тем, что веса базовых слоев сети (backbone и neck) были взяты из претренированной на ImageNet датасете аналогичной архитектуры и в процессе обучения не изменялись. Такой прием называется transfer learning. Про него вы можете подробнее прочитать в этой заметке.

Darknet

Оригинальная реализация YOLOV4 написана на C c использованием CUDA C. Было принято решение использовать оригинальную реализацию модели, хотя обычно доминируют эксперименты на Python. Это накладывало свои ограничения и риски, связанные с необходимость разбираться в коде на C, в случае каких-то проблем или переделки частей под свои нужды. Подробной документации с примерами не было, поэтому пришлось в некоторых местах смотреть исходный код на C.

Для успешного запуска нужно было решить несколько проблем:

  1. Собрать проект.
  2. Понять что необходимо для обучения модели.
  3. Обучить модель.
  4. Подготовить код для использования.

Сборка проекта

Первая сборка проекта происходила на Windows. Для сборки использовался CMake, поэтому особых проблем с этим не возникло. Единственная проблема была с компиляцией динамической библиотеки для обёртки на Python. Пришлось редактировать файл с проектом для Visual Studio, чтобы включить поддержку CUDA. Динамическая библиотека была нужна т. к. это позволяло использовать код на Python для запуска модели.

После было принято решение перенести сборку внутрь Docker контейнера. Для того чтобы оставить возможность использовать видеокарту был установлен NVIDIA Container Toolkit. Это позволяет достаточно просто организовать перенос проекта на другую машину при необходимости, а также упрощает дальнейшее использование. Благодаря наличию различных образов nvidia/cuda на Docker Hub, можно достаточно просто менять конфигурации. Например, переключение между различными версиями CUDA или cuDNN.

Необходимые файлы для обучения

Для обучения модели нужно было подготовить данные и дополнительные файлы как было описано в самом начале, а также правильно настроить конфигурационный файл. В репозитории предлагается взять за основу образец, который представляет из себя текстовый файл и поменять в нужных местах значения параметров. В основном всё зависит от числа классов. Т. к. у нас число классов могло варьироваться, то постоянно так делать было нецелесообразно. Конфигурационный файл был шаблонизирован с помощью выражений jinja2. Код на Python позволял генерировать нужные конфигурационные файлы в зависимости от входных параметров.

Обучение модели

Обучение модели производилось в контейнере NVIDIA Docker. Занимало порядка 12 часов на RTX 2080 Ti. Прогресс можно было отслеживать периодически копируя график функции потерь из контейнера на хост, где запущен контейнер. Красным показано значение mAP на тестовой выборке.


Рис. 18 График обучения YOLOv4.

Подготовка модели для использования

После обучения модели была необходимость в ее использовании для визуализации результатов. От кода на Python пришлось отказаться т. к. были проблемы с неожиданным завершением программы. Некоторые части кода пришлось дописать на языке С под собственные нужды.

После всех экспериментов были выявлены следующие зависимости:

  • Обучение на смеси данных (реальные + искусственные) приводит к небольшому ухудшению обобщающей способности (это связано с тем, что в данной итерации генератора синтетических данных присутствуют недостаточно разнообразные данные и однотипное освещение);
  • После недолгого подбора гиперпараметров для архитектуры DetectoRS удалось добиться показателя $mAP (IoU=0.5) = 0.85$, а для архитектуры YOLOv4 $mAP(IoU=0.5) = 0.74$ ;
  • Некоторые типы дефектов, такие как разного рода трещины или выход труб из ряда, детектируются лучше чем иные типы дефектов, такие как вздутие труб. Это можно объяснить не только дисбалансом и малым количеством примеров, но и тем, что для определения некоторых типов дефектов, нужно больше пространственной информации (аналогично этому, в реальной детекции дефектов, некоторые дефекты определяются на глаз, а некоторые требуют специальных измерительных приборов). Потенциально, использование помимо RGB каналов с камеры также еще и канала глубины (RGB-D) могло бы помочь с детектированием этих сложных пространственных дефектов: в этом случае мы смогли бы прибегнуть к методам и алгоритмама 3D ML.


Рис. 19 Сравнение работы архитектур, обученных на разных датасетах: Real — только реальные изображение, Mixed — обучении на смеси реальных и синтетических изображений, Mask — обучения на реальных изображениях, с многоугольной разметкой областей.

onf 8lgpgc91cdpo4z9bynpptcg
Рис. 20 Пример некорректной детекции модели DetectoRS, обученной на синтетических данных — отсутствие посторонних предметов в синтетическом датасете приводит к определению куска деревянной балки как трещины.


Рис. 21 Пример детекции дефектов в виде многоугольных масок модели DetectoRS, обученной на датасете реальных изображений с соответствующей разметкой.

fxwseglcfhkluaz20sjl
Рис. 22 Пример детекции дефектов в виде прямоугольников для одного и того же изображения с помощью разных архитектур.

В итоге, был получен работающий прототип ПО с интерфейсом, который можно было бы просто использовать как со стороны пользователя так и для разработчиков. Так как проекты на основе ML часто требуют вычислительных мощностей видеокарт NVIDIA для своей работы, то было принято решение сделать клиент-серверное приложение. При необходимости всё можно развернуть на одном компьютере. Если же есть в наличии свободный сервер с видеокартой, то основную логику можно перенести туда, оставив возможность любым пользователям использовать сервис даже на слабом по вычислительным возможностям компьютере.

Для архитектуры ПО была выбрана следующая схема:

Финальное решение с интерфейсом

В итоге, был получен работающий прототип ПО с интерфейсом, который можно было бы просто использовать как со стороны пользователя так и для разработчиков. Так как проекты на основе ML часто требуют вычислительных мощностей видеокарт NVIDIA для своей работы, то было принято решение сделать клиент-серверное приложение. При необходимости всё можно развернуть на одном компьютере. Если же есть в наличии свободный сервер с видеокартой, то основную логику можно перенести туда, оставив возможность любым пользователям использовать сервис даже на слабом по вычислительным возможностям компьютере.

Для архитектуры ПО была выбрана следующая схема:


Рис. 23 Схема архитектуры прототипа ПО для детектирования дефектов на изображениях.

Все модели для детектирования дефектов работают внутри NVIDIA Docker. Остальные части, кроме Web-интерфейса внутри обычных контейнеров Docker. Логика работы следующая:

  1. Пользователь загружает изображение и выбирает нужную модель вместе с порогом принятия решения.
  2. Изображение отправляется на сервер. Оно сохраняется на диске и сообщение с заданием на обработку попадает в RabbitMQ, в соответствующую очередь.
  3. Модель берёт сообщение с заданием из очереди, когда готова выполнить предсказание. Выполняет предсказание и сохраняет необходимые файлы на диск. Отправляет сообщение в RabbitMQ о готовности результат.
  4. Когда результат готов он отображается в web-интерфейсе.

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

RabbitMQ также позволяет изолировать систему друг от друга и начать разработку независимо. При разработке сервера не надо дожидаться реализации ML моделей и наоборот. Каждая система общается только с RabbitMQ. В случае падения какого-то сервиса сообщение не потеряется, если соответствующим образом настроить RabbitMQ.

t1twvkhva0tcwodo5mc0ubmkqbu
Рис. 24 Демонстрация работы созданного прототипа ПО для детекции дефектов на трубах.

В заключении, отметим, что при разработке ПО, в котором используются модели машинного обучения, требуется очень много обмениваться данными и целыми пайплайнами эксперементов с членами команды. Для того, чтобы это было удобно делать мы пользовались на протяжении всего проекта системой DVC. Примеры использования этой системы контроля данных и моделей машинного обучения можно посмотреть в этой статье на хабре.

Статьи описывающие SOTA глубокие архитектуры в задаче детекции:

  • Qiao, S., Chen, L.C. and Yuille, A., 2020. DetectoRS: Detecting Objects with Recursive Feature Pyramid and Switchable Atrous Convolution. arXiv preprint arXiv:2006.02334.
  • Bochkovskiy, A., Wang, C.Y. and Liao, H.Y.M., 2020. YOLOv4: Optimal Speed and Accuracy of Object Detection. arXiv preprint arXiv:2004.10934.
  • Tan, M., Pang, R. and Le, Q.V., 2020. Efficientdet: Scalable and efficient object detection. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (pp. 10781-10790).

Статьи, посвященные выбору метрик качества в задачах детекции объектов на изображениях:

  • Zhao, Z.Q., Zheng, P., Xu, S.T. and Wu, X., 2019. Object detection with deep learning: A review. IEEE transactions on neural networks and learning systems, 30(11), pp.3212-3232.

Фундаментальные монографии на тему современного компьютерного зрения:

  • Szeliski, R., 2010. Computer vision: algorithms and applications. Springer Science & Business Media.
  • Nixon, M. and Aguado, A., 2019. Feature extraction and image processing for computer vision. Academic press.
  • Jiang, X. ed., 2019. Deep Learning in Object Detection and Recognition. Springer.
  • Pardo, A. and Kittler, J. eds., 2015. Progress in Pattern Recognition, Image Analysis, Computer Vision, and Applications: 20th Iberoamerican Congress, CIARP 2015, Montevideo, Uruguay, November 9-12, 2015, Proceedings (Vol. 9423). Springer.

На днях в продажу поступила обновлённая версия одного из самых популярных устройств с Алисой — Станции Мини. Может показаться, что это эволюционное развитие, работать над которым было проще, чем создавать оригинал. Но новая Мини отличается от предыдущей примерно всем: от внешнего вида и начинки до условий производства — ковид и глобальный дефицит компонентов стали для hardware-команды большим вызовом.

bojoq9spdz164vvkzf ztcjsyzw

Сегодня я поделюсь с вами несколькими историями о новой колонке. Вы узнаете про одно из самых необычных применений фазоинвертора, про то, как мы достаточно комично сушили фотополимерные модели в домашних условиях. Расскажу, где спрятан сервисный разъём и как работает управление просмотром ТВ через ещё одно новое устройство — Модуль. Будут и другие детали, которые показались нам интересными.

С чего всё начиналось

В марте 2021 года мы впервые раскрыли число проданных колонок с Алисой. На тот момент их было уже более 1,3 миллиона. Существенный вклад в это число внесла Станция Мини. Её без лишней скромности можно назвать самой народной умной колонкой на российском рынке. Компактные габариты, невысокая цена, отзывчивый голосовой помощник, крепкое сообщество пользователей. Поэтому неудивительно, что ещё в начале прошлого года мы приступили к работе над обновлённой версией. И почти сразу оказались на развилке.

Любая разработка начинается с анализа потребностей. Мы видели явный запрос от пользователей Мини — получить более мощный звук. Проблема в том, что такие улучшения заметно влияют и на цену устройства, и на его габариты. Что делать: пойти на небольшие шаги, сохранив прежнюю или близкую к ней цену, или на значительные доработки со всеми вытекающими последствиями? Нам удалось радикально решить эту дилемму — сделать две модели.

pkrgehtfi57

В июле мы запустили в продажу Станцию Лайт. Она заняла нишу самых доступных умных колонок с Алисой — с ценой даже ниже, чем у оригинальной Мини. При этом подросла мощность звука (5 Вт вместо 3 Вт) и появилась поддержка частоты 5 ГГц для Wi-Fi-соединения.

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

История про звук

Самое очевидное изменение — новый динамик. В оригинальной Станции Мини использовался трёхваттный динамик диаметром 1,5 дюйма. Мы его подобрали по каталогу доступных компонентов: так проще и быстрее, если нет высоких требований к звуку. А вот для новой Мини приоритеты пересмотрели, поэтому динамик проектировали специально под устройство: вы не найдёте на рынке точную копию. Он стал чуть больше (1,75 дюйма), но мощнее сразу в три раза. Даже магнит для динамика мы взяли более дорогой — неодимовый, а не ферритовый, хотя в подобных бытовых девайсах обычно экономят и не заморачиваются.
nl6agjhwapzqrcs0hy3uwvfgzw
Следующий очевидный шаг — более высококлассный усилитель. В новой колонке тот же самый усилитель Hi-Fi-класса, что и в нашем флагмане — Станции Макс, с той разницей, что в Максе трёхполосный звук, поэтому там два таких усилителя.

Для питания устройства используется напряжение 15 вольт — втрое больше, чем в оригинале. Это позволило нам обеспечить оптимальный режим работы усилителя, уменьшить THD и получить запас по выходной мощности. В то же время мы вписались в требования стандарта по электромагнитной совместимости.

И именно чтобы добиться напряжения 15 вольт, мы приняли сложное решение (за которое нас точно будут ругать на Хабре!) — отказались от разъёма USB Type-C в пользу классического DC jack. Type-C без поддержки протокола Power Delivery (PD) не может корректно обеспечить напряжение выше 5 вольт, а добавление PD заметно влияет на цену.

При этом наличие порта Type-C давало бы ложное ощущение совместимости. Люди бы пробовали питать колонку от произвольных блоков питания — большинство из которых PD не поддерживают — и получали непредсказуемые эффекты, для обработки которых пришлось бы усложнить схемотехнику и написать довольно много кода. Так что мы решили не использовать в Мини универсальный с виду порт, который не можем сделать по-настоящему универсальным.

Но вернёмся к звуку. В акустике есть простой принцип: чем больше акустическая камера, тем проще достичь хорошего звука — вот почему качественные аудиоустройства редко бывают компактных размеров. По этой причине и новая Мини стала немного больше — как бы нам ни хотелось, получить заметно более качественный и мощный звук без увеличения размеров не получалось.

biizlhcdzhhfashwvc cbcqe4cw

Давайте посмотрим на акустическую схему. Динамик установлен в середине Станции и направлен вниз. Выходя из него, звук попадает на акустическую линзу, которая служит дном колонки. Роль линзы в том, чтобы рассеивать идущий вниз из динамика звук горизонтально во все стороны. Качество звука во многом зависит именно от этих компонентов, а не только от динамика. И здесь начинаются не самые очевидные решения. Например, мы поняли, что простая конусообразная форма центральной части линзы хорошо рассеивает звук, но при этом на него негативно влияют воздушные карманы вокруг конуса. По итогам экспериментов получилось не совсем обычная форма — середина уже не конусообразная, а гораздо более пологая: это обеспечивает более свободное прохождение воздуха.

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

Стенки у основания корпуса — там, где выходит звук, — решётчатые. Отдельной неожиданной задачей для нас стал долгий подбор формы и размера отверстий решётки. Слишком маленькие отверстия мешают прохождению звука и портят общее качество звучания. Слишком большие — приводят к тому, что акустическая ткань, натянутая поверх корпуса устройства, или провисает сразу, или со временем теряет форму под действием потоков воздуха через решётку — в наших aging-тестах мы поймали такой эффект. После множества итераций мы в конце концов нашли ту оптимальную форму решётки, которая показана на изображении.

tgnzkfsnjthltr7qb7raapdl0vy

Одно из часто встречающихся решений для расширения нижней части звукового диапазона (увеличения басовой составляющей, что принципиально важно в таких компактных устройствах) — фазоинвертор. Самый простой и часто применяемый фазоинвертор — это трубка в составе акустической системы, длина и площадь сечения которой подбираются таким образом, чтобы собственная резонансная частота корпуса вместе со встроенным в него фазоинвертором была меньше резонансной частоты динамика. Получается, что мы «подхватываем» хвост низкочастотного диапазона и значительно повышаем общий КПД системы. Идея выглядит просто, но есть тысяча нюансов в её реализации. Например, классическая трубка нам практически не помогала — у неё получалась слишком малая длина, а чем длина больше, тем более низкие частоты можно воспроизвести. Относительно быстро у нас родилась идея кругового фазоинвертора. Грубо говоря — трубки, проложенной вдоль стенок корпуса: один конец выходит внутрь акустической камеры, другой — наружу. Но потом ушло два месяца на расчёты и эксперименты, чтобы подобрать оптимальную длину, сечение, а главное — вписать это в конструкцию устройства вместе с основной платой и платой LED-экрана.

zk3dia3mjezul srph9kiis93ai

Фазоинвертор без крышки

В дизайне фазоинверторов важна конфигурация входного и выходного отверстий — обратите внимание на придуманную нами форму. Стенки в этих местах не должны содержать стыков, а крепления и другие компоненты потребовалось расположить так, чтобы они даже частично не перекрывали потоки воздуха через трубку. Кстати, у фазоинвертора оказалось ещё одно неожиданно полезное применение — читайте об этом чуть ниже.

Мощный динамик и усилитель, дизайн акустической линзы, решёток и акустической камеры в целом, а также нестандартный фазоинвертор — за счёт этих слагаемых нам удалось поднять качество звука новой Мини, и мы надеемся, что пользователи это заметят.

История про охлаждение

Эта история — снова про габариты корпуса против мощности звука. Да, габариты по сравнению с оригинальной Мини подросли, но совсем незначительно, в то же время звуковая мощность увеличилась в три раза. А чем больше и мощнее динамик, тем сильнее он греется (у любых динамиков очень невысокий КПД превращения подводимой энергии питания в звуковые волны — бóльшая часть этой энергии тратится на тепло). И компоненты платы тоже нагреваются — в усилителе нас спасает его высокоэффективный класс и автовыключение при простое, но при проигрывании музыки 10-15% мощности неизбежно превращаются в тепло. Греется многое: процессор, потому что на нём постоянно «крутится» механизм голосовой активации (чтобы колонка реагировала на слово «Алиса»), модуль Wi-Fi и преобразователи DC-DC, благодаря которым вырабатываются все нужные напряжения и у которых хоть и высокий, но не стопроцентный КПД.

Чтобы не вызывать негативных ощущений, когда пользователь берёт устройство в руки или регулирует громкость сенсорными кнопками, мы добавили металлический радиатор. Хотя, если быть совсем корректным, то это не радиатор, а теплораспределительная пластина — прямого излучения тепла от неё не происходит. Пластина установлена в верхней части колонки: сверху плотно прилегает к плате, а снизу — к фазоинвертору. Причём если она лишь более равномерно распределяет температуру по плате, то наличие фазоинвертора позволяет постепенно отводить тепло. Сначала — на саму трубку, а затем и наружу, поскольку мембрана динамика колеблется вверх-вниз — напомню, что динамик расположен вертикально. При обратном ходе создаваемым разряжением мембрана «выталкивает» воздух наружу через фазоинвертор.

Можете проделать простой эксперимент: когда колонка играет на высокой громкости, поднесите руку к выходу фазоинвертора (где он прячется под тканью, можно посмотреть на схеме выше) и вы ощутите лёгкий поток тёплого воздуха.

История про разработку в пандемию

Начальные стадии разработки новой Станции Мини во многом пришлись на первую волну пандемии весной 2020 года, когда ограничения были наиболее жёсткими — помните, как в Москве нужно было получать пропуска для поездок? Естественно, что большинство сотрудников Яндекса, включая и команду умных устройств, работали из дома. Сломалось межкомандное взаимодействие — одно дело сидеть за соседними столами в офисе, другое — на самоизоляции по домам, ещё и без наработанной привычки удалённых коммуникаций. Но почему это вдвойне проблемно в случае с hardware-разработкой?

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

В нашем случае, поскольку мы сильно завязаны на акустику устройства, проработка начинается со звука — акустический инженер, используя полученные от инженера-конструктора предварительные 3D-модели, придумывает, как заставить будущую колонку звучать максимально хорошо. Это множество расчётов и экспериментов (помните рассказ про фазоинвертор выше?), в результате рождается какой-то вариант внутренней конструкции. Получив его, инженер-конструктор делает первую оптимизацию — смотрит, насколько сложно это производить массово. Вполне может оказаться, что нужно вернуться на шаг назад, потому что придуманная конструкция оказалась слишком сложной: могут быть детали, которые легко напечатать на 3D-принтере, но нереально «пролить» в термопластавтомате на фабрике. Параллельно конструктор работает с инженерами-электронщиками — они совместно стараются решить, как оптимально расположить схемотехнику на платах и скомпоновать их в корпусе, учитывая акустику, охлаждение и потенциальную сложность последующей сборки.

Предположим, проблемы конструкции более-менее решены. В этот момент мы начинаем собирать прототипы целиком, вместе с платами. Соответственно, платы надо паять, а устройства — собирать воедино. Собранный девайс должен начать оживать — для него требуется прошивка, значит, к процессу подключаются программисты. Подробнее об этапах разработки можно почитать в одной из наших предыдущих статей, а ещё подробнее мы описывали процесс в одном из стримов Я.Железа:

Если собрать всё сказанное воедино, станет понятно: разработка — это множество итераций, требующих постоянных обсуждений и взаимодействий внутри команды, «мозговых штурмов» для сложных случаев, а также изготовления и тестирования прототипов.

Чтобы быстро изготавливать прототипы, для сборки печатных плат нужен монтажник и склад компонентов, а для корпусов — 3D-принтер и фрезерный станок. Всё оборудование вместе с деталями всегда находилось в офисе, где к нему был доступ у каждого, а тут пришлось как-то разделить его по участникам разработки. Конечно, любой электронщик в нашей hardware-команде вполне прилично паяет, но если этим заниматься постоянно, то напрашивается аналогия с забиванием гвоздей микроскопом — можно, но очень неэффективно. С монтажом выход был найден: склад компонентов по большей части остался в офисе, а для одного из монтажников выписали спецпропуск, обеспечивающий передвижение по городу и доступ в офис по необходимости.

Но что душой кривить — было больно: с началом пандемии летевший на полном ходу поезд разработки как будто врезался в невидимую стену.

Помогли коллеги из Яндекс Go: мы постоянно стали обмениваться образцами и компонентами, благо московский трафик был в разы свободнее обычного. Как-то водитель такси, забирая в четвёртый раз за день от одного и того же отправителя коробочки и пакетики для развоза, поинтересовался, уж не наркотики ли мы передаём.

Недостаток оборудования (не поставишь же каждому по дорогущему 3D-принтеру с необходимыми аксессуарами!) временами приходилось компенсировать подручными средствами. Например, один из разработчиков вместо «сушилки» сырых, только что напечатанных на SLA-принтере 3D-моделей использовал кастрюлю. Изнутри он обклеил её крышку лентой из ультрафиолетовых светодиодов: модели сушатся не только путём нагрева, но и за счёт специального освещения.

lcj4rupyy6r1g1oi8lebmevlff4

История про сервисный разъём — угадаете, где он спрятан?

Узнать ответ

В процессе тестирования на фабрике нужно иметь возможность подключиться к колонке в сервисном режиме, чтобы запустить тесты и убедиться в отсутствии брака. Часто это требуется и разработчикам при отладке. Со Станцией Мини проблем не было — мы подключались через свободные линии разъёма USB Type-C, который одновременно служил и портом питания. В новой Мини, как я уже писал, мы отказались от Type-C в пользу DC jack, который ни для чего, кроме питания, использоваться не может.

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

x b llivpscllsib1 cwk2m0zou

Мы решили, что сервисным разъёмом будет мини-джек 3,5 мм, основное предназначение которого — вывод звука на колонки или наушники. Через него вполне возможно передавать данные, просто он редко используется для передачи чего-либо, кроме звука. Но мы здесь не первопроходцы: скажем, последние iPod shuffle от Apple синхронизировались с компьютером именно через мини-джек.

Небольшие накладные расходы себя оправдали: теперь не нужно ничего отклеивать и приклеивать назад.

История про LED-экран

Спереди у новой Станции Мини, за тканью, есть экран — почти как у Станции Макс. Он выполнен проще, но мы всё равно постарались сделать его визуально интересным. Светодиоды образуют числовое табло, на котором показывается время, температура за окном или таймер — в зависимости от запросов к Алисе. В правом верхнем углу табло есть ещё один круглый светящийся сегмент — знак градуса для показа погоды, который также горит при включённом будильнике.

С одной стороны, в Станции Макс мы уже решали задачу размещения яркого белого экрана за тканью — и подробно рассказывали об этом на Хабре. Дисплей флагманской колонки получил множество позитивных отзывов — как раз потому, что он полюбился пользователям, мы и решили, что обязательно найдём способ добавить что-то похожее в младшую версию.

С другой стороны, адаптация решения под небольшую колонку потребовала усилий. Светодиоды в Станции Мини освещают сегменты табло — это не экран из точек, как в Максе. Ткань тоже другая, с более частыми ячейками: сначала мы попробовали использовать такую же ткань, как в старшей колонке, но на маленьком устройстве она смотрелась как «кольчуга».

qmvrqcw6kj5b21vl9jkiygbzuqm

Поэтому эксперименты по размещению светодиодов практически начались заново. Мы по-прежнему используем конструкцию собственной разработки: с узким основанием диода и широкой внешней частью, чтобы свечение немного рассеивалось и границы между сегментами чисел были менее заметны. Этому же эффекту способствует рассеивающая плёнка, которая, напомню, расположена под тканью. Но всё зависит от того, насколько глубоко внутрь корпуса поместить экран и какую выбрать толщину рассеивателя. При слишком глубоком расположении картинка смотрелась равномернее, но падала яркость, а плёнка могла вызвать боковую утечку света. Другая по сравнению со Станцией Макс ткань тоже накладывала ограничения: на более частой сетке легче увидеть огрехи изображения, так что нужно было добиться большей чёткости. Кроме того, под тканями разных цветов (а новая Мини, напомню, доступна в четырёх цветах) цифры смотрелись по-разному, при этом конфигурацию дисплея нужно было сделать одинаковой — придумывать разные конфигурации было бы неразумно с точки зрения простоты производства. Мы постарались найти баланс во всех этих вопросах.

2 iuer7enuqfvp5de1yhdrizfjq

У экрана есть ещё один секрет. Хотя Станцию Мини обычно ставят ниже уровня глаз, людям чаще будет казаться, что экран смотрит прямо на них: мы сделали нижние сегменты цифр чуть шире верхних и тем самым немного снизили эффект параллакса.

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

История про верхнюю панель

Громкость теперь можно менять кнопками — или, как обычно, попросив Алису. Мы добавили кнопки по просьбам владельцев оригинальной Мини и ожидаем, что люди будут довольно часто управлять устройством с их помощью. Во-первых, маленькие колонки обычно ставят в такие места, до которых легко дотянуться рукой, например на кухонный или рабочий стол. Во-вторых, Станцией Мини часто пользуются дети. Ориентируясь на непрерывное (и не обязательно аккуратное) использование кнопок, мы сделали их сенсорными, а заодно и более лаконичными по своей конструкции внутри корпуса. В конце концов, это компактное устройство, в котором каждый миллиметр на счету.

7q1fqcqpoivopp1jcquqyve89ly

Между кнопками расположен логотип Алисы, но его можно увидеть только во время диалога с помощником. В остальное время, даже если приглядеться, очертания лого на пластике не видны.

Для достижения такого эффекта мы применили технологию In-Mold Labeling (декорирование внутри формы). Изготавливается многослойная плёнка с вырезом, повторяющим очертания логотипа. Эта плёнка помещается в пресс-форму — в неё же под давлением отливается прозрачный пластик. Если получившуюся поверхность не освещать снизу, она кажется однородной. Кстати, под логотипом скрывается ещё одна сенсорная клавиша: Play/Pause. Механическим остался только переключатель Mute на задней стороне, который по-прежнему физически отключает микрофоны.

История про Модуль с Яндекс.ТВ, тандем и стереопару

Вместе с обновлённой Станцией Мини мы выпустили Модуль — он подключается по HDMI к телевизору и позволяет смотреть контент из КиноПоиска и других источников. Оба новых устройства дополняют экосистему, которую мы строим с целью сделать каждый девайс удобнее. Модуль — отличный пример: если он у вас тоже есть и вы настроили режим под названием тандем, то Алиса на Станции Мини сможет, например, включить для вас сериал на большом экране через Модуль. Чтобы описать, как работает тандем, сначала надо рассказать про софтверную часть устройств. Схематично любую колонку Яндекса можно представить так:

h9ikvzwmmmr

Приложение для каждого устройства включает в себя два компонента:

  • Девайсо-специфичный код, который в случае Мини отвечает за работу с усилителем, сенсорными кнопками, LED-экраном и так далее.
  • IO SDK — одинаковый для всех Станций и Модуля. Именно IO SDK обменивается сигналами с облачной инсталляцией Алисы — куда также можно подключиться из мобильного приложения Яндекс на телефоне:

omi53y2jui084k0dy g0tn83 tu

Модуль устроен похоже:

Операционная система Модуля основана на Android Open Source Project (AOSP). В Модуле больше приложений, например КиноПоиск и лончер (домашний экран телевизора). Компонентом одного из приложений служит тот же самый IO SDK.

Когда вы в мобильном приложении Яндекс настраиваете тандем, бэкенд отправляет в IO SDK Станции и Модуля конфигурационные сигналы о том, что Станция теперь управляет Модулем. Это меняет логику работы колонки: если попросить включить фильм, она уже не ответит, что ей нужен экран. Вместо этого Станция локально найдёт Модуль и отправит ему управляющий сигнал о запуске фильма:

tq48uk1cg4luci

Мой коллега Иван Калинин придумал имя для протокола локального общения между двумя устройствами: GLAGOL. Это рекурсивный акроним: GLAGOL is Lookup And Governance Over LAN («поиск и управление по локальной сети»). Он основан на мультикастном DNS и механизме веб-сокетов. Существуют аналоги GLAGOL, но реализация для Станций и Модуля целиком написана в Яндексе.

Чтобы произвольное устройство не могло таким же образом подключиться к Модулю (и, например, перехватить какие-нибудь персональные данные), IO SDK Модуля авторизовывает клиента и убеждается, что управляющим сигналам от данной колонки можно доверять, что это безопасно.

В ближайшие недели мы откроем для всех ещё одну интересную возможность — объединять две Станции Мини в стереопару. Функция уже доступна, но пока в бета-режиме. Она дополняет технологию мультирум, благодаря которой одну и ту же музыку легко запустить сразу на нескольких устройствах (работает с момента выпуска Станции Макс). Стереопара отличается от мультирума тем, что звук действительно разделяется между колонками на правый и левый каналы, создавая более объёмное звучание. Принцип и технологии здесь те же, что и при общении между Станцией и Модулем в тандеме. Новый режим даже настраивается там же — в приложении Яндекс, только конфигурация немного иная. Например, когда стереопара активна, можно управлять воспроизведением при помощи кнопок на любой колонке. А, скажем, микрофоны работают только на одной из них, чтобы вторая вас не слышала и голосовые команды не дублировались.

История про «чипагеддон», или Как мы научились выживать в условиях постоянного дефицита полупроводников

Безусловно, нашу работу осложнили ограничения весны 2020 года, об этом я писал выше. Но гораздо более серьёзный вызов ждал нас впереди — на Хабре, наверное, нет того, кто не слышал о дефиците на рынке полупроводников, затронувшем практически все отрасли промышленности. С лёгкой руки журналистов появился термин «чипагеддон» (сhipageddon), который очень хорошо характеризует суть явления.

В качестве причин обычно называют изменения в потребительском спросе за последние полтора года, ошибки автопроизводителей в оценке рынка после первой волны ковида, торговую войну США и Китая, концентрацию 70% мирового рынка чипов на Тайване (остров, к тому же, этой весной пострадал от сильнейшей засухи) и принципиальную проблему системы «just in time». Согласно последней, предприятия держали нулевые или минимальные складские запасы комплектующих. Естественно, появляются и конспиративные теории о заговоре компаний с целью увеличения прибыли, куда же без этого. :)

Мы как крупный производитель, потребляющий миллионы чипов в год и работающий напрямую с большинством вендоров компонентов, неожиданно стали сталкиваться с нарастающей волной дефицита. Усилители, модули Wi-Fi, микросхемы конвертеров DC-DC и банальные LDO вдруг начали случайным образом исчезать. О каких-то поставках удавалось передоговориться с производителями, что-то приходилось покупать на спекулятивном рынке по ценам в пять и даже в десять раз выше. Чему-то удавалось найти замены, которые электронщики Яндекса в срочном порядке валидировали. Но в целом ситуация была очень тяжёлой, и наша объединённая московско-шеньчженьская команда закупок компонентов работала на износ и делала невероятные вещи, чтобы спасти ситуацию.

Пришлось перестраивать работу на ходу и разрабатывать ревизии, то есть варианты устройства с альтернативным набором компонентов. Казалось бы, здесь нет ничего сложного, но наш процесс разработки подразумевает тщательное тестирование выбранных деталей. Для более простых, вроде новых DC-DC, это функциональные и aging-тесты; для более сложных, таких как модуль Wi-Fi, это целый пласт работ по проверке, обязательно включающий в себя бета-тестирование внутри команды Яндекса.

В итоге основные сложности были преодолены, но выход новой Мини на рынок, к сожалению, задержался. Зато мы поняли, как перестроить процесс разработки — острота кризиса полупроводников сейчас чуть спала, но он вовсе не думает заканчиваться. Теперь мы гораздо активнее смотрим по сторонам и валидируем существенно больше потенциально подходящих компонентов, а разработку до PVT-стадии ведём в двух ветках (в массовое производство уходит та из них, компоненты для которой оказываются более доступными для поставки). Да, процесс стал ресурсозатратнее, но и мы стали гораздо устойчивее к дефициту.

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

  • Сложный план и сочинение чацкий и молчалин
  • Сложные слова для сочинения
  • Сложные предложения из рассказа бедная лиза
  • Сложный план по рассказу дубровский
  • Сложные предложения из рассказа капитанская дочка