自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

冰球運(yùn)動(dòng)的AI科技感:用計(jì)算機(jī)視覺跟蹤球員

譯文 精選
人工智能
本文將介紹使用PyTorch、計(jì)算機(jī)視覺庫(kù)OpenCV和卷積神經(jīng)網(wǎng)絡(luò)(CNN)開發(fā)一個(gè)跟蹤冰球球員、球隊(duì)及其基本表現(xiàn)統(tǒng)計(jì)數(shù)據(jù)的YOLOv8x模型的全過程。

譯者 | 朱先忠

審校 | 重樓

引言

如今,我并沒有像我孩提夢(mèng)想的那樣成為一名冰球運(yùn)動(dòng)員;但是,從我小時(shí)候起,冰球就成為我生活的一部分。最近,我有機(jī)會(huì)在秘魯首都利馬舉行的第一屆冰球錦標(biāo)賽(3對(duì)3)中成為裁判的技術(shù)支持人員,并保存下來一些統(tǒng)計(jì)數(shù)據(jù)。當(dāng)然,這項(xiàng)工作的完滿完成還涉及到秘魯直排冰球協(xié)會(huì)(APHL)的努力和一次由友誼聯(lián)盟成功組織的友好訪問。

為了增加人工智能技術(shù)的應(yīng)用測(cè)試,我使用了PyTorch、計(jì)算機(jī)視覺技術(shù)和卷積神經(jīng)網(wǎng)絡(luò)(CNN)來構(gòu)建一個(gè)模型,該模型可以跟蹤球員和球隊(duì),并收集一些基本的表現(xiàn)統(tǒng)計(jì)數(shù)據(jù)。

本文旨在為設(shè)計(jì)和部署這一類模型提供一個(gè)快速指南。雖然這個(gè)模型還需要一些微調(diào),但我希望它能幫助任何人了解應(yīng)用于體育領(lǐng)域的計(jì)算機(jī)視覺的有趣世界。此外,我要感謝秘魯直排冰球協(xié)會(huì)(APHL)允許我在這個(gè)項(xiàng)目中使用比賽的40秒視頻樣本(你可以在項(xiàng)目的GitHub存儲(chǔ)庫(kù)中找到該視頻的輸入樣本)。

系統(tǒng)架構(gòu)

在著手開發(fā)這個(gè)項(xiàng)目之前,我做了一些研究,以找到一個(gè)基礎(chǔ)型框架,方便直接拿過來使用,從而避免“重新發(fā)明輪子”。我發(fā)現(xiàn),在使用計(jì)算機(jī)視覺追蹤球員方面,已經(jīng)存在很多與足球有關(guān)的有趣工作(這并不奇怪,因?yàn)樽闱蚴鞘澜缟献钍軞g迎的團(tuán)隊(duì)運(yùn)動(dòng))。然而,我沒有找到很多的與冰球有關(guān)的資源。還好,我發(fā)現(xiàn)Roboflow提供了一些有趣的預(yù)訓(xùn)練模型和數(shù)據(jù)集用于訓(xùn)練開發(fā)者自己的模型,但他們使用了托管模型技術(shù),從而導(dǎo)致會(huì)出現(xiàn)一些延遲問題,我將在文章稍后作進(jìn)一步的解釋。

最后,我按照本教程中解釋的基本原理和跟蹤方法,利用足球材料來讀取視頻幀并獲取單個(gè)跟蹤ID(如果你有興趣更好地了解一些基本的計(jì)算機(jī)視覺技術(shù),我建議你至少觀看本教程的前一個(gè)半小時(shí),教程地址:https://youtu.be/neBZ6huolkg?feature=shared)。

在完成跟蹤ID標(biāo)注后,我構(gòu)建出自己的開發(fā)線路。在閱讀本文時(shí),我們將看到該項(xiàng)目如何從一個(gè)簡(jiǎn)單的對(duì)象檢測(cè)任務(wù)演變?yōu)橐粋€(gè)完整的能夠檢測(cè)球員、團(tuán)隊(duì)并提供一些基本性能指標(biāo)的模型(文章中嵌入的所有從01到08編號(hào)的樣本片段,都是作者自己創(chuàng)建的)。

AI冰球應(yīng)用程序模型架構(gòu)

跟蹤機(jī)制

跟蹤機(jī)制是本文構(gòu)建模型的支柱部分。它確保視頻中每個(gè)檢測(cè)到的對(duì)象都被識(shí)別并分配一個(gè)唯一的標(biāo)識(shí)符,在每一幀中都保持這種標(biāo)識(shí)。具體說來,跟蹤機(jī)制的主要組成部分包括以下組件:

  • YOLO算法:這是一種強(qiáng)大的實(shí)時(shí)目標(biāo)檢測(cè)算法,最初于2015年在《You Only Look:Unified,real time object detection》一文中介紹。它在檢測(cè)大約80個(gè)預(yù)訓(xùn)練種類的速度和多功能性方面脫穎而出(值得注意的是,它也可以在自定義數(shù)據(jù)集上訓(xùn)練以檢測(cè)特定對(duì)象)。對(duì)于我們的使用場(chǎng)景來說,我們將依賴YOLOv8x,這是Ultralytics.com公司基于之前的YOLO版本構(gòu)建的計(jì)算機(jī)視覺模型。你可以在鏈接https://github.com/ultralytics/ultralytics處下載。
  • ByteTrack跟蹤器:要理解ByteTrack,我們必須先了解一下MOT(多對(duì)象跟蹤),它涉及跟蹤視頻序列中多個(gè)對(duì)象隨時(shí)間的移動(dòng),并將當(dāng)前幀中檢測(cè)到的對(duì)象與前一幀中的相應(yīng)對(duì)象鏈接起來。為了實(shí)現(xiàn)這一點(diǎn),我們將使用ByteTrack(在2021年的論文《ByteTrack:通過關(guān)聯(lián)每個(gè)檢測(cè)框進(jìn)行多目標(biāo)跟蹤》中介紹)。為了實(shí)現(xiàn)ByteTrack跟蹤器并為檢測(cè)到的對(duì)象分配跟蹤ID,我們將依賴基于Python的Supervision庫(kù)([譯者注]這是一款出色的開源的基于Python的計(jì)算機(jī)視覺低代碼工具,其設(shè)計(jì)初衷是為用戶提供一個(gè)便捷且高效的接口,用以處理數(shù)據(jù)集并直觀地展示檢測(cè)結(jié)果)。
  • OpenCV庫(kù):這是Python中用于各種計(jì)算機(jī)視覺任務(wù)的知名的開源庫(kù)。對(duì)于我們的使用場(chǎng)景來說,我們將依靠OpenCV庫(kù)實(shí)現(xiàn)可視化和注釋視頻幀,并為每個(gè)檢測(cè)到的對(duì)象添加邊界框和文本。

為了構(gòu)建我們的跟蹤機(jī)制,我們將從以下兩個(gè)步驟開始:

  • 使用ByteTrack部署YOLO模型來檢測(cè)對(duì)象(在我們的例子中是球員)并分配唯一的跟蹤ID。
  • 初始化字典,以便將對(duì)象軌跡存儲(chǔ)在pickle(pkl)文件中。這是非常有用的,因?yàn)檫@可以避免每次運(yùn)行代碼時(shí)都執(zhí)行逐幀視頻對(duì)象檢測(cè)過程,并節(jié)省大量時(shí)間。

我們需要安裝以下Python包:

pip install ultralytics
pip install supervision
pip install opencv-python

接下來,我們將指定我們的庫(kù)以及示例視頻文件和pickle文件的路徑(如果存在的話;如果不存在,代碼將創(chuàng)建一個(gè)新的文件并將其保存在相同的路徑中):

#**********************************庫(kù)*********************************#
from ultralytics import YOLO
import supervision as sv
import pickle
import os
import cv2

#輸入:視頻文件
video_path = 'D:/PYTHON/video_input.mp4'
#輸出:視頻文件
output_video_path = 'D:/PYTHON/output_video.mp4'
# PICKLE文件(如果存在的話;如果不存在,代碼將創(chuàng)建一個(gè)新的文件并將其保存在相同的路徑中)
pickle_path = 'D:/PYTHON/stubs/track_stubs.pkl'

現(xiàn)在,讓我們繼續(xù)定義我們的跟蹤機(jī)制(你可以在項(xiàng)目的GitHub存儲(chǔ)庫(kù)中找到此視頻輸入示例):

#*********************************跟蹤機(jī)制**************************#
class HockeyAnalyzer:
def __init__(self, model_path):
self.model = YOLO(model_path) 
self.tracker = sv.ByteTrack()

def detect_frames(self, frames):
batch_size = 20 
detections = [] 
for i in range(0, len(frames), batch_size):
detections_batch = self.model.predict(frames[i:i+batch_size], conf=0.1)
detections += detections_batch
return detections

#********從文件加載軌跡或檢測(cè)對(duì)象-保存PICKLE文件************#

def get_object_tracks(self, frames, read_from_stub=False, stub_path=None):
if read_from_stub and stub_path is not None and os.path.exists(stub_path):
with open(stub_path, 'rb') as f:
tracks = pickle.load(f)
return tracks

detections = self.detect_frames(frames)

tracks = {"person": []}

for frame_num, detection in enumerate(detections):
cls_names = detection.names
cls_names_inv = {v: k for k, v in cls_names.items()}

# 跟蹤機(jī)制
detection_supervision = sv.Detections.from_ultralytics(detection)
detection_with_tracks = self.tracker.update_with_detections(detection_supervision)
tracks["person"].append({})

for frame_detection in detection_with_tracks:
bbox = frame_detection[0].tolist()
cls_id = frame_detection[3]
track_id = frame_detection[4]

if cls_id == cls_names_inv.get('person', None):
tracks["person"][frame_num][track_id] = {"bbox": bbox}

for frame_detection in detection_supervision:
bbox = frame_detection[0].tolist()
cls_id = frame_detection[3]

if stub_path is not None:
with open(stub_path, 'wb') as f:
pickle.dump(tracks, f)

return tracks

#***********************邊界框與跟蹤ID**************************#

def draw_annotations(self, video_frames, tracks):
output_video_frames = []
for frame_num, frame in enumerate(video_frames):
frame = frame.copy() 
player_dict = tracks["person"][frame_num]

# 繪制球員
for track_id, player in player_dict.items():
color = player.get("team_color", (0, 0, 255))  
bbox = player["bbox"]
x1, y1, x2, y2 = map(int, bbox)         
# 邊界框
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
# Track_id 
cv2.putText(frame, str(track_id), (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, color, 2)

output_video_frames.append(frame)

return output_video_frames

上面的方法中,首先初始化YOLO模型和ByteTrack跟蹤器。接下來,每幀以20個(gè)為一批次進(jìn)行處理,使用YOLO模型檢測(cè)和收集每個(gè)批次中的對(duì)象。如果pickle文件在其路徑中可用,則它會(huì)從文件中預(yù)計(jì)算軌跡。如果pickle文件不可用(你是第一次運(yùn)行代碼或刪除了之前的pickle文件),get_object_tracks會(huì)將每個(gè)檢測(cè)轉(zhuǎn)換為ByteTrack所需的格式,用這些檢測(cè)更新跟蹤器,并將跟蹤信息存儲(chǔ)在指定路徑中的新pickle文件中。最后,對(duì)每一幀進(jìn)行迭代,為每個(gè)檢測(cè)到的對(duì)象繪制邊界框和跟蹤ID。

要執(zhí)行跟蹤器并保存帶有邊界框和跟蹤ID的新輸出視頻,可以使用以下代碼:

#*************** 執(zhí)行跟蹤機(jī)制并輸出視頻****************#

#讀取視頻幀
video_frames = []
cap = cv2.VideoCapture(video_path)
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
video_frames.append(frame)
cap.release()

#********************* 用YOLO執(zhí)行跟蹤方法**********************#
tracker = HockeyAnalyzer('D:/PYTHON/yolov8x.pt')
tracks = tracker.get_object_tracks(video_frames, read_from_stub=True, stub_path=pickle_path)
annotated_frames = tracker.draw_annotations(video_frames, tracks)

#***********************保存視頻文件************************************#
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
height, width, _ = annotated_frames[0].shape
out = cv2.VideoWriter(output_video_path, fourcc, 30, (width, height))

for frame in annotated_frames:
out.write(frame)
out.release()

如果代碼中的所有內(nèi)容都正常工作,你應(yīng)該會(huì)觀察到一個(gè)類似于示例剪輯01中所示的視頻輸出。

示例剪輯01:基本跟蹤機(jī)制(對(duì)象和跟蹤ID)

【Tip-01】不要低估你的計(jì)算機(jī)的計(jì)算能力!第一次運(yùn)行代碼時(shí),預(yù)計(jì)幀處理需要一些時(shí)間,這具體取決于你的計(jì)算機(jī)的計(jì)算能力。對(duì)我來說,只使用CPU設(shè)置需要45到50分鐘(考慮CUDA作為選項(xiàng))。YOLOv8x跟蹤機(jī)制雖然強(qiáng)大,但需要大量的計(jì)算資源(有時(shí),我的內(nèi)存達(dá)到99%,但愿它不會(huì)崩潰?。?。如果你在使用此版本的YOLO時(shí)遇到問題,Ultralytics的GitHub上提供了更輕量級(jí)的模型,以平衡準(zhǔn)確性和計(jì)算能力。

溜冰場(chǎng)

正如你從本文一開始所看到的,開發(fā)中我們面臨著不少挑戰(zhàn)。首先,正如預(yù)期的那樣,上面創(chuàng)建的模型拾取了所有運(yùn)動(dòng)物體——球員、裁判,甚至是溜冰場(chǎng)外的人。其次,那些紅色的邊界框可能會(huì)使跟蹤球員有點(diǎn)不清楚,也不利于演示。在本節(jié)中,我們將專注于將檢測(cè)范圍縮小到溜冰場(chǎng)內(nèi)的物體。此外,我們將把這些邊界框替換為底部的橢圓,以確保更清晰的可見性。

首先,讓我們先從使用方框切換到使用橢圓。為了實(shí)現(xiàn)這一點(diǎn),我們只需在現(xiàn)有代碼中的標(biāo)簽和邊界框上方添加一個(gè)新方法:

#************用橢圓代替邊界框來跟蹤球員的設(shè)計(jì)**************#

def draw_ellipse(self, frame, bbox, color, track_id=None, team=None):
y2 = int(bbox[3])
x_center = (int(bbox[0]) + int(bbox[2])) // 2
width = int(bbox[2]) - int(bbox[0])
color = (255, 0, 0)
text_color = (255, 255, 255)

cv2.ellipse(
frame,
center=(x_center, y2),
axes=(int(width) // 2, int(0.35 * width)),
angle=0.0,
startAngle=-45,
endAngle=235,
color=color,
thickness=2,
lineType=cv2.LINE_4
)

if track_id is not None:
rectangle_width = 40
rectangle_height = 20
x1_rect = x_center - rectangle_width // 2
x2_rect = x_center + rectangle_width // 2
y1_rect = (y2 - rectangle_height // 2) + 15
y2_rect = (y2 + rectangle_height // 2) + 15

cv2.rectangle(frame,
(int(x1_rect), int(y1_rect)),
(int(x2_rect), int(y2_rect)),
color,
cv2.FILLED)

x1_text = x1_rect + 12
if track_id > 99:
x1_text -= 10
font_scale = 0.4
cv2.putText(
frame,
f"{track_id}",
(int(x1_text), int(y1_rect + 15)),
cv2.FONT_HERSHEY_SIMPLEX,
font_scale,
text_color,
thickness=2
)

return frame

然后,我們還需要通過調(diào)用橢圓方法替換邊界框和ID來更新注釋步驟:

#***********************邊界框和跟蹤ID**************************#

def draw_annotations(self, video_frames, tracks):
output_video_frames = []
for frame_num, frame in enumerate(video_frames):
frame = frame.copy() 
player_dict = tracks["person"][frame_num]

# 繪制球員
for track_id, player in player_dict.items():
bbox = player["bbox"]

#繪制橢圓和跟蹤ID
self.draw_ellipse(frame, bbox, (0, 255, 0), track_id)

x1, y1, x2, y2 = map(int, bbox)

output_video_frames.append(frame)

return output_video_frames

通過上面這些更改,你的輸出視頻應(yīng)該看起來更整潔一些,如示例剪輯02所示。

示例剪輯02:用橢圓替換邊界框

現(xiàn)在,為了處理溜冰場(chǎng)的邊界,我們需要對(duì)計(jì)算機(jī)視覺中的分辨率有一些基本的了解。在我們的示例中,我們使用的是720p(1280x720像素)格式;這意味著,我們處理的每一幀或圖像的尺寸為1280像素(寬)乘720像素(高)。

使用720p(1280x720像素)格式意味著什么?這意味著,圖像由水平1280像素和垂直720像素組成。此格式的坐標(biāo)從圖像左上角的(0,0)開始,x坐標(biāo)隨著向右移動(dòng)而增加,y坐標(biāo)隨著向下移動(dòng)而增加。這些坐標(biāo)用于標(biāo)記圖像中的特定區(qū)域,例如將(x1,y1)用于框的左上角,將(x2,y2)用于框右下角。了解這一點(diǎn)將有助于我們測(cè)量距離和速度,并決定我們想在視頻中集中分析的位置。

也就是說,我們將使用以下代碼開始用綠線標(biāo)記幀邊界:

#********************* 幀的邊界定義***********************
import cv2

video_path = 'D:/PYTHON/video_input.mp4'
cap = cv2.VideoCapture(video_path)

#**************讀取、定義和繪制幀的角****************
ret, frame = cap.read()

bottom_left = (0, 720)
bottom_right = (1280, 720)
upper_left = (0, 0)
upper_right = (1280, 0)

cv2.line(frame, bottom_left, bottom_right, (0, 255, 0), 2)
cv2.line(frame, bottom_left, upper_left, (0, 255, 0), 2)
cv2.line(frame, bottom_right, upper_right, (0, 255, 0), 2)
cv2.line(frame, upper_left, upper_right, (0, 255, 0), 2)

#*******************保存帶有標(biāo)記角的幀*********************
output_image_path = 'rink_area_marked_VALIDATION.png'
cv2.imwrite(output_image_path, frame)
print("Rink area saved:", output_image_path)

結(jié)果應(yīng)該是一個(gè)綠色矩形,如示例片段03中的(a)所示。但是,為了只跟蹤溜冰場(chǎng)內(nèi)的運(yùn)動(dòng)物體,我們需要一個(gè)更類似于(b)中的劃界。

圖03:溜冰場(chǎng)的邊界定義

正確地得到(b)中的劃界就像一個(gè)反復(fù)試驗(yàn)的過程。在這個(gè)過程中,你需要測(cè)試不同的坐標(biāo),直到找到最適合你的模型的邊界。起初,我的目標(biāo)是精確地匹配溜冰場(chǎng)的邊界。然而,發(fā)現(xiàn)跟蹤系統(tǒng)在溜冰場(chǎng)的邊緣附近判別比較困難。為了提高準(zhǔn)確性,我稍微擴(kuò)大了一下邊界,以確保捕捉到溜冰場(chǎng)內(nèi)的所有跟蹤對(duì)象,同時(shí)排除場(chǎng)外的跟蹤對(duì)象。最后,如(b)中顯示的結(jié)果是我所能得到的最好的結(jié)果(你仍然可以在更好的情況下工作),這個(gè)邊界由下面這些關(guān)鍵坐標(biāo)定義:

  • 左下角:(-450,710)
  • 右下角:(2030,710)
  • 左上角:(352,61)
  • 右上角:(948,61)

最后,我們將定義另外兩個(gè)區(qū)域:白隊(duì)和黃隊(duì)的進(jìn)攻區(qū)(每支球隊(duì)的目標(biāo)都是得分)。這將使我們能夠收集對(duì)手區(qū)域內(nèi)每支球隊(duì)的一些基本位置統(tǒng)計(jì)數(shù)據(jù)和壓力指標(biāo)。

圖04:進(jìn)攻區(qū)

#**************黃隊(duì)進(jìn)攻區(qū)****************
Bottom Left Corner: (-450, 710)
Bottom Right Corner: (2030, 710)
Upper Left Corner: (200, 150)
Upper Right Corner: (1160, 150)

#**************白隊(duì)進(jìn)攻區(qū)****************
Bottom Left Corner: (180, 150)
Bottom Right Corner: (1100, 150)
Upper Left Corner: (352, 61)
Upper Right Corner: (900, 61)

我們將暫時(shí)擱置這些坐標(biāo),并在下一步解釋我們將如何對(duì)每個(gè)團(tuán)隊(duì)進(jìn)行分類。然后,我們將把它們整合到我們最初的跟蹤方法中。

使用深度學(xué)習(xí)進(jìn)行團(tuán)隊(duì)預(yù)測(cè)

自Warren McCulloch和Walter Pitts于1943年發(fā)表論文《神經(jīng)活動(dòng)中內(nèi)在思想的邏輯演算》(https://www.cs.cmu.edu/~./epxing/Class/10715/reading/McCulloch.and.Pitts.pdf)以來,已經(jīng)過去了80多年,該論文為早期神經(jīng)網(wǎng)絡(luò)研究奠定了堅(jiān)實(shí)的基礎(chǔ)。后來,在1957年,一個(gè)簡(jiǎn)化神經(jīng)元的數(shù)學(xué)模型(接收輸入,對(duì)這些輸入應(yīng)用權(quán)重,求和并輸出二進(jìn)制結(jié)果)啟發(fā)了Frank Rosenblatt構(gòu)建Mark I(https://news.cornell.edu/stories/2019/09/professors-perceptron-paved-way-ai-60-years-too-soon)。這是第一個(gè)旨在演示感知器概念的硬件實(shí)現(xiàn),感知器是一種能夠從數(shù)據(jù)中學(xué)習(xí)以進(jìn)行二進(jìn)制分類的神經(jīng)網(wǎng)絡(luò)模型。從那時(shí)起,讓計(jì)算機(jī)像我們一樣思考的探索就沒有放緩。如果這是你第一次深入學(xué)習(xí)神經(jīng)網(wǎng)絡(luò),或者你想更新和加強(qiáng)你的知識(shí),那么我建議你閱讀Shreya Rao的系列文章(https://medium.com/@shreya.rao/list/deep-learning-illustrated-ae6c27de1640),這可以作為深度學(xué)習(xí)的一個(gè)很好的起點(diǎn)。此外,你可以訪問我鏈接處https://medium.com/@raul.vizcarrach/list/neural-networks-098e9b594f19收集的故事集(來自于不同的投稿者),你可能會(huì)發(fā)現(xiàn)這些故事很有用。

那么,為什么選擇卷積神經(jīng)網(wǎng)絡(luò)(CNN)呢?老實(shí)說,這不是我的第一選擇。最初,我嘗試使用LandingAI構(gòu)建一個(gè)模型,LandingAI是一個(gè)用戶友好的云部署平臺(tái),通過API連接Python。然而,這一方案出現(xiàn)了延遲問題(在線處理超過1000幀)。盡管Roboflow中的預(yù)訓(xùn)練模型具有高質(zhì)量的數(shù)據(jù)集和預(yù)訓(xùn)練模型,但它們也出現(xiàn)了類似的延遲問題。意識(shí)到需要在本地運(yùn)行它,我嘗試了一種基于MSE的方法來對(duì)球衣顏色進(jìn)行分類,以便進(jìn)行球隊(duì)和裁判檢測(cè)。雖然這聽起來像是最終的解決方案,但它的準(zhǔn)確性很低。經(jīng)過幾天的反復(fù)試驗(yàn),我最終改用CNN。在不同的深度學(xué)習(xí)方法中,CNN非常適合對(duì)象檢測(cè),不像LSTM或RNN更適合語(yǔ)言轉(zhuǎn)錄或翻譯等順序數(shù)據(jù)。

在深入分析代碼之前,讓我們先了解一下CNN架構(gòu)的一些基本概念:

  • 學(xué)習(xí)樣本數(shù)據(jù)集:數(shù)據(jù)集分為三類:裁判、Team_Away(白色球衣球員)和Team_Home(黃色球衣球員)。每個(gè)類別的樣本被分為兩組:訓(xùn)練數(shù)據(jù)和驗(yàn)證數(shù)據(jù)。CNN將在每個(gè)訓(xùn)練輪次中使用訓(xùn)練數(shù)據(jù)來“學(xué)習(xí)”多層模式。驗(yàn)證數(shù)據(jù)將在每次迭代結(jié)束時(shí)用于評(píng)估模型的性能,并衡量其對(duì)新數(shù)據(jù)的泛化程度。創(chuàng)建樣本數(shù)據(jù)集并不難;我花了大約30到40分鐘從視頻中裁剪每個(gè)類別的示例圖像,并將其組織到子目錄中。我設(shè)法創(chuàng)建了一個(gè)大約90張圖片的示例數(shù)據(jù)集,你可以在項(xiàng)目的GitHub存儲(chǔ)庫(kù)中找到。
  • 模型是如何學(xué)習(xí)的?:輸入數(shù)據(jù)在神經(jīng)網(wǎng)絡(luò)的每一層中移動(dòng),神經(jīng)網(wǎng)絡(luò)可以有一層或多層連接在一起進(jìn)行預(yù)測(cè)。每一層都使用一個(gè)激活函數(shù)來處理數(shù)據(jù),以進(jìn)行預(yù)測(cè)或?qū)?shù)據(jù)進(jìn)行更改。這些層之間的每個(gè)連接都有一個(gè)權(quán)重,它決定了一個(gè)層的輸出對(duì)下一個(gè)層有多大的影響。目標(biāo)是找到這些權(quán)重的正確組合,以盡量減少預(yù)測(cè)結(jié)果時(shí)的錯(cuò)誤。通過稱為反向傳播和損失函數(shù)的過程,該模型調(diào)整這些權(quán)重以減少誤差并提高準(zhǔn)確性。這個(gè)過程在所謂的訓(xùn)練輪次(前向傳遞+反向傳播)中重復(fù),隨著模型從錯(cuò)誤中學(xué)習(xí),它在每個(gè)周期的預(yù)測(cè)能力越來越好。
  • 激活函數(shù):如前所述,激活函數(shù)在模型的學(xué)習(xí)過程中起著重要作用。我選擇了ReLU(校正線性單元)算法,因?yàn)樗杂?jì)算效率高和緩解所謂的消失梯度問題(多層網(wǎng)絡(luò)可能會(huì)有效地停止學(xué)習(xí))而聞名。雖然ReLU工作良好,但其他激活函數(shù)如sigmoid、tanh或swish等也各有其用途,具體取決于網(wǎng)絡(luò)的復(fù)雜程度。
  • 訓(xùn)練輪次(epoch):設(shè)定正確的訓(xùn)練輪次需要實(shí)驗(yàn)。你應(yīng)該考慮數(shù)據(jù)集的復(fù)雜性、CNN模型的架構(gòu)和計(jì)算資源等因素。在大多數(shù)情況下,最好在每次迭代中監(jiān)控模型的性能,并在改進(jìn)變得最小時(shí)停止訓(xùn)練,以防止過擬合??紤]到我的小訓(xùn)練數(shù)據(jù)集,我決定以10個(gè)訓(xùn)練輪次作為基線。然而,在其他情況下,可能需要根據(jù)指標(biāo)性能和驗(yàn)證結(jié)果進(jìn)行調(diào)整。
  • Adam(自適應(yīng)矩估計(jì)):最終,目標(biāo)是減少預(yù)測(cè)輸出和真實(shí)輸出之間的誤差。如前所述,反向傳播在這里起著關(guān)鍵作用,它通過調(diào)整和更新神經(jīng)網(wǎng)絡(luò)權(quán)重來隨著時(shí)間的推移改進(jìn)預(yù)測(cè)。雖然反向傳播基于損失函數(shù)的梯度處理權(quán)重更新,但Adam算法通過動(dòng)態(tài)調(diào)整學(xué)習(xí)率來逐步最小化誤差或損失函數(shù),從而增強(qiáng)了這一過程。換句話說,它可以微調(diào)模型的學(xué)習(xí)速度。

也就是說,為了運(yùn)行我們的CNN模型,我們需要以下Python包:

pip install torch torchvision 
pip install matplotlib 
pip install scikit-learn

【Tip-02】確保PyTorch安裝正確。我所有的工具都是在Anaconda環(huán)境中設(shè)置的,當(dāng)我安裝PyTorch時(shí),起初它似乎設(shè)置得很正確。然而,在運(yùn)行一些庫(kù)時(shí)卻出現(xiàn)了一些問題。起初,我以為這是代碼原因,但經(jīng)過幾次修改都沒有成功,最后我不得不重新安裝Anaconda并在干凈的環(huán)境中重新安裝了PyTorch。最終,問題就解決了!

接下來,我們將指定我們的庫(kù)和樣本數(shù)據(jù)集的路徑:

# ************卷積神經(jīng)網(wǎng)絡(luò)三類檢測(cè)**************************
# 裁判
# 白隊(duì)(Team_away)
# 黃隊(duì) (Team_home)

import os
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision.transforms as transforms
import torchvision.datasets as datasets
from torch.utils.data import DataLoader
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
import matplotlib.pyplot as plt

#培訓(xùn)和驗(yàn)證數(shù)據(jù)集
#從項(xiàng)目的GitHub存儲(chǔ)庫(kù)下載teams_sample_dataset文件
data_dir = 'D:/PYTHON/teams_sample_dataset'

首先,我們將確保每張圖片大小相等(調(diào)整為150x150像素),然后將其轉(zhuǎn)換為代碼可以理解的格式(在PyTorch中,輸入數(shù)據(jù)通常表示為Tensor對(duì)象)。最后,我們將調(diào)整顏色,使模型更容易使用(歸一化),并設(shè)置一個(gè)加載圖像的過程。這些步驟共同幫助準(zhǔn)備圖片并對(duì)其進(jìn)行組織,以便模型能夠有效地開始從中學(xué)習(xí),避免數(shù)據(jù)格式引起的偏差。

#******************************數(shù)據(jù)轉(zhuǎn)換***********************************
transform = transforms.Compose([
transforms.Resize((150, 150)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])

#加載數(shù)據(jù)集
train_dataset = datasets.ImageFolder(os.path.join(data_dir, 'train'), transform=transform)
val_dataset = datasets.ImageFolder(os.path.join(data_dir, 'val'), transform=transform)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)

接下來,我們將定義CNN的架構(gòu):

#********************************CNN模型架構(gòu)**************************************
class CNNModel(nn.Module):
def __init__(self):
super(CNNModel, self).__init__()
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
self.fc1 = nn.Linear(128 * 18 * 18, 512)
self.dropout = nn.Dropout(0.5)
self.fc2 = nn.Linear(512, 3)  #三個(gè)類(裁判,白隊(duì),黃隊(duì))

def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = self.pool(F.relu(self.conv3(x)))
x = x.view(-1, 128 * 18 * 18)
x = F.relu(self.fc1(x))
x = self.dropout(x)
x = self.fc2(x)  
return x

我們的CNN模型有三層(conv1、conv2、conv3)。數(shù)據(jù)從卷積層(conv)開始,在那里應(yīng)用了激活函數(shù)(ReLU)。此函數(shù)使網(wǎng)絡(luò)能夠?qū)W習(xí)數(shù)據(jù)中的復(fù)雜模型和關(guān)系。隨后,池化層被激活。什么是最大池化?這是一種在保留重要特征的同時(shí)減小圖像大小的技術(shù),有助于高效訓(xùn)練和優(yōu)化內(nèi)存資源。這個(gè)過程在conv1到conv3之間重復(fù)。最后,數(shù)據(jù)通過完全連接的層(fc1、fc2)進(jìn)行最終分類(或決策)。

下一步,我們初始化模型,將分類交叉熵配置為損失函數(shù)(通常用于分類任務(wù)),并指定Adam作為我們的優(yōu)化器。如前所述,我們將在10個(gè)訓(xùn)練輪次的完整周期內(nèi)執(zhí)行我們的模型。

#********************************CNN訓(xùn)練**********************************************

# 模型損失函數(shù)-優(yōu)化器
model = CNNModel()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

#*********************************訓(xùn)練*************************************************
num_epochs = 10
train_losses, val_losses = [], []

for epoch in range(num_epochs):
model.train()
running_loss = 0.0
for inputs, labels in train_loader:
optimizer.zero_grad()
outputs = model(inputs)
labels = labels.type(torch.LongTensor)  
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()

train_losses.append(running_loss / len(train_loader))

model.eval()
val_loss = 0.0
all_labels = []
all_preds = []
with torch.no_grad():
for inputs, labels in val_loader:
outputs = model(inputs)
labels = labels.type(torch.LongTensor)  
loss = criterion(outputs, labels)
val_loss += loss.item()
_, preds = torch.max(outputs, 1)  
all_labels.extend(labels.tolist())
all_preds.extend(preds.tolist())

為了跟蹤性能,我們將添加一些代碼來跟蹤訓(xùn)練進(jìn)度,打印驗(yàn)證指標(biāo)并繪制出來。最后,我們將模型保存為hockey_team_classifier.cth,并保存在你選擇的指定路徑中。

#********************************指標(biāo)性能************************************

val_losses.append(val_loss / len(val_loader))
val_accuracy = accuracy_score(all_labels, all_preds)
val_precision = precision_score(all_labels, all_preds, average='macro', zero_division=1)
val_recall = recall_score(all_labels, all_preds, average='macro', zero_division=1)
val_f1 = f1_score(all_labels, all_preds, average='macro', zero_division=1)

print(f"Epoch [{epoch + 1}/{num_epochs}], "
f"Loss: {train_losses[-1]:.4f}, "
f"Val Loss: {val_losses[-1]:.4f}, "
f"Val Acc: {val_accuracy:.2%}, "
f"Val Precision: {val_precision:.4f}, "
f"Val Recall: {val_recall:.4f}, "
f"Val F1 Score: {val_f1:.4f}")

#*******************************顯示指標(biāo) &性能**********************************
plt.plot(train_losses, label='Train Loss')
plt.plot(val_losses, label='Validation Loss')
plt.legend()
plt.show()

# 保存GH_CV_track_teams代碼的模型
torch.save(model.state_dict(), 'D:/PYTHON/hockey_team_classifier.pth')

此外,在運(yùn)行完上述所有步驟(你可以在項(xiàng)目的GitHub存儲(chǔ)庫(kù)中找到完整的代碼)后,除了你的“pth”文件外,你還應(yīng)該看到以下輸出(指標(biāo)可能略有不同):

圖05:CNN模型性能指標(biāo)

#**************CNN PERFORMANCE ACROSS TRAINING EPOCHS************************

Epoch [1/10], Loss: 1.5346, Val Loss: 1.2339, Val Acc: 47.37%, Val Precision: 0.7172, Val Recall: 0.5641, Val F1 Score: 0.4167
Epoch [2/10], Loss: 1.1473, Val Loss: 1.1664, Val Acc: 55.26%, Val Precision: 0.6965, Val Recall: 0.6296, Val F1 Score: 0.4600
Epoch [3/10], Loss: 1.0139, Val Loss: 0.9512, Val Acc: 57.89%, Val Precision: 0.6054, Val Recall: 0.6054, Val F1 Score: 0.5909
Epoch [4/10], Loss: 0.8937, Val Loss: 0.8242, Val Acc: 60.53%, Val Precision: 0.7222, Val Recall: 0.5645, Val F1 Score: 0.5538
Epoch [5/10], Loss: 0.7936, Val Loss: 0.7177, Val Acc: 63.16%, Val Precision: 0.6667, Val Recall: 0.6309, Val F1 Score: 0.6419
Epoch [6/10], Loss: 0.6871, Val Loss: 0.7782, Val Acc: 68.42%, Val Precision: 0.6936, Val Recall: 0.7128, Val F1 Score: 0.6781
Epoch [7/10], Loss: 0.6276, Val Loss: 0.5684, Val Acc: 78.95%, Val Precision: 0.8449, Val Recall: 0.7523, Val F1 Score: 0.7589
Epoch [8/10], Loss: 0.4198, Val Loss: 0.5613, Val Acc: 86.84%, Val Precision: 0.8736, Val Recall: 0.8958, Val F1 Score: 0.8653
Epoch [9/10], Loss: 0.3959, Val Loss: 0.3824, Val Acc: 92.11%, Val Precision: 0.9333, Val Recall: 0.9213, Val F1 Score: 0.9243
Epoch [10/10], Loss: 0.2509, Val Loss: 0.2651, Val Acc: 97.37%, Val Precision: 0.9762, Val Recall: 0.9792, Val F1 Score: 0.9769

在完成10個(gè)迭代周期后,CNN模型的性能指標(biāo)有所改善。最初,在第一個(gè)訓(xùn)練輪次中,該模型的訓(xùn)練損失為1.5346,驗(yàn)證準(zhǔn)確率為47.37%。我們應(yīng)該如何理解這種初始訓(xùn)練結(jié)果呢?

準(zhǔn)確性是評(píng)估分類性能的最常見指標(biāo)之一。在我們的例子中,它代表了正確預(yù)測(cè)的類別在總數(shù)中所占的比例。然而,僅靠高精度并不能保證整體模型性能;你仍然可能對(duì)特定類別的預(yù)測(cè)得到很差的預(yù)測(cè)結(jié)果(正如我在早期試驗(yàn)中所經(jīng)歷的那樣)。關(guān)于訓(xùn)練損失函數(shù),它衡量模型學(xué)習(xí)將輸入數(shù)據(jù)映射到正確標(biāo)簽的有效性。由于我們使用的是分類函數(shù),交叉熵?fù)p失量化了預(yù)測(cè)的類別概率和實(shí)際標(biāo)簽之間的差異。像1.5346這樣的起始值表示預(yù)測(cè)類別和實(shí)際類別之間存在顯著差異;理想情況下,隨著訓(xùn)練的進(jìn)行,該值應(yīng)接近0。隨著時(shí)間的推移,我們觀察到訓(xùn)練損失顯著下降,驗(yàn)證準(zhǔn)確性提高。到最后一個(gè)訓(xùn)練輪次,訓(xùn)練和驗(yàn)證損失分別達(dá)到0.2509和0.2651的低點(diǎn)。

為了測(cè)試我們的CNN模型,我們可以選擇一個(gè)球員圖像樣本并評(píng)估其預(yù)測(cè)能力。為了進(jìn)行測(cè)試,你可以運(yùn)行以下代碼并使用項(xiàng)目GitHub存儲(chǔ)庫(kù)中的validation_dataset文件夾。

# *************用樣本數(shù)據(jù)集測(cè)試CNN模型***************************

import os
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as transforms
from PIL import Image

# 用于驗(yàn)證的樣本數(shù)據(jù)集
test_dir = 'D:/PYTHON/validation_dataset'

# 團(tuán)隊(duì)預(yù)測(cè)的CNN模型
class CNNModel(nn.Module):
def __init__(self):
super(CNNModel, self).__init__()
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
self.fc1 = nn.Linear(128 * 18 * 18, 512)
self.dropout = nn.Dropout(0.5)
self.fc2 = nn.Linear(512, 3) 

def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = self.pool(F.relu(self.conv3(x)))
x = x.view(-1, 128 * 18 * 18)
x = F.relu(self.fc1(x))
x = self.dropout(x)
x = self.fc2(x)  
return x

# 之前保存的CNN模型
model = CNNModel()
model.load_state_dict(torch.load('D:/PYTHON/hockey_team_classifier.pth'))
model.eval()

transform = transforms.Compose([
transforms.Resize((150, 150)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])

#******************樣本圖像的迭代精度檢驗(yàn)*****************************

class_names = ['team_referee', 'team_away', 'team_home']

def predict_image(image_path, model, transform):
# 加載數(shù)據(jù)集
image = Image.open(image_path)
image = transform(image).unsqueeze(0)  

# 進(jìn)行預(yù)測(cè)
with torch.no_grad():
output = model(image)
_, predicted = torch.max(output, 1)  
team = class_names[predicted.item()]
return team

for image_name in os.listdir(test_dir):
image_path = os.path.join(test_dir, image_name)
if os.path.isfile(image_path):  
predicted_team = predict_image(image_path, model, transform)
print(f'Image {image_name}: The player belongs to {predicted_team}')

輸出應(yīng)該看起來像下面這樣:

# *************CNN MODEL TEST - OUTPUT ***********************************#

Image Away_image04.jpg: The player belongs to team_away
Image Away_image12.jpg: The player belongs to team_away
Image Away_image14.jpg: The player belongs to team_away
Image Home_image07.jpg: The player belongs to team_home
Image Home_image13.jpg: The player belongs to team_home
Image Home_image16.jpg: The player belongs to team_home
Image Referee_image04.jpg: The player belongs to team_referee
Image Referee_image09.jpg: The player belongs to team_referee
Image Referee_image10.jpg: The player belongs to team_referee
Image Referee_image11.jpg: The player belongs to team_referee

正如你所看到的,該模型在識(shí)別球隊(duì)和排除裁判作為團(tuán)隊(duì)球員方面表現(xiàn)出了很好的能力。

【提示03】我在CNN設(shè)計(jì)過程中學(xué)到的一點(diǎn)是,增加復(fù)雜性并不總是能提高性能。最初,我嘗試了更深層次的模型(更多的卷積層)和基于顏色的增強(qiáng),以提高球員的球衣識(shí)別率。然而,在我的小數(shù)據(jù)集中,我遇到了過擬合,而不是學(xué)習(xí)可泛化的特征(所有圖像都被預(yù)測(cè)為白人球隊(duì)球員或裁判)。像dropout和批歸一化這樣的正則化技術(shù)也很重要;它們有助于在訓(xùn)練過程中施加約束,確保模型能夠很好地泛化到新數(shù)據(jù)。就結(jié)果而言,更少有時(shí)意味著更多。

組合應(yīng)用

將所有上面這些技術(shù)放在一起應(yīng)用時(shí)需要對(duì)前面描述的跟蹤機(jī)制進(jìn)行一些調(diào)整。下面描述更新代碼的逐步分解過程。

首先,我們將設(shè)置所需的庫(kù)和路徑。請(qǐng)注意,現(xiàn)在指定了pickle文件和CNN模型的路徑。這一次,如果在路徑中找不到pickle文件,代碼將拋出錯(cuò)誤。如果需要,使用前面的代碼生成pickle文件,并使用此更新版本執(zhí)行視頻分析:

import cv2
import numpy as np
from ultralytics import YOLO
import pickle
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as transforms
from PIL import Image

# 模型輸入
model_path = 'D:/PYTHON/yolov8x.pt'
video_path = 'D:/PYTHON/video_input.mp4'
output_path = 'D:/PYTHON/output_video.mp4'
tracks_path = 'D:/PYTHON/stubs/track_stubs.pkl'
classifier_path = 'D:/PYTHON/hockey_team_classifier.pth'

接下來,我們將加載模型,指定溜冰場(chǎng)坐標(biāo),并像以前一樣,以20個(gè)為一批啟動(dòng)檢測(cè)每幀中對(duì)象的過程。請(qǐng)注意,目前,我們將只使用溜冰場(chǎng)邊界來將分析重點(diǎn)放在溜冰場(chǎng)上。在本文的最后步驟中,當(dāng)我們包含性能統(tǒng)計(jì)數(shù)據(jù)時(shí),我們將使用進(jìn)攻區(qū)域坐標(biāo)。

#***************************加載模型和溜冰場(chǎng)坐標(biāo)********************#
class_names = ['Referee', 'Tm_white', 'Tm_yellow']

class HockeyAnalyzer:
def __init__(self, model_path, classifier_path):
self.model = YOLO(model_path)
self.classifier = self.load_classifier(classifier_path)
self.transform = transforms.Compose([
transforms.Resize((150, 150)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])
self.rink_coordinates = np.array([[-450, 710], [2030, 710], [948, 61], [352, 61]])
self.zone_white = [(180, 150), (1100, 150), (900, 61), (352, 61)]
self.zone_yellow = [(-450, 710), (2030, 710), (1160, 150), (200, 150)]

#********************檢測(cè)每幀中的對(duì)象 **********************************#
def detect_frames(self, frames):
batch_size = 20 
detections = [] 
for i in range(0, len(frames), batch_size):
detections_batch = self.model.predict(frames[i:i+batch_size], conf=0.1)
detections += detections_batch
return detections

接下來,我們將添加預(yù)測(cè)每個(gè)球員球隊(duì)的過程:

#*********************** 加載CNN模型**********************************************#

def load_classifier(self, classifier_path):
model = CNNModel()
model.load_state_dict(torch.load(classifier_path, map_location=torch.device('cpu')))
model.eval()
return model

def predict_team(self, image):
with torch.no_grad():
output = self.classifier(image)
_, predicted = torch.max(output, 1)
predicted_index = predicted.item()
team = class_names[predicted_index]
return team

下一步,我們將添加前面描述的方法,從邊界框切換到橢圓:

#************ 使用橢圓來跟蹤球員,而不是邊界框*******************#
def draw_ellipse(self, frame, bbox, color, track_id=None, team=None):
y2 = int(bbox[3])
x_center = (int(bbox[0]) + int(bbox[2])) // 2
width = int(bbox[2]) - int(bbox[0])

if team == 'Referee':
color = (0, 255, 255)
text_color = (0, 0, 0)
else:
color = (255, 0, 0)
text_color = (255, 255, 255)

cv2.ellipse(
frame,
center=(x_center, y2),
axes=(int(width) // 2, int(0.35 * width)),
angle=0.0,
startAngle=-45,
endAngle=235,
color=color,
thickness=2,
lineType=cv2.LINE_4
)

if track_id is not None:
rectangle_width = 40
rectangle_height = 20
x1_rect = x_center - rectangle_width // 2
x2_rect = x_center + rectangle_width // 2
y1_rect = (y2 - rectangle_height // 2) + 15
y2_rect = (y2 + rectangle_height // 2) + 15

cv2.rectangle(frame,
(int(x1_rect), int(y1_rect)),
(int(x2_rect), int(y2_rect)),
color,
cv2.FILLED)

x1_text = x1_rect + 12
if track_id > 99:
x1_text -= 10
font_scale = 0.4
cv2.putText(
frame,
f"{track_id}",
(int(x1_text), int(y1_rect + 15)),
cv2.FONT_HERSHEY_SIMPLEX,
font_scale,
text_color,
thickness=2
)

return frame

現(xiàn)在,是時(shí)候添加分析器了,包括讀取pickle文件,將分析范圍縮小到我們之前定義的溜冰場(chǎng)邊界內(nèi),并調(diào)用CNN模型來識(shí)別每個(gè)球員的球隊(duì)并添加標(biāo)簽。請(qǐng)注意,我們提供了一個(gè)函數(shù),可以用不同的顏色標(biāo)記裁判,并更改其橢圓的顏色。在代碼的最后部分,將處理后的幀寫入輸出視頻。

#*******************加載跟蹤的數(shù)據(jù) (pickle 文件)**********************************#

def analyze_video(self, video_path, output_path, tracks_path):
with open(tracks_path, 'rb') as f:
tracks = pickle.load(f)

cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print("Error: Could not open video.")
return

fps = cap.get(cv2.CAP_PROP_FPS)
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter(output_path, fourcc, fps, (frame_width, frame_height))

frame_num = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break

#***********檢查球員是否落入溜冰場(chǎng)區(qū)域**********************************#
mask = np.zeros(frame.shape[:2], dtype=np.uint8)
cv2.fillConvexPoly(mask, self.rink_coordinates, 1)
mask = mask.astype(bool)
# 繪制溜冰場(chǎng)邊緣
#cv2.polylines(frame, [self.rink_coordinates], isClosed=True, color=(0, 255, 0), thickness=2)

# 從幀中獲取跟蹤信息
player_dict = tracks["person"][frame_num]
for track_id, player in player_dict.items():
bbox = player["bbox"]

# 檢查球員是否落入溜冰場(chǎng)區(qū)域
x_center = int((bbox[0] + bbox[2]) / 2)
y_center = int((bbox[1] + bbox[3]) / 2)

if not mask[y_center, x_center]:
continue  

#**********************************球隊(duì)預(yù)測(cè)********************************************#
x1, y1, x2, y2 = map(int, bbox)
cropped_image = frame[y1:y2, x1:x2]
cropped_pil_image = Image.fromarray(cv2.cvtColor(cropped_image, cv2.COLOR_BGR2RGB))
transformed_image = self.transform(cropped_pil_image).unsqueeze(0)
team = self.predict_team(transformed_image)

#************ 使用橢圓追蹤球員和標(biāo)簽******************************************#
self.draw_ellipse(frame, bbox, (0, 255, 0), track_id, team)

font_scale = 1  
text_offset = -20  

if team == 'Referee':
rectangle_width = 60
rectangle_height = 25
x1_rect = x1
x2_rect = x1 + rectangle_width
y1_rect = y1 - 30
y2_rect = y1 - 5
#針對(duì)裁判使用不同設(shè)置
cv2.rectangle(frame,
(int(x1_rect), int(y1_rect)),
(int(x2_rect), int(y2_rect)),
(0, 0, 0),  
cv2.FILLED)
text_color = (255, 255, 255)  
else:
if team == 'Tm_white':
text_color = (255, 215, 0)  #白隊(duì):藍(lán)色標(biāo)簽
else:
text_color = (0, 255, 255)  #黃隊(duì):黃色標(biāo)簽

#繪制球隊(duì)標(biāo)簽
cv2.putText(
frame,
team,
(int(x1), int(y1) + text_offset), 
cv2.FONT_HERSHEY_PLAIN,            
font_scale,
text_color,
thickness=2
)

# 寫輸出視頻
out.write(frame)
frame_num += 1

cap.release()
out.release()

最后,我們實(shí)現(xiàn)CNN的架構(gòu)(在CNN設(shè)計(jì)過程中定義)并執(zhí)行Hockey分析器:

#**********************CNN模型架構(gòu)******************************#
class CNNModel(nn.Module):
def __init__(self):
super(CNNModel, self).__init__()
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
self.fc1 = nn.Linear(128 * 18 * 18, 512)
self.dropout = nn.Dropout(0.5)
self.fc2 = nn.Linear(512, len(class_names))  

def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = self.pool(F.relu(self.conv3(x)))
x = x.view(-1, 128 * 18 * 18)
x = F.relu(self.fc1(x))
x = self.dropout(x)
x = self.fc2(x)
return x

#*********執(zhí)行HockeyAnalyzer/分類器并保存輸出***********#
analyzer = HockeyAnalyzer(model_path, classifier_path)
analyzer.analyze_video(video_path, output_path, tracks_path)

運(yùn)行所有步驟后,你的視頻輸出應(yīng)該如下所示:

示例剪輯06:跟蹤球員和團(tuán)隊(duì)

請(qǐng)注意,在最后一次更新中,物體檢測(cè)僅在溜冰場(chǎng)內(nèi)進(jìn)行,球隊(duì)和裁判都是不同的。雖然CNN模型仍需要微調(diào),偶爾會(huì)對(duì)一些球員失去穩(wěn)定性,但它在整個(gè)視頻中仍然基本可靠和準(zhǔn)確。

速度、距離和進(jìn)攻壓力

實(shí)驗(yàn)結(jié)果證明,本示例開發(fā)的模型方案在跟蹤球隊(duì)和球員信息方面展現(xiàn)出強(qiáng)大能力,這為衡量各種指標(biāo)的應(yīng)用開辟了令人興奮的可能性,例如生成熱圖、分析速度和覆蓋距離、跟蹤區(qū)域入口或出口等動(dòng)作,甚至還可以更深入地了解詳細(xì)的球員指標(biāo)。不妨讓我們作一個(gè)測(cè)試,我們添加三個(gè)性能指標(biāo):每位球員的平均速度、每支球隊(duì)的滑冰距離和進(jìn)攻壓力(以每支球隊(duì)在對(duì)手區(qū)域內(nèi)投入的距離值百分比來衡量)。我會(huì)把更詳細(xì)的統(tǒng)計(jì)數(shù)據(jù)留給你!

最開始,我們將溜冰場(chǎng)的坐標(biāo)從基于像素的測(cè)量值調(diào)整為基于米單位。這種調(diào)整使我們能夠以米而不是像素讀取數(shù)據(jù)。視頻中看到的溜冰場(chǎng)的真實(shí)尺寸約為15mx30m(寬15米,高30米)。為了便于這種轉(zhuǎn)換,我們引入了一種將像素坐標(biāo)轉(zhuǎn)換為米的方法。通過定義溜冰場(chǎng)的實(shí)際尺寸并使用其角的像素坐標(biāo)(從左到右和從上到下),我們獲得了轉(zhuǎn)換因子。這些因素將支持我們估算以米為單位的距離和以米每秒為單位的速度的過程。(你可以探索和應(yīng)用的另一種有趣的技術(shù)是透視變換)

#*********************加載模型和溜冰場(chǎng)坐標(biāo)*****************#
class_names = ['Referee', 'Tm_white', 'Tm_yellow']

class HockeyAnalyzer:
def __init__(self, model_path, classifier_path):
*
*
*
*
*
*
self.pixel_to_meter_conversion() #<------ 添加這個(gè)工具方法

#***********將溜冰場(chǎng)的坐標(biāo)從基于像素的測(cè)量值調(diào)整為基于米單位***************************#
def pixel_to_meter_conversion(self):
#溜冰場(chǎng)實(shí)際尺寸(單位:米)
rink_width_m = 15
rink_height_m = 30

#溜冰場(chǎng)尺寸的像素坐標(biāo)
left_pixel, right_pixel = self.rink_coordinates[0][0], self.rink_coordinates[1][0]
top_pixel, bottom_pixel = self.rink_coordinates[2][1], self.rink_coordinates[0][1]

#轉(zhuǎn)換因子
self.pixels_per_meter_x = (right_pixel - left_pixel) / rink_width_m
self.pixels_per_meter_y = (bottom_pixel - top_pixel) / rink_height_m

def convert_pixels_to_meters(self, distance_pixels):
#把像素轉(zhuǎn)換成米
return distance_pixels / self.pixels_per_meter_x, distance_pixels / self

我們現(xiàn)在準(zhǔn)備為每個(gè)球員增加速度,以米每秒為單位。為此,我們需要進(jìn)行三處修改。首先,在HockeyAnalyzer類中啟動(dòng)一個(gè)名為previous_positions的空字典,以幫助我們比較球員的當(dāng)前和先前位置。同樣,我們將創(chuàng)建一個(gè)team_stats結(jié)構(gòu)來存儲(chǔ)每個(gè)團(tuán)隊(duì)的統(tǒng)計(jì)數(shù)據(jù),以便進(jìn)一步可視化。

接下來,我們將添加一種速度方法來估計(jì)球員的速度(以每秒像素為單位),然后使用轉(zhuǎn)換因子(前面解釋過)將其轉(zhuǎn)換為每秒米數(shù)。最后,根據(jù)analyze_video方法,我們將調(diào)用新的速度方法,并將速度添加到每個(gè)被跟蹤的對(duì)象(球員和裁判)中。下面給出的是修改后的代碼:

#*********************加載模型和溜冰場(chǎng)坐標(biāo)*****************#
class_names = ['Referee', 'Tm_white', 'Tm_yellow']

class HockeyAnalyzer:
def __init__(self, model_path, classifier_path):
*
*
*
*
*
*
*
self.pixel_to_meter_conversion() 
self.previous_positions = {} #<------初始化空字典 
self.team_stats = {
'Tm_white': {'distance': 0, 'speed': [], 'count': 0, 'offensive_pressure': 0},
'Tm_yellow': {'distance': 0, 'speed': [], 'count': 0, 'offensive_pressure': 0}
} #<------ 初始化空字典

#**************** 速度:米每秒********************************#
def calculate_speed(self, track_id, x_center, y_center, fps):
current_position = (x_center, y_center)
if track_id in self.previous_positions:
prev_position = self.previous_positions[track_id]
distance_pixels = np.linalg.norm(np.array(current_position) - np.array(prev_position))
distance_meters_x, distance_meters_y = self.convert_pixels_to_meters(distance_pixels)
speed_meters_per_second = (distance_meters_x**2 + distance_meters_y**2)**0.5 * fps
else:
speed_meters_per_second = 0
self.previous_positions[track_id] = current_position
return speed_meters_per_second

#******************* 加載跟蹤數(shù)據(jù) (pickle 文件)**********************************#

def analyze_video(self, video_path, output_path, tracks_path):
with open(tracks_path, 'rb') as f:
tracks = pickle.load(f)

*
*
*
*
*
*
*
*
# 繪制球隊(duì)標(biāo)簽
cv2.putText(
frame,
team,
(int(x1), int(y1) + text_offset), 
cv2.FONT_HERSHEY_PLAIN,            
font_scale,
text_color,
thickness=2
)

#**************添加下面幾行代碼--->:

speed = self.calculate_speed(track_id, x_center, y_center, fps)
#速度標(biāo)簽 
speed_font_scale = 0.8  
speed_y_position = int(y1) + 20
if speed_y_position > int(y1) - 5:
speed_y_position = int(y1) - 5

cv2.putText(
frame,
f"Speed: {speed:.2f} m/s",  
(int(x1), speed_y_position),  
cv2.FONT_HERSHEY_PLAIN,       
speed_font_scale,
text_color,
thickness=2
)

# 寫輸出視頻
out.write(frame)
frame_num += 1

cap.release()
out.release()

如果你在添加上面這幾行新代碼時(shí)遇到問題,你可以隨時(shí)訪問該項(xiàng)目的GitHub存儲(chǔ)庫(kù)(https://github.com/rvizcarra15/IceHockey_ComputerVision_PyTorch),在那里你可以找到完整的參考代碼。此時(shí),你的視頻輸出結(jié)果應(yīng)該像下圖的樣子(請(qǐng)注意,速度已添加到每個(gè)播放器的標(biāo)簽中):

示例剪輯07:跟蹤球員和速度

最后,讓我們添加一個(gè)統(tǒng)計(jì)板,借助這個(gè)統(tǒng)計(jì)板我們可以跟蹤每支球隊(duì)每位球員的平均速度,以及其他指標(biāo),如覆蓋距離和對(duì)手區(qū)域的進(jìn)攻壓力。

我們已經(jīng)定義了進(jìn)攻區(qū),并將其整合到我們的代碼中?,F(xiàn)在,我們需要跟蹤每個(gè)球員進(jìn)入對(duì)手區(qū)域的頻率。為了實(shí)現(xiàn)這一點(diǎn),我們將使用光線投射算法實(shí)現(xiàn)一種方法。該算法檢查球員的位置是否在白隊(duì)或黃隊(duì)的進(jìn)攻區(qū)域內(nèi)。它的工作原理是從球員到目標(biāo)區(qū)域畫一條假想線。如果線穿過一個(gè)邊界,則球員在里面;如果它穿過更多邊界(在我們的例子中,四個(gè)邊界中的兩個(gè)),則球員就在外面。然后,該代碼掃描整個(gè)視頻,以確定每個(gè)被跟蹤對(duì)象的區(qū)域狀態(tài)。

#************ 在目標(biāo)區(qū)域中定位球員的位置***********************#

def is_inside_zone(self, position, zone):
x, y = position
n = len(zone)
inside = False
p1x, p1y = zone[0]
for i in range(n + 1):
p2x, p2y = zone[i % n]
if y > min(p1y, p2y):
if y <= max(p1y, p2y):
if x <= max(p1x, p2x):
if p1y != p2y:
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
if p1x == p2x or x <= xinters:
inside = not inside
p1x, p1y = p2x, p2y
return inside

現(xiàn)在,我們將通過添加一種方法來處理表現(xiàn)指標(biāo),該方法以表格形式顯示每支球隊(duì)的平均球員速度、總覆蓋距離和進(jìn)攻壓力(在對(duì)手區(qū)域花費(fèi)的時(shí)間百分比)。使用OpenCV,我們將把這些指標(biāo)格式化為覆蓋在視頻上的表格,并引入動(dòng)態(tài)更新機(jī)制,以便在游戲過程中保持實(shí)時(shí)統(tǒng)計(jì)數(shù)據(jù)。

#*******************************性能度量*********************************************#
def draw_stats(self, frame):
avg_speed_white = np.mean(self.team_stats['Tm_white']['speed']) if self.team_stats['Tm_white']['count'] > 0 else 0
avg_speed_yellow = np.mean(self.team_stats['Tm_yellow']['speed']) if self.team_stats['Tm_yellow']['count'] > 0 else 0
distance_white = self.team_stats['Tm_white']['distance']
distance_yellow = self.team_stats['Tm_yellow']['distance']

offensive_pressure_white = self.team_stats['Tm_white'].get('offensive_pressure', 0)
offensive_pressure_yellow = self.team_stats['Tm_yellow'].get('offensive_pressure', 0)

Pressure_ratio_W = offensive_pressure_white/distance_white   *100  if self.team_stats['Tm_white']['distance'] > 0 else 0
Pressure_ratio_Y = offensive_pressure_yellow/distance_yellow *100  if self.team_stats['Tm_yellow']['distance'] > 0 else 0

table = [
["", "Away_White", "Home_Yellow"],
["Average Speed\nPlayer", f"{avg_speed_white:.2f} m/s", f"{avg_speed_yellow:.2f} m/s"],
["Distance\nCovered", f"{distance_white:.2f} m", f"{distance_yellow:.2f} m"],
["Offensive\nPressure %", f"{Pressure_ratio_W:.2f} %", f"{Pressure_ratio_Y:.2f} %"],
]

text_color = (0, 0, 0)  
start_x, start_y = 10, 590  
row_height = 30     # 管理行間高度
column_width = 150  # 管理行間寬度
font_scale = 1  

def put_multiline_text(frame, text, position, font, font_scale, color, thickness, line_type, line_spacing=1.0):
y0, dy = position[1], int(font_scale * 20 * line_spacing)  # 在此處調(diào)整行距
for i, line in enumerate(text.split('\n')):
y = y0 + i * dy
cv2.putText(frame, line, (position[0], y), font, font_scale, color, thickness, line_type)

for i, row in enumerate(table):
for j, text in enumerate(row):
if i in [1,2, 3]:  
put_multiline_text(
frame,
text,
(start_x + j * column_width, start_y + i * row_height),
cv2.FONT_HERSHEY_PLAIN,
font_scale,
text_color,
1,
cv2.LINE_AA,
line_spacing= 0.8 
)
else:
cv2.putText(
frame,
text,
(start_x + j * column_width, start_y + i * row_height),
cv2.FONT_HERSHEY_PLAIN,
font_scale,
text_color,
1,
cv2.LINE_AA,
)       

#****************** 跟蹤和更新游戲統(tǒng)計(jì)數(shù)據(jù)****************************************#

def update_team_stats(self, team, speed, distance, position):
if team in self.team_stats:
self.team_stats[team]['speed'].append(speed)
self.team_stats[team]['distance'] += distance
self.team_stats[team]['count'] += 1

if team == 'Tm_white':
if self.is_inside_zone(position, self.zone_white):
self.team_stats[team]['offensive_pressure'] += distance
elif team == 'Tm_yellow':
if self.is_inside_zone(position, self.zone_yellow):
self.team_stats[team]['offensive_pressure'] += distance
為了在視頻中顯示統(tǒng)計(jì)數(shù)據(jù),我們必須調(diào)用analyze_video方法,所以一定要在定義速度標(biāo)簽之后,在處理輸出視頻之前添加下面這些額外的代碼行:
*
*
*
*
*
*
*
#速度標(biāo)簽 
speed_font_scale = 0.8  
speed_y_position = int(y1) + 20
if speed_y_position > int(y1) - 5:
speed_y_position = int(y1) - 5

cv2.putText(
frame,
f"Speed: {speed:.2f} m/s",  
(int(x1), speed_y_position),  
cv2.FONT_HERSHEY_PLAIN,       
speed_font_scale,
text_color,
thickness=2
)
#**************添加下面幾行代碼--->:

distance = speed / fps
position = (x_center, y_center)
self.update_team_stats(team, speed, distance, position)

# 寫輸出視頻
out.write(frame)
frame_num += 1

每個(gè)球員覆蓋的距離(以米為單位)是通過將他們的速度(以米每秒為單位)除以幀率(每秒幀數(shù))來計(jì)算的。這種計(jì)算使我們能夠估計(jì)每個(gè)球員在視頻中的每一幀變化之間移動(dòng)了多遠(yuǎn)。如果一切正常,你的最終視頻輸出應(yīng)該如下:

示例剪輯08:示例程序最終輸出結(jié)果

待考慮因素和未來的工作

本文示例中實(shí)現(xiàn)的模型是使用計(jì)算機(jī)視覺跟蹤冰球比賽(或任何團(tuán)隊(duì)運(yùn)動(dòng))中有關(guān)球員的基本設(shè)置。但是,可以進(jìn)行很多微調(diào)來改進(jìn)它并添加新功能。以下是我為下一個(gè)2.0版本所做的一些想法,你也可以考慮:

  • 跟蹤冰球的挑戰(zhàn):考慮到冰球與足球或籃球相比的大小,跟蹤冰球具有挑戰(zhàn)性,這取決于你的相機(jī)朝向哪個(gè)方向和分辨率。但如果你實(shí)現(xiàn)了這一點(diǎn),那么跟蹤將表現(xiàn)出非常有趣的可能性,比如控球時(shí)間指標(biāo)、進(jìn)球機(jī)會(huì)或投籃數(shù)據(jù)。這也適用于個(gè)人表演;在冰球中,球員的變化比其他團(tuán)隊(duì)運(yùn)動(dòng)要頻繁得多,因此跟蹤每個(gè)球員在一個(gè)時(shí)期的表現(xiàn)是一個(gè)挑戰(zhàn)。
  • 計(jì)算資源,哦,為什么要計(jì)算呢?我在CPU上運(yùn)行了所有代碼,但遇到了問題(有時(shí)會(huì)導(dǎo)致藍(lán)屏),由于在設(shè)計(jì)過程中內(nèi)存不足(考慮使用CUDA設(shè)置)。我們的示例視頻長(zhǎng)約40秒,最初大小為5MB,但在運(yùn)行模型后,輸出增加到34MB。想象一下一個(gè)20分鐘時(shí)間長(zhǎng)的游戲的情況吧。因此,在擴(kuò)展時(shí),你應(yīng)該考慮計(jì)算資源和存儲(chǔ)。
  • 不要低估MLOps:為了快速部署和擴(kuò)展,我們需要高效、支持頻繁執(zhí)行且可靠的機(jī)器學(xué)習(xí)管道。這涉及到考慮采用持續(xù)集成部署訓(xùn)練方法。我們的用例是為特定場(chǎng)景構(gòu)建的,但如果條件發(fā)生變化,比如相機(jī)方向或球衣顏色,該怎么辦?為了擴(kuò)大規(guī)模,我們必須采取CI/CD/CT的思維方式。

最后,我希望你覺得本文提供的這個(gè)計(jì)算機(jī)視覺演示項(xiàng)目非常有趣,你可以在GitHub存儲(chǔ)庫(kù)(https://github.com/rvizcarra15/IceHockey_ComputerVision_PyTorch)中訪問完整的代碼。如果你想支持該地區(qū)直排冰球和冰球的發(fā)展,請(qǐng)關(guān)注APHL(我們總是需要你想為年輕球員捐贈(zèng)的二手設(shè)備,并致力于建設(shè)我們的第一個(gè)官方冰球場(chǎng)),在全球范圍內(nèi),請(qǐng)關(guān)注并支持友誼聯(lián)盟(https://friendshipleague.org/)。

原文標(biāo)題:Spicing up Ice Hockey with AI: Player Tracking with Computer Vision,作者:Raul Vizcarra Chirinos

鏈接:https://towardsdatascience.com/spicing-up-ice-hockey-with-ai-player-tracking-with-computer-vision-ce9ceec9122a

想了解更多AIGC的內(nèi)容,請(qǐng)?jiān)L問:

51CTO AI.x社區(qū)

http://www.scjtxx.cn/aigc/

責(zé)任編輯:姜華 來源: 51CTO內(nèi)容精選
相關(guān)推薦

2023-06-26 10:44:42

2024-09-12 17:19:43

YOLO目標(biāo)檢測(cè)深度學(xué)習(xí)

2019-10-31 16:14:28

物聯(lián)網(wǎng)機(jī)器學(xué)習(xí)AI

2022-11-02 11:34:45

2023-07-07 10:53:08

2022-01-06 07:51:18

AI 計(jì)算機(jī)視覺

2023-05-19 10:49:39

2021-05-19 09:00:00

人工智能機(jī)器學(xué)習(xí)技術(shù)

2023-04-04 08:25:31

計(jì)算機(jī)視覺圖片

2024-03-01 10:08:43

計(jì)算機(jī)視覺工具開源

2023-09-14 10:48:59

算法編程

2023-03-08 10:15:43

AI計(jì)算機(jī)視覺

2023-09-04 15:15:17

計(jì)算機(jī)視覺人工智能

2019-07-18 09:00:52

AI人工智能計(jì)算機(jī)視覺

2020-08-04 10:24:50

計(jì)算機(jī)視覺人工智能AI

2020-11-05 13:50:23

計(jì)算機(jī)視覺

2022-12-28 10:21:00

騰訊云計(jì)算機(jī)視覺

2023-11-20 22:14:16

計(jì)算機(jī)視覺人工智能

2009-10-11 01:03:17

曙光案例氣象

2009-07-10 18:48:17

曙光高性能計(jì)算天氣
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)