프로젝트 '재움' 개발과정(1)
안녕하세요! 얼마 전 프로젝트 '재움'의 개발을 시작했습니다.
웹페이지에서 웹캠으로 실시간으로 영상을 촬영하는데 10초에 한 번씩 캡처를 하여 저의 로컬 폴더에 저장하도록 해보겠습니다.
로그인 회원가입 서비스는 개발 마지막에 적용할 것이기 때문에 사용자를 구별하기 위해 닉네임으로 임의 대체하였습니다.
record.html
<!DOCTYPE html>
<html>
<head>
<title>웹 카메라 촬영</title>
</head>
<body>
<!--닉네임 입력칸-->
<input name="nickname" id="nickname" placeholder="닉네임을 입력해주세요" />
<!--웹캠-->
<video id="camera" autoplay></video>
<button id="captureButton">촬영</button>
<button id="endButton">종료</button>
<script src="static/js/record.js"></script>
</body>
</html>
이제 촬영버튼을 누르면 웹캠이 실행되도록 javaScript코드를 작성해 줍니다.
1. 촬영버튼을 누르면 웹캠이 실행되고 10초마다 한 번씩 캡처한 이미지를 서버로 보내줍니다.
2. 종료버튼을 누르면 웹캠이 종료됩니다.
record.js
// HTML 문서에서 camera id를 가진 비디오 요소를 가져옵니다.
const videoElement = document.getElementById("camera");
// captureButton id를 가진 버튼 요소를 가져옵니다.
const captureButton = document.getElementById("captureButton");
// endButton id를 가진 버튼 요소를 가져옵니다.
const endButton = document.getElementById("endButton");
let mediaStream; // 비디오 스트림 객체
let captureStartTime; // 촬영 시작 시간
let captureEndTime; // 촬영 종료 시간
let intervalId; // Interval 핸들러 ID를 저장할 변수
촬영버튼을 클릭했을 때의 이벤트를 작성해 줍니다.
// "촬영" 버튼 클릭 시 비디오 실행 및 촬영 시작 시간 저장
captureButton.addEventListener("click", function () {
// 웹 카메라에 액세스하여 비디오 스트림 가져오기
navigator.mediaDevices
.getUserMedia({ video: true })
.then(function (stream) {
mediaStream = stream; // 비디오 스트림 저장
videoElement.srcObject = stream;
captureStartTime = new Date(); // 촬영 시작 시간 저장
// 10초에 한 번 프레임 캡처 및 업로드
intervalId = setInterval(captureAndUploadFrame, 10000);
})
.catch(function (error) {
console.error("웹 카메라 액세스 오류:", error);
});
});
캡처한 이미지 데이터를 서버로 보내주는 함수를 작성해 줍니다.
촬영버튼을 눌렀을 때 이미지 캡처를 위해 아래와 같이 captureAndUploadFrame함수가 사용됩니다.
intervalId = setInterval(captureAndUploadFrame, 10000);
function captureAndUploadFrame() {
// HTML <canvas> 요소를 동적으로 생성합니다. 이 캔버스는 이미지를 그리기 위한 렌더링 대상이 됩니다.
const canvas = document.createElement("canvas");
// 캔버스 크기 설정
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
canvas
// 2D 그래픽 컨텍스트를 가져옵니다.
.getContext("2d")
// videoElement의 비디오 프레임을 캔버스에 그립니다.
.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
// 캡처된 이미지를 Blob 형태로 변환
canvas.toBlob(function (blob) {
// Blob 데이터를 image(key)로 FormData에 추가
const formData = new FormData();
formData.append("image", blob, "captured_image.jpg");
console.log("formData : ", formData);
// 서버로 이미지 데이터 전송 (fetch API 사용)
fetch("/record/uploadFrame", {
method: "POST",
body: formData,
})
.then(function (response) {
console.log("response : ", response);
if (response.ok) {
console.log("이미지 업로드 성공!");
} else {
console.error("이미지 업로드 실패");
}
})
.catch(function (error) {
console.error("이미지 업로드 중 오류 발생:", error);
});
}, "image/jpg"); // jpg로 이미지 저장
}
fetch를 통해 전달된 이미지 데이터를 받을 서버단을 작성해 주겠습니다.
서버 엔드포인트의 URL을 /record/uploadFrame으로 설정해 주었기에 해당 URL로 받아주어야 합니다.
먼저 이미지 데이터를 저장할 폴더를 프로젝트 내부에 uploaded_frames 폴더를 생성해 주었습니다.
api_record.py
from fastapi import APIRouter, File, UploadFile
# "Record"라는 태그를 가지며, 404 응답 코드에 대한 설명도 정의
router = APIRouter(
tags=["Record"],
responses={404: {"description": "Not found"}},
)
@router.post("/record/uploadFrame")
async def upload_image(image: UploadFile):
# 이미지를 캡처하고 uploaded_frames에 저장합니다.
file_path = f"uploaded_frames/{image.filename}"
with open(file_path, "wb") as image_file:
image_file.write(image.file.read())
return {"message": "이미지 저장 및 처리 완료"}
javaScript의 formData.append("image", blob, "captured_image.jpg"); 이 부분을 통해
이미지 파일의 이름은 captured_image.jpg로 저장됩니다.
지금은 이미지가 캡처될 때마다 덧붙여집니다.
저장되는 것을 확인하였으니 종료버튼을 눌렀을 때의 이벤트도 만들어보겠습니다.
record.js에 이어서 작성해 주시면 됩니다.
// "종료" 버튼 클릭 시 비디오 중지 및 촬영 종료 시간 저장
endButton.addEventListener("click", function () {
mediaStream.getTracks().forEach(function (track) {
track.stop(); // 비디오 스트림 중지
});
videoElement.srcObject = null; // 비디오 정지
captureEndTime = new Date(); // 촬영 종료 시간 저장
// 촬영 중지를 위해 Interval을 제거
if (intervalId) {
clearInterval(intervalId);
}
이렇게 실시간으로 영상이 촬영되면서 10초마다 이미지 데이터를 서버에 보내 저장하는 로직을 작성해 보았습니다.
이제 이미지가 저장될 때 이전에 작성한 자세인식을 토대로 랜드마크와 라벨을 추가해서 저장해 주겠습니다.
api_record.py를 이어서 작성합니다.
먼저 상단에 필요한 모듈들을 import 해줍니다.
import cv2
import mediapipe as mp
import os
from fastapi_app.utils.pose_recognize.classify_pose import classify_pose
from fastapi_app.utils.pose_recognize.detect_pose import detect_pose
저장된 이미지를 다시 읽어 랜드마크를 감지합니다.
@router.post("/record/uploadFrame")
async def upload_image(image: UploadFile):
# 이미지를 캡처하고 저장합니다.
file_path = f"uploaded_frames/{image.filename}"
with open(file_path, "wb") as image_file:
image_file.write(image.file.read())
####### 아래부터 신규 코드 #######
# 캡처한 이미지를 읽습니다.
image = cv2.imread(f"uploaded_frames/{image.filename}")
# 랜드마크를 감지합니다.
response = detect_pose(image, pose, display=False)
조건문을 작성해 줍니다.
랜드마크가 찍혔다면 classify_pose함수를 통해 라벨을 분류합니다.
분류된 후에는 라벨링 되기 전 이미지는 삭제합니다.
현재날짜와 라벨을 이름으로 라벨링 된 이미지를 저장합니다.
찍히지 않았다면 캡처된 이미지를 삭제합니다.
if response is not None:
landmarks_image, landmarks = response
frame_height, frame_width, _ = landmarks_image.shape
fps = 50
# 랜드마크를 기반으로 라벨을 분류합니다.
labeled_image, labels = classify_pose(landmarks, landmarks_image, fps, frame_height, frame_width, display=False)
print("labels : " , labels)
# 라벨링 되기 전 이미지는 삭제합니다.
if os.path.exists(file_path):
os.remove(file_path)
# 현재 날짜와 시간을 가져옵니다.
current_datetime = datetime.now().strftime("%Y-%m-%d")
# 라벨링된 이미지의 이름 지정
file_path = f"uploaded_frames/{current_datetime}_{labels}.jpg"
# 이미지를 저장합니다.
cv2.imwrite(file_path, labeled_image)
return {"message": "이미지 저장 및 처리 완료", "labels": labels}
else:
# 랜드마크를 감지하지 못한 경우 이미지를 삭제
os.remove(file_path)
return {"message": "랜드마크를 감지하지 못하거나 라벨을 추가하지 못함"}
위 코드를 통해 아래와 같은 결과를 확인할 수 있습니다.