과제 목적
•
작품주제: 전신마비 환자들을 위한 얼굴조종 자동차
전신마비 환자나 신체적 장애로 인해 손이나 팔을 자유롭게 사용할 수 없는 사람들도 이동 보조기기나 휠체어등을 스스로 제어할 수 있도록 돕는 기술을 개발하는 것이다. 이를 위해 웹캠 기반의 머리 움직임 인식 기술과 마이크로비트, 블루투스 통신을 활용하여 사용자가 고개를 움직이는 것만으로 전진, 후진, 좌우 이동 등의 조작을 수행할 수 있는 시스템을 구현하였다. 사용자의 신체 조건에 맞춘 조작 방식 연구를 목표로 하였다.
최종 프로젝트 영상
프로젝트 코드
•
얼굴 앱 웹사이트:
•
블록코드:
•
코드펜 링크:
CodePen얼굴조종 컨트롤러
•
코드펜 코드:
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Face Tracking + Micro:bit Controller</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(135deg,#dfe9f3,#ffffff);
min-height: 100vh;
padding: 2rem;
color: #222;
}
h1 { font-size: 1.8rem; font-weight: 700; margin-bottom: 0.5rem; }
#status { margin-bottom:1rem; padding:.5rem 1rem; border-radius:1rem; background:rgba(255,255,255,0.7); font-weight:600; }
video { margin-top: 10px; border-radius:10px; width:400px; border:2px solid #00eaff; }
#gaugeContainer { margin-top:20px; width:420px; display:flex; justify-content:space-between; }
.gauge { width:180px; height:180px; border-radius:50%; border:8px solid #333; position:relative; }
.needle { width:4px; height:70px; background:red; position:absolute; bottom:50%; left:50%; transform-origin:bottom center; }
.label { text-align:center; margin-top:5px; color:#0ff; font-size:20px; }
.valueBox { margin-top:10px; font-size:22px; text-align:center; color:yellow; }
#direction { margin-top:20px; font-size:28px; color:lime; font-weight:bold; }
#connectButton, #disconnectButton { margin:.4rem; padding:.7rem 1.6rem; border:none; border-radius:1rem; font-weight:600; font-size:1rem; color:#fff; cursor:pointer; }
#connectButton { background:#4caf50; } #disconnectButton { background:#f44336; }
#messages { margin-bottom:1rem; padding:.5rem 1rem; border-radius:1rem; background:rgba(255,255,255,0.7); }
</style>
</head>
<body>
<h1>마이크로비트 얼굴 컨트롤러</h1>
<div id="status">미연결</div>
<div id="messages">
<div id="sent">HTML → micro:bit: –</div>
<div id="received">micro:bit → HTML: –</div>
</div>
<div>
<button id="connectButton">🔗 Connect</button>
<button id="disconnectButton" disabled>🔒 Disconnect</button>
</div>
<video id="webcam" autoplay playsinline></video>
<div id="gaugeContainer">
<div>
<div class="gauge"><div id="yawNeedle" class="needle"></div></div>
<div class="label">Yaw</div>
<div id="yawValue" class="valueBox">Yaw: 0°</div>
</div>
<div>
<div class="gauge"><div id="pitchNeedle" class="needle"></div></div>
<div class="label">Pitch</div>
<div id="pitchValue" class="valueBox">Pitch: 0°</div>
</div>
</div>
<div id="direction">대기</div>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils"></script>
<script>
/* ===== Micro:bit 블루투스 연결 ===== */
const UART_SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
let device, txChar, rxChar;
let isConnected = false;
let sending = false;
let sendQueue = [];
const statusEl = document.getElementById('status');
const sentEl = document.getElementById('sent');
const receivedEl = document.getElementById('received');
const btnConnect = document.getElementById('connectButton');
const btnDisc = document.getElementById('disconnectButton');
function logStatus(msg){ statusEl.textContent = msg; }
function logSent(cmd){
sentEl.textContent = `HTML → micro:bit: ${cmd}`;
sentEl.classList.remove('sent-active');
void sentEl.offsetWidth;
sentEl.classList.add('sent-active');
}
async function safeSend(cmd){
sendQueue.push(cmd);
if (sending) return;
sending = true;
while (sendQueue.length > 0 && isConnected && txChar){
const next = sendQueue.shift();
const data = new TextEncoder().encode(next + '\n');
try {
if(txChar.properties.writeWithoutResponse) await txChar.writeValueWithoutResponse(data);
else await txChar.writeValue(data);
logSent(next);
await new Promise(r=>setTimeout(r,30));
} catch(e){
logStatus('⚠️ 전송 오류: '+e.message+' (재시도)');
await new Promise(r=>setTimeout(r,100));
}
}
sending=false;
}
btnConnect.addEventListener('click', async ()=>{
try{
logStatus('🔍 디바이스 검색 중…');
device = await navigator.bluetooth.requestDevice({
filters:[{namePrefix:'BBC micro:bit'}],
optionalServices:[UART_SERVICE_UUID]
});
const server = await device.gatt.connect();
logStatus('서비스 연결 중…');
const svc = await server.getPrimaryService(UART_SERVICE_UUID);
const chars = await svc.getCharacteristics();
txChar = rxChar = null;
chars.forEach(ch=>{
const p = ch.properties;
if((p.write||p.writeWithoutResponse)&&!txChar) txChar=ch;
if((p.notify||p.indicate)&&!rxChar) rxChar=ch;
});
rxChar.addEventListener('characteristicvaluechanged', e=>{
const v = new TextDecoder().decode(e.target.value).trim();
receivedEl.textContent = `micro:bit → HTML: ${v}`;
});
await rxChar.startNotifications();
isConnected = true;
btnConnect.disabled = true;
btnDisc.disabled = false;
logStatus('✅ 연결 완료');
} catch(e){ logStatus('❌ 연결 실패: '+e.message); }
});
btnDisc.addEventListener('click', ()=>{
if(device?.gatt.connected) device.gatt.disconnect();
isConnected = false;
btnConnect.disabled = false;
btnDisc.disabled = true;
logStatus('🔌 연결 해제됨');
});
/* ===== FaceMesh + 방향 결정 ===== */
const video = document.getElementById("webcam");
const yawNeedle = document.getElementById("yawNeedle");
const pitchNeedle = document.getElementById("pitchNeedle");
const yawValueBox = document.getElementById("yawValue");
const pitchValueBox = document.getElementById("pitchValue");
const directionBox = document.getElementById("direction");
let camera = null;
// 얼굴 좌우 중앙 기준 코 위치로 Yaw 계산
function calculateAngles(landmarks){
const noseTip = landmarks[1];
const leftFace = landmarks[234];
const rightFace = landmarks[454];
const forehead = landmarks[10];
const chin = landmarks[152];
const faceCenterX = (leftFace.x + rightFace.x)/2;
const yaw = (noseTip.x - faceCenterX) * 300; // 정면 = 0°
const pitch = ((forehead.y - noseTip.y) - (noseTip.y - chin.y)) * 150;
return { yaw, pitch };
}
function updateGauge(yaw, pitch){
yaw = Math.max(-90, Math.min(90, yaw));
pitch = Math.max(-90, Math.min(90, pitch));
yawNeedle.style.transform = `rotate(${yaw}deg)`;
pitchNeedle.style.transform = `rotate(${pitch}deg)`;
yawValueBox.textContent = `Yaw: ${yaw.toFixed(2)}°`;
pitchValueBox.textContent = `Pitch: ${pitch.toFixed(2)}°`;
// 방향 판단
let cmd = '';
if(pitch > 20) { cmd = 'w'; directionBox.textContent = '직진'; }
else if(yaw > 20) { cmd = 'd'; directionBox.textContent = '우회전'; }
else if(yaw < -20) { cmd = 'a'; directionBox.textContent = '좌회전'; }
else { directionBox.textContent = '대기'; }
if(cmd) safeSend(cmd);
}
async function startFaceTracking(){
const faceMesh = new FaceMesh({ locateFile:file=>`https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}` });
faceMesh.setOptions({ maxNumFaces:1, refineLandmarks:true, minDetectionConfidence:0.6, minTrackingConfidence:0.6 });
faceMesh.onResults(results=>{
if(results.multiFaceLandmarks?.length>0){
const { yaw, pitch } = calculateAngles(results.multiFaceLandmarks[0]);
updateGauge(yaw, pitch);
}
});
camera = new Camera(video, { onFrame: async ()=>await faceMesh.send({ image:video }), width:640, height:480 });
camera.start();
}
navigator.mediaDevices.getUserMedia({ video:true })
.then(stream=>{ video.srcObject=stream; startFaceTracking(); })
.catch(err=>alert("카메라 권한 필요!"));
</script>
</body>
</html>
Python
복사
기타 자료( 문제점 및 해결방안)
•
문제점 및 해결방안: 집에서 처음에 자동차의 좌우를 돌리는 서보모터의 각을 정의하는 과정에서 너무 큰 값으로 설정해 모터가 계속 돌아 타는 냄새가 났다. 또 움직일 때만 좌우 조정이 되고 초음파 센서에 감지가 되어도 자동차가 멈추지않아 작동 관리가 까다로웠다.
•
문제 사진 및 오류 코드:
•
해결방안: 기준 각을 180도 설정한 뒤 하드웨어를 분리해 처음부터 모터 작동여부를 확인하였다. 또한 조건문에 포함되는 여부를 바꾸어 각종 문제를 해결해 나가고 LED와 스피커를 활용해 졸음운전 시스템까지 넣었다.
•
해결 후 코드 및 사진
•
Canva:
느낀점
이번 프로젝트를 통해 장애인과 전신마비 환자들에게 도움이 될 수 있는 기술을 직접 구현해 보면서 큰 보람을 느꼈다. 가장 인상 깊었던 점은 서로 다른 기술들을 하나의 시스템 안에서 자연스럽게 연결하는 과정이었다. 얼굴 제어, 초음파 센서, 졸음운전 감지 기능은 각각 작동 방식도 다르고 필요한 데이터 처리 방식도 달라서 어려울 것 같았지만 하나씩 분석하고, 서로 간섭이 나지 않도록 코드를 제작하는 과정을 거치면서 시스템 전체가 유기적으로 움직이기 시작했다. 특히 미디어파이프의 얼굴 인식 값과 초음파 센서의 거리 값, LED출력 값이 하나의 동작 흐름 속에서 매끄럽게 이어질 때 큰 성취감을 느꼈다. 특히 미디어파이프와 블록 코드를 활용해 작은 자동차가 실제로 얼굴 움직임에 반응하고, 초음파 센서로 장애물을 감지해 스스로 멈추는 모습을 보았을 때 기술이 사람을 도울 수 있다는 가능성을 느낄 수 있었다. 눈을 오래 감을 경우 LED와 비상음이 울리도록 구현하는 과정에서도 여러 오류를 마주했지만, 이유를 분석하고 고쳐나가는 과정을 통해 개발자의 사고방식을 배울 수 있었다. 또한 직접 영상을 촬영·편집하고, 얼굴의 움직임에 따라 직진, 좌,우회전을 평가하는 웹사이트까지 제작하면서 프로젝트를 전체적으로 구성하는 능력도 키울 수 있었다. 많은 어려움이 있었지만 하나씩 해결하며 완성도를 높이는 과정이 매우 의미 있었고, 앞으로도 사람들에게 실질적인 도움을 줄 수 있는 기술 개발을 해보고 싶다는 동기부여를 얻었다. 이번 경험은 나에게 기술의 가치와 개발의 즐거움을 모두 느끼게 해준 소중한 계기가 되었다.







