Нормали к поверхности
Оценка нормалей к поверхности предсказывает, в какую сторону обращена каждая видимая поверхность. В 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.pngpath: 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 и в
полной матрице экспорта. В разделе
экспорт перечислены аргументы, которые принимает каждый формат.