Compare commits

..

No commits in common. "e3b5d2d6cc6282b6af2aa0ba6bc5d137bedc57f1" and "1dd318f23e9da9cbea28bf6441978b4167fcb01e" have entirely different histories.

16 changed files with 67 additions and 385 deletions

View File

@ -1,74 +0,0 @@
#!/bin/bash
die() { echo "ERROR: $*"; exit 2; }
warn() { echo "WARNING: $*"; }
for cmd in mkdocs pdoc3 genbadge; do
command -v "$cmd" >/dev/null ||
warn "missing $cmd: run \`pip install $cmd\`"
done
PACKAGE="etrack"
PACKAGESRC="src/$PACKAGE"
PACKAGEROOT="$(dirname "$(realpath "$0")")"
BUILDROOT="$PACKAGEROOT/site"
# check for code coverage report:
# need to call nosetest with --with-coverage --cover-html --cover-xml
HAS_COVER=false
test -d cover && HAS_COVER=true
echo
echo "Clean up documentation of $PACKAGE"
echo
rm -rf "$BUILDROOT" 2> /dev/null || true
mkdir -p "$BUILDROOT"
if command -v mkdocs >/dev/null; then
echo
echo "Building general documentation for $PACKAGE"
echo
cd "$PACKAGEROOT"
cp .mkdocs.yml mkdocs-tmp.yml
if $HAS_COVER; then
echo " - Coverage: 'cover/index.html'" >> mkdocs-tmp.yml
fi
mkdir -p docs
sed -e 's|docs/||; /\[Documentation\]/d; /\[API Reference\]/d' README.md > docs/index.md
mkdocs build --config-file mkdocs.yml --site-dir "$BUILDROOT"
rm mkdocs-tmp.yml docs/index.md
cd - > /dev/null
fi
if $HAS_COVER; then
echo
echo "Copy code coverage report and generate badge for $PACKAGE"
echo
cd "$PACKAGEROOT"
cp -r cover "$BUILDROOT/"
genbadge coverage -i coverage.xml
# https://smarie.github.io/python-genbadge/
mv coverage-badge.svg site/coverage.svg
cd - > /dev/null
fi
if command -v pdoc3 >/dev/null; then
echo
echo "Building API reference docs for $PACKAGE"
echo
cd "$PACKAGEROOT"
pdoc3 --html --config latex_math=True --config sort_identifiers=False --output-dir "$BUILDROOT/api-tmp" $PACKAGESRC
mv "$BUILDROOT/api-tmp/$PACKAGE" "$BUILDROOT/api"
rmdir "$BUILDROOT/api-tmp"
cd - > /dev/null
fi
echo
echo "Done. Docs in:"
echo
echo " file://$BUILDROOT/index.html"
echo

View File

@ -1,35 +0,0 @@
# E-Fish tracking
Tool for easier handling of tracking results.
## Installation
### 1. Clone git repository
```shell
git clone https://whale.am28.uni-tuebingen.de/git/jgrewe/efish_tracking.git
```
### 2. Change into directory
```shell
cd efish_tracking
````
### 3. Install with pip
```shell
pip3 install -e . --user
```
The ```-e``` installs the package in an *editable* model that you do not need to reinstall whenever you pull upstream changes.
If you leave away the ```--user``` the package will be installed system-wide.
## TrackingResults
Is a class that wraps around the *.h5 files written by DeepLabCut
## ImageMarker
Class that allows for creating MarkerTasks to get specific positions in a video.

View File

@ -1,4 +0,0 @@
# TrackingData
Class that represents the position data associated with one noe/bodypart.

View File

@ -1,17 +0,0 @@
site_name: etrack
repo_url: https://github.com/bendalab/etrack/
edit_uri: ""
site_author: Jan Grewe jan.grewe@g-node.org
theme: readthedocs
nav:
- Home: 'index.md'
- 'User guide':
- 'etrack': 'etrack.md'
- 'TrackingData' : 'trackingdata.md'
- 'Code':
- API reference: 'api/index.html'

View File

@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
name = "etrack"
dynamic = ["version"]
dependencies = [
"hdf5",
"nixio>=1.5",
"nixtrack",
"numpy",
"matplotlib",

View File

@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
""" etrack package for easier reading and handling of efish tracking data.
Copyright © 2024, Jan Grewe
Redistribution and use in source and binary forms, with or without modification, are permitted under the terms of the BSD License. See LICENSE file in the root of the Project.
"""
from .image_marker import ImageMarker, MarkerTask
from .tracking_result import TrackingResult, coordinate_transformation
from .arena import Arena, Region

View File

@ -1,13 +1,12 @@
"""
Classes to construct the arena in which the animals were tracked.
"""
import logging
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from skimage.draw import disk
from .util import RegionShape, AnalysisType, Illumination
from IPython import embed
class Region(object):
@ -120,14 +119,7 @@ class Region(object):
@property
def position(self):
"""
Get the position of the arena.
Returns
-------
tuple
A tuple containing the x-coordinate, y-coordinate, width, and height of the arena.
"""
"""Returns the position and extent of the region as 4-tuple, (x, y, width, height)"""
x = self._min_extent[0]
y = self._min_extent[1]
width = self._max_extent[0] - self._min_extent[0]
@ -302,39 +294,39 @@ class Region(object):
return np.array(entering), np.array(leaving)
def patch(self, **kwargs):
"""
Create and return a matplotlib patch object based on the shape type of the arena.
Parameters:
- kwargs: Additional keyword arguments to customize the patch object.
Returns:
- A matplotlib patch object representing the arena shape.
If the 'fc' (facecolor) keyword argument is not provided, it will default to None.
If the 'fill' keyword argument is not provided, it will default to False.
For rectangular arenas, the patch object will be a Rectangle with width and height
based on the arena's position.
For circular arenas, the patch object will be a Circle with radius based on the
arena's extent.
Example usage:
```
arena = Arena()
patch = arena.patch(fc='blue', fill=True)
ax.add_patch(patch)
```
"""
if "fc" not in kwargs:
kwargs["fc"] = None
kwargs["fill"] = False
if self._shape_type == RegionShape.Rectangular:
w = self.position[2]
h = self.position[3]
return patches.Rectangle(self._origin, w, h, **kwargs)
else:
return patches.Circle(self._origin, self._extent, **kwargs)
"""
Create and return a matplotlib patch object based on the shape type of the arena.
Parameters:
- kwargs: Additional keyword arguments to customize the patch object.
Returns:
- A matplotlib patch object representing the arena shape.
If the 'fc' (facecolor) keyword argument is not provided, it will default to None.
If the 'fill' keyword argument is not provided, it will default to False.
For rectangular arenas, the patch object will be a Rectangle with width and height
based on the arena's position.
For circular arenas, the patch object will be a Circle with radius based on the
arena's extent.
Example usage:
```
arena = Arena()
patch = arena.patch(fc='blue', fill=True)
ax.add_patch(patch)
```
"""
if "fc" not in kwargs:
kwargs["fc"] = None
kwargs["fill"] = False
if self._shape_type == RegionShape.Rectangular:
w = self.position[2]
h = self.position[3]
return patches.Rectangle(self._origin, w, h, **kwargs)
else:
return patches.Circle(self._origin, self._extent, **kwargs)
def __repr__(self):
return f"Region: '{self._name}' of {self._shape_type} shape."

View File

@ -1,11 +1,8 @@
"""
Module that defines the ImageMarker and MarkerTask classes to manually mark things in individual images.
"""
import os
import matplotlib.pyplot as plt
import cv2
import os
import sys
import matplotlib.pyplot as plt
from IPython import embed
class ImageMarker:

View File

@ -6,9 +6,6 @@
# Redistribution and use in source and binary forms, with or without
# modification, are permitted under the terms of the BSD License. See
# LICENSE file in the root of the Project.
"""
Package info.
"""
import os
import json

View File

@ -1,3 +0,0 @@
"""
Reader classes for DeepLabCut, or SLEAP written data files.
"""

View File

@ -7,7 +7,7 @@ from ..tracking_data import TrackingData
class DLCReader(object):
"""Class that represents the tracking data stored in a DeepLabCut hdf5 file."""
def __init__(self, results_file, crop=(0, 0)) -> None:
"""
If the video data was cropped before tracking and the tracked positions are with respect to the cropped images, we may want to correct for this to convert the data back to absolute positions in the video frame.

View File

@ -9,8 +9,7 @@ from IPython import embed
class NixtrackData(object):
"""Wrapper around a nix data file that has been written accorind to the nixtrack model (https://github.com/bendalab/nixtrack)
"""
def __init__(self, filename, crop=(0, 0)) -> None:
"""
If the video data was cropped before tracking and the tracked positions are with respect to the cropped images, we may want to correct for this to convert the data back to absolute positions in the video frame.
@ -38,93 +37,28 @@ class NixtrackData(object):
@property
def filename(self):
"""
Returns the name of the file associated with the NixtrackData object.
Returns:
str: The name of the file.
"""
return self._file_name
@property
def bodyparts(self):
"""
Returns the bodyparts of the dataset.
Returns:
list: A list of bodyparts.
"""
return self._dataset.nodes
def _correct_cropping(self, orgx, orgy):
"""
Corrects the coordinates based on the cropping values, If it cropping was done during tracking.
Args:
orgx (int): The original x-coordinate.
orgy (int): The original y-coordinate.
Returns:
tuple: A tuple containing the corrected x and y coordinates.
"""
x = orgx + self._crop[0]
y = orgy + self._crop[1]
return x, y
@property
def fps(self):
"""Property that holds frames per second of the original video.
Returns
-------
int : the frames of second
"""
return self._dataset.fps
@property
def tracks(self):
"""
Returns a list of tracks from the dataset.
Returns:
list: A list of tracks.
"""
return [t[0] for t in self._dataset.tracks]
def track_data(self, bodypart=0, track=-1, fps=None):
"""
Retrieve tracking data for a specific body part and track.
Parameters
----------
bodypart : int or str
Index or name of the body part to retrieve tracking data for.
track : int or str
Index of the track to retrieve tracking data for.
fps : float
Frames per second of the tracking data. If not provided, it will be retrieved from the dataset.
Returns
-------
TrackingData: An object containing the x and y positions, time, score, body part name, and frames per second.
return self._dataset.tracks
Raises
------
ValueError: If the body part or track is not valid.
"""
def track(self, bodypart=0, fps=None):
if isinstance(bodypart, nb.Number):
bp = self.bodyparts[bodypart]
elif isinstance(bodypart, (str)) and bodypart in self.bodyparts:
bp = bodypart
else:
raise ValueError(f"Body part {bodypart} is not a tracked node!")
if track not in self.tracks:
raise ValueError(f"Track {track} is not a valid track name!")
if not isinstance(track, (list, tuple)):
track = [track]
elif isinstance(track, int):
track = [self.tracks[track]]
if fps is None:
fps = self._dataset.fps

View File

@ -1,7 +1,3 @@
"""
Module that defines the TrackingData class that wraps the position data for a given node/bodypart that has been tracked.
"""
import numpy as np
@ -10,37 +6,23 @@ class TrackingData(object):
These data are the x, and y-positions, the time at which the agent was detected, and the quality associated with the position estimation.
TrackingData contains these data and offers a few functions to work with it.
Using the 'quality_threshold', 'temporal_limits', or the 'position_limits' data can be filtered (see filter_tracks function).
The 'interpolate' function allows to fill up the gaps that may result from filtering with linearly interpolated data points.
The 'interpolate' function allows to fill up the gaps that be result from filtering with linearly interpolated data points.
More may follow...
"""
def __init__(self, x, y, time, quality, node="", fps=None,
quality_threshold=None, temporal_limits=None, position_limits=None) -> None:
"""
Initialize a TrackingData object.
Parameters
----------
x : float
The x-coordinates of the tracking data.
y : float
The y-coordinates of the tracking data.
time : float
The time vector associated with the x-, and y-coordinates.
quality : float
The quality score associated with the position estimates.
node : str, optional
The node name associated with the data. Default is an empty string.
fps : float, optional
The frames per second of the tracking data. Default is None.
quality_threshold : float, optional
The quality threshold for the tracking data. Default is None.
temporal_limits : tuple, optional
The temporal limits for the tracking data. Default is None.
position_limits : tuple, optional
The position limits for the tracking data. Default is None.
"""
def __init__(
self,
x,
y,
time,
quality,
node="",
fps=None,
quality_threshold=None,
temporal_limits=None,
position_limits=None,
) -> None:
self._orgx = x
self._orgy = y
self._orgtime = time
@ -79,19 +61,11 @@ class TrackingData(object):
@property
def quality_threshold(self):
"""Property that holds the quality filter setting.
Returns
-------
float : the quality threshold
"""
return self._threshold
@quality_threshold.setter
def quality_threshold(self, new_threshold):
"""Setter of the quality threshold that should be applied when filtering the data. Setting this to None removes the quality filter.
Data points that have a quality score below the given threshold are discarded.
"""Setter of the quality threshold that should be applied when filterin the data. Setting this to None removes the quality filter.
Parameters
----------
@ -102,18 +76,11 @@ class TrackingData(object):
@property
def position_limits(self):
"""
Get the position limits of the tracking data.
Returns:
tuple: A 4-tuple containing the start x, and y positions, width and height limits.
"""
return self._position_limits
@position_limits.setter
def position_limits(self, new_limits):
"""Sets the limits for the position filter. 'new_limits' must be a 4-tuple of the form (x0, y0, width, height). If None, the limits will be removed.
Data points outside the position limits are discarded.
Parameters
----------
@ -134,30 +101,16 @@ class TrackingData(object):
@property
def temporal_limits(self):
"""
Get the temporal limits of the tracking data.
Returns:
tuple: A tuple containing the start and end time of the tracking data.
"""
return self._time_limits
@temporal_limits.setter
def temporal_limits(self, new_limits):
"""Limits for temporal filter. The limits must be a 2-tuple of start and end time. Setting this to None removes the filter.
Data points the are associated with times outside the limits are discarded.
Parameters
----------
new_limits : 2-tuple
The new limits in the form (start, end) given in seconds.
Returns
-------
None
Raises
------
ValueError if the limits are not valid.
"""
if new_limits is not None and not (
isinstance(new_limits, (tuple, list)) and len(new_limits) == 2
@ -213,7 +166,7 @@ class TrackingData(object):
self._quality = self._quality[indices]
def positions(self):
"""Returns the filtered data (if filters have been applied, otherwise the original data).
"""Returns the filtered data (if filters have been applied).
Returns
-------
@ -231,7 +184,7 @@ class TrackingData(object):
def speed(self, x=None, y=None, t=None):
""" Returns the agent's speed as a function of time and position. The speed estimation is associated to the time/position between two sample points. If any of the arguments is not provided, the function will use the x,y coordinates that are stored within the object, otherwise, if all are provided, the user-provided values will be used.
Since the velocities are estimated from the difference between two sample points the returned velocities and positions are assigned to positions and times between the respective sampled positions/times.
Since the velocities are estimated from the difference between two sample points the returned velocities are assigned to positions and times between the respective sampled positions/times.
Parameters
----------
@ -251,12 +204,11 @@ class TrackingData(object):
The position
"""
if x is None or y is None or t is None:
x = self._x.copy()
y = self._y.copy()
t = self._time.copy()
dt = np.diff(t)
speed = np.sqrt(np.diff(x)**2 + np.diff(y)**2) / dt
t = t[:-1] + dt / 2
x = self._x
y = self._y
t = self._time
speed = np.sqrt(np.diff(x)**2 + np.diff(y)**2) / np.diff(t)
t = t[:-1] + np.diff(t) / 2
x = x[:-1] + np.diff(x) / 2
y = y[:-1] + np.diff(y) / 2

View File

@ -1,6 +1,3 @@
"""
Module containing utility functions and enum classes.
"""
from enum import Enum
class Illumination(Enum):
@ -25,26 +22,11 @@ class RegionShape(Enum):
class AnalysisType(Enum):
"""
Enumeration representing different types of analysis used when analyzing whether
positions fall into a given region.
Possible types:
AnalysisType.Full: considers both, the x- and the y-coordinates
AnalysisType.CollapseX: consider only the x-coordinates
AnalysisType.CollapseY: consider only the y-coordinates
"""
Full = 0
CollapseX = 1
CollapseY = 2
def __str__(self) -> str:
"""
Returns the string representation of the analysis type.
Returns:
str: The name of the analysis type.
"""
return self.name
class PositionType(Enum):

View File

@ -1,32 +0,0 @@
import pytest
import etrack as et
from IPython import embed
dataset = "test/2022lepto01_converted_2024.03.27_0.mp4.nix"
@pytest.fixture
def nixtrack_data():
# Create a NixTrackData object with some test data
return et.NixtrackData(dataset)
def test_basics(nixtrack_data):
assert nixtrack_data.filename == dataset
assert len(nixtrack_data.bodyparts) == 5
assert len(nixtrack_data.tracks) == 2
assert nixtrack_data.fps == 25
def test_trackingdata(nixtrack_data):
with pytest.raises(ValueError):
nixtrack_data.track_data(bodypart="test")
nixtrack_data.track_data(track="fish")
assert nixtrack_data.track_data("center") is not None
assert nixtrack_data.track_data("center", "none") is not None
if __name__ == "__main__":
pytest.main()