


저는 개인 공간을 중요하게 생각합니다. 대부분의 사람은 요청하지 않아도 이를 존중하지만, 일을 하려고 할 때 늘 누군가는 조금 지나치게 가까이 다가옵니다.
그래서 Project One에서는 이 문제를 해결할 무언가를 만들었습니다. Personal Space Enforcer는 전면에 카메라가 달린 소형 보초 터렛입니다. 방을 감시하며 보이는 얼굴을 인식하고, 여러분의 얼굴을 인식하면 터렛이 회전해 여러분이 움직일 때까지 가슴을 향해 레이저 포인터를 비춥니다.
흥미로운 점은 누구를 대상으로 삼는가입니다. 실제로 이름을 알아낼 수 있는 사람에게만 발사합니다. 모델은 고정된 얼굴 그룹으로 학습되므로, 등록되어 있지 않으면 대조할 대상이 없어 그냥 내버려 둡니다. 셔츠에 빨간 점이 찍혔다면 여러분은 목록에 있는 것입니다.
이 Instructable에서는 전체 과정을 다룹니다. 직접 얼굴 데이터셋을 수집하고 라벨링하는 방법, 노트북에서 YOLO26n 모델을 학습하는 방법, Raspberry Pi로 옮기는 방법, 해당 모델의 출력으로 두 개의 서보와 레이저를 구동하는 방법, 모든 정보를 데이터베이스에 기록하는 방법, 그리고 코드를 건드리지 않고 얼굴을 등록 및 삭제할 수 있도록 Gradio 인터페이스로 감싸는 방법을 설명합니다.
저는 Howest의 1학년 Creative Technologies & AI 학생이며, 이 프로젝트는 처음부터 끝까지 완성한 첫 프로젝트였습니다. 순조롭게 진행된 부분만이 아니라, 해결 방법을 알아내는 데 가장 오래 걸린 부분들을 기록하려고 했습니다.
무엇보다 먼저 레이저에 대해 한마디 하겠습니다. 이 장치는 저출력 레이저 포인터 모듈을 사용하며, 정말 중요한 한 가지는 절대 누구의 눈도 향하지 않도록 하는 것입니다. 저는 감지된 얼굴보다 아래쪽을 조준하고, 틸트 서보에 물리적 스토퍼를 설치해 소프트웨어가 원하더라도 설정된 각도 이상으로 빔을 올릴 수 없게 했습니다. 이 장치를 만든다면 반드시 스토퍼를 설치하세요. 그것이 장난감과 사람을 다치게 할 수 있는 물건의 차이입니다.
켜기/끄기 스위치는 없습니다. 터렛은 전원이 공급되면 시작하고 플러그를 뽑으면 멈춥니다 — 5단계 끝부분의 참고 사항을 확인하세요.
이 장치를 처음부터 제작하는 데 드는 비용은 대략 [FILL: total]입니다. 부품 번호, 공급업체 및 가격이 포함된 전체 BOM은 여기에서 확인할 수 있습니다: [FILL: BOM의 정확한 링크 — 프레젠테이션에는 "내 GitHub에 있다"가 아니라 파일로 바로 연결되는 링크가 필요합니다. 예: https://github.com/…/blob/main/docs/BOM.xlsx]

제작 단계를 설명하기 전에, 전체 시스템이 실행되는 루프를 살펴보겠습니다.
LED는 현재 상태를 한눈에 보여 줍니다. 탐색 중에는 녹색, 고정 조준 중에는 빨간색으로 표시됩니다. LCD는 오류 표시용이므로, 터렛이 헤드리스로 실행되는 동안 문제가 발생해도 모니터를 연결하지 않고 원인을 확인할 수 있습니다.

이것은 분류 모델이 아니라 객체 탐지 모델이므로, 각 이미지에는 단순히 폴더 이름만 있는 것이 아니라 얼굴 주위에 라벨이 지정된 바운딩 박스가 있어야 합니다.
7명의 사람을 촬영했으며, 각 사람당 수백 장의 이미지를 확보했습니다. 사진은 다양한 조건에서 촬영했습니다. 서로 다른 환경, 조명, 거리, 옷, 주변 사람 수, 각도 등을 포함했습니다. 다양성이 클수록 좋습니다.
그런 다음 Roboflow를 사용해 이미지에 라벨을 지정하고, 이미지 분할도 수행한 뒤 데이터셋을 내보냈습니다.
다르게 한다면: 얼굴이 아닌 머리카락, 안경 또는 기타 바뀔 수 있는 요소가 라벨에 포함되지 않도록 하는 데 더 집중했을 것입니다. 사진 종류를 충분히 다양하게 구성한다면, 이것으로 충분할 것입니다.
데이터셋에 포함된 모든 사람은 촬영과 자신의 얼굴을 모델에 사용하는 데 동의했습니다. 이 프로젝트를 재현한다면 먼저 허락을 받으세요. 사람을 향해 레이저를 조준하는 기계를 학습시키는 일이기 때문입니다.

YOLO26n은 Pi가 아니라 노트북의 GPU에서 학습시켰습니다. Pi에서도 모델을 실행할 수는 있지만, 학습하기에는 너무 느립니다.
결과는 만족스러웠지만, 학습에 사용한 대상의 다양성이 충분하지 않았다는 점은 언급할 필요가 있습니다. 모두 대략 비슷한 연령대의 백인 남성이었습니다.
학습된 가중치는 여기에서 확인할 수 있습니다: https://github.com/howest-mct/2025-26-projectone-ctai-RaduNicolae2006/blob/main/RPi/ai/best.pt
Pi로 이동하기 VS Code Remote SSH를 사용해 가중치 파일을 복사했습니다. Pi에 연결한 다음 파일을 프로젝트 폴더로 드래그하면 됩니다.
모든 장치는 두 개의 버스와 네 개의 GPIO 라인에 연결됩니다.
장치:
PCA9685 서보 드라이버
LCD (PCF8574 백팩)
주소:
0x40
0x27
서보:
Pan (좌우)
Tilt (상하)
PCA9685 채널:
0
4
두 서보 모두 50 Hz에서 작동하며, 180° 범위에 걸쳐 500–2500 µs 펄스 범위를 사용합니다.
취미용 서보를 Pi 자체 핀이 아니라 전용 PWM 보드에서 구동하는 것이 좋습니다. PCA9685는 16개 채널을 모두 하드웨어에서 생성하므로 서보 타이밍이 Python과 CPU 시간을 두고 경쟁하지 않습니다. 따라서 추론 스레드가 바빠져도 서보가 떨리지 않습니다.
신호:
레이저 신호
빨간색 LED
녹색 LED
파란색 LED
핀:
23
17
27
22
LED는 공통 애노드 방식입니다. 공통 핀은 3.3 V에 연결하고 각 색상 핀을 Low로 당기면 켜지므로, 소프트웨어에서 PWM 듀티 사이클을 반전해야 합니다. 세 하위 LED의 순방향 전압과 효율이 서로 다르기 때문에 각 채널에는 보정 배수를 적용합니다. 빨간색은 0.4, 녹색은 0.7, 파란색은 1.0입니다. 이 보정이 없으면 흰색이 분홍색으로 표시됩니다.
레이저 모듈에는 자체 전류 제한 기능이 있으므로 GPIO 라인은 모듈의 Signal 핀만 구동합니다.
서보에 전원 공급하기 레이저는 Pi 자체의 5V 핀으로 전원을 공급하지만, 서보는 브라운아웃을 방지하기 위해 별도의 전원 공급 장치를 사용합니다. 모든 전자 장치는 공통 접지를 공유합니다.
배선에서의 레이저 안전 핀은 출력으로 설정되며 Laser 객체가 생성되는 즉시 LOW로 구동되므로, 다른 어떤 항목이 초기화되기 전에도 빔이 꺼져 있습니다. 또한 close()는 종료 시 레이저가 꺼진 상태가 되도록 보장합니다. 밝기 제어를 명시적으로 사용하는 경우가 아니면 해당 핀에서는 PWM이 실행되지 않습니다. 듀티 사이클 0%의 PWM 스레드는 핀을 LOW로 유지하며 정적 HIGH 신호와 충돌하므로 레이저가 절대 켜지지 않습니다.

하우징은 레이저 절단한 합판으로, Howest Makerlab에서 절단했으며, 감시 터렛처럼 보이도록 제작하고 스프레이 페인트로 도색했습니다.
팬 및 틸트 메커니즘은 의도적으로 단순하게 구성했습니다. 첫 번째 서보는 양면테이프로 인클로저에 고정했습니다. 디스크를 서보 혼에 순간접착제로 붙이고, 두 번째 서보를 그 디스크 위에 올려 다른 작은 부품 몇 개와 함께 접착했습니다. 카메라와 레이저는 두 번째 서보에 장착됩니다. 우아한 구조는 아니지만 터렛이 회전할 때 카메라가 흔들리지 않을 만큼 견고하며, 추적에서 실제로 중요한 것은 이것뿐입니다.
배선 대부분은 인클로저 내부에 남아 있습니다. 서보, 카메라, 레이저에 도달해야 하는 배선만 밖으로 나오며, 각각 전용 구멍을 따로 뚫었기 때문에 전선이 책상 전체로 퍼지지 않고 정해진 위치에 그대로 있습니다.
절단 파일은 Adobe Illustrator에서 제작했습니다.
수업용으로 제공된 준비된 Raspberry Pi OS 이미지에서 시작했으므로 이를 재배포할 수는 없습니다. 깨끗한 Bookworm 설치에서는 I2C 활성화, 가상 환경, 그리고 RPi/requirements.txt의 패키지가 필요합니다:
bash
앱을 실행하는 사용자는 i2c 및 gpio 그룹에 속해야 합니다. /dev/i2c-* 및 /dev/gpiochip* 아래의 디바이스 노드는 root:<group> 소유의 mode 660으로 설정되어 있으므로, 그룹 멤버십이 하드웨어 접근 권한을 부여합니다. 대신 root로 실행하는 것은 좋지 않습니다. 해당 노드의 소유권이나 상태를 바꿔 이후 non-root 경로가 작동하지 않을 수 있습니다.
systemd 유닛 personal-space-enforcer.service:
ini
bash
이 유닛에는 겉보기보다 중요한 세부 사항이 세 가지 있습니다.
Wants/After=network-online.target은 WiFi가 연결되기 전에 앱이 실행되는 것을 막습니다. 그렇지 않으면 앱이 아직 할당되지 않은 주소를 광고하게 됩니다.
위에서 설명한 그룹 권한 문제 때문에 root가 아닌 User=student를 사용합니다.
TimeoutStopSec=20은 종료 핸들러가 실행될 시간을 충분히 제공합니다. systemd가 SIGTERM을 보내면 앱은 이를 처리하고 레이저를 끈 다음, 종료하기 전에 두 서보를 모두 90°로 되돌립니다. 이는 Ctrl+C와 동일한 정리 경로입니다. 더 긴 타임아웃이 없으면 systemd가 정리 도중 프로세스에 SIGKILL을 보내 빔이 켜진 상태로 남을 수 있습니다.
앱은 0.0.0.0:7860에 바인딩됩니다. http://<pi-ip>:7860을 열거나, 컴퓨터가 지원한다면 Pi의 mDNS 이름을 사용하세요.
IP를 찾아다닐 필요는 없습니다. 누군가 연결할 때까지 LCD에 접속할 주소가 표시되며, 뷰어가 연결되면 실시간 상태 화면으로 전환됩니다.
물리적인 전원 버튼은 없습니다. 종료는 웹 인터페이스에서 수행합니다: Debugging → System → Arm power-off → Shut Down & Power Off. 이 작업은 레이저를 끄고 서보를 90° 위치로 되돌린 다음 Pi를 정지하므로, 빔이 켜져 있거나 서보가 중심에서 벗어난 상태, 또는 SD 카드에 쓰기 작업이 진행 중인 상태로 두 전원 공급 장치를 뽑는 일을 방지할 수 있습니다. 실수로 클릭하는 것을 막는 보호 장치도 두 가지 있습니다. 암 설정 체크박스가 선택되지 않으면 핸들러가 클릭을 무시하고, 브라우저가 먼저 확인을 요청합니다.
앱이 non-root 사용자로 실행되므로 전원을 끄려면 범위가 제한된 sudo 규칙 하나가 필요합니다:
bash
이 규칙은 /usr/sbin/poweroff 명령 하나만 정확히 허용하며, 그 외에는 아무것도 허용하지 않습니다.
YOLO는 모든 프레임이 아니라 스트리밍되는 프레임 5개마다 한 번씩 실행됩니다. 감지 사이에는 optical-flow tracker가 기존 박스를 이동시키므로, Pi가 초당 30회 추론을 실행하지 않아도 오버레이가 끊김 없이 따라갑니다.
두 가지 설정으로 HUD 깜박임을 방지합니다. 무언가를 찾은 스캔 후 다음 1초 동안 빈 스캔이 발생해도 타깃을 지우지 않습니다. 스캔이 다시 성공하거나 시간 창이 만료될 때까지 optical flow가 박스를 이어서 추적합니다. 또한 YOLO의 non-max suppression은 클래스 내에서만 중복을 제거하므로, 하나의 얼굴이 동시에 서로 다른 두 사람으로 검출될 수 있습니다. 로깅에서는 IoU가 0.5를 초과하는 박스를 같은 얼굴로 처리하고, 신뢰도가 가장 높은 식별 결과만 유지합니다.
YOLO가 순간적으로 실행될 때 모든 코어를 점유하지 않도록 추론 스레드 수를 제한합니다. 브라우저용 비디오는 모든 프레임에서 소프트웨어로 인코딩되며, 감지 작업이 해당 인코더의 처리를 방해하면 스트림이 끊깁니다. 코어 하나를 비워 두면 이 문제가 해결됩니다.
프레임마다 한 번씩 조정하는 순수 비례 제어입니다:
두 축 모두 Kp = 0.04를 사용합니다. 타깃 지점은 수평으로 중앙에 있지만 프레임의 중간이 아니라 아래쪽으로 3분의 1 지점에 있습니다. 이렇게 해야 고정된 레이저 오프셋이 얼굴이 아닌 몸통을 향합니다.
실제 하드웨어에서 사용할 수 있도록 두 가지 보호 장치를 적용했습니다. 25픽셀 데드존을 두어 이미 충분히 가까운 얼굴은 중앙에 있는 것으로 간주하므로 서보가 윙윙거리며 움직이지 않습니다. 또한 각 보정을 프레임당 3°로 제한하여 얼굴이 프레임을 가로질러 갑자기 이동하거나 잘못된 검출이 발생해도 터렛이 갑자기 튀거나 플랫폼 전체가 흔들리지 않고 여러 프레임에 걸쳐 부드럽게 이동합니다.
여러 얼굴이 화면에 있으면 가장 큰 박스를 선택합니다. 가장 큰 얼굴이 가장 가까운 사람이라는 가정에 따른 것입니다.
코드만으로는 알아낼 수 없는 한 가지가 있습니다: 양의 오차에 양의 각도 변화가 필요한지 음의 각도 변화가 필요한지는 서보가 어떻게 장착되었는지에 전적으로 달려 있습니다. 이 빌드에서는 두 축이 모두 반전되어 있습니다. 한 번에 한 축씩 작은 단계 크기로 테스트하세요. 얼굴을 프레임 오른쪽에 두었을 때 터렛이 자신을 향하지 않고 멀어지는 방향으로 팬하면 해당 축을 반전하세요.
다음 조건을 모두 만족할 때만 레이저가 켜지며, 어느 하나라도 충족되지 않는 순간 꺼집니다:
Lock에는 히스테리시스가 적용됩니다. 25 px 안에서는 활성화되지만 오차가 40 px를 초과할 때까지 해제되지 않으므로, 데드존 경계에서 서성이는 얼굴에도 빔이 깜빡이지 않고 안정적으로 유지됩니다.
Settle은 눈 안전을 위한 보호 장치이며, 매 프레임마다 확인됩니다. 서보가 움직이는 동안에는 빔이 차단되고, 서보가 3개 프레임 동안 정지한 뒤에야 다시 켜집니다. 이 때문에 빔이 표적을 향해 이동하는 동안 사람 머리 높이에서 방 안을 가로질러 레이저를 쓸어 버리는 일이 방지됩니다.
Operation 페이지에는 데모용 Force Laser 오버라이드도 있습니다. Force Off가 항상 최우선입니다. Force On은 게이트를 완전히 우회하므로, 이름 그대로 매우 위험합니다. 따라서 teardown에서는 오버라이드를 다시 off로 고정하여, 종료 과정에서 worker thread가 경쟁 상태를 일으켜도 빔을 다시 활성화할 수 없게 합니다.

Docker에서 실행되는 Postgres 16과 두 개의 테이블입니다.
sql
사람이 계속 추적되는 동안에는 timestamp_lost가 null로 유지됩니다.
로그가 한 프레임짜리 행으로 넘쳐나지 않고 읽기 쉽게 유지되도록 두 가지 규칙을 적용합니다. 얼굴이 3초 동안 시야에 계속 있어야 appearance가 실제로 기록되므로, 지나가는 사람 때문에 행이 생성되지 않습니다. 또한 한 번 기록된 표적은 2초 동안 사라져도 해당 행을 닫지 않습니다. 이 유예 기간이 YOLO 프레임 하나가 누락되는 상황을 흡수하므로, 한 번의 appearance가 다섯 개로 분할되지 않습니다.
앱은 데이터베이스 자격 증명을 절대 보유하지 않습니다. HTTP를 통해 API의 /db-health 엔드포인트를 호출하여 상태를 확인하며, Postgres와 통신하는 유일한 구성 요소는 API입니다.
단일 ASGI 앱에서 포트 7860으로 제공되는 네 개의 페이지이며, Gradio UI와 WebRTC 시그널링을 모두 처리합니다.
HUD는 구현 방식 때문에 언급할 가치가 있습니다. 영상 프레임에 직접 그리지 않습니다. 초당 10회 작은 JSON payload를 브라우저로 전송하고 오버레이는 클라이언트 측에서 렌더링하므로, Pi가 모든 프레임을 인코딩하기 전에 그래픽을 합성할 필요가 없습니다.
영상은 WebRTC를 통해 LAN에서 피어 투 피어 방식으로 브라우저에 전달됩니다. 같은 네트워크에서는 STUN 또는 TURN 서버가 필요하지 않습니다.
데이터베이스 스택은 RPi/api에 있으며, Postgres, pgAdmin 및 FastAPI 서비스로 구성됩니다.
bash
Postgres는 데이터를 named volume에 저장하는 postgres:16-alpine으로 실행되며, db/init는 /docker-entrypoint-initdb.d에 마운트됩니다. 해당 디렉터리의 모든 .sql 파일은 volume이 처음 생성될 때 한 번 실행되므로, 새로 시작하면 스키마가 자동으로 생성됩니다. 이후 스키마를 수정해도 docker compose down -v를 실행하기 전까지는 아무 일도 일어나지 않습니다. 이 명령은 volume을 삭제하여 스키마를 다시 실행하게 합니다. 누구나 한 번쯤 이 부분에서 실수합니다.
API는 단순한 depends_on이 아니라 Postgres healthcheck가 통과되기를 기다리므로, 아직 시작 중인 데이터베이스에 쿼리를 보내기 시작하지 않습니다.
pgAdmin은 포트 5050, API는 8000, Postgres는 5432를 사용합니다.
원문: https://www.instructables.com/Facial-Recognition-Laser-Turret/