Посмотреть как Markdown

Нормали к поверхности

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

Определение

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

Предсказание заполняет result.normal_map — структуру NormalMap с массивом (H, W, 3) типа float32 на исходном холсте изображения, доступную также как result.normals. Векторы заданы в системе координат камеры OpenCV, которую использует LibreYOLO: +x вправо, +y вниз и +z вглубь сцены; они направлены к камере, поэтому фронтально-параллельная поверхность даёт (0, 0, -1). .assert_normalized() проверяет, что каждый пиксель конечен и имеет единичную длину в пределах допуска. result.boxes остаётся пустым, поэтому conf, iou и max_det ни на что не влияют, а Results.plot() эту задачу покрывает.

Модели

Задачу normal обслуживают два семейства.

MoGe-2 — специализированное семейство: монокулярная геометрическая модель в трёх размерах энкодера, которая работает за один прямой проход. LibreYOLO не копирует эти чекпойнты в свою организацию: при загрузке нужный размер скачивается из официальных репозиториев на зафиксированной ревизии и проверяется по записанному SHA-256.

LibreMODUS выдаёт нормали как одну из целей any-to-any модели и может принимать на вход карту глубины вместо RGB-изображения. Ему нужен extra modus и собственный аутентифицированный аккаунт Hugging Face, и он не предлагает ни val(), ни export(), поэтому в разделах про валидацию и экспорт ниже он не участвует.

Предсказание

Веса MoGe-2 скачиваются при первом запуске и кэшируются локально.

Предсказание поля нормалей
from libreyolo import LibreYOLO, SAMPLE_IMAGE model = LibreYOLO("LibreMoGe2s-normal.pt")result = model(SAMPLE_IMAGE, save=True) normals = result.normal_mapprint(normals.data.shape)      # (H, W, 3) единичные векторы float32normals.assert_normalized()    # бросает исключение, если хоть у одного пикселя длина не единичная
Чтение одного пикселя
from libreyolo import LibreYOLO, SAMPLE_IMAGE model = LibreYOLO("LibreMoGe2s-normal.pt")result = model(SAMPLE_IMAGE) # Система координат камеры OpenCV: +x вправо, +y вниз, +z вглубь сцены.# Поверхность, обращённая к камере, даёт примерно (0, 0, -1).field = result.normals.datah, w = field.shape[:2]print(field[h // 2, w // 2])
Сохранение визуализации
from libreyolo import LibreYOLO, SAMPLE_IMAGE model = LibreYOLO("LibreMoGe2s-normal.pt")result = model(SAMPLE_IMAGE) # plot() рисует поле; он определён для результатов normal и edge.result.plot().save("normals.png")

imgsz должен нацело делиться на размер патча ViT-энкодера — LibreYOLO проверяет это до запуска. Предсказание по списку изображений выполняет один прямой проход на каждое изображение; быстрого пути со сложенным батчем у этой задачи нет. Об источниках, стриминге и обработке результатов — в разделе предсказание.

Формат датасета

При валидации нормалей каждому изображению соответствует одноимённый трёхканальный 16-битный PNG того же разрешения и, необязательно, маска валидности.

dataset/
  data.yaml
  images/
    val/room.jpg
  normals/
    val/room.png
  masks/
    val/room.png
yaml
path: dataset
train: images/train
val: images/val
normals_dir: normals
masks_dir: masks
nc: 1
names: {0: normal}

Целевой PNG — строго трёхканальный uint16, каналы хранятся как RGB. Декодирование — n = png / 65535 * 2 - 1 с последующей перенормировкой каждого вектора, а декодированные векторы заданы в той же системе координат камеры OpenCV, что и предсказания. Пиксель маски считается валидным, когда он ненулевой; без файла маски валиден каждый конечный ненулевой декодированный вектор. Невалидные и дополненные целевые пиксели хранятся внутри как (0, 0, 0) и никогда не влияют на метрику. Полный контракт — в разделе форматы датасетов.

Обучение

Ни у одного из двух семейств для нормалей нет реализации обучения: train() выбрасывает NotImplementedError у обоих. На странице MoGe-2 указаны его зафиксированные официальные чекпойнты — для предсказания, валидации и экспорта.

Валидация

val() измеряет угол между каждым предсказанным вектором и соответствующим вектором эталонной разметки (ground truth) по тем пикселям, которые датасет помечает как валидные.

Валидация и чтение ключей метрик
from libreyolo import LibreYOLO model = LibreYOLO("LibreMoGe2s-normal.pt")metrics = model.val(data="my-dataset.yaml", imgsz=518) print(metrics["metrics/mean_angular_error"])     # градусыprint(metrics["metrics/median_angular_error"])   # градусыprint(metrics["metrics/within_11_25"])           # процент пикселейprint(metrics["metrics/within_22_5"], metrics["metrics/within_30"])

metrics/mean_angular_error и metrics/median_angular_error — этот угол в градусах, меньше — лучше. metrics/within_11_25, metrics/within_22_5 и metrics/within_30 — процент валидных пикселей, у которых угловая ошибка укладывается в 11.25, 22.5 и 30 градусов, так что больше — лучше. Обратите внимание на единицу измерения: эти три метрики — проценты, а не доли. fitness — это metrics/within_11_25, делённое на 100, что ставит выбор лучшего чекпойнта на ту же шкалу [0, 1], что и во всех остальных задачах.

Экспорт

Экспортированная модель нормалей загружается обратно через LibreYOLO() по суффиксу файла, поэтому файл .onnx ведёт себя как чекпойнт и возвращает тот же Results.

Экспорт
from libreyolo import LibreYOLO model = LibreYOLO("LibreMoGe2s-normal.pt")model.export(format="onnx", imgsz=518)
Запуск экспортированного файла
from libreyolo import LibreYOLO, SAMPLE_IMAGE # Фабрика выбирает загрузчик по суффиксу файла, поэтому экспортированный# артефакт загружается как любой чекпойнт и возвращает тот же Results.model = LibreYOLO("LibreMoGe2s-normal.onnx")result = model(SAMPLE_IMAGE) print(result.normal_map.data.shape)

Экспорт нормалей работает по контракту среды выполнения с фиксированным разрешением и batch=1: dynamic и batch, отличный от 1, отклоняются, а imgsz должен нацело делиться на размер патча энкодера. Поддержка по форматам — на странице MoGe-2 и в полной матрице экспорта. В разделе экспорт перечислены аргументы, которые принимает каждый формат.

Проверено с LibreYOLO v1.5.0.