aniread (development version)
Removed
-
The unused output validators,
ensure_output_header_names(),ensure_output_header_class()andensure_output_no_nan()(#123). No reader called them — their only callers were their own tests — so nothing they promised was ever enforced, and the tests passing gave the impression that it was.They could not be wired in as they stood: they require exactly
time,individual,keypoint,x,yandconfidence, which is a narrower contract than the aniframe has had for some time. Five of the seven sample sources fail it —read_deeplabcut()returns noindividual,read_anipose()andread_c3d()returnz,read_freemocap()returnsmodel, andread_fictrac()andread_c3d()return noconfidence.anicore::validate_aniframe()is the metadata-aware successor: it checks the frame against what it declares rather than against a fixed column list.
Added
-
read_sleap()reads SLEAP’s analysis CSV export (#87).get_supported_sources()advertisedcsvfor SLEAP while the reader aborted with “We hope to support SLEAP CSV import soon!”, so the registry had been narrowed toh5as a stopgap; it advertises both again.The columns are
track,frame_idx,instance.scoreand a.x/.y/.scoretriple per node, which is how sleap-io defines the format. Node names are read from the columns rather than assumed, since a recording has whatever skeleton it was tracked with, andinstance.scoreis dropped rather than becoming a keypoint calledinstance— it scores the whole instance, where the h5 reader takes confidence from the per-node scores.One recording reads the same from either export, checked against the h5 it was generated from. Two things make that true:
timecounts from 1 as it does for the h5, whereframe_idxcounts from 0; and a frame in which an instance was not detected comes back as an all-NArow rather than being absent, since the CSV holds a row per instance and omits those entirely — the same reinstatementread_octron()does. detect_source()recognises a SLEAP analysis CSV, by theframe_idxandinstance.scorecolumns sleap-io uses to identify one. It previously inspected only HDF5 names, so a SLEAP CSV was not detected at all andread_dataset()could not route it.read_sleap()records which export it read in thesource_formatmetadata field.
Changed
-
read_sleap()names individuals by the track SLEAP recorded, rather than by position (#125). The h5 reader readtrack_namesonly to count them and then labelled individualsindividual1,individual2, …, discarding names the file already held — soSLEAP_three-mice_Aeon_mixed-labels.analysis.h5came back asindividual1/2/3instead ofAEON3B_NTP/TP1/TP2. A recording with no tracks, such as a single untracked instance, still falls back to the positional names, because there is nothing else to use.This changes the
individualvalues returned for any h5 with named tracks. Code matching on"individual1"will need the real name instead;levels(data$individual)shows them.
Added
-
read_trex()reads TRex’s native.npzexport (#116). It is a zip of.npyarrays, one file per tracked individual, so a whole recording is the vector of pathsget_sample_data("trex")returns — which previously errored, because the registry declared TRex a CSV-only source. The arrays are parsed directly rather than through a new dependency:.npyis a short header over a raw buffer, andunz()reads zip members without unpacking.The
.npzcarries what the CSV export does not, so three of this reader’s documented limitations turn out to be limitations of the CSV rather than of TRex:individualis the identity TRex assigned instead ofNA,confidenceis its per-framedetection_pinstead ofNA, and the pose keypoints are present at all. The frame rate and frame size are recorded too, sosampling_rateis set and the reflection tobottom_leftno longer has to guess the frame height frommax(y). read_trex()gains aformatargument, defaulting to"auto", which reads the export from the file rather than its extension.
Changed
get_sample_data("trex")defaults to"five-locusts"(#116). The previous default,"beetles", is a 19-frame CSV excerpt with one unnamed individual and no confidence — too small to carry an example or a tutorial."five-locusts"is a real 2845-frame recording of five individuals with pose and detection probability."beetles"is still available, and is still the fixture exercising the CSV path.read_trex()treatsInfas missing in both exports. TRex marks a frame it could not track with an infinity rather than aNaN— its own documentation masksnp.infout before plotting — so these were reaching the aniframe and propagating through every downstream calculation. Usesanicore::convert_inf_to_na(), added for this.read_trex()no longer requires the CSV’s optional columns.VX,VYandtimestampwere dropped by name, which errors on a file that does not have them — and which columns a TRex CSV carries is set per run by itsoutput_fieldsparameter.read_trex()declaresunit_timeas"s". TRex reports seconds in both exports, and leaving it unset meantanicore::set_sampling_rate()treated the column as frames and divided it by the frame rate.
Added
read_freemocap()reads the 9-column tidy export (#117). FreeMoCap added areprojection_errorcolumn at v1.8.0; the reader accepted a file with fewer than ten columns and rejected everything else, so the current export was read only by accident of that threshold. Both the 8- and the 9-column form are now read deliberately.FreeMoCap data gains a
confidencecolumn, fromreprojection_errorwhere the file has one and all-NAwhere it does not. The two run in opposite directions — an error is a distance in pixels, so zero is best, whileconfidenceeverywhere else in aniread comes from a likelihood or probability where larger is best — so it is mapped through1 / (1 + error)rather than renamed. That is monotone onto(0, 1], gives 1 for a perfect reprojection, and is invertible: the original error is1 / confidence - 1. Renaming it would have madeaniprocess::filter_na_across(method = "confidence")discard the best-tracked points.read_freemocap()reads theby_trajectoryexport and the per-model wide files inoutput_data/(mediapipe_body_3d_xyz.csvand siblings), which it previously rejected (#117). Neither carries a frame column — the row position is the frame — and neither names its models in the data, so point names are parsed the way FreeMoCap’s ownDataSaver._parse_keypoint_name()parses them. One recording therefore gives the samemodelandkeypointvalues whichever of the three layouts it is read from, and identical coordinates: checked across all 126,096 rows of the v1.8.0 release asset.read_freemocap()gains aformatargument, defaulting to"auto", which reads the layout from the column names. It followsread_boris(), whoseformat = c("auto", ...)is the pattern the other readers should converge on (#118).The layout a file was read as is recorded in the
source_formatmetadata field, as"by_frame_8col"or"by_frame_9col", so drift between FreeMoCap releases is visible on the aniframe rather than only in whether reading happened to work.
Fixed
detect_freemocap_format()no longer mistakes aby_trajectoryfile for a wide one. It told them apart by aframecolumn that FreeMoCap does not write in either; they are distinguished by the timestamps, which onlyby_trajectorycarries.detect_source()recognises FreeMoCap files written by v1.8.0 and later (#117). It compared the header for exact equality with the eight columns of the older export, so a file withreprojection_errorwas not identified as FreeMoCap at all andread_dataset()failed on it. The header is now matched by inclusion, which also survives the next column FreeMoCap appends.
Changed
-
read_freemocap()’s error names the layout it found rather than only the one it wanted. Told aby_trajectory.csvor a per-modelmediapipe_body_3d_xyz.csv, it said to look for a file ending inby_frame.csv— unhelpful when the recording never produced one. Neither layout is read yet; both are now recognised well enough to say so.
aniread 0.7.0 (2026-08-28)
Added
-
read_custom()takes anindexargument, so a frame indexed by something other thantimecan be read (#107). The index used to be smuggled in throughvariables_whenand told apart by the literal string"time"; since these became separate roles in anicore,read_custom()’s own documented example — a frame indexed byframewithintrial— could not be expressed at all.c("trial", "frame")normalised to"trial", leaving the frame indexed by atimecolumn the data does not have.
Changed
The core data structures come from
anicore, which is what theaniframepackage was renamed to in its 0.8.0 (animovement/anicore#84). Theaniframeclass keeps its name; only the package providing it changed, soanicorereplacesaniframeinImportsand in everyaniframe::call.The minimum
anicoreis 0.8.0, which is the first version published under that name. The constraint read>= 0.6.0— a version ofanicorethat never existed, carried over unchanged fromaniframewhen the dependency was renamed.Axis geometry is declared through
anicore’s axis directions and extents, replacingset_origin()andset_y_height(), anddefault_metadata()follows its rename tolist_default_metadata().
Fixed
-
read_trackball()anddetect_source()cope with a run of serial-port junk before the first complete record, not just a single partial row (#94). The skip was computed fromutils::count.fields(), which silently drops blank lines, so on a capture with blank lines among the junk it landed early — on a noise line, which was then accepted as a header, and the read failed withColumn index 4 is out of bounds.
aniread 0.6.0 (2026-08-18)
Added
read_dataset()reads any supported format through one entry point, working out which source software wrote the file rather than requiring you to know in advance (#73). Passsourceto name the format explicitly, or...to reach a reader’s own arguments. It returns whatever the underlying reader returns — ananiframe, or ananieventforread_boris().detect_source()reports which software wrote a file without reading it. Candidates are narrowed by suffix, then each detector inspects the contents, which matters because twelve sources read.csv. DeepLabCut and LightningPose export structurally identical files, so it returns the combined name"deeplabcut/lightningpose"rather than guessing. Detectors needing an optional package (rhdf5,arrow,xml2,c3dr) are skipped when it is absent, and the error names what was skipped.?read_trackballdocuments the raw Bonsai layout, the requirement thatcol_timebe a shared clock with two sensors, and that empty time bins are filled with zero motion — an assumption about this logger rather than about optical flow generally.
Changed
get_supported_sources()no longer listscsvas a SLEAP suffix —read_sleap()cannot read it, and auto-detection would have routed such files straight into that error. Restored when the reader gains support (#87).get_supported_sources()renames thetrackballsource totrackball_bonsai, matching thesourcemetadataread_trackball()actually stamps.
Fixed
read_trackball()reads real two-sensor Bonsai optical-flow captures (#85). It previously either aborted with an error pointing nowhere near the cause, or silently returned a misaligned trajectory. Sensor alignment,start_datetime, corrupt rows, leading junk, microsecond clocks, gap filling and argument handling were each at fault; see the PR for the breakdown.read_trackball()warns whencol_timeresolves to a non-datetime column and two sensors are given — a per-board counter has a sensor-local origin and cannot align two files. Warning classaniread_sensor_local_clock.read_animalta()works out which export layout a file uses instead of being told (#88).detaileddefaults to"auto"and reads the answer from the header. This was the one case wheredetect_source()identified a file correctly andread_dataset()then failed on it.detect_source()recognises a Bonsai optical-flow capture whether or not it carries a header row.ensure_header_match()no longer rejects a charactercol_timeon files that do have named headers.
aniread 0.5.1
Added
-
get_supported_sources()returns the source softwareanireadcan read as a tibble ofsource/reader/suffix, so downstream packages can discover supported formats programmatically instead of hard-coding them. Closes #74.
Fixed
-
read_octron()no longer drops frames in which nothing was detected. Octron omits such frames entirely; the reader now reinstates them as all-NA rows across the full track × frame grid (using the analysed-frame count from the CSV header) so the time axis is gap-free. Closes #80. -
read_boris(unit_time = "frame")no longer fails on exports with an inconsistent image index (e.g. a STOP on the last video frame recorded as frame 1, givingstop < start). When FPS is known, the offending frame interval is recovered fromround(time_s * fps). Closes #81.
aniread 0.5.0
Added
-
read_boris()imports behavioural events from a BORIS export into ananicore::anievent(). Supports the two flat-text BORIS exports — aggregated events (one row per bout) and tabular events (one row per START / STOP / POINT transition; paired into bouts by the reader) — and auto-detects the format from the file’s first row. Channels are taken from BORIS’sBehavioral categorywhen populated, falling back to the literal"behavior"; modifiers travel via themodifierslist-column in both the newer multi-column (Modifier #1,Modifier #2, …) and the legacy single-column pipe-separated layouts. State-vs-point classification is recorded inmetadata$variables_event.unit_time = "s"(default) readsStart (s)/Stop (s); passunit_time = "frame"to use the image-index columns instead, which keeps event timestamps row-aligned with a host aniframe (and falls back to seconds when no image-index columns are present). FPS is recorded assampling_ratemetadata without rescaling timestamps. Closes #76.
Fixed
- File validation no longer rejects readable files on Windows network (UNC) shares.
file.access()returns false negatives for read permission on such paths; the read check now falls back to a non-destructive open attempt whenfile.access()reports no access.
Changed
-
aniframe (>= 0.6.0)is now required, sinceread_boris()produces ananieventobject — a new class added in aniframe 0.6.0.
aniread 0.4.1
Added
-
read_octron()gains apropertiesargument for picking which region-property columns to read ("all"by default; pass a character vector for a subset orNULLto skip them).areais auto-included whenmethod = "weighted". -
read_octron()normalises hyphens to underscores in column names (moments_hu-0→moments_hu_0).
Changed
-
read_octron()is now substantially faster on large multi-segment files, especially when only a fewpropertiesare requested.
aniread 0.4.0
Changed
- Readers whose source data uses image (top-left) origin now reflect
yso the returned aniframe is in the conventionalbottom_leftorigin. This fixes plots being upside-down without manual reorientation. Affectsread_animalta(),read_bonsai(),read_deeplabcut(),read_fasttrack(),read_idtracker(),read_lightningpose(),read_movement(),read_octron(),read_sleap(),read_trackmate(), andread_trex()(#61). -
aniframe (>= 0.5.0)is now required, since the reflection uses the newset_origin()/set_y_height()API.
Added
- All affected readers gain an optional
video_heightargument for supplying the source frame height when the format does not record it (DeepLabCut, LightningPose, SLEAP, AnimalTA, Bonsai, FastTrack, TRex, idtracker.ai CSV, movement netCDF). When omitted, the reader falls back to source-extracted values where available, and finally tomax(y). -
read_idtracker()now reads/heightfrom the trajectories h5 file by default. -
read_trackmate()now reads the frame height fromSettings/ImageData/@heightin the XML by default. -
read_octron()continues to readvideo_height:from the CSV header, but now also accepts avideo_heightoverride and stores the value in the aniframe metadata. -
read_octron()gains amethodargument to handle frames where Octron emitted multiple mask segments for the same track (#67). One of"weighted"(default; area-weighted mean of position and shape props, sum of areas),"largest"(single largest segment per row), or"segments"(one row per segment, with a newsegmentidentity variable).
Fixed
-
read_idtracker()now accepts both the legacysecondsand the newertimeleading column in idtracker.ai CSV exports (#60).
aniread 0.3.2
Added
-
read_c3d()for C3D motion-capture data, andread_fasttrack()for FastTrack data. -
read_deeplabcut()reads HDF5 exports as well as CSV.
Fixed
-
read_octron()handles the newer Octron output format.
aniread 0.3.1
Added
-
read_aniframe()reads a saved aniframe back from parquet. -
read_movement()imports data from the movement Python package. -
read_trackmate()reads TrackMate XML. Adapted from the reader in TrackMateR, with thanks to @quantixed. -
read_octron()reads Octron CSV. -
calibrate_trackball()for trackball calibration.
aniread 0.3.0
Added
-
write_aniframe()writes an aniframe to parquet, andwrite_intracktive()exports for intracktive. -
read_anipose(),read_fictrac(),read_freemocap()andread_custom(). -
get_sample_data()fetches example files for the readers.
aniread 0.2.0
Added
- The first readers:
read_deeplabcut(),read_sleap(),read_lightningpose(),read_trex(),read_idtracker(),read_animalta(),read_bonsai(),read_movement()andread_trackball(), withvalidate_trackball(). - A
NEWS.mdfile, to track changes to the package.