Skip to content

Commit 8014b4b

Browse files
authored
Merge pull request #43 from KumarLabJax/fix-keypoint-masking
Bugfixes on features and changing internal representation to always be (x,y).
2 parents 271f29d + 2e9554a commit 8014b4b

File tree

17 files changed

+88
-104
lines changed

17 files changed

+88
-104
lines changed

src/feature_extraction/base_features/centroid_velocity.py

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,19 @@ def per_frame(self, identity: int) -> np.ndarray:
4242
# get centroids for all frames where this identity is present
4343
centroids = [convex_hulls[i].centroid for i in indexes]
4444

45-
# convert to numpy array of x,y points of the centroids
46-
points = np.asarray([[p.x, p.y] for p in centroids])
45+
# get centroids for all frames where this identity is present
46+
centroid_centers = np.full([self._poses.num_frames, 2], np.nan, dtype=np.float32)
47+
for i in indexes:
48+
centroid_centers[i, :] = np.asarray(convex_hulls[i].centroid.xy).squeeze()
4749

48-
if points.shape[0] > 1:
49-
# compute x,y velocities
50-
# pass indexes so numpy can figure out spacing
51-
v = np.gradient(points, indexes, axis=0)
50+
v = np.gradient(centroid_centers, axis=0)
5251

53-
# compute direction of velocities
54-
d = np.degrees(np.arctan2(v[:, 1], v[:, 0]))
52+
# compute direction of velocities
53+
d = np.degrees(np.arctan2(v[:, 1], v[:, 0]))
5554

56-
# subtract animal bearing from orientation
57-
# convert angle to range -180 to 180
58-
values[indexes] = (((d - bearings[indexes]) + 360) % 360) - 180
55+
# subtract animal bearing from orientation
56+
# convert angle to range -180 to 180
57+
values = (((d - bearings) + 180) % 360) - 180
5958

6059
return {'centroid_velocity_dir': values}
6160

@@ -92,18 +91,12 @@ def per_frame(self, identity: int) -> np.ndarray:
9291
indexes = np.arange(self._poses.num_frames)[frame_valid == 1]
9392

9493
# get centroids for all frames where this identity is present
95-
centroids = [convex_hulls[i].centroid for i in indexes]
96-
97-
# convert to numpy array of x,y points of the centroids
98-
points = np.asarray([[p.x, p.y] for p in centroids])
99-
100-
if points.shape[0] > 1:
101-
# compute x,y velocities
102-
# pass indexes so numpy can figure out spacing
103-
v = np.gradient(points, indexes, axis=0)
94+
centroid_centers = np.full([self._poses.num_frames, 2], np.nan, dtype=np.float32)
95+
for i in indexes:
96+
centroid_centers[i, :] = np.asarray(convex_hulls[i].centroid.xy).squeeze()
10497

105-
# compute magnitude of velocities
106-
values[indexes] = np.sqrt(
107-
np.square(v[:, 0]) + np.square(v[:, 1])) * fps
98+
# get change over frames
99+
v = np.gradient(centroid_centers, axis=0)
100+
values = np.linalg.norm(v, axis=-1) * fps * self._pixel_scale
108101

109102
return {'centroid_velocity_mag': values}

src/feature_extraction/base_features/point_speeds.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,10 @@ def per_frame(self, identity: int) -> np.ndarray:
2525
speeds = {}
2626

2727
# calculate velocities for each point
28+
xy_deltas = np.gradient(poses, axis=0)
29+
point_velocities = np.linalg.norm(xy_deltas, axis=-1) * fps
30+
2831
for keypoint in PoseEstimation.KeypointIndex:
29-
# grab all of the values for this point
30-
points = np.ma.array(poses[:, keypoint, :], mask=np.stack([~point_masks[:, keypoint], ~point_masks[:, keypoint]]), dtype=np.float32)
31-
point_velocities = np.gradient(points, axis=0)
32-
point_velocities.fill_value = 0
33-
speeds[f"{keypoint.name} speed"] = point_velocities
34-
35-
# convert the velocities to speed and convert units
36-
for key, val in speeds.items():
37-
speeds[key] = np.linalg.norm(val, axis=-1) * fps
32+
speeds[f"{keypoint.name} speed"] = point_velocities[:, keypoint.value]
3833

3934
return speeds

src/feature_extraction/base_features/point_velocities.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,11 @@ def per_frame(self, identity: int) -> np.ndarray:
3333
bearings = self._poses.compute_all_bearings(identity)
3434

3535
directions = {}
36+
xy_deltas = np.gradient(poses, axis=0)
37+
angles = np.degrees(np.arctan2(xy_deltas[:, :, 1], xy_deltas[:, :, 0]))
3638

3739
for keypoint in PoseEstimation.KeypointIndex:
38-
# compute x,y velocities
39-
# pass indexes so numpy can figure out spacing
40-
points = np.ma.array(poses[:, keypoint, :], mask=np.stack([~point_masks[:, keypoint], ~point_masks[:, keypoint]]), dtype=np.float32)
41-
point_velocities = np.gradient(points, axis=0)
42-
43-
# compute the orientation, and adjust based on the animal's bearing
44-
adjusted_angle = (((np.degrees(np.arctan2(point_velocities[:, 1], point_velocities[:, 0])) - bearings) + 360) % 360) - 180
45-
adjusted_angle.fill_value = np.nan
46-
directions[f"{keypoint.name} velocity direction"] = adjusted_angle.filled()
40+
directions[f"{keypoint.name} velocity direction"] = ((angles[:, keypoint.value] - bearings + 360) % 360) - 180
4741

4842
return directions
4943

src/feature_extraction/features.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from .segmentation_features import SegmentationFeatureGroup
1717

1818

19-
FEATURE_VERSION = 9
19+
FEATURE_VERSION = 10
2020

2121
_FEATURE_MODULES = [
2222
BaseFeatureGroup,

src/feature_extraction/landmark_features/corner.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import math
21
import typing
32

43
import numpy as np
@@ -51,9 +50,6 @@ def cache_features(self, identity: int):
5150
closest_corners = np.full(self._poses.num_frames, -1, dtype=np.int8)
5251
corners = self._poses.static_objects['corners']
5352

54-
# points and convex hulls are in y,x
55-
# corners are x,y so flip them to match points and convex hulls
56-
corners = np.flip(corners, axis=-1)
5753
arena_center_np = np.mean(corners, axis=0)
5854
arena_center = Point(arena_center_np[0], arena_center_np[1])
5955

@@ -88,7 +84,7 @@ def cache_features(self, identity: int):
8884

8985
center_dist = self_shape.distance(arena_center)
9086
# Note that self_shape.xy stores a [2,1] point data, but cv2 needs shape [2]
91-
wall_dist = cv2.pointPolygonTest(corners.astype(np.float32), np.asarray(self_shape.centroid.xy).squeeze(), True)
87+
wall_dist = cv2.pointPolygonTest(corners.astype(np.float32), np.asarray(self_shape.centroid.xy).squeeze() * self._pixel_scale, True)
9288

9389
corner_distances[frame] = distance * self._pixel_scale
9490
center_distances[frame] = center_dist * self._pixel_scale
@@ -152,9 +148,9 @@ def compute_angle(a, b, c):
152148

153149
# most of the point types are unsigned short integers
154150
# cast to signed types to avoid underflow issues during subtraction
155-
angle = math.degrees(
156-
math.atan2(int(c[1]) - int(b[1]), int(c[0]) - int(b[0])) -
157-
math.atan2(int(a[1]) - int(b[1]), int(a[0]) - int(b[0]))
151+
angle = np.degrees(
152+
np.arctan2(c[1] - b[1], c[0] - b[0]) -
153+
np.arctan2(a[1] - b[1], a[0] - b[0])
158154
)
159155
return ((angle + 180) % 360) - 180
160156

src/feature_extraction/landmark_features/food_hopper.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ def per_frame(self, identity: int) -> np.ndarray:
2727
if self._pixel_scale is not None:
2828
hopper = hopper * self._pixel_scale
2929

30-
# swap the point x,y values and change dtype to float32 for open cv
31-
hopper_pts = hopper[:, [1, 0]].astype(np.float32)
30+
# change dtype to float32 for open cv
31+
hopper_pts = hopper.astype(np.float32)
3232

3333
points, _ = self._poses.get_identity_poses(identity, self._pixel_scale)
3434

@@ -42,8 +42,7 @@ def per_frame(self, identity: int) -> np.ndarray:
4242
if key_point in _EXCLUDED_POINTS:
4343
continue
4444

45-
# swap our x,y to match the opencv coordinate space
46-
pts = points[:, key_point.value, [1, 0]]
45+
pts = points[:, key_point.value, :]
4746

4847
distance = np.asarray([cv2.pointPolygonTest(hopper_pts, (p[0], p[1]), True) for p in pts])
4948
distance[np.isnan(pts[:, 0])] = np.nan

src/feature_extraction/segmentation_features/shape_descriptors.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ def per_frame(self, identity: int) -> np.ndarray:
8585
hole_area_ratio[frame] = (hole_areas * self._pixel_scale**2)/self._moment_cache.get_moment(frame, 'm00')
8686

8787
# Calculate the centroid speeds
88-
centroid_speeds = np.hypot(np.gradient(x), np.gradient(y))
89-
88+
centroid_speeds = np.hypot(np.gradient(x), np.gradient(y)) * self._poses.fps
89+
9090
values = {}
9191
values['centroid_speed'] = centroid_speeds
9292
values['ellipse_w'] = ellipse_w

src/feature_extraction/social_features/social_distance.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def __init__(self, poses: PoseEstimation, identity: int,
3838
continue
3939

4040
# Find the distance and identity of the closest animal at each
41-
# frame, as well as the distance, identity and angle of the closes
41+
# frame, as well as the distance, identity and angle of the closest
4242
# animal in field of view. In order to calculate this we require
4343
# that both animals have a valid convex hull and the the self
4444
# identity has a valid nose point and base neck point (which is
@@ -59,7 +59,7 @@ def __init__(self, poses: PoseEstimation, identity: int,
5959

6060
self_base_neck_point = points[idx.BASE_NECK, :]
6161
self_nose_point = points[idx.NOSE, :]
62-
other_centroid = np.array(other_shape.centroid.coords[0])
62+
other_centroid = np.array(other_shape.centroid.xy).squeeze() * self._pixel_scale
6363

6464
view_angle = self.compute_angle(
6565
self_nose_point,
@@ -95,11 +95,9 @@ def compute_angle(a, b, c):
9595
:return: angle between AB and BC with range [-180, 180)
9696
"""
9797

98-
# point types in the pose files are typically unsigned 16 bit integers,
99-
# cast to signed types to avoid underflow during subtraction
100-
angle = math.degrees(
101-
math.atan2(int(c[1]) - int(b[1]), int(c[0]) - int(b[0])) -
102-
math.atan2(int(a[1]) - int(b[1]), int(a[0]) - int(b[0]))
98+
angle = np.degrees(
99+
np.arctan2(c[1] - b[1], c[0] - b[0]) -
100+
np.arctan2(a[1] - b[1], a[0] - b[0])
103101
)
104102
return ((angle + 180) % 360) - 180
105103

src/pose_estimation/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import h5py
66

7-
from .pose_est import PoseEstimation, PoseHashException
7+
from .pose_est import PoseEstimation, PoseHashException, MINIMUM_CONFIDENCE
88
from .pose_est_v2 import PoseEstimationV2
99
from .pose_est_v3 import PoseEstimationV3
1010
from .pose_est_v4 import PoseEstimationV4

src/pose_estimation/pose_est.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from src.utils import hash_file
1212

13+
MINIMUM_CONFIDENCE = 0.3
1314

1415
class PoseHashException(Exception):
1516
pass
@@ -155,8 +156,8 @@ def get_identity_convex_hulls(self, identity):
155156
The convex hulls are calculated using all valid points except for the
156157
middle of tail and tip of tail points.
157158
:param identity: identity to return points for
158-
:return: the convex hulls (array elements will be None if there is no
159-
valid convex hull for that frame)
159+
:return: the convex hulls in pixel units (array elements will be None
160+
if there is no valid convex hull for that frame)
160161
"""
161162

162163
if identity in self._convex_hull_cache:
@@ -208,7 +209,7 @@ def compute_bearing(self, points):
208209
angle_rad = np.arctan2(base_neck_offset_xy[1],
209210
base_neck_offset_xy[0])
210211

211-
return angle_rad * (180 / np.pi)
212+
return np.degrees(angle_rad)
212213

213214
def compute_all_bearings(self, identity):
214215
bearings = np.full(self.num_frames, np.nan, dtype=np.float32)

0 commit comments

Comments
 (0)