Skip to content

Commit cad1954

Browse files
committed
Prepare release of v1.0.0
Added triggers!
1 parent 3a4259a commit cad1954

File tree

2 files changed

+211
-5
lines changed

2 files changed

+211
-5
lines changed

experiments/triggers.py

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,129 @@
1-
from utils.analysis import EllipseROI, RectangleROI
1+
from utils.analysis import angle_between_vectors, calculate_distance, EllipseROI, RectangleROI
2+
from utils.configloader import RESOLUTION
3+
4+
5+
class HeaddirectionTrigger:
6+
"""Trigger to check if animal is turning head in a specific angle and egocentric direction"""
7+
def __init__(self, angle: int, head_dir: str = 'both', debug: bool = False):
8+
"""
9+
Initialising trigger with following parameters:
10+
:param int angle: angle to meet for condition
11+
:param str head_dir: head direction from egocentric position of the animal (left, right or both)
12+
13+
"""
14+
self._head_dir = head_dir
15+
self._angle = angle
16+
self._debug = debug
17+
18+
def check_skeleton(self, skeleton: dict):
19+
"""
20+
Checking skeleton for trigger
21+
:param skeleton: a skeleton dictionary, returned by calculate_skeletons() from poser file
22+
:return: response, a tuple of result (bool) and response body
23+
Response body is used for plotting and outputting results to trials dataframes
24+
['tailroot', 'neck', 'nose'] you need to pass this to angle between vectors to get headdirection
25+
"""
26+
tailroot_x, tailroot_y = skeleton['tailroot']
27+
neck_x, neck_y = skeleton['neck']
28+
nose_x, nose_y = skeleton['nose']
29+
ret_head_dir, angle = angle_between_vectors(tailroot_x, tailroot_y, neck_x, neck_y , nose_x, nose_y)
30+
true_angle = 180 - abs(angle)
31+
32+
if true_angle <= self._angle:
33+
if self._head_dir == ret_head_dir:
34+
result = True
35+
elif self._head_dir == 'both':
36+
result = True
37+
else:
38+
result = False
39+
40+
41+
color = (0, 255, 0) if result else (0, 0, 255)
42+
if self._debug:
43+
center = (nose_x, nose_y)
44+
45+
response_body = {'plot': {'text': dict(text=str(true_angle),
46+
org=skeleton[self._end_point],
47+
color=(255, 255, 255)),
48+
'circle': dict(center= center,
49+
radius= 5,
50+
color=color)
51+
}}
52+
else:
53+
response_body = {'angle': true_angle}
54+
55+
response = (result, response_body)
56+
return response
57+
58+
59+
class DirectionTrigger:
60+
"""
61+
Trigger to check if animal is looking in direction of some point
62+
"""
63+
def __init__(self, point: tuple, angle: int, bodyparts: iter, debug: bool = False):
64+
"""
65+
Initialising trigger with following parameters:
66+
:param tuple point: a point of interest in (x,y) format.
67+
:param int angle: angle, at which animal is considered looking at the screen
68+
:param iter bodyparts: a pair of joints of animal (tuple or list) that represent 'looking vector' like (start, end)
69+
For example,
70+
('neck', 'nose') pair would mean that direction in which animal is looking defined by vector from neck to nose
71+
"""
72+
self._angle = angle
73+
self._debug = debug
74+
self._point = point
75+
self._start_point, self._end_point = bodyparts
76+
77+
def check_skeleton(self, skeleton: dict):
78+
"""
79+
Checking skeleton for trigger
80+
:param skeleton: a skeleton dictionary, returned by calculate_skeletons() from poser file
81+
:return: response, a tuple of result (bool) and response body
82+
Response body is used for plotting and outputting results to trials dataframes
83+
"""
84+
start_x, start_y = skeleton[self._start_point]
85+
end_x, end_y = skeleton[self._end_point]
86+
direction_x, direction_y = self._point
87+
head_dir, angle = angle_between_vectors(direction_x, direction_y, start_x, start_y, end_x, end_y)
88+
true_angle = 180 - abs(angle)
89+
90+
result = true_angle <= self._angle
91+
92+
color = (0, 255, 0) if result else (0, 0, 255)
93+
if self._debug:
94+
response_body = {'plot': {'line': dict(pt1=skeleton[self._end_point],
95+
pt2=self._point,
96+
color=color),
97+
'text': dict(text=str(true_angle),
98+
org=skeleton[self._end_point],
99+
color=(255, 255, 255))}}
100+
else:
101+
response_body = {'angle': true_angle}
102+
103+
response = (result, response_body)
104+
return response
105+
106+
107+
class ScreenTrigger(DirectionTrigger):
108+
"""
109+
Trigger to check if animal is looking at the screen
110+
"""
111+
def __init__(self, direction: str, angle: int, bodyparts: iter, debug: bool = False):
112+
"""
113+
Initialising trigger with following parameters:
114+
:param direction: a direction where the screen is located in the stream or video.
115+
All possible directions: 'North' (or top of the frame), 'East' (right), 'South' (bottom), 'West' (left)
116+
Note that directions are not tied to real-world cardinal directions
117+
:param angle: angle, at which animal is considered looking at the screen
118+
:param bodyparts: a pair of joints of animal (tuple or list) that represent 'looking vector' like (start, end)
119+
For example,
120+
('neck', 'nose') pair would mean that direction in which animal is looking defined by vector from neck to nose
121+
"""
122+
self._direction = direction
123+
max_x, max_y = RESOLUTION
124+
direction_dict = {'North': (int(max_x / 2), 0), 'South': (int(max_x / 2), max_y),
125+
'West': (0, int(max_y / 2)), 'East': (max_x, int(max_y / 2))}
126+
super().__init__(direction_dict[self._direction], angle, bodyparts, debug)
2127

3128

4129
class RegionTrigger:
@@ -87,3 +212,51 @@ def check_skeleton(self, skeleton: dict):
87212
result, response_body = super().check_skeleton(skeleton)
88213
response = (not result, response_body) # flips result bool
89214
return response
215+
216+
217+
class FreezeTrigger:
218+
"""
219+
Trigger to check if animal is in freezing state
220+
"""
221+
def __init__(self, threshold: int, debug: bool = False):
222+
"""
223+
Initializing trigger with given threshold
224+
:param threshold: int in pixel how much of a movement does not count
225+
For example threshold of 5 would mean that all movements less then 5 pixels would be ignored
226+
"""
227+
self._threshold = threshold
228+
self._skeleton = None
229+
self._debug = debug # not used in this trigger
230+
231+
def check_skeleton(self, skeleton: dict):
232+
"""
233+
Checking skeleton for trigger
234+
:param skeleton: a skeleton dictionary, returned by calculate_skeletons() from poser file
235+
:return: response, a tuple of result (bool) and response body
236+
Response body is used for plotting and outputting results to trials dataframes
237+
"""
238+
# choosing a point to draw near the skeleton
239+
org_point = skeleton[list(skeleton.keys())[0]]
240+
joint_moved = []
241+
if self._skeleton is None:
242+
result = False
243+
text = 'Not freezing'
244+
self._skeleton = skeleton
245+
else:
246+
for joint in skeleton:
247+
joint_travel = calculate_distance(skeleton[joint], self._skeleton[joint])
248+
joint_moved.append(abs(joint_travel) <= self._threshold)
249+
if all(joint_moved):
250+
result = True
251+
text = 'Freezing'
252+
else:
253+
result = False
254+
text = 'Not freezing'
255+
self._skeleton = skeleton
256+
257+
color = (0, 255, 0) if result else (0, 0, 255)
258+
response_body = {'plot': {'text': dict(text=text,
259+
org=org_point,
260+
color=color)}}
261+
response = (result, response_body)
262+
return response

utils/analysis.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,9 @@ def calculate_distance(point1: tuple, point2: tuple) -> float:
118118
def calculate_distance_for_bodyparts(dataframe: pd.DataFrame, body_parts: Union[List[str], str]) -> List[pd.Series]:
119119
"""
120120
Calculating distances traveled for each frame for desired body parts
121-
:param dataframe: dataframe to calculate distances on
121+
:param dataframe DataFrame: dataframe to calculate distances on
122122
Should have columns with X and Y coordinates of desired body_parts
123-
:param body_parts: (str or list of str) part or parts to calculate distances for
123+
:param body_parts str or list of str: part or parts to calculate distances for
124124
Can be either string or list of strings
125125
:return list: returns list of pd.Series with distances for each bodypart
126126
"""
@@ -159,9 +159,9 @@ def calc_distance(bodypart):
159159
def calculate_speed_for_bodyparts(dataframe: pd.DataFrame, body_parts: Union[List[str], str]) -> List[pd.Series]:
160160
"""
161161
Calculating speed in pixels per seconds for each frame for desired body parts
162-
:param dataframe: dataframe to calculate speeds on
162+
:param dataframe DataFrame: dataframe to calculate speeds on
163163
Should have columns distances travelled for each desired body part
164-
:param body_parts: part or parts to calculate distances for
164+
:param body_parts str or list of str: part or parts to calculate distances for
165165
Can be either string or list of strings
166166
:return list: returns list of pd.Series with speeds for each bodypart
167167
"""
@@ -205,6 +205,39 @@ def check_for_distance(bodypart):
205205
results.append(temp_df.apply(speed_func, axis=1, args=(body_parts,)))
206206
return results
207207

208+
def angle_between_vectors(xa: int, ya: int, xb: int, yb: int, xc: int, yc: int) -> Tuple[str, float]:
209+
"""
210+
Calculating angle between vectors, defined by coordinates
211+
Returns angle and direction (left, right, forward or backward)
212+
*ISSUE* - if y axis is reversed, directions would also be reversed
213+
"""
214+
# using atan2() formula for both vectors
215+
dir_ab = math.atan2(ya - yb, xa - xb)
216+
dir_bc = math.atan2(yb - yc, xb - xc)
217+
218+
# angle between vectors in radians
219+
rad_angle = dir_ab - dir_bc
220+
pi = math.pi
221+
222+
# converting to degrees
223+
angle = rad_angle
224+
if pi < angle:
225+
angle -= 2 * pi
226+
elif - pi > angle:
227+
angle += 2 * pi
228+
angle = math.degrees(angle)
229+
230+
# defining the direction
231+
if 180 > angle > 0:
232+
direction = 'left'
233+
elif -180 < angle < 0:
234+
direction = 'right'
235+
elif abs(angle) == 180:
236+
direction = 'backwards'
237+
else:
238+
direction = 'forward'
239+
240+
return direction, angle
208241

209242
## miscellaneous ##
210243
def cls():

0 commit comments

Comments
 (0)