1. 동영상 데이터
컴퓨터 비전에서 동영상 데이터는 연속된 이미지 프레임들의 집합으로, 단순히 공간적 정보(픽셀, 객체의 위치, 형태 등)뿐만 아니라 시간에 따른 변화(움직임, 동작, 상호작용)를 함께 포함한다는 점에서 정적인 이미지와 구별됩니다. 이러한 시공간적 특성을 활용하여 객체 추적, 행동 인식, 장면 이해, 비디오 요약 등의 다양한 응용이 가능하며, 처리 과정에서는 프레임 간 상관관계와 움직임 정보를 효과적으로 추출하는 것이 핵심 과제입니다.

2. 동영상 분석 기법
1. 시계열 분석 (3D CNN)
시계열 분석에서 사용하는 3D CNN은 동영상 데이터를 단순히 독립된 이미지 프레임으로 보지 않고, 공간(가로·세로) + 시간 축(프레임 순서)을 함께 고려하여 특징을 추출하는 방법입니다. 즉, 2D CNN이 한 장의 이미지에서 주변 픽셀 간의 상관관계를 분석하는 것과 달리, 3D CNN은 연속된 여러 프레임을 동시에 입력으로 받아 움직임, 변화, 흐름과 같은 시간적 패턴까지 학습할 수 있습니다. 이러한 특성 덕분에 사람의 행동 인식, 제스처 분석, 스포츠 장면 분석처럼 동영상의 연속성과 동작 맥락이 중요한 문제에서 효과적으로 활용됩니다.
2. 이미지 합성 (2D CNN 변형)
이미지 합성(2D CNN 변형) 기법은 동영상의 모든 프레임을 시간 축으로 직접 처리하지 않고, 여러 장의 프레임을 하나의 이미지로 합쳐 2D CNN으로 분석하는 방식입니다. 예를 들어, N개의 연속된 프레임을 채널 단위로 이어 붙여 입력하면 기존 RGB(3채널) 대신 N채널 이미지를 구성할 수 있으며, 이를 통해 CNN이 공간적 특징을 추출하면서 제한적으로 시간적 변화를 반영하게 됩니다. 이 방법은 3D CNN에 비해 연산량과 메모리 사용이 크게 줄어들어 효율적이며, 시간적 흐름보다 정적인 장면 정보가 중요한 경우 유리하지만, 프레임 간의 세밀한 움직임이나 시계열적 맥락을 충분히 반영하지 못한다는 한계가 있습니다.
3. Attention 기법
Attention 기법을 활용한 동영상 처리는 모든 프레임을 동일하게 다루는 대신, 각 프레임의 중요도를 학습하여 가중치를 다르게 부여하는 방식입니다. 즉, 동영상 전체 길이 중에서 특정 순간이 전체 의미나 분류 결과를 크게 좌우할 수 있는데, Attention은 이런 중요한 프레임에 더 많은 비중을 두어 분석합니다. 이 과정에서 모델은 각 프레임에서 추출된 특징들을 종합하고, 의미 있는 프레임은 강조하고 덜 중요한 프레임은 억제하여 최종적인 표현을 생성합니다. 이러한 방식은 긴 동영상에서도 핵심 장면을 효과적으로 포착할 수 있어, 행동 인식, 영상 요약, 이상행동 탐지 같은 응용에 특히 강력하게 활용됩니다.
3. WLASL(World-Level American Sign Language) 데이터셋
WLASL(World-Level American Sign Language) 데이터셋은 미국 수화를 단어 단위로 인식하기 위한 대규모 비디오 데이터셋으로, 수화 동작이 담긴 짧은 영상 클립들을 포함하고 있습니다. 이 데이터셋은 동일한 단어를 다양한 수화 사용자들이 수행한 영상을 제공하여 표현 방식의 다양성을 반영하며, 시계열적 동작과 손 모양, 움직임, 얼굴 표정 등 복합적인 비주얼 특징을 학습할 수 있도록 구성되어 있습니다. 따라서 WLASL은 수화 인식 모델, 제스처 인식, 비디오 기반 행동 인식 연구에 널리 활용되며, 실제 수화 번역 시스템이나 청각장애인을 위한 보조 기술 개발의 핵심 자원으로 쓰입니다.
!kaggle datasets download risangbaskoro/wlasl-processed
!unzip -q wlasl-processed.zip
import json
import os
import shutil
from PIL import Image
from tqdm import tqdm
import json
data_root = '.'
annotation_filename = 'WLASL_v0.3.json'
video_dir = os.path.join(data_root, 'videos')
with open(os.path.join(data_root, annotation_filename), 'r')as json_f:
annotations = json.load(json_f)
annotations[:2]
from collections import defaultdict
class_map = defaultdict(int)
for annot in annotations:
cls_cnt = 0
instances = annot['instances']
for instance in instances:
video_filename = f"{instance['video_id']}.mp4"
video_path = os.path.join(video_dir, video_filename)
if not os.path.exists(video_path):
continue
else:
cls_cnt += 1
class_map[annot['gloss']] += cls_cnt
class_list = []
class_cnt = []
for k, v in class_map.items():
class_list.append(k)
class_cnt.append(v)
print(f'Class {k} counts : {v}')
import matplotlib.pyplot as plt
plt.bar(class_list, class_cnt, color='blue')
plt.title('Class Count Visualization')
plt.xlabel('Class Number')
plt.ylabel('Count of Classes')
plt.show()
max_cnt = 14
new_annotations = []
class_list = []
class_cnt = []
for annot in annotations:
cls_cnt = class_map[annot['gloss']]
if cls_cnt < max_cnt:
continue
else:
class_list.append(annot['gloss'])
class_cnt.append(cls_cnt)
for instance in annot['instances']:
video_filename = f"{instance['video_id']}.mp4"
video_path = os.path.join(video_dir, video_filename)
if not os.path.exists(video_path):
continue
new_annotations.append(
{
'filename' : f"{instance['video_id']}.mp4",
'label': annot['gloss'],
'split': instance['split']
}
)
print(new_annotations[:3])
print(f"데이터 개수 : {len(new_annotations)}")
print(f"클래스 개수 : {len(class_list)}")
print(f"클래스 목록 : {class_list}")
annotations = new_annotations
import torch
import torch.nn.functional as F
from torch.utils.data import Dataset
from PIL import Image
import cv2
from PIL import Image
import numpy as np
import random
class VideoDataset(Dataset):
def __init__(self,
video_root,
annotations,
class_list,
transform=None):
self.video_root = video_root
self.annotations = annotations
self.transform = transform
self.class_list = class_list # 클래스의 목록
self.num_classes = len(self.class_list)
def __len__(self):
return len(self.annotations)
def __getitem__(self, idx):
annot = self.annotations[idx]
filename = annot['filename']
label = annot['label']
video_path = os.path.join(self.video_root, filename)
cap = cv2.VideoCapture(video_path)
num_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
num_to_select = 12
if num_frames < num_to_select:
max_frame_idx = num_frames - 1
frame_idxs = [i for i in range(num_frames)]
frame_idxs += [max_frame_idx for i in range(num_to_select - num_frames)]
else:
frame_idxs = np.sort(np.random.choice(num_frames, size=num_to_select, replace=False))
frames = []
for frame_idx in frame_idxs:
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
ret, frame = cap.read()
if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frame = self._crop_to_square(frame)
else:
frame = Image.new('RGB', (224, 224), (0, 0, 0))
frame = np.array(frame)
# print('frame read error')
if self.transform:
frame = self.transform(image=frame)['image']
frames.append(frame)
cap.release()
if self.transform:
frames = torch.stack(frames)
label = class_list.index(label)
target = torch.tensor(label, dtype=torch.long)
return frames, target
def _crop_to_square(self, image):
height, width = image.shape[:2]
center_x, center_y = width // 2, height // 2
if width > height:
new_width = new_height = height
else:
new_width = new_height = width
left_x = center_x - new_width // 2
top_y = center_y - new_height // 2
cropped_image = image[top_y:top_y + new_height, left_x:left_x + new_width]
return cropped_image
import matplotlib.pyplot as plt
def draw_frames(images):
fig, axs = plt.subplots(2, 6, figsize=(12, 6))
for i, ax in enumerate(axs.flat):
if i >= len(images):
break
ax.imshow(images[i])
plt.tight_layout()
plt.show()
dataset = VideoDataset(annotations=annotations, video_root=video_dir, class_list=class_list)
data = dataset[0]
draw_frames(data[0])
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
def draw_images(images, classes):
fig, axs = plt.subplots(2, 6, figsize=(12, 6))
for i, ax in enumerate(axs.flat):
if i >= len(images):
break
ax.imshow(images[i])
ax.set_title(classes[i])
plt.tight_layout()
plt.show()
from PIL import Image
import numpy as np
import random
from glob import glob
random.shuffle(annotations)
dataset = VideoDataset(video_root=video_dir, annotations=annotations, class_list=class_list)
sample_images = []
sample_classes = []
sample_cnt = 0
max_cnt = 1
images, label = dataset[0]
label = class_list[int(label)]
label = [label for i in range(len(images))]
draw_images(images, label)
annotations[0]
random.shuffle(annotations)
train_annot = []
val_annot = []
val_cls_check = {}
for annot in annotations:
if annot['label'] in val_cls_check:
train_annot.append(annot)
continue
else:
val_annot.append(annot)
val_cls_check[annot['label']] = 1
print(f'학습 데이터 개수 : {len(train_annot)}')
print(f'검증 데이터 개수 : {len(val_annot)}')
hyper_params = {
'num_epochs': 8,
'lr': 0.0001,
'image_size': 224,
'train_batch_size': 12,
'val_batch_size': 8,
'print_preq': 0.1
}
import albumentations as A
from albumentations.pytorch import ToTensorV2
sample_transform = A.Compose([
# A.ShiftScaleRotate(rotate_limit=20, shift_limit=0.1, scale_limit=0.05, p=0.5, border_mode=0),
# A.ColorJitter(p=0.5),
# A.RandomBrightnessContrast(p=0.5),
A.LongestMaxSize(max_size=hyper_params['image_size'],
always_apply=True),
A.PadIfNeeded(min_height=hyper_params['image_size'],
min_width=hyper_params['image_size'],
always_apply=True,
border_mode=0),
A.Normalize(p=1.0, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
ToTensorV2()
])
sample_dataset = VideoDataset(video_root=video_dir,
annotations=annotations,
class_list=class_list,
transform=sample_transform)
import torch
from torchvision.transforms.functional import to_pil_image
def denormalize(tensor, mean, std):
mean = torch.tensor(mean).reshape(-1, 1, 1)
std = torch.tensor(std).reshape(-1, 1, 1)
tensor = tensor * std + mean
tensor = torch.clamp(tensor, 0, 1)
return tensor
def tensor_to_pil(tensor, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)):
tensor = denormalize(tensor, mean, std)
pil_image = to_pil_image(tensor)
return pil_image
transformed_images = []
targets = []
max_cnt = 1
target_classes = []
class_list = sample_dataset.class_list
for idx, (images, target) in enumerate(sample_dataset):
if idx == max_cnt:
break
for image in images:
transformed_images.append(tensor_to_pil(image))
target_classes.append(class_list[int(target)])
draw_images(transformed_images, target_classes)
train_transform = A.Compose([
# A.ShiftScaleRotate(rotate_limit=20, shift_limit=0.1, scale_limit=0.05, p=0.5, border_mode=0),
# A.ColorJitter(p=0.5),
# A.RandomBrightnessContrast(p=0.5),
A.LongestMaxSize(max_size=hyper_params['image_size'],
always_apply=True),
A.PadIfNeeded(min_height=hyper_params['image_size'],
min_width=hyper_params['image_size'],
always_apply=True,
border_mode=0),
A.Normalize(p=1.0, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ## 이미지 픽셀 값 정규화
ToTensorV2() ## 모델에 입력할 때 사용
])
val_transform = A.Compose([
A.LongestMaxSize(max_size=hyper_params['image_size'],
always_apply=True),
A.PadIfNeeded(min_height=hyper_params['image_size'],
min_width=hyper_params['image_size'],
always_apply=True,
border_mode=0),
A.Normalize(p=1.0, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
ToTensorV2()
])
train_dataset = VideoDataset(video_root=video_dir,
annotations=train_annot,
class_list=class_list,
transform=train_transform)
train_dataloader = torch.utils.data.DataLoader(train_dataset, num_workers=2, batch_size=hyper_params['train_batch_size'], shuffle=True)
val_dataset = VideoDataset(video_root=video_dir,
annotations=val_annot,
class_list=class_list,
transform=val_transform)
val_dataloader = torch.utils.data.DataLoader(val_dataset, num_workers=2, batch_size=hyper_params['val_batch_size'], shuffle=True)
from torchvision import models
import torch.nn as nn
model = models.video.r3d_18(pretrained=True)
model.fc = nn.Linear(512, len(class_list))
model
import torch
import torch.nn as nn
import torch.optim as optim
def calculate_accuracy(y_true, y_pred):
y_true = np.array(y_true)
y_pred = np.array(y_pred)
correct = (y_pred == y_true)
accuracy = correct.sum() / correct.size
return accuracy
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=hyper_params['lr'])
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
num_epochs = hyper_params['num_epochs']
model_save_dir = './WLASL_train_results'
os.makedirs(model_save_dir, exist_ok=True)
best_acc = 0.0
for epoch in range(num_epochs):
model.train()
running_loss = 0.0
epoch_loss = 0.0
print_every = max(1, int(len(train_dataloader) * hyper_params['print_preq']))
for idx, (images, targets) in enumerate(train_dataloader):
images, targets = images.to(device), targets.to(device)
### images : [배치, 프레임개수, 채널, 높이, 너비 ] => [배치, 채널, 프레임개수, 높이, 너비 ]
images = images.permute(0, 2, 1, 3, 4)
outputs = model(images)
loss = criterion(outputs, targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()
running_loss += loss.item()
epoch_loss += loss.item()
if (idx + 1) % print_every == 0 or (idx + 1) == len(train_dataloader):
window = (idx % print_every) + 1
print(f"Epoch [{epoch+1}/{num_epochs}], "
f"Iter [{idx+1}/{len(train_dataloader)}] "
f"Loss: {running_loss / window:.4f}")
running_loss = 0.0
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {epoch_loss/len(train_dataloader):.4f}")
model.eval()
y_true, y_pred = [], []
val_loss_sum, val_n = 0.0, 0
with torch.no_grad():
for images, targets in val_dataloader:
images, targets = images.to(device), targets.to(device)
images = images.permute(0, 2, 1, 3, 4)
outputs = model(images)
val_loss_sum += criterion(outputs, targets).item() * targets.size(0)
val_n += targets.size(0)
_, preds = torch.max(outputs, 1)
y_true.extend(targets.cpu().numpy().tolist())
y_pred.extend(preds.cpu().numpy().tolist())
acc = calculate_accuracy(y_true, y_pred)
val_loss = val_loss_sum / max(val_n, 1)
print(f"val_loss: {val_loss:.4f} accuracy: {acc*100:.2f}")
if acc > best_acc:
model_save_path = os.path.join(model_save_dir, f'best_model.pth')
torch.save(model.state_dict(), model_save_path)
best_acc = acc
weight_path = 'WLASL_train_results/best_model.pth'
device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = models.video.r3d_18(pretrained=True)
model.fc = nn.Linear(512, len(class_list))
model.load_state_dict(torch.load(weight_path, map_location='cpu'))
model.to(device)
def crop_to_square(image):
height, width = image.shape[:2]
# 이미지의 중심 좌표
center_x, center_y = width // 2, height // 2
if width > height:
new_width = new_height = height
else:
new_width = new_height = width
left_x = center_x - new_width // 2
top_y = center_y - new_height // 2
cropped_image = image[top_y:top_y + new_height, left_x:left_x + new_width]
return cropped_image
import cv2
from glob import glob
video_path_list = list(glob(f"{video_dir}/*.mp4"))
video_path = random.sample(video_path_list, 1)[0]
cap = cv2.VideoCapture(video_path)
num_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
frame_idxs = np.sort(np.random.choice(num_frames-4, size=12, replace=False))
frames = []
for frame_idx in frame_idxs:
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
ret, frame = cap.read()
if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frame = crop_to_square(frame)
else:
frame = np.zeros((256, 256, 3))
print('frame read error')
frame = val_transform(image=frame)['image']
frames.append(frame)
frames = torch.stack(frames)
frames.shape
model.eval()
with torch.no_grad():
frames = frames.unsqueeze(0).permute(0, 2, 1, 3, 4)
output = model(frames.to(device))
# preds = F.softmax(output)[0].detach().cpu().numpy().tolist()
preds = F.softmax(output, dim=1)[0].detach().cpu().numpy().tolist()
for pred, cls in zip(preds, class_list):
print(f"{cls} : {pred*100:.2f}%")'인공지능 > 컴퓨터 비전' 카테고리의 다른 글
| Sokoto Coventry Fingerprint Dataset (0) | 2025.09.08 |
|---|---|
| ViT(Vistion Transformer) (0) | 2025.09.08 |
| 차량 파손 데이터셋 (1) | 2025.08.21 |
| Segmentation (4) | 2025.08.11 |
| 이안류 CCTV 데이터셋 (1) | 2025.08.11 |