Tracks recorded from an overhead camera live in pixel coordinates that are distorted by the lens and offset from the arena centre. To analyse movement in a true metric (or at least undistorted) frame you need the camera’s calibration: its intrinsic matrix and distortion coefficients.
radiatR does not estimate calibrations itself.
Estimating intrinsics from a checkerboard sequence is a solved problem
with mature, well-tested implementations — MATLAB’s Computer Vision
Toolbox, OpenCV’s calibrateCamera, and others.
radiatR’s job is to import the coefficients those tools produce
and apply them to your tracks. This vignette covers that
import-and-correct workflow.
The model radiatR applies
radiatR uses the pinhole + Brown-Conrady model shared by MATLAB and OpenCV:
-
Focal lengths
fx,fyand principal pointcx,cy, in pixels. -
Radial distortion
k1,k2,k3and tangential distortionp1,p2.
These are stored in a CalModel
object, which calibrate_positions() applies to a
TrajSet.
A subtlety worth flagging up front: internally radiatR keeps the
intrinsic matrix in MATLAB’s transposed convention — focal
lengths on the diagonal, principal point in the bottom
row (K[3, 1:2]), not the right-hand column. You
never have to assemble that matrix by hand: cal_model() and
read_calibration() own it for you.
Building a model from known coefficients
If you already have the numbers — from a calibration report, a paper,
or a colleague — cal_model() assembles the
CalModel directly:
model <- cal_model(
fx = 800, fy = 800, # focal lengths (px)
cx = 640, cy = 360, # principal point (px)
k1 = -0.28, k2 = 0.05,
p1 = 1e-3, p2 = -5e-4
)
model
#> An object of class "CalModel"
#> Slot "K":
#> [,1] [,2] [,3]
#> [1,] 800 0 0
#> [2,] 0 800 0
#> [3,] 640 360 1
#>
#> Slot "k":
#> k1 k2 k3 p1 p2
#> -0.2800 0.0500 0.0000 0.0010 -0.0005
#>
#> Slot "F":
#> [1] 800 800What F controls
The F argument sets the units of the corrected output.
When F is left as NULL (the default) it is set
to c(fx, fy), which means the output stays in
undistorted pixels — distortion and the principal-point
offset are removed, but the scale is unchanged. To obtain a metric
result instead, pass the physical focal length in millimetres:
model_mm <- cal_model(fx = 800, fy = 800, cx = 640, cy = 360,
k1 = -0.28, k2 = 0.05, F = 20)Importing a calibration file
In practice you will export the calibration from another tool and
read it with read_calibration(), which auto-detects the
format from the file extension:
# MATLAB Computer Vision Toolbox (.mat)
model <- read_calibration("camera_params.mat")
# OpenCV FileStorage (.yml / .yaml / .json)
model <- read_calibration("opencv_calib.yml")
# Plain CSV (wide or parameter/value)
model <- read_calibration("calib.csv")MATLAB
Save the relevant fields of a cameraParameters /
cameraIntrinsics object to a .mat file.
radiatR recognises both the pre-R2022b IntrinsicMatrix
(already in the transposed convention) and the newer standard
K, alongside RadialDistortion and
TangentialDistortion. Reading .mat files needs
the R.matlab package.
% in MATLAB, after calibrating
params = cameraParams;
IntrinsicMatrix = params.IntrinsicMatrix;
RadialDistortion = params.RadialDistortion;
TangentialDistortion = params.TangentialDistortion;
save('camera_params.mat', 'IntrinsicMatrix', ...
'RadialDistortion', 'TangentialDistortion');MATLAB reports the principal point in 1-based
pixels. read_calibration() subtracts one by default
(principal_base = 1) so it lines up with the 0-based
coordinates produced by most tracking tools. Override
principal_base if your tracker uses a different
convention.
OpenCV
cv::FileStorage writes a camera_matrix and
distortion_coefficients (in the order
k1, k2, p1, p2, k3). radiatR reads both the YAML and JSON
variants and tolerates the non-standard %YAML:1.0 header
and !!opencv-matrix tags. YAML needs the
yaml package; JSON needs jsonlite.
OpenCV principal points are already 0-based, so no shift is applied by
default.
Correcting points
For a single landmark, cam_cal_pt() applies the model to
one pixel coordinate; cam_cal_many() does the same for a
table of points in one pass:
pts <- matrix(
c(640, 360,
700, 420,
580, 320),
ncol = 2, byrow = TRUE,
dimnames = list(NULL, c("x", "y"))
)
cam_cal_many(pts, model@K, model@k, model@F)
#> [,1] [,2]
#> [1,] 0.00000 0.00000
#> [2,] 60.19038 60.17675
#> [3,] -60.13549 -40.09905The point at the principal point (640, 360) maps to the
origin; others are recentred, undistorted, and scaled by
F.
Correcting a whole TrajSet
The usual entry point is calibrate_positions(), which
corrects every track in a TrajSet, rewrites its
x/y columns in place, and re-derives
angle from the corrected geometry. The bundled millipede
example provides real pixel-space tracks from a camera above a circular
arena.
track_dir <- system.file("extdata", "tracks", package = "radiatR")
manifest_path <- system.file("extdata", "millipede_trials.csv", package = "radiatR")
file_tbl <- import_tracks(track_dir)
manifest <- import_info(manifest_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
# Read the first track file in pixel space
track_raw <- read.delim(
file.path(track_dir, file_tbl$track[1]),
sep = "\t", header = FALSE
)[, 1:3]
names(track_raw) <- c("frame", "x", "y")
track_raw$id <- tools::file_path_sans_ext(basename(file_tbl$track[1]))
ts_px <- TrajSet(
track_raw,
id = "id",
time = "frame", x = "x", y = "y",
normalize_xy = FALSE
)
ts_mm <- calibrate_positions(ts_px, model)
head(ts_mm@data)
#> frame x y id radius angle
#> 1 1 -187.1821 12.50332 10_1_point02 588.4075 3.074894
#> 2 2 -184.6893 10.08415 10_1_point02 588.7537 3.087046
#> 3 3 -187.1788 10.08701 10_1_point02 586.9042 3.087755
#> 4 4 -188.8331 10.08893 10_1_point02 585.6785 3.088216
#> 5 5 -187.1917 17.31612 10_1_point02 591.4184 3.049350
#> 6 6 -183.8896 22.11961 10_1_point02 596.8850 3.021880In a real workflow, replace the illustrative model above
with one imported via read_calibration() from a calibration
captured with the same camera and lens.
Convergence diagnostics
Removing distortion requires inverting the distortion model, which
radiatR does with a fixed-point solver (as OpenCV’s
undistortPoints does). The defaults
(max_iterations = 5, tolerance = 1e-9) are
usually ample. For extreme distortion or points far from the optical
axis you can relax them:
cam_cal_pt(650, 380, model@K, model@k, model@F,
max_iterations = 10, tolerance = 1e-12)If convergence fails the function returns the last iterate; check your coefficients or tighten the stopping criteria.
Summary
- radiatR imports calibrations; it does not estimate them. Calibrate in MATLAB, OpenCV, or another tool, then bring the coefficients in.
-
read_calibration()reads MATLAB.mat, OpenCV YAML/JSON, and plain CSV;cal_model()builds a model from coefficients you already hold. - Apply the resulting
CalModelto single points (cam_cal_pt()), tables (cam_cal_many()), or whole trajectory sets (calibrate_positions()). - Mind the conventions: principal-point pixel base (1-based MATLAB vs
0-based OpenCV) and the
Fscale factor (undistorted pixels by default, metric if you pass a physical focal length).