Normales de superficie
La estimación de normales de superficie predice hacia dónde mira cada superficie visible. LibreYOLO la expone como la tarea normal, que devuelve un campo denso de vectores unitarios sobre el lienzo de la imagen original.
Definición
La tarea normal predice un vector unitario de tres componentes por píxel a
partir de una sola imagen RGB: la dirección hacia la que mira la superficie en
ese píxel. A diferencia de la profundidad, la salida no tiene una escala libre,
así que dos predicciones son directamente comparables sin alinearlas.
Una predicción rellena result.normal_map, un payload NormalMap que contiene
un array (H, W, 3) float32 sobre el lienzo de la imagen original, accesible
también como result.normals. Los vectores usan el sistema de cámara de OpenCV
de LibreYOLO, con +x a la derecha, +y hacia abajo y +z hacia la escena, y
miran hacia la cámara, así que una superficie fronto-paralela da (0, 0, -1).
.assert_normalized() comprueba que todos los píxeles son finitos y tienen
longitud unitaria dentro de una tolerancia. result.boxes queda vacío, así que
conf, iou y max_det no tienen efecto, y Results.plot() cubre esta tarea.
Modelos
Dos familias cubren normal.
MoGe-2 es la dedicada: un modelo de geometría monocular de un solo forward, en tres tamaños de encoder. LibreYOLO no copia estos checkpoints a su propia organización; cargar uno descarga el tamaño correspondiente desde los repositorios oficiales en una revisión fijada y lo verifica contra un SHA-256 registrado.
LibreMODUS produce normales como uno de los objetivos
de un modelo any-to-any, y puede tomar como entrada un mapa de profundidad en
lugar de una imagen RGB. Necesita el extra modus y tu propia cuenta autenticada
de Hugging Face, y no ofrece ni val() ni export(), así que no participa en las
secciones de validación y exportación de más abajo.
Predicción
Los pesos de MoGe-2 se descargan en el primer uso y se cachean en local.
from libreyolo import LibreYOLO, SAMPLE_IMAGE model = LibreYOLO("LibreMoGe2s-normal.pt")result = model(SAMPLE_IMAGE, save=True) normals = result.normal_mapprint(normals.data.shape) # vectores unitarios float32 (H, W, 3)normals.assert_normalized() # falla si algún píxel no tiene longitud unitariafrom libreyolo import LibreYOLO, SAMPLE_IMAGE model = LibreYOLO("LibreMoGe2s-normal.pt")result = model(SAMPLE_IMAGE) # Sistema de cámara de OpenCV: +x a la derecha, +y hacia abajo, +z hacia# la escena. Una superficie que mira a la cámara da algo cercano a (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() dibuja el campo; está definido para resultados de normales y de bordes.result.plot().save("normals.png")imgsz debe ser divisible por el tamaño de parche del encoder ViT, algo que
LibreYOLO comprueba antes de que empiece la ejecución. Predecir sobre una lista
de imágenes ejecuta un forward por imagen; esta tarea no tiene una ruta rápida
por batch apilado. Consulta predicción para fuentes, streaming y
manejo de resultados.
Formato del dataset
La validación de normales empareja cada imagen con un PNG de 16 bits y tres canales, con el mismo nombre base y la misma resolución, más una máscara de validez opcional.
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}El PNG objetivo es exactamente uint16 de tres canales, con los canales
almacenados como RGB. La decodificación es n = png / 65535 * 2 - 1 seguida de
renormalizar cada vector, y los vectores decodificados usan el mismo sistema de
cámara de OpenCV que las predicciones. Un píxel de la máscara cuenta como válido
cuando es distinto de cero; sin archivo de máscara, todo vector decodificado
finito y distinto de cero es válido. Los píxeles objetivo inválidos y los de
padding se guardan internamente como (0, 0, 0) y nunca contribuyen a una
métrica. Consulta formatos de dataset para el
contrato completo.
Entrenamiento
Ninguna de las dos familias de normales tiene implementación de entrenamiento:
train() lanza NotImplementedError en ambas. La página de MoGe-2 señala sus
checkpoints oficiales fijados para predecir, validar y exportar.
Validación
val() mide el ángulo entre cada vector predicho y su vector de ground truth,
sobre los píxeles que el dataset marca como válidos.
from libreyolo import LibreYOLO model = LibreYOLO("LibreMoGe2s-normal.pt")metrics = model.val(data="my-dataset.yaml", imgsz=518) print(metrics["metrics/mean_angular_error"]) # gradosprint(metrics["metrics/median_angular_error"]) # gradosprint(metrics["metrics/within_11_25"]) # porcentaje de píxelesprint(metrics["metrics/within_22_5"], metrics["metrics/within_30"])metrics/mean_angular_error y metrics/median_angular_error son ese ángulo en
grados, y cuanto más bajo mejor. metrics/within_11_25, metrics/within_22_5 y
metrics/within_30 son el porcentaje de píxeles válidos cuyo error angular queda
por debajo de 11,25, 22,5 y 30 grados, así que cuanto más alto mejor. Fíjate en
la unidad: esos tres son porcentajes, no fracciones. fitness es
metrics/within_11_25 dividido entre 100, lo que sitúa la selección del mejor
checkpoint en la misma escala [0, 1] que en todas las demás tareas.
Exportación
Un modelo de normales exportado se vuelve a cargar con LibreYOLO() según la
extensión de su archivo, así que un .onnx se comporta como un checkpoint y
devuelve el mismo Results.
from libreyolo import LibreYOLO model = LibreYOLO("LibreMoGe2s-normal.pt")model.export(format="onnx", imgsz=518)from libreyolo import LibreYOLO, SAMPLE_IMAGE # La factory decide según la extensión del archivo, así que un artefacto# exportado se carga como cualquier checkpoint y devuelve el mismo Results.model = LibreYOLO("LibreMoGe2s-normal.onnx")result = model(SAMPLE_IMAGE) print(result.normal_map.data.shape)La exportación de normales usa un contrato de runtime de resolución fija y batch
1: se rechazan dynamic y cualquier batch distinto de 1, y imgsz debe ser
divisible por el tamaño de parche del encoder. La cobertura por formato está en
la página de MoGe-2 y en la
matriz completa de exportación.
Exportación lista los argumentos que acepta cada formato.