공부/인공지능

얼굴 인식으로 '내가 방에 있는지' 판정하다 포기했습니다 — CCTV 재실 판정은 공간 마커 규칙으로

TechToast 2026. 10. 15. 10:20
반응형

한 줄 요약부터. 연구실 문패를 '재실/부재/강의/퇴근'으로 자동 표시하려면, 결국 한 가지를 알아야 합니다. 지금 방 안에 있는 그 사람이 '나'인지가요. 사람 감지 이벤트만으론 학생이 들어와도 '재실'이라 떠버리니까요. 그래서 라즈베리파이 + CCTV 이벤트를 얼굴 인식으로 판정해봤는데, 같은 사람 임베딩끼리도 거리가 0.86에서 1.5까지 흔들리는 걸 실측으로 확인한 뒤 1:1 동일성 판정은 포기했습니다. 대신 좌표 존(공간 마커) + 요일 패턴 규칙으로 갈아탔습니다. 평일 근무시간에는 97% 이상 '교수'로 판정되는 걸 보고, 얼굴 인식은 '화질이 좋을 때만 쓰는 보조'로 내린 이야기입니다.

 

무엇을 만들고 있었나

연구실 문패는 e-paper 화면에 '내 방/방문자/공사(주말)' 같은 상태를 띄우는 자동화입니다. 그런데 이걸 제대로 돌리려면 '지금 방 안에 사람이 있는지'를 넘어서 '그 사람이 나(교수)인지'까지 알아야 합니다. 사람 감지 이벤트만 있으면 학생이 들어와도 문패가 '재실'이라고 떠버리니까, 판정 기준이 무너집니다.

 

구성은 이렇습니다. 라즈베리파이에 Frigate를 올려 CCTV 영상을 돌리면, 사람 감지 이벤트가 bbox 좌표(640×368 기준)와 함께 MQTT/API로 내려옵니다. 이 이벤트 ID를 받아서 얼굴 분류 서비스가 원본 해상도 프레임(2560×1440)에서 얼굴을 찾고 임베딩을 구해 '나 vs 타인'을 가리는 구조였어요. 판정 파이프라인은 재실 게이트(presence-gate), 타임라인 대시보드(dashboard), 자동 상태 데몬(autostatus), 재실/퇴근 기록 레코더(room-presence-record)로 나뉩니다.

 

여기서 원본 해상도가 왜 필요했는지가 재밌는 대목입니다. 축소 스냅샷(640×368)에서는 얼굴이 너무 작아서 인식이 안 됩니다. 그런데 Frigate 클립의 원본은 워크스테이션의 오프로드 디렉터리에만 있고, 기존 WSL 얼굴 서비스는 같은 머신 내부에서만 바인딩돼서 다른 기기에서는 접근 자체가 안 됐습니다. 그래서 워크스테이션 로컬에 얼굴 분류(face_classify)와 고품질 임베딩 누적기(face_harvest)를 새로 구축했습니다. 연구실 자동화에서 가장 자주 발목을 잡는, 서비스 바인딩 끝판왕 삽질이었습니다.

1:1 얼굴 동일성 판정, 화질 때문에 무너졌습니다

처음 전략은 절대 임계값 하나로 1:1 동일성을 판정하는 것이었습니다. 그런데 카메라가 원거리·측면·야간 IR 위주로 돌아가다 보니 얼굴이 작고, 같은 사람(제 얼굴)끼리의 임베딩 거리 분포가 너무 넓었습니다. 실측 수치가 결정적이었어요. 같은 사람 임베딩 간 거리가 p90=1.42, 범위로는 0.86에서 1.5까지 벌어졌습니다. 단일 절대 임계값, 그러니까 0.45든 0.6이든 하나로는 판정이 불가능합니다. 같은 사람끼리도 1.5까지 벌어지는데, 그 사이 어딘가에 선을 긋고 '이 위는 남이고, 이 아래는 나다'라고 자르면 무엇을 넣든 오탐이 나오는 셈이에요. 여관방에 단체 손님이 들어왔는데 '몇 명쯤 들어와야 우리 손님'이라는 기준을 못 세우는 것과 같습니다.

 

혹시 몰라서 세워둔 '영상에 인물이 보이면 무조건 교수'라는 폴백도 실패했습니다. 영상에 보이는 인물 수, 그러니까 실제 1명인지 2명인지조차 맞히지 못했거든요. 넓은 거리 분포 앞에서는 '검출은 되는 게 처참하다'는 정도가 아니었습니다.

 

그래서 전략을 돌렸습니다. 절대 임계값 대신 상대 비교, 즉 '교수 프로필 vs 타인(방문자) 프로필 중 어느 쪽에 더 가까운가'로 판정하는 방식입니다. 근접·정면일 때는 insightface가 '나(교수)'를 거리 0.859로 정확히 잡아냅니다(판정 3.4초). 화질이 받쳐주는 순간에는 얼굴 인식이 꽤 쓸 만합니다.

 

하지만 상대 비교도 결국 화질이 받쳐줘야 합니다. 그래서 가장 안정적인 신호인 좌표 존(공간 마커) 규칙을 주축으로 옮겼습니다. Frigate 이벤트 bbox를 좌표로 매핑하는 규칙입니다.

 

평일 + 안쪽(책상, cx<400, cy>120)   → 교수(재실)
평일 + 문쪽(우·상, cx>400, cy<180)  → 방문자
주말                                  → 공사

 

실측 검증에서 평일 근무시간에 방 안쪽(책상 주변)에 사람이 있으면 97% 이상 '교수'로 판정됐습니다. 문쪽 영역은 방문자 신호와 정확히 일치했고요. 얼굴 분류 서비스가 이 규칙으로 who(me/visitor/other/unknown) 필드를 계산해 대시보드에 '내 방/방문자/공사(주말)' 라벨을 붙입니다. 사람으로 따지면 "얼굴이 안 보이니, 어느 구역에 있는지로 판단하자"로 방향을 튼 겁니다.

 

그래도 얼굴 인식을 어엿한 보조로 살려두려면 고품질 임베딩이 필요합니다. 현재 professor.npy에는 23개 임베딩만 누적돼 있고, 그것도 원거리·측면 위주라 분산이 큽니다. visitor.npy는 0개입니다. 주말 공사직이나 방문자가 원거리였던 탓에 얼굴 검출 자체가 실패한 거예요. 상대 비교를 하려면 타인 프로필도 있어야 하는데, 지금은 프로필이 비어 있다는 뜻입니다. 개선은 의외로 단순합니다. 카메라 앞에 근접 정면으로 30초에서 1분쯤 서 있으면 face_harvest가 고품질 임베딩을 자동으로 누적해줍니다. 같은 사람 임베딩끼리의 분산(in-class variance)이 줄어들면 상대 비교의 신뢰도도 따라 올라갑니다. 얼굴 인식을 되살리는 열쇠는 모델이 아니라 '좋은 사진을 더 모으는 일'이었어요.

대시보드가 '업데이트 안 된다'던 진짜 원인

데이터는 최신인데 화면만 안 바뀌는 문제를 한참 겪었습니다. 서버가 죽었나 하고 뒤지게 만드는 문제였는데, 원인이 두 개였어요. 하나는 페이지가 load()를 딱 한 번 호출하고 멈춘다는 것이고, 다른 하나는 HTTP 응답 헤더에 Cache-Control: max-age=3600이 박혀 있어서 브라우저가 1시간 동안 구버전 페이지를 캐시한다는 점이었습니다. 데이터는 매번 최신인데 브라우저만 옛날 화면을 들고 있는 셈이라, 문제를 보기도 전에 방향을 잘못 잡게 됩니다.

 

해결은 순서대로 붙였습니다. setInterval 기반 15초 자동 폴링, 데이터 시그니처를 비교해 불필요한 리렌더 방지, 현재 선택된 에피소드(SEL) 상태 유지, 그리고 정적 파일과 메인 페이지 헤더를 Cache-Control: no-cache로 바꿨습니다.

 

여기서 얻은 교훈은 하나입니다. kiosk처럼 한참 열어두는 대시보드는 no-cache나 짧은 max-age가 필수입니다. 아무리 데이터를 잘 만들었어도 브라우저 캐시 하나로 전부 무효가 되는 셈이에요.

재실/퇴근을 기록하는 상태 머신

얼굴/존 판정을 화면 표시로만 쓰면 끝이 아니에요. 출퇴근 기록까지 만들었습니다. 퇴근 상태에서 '나'가 감지되면 재실로 기록하고, 재실 상태에서 90분간 '나'가 감지되지 않으면 퇴근으로 기록합니다. 재실과 퇴근 사이 전환에는 10분 쿨다운을 걸어 오탐으로 인한 재발을 막았습니다. e-paper는 상태가 바뀔 때만 화면을 갱신해야 마모가 적기 때문에, 상태가 변할 때만 표시 API를 호출하고 웹에서 수동으로 바꾼 경우에는 30분간 자동화가 양보하도록 했습니다. 기록은 records.jsonl과 state.json으로 남기고, 최근 3시간(15건)을 시드로 상태를 복원합니다. Frigate 이벤트 폴링은 45초 간격으로 돌고 있습니다.

정직한 한계와 다음 계획

가장 큰 한계는, CCTV가 움직임이 없으면 이벤트 자체가 생기지 않는다는 점입니다. 책상에 앉아 거의 안 움직이면 사람이 있어도 이벤트가 없어서 '퇴근'으로 오판할 수 있어요. 그래서 임계값을 90분으로 높게 잡고, 며칠 데이터를 수집한 뒤 LEAVE_THRESH_MIN과 ARRIVE_GAP_MIN을 튜닝할 계획입니다.

 

1:1 동일성 판정은 화질 한계 때문에 물리적으로 포기했다고 보는 게 정확합니다(거리 분포 0.86~1.5). 근접 정면 샘플을 더 누적하면 상대 비교의 신뢰도는 오르겠지만, 주축은 역시 좌표 존 규칙으로 남습니다. visitor 프로필도 아직 없어서, 근접 방문자가 생겨야 visitor.npy가 채워질 겁니다. 그 외에도 서버 API가 전면 재작성되며 데몬들을 재정렬한 이력, 컨테이너 기동을 사람 이벤트로 오탐하는 문제 같은 실무 삽질도 꽤 있었네요.

 

그래서 지금은 이렇게 요약할 수 있습니다. 공간 마커 규칙이 주축을 잡고, 얼굴 인식은 화질이 좋을 때만 보조로 세워둔 구조가 완성됐습니다. 어려운 판정일수록 화려한 모델보다, '방 안 어디에 서 있는지'라는 평범한 좌표가 곧바로 답이 되어주네요.

반응형