아파트 평면도 이미지 한 장을 올리면 벽·문·창문·방이 자동으로 잡혀 3D 레이아웃으로 서는 기능을 만들었습니다. 사용자가 할 일은 기준 벽 하나를 클릭하고 실제 길이를 입력하는 것뿐입니다. 그런데 완성된 코드에는 딥러닝 모델이 하나도 없습니다.
AICella는 방 치수를 넣고 가구를 2D·3D로 배치해 보는 룸 플래너입니다. 집 다락방에 책상 세 개를 어떻게 넣을지 고민하다 시작한 프로젝트인데, 쓰다 보니 방 크기를 일일이 재서 입력하는 게 가장 번거로웠습니다. 이사 갈 집이라면 손에 있는 건 분양 홍보용 평면도 이미지 한 장이고요.
그래서 "평면도 이미지를 읽어 레이아웃을 만드는 기능"을 이슈로 올렸고, 작업을 시작하면서 클로드코드에게 이렇게 물었습니다. "렌더 이미지도, 이런 이미지도 분석되는 거지?" 당연히 "AI가 알아서 읽어 주겠지"라는 기대가 깔려 있던 질문이었습니다. 이 글은 그 기대가 어떻게 바뀌었는지에 대한 기록입니다.
처음 계획: 평면도 전용 딥러닝 모델
처음 이슈(#68)는 한 줄짜리였고, 이를 분석 결과 모델·벽선·문과 창문·가구·검토 화면으로 쪼개 일곱 개 이슈(#69~#75)로 나눌습니다. 첫 버전은 브라우저에서 픽셀을 대충 추정하는 방식이었는데, 방·문·창문 인식이 부정확해서 분석을 확정하면 공간 연결 검증에서 자주 막혔습니다.
그래서 새로 올린 것이 #83 "CubiCasa5k 기반 평면도 딥러닝 분석 어댑터 구현"입니다. CubiCasa5k는 평면도 5천 장으로 학습한 공개 데이터셋이자 모델입니다. 계획은 이랬습니다.
- Python 추론 래퍼를 만들고 모델 가중치를 서버에 둔다.
- Node 백엔드의 분석 서비스가 추론 프로세스를 호출한다.
- 세그멘테이션 결과를 AICella의 rooms[] 구조와 벽·문·창문·가구 후보로 바꾼다.
PyTorch, OpenCV, EasyOCR, Shapely까지 설치해 두었습니다. 완료 조건에는 "기존 방 크기나 임의 4000mm 기준을 쓰지 않는다"는 항목도 넣었습니다. 실제 치수를 모를 때 기존 방 크기나 임의의 값에 기대지 말고, 도면 자체에서 치수를 얻으라는 뜻이었습니다.
딥러닝을 접고, OpenCV도 접었다
결론부터 말하면 딥러닝 어댑터는 만들지 않았습니다. 모델을 구하는 데 세 번 실패했습니다.
|
시도한 모델 |
결과 |
|---|---|
|
CubiCasa5k 공식 |
README의 체크포인트 링크가 404. 평가 코드는 Python 3.6.5 + PyTorch 1.0.0 + LMDB를 요구하고 단일 이미지 추론 CLI가 아님 |
|
cubicasa-segformer-v2 |
공개 지표 mIoU 0.122. 대상 도면에서 벽 2개, 방 0개 |
|
Yolo_segmentation_cubicasa |
confidence를 0.001까지 낮춰도 검출 0개 |
모델을 구했더라도 문제가 남습니다. 서버에 PyTorch 런타임이 필요한데, AICella는 Tauri로 윈도우·맥용 오프라인 앱을 패키징합니다. 가구 배치 앱 하나에 딥러닝 런타임을 담는 건 말이 안 됩니다.
대안으로 OpenCV Hough 변환을 넣어 둔 코드가 있었는데, 열어 보니 방과 개구부를 빈 배열로 돌려주고 있었습니다. 분석 API는 사실상 항상 실패 경로만 타고 있었던 겁니다. Hough는 선을 극좌표 누적기에 투표시키는 방식이라 스캔 도면에 맞습니다. 굵은 벽 하나가 여러 파편으로 쪼개지고 두께 정보도 사라집니다.
여기서 처음 질문의 "렌더 이미지"가 다시 나옵니다. 분양 홍보용 평면도는 스캔이 아니라 벡터를 그대로 렌더한 이미지입니다. 픽셀이 이미 정확하니 투표로 선을 추정할 이유가 없고, 오히려 해롭습니다. 렌더 이미지라서 어려운 게 아니라, 렌더 이미지라서 오히려 쉬운 문제였던 겁니다.
벽의 틈이 문이고, 남은 빈자리가 방이다
최종 방식은 순수 TypeScript로 짠 결정론적 추출기이고, 브라우저에서 돌아갑니다. 모든 단계가 관찰 하나에서 나옵니다. 벽의 틈이 곧 문·창문이고, 그 틈을 메우고 남은 빈 공간이 곧 방입니다.
- 벽 마스크 — 어두운 픽셀을 5×5 형태학적 열림으로 걸러 두꺼운 구조만 남깁니다. 바닥 나뭇결이나 타일 격자선은 1~2px이라 여기서 사라집니다. 이 단계를 빼면 무늬선이 벽으로 보여 방 하나가 수십 조각으로 쪼개집니다.
- 벽 추출 — 행·열 런렌스를 인접 축으로 병합해 사각형을 만듭니다. 중심선과 두께가 그대로 나옵니다.
- 개구부 — 같은 직선 위 벽들 사이의 틈입니다. 틈이 비었으면 문, 유리선이 채우고 있으면 창문입니다.
- 누수 밀봉 — 벽 끝의 틈을 막을지는 폭이 아니라 "막지 않으면 방이 도면 밖으로 새는가"로 판단합니다. 폭 임계값으로 일괄 처리하면 욕실 같은 방이 둘로 쪼개집니다.
- 방 — 틈을 메우고 flood fill 합니다. 도면 바깥과 이어진 영역은 버립니다.
표제란 테두리처럼 도면 폭을 거의 다 가로지르는 선은 실제 벽이 아니므로 비율 0.9로 갈라냅니다. 같은 아파트 평면도에서 두 방식을 재면 이렇습니다.
|
항목 |
기존(Hough) |
런렌스 병합 |
|---|---|---|
|
벽 |
파편 178개 |
29개, 두께 8~9px 포함 |
|
방 |
0개 |
11개 |
|
개구부 |
0개 |
문 7 · 창문 4 |
|
판독 시간 |
— |
512ms(브라우저) |
판독은 브라우저가 하고 서버는 검증한 뒤 보관만 합니다. Python도 OpenCV도 필요 없으니 Tauri 오프라인 패키징과도 맞습니다.
재어 보고 뒤집은 판단 세 가지
이번 작업에서 가장 좋았던 점은 그럴듯한 가정을 측정으로 세 번 버렸다는 것입니다.
문과 창문은 검정으로는 안 갈린다. 처음에는 틈 안의 어두운 픽셀 비율로 문과 창문을 구분하려 했는데 전혀 갈리지 않았습니다. 유리선과 문의 스윙 호가 검정이 아니라 연한 회색이었기 때문입니다. "흰색이 아닌 픽셀"로 잰 잉크 마스크로 바꾸자 문은 0.31~0.45, 창문은 0.70~0.77로 깔끔하게 갈렸습니다.
발코니 창호는 문이다. 유리선 기준으로는 창문이지만 사람이 걸어서 지나가는 곳입니다. AICella의 레이아웃 검증은 공간 연결을 문으로만 인정하므로, 이대로라면 발코니가 고립돼 ROOM_DISCONNECTED 오류로 저장이 막혔습니다. 양쪽이 모두 방인 개구부는 통로로 보도록 고치고 나서야 검증을 통과했습니다.
문 폭으로 축척을 검산할 수 없다. 문은 보통 800~900mm니까 사용자가 넣은 축척을 문 폭으로 역산해 경고하려 했습니다. 그런데 이 도면은 문 틈이 50px인데 침실 폭이 321px이라, 어떤 축척을 넣어도 문이 실제보다 좁게 나옵니다. 스윙 호를 확대해 보니 분양 홍보용 평면도는 문을 축척보다 좁게 그리고 있었습니다. 올바른 축척에서도 경고가 뜨니 이 검산은 철회했고, 연면적이 10~1,000㎡를 벗어날 때만, 즉 자릿수를 틀렸을 때만 알리도록 바꿨습니다.
세 번째가 가장 기억에 남습니다. 도면은 사실을 그린 그림이라고 믿었는데, 홍보용 도면은 보기 좋게 그린 그림이었습니다. 모델에 맡겼다면 이런 사실은 영영 모르고 지나갔을 겁니다.
사람의 몫은 클릭 한 번, 그리고 그 한 번을 위한 화면
판독은 픽셀까지만 합니다. mm로 바꾸려면 기준 길이가 하나 필요하고, 그건 도면만 봐서는 알 수 없습니다. 검토 화면은 판독 결과를 도면 위에 겹쳐 보여 주고, 사용자는 판독된 벽 하나를 클릭해 실제 길이를 입력합니다.
기준선을 직접 드래그하게 하지 않은 데는 이유가 있습니다. 드래그는 픽셀 단위 손떨림이 그대로 축척 오차가 됩니다. 끝점이 이미 정확한 벽을 고르게 하면 그 오차가 없습니다. 실측에서는 기준 벽에 10,700mm를 넣자 침실이 2,995×3,275mm, 3,862×3,284mm로 잡혔고 경고는 뜨지 않았습니다. 적용하면 공간 7개, 개구부 11개가 평면과 3D로 섭니다.
문제는 이 검토 화면을 어디에 둘지였습니다. 당시 편집 화면은 왼쪽 패널에 섹션 9개가 쌓여 1440×900에서도 2.4화면을 스크롤해야 했습니다. 가구를 클릭하면 속성 패널이 뜨는데 문을 클릭하면 아무 일도 없었습니다. 그래서 도면 분석과 같은 시기에 편집 화면을 3단으로 재편했습니다.
|
영역 |
역할 |
|---|---|
|
왼쪽 280px |
무엇을 넣을 수 있나 — 가구·문·창문 프리셋 버튼, 평면도 분석 |
|
캔버스 |
직접 조작만 — 회전 핸들, 치수선 |
|
오른쪽 300px |
지금 선택된 것의 속성. 선택이 없으면 현재 공간 속성 |
모바일(720px 이하)에서는 두 패널이 바텀 시트가 됩니다. 64px · 화면 45% · 90% 세 단계로 스냅하고, 뭔가를 선택하면 속성 탭으로 바뀌며 중간 단계까지 올라옵니다. 375×812에서 패널 스크롤이 8.4화면이던 것을 없앴고, 캔버스 높이는 647px에서 731px로 늘었습니다. 폰에서 도면 이미지를 올리고 벽 하나를 누르는 흐름이 여기서 비로소 가능해졌습니다.
그래서, 렌더 이미지는 되고 이런 이미지는?
처음 질문에 이제는 정확하게 답할 수 있습니다. 분양 홍보용처럼 벡터로 렌더된 평면도는 됩니다. 스캔본이나 휴대폰으로 찍은 사진은 아직 안 됩니다. 기울기 보정과 잡음 제거가 먼저 필요합니다. "AI가 다 읽어 주겠지"에서 "어떤 이미지를 왜 읽을 수 있는지 안다"로 바뀐 것이 이번 작업의 가장 큰 수확입니다.
남은 일은 문서(docs/ISSUE-83-FLOORPLAN-ANALYSIS.md)에 적어 두었습니다.
- 회귀셋 확장 — 실제 도면이 1장뿐입니다. 임계값(어두움 90, 열림 5×5, 창문 0.55)이 다른 도면에서 버틸지 모릅니다. 가장 큰 위험이고, 최소 5장이 필요합니다.
- 후보 수정 — 검토 화면에서 잘못 잡힌 벽이나 문을 고칠 수 없고, 전부 적용하거나 전부 버려야 합니다.
- 방 이름 — 도면 글자를 읽지 않습니다. 거실·주방·현관은 벽으로 나뉘지 않아 한 공간으로 잡힙니다.
- 가구 추출 — 범위에서 뺐지만 API 계약(kind: 'furniture')은 열어 두었습니다.
하나 더 적어 두면, 이번에 함께 잡은 버그가 있습니다. 분석 확정 API가 500을 내고 있었는데, HTTP 클라이언트가 본문 없는 POST에도 Content-Type: application/json을 붙이고 Fastify 기본 파서가 빈 본문을 처리하지 못했기 때문입니다. 라우트 테스트는 헤더를 붙이지 않아 통과하고 있었고요. 테스트가 초록불이어도 실제 브라우저에서 업로드부터 확정까지 한 번 돌려 봐야 하는 이유입니다.
제목은 "AI를 뺀 이야기"지만 정확히 말하면 모델을 뺀 이야기입니다. 모델 세 개를 시험하고, Hough를 걷어내고, 런렌스 추출기를 짜고, 183개 테스트를 통과시킨 것은 모두 클로드코드였습니다. 제가 한 일은 질문을 던지고, 측정 결과를 보고, "이건 버리자"고 판단한 것입니다. AI를 무엇에 쓸지 정하는 것까지가 아직은 사람의 몫인 듯합니다.
댓글 0개