과정/2차 AI프로젝트 서비스 개발

프로젝트 '재움' 개발과정(1)

줘요 2023. 10. 19. 02:05

안녕하세요! 얼마 전 프로젝트 '재움'의 개발을 시작했습니다.

 

웹페이지에서 웹캠으로 실시간으로 영상을 촬영하는데 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": "랜드마크를 감지하지 못하거나 라벨을 추가하지 못함"}

 

위 코드를 통해 아래와 같은 결과를 확인할 수 있습니다.