3 minute read

Ya que los resultados del entrenamiento no fueron lo suficientemente buenos, se propuso entrenar el modelo con una mayor cantidad de imágenes. Además, también se planteó implementar un modelo de segmentación LiDAR.

Entrenamiento del modelo de segmentación semántica

Para el primer entrenamiento se uso un dataset con 150 imágenes, una cantidad bastante reducida, lo que provocó varios fallos en la segmentación semántica, como:

  • Regiones del cielo clasificadas como negro
  • Zonas moradas (clase camino) apareciendo sobre áreas de césped

Para esta ocasión, se uso un dataset con 1500 imágenes, es decir, 10 veces más grande que en el anterior entrenamiento, lo que permitió obtener una segmentación más precisa, eliminando los errores anteriores.

Resultado

Modelo de segmentación LiDAR

Después de introducir un modelo de segmentación semántica en CARLA, se propuso el añadir también uno de segmentación LiDAR. Para ello, se contactó con David Pascual, quién proporcionó un script de inferencia (inference.py) que se encargaba de aplicar los modelos entrenados previamente por él en nubes de puntos en formato .bin. Por lo tanto, hubo que hacer pequeños cambios en el código para que funcionase en tiempo real en el script de CARLA. Para el modelo, se decidió usar un entorno con mmdetection3d por recomendación de David, ya que produjo buenos resultados.

get_sample

Está función es la encargada de recibir la nube de puntos en Nx3 o Nx4 y prepar un diccionario compatible con la pipeline de mmdetection3d.

def get_sample(points: np.ndarray, has_intensity=True):
    n_feats = 4 if has_intensity else 3

    # Recortamos a las dimensiones que quiera el modelo (x,y,z / x,y,z,intensidad)
    points = points[:, : n_feats].astype(np.float32)

    sample = {
        "points": torch.from_numpy(points).float(), # Convertir a tensor
        "pts_semantic_mask_path": None,
        "sample_id": None,
        "sample_idx": None,
        "num_pts_feats": n_feats,
        "lidar_path": None,
    }

Posteriormente se aplica el Pack3DDetInputs para generar los datos esperados por el modelo.

    # Lista de transforms (sin LoadPointsFromFile, porque ya tenemos el array)
    transforms = []
    # Esto es lo que Pack3DDetInputs espera: sample["points"]


    if sample["pts_semantic_mask_path"] is not None:
        transforms.append(
            LoadAnnotations3D(
                with_bbox_3d=False,
                with_label_3d=False,
                with_seg_3d=True,
                seg_3d_dtype="np.uint32",
                seg_offset=65536,
                dataset_type="semantickitti",
            )
        )
    transforms.append(
        Pack3DDetInputs(
            keys=["points", "pts_semantic_mask"],
            meta_keys=["sample_idx", "lidar_path", "num_pts_feats", "sample_id"],
        )
    )

    pipeline = Compose(transforms)
    sample = pipeline(sample)

    return sample

get_lut

Se encarga de generar la tabla de colores RGb para cada etiqueta de segmentación.

def get_lut(ontology):
    max_idx = max(class_data["idx"] for class_data in ontology.values())
    lut = np.zeros((max_idx + 1, 3), dtype=np.uint8)
    for class_data in ontology.values():
        lut[class_data["idx"]] = class_data["rgb"]
    return lut

load_segmentation_model_lidar

Carga el modelo entrenado anteriormente.

def load_segmentation_model_lidar(model_path: str, device: str | None = None):
    if device is None:
        device = "cuda" if torch.cuda.is_available() else "cpu"
    model = torch.load(model_path, map_location=device)
    return model.to(device).eval()

inference

Es donde se realiza todo el proceso de inferencia. Primero se prepara la imágen convirtiendo el array de numpy a torch.Tensor y se empaqueta en un diccionario con la estructura que espera mmdetection3d, se le aplican las transformaciones necesarias para que el modelo reciba los datos y se separan los datos en los puntos preprocesado y los metadatos asociados. Despues, se llama al modelo en modo predict, que devuelve una lista de resultados, y también se obtiene un array pred con los id de la clase predicha para los puntos. Finalmente, se aplica los colores a cada punto dependiendo del id de clase, en caso de no pasar una ontology externa, se asigna un color aleatorio a cada id.

Se devuelve la clase predicha de cada punto (pred), el color normalizado de cada punto (colors) y el diccionario usado para la asignación de colores, para poder reusarse entre frames.

def inference(
    model,
    points: np.ndarray,
    has_intensity: bool = False,
    ontology: dict | None = None,
    device: str = "cuda",
):
    """
    points: np.array (N,4) o (N,3)
    """
    # 1) Preparar el sample
    sample = get_sample(points, has_intensity)
    sample = COLLATE_FN([sample])
    sample = model.data_preprocessor(sample, training=False)
    inputs, data_samples = sample["inputs"], sample["data_samples"]

    # 2) Inferencia
    output = model(inputs, data_samples, mode="predict")[0]
    pred = output.pred_pts_seg.pts_semantic_mask.squeeze().cpu().numpy()  # (N,)

    # 3) Ontología / colores
    unique_labels = np.unique(pred)
    if ontology is None:
        ontology = {
            str(int(label)): {
                "idx": int(label),
                "rgb": np.random.randint(0, 256, size=3).tolist(),
            }
            for label in unique_labels
        }

    lut = get_lut(ontology)
    colors = lut[pred] / 255.0  # (N,3) en [0,1]

    return pred, colors, ontology

Resultado

Este es el primer resultado obtenido con el modelo proporcionado por David, que no esta entrenado para CARLA, y el ontology.json usado para segmentación semántica.