Overview
radiatR provides a flexible loader framework that turns
tracking exports from a wide variety of systems into
TrajSet objects. The helpers cover three typical tasks:
- Reading experiment manifests (
import_info()andimport_tracks()). - Ingesting trajectory tables with
TrajSet_read()orTrajSet_read_dir(). - Extending the loader registry with
register_loader_dialect()for custom formats.
This vignette walks through each step using the bundled millipede example data and finishes with a user-defined loader for a custom format.
Loading a Real Experiment
The package ships the trials from a Cylindroiulus punctatus
(millipede) visual orientation experiment. Tracks are stored as paired
tab-separated text files: _point01.txt holds two landmark
rows (arena centre + target location on the wall);
_point02.txt holds the full per-frame xy trajectory.
Step 1 — discover files
import_tracks() scans a directory for
_point01.txt / _point02.txt pairs and returns
a tibble of basenames.
track_dir <- system.file("extdata", "tracks", package = "radiatR")
file_tbl <- import_tracks(track_dir)
head(file_tbl)
#> # A tibble: 6 × 3
#> basename landmark track
#> <chr> <chr> <chr>
#> 1 10_1 10_1_point01.txt 10_1_point02.txt
#> 2 10_10 10_10_point01.txt 10_10_point02.txt
#> 3 10_11 10_11_point01.txt 10_11_point02.txt
#> 4 10_12 10_12_point01.txt 10_12_point02.txt
#> 5 10_13 10_13_point01.txt 10_13_point02.txt
#> 6 10_14 10_14_point01.txt 10_14_point02.txtStep 2 — read the manifest
import_info() parses the trial manifest CSV. Use
cond_cols to generate a compound condition column from
multiple metadata fields.
manifest_path <- system.file("extdata", "millipede_trials.csv", package = "radiatR")
manifest <- import_info(manifest_path, cond_cols = c("type", "arc"))
head(manifest)
#> file arc id stimulus_period recorded_radian recorded_degree type obstacle
#> 1 con_1 0 1 0 -1.473503 -84.42550 control none
#> 2 con_2 0 2 0 1.295963 74.25323 control none
#> 3 con_3 0 3 0 -0.314510 -18.02010 control none
#> 4 con_5 0 5 0 -2.070025 -118.60370 control none
#> 5 con_6 0 6 0 -2.907256 -166.57352 control none
#> 6 con_7 0 7 0 -1.601138 -91.73845 control none
#> cond
#> 1 control_0
#> 2 control_0
#> 3 control_0
#> 4 control_0
#> 5 control_0
#> 6 control_0Step 3 — join metadata
load_tracks() merges the manifest into
file_tbl, matching on the file column. The
resulting tibble carries arc, type, obstacle, and id alongside each file
path.
file_tbl <- load_tracks(file_tbl, manifest, track_dir)
#> Warning in .augment_with_manifest(file_tbl, df, manifest_cols): Entries in
#> `file_tbl` with no matching metadata: con_19
#> Warning in .augment_with_manifest(file_tbl, df, manifest_cols): Rows in
#> `manifest` with no corresponding track: con_101, con_102, con_104, con_105,
#> con_108, con_109, con_110, con_112, con_116, con_117, con_119, con_120,
#> con_121, 5_101, 5_102, 5_103, 5_104, 5_108, 5_110, 5_117, 5_118, 5_119, 5_121,
#> 10_101, 10_102, 10_105, 10_107, 10_108, 10_109, 10_110, 10_111, 10_112, 10_113,
#> 10_114, 10_116, 10_117, 10_119, 10_121, 15_101, 15_102, 15_104, 15_105, 15_107,
#> 15_109, 15_110, 15_112, 15_116, 15_119, 15_120, 15_121, 20_101, 20_102, 20_103,
#> 20_104, 20_105, 20_106, 20_107, 20_108, 20_109, 20_110, 20_112, 20_116, 20_117,
#> 20_119, 20_121, 30_101, 30_102, 30_104, 30_105, 30_106, 30_107, 30_108, 30_109,
#> 30_113, 30_114, 30_115, 30_116, 30_117, 30_119, 30_121, 40_101, 40_102, 40_103,
#> 40_104, 40_105, 40_107, 40_108, 40_109, 40_110, 40_112, 40_118, 40_119, 40_121,
#> 50_12, 50_34, 50_101, 50_104, 50_105, 50_107, 50_108, 50_109, 50_110, 50_112,
#> 50_113, 50_119, 50_121
head(file_tbl[, c("basename", "arc", "type", "obstacle", "id")])
#> # A tibble: 6 × 5
#> basename arc type obstacle id
#> <chr> <int> <chr> <chr> <int>
#> 1 10_1 10 stimulus none 1
#> 2 10_10 10 stimulus none 10
#> 3 10_11 10 stimulus none 11
#> 4 10_12 10 stimulus none 12
#> 5 10_13 10 stimulus none 13
#> 6 10_14 10 stimulus none 14Step 4 — extract and normalise trajectories
get_all_object_pos() reads each file pair, uses the
landmark rows to establish the arena geometry, and returns a
TrajSet with normalised unit-circle coordinates.
ts <- suppressWarnings(get_all_object_pos(file_tbl = file_tbl, track_dir = track_dir))
ts
#> TrajSet: 235 trajectories, 44331 observations
#> Columns: id='trial_id', time='frame', angle='rel_theta' (radians), x='trans_x', y='trans_y', rel_x='rel_x', rel_y='rel_y'
#> Transform steps: unit_circle_mapping
#> # A tibble: 6 × 15
#> trial_id frame x y trans_x trans_y trans_rho abs_theta rel_theta
#> <chr> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 10_1_1 1 456. 372. 0.00511 0 0.00511 0 6.21
#> 2 10_1_1 2 458. 370. 0.0128 0.00770 0.0149 0.541 0.464
#> 3 10_1_1 3 456. 370. 0.00511 0.00770 0.00924 0.985 0.908
#> 4 10_1_1 4 454. 370. 0 0.00770 0.00770 1.57 1.49
#> 5 10_1_1 5 456. 377. 0.00511 -0.0153 0.0162 5.03 4.96
#> 6 10_1_1 6 459. 382. 0.0154 -0.0307 0.0343 5.18 5.10
#> # ℹ 6 more variables: rel_x <dbl>, rel_y <dbl>, video <chr>, order <chr>,
#> # vid_ord <chr>, radius <dbl>dtrack format
The bundled example data was tracked with dtrack (https://bitbucket.org/jochensmolka/dtrack), a desktop tracking tool for video recordings of freely-moving animals. The data are from:
Kirwan J.D. & Nilsson D.-E. (2019). A millipede compound eye mediating low-resolution vision. Vision Research 165, 36–44. https://doi.org/10.1016/j.visres.2019.09.003
dtrack exports tab-separated text files with columns
frame, x, y, and a fourth
confidence/flag column. Use dtrack_read() to load a single
trajectory file directly:
track_file <- system.file(
"extdata", "tracks", "10_10_point02.txt",
package = "radiatR"
)
ts_raw <- dtrack_read(track_file)
head(ts_raw@data[, c("id", "frame", "x", "y")])
#> id frame x y
#> 1 10_10_point02 1 469.83 379.47
#> 2 10_10_point02 2 477.73 386.58
#> 3 10_10_point02 3 479.31 388.95
#> 4 10_10_point02 4 482.48 400.81
#> 5 10_10_point02 5 484.85 406.34
#> 6 10_10_point02 6 484.06 417.40The _point01 / _point02 role split used in
the bundled data — landmarks in one file, trajectory in the other — is
specific to this experiment and is not a general dtrack convention.
Tracking-tool format support
radiatR reads tabular (CSV/TSV) exports from many
tracking tools through named dialects. Each dialect maps a
tool’s column conventions onto the TrajSet model of one
position per individual per frame. Pass the dialect name to
TrajSet_read(path, dialect = "name"); tool-specific options
go through dialect_args.
The table below summarises what is and is not currently handled. Two limitations are general:
-
Tabular text only. Dialects read CSV/TSV (and,
where the optional packages are installed, JSON/XML/Parquet). Native
binary formats — such as NumPy
.npzor MATLAB.mat— are not read except where noted (ctraxreads.matvia theR.matlabpackage). -
Position, not posture.
radiatRmodels a single position per individual per frame. Per-frame posture data (variable-length midline or outline polylines) is outside this model. Where a tool tracks discrete body keypoints, those can be loaded as extra columns and turned into a body-axis heading with thebodypart_axisrule orpose_to_headings(); dense posture polylines are not ingested.
| Dialect | Handled | Not handled |
|---|---|---|
trex |
Positional CSV: plain X/Y, the
#wcentroid / #centroid /
#pcentroid centroid variants, and TRex’s (cm)
/ (px) unit annotations in headers. Per-individual
_fishN files (id from filename) and aggregated files with
an id column. |
Native .npz export; posture export
(midline_points, outline_points). |
deeplabcut / deeplabcut_multiheader
|
Multi-bodypart CSV; single bodypart, or a likelihood-weighted
centroid of several; three-row scorer/bodypart/coord header. Bodypart
columns are preserved for bodypart_axis. |
Native HDF5 (.h5) export. |
sleap |
Analysis CSV: <node>.x / .y /
.score; multi-node centroid; track and
frame_idx. |
Native .slp / HDF5 export. |
ethovision |
Centre position, and multi-zone exports (nose, tail, etc.); zone centroid or single-zone selection. | Native .xlsx (export to CSV first). |
anymaze |
Centre position, optional nose/tail zones, units row. | — |
trackmate |
TRACK_ID / FRAME / POSITION_X
/ POSITION_Y CSV. |
Native TrackMate .xml. |
ctrax |
.mat file with the trx struct (centroid +
ellipse theta/a/b); needs
R.matlab. |
— |
idtrackerai_wide, toxtrac,
boris_xy, tracktor, dtrack
|
Their standard CSV/TSV column layouts. | Tool-specific binary or session formats. |
When a tool’s export is not directly supported, two escape hatches
remain: convert to a tidy CSV and use TrajSet_read() with
an explicit mapping, or register a custom dialect (see
Registering a Custom Dialect below).
Bundled single-file examples
The package ships small real exports from three tools so the dialects
can be tried without any external data. Each loads with one
TrajSet_read() call, pointing the dialect
argument at the matching tool.
DeepLabCut — a three-row scorer/bodypart/coord
header; by default the position is the likelihood-weighted centroid of
all bodyparts, and each bodypart’s coordinates are retained for use with
the bodypart_axis rule.
dlc_path <- system.file("extdata", "dlc_CollectedData_Pranav.csv",
package = "radiatR")
ts_dlc <- TrajSet_read(dlc_path, dialect = "deeplabcut_multiheader")
#> New names:
#> • `Pranav` -> `Pranav...2`
#> • `Pranav` -> `Pranav...3`
#> • `Pranav` -> `Pranav...4`
#> • `Pranav` -> `Pranav...5`
#> • `Pranav` -> `Pranav...6`
#> • `Pranav` -> `Pranav...7`
#> • `Pranav` -> `Pranav...8`
#> • `Pranav` -> `Pranav...9`
ts_dlc
#> TrajSet: 1 trajectories, 116 observations
#> Columns: id='id', time='time', angle='angle' (radians), x='x', y='y', raw_x='x_raw', raw_y='y_raw'
#> id time x y snout_x snout_y leftear_x leftear_y rightear_x
#> 1 1 1 0.1713185 0.9852157 21.521 265.428 33.819 265.941 19.984
#> 2 1 2 0.1261041 0.9920170 10.248 288.487 19.984 297.198 12.298
#> 3 1 3 0.1410944 0.9899961 24.596 354.075 38.431 354.075 23.058
#> 4 1 4 0.2247796 0.9744096 73.787 374.572 78.911 366.373 57.390
#> 5 1 5 0.2005174 0.9796901 38.431 333.066 50.729 341.777 39.968
#> 6 1 6 0.1613250 0.9869013 23.571 327.430 30.745 339.215 28.183
#> rightear_y tailbase_x tailbase_y x_raw y_raw rho angle
#> 1 250.056 87.110 152.698 40.60850 233.5307 1 1.398629
#> 2 281.313 95.821 221.361 34.58775 272.0897 1 1.444356
#> 3 337.166 99.408 256.205 46.37325 325.3802 1 1.429230
#> 4 361.761 106.581 270.040 79.16725 343.1865 1 1.344079
#> 5 323.331 131.177 273.627 65.07625 317.9502 1 1.368910
#> 6 321.793 131.177 318.719 53.41900 326.7892 1 1.408763SLEAP — an analysis CSV with
<node>.x / .y / .score
columns, track as the individual and frame_idx
as time.
sleap_path <- system.file("extdata", "sleap_example.csv", package = "radiatR")
ts_sleap <- TrajSet_read(sleap_path, dialect = "sleap")
ts_sleap
#> TrajSet: 1 trajectories, 1 observations
#> Columns: id='id', time='time', angle='angle' (radians), x='x', y='y', raw_x='x_raw', raw_y='y_raw'
#> id time x y a_x a_y b_x b_y x_raw
#> 1 <NA> 0 0.7780386 0.6282165 205.9301 187.8896 278.6352 203.3659 242.2826
#> y_raw rho angle
#> 1 195.6278 1 0.6792588Tracktor — a tidy frame /
pos_x / pos_y CSV (one individual here).
tracktor_path <- system.file("extdata", "tracktor_example.csv",
package = "radiatR")
ts_tracktor <- TrajSet_read(tracktor_path, dialect = "tracktor")
#> New names:
#> New names:
#> • `` -> `...1`
ts_tracktor
#> TrajSet: 1 trajectories, 547 observations
#> Columns: id='id', time='time', angle='angle' (radians), x='x', y='y', raw_x='x_raw', raw_y='y_raw'
#> id time x y x_raw y_raw rho angle
#> 1 1 3 0.9774197 0.2113071 1528.267 330.3940 1 0.2129121
#> 2 1 4 0.9773659 0.2115559 1528.014 330.7465 1 0.2131666
#> 3 1 5 0.9772680 0.2120079 1527.535 331.3825 1 0.2136291
#> 4 1 6 0.9772460 0.2121091 1527.485 331.5372 1 0.2137326
#> 5 1 7 0.9772475 0.2121024 1527.366 331.5003 1 0.2137258
#> 6 1 8 0.9772470 0.2121043 1527.536 331.5405 1 0.2137277The bundled dtrack example is larger and is shown in full in the Loading a Real Experiment section above.
Reading Tabular Trajectories
For data already in a data frame or CSV, TrajSet_read()
is the central entry point.
example_df <- data.frame(
id = rep(c("A", "B"), each = 4),
time = rep(seq(0, 3), times = 2),
x = c(1, 1.2, 1.4, 1.5, -0.2, -0.1, 0, 0.15),
y = c(0, 0.1, 0.3, 0.4, 1.0, 1.1, 1.3, 1.5)
)
ts_df <- TrajSet_read(example_df, mapping = list(id = "id", time = "time", x = "x", y = "y"))
ts_df
#> TrajSet: 2 trajectories, 8 observations
#> Columns: id='id', time='time', angle='angle' (radians), x='x', y='y', raw_x='x_raw', raw_y='y_raw'
#> id time x y x_raw y_raw rho angle
#> 1 A 0 1.00000000 0.00000000 1.0 0.0 1 0.00000000
#> 2 A 1 0.99654576 0.08304548 1.2 0.1 1 0.08314123
#> 3 A 2 0.97780241 0.20952909 1.4 0.3 1 0.21109333
#> 4 A 3 0.96623494 0.25766265 1.5 0.4 1 0.26060239
#> 5 B 0 -0.19611614 0.98058068 -0.2 1.0 1 1.76819189
#> 6 B 1 -0.09053575 0.99589321 -0.1 1.1 1 1.66145621To batch a directory of CSV files, TrajSet_read_dir()
calls TrajSet_read() for each match and row-binds the
result.
csv_dir <- tempfile()
dir.create(csv_dir)
write.csv(example_df, file.path(csv_dir, "trial01.csv"), row.names = FALSE)
write.csv(transform(example_df, id = paste0(id, "_2")),
file.path(csv_dir, "trial02.csv"), row.names = FALSE)
dset <- TrajSet_read_dir(csv_dir, pattern = "\\.csv$")
length(dset)
#> [1] 4
unlink(csv_dir, recursive = TRUE)Manifest Helpers (Synthetic Example)
For a quick demonstration without real files,
import_info() works on any manifest CSV, and
import_tracks() works on any directory following the
_point01 / _point02 naming convention.
manifest_path <- tempfile(fileext = ".csv")
writeLines(
c("file,type, arc", "trial01.csv, baseline,90", "trial02.csv,stimulus,135"),
manifest_path
)
import_info(manifest_path, cond_cols = c("type", "arc"))
#> file type arc cond
#> 1 trial01.csv baseline 90 baseline_90
#> 2 trial02.csv stimulus 135 stimulus_135Registering a Custom Dialect
When your exporter uses a non-standard format, register a custom
function. A dialect receives either a data frame or a file path and must
return a data frame with at least id, time,
and either angle or (x, y).
Below we define a loader for a hypothetical JSON file where each
track is stored under an ID key with arrays of timestamp,
px, and py.
fake_json <- tempfile(fileext = ".json")
jsonlite::write_json(
list(
tracks = list(
A = list(timestamp = c(0, 1, 2), px = c(0, 1, 2), py = c(0, 0.2, 0.5)),
B = list(timestamp = c(0, 1, 2), px = c(1, 1.3, 1.5), py = c(0, -0.1, -0.2))
)
),
fake_json,
auto_unbox = TRUE
)
json_tracks_fn <- function(x) {
dat <- if (is.character(x) && file.exists(x)) jsonlite::fromJSON(x) else x
entries <- purrr::imap(dat$tracks, function(track, id) {
data.frame(
id = id,
time = track$timestamp,
x = track$px,
y = track$py,
stringsAsFactors = FALSE
)
})
do.call(rbind, entries)
}
register_loader_dialect("json_tracks", json_tracks_fn)After registration, call the dialect function directly on the file to
get a tidy data frame, then pass it to TrajSet_read(). (For
standard tabular formats that R’s CSV/TSV readers handle natively,
TrajSet_read(path, dialect = "name") works directly—but
non-tabular formats such as JSON require this two-step approach.)
ts_json <- TrajSet_read(json_tracks_fn(fake_json))
ts_json
#> TrajSet: 2 trajectories, 6 observations
#> Columns: id='id', time='time', angle='angle' (radians), x='x', y='y', raw_x='x_raw', raw_y='y_raw'
#> id time x y x_raw y_raw rho angle
#> A.1 A 0 0.0000000 0.0000000 0.0 0.0 NA 0.0000000
#> A.2 A 1 0.9805807 0.1961161 1.0 0.2 1 0.1973956
#> A.3 A 2 0.9701425 0.2425356 2.0 0.5 1 0.2449787
#> B.1 B 0 1.0000000 0.0000000 1.0 0.0 1 0.0000000
#> B.2 B 1 0.9970545 -0.0766965 1.3 -0.1 1 6.2064134
#> B.3 B 2 0.9912279 -0.1321637 1.5 -0.2 1 6.1506338Dialects live in-memory for the current session. To persist them
across projects, place the registration call in a package or in an
.Rprofile that you source before using
radiatR.