Spaces:
Runtime error
Runtime error
Update trackers/tracker.py
Browse files- trackers/tracker.py +372 -364
trackers/tracker.py
CHANGED
@@ -1,364 +1,372 @@
|
|
1 |
-
from ultralytics import YOLO
|
2 |
-
import
|
3 |
-
import
|
4 |
-
import
|
5 |
-
import
|
6 |
-
import
|
7 |
-
import
|
8 |
-
|
9 |
-
from
|
10 |
-
from
|
11 |
-
|
12 |
-
|
13 |
-
from deep_sort_realtime.deepsort_tracker import DeepSort
|
14 |
-
|
15 |
-
|
16 |
-
class Tracker:
|
17 |
-
def __init__(self, model_path):
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
return
|
87 |
-
|
88 |
-
def
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
detections
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
#
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
self.
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
#
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from ultralytics import YOLO
|
2 |
+
import torch
|
3 |
+
import supervision as sv
|
4 |
+
import pickle
|
5 |
+
import os
|
6 |
+
import numpy as np
|
7 |
+
import pandas as pd
|
8 |
+
import cv2
|
9 |
+
from collections import defaultdict
|
10 |
+
from utils import get_center_of_bbox, get_bbox_width
|
11 |
+
from PIL import ImageFont, ImageDraw, Image
|
12 |
+
|
13 |
+
from deep_sort_realtime.deepsort_tracker import DeepSort
|
14 |
+
|
15 |
+
|
16 |
+
class Tracker:
|
17 |
+
def __init__(self, model_path):
|
18 |
+
# YOLO 모델 로드
|
19 |
+
self.model = YOLO(model_path)
|
20 |
+
|
21 |
+
# GPU가 가능하면 GPU로, 없으면 CPU로 이동 후 FP16 모드 적용 (메모리 절약)
|
22 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
23 |
+
self.model.to(device).half()
|
24 |
+
|
25 |
+
# DeepSORT 초기화
|
26 |
+
self.tracker = DeepSort(
|
27 |
+
max_age=30,
|
28 |
+
n_init=3,
|
29 |
+
nms_max_overlap=1.0,
|
30 |
+
max_cosine_distance=0.2,
|
31 |
+
nn_budget=None,
|
32 |
+
override_track_class=None,
|
33 |
+
embedder="mobilenet",
|
34 |
+
half=True,
|
35 |
+
bgr=True
|
36 |
+
)
|
37 |
+
|
38 |
+
self.previous_ball_owner = None
|
39 |
+
self.previous_team = None
|
40 |
+
self.commentary = ""
|
41 |
+
self.frame_count = 0
|
42 |
+
|
43 |
+
# 예전처럼 NanumGothic 사용
|
44 |
+
self.default_font = ImageFont.truetype("fonts/NanumGothic.ttf", 32)
|
45 |
+
|
46 |
+
def draw_text_centered(self, frame, text, font_size=32, y_offset=150, font_path="fonts/NanumGothic.ttf"):
|
47 |
+
if not text:
|
48 |
+
return frame
|
49 |
+
|
50 |
+
text = text.replace('"', '').replace("\n", " ") # 한 줄로
|
51 |
+
|
52 |
+
# 폰트 및 텍스트 크기
|
53 |
+
font = self.default_font
|
54 |
+
bbox = font.getbbox(text)
|
55 |
+
text_width = bbox[2] - bbox[0]
|
56 |
+
text_height = bbox[3] - bbox[1]
|
57 |
+
|
58 |
+
img_height, img_width, _ = frame.shape
|
59 |
+
x = (img_width - text_width) // 2
|
60 |
+
y = img_height - y_offset
|
61 |
+
|
62 |
+
# 그리기
|
63 |
+
img_pil = Image.fromarray(frame)
|
64 |
+
draw = ImageDraw.Draw(img_pil)
|
65 |
+
draw.text((x, y), text, font=font, fill=(255, 255, 255))
|
66 |
+
|
67 |
+
return np.asarray(img_pil)
|
68 |
+
|
69 |
+
|
70 |
+
def add_positions_to_tracks(self, tracks):
|
71 |
+
for current_frame in range(len(tracks['players'])):
|
72 |
+
for object_type, object_tracks in tracks.items():
|
73 |
+
if current_frame < len(object_tracks):
|
74 |
+
frame_tracks = object_tracks[current_frame]
|
75 |
+
if isinstance(frame_tracks, dict):
|
76 |
+
for track_id, track_info in frame_tracks.items():
|
77 |
+
bbox = track_info['bbox']
|
78 |
+
position = get_center_of_bbox(bbox)
|
79 |
+
track_info['position'] = position
|
80 |
+
|
81 |
+
def interpolate_ball_positions(self, ball_positions):
|
82 |
+
ball_positions = [x.get(1, {}).get('bbox', [np.nan]*4) for x in ball_positions]
|
83 |
+
df_ball_positions = pd.DataFrame(ball_positions, columns=['x1', 'y1', 'x2', 'y2'])
|
84 |
+
df_ball_positions = df_ball_positions.interpolate().bfill()
|
85 |
+
ball_positions = [{1: {"bbox": x.tolist()}} for x in df_ball_positions.to_numpy()]
|
86 |
+
return ball_positions
|
87 |
+
|
88 |
+
def detect_frames(self, frames):
|
89 |
+
batch_size = 20
|
90 |
+
detections = []
|
91 |
+
for i in range(0, len(frames), batch_size):
|
92 |
+
detections_batch = self.model.predict(frames[i:i + batch_size], conf=0.1)
|
93 |
+
detections += detections_batch
|
94 |
+
return detections
|
95 |
+
|
96 |
+
def get_object_tracks(self, frames, read_from_stub=False, stub_path=None):
|
97 |
+
if read_from_stub and stub_path and os.path.exists(stub_path):
|
98 |
+
with open(stub_path, 'rb') as f:
|
99 |
+
tracks = pickle.load(f)
|
100 |
+
return tracks
|
101 |
+
|
102 |
+
detections = self.detect_frames(frames)
|
103 |
+
tracks = {
|
104 |
+
"players": [],
|
105 |
+
"referees": [],
|
106 |
+
"ball": []
|
107 |
+
}
|
108 |
+
|
109 |
+
for frame_num, detection in enumerate(detections):
|
110 |
+
cls_names = detection.names
|
111 |
+
cls_names_inv = {v: k for k, v in cls_names.items()}
|
112 |
+
detection_supervision = sv.Detections.from_ultralytics(detection)
|
113 |
+
|
114 |
+
# 골키퍼를 플레이어로 통합
|
115 |
+
for idx, class_id in enumerate(detection_supervision.class_id):
|
116 |
+
if cls_names[class_id] == "goalkeeper":
|
117 |
+
detection_supervision.class_id[idx] = cls_names_inv["player"]
|
118 |
+
|
119 |
+
# 현재 프레임의 감지 결과를 저장할 딕셔너리
|
120 |
+
current_frame_tracks = {
|
121 |
+
"players": {},
|
122 |
+
"referees": {},
|
123 |
+
"ball": {}
|
124 |
+
}
|
125 |
+
|
126 |
+
# 각 감지된 객체에 대해 처리
|
127 |
+
for idx in range(len(detection_supervision.xyxy)):
|
128 |
+
bbox = detection_supervision.xyxy[idx].tolist()
|
129 |
+
cls_id = detection_supervision.class_id[idx]
|
130 |
+
cls_name = cls_names[cls_id]
|
131 |
+
position = get_center_of_bbox(bbox)
|
132 |
+
|
133 |
+
# 이전 프레임의 트랙들과 비교하여 매칭
|
134 |
+
matched_track_id = self.match_with_previous_tracks(position, cls_name)
|
135 |
+
|
136 |
+
# 매칭된 트랙 ID가 없으면 새로운 트랙으로 추가
|
137 |
+
if matched_track_id is None:
|
138 |
+
matched_track_id = self.next_track_id
|
139 |
+
self.next_track_id += 1
|
140 |
+
|
141 |
+
# 트랙 히스토리 업데이트
|
142 |
+
self.track_history[matched_track_id] = {
|
143 |
+
"position": position,
|
144 |
+
"bbox": bbox,
|
145 |
+
"last_seen": frame_num
|
146 |
+
}
|
147 |
+
|
148 |
+
# 현재 프레임 트랙에 추가
|
149 |
+
if cls_name == 'player':
|
150 |
+
current_frame_tracks["players"][matched_track_id] = {"bbox": bbox}
|
151 |
+
elif cls_name == 'referee':
|
152 |
+
current_frame_tracks["referees"][matched_track_id] = {"bbox": bbox}
|
153 |
+
elif cls_name == 'ball':
|
154 |
+
current_frame_tracks["ball"][1] = {"bbox": bbox} # 볼은 항상 ID 1로 유지
|
155 |
+
|
156 |
+
# 오래된 트랙 제거 (예: 30프레임 이상 감지되지 않은 트랙)
|
157 |
+
self.remove_stale_tracks(frame_num, max_age=30)
|
158 |
+
|
159 |
+
# 트랙 결과 저장
|
160 |
+
tracks["players"].append(current_frame_tracks["players"])
|
161 |
+
tracks["referees"].append(current_frame_tracks["referees"])
|
162 |
+
tracks["ball"].append(current_frame_tracks["ball"])
|
163 |
+
|
164 |
+
if stub_path:
|
165 |
+
with open(stub_path, 'wb') as f:
|
166 |
+
pickle.dump(tracks, f)
|
167 |
+
|
168 |
+
return tracks
|
169 |
+
|
170 |
+
def match_with_previous_tracks(self, position, cls_name, max_distance=50):
|
171 |
+
"""
|
172 |
+
현재 감지된 객체를 이전 트랙들과 비교하여 매칭
|
173 |
+
"""
|
174 |
+
min_distance = float('inf')
|
175 |
+
matched_track_id = None
|
176 |
+
|
177 |
+
for track_id, track_info in self.track_history.items():
|
178 |
+
if track_info.get('class_name') != cls_name:
|
179 |
+
continue
|
180 |
+
prev_position = track_info['position']
|
181 |
+
distance = np.linalg.norm(np.array(position) - np.array(prev_position))
|
182 |
+
if distance < min_distance and distance < max_distance:
|
183 |
+
min_distance = distance
|
184 |
+
matched_track_id = track_id
|
185 |
+
|
186 |
+
return matched_track_id
|
187 |
+
|
188 |
+
def remove_stale_tracks(self, current_frame_num, max_age=30):
|
189 |
+
"""
|
190 |
+
오래된 트랙을 제거하여 메모리 관리 및 트랙 ID 재사용 방지
|
191 |
+
"""
|
192 |
+
stale_track_ids = []
|
193 |
+
for track_id, track_info in self.track_history.items():
|
194 |
+
if current_frame_num - track_info['last_seen'] > max_age:
|
195 |
+
stale_track_ids.append(track_id)
|
196 |
+
for track_id in stale_track_ids:
|
197 |
+
del self.track_history[track_id]
|
198 |
+
|
199 |
+
def update_ball_owner(self, player_id, team_id):
|
200 |
+
if self.previous_ball_owner is None:
|
201 |
+
self.commentary = f"Player {player_id} has the ball."
|
202 |
+
elif player_id != self.previous_ball_owner:
|
203 |
+
if team_id != self.previous_team:
|
204 |
+
self.commentary = f"플레이어 {self.previous_ball_owner}이 공을 뺏겼습니다. 플레이어 {player_id}가 공을 소유중입니다.\n Player {self.previous_ball_owner} lost the ball. Player {player_id} now has it."
|
205 |
+
else:
|
206 |
+
self.commentary = f"플레이어 {self.previous_ball_owner}가 플레이어 {player_id}에게 패스를 하였습니다.\n Player {self.previous_ball_owner} passed the ball to {player_id}."
|
207 |
+
self.previous_ball_owner = player_id
|
208 |
+
self.previous_team = team_id
|
209 |
+
|
210 |
+
def draw_text(self, frame, text, y_position, font_size=32, color=(255, 255, 255), font_path="fonts/NotoSansCJKkr-Regular.ttf"):
|
211 |
+
img_pil = Image.fromarray(frame)
|
212 |
+
draw = ImageDraw.Draw(img_pil)
|
213 |
+
|
214 |
+
try:
|
215 |
+
font = ImageFont.truetype(font_path, font_size)
|
216 |
+
except OSError:
|
217 |
+
print(f"⚠️ 폰트 로드 실패: {font_path} → 기본 폰트로 대체")
|
218 |
+
font = ImageFont.load_default()
|
219 |
+
|
220 |
+
# 깨지는 문자 제거 (선택)
|
221 |
+
text = text.replace('"', '')
|
222 |
+
|
223 |
+
text_bbox = draw.textbbox((0, 0), text, font=font)
|
224 |
+
text_width = text_bbox[2] - text_bbox[0]
|
225 |
+
x_position = (frame.shape[1] - text_width) // 2
|
226 |
+
draw.text((x_position, y_position), text, font=font, fill=color)
|
227 |
+
|
228 |
+
return np.array(img_pil)
|
229 |
+
|
230 |
+
def draw_annotations(self, video_frames, tracks, team_ball_control, subtitle_texts, event_texts):
|
231 |
+
output_video_frames = []
|
232 |
+
for frame_num, frame in enumerate(video_frames):
|
233 |
+
frame = frame.copy()
|
234 |
+
|
235 |
+
player_dict = tracks["players"][frame_num]
|
236 |
+
ball_dict = tracks["ball"][frame_num]
|
237 |
+
referee_dict = tracks["referees"][frame_num]
|
238 |
+
|
239 |
+
# 플레이어 그리기
|
240 |
+
for track_id, player in player_dict.items():
|
241 |
+
color = player.get("team_color", (0, 0, 255))
|
242 |
+
frame = self.draw_ellipse(frame, player["bbox"], color, track_id)
|
243 |
+
|
244 |
+
if player.get('has_ball', False):
|
245 |
+
frame = self.draw_triangle(frame, player["bbox"], (0, 0, 255))
|
246 |
+
|
247 |
+
# 속도 표시
|
248 |
+
speed = player.get('speed', 0)
|
249 |
+
position = player.get('position', (0, 0))
|
250 |
+
|
251 |
+
# 심판 그리기
|
252 |
+
for track_id, referee in referee_dict.items():
|
253 |
+
frame = self.draw_ellipse(frame, referee["bbox"], (0, 255, 255), track_id)
|
254 |
+
|
255 |
+
# 볼 그리기
|
256 |
+
for track_id, ball in ball_dict.items():
|
257 |
+
frame = self.draw_triangle(frame, ball["bbox"], (0, 255, 0))
|
258 |
+
|
259 |
+
# 팀 볼 컨트롤 그리기
|
260 |
+
frame = self.draw_team_ball_control(frame, frame_num, np.array(team_ball_control))
|
261 |
+
|
262 |
+
# 자막 (GPT 생성) 먼저 그리기 - 위쪽 위치
|
263 |
+
frame = self.draw_subtitle(frame, subtitle_texts[frame_num] if frame_num < len(subtitle_texts) else "")
|
264 |
+
|
265 |
+
# 이벤트 자막은 아래쪽 위치
|
266 |
+
frame = self.draw_event_subtitle(frame, event_texts[frame_num] if frame_num < len(event_texts) else "")
|
267 |
+
|
268 |
+
|
269 |
+
output_video_frames.append(frame)
|
270 |
+
|
271 |
+
return output_video_frames
|
272 |
+
|
273 |
+
def draw_subtitle(self, frame, subtitle_text):
|
274 |
+
return self.draw_text_centered(frame, subtitle_text, y_offset=160)
|
275 |
+
|
276 |
+
def draw_event_subtitle(self, frame, event_text):
|
277 |
+
return self.draw_text_centered(frame, event_text, y_offset=100)
|
278 |
+
|
279 |
+
|
280 |
+
def draw_triangle(self, frame, bbox, color):
|
281 |
+
y = int(bbox[1])
|
282 |
+
x, _ = get_center_of_bbox(bbox)
|
283 |
+
|
284 |
+
triangle_points = np.array([
|
285 |
+
[x, y],
|
286 |
+
[x - 10, y - 20],
|
287 |
+
[x + 10, y - 20],
|
288 |
+
])
|
289 |
+
cv2.drawContours(frame, [triangle_points], 0, color, cv2.FILLED)
|
290 |
+
cv2.drawContours(frame, [triangle_points], 0, (0, 0, 0), 2)
|
291 |
+
|
292 |
+
return frame
|
293 |
+
|
294 |
+
def draw_ellipse(self, frame, bbox, color, track_id=None):
|
295 |
+
y2 = int(bbox[3])
|
296 |
+
x_center, _ = get_center_of_bbox(bbox)
|
297 |
+
width = get_bbox_width(bbox)
|
298 |
+
|
299 |
+
cv2.ellipse(
|
300 |
+
frame,
|
301 |
+
center=(x_center, y2),
|
302 |
+
axes=(int(width), int(0.35 * width)),
|
303 |
+
angle=0.0,
|
304 |
+
startAngle=-45,
|
305 |
+
endAngle=235,
|
306 |
+
color=color,
|
307 |
+
thickness=2,
|
308 |
+
lineType=cv2.LINE_4
|
309 |
+
)
|
310 |
+
|
311 |
+
rectangle_width = 40
|
312 |
+
rectangle_height = 20
|
313 |
+
x1_rect = x_center - rectangle_width // 2
|
314 |
+
x2_rect = x_center + rectangle_width // 2
|
315 |
+
y1_rect = (y2 - rectangle_height // 2) + 15
|
316 |
+
y2_rect = (y2 + rectangle_height // 2) + 15
|
317 |
+
|
318 |
+
if track_id is not None:
|
319 |
+
cv2.rectangle(frame,
|
320 |
+
(int(x1_rect), int(y1_rect)),
|
321 |
+
(int(x2_rect), int(y2_rect)),
|
322 |
+
color,
|
323 |
+
cv2.FILLED)
|
324 |
+
|
325 |
+
x1_text = x1_rect + 12
|
326 |
+
if track_id > 99:
|
327 |
+
x1_text -= 10
|
328 |
+
|
329 |
+
cv2.putText(
|
330 |
+
frame,
|
331 |
+
f"{track_id}",
|
332 |
+
(int(x1_text), int(y1_rect + 15)),
|
333 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
334 |
+
0.6,
|
335 |
+
(0, 0, 0),
|
336 |
+
2
|
337 |
+
)
|
338 |
+
|
339 |
+
return frame
|
340 |
+
|
341 |
+
def draw_team_ball_control(self, frame, frame_num, team_ball_control, FPS=30):
|
342 |
+
# 반투명한 배경 박스 (왼쪽 상단)
|
343 |
+
overlay = frame.copy()
|
344 |
+
cv2.rectangle(overlay, (30, 30), (500, 110), (255, 255, 255), -1)
|
345 |
+
alpha = 0.4
|
346 |
+
cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0, frame)
|
347 |
+
|
348 |
+
# 현재 프레임까지의 팀 점유 데이터
|
349 |
+
team_ball_control_till_frame = team_ball_control[:frame_num + 1]
|
350 |
+
|
351 |
+
# 0(소유 불확실) 제외
|
352 |
+
valid_frames = team_ball_control_till_frame[team_ball_control_till_frame > 0]
|
353 |
+
|
354 |
+
# 아직 소유 팀이 안 잡혔으면 0% 표시
|
355 |
+
if len(valid_frames) == 0:
|
356 |
+
team_1_ratio = team_2_ratio = 0
|
357 |
+
else:
|
358 |
+
# 팀별 프레임 수
|
359 |
+
team_1_frames = np.sum(valid_frames == 1)
|
360 |
+
team_2_frames = np.sum(valid_frames == 2)
|
361 |
+
|
362 |
+
total_frames = team_1_frames + team_2_frames
|
363 |
+
team_1_ratio = team_1_frames / total_frames
|
364 |
+
team_2_ratio = team_2_frames / total_frames
|
365 |
+
|
366 |
+
# 점유율 표시
|
367 |
+
cv2.putText(frame, f"Team 1 Ball Control: {team_1_ratio * 100:.1f}%", (50, 70),
|
368 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
|
369 |
+
cv2.putText(frame, f"Team 2 Ball Control: {team_2_ratio * 100:.1f}%", (50, 100),
|
370 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
|
371 |
+
|
372 |
+
return frame
|