| Title: | An R package for reading and writing movement data |
|---|---|
| Description: | An R package for reading and writing movement data. |
| Authors: | Mikkel Roald-Arbøl [aut, cre, cph] (ORCID: <https://orcid.org/0000-0002-9998-0058>) |
| Maintainer: | Mikkel Roald-Arbøl <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.7.0.9006 |
| Built: | 2026-10-14 18:54:55 UTC |
| Source: | https://github.com/animovement/aniread |
Reads a calibration recording and computes calibration values. Spin the ball a known number of times in one direction while recording.
calibrate_trackball( path, ball_diameter, ball_rotations, col_dx = "x", col_dy = "y" )calibrate_trackball( path, ball_diameter, ball_rotations, col_dx = "x", col_dy = "y" )
path |
Path to calibration CSV file. |
ball_diameter |
Diameter of the trackball in desired output units (e.g., mm or cm). |
ball_rotations |
Number of complete rotations performed. |
col_dx |
Column name for x-axis values. |
col_dy |
Column name for y-axis values. |
A list with two elements:
counts_per_rotation: Sensor counts per full rotation (for ball_calibration in read_trackball).
calibration_factor: Distance per sensor count (for anicore::convert_unit_space()).
# Roll the ball a known number of turns, then convert sensor counts to cm path <- system.file("extdata", "trackball_sensor_1.csv", package = "aniread") calibrate_trackball(path, ball_diameter = 5, ball_rotations = 10)# Roll the ball a known number of turns, then convert sensor counts to cm path <- system.file("extdata", "trackball_sensor_1.csv", package = "aniread") calibrate_trackball(path, ball_diameter = 5, ball_rotations = 10)
Works out which of the supported source software formats a file is in, by
looking at the file itself rather than trusting its name. This is what
read_dataset() uses when source = "auto".
Candidates are first narrowed to the sources registered for the file's suffix, then each candidate's detector inspects the file's contents. Exactly one match is required.
detect_source(paths)detect_source(paths)
paths |
Path to the file. If several paths are given (as
|
DeepLabCut and LightningPose export structurally identical CSV files, so a
file in that format matches both. Rather than guess, detect_source()
returns the combined name "deeplabcut/lightningpose"; read_dataset()
reads it with read_deeplabcut() - the parse is the same either way - and
records the combined name as the source, so the ambiguity is preserved
rather than silently resolved.
Detectors for HDF5, Parquet, XML and C3D files need an optional package
(rhdf5, arrow, xml2, c3dr). When one is not installed its sources
are skipped, and if nothing is detected the error names both the skipped
sources and the packages that would have been consulted.
The source name, as it appears in get_supported_sources(), or the
combined "deeplabcut/lightningpose".
A detector is a function of a single path returning TRUE when the file is
of that source. It must be cheap - read a handful of lines, or an HDF5
index, never the whole file - and it need not be defensive: detect_source()
runs each one with messages and warnings suppressed and treats an error as
"did not match".
read_dataset() to read the file, get_supported_sources() for
the supported formats.
detect_source(system.file("extdata", "sleap.analysis.csv", package = "aniread")) # DeepLabCut and Lightning Pose write the same CSV layout detect_source(system.file("extdata", "deeplabcut.csv", package = "aniread"))detect_source(system.file("extdata", "sleap.analysis.csv", package = "aniread")) # DeepLabCut and Lightning Pose write the same CSV layout detect_source(system.file("extdata", "deeplabcut.csv", package = "aniread"))
Downloads sample data for different animal tracking software and returns the path to the downloaded file. The function caches the data to avoid repeated downloads.
get_sample_data( source, dataset = NULL, cache_dir = tempdir(), quiet = FALSE, list_datasets = FALSE )get_sample_data( source, dataset = NULL, cache_dir = tempdir(), quiet = FALSE, list_datasets = FALSE )
source |
Character string specifying either a source name, as
Alternatively, provide a URL string (starting with "http://" or "https://") to download a file from a custom location. |
dataset |
Character string specifying which dataset to download for sources
that have multiple options. If NULL (default), the first listed dataset is used.
Call |
cache_dir |
Character string specifying the directory where to cache the downloaded
files. Defaults to a temporary directory using |
quiet |
TRUE/FALSE. TRUE suppresses inform messages. |
list_datasets |
TRUE/FALSE. If TRUE, prints available sources and datasets. Can be called with or without specifying a source. |
The function downloads sample data and caches it locally. If the file already exists in the cache directory, it will use the cached version instead of downloading again.
Some sources have multiple datasets available. The first dataset listed for each
source is used by default when dataset = NULL.
Special handling for TRex datasets: TRex datasets are distributed as zip files containing multiple individual tracking files (one per animal). The function automatically extracts these and returns a vector of paths to the individual files.
The predefined data sources are hosted at:
https://gin.swc.ucl.ac.uk/neuroinformatics/movement-sample-data
https://github.com/animovement/movement-data
Character string (or vector) with the path(s) to the downloaded file(s).
For a dataset distributed as a zip file (the TRex "five-locusts" and
trackball datasets), returns a character vector of paths to the files it
unpacks to. For all others, returns a single file path. Returns NULL
invisibly if list_datasets = TRUE.
# See all available sources and datasets get_sample_data(list_datasets = TRUE) # See datasets for a specific source get_sample_data("sleap", list_datasets = TRUE) # Downloading needs an internet connection # Get default dataset for SLEAP path <- get_sample_data("sleap") # Get a specific SLEAP dataset path <- get_sample_data("sleap", dataset = "zebras_drone") # Get TRex data (returns vector of paths to individual files) paths <- get_sample_data("trex") # Download from a custom URL # path <- get_sample_data("https://example.com/data/tracking.csv")# See all available sources and datasets get_sample_data(list_datasets = TRUE) # See datasets for a specific source get_sample_data("sleap", list_datasets = TRUE) # Downloading needs an internet connection # Get default dataset for SLEAP path <- get_sample_data("sleap") # Get a specific SLEAP dataset path <- get_sample_data("sleap", dataset = "zebras_drone") # Get TRex data (returns vector of paths to individual files) paths <- get_sample_data("trex") # Download from a custom URL # path <- get_sample_data("https://example.com/data/tracking.csv")
Returns the tracking / event software and file formats that aniread
supports, with the function that reads or writes each and the file
suffix(es) it handles. This lets downstream packages discover the
supported formats programmatically instead of hard-coding the list -
mirroring movement's get_supported_source_software().
get_supported_sources()get_supported_sources()
Suffixes are returned without a leading dot (e.g. "csv", "h5"),
matching the convention used throughout aniread (see the
expected_suffix argument of the internal file validator). The
generic read_custom() reader is intentionally omitted because it has
no fixed source software or file suffix.
The source names of the "read" rows are exactly those accepted by the
source argument of read_dataset(), and returned by detect_source().
Those of the "write" rows are accepted by the format argument of
write_dataset().
A tibble with one row per supported source and direction, and the columns:
sourceCharacter. The source software / format name.
directionCharacter. "read" or "write".
funCharacter. The aniread function that reads or writes
it.
suffixList column of character vectors - the file suffix(es) the function reads or writes.
formatList column of character vectors - the export
layouts a reader tells apart, as it records them in the
source_format metadata field. Empty for the "write" rows, and
for aniframe, whose files keep the source_format of the data
they hold.
get_supported_sources() # The formats aniread can write: supported <- get_supported_sources() supported[supported$direction == "write", ] # The export layouts FreeMoCap has written: readers <- supported[supported$direction == "read", ] readers$format[[which(readers$source == "freemocap")]] # Which sources read HDF5 (`.h5`) files? readers$source[vapply(readers$suffix, \(s) "h5" %in% s, logical(1))]get_supported_sources() # The formats aniread can write: supported <- get_supported_sources() supported[supported$direction == "write", ] # The export layouts FreeMoCap has written: readers <- supported[supported$direction == "read", ] readers$format[[which(readers$source == "freemocap")]] # Which sources read HDF5 (`.h5`) files? readers$source[vapply(readers$suffix, \(s) "h5" %in% s, logical(1))]
Reads movement data from a Parquet file and returns an aniframe object. Parquet files are required as they preserve the metadata necessary for aniframe objects.
read_aniframe(path)read_aniframe(path)
path |
Path to a Parquet file. |
The aniframe that was written, with its class: an anipoint, an
anievent, or another frame built on them, such as an anijoint. time is
as it was written; see "Time" in read_dataset().
path <- tempfile(fileext = ".parquet") write_aniframe(anicore::example_anipoint(), path) read_aniframe(path)path <- tempfile(fileext = ".parquet") write_aniframe(anicore::example_anipoint(), path) read_aniframe(path)
Read a data frame from AnimalTA. AnimalTA exports tracking
data in image (top-left) coordinates; the reader reflects y so the
returned aniframe is in the conventional bottom_left origin.
AnimalTA writes positions in three layouts, and the reader reads all
three. Which one it read is recorded in the source_format metadata
field.
"fixed": the coordinates file of a video tracked with a fixed number
of targets (coordinates/<video>_Coordinates.csv, or
corrected_coordinates/<video>_Corrected.csv once corrected). One row
per frame, with a pair of columns per target, X_Arena<a>_Ind<i> and
Y_Arena<a>_Ind<i>. When a target alone in its arena was tracked with
AnimalTA's "Separate head from tail" option, its pair is replaced by
X_Arena<a>_Ind<i>_Head, Y_..._Head, X_..._Tail and Y_..._Tail.
Once the tracking is corrected, AnimalTA holds them as two targets and
names the same columns X_Arena<a>_Ind<i>_part0 and so on for the head
and ..._part1 for the tail. Both become the keypoints head and tail
of Ind<i>; AnimalTA writes no centroid for such a target. Every other
target has the keypoint centroid, AnimalTA's name for the one position
it tracks.
"variable": the coordinates file of a video tracked with a variable
number of targets. One row per target and frame, in the columns
Frame, Time, Arena, Ind, X and Y.
"detailed": a file from the "Detailed data" folder that AnimalTA's
"Run analyses" writes, Results/Detailed_data/<video>/Arena_<a><target>.csv.
It holds one target, one row per frame, and the columns ticked under
"Detailed data columns": by default Time, X, Y (X_Smoothed and
Y_Smoothed when a smoothing filter was applied) and measures derived
from them, such as Distance and Speed. Only the time and positions
are kept. The target is named from the file name. Several files, one per
target, can be read at once by passing their paths together. A target
tracked with "Separate head from tail" has two files,
Arena_<a>Ind<i>_part0.csv for its head and Arena_<a>Ind<i>_part1.csv
for its tail, which read together as the keypoints head and tail of
one individual.
AnimalTA's head and tail are the two ends of the animal's skeleton, kept
apart from frame to frame by distance. AnimalTA does not work out which end
is the head, so check that head is the head before relying on it.
Positions in the coordinates files are in pixels. Positions in a detailed
file are in the unit of the scale set in AnimalTA, and in pixels when none
was set. The file does not record which, so unit_space keeps its default
of "px"; if a scale was set, declare its unit with
anicore::set_metadata(), and give video_height in that unit too.
AnimalTA writes the time since the start of the video in seconds in its
Time column, rounded to 0.01 s in the coordinates files and to 0.001 s in
a detailed file. The reader keeps it as time, so unit_time is "s". A
detailed file exported without the Time column but with Frame is
timed by its frame number instead, with unit_time "frame".
AnimalTA does not write the frame rate, but each row is a frame at the
rate it tracked at: the coordinates files give its number in Frame, and
a detailed file has a row for every frame. When every row agrees on one
rate, to within the rounding of Time, it becomes sampling_rate: the
frames between the first and last row over the seconds between them, or
the whole number nearest to it when that fits every row as well. Otherwise
it is left NA. In a short file the rounding leaves the rate uncertain by
about one rounding step over the file's duration.
AnimalTA numbers individuals from 0 within each arena, so an individual is
identified by its arena and its name together. The arena is its own
identity column, arena, holding AnimalTA's arena number ("0", "1",
...), and individual holds AnimalTA's name for the target: Ind<i>, or
the name it was given in AnimalTA, with its case kept. Every layout gives
the same values: the fixed layout's columns are named X_Arena<a>_Ind<i>,
the variable layout's Arena and Ind columns hold the same arena and
target, and so does a detailed file's name, Arena_<a>Ind<i>.csv. A
detailed file whose name is not in that form is named after the file, with
its arena NA. arena, individual and keypoint are the aniframe's
identity keys, so functions that work per individual keep arenas apart.
The arena column is there for every file, also when it has a single
arena.
read_animalta( path, format = c("auto", "fixed", "variable", "detailed"), video_height = NULL )read_animalta( path, format = c("auto", "fixed", "variable", "detailed"), video_height = NULL )
path |
Path to an AnimalTA file. Several detailed files, one per target, can be given together. |
format |
|
video_height |
Optional numeric height of the source video frame, in
pixels for the coordinates files. AnimalTA does not record this in the
export, so when not supplied the maximum observed |
a movement dataframe. time is AnimalTA's Time (or Frame),
with the first frame of the video at 0; see "Time" in read_dataset().
Chiara, V., & Kim, S.-Y. (2023). AnimalTA: A highly flexible and easy-to-use program for tracking and analysing animal movement in different environments. Methods in Ecology and Evolution, 14, 1699–1707. doi:10.1111/2041-210X.14115.
path <- system.file("extdata", "animalta.csv", package = "aniread") read_animalta(path)path <- system.file("extdata", "animalta.csv", package = "aniread") read_animalta(path)
Reads CSV files output by Anipose's 3D triangulation pipeline and converts them into aniframe format. The function handles the pivot from wide format (one column per keypoint-coordinate combination) to long format with separate rows for each keypoint at each time point.
read_anipose(path, unit_space = "mm")read_anipose(path, unit_space = "mm")
path |
Character string specifying the path to an Anipose CSV file.
Typically these are found in the |
unit_space |
Character string specifying the spatial units of the coordinates. This should match the units you used for your calibration board dimensions (e.g., "mm", "cm", "m"). Default is "mm". |
An aniframe (tibble) with the following columns:
time: Frame number, Anipose's fnum, with the first frame of the
video at 0; see "Time" in read_dataset()
keypoint: Name of the tracked body part
x, y, z: 3D coordinates in the specified units
confidence: Mean detection confidence across cameras (0-1 scale)
The aniframe includes metadata about the data source, filename, spatial units, and coordinate system.
Anipose outputs CSV files with columns like bodypart_x, bodypart_y,
bodypart_z, bodypart_error, bodypart_ncams, and bodypart_score for
each tracked keypoint. The coordinates are in whatever units you specified
for your calibration board dimensions.
Karashchuk, P., Rupp, K. L., Dickinson, E. S., Walling-Bell, S., Sanders, E., Azim, E., Brunton, B. W., & Tuthill, J. C. (2021). Anipose: A toolkit for robust markerless 3D pose estimation. Cell Reports, 36(13), 109730. doi:10.1016/j.celrep.2021.109730.
path <- system.file("extdata", "anipose.csv", package = "aniread") # Read anipose data with default millimeter units read_anipose(path) # Read with centimeter units read_anipose(path, unit_space = "cm")path <- system.file("extdata", "anipose.csv", package = "aniread") # Read anipose data with default millimeter units read_anipose(path) # Read with centimeter units read_anipose(path, unit_space = "cm")
Read a Bonsai data frame. Bonsai centroid coordinates
come from camera/video pipelines that use image (top-left) origin;
the reader reflects y so the returned aniframe is in the
conventional bottom_left origin.
Bonsai workflows are user-defined, so what the CSV records about the
video depends on the workflow. When it writes the image alongside the
centroid, every row carries the image's Size.Width and Size.Height.
When all the images in the file have the same size, that is the frame
size: y is reflected around its height, and its width and height are
recorded as the axis_extents of x and y. Otherwise pass video_height,
or the largest y is used as the height and no extent is recorded for x.
When the workflow records the blob's Orientation, it is kept as
orientation_axis: the angle of the blob's long axis, in radians from x
toward y, in (-pi/2, pi/2]. It is axial — the blob has no front — so it
is not declared as the frame's orientation (a yaw), and it is turned with
y when y is reflected.
time is the seconds elapsed since the first Timestamp, so unit_time
is "s", and the first Timestamp itself is kept as start_datetime.
sampling_rate is left NA. A Bonsai Timestamp is the time the
software received each frame, not when the camera captured it, so the
intervals between rows vary from frame to frame and do not state the
camera's rate. If you know it, set it with anicore::set_metadata().
read_bonsai(path, video_height = NULL)read_bonsai(path, video_height = NULL)
path |
Path to a Bonsai data file |
video_height |
Optional numeric height of the source video frame in pixels. Takes precedence over the height the file records. |
a movement dataframe. time is in seconds from the first row; see
"Time" in read_dataset().
Lopes, G., Bonacchi, N., Frazão, J., et al. (2015). Bonsai: an event-based framework for processing and controlling data streams. Frontiers in Neuroinformatics, 9, 7. doi:10.3389/fninf.2015.00007.
path <- system.file("extdata", "bonsai.csv", package = "aniread") read_bonsai(path)path <- system.file("extdata", "bonsai.csv", package = "aniread") read_bonsai(path)
Read behavioural events from a
BORIS export into an
anicore::anievent(). Two flat-text BORIS exports are
supported: aggregated events (one row per bout, the default
export shape) and tabular events (one row per START / STOP /
POINT transition; paired into bouts by the reader).
Time units come from the columns BORIS provides. The default
unit_time = "s" uses Start (s) / Stop (s) and works on any
BORIS export. With unit_time = "frame" the reader uses the
Image index start / Image index stop columns instead, with the
first frame of the video at 0 (BORIS counts a video's frames from 0,
and the images of an observation of images from 1, which are shifted
so the first image is 0); frames
stay aligned with rows of a host anicore::anipoint(), which
keeps event timing robust against effective-FPS drift when the
export is paired with movement data. If "frame" is
requested but the export carries no image-index columns, the
reader falls back to "s" with an informational message. FPS is
recorded as sampling_rate metadata without rescaling the
timestamps; call anicore::convert_unit_time() later if you
need to convert between frames and seconds.
Channels: each row's channel is the value of BORIS's
Behavioral category column when populated, falling back to the
literal "behavior" otherwise, including where the aggregated
export writes "Not defined" for a behaviour without a category;
label is the behaviour name,
and type is "state" or "point" mapped from BORIS's
Behavior type column. Overlap between bouts of the same channel
is permitted on the anievent side; anicore::validate_anievent()
flags overlapping state bouts with a warning rather than rejecting
them. Modifiers travel via the optional
modifiers list-column; the multi-column (Modifier #1,
Modifier #2, ...) layout, the legacy pipe-separated
single-column (a|b|None) layout, and comma-separated values
within a single slot are all accepted.
read_boris( path, format = c("auto", "aggregated", "tabular"), unit_time = c("s", "frame") )read_boris( path, format = c("auto", "aggregated", "tabular"), unit_time = c("s", "frame") )
path |
Path to a BORIS export ( |
format |
One of |
unit_time |
One of |
An anicore::anievent() with metadata fields source,
source_format (the format read), filename, unit_time, and
sampling_rate populated. start and
stop are BORIS's times in seconds, or its frame numbers with the
first frame of the video at 0; see "Time" in read_dataset(). The
sampling rate is set when every media file in the export has the
same FPS; an observation of several media files lists one FPS per
file in a single cell, separated by ; (and | between players).
Friard, O., & Gamba, M. (2016). BORIS: a free, versatile open-source event-logging software for video/audio coding and live observations. Methods in Ecology and Evolution, 7(11), 1325-1330. doi:10.1111/2041-210X.12584.
path <- system.file("extdata", "boris.csv", package = "aniread") read_boris(path)path <- system.file("extdata", "boris.csv", package = "aniread") read_boris(path)
Reads a C3D file and returns the data as an aniframe with associated metadata including source software, units, and sampling rate.
read_c3d(path)read_c3d(path)
path |
Path to a |
C3D numbers frames from 1, and a file's header records the frame of the
recording its first frame is, which is not frame 1 when the recording was
trimmed. time is in seconds from the first frame of the recording, so
a file that starts at frame 1 starts at time = 0, and one trimmed to
start at frame 705 of a 200 Hz recording starts at 3.52 s. A recording
longer than 65535 frames records its first frame in the TRIAL group's
ACTUAL_START_FIELD parameter instead, which is used when it is there.
An aniframe with columns time, keypoint, x, y, and z.
Metadata includes source software, filename, time/space units, and
sampling rate. time is in seconds, with the first frame of the
recording at 0; see "Time" in read_dataset().
c3dr::c3d_read() for lower-level C3D access.
# Five frames of a static trial from Vicon Nexus, at 100 Hz path <- system.file("extdata", "c3d.c3d", package = "aniread") read_c3d(path)# Five frames of a static trial from Vicon Nexus, at 100 Hz path <- system.file("extdata", "c3d.c3d", package = "aniread") read_c3d(path)
Reads a file and selects/renames specified columns to create an aniframe.
read_custom( path, cols, variables_what = NULL, variables_when = NULL, variables_where = NULL, index = NULL, metadata = list() )read_custom( path, cols, variables_what = NULL, variables_when = NULL, variables_where = NULL, index = NULL, metadata = list() )
path |
Character string specifying the path to the file to read. |
cols |
A named vector specifying which columns to keep and how to rename
them, e.g. |
variables_what |
Character vector of identity columns that together define a unique entity. Uses aniframe defaults if NULL. |
variables_when |
Character vector of temporal columns that together define a unique timepoint. Uses aniframe defaults if NULL. |
variables_where |
Character vector of spatial columns that together define position. If NULL, detected from data. |
index |
Length-one character vector naming the column the frame is
indexed by – the position of each row within its temporal context. If
|
metadata |
A list of metadata to attach to the resulting aniframe. Default is an empty list. |
An aniframe with the selected and renamed columns, and attached
metadata. time is the column as it is in the file; see "Time" in
read_dataset() for the convention the other readers follow.
# A small file with columns named the tracker's way path <- tempfile(fileext = ".csv") write.csv( data.frame( frame = c(1, 2, 3, 1, 2), pos_x = c(0.1, 0.4, 0.9, 2.0, 2.2), pos_y = c(1.0, 1.2, 1.1, 0.5, 0.7), track_id = c(1, 1, 1, 2, 2), trial_num = 1 ), path, row.names = FALSE ) # Simple case: rename columns to standard names, use defaults read_custom(path, cols = c(time = "frame", x = "pos_x", y = "pos_y")) # With identity variable read_custom( path, cols = c(track = "track_id", time = "frame", x = "pos_x", y = "pos_y"), variables_what = "track" ) # Full specification with trials read_custom( path, cols = c( id = "track_id", trial = "trial_num", frame = "frame", x = "pos_x", y = "pos_y" ), variables_what = "id", variables_when = "trial", index = "frame" ) # Using column positions read_custom(path, cols = c(time = 1, x = 2, y = 3))# A small file with columns named the tracker's way path <- tempfile(fileext = ".csv") write.csv( data.frame( frame = c(1, 2, 3, 1, 2), pos_x = c(0.1, 0.4, 0.9, 2.0, 2.2), pos_y = c(1.0, 1.2, 1.1, 0.5, 0.7), track_id = c(1, 1, 1, 2, 2), trial_num = 1 ), path, row.names = FALSE ) # Simple case: rename columns to standard names, use defaults read_custom(path, cols = c(time = "frame", x = "pos_x", y = "pos_y")) # With identity variable read_custom( path, cols = c(track = "track_id", time = "frame", x = "pos_x", y = "pos_y"), variables_what = "track" ) # Full specification with trials read_custom( path, cols = c( id = "track_id", trial = "trial_num", frame = "frame", x = "pos_x", y = "pos_y" ), variables_what = "id", variables_when = "trial", index = "frame" ) # Using column positions read_custom(path, cols = c(time = 1, x = 2, y = 3))
One entry point for every format aniread supports. By default the source
software is worked out from the file itself, so you do not have to know
which reader a file needs before opening it.
read_dataset(paths, source = "auto", ...)read_dataset(paths, source = "auto", ...)
paths |
Path to the file to read. A few readers take more than one
path - |
source |
Which source software wrote the file. |
... |
Passed on to the reader for |
read_dataset() is a dispatcher, not a new reader: it works out which
reader to call and calls it. The object you get back is exactly what the
underlying reader returns - an anipoint for tracking
data, or an anievent for behavioural events from
read_boris().
DeepLabCut and LightningPose CSV exports are structurally identical, so a
file in that format cannot be attributed to one or the other. Such a file is
read with read_deeplabcut() - the parse is the same either way - and its
source metadata is set to "deeplabcut/lightningpose" to record that the
distinction is undetermined. Pass source explicitly to override this.
Every reader records the export it read in the source_format metadata
field, so a file in a layout a reader does not expect can be traced back
to it; get_supported_sources() lists the values each reader records.
The version of the software that wrote the file goes in source_version
only where the file states it, and is NA otherwise.
An anipoint or
anievent, depending on the reader. time (or an
event's start and stop) follows the convention in the Time section.
Every reader gives time as the time elapsed since the first frame of the
video, so the first frame of the video is at time = 0, whatever the unit
(#150):
In frames, time is the frame number counted from 0: the first frame of
the video is 0, the next 1, and so on. Converting to seconds is then a
division by the frame rate, with no offset.
In seconds, the first frame of the video is at 0 s.
Offsets are kept. A file whose first row is frame 500 of the video has
time = 500 there, not 0, so it stays aligned with the video.
Sources that count frames from 0 are read as they are. Sources that count
them from 1 are shifted by one. Where a source gives times in seconds,
they are kept. The table lists each source's own convention, what
time was before aniread 0.8.0 and what it is now. Sources not marked as
changed read as before.
| Source (reader) | The source's own time | time before |
time now |
aniframe (read_aniframe()) |
As written by write_aniframe() |
As written | Unchanged |
AnimalTA (read_animalta()) |
Frame counts the video's frames from 0, keeping a cropped start; Time is the frame over the frame rate, in s |
Time in s; a detailed file without Time in frames, from its Frame |
Unchanged |
Anipose (read_anipose()) |
fnum counts frames from 0 |
fnum |
Unchanged |
Bonsai (read_bonsai()) |
A clock time per row, set by the workflow; no frame number | Seconds from the first row | Unchanged |
BORIS (read_boris()) |
Start (s)/Stop (s) are media time in s. The image index counts a video's frames from 0 (mpv's frame number), and the images of an observation of images from 1 |
In s, or the image index as written | In s unchanged; in frames, an observation of images is shifted by 1 so its first image is 0, and a video's frames are unchanged |
C3D (read_c3d()) |
Frames count from 1; the header records the recording frame the file starts at | Seconds from the file's first frame, which was always 0 | Seconds from the recording's first frame: 0 for a file that starts at frame 1, (first frame - 1) / rate for a trimmed one |
DeepLabCut (read_deeplabcut()) |
The row index counts frames from 0 | The index | Unchanged |
FastTrack (read_fasttrack()) |
imageNumber is the video's frame, from 0 |
imageNumber |
Unchanged |
FicTrac (read_fictrac()) |
A clock time per row, in ms | Seconds from the first row | Unchanged |
FreeMoCap (read_freemocap()) |
frame counts from 0, or the row is the frame; v1 also writes timestamps |
Seconds from the first frame's timestamp, or the frame from 0 | Unchanged |
idtracker.ai .h5 (read_idtracker()) |
Rows are the video's frames, from 0 | The row, counted from 1 | The frame, from 0 |
| idtracker.ai CSV, with a time column | time (or seconds) is the frame over the frame rate, in s |
The time in s | Unchanged |
| idtracker.ai CSV, without a time column | Rows are the video's frames, from 0 | The row, counted from 1 | The frame, from 0 |
| idtracker.ai tidy CSV and Parquet | frame counts from 0; time is the frame over the frame rate, or over 1 when the rate is unknown |
The time in s, or the frame + 1 | In s unchanged; without a rate, the frame, from 0 |
Lightning Pose (read_lightningpose()) |
The row index counts frames from 0 | The index | Unchanged |
movement (read_movement()) |
The time coordinate: frames from 0, or the frame over the frame rate in s |
As written | Unchanged |
OCTRON (read_octron()) |
frame_idx is the video's frame, from 0 |
frame_idx |
Unchanged |
SLEAP .h5 (read_sleap()) |
The frame axis starts at frame_idx 0 |
The position on the axis, counted from 1 | frame_idx, from 0 |
| SLEAP CSV | frame_idx counts from 0 |
frame_idx + 1 |
frame_idx |
Trackball (read_trackball()) |
Sensor samples with a clock or counter; no video | Seconds from the first shared sample | Unchanged |
TrackMate (read_trackmate()) |
FRAME counts from 0; POSITION_T is the frame times the frame interval |
POSITION_T |
Unchanged |
TRex (read_trex()) |
frame counts from 0; time is in s, 0 at frame 0 |
time |
Unchanged |
read_custom() |
Whatever the file holds | As in the file | Unchanged |
Where a source keeps only clock times (Bonsai, FicTrac, the FreeMoCap v1
timestamps), nothing in the file marks the first frame of the video, so
the first row is taken as 0. That is the first frame the tool logged:
FicTrac writes no row for a frame it could not match, so if it lost its
first frames, 0 is the first one it matched. A SLEAP .h5 written without its leading untracked frames
(sleap-io's all_frames = FALSE) does not record where it starts, so its
first row reads as frame 0.
detect_source() to detect the format without reading,
get_supported_sources() for what is supported, and the individual
read_*() functions for format-specific arguments.
# Let aniread work out the format path <- system.file("extdata", "sleap.analysis.csv", package = "aniread") read_dataset(path) # Name it explicitly read_dataset(path, source = "sleap") # Reader-specific arguments pass straight through paths <- c( system.file("extdata", "trackball_sensor_1.csv", package = "aniread"), system.file("extdata", "trackball_sensor_2.csv", package = "aniread") ) read_dataset( paths, setup = "of_free", sampling_rate = 60, col_time = "datetime" )# Let aniread work out the format path <- system.file("extdata", "sleap.analysis.csv", package = "aniread") read_dataset(path) # Name it explicitly read_dataset(path, source = "sleap") # Reader-specific arguments pass straight through paths <- c( system.file("extdata", "trackball_sensor_1.csv", package = "aniread"), system.file("extdata", "trackball_sensor_2.csv", package = "aniread") ) read_dataset( paths, setup = "of_free", sampling_rate = 60, col_time = "datetime" )
Read files from DeepLabCut (DLC) in either csv or h5 format.
DeepLabCut stores predictions in image (top-left) coordinates; the
reader reflects y so the returned aniframe is in the conventional
bottom_left origin. DLC's csv/h5 exports do not contain the source
video resolution (it lives in the project's config.yaml), so pass
video_height to get an accurate flip; otherwise max(y) is used
as a fallback.
read_deeplabcut(path, video_height = NULL)read_deeplabcut(path, video_height = NULL)
path |
Path to a DeepLabCut data file |
video_height |
Optional numeric height of the source video frame in pixels. Not used for 3D files. |
an aniframe. time is DeepLabCut's frame index, with the first
frame of the video at 0; see "Time" in read_dataset().
The file is read from its column header, the scorer, individuals
(multi-animal projects only), bodyparts and coords levels, so the
same data reads the same from a csv and an h5. An h5 is read whichever
way pandas stored it: DeepLabCut writes the "table" format, while a file
saved again with pandas' defaults, as movement's to_dlc_file() does,
is in the "fixed" format. Predictions are read from the HDF key
df_with_missing, and the stitched tracklets of a multi-animal project
(*_el.h5) from the key tracks.
Of the coords, x, y, z and likelihood are read, and likelihood
becomes confidence. Any other coords, such as the variances the
Ensemble Kalman Smoother adds, are not read.
A multi-animal project tracks the bodyparts it does not assign to an
animal (its uniquebodyparts) under the pseudo-individual single.
They are kept as the keypoints of an individual called "single".
The layout read is recorded in the source_format metadata field: the
file type, single- or multi-animal, and _3d for triangulated positions,
such as "csv_single_animal" or "h5_multi_animal".
DeepLabCut's triangulation writes x, y and z and no likelihood, so
confidence is NA. Triangulated positions are in the units and frame
of the stereo calibration rather than in image pixels, so they are
returned as stored: y is not reflected and video_height is not used.
The aniframe is 3D from its z column.
Mathis, A., Mamidanna, P., Cury, K. M., Abe, T., Murthy, V. N., Mathis, M. W., & Bethge, M. (2018). DeepLabCut: markerless pose estimation of user-defined body parts with deep learning. Nature Neuroscience, 21, 1281-1289. doi:10.1038/s41593-018-0209-y.
Nath, T., Mathis, A., Chen, A. C., Patel, A., Bethge, M., & Mathis, M. W. (2019). Using DeepLabCut for 3D markerless pose estimation across species and behaviors. Nature Protocols, 14, 2152-2176. doi:10.1038/s41596-019-0176-0.
path <- system.file("extdata", "deeplabcut.csv", package = "aniread") read_deeplabcut(path)path <- system.file("extdata", "deeplabcut.csv", package = "aniread") read_deeplabcut(path)
Reads a FastTrack tracking result file (.txt format) and returns an
aniframe with keypoints for head, body, and tail positions.
FastTrack stores positions in image (top-left) coordinates; the
reader reflects y so the returned aniframe is in the conventional
bottom_left origin. The tracking file does not record the source
video resolution, so pass video_height to get an accurate flip —
otherwise max(y) is used as a fallback.
read_fasttrack(path, video_height = NULL)read_fasttrack(path, video_height = NULL)
path |
Path to a FastTrack tracking.txt file. |
video_height |
Optional numeric height of the source video frame in pixels. |
An aniframe. time is FastTrack's imageNumber, with the first
frame of the video at 0; see "Time" in read_dataset().
Gallois, B., & Candelier, R. (2021). FastTrack: An open-source software for tracking varying numbers of deformable objects. PLOS Computational Biology, 17(2), e1008697. doi:10.1371/journal.pcbi.1008697.
path <- system.file("extdata", "fasttrack.txt", package = "aniread") read_fasttrack(path)path <- system.file("extdata", "fasttrack.txt", package = "aniread") read_fasttrack(path)
This helper loads a FicTrac *.dat file, keeps the timestamp, the 2‑D
fictive path and the animal's heading, converts the timestamps to seconds,
and returns the result as an anipoint. FicTrac's "integrated animal
heading" (the direction the animal faces) becomes yaw and is declared as
the frame's orientation, in radians from x toward y. FicTrac's movement
direction is the direction of travel, derivable from the path, and is not
kept. If the physical ball radius is supplied, the positions are scaled
accordingly and the spatial unit metadata is set.
read_fictrac(path, ball_radius = NULL, unit_ball_radius = "cm")read_fictrac(path, ball_radius = NULL, unit_ball_radius = "cm")
path |
Character. Path to the FicTrac |
ball_radius |
Numeric (optional). Physical radius of the tracking ball.
When supplied the |
unit_ball_radius |
Character. Unit of |
FicTrac 2 has written three layouts, told apart by their number of columns. Columns 1 to 21 are the same in all of them; the rest hold timestamps:
23 columns (FicTrac 2.0 to 2.02): 22 is the timestamp, 23 the
sequence counter. time is taken from the timestamp, which is the
position in the video file (ms) or the frame capture time (ms).
24 columns (untagged versions from July 2019, before 2.03): 22 is
the frame capture time in ms since midnight, 23 the sequence counter, 24
the time since the last frame (ms). time is taken from column 22.
25 columns (FicTrac 2.03 onward): 22 is the timestamp, 23 the
sequence counter, 24 the time since the last frame, 25 the "alt.
timestamp", the frame capture time in ms since midnight. time is taken
from column 25.
Which layout was read is recorded in the source_format metadata field,
as "23col", "24col" or "25col".
time counts in seconds from the first row. A time in ms since midnight
starts again from zero at midnight, so where it drops by more than twelve
hours from one row to the next, the reader takes it as having passed
midnight and adds a day from there on. A recording that crosses midnight
therefore keeps running forward rather than jumping back.
An anipoint with columns time, x, y and yaw. Metadata
includes the source ("fictrac"), original filename, sampling rate,
time unit ("s"), space unit (either "none" or the value of
unit_ball_radius), and a Cartesian 2‑D coordinate system. time is in
seconds from the first row; see "Time" in read_dataset().
Moore, R. J. D., Taylor, G. J., Paulk, A. C., Pearson, T., van Swinderen, B., & Srinivasan, M. V. (2014). FicTrac: a visual method for tracking spherical motion and generating fictive animal paths. Journal of Neuroscience Methods, 225, 106-119. doi:10.1016/j.jneumeth.2014.01.010.
path <- system.file("extdata", "fictrac.dat", package = "aniread") traj <- read_fictrac(path, ball_radius = 0.5, unit_ball_radius = "cm") head(traj)path <- system.file("extdata", "fictrac.dat", package = "aniread") traj <- read_fictrac(path, ball_radius = 0.5, unit_ball_radius = "cm") head(traj)
Reads the layouts FreeMoCap v1 and v2 write, dispatching on the column names rather than the file name:
<recording>_by_frame.csv: the v1 tidy export. FreeMoCap added a
reprojection_error column at v1.7.4, so this exists in an 8- and a
9-column form; both are read.
<recording>_by_trajectory.csv: v1's one column triple per tracked
point, with the camera timestamps alongside.
output_data/mediapipe_*_3d_xyz.csv: v1's per-model wide files, one
model each and no timestamps.
output_data/freemocap_data_by_frame.csv, and the .parquet beside it:
the v2 tidy export, one row per keypoint per trajectory per frame (see
trajectory). Reading the Parquet file needs the arrow package.
output_data/<tracker>_<aspect>_<trajectory>.csv: v2's per-trajectory
files, such as mediapipe_body_3d_xyz.csv. They keep v1's names but are
long (frame, keypoint, x, y, z), so they are told from v1's wide
files by their columns. Their model is read from the file name.
Which layout was read is recorded in the source_format metadata field.
Point names are parsed the way FreeMoCap's own data saver parses them, so
the same recording gives the same model and keypoint values whichever
layout it is read from.
read_freemocap( path, format = c("auto", "by_frame", "by_trajectory", "wide", "v2_by_frame", "v2_trajectory"), trajectory = c("3d_xyz", "rigid_3d_xyz") )read_freemocap( path, format = c("auto", "by_frame", "by_trajectory", "wide", "v2_by_frame", "v2_trajectory"), trajectory = c("3d_xyz", "rigid_3d_xyz") )
path |
Path to a FreeMoCap CSV, or a v2 Parquet file. |
format |
|
trajectory |
Which positions to read from the v2 tidy export:
|
confidence comes from reprojection_error, which the 9-column
by_frame export and the v2 tidy export carry; every other layout gives
all-NA confidence. The two run in opposite directions: a reprojection
error is a distance in pixels, so zero is perfect and larger is worse,
whereas every other reader in aniread fills confidence from a likelihood
or a probability where larger is better. Storing the error unchanged would
make aniprocess::mask_na_across(method = "confidence") mask the best
points, so it is mapped through
which is monotone decreasing onto : a zero error gives 1.
The mapping is invertible, so the original error is recoverable as
1 / confidence - 1.
v2 writes its output through skellyforge. Its tidy export names each model
<tracker>.<aspect> (mediapipe.body, rtmpose.left_hand); these are
read as <tracker>_<aspect> (mediapipe_body, rtmpose_left_hand), the
naming of v1's model column and of v2's own per-trajectory file names.
The hands keep separate models, since v2's hand keypoint names do not say
which hand they belong to.
Each keypoint appears once per trajectory type, so only one set of
positions is read, chosen by trajectory. The centres of mass
(total_body_center_of_mass and segment_center_of_mass) are added as
keypoints of a <tracker>_com model, as v1 kept them in mediapipe_com,
with the keypoint names v2 gives them. (Only the body has centres of mass
in FreeMoCap's models; another aspect's would go to <tracker>_<aspect>_com,
so that segment names cannot collide.) Any other trajectory type is not
read.
v2 writes no timestamps, so time is the frame number and sampling_rate
is left unset. FreeMoCap's main pipeline does not attach reprojection
errors to the skeleton it saves, so confidence is usually all NA too.
detect_source() recognises the v2 tidy CSV. It does not claim the
per-trajectory files, whose five columns any tidy export could have, or
the Parquet file; read those with read_freemocap() directly.
An aniframe with time, model, keypoint, confidence and
x/y/z in millimetres on a 3D cartesian coordinate system. time
is seconds elapsed from start_datetime where the layout carries
timestamps, and frames where it does not. Either way the first frame is
at 0; see "Time" in read_dataset().
Matthis, J. S., Queen, P., Cherian, A., Wirth, T., Idehen, E., et al. FreeMoCap: a free, open-source markerless motion capture system (software). doi:10.5281/zenodo.7233713.
path <- system.file("extdata", "freemocap.csv", package = "aniread") read_freemocap(path) # The same recording in its by_trajectory form path <- system.file( "extdata", "freemocap_by_trajectory.csv", package = "aniread" ) read_freemocap(path) # FreeMoCap v2's tidy export, reading the rigid-body positions path <- system.file("extdata", "freemocap_v2.csv", package = "aniread") read_freemocap(path, trajectory = "rigid_3d_xyz")path <- system.file("extdata", "freemocap.csv", package = "aniread") read_freemocap(path) # The same recording in its by_trajectory form path <- system.file( "extdata", "freemocap_by_trajectory.csv", package = "aniread" ) read_freemocap(path) # FreeMoCap v2's tidy export, reading the rigid-body positions path <- system.file("extdata", "freemocap_v2.csv", package = "aniread") read_freemocap(path, trajectory = "rigid_3d_xyz")
idtracker.ai stores trajectories in image (top-left) coordinates; the
reader reflects y so the returned aniframe is in the conventional
bottom_left origin. For h5 files the frame height is read from the
file, as described below. CSV exports do not include the frame
height, so pass video_height explicitly to get an accurate flip
(otherwise max(y) is used as a fallback).
read_idtracker( path, path_probabilities = NULL, video_height = NULL, format = c("auto", "h5", "csv", "csv_tidy", "parquet") )read_idtracker( path, path_probabilities = NULL, video_height = NULL, format = c("auto", "h5", "csv", "csv_tidy", "parquet") )
path |
Path to an idtracker.ai data frame |
path_probabilities |
Path to a csv file with probabilities. Only needed if you are reading csv files as they are included in h5 files. |
video_height |
Optional numeric height of the source video frame in pixels. Overrides the value read from the h5 or Parquet file when both are available. |
format |
Which idtracker.ai export |
An h5 file also records, as attributes of its root, how the recording was tracked, and the reader keeps what has a place in the metadata:
version, the idtracker.ai version that tracked it, becomes the
source_version metadata field.
frames_per_second, the frame rate of the video, becomes the
sampling_rate metadata field. time stays the frame number, counted
from 0 as idtracker.ai counts frames, so unit_time is still "frame";
the frame rate is what converts it to seconds.
height, the height of the video frame, is what y is reflected
around, unless video_height is given. A height dataset is used
when there is no such attribute.
width, the width of the video frame, written by newer releases,
becomes the x extent in the axis_extents metadata field.
identities_labels, the names of the identities, which can be changed
in idtracker.ai's validator, name the individuals. idtracker.ai writes
"1", "2", ... when none were set, which are also the names used
when the file has no usable labels (one distinct, non-empty label per
individual).
source_version and sampling_rate stay NA when the file does not
record them, and no x extent is recorded without a width.
The CSV export keeps these in a separate attributes.json rather than in
trajectories.csv, and the reader does not read it, so a frame read from
the CSV has no source_version; set it with anicore::set_metadata().
Its time column (seconds, or time in newer releases) is the time in
seconds, so unit_time is "s", where a frame read from the h5 has
"frame". The time column still states the frame rate: idtracker.ai
writes one row per frame, with the time as the row number divided by the
frame rate, rounded to 1 ms. The reader takes the rate from the rows over
the time they span, or the whole number nearest to it when that fits every
row as well, and sets it as sampling_rate. It is left NA when the
times are not evenly spaced.
When idtracker.ai could not read the video's frame rate, it writes the
CSV export without a time column, and frames_per_second as null in
attributes.json. time is then the frame number, the row counted from
0 as the h5 reader counts frames, unit_time is "frame", and
sampling_rate is NA. Set the rate with anicore::set_metadata() if you know it.
a movement dataframe. The first frame of the video is at
time = 0, in frames or in seconds; see "Time" in read_dataset().
idtracker.ai 6.0.13 added a Parquet export, trajectories.parquet, and
6.0.14 a tidy CSV export, trajectories_tidy.csv. Both hold one row per
frame and individual, with the columns frame, time, individual, x,
y and probability, and are written only when asked for in
TRAJECTORIES_FORMATS (or by idtrackerai_format --formats csv_tidy parquet).
The Parquet file stores the attributes the h5 keeps (version,
frames_per_second, height, width, identities_labels, ...) as JSON
in its own metadata, and the reader uses them as it does the h5's.
The tidy CSV keeps them in attributes_tidy.json beside it, which the
reader reads when it is there, since idtracker.ai writes the two together
as one export.
frame and individual count from 0. Individuals are numbered from 1, as
the h5 reader numbers them, and named by identities_labels where those
are usable. probability becomes confidence. When the frame rate is
known, time is the file's time in seconds, which is the frame over the
frame rate, as the CSV export times its rows, with unit_time "s" and
the frame rate as sampling_rate. When idtracker.ai could not read the
frame rate it still writes a time column, but as the frame over 1, so
time is then the frame, counted from 0, with unit_time "frame" and
sampling_rate NA. Without
attributes_tidy.json, a tidy CSV whose time equals its frame in every
row is read as one without a frame rate, and otherwise the rate is taken
from the two columns, as for the CSV export.
Romero-Ferrero, F., Bergomi, M. G., Hinz, R. C., Heras, F. J. H., & de Polavieja, G. G. (2019). idtracker.ai: tracking all individuals in small or large collectives of unmarked animals. Nature Methods, 16, 179-182. doi:10.1038/s41592-018-0295-5.
Torrents, J., Costa, T., & de Polavieja, G. G. (2025). New idtracker.ai: rethinking multi-animal tracking as a representation learning problem to increase accuracy and reduce tracking times. eLife (reviewed preprint). doi:10.7554/eLife.107602.
path <- system.file("extdata", "idtracker.csv", package = "aniread") read_idtracker(path)path <- system.file("extdata", "idtracker.csv", package = "aniread") read_idtracker(path)
Read csv files from LightningPose (LP). Like DeepLabCut, the source
is in image (top-left) coordinates and the reader reflects y to
bottom_left.
read_lightningpose(path, video_height = NULL)read_lightningpose(path, video_height = NULL)
path |
Path to a LightningPose data file |
video_height |
Optional numeric height of the source video frame
in pixels. Falls back to |
Lightning Pose writes DeepLabCut's csv layout, so the file is read by
read_deeplabcut(). That includes the output of its Ensemble Kalman
Smoother (EKS, scorer ensemble-kalman_tracker), which has nine coords
per keypoint: x, y and likelihood are read, and the ensemble
medians and variances and the posterior variances (x_ens_median,
y_ens_median, x_ens_var, y_ens_var, x_posterior_var,
y_posterior_var) are not. Its multi-camera 3D output,
multicam_3d_results.csv, reads as 3D with x, y and z, without
confidence and without reflecting y; see read_deeplabcut().
an aniframe. time is Lightning Pose's frame index, with the
first frame of the video at 0; see "Time" in read_dataset().
Biderman, D., Whiteway, M. R., Hurwitz, C., et al. (2024). Lightning Pose: improved animal pose estimation via semi-supervised learning, Bayesian ensembling and cloud-native open-source tools. Nature Methods, 21, 1316-1328. doi:10.1038/s41592-024-02319-1.
Aharon, L., Whiteway, M. R., et al. (2026). Lightning Pose 3D: an uncertainty-aware framework for data-efficient multi-view animal pose estimation. bioRxiv. doi:10.64898/2026.04.20.719731.
path <- system.file("extdata", "lightningpose.csv", package = "aniread") read_lightningpose(path)path <- system.file("extdata", "lightningpose.csv", package = "aniread") read_lightningpose(path)
Imports pose estimation data from netCDF/HDF5 files created by the movement Python package.
read_movement(path, video_height = NULL)read_movement(path, video_height = NULL)
path |
Path to an HDF5 file ( |
video_height |
Optional numeric height of the source video frame
in pixels. movement does not currently store this in the netCDF
attributes, so without it |
The movement package stores pose estimation data in a specific netCDF/HDF5 structure
with datasets for individuals, keypoints, position coordinates, confidence
scores, and time. This function reads that structure and reshapes it into
a tidy aniframe format. The underlying tracking software outputs
image (top-left) coordinates, so the reader reflects y to
bottom_left before returning.
Since version 0.17.0, movement names the dimensions individual and
keypoint; files saved by earlier versions name them individuals and
keypoints. Both read the same way.
The axes are taken from the file's space coordinate, so a 2D dataset
gives x and y and a 3D one x, y and z. The confidence
variable becomes the confidence column. movement stores it either for
each point, with dimensions (time, keypoint, individual), or for
each individual, with dimensions (time, individual); a per-individual
score is repeated for every keypoint of that individual.
Only poses datasets are read. A bounding boxes dataset (ds_type
"bboxes") or a multi-view dataset, which has a further view
dimension, is an error.
The file's root attributes describe the recording, and the reader keeps what has a place in the metadata:
source_software becomes source, and the name of source_file
becomes filename. A file without source_file (a dataset movement
built from arrays rather than loaded from a file) takes the name of the
file read. source_format is "netcdf", the format movement saves
to, whichever suffix the file has.
time_unit becomes unit_time. movement writes "seconds" when it was
given the frame rate and "frames" when it was not; these become "s"
and "frame".
fps becomes sampling_rate. movement writes it only when it was
given one, so a file whose time is in frames leaves sampling_rate
NA. Set it with anicore::set_metadata() if you know it.
An aniframe. time is movement's time coordinate, with the first
frame of the video at 0; see "Time" in read_dataset().
Sirmpilatze, N., Lo, C. H., Miñano, S., Peri, B. D., Sharma, D., Porta, L., Varela, I., & Tyson, A. L. movement: Python tools for analysing body movements across space and time (software). doi:10.5281/zenodo.12755724.
# Two mice tracked with SLEAP, saved by movement with an fps of 50 path <- system.file("extdata", "movement.nc", package = "aniread") read_movement(path)# Two mice tracked with SLEAP, saved by movement with an fps of 50 path <- system.file("extdata", "movement.nc", package = "aniread") read_movement(path)
Reads CSV files exported from Octron video segmentation software.
The function parses the metadata header and returns tracking data
as an aniframe with centroid positions, bounding box corners, and
shape descriptors. Octron stores positions in image (top-left)
coordinates; the reader reflects y so the returned aniframe is in
the conventional bottom_left origin. The frame height is read
from the CSV header (video_height:) by default.
read_octron( path, keep_bbox = FALSE, video_height = NULL, method = c("weighted", "largest", "segments"), properties = "all" )read_octron( path, keep_bbox = FALSE, video_height = NULL, method = c("weighted", "largest", "segments"), properties = "all" )
path |
Path to the Octron CSV file. |
keep_bbox |
Keep bounding box coordinates? Default FALSE. |
video_height |
Optional numeric height of the source video frame in pixels. Overrides the value parsed from the CSV header when both are available. |
method |
Strategy for resolving frames where Octron emitted multiple mask segments per track. One of:
When the source CSV contains no tuple-valued rows, all three methods produce identical numeric output. |
properties |
Which scikit-image / Octron region-property columns to read. Newer Octron exports include dozens of per-segment shape, intensity and moment descriptors that can dominate read time on tuple-heavy files. One of:
When |
Newer Octron exports (>= the multi-blob handling in
OCTRON-GUI #63)
may emit per-segment columns as tuple-strings, e.g. "(120.5, 85.3)",
when YOLO detects multiple disconnected mask segments belonging to
the same track in a single frame. The method argument controls
how those rows are reduced to scalar values, or whether they are
expanded into one row per segment.
An anipoint. time is OCTRON's frame_idx, with the first frame
of the video at 0; see "Time" in read_dataset().
Octron's orientation is scikit-image's region property: the angle of the
mask's long axis, axial (the mask has no front), measured from the image's
row axis. It is kept as Octron wrote it, in image coordinates: it is not
reflected with y and not declared as the frame's orientation.
Jacobsen, R. I., et al. (2025). OCTRON: a general purpose segmentation and tracking pipeline for behavioral experiments. bioRxiv. doi:10.64898/2025.12.20.695663.
path <- system.file("extdata", "octron.csv", package = "aniread") read_octron(path)path <- system.file("extdata", "octron.csv", package = "aniread") read_octron(path)
Reads either of SLEAP's analysis exports: the HDF5 file, or the CSV
with columns track, frame_idx, instance.score and a
.x/.y/.score triple per node.
read_sleap(path, video_height = NULL)read_sleap(path, video_height = NULL)
path |
A SLEAP analysis file, either HDF5 ( |
video_height |
Optional numeric height of the source video frame in pixels. |
SLEAP stores predictions in image (top-left) coordinates; the reader
reflects y so the returned aniframe is in the conventional
bottom_left origin. Neither export includes the source video
resolution, so pass video_height to get an accurate flip — otherwise
max(y) is used as a fallback.
individual is the track name SLEAP recorded, from either export. A
recording with no tracks - a single unnamed instance, or predictions that
were never tracked - has no names to use, and falls back to
individual1, individual2, and so on. In the CSV, where such rows have
an empty track, they are numbered in the order they appear within each
frame. sleap-io (which writes SLEAP's exports from SLEAP 1.6.3 on) gives
the instances of an untracked recording the names track_0, track_1,
... in the .h5, and those are kept like any other track name.
Both the files SLEAP wrote itself and those written by sleap-io are read.
sleap-io can store the .h5 arrays in another axis order (its "standard"
preset, or custom axes) and records the order in each dataset's dims
attribute, which the reader follows; a file without it has SLEAP's
original layout. The frame axis of a sleap-io .h5 runs to the end of the
video, so frames after the last detection come back as NA rows, as
undetected frames always have. Very old .h5 files have no
point_scores, and give NA confidence.
The .h5 also carries the skeleton the recording was tracked with and a
record of the run, and both are kept:
The skeleton is attached as the frame's keypoint structure, read as
read_structure_sleap() reads it: the nodes become points and the body
edges segments. A file written without edges gives points alone.
The SLEAP version that ran the tracking, from the file's provenance
record, becomes the source_version metadata field. When the record
names none, a file written by sleap-io gives the sleap-io version that
wrote it instead, as "sleap-io 0.9.2" for example. It stays NA when
the file records neither.
The CSV carries neither, so a frame read from it has no structure and no
source_version. Attach one with read_structure() and
anicore::set_structure().
a movement dataframe. time is SLEAP's frame index, so the first
frame of the video is at time = 0; see "Time" in read_dataset().
Pereira, T. D., Tabris, N., Matsliah, A., et al. (2022). SLEAP: A deep learning system for multi-animal pose tracking. Nature Methods, 19, 486-495. doi:10.1038/s41592-022-01426-1.
# Two flies tracked with SLEAP, in the analysis CSV path <- system.file("extdata", "sleap.analysis.csv", package = "aniread") read_sleap(path) # The same predictions in the analysis .h5, whose frames run to the end of # the video, and which also carries the skeleton and the SLEAP version path <- system.file("extdata", "sleap.analysis.h5", package = "aniread") flies <- read_sleap(path) flies anicore::get_structure(flies) anicore::get_metadata(flies, "source_version")# Two flies tracked with SLEAP, in the analysis CSV path <- system.file("extdata", "sleap.analysis.csv", package = "aniread") read_sleap(path) # The same predictions in the analysis .h5, whose frames run to the end of # the video, and which also carries the skeleton and the SLEAP version path <- system.file("extdata", "sleap.analysis.h5", package = "aniread") flies <- read_sleap(path) flies anicore::get_structure(flies) anicore::get_metadata(flies, "source_version")
Reads the skeleton a pose-estimation project defines — its keypoints and
the edges between them — as an anicore::anistructure() with points and
segments. Attach it to a frame with anicore::set_structure().
read_structure_deeplabcut() reads a DeepLabCut project's
config.yaml: bodyparts, or multianimalbodyparts in a multi-animal
project, and skeleton. A multi-animal project's uniquebodyparts are
left out: DeepLabCut assigns them to a separate "single" individual,
not to each animal's body.
read_structure_sleap() reads a SLEAP .slp file (project or
predictions) or an analysis .h5 export. Only body edges become
segments; SLEAP's symmetry edges pair left and right keypoints and are
not segments.
read_structure() detects which from the file, as read_dataset() does
for frames.
Edges keep the direction the file lists them in, which is what
anicore::as_anisegment() measures along.
A frame reader attaches whatever its data file itself contains, so whether you need these depends on where the tool keeps its skeleton:
A SLEAP analysis .h5 carries its skeleton, and read_sleap() attaches
it as it reads the tracks.
DeepLabCut keeps its skeleton in the project's config.yaml, not in the
data file, so read_deeplabcut() attaches none. Read the config with
read_structure() and attach it with anicore::set_structure().
SLEAP's analysis CSV carries no skeleton either. Read it from the .slp
or .h5 of the same recording.
read_structure(path, source = "auto") read_structure_deeplabcut(path) read_structure_sleap(path, skeleton = NULL)read_structure(path, source = "auto") read_structure_deeplabcut(path) read_structure_sleap(path, skeleton = NULL)
path |
Path to the file. |
source |
|
skeleton |
For a |
# A DeepLabCut project keeps its skeleton in config.yaml config <- tempfile(fileext = ".yaml") writeLines( c( "bodyparts:", "- head", "- stinger", "skeleton:", "- - head", " - stinger" ), config ) skeleton <- read_structure(config) skeleton # Attach it to a frame with the same body parts path <- system.file("extdata", "deeplabcut.csv", package = "aniread") read_deeplabcut(path) |> anicore::set_structure(skeleton) # A SLEAP analysis .h5 carries the skeleton of its tracks path <- system.file("extdata", "sleap.analysis.h5", package = "aniread") skeleton <- read_structure(path) skeleton# A DeepLabCut project keeps its skeleton in config.yaml config <- tempfile(fileext = ".yaml") writeLines( c( "bodyparts:", "- head", "- stinger", "skeleton:", "- - head", " - stinger" ), config ) skeleton <- read_structure(config) skeleton # Attach it to a frame with the same body parts path <- system.file("extdata", "deeplabcut.csv", package = "aniread") read_deeplabcut(path) |> anicore::set_structure(skeleton) # A SLEAP analysis .h5 carries the skeleton of its tracks path <- system.file("extdata", "sleap.analysis.h5", package = "aniread") skeleton <- read_structure(path) skeleton
Read trackball data from a variety of setups and configurations.
read_trackball( paths, setup = c("of_free", "of_fixed"), sampling_rate, col_time = "time", col_dx = "x", col_dy = "y", counts_per_rotation = NULL, ball_diameter = NULL, dots_per_cm = NULL, quiet = TRUE )read_trackball( paths, setup = c("of_free", "of_fixed"), sampling_rate, col_time = "time", col_dx = "x", col_dy = "y", counts_per_rotation = NULL, ball_diameter = NULL, dots_per_cm = NULL, quiet = TRUE )
paths |
Two file paths, one for each sensor (although one is allowed for a fixed setup, |
setup |
Which type of experimental setup was used. Expects either |
sampling_rate |
Sampling rate tells the function how long time it should integrate over. A sampling rate of 60(Hz) will mean windows of 1/60 sec are used to integrate over. |
col_time |
Which column contains the information about time. Can be
specified either by the column number (numeric) or the name of the column if
it has one (character). Should either be a datetime (POSIXt) or seconds
(numeric). With two sensors, this must be a clock the two sensors share.
In a Bonsai capture that is the PC datetime (column 4), not the per-board
device counter (column 3), whose origin is sensor-local and therefore
cannot be used to cross-reference the two files. A warning is emitted if
|
col_dx |
Column name for x-axis values |
col_dy |
Column name for y-axis values |
counts_per_rotation |
For |
ball_diameter |
For |
dots_per_cm |
For |
quiet |
If |
Raw Bonsai optical-flow captures are headerless CSV files, optionally preceded
by serial-port junk – a partial row, or a run of noise lines and blanks
before the first complete record – with the layout
dx, dy, device_clock_us, pc_datetime, interval_s. Address their columns by
number (col_dx = 1, col_dy = 2, col_time = 4).
Such captures are not gap-free: on this rig the COM port emits no row while
the ball is still, so multi-second gaps are normal. Readings are integrated
into 1 / sampling_rate windows, and windows containing no reading are
filled with zero motion so the returned time grid is regular regardless of
the gaps in the input. Note that this treats a missing sample as no motion,
which is a property of this logger rather than of optical flow in general; a
sensor that drops samples for other reasons would need its gaps treated as
missing data instead.
With two sensors the output covers only the intersection of the two
recordings - readings from before the second sensor started, or after the
first stopped, are discarded rather than zero-filled. time = 0 is the first
shared sample, and the start_datetime metadata is the wall-clock instant of
that sample.
The sensor has no fixed rate of its own: it reports motion as it happens,
and nothing while the ball is still. The frame's sampling_rate is
therefore the rate of the integration windows, and its
source_sampling_rate is NaN, anicore's marker for a device with no
fixed rate (see anicore::set_metadata()), rather than NA, which would
mean the rate has not been declared.
a movement dataframe. time is in seconds from the first sample
both sensors share; see "Time" in read_dataset(). sampling_rate is
the sampling_rate given, the rate of the integration windows, and
source_sampling_rate is NaN, since the sensor has no fixed rate.
# A free-floating ball is tracked by two optical flow sensors, # so both files are supplied together paths <- c( system.file("extdata", "trackball_sensor_1.csv", package = "aniread"), system.file("extdata", "trackball_sensor_2.csv", package = "aniread") ) read_trackball(paths, setup = "of_free", sampling_rate = 60, col_time = "t")# A free-floating ball is tracked by two optical flow sensors, # so both files are supplied together paths <- c( system.file("extdata", "trackball_sensor_1.csv", package = "aniread"), system.file("extdata", "trackball_sensor_2.csv", package = "aniread") ) read_trackball(paths, setup = "of_free", sampling_rate = 60, col_time = "t")
Parses a TrackMate XML file and returns spot data from filtered tracks
as an aniframe. TrackMate stores spot coordinates in image (top-left)
coordinates; the reader reflects y so the returned aniframe is in the
conventional bottom_left origin. The frame height is read from
Settings/ImageData in the XML by default: height is in pixels and
pixelheight in spatialunits, so their product is the height in the
unit of the positions. Without a pixelheight, height is used as it is.
read_trackmate(path, slim = TRUE, video_height = NULL)read_trackmate(path, slim = TRUE, video_height = NULL)
path |
Path to the TrackMate XML file. |
slim |
If TRUE, return only essential columns (default TRUE). |
video_height |
Optional numeric height of the source frame in the
spatial unit reported by the XML. Overrides the height read from
|
Each track is identified in track by the name TrackMate gave it.
TrackMate names tracks Track_0, Track_1, ..., and the column already
says they are tracks, so a name of that form becomes its number alone:
Track_12 is read as 12. A name you gave a track in TrackMate, anything
not of the form Track_<number>, is kept as written. Each track's name is
read on its own, so a file in which you renamed some tracks gives, say,
0, 2 and Cell A. The numeric TRACK_ID is used instead when any
track has no name, or, with a warning, when two tracks would get the same
id (such as a track you renamed 3 beside Track_3). The levels of
track are the numbers in numeric order, 2 before 10, followed by
any names sorted as text (capitals first, in every locale).
A track that divides, as a cell lineage does, holds more than one spot in
a frame from its first division on, which a frame keyed by track and
time cannot hold. Such a track is split into its branches, each the
stretch of the track between divisions, and each branch becomes a track
of its own. The parent column records the lineage, as cell tracking
does (the P of the Cell Tracking Challenge's res_track.txt, Ultrack's
parent_track_id): it holds the id of the track a track divided from,
and is NA for a track that did not divide from another, so for every
track of a file without divisions. The branch before the first division
keeps the track's id. The other branches are numbered on from the
largest number that identifies any track in the file, by TRACK_ID or
by name, so a new id never names another track you can see in TrackMate.
They are numbered track by track, in the order of the tracks' ids, and
within a track by the frame they start in, then by the id of the track
they divided from, so sisters are numbered together, then by x and y. A
daughter starts at its first spot after the division, so nothing is
measured across it. A track whose branches also merge, or whose spots
share a frame in any other way, is left whole, with a warning about the
duplicate track-frame combinations. A split or merge that never puts two
spots in one frame, as when a link skips a frame beside another that does
not, needs no splitting.
parent is not an identity key: the keys are track and keypoint. It
is a factor with the same levels as track, so its values match the
ids in track. One row per track gives the lineage, as the Cell Tracking
Challenge's L B E P table (label, first and last time, parent). The
frame is grouped by its keys, so drop to a plain tibble first:
data |>
dplyr::as_tibble() |>
dplyr::summarise(
start = min(time),
end = max(time),
parent = dplyr::first(parent),
.by = track
)
The XML also records how the image was calibrated, and the reader keeps what has a place in the metadata:
timeunits and spatialunits, from the Model element, become
unit_time and unit_space. TrackMate takes them from the image's
calibration in ImageJ, where they are free text, so the usual spellings
are recognised: "sec", "msec", "min", "frame" and so on for time,
"pixel", "micron", "um" (with or without the micro sign), "mm"
and so on for space. A unit with no equivalent in anicore (days or
inches, say) becomes "unknown" or "none", with a warning, rather
than stopping the read. A blank spatial unit, which TrackMate writes for
an image whose length unit is a space, is read as pixels when the pixel
size in Settings/ImageData is 1 or not recorded, as ImageJ calls an
image with no length unit "pixel". With any other pixel size the
positions are scaled by a unit nobody named, so it becomes "none"
with a warning.
timeinterval, from Settings/ImageData, is the time between frames in
timeunits. Its reciprocal, converted to Hz, becomes sampling_rate.
It stays NA when the interval is missing or not positive, or when the
time unit is frames or not recognised.
version, from the root element, is the TrackMate version that wrote
the file and becomes source_version.
TrackMate cannot tell an uncalibrated image from one calibrated at one
second per frame. An image with no time calibration has a frame interval
of 0, which TrackMate replaces with 1, and ImageJ's default time unit is
"sec". A file with timeunits = "sec" and timeinterval = 1 may
therefore really count frames, in which case time is the frame number
and the 1 Hz sampling_rate is not the camera's. If you know the real
rate, set it with anicore::set_metadata().
An aniframe with columns including time, track, keypoint,
parent, x, y, z and frame.
Tinevez, J.-Y., Perry, N., Schindelin, J., Hoopes, G. M., Reynolds, G. D., Laplantine, E., Bednarek, S. Y., Shorte, S. L., & Eliceiri, K. W. (2017). TrackMate: An open and extensible platform for single-particle tracking. Methods, 115, 80-90. doi:10.1016/j.ymeth.2016.09.016.
Ershov, D., Phan, M.-S., Pylvänäinen, J. W., et al. (2022). TrackMate 7: integrating state-of-the-art segmentation algorithms into tracking pipelines. Nature Methods, 19, 829-832. doi:10.1038/s41592-022-01507-1.
# A cell that divides into tracks 1 and 2, whose parent is track 0 path <- system.file("extdata", "trackmate.xml", package = "aniread") data <- read_trackmate(path) data # The lineage: one row per track, with the track it divided from data |> dplyr::as_tibble() |> dplyr::summarise( start = min(time), end = max(time), parent = dplyr::first(parent), .by = track )# A cell that divides into tracks 1 and 2, whose parent is track 0 path <- system.file("extdata", "trackmate.xml", package = "aniread") data <- read_trackmate(path) data # The lineage: one row per track, with the track it divided from data |> dplyr::as_tibble() |> dplyr::summarise( start = min(time), end = max(time), parent = dplyr::first(parent), .by = track )
Reads movement tracking data exported from TRex (Walter & Couzin, 2021), from either of its two exports.
The .npz export is TRex's native one: a zip of .npy arrays, one file
per tracked individual, so a whole recording is a vector of paths. It
carries the pose keypoints, a per-frame detection probability, the
identity, and the recording's frame rate and frame size. The CSV export
carries the centroid and midline of a single individual and none of that
metadata, so several columns come back NA.
Both exports give time in seconds, so unit_time is "s". The .npz
records the frame rate as frame_rate, which becomes sampling_rate. The
CSV does not record it, but when it has both a frame and a time column,
and every time is the frame number divided by one rate (to within the
rounding of time), that rate becomes sampling_rate: the frames between
the first and last row over the seconds between them, or the whole number
nearest to it when that fits every row as well. Otherwise, as when the
times are camera timestamps that vary from frame to frame, it is left NA.
Both exports give positions in centimetres, so unit_space is "cm".
TRex converts from pixels with its cm_per_pixel, the real width of the
image (meta_real_width) over its width in pixels. When nobody set the
real width, TRex assumes 30 cm, so the centimetres are only as real as
that setting. The .npz records the frame size as video_size, in
pixels, and cm_per_pixel; their product is the frame's width and height
in centimetres, which become the axis_extents of x and y and what y is
reflected around.
read_trex(path, format = c("auto", "csv", "npz"), video_height = NULL)read_trex(path, format = c("auto", "csv", "npz"), video_height = NULL)
path |
Character string specifying the path to a TRex CSV file. The file should contain columns for:
|
format |
Which export to read. |
video_height |
Optional numeric height of the source video frame
in the same spatial units as the tracking output, centimetres. The |
The function performs several processing steps:
Validates the input file format (must be CSV)
Reads the data using vroom for efficient processing
Cleans column names to a consistent format
Restructures the data from wide to long format
Initializes metadata fields required for movement data
A data frame containing movement data with the following columns:
time: TRex's time, in seconds, with the first frame of the video
at 0; see "Time" in read_dataset()
individual: Factor. The identity TRex assigned, from the .npz
export; NA from the CSV export, which does not record it
keypoint: Factor identifying tracked points (e.g., "head", "centroid")
x: x-coordinates in centimeters
y: y-coordinates in centimeters
confidence: Numeric. TRex's per-frame detection_p from the .npz
export; NA from the CSV export, which does not record it
yaw: TRex's ANGLE, the direction the individual faces, from its
posture, when the export includes it. Declared as the frame's
orientation, so it is reflected with y; the same value on every
keypoint of an individual.
Walter, T., & Couzin, I. D. (2021). TRex, a fast multi-animal tracking system with markerless identification, and 2D estimation of posture and visual fields. eLife, 10, e64000. doi:10.7554/eLife.64000.
TRex software: https://trex.run
# TRex's native export: one .npz per tracked individual path <- system.file("extdata", "trex_id3.npz", package = "aniread") read_trex(path) # A whole recording is the vector of paths get_sample_data() returns read_trex(get_sample_data("trex"))# TRex's native export: one .npz per tracked individual path <- system.file("extdata", "trex_id3.npz", package = "aniread") read_trex(path) # A whole recording is the vector of paths get_sample_data() returns read_trex(get_sample_data("trex"))
This high‑level wrapper writes an aniframe object to a file in one of the
supported formats (parquet, csv, or tsv). We highly recommend using parquet
as neither csv or tsv can preserve the metadata.
write_aniframe(data, filename, ...)write_aniframe(data, filename, ...)
data |
An aniframe object (see |
filename |
Character string specifying the output file name. The file extension determines which writer is used. |
... |
Additional arguments passed to the format‑specific writer
( |
Supported extensions are "parquet", "csv" and "tsv". We highly recommend using parquet
as neither csv or tsv can preserve the metadata.
CSV/TSV files are written with vroom for fast I/O: .csv comma-separated
and .tsv tab-separated, unless delim is passed.
Parquet files are written with the arrow package is installed (install‑on‑demand if missing).
The original data object (invisibly), enabling pipe‑friendly usage.
## Create a small aniframe for demonstration df <- anicore::example_anipoint() ## Write the aniframe as CSV write_aniframe(df, tempfile(fileext = ".csv")) ## Write the same aniframe as Parquet write_aniframe(df, tempfile(fileext = ".parquet"))## Create a small aniframe for demonstration df <- anicore::example_anipoint() ## Write the aniframe as CSV write_aniframe(df, tempfile(fileext = ".csv")) ## Write the same aniframe as Parquet write_aniframe(df, tempfile(fileext = ".parquet"))
One entry point for every format aniread writes, the counterpart of
read_dataset(). By default the format is worked out from the file
suffix.
write_dataset(data, path, format = NULL, by = NULL, ...)write_dataset(data, path, format = NULL, by = NULL, ...)
data |
An aniframe: an anipoint, anievent, or another frame built on them. |
path |
Path to write to. |
format |
Which format to write. |
by |
Key columns to write one file per combination of, or |
... |
Passed on to the writer for |
write_dataset() is a dispatcher, not a new writer: it works out which
writer to call and calls it.
When several formats write the same suffix, the first listed in
get_supported_sources() is inferred: a .csv is written as a plain table
by write_aniframe(), and inTRACKtive's CSV needs
format = "intracktive".
Parquet keeps everything: read_dataset() gives back the same class,
grouping and metadata that were written. Other formats keep what the
format can hold.
data, invisibly.
With by, each file is named from path with the key values appended
before the suffix: "mice.csv" with by = "individual" gives
mice_individual-mouse1.csv, mice_individual-mouse2.csv, and so on, and
several keys give mice_individual-mouse1_session-2.csv.
For other names, put the keys in path in braces:
"{session}/mice_{individual}.csv". The braces then set by, so it can be
left out, and directories in path are created. Characters other than
letters, digits, ., _ and - in a value become -.
Each file holds an aniframe with the same class and metadata as data.
read_dataset() to read the file back, get_supported_sources()
for what can be written, and the individual write_*() functions for
format-specific arguments.
data <- anicore::example_anipoint() path <- tempfile(fileext = ".parquet") write_dataset(data, path) read_dataset(path) # Name the format where the suffix is shared write_dataset(data, tempfile(fileext = ".csv"), format = "intracktive") # One file per individual, or per track dir <- tempfile() dir.create(dir) write_dataset(data, file.path(dir, "mice.parquet"), by = "individual") write_dataset(data, file.path(dir, "{individual}/{keypoint}.parquet")) list.files(dir, recursive = TRUE)data <- anicore::example_anipoint() path <- tempfile(fileext = ".parquet") write_dataset(data, path) read_dataset(path) # Name the format where the suffix is shared write_dataset(data, tempfile(fileext = ".csv"), format = "intracktive") # One file per individual, or per track dir <- tempfile() dir.create(dir) write_dataset(data, file.path(dir, "mice.parquet"), by = "individual") write_dataset(data, file.path(dir, "{individual}/{keypoint}.parquet")) list.files(dir, recursive = TRUE)
Converts an aniframe to the CSV format required by inTRACKtive for browser-based interactive visualization of tracking data. The function creates unique integer track identifiers from combinations of the aniframe's identity keys, and writes the lineage of dividing tracks.
write_intracktive(data, filename, quiet = FALSE)write_intracktive(data, filename, quiet = FALSE)
data |
An aniframe containing tracking data with its index (usually
|
filename |
File path to write the CSV. |
quiet |
Suppress messages. TRUE/FALSE. Defaults to FALSE. |
inTRACKtive requires tracking data with a unique integer track_id for each
tracked object. This function numbers the tracks from 1, one for each
combination of the aniframe's identity keys (anicore::get_keys()), such
as individual and keypoint, or track and keypoint for a frame read
by read_trackmate().
The output format includes:
track_id: Integer identifier for each unique track
t: The frame number of each row, counted from 0 (see below)
x, y: Spatial coordinates
z: Optional third dimension if present
parent_track_id: Only when data has a parent column, as
read_trackmate() gives a frame with dividing tracks. parent names the
track a track divided from, and parent_track_id is that track's
track_id (with the same values of the other keys), so inTRACKtive draws
the lineage. A track with no parent gets -1, inTRACKtive's value for
one, as does a track whose parent is not in data (with a warning).
Without a parent column the column is left out, which inTRACKtive
reads as no divisions.
inTRACKtive reads t as whole frames counted from 0, so the index is
written in frames, as anicore::convert_unit_time() gives them with
"frame":
A frame whose unit_time is "frame" is written as it is. Its times must
be whole numbers.
A frame whose time is in another unit, such as seconds or minutes, is
converted to frames with its sampling_rate, so that the frame at time 0
is t = 0: at 30 Hz, times of 0, 1/30 and 2/30 seconds are written as 0,
1 and 2. A frame with a column named frame holding the recorded frame
numbers, as anicore::set_index() keeps them, is written with those.
The writer stops rather than write frame numbers it would have to invent:
when the time is not in frames and no sampling_rate is declared, or when
the times are not regularly spaced at that rate. Declare the rate with
anicore::set_metadata(data, sampling_rate = ), or keep the recorded frame
numbers in a column named frame. data itself is not changed.
The resulting CSV can be converted to inTRACKtive's Zarr format using their command-line tools or Python package.
Returns the input data unchanged.
Huijben, T. A. P. M., Anderson, A. G., Sweet, A., et al. (2025). inTRACKtive: a web-based tool for interactive cell tracking visualization. Nature Methods. doi:10.1038/s41592-025-02777-1.
# Two flies tracked with SLEAP, one track for each fly and keypoint flies <- read_sleap( system.file("extdata", "sleap.analysis.csv", package = "aniread") ) path <- tempfile(fileext = ".csv") write_intracktive(flies, path) head(read.csv(path))# Two flies tracked with SLEAP, one track for each fly and keypoint flies <- read_sleap( system.file("extdata", "sleap.analysis.csv", package = "aniread") ) path <- tempfile(fileext = ".csv") write_intracktive(flies, path) head(read.csv(path))