진짜 최종최종최종

SAMURAI 기반 데이터 전처리 및 학습 정확도 향상 계획

SAMURAI를 활용하여 데이터 전처리 단계에서 불필요한 배경 데이터를 제거하고 모델 학습의 정확도를 높이기 목표

초기 데이터 구조

  • 12개 행동, 각 행동당 86개 폴더
  • 각 폴더에는 약 13장의 프레임 이미지와 해당 키포인트를 나타내는 JSON 파일 포함

SAMURAI를 활용한 데이터 전처리

데이터셋의 품질 개선을 목적

  1. 강아지 객체 탐지
    SAMURAI 모델 사용
  • 각 프레임 이미지에서 강아지 영역을 탐지
  • 탐지된 강아지 영역의 경계 상자 (bounding box)를 얻음
  1. 강아지 영역 크롭
  • 탐지된 경계 상자를 기준으로 이미지를 크롭하여 강아지 영역만 남김
  • 배경 데이터와 잡음을 제거하여 학습 데이터의 품질을 개선
  1. 키포인트 조정
  • 크롭된 이미지의 좌표계를 기준으로 JSON 파일의 키포인트를 변환
  • 강아지 객체의 상대적 위치를 유지하도록 수정
  1. CSV 데이터 생성
  • 크롭된 강아지 영역과 수정된 키포인트 데이터를 새로운 CSV로 기록
  • 최종 CSV는 모델 학습에 사용될 고품질 데이터를 포함

학습 데이터 준비

  1. 데이터셋 변환
  • 크롭된 강아지 영역 이미지를 기반으로 행동 라벨과 키포인트 데이터를 결합
  • CSV 파일을 읽어 스켈레톤 데이터와 라벨로 변환

추가할까
데이터 증강

  • 영상 데이터 증강 : 이미지 변환 (회전, 크기 조절, 밝기 및 색상 변화), 시간적 증강(영상에서 프레임을 무작위로 선택하거나 순서를 변경하여 모델이 시간적 패턴에 더 강건하도록)
  • 스켈레톤 데이터 증강 : 키포인트 데이터를 변형하거나 노이즈를 추가하여 스켈레톤 기반 모델의 일반화 성능을 높일 수 있음
  1. 데이터 로더 생성
  • PyTorch DataLoader를 사용해 데이터를 배치 단위로 처리

 

모델 훈련

  1. ST-GCN + SAMURAI-like 3D CNN의 통합 학습
  • SAMURAI-like 3D CNN은 추가적인 특징 추출 단계, 공간적-시간적 행동 패턴을 더욱 복잡하고 정교하게 학습
  • ST-GCN이 학습한 특징 (스켈레톤 데이터 기반)과 SAMURAI-like 3D CNN이 학습한 특징 (이미지 기반)을 융합하여 더 정확하고 강력한 예측이 가능
  • ST-GCN은 스켈레톤 데이터를 활용한 행동 분석에 뛰어난 성능을 보이지만, 이미지에서 강아지의 전체적인 역동적 움직임을 학습하는 데는 제한적
  • SAMURAI-like 3D CNN은 원본 이미지에서 동적 특징을 학습, 이는 ST-GCN과 상호보완적인 역할

두 모델의 출력을 융합 계층(Fusion Layer)에서 결합하면, 각 모델의 장점을 모두 활용할 수 있다.

  • ST-GCN : 스켈레톤 기반 공간적-시간적 특징 학습
  • SAMURAI-like 3D CNN : 이미지 기반 동적 패턴 학습

이렇게 학습된 모델은 영상 데이터를 프레임 단위로 처리하여 각 프레임 (또는 연속된 프레임의 시퀀스)에서 강아지의 행동을 예측한다.

 


 

한계

SAM 2.1로 시도했는데, 계속 아래와 같은 에러발생

initialize_and_load_sam_model
    model_path = cfg.sam_model_path
omegaconf.errors.ConfigAttributeError: Key 'sam_model_path' is not in struct       
    full_key: sam_model_path
    object_type=dict

Set the environment variable HYDRA_FULL_ERROR=1 for a complete stack trace.     



ConfigCompositionException 오류는 Hydra가 YAML 설정 파일을 파싱하거나 컴포지션(구성)을 할 때 문제가 생겼을 때 발생

.
.
.

아무리해도 고쳐지지 않아서 github issue를 확인해보니.. issue에 2.1은 감지를 제대로 하지 못한다는 나와 비슷한 경우의 문제를 발견... SAM2 로 사용..?
.
.
.

RuntimeError: Found no NVIDIA driver on your system. Please check that you have an NVIDIA GPU and installed a driver from http://www.nvidia.com/Download/index.aspx

 

 

PyTorch는 주로 NVIDIA CUDA를 기반으로 하며, AMD GPU의 경우 기본적으로 지원되지 않습니다
SAM-2는 CUDA 확장을 사용하는 것으로 보이며, NVIDIA GPU에서만 CUDA를 사용할 수 있습니다. AMD GPU는 CUDA를 지원하지 않기 때문에, SAM-2의 일부 기능은 사용할 수 없을 가능성이 큽니다.

CUDA를 비활성화하고 CPU에서 실행, OpenCL이나 ROCm 기반의 설정을 고려, ROCm은 Linux 환경으로 전환해야한다고 해서 패스, OpenCL 사용하려고 했는데... 이것도 마찬가지

CPU에서 실행하려면, 코드에서 명시적으로 device를 CPU로 설정해야 합니다

device = torch.device("cpu")
sam_model.to(device)

 

 

강제로 cpu로 시도하려고 하였으나
기능을 사용하려면 CUDA 버전과 GPU 아키텍처가 적합해야 하는데, 현재 환경에서는 사용할 수 없다는 내용이 계속 발견..
.
.
.

samurai가 nvidia 에서 훈련시킨걸로 나의 그래픽카드로는 계속 안된다고 떠.... 서 ..... 학원 컴퓨터에서 해야할 거 같은데.. 시간이 되려나...............

0,data/train/feetup/frame_feetup\dog-feetup-001230\frame_0_timestamp_0.jpg,1.0666666666666667,0.28854166666666664,0.9666666666666667,0.18489583333333334,0.799074074074074,0.121875,1.0694444444444444,0.28854166666666664,1.2972222222222223,0.04114583333333333,0.9462962962962963,0.20208333333333334,1.1092592592592592,0.2171875,0.9101851851851852,0.24739583333333334,1.0638888888888889,0.3703125,1.0703703703703704,0.30416666666666664,1.0407407407407407,0.2609375,1.0638888888888889,0.3619791666666667,1.0509259259259258,0.259375,0.9962962962962963,0.035416666666666666,1.2740740740740741,0.13333333333333333
0,data\train\sit\frame_sit\dog-sit-002372\frame_0_timestamp_0.jpg,386,472,582,438,846,582,794,663,733,896,950,900,1060,1145,1267,315,108,445,434,452,788,785,1021,1074,877,920,907,961,781,853

오!!! 시간은 좀 오래 걸리지만 그래도 SAM 2, SAMURAI 적용 하니깐 키포인트가 소수점에서 정수로 변했다..!

소수점 좌표는 보통 정규화된 상태로, 이미지 크기(예: 너비, 높이)와 독립적

SAMURAI가 데이터 후처리 과정에서 좌표를 다시 이미지 크기(픽셀 단위)에 맞추는 정규화 해제를 수행했을 가능성 큼

정수 좌표는 이미지의 실제 해상도와 일치하므로, 시각화와 디버깅이 훨씬 쉬워진다고 함!

쨌뜬 좋은 방향 인거 같다 맞아..!

 


 

시간이 진짜 7시간 지나도 csv 처리가 안되서 데이터 양을 줄이고, 다시 해보는걸로..!

각 행동당 86개의 영상으로 처리할 수 있도록 해보자

csv 개수는 대략 750개 정도

RandomOverSampler와 SMOTE는 모두 데이터 불균형 문제를 해결하기 위해 데이터 오버샘플링을 수행하는 방법 이지만 방식은 다르다.

현재 RandomOverSampler 사용

RandomOverSampler

  • 기존 데이터의 샘플을 단순히 복제하여 부족한 클래스의 샘플을 늘린다.
  • 빠르고 간단
  • 원본 데이터의 샘플을 그대로 복사하기에 데이터에 아무런 변형이 가해지지 않음
  • 과적합의 위험이 높음 (🫨)
  • 데이터 크기가 작고, 빠르게 균형을 맞추고 싶을 때, 데이터를 변형할 필요가 없는 경우 사용

 

SMOTE (Synthetic Minority Oversampling Technique)

  • 부족한 클래스의 샘플과 가까운 샘플들 사이에서 새로운 샘플을 생성
  • 새로운 데이터를 생성하기 때문에 원본 데이터보다 다양성이 높아짐
  • 과적합 위험이 낮아짐
  • 데이터가 고차원일 경우, 부정확한 샘플이 생성될 가능성(노이즈 문제) -> 이게 걱정되는데 ;;
  • 데이터 크기가 적당히 크고, 불균형 클래스의 다양성을 높이고 싶을 때 간단한 복제가 아닌 새로운 데이터를 생성하고 싶을 때

=> SMOTE로 변경 후 결과를 봐보자

 


 

SAMURAI 설치

앞에서는 정리를 못한 것 같아서

SAMURAI는 SAM 2 기반으로 설계되었으므로 SAM 2와 SAMURAI 설치가 필요

SAM 2 설치

git clone https://github.com/facebookresearch/sam2.git
cd sam2
pip install -e .
pip install -e ".[notebooks]"

SAMURAI 설치

SAM 2가 설치된 상태에서, SAMURAI 코드를 다운로드하고 실행

git clone https://github.com/yangchris11/samurai.git
cd samurai

 

SAMURAI는 SAM 2 위에서 동작하기 때문에 SAM 2를 설치한 디렉터리 경로가 PYTHONPATH에 포함되어야함

SAMURAI를 사용하기 위해서는 segment-anything 라이브러리를 설치해야한다.

pip install matplotlib==3.7 tikzplotlib jpeg4py opencv-python lmdb pandas scipy loguru

 

이 패키지들은 데이터 시각화, 이미지 처리, 데이터 저장 및 로딩, 로그 기록 등 다양한 기능 제공, 각각은 SAMURAI에서 특정 역할을 하며, 특히 데이터 처리와 분석 과정에서 필수적

 

체크포인트 다운로드

SAMURAI는 SAM 2.1의 체크포인트를 사용

cd checkpoints
./download_ckpts.sh

 

이론

SAMURAI 적용을 모델 훈련 부분에서가 아닌 데이터 csv 변형할 때 적용, 강아지 객체를 감지한 후, 해당 영역만 크롭하여 사용하면 모델이 학습해야할 데이터의 잡음을 줄임, 강아지 영역 외의 불필요한 배경 데이터를 제거할 수 있음 /
SAMURAI로 감지된 강아지 영역만 CSV에 기록함으로써, 학습 데이터의 정확도를 높이고, 데이터셋 품질을 개선하는 목적

 

3차

80:10:10 비율 (train : validation : test 의 csv 파일)

 

데이터 증강 샘플링, 오버 샘플링 적용해서 클래스 비율을 맞춤

 

 


Train Loss와 Validation Loss 간의 차이가 계속 존재하고 있으며, 특히 Validation Accuracy가 매우 낮은 상태로 유지

검증 정확도가 10% 부근 정체 -> 모델이 데이터 분포를 제대로 학습하지 못하고 있음.. 데이터의 불균형 문제 또는 모델이 너무 단순해서..? .. .

loss 불안정한 패턴은 학습과 검증 데이터 간 분포 차이 또는 과적합 때문일 수 있음

강사님의 조언을 듣고, SAMURAI를 기존 코드에 적용해보고자함.. 제발 훈련이 진행되고, 정확도가 올라가기를 빌면서 🙏

기존 구조는
Flask 서버 -> 영상 전처리 -> 관절 데이터 추출 -> ST-GCN -> 예측결과
SAMURI를 적용하면
Flask 서버 -> 영상 전처리 -> 관절 데이터 추출 + 프레임 피처 추가 -> SAMURAI (ST-GCN + 스파스 융합) -> 예측 결과

 

SAMURAI는 ST-GCN에 추출한 관절 특징과 프레임 이미지 피처를 통합하여 동작

객체를 잡는 것은 SAMURAI가 자동으로 수행 가능
-> 일단 SAMURAI에 입력된 데이터를 기반으로 자동으로 중요한 객체를 학습하고, 예측에 반영

train_model에서 기존 ST-GCN 모델 학습 단계에 SAMURAI를 추가, 관절 데이터와 프레임 피처를 입력으로 학습
test_model_with_video 에서 사용자로부터 받은 영상을 분석할 때 SAMURAI를 사용하여 예측 / 관절 데이터와 추가 프레임 정보를 SAMURAI에 전달하여 동작
FLASK 서버에서 사용자로부터 받은 영상을 처리하여 관절 데이터와 프레임 피처를 추출한 뒤, SAMURAI를 통해 예측 결과 반환

 

프레임 피처는 비디오나 연속적인 데이터를 다룰 때, 각 프레임에서 추출된 유용한 정보


 

1차

frame_features가 주어지지 않거나 더미 데이터(0으로 채워진 텐서)일 경우 ST-GCN만 사용

 

학습(Train)과 검증(Validation)의 손실(Loss) 및 정확도(Accuracy)

Train Loss -> 학습 손실이 초기에는 감소하다가 어느 순간 거의 일정하게 유지
Validation Loss -> 검증 손실이 초기에는 감소하지만, 에포크 10 이후 갑작스럽게 증가하거나 크게 변동, 모델이 과적합(Overfitting)

Train Accuracy -> 40% 까지 증가 , 모델 학습 데이터를 잘 예측하고 있다는 신호
Validation Accuracy -> 일정 수준(약 10%)에서 머물러있음..

 

 

진짜 미치겠네.. 일단 samurai를 도입했을 때 train 정확도가 올라감 흠..

validation loss도 전의 값에서는 말도안되는 값이 나왔는데 확실히 잡힘 오...


 

2차

frame_features 데이터를 미리 생성하지 않아도 되도록, 모델 내부에서 SAMURAI처럼 프레임 피처를 동적으로 추출하는 모듈을 추가

Linear 계층 -> 3D CNN (공간적(이미지) 정보뿐만 아니라 시간적(프레임 간) 정보를 동시에 학습) 변경

linear 계층을 사용하는 것은 영상 분석에서도 적절할 수 있지만, 주로 시각적 또는 시공간적 정보를 충분히 캡처하지 못할 가능성... 왜 이걸 생각 못했지 완전 다르게 생각했다..

시공간적 패턴을 학습하려면 3D CNN, RNN/LSTM, 또는 Transformer 기반 아키텍처를 고려

class Config:
    def __init__(self):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.num_classes = 12  # 행동 클래스 수
        self.num_joints = 15  # 관절 개수
        self.num_frames = 30  # 프레임 수
        self.batch_size = 16  # 배치 크기
        self.learning_rate = 0.001  # 학습률
        self.num_epochs = 20  # 에포크 수
        self.models_dir = "./models"  # 모델 저장 경로
        self.train_csv = "./data/train.csv"  # 학습 데이터 경로
        self.val_csv = "./data/val.csv"  # 검증 데이터 경로

 

1초당 30장의 프레임으로 분석

 

 

validation loss의 불안정성과 validation Accuracy가 매우 낮음

위의 결과와 비슷한걸 보니.. 데이터의 문제인가 행동마다의 개수가 달라서 내가 그 조건을 빠뜨린 것 같다.. 다시

 


 

현재 데이터를 train.csv, val.csv, test.csv로 나누었는데 일단 나눈 이유는 모델 학습과 검증을 명확히 구분하기 위해서..

    train_data, temp_data = train_test_split(data, test_size=0.3, random_state=42, stratify=data['label'])
    validation_data, test_data = train_test_split(temp_data, test_size=0.5, random_state=42, stratify=temp_data['label'])

 

현재 비율을 70:15:15
데이터의 문제일까.. 현재 데이터는 87764개의 샘플이 있고, 12개의 행동으로 분류되어있다. 각 행동에 대한 샘플 수는 다르기에.. 클래스 불균형 문제있을 가능성

다음은 80:10:10으로 바꾸고 실행

'팀프로젝트 - TailsRoute' 카테고리의 다른 글

TEAM Project (12.01) - SAMURAI, SAM 2 설치  (0) 2024.12.02
TEAM Project (11.30)  (0) 2024.12.01
TEAM Project (11.28)  (0) 2024.11.28
TEAM Project (11.26)  (0) 2024.11.26
TEAM Project (11.25) - 카카오 소셜 로그인 구현  (0) 2024.11.25

삽질을 연달아.. 최종최종

  1. 사용자가 영상을 보내면 관절 데이터를 추출해서 ST-GCN 에 넣는다.
  2. ST-GCN 훈련 : 내가 가진 CSV 데이터로 ST-GCN을 가르친다.
  3. 결과
frame_number,frame_path,x1,y1,x3,y2,x5,y3,x7,y4,x9,y5,x11,y6,x13,y7,x15,y8,x17,y9,x19,y10,x21,y11,x23,y12,x25,y13,x27,y14,x29,y15,label
0,data/train/bodylower/frame_bodylower\dog-bodylower-001010\frame_0_timestamp_0.jpg,0.5962962962962963,0.6640625,0.587037037037037,0.45625,0.7,0.5604166666666667,0.6768518518518518,0.6489583333333333,0.5768518518518518,0.271875,0.3555555555555555,0.5359375,0.8203703703703704,0.4427083333333333,0.375,0.6536458333333334,0.8055555555555556,0.5598958333333334,0.3583333333333333,0.2822916666666666,0.8333333333333334,0.2703125,0.3861111111111111,0.6375,0.8046296296296296,0.5583333333333333,0.7287037037037037,0.178125,0.8055555555555556,0.2119791666666666,0

 

내가 가진 CSV 파일을 ST-GCN 모델을 훈련시킨다.
Skeleton 데이터 (x,y 좌표)와 레이블(label)을 통해 모델은 행동을 학습하고 예측할 수 있음

 


 

 

결론적으로 이 페이지는 강아지 행동 분류를 위한 ST-GCN (Spatio-Temporal Graph Convolutional Network) 모델을 구현하는 데 중점을 둠. 강아지의 관절 위치 (x, y좌표) 데이터를 사용하여 다양한 행동을 분류하는 모델을 훈련시킨다. 이 모델은 비디오 데이터나 연속된 이미지 프레임을 통해 강아지의 움직임을 분석하고, 특정 행동 (ex : 앉기, 누워 있기 등)을 예측할 수 있다.

코드구조

  1. Config -> 모델 훈련에 필요한 모든 설정을 저장하는 클래스
 self.behavior_classes = {
   0: '몸 낮추기',         # bodylower
   1: '몸 긁기',           # bodyscratch
   2: '몸 흔들기',         # bodyshake
   3: '앞발 들기',         # feetup
   4: '한쪽 발 들기',      # footup
   5: '고개 돌리기',       # heading
   6: '누워 있기',         # lying
   7: '마운팅',            # mounting
   8: '앉아 있기',         # sit
   9: '꼬리 흔들기',       # tailing
   10: '돌아보기',         # turn
   11: '걷거나 뛰기'       # walkrun
}
  1. Dataset -> PyTorch의 Dataset을 상속받아, 데이터를 모델에 맞는 형태로 변환
  • 각 샘플은 강아지의 관절 좌표 데이터를 포함하여, x와 y 좌표가 joints 수만큼 나열되어있다. 이 데이터는 프레임 수, 관절 수, x/y 좌표의 3D 텐서로 변환된다.
  • __getitem__ 메서드는 데이터셋에서 특정 인덱스의 데이터를 반환합니다. 반환되는 데이터는 강아지의 관절 좌표 데이터와 해당 행동에 대한 레이블
  1. STGCN -> 그래프 합성곱 네트워크, 각 관절을 그래프의 노드로 보고, 관절 간 연결 관계를 그래프의 엣지로 모델링 / 이 네트워크는 공간적 및 시간적 특성을 모두 학습할 수 있도록 설계
  2. trin_behavior_model.py
    -> 모델을 훈련시키는 함수, 각 에포크마다 훈련 손실과 정확도를 계산하고, 검증 데이터에서 평가를 수행

 

 

 

 

 

 

 

 

 

 

정확도의 최대값은 37%.. 그리고 손실은 1.89
지금까지 나왔던 것중에 정확한 듯.. 영상으로는! 역시 이미지보다 난이도가 더 높은 것 같다..

문제 해결 및 디버깅

데이터셋 불일치 문제

CSV 파일에 있는 관절 좌표 열 이름과 모델이 원하는 형식이 맞지 않아 계속 에러 발생..

csv 파일에서 관절 좌표는 강아지의 15개 관절에 대한 x, y좌표가 있어야하고, 이 데이터를 csv파일에서 어떻게 추출?

x1,y1,x3,y2,x5,y3,x7,y4,x9,y5,x11,y6,x13,y7,x15,y8,x17,y9,x19,y10,x21,y11,x23,y12,x25,y13,x27,y14,x29,y15 강아지의 15개의 관절에 대한 2D 좌표, 이 데이터는 총 30개의 열이 필요

joint_columns 정의하기

데이터를 모델이 이해할 수 있게 처리해야하는데, 15개의 관절에 대해 열의 이름을 self.joint_columns로 정의

CSV 파일에 있는 관절 좌표 열이 코드에서 정의한 joint_columns과 정확히 일치해야한다. ( -> 당연히 1~15 순으로 되어있는 줄 알고 순서대로 작성했다가 확인해보니, 순서대로 적혀 있지 않았음.. 역시.. 예측은 좋지않아.. 다 꼼꼼히 확인해야지..)

 

 


fc 계층의 입력 텐서 크기와 선형 계층의 가중치 크기가 일치하지 않아서 에러 발생

RuntimeError: mat1 and mat2 shapes cannot be multiplied (16x115200 and 192000x12)

선형 계층(fc)에 입력되는 텐서 크기와 선형 계층의 가중치 크기가 맞지 않아서 발생하는 에러

이게 뭔 소리지..

mat1의 크기: (16, 115200) → 배치 크기 16, 피처 크기 115200
mat2의 크기: (192000, 12) → 선형 계층의 가중치 크기

이 문제는 모델에서 Conv 계층을 거친 후에 나온 출력 텐서를 평탄화해서 선형 계층에 넣는데 이때 계산 방식이 잘못 된 것

선형 계층은 사실 2D 텐서만 처리, 텐서라는 건 데이터가 여러 차원으로 배열된 걸 말하는데, 선형 계층에서는 이런 배열을 2D 배열로 바꿔줘야한다더라..

(배치 크기, 피처 크기) 형태로 바꿔야 한다는 거지. 그래서 x = x.view(x.size(0), -1) 이걸 사용

배치 크기를 제외한 나머지 차원을 자동으로 계산해서 평탄화해주는 역할

내가 겪은 문제는 Conv 계층에서 나오는 출력 크기와 fc 계층의 입력 크기를 계산하는 방식이 맞지 않아서 발생
모델 설계에서, Conv 계층을 통과하고 나면 출력 텐서의 크기가 (batch_size, channels, frames, joints) 이런 형태로 나오는데, (16, 256, 30, 15)라면, 이걸 평탄화해서 256 30 15 = 115200 이렇게 2D로 바꿔줘야... 한다더라..

그래서 어떻게 해결?

텐서의 크기를 맞추기 위해서는 Conv 계층에서 나오는 텐서 크기를 정확하게 계산하고, 그에 맞춰 fc 계층의 입력 크기를 설정해야된다...!!

서블릿의 주요 기능

  1. 클라이언트로부터 요청을 받는다.
  2. 데이터베이스 연동과 같은 비즈니스 로직을 처리한다.
  3. 처리된 결과를 클라이언트에 돌려준다.

클라이언트가 서블릿에 요청을 하면 먼저 톰캣 컨테이너가 받는다. 그 다음 사용자의 요청이나 응답에 대한 HttpServletRequest 객체와 HttpServletResponse 객체를 만들고 서블릿의 doGet()이나 doPost() 메서드를 호출하면서 이 객체들을 전달한다.

톰캣이 사용자의 요청에 대한 정보를 모든 HttpServletRequest 객체의 속성으로 담아 메서드로 전달하므로 각 HttpServletRequest에서 제공하는 메서드들은 매개변수로 넘어온 객체들을 이용하여 사용자가 전송한 데이터를 받아오거나 응답할 수 있는 것이다.

<script type="text/javascript">
		function LoginForm__submit(form) {
			let loginId = form.loginId.value.trim();
			let loginPw = form.loginPw.value.trim();
			if (loginId.length == 0) {
				alert('아이디 입력해주세요');
				return;
			}
			if (loginPw.length == 0) {
				alert('비번 입력해주세요');
				return;
			}
			form.submit();
		}
	</script>


	<form method="POST" action="doLogin"
		onsubmit="LoginForm__submit(this); return false;">
		<div>
			아이디 : <input autocomplete="off" type="text" placeholder="아이디 입력해주세요"
				name="loginId" />
		</div>
		<div>
			비밀번호 : <input autocomplete="off" type="text" placeholder="비밀번호 입력해주세요"
				name="loginPw" />
		</div>
		<button type="submit">로그인</button>
	</form>


	<div>
		<a style="color: green" href="list">리스트로 돌아가기</a>
	</div>

사용자가 아이디와 비밀번호를 입력 후 로그인을 클릭하게 되면 form 태그의 action 속성은 데이터를 전송할 서블릿이나 JSP의 이름을 지정한다. 위에서는 지정된 이름이 doLogin인 서블릿으로 아이디와 비밀번호가 전송된다.

이벤트 발생과 action에 지정된 URL이 적용되는 그 사이의 시점에 처리할 동작을 onSubmit 속성을 통해 지정할 수 있는데, 연결된 함수에 true를 반환하면 form이 전송, false를 반환한다면 form이 전송되지 않는다.

false가 반환되면 이벤트 처리를 그대로 강제 종료 시키기에 action이 처리되지 않는다. 즉, 수동으로 이벤트 처리를 막아두는 것이다.

위 상황에서는 function도 포함 시켜 주었는데, 이를 해주지 않았을 경우 아이디 또는 비밀번호를 작성하지 않아도 form이 실행되어 넘어가버리는 상황이 발생하였다. 따라서 return false로 막아주고, 그 앞에 function을 넣어 아이디 또는 비밀번호가 올바르게 작성되었는지 확인해주고 function에서 submit을 수동으로 작동시켜주었다.

Flask와 Spring Boot를 활용한 비디오 업로드 및 분석 시스템 구현

흐름

  1. 사용자 업로드 -> 사용자가 브라우저에서 비디오 파일을 업로드
  2. Spring Boot 처리
    -> 업로드된 비디오 파일을 Flask 서버로 전달
    -> Flask 서버가 비디오를 분석하고 결과를 반환
  3. 결과 표시
    -> Spring Boot가 분석 결과를 HTML로 렌더링하여 사용자에게 같은 페이지에서 결과 표시

 

해당 페이지의 Spring Boot 디렉터리 구조

 

com
└── project
└── tailsroute
├── api
│ └── RestTemplateConfig.java // RestTemplate 설정
├── controller
│ └── BehaviorController.java // 요청 처리 컨트롤러
├── service
│ └── BehaviorService.java // Flask 서버와 통신
├── util
│ └── MultipartFileResource.java // 파일 처리 유틸리티
└── templates
└── videoAnalysis.html // 업로드와 결과 HTML

 

  1. RestTemplate 설정
  • Flask 서버와 HTTP 통신을 위해 RestTemplate을 설정
package com.project.tailsroute.api;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

 

RestTemplate는 스프링 프레임워크에서 제공하는 클래스로, 다른 프로그램(또는 서버)에 HTTP 요청을 보낼 때 사용된다. 주로, RESTful 웹 서비스와 통신할 때 사용하며, 클라이언트 역할을 한다. 이를 통하여 GET, POST, PUT, DELETE 등의 HTTP 요청을 보냄

  • 클라이언트 측에서 다른 서버에 요청을 보내고, 응답을 받을 수 있으며, JSON, XML 같은 데이터를 요청하거나 받을 수 있음
  • 요청을 보내고, 응답이 돌아올 때까지 기다리는 동기적인 작업 흐름을 작동
  • HTTP 요청을 위한 코드를 단순화하고, 예외 처리를 쉽게 할 수 있도록 도와준다.

 

package com.project.tailsroute.util;

import org.springframework.core.io.ByteArrayResource;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

public class MultipartFileResource extends ByteArrayResource {

    private final String filename;

    public MultipartFileResource(MultipartFile file) throws IOException {
        super(file.getBytes());
        this.filename = file.getOriginalFilename();
    }

    @Override
    public String getFilename() {
        return this.filename;
    }
}
package com.project.tailsroute.service;

import com.project.tailsroute.util.MultipartFileResource;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@Service
public class BehaviorService {

    private final RestTemplate restTemplate;

    public BehaviorService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public String analyzeVideo(MultipartFile file) throws IOException {
        String url = "http://localhost:5000/analyze"; // Flask 서버 URL

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);

        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        body.add("file", new MultipartFileResource(file));

        HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);

        ResponseEntity<String> response = restTemplate.postForEntity(url, requestEntity, String.class);

        if (response.getStatusCode() == HttpStatus.OK) {
            return response.getBody();
        } else {
            throw new RuntimeException("Flask 서버에서 오류 발생: " + response.getStatusCode());
        }
    }
}
package com.project.tailsroute.controller;

import com.project.tailsroute.service.BehaviorService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Controller
@RequestMapping("/usr/behaviorAnalysis")
public class BehaviorController {

    private final BehaviorService behaviorService;

    public BehaviorController(BehaviorService behaviorService) {
        this.behaviorService = behaviorService;
    }

    @GetMapping("/videoAnalysis")
    public String videoAnalysisPage(Model model) {
        model.addAttribute("result", null);
        return "videoAnalysis";
    }

    @PostMapping("/videoAnalysis")
    public String analyzeVideo(@RequestParam("file") MultipartFile file, Model model) {
        try {
            String result = behaviorService.analyzeVideo(file);
            model.addAttribute("result", result);
        } catch (Exception e) {
            model.addAttribute("result", "오류 발생: " + e.getMessage());
        }
        return "videoAnalysis";
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>비디오 분석</title>
</head>
<body>
    <h1>비디오 업로드 및 분석</h1>

    <form action="/usr/behaviorAnalysis/videoAnalysis" method="post" enctype="multipart/form-data">
        <label for="video">비디오 파일 선택:</label>
        <input type="file" id="video" name="file" accept="video/*" required>
        <br><br>
        <button type="submit">업로드 및 분석</button>
    </form>

    <hr>

    <h2>분석 결과</h2>
    <div>
        <pre>
            <p th:text="${result} != null ? result : '분석 결과가 여기에 표시됩니다.'"></p>
        </pre>
    </div>
</body>
</html>

+ Recent posts