Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example 'aeon/analysis/' refactor #245

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
145 changes: 0 additions & 145 deletions aeon/analysis/plotting.py

This file was deleted.

1 change: 0 additions & 1 deletion aeon/analysis/readme.md

This file was deleted.

File renamed without changes.
1 change: 1 addition & 0 deletions aeon/processing/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#
32 changes: 0 additions & 32 deletions aeon/analysis/utils.py → aeon/processing/core/core.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,6 @@
import numpy as np
import pandas as pd

def distancetravelled(angle, radius=4.0):
'''
Calculates the total distance travelled on the wheel, by taking into account
its radius and the total number of turns in both directions across time.

:param Series angle: A series of magnetic encoder measurements.
:param float radius: The radius of the wheel, in metric units.
:return: The total distance travelled on the wheel, in metric units.
'''
maxvalue = int(np.iinfo(np.uint16).max >> 2)
jumpthreshold = maxvalue // 2
turns = angle.astype(int).diff()
clickup = (turns < -jumpthreshold).astype(int)
clickdown = (turns > jumpthreshold).astype(int) * -1
turns = (clickup + clickdown).cumsum()
distance = 2 * np.pi * radius * (turns + angle / maxvalue)
distance = distance - distance[0]
return distance

def visits(data, onset='Enter', offset='Exit'):
'''
Computes duration, onset and offset times from paired events. Allows for missing data
Expand Down Expand Up @@ -119,16 +100,3 @@ def sessiontime(index, start=None):
def distance(position, target):
"""Computes the euclidean distance to a specified target."""
return np.sqrt(np.square(position[['x','y']] - target).sum(axis=1))
Comment on lines 100 to 102
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Note: This review was outside of the patch, so it was mapped to the patch with the greatest overlap. Original lines [100-103]

There is a minor issue with the code formatting. It's a good practice to always end your file with a newline. This is because some Unix tools might not recognize or play well with the last line if newline is missing.

- 103: \ No newline at end of file
+ 103: 


def activepatch(wheel, in_patch):
'''
Computes a decision boundary for when a patch is active based on wheel movement.

:param Series wheel: A pandas Series containing the cumulative distance travelled on the wheel.
:param Series in_patch: A Series of type bool containing whether the specified patch may be active.
:return: A pandas Series specifying for each timepoint whether the patch is active.
'''
exit_patch = in_patch.astype(np.int8).diff() < 0
in_wheel = (wheel.diff().rolling('1s').sum() > 1).reindex(in_patch.index, method='pad')
epochs = exit_patch.cumsum()
return in_wheel.groupby(epochs).apply(lambda x:x.cumsum()) > 0
63 changes: 61 additions & 2 deletions aeon/analysis/movies.py → aeon/processing/core/video.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,67 @@
import cv2
import math

import numpy as np
import pandas as pd
import aeon.io.video as video


Comment on lines +3 to +7
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

There are unnecessary blank lines. It's a good practice to remove them to improve code readability.

- 
- 

def frames(data):
'''
Extracts the raw frames corresponding to the provided video metadata.

:param DataFrame data:
A pandas DataFrame where each row specifies video acquisition path and frame number.
:return:
An object to iterate over numpy arrays for each row in the DataFrame,
containing the raw video frame data.
'''
capture = None
filename = None
index = 0
try:
for frameidx, path in zip(data._frame, data._path):
if filename != path:
if capture is not None:
capture.release()
capture = cv2.VideoCapture(path)
filename = path
index = 0

if frameidx != index:
capture.set(cv2.CAP_PROP_POS_FRAMES, frameidx)
index = frameidx
success, frame = capture.read()
if not success:
raise ValueError('Unable to read frame {0} from video path "{1}".'.format(frameidx, path))
yield frame
index = index + 1
finally:
if capture is not None:
capture.release()
Comment on lines +18 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The frames function is missing error handling for the case when the video file cannot be opened. This could lead to unexpected behavior if an invalid path is provided.

                capture = cv2.VideoCapture(path)
+                if not capture.isOpened():
+                    raise IOError('Unable to open video file "{0}".'.format(path))
                filename = path



def export(frames, file, fps, fourcc=None):
'''
Exports the specified frame sequence to a new video file.

:param iterable frames: An object to iterate over the raw video frame data.
:param str file: The path to the exported video file.
:param fps: The frame rate of the exported video.
:param optional fourcc:
Specifies the four character code of the codec used to compress the frames.
'''
writer = None
try:
for frame in frames:
if writer is None:
if fourcc is None:
fourcc = cv2.VideoWriter_fourcc('m','p','4','v')
writer = cv2.VideoWriter(file, fourcc, fps, (frame.shape[1], frame.shape[0]))
writer.write(frame)
finally:
if writer is not None:
writer.release()


def gridframes(frames, width, height, shape=None):
'''
Expand Down Expand Up @@ -99,7 +158,7 @@ def collatemovie(clipdata, fun):
:return: The sequence of processed frames representing the collated movie.
'''
clipcount = len(clipdata.groupby('clip_sequence').frame_sequence.count())
allframes = video.frames(clipdata.sort_values(by=['frame_sequence', 'clip_sequence']))
allframes = frames(clipdata.sort_values(by=['frame_sequence', 'clip_sequence']))
return groupframes(allframes, clipcount, fun)

def gridmovie(clipdata, width, height, shape=None):
Expand Down
34 changes: 34 additions & 0 deletions aeon/processing/foraging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import numpy as np


def distancetravelled(angle, radius=4.0):
'''
Calculates the total distance travelled on the wheel, by taking into account
its radius and the total number of turns in both directions across time.

:param Series angle: A series of magnetic encoder measurements.
:param float radius: The radius of the wheel, in metric units.
:return: The total distance travelled on the wheel, in metric units.
'''
maxvalue = int(np.iinfo(np.uint16).max >> 2)
jumpthreshold = maxvalue // 2
turns = angle.astype(int).diff()
clickup = (turns < -jumpthreshold).astype(int)
clickdown = (turns > jumpthreshold).astype(int) * -1
turns = (clickup + clickdown).cumsum()
distance = 2 * np.pi * radius * (turns + angle / maxvalue)
distance = distance - distance[0]
return distance
Comment on lines +4 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The distancetravelled function seems to be well implemented. It calculates the total distance travelled on a wheel, taking into account its radius and the total number of turns in both directions across time. The logic is sound and it uses numpy for efficient numerical computations. However, there's no error handling if the input parameters are not as expected. For instance, if angle is not a Series or radius is not a float, the function will fail. Consider adding some basic type checking and error handling to improve robustness.

def distancetravelled(angle, radius=4.0):
+    if not isinstance(angle, pd.Series):
+        raise TypeError("angle must be a pandas Series")
+    if not isinstance(radius, (int, float)):
+        raise TypeError("radius must be a numeric value")
    '''


def activepatch(wheel, in_patch):
'''
Computes a decision boundary for when a patch is active based on wheel movement.

:param Series wheel: A pandas Series containing the cumulative distance travelled on the wheel.
:param Series in_patch: A Series of type bool containing whether the specified patch may be active.
:return: A pandas Series specifying for each timepoint whether the patch is active.
'''
exit_patch = in_patch.astype(np.int8).diff() < 0
in_wheel = (wheel.diff().rolling('1s').sum() > 1).reindex(in_patch.index, method='pad')
epochs = exit_patch.cumsum()
return in_wheel.groupby(epochs).apply(lambda x:x.cumsum()) > 0
Comment on lines +23 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The activepatch function computes a decision boundary for when a patch is active based on wheel movement. The logic seems correct and it uses pandas for efficient data manipulation. However, similar to the distancetravelled function, there's no error handling if the input parameters are not as expected. If wheel or in_patch are not Series, the function will fail. Consider adding some basic type checking and error handling to improve robustness.

def activepatch(wheel, in_patch):
+    if not isinstance(wheel, pd.Series):
+        raise TypeError("wheel must be a pandas Series")
+    if not isinstance(in_patch, pd.Series):
+        raise TypeError("in_patch must be a pandas Series")
    '''

1 change: 1 addition & 0 deletions aeon/processing/social/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#
1 change: 1 addition & 0 deletions aeon/processing/social/social.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Functions related to training multianimal sleap ID model from Aeon videos would live here.
File renamed without changes.