우리가 이 중 가장 집중한 항목은 볼 소유율이다.
풋살과 가장 비슷한 스포츠인 축구에서는 분석 항목에 필수적으로 들어가는 것이 점유율이기 때문이다.
점유율의 정확한 정의는 다음과 같다
점유율
팀 패스수 / (팀 패스 수 + 상대팀 패스 수)
실제 경기에서 패스 수를 세는 방법은, 각 팀의 패스를 직접 카운트 하는 사람이 있다고 한다.
이는 아마추어 경기에서는 사실상 이뤄지기 어려운 작업이라고 생각했고, 이를 자동화하여 유의미한 피드백을 주고 싶었던 것이 프로젝트의 목적이다.
그러나 1대1 간의 정확한 패스 수 카운트 구현을 마쳤음에도, 드론으로 전체적인 경기장을 찍은 2차원 영상 위에서는 다양한 이유로 정확한 패스를 감지해낼 수 없었다. 이는 패스수를 통해 점유율을 감지하는 것이 유의미한 숫자가 나올 수 없을 것이라는 판단에 이르렀다.
그래서 그 대안으로 각 팀이 볼을 소유한 시간을 기준으로 소유율을 나타내고자 새로 정의하였다.
⭐ 우리가 정의한 볼 소유율
각 팀이 볼을 소유한 시간의 비율
팀 볼 소유 시간 / (팀 볼 소유 시간 + 상대팀 볼 소유 시간)
유튜브에 나와있는 적절한 영상을 이용해 [yello_team, red_team, ball]의 데이터셋을 학습시켰다. 그 과정은 이전 졸업 프로젝트 기술 블로그와 같으니 생략하고 넘어가겠다.
[Deep Learning] Yolov5를 이용한 선수, 공 추적 프로젝트
출처 : https://www.youtube.com/watch?v=i_Wdb9Qd7Cc
class Team:
def __init__(self):
self.location_list = [] # 매 프레임 팀 좌표 리스트
self.all_location_list = [] # 경기중 모든 팀 좌표 리스트
def setLocation(self, list):
self.location_list.append(list) # 선수의 좌표 추가
def resetLocation(self):
self.location_list = [] # 좌표 리셋
# 저장해놓은 바운딩 박스 좌표 얻기
def getLeftX(self, index):
return self.location_list[index][0]
def getRightX(self, index):
return self.location_list[index][2]
def getUpY(self, index):
return self.location_list[index][3]
def getDownY(self, index):
return self.location_list[index][1] # 바운딩 박스 왼쪽 X 좌표 얻기
def callLocation(self, index):
return self.location_list[index]
def setAllLocation(self, list):
self.all_location_list.append(list)
def callAllLocation(self):
return self.all_location_list
class Ball:
def __init__(self):
self.location = []
def setLocation(self, list):
self.location.append(list) # 현재 위치
def resetLocation(self):
self.location = [] # 좌표 리셋
def getLeftX(self):
return self.location[0][0]
def getRightX(self):
return self.location[0][2]
def getUpY(self):
return self.location[0][3]
def getDownY(self):
return self.location[0][1]
def printLocation(self):
print(self.location)
team1 = Team()
team2 = Team()
ball = Ball()
nowTeam1 = "" # 현재 공을 가졌다고 판단되었는지 true or ""
nowTeam2 = ""
nowTeam = "" # 현재 공을 소유한 팀
team1_possession = 0 # 소유율
team2_possession = 0
이렇게 Team
과 Ball
을 정의해서 사용했다.
아래는 yolov5에 기본적으로 있는 detection 코드이다.
# Process detections
for i, det in enumerate(pred): # detections per image
seen += 1
if webcam: # nr_sources >= 1
p, im0, _ = path[i], im0s[i].copy(), dataset.count
p = Path(p) # to Path
s += f'{i}: '
txt_file_name = p.name
save_path = str(save_dir / p.name) # im.jpg, vid.mp4, ...
else:
p, im0, _ = path, im0s.copy(), getattr(dataset, 'frame', 0)
p = Path(p) # to Path
# video file
if source.endswith(VID_FORMATS):
txt_file_name = p.stem
save_path = str(save_dir / p.name) # im.jpg, vid.mp4, ...
# folder with imgs
else:
txt_file_name = p.parent.name # get folder name containing current img
save_path = str(save_dir / p.parent.name) # im.jpg, vid.mp4, ...
curr_frames[i] = im0
txt_path = str(save_dir / 'tracks' / txt_file_name) # im.txt
s += '%gx%g ' % im.shape[2:] # print string
imc = im0.copy() if save_crop else im0 # for save_crop
annotator = Annotator(im0, line_width=line_thickness, example=str(names))
if hasattr(tracker_list[i], 'tracker') and hasattr(tracker_list[i].tracker, 'camera_update'):
if prev_frames[i] is not None and curr_frames[i] is not None: # camera motion compensation
tracker_list[i].tracker.camera_update(prev_frames[i], curr_frames[i])
💻 아래는 내가 구현한 코드이다.
여기서 매 프레임마다 각 팀 선수와 공 좌표를 얻어서 아까 선언했던 객체에 넣어주는 작업을 했다.
if det is not None and len(det):
for c in det:
cls = int(c[5]) # 클래스
location = []
location.append(int(c[0])) # 왼쪽 x
location.append(int(c[1])) # 위쪽 y
location.append(int(c[2])) # 오른쪽 x
location.append(int(c[3])) # 아래쪽 y
# yello team일 때
if(cls == 0):
team1.setLocation(location)
# red team일 때
elif(cls == 1):
team2.setLocation(location)
# ball일 때
elif(cls == 2):
ball.setLocation(location)
아래는 얻어온 좌표를 이용해 볼 소유를 탐지하는 구현이다.
먼저 저장해둔 공의 좌표를 가져왔다.
가끔 인식 오류로 인해 인식한 공이 없거나 두개 이상인 경우에는 취급하지 않고 볼 소유를 탐지하지 않도록 했다
# 인식한 공이 하나일 때만 취급
if(len(ball.location) == 1):
# 저장해둔 공 좌표 얻기
B_LeftX = ball.getLeftX()
B_RightX = ball.getRightX()
B_UpY = ball.getUpY()
B_DownY = ball.getDownY()
# 볼 중앙 좌표 얻기
Ball_X = (B_LeftX + B_RightX) / 2
Ball_Y = (B_UpY + B_DownY) / 2
# 볼 바운딩 박스 넓이 얻기
ballWidth = B_RightX - B_LeftX
boundary = ballWidth
다음 각 팀 선수들의 좌표와 공의 좌표를 비교해서 볼 소유를 감지했다.
얻어놓은 볼 넓이를 boundary값으로 설정해 각 팀 선수들의 바운딩박스보다 boundary값만큼 더 확장된 영역에서 ball의 좌표가 겹쳐지는 경우 볼 소유로 판단했다.
그림으로 표현하면 다음과 같다.
볼 소유가 확정된 경우 히트맵 구현을 위해 볼을 소유한 선수의 좌표를 누적으로 저장시킨다.
# yellow team 선수들의 좌표와 공의 좌표 비교
for index_1 in range(0,len(team1.location_list)):
# 선수 좌표 얻기
LeftX = team1.getLeftX(index_1)
RightX = team1.getRightX(index_1)
UpY = team1.getUpY(index_1)
DownY = team1.getDownY(index_1)
# 선수가 공을 가졌는지 판단
if(LeftX-boundary < Ball_X and Ball_X < RightX+boundary) and (DownY-boundary < Ball_Y and Ball_Y < UpY+boundary):
nowTeam1 = "true"
nowTeam = "team1"
team1.setAllLocation([LeftX, UpY, RightX, DownY]) # 좌표 저장
# red team 선수들의 좌표와 공의 좌표 비교
### ...yello team과 같음...
볼 소유 판단이 끝난 후에는 해당 팀의 소유를 1 증가시킨다.
if(not(nowTeam1 == "true" and nowTeam2 == "true") and (nowTeam1 == "true" or nowTeam2 == "true")):
if(nowTeam == "team1"):
team1_possession +=1
elif(nowTeam == "team2"):
team2_possession +=1
매 프레임 볼 소유 탐지가 끝난 후에는 모든 좌표를 리셋해주었다.
# 공과 팀 선수들의 좌표 리셋
team1.resetLocation()
team2.resetLocation()
ball.resetLocation()
nowTeam1 = ""
nowTeam2 = ""
nowTeam = ""
볼을 소유한 팀의 좌표를 저장해둔 배열을 이용해 각 팀의 heatmap을 구현한다.
heatmap 구현은 다른 팀원이 해주었고, 나는 그 함수를 불러 사용하는 식으로 연결했다.
print("team1_possession result : ", team1_possession)
print("team2_possession result : ", team2_possession)
HF.heatmap("team1", team1.callAllLocation()) # team1 히트맵
HF.heatmap("team2", team2.callAllLocation()) # team2 히트맵
해당 코드들을 깃허브에 올려놓았고, 텐센트 클라우드의 가상환경에서 돌릴 수 있도록 연결해 놓았다.
실행 명령어는 다음과 같다.
python trackPass.py --yolo-weight {학습시킨 모델} --source {영상 경로}
팀원들과 직접 소유 시간을 측정해보았고, 이를 자동 분석 결과와 비교해보았다.
자동 분석
team1 소유율 : 31%
team2 소유율 : 69%
수동 분석
team1 소유율 : 35%
team2 소유율 : 65%
😀!! 예상했던 것보다 높은 정확도가 나와서 놀랐다.
이를 통해 유의미한 결과를 낼 수 있다는 결론을 얻었다.
처음 다뤄보는 컴퓨터 비전, 딥러닝에서 데이터 라벨링과 학습시키는 것부터, object detection을 하고 그 정보를 받아와 유의미한 정보를 얻어내는 과정이 쉽지만은 않았다. 원했던 구현을 하기 위한 레퍼런스도 많지 않았기에, 직접 구현을 해보면서 하나 하나 디벨롭해가는 과정이 길었다.
또, 아쉬운 점이 있다면 2D 상에서 이 모든 것을 구현해야 했기에, 구현할 수 있는 것에 대한 제약이 많았다. 그렇지만 "아마추어"를 위한 서비스로 "가성비"가 좋고, 필요한 핵심만 간단히 얻어낼 수 있는 것에 집중하고자 하여 최대한 유의미한 결과를 뽑아내고자 노력했다.
제대로 다뤄본 적이 없었던 딥러닝을 이렇게 진득하게 다뤄보면서, 재밌기도 했고 또 딥러닝 모델에 대한 이해도 해볼 수 있었던 유익한 시간이었던 것 같다.