Skip to content
Permalink
Browse files

Added goal-cam for more reliable goal detection

  • Loading branch information...
andreasschmidtjensen committed Apr 23, 2019
1 parent 6b02006 commit 9d3e2ad38b210d218ea8d3682fbc39f262ab6ee4
Binary file not shown.
Binary file not shown.
@@ -46,9 +46,10 @@ def stats():

if __name__ == '__main__':
source_type = 'webcam'
path = 0
path = 1

controller = Controller(source_type, path,
controller = Controller(source_type=source_type,
paths={"camera_top": 1, "camera_home": 0},
arduino_config={"port": "COM3", "features": ["display", "sound"]},
debug=True)
controller.start()
@@ -15,12 +15,12 @@


class Controller(Thread):
def __init__(self, source_type, path, arduino_config={}, debug=False):
def __init__(self, source_type, paths, arduino_config={}, debug=False):
super(Controller, self).__init__()

self.fps = FPS().start()

self.field = SoccerField(YOLO_SIZE, debug)
self.field = SoccerField(YOLO_SIZE, paths, debug)

self.detector = Detector()

@@ -33,6 +33,7 @@ def __init__(self, source_type, path, arduino_config={}, debug=False):

self.debug = debug

path = paths["camera_top"]
if source_type == 'webcam':
self.source = WebcamVideoStream(int(path)).start()
elif source_type == 'video':
@@ -63,6 +64,8 @@ def run(self):
self.detector.calculate_field(frame)
self.recalculate = False

self.field.goal_checker.update_baselines()

if self.debug:
img = cv2.resize(frame.copy(), YOLO_SIZE)
if self.detector.corners is not None:
@@ -86,8 +89,9 @@ def run(self):
env = np.zeros((YOLO_SIZE[0], YOLO_SIZE[1], 3), np.uint8)
env = self.field.draw(env)
self.snapshots["ENVIRONMENT"] = env
self.field.goal_checker.update_snapshots(self.snapshots)

if self.arduino.has_feature("display"):
if self.arduino is not None and self.arduino.has_feature("display"):
self.arduino.print_score(*self.field.get_score())

def schedule_recalculation(self):
@@ -99,7 +103,7 @@ def get_stats(self):
for goal in self.field.score:
score[goal["team"]] += 1
player = ""
if "row" in goal["player"]:
if "player" in goal and "row" in goal["player"]:
player = "(%s, %s)" % (goal["player"]["row"], goal["player"]["position"])
goals.append({
"time": goal["ts"].strftime("%X"),
@@ -2,46 +2,59 @@

import numpy as np

import tablesoccer.Players as Players
from util.motion_detection import MotionDetector

GOAL_AREA = (100, 100)


class GoalChecker:

def __init__(self):
def __init__(self, paths):
self.ball_in_goal_area = False, -1 # (In Goal Area, Team)
self.frames_without_ball = False

self.goal_checkers = {
0: MotionDetector(paths["camera_home"]),
#1: MotionDetector(paths["camera_away"])
}

self.ball_history = []
self.player_history = []

self.goal_left = None
self.goal_right = None

def check_for_goal(self, field):
"""
We check for goal by checking if the ball disappears in a specific rectangle in front of the goal.
The process:
1) Ball is in the field
2) Ball is in rectangle in front of goal
3) Ball disappears from field
4) Ball is gone from the field for 15 frames
5) Goal is scored!
We keep the history of the player position and the ball position to be able to figure out who shot.
We find the player by traversing back in the history until the direction of the ball changes. This time in
history is assumed to be the time when the shot was taken.
self.goal_scored = True

def check_for_motion(self, field, team):
"""
1) Check if there is motion and if goal_scored==False
2) Check that ball is not in field
3) If 1 & 2 are True, set flag goal_scored=True and return True
:param field:
:return:
:param team
:return: True if motion, False otherwise
"""
if team not in self.goal_checkers:
return False

ball_gone = field.ball.get_position() is None
motion = self.goal_checkers[team].motion_detected()

if motion and not self.goal_scored and ball_gone:
self.goal_scored = True
return True

if not ball_gone:
self.goal_scored = False
self.goal_checkers[team].update_baseline()

return False

def check_for_goal(self, field):
# Get data from the field
ball = field.ball
center = field.center
field_width, _ = field.field_shape
in_field = ball.get_position() is not None

# update the goal location based on the center of the field
self.goal_left = (0, center[1])
@@ -54,79 +67,69 @@ def check_for_goal(self, field):
})
self.player_history.append(field.players.players)

if in_field:
# the ball is currently in the field so we don't have a goal
self.frames_without_ball = 0

# create a rectangle in front of the goal for detecting the ball
rect_goal_a = [self.goal_left[0] - GOAL_AREA[0] / 2, self.goal_left[1] - GOAL_AREA[1] / 2,
self.goal_left[0] + GOAL_AREA[0] / 2, self.goal_left[1] + GOAL_AREA[1] / 2]

rect_goal_b = [self.goal_right[0] - GOAL_AREA[0] / 2, self.goal_right[1] - GOAL_AREA[1] / 2,
self.goal_right[0] + GOAL_AREA[0] / 2, self.goal_right[1] + GOAL_AREA[1] / 2]

# we check if the ball is in the goal area rectangle
if rect_goal_a[0] < ball.get_position()[0] < rect_goal_a[2] and rect_goal_a[1] < ball.get_position()[1] < rect_goal_a[3]:
self.ball_in_goal_area = True, Players.TEAM_AWAY # Yes, in the away area
elif rect_goal_b[0] < ball.get_position()[0] < rect_goal_b[2] and rect_goal_b[1] < ball.get_position()[1] < rect_goal_b[3]:
self.ball_in_goal_area = True, Players.TEAM_HOME # Yes, in the home area
else:
self.ball_in_goal_area = False, -1 # No, no ball in the area

# Check if the ball has disappeared but was in the goal area just before
if in_field is False and self.ball_in_goal_area[0]:
# Count number of frames without a ball detection
self.frames_without_ball = self.frames_without_ball + 1

# when more 15 frames has occurred without a ball (and it was in the goal area just before) => goal
if self.frames_without_ball >= 15:

team = self.ball_in_goal_area[1]
self.frames_without_ball = 0
self.ball_in_goal_area = False, -1

# find first frame with a different direction, this is when the shot on goal was made
dir_at_goal = self.ball_history[-1]["dir"]
direction = dir_at_goal
i = -1
while direction == dir_at_goal:
i -= 1

if len(self.ball_history) < abs(i):
# no more moves in history
print("> could not see scoring player")
i = 0
break

direction = self.ball_history[i]["dir"]

player_info = {}
if i < 0:
# scoring player was found
pos = self.ball_history[i]["position"]

# find the closest player to the shooting position from the player history
min_dist = 1000
for r, row in enumerate(self.player_history[i]):
for p, player in enumerate(row.get_players()):
# calc distance between shooting position and player
if pos is None or player[0:2] is None:
continue

dist = np.linalg.norm(np.array(pos) - np.array(player[0:2]))
if dist < min_dist:
min_dist = dist
player_info["row"] = r
player_info["position"] = p

player_info["shot_position"] = pos

# notify field that a goal was scored
field.goal_scored({
"team": team,
"ts": datetime.datetime.now(),
"player": player_info
})
# check for goals
home_scored = self.check_for_motion(field, 0)
away_scored = self.check_for_motion(field, 1)

if home_scored or away_scored:
self.handle_goal_scored(field, 0 if home_scored else 1)

def handle_goal_scored(self, field, team):
# find first frame with a different direction, this is when the shot on goal was made
dir_at_goal = self.ball_history[-1]["dir"]
direction = dir_at_goal
i = -1
while direction == dir_at_goal:
i -= 1

if len(self.ball_history) < abs(i):
# no more moves in history
print("> could not see scoring player")
i = 0
break

direction = self.ball_history[i]["dir"]

player_info = None
if i < 0:
# scoring player was found
pos = self.ball_history[i]["position"]

# find the closest player to the shooting position from the player history
min_dist = 1000
for r, row in enumerate(self.player_history[i]):
for p, player in enumerate(row.get_players()):
# calc distance between shooting position and player
if pos is None or player[0:2] is None:
continue

dist = np.linalg.norm(np.array(pos) - np.array(player[0:2]))
if dist < min_dist:
min_dist = dist
player_info = {"row": r, "position": p, "shot_position": pos}

# notify field that a goal was scored
result = {
"team": team,
"ts": datetime.datetime.now()
}
if player_info is not None:
result["player"] = player_info
field.goal_scored(result)

def update_baselines(self):
for _, goal_checker in self.goal_checkers.items():
goal_checker.update_baseline()

def update_snapshots(self, snapshots):
if 0 in self.goal_checkers:
snapshots["CAM_HOME_BASELINE"] = self.goal_checkers[0].baseline
snapshots["CAM_HOME"] = self.goal_checkers[0].latest_frame
snapshots["CAM_HOME_DIFF"] = self.goal_checkers[0].latest_diff
if 1 in self.goal_checkers:
snapshots["CAM_AWAY_BASELINE"] = self.goal_checkers[1].baseline
snapshots["CAM_AWAY"] = self.goal_checkers[1].latest_frame
snapshots["CAM_AWAY_DIFF"] = self.goal_checkers[1].latest_diff

def draw_goal_area(self, draw):
"""
@@ -8,14 +8,14 @@


class SoccerField:
def __init__(self, field_shape, debug=False):
def __init__(self, field_shape, paths, debug=False):
self.center = None
self.corners = None
self.field_shape = field_shape

self.ball = Ball()
self.players = None
self.goal_checker = GoalChecker()
self.goal_checker = GoalChecker(paths)

self.score = []
self.possession = None
@@ -108,10 +108,11 @@ def draw(self, canvas):
for goal in self.score:
score[goal["team"]] += 1

x = goal["player"]["shot_position"][0]
y = goal["player"]["shot_position"][1]
draw.line((x - 2, y - 2, x + 2, y + 2), fill=(255, 120, 120, 255), width=1)
draw.line((x + 2, y - 2, x - 2, y + 2), fill=(255, 120, 120, 255), width=1)
if "player" in goal:
x = goal["player"]["shot_position"][0]
y = goal["player"]["shot_position"][1]
draw.line((x - 2, y - 2, x + 2, y + 2), fill=(255, 120, 120, 255), width=1)
draw.line((x + 2, y - 2, x - 2, y + 2), fill=(255, 120, 120, 255), width=1)

draw.text((0, self.field_shape[1]-15), "Home %s - %s Away" % tuple(score), font=font)

@@ -39,9 +39,11 @@ <h1 class="display-5" data-bind="with: possession"><span data-bind="text: home">
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="env" role="tabpanel" aria-labelledby="env-tab">
<img src="{{ url_for('video', feed='ENVIRONMENT') }}" class="video" />
<img src="{{ url_for('video', feed='CAM_HOME_DIFF') }}" class="video" />
</div>
<div class="tab-pane fade" id="cam" role="tabpanel" aria-labelledby="cam-tab">
<img src="{{ url_for('video', feed='TRANSFORMED') }}" class="video" />
<img src="{{ url_for('video', feed='CAM_HOME') }}" class="video" />
</div>
</div>
</div>
@@ -0,0 +1,55 @@
"""
Credit: https://www.pyimagesearch.com/2015/05/25/basic-motion-detection-and-tracking-with-python-and-opencv/
"""
from imutils.video import VideoStream
import imutils
import cv2


class MotionDetector:
def __init__(self, source, min_area=500):
self.min_area = min_area

self.vs = VideoStream(src=source).start()
self.baseline = None # baseline frame
self.latest_frame = None # for snapshot
self.latest_diff = None # for snapshot
self.update_baseline()

def __get_frame(self):
frame = self.vs.read()
frame = imutils.resize(frame, width=500)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (21, 21), 0)

return gray

def update_baseline(self):
"""
Read from camera and take the content as the 'baseline', i.e. without anything in the picture.
We use this baseline to calculate if something new is in the picture.
"""
self.baseline = self.__get_frame()

def motion_detected(self):
self.latest_frame = self.__get_frame()
frame = self.latest_frame

# compute the absolute difference between the current frame and first frame
frameDelta = cv2.absdiff(self.baseline, frame)
thresh = cv2.threshold(frameDelta, 25, 255, cv2.THRESH_BINARY)[1]

# dilate the threshold image to fill in holes, then find contours on threshold image
thresh = cv2.dilate(thresh, None, iterations=2)

self.latest_diff = thresh

contours = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
contours = imutils.grab_contours(contours)

for c in contours:
if cv2.contourArea(c) >= self.min_area:
return True

return False

0 comments on commit 9d3e2ad

Please sign in to comment.
You can’t perform that action at this time.