├── .flake8
├── .github
└── workflows
│ ├── publish-docs.yml
│ ├── publish-package.yml
│ └── test-windows.yml
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
├── codeball
├── __init__.py
├── codeball_frames.py
├── game_dataset.py
├── patterns
│ ├── __init__.py
│ ├── passes_into_the_box.py
│ ├── patterns.py
│ ├── patterns_config.json
│ ├── set_pieces.py
│ └── team_stretched.py
├── tactical.py
├── tests
│ ├── __init__.py
│ ├── files
│ │ ├── code_xml.xml
│ │ ├── events.json
│ │ ├── game_dataset.obj
│ │ ├── metadata.xml
│ │ └── tracking.txt
│ ├── test_models.py
│ ├── test_patterns.py
│ └── test_visualizations.py
├── utils
│ ├── __init__.py
│ └── json_encoders.py
└── visualizations.py
├── docs
├── CNAME
├── changelog.md
├── codeball-frames.md
├── examples
│ ├── example-pattern.md
│ ├── game_dataset.ipynb
│ ├── output.patt
│ └── run_patterns.ipynb
├── format-for-play.md
├── game-dataset.md
├── how-to-import-to-play.md
├── index.md
├── media
│ ├── passes_into_the_box.gif
│ ├── set_pieces.gif
│ ├── sprint.gif
│ └── team_stretched.gif
├── metrica-play-api.md
├── patterns.md
├── tactical.md
└── visualizations.md
├── mkdocs.yml
├── pyproject.toml
├── requirements.txt
└── setup.py
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = E203, E266, E501, W503, F403, F401
3 | max-line-length = 79
4 | max-complexity = 18
5 | select = B,C,E,F,W,T4,B9
--------------------------------------------------------------------------------
/.github/workflows/publish-docs.yml:
--------------------------------------------------------------------------------
1 | name: Publish documentation
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | deploy:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Set up Python
13 | uses: actions/setup-python@v2
14 | with:
15 | python-version: '3.8'
16 | - name: Install dependencies
17 | run: |
18 | python -m pip install --upgrade pip
19 | pip install mkdocs-material mkdocs-jupyter nbconvert==5.6.1
20 | - name: Publish
21 | run: mkdocs gh-deploy --force
--------------------------------------------------------------------------------
/.github/workflows/publish-package.yml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | deploy:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Set up Python
13 | uses: actions/setup-python@v2
14 | with:
15 | python-version: '3.8'
16 | - name: Install dependencies
17 | run: |
18 | python -m pip install --upgrade pip
19 | pip install setuptools wheel twine
20 | - name: Build and publish
21 | env:
22 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
23 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
24 | run: |
25 | python setup.py sdist bdist_wheel
26 | twine upload dist/*
--------------------------------------------------------------------------------
/.github/workflows/test-windows.yml:
--------------------------------------------------------------------------------
1 | name: Python Package Windows
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | build:
13 |
14 | runs-on: windows-latest
15 | strategy:
16 | matrix:
17 | python-version: [3.7, 3.8]
18 |
19 | steps:
20 | - uses: actions/checkout@v2
21 | - name: Set up Python ${{ matrix.python-version }}
22 | uses: actions/setup-python@v1
23 | with:
24 | python-version: ${{ matrix.python-version }}
25 | - name: Install dependencies
26 | run: |
27 | python -m pip install --upgrade pip
28 | pip install black flake8 pytest
29 | pip install -e .
30 | - name: Lint with flake8
31 | run: |
32 | flake8 .
33 | - name: Code formatting
34 | run: |
35 | pip install black
36 | black .
37 | - name: Test with pytest
38 | run: |
39 | pytest
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | env
6 | dist
7 | *.egg-info
8 | build
9 | pip-wheel-metadata
10 | site
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/ambv/black
3 | rev: 20.8b1
4 | hooks:
5 | - id: black
6 | language_version: python3.8
7 | - repo: https://gitlab.com/pycqa/flake8
8 | rev: 3.8.3
9 | hooks:
10 | - id: flake8
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Metrica Sports
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # codeball: data driven tactical and video analysis of soccer games
2 |
3 | [](https://pypi.org/project/codeball/)
4 | [](https://pepy.tech/project/codeball)
5 | 
6 | 
7 | [](https://metrica-sports.com/)
8 | --------
9 |
10 | ## Why codeball?
11 |
12 | While there are several pieces of code / repositories around that provide different tools and bits of codes to do tactical analysis of individual games, there is no centralized place in which they live. Moreover, most of the analysis done is usually not linked or easy to link with the actual footage of the match. Codeball's objective is to change that by:
13 |
14 | 1. Building a central repository for different types of data driven tactical analysis methods / tools.
15 | 2. Making it easy to link those analyses with a video of the game in different formats.
16 |
17 | ## What can you do with it
18 |
19 | The main types of work / development you can do with codeball are:
20 |
21 | #### Work with tracking and event data
22 |
23 | - Codeball creates subclasses of *Pandas DataFrames* for events and tracking data; and provides you with handy methods to work with the data.
24 | - Work with or create your own tactical models like *Zones* so that you can for example do `game_dataset.events.into(Zones.OPPONENT_BOX)` and it will return a DataFrame only with the events into the opponents box. You can also chain methods, like `game_dataset.events.type("PASS").into(Zones.OPPONENT_BOX)` and will return only passes into the box. Or for example do `game_dataset.tracking.team('FIFATMA').players('field').dimension('x')` to get the x coordinate of the field players (no goalkeeper data) for team with id FIFATMA.
25 | - [Not yet implemented] Easily access tactical tools or methods like computing passes networks, pitch control,EPV models, etc
26 |
27 | #### Create Patterns to analyze the game
28 |
29 | - Analyze games based on Patterns. A Pattern is a unit of analysis that looks for moments in the game in which a certain thing happens. That certain thing is defined inside the Pattern, but codeball provides tools to easily create them, configure them and export them in different formats for different platforms.
30 | - You can create your own patterns, or also use the ones provided with the package and configure them to your liking.
31 |
32 | #### Add annotations to the events for Metrica Play
33 |
34 | - Codeball incorporates all the annotations models and API information needed to import events with annotations into Metrica Play.
35 | - You can add directly from the code any visualization available in Metrica Play (spotlights, rings, future trail, areas, drawings, text, etc) to any event.
36 |
37 | ## Example
38 |
39 | You can use any of the above functionality independently. However they are most powerful when combined. As an example, the below code defines a pattern that will look for all passes into the opponent's box. Moreover to be imported into Metrica Play, it will add an arrow and a 2s pause in the video at the moment of the pass, and will add an arrow to the 2D field indicating start and end position of the pass.
40 |
41 | ```python
42 | class PassesIntoTheBox(Pattern):
43 | def __init__(
44 | self,
45 | game_dataset: GameDataset,
46 | name: str,
47 | code: str,
48 | in_time: int = 0,
49 | out_time: int = 0,
50 | parameters: dict = None,
51 | ):
52 | super().__init__(
53 | name, code, in_time, out_time, parameters, game_dataset
54 | )
55 |
56 | def run(self) -> List[PatternEvent]:
57 |
58 | passes_into_the_box = (
59 | self.game_dataset.events.type("PASS")
60 | .into(Zones.OPPONENT_BOX)
61 | .result("COMPLETE")
62 | )
63 |
64 | return [
65 | self.build_pattern_event(event_row)
66 | for i, event_row in passes_into_the_box.iterrows()
67 | ]
68 |
69 | def build_pattern_event(self, event_row) -> PatternEvent:
70 | pattern_event = self.from_event(event_row)
71 | pattern_event.add_arrow(event_row)
72 | pattern_event.add_pause(pause_time=2000)
73 |
74 | return pattern_event
75 | ```
76 |
77 | The above code produces this output when imported into Metrica Play:
78 |
79 |
"
138 | },
139 | "metadata": {},
140 | "execution_count": 6
141 | }
142 | ],
143 | "source": [
144 | "game_dataset.tracking.team('FIFATMA').players('field').dimension('x').head()"
145 | ]
146 | },
147 | {
148 | "source": [
149 | "## Events\n",
150 | "\n",
151 | "Similarly, GameDataset.events holds a TrackingFrame with all the tacking data of the game, and if you want to filter it, you can do so using it's methods. For example to get all event that go into the opponent box you can do `game_dataset.events.into(Zones.OPPONENT_BOX)`, or if you want to get all the passes you can do:"
152 | ],
153 | "cell_type": "markdown",
154 | "metadata": {}
155 | },
156 | {
157 | "cell_type": "code",
158 | "execution_count": 15,
159 | "metadata": {},
160 | "outputs": [
161 | {
162 | "output_type": "execute_result",
163 | "data": {
164 | "text/plain": " event_id event_type result success period_id timestamp end_timestamp \\\n1 None PASS COMPLETE True 1 14.44 15.08 \n3 None PASS COMPLETE True 1 15.36 17.04 \n5 None PASS COMPLETE True 1 18.60 20.28 \n7 None PASS COMPLETE True 1 21.20 23.20 \n9 None PASS COMPLETE True 1 23.92 25.12 \n\n ball_state ball_owning_team team_id player_id coordinates_x \\\n1 alive FIFATMA FIFATMA P3577 0.49875 \n3 alive FIFATMA FIFATMA P3574 0.50300 \n5 alive FIFATMA FIFATMA P3575 0.33014 \n7 alive FIFATMA FIFATMA P3569 0.19071 \n9 alive FIFATMA FIFATMA P3570 0.20244 \n\n coordinates_y end_coordinates_x end_coordinates_y receiver_player_id \\\n1 0.51275 0.50136 0.51295 P3574 \n3 0.51500 0.36627 0.36551 P3575 \n5 0.40293 0.19398 0.60179 P3569 \n7 0.57078 0.20094 0.18478 P3570 \n9 0.18002 0.31899 0.01941 P3571 \n\n inverted \n1 True \n3 True \n5 True \n7 True \n9 True ",
165 | "text/html": "
\n\n
\n \n
\n
\n
event_id
\n
event_type
\n
result
\n
success
\n
period_id
\n
timestamp
\n
end_timestamp
\n
ball_state
\n
ball_owning_team
\n
team_id
\n
player_id
\n
coordinates_x
\n
coordinates_y
\n
end_coordinates_x
\n
end_coordinates_y
\n
receiver_player_id
\n
inverted
\n
\n \n \n
\n
1
\n
None
\n
PASS
\n
COMPLETE
\n
True
\n
1
\n
14.44
\n
15.08
\n
alive
\n
FIFATMA
\n
FIFATMA
\n
P3577
\n
0.49875
\n
0.51275
\n
0.50136
\n
0.51295
\n
P3574
\n
True
\n
\n
\n
3
\n
None
\n
PASS
\n
COMPLETE
\n
True
\n
1
\n
15.36
\n
17.04
\n
alive
\n
FIFATMA
\n
FIFATMA
\n
P3574
\n
0.50300
\n
0.51500
\n
0.36627
\n
0.36551
\n
P3575
\n
True
\n
\n
\n
5
\n
None
\n
PASS
\n
COMPLETE
\n
True
\n
1
\n
18.60
\n
20.28
\n
alive
\n
FIFATMA
\n
FIFATMA
\n
P3575
\n
0.33014
\n
0.40293
\n
0.19398
\n
0.60179
\n
P3569
\n
True
\n
\n
\n
7
\n
None
\n
PASS
\n
COMPLETE
\n
True
\n
1
\n
21.20
\n
23.20
\n
alive
\n
FIFATMA
\n
FIFATMA
\n
P3569
\n
0.19071
\n
0.57078
\n
0.20094
\n
0.18478
\n
P3570
\n
True
\n
\n
\n
9
\n
None
\n
PASS
\n
COMPLETE
\n
True
\n
1
\n
23.92
\n
25.12
\n
alive
\n
FIFATMA
\n
FIFATMA
\n
P3570
\n
0.20244
\n
0.18002
\n
0.31899
\n
0.01941
\n
P3571
\n
True
\n
\n \n
\n
"
166 | },
167 | "metadata": {},
168 | "execution_count": 15
169 | }
170 | ],
171 | "source": [
172 | "game_dataset.events.type('PASS').head()"
173 | ]
174 | },
175 | {
176 | "cell_type": "markdown",
177 | "metadata": {},
178 | "source": [
179 | "Since in this game tracking and event daa come from the same provider, GameDataset.metadata for this case is the same as GameDataset.tracking.metadata and GameDataset.events.metadata. There ou can access metadata about the data like frame rate, field dimensions, teams and players details, etc. Like:"
180 | ]
181 | },
182 | {
183 | "cell_type": "code",
184 | "execution_count": 23,
185 | "metadata": {},
186 | "outputs": [
187 | {
188 | "output_type": "execute_result",
189 | "data": {
190 | "text/plain": "'Player 5'"
191 | },
192 | "metadata": {},
193 | "execution_count": 23
194 | }
195 | ],
196 | "source": [
197 | "game_dataset.metadata.teams[0].players[5].name"
198 | ]
199 | },
200 | {
201 | "cell_type": "code",
202 | "execution_count": 24,
203 | "metadata": {},
204 | "outputs": [
205 | {
206 | "output_type": "execute_result",
207 | "data": {
208 | "text/plain": "25"
209 | },
210 | "metadata": {},
211 | "execution_count": 24
212 | }
213 | ],
214 | "source": [
215 | "game_dataset.metadata.frame_rate"
216 | ]
217 | },
218 | {
219 | "cell_type": "code",
220 | "execution_count": 25,
221 | "metadata": {},
222 | "outputs": [
223 | {
224 | "output_type": "execute_result",
225 | "data": {
226 | "text/plain": "Score(home=0, away=2)"
227 | },
228 | "metadata": {},
229 | "execution_count": 25
230 | }
231 | ],
232 | "source": [
233 | "game_dataset.metadata.score"
234 | ]
235 | },
236 | {
237 | "source": [
238 | "For more details about metadata attributes and methods see [kloppy's documentation](https://kloppy.pysport.org/)."
239 | ],
240 | "cell_type": "markdown",
241 | "metadata": {}
242 | }
243 | ]
244 | }
--------------------------------------------------------------------------------
/docs/examples/output.patt:
--------------------------------------------------------------------------------
1 | {
2 | "events": [
3 | {
4 | "pattern_code": "MET_002",
5 | "start_time": 12000,
6 | "event_time": 14000,
7 | "end_time": 17000,
8 | "coordinates": [
9 | [
10 | 0.49875,
11 | 0.51275
12 | ],
13 | [
14 | 0.50136,
15 | 0.51295
16 | ]
17 | ],
18 | "visualizations": {
19 | "start_time": 12000,
20 | "end_time": 17000,
21 | "players": "P3577",
22 | "tool_id": "players",
23 | "options": {
24 | "id": false,
25 | "speed": false,
26 | "spotlight": true,
27 | "ring": false,
28 | "spotlightColor": "#FFFFFF",
29 | "ringColor": "#000000",
30 | "size": 1.0
31 | }
32 | },
33 | "tags": "FIFATMA"
34 | },
35 | {
36 | "pattern_code": "MET_002",
37 | "start_time": 43000,
38 | "event_time": 45000,
39 | "end_time": 48000,
40 | "coordinates": [
41 | [
42 | 0.5,
43 | 0.5
44 | ],
45 | [
46 | 0.65727,
47 | 0.09506
48 | ]
49 | ],
50 | "visualizations": {
51 | "start_time": 43000,
52 | "end_time": 48000,
53 | "players": "P3588",
54 | "tool_id": "players",
55 | "options": {
56 | "id": false,
57 | "speed": false,
58 | "spotlight": true,
59 | "ring": false,
60 | "spotlightColor": "#FFFFFF",
61 | "ringColor": "#000000",
62 | "size": 1.0
63 | }
64 | },
65 | "tags": "FIFATMB"
66 | }
67 | ],
68 | "insert": {
69 | "patterns": [
70 | {
71 | "name": "Team Stretched",
72 | "code": "MET_001"
73 | },
74 | {
75 | "name": "Set Pieces",
76 | "code": "MET_002"
77 | },
78 | {
79 | "name": "Passes into the box",
80 | "code": "MET_003"
81 | }
82 | ]
83 | }
84 | }
--------------------------------------------------------------------------------
/docs/examples/run_patterns.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "metadata": {
3 | "language_info": {
4 | "codemirror_mode": {
5 | "name": "ipython",
6 | "version": 3
7 | },
8 | "file_extension": ".py",
9 | "mimetype": "text/x-python",
10 | "name": "python",
11 | "nbconvert_exporter": "python",
12 | "pygments_lexer": "ipython3",
13 | "version": "3.8.2-final"
14 | },
15 | "orig_nbformat": 2,
16 | "kernelspec": {
17 | "name": "python_defaultSpec_1600218423960",
18 | "display_name": "Python 3.8.2 64-bit ('env': venv)"
19 | }
20 | },
21 | "nbformat": 4,
22 | "nbformat_minor": 2,
23 | "cells": [
24 | {
25 | "cell_type": "markdown",
26 | "metadata": {},
27 | "source": [
28 | "# Modules\n",
29 | "GameDataset is a class that will hold methods and data for one game. PatternsSet is a calss that willhold methods, patterns, and pattern_events data for this game. "
30 | ]
31 | },
32 | {
33 | "cell_type": "code",
34 | "execution_count": 3,
35 | "metadata": {},
36 | "outputs": [],
37 | "source": [
38 | "import sys\n",
39 | "sys.path.insert(1, '../../')\n",
40 | "\n",
41 | "from codeball import GameDataset, PatternsSet"
42 | ]
43 | },
44 | {
45 | "cell_type": "markdown",
46 | "metadata": {},
47 | "source": [
48 | "\n",
49 | "# Initialize GameDataset\n",
50 | "\n",
51 | "Define data files. Currently reading the files in the test folder of the package. Initialize game dataset. This loads the data for each data type using Kloppy, and then stores it on game_dataset as instances of TrackinDataFrame and EventsDataFrame. Both of them are subclasses of a pandas Dataframe. Other than holding the data in a dataframe, they also have methods to work with, filter etc the data they contain."
52 | ]
53 | },
54 | {
55 | "cell_type": "code",
56 | "execution_count": 5,
57 | "metadata": {},
58 | "outputs": [],
59 | "source": [
60 | "\n",
61 | "metadata_file = (r\"../../codeball/tests/files/metadata.xml\")\n",
62 | "tracking_file = (r\"../../codeball/tests/files/tracking.txt\")\n",
63 | "events_file = (r\"../../codeball/tests/files/events.json\")\n",
64 | "\n",
65 | "game_dataset = GameDataset(\n",
66 | " tracking_metadata_file=metadata_file,\n",
67 | " tracking_data_file=tracking_file,\n",
68 | " events_metadata_file=metadata_file,\n",
69 | " events_data_file=events_file,\n",
70 | ")"
71 | ]
72 | },
73 | {
74 | "cell_type": "markdown",
75 | "metadata": {},
76 | "source": [
77 | "# Instantiate PatternSet and run and export patterns for play. \n",
78 | "The first step is to instantiate a PatternsSet instance. It takes as arugment a GameDataset instance so that all patterns can have access to the data of the game. Conceptually a pattern is an analysis that will return the moments in the game a certain thing happend, with that thing being defined in the pattern / analysis. For example, look for all passes into the box. \n",
79 | "\n",
80 | "Next step is to initialize the patterns by reading the patterns config from ../codeball/patterns/patterns_config.json. However you can specify your own pattern config by providing it as an input to initialize_patterns. Then `run_patterns` iterates over all the patterns in the PatternSet and runs them. Finally, `save_patterns_for_play` method takes all the Patterns and PatternEvents in the PatternsSet and outputs them on a json fotmat that can be imported into Metrica Play via Metrica Cloud. \n",
81 | "\n",
82 | "If you didn't clone the repo, you can get the config file [here](https://github.com/metrica-sports/codeball/blob/master/codeball/patterns/patterns_config.json).\n"
83 | ]
84 | },
85 | {
86 | "cell_type": "code",
87 | "execution_count": 8,
88 | "metadata": {},
89 | "outputs": [],
90 | "source": [
91 | "patterns_set = PatternsSet(game_dataset=game_dataset)\n",
92 | "\n",
93 | "patterns_set.initialize_patterns(config_file=r\"../../codeball/patterns/patterns_config.json\")\n",
94 | " \n",
95 | "patterns_set.run_patterns()\n",
96 | "\n",
97 | "patterns_set.save_patterns_for_play(\"output.patt\")"
98 | ]
99 | }
100 | ]
101 | }
--------------------------------------------------------------------------------
/docs/format-for-play.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Patterns and events format for Play
4 | This documentation describes the API to import into import events with annotations associated to them into Play. We refer to this type of events as `patterns`. Patterns are imported into Play via a `json` file with a `.patt` extension. Below an explanation of how this file should be formatted. This documentation is compatible with versions **2.5.0 of Play by Metrica Sports** or higher. Old patterns files are still supported, see section [Versions](#versions).
5 |
6 | ## Format
7 |
8 | The patterns files is in JSON format. It has to main entry points: `events` and `insert`
9 |
10 | The **events** entry is an array containing all the events you detect for each game. Those events are going to be stored and associated to the game you are uploading them to.
11 |
12 | The **insert** entry is an object that will contain a declaration of `patterns` , `tags` and `tag_groups` you want to create. **It's important to notice that this will be created and store in the database and will be shared among all you games.**
13 |
14 | You don't need to create all the patterns, tags and groups each time. If you want to add them to all of your files, that's not a problem. As long the unique code you added to any of them is already in the database there will be no duplicates.
15 |
16 | You can use the **insert** field for updating your patterns, tags and tag_groups. More on that in the Patterns, Tags and Tag Groups sections.
17 |
18 | ```
19 | {
20 | "events": [
21 | // Here you'll list al events you want to create
22 | ],
23 | "insert": {
24 | "patterns": [
25 | // By listing patterns here you'll be able to create
26 | // and update patterns
27 | ],
28 | "tag_groups": [
29 | // By listing tag groups here you'll be able to create
30 | // and update tag groups
31 | ],
32 | "tags": [
33 | // By listing tags here you'll be able to create
34 | // and update tags
35 | ]
36 | }
37 | }
38 | ```
39 |
40 | ## Prefix
41 |
42 | Every time you want to create something in our database, like a Pattern, Tag Group or Tag you'll need to add a code to it. You can manage your codes any way you like as long you create unique codes for each resource. We'll provide you wit a **prefix** you'll need to add to your codes. If a resource you want to create doesn't have the appropriate prefix it will be omitted. Let's say you want to create some patterns and your prefix is **RCNG.** You could do something like `RCNG_001` and `RCNG_002` or `RCNG_COUNTER` and `RCNG_POSESSION`. It's up to you how you manage codes, but the prefix is mandatory.
43 |
44 | ## Patterns
45 |
46 | We call Pattern to a type of detection. Let's say you create an algorithm for detecting Counter-attacks. You'll create a Pattern called Counter-Attacks and each Counter-attack you detect will be an event associated to this Pattern.
47 |
48 | A Pattern needs to have a unique code and a name, that's it. If a code already exists on the database it would not create the pattern again it will update the name to what's written in the current file. It's important to remember that Patterns are shared by all your games, so updating the name will have an effect on previous and future games.
49 |
50 | ```
51 | "patterns": [
52 | {
53 | "name": "Counter Attack",
54 | "code": "RCNG_001"
55 | },
56 | {
57 | "name": "Defensive Positioning",
58 | "code": "RCNG_002"
59 | }
60 | ],
61 | ```
62 |
63 | This is how patterns will be listed in Play for each game:
64 |
65 | 
66 |
67 | ## Tags and Tag Groups
68 |
69 | For any given Pattern you create, most likely you'll want to create some Tags and Tag Groups as well. Let's say you have your Counter Attacks pattern and you want to be able to filter by "Fast" and "Slow", or "Successful" and "Failed". For that you can create Tags. And those Tags can be grouped. For example, "Fast" and "Slow" could be two tags on the group "Counter Speed".
70 |
71 | For creating a Tag Group you only need to add a unique code and a name.
72 |
73 | For creating a Tag you'll need to add a unique code, a name and the code of the group the tags belongs to.
74 |
75 | ```
76 | "tag_groups": [
77 | {
78 | "name": "Counter Speed",
79 | "code": "RCNG_GROUP_001"
80 | },
81 | {
82 | "name": "Counter Quality",
83 | "code": "RCNG_GROUP_002"
84 | }
85 | ],
86 | "tags": [
87 | {
88 | "name": "Fast",
89 | "code": "RCNG_TAG_001",
90 | "group": "RCNG_GROUP_001"
91 | },
92 | {
93 | "name": "Slow",
94 | "code": "RCNG_TAG_002",
95 | "group": "RCNG_GROUP_001"
96 | }
97 | ],
98 | ```
99 |
100 | You'll see Tags organized by groups in the filter section when a Pattern is selected
101 |
102 | 
103 |
104 | Once you create a Tag Group, and associate a Tag with it, the tag associated to a group will always be organized based no that group.
105 |
106 | An important point. Players and Team codes, for example **ESPBCN** or **P030** can be used as tags directly. So no need to create tags for teams and players. Just use them as any other tags you have created.
107 |
108 | ## Creating and updating
109 |
110 | This information about patterns, tags and tag groups, can be included every time the json is uploaded. However they are only needed when you want to create one of them, or when you want to update on of them (e.g. change name).
111 |
112 |
113 | # Events
114 |
115 | On event is one detection of a Pattern. Events happen at a given time in the video and have a duration and many other properties. An Events belongs to a Pattern and has Tags associated to it. This is how, when you select a Pattern you will see a list with all the Events belonging to that Pattern. And when you select an event you will see a list with all the tags added to that Event. This is how you construct events. There are three main categories of concepts. 1) One is the information about the event itself. 2) Another is the information about the annotations. 3) The last one is the tags and team information for filtering in Play.
116 |
117 | An example event with annotations looks like this:
118 |
119 | ```
120 | {
121 | "pattern": RCNG_PATTERN_001,
122 | "start_time": 5000,
123 | "event_time": 10000,
124 | "end_time": 25000,
125 | "coordinates": [
126 | [0.39,0.51],
127 | [0.44,0.42]
128 | ],
129 | "visualizations": {
130 | "start_time": 7000,
131 | "end_time": 23000,
132 | "players": "P030",
133 | "tool_id": "players",
134 | "options": {
135 | "speed": 1
136 | },
137 | "version": 2
138 | },
139 | "tags": [
140 | "ESPBCN",
141 | "P030",
142 | "RCGN_TAG_001",
143 | "RCGN_TAG_007"
144 | ],
145 | "team": "ESPBCN"
146 | }
147 | ```
148 |
149 | This is an example of a sprint type event which has a `speed` visualization.
150 |
151 | This is an event that belongs to the `pattern` code `RCNG_PATTERN_001`, that starts at time `5000` and ends at time `25000`, with the event indicator being located at `10000` in the timeline of the video. All times are in milliseconds.
152 |
153 | In this case, in Play the event will be located at time `10000` in the timeline that , but when you select it, the video will play from time `5000` to `25000`.
154 |
155 | Moreover, this event has coordinates. There are two options for coordinates. If you provide just a pair of xy coordinates, it will show a dot on the 2D field in Play. If you provide two pairs, it will show an arrow. In this case it will show an arrow in the 2D field (NOT in the video) going from `[0.39, 0.51]` to `[0.44, 0.42]`.
156 |
157 | This event also has a annotation. In this case, the speed will show up for this player from time `7000` to `23000` for player `P030`. To code for that, the `tool_id` is set to `players` type visualization that has `speed` as `1` (true). This visualization is also of `version = 2`, which means it's formatted for the new visualizations in version 2.5.0 of Play and onwards. See section [Versions](#versions).
158 |
159 | Finally, this event has the tags `"ESPBCN","P030",RCGN_TAG_001,RCGN_TAG_007` and belongs to the team `ESPBCN`.
160 |
161 | Below a summary of the information about fields related events and fields related to annotations.
162 |
163 | ## Fields common to all events
164 |
165 | ```
166 | {
167 | "pattern": 15, // Pattern code
168 | "start_time": 5000, // Number in milliseconds
169 | "event_time": 10000, // Number in milliseconds
170 | "end_time": 25000, // Number in milliseconds
171 | "coordinates": [ // In normalized coordinates. `null` if empty.
172 | [0.39,0.51],
173 | [0.44,0.42]
174 | ]
175 | "visualizations":[...], // See below. `null` if empty.
176 | "tags": [ // Codes of the tags. `[]` if empty.
177 | "ESPBCN",
178 | "P030"
179 | ],
180 | "team": "ESPBCN" // `null` if empty.
181 | }
182 | ```
183 |
184 |
185 | ## Fields common to all visualizations
186 |
187 | The following attributes have to be defined for each tool. Annotations' order matters, they will be created and displayed in the same order they are declared, first annotation will be rendered at the bottom/background.
188 | ```
189 | {
190 | start_time : 1040, // Milliseconds
191 | end_time : 2130, // Milliseconds
192 | tool_id : 'players', // The ID of the tool
193 | ... // Each tool could have other mandatory attributes (players, team, points, etc.)
194 | options : {}, // Optional object attribute for the tool
195 | version : 2 // Which version of API it's the viz compatible with
196 | }
197 | ```
198 |
199 | ## Versions
200 | In version **2.5.0** of Play by Metrica Sports we introduced [new and improved visualizations](https://metrica-sports.com/a-wonderful-day-for-play/). These new visualizations add several new parameters and options to the visualizations, thus the API for adding visualizations has changed. In particular around the available options for each type of visualization.
201 |
202 | For example whereas on the original version (version 1), to define a ring you would do:
203 | ```
204 | {
205 | start_time : 1040,
206 | end_time : 2130,
207 | tool_id : 'players',
208 | players : ['P001', 'P002'],
209 | options : { ring: true, color: '#FFFFFF' },
210 | }
211 | ```
212 |
213 | On the new version (version 2) rings have a border and a fill that can de defined separately. So to define a ring you need to do:
214 | ```
215 | {
216 | start_time : 1040,
217 | end_time : 2130,
218 | tool_id : 'players',
219 | players : ['P001', 'P002'],
220 | options : { ringFill: true, ringBorder: true, ringFillColor: '#FFFFFF', ringBorderColor: '#FFFF00' },
221 | version : 2
222 | }
223 | ```
224 |
225 |
226 | Note that in the last one we noted that this visualization is of version 2 so that the API know how to read this visualization. However, while different in formats, the API on version 2.5.0 onwards is compatible with older versions. If a visualization doesn't have a version indicated in the options it will be assumed it's from version 2, and the options will be populated accordingly. For example, if you import the above example of a version 1 ring, it would be like importing:
227 | ```
228 |
229 | {
230 | start_time : 1040,
231 | end_time : 2130,
232 | tool_id : 'players',
233 | players : ['P001', 'P002'],
234 | options : { ringFill: true, ringBorder: true, ringFillColor: '#FFFFFF', ringBorderColor: '#FFFFFF' },
235 | version : 2
236 | }
237 | ```
--------------------------------------------------------------------------------
/docs/game-dataset.md:
--------------------------------------------------------------------------------
1 | # What's a GameDataset?
2 |
3 | A GameDataset is a class that serves 2 purposes:
4 |
5 | 1. Hold CodeballFrames for tracking, event, and other data types
6 | 2. Provide methods to enrich those CodeballFrames
7 | 3. Provide auxiliary methods to process and handle data that require information from the game_dataset (e.g. frame rate)
8 |
9 | ## Attributes
10 |
11 | * ***tracking***: contains a `TrackingFrame`
12 | * ***events***: contains an `EventsFrame`
13 |
14 | ## Properties
15 |
16 | * ***game_dataset_type***: return an Enum with the type of the dataset, which could be [ONLY_TRACKING, ONLY_EVENTS, FULL_SAME_PROVIDER, FULL_MIXED_PROVIDERS]
17 | * ***metadata***: the metadata of the dataset (unless it's a FULL_MIXED_PROVIDERS type) that comes from loading the data with kloppy.
18 |
19 | ## Enrichment methods
20 |
21 | There is a main method ***_enrich_data*** that runs all the below:
22 |
23 | * ***_build_possessions***
24 | * ***_set_periods_attacking_direction***
25 | * ***_enrich_events***
26 | * ***_enrich_tracking***
27 |
28 | ## Auxiliar methods
29 | * ***find_interval***: given a Series of bool values computes the intervals of True values.
30 | * ***frame_to_milliseconds***: given a frame number it returns the value in milliseconds of that moment in the video.
31 |
32 |
--------------------------------------------------------------------------------
/docs/how-to-import-to-play.md:
--------------------------------------------------------------------------------
1 |
2 | Once you have created a .patt file, you can import it to Metrica Play via [Metrica Cloud](https://cloud.metrica-play.com/). To do so you need to select the Video Project you want to upload a file and upload it under the more options section (three dots all the way to the left of the Video Project). You'll get a notification informing you if the upload was or wasn't successful.
3 |
4 | Once the file is uploaded, you can go to Metrica Play and download the Video Project from the DB Manager. If you already did that, you can:
5 |
6 | 1. Select the Video Project in the Video manager, and then on Pattern Events more options select Retry download.
7 | 2. Close and open the application again and will get a notification informing you there is a new file and you can click directly there to download it.
8 |
9 | If you had uploaded a file already and want to upload a new one, go to Cloud, delete the previously uploaded file and do any of the two steps described above.
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # codeball: data driven tactical and video analysis of soccer games
2 |
3 | [](https://pypi.org/project/codeball/)
4 | [](https://pepy.tech/project/codeball)
5 | 
6 | 
7 | [](https://metrica-sports.com/)
8 | --------
9 |
10 | ## Why codeball?
11 |
12 | While there are several pieces of code / repositories around that provide different tools and bits of codes to do tactical analysis of individual games, there is no centralized place in which they live. Moreover, most of the analysis done is usually not linked or easy to link with the actual footage of the match. Codeball's objective is to change that by:
13 |
14 | 1. Building a central repository for different types of data driven tactical analysis methods / tools.
15 | 2. Making it easy to link those analyses with a video of the game in different formats.
16 |
17 | ## What can you do with it
18 |
19 | The main types of work / development you can do with codeball are:
20 |
21 | #### Work with tracking and event data
22 |
23 | - Codeball creates subclasses of *Pandas DataFrames* for events and tracking data; and provides you with handy methods to work with the data.
24 | - Work with or create your own tactical models like *Zones* so that you can for example do `game_dataset.events.into(Zones.OPPONENT_BOX)` and it will return a DataFrame only with the events into the opponents box. You can also chain methods, like `game_dataset.events.type("PASS").into(Zones.OPPONENT_BOX)` and will return only passes into the box. Or for example do `game_dataset.tracking.team('FIFATMA').players('field').dimension('x')` to get the x coordinate of the field players (no goalkeeper data) for team with id FIFATMA.
25 | - [Not yet implemented] Easily access tactical tools or methods like computing passes networks, pitch control,EPV models, etc
26 |
27 | #### Create Patterns to analyze the game
28 |
29 | - Analyze games based on Patterns. A Pattern is a unit of analysis that looks for moments in the game in which a certain thing happens. That certain thing is defined inside the Pattern, but codeball provides tools to easily create them, configure them and export them in different formats for different platforms.
30 | - You can create your own patterns, or also use the ones provided with the package and configure them to your liking.
31 |
32 | #### Add annotations to the events for Metrica Play
33 |
34 | - Codeball incorporates all the annotations models and API information needed to import events with annotations into Metrica Play.
35 | - You can add directly from the code any visualization available in Metrica Play (spotlights, rings, future trail, areas, drawings, text, etc) to any event.
36 |
37 | ## Example
38 |
39 | You can use any of the above functionality independently. However they are most powerful when combined. As an example, the below code defines a pattern that will look for all passes into the opponent's box. Moreover to be imported into Metrica Play, it will add an arrow and a 2s pause in the video at the moment of the pass, and will add an arrow to the 2D field indicating start and end position of the pass.
40 |
41 | ```python
42 | class PassesIntoTheBox(Pattern):
43 | def __init__(
44 | self,
45 | game_dataset: GameDataset,
46 | name: str,
47 | code: str,
48 | in_time: int = 0,
49 | out_time: int = 0,
50 | parameters: dict = None,
51 | ):
52 | super().__init__(
53 | name, code, in_time, out_time, parameters, game_dataset
54 | )
55 |
56 | def run(self) -> List[PatternEvent]:
57 |
58 | passes_into_the_box = (
59 | self.game_dataset.events.type("PASS")
60 | .into(Zones.OPPONENT_BOX)
61 | .result("COMPLETE")
62 | )
63 |
64 | return [
65 | self.build_pattern_event(event_row)
66 | for i, event_row in passes_into_the_box.iterrows()
67 | ]
68 |
69 | def build_pattern_event(self, event_row) -> PatternEvent:
70 | pattern_event = self.from_event(event_row)
71 | pattern_event.add_arrow(event_row)
72 | pattern_event.add_pause(pause_time=2000)
73 |
74 | return pattern_event
75 | ```
76 |
77 | The above code produces this output when imported into Metrica Play:
78 |
79 |
80 |
81 |
82 |
83 | ## Supported Data Providers
84 |
85 | This package is very much WIP. At the moment it only works based on Metrica Sports Elite datasets. However, it uses Kloppy to read in the data so that in the near future will support data from any provider.
86 |
87 | ## Trying it out
88 |
89 | There are no open source Elite datasets at the moment that work with this package. However if you are interested in testing it out and/or developing your own patterns and/or test them in Metrica Play reach out to bruno@metrica-sports.com or [@brunodagnino](https://twitter.com/brunodagnino) on Twitter.
90 |
91 | ## Install it
92 |
93 | Installers for the latest released version are available at the [Python package index](https://pypi.org/project/codeball).
94 |
95 | ```sh
96 | pip install codeball
97 | ```
98 |
99 | ## Contribute
100 |
101 | While created and maintained by Metrica Sports, it's distributed under an MIT license and it welcomes contributions from members of the community, clubs and other companies. You can find the repository on [Github](https://github.com/metrica-sports/codeball). Also, if you have ideas for patterns we should implement, or methods we should include (e.g. pitch control, EPV, similarity search, etc), let us know! You can create an issue on the repo, or reach out to bruno@metrica-sports.com or [@brunodagnino](https://twitter.com/brunodagnino) on Twitter.
102 |
103 | ## Documentation
104 |
105 | Check the [documentation](https://codeball.metrica-sports.com) for a more detailed explanation of this package.
106 |
107 | ## Tentative TODO
108 |
109 | This is a very incomplete list of the things we have in mind, and it will probably change as we get input from the community / users. However it gives you a rough idea of the direction in which we want to go with this project!
110 |
111 | * more Zones (half spaces, thirds, 14, etc) - [done]
112 | * crete types for players, events, etc to filter the data.
113 | * more ways to filter event and tracking data (e.g pass length)
114 | * more patterns (currently 4 in the making)
115 | * pitch control from `game_dataset.pitch_control([frame/s])`, same with EPV.
116 | * easily query xG, g+, xT, etc for events
117 | * corner strategy classifier.
118 | * support for other providers, likely StatsBomb next.
119 | * export events in xml format
120 | * methods to easily sync tracking and event from different providers.
121 | * any suggestions?
--------------------------------------------------------------------------------
/docs/media/passes_into_the_box.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/metrica-sports/codeball/06161ac69f9b7b53e3820537e780ab962c4349e6/docs/media/passes_into_the_box.gif
--------------------------------------------------------------------------------
/docs/media/set_pieces.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/metrica-sports/codeball/06161ac69f9b7b53e3820537e780ab962c4349e6/docs/media/set_pieces.gif
--------------------------------------------------------------------------------
/docs/media/sprint.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/metrica-sports/codeball/06161ac69f9b7b53e3820537e780ab962c4349e6/docs/media/sprint.gif
--------------------------------------------------------------------------------
/docs/media/team_stretched.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/metrica-sports/codeball/06161ac69f9b7b53e3820537e780ab962c4349e6/docs/media/team_stretched.gif
--------------------------------------------------------------------------------
/docs/metrica-play-api.md:
--------------------------------------------------------------------------------
1 | # What can you do with Play's API?
2 |
3 | > Note that to import a pattern file to Play you do not need to use codeball. As long as the file you import is formatted as this documentation indicates you'll be able to import the file. How you create that file can be any language, any library. Codeball is one way to do it, but not the only one! Feel free to use whatever language or library/is you feel more comfortable with!
4 |
5 | Play API for patterns and visualizations allows you to import patterns and events into Play. The interesting part is that not only it allows you to import events that have a start/end time, tags, etc; but also to add visualizations directly from the code. For example the following json code:
6 |
7 | ```json
8 | {
9 | "pattern": "RCNG_PATTERN_001",
10 | "start_time": 5000,
11 | "event_time": 10000,
12 | "end_time": 25000,
13 | "coordinates": [
14 | [0.39,0.51],
15 | [0.44,0.42]
16 | ],
17 | "visualizations": {
18 | "start_time": 7000,
19 | "end_time": 23000,
20 | "players": "P030",
21 | "tool_id": "players",
22 | "options": {
23 | "speed": 1
24 | },
25 | "version": 2
26 | },
27 | "tags": [
28 | "ESPBCN",
29 | "P030",
30 | "RCGN_TAG_001",
31 | "RCGN_TAG_007"
32 | ],
33 | "team": "ESPBCN"
34 | }
35 | ```
36 |
37 | This is an example of a sprint type event which has a `speed` visualization. This event belongs to the `pattern` code `RCNG_PATTERN_001`, that starts at time `5000` and ends at time `25000`, with the event indicator being located at `10000` in the timeline of the video. All times are in milliseconds. In this case, in Play the event will be located at time `10000` in the timeline that , but when you select it, the video will play from time `5000` to `25000`.
38 |
39 | Moreover, this event has coordinates. There are two options for coordinates. If you provide just a pair of xy coordinates, it will show a dot on the 2D field in Play. If you provide two pairs, it will show an arrow. In this case it will show an arrow in the 2D field (NOT in the video) going from `[0.39, 0.51]` to `[0.44, 0.42]`.
40 |
41 | This event also has a annotation. In this case, the speed will show up for this player from time `7000` to `23000` for player `P030`. To code for that, the `tool_id` is set to `players` type visualization that has `speed` as `1` (true). This visualization is also of version = 2, which means it's formatted for the new visualizations in version 2.5.0 of Play and onwards. See section Versions.
42 |
43 | Finally, this event has the tags `"ESPBCN","P030",RCGN_TAG_001,RCGN_TAG_007` and belongs to the team `ESPBCN`.
44 |
45 | And by producing this file and importing it to Play via Metrica Cloud, you'll see this result automatically.
46 |
47 |
48 |
49 |
50 |
51 | # Import to Play
52 | To import a json file to Play, you have to do it via the video project created in Metrica Cloud. To do so go to this option on your video project:
53 |
--------------------------------------------------------------------------------
/docs/patterns.md:
--------------------------------------------------------------------------------
1 | ## Configuration
2 |
3 | All the patterns below, are available included in Codeball and ready to be used. They have a default configuration included with the package, but you can create your own config file if you want to change for example, the name of the patterns, the in and out time, or the parameters the use to compute events. The configuration for a pattern looks like the below. For more details check the example pattern in the Examples section.
4 |
5 | ```json
6 | {
7 | "include": true,
8 | "name": "Passes into the box",
9 | "code": "MET_003",
10 | "pattern_class": "PassesIntoTheBox",
11 | "parameters": null,
12 | "in_time": 2,
13 | "out_time": 2
14 | }
15 | ```
16 |
17 | ****
18 |
19 | ## Available patterns
20 |
21 | ### TeamStretched
22 |
23 | This pattern looks for moments in which the team is stretched horizontally while defending for more than 5 seconds. It returns those moments with a TeamSize length visualization for the duration of the infringement. This pattern doesn't add anything on the 2D field.
24 |
25 | Parameters:
26 |
27 | * team: str -> Code of the team you want to analyze
28 | * threshold: float -> What is the stretch threshold in meters
29 |
30 | In this example the threshold is at 35 meters.
31 |
32 |
33 |
34 |
35 |
36 | ****
37 |
38 | ### SetPieces
39 |
40 | This pattern return set Pieces include: kick offs, throw ins, corner kicks penalties, free kicks. Beside indicating the moment of the game in which they tke place, it adds a spotlight on the player tacking the set piece. This pattern also adds a dot on the 2D field for each event.
41 |
42 |
43 |
44 |
45 |
46 | ****
47 |
48 | ### PassesIntoTheBox
49 |
50 | This pattern finds completed passes into the opponent box. For each one of those passes, it creates a pattern event that at the moment of the pass makes a 2s pause and draws an arrow on the video showing the pass. This pattern also adds an arrow on the 2D field for each event.
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/docs/tactical.md:
--------------------------------------------------------------------------------
1 | # Tactical Module
2 | The tactical module includes different classes and methods to help work with the data from a tactical perspective.
3 |
4 | ## Areas
5 | An Area is a class used to define an area of the pitch. You can define simple rectangular areas, or more complex polygonal ones. To define an area you need to provide 2 or more points (in normalized coordinates, for example (0.5,0.5) would be the center of the pitch). If you provide only two points it will define a rectangular area, in which the first point is the top-left one and the second one is the bottom-right one. If you provide 3 or more points it will be consider a polygon.
6 |
7 | Areas have a `type` attribute that will return an Enum with either `AreaType.RECTANGLE` or `AreaType.POLYGON`.
8 |
9 | ## Zones
10 | Zones is an Enum that defines a list of zones that are tactically relevant. Each type could be defined by one or more `Area`. The list of currently defined Zones is:
11 |
12 | - OPPONENT_BOX
13 | - OWN_BOX
14 | - ATTACKING_THIRD
15 | - MIDDLE_THIRD
16 | - DEFENDING_THIRD
17 | - OWN_HALF
18 | - OPPONENT_HALF
19 | - LEFT_HALF_SPACE
20 | - RIGHT_HALF_SPACE
21 | - HALF_SPACES
22 | - CENTRE
23 | - LEFT_WING
24 | - RIGHT_WING
25 | - WINGS
26 | - ZONE_14
27 |
28 | ## Using Areas and Zones
29 | You can use Areas and Zones in the methods provided by `CodeballFrames`. For example you can do:
30 | ```python
31 | EventsFrame.into(Zones.HALF_SPACES)
32 | ```
33 |
34 | Or you can define your own areas and then provide them as filters:
35 | ```python
36 | custom_area = Area((0.16,0),(0.84,1))
37 | EventsFrame.into(custom_area)
38 | ```
39 |
40 | Finally, you can combine and provide one or more areas/zones, or a mix of zones and areas:
41 | ```python
42 | custom_area = Area((0.16,0),(0.84,1))
43 | EventsFrame.into(Zones.HALF_SPACES,custom_area)
44 | ```
--------------------------------------------------------------------------------
/docs/visualizations.md:
--------------------------------------------------------------------------------
1 | # Visualizations types and settings
2 | This section describes all the possible visualizations that can be added to an event, as well as the API to be imported into Play. For each one of these possibilities, there is a dataclass defined in `visualizations.py` so that they can be easily added from the code.
3 |
4 | ## Fields common to all visualizations
5 |
6 | The following attributes have to be defined for each tool. Annotations' order matters, they will be created and displayed in the same order they are declared, first annotation will be rendered at the bottom/background.
7 | ```
8 | {
9 | start_time: 1040, // Milliseconds
10 | end_time: 2130, // Milliseconds
11 | tool_id: 'players', // The ID of the tool
12 | ... // Each tool could have other mandatory attributes
13 | options: {}, // Optional object attribute for the tool
14 | version: 2 // Which version of API it's the viz compatible with
15 | }
16 | ```
17 |
18 | ## Tools
19 | This is a list of all the tools that is possible to add as annotations. Each option of each tool is optional. If you don't include some of them, default value will be used. The `options` attribute and the children of it are optional. If you don't include some or all of them, default values will be used
20 |
21 | ### Players
22 | The `tool_id` is `players`.
23 |
24 | **Players**
25 | ```
26 | players: ['P001', 'P002']
27 | ```
28 |
29 | **Options**
30 | ```
31 | options: {
32 | id: false,
33 | speed: false,
34 | size: 1.0, // [0.2, 2.5]
35 | color: '#000000',
36 | boxPositionDown: false,
37 | spotlight: false,
38 | spotlightSize: 0.5, // Multiplier [0.2, 4.0]
39 | spotlightColor: '#FFFFFF',
40 | spotlightOpacity: 0.43, // [0.0, 1.0]
41 | spotlightHeight: 2.0, // [0.1, 10.0]
42 | ringSize: 0.73,
43 | ringBorder: false,
44 | ringBorderColor: '#FFFFFF',
45 | ringFill: false,
46 | ringFillColor: '#DC3322',
47 | is3d: false
48 | }
49 |
50 | ```
51 | ***
52 | ### Trails
53 | The `tool_id` is `trails`.
54 |
55 | **Players**
56 | ```
57 | players: ['P001', 'P002']
58 | ```
59 |
60 | **Options**
61 | ```
62 | options: {
63 | color: '#0062ad',
64 | continuous: true,
65 | dotted: false,
66 | dashSize: 1.0, // Multiplier [0.2, 2.5]. Only Dotted
67 | is3d: false,
68 | ringBorder: true,
69 | offsetOpacity: 0.26, // [0.0, 1.0]
70 | opacity: 1.0, // [0.0, 1.0]
71 | ringBorderColor: "#ffffff",
72 | ringFill: true,
73 | ringFillColor: '#009cdd',
74 | ringSize: 1.0, // Multiplier [0.6, 4.0]
75 | seconds: 5.0, // [1.0, 99.0]
76 | thickness: 0.1, // Multiplier [0.1, 5.0]. Only in 3D
77 | width: 0.24 // Multiplier [0.1, 2.0]
78 | }
79 | ```
80 | ***
81 | ### Future Trails
82 | The `tool_id` is `futureTrails`.
83 |
84 | **Players**
85 | ```
86 | players: ['P001', 'P002']
87 | ```
88 |
89 | **Options**
90 | ```
91 | options: {
92 | color: '#ff9e2d',
93 | continuous: true,
94 | dashSize: 0.6, // Multiplier [0.2, 2.5]. Only Dotted
95 | dotted: false,
96 | is3d: false,
97 | offsetOpacity: 0.05, // [0.0, 1.0]
98 | opacity: 1.0, // [0.0, 1.0]
99 | ringBorder: true,
100 | ringBorderColor: "#ffffff",
101 | ringFill: true,
102 | ringFillColor: '#ffdc3a',
103 | ringSize: 1.0, // Multiplier [0.6, 4.0]
104 | seconds: 5.0, // [1.0, 99.0]
105 | thickness: 0.23, // Multiplier [0.1, 5.0]. Only in 3D
106 | width: 0.29 // Multiplier [0.1, 2.0]
107 | }
108 | ```
109 | ***
110 | ### Magnifiers
111 | The `tool_id` is `magnifiers`.
112 |
113 | **Players**
114 | ```
115 | players: ['P001', 'P002']
116 | ```
117 |
118 | **Options**
119 | ```
120 | options: {
121 | color: '#ffffff',
122 | zoom: 1.0, // [0.2, 1.5]
123 | size: 1.0 // [0.5, 1.5]
124 | }
125 | ```
126 | ***
127 | ### Measurer
128 | The `tool_id` is `measurer`.
129 |
130 | **Players**
131 | ```
132 | players: ['P001', 'P002']
133 | ```
134 |
135 | **Options**
136 | ```
137 | options: {
138 | borderColor: '#dc3322',
139 | borderEdgeOpacity: 0.4, // [0.0, 1.0]
140 | borderOpacity: 0.9, // [0.0, 1.0]
141 | closed: false,
142 | continuous: true,
143 | dashSize: 1.45, // Multiplier [0.2, 2.5]. Only Dotted
144 | distance: true,
145 | distanceColor: '#ffffff',
146 | distanceIs3d: false,
147 | distancePosition: 0.92, // Multiplier [0.5, 2.0]
148 | distanceOpacity: 1.0, // [0.0, 1.0]
149 | distanceSize: 1.01, // Multiplier [0.5, 1.5]
150 | dotted: false,
151 | fillColor: '#dc3322', Only Closed
152 | fillOpacity: 0.42, // [0.0, 1.0]
153 | is3d: false,
154 | ringBorder: true,
155 | ringBorderColor: '#ffffff',
156 | ringFill: true,
157 | ringFillColor: '#dc3322',
158 | ringSize: 0.91, // Multiplier [0.6, 4.0]
159 | thickness: 0.13, // Multiplier [0.0, 5.0]. Only in 3D
160 | width: 0.23, // Multiplier [0.15, 2.0]
161 | }
162 | ```
163 | ***
164 | ### Team Size
165 | The `tool_id` is `teamSize`.
166 |
167 | **Team**
168 | ```
169 | team: 'T001'
170 | ```
171 |
172 | **Line**
173 | ```
174 | line: 'width' // Values: 'width' or 'length'
175 | ```
176 |
177 | **Options**
178 | ```
179 | options: {
180 | continuous: true,
181 | dotted: false,
182 | color: '#683391',
183 | x: 0.0,
184 | y: 0.0,
185 | width: 0.23, // [0.1, 5.0]
186 | edgeOpacity: 0.0, // [0.0, 1.0]
187 | opacity: 1.0, // [0.0, 1.0]
188 | thickness: 0.22, // Multiplier [0.0, 5.0]. Only in 3D
189 | dashSize: 0.6, // Multiplier [0.2, 2.5]. Only Dotted
190 | distance: true,
191 | distanceColor: '#ffffff',
192 | distancePosition: 1.12, // [0.5, 2.0]
193 | distanceOpacity: 1.0, // Multiplier [0.0, 1.0]
194 | distanceSize: 1.3, // Multiplier [0.5, 1.5]
195 | distanceIs3d: false,
196 | is3d: false
197 | }
198 | ```
199 | ***
200 | ### Tactical Lines
201 | The `tool_id` is `tacticalLines`.
202 |
203 | **Team**
204 | ```
205 | team: 'T001'
206 | ```
207 |
208 | **Line**
209 | ```
210 | line: 'defenders' // Values: 'defenders', 'midfielders' or 'strikers'
211 | ```
212 |
213 | **Options**
214 | ```
215 | options: {
216 | borderColor: "#ffffff",
217 | borderEdgeOpacity: 0.3, // [0.1, 1.0]
218 | borderOpacity: 1.0, // [0.0, 1.0]
219 | fillColor: "#ffffff",
220 | fillOpacity: 0.4, // [0.0, 1.0]
221 | closed: false, // Only used when line is 'midfielders'
222 | width: 0.23, // [0.1, 2.0]
223 | thickness: 0.3, // Multiplier [0.0, 5.0]. Only in 3D
224 | dashSize: 1.0, // Multiplier [0.2, 2.5]. Only Dotted
225 | continuous: true,
226 | dotted: false,
227 | is3d: false,
228 | ringBorder: true,
229 | ringBorderColor: "#ffffff",
230 | ringFill: true,
231 | ringFillColor: "#ffffff",
232 | ringSize: 0.6, // [0.6, 4.0]
233 | distance: true,
234 | distanceColor: "#ffffff",
235 | distancePosition: 0.5, // Multiplier [0.5, 2.0]
236 | distanceOpacity: 1.0, // [0.0, 1.0]
237 | distanceSize: 0.73, // [0.5, 1.5]
238 | distanceIs3d: false
239 | }
240 | ```
241 | ***
242 | ### Line 3D
243 | The `tool_id` is `line3d`.
244 |
245 | **Points**
246 | ```
247 | // Normalized
248 | points: {
249 | start: { x: 0.0, y: 0.0 },
250 | end: { x: 0.0, y: 0.0 }
251 | }
252 | ```
253 |
254 | **Options**
255 | ```
256 | options: {
257 | arrowheadWidth: 1.5, // [0.99, 2.0]
258 | color: '#ff4f43',
259 | continuous: true,
260 | curvature: 0.0, // [-1.0, 1.0]
261 | dashSize: 0.4, // Multiplier [0.2, 2.5]
262 | distance: false,
263 | distanceColor: '#ffffff',
264 | distancePosition: 0.92, // Multiplier [0.5, 2.0]
265 | distanceOpacity: 1.0, // [0.0, 1.0]
266 | distanceSize: 1.01, // Multiplier [0.5, 4.0]
267 | distanceIs3d: false,
268 | dotted: false,
269 | dynamic: false,
270 | edgeOpacity: 0.2, // [0.0, 1.0]
271 | opacity: 0.9, // [0.0, 1.0]
272 | height: 0.075, // [0.0, 0.15]
273 | heightCenter: 0.0, // [-1.0, 1.0]
274 | is3d: false,
275 | pinned: false,
276 | thickness: 0.15, // Multiplier [0.0, 5.0]
277 | width: 0.5 // [0.1, 5.0]
278 | }
279 | ```
280 | ***
281 | ### Freehand
282 | The `tool_id` is `freehand`.
283 |
284 | **Points**
285 | ```
286 | // Normalized: screen-space or field-space if 'is3d' is enabled.
287 | points: [
288 | { x: 0.0, y: 0.0 },
289 | ...,
290 | { x: 0.0, y: 0.0 }
291 | ]
292 | ```
293 |
294 | **Options**
295 | ```
296 | options: {
297 | color: '#9edd34',
298 | arrowheadWidth: 3.0, // [0.99, 5.0]
299 | continuous: true,
300 | dashSize: 0.5, // Multiplier [0.2, 2.0]. Only Dotted
301 | dotted: false,
302 | offsetOpacity: 0.2, // [0.0, 1.0]
303 | opacity: 0.9, // [0.0, 1.0]
304 | is3d: false,
305 | pinned: false,
306 | thickness: 0.07, // Multiplier [0.0, 5.0]. Only in 3D
307 | width: 0.2 // [0.1, 0.2]
308 | }
309 | ```
310 | ***
311 | ### Circle
312 | The `tool_id` is `circle`.
313 |
314 | **Center and Radius**
315 | ```
316 | // Normalized: screen-space or field-space if 'is3d' is enabled.
317 | center: { x: 0.0, y: 0.0 },
318 | radius: { x: 0.1, y: 0.1 }
319 | ```
320 |
321 | **Options**
322 | ```
323 | options: {
324 | borderColor: '#b3b3b3',
325 | borderOpacity: 1.0, // [0.0, 1.0]
326 | fillColor: '#ffdc3a',
327 | fillOpacity: 0.26, // [0.0, 1.0]
328 | fillSolid: true,
329 | fillPattern: false,
330 | is3d: false,
331 | pinned: false,
332 | thickness: 0.5, // Multiplier [0.0, 5.0]. Only in 3D
333 | width: 0.62 // [0.0, 3.0]
334 | }
335 | ```
336 | ***
337 | ### Shape
338 | The `tool_id` is `shape`.
339 |
340 | **Points**
341 | ```
342 | // Normalized: screen-space or field-space if 'is3d' is enabled.
343 | points: [
344 | { x: 0.0, y: 0.0 },
345 | ...,
346 | { x: 0.0, y: 0.0 }
347 | ]
348 | ```
349 |
350 | **Options**
351 | ```
352 | options: {
353 | borderColor: '#0062ad',
354 | borderOpacity: 1.0, // [0.1, 1.0]
355 | borderContinuous: false,
356 | borderDotted: true,
357 | closed: false,
358 | dashSize: 0.6, // Multiplier [0.5, 1.5]. Only Dotted
359 | distance: true,
360 | distanceColor: "#ffffff",
361 | distancePosition: 1.0, // Multiplier [0.5, 2.0]
362 | distanceOpacity: 1.0, // [0.0, 1.0]
363 | distanceSize: 1.0, // [0.5, 1.5]
364 | distanceIs3d: false,
365 | fillColor: '#0062ad',
366 | fillOpacity: 0.25, // [0.0, 1.0]
367 | fillSolid: true,
368 | fillPattern: false,
369 | is3d: false,
370 | pinned: false,
371 | thickness: 0.1, // Multiplier [0.0, 5.0]. Only in 3D
372 | width: 0.15 // [0.0, 2.0]
373 | }
374 | ```
375 | ***
376 | ### Arrow
377 | The `tool_id` is `arrow`.
378 |
379 | **Points**
380 | ```
381 | // Normalized: screen-space or field-space if 'is3d' is enabled.
382 | points: {
383 | start: { x: 0.0, y: 0.0 },
384 | end: { x: 0.0, y: 0.0 }
385 | }
386 | ```
387 |
388 | **Options**
389 | ```
390 | options: {
391 | arrowheadWidth: 1.5, // [0.99, 2.0]
392 | color: '#ff4f43',
393 | continuous: true,
394 | curvature: 0.0, // Multiplier [-1.0, 1.0]. Only in 3D
395 | dashSize: 0.4, // Multiplier [0.2, 2.5]. Only Dotted
396 | distance: false,
397 | distanceColor: "#ffffff",
398 | distancePosition: 0.92, // Multiplier [0.5, 2.0]
399 | distanceOpacity: 1.0, // [0.0, 1.0]
400 | distanceSize: 1.01, // [0.5, 4.0]
401 | distanceIs3d: false,
402 | dotted: false,
403 | dynamic: false,
404 | edgeOpacity: 0.2, // [0.0, 1.0]
405 | opacity: 0.9, // [0.0, 1.0]
406 | height: 0.0, // [0.0, 0.15]
407 | heightCenter: 0.0, // [-1.0, 1.0]
408 | is3d: false,
409 | pinned: false,
410 | thickness: 0.15, // Multiplier [0.0, 5.0]. Only in 3D
411 | width: 0.5 // [0.1, 5.0]
412 | }
413 | ```
414 | ***
415 | ### Dragger
416 | The `tool_id` is `dragger`.
417 |
418 | **Points**
419 | ```
420 | // Normalized: screen-space only. Flag 'is3d' is only applied to arrow.
421 | points: {
422 | start: { x: 0.0, y: 0.0 },
423 | end: { x: 0.0, y: 0.0 }
424 | }
425 | ```
426 |
427 | **Options**
428 | ```
429 | options: {
430 | arrowColor: '#ffdc3a',
431 | arrowContinuous: true,
432 | arrowDashSize: 0.6, // Multiplier [0.6, 2.5]. Only arrowDotted
433 | arrowDotted: false,
434 | arrowDynamic: false,
435 | arrowheadWidth: 2.0, // [0.0, 2.0]
436 | arrowEdgeOpacity: 0.2, // [0.0, 1.0]
437 | arrowOpacity: 0.9, // [0.0, 1.0]
438 | arrowOffsetY: 0.46, // [-1.0, 1.0]
439 | arrowThickness: 0.23, // Multiplier [0.0, 5.0]. Only in 3D
440 | arrowWidth: 0.65, // [0.2, 5.0]
441 | distance: false,
442 | distanceColor: "#ffffff",
443 | distanceIs3d: false,
444 | distancePosition: 1.0, // [0.5, 2.0]
445 | distanceOpacity: 1.0, // [0.0, 1.0]
446 | distanceSize: 1.0, // [0.5, 4.0]
447 | fade: 0.5, // [0.1, 1.0]
448 | is3d: false, // Refer to arrow
449 | opacity: 0.4, // [0.0, 1.0]
450 | scale: 1.0, // [0.2, 2.0]
451 | size: 1.0, // [0.5, 4.0]
452 | smoothing: 0.05, // [0.0, 1.0]
453 | threshold: 0.2 // [0.0, 1.0]
454 | }
455 | ```
456 | ***
457 | ### Text Box
458 | The `tool_id` is `textBox`.
459 |
460 | **Text**
461 | ```
462 | text: 'Insert Text'
463 | ```
464 |
465 | **Position**
466 | ```
467 | position: { x: 0.45, y: 0.45 } // Normalized: screen-space or field-space if 'is3d' is enabled.
468 | ```
469 |
470 | **Options**
471 | ```
472 | options: {
473 | width: 0.1,
474 | height: 0.1,
475 | size: 1.0, // [0.5, 4.0]
476 | rotation: 0.0, // [-Math.PI, Math.PI]
477 | color: "#ffffff",
478 | opacity: 1.0, // [0.0, 1.0]
479 | align: 'center',
480 | background: false,
481 | backgroundColor: "#000000",
482 | backgroundOpacity: 0.5, // [0.0, 1.0]
483 | is3d: false
484 | }
485 | ```
486 | ***
487 | ### Image
488 | The `tool_id` is `image`. Image will be downloaded locally and placed in the workspace path.
489 |
490 | **URL**
491 | ```
492 | url: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png'
493 | ```
494 |
495 | **Position**
496 | ```
497 | position: { x: 0.25, y: 0.25 } // Normalized: screen-space or field-space if 'is3d' is enabled.
498 | ```
499 |
500 | **Scale**
501 | ```
502 | scale: { x: 0.5, y: 0.5 } // Normalized: screen-space or field-space if 'is3d' is enabled.
503 | ```
504 |
505 | **Options**
506 | ```
507 | options: {
508 | rotation: 0.0, // [-Math.PI, Math.PI]
509 | opacity: 1.0, // [0.0, 1.0]
510 | is3d: false
511 | }
512 | ```
513 |
514 | ***
515 | ### Pause
516 | The `tool_id` is `pause`.
517 |
518 | **Pause Time**
519 | ```
520 | pause_time: 5000 // Milliseconds
521 | ```
522 | ***
523 | ### Chroma-Key
524 | The `tool_id` is `chromaKey`. It'll be computed on each clip created. Options should be set according to the scene, so if it remains similar during a game, maybe you want to adapt these values from a sample clip in Play and use them in all chroma-key events. Otherwise, you should not pass any option, use default values and fit them in each clip if needed.
525 |
526 | Since the order in which visualizations added declared in the event is preserved when they are imported in Play, the chroma-key tool should be added in the specific desired position. For example, if you want to add shape in the field and an arrow, but chroma key only to have an effect on the shape on the field, the order in the event should be: shape - chroma key - arrow.
527 |
528 | **Options**
529 | ```
530 | options: {
531 | threshold: 0.01, // [0.0, 1.0]
532 | smoothing: 0.1 // [0.0, 1.0]
533 | }
534 | ```
535 |
536 | ### Timer
537 | The `tool_id` is `timer`.
538 |
539 | **Options**
540 | ```
541 | options: {
542 | x: 0.825, // Normalized: screen-space or field-space if 'is3d' is enabled.
543 | y: 0.02, // Normalized: screen-space or field-space if 'is3d' is enabled.
544 | width: 0.16,
545 | height: 0.15,
546 | offsetTime: 0, // Unix timestamp.
547 | decimals: 0, // 0 (no decimals), 1 (tenths of second), 2 (hundredths of second) and 3 (thousandths of second)
548 | size: 2.7, // [0.5, 4.0]
549 | rotation: 0.0, [-Math.PI, Math.PI]
550 | color: '#ffffff',
551 | opacity: 1.0, // [0.0, 1.0]
552 | background: true,
553 | backgroundColor: '#000000',
554 | backgroundOpacity: 0.75, // [0.0, 1.0]
555 | clockwise: true,
556 | is3d: false
557 | }
558 | ```
559 |
560 | ### Offside
561 | The `tool_id` is `offside`.
562 |
563 | **Options**
564 | ```
565 | options: {
566 | teamId: 'T001',
567 | defender: true,
568 | defenderColor: '#ffffff',
569 | defenderOpacity: 1.0, // [0.0, 1.0]
570 | defenderContinuous: true,
571 | defenderDotted: false,
572 | defenderWidth: 0.12, // [0.05, 1.0]
573 | defenderThickness: 0.05, // [0.0, 7.5]
574 | defenderDashSize: 0.6, // [0.2, 2.5]
575 | defenderFieldColor: '#000000',
576 | defenderFieldOpacity: 0.55, // [0.0, 1.0]
577 | defenderFieldFade: 5.0, // [1.0, 8.0]
578 | attacker: true,
579 | automaticColor: false,
580 | attackerColor: '#ffff00',
581 | attackerOpacity: 1.0, // [0.0, 1.0]
582 | attackerContinuous: true,
583 | attackerDotted: false,
584 | attackerWidth: 0.12, // [0.05, 1.0]
585 | attackerThickness: 0.05, // [0.0, 7.5]
586 | attackerDashSize: 0.6, // [0.2, 2.5]
587 | attackerFieldColor: '#ffff00',
588 | attackerFieldOpacity: 0.0, // [0.0, 1.0]
589 | attackerFieldFade: 2.0 // [1.0, 8.0]
590 | }
591 | ```
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Codeball v0.4.0
2 | site_url: https://codeball.metrica-sports.com
3 | repo_url: https://github.com/metrica-sports/codeball
4 | repo_name: 'Codeball'
5 | edit_uri: blob/master/docs/
6 |
7 | theme:
8 | name: material
9 | palette:
10 | primary: teal
11 |
12 | plugins:
13 | - search
14 | - mkdocs-jupyter:
15 | include_source: True
16 |
17 | nav:
18 | - Home: index.md
19 | - GameDataset: game-dataset.md
20 | - CodeballFrames: codeball-frames.md
21 | - Patterns: patterns.md
22 | - Tactical: tactical.md
23 | - Metrica Play API:
24 | - What you can do: metrica-play-api.md
25 | - File Formatting: format-for-play.md
26 | - Visualizations: visualizations.md
27 | - How to import a file: how-to-import-to-play.md
28 | - Examples:
29 | - GameDataset: examples/game_dataset.ipynb
30 | - Example pattern: examples/example-pattern.md
31 | - Run patterns: examples/run_patterns.ipynb
32 | - Changelog: changelog.md
33 |
34 | markdown_extensions:
35 | - pymdownx.highlight:
36 | use_pygments: true
37 | linenums: true
38 | linenums_style: table
39 |
40 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 79
3 | include = '\.pyi?$'
4 | exclude = '''
5 | /(
6 | \.git
7 | | \.pytest_cache
8 | | \.mypy_cache
9 | | \.vscode
10 | | \.env
11 | )/
12 | '''
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/metrica-sports/codeball/06161ac69f9b7b53e3820537e780ab962c4349e6/requirements.txt
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 | import pathlib
3 |
4 | here = pathlib.Path(__file__).parent.resolve()
5 |
6 | long_description = (here / "README.md").read_text(encoding="utf-8")
7 |
8 | setup(
9 | name="codeball",
10 | version="v0.4.0",
11 | author="Bruno Dagnino",
12 | author_email="bruno@metrica-sports.com",
13 | url="https://github.com/metrica-sports/codeball",
14 | packages=find_packages(exclude=["tests"]),
15 | description="Data driven tactical and video analysis of soccer games",
16 | long_description=long_description,
17 | long_description_content_type="text/markdown",
18 | classifiers=[
19 | "Development Status :: 3 - Alpha",
20 | "Intended Audience :: Developers",
21 | "Intended Audience :: Science/Research",
22 | "Programming Language :: Python :: 3",
23 | "Programming Language :: Python :: 3.7",
24 | "Programming Language :: Python :: 3.8",
25 | "License :: OSI Approved :: MIT License",
26 | "Topic :: Scientific/Engineering",
27 | ],
28 | python_requires=">=3.7, <4",
29 | install_requires=[
30 | "kloppy == 1.7.0",
31 | "pandas>=1.0.5",
32 | ],
33 | extras_require={
34 | "test": ["pytest"],
35 | "development": ["pre-commit"],
36 | },
37 | )
38 |
--------------------------------------------------------------------------------