Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pose estimate #15

Merged
merged 30 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ddf658c
create new branch to implement pose estimation
dweizzz Oct 1, 2023
3942f99
Pose Estimation
dweizzz Oct 15, 2023
8b16843
Fix ignored files, print angles
dweizzz Oct 15, 2023
57f0f1d
Pose Estimation
dweizzz Oct 15, 2023
be4cd9f
Pose Estimation
dweizzz Oct 21, 2023
4f9b2b5
slight alteration
dweizzz Oct 21, 2023
e49ecbf
Add naive strategy for shot detection
audreywangg Oct 24, 2023
7b601cd
ci testing
bzhang1945 Oct 21, 2023
da10d15
Ensure ffmpeg dependency is installed
kchiem12 Oct 1, 2023
3ab96a3
Implement multithreading for object tracking
kchiem12 Oct 15, 2023
387aee7
reencoding fix + minor changes
bzhang1945 Oct 22, 2023
5e9033a
Add ShotAttempt class in state
Mikonooooo Oct 20, 2023
0343616
Update ball state tracking for multiple balls
Mikonooooo Oct 20, 2023
33befe7
Implement PlayerState and Frame
Mikonooooo Oct 21, 2023
fc6ea20
Update SORT parsing with new format
Mikonooooo Oct 21, 2023
bd1f592
Moved filtering from team_detect to parse as cleaner
Mikonooooo Oct 21, 2023
1db7864
Saving some stuff
Mikonooooo Oct 21, 2023
187d321
shot detect and singleton ball fix
bzhang1945 Oct 21, 2023
24ee3f8
Moved frame creation, counting, filtering into state
Mikonooooo Oct 22, 2023
57c5cdb
Reworked possession processing for state
Mikonooooo Oct 23, 2023
8c2d37c
Reworked team detect
Mikonooooo Oct 23, 2023
97fa9ca
Removed general detect, Reason: Obsolete man
Mikonooooo Oct 23, 2023
038016b
Reworked shot detection for new state
Mikonooooo Oct 23, 2023
d304667
Fix bugs, app now running
Mikonooooo Oct 23, 2023
2460837
Fix court truth map fix
Mikonooooo Oct 23, 2023
b746bf1
Update README to simpler command
Mikonooooo Oct 23, 2023
b77058c
Update ci.yml
Mikonooooo Oct 23, 2023
1d4d580
Adjusted main to accept command-line arg
Mikonooooo Oct 23, 2023
388cfb0
Adjusted training_data, added short video to data
Mikonooooo Oct 25, 2023
e5e1445
Merge branch 'ball-202' into pose-estimate
Mikonooooo Oct 25, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
venv
.env
__pycache__
yolov8m-pose.pt
*.mp4
ball/lib/python3.11/site-packages/torch/lib/libtorch_cpu.dylib
venv/lib/python3.11/site-packages/torch/lib/libtorch_cpu.dylib
ball/
tmp/
tmp/*.json
Binary file added best.pt
Binary file not shown.
14 changes: 10 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ opencv-python==4.7.0.72
matplotlib>=3.2.2
Pillow>=7.1.2
PyYAML>=5.3.1
torch==2.0.1
torchvision==0.15.2
torch==2.0.1 # Check if this version exists, if not, use the latest stable version
torchvision==0.15.2 # Same check as torch
tqdm>=4.41.0
seaborn
scipy
Expand All @@ -46,7 +46,13 @@ imageio

# View
streamlit>=1.18.1
hydralit_components>= 1.0.10
hydralit_components>=1.0.10

# Misc
pylint
pylint

# Additional Dependencies for Pose Estimation
json5
ultralytics
imageio==2.9.0
imageio-ffmpeg>=0.4.3
5 changes: 3 additions & 2 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ def main(video_path):

modelrunner = ModelRunner(video_path, model_vars)
modelrunner.run()
people_output, ball_output = modelrunner.fetch_output()
modelrunner.pose()
people_output, ball_output, pose_output = modelrunner.fetch_output()
output_video_path = 'tmp/court_video.mp4'
output_video_path_reenc = 'tmp/court_video_reenc.mp4'

processrunner = ProcessRunner(video_path, people_output, ball_output, output_video_path,
processrunner = ProcessRunner(video_path, people_output, ball_output, output_video_path,
output_video_path_reenc)
processrunner.run()
results = processrunner.get_results()
Expand Down
22 changes: 17 additions & 5 deletions src/modelrunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import pickle
import subprocess
from typing import Tuple
from pose_estimation.pose_estimate import PoseEstimator
from ultralytics import YOLO

class ModelRunner:
"""
Expand All @@ -15,6 +17,7 @@ class ModelRunner:
def __init__(self, video_path, model_vars) -> None:
self.video_path = video_path
self.frame_reduction_factor = model_vars['frame_reduction_factor']
self.pose_estimator = PoseEstimator(video_path=video_path)


def drop_frames(self, input_path) -> str:
Expand Down Expand Up @@ -51,15 +54,24 @@ def run(self):
with open('tmp/output.pickle', 'rb') as f:
self.output_dict = pickle.load(f)


def fetch_output(self) -> Tuple[str, str]:
def pose(self):
model = YOLO('best.pt')
results = model(
source = self.video_path,
show=False,
conf=0.3,
verbose = False
)
self.pose_estimator.estimate_pose(results = results)

def fetch_output(self) -> Tuple[str, str, str]:
"""
Converts the people and ball model output in self.output.dict into txt files.
Returns a tuple of the people and ball txt output paths.
"""
ball_list = [tuple(round(num) for num in tup)
ball_list = [tuple(round(num) for num in tup)
for tup in self.output_dict['basketball_data'][0]]
people_list = [tuple(round(num) for num in tup)
people_list = [tuple(round(num) for num in tup)
for tup in self.output_dict['person_data'][0]]
ball_data = [(' '.join(map(str, ball[0:7])) + ' -1 -1 -1 -1')
for ball in ball_list]
Expand All @@ -72,4 +84,4 @@ def fetch_output(self) -> Tuple[str, str]:
with open('tmp/people.txt', 'w') as f:
f.write('\n'.join(people_data))

return 'tmp/people.txt', 'tmp/ball.txt'
return 'tmp/people.txt', 'tmp/ball.txt', 'tmp/pose.txt'
80 changes: 80 additions & 0 deletions src/pose_estimation/pose_estimate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import torch
import math
import json
from ultralytics import YOLO

class PoseEstimator:
def __init__(self, model_path='best.pt', video_path='res/pose_results/test_multiple_people.mp4', combinations=None):
# Initialize paths, model, and combinations of keypoints to calculate angles
self.model_path = model_path
self.video_path = video_path
self.model = YOLO(model_path) # Load the YOLO model

# Combinations of points to calculate 8 angles
self.combinations = combinations if combinations is not None else [
(5, 7, 9), (6, 8, 10), (11, 13, 15), (12, 14, 16),
(5, 6, 8), (6, 5, 7), (11, 12, 14), (12, 11, 13)
]

# Names corresponding to the adjusted 8 angle types
self.angle_names = [
"left_elbow", "right_elbow", "left_knee", "right_knee",
"right_shoulder", "left_shoulder",
"right_hip", "left_hip"
]

@staticmethod
def compute_angle(p1, p2, p3):
# Calculate angle given 3 points using the dot product and arc cosine
vector_a = p1 - p2
vector_b = p3 - p2

# Normalize the vectors (to make them unit vectors)
vector_a = vector_a / torch.norm(vector_a)
vector_b = vector_b / torch.norm(vector_b)

# Compute the angle
cosine_angle = torch.sum(vector_a * vector_b)
angle_radians = torch.acos(cosine_angle)
angle_degrees = angle_radians * 180 / math.pi

return angle_degrees

def estimate_pose(self, results):
model = YOLO(self.model_path)

# Initialize an empty list to store pose data
pose_data = []

for frame_idx, result in enumerate(results):
keypoints = result.keypoints.data[:, :, :2].numpy() # Extracting the (x, y) coordinates
confidences = result.keypoints.conf.numpy().tolist() # Extracting the confidences
boxes = result.boxes.xyxy.numpy().tolist() # Extracting bounding boxes
frame_pose_data = {
'frame': frame_idx,
'persons': [],
'boxes': boxes,
'keypoints': keypoints.tolist(),
'confidences': confidences
}

for person_idx, (person_keypoints, person_confidences, box) in enumerate(zip(keypoints, confidences, boxes)):
person_data = {
'keypoints': person_keypoints.tolist(),
'confidences': person_confidences,
'box': box,
'angles': {}
}

for idx, combination in enumerate(self.combinations):
if all(idx < len(person_keypoints) for idx in combination):
p1, p2, p3 = (person_keypoints[i] for i in combination)
angle_degrees = self.compute_angle(torch.tensor(p1), torch.tensor(p2), torch.tensor(p3))
person_data['angles'][self.angle_names[idx]] = angle_degrees.item()

frame_pose_data['persons'].append(person_data)

pose_data.append(frame_pose_data)

with open("tmp/pose_data.json", "w") as f:
json.dump(pose_data, f)
6 changes: 5 additions & 1 deletion src/processing/courtline_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,11 @@ def _evaluate_homography(self,pts_src:list,pts_dst:list):
assert(pts_src is not None)
mapped_edge_img = self._apply_gray_homography(self._MASK_COURT_EDGES,pts_src,pts_dst=pts_dst)
total_max_overlap = self._max_pixel_overlap(self._MASK_COURT_EDGES,pts_src,pts_dst=pts_dst)
goodness = float(np.count_nonzero(mapped_edge_img > 100)) / total_max_overlap
if total_max_overlap != 0:
goodness = float(np.count_nonzero(mapped_edge_img > 100)) / total_max_overlap
else:
goodness = 0

return goodness

def _get_four_intersections(self,l1:list,l2:list,l3:list,l4:list,relax_factor=0):
Expand Down
4 changes: 2 additions & 2 deletions src/processrunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class ProcessRunner:
Performs player, team, shot, and courtline detection in sequence.
Effect: updates GameState with statistics and produces courtline video.
"""
def __init__(self, video_path, players_tracking, ball_tracking, output_video_path,
def __init__(self, video_path, players_tracking, ball_tracking, output_video_path,
output_video_path_reenc) -> None:
self.video_path = video_path
self.players_tracking = players_tracking
Expand Down Expand Up @@ -67,7 +67,7 @@ def run_video_render(self):
"""Runs video rendering and reencodes, stores to output_video_path_reenc."""
videoRender = video_render.VideoRender(self.homography)
videoRender.render_video(self.state.states, self.state.players, self.output_video_path)
videoRender.reencode(self.output_video_path,
videoRender.reencode(self.output_video_path,
self.output_video_path_reenc)


Expand Down
Binary file modified tmp/court_video.mp4
Binary file not shown.