Skip to content

Commit bad1df0

Browse files
committed
First code commit
Basic features are present. More testing needed.
1 parent 39f0ed2 commit bad1df0

13 files changed

+388
-0
lines changed

.gitignore

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
.idea/
2+
.pytest_cache/
3+
4+
*.pyc
5+
6+
nlopt-2.4.2-dll64/
7+
8+
example/*.dat
9+
example/*.log
10+
example/test-template.png
11+
example/output.txt
12+
13+
fictrac/
14+
15+
16+
# Byte-compiled / optimized / DLL files
17+
__pycache__/
18+
*.py[cod]
19+
*$py.class
20+
21+
# C extensions
22+
*.so
23+
24+
# Distribution / packaging
25+
.Python
26+
build/
27+
develop-eggs/
28+
dist/
29+
downloads/
30+
eggs/
31+
.eggs/
32+
lib/
33+
lib64/
34+
parts/
35+
sdist/
36+
var/
37+
wheels/
38+
*.egg-info/
39+
.installed.cfg
40+
*.egg
41+
MANIFEST
42+
43+
# PyInstaller
44+
# Usually these files are written by a python script from a template
45+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
46+
*.manifest
47+
*.spec
48+
49+
# Installer logs
50+
pip-log.txt
51+
pip-delete-this-directory.txt
52+
53+
# Unit test / coverage reports
54+
htmlcov/
55+
.tox/
56+
.nox/
57+
.coverage
58+
.coverage.*
59+
.cache
60+
nosetests.xml
61+
coverage.xml
62+
*.cover
63+
.hypothesis/
64+
.pytest_cache/
65+
66+
# Translations
67+
*.mo
68+
*.pot
69+
70+
# Django stuff:
71+
*.log
72+
local_settings.py
73+
db.sqlite3
74+
75+
# Flask stuff:
76+
instance/
77+
.webassets-cache
78+
79+
# Scrapy stuff:
80+
.scrapy
81+
82+
# Sphinx documentation
83+
docs/_build/
84+
85+
# PyBuilder
86+
target/
87+
88+
# Jupyter Notebook
89+
.ipynb_checkpoints
90+
91+
# IPython
92+
profile_default/
93+
ipython_config.py
94+
95+
# pyenv
96+
.python-version
97+
98+
# celery beat schedule file
99+
celerybeat-schedule
100+
101+
# SageMath parsed files
102+
*.sage.py
103+
104+
# Environments
105+
.env
106+
.venv
107+
env/
108+
venv/
109+
ENV/
110+
env.bak/
111+
venv.bak/
112+
113+
# Spyder project settings
114+
.spyderproject
115+
.spyproject
116+
117+
# Rope project settings
118+
.ropeproject
119+
120+
# mkdocs documentation
121+
/site
122+
123+
# mypy
124+
.mypy_cache/
125+
.dmypy.json
126+
dmypy.json
127+
128+
# Pyre type checker
129+
.pyre/

example/config.txt

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
## FicTrac config file (build Nov 2 2018)
2+
c2a_cnrs_yz : { 304, 37, 293, 37, 293, 48, 304, 48 }
3+
c2a_r : { -1.209684, 1.208704, 1.208508 }
4+
c2a_src : c2a_cnrs_yz
5+
c2a_t : { 5.318191, -13.863654, 658.317226 }
6+
do_display : y
7+
do_socket : y
8+
max_bad_frames : -1
9+
opt_bound : 0.25
10+
opt_do_global : n
11+
opt_max_err : -1.000000
12+
opt_max_evals : 50
13+
opt_tol : 0.001
14+
q_factor : 9
15+
roi_c : { 0.002001, 0.006799, 0.999975 }
16+
roi_circ : { 82, 162, 97, 118, 131, 81, 289, 94, 324, 162 }
17+
roi_ignr : { { 46, 186, 358, 192, 364, 286, 44, 284 }, { 193, 125, 205, 120, 224, 127, 234, 134, 237, 154, 229, 170, 209, 173, 189, 170, 184, 155, 181, 141 } }
18+
roi_r : 0.022890
19+
save_debug : n
20+
save_raw : 0
21+
socket_port : 5556
22+
src_fn : test.mp4
23+
src_fps : -1.000000
24+
thr_ratio : 1.25
25+
thr_win_pc : 0.25
26+
vfov : 3.085
27+

example/run_example.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
from pybmt.callback.threshold_callback import ThresholdCallback
3+
from pybmt.fictrac.driver import FicTracDriver
4+
5+
def run_pybmt_example():
6+
7+
fictrac_config = "config.txt"
8+
fictrac_console_out = "output.txt"
9+
10+
# Instantiate the callback object thats methods are invoked when new tracking state is detected.
11+
callback = ThresholdCallback()
12+
13+
# Instantiate a FicTracDriver object to handle running of FicTrac in the background and communication
14+
# of program state.
15+
tracDrv = FicTracDriver(config_file=fictrac_config, console_ouput_file=fictrac_console_out,
16+
#remote_endpoint_url="localhost:5556",
17+
track_change_callback=callback, plot_on=False)
18+
19+
# This will start FicTrac and the
20+
tracDrv.run()
21+
22+
if __name__ == "__main__":
23+
run_pybmt_example()

example/test-configImg.png

82.1 KB
Loading

example/test.mp4

660 KB
Binary file not shown.

pybmt/__init__.py

Whitespace-only changes.

pybmt/callback/__init__.py

Whitespace-only changes.

pybmt/callback/callback.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
3+
class PyBMTCallback:
4+
"""
5+
FlyVRCallback is a base class that derived classes should use to implement control logic for closed loop experiments.
6+
It provides developers with method entry points for injecting custom logic for processing tracking signals and
7+
triggering stimuli. This class should never be instantiated directly, it provides only an abstract interface.
8+
"""
9+
10+
def setup_callback(self):
11+
"""
12+
This method is called once and only once before any event processing is triggered. Place any one time setup
13+
functionality within this function.
14+
15+
:return: None
16+
"""
17+
pass
18+
19+
def shutdown_callback(self):
20+
"""
21+
This method is called once and only once before exiting the programe. Place any one time shutdown
22+
functionality within this function.
23+
24+
:return: None
25+
"""
26+
pass
27+
28+
def process_callback(self, track_state):
29+
"""
30+
This method is called each time an update is detected in the online tracking state. Code placed within this
31+
method should execute as quickly and deterministically as possible.
32+
33+
:param tracking_update: A ctypes structure of type fictrac.SHMEMFicTracState
34+
:return: bool True to keep running, False to stop running.
35+
"""
36+
pass

pybmt/callback/threshold_callback.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from pybmt.callback.callback import PyBMTCallback
2+
from collections import deque
3+
4+
from pybmt.fictrac.state import FicTracState
5+
6+
7+
class ThresholdCallback(PyBMTCallback):
8+
"""
9+
This class implements control logic for triggering a stimulus when tracking velocity reaches a certain
10+
threshold. It is just an example of how things can work in a closed loop experiment where tracking state triggers
11+
stimuli response.
12+
"""
13+
14+
def __init__(self, speed_threshold=0.009, num_frames_mean=25):
15+
"""
16+
Setup a closed loop experiment that keeps track of a running average of the ball speed and generates a stimulus
17+
when the speed crosses a threshold.
18+
19+
:param speed_threshold: The speed threshold that must be reached to generate a stimulus.
20+
:param num_frames_mean: The number frames to use in computing the average.
21+
"""
22+
23+
# Call the base class constructor
24+
super(ThresholdCallback, self).__init__()
25+
26+
self.speed_threshold = speed_threshold
27+
self.num_frames_mean = num_frames_mean
28+
29+
def setup_callback(self):
30+
"""
31+
We don't need to do much here.
32+
33+
:return:
34+
"""
35+
36+
# Our buffer of past speeds
37+
self.speed_history = deque(maxlen=self.num_frames_mean)
38+
39+
self.is_signal_on = False
40+
41+
42+
def process_callback(self, track_state: FicTracState):
43+
"""
44+
This function is called with each update of fictrac's tracking state.
45+
A closed loop experiment that keeps track of a running average of the ball speed and generates a stimulus
46+
when the speed crosses a threshold.
47+
48+
:param track_state:
49+
:return:
50+
"""
51+
52+
# Get the current ball speed
53+
speed = track_state.speed
54+
55+
# Add the speed to our history
56+
self.speed_history.append(speed)
57+
58+
# Get the running average speed
59+
avg_speed = sum(self.speed_history)/len(self.speed_history)
60+
61+
if avg_speed > self.speed_threshold and not self.is_signal_on:
62+
print("Stimulus ON!")
63+
self.is_signal_on = True
64+
65+
if avg_speed < self.speed_threshold and self.is_signal_on:
66+
print("Stimulus OFF!")
67+
self.is_signal_on = False
68+
69+
return True
70+
71+
def shutdown_callback(self):
72+
"""
73+
We don't need to do anything special during shutdown.
74+
75+
:return:
76+
"""
77+
pass

pybmt/tools.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
def which(program):
2+
"""
3+
Locate an executable's path that is on the PATH
4+
5+
:param program: The name of the executable.
6+
:return: The full path to the executable
7+
"""
8+
import os
9+
def is_exe(fpath):
10+
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
11+
12+
fpath, fname = os.path.split(program)
13+
if fpath:
14+
if is_exe(program):
15+
return program
16+
else:
17+
for path in os.environ["PATH"].split(os.pathsep):
18+
path = path.strip('"')
19+
20+
exe_file = os.path.join(path, program)
21+
if is_exe(exe_file):
22+
return exe_file
23+
24+
return None
25+
26+
def get_flyvr_git_hash():
27+
28+
hash = None
29+
try:
30+
31+
# Find the directory location of the script, the actual repo directory is the parent of this directory
32+
# because we are in common.
33+
import os
34+
repo_dir = os.path.dirname(os.path.realpath(__file__))
35+
36+
import subprocess
37+
hash = subprocess.check_output(['git', '--git-dir={}\\..\\.git'.format(repo_dir), 'rev-parse', 'HEAD'])
38+
39+
except:
40+
print("Warning: Problem getting git hash for current fly-vr repo. Won't log in experiment output.")
41+
42+
return hash

requirements.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
numpy
2+
pyzmq
3+
matplotlib

setup.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from setuptools import setup
2+
3+
setup(
4+
name='pybmt',
5+
version='0.1',
6+
python_requires='>3.6',
7+
description='Python Ball Motion Tracking (pymbt): A python interface for closed loop fictrac (https://github.com/rjdmoore/fictrac)',
8+
author='David Turner',
9+
author_email='[email protected]',
10+
license='Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License',
11+
keywords='fictive, tracking, animal tracking, webcamera, sphere, closed loop',
12+
url="https://github.com/murthylab/pybmt",
13+
packages=['pybmt'],
14+
install_requires=[
15+
'pyzmq',
16+
'numpy',
17+
'matplotlib',
18+
'pytest'],
19+
zip_safe=False
20+
)

0 commit comments

Comments
 (0)