LaLaLoc++ 논문 및 코드리뷰 (1)

8 분 소요

시작하면서.

산학과제 적용을 위해 이런저런 기술과 논문을 찾아보던 중, 가장 유력한 방법에 대한 논문을 찾았다.
그런 이유로, 깊게 리뷰해보면서, 코드와 같이 어떻게 구현되었는지 쫒차가 보려 한다. 사고의 흐름대로 읽어가는 것이니, 혹여나 이 페이지를 보신분께선 별 도움 안될것 같아 미리 심심한 양해를 구한다.

방금 검색해봤다. 영화 라라랜드(La La Land)가 언제 상영되었는가 했더만 2016년도다. LaLaLoc…… 굉장히 합리적인 의심이 든다.

2026-06-30-22-53-31
2026-06-30-22-53-31

Introduction

Floor palns are Ubiqquitous in built, indoor environement. 와.. 유비쿼터스란 말 참 오랜만에 본다. 논문을 쓴 저자의 소속이 옥스포드라서 그런가. 단어의 선정 하나하나가 참 고급지네…

The prevalance of these documents across domains is not an hisroical fluke, but instead because thet have a number of useful characteristic. ..? H 앞인데 an 이네. fluke가 우연이란 뜻이었구… 후루꾸…아… 문장이 정말… 고급지네..

  • sturctural plan – stored in local govermetn, – real-estate agent – fire-safey

무엇이 Useful 하냐.

첫번째는 Permanence(영속성) 다. 가구나 물체들은 언제든 옮겨지고 바뀔수 있지만, 영구적인 정보들은 항상 도면에 남아있게 마련이다. 두번째는 Expressiveness(표현성)이다. 이 도면을 보는 사람(reader)에게 내부 정보에 대한 이해(good basis for understanding)를 제공한다. 세번째는 Convenience(편리함)이다. 이런 도면정보는 용량도 크지 않고, 인쇄하기도 쉽고, 디지털정보로 가지고다니기도 편하다.

2026-06-30-12-29-16
2026-06-30-12-29-16

그러한 전처로. We turn our attention to direct interpretation of structural blueprints.

Commonly localisa..tion? 에? 영국식 표현인가?

일반적으로 Localization 방법에서 Prior는 3D 포인트 클라우드의 형태를 띄고 있거나, [24]
[24]Sarlin, P.E., Cadena, C., Siegwart, R., Dymczyk, M.: From coarse to fine: Robust hierarchical localization at large scale. In: Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. pp. 12716–12725 (2019)

여러 연속된 이미지로 저장되어있거나. [1,2]->1번은 NetVLAD다.

  1. Arandjelovic, R., Gronat, P., Torii, A., Pajdla, T., Sivic, J.: Netvlad: Cnn architecture for weakly supervised place recognition. In: Proceedings of the IEEE conference on computer vision and pattern recognition. pp. 5297–5307 (2016)
  2. Balntas, V., Li, S., Prisacariu, V.: Relocnet: Continuous metric learning relocalisation using neural nets. In: Proceedings of the European Conference on Computer Vision (ECCV). pp. 751–767 (2018)

신경망의 가중치로 저장하고 있다. [4]

  1. Brachmann, E., Krull, A., Nowozin, S., Shotton, J., Michel, F., Gumhold, S., Rother, C.: Dsac-differentiable ransac for camera localization. In: Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. pp. 6684–6692 (2017)

이제 여기서 LaLaLoc++를 제안한다. 방법이 좀 특이하다.

기존의 floor plan localization 방법들은 먼저 평면도를 명시적인(explicit) 3차원 구조로 변환해야 했다. 이를 위해 알려진 천장 높이를 사용하거나, 사람이 직접 천장 높이와 카메라 높이를 추정하여 floor plan을 3차원으로 돌출(extrusion)시키는 과정이 필요했다.

그러나 LaLaLoc++는 2차원 floor plan에 대한 일반적인 사전 지식을 학습함으로써, 별도의 명시적 3차원 모델을 만들지 않고도 한 번의 추론만으로 floor plan 전체에서 3차원 구조를 암묵적으로(hallucinate) 추정할 수 있다.(원문 번역한 내용이다.) Floor Plan Comprehension Module 를 통해 기존 방법에서 필요했던 알려진 카메라 높이나 천장 높이 가정을 제거하고, RGB panorama embedding 모듈과 결합을 통해 성능을 높였다고 한다.

Method

LaLaLoc++ 는 2D 캐드데이터와 파노라마 이미지라는 다른 데이터 형태(Modality)를 두개의 네트워크를 통해 처리한다.

Floor Plan Comprehension Module(Φ_plan)

2차원 floor plan으로부터 각 위치에서 관측 가능한(room layout) 구조를 추론한다.
즉, floor plan 전체의 x-y 평면에 대해 모든 위치마다 하나의 잠재 벡터(latent vector)를 생성한다.

2026-07-01-00-26-48
2026-07-01-00-26-48

Image Embedding Module(Φ_image) with Long Attention

RGB 파노라마 영상으로부터 방 구조(room layout)를 추출하여 잠재 표현(latent representation)으로 변환한다.

2026-07-01-00-46-44
2026-07-01-00-46-44

이 두 모듈은 동일한 잠재 공간(shared latent space) 으로 매핑되도록 학습된다. 개념 자체는 LaLaLoc 오리지널과 유사한것 같다. 기존에는 2D 캐드 도면을 3차원공간으로 추정하는 과정을 통해 했다면, 이번엔 특별한 방식을 써서 그 과정을 생략햇다는게 눈여겨볼만한 포인트일것 같다.

마찬가지로 특정 위치에서 촬영된 파노라마를 Φ_image로 임베딩한 결과와, 같은 위치의 floor plan feature는 서로 동일한 잠재 표현을 갖도록 학습된다.

Localisation

위치를 찾는 컨셉 자체는 그렇게 특별한 건 없다. 이미 FloorPlan(2D캐드맵)을 네트워크를 통과시켜 $\Phi_{plan}$을 구했다. 캐드 도면 픽셀 하나하나마다 Dense하게 구했고, 더불어 파노라마 이미지를 통해 $\Phi_{imgage}$를 구했으므로, 그 둘간에 가장 유사한 지점을 찾으면 된다. 이게 시작점이된다.

이후, $\Phi_{plan}$을 $G$로, $\Phi_{imgage}$을 $f$ 로 표현을 바꿔 $ f-G[p_{0}] _2$ 가 최소화 되도록 pose refinement를 시행하는, 이걸 Sub pixel interpolation이라고 명명하고 있다.

왜 이 짓을 또하지? 라는 생각이 드는데.

이게 Grid 단위가 커서 그렇다는 것 같다…..ㅡ_ㅡ.. 그래서 주변 그리드와 내삽을 통해 정하면 오차율을 줄일 수 있다고……

이걸 굳이 여기서 줄여야 하나 싶긴 하다. 카메라 이미지로 찾았으면, 전통적인 로봇슬램이라면 다른 거리 측정센서를 통해 보정해도 될텐데…… 어쨌거나 논문은 그렇게 써져 있다. 기존 LaLaLoc 오리지날 버전 대비해서 이 부분을 개선해서 속도가 빨라졌다고 한다.

2026-07-01-12-12-43
2026-07-01-12-12-43

숫자 2 옆에 dot 연산이 있다. 이건 look-up 함수를 쓴다는 의미라고 한다. 정말로 별거 없단 소리네. 알고리즘 자체는 매우 다순하단 소리인데.

Training

트레이닝 과정이 좀 궁금하긴 했다. 도대체 뭘 학습하는걸까.

$\Phi_{plan}$ 의 학습

먼저 캐드 도면을 네트워크를 통과시켜 각 지점의 latenet vector를 얻는다.
그리고, 이걸 Layout Decoder에 통과시키면 Layout에 대한 Depth 이미지를 얻는다고 한다.

\[L_{decode} = |L' - L|\]

여기서 L’은 예측한 Layout이고, L은 redering한 Layout이라는데, 이 renering이란게 2D 캐드를 이용해서 3D로 복원한걸 말한다. 이걸 실제 3D 값과 비교해서 비슷하게 출력하도록 학습하는 과정이 핵심이다. VAE나, GAN을 쓸법도 한데, 그때 그 시절이어서 그런지 여기선 U-net을 이용한다.

독특한 단어를 사용하는데, hallucination of 3D structure라는 말이 등장한다. 이 허상이란게 2D캐드를 통해 만들어낸 3D공간이란 의미다.

$\Phi_{image}$ 의 학습

이미지의 학습은 간단하다는데. 카메라를 통해 얻은 RGB 이미지를 이제 같은 feature공간으로 보내야 한다. 어쨋거나, Position에대한 GT 알고 있으므로(학습이니까). 2D 도면을 통해 얻은 latent를 GT로, 잡아 Loss를 정의하고 학습한다.

end-to-end네..? 중간에 뭐 없고 쭉 간다고..?

\[\ell_{L2} = |f-g|_{2\cdot}\]

이렇게 학습하면, Map을 학습하는 네트워크와, 이미지를 학습하는 네트워크의 정의는 완료된다.

앞어서 서술했지만, Map은 U-net 구조를 통해 학습하고, 이미지는 Resnet-50과 Transformer구조를 사용한다고 한다.

한번 해보자.

Code review

Github 주소는 https://github.com/ActiveVisionLab/LaLaLoc 이다. LaLaLoc++도 업데이트 되어있다.

2026-07-01-15-03-45
2026-07-01-15-03-45

Readme 를 따라 다음과 같이 실행시키면 된다.

실행은 두가지로 나뉘는데, 우선 Map을 학습하는 Layout/Plan 먼저 알아보자.

먼저 버전이었던 LaLaLoc을 실행시키고자 한다면,

python train.py -c configs/layout_branch.yaml \
    DATASET.PATH [path/to/dataset]

아니면 LaLaLoc++ 를 실행시키고자 한다면 명령어를 다음과 같이 바꾸면 된다.

python train.py -c configs/lalaloc_pp/layout_branch.yaml \
    DATASET.PATH [path/to/dataset]

그 다음, 이걸 입력이미지와 학습시키는 과정인 Image Branch는 다음과 같이 실행시킨다.

# LaLaLoc image branch
python train.py -c configs/image_branch.yaml \
    DATASET.PATH [path/to/dataset] \
    TRAIN.SOURCE_WEIGHTS [path/to/layout_branch_checkpoint]
# LaLaLoc++ image branch
python train.py -c configs/lalaloc_pp/image_branch.yaml \
    DATASET.PATH [path/to/dataset] \
    TRAIN.SOURCE_WEIGHTS [path/to/plan_branch_checkpoint]

덧붙여서 테스트 방법도 붙여 놓는다.

python train.py -c configs/layout_branch.yaml -t [path/to/checkpoint] \
    DATASET.PATH [path/to/dataset] \
    SYSTEM.NUM_GPUS 1 \
    TEST.VOGEL_DISC_REFINE True \
    TEST.LATENT_POSE_OPTIMISATION True \
    TEST.POSE_SAMPLE_STEP 500

테스트 방법은 굳이 LaLaLoc++ 를 나누지 않는다고 한다. 학습과정은 다르지만, 테스트 방식은 같다는 말인듯.

이미지 테스트 방법은 다음과 같다.

# LaLaLoc image branch
python train.py -c configs/image_branch.yaml -t [path/to/checkpoint] \
    DATASET.PATH [path/to/dataset] \
    SYSTEM.NUM_GPUS 1 \
    TEST.VOGEL_DISC_REFINE True \
    TEST.LATENT_POSE_OPTIMISATION True \
    TEST.POSE_SAMPLE_STEP 500
# LaLaLoc++ image branch
python train.py -c configs/lalaloc_pp/transfomer_image_branch.yaml -t [path/to/checkpoint] \
    DATASET.PATH [path/to/dataset] \
    SYSTEM.NUM_GPUS 1 \

여기까지를 참고로 이제 사용된 코드를 하나하나 뜯어보자.

train.py

앞에 있는 부분을 Dependancy 부분이라고 하는구나. 그 부분은 생략하고.

우선 config 를 불러오는 방법이 좀 특이하다. YACS 방식이라고 하는데, 이건 나중에 차차 공부해가야 할 것 같다. 우선 그렇게 알고 있자.

    config = get_cfg_defaults()
    config.merge_from_file(args.config_file)
    config.merge_from_list(args.opts)
    if args.val:
        config.TEST.VAL_AS_TEST = True
    config.freeze()
    print(config)

이어서 학습하는 것인지 확인한다. command라인에서 받는다.


    if args.checkpoint_file:
        resume_path = args.checkpoint_file
    else:
        resume_path = None

이제 어떤 모델에 대한 학습인지 결정한다. LaLaLoc++인지 아니면 오리지날 버전인지.
그리고 RGB 이미지인지, 아니면 캐드 도면인지 arg로 받은 것을 기반으로 핸들링한다.

    if config.MODEL.TYPE == "lalaloc":
        if config.MODEL.QUERY_TYPE == "image":
            model = ImageFromLayout(config)
        elif config.MODEL.QUERY_TYPE == "layout":
            model = Layout2LayoutDecode(config)
        else:
            raise NotImplementedError(
                "The query type, {}, isn't recognised.".format(config.MODEL.QUERY_TYPE)
            )
    elif config.MODEL.TYPE == "lalaloc++":
        if config.MODEL.QUERY_TYPE == "image":
            model = FloorPlanUnetImage(config)
        elif config.MODEL.QUERY_TYPE == "layout":
            model = FloorPlanUnetLayout(config)
        else:
            raise NotImplementedError(
                "The query type, {}, isn't recognised.".format(config.MODEL.QUERY_TYPE)
            )

학습전 텐서보드도 정리하고.

logger = loggers.TensorBoardLogger(config.OUT_DIR)
checkpoint_callback = ModelCheckpoint(save_top_k=-1,)

여기까지 수행하면, /lalaloc/config/dafult.py 에 설정된 기본 설정에다, 명령어에서 지정된 arg를 덮어써서 실행을 위한 설정이 완료가 된다.

이제 학습을 실질적으로 하는 부분으로 넘어온다.

trainer의 설정을 보자. 이 설정 후, trainer.fit(model)을 실행하면 학습이 진행된다.

trainer = pl.Trainer(
        max_epochs=config.TRAIN.NUM_EPOCHS,
        gpus=config.SYSTEM.NUM_GPUS,
        logger=logger,
        distributed_backend=config.SYSTEM.DISTRIBUTED_BACKEND,
        limit_val_batches=25,
        resume_from_checkpoint=resume_path,
        num_sanity_val_steps=2,
        check_val_every_n_epoch=config.TRAIN.TEST_EVERY,
        callbacks=[checkpoint_callback],
    )

training 관련 arg 들은 평범하다. epoch숫자도 정의하고, GPU개수도 정의하고.

그 외에는 checkpoint있으면 이어하는 코드니 별건 없다..

어라..? 본 코드는 어디있는거지? 하고 찾았더니, dependency가 있는 헤더부분에 있네.

from lalaloc.config import get_cfg_defaults, parse_args
from lalaloc.model import (
    FloorPlanUnetImage,
    FloorPlanUnetLayout,
    ImageFromLayout,
    Layout2LayoutDecode,
)

lalaloc 폴더 안에 config폴더와 model 폴더가 있다. 여기서 각각 해당하는 모듈들을 가져오도록 설계 되어있다.

다음번에는 내부 모듈들을 좀 더 뜯어봐야겠네.

\lalaloc\model\__init__.py

방금전 train.py 의 헤더에 있던 dependency 부분에서, lalaloc.model 에 4개의 모듈의 추가되어있다. FloorPlanUnetImage, FloorPlanUnetLayout, ImageFromLayout, Layout2LayoutDecode 인데, 이 진입부분이 여기에 선언되어있다.

from .lalaloc import ImageFromLayout, Layout2LayoutDecode
from .lalaloc_pp import FloorPlanUnetImage, FloorPlanUnetLayout

이 두줄 코드에서 각각의 모델이 어디에 선언되어있는지 알 수 있다.

lalaloc.py에는 ImageFromLayout, Layout2LayoutDecode 가 있고, lalaloc_pp.py에는 FloorPlanUnetImage, FloorPlanUnetLayout가 있다는 걸 알 수 있다. 논문에서 언급한대로 Image에서 latent로 바뀌는 부분은 변동이 거의 없지만, 2D 캐드도면의 처리는 lalaloc 대비 lalaloc++가 많이 간소화 되었기에, 별도의 파일로 나뉘어져있음을 알 수 있다.

이제 좀더 깊이 들어가보자.

\lalaloc\model\lalaloc.py

헤더 부분에서 눈여겨봐야할건 자체적으로 만든 파일들이다. 이것도 연결해서 봐야 한다는 뜻이다.

from .lalaloc_base import Image2LayoutBase, Layout2LayoutBase
from .modules import LayoutDecoder
from .losses import triplet_loss, bbs_loss
  • from .lalaloc_base 기본 파일이 존재한다. Image2LayoutBase
  • from .modules LayoutDecoder가 여기서 불려진다.
  • from .losses import triplet_loss, bbs_loss loss는 두가지가 정의되었음을 알 수 있다. 논문에 loss관련해서, triplet이나 bbs는 없었던것 같은데? 아마도 lalaloc original에서 사용된 것 같다.
class ImageFromLayout(Image2LayoutBase):

이미지를 Layout에서 검색해 찾을 수 있도록 Latent로 변환해야 한다.
이 클래스는 그 변환하는 모델을 정의한다.

내부에는 두개의 인스턴스가 있는데, 하나는 Layout2Layout 가중치를 불러오는 부분이 있고, 또 하나는 학습을 하는 과정이 있다. 이건 LaLaLoc 오리지널 논문을 봐야 조금 더 이해가 갈 듯한데, 일단 여기서 그러려니 하고 넘어가보자.

    def load_weights_from_l2l(self, ckpt_path):
        if not ckpt_path:
            warnings.warn("No source for the layout branch weights was specified")
            return
        # load weights from Layout2Layout model
        ckpt_dict = torch.load(ckpt_path)
        model_weights = ckpt_dict["state_dict"]

        # load "embedder" weights into "reference_embedder"
        load_dict = {}
        for k, v in model_weights.items():
            modules = k.split(".")
            parent = modules[0]
            if parent == "embedder":
                child = ".".join(modules[1:])
                load_dict[child] = v
        self.reference_embedder.load_state_dict(load_dict)

        # freeze reference_embedder weights
        for p in self.reference_embedder.parameters():
            p.requires_grad = False

이 부분이 Layout2Layout 가중치를 불러오는 부분이다. Image branch를 처음부터 학습하는 것이 아니라, 미리 학습된 “Layout → latent embedding” 네트워크의 가중치를 불러와서 고정(freeze)하고, Image branch가 그 latent space에 맞춰 들어가도록 하는 첫번째단계에 해당한다. 코드를 보면 한줄 한줄 가중치 불러오고, 파라미터를 고정한다. 코드 자체는 단순하다. 이 논문들 특징이 코드 자체는 참 단순하다…

이 부분의 개념은 LaLaLoc 오리지널 논문에서 이 도표를 참조해야 하는 부분이다.

2026-07-07-21-35-34
2026-07-07-21-35-34
    def training_step(self, batch, batch_idx):
        for m in self.reference_embedder.modules():
            if isinstance(m, nn.BatchNorm2d):
                m.eval()

        # compute query and reference embeddings
        query_image = batch["panorama"]
        query_embed = self.forward(q=query_image)

        reference_layouts = batch["pano_layout"].unsqueeze(1)
        reference_embed = self.forward(r=reference_layouts).squeeze(1)

        # perform L2 distance loss
        loss = ((query_embed - reference_embed) ** 2).sum(dim=1).sqrt().mean()

        stats_to_log = {"train/l2_loss": loss.item()}
        return {"loss": loss, "log": stats_to_log}

Training 과정도 뭔가 특별한 부분은 잘 보이지 않는다.
파노라마 이미지를 가져와서 포워드 레이어를 통과하고…… 차원 맞추고…. loss(L2)계산하고… 다만 Loss를 반환하는 구조에서, 파이토치 라이트닝의 특징상 이렇게 반환한다는데 잘 모르겠다..
뭐 보면 볼수록 모르는게 많은지…. 원래 optimizer.zero_grad() loss.backward() optimizer.step() 뭐 이런게 붙어있어야 하는데,
라이트닝을 쓰면 loss만 반환하면 알아서 한다는구만. 신기함.

오늘의 요약

  • Python Lightning
  • 와… 이거 프로젝트 구조 좋다.

댓글남기기