diff --git a/aeon/analysis/plotting.py b/aeon/analysis/plotting.py deleted file mode 100644 index cf05d075..00000000 --- a/aeon/analysis/plotting.py +++ /dev/null @@ -1,145 +0,0 @@ -import math - -import matplotlib.colors as colors -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from matplotlib.collections import LineCollection - -from aeon.analysis.utils import * - - -def heatmap(position, frequency, ax=None, **kwargs): - """ - Draw a heatmap of time spent in each location from specified position data and sampling frequency. - - :param Series position: A series of position data containing x and y coordinates. - :param number frequency: The sampling frequency for the position data. - :param Axes, optional ax: The Axes on which to draw the heatmap. - """ - if ax is None: - ax = plt.gca() - _, _, _, mesh = ax.hist2d( - position.x, - position.y, - weights=np.ones(len(position)) / frequency, - norm=colors.LogNorm(), - **kwargs, - ) - ax.invert_yaxis() - cbar = plt.colorbar(mesh, ax=ax) - cbar.set_label("time (s)") - return mesh, cbar - - -def circle(x, y, radius, fmt=None, ax=None, **kwargs): - """ - Plot a circle centered at the given x, y position with the specified radius. - - :param number x: The x-component of the circle center. - :param number y: The y-component of the circle center. - :param number radius: The radius of the circle. - :param str, optional fmt: The format used to plot the circle line. - :param Axes, optional ax: The Axes on which to draw the circle. - """ - if ax is None: - ax = plt.gca() - points = pd.DataFrame(np.linspace(0, 2 * math.pi, 360), columns=["angle"]) - points["x"] = radius * np.cos(points.angle) + x - points["y"] = radius * np.sin(points.angle) + y - ax.plot(points.x, points.y, fmt, **kwargs) - - -def rateplot( - events, - window, - frequency, - weight=1, - start=None, - end=None, - smooth=None, - center=True, - ax=None, - **kwargs, -): - """ - Plot the continuous event rate and raster of a discrete event sequence, given the specified - window size and sampling frequency. - - :param Series events: The discrete sequence of events. - :param offset window: The time period of each window used to compute the rate. - :param DateOffset, Timedelta or str frequency: The sampling frequency for the continuous rate. - :param number, optional weight: A weight used to scale the continuous rate of each window. - :param datetime, optional start: The left bound of the time range for the continuous rate. - :param datetime, optional end: The right bound of the time range for the continuous rate. - :param datetime, optional smooth: The size of the smoothing kernel applied to the continuous rate output. - :param DateOffset, Timedelta or str, optional smooth: - The size of the smoothing kernel applied to the continuous rate output. - :param bool, optional center: Specifies whether to center the convolution kernels. - :param Axes, optional ax: The Axes on which to draw the rate plot and raster. - """ - label = kwargs.pop("label", None) - eventrate = rate( - events, window, frequency, weight, start, end, smooth=smooth, center=center - ) - if ax is None: - ax = plt.gca() - ax.plot( - (eventrate.index - eventrate.index[0]).total_seconds() / 60, - eventrate, - label=label, - **kwargs, - ) - ax.vlines( - sessiontime(events.index, eventrate.index[0]), -0.2, -0.1, linewidth=1, **kwargs - ) - - -def set_ymargin(ax, bottom, top): - """ - Set the vertical margins of the specified Axes. - - :param Axes ax: The Axes for which to specify the vertical margin. - :param number bottom: The size of the bottom margin. - :param number top: The size of the top margins. - """ - ax.set_ymargin(0) - ax.autoscale_view() - ylim = ax.get_ylim() - delta = ylim[1] - ylim[0] - bottom = ylim[0] - delta * bottom - top = ylim[1] + delta * top - ax.set_ylim(bottom, top) - - -def colorline( - x, - y, - z=None, - cmap=plt.get_cmap("copper"), - norm=plt.Normalize(0.0, 1.0), - ax=None, - **kwargs, -): - """ - Plot a dynamically colored line on the specified Axes. - - :param array-like x, y: The horizontal / vertical coordinates of the data points. - :param array-like, optional z: - The dynamic variable used to color each data point by indexing the color map. - :param str or ~matplotlib.colors.Colormap, optional cmap: - The colormap used to map normalized data values to RGBA colors. - :param matplotlib.colors.Normalize, optional norm: - The normalizing object used to scale data to the range [0, 1] for indexing the color map. - :param Axes, optional ax: The Axes on which to draw the colored line. - """ - if ax is None: - ax = plt.gca() - if z is None: - z = np.linspace(0.0, 1.0, len(x)) - z = np.asarray(z) - points = np.array([x, y]).T.reshape(-1, 1, 2) - segments = np.concatenate([points[:-1], points[1:]], axis=1) - lines = LineCollection(segments, array=z, cmap=cmap, norm=norm, **kwargs) - ax.add_collection(lines) - return lines diff --git a/aeon/analysis/readme.md b/aeon/analysis/readme.md deleted file mode 100644 index 4287ca86..00000000 --- a/aeon/analysis/readme.md +++ /dev/null @@ -1 +0,0 @@ -# \ No newline at end of file diff --git a/aeon/analysis/__init__.py b/aeon/processing/__init__.py similarity index 100% rename from aeon/analysis/__init__.py rename to aeon/processing/__init__.py diff --git a/aeon/processing/core/__init__.py b/aeon/processing/core/__init__.py new file mode 100644 index 00000000..792d6005 --- /dev/null +++ b/aeon/processing/core/__init__.py @@ -0,0 +1 @@ +# diff --git a/aeon/analysis/utils.py b/aeon/processing/core/core.py similarity index 77% rename from aeon/analysis/utils.py rename to aeon/processing/core/core.py index d443f542..7c4ad974 100644 --- a/aeon/analysis/utils.py +++ b/aeon/processing/core/core.py @@ -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 @@ -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)) - -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 \ No newline at end of file diff --git a/aeon/analysis/movies.py b/aeon/processing/core/video.py similarity index 69% rename from aeon/analysis/movies.py rename to aeon/processing/core/video.py index 3092105b..9865ac00 100644 --- a/aeon/analysis/movies.py +++ b/aeon/processing/core/video.py @@ -1,8 +1,67 @@ import cv2 import math + import numpy as np import pandas as pd -import aeon.io.video as video + + +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() + + +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): ''' @@ -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): diff --git a/aeon/processing/foraging.py b/aeon/processing/foraging.py new file mode 100644 index 00000000..4e73b515 --- /dev/null +++ b/aeon/processing/foraging.py @@ -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 + +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 diff --git a/aeon/processing/social/__init__.py b/aeon/processing/social/__init__.py new file mode 100644 index 00000000..792d6005 --- /dev/null +++ b/aeon/processing/social/__init__.py @@ -0,0 +1 @@ +# diff --git a/aeon/processing/social/social.py b/aeon/processing/social/social.py new file mode 100644 index 00000000..e4870b4f --- /dev/null +++ b/aeon/processing/social/social.py @@ -0,0 +1 @@ +# Functions related to training multianimal sleap ID model from Aeon videos would live here. \ No newline at end of file diff --git a/aeon/schema/dataset.py b/docs/examples/dataset.py similarity index 100% rename from aeon/schema/dataset.py rename to docs/examples/dataset.py