
Автор: Кристина Паревская
Мы живем в мире быстро развивающихся технологий. С каждым годом frontend-разработка проще не становится. Сегодня frontend-разработчики могут не просто создавать обычные формы, но и игры, и даже запускать модели ИИ для выполнения задач, например, распознавания объекта. В данной статье будет рассказано, как на примере системы по распознаванию возгораний объекта в доме можно без backend части добавить в свое приложение модель для обнаружения пожара.
Погружаемся в тему пожаров и возгораний
Распознавание возгораний объектов на ранних стадиях является важной и актуальной проблемой в наши дни, решение которой снизит экономический риски и спасет жизни многих людей.
Такие компании, как Johnson Controls, Honeywell International, Inc., GENTEX CORPORATION, Siemens, Robert Bosch GmbH, Halmaplc, Eaton, Raytheon Technologies Corporation уделяют свое внимание исследованиям в области распознавания возгораний объектов и предлагают свои решения по устранению пожаров. Этими компаниями движут желание помочь людям, быстрое развитие беспроводных технологий и развитие строительной отрасли, охватившей весь мир.

Размер рынка систем защиты от пожаров в 2023 году по данным Grand View Research составлял 83.28 млрд долларов. По данным того же исследования прогноз размера рынка к 2030 году будет составлять 130.37 млрд долларов. Таким образом, темпы роста (CAGR) будут составлять 6.6% (рисунок 1).
На точность выявления пожаров оказывают влияние различные факторы, среди которых уровень освещенности, погодные условия, прозрачность окружающей среды, а также сходство характерных признаков с облачностью и туманом. Дополнительные трудности при обнаружении пожаров вызывают непостоянные характеристики, такие как форма, цвет и текстура. Пожар относится к числу наиболее сложных объектов для обнаружения, что обусловлено необходимостью создания специализированных методов и алгоритмов, которые принимают во внимание помехи, способные влиять на точность и надежность выявления объектов в видеопотоке.
На сегодняшний день создано большое количество сайтов, приложений и различных сервисов, решающих данную проблему. При разработке приложения планируется взять лучшее от существующих приложений, и избавиться от их недостатков, улучшив важные при разработке показатели, а именно скорость распознавания и точность распознавания.
Используемые технологии
Для разработки приложения, способного распознавать возгорание объекта были выбраны следующие технологии:
язык программирования: JavaScript, TypeScript, Python, CSS, HTML;
препроцессор: SCSS;
сборщик: Vite;
база данных: PostgreSQL;
фреймворк: Express;
библиотека: React, YOLOv8.
Что такое ONNX Runtime?
ONNX Runtime — кроссплатформенный ускоритель моделей машинного обучения с гибким интерфейсом для интеграции библиотек, специфичных для оборудования. ONNX Runtime можно использовать с моделями из PyTorch, Tensorflow/Keras, TFLite, scikit-learn и других фреймворков.
Причины, почему нам может понадобиться использовать ONNX Runtime:
ускорение инференса. За счёт различных оптимизаций (удаление лишних узлов, фьюзинг слоёв и др.) ONNX Runtime делает инференс значительно быстрее, чем исходный PyTorch;
инференс на различных языках программирования, различных устройствах и бэкендах (providers). Сейчас на официальной странице указано множество языков программирования, архитектур вычислителей и бэкендов, так что каждый сможет найти подходящий инструмент для своего приложения.
Более того, разработчики ONNX Runtime постоянно расширяют возможности библиотеки, так что это не просто забытая технология.
Как работает ONNX Runtime
ONNX Runtime Web (ORT Web) ускоряет вывод модели в браузере как на CPU, так и на GPU, через WebAssembly (WASM) и бэкэнды WebGL по отдельности. Для вывода CPU ORT Web компилирует собственный движок CPU ONNX Runtime в бэкэнд WASM с помощью Emscripten. WebGL — популярный стандарт для доступа к возможностям GPU, принятый ORT Web для достижения высокой производительности на GPU. Схему работы можно увидеть на рисунке 2.

Для получения подробной информации о работе ONNX Runtime Web рекомендую ознакомиться со статьей “ONNX Runtime Web — running your machine learning model in browser” (https://opensource.microsoft.com/blog/2021/09/02/onnx-runtime-web-running-your-machine-learning-model-in-browser/)
Перевод модели в формат ONNX
Для сохранения моделей используется общепринятый формат ONNX, в который можно перевести если не любую модель, то почти любую. Чтобы это сделать можно воспользоваться следующим скриптом:
from ultralytics import YOLO
model = YOLO("best.pt")
model.export(format="onnx")
Подключение библиотеки
Для подключения библиотеки можно воспользоваться одним из двух способов:
использовать npm-пакет:
# install latest release version
npm install onnxruntime-web
# install nightly build dev version
npm install onnxruntime-web@dev
2. использовать скрипт:
<script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>
Загрузка модели
Первым шагом в worker вынесем логику, ответственную за загрузку модели. Его использование позволило не блокировать пользовательский интерфейс при загрузке модели.
Сначала импортируем библиотеку:
importScripts('https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js');
Для запуска модели создадим функцию runModel. Функция принимает на вход input – массив, который получен функцией prepareInputData (код функции будет описан ниже).
let model = null;
async function runModel(input) {
if (!model) {
model = await ort.InferenceSession.create('./best.onnx');
}
input = new ort.Tensor(Float32Array.from(input), [1, 3, 640, 640]);
const outputs = await model.run({ images: input });
return outputs['output0'].data;
}
В первых трех строчках функции происходит загрузка модели.
В четвертой строке осуществляется подготовка входного массива. ONNX Runtime требует преобразовать его во внутренний ort.Tensor объект. Конструктор этого объекта требует указать массив чисел, преобразованный в Float32, и форму, которую должен иметь этот массив [1,3,640,640].
Массив [1,3,640,640] показывает размеры тензора. Массив должен быть четырехмерным, который показывает, что одно изображение содержит 3 матрицы чисел с плавающей точкой (матрицы цветовых компонентов, значения которых от 0 до 255) размером 640x640.
На последних строчках функции получаем выходные данные и возвращаем данные первого. «output0» это имя выходного значения для YOLOv8.
В результате функция возвращает массив с формой (1,84,8400).
onmessage является обработчиком для worker. Метод нужен для отправки в основной поток выходных данных.
onmessage = async (event) => {
const input = event.data;
const output = await runModel(input);
postMessage(output);
};
Основная функция
Функция prepareVideoData отвечает за инициализацию canvas, подготовку данных для кадра и отрисовку областей при нахождении смога или огня.
export const prepareVideoData = (video) => {
const canvas = document.querySelector('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const context = canvas.getContext('2d');
interval = setInterval(() => {
context.drawImage(video, 0, 0);
drawBoxGroup(canvas, boxes);
const input = prepareInputData(canvas);
if (!busy) {
worker.postMessage(input);
busy = true;
}
}, 30);
return interval;
};
Подготовка данных
Функция prepareInputData ответственна за деление изображения на каналы rgb. Такое деление необходимо для дальнейшей работы с данными в функции prepareOutput.
function prepareInputData(img) {
const canvas = document.createElement('canvas');
canvas.width = IMG_SIZE;
canvas.height = IMG_SIZE;
const context = canvas.getContext('2d');
context.drawImage(img, 0, 0, IMG_SIZE, IMG_SIZE);
const data = context.getImageData(0, 0, IMG_SIZE, IMG_SIZE).data;
const red = [],
green = [],
blue = [];
for (let index = 0; index < data.length; index += 4) {
red.push(data[index] / 255);
green.push(data[index + 1] / 255);
blue.push(data[index + 2] / 255);
}
return [...red, ...green, ...blue];
}
В первой части функции создается невидимый холст размером 640x640 и отображается на нем входное изображение. Это необходимо для получения доступа к пикселям.
После делим изображение на каналы rgb (red, green, blue) и объединяем их вместе в один массив.
Подготовка выходных данных
Функция prepareOutput ответственна за подготовку изображения и вывод его пользователю.
function prepareOutput(output, img_width, img_height) {
let boxes = [];
for (let index = 0; index < 8400; index++) {
const [class_id, prob] = [...Array(CLASSES.length).keys()]
.map((col) => [col, output[8400 * (col + 4) + index]])
.reduce((accum, item) => (item[1] > accum[1] ? item : accum), [0, 0]);
if (prob < 0.5) {
continue;
}
На вход передаются данные, подготовленные функцией, описанной выше и размеры изображения.
В цикле генерируются class_id и prob (максимальная вероятность) для этого класса. Для их получения необходимо сгенерировать массив, значения которого будут от 0 до количества меток.
Затем map функция создает массив вероятностей для каждого class_id, где каждый элемент собирается как [class_id, probability].
После функция reduce сводит массив к одному элементу, который содержит максимальную вероятность и ее идентификатор класса.
Если вероятность класса меньше 0.5, то объект пропускается, в противном случае ищется метка класса.
const label = CLASSES[class_id];
const xc = output[index];
const yc = output[8400 + index];
const w = output[2 * 8400 + index];
const h = output[3 * 8400 + index];
const x1 = ((xc - w / 2) / IMG_SIZE) * img_width;
const y1 = ((yc - h / 2) / IMG_SIZE) * img_height;
const x2 = ((xc + w / 2) / IMG_SIZE) * img_width;
const y2 = ((yc + h / 2) / IMG_SIZE) * img_height;
boxes.push([x1, y1, x2, y2, label, prob]);
}
boxes = boxes.sort((box1, box2) => box2[5] - box1[5]);
const result = [];
while (boxes.length > 0) {
result.push(boxes[0]);
boxes = boxes.filter(
(box) => iou(boxes[0], box) < 0.7 || boxes[0][4] !== box[4]
);
}
return result;
}
Далее находим координаты box-элементов и возвращаем результат.
Для реализации функции понадобилось реализовать функции, которых нет в языке программирования JS. Это iou (функция, которая отвечает за высчитывание метрики, которая используется для оценки точности алгоритмов объектного обнаружения, измеряющая степень перекрытия между предсказанным и реальным ограничивающим прямоугольником), intersection и coefficient.
Заключительным шагом происходит фильтрация массива boxes методом «non-maximum suppression». Это этап постобработки, применяемый после того, как модель обнаружения объектов сгенерировала свой начальный набор ограничительных рамок-кандидатов. Логика этого метода заключается в следующем:
отсортировать границы от большей вероятности к меньшей;
наибольшую вероятность сохраняем в подготовленный массив;
удаляем все поля с вероятностью больше 0,7.
Весь код функции представлен ниже:
function prepareOutput(output, img_width, img_height) {
let boxes = [];
for (let index = 0; index < 8400; index++) {
const [class_id, prob] = [...Array(CLASSES.length).keys()]
.map((col) => [col, output[8400 * (col + 4) + index]])
.reduce((accum, item) => (item[1] > accum[1] ? item : accum), [0, 0]);
if (prob < 0.5) {
continue;
}
const label = CLASSES[class_id];
const xc = output[index];
const yc = output[8400 + index];
const w = output[2 * 8400 + index];
const h = output[3 * 8400 + index];
const x1 = ((xc - w / 2) / IMG_SIZE) * img_width;
const y1 = ((yc - h / 2) / IMG_SIZE) * img_height;
const x2 = ((xc + w / 2) / IMG_SIZE) * img_width;
const y2 = ((yc + h / 2) / IMG_SIZE) * img_height;
boxes.push([x1, y1, x2, y2, label, prob]);
}
boxes = boxes.sort((box1, box2) => box2[5] - box1[5]);
const result = [];
while (boxes.length > 0) {
result.push(boxes[0]);
boxes = boxes.filter(
(box) => iou(boxes[0], box) < 0.7 || boxes[0][4] !== box[4]
);
}
return result;
}
Для получения подробной информации по разработке с использованием модели YOLOv8 рекомендую ознакомиться с серией статей «How to detect objects on images using the YOLOv8 neural network» (https://dev.to/andreygermanov/a-practical-introduction-to-object-detection-with-yolov8-neural-network-3n8).
Таким образом, была создана часть приложения, ответственная за распознавание с использованием модели YOLOv8. Приложение по обнаружению огня можно увидеть на рисунке 3.

Вид готового приложения
Готовое MVP приложения выглядит следующим образом (рисунок 4).

При состоянии системы, когда пожар не был обнаружен, интерфейс главной страницы информирует пользователя, что очагов возгорания обнаружено не было (рисунке 4). Кнопка находится в состоянии disabled с соответствующей надписью, элемент сигнализации без побуждающей к действию анимации.
На рисунке 5 приведен пример состояния главной страницы, когда пожар обнаружен. Желтый элемент, он же сигнализация, приобретает анимацию трясения, кнопка обнаружения пожара приобретает красный цвет. Нажатие на последнюю из них переводит пользователя на страницу пожарная тревога.

Страница «Пожарная тревога» (рисунок 6) содержит статус обнаружения пожара, блок «Обнаружение пожара» с детальной информацией по дому, в какой квартире, на каком этаже было обнаружено возгорание. При нажатии на номер квартиры, где был обнаружен пожар, появляется окно, в котором отображается видео в реальном времени с обнаруженным очагом возгорания. Для обнаружения пожара используется модель ИИ.

На панели управления есть два блока «Управление всеми сигнализациями» и «Управление сигнализациями по этажам», активация сигнализации этажа включает все соответствующие сигнализации (рисунок 7).

Для удобства есть блок «История срабатываний», чтобы всегда иметь доступ к информации по срабатыванию сигнализаций (рисунок 8).

Заключение
В статье были показаны возможности frontend-части приложения, когда без знаний backend и глубоких знаний ML можно добавить в приложение модель ИИ, и быстро проверить ее работоспособность.
Может быть, идея приложения кого-то вдохновит на создание большого сервиса, который мог бы хранить информацию по распознаванию пожара не в 1 доме, а в нескольких, что существенно бы улучшило ситуацию с возникновением пожаров по всему городу, а, возможно, даже и стране. Все зависит от вашей фантазии и возможностей.
Полезные ссылки для дальнейшего изучения