오늘은 아래의 깃헙에서 테니스코트의 포인트지점을 학습하는 cnn만들기를 학습해본다.
GitHub - yastrebksv/TennisCourtDetector: Deep learning network for detecting tennis court
GitHub - yastrebksv/TennisCourtDetector: Deep learning network for detecting tennis court
Deep learning network for detecting tennis court. Contribute to yastrebksv/TennisCourtDetector development by creating an account on GitHub.
github.com
1. gdown 할 때
올바른 URL형식이어야 한다.

이걸 그대로 가져다 쓰면, 직접 다운로드할 수 있는 링크가 아니기 때문에 오류 발생
해결 방법은 --fuzzy옵션 추가하기
! gdown --fuzzy "https://drive.google.com/file/d/1lhAaeQCmk2y440PmagA0KmIVBIysVMwu/view?usp=drive_link"
unzip
!unzip "tennis_court_det_dataset.zip"
2. 파이토치 class 정의
파이토치에서느 데이터를 쉽게 다룰 수 있도록 Dataset이라는 클래스를 제공한다. 이 클래스를 상속받아, 우리가 원하는 데이터셋을 정의할 수 있다.
보통 이 클래스를 상속받아 만들 때는 세 가지 메서드를 정의한다.
#라이브러리 임포트
import torch
from torch.utils.data import Dataset, DataLoader
from torchvision import models, transforms
import json
import cv2
import numpy as np
#만약 그래픽카드 좋은게 있다면 cuda로 사용
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device
1) __init__
데이터셋을 초기화하는 코드로 파일을 불러오거나 디렉터리를 설정하는 부분이다.
class KeypointsDataset(Dataset):
def __init__(self, img_dir, data_file):
self.img_dir = img_dir
with open(data_file, "r") as f:
self.data= json.load(f)
self.transforms = transforms.Compose([
transforms.ToPILImage(),
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
KeypointsDataset 클래스는 Dataset을 상속하고 있음
Dataset는 파이토치의 Dataset클래스이다.
특히 transforms는 전치리를 위한 코드이다.
transforms.ToPILImage()는 openCV나 numpy배열 형태로 저장된 이미지를 PIL이미지로 변환한다.
PIL = python imaging library
transforms.resize((224,224))는 이미지 입력크기를 통일시키는거다.
다음으로 중요한 것은 totensor()와 normalize()이다. 이 두가지는 이미지를 컴퓨터가 이해할 수 있는 숫자로 바꾸는 작업이다.
첫 번째, transforms.ToTensor()는 이미지를 파이토치의 텐서형태로 변환한다.
이미지는 보통 rgb 3개의 색상채널로 나뉘어져 있는데, 각 픽셀은 0-255 범위의 숫자로 저장된다. 그런데 이 숫자는 컴퓨터가 계산하기에는 너무 큰 숫자인거지 그래서 각 값을 255로 나누어서 범위를 바꿔주는거다.
결국, 픽셀값이 [0,255] 사이의 값에서 [0,1]의 값으로 변환한다.
둘째, normalize() 정규화는 이미지 데이터를 더 균형 있게 만들어주기 위한 과정으로 값을 표준화한다.
transforms.Normalize(mean, std)는 평균과 표준편차를 이용해 정규화를 하는 것이다. 정규화를 하는 이유는 이미지마다 밝기와 색상이 다 다르기 때문에 같은 물건이리ㅏ도 조명에 따라 색이 다르게 보일 수 있다. 따라서 컴퓨터가 더 정확하게 학습하려면 데이터를 균형있게 조정해야 하기 때문에 모든 이미지의 색상값을 맞춰주는것이다.
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
여기서 각각의 숫자는 각 채널 rgb의 평균값과 표준편차값을 나타낸다. 이 숫자들은 imageNey데이터셋을 기준으로 정한 값이다.
2) __len__
데이터셋의 크기 반환하는 함수
def __len__(self):
return len(self.data)
3) __getitem__(self, idx)
데이터셋에서 idx번째 샘플을 가져오는 함수
def __getitem__(self, idx):
item = self.data[idx]
img = cv2.imread(f"{self.img_dir}/{item['id']}.png")
h,w = img.shape[:2]
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = self.transforms
kps = np.array(item['kps']),flatten()
kps = kps.astype(np.float32)
kps[::2]*=224.0/w
kps[1::2]*=224.0/h
return img, kps
flatten() 함수로 2d를 1d로 배열 바꿈
kps의 구조를 보면 아래와 같다.
kps = [34, 58, 100, 120, 150, 200] # (x1, y1, x2, y2, x3, y3)
[::2] 2개씩 건너뛰어 세기
[1::2] 1부터 2개씩 건너뛰어 세기
3. 파이토치 데이터 로더

train_dataset = KeypointsDataset("data/images", "data/data_train.json")
val_dataset = KeypointsDataset("data/images", "data/data_val.json")
train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=8, shuffle=True)
1) batch_size=8
한 번에 8개씩 데이터를 가져옴
2)shuffle-True 매 에폭마다 데이터를 랜덤하게 섞음
4. 모델생성
model = models.resnet50(pretrained=True)
model.fc = torch.nn.Linear(model.fc.in_features, 14*2)
models.resnet50(pretrained=True)
르넷50 모델을 불러오는 코드로, pretrained 된 학습 가중치를 가져온다.
르넷은 이미지 분류모델인데, 우리가 원하는 과제인 테니스장 키포인트검출 예측 문제를 풀기 위해서는 해당 모델을 튜닝해야 한다.
model.fc = torch.nn.Linear(model.fc.in_features, 14*2)
마지막 부분이 fc층인데, 이 층을 우리 문제에 맞게 변형한다.
model.fc.in_features 는 입력 뉴런 개수를 가져오는데 르넷50은 2048개이다. 해당 출력을 14*2로 바꾸고, 이걸 model.fc = torch.nn.Linear(2048, 14*2)에 넣으면 마지막층이 새로운 층으로 교체된다.
해당 모델을 살펴보자. 맨 마지막에 fc층(출력층)이 있다.
model.to(device)
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): Bottleneck(
(conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
(layer2): Sequential(
(0): Bottleneck(
(conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(3): Bottleneck(
(conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
(layer3): Sequential(
(0): Bottleneck(
(conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(3): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(4): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(5): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
(layer4): Sequential(
(0): Bottleneck(
(conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=2048, out_features=28, bias=True)
)
model = model.to(device)
5. deep learning 모델 학습
criterion = torch.nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
손실함수 loss function 계산 --> criterion
MSELoss()는 평균제곱오차
optimizer는가중치를 어떻게 업데이트할지 결정하는 역할. 여기서는 아담을 쓰는데 아담은 Adaptive moment estimation이라고 딥러닝에서 자주 쓰이는 효율적인 경사하강법 알고리즘이다. 아담은 모멘텀과 rmsprop을 조합한 방식이라 학습 속도가 빠르고 안정적이다.
epochs = 50
for epoch in range(epochs):
for i, (imgs, kps) in enumerate(train_loader):
imgs = imgs.to(device)
kps = kps.to(device)
optimizer.zero_grad()
outputs = model(imgs)
loss = criterion(outputs, kps)
loss.backward()
optimizer.step()
if i % 10 == 0:
print(f"Epoch {epoch}, iter {i}, loss: {loss.item()}")
epochs = 50 총 50회 반복
kps.to(device)
가져온 데이터를 device로 이동
만약에 device가 gpu일 때 등등 사용하기
zero_grad()는 기울기 값을 초기화. 이전의 학습 기울기를 없애기 위해서. 현재의 기울기를 초기화하는 함수이다. 모델의 가중치는 계속 업데이트되며 저장되면서도 옵티마이저의 상태(즉, 이전 업데이트 정보를 포함한 옵티마이저의 파라미터)는 사라지지 않는다.
outputs는 예측값
loss는 손실 결과. ouputs(예측결과), kps(실제결과) 이용한다.
backward()는 역전파
optimizer.step()으로 모델 가중치 업데이트
모델은 model(imgs)에서처럼 가중치를 업데이트하고, 학습을 통해 계속 모델의 파라미터가 변함.


6. 모델 저장하기
1) 모델 가중치와 편향값
torch.save(model.state_dict(), "Keypoints_model_30.pth")
모델을 다시 학습하는데는 시간이 오래 걸린다.
학습된 모델을 저장하면 다시 학습할 필요 없이 불러와서 바로 사용할 수 있다.
model.state_dict()는 모델의 가중치와 편향값만 저장
.pth확장자는 파이토치 모델 저장용
모델 다시 불러오기
model.load_state_dict(torch.load("Keypoints_model_30.pth"))
2) 옵티마이저
torch.save(optimizer.state_dict(), "optimizer_30.pth")
optimizer.load_state_dict(torch.load("optimizer_30.pth"))
현재 작업 디렉토리에 저장된다.
'머신러닝 > 영상인식' 카테고리의 다른 글
영상인식 기초, 기존 모델 가져와서 적용, ball detection, interpolate(), get() (0) | 2025.03.16 |
---|---|
영상인식 기초, pickle 저장하기, yolo11x.pt, player검출하기, __init__.py 역할 (0) | 2025.03.16 |
영상인식, roboflow, 데이터라벨링, 가지고 있는 데이터를 이용해 모델 만들기 (1) | 2025.03.09 |
YOLO 기초, 영상 인식, 영상에서 사물인식하기 yolo11x, yolo11n (0) | 2025.03.08 |
depth pro, YOLO11, 거리재기 (0) | 2025.03.07 |