Skip to content

Commit ce8e96a

Browse files
Swap Packaging to Poetry (#60)
* Swapping packaging to Poetry - main thing is removal of setup.py and requirements.txt, instead we have pyproject.toml and poetry.lock. pyproject is the project description that contains everything that setup.py does (i was careful to keep it as similar as possible to the existing one while also adding version constraints that allow a successful build/install), and the poetry.lock contains the exact specification of what to install - I bumped up the minimum python from 3.5 to 3.6.1 to be able to install a few packages, both 3.5 and 3.6 are EOL and we should think of bumping this up further. Also - added some type hints to the DLCLive class which was bothering me to not have tab completion when i've been using it lol - DLCLive object tries to mutate something that is by default a tuple, which should cause an error if anyone tries to use tflite and dynamic (instead of warning and repairing as is intended) - Using Pathlib to resolve the path and check that it exists when loading configuration, this gives more helpful error messages bc people know where DLC is looking in their filesystem/resolves ambiguity of relative paths - added a CITATION.cff file <3 * oop forgot to quote the pseudo-imported Processor class * did not see we had github actions in here! - removed pip install requirements - removed pytest tests (i don't see any!?) - installing package and running test from already checked-out copy rather than pip installing from git:// - the packaging script doesn't look like it's doing anything, but i'd be happy to write one that autodeploys on tagged commits to master * - added argument parser to be able to suppress display on testing, and then call it when doing the tests - also install and run using poetry because windows paths are an abominable hell - tensorflow 2.7 only support python >3.7 * ohhhh Display is still instantiated even if the `display` arg is false because it gets instantiated before the check happens whether it should be saved or not. also limited tf versions for python <3.7 because it has to like do the ridiculous pip thing where it downloads all of them for some reason * arg not being passed through, does windows want to run poetry yet? * OH need to put argparser within the function - also cleaned up file handling in test function. Previously the video file was downloaded and not used, and lots of implicit paths that seem to be causing people trouble when running it. * try catch removing temporary directory because WINDOWS GOD DAMN IT * ok 3.6 is EOL anyway and it's causing all the problems
1 parent a72968b commit ce8e96a

File tree

9 files changed

+1471
-124
lines changed

9 files changed

+1471
-124
lines changed

.github/workflows/python-package.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
strategy:
1717
# You can use PyPy versions in python-version. For example, pypy2 and pypy3
1818
matrix:
19-
python-version: [3.6, 3.7, 3.8]
19+
python-version: [3.7, 3.8, 3.9]
2020

2121
steps:
2222
- uses: actions/checkout@v2
@@ -29,4 +29,4 @@ jobs:
2929
run: python -c "import sys; print(sys.version)"
3030
#test installation of DLC-core dependencies:
3131
- name: Install dependencies
32-
run: pip install -r requirements.txt
32+
run: pip install .

.github/workflows/testing.yml

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,6 @@ jobs:
3131
uses: actions/setup-python@v2
3232
with:
3333
python-version: ${{ matrix.python-version }}
34-
35-
- name: Install dependencies
36-
run: |
37-
python -m pip install --upgrade pip setuptools wheel
38-
pip install -r requirements.txt
3934
- name: Install ffmpeg
4035
run: |
4136
if [ "$RUNNER_OS" == "Linux" ]; then
@@ -47,12 +42,8 @@ jobs:
4742
choco install ffmpeg
4843
fi
4944
shell: bash
50-
51-
- name: Run pytest tests
52-
run: |
53-
pip install pytest
54-
python -m pytest
55-
- name: Run functional tests
45+
- name: Install and test
5646
run: |
57-
pip install git+git://github.com/${{ github.repository }}.git@${{ github.sha }}
58-
dlc-live-test
47+
python -m pip install --upgrade pip wheel poetry
48+
python -m poetry install
49+
python -m poetry run dlc-live-test --nodisplay

CITATION.cff

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# This CITATION.cff file was generated with cffinit.
2+
# Visit https://bit.ly/cffinit to generate yours today!
3+
4+
cff-version: 1.2.0
5+
title: >-
6+
Real-time, low-latency closed-loop feedback using
7+
markerless posture tracking
8+
message: >-
9+
If you utilize our tool, please [cite Kane et al,
10+
eLife
11+
2020](https://elifesciences.org/articles/61909).
12+
The preprint is available here:
13+
https://www.biorxiv.org/content/10.1101/2020.08.04.236422v2
14+
type: software
15+
authors:
16+
- given-names: Gary
17+
name-particle: A
18+
family-names: Kane
19+
affiliation: >-
20+
The Rowland Institute at Harvard, Harvard
21+
University, Cambridge, United States
22+
- given-names: Gonçalo
23+
family-names: Lopes
24+
affiliation: 'NeuroGEARS Ltd, London, United Kingdom'
25+
- given-names: Jonny
26+
name-particle: L
27+
family-names: Saunders
28+
affiliation: >-
29+
Institute of Neuroscience, Department of
30+
Psychology, University of Oregon, Eugene,
31+
United States
32+
- given-names: Alexander
33+
family-names: Mathis
34+
affiliation: >-
35+
The Rowland Institute at Harvard, Harvard
36+
University, Cambridge, United States; Center
37+
for Neuroprosthetics, Center for Intelligent
38+
Systems, & Brain Mind Institute, School of Life
39+
Sciences, Swiss Federal Institute of Technology
40+
(EPFL), Lausanne, Switzerland
41+
- given-names: Mackenzie
42+
name-particle: W
43+
family-names: Mathis
44+
affiliation: >-
45+
The Rowland Institute at Harvard, Harvard
46+
University, Cambridge, United States; Center
47+
for Neuroprosthetics, Center for Intelligent
48+
Systems, & Brain Mind Institute, School of Life
49+
Sciences, Swiss Federal Institute of Technology
50+
(EPFL), Lausanne, Switzerland
51+
52+
date-released: 2020-08-05
53+
doi: "10.7554/eLife.61909"
54+
license: "AGPL-3.0-or-later"
55+
version: "1.0.2"

dlclive/check_install/check_install.py

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,42 +7,88 @@
77

88

99
import os
10+
import sys
1011
import shutil
12+
import warnings
13+
1114
from dlclive import benchmark_videos
1215
import urllib.request
16+
import argparse
17+
from pathlib import Path
18+
import tarfile
19+
20+
21+
def urllib_pbar(count, blockSize, totalSize):
22+
percent = int(count * blockSize * 100 / totalSize)
23+
outstr = f"{round(percent)}%"
24+
sys.stdout.write(outstr)
25+
sys.stdout.write("\b"*len(outstr))
26+
sys.stdout.flush()
1327

28+
def main(display:bool=None):
29+
parser = argparse.ArgumentParser(
30+
description="Test DLC-Live installation by downloading and evaluating a demo DLC project!")
31+
parser.add_argument('--nodisplay', action='store_false', help="Run the test without displaying tracking")
32+
args = parser.parse_args()
1433

15-
def main():
34+
if display is None:
35+
display = args.nodisplay
36+
37+
if not display:
38+
print('Running without displaying video')
1639

1740
# make temporary directory in $HOME
1841
print("\nCreating temporary directory...\n")
19-
home = os.path.expanduser("~")
20-
tmp_dir = os.path.normpath(f"{home}/dlc-live-tmp")
21-
os.makedirs(tmp_dir, exist_ok=True)
22-
os.chdir(tmp_dir)
42+
tmp_dir = Path().home() / 'dlc-live-tmp'
43+
tmp_dir.mkdir(mode=0o775,exist_ok=True)
44+
45+
video_file = str(tmp_dir / 'dog_clip.avi')
46+
model_tarball = tmp_dir / 'DLC_Dog_resnet_50_iteration-0_shuffle-0.tar.gz'
47+
model_dir = model_tarball.with_suffix('').with_suffix('') # remove two suffixes (tar.gz)
48+
2349

2450
# download dog test video from github:
51+
print(f"Downloading Video to {video_file}")
2552
url_link = "https://github.com/DeepLabCut/DeepLabCut-live/blob/master/check_install/dog_clip.avi?raw=True"
26-
urllib.request.urlretrieve(url_link, "dog_clip.avi")
27-
video_file = os.path.join(url_link, "dog_clip.avi")
53+
urllib.request.urlretrieve(url_link, video_file, reporthook=urllib_pbar)
2854

2955
# download exported dog model from DeepLabCut Model Zoo
30-
print("Downloading full_dog model from the DeepLabCut Model Zoo...")
31-
model_url = "http://deeplabcut.rowland.harvard.edu/models/DLC_Dog_resnet_50_iteration-0_shuffle-0.tar.gz"
32-
os.system(f"curl {model_url} | tar xvz")
56+
if Path(model_tarball).exists():
57+
print('Tarball already downloaded, using cached version')
58+
else:
59+
print("Downloading full_dog model from the DeepLabCut Model Zoo...")
60+
model_url = "http://deeplabcut.rowland.harvard.edu/models/DLC_Dog_resnet_50_iteration-0_shuffle-0.tar.gz"
61+
urllib.request.urlretrieve(model_url, str(model_tarball), reporthook=urllib_pbar)
62+
63+
print('Untarring compressed model')
64+
model_file = tarfile.open(str(model_tarball))
65+
model_file.extractall(str(model_dir.parent))
66+
model_file.close()
67+
68+
# assert these things exist so we can give informative error messages
69+
assert Path(video_file).exists()
70+
assert Path(model_dir).exists() and Path(model_dir).is_dir()
3371

3472
# run benchmark videos
3573
print("\n Running inference...\n")
36-
model_dir = "DLC_Dog_resnet_50_iteration-0_shuffle-0"
37-
print(video_file)
38-
benchmark_videos(model_dir, video_file, display=True, resize=0.5, pcutoff=0.25)
74+
# model_dir = "DLC_Dog_resnet_50_iteration-0_shuffle-0"
75+
# print(video_file)
76+
benchmark_videos(str(model_dir), video_file, display=display, resize=0.5, pcutoff=0.25)
3977

4078
# deleting temporary files
4179
print("\n Deleting temporary files...\n")
42-
shutil.rmtree(tmp_dir)
80+
try:
81+
shutil.rmtree(tmp_dir)
82+
except PermissionError:
83+
warnings.warn(f'Could not delete temporary directory {str(tmp_dir)} due to a permissions error, but otherwise dlc-live seems to be working fine!')
4384

4485
print("\nDone!\n")
4586

4687

4788
if __name__ == "__main__":
48-
main()
89+
90+
91+
display = args.nodisplay
92+
93+
94+
main(display=args.nodisplay)

dlclive/dlclive.py

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
import warnings
1212
import numpy as np
1313
import tensorflow as tf
14+
import typing
15+
from pathlib import Path
16+
from typing import Optional, Tuple, List
1417

1518
try:
1619
TFVER = [int(v) for v in tf.__version__.split(".")]
@@ -32,7 +35,8 @@
3235
from dlclive.display import Display
3336
from dlclive import utils
3437
from dlclive.exceptions import DLCLiveError, DLCLiveWarning
35-
38+
if typing.TYPE_CHECKING:
39+
from dlclive.processor import Processor
3640

3741
class DLCLive(object):
3842
"""
@@ -99,23 +103,23 @@ class DLCLive(object):
99103

100104
def __init__(
101105
self,
102-
model_path,
103-
model_type="base",
104-
precision="FP32",
106+
model_path:str,
107+
model_type:str="base",
108+
precision:str="FP32",
105109
tf_config=None,
106-
cropping=None,
107-
dynamic=(False, 0.5, 10),
108-
resize=None,
109-
convert2rgb=True,
110-
processor=None,
111-
display=False,
112-
pcutoff=0.5,
113-
display_radius=3,
114-
display_cmap="bmy",
110+
cropping:Optional[List[int]]=None,
111+
dynamic:Tuple[bool, float, float]=(False, 0.5, 10),
112+
resize:Optional[float]=None,
113+
convert2rgb:bool=True,
114+
processor:Optional['Processor']=None,
115+
display:typing.Union[bool, Display]=False,
116+
pcutoff:float=0.5,
117+
display_radius:int=3,
118+
display_cmap:str="bmy",
115119
):
116120

117121
self.path = model_path
118-
self.cfg = None
122+
self.cfg = None # type: typing.Optional[dict]
119123
self.model_type = model_type
120124
self.tf_config = tf_config
121125
self.precision = precision
@@ -125,11 +129,12 @@ def __init__(
125129
self.resize = resize
126130
self.processor = processor
127131
self.convert2rgb = convert2rgb
128-
self.display = (
129-
Display(pcutoff=pcutoff, radius=display_radius, cmap=display_cmap)
130-
if display
131-
else None
132-
)
132+
if isinstance(display, Display):
133+
self.display = display
134+
elif display:
135+
self.display = Display(pcutoff=pcutoff, radius=display_radius, cmap=display_cmap)
136+
else:
137+
self.display = None
133138

134139
self.sess = None
135140
self.inputs = None
@@ -141,7 +146,7 @@ def __init__(
141146
# checks
142147

143148
if self.model_type == "tflite" and self.dynamic[0]:
144-
self.dynamic[0] = False
149+
self.dynamic = (False, *self.dynamic[1:])
145150
warnings.warn(
146151
"Dynamic cropping is not supported for tensorflow lite inference. Dynamic cropping will not be used...",
147152
DLCLiveWarning,
@@ -158,14 +163,14 @@ def read_config(self):
158163
error thrown if pose configuration file does nott exist
159164
"""
160165

161-
cfg_path = os.path.normpath(self.path + "/pose_cfg.yaml")
162-
if not os.path.isfile(cfg_path):
166+
cfg_path = Path(self.path).resolve() / "pose_cfg.yaml"
167+
if not cfg_path.exists():
163168
raise FileNotFoundError(
164-
f"The pose configuration file for the exported model at {cfg_path} was not found. Please check the path to the exported model directory"
169+
f"The pose configuration file for the exported model at {str(cfg_path)} was not found. Please check the path to the exported model directory"
165170
)
166171

167172
ruamel_file = ruamel.yaml.YAML()
168-
self.cfg = ruamel_file.load(open(cfg_path, "r"))
173+
self.cfg = ruamel_file.load(open(str(cfg_path), "r"))
169174

170175
@property
171176
def parameterization(self) -> dict:

0 commit comments

Comments
 (0)