Skip to content
This repository has been archived by the owner on Apr 15, 2024. It is now read-only.

Basic unit tests, logging, exception refactoring #25

Merged
merged 26 commits into from
Jul 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3477a34
Fix duplicate decleration of install_requires
cfculhane Jul 19, 2020
beeeedc
Adding manifest to include requirements file, and adding test require…
cfculhane Jul 19, 2020
6a5d41e
Adding __init__ to package and submodules
cfculhane Jul 19, 2020
7ac2b66
Changing export directory of processed file to be inside it's data ex…
cfculhane Jul 19, 2020
d6986cc
Adding logging module and config
cfculhane Jul 19, 2020
41274b7
Update requirements for Yaml required for logging
cfculhane Jul 19, 2020
9e3fb72
Updating argument_parser to parse args in a seperate method, allowing…
cfculhane Jul 19, 2020
a07c99d
Update main run_eyeloop to handle custom args, remove unsave exec() c…
cfculhane Jul 19, 2020
302b789
cv.Importer now handles running out of frames more elegantly, closing…
cfculhane Jul 19, 2020
f584e86
Moving logger location to inside output dir for a particular session
cfculhane Jul 19, 2020
2f4a199
Updating DAQ to open and append, rather than locking the file. Also r…
cfculhane Jul 19, 2020
2ba409a
Adding basic integration tests, tox test runner config
cfculhane Jul 19, 2020
f942dae
Pass logger into EyeLoop to faciliate other loggers being used in tes…
cfculhane Jul 19, 2020
5f7d948
Add logging to engine.engine.Engine, update check_blink function to r…
cfculhane Jul 19, 2020
4e2a1c8
Update tox to ref py3.8 to match travis, update travis file to run tox
cfculhane Jul 19, 2020
4039f32
UPdate entry point console script
cfculhane Jul 19, 2020
0cf2f85
Adding tox install to travis config
cfculhane Jul 19, 2020
e57148b
Cleanup test_integration.py
cfculhane Jul 19, 2020
7e1a837
In setup.py revert change to find_namespace_packages, is now back to …
cfculhane Jul 19, 2020
7472582
Update travis to use tox-travis plugin
cfculhane Jul 19, 2020
d32192a
Trying a different way of using tox with travis...
cfculhane Jul 19, 2020
ee5ec11
More travis frustration, downgrading to py37 on travis and tox to see…
cfculhane Jul 19, 2020
5bb4703
trying straight pytest instead
cfculhane Jul 19, 2020
e85ac48
Simplyifying travis even more, removing coverage and report generation
cfculhane Jul 19, 2020
c9cb8db
Reverting travis and tox build sto py 3.8
cfculhane Jul 19, 2020
539ab75
Tidy up shared_logging module
cfculhane Jul 19, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ coverage.xml
*.cover
.hypothesis/
.pytest_cache/
/tests/reports/

# Translations
*.mo
Expand Down
8 changes: 6 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
language: python

python:
- "3.8"

before_install:
- "pip install -U pip"
- "python setup.py install"
install:
- pip install -e .
- pip install pytest pandas
dist: xenial
services:
- xvfb

script: eyeloop --video 'misc/travis-sample/Frmd7.m4v'
script: pytest
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include requirements.txt requirements_testing.txt tox.ini
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,17 @@ The default graphical user interface in EyeLoop is [*minimum-gui*.](https://gith

> EyeLoop is compatible with custom graphical user interfaces through its modular logic. [Click here](https://github.com/simonarvin/eyeloop/blob/master/eyeloop/guis/README.md) for instructions on how to build your own.

## Running unit tests ##

Install testing requirements by running in a terminal:

`pip install -r requirements_testing.txt`

Then run tox: `tox`

Reports and results will be outputted to `/tests/reports`


## Known issues ##
- [ ] Respawning/freezing windows when running *minimum-gui* in Ubuntu.

Expand Down
2 changes: 2 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Extra configuration for pytest
# The presence of this file in {project_dir} will force pytest to add the dir to PYTHONPATH
7 changes: 7 additions & 0 deletions eyeloop/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
__all__ = ["constants", "engine", "extractors", "guis", "importers", "utilities", "run_eyeloop", "config"]

from eyeloop import utilities
from eyeloop import constants
from eyeloop import guis
from eyeloop import importers
from eyeloop import utilities
Empty file added eyeloop/constants/__init__.py
Empty file.
Empty file added eyeloop/engine/__init__.py
Empty file.
62 changes: 36 additions & 26 deletions eyeloop/engine/engine.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import logging
import time
from typing import Optional

import cv2

Expand All @@ -7,6 +9,8 @@
from eyeloop.engine.processor import Shape
from eyeloop.utilities.general_operations import to_int, tuple_int

logger = logging.getLogger(__name__)


class Engine:
def __init__(self, eyeloop):
Expand All @@ -21,6 +25,7 @@ def __init__(self, eyeloop):
else: # Enables markers to remove artifacts. -m 1
self.place_markers = self.real_place_markers
self.marks = []
self.blink: Optional[int] = None

if config.arguments.tracking == 0: # Recording mode. --tracking 0
self.iterate = self.record
Expand Down Expand Up @@ -103,7 +108,8 @@ def arm(self, width, height, image) -> None:
self.norm_cr_artefact = int(6 * self.norm)

self.mean = np.mean(image)
self.blink_threshold = 25.1*np.log(np.var(image)) - 182 #0.046 * np.var(image) - 68.11
# self.blink_threshold = 25.1 * np.log(np.var(image)) - 182 # 0.046 * np.var(image) - 68.11
self.blink_threshold = 0.046 * np.var(image) - 58

self.base_mean = -1
self.blink = 0
Expand All @@ -119,28 +125,35 @@ def arm(self, width, height, image) -> None:
for cr_processor in self.cr_processors:
cr_processor.binarythreshold = float(np.min(self.source)) * .7

def check_blink(self, threshold=5) -> bool:
def check_blink(self, threshold: Optional[float] = None) -> bool:
"""
Analyzes the monochromatic distribution of the frame,
to infer blinking. Change in mean during blinking is very distinct.
:returns: True if blink detected, False otherwise
"""

# TODO calculate threshold using another method?
if threshold is None:
threshold = self.blink_threshold
# print(f"threshold = {threshold}")
mean = np.mean(self.source)
delta = self.mean - mean
self.mean = mean

# print("delta", delta)
if abs(delta) > self.blink_threshold:
self.blink = 10
print("blink!")
return False
if threshold < 0:
raise ValueError(f"check_blink() threshold must be greater than 0! Threshold was {threshold}")

# print(f"blink delta = {abs(delta)}")
if abs(delta) > threshold:
self.blink = 10 # TODO does this parameter mean a blink lasts for 10 frames? Should this be here?
print("blink detected!")
return True
elif self.blink != 0:

self.blink -= 1
return True
else:
self.blink = 0
return False
self.blink = 0

return True

def track(self) -> None:
"""
Expand All @@ -154,10 +167,11 @@ def track(self) -> None:

timestamp = time.time()
cr_width = cr_height = cr_center = cr_angle = pupil_center = pupil_width = pupil_height = pupil_angle = -1
blink = 0
cr_log=self.cr_log_stock.copy()

if self.check_blink():
cr_log = self.cr_log_stock.copy()

if self.check_blink() is False:
blink = 0
try:
pupil_area = self.pupil_processor.area
offsetx, offsety = -self.pupil_processor.corners[0]
Expand All @@ -176,7 +190,7 @@ def track(self) -> None:
cr_center, cr_width, cr_height, cr_angle, cr_dimensions_int = cr_processor.ellipse.parameters()
cr_log[i] = ((cr_width, cr_height), cr_center, cr_angle)
except:
pass #for now, let's pass any errors arising from Shape().track() - this caused EyeLoop to crash when cr_processor.track() failed.
pass # for now, let's pass any errors arising from Shape().track() - this caused EyeLoop to crash when cr_processor.track() failed.

self.refresh_pupil(self.pupil_source) # lambda _: None when pupil not selected in gui.
self.place_markers() # lambda: None when markerless (-m 0).
Expand All @@ -188,13 +202,13 @@ def track(self) -> None:

else:
blink = 1

self.blink_i = blink

try:
config.graphical_user_interface.update_track(blink)
except Exception as e:
print("Error! Did you assign the graphical user interface (GUI) correctly?")
print("Error message: ", e)
logger.exception("Did you assign the graphical user interface (GUI) correctly? Attempting to release()")
self.release()
return

Expand All @@ -217,8 +231,8 @@ def activate(self) -> None:
for extractor in self.extractors:
try:
extractor.activate()
except:
pass
except AttributeError:
logger.warning(f"Extractor {extractor} has no activate() method")

def release(self) -> None:
"""
Expand All @@ -231,14 +245,10 @@ def release(self) -> None:
for extractor in self.extractors:
try:
extractor.release()
except:
pass

try:
config.importer.release()
except:
pass
except AttributeError:
logger.warning(f"Extractor {extractor} has no release() method")

config.importer.release()

def update_feed(self, img) -> None:

Expand Down
20 changes: 9 additions & 11 deletions eyeloop/extractors/DAQ.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import json
import time
import logging
from pathlib import Path


class DAQ_extractor:
def __init__(self, dir):
self.dir = dir
timestamp = time.strftime("%Y%m%d-%H%M%S")
filename = "{}/log{}.json".format(dir, timestamp)
self.log = open(filename, 'w')
def __init__(self, output_dir):
self.output_dir = output_dir
self.datalog_path = Path(output_dir, f"datalog.json")

def fetch(self, engine):
try:
self.log.write(json.dumps(engine.dataout) + "\n")
except:
pass
with open(self.datalog_path, "a") as datalog:
datalog.write(json.dumps(engine.dataout) + "\n")

def release(self):
self.log.close()
logging.debug("DAQ_extractor.release() called")
pass

# def set_digital_line(channel, value):
# digital_output = PyDAQmx.Task()
Expand Down
Empty file added eyeloop/extractors/__init__.py
Empty file.
Empty file added eyeloop/guis/__init__.py
Empty file.
3 changes: 2 additions & 1 deletion eyeloop/guis/minimum/minimum_gui.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from pathlib import Path

import numpy as np

Expand Down Expand Up @@ -224,7 +225,7 @@ def arm(self, width: int, height: int) -> None:
self.binary_height = max(height, 200)

fourcc = cv2.VideoWriter_fourcc(*'MPEG')
output_vid = config.arguments.output_dir / "output.avi"
output_vid = Path(config.file_manager.new_folderpath, "output.avi")
self.out = cv2.VideoWriter(str(output_vid), fourcc, 50.0, (self.width, self.height))

self.PStock = np.zeros((self.binary_height, self.binary_width))
Expand Down
Empty file added eyeloop/importers/__init__.py
Empty file.
33 changes: 18 additions & 15 deletions eyeloop/importers/cv.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import logging
from pathlib import Path
from typing import Optional, Callable

import cv2

import eyeloop.config as config
from eyeloop.importers.importer import IMPORTER

logger = logging.getLogger(__name__)


class Importer(IMPORTER):

def __init__(self) -> None:
super().__init__()
self.route_frame: Optional[Callable] = None # Dynamically assigned at runtime depending on input type

def first_frame(self) -> None:
self.vid_path = Path(config.arguments.video)
Expand All @@ -31,7 +36,9 @@ def first_frame(self) -> None:
except:
image = image[..., 0]
else:
raise ValueError("Failed to initialize video stream.\nMake sure that the video path is correct, or that your webcam is plugged in and compatible with opencv.")
raise ValueError(
"Failed to initialize video stream.\n"
"Make sure that the video path is correct, or that your webcam is plugged in and compatible with opencv.")

elif self.vid_path.is_dir():

Expand All @@ -45,7 +52,8 @@ def first_frame(self) -> None:
height, width, _ = image.shape
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
self.route_frame = self.route_sequence_sing
except: # TODO fix bare except
except Exception: # TODO fix bare except
logger.exception("first_frame() error: ")
height, width = image.shape
self.route_frame = self.route_sequence_flat

Expand All @@ -57,14 +65,9 @@ def first_frame(self) -> None:
def route(self) -> None:
self.first_frame()
while True:
try:
if self.route_frame is not None:
self.route_frame()
except ValueError:
config.engine.release()
print("Importer released (1).")
break
except TypeError:
print("Importer released (2).")
else:
break

def proceed(self, image) -> None:
Expand All @@ -86,7 +89,6 @@ def route_sequence_flat(self) -> None:

self.proceed(image)


def route_cam(self) -> None:
"""
Routes the capture frame to:
Expand All @@ -97,14 +99,15 @@ def route_cam(self) -> None:
_, image = self.capture.read()
if image is not None:
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
self.proceed(image)
else:
raise ValueError("No more frames.")
return

self.proceed(image)
logger.info("No more frames to process, exiting.")
self.release()

def release(self) -> None:
logger.debug(f"cv.Importer.release() called")
if self.capture is not None:
self.capture.release()

self.route_frame = lambda _: None
self.route_frame = None
cv2.destroyAllWindows()
Loading