├── .Rbuildignore ├── .github ├── .gitignore └── workflows │ ├── R-CMD-check.yaml │ └── pkgdown.yaml ├── .gitignore ├── DESCRIPTION ├── LICENSE ├── NAMESPACE ├── NEWS.md ├── R ├── TrackMateR-package.R ├── calculateAlpha.R ├── calculateCVE.R ├── calculateFD.R ├── calculateJD.R ├── calculateMSD.R ├── calculateTrackDensity.R ├── compareDatasets.R ├── correctTrackMateData.R ├── fittingJD.R ├── gt_functions.R ├── makeComparison.R ├── makeSummaryReport.R ├── readTrackMateXML.r ├── reportDataset.R ├── tm_misc.R └── tm_plots.R ├── README.Rmd ├── README.md ├── TrackMateR.Rproj ├── _pkgdown.yml ├── inst └── extdata │ └── ExampleTrackMateData.xml ├── man ├── calculateAlpha.Rd ├── calculateCVE.Rd ├── calculateFD.Rd ├── calculateJD.Rd ├── calculateMSD.Rd ├── calculateTrackDensity.Rd ├── compareDatasets.Rd ├── compareGTDatasets.Rd ├── correctTrackMateData.Rd ├── figures │ ├── README-example-1.png │ ├── README-unnamed-chunk-2-1.png │ ├── logo.png │ └── logo.svg ├── findLog2YAxisLimits.Rd ├── find_distances.Rd ├── find_td_A1.Rd ├── find_td_A2.Rd ├── find_td_area.Rd ├── fittingJD.Rd ├── makeComparison.Rd ├── makeSummaryReport.Rd ├── mergeDataFramesForExport.Rd ├── plot_tm_MSD.Rd ├── plot_tm_NMSD.Rd ├── plot_tm_allTracks.Rd ├── plot_tm_alpha.Rd ├── plot_tm_cumdistOverTime.Rd ├── plot_tm_dee.Rd ├── plot_tm_displacementHist.Rd ├── plot_tm_displacementOverTime.Rd ├── plot_tm_durationHist.Rd ├── plot_tm_fd.Rd ├── plot_tm_intensityHist.Rd ├── plot_tm_neighbours.Rd ├── plot_tm_speed.Rd ├── plot_tm_width.Rd ├── processEllipsis.Rd ├── readGTFile.Rd ├── readTrackMateXML.Rd ├── reportDataset.Rd └── setupOutputPath.Rd └── vignettes ├── TrackMateR.Rmd ├── comparison.Rmd └── recalibration.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^_pkgdown\.yml$ 4 | ^docs$ 5 | ^pkgdown$ 6 | ^\.github$ 7 | ^README.Rmd$ 8 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: R-CMD-check 10 | 11 | jobs: 12 | R-CMD-check: 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | R_KEEP_PKG_SOURCE: yes 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - uses: r-lib/actions/setup-r@v2 21 | with: 22 | use-public-rspm: true 23 | 24 | - uses: r-lib/actions/setup-r-dependencies@v2 25 | with: 26 | extra-packages: any::rcmdcheck, any::XML 27 | needs: check 28 | 29 | - uses: r-lib/actions/check-r-package@v2 30 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown 13 | 14 | jobs: 15 | pkgdown: 16 | runs-on: ubuntu-latest 17 | # Only restrict concurrency for non-PR jobs 18 | concurrency: 19 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 20 | env: 21 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - uses: r-lib/actions/setup-pandoc@v2 26 | 27 | - uses: r-lib/actions/setup-r@v2 28 | with: 29 | use-public-rspm: true 30 | 31 | - uses: r-lib/actions/setup-r-dependencies@v2 32 | with: 33 | extra-packages: any::pkgdown, any::XML, local::. 34 | needs: website 35 | 36 | - name: Build site 37 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 38 | shell: Rscript {0} 39 | 40 | - name: Deploy to GitHub pages 🚀 41 | if: github.event_name != 'pull_request' 42 | uses: JamesIves/github-pages-deploy-action@4.1.4 43 | with: 44 | clean: false 45 | branch: gh-pages 46 | folder: docs 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # History files 2 | .Rhistory 3 | .Rapp.history 4 | 5 | # Session Data files 6 | .RData 7 | 8 | # User-specific files 9 | .Ruserdata 10 | 11 | # Example code in package build process 12 | *-Ex.R 13 | 14 | # Output files from R CMD build 15 | /*.tar.gz 16 | 17 | # Output files from R CMD check 18 | /*.Rcheck/ 19 | 20 | # RStudio files 21 | .Rproj.user/ 22 | 23 | # produced vignettes 24 | vignettes/*.html 25 | vignettes/*.pdf 26 | 27 | # OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 28 | .httr-oauth 29 | 30 | # knitr and R markdown default cache directories 31 | *_cache/ 32 | /cache/ 33 | 34 | # Temporary files created by R markdown 35 | *.utf8.md 36 | *.knit.md 37 | 38 | # R Environment Variables 39 | .Renviron 40 | docs 41 | 42 | # Mac droppings 43 | .DS_Store -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: TrackMateR 2 | Type: Package 3 | Title: Working with TrackMate outputs in R 4 | Version: 0.3.10 5 | Authors@R: 6 | person(given = "Stephen J", 7 | family = "Royle", 8 | role = c("cre", "aut"), 9 | email = "admin@quantixed.org", 10 | comment = c(ORCID = "0000-0001-8927-6967")) 11 | Maintainer: Stephen Royle 12 | Description: TrackMate, a plugin for ImageJ/Fiji, is a popular single-particle tracking solution. 13 | The aim of TrackMateR is to import TrackMate data into R for further analysis and visualization. 14 | License: MIT + file LICENSE 15 | Encoding: UTF-8 16 | LazyData: true 17 | RoxygenNote: 7.3.1 18 | Depends: R (>= 2.10) 19 | Config/rcmdcheck/ignore-inconsequential-notes: true 20 | VignetteBuilder: 21 | knitr, 22 | rmarkdown 23 | Imports: 24 | doParallel, 25 | dplyr, 26 | foreach, 27 | ggplot2, 28 | ggforce, 29 | patchwork, 30 | parallelly, 31 | reshape2, 32 | utils, 33 | XML, 34 | zoo 35 | Suggests: 36 | knitr, 37 | rmarkdown 38 | URL: https://quantixed.github.io/TrackMateR/ 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2022 2 | COPYRIGHT HOLDER: Stephen J. Royle 3 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(calculateAlpha) 4 | export(calculateCVE) 5 | export(calculateFD) 6 | export(calculateJD) 7 | export(calculateMSD) 8 | export(calculateTrackDensity) 9 | export(compareDatasets) 10 | export(compareGTDatasets) 11 | export(correctTrackMateData) 12 | export(find_td_area) 13 | export(fittingJD) 14 | export(makeComparison) 15 | export(makeSummaryReport) 16 | export(plot_tm_MSD) 17 | export(plot_tm_NMSD) 18 | export(plot_tm_allTracks) 19 | export(plot_tm_alpha) 20 | export(plot_tm_cumdistOverTime) 21 | export(plot_tm_dee) 22 | export(plot_tm_displacementHist) 23 | export(plot_tm_displacementOverTime) 24 | export(plot_tm_durationHist) 25 | export(plot_tm_fd) 26 | export(plot_tm_intensityHist) 27 | export(plot_tm_neighbours) 28 | export(plot_tm_speed) 29 | export(plot_tm_width) 30 | export(readGTFile) 31 | export(readTrackMateXML) 32 | export(reportDataset) 33 | import(doParallel) 34 | import(dplyr) 35 | import(ggplot2) 36 | import(parallelly) 37 | import(patchwork) 38 | importFrom(XML,getNodeSet) 39 | importFrom(XML,xmlDoc) 40 | importFrom(XML,xmlGetAttr) 41 | importFrom(XML,xmlParse) 42 | importFrom(XML,xpathApply) 43 | importFrom(XML,xpathSApply) 44 | importFrom(foreach,"%do%") 45 | importFrom(foreach,"%dopar%") 46 | importFrom(foreach,foreach) 47 | importFrom(ggforce,geom_sina) 48 | importFrom(graphics,frame) 49 | importFrom(graphics,hist) 50 | importFrom(parallelly,availableCores) 51 | importFrom(reshape2,melt) 52 | importFrom(stats,approx) 53 | importFrom(stats,coef) 54 | importFrom(stats,dist) 55 | importFrom(stats,lm) 56 | importFrom(stats,median) 57 | importFrom(stats,na.omit) 58 | importFrom(stats,nls) 59 | importFrom(stats,quantile) 60 | importFrom(stats,sd) 61 | importFrom(utils,data) 62 | importFrom(utils,install.packages) 63 | importFrom(utils,read.csv) 64 | importFrom(utils,write.csv) 65 | importFrom(zoo,rollmean) 66 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # TrackMateR 2 | 3 | # TrackMateR 0.3.10 4 | 5 | - Fixed an issue where sparse TrackMate XML files caused `compareDatasets()` to fail 6 | - These files are skipped and a warning is issued. They can be loaded and analyzed manually if desired (limit is more than 3 tracks with at least one with 10 frames) 7 | 8 | # TrackMateR 0.3.9 9 | 10 | - Warning for TrackMate XML files that contain tracks with more than one point per frame 11 | - Added xy scaling for shape descriptors 12 | 13 | # TrackMateR 0.3.8 14 | 15 | - Added report of intensity and duration to workflow 16 | 17 | # TrackMateR 0.3.7 18 | 19 | - Added estimator of diffusion co-efficient and histogram of D (per track) 20 | 21 | # TrackMateR 0.3.6 22 | 23 | - Ability to load a ground truth csv dataset 24 | - Compare multiple ground truth datasets 25 | 26 | # TrackMateR 0.3.5 27 | 28 | - More flexibility to automated workflows (axes on MSD plots can be configured) 29 | - Minor fixes to plots in `compareDatasets()` 30 | - Better handling in jump distance calculation 31 | - Test datasets available for multiple comparisons 32 | - Improved documentation 33 | 34 | # TrackMateR 0.3.4 35 | 36 | - Fixes for parallelisation. Windows users still do not benefit from parallel processing that Mac/Linux users enjoy. 37 | 38 | # TrackMateR 0.3.3 39 | 40 | - More flexibility to automated workflows by allowing parameters to be passed via ellipsis to `reportDataset()` and `compareDatasets()` 41 | - Better resilience to failures in jump distance fitting, and reading XML 42 | 43 | # TrackMateR 0.3.2 44 | 45 | - Fixes for edge cases and issues 46 | 47 | # TrackMateR 0.3.1 48 | 49 | - Fixes for parallelisation issue and better handling of fitting errors 50 | 51 | # TrackMateR 0.3.0 52 | 53 | - First release on GitHub 54 | -------------------------------------------------------------------------------- /R/TrackMateR-package.R: -------------------------------------------------------------------------------- 1 | #' @importFrom foreach %do% 2 | #' @importFrom foreach %dopar% 3 | #' @importFrom foreach foreach 4 | #' @importFrom ggforce geom_sina 5 | #' @importFrom graphics frame 6 | #' @importFrom graphics hist 7 | #' @importFrom parallelly availableCores 8 | #' @importFrom reshape2 melt 9 | #' @importFrom stats approx 10 | #' @importFrom stats quantile 11 | #' @importFrom stats sd 12 | #' @importFrom stats lm 13 | #' @importFrom stats median 14 | #' @importFrom stats na.omit 15 | #' @importFrom stats coef 16 | #' @importFrom stats dist 17 | #' @importFrom stats nls 18 | #' @importFrom utils data 19 | #' @importFrom utils install.packages 20 | #' @importFrom utils read.csv 21 | #' @importFrom utils write.csv 22 | #' @importFrom XML xmlParse 23 | #' @importFrom XML getNodeSet 24 | #' @importFrom XML xpathSApply 25 | #' @importFrom XML xmlGetAttr 26 | #' @importFrom XML xmlDoc 27 | #' @importFrom XML xpathApply 28 | #' @importFrom zoo rollmean 29 | #' @import ggplot2 30 | #' @import dplyr 31 | #' @import patchwork 32 | #' @import doParallel 33 | #' @import parallelly 34 | NULL 35 | #> NULL 36 | -------------------------------------------------------------------------------- /R/calculateAlpha.R: -------------------------------------------------------------------------------- 1 | #' Calculate alpha (relationship between MSD and normal diffusion) 2 | #' 3 | #' Alpha is the MSD exponent. 4 | #' Normal diffusion is alpha = 1. Subdiffusion is alpha < 1 and superdiffusion is alpha > 1. 5 | #' Input is a data matrix of msd curves. 6 | #' Output is mean of log2(alpha), one value for each trace. 7 | #' D, calculated from a fit of the first four data points is also outputted. 8 | #' 9 | #' @param alphaMat matrix of msd curves, each col is a track, each row is time lag (will contain NAs) 10 | #' @param tstep variable. Time step in seconds 11 | #' @return data frame 12 | #' @export 13 | 14 | 15 | calculateAlpha <- function(alphaMat,tstep) { 16 | # check that alphaMat is at least four rows by two columns 17 | if(nrow(alphaMat) < 4 | ncol(alphaMat) < 2) { 18 | alphaDF <- data.frame(trace = character("1"), 19 | alpha = numeric(1), 20 | dee = numeric(1)) 21 | return(alphaDF) 22 | } 23 | # make time vector 24 | tee <- (1 : nrow(alphaMat)) * tstep 25 | # make empty vectors for the results 26 | alphaVec <- deeVec <- rep(NA, ncol(alphaMat)) 27 | # check that we have four contiguous points for each col 28 | check <- colSums(alphaMat[1:4,]) 29 | for(i in 1 : ncol(alphaMat)) { 30 | if(is.na(check[i])) { 31 | next 32 | } 33 | tempdf <- data.frame(mean = alphaMat[,i], 34 | t = tee) 35 | # fit to first four data points 36 | if(all(is.na(tempdf$mean)) | all(is.na(tempdf$t))) { 37 | next 38 | } 39 | mod <- lm(mean ~ t, data = tempdf[1:4,]) 40 | # make a column containing model y points for each t 41 | tempdf$pred <- (mod$coefficients[2] * tempdf$t) + mod$coefficients[1] 42 | tempdf$alpha <- tempdf$mean / tempdf$pred 43 | tempdf$alpha <- suppressWarnings(log2(tempdf$alpha)) 44 | alphaVec[i] <- mean(tempdf$alpha, na.rm = TRUE) 45 | deeVec[i] <- tempdf$pred[1] / (4 * tempdf$t[1]) 46 | } 47 | # turn into data frame 48 | alphaDF <- data.frame(trace = colnames(alphaMat), 49 | alpha = alphaVec, 50 | dee = deeVec) 51 | 52 | return(alphaDF) 53 | } 54 | 55 | -------------------------------------------------------------------------------- /R/calculateCVE.R: -------------------------------------------------------------------------------- 1 | #' Calculate CVE (covariance-based estimator) 2 | #' 3 | #' From Vestergaard et al., Physical Review E (2019) 89, 022726 4 | #' Input is two data matrices of X and Y displacements for each trace. 5 | #' Output is estimator of D and sigma for each trace. 6 | #' 7 | #' @param xMat matrix of x displacements, each col is a track, each row is time (will contain NAs) 8 | #' @param yMat matrix of y displacements, each col is a track, each row is time (will contain NAs) 9 | #' @param tlist list of trace names 10 | #' @param tstep variable. Time step in seconds 11 | #' @return data frame 12 | #' @export 13 | 14 | 15 | calculateCVE <- function(xMat, yMat, tlist, tstep) { 16 | # setup NULL return 17 | cveDF <- data.frame(trace = character("1"), 18 | dee = numeric(1), 19 | estsigma = numeric(1)) 20 | # check that data is not NULL 21 | if(is.null(xMat) | is.null(yMat) | is.null(tlist) | is.null(tstep)) { 22 | return(cveDF) 23 | } else { 24 | # check that length of nrow(xMat) > 0 is TRUE 25 | if(!isTRUE(nrow(xMat) > 0)) { 26 | return(cveDF) 27 | } 28 | } 29 | # check that data is at least four rows by two columns 30 | if(nrow(xMat) < 4 | ncol(xMat) < 2) { 31 | return(cveDF) 32 | } 33 | # calculation of the estimators of D and sigma^2 34 | estDx <- colMeans(xMat^2, na.rm = TRUE) / 2 / tstep + colMeans(xMat[-1,] * xMat[-(nrow(xMat)),], na.rm = TRUE) / tstep 35 | estDy <- colMeans(yMat^2, na.rm = TRUE) / 2 / tstep + colMeans(yMat[-1,] * xMat[-(nrow(yMat)),], na.rm = TRUE) / tstep 36 | estD <- rowMeans(cbind(estDx,estDy)) 37 | R <- 1/6 # R: motion blur coefficient, representing the effect of motion blur. It varies [0, 1/4], R=0 in case of no blur. R=1/6 if the camera shutter is kept open for the full duration of the time-lapse. 38 | estSigma2x <- R * colMeans(xMat^2, na.rm = TRUE) + (2*R-1) * colMeans(xMat[-1,] * xMat[-(nrow(xMat)),], na.rm = TRUE) 39 | estSigma2y <- R * colMeans(yMat^2, na.rm = TRUE) + (2*R-1) * colMeans(yMat[-1,] * xMat[-(nrow(yMat)),], na.rm = TRUE) 40 | estSigma2 <- rowMeans(cbind(estSigma2x, estSigma2y)) 41 | 42 | # turn into data frame 43 | cveDF <- data.frame(trace = tlist, 44 | dee = estD, 45 | estsigma = suppressWarnings(sqrt(estSigma2))) 46 | 47 | return(cveDF) 48 | } 49 | 50 | -------------------------------------------------------------------------------- /R/calculateFD.R: -------------------------------------------------------------------------------- 1 | #' Calculate fractal dimension (FD) 2 | #' 3 | #' Calculate for each track, its fractal dimension 4 | #' Katz & George (1985) define fractal dimension (D) as log(n) / (log(n) + log(d/L)) 5 | #' where n is the number of steps, d is the longest of all possible point-to-point distances and 6 | #' L is the cumulative length of the track. 7 | #' D is ~1 for directed trajectories, ~2 for confined and ~3 for subdiffusion 8 | #' Here we calculate this and store D (called fd) and d (called wide) for return. 9 | #' Note that this method does not take into account gaps in the track. 10 | #' For a track with many gaps, n will be lowered. 11 | #' 12 | #' @param dataList list of a data frame (must include at a minimum - trace (track ID), x, y and frame (in real coords)) and a calibration data frame 13 | #' @return data frame 14 | #' @examples 15 | #' xmlPath <- system.file("extdata", "ExampleTrackMateData.xml", package="TrackMateR") 16 | #' tmObj <- readTrackMateXML(XMLpath = xmlPath) 17 | #' tmObj <- correctTrackMateData(dataList = tmObj, xyscalar = 0.04) 18 | #' fdDF <- calculateFD(dataList = tmObj) 19 | #' @export 20 | 21 | calculateFD <- function(dataList) { 22 | x <- y <- NULL 23 | 24 | if(inherits(dataList, "list")) { 25 | df <- dataList[[1]] 26 | } else { 27 | cat("Function requires a list of TrackMate data and calibration data\n") 28 | return(NULL) 29 | } 30 | 31 | # make a list of all traces (this is the filtered list of traces from TrackMate XML) 32 | traceList <- unique(df$trace) 33 | # data frame to hold results 34 | result <- data.frame(trace = rep("", length(traceList)), 35 | wide = rep(NA, length(traceList)), 36 | fd = rep(NA, length(traceList))) 37 | ii <- 1 38 | for (i in traceList){ 39 | a <- df %>% 40 | filter(trace == i) %>% 41 | select(x, y) 42 | b <- dist(a) 43 | n <- nrow(a) 44 | d <- max(b, na.rm = TRUE) 45 | l <- max(df$cumulative_distance[df$trace == i]) 46 | # fractal dimension 47 | fd <- log(n) / log( n * d * l^-1) 48 | result$trace[ii] <- i 49 | result$wide[ii] <- d 50 | result$fd[ii] <- fd 51 | ii <- ii + 1 52 | } 53 | 54 | return(result) 55 | } 56 | -------------------------------------------------------------------------------- /R/calculateJD.R: -------------------------------------------------------------------------------- 1 | #' Calculate Jump Distance (JD) 2 | #' 3 | #' Calculation of the JD of multiple tracks. 4 | #' Calculation is equivalent to a single time lag point on the ensemble MSD curve, typically represented as a histogram 5 | #' Input is a data frame of tracks imported using readTrackMateXML() 6 | #' The default time step is one frame - which is the equivalent to the plot generated to show displacement versus time. 7 | #' 8 | #' @param dataList list of data frame (must include at a minimum - trace (track ID), x, y and t (in real coords)) and calibration 9 | #' @param deltaT integer to represent the multiple of frames that are to be analysed 10 | #' @param nPop integer (either 1,2 or 3) number of populations for the jump distance fitting 11 | #' @param mode string indicated ECDF (default) or hist (histogram) 12 | #' @param init initialisation parameters for the nls fit for example list(D2 = 200, D1 = 0.1) or list(D2 = 0.01, D1=0.1, D3=10, D4=100) 13 | #' @param timeRes time resolution per unit of jump. Frame interval is 0.5 s and jump interval is two steps, timeRes = 1. 14 | #' @param breaks number of bins for histogram. With ECDF breaks can be high e.g. 100, for mode = "hist" they should be low, perhaps 30. 15 | #' @return a list of data frame of jump distances, NAs removed; and a list of parameters called jumpParam (jumptime, deltaT, nPop) 16 | #' @examples 17 | #' xmlPath <- system.file("extdata", "ExampleTrackMateData.xml", package="TrackMateR") 18 | #' tmObj <- readTrackMateXML(XMLpath = xmlPath) 19 | #' tmObj <- correctTrackMateData(tmObj, xyscalar = 0.04) 20 | #' jdObj <- calculateJD(dataList = tmObj, deltaT = 2) 21 | #' @export 22 | 23 | calculateJD <- function(dataList, deltaT = 1, nPop = 2, mode = "ECDF", init = NULL, timeRes = 1, breaks = 100) { 24 | x <- y <- NULL 25 | 26 | if(inherits(dataList, "list")) { 27 | df <- dataList[[1]] 28 | calibration <- dataList[[2]] 29 | } else { 30 | cat("Function requires a list of TrackMate data and calibration data\n") 31 | return(NULL) 32 | } 33 | 34 | if(deltaT < 1 | deltaT %% 1 != 0) { 35 | # check is an integer >= 1 36 | return(NULL) 37 | } 38 | # make a list of all traces (this is the filtered list of traces from TrackMate XML) 39 | traceList <- unique(df$trace) 40 | tList <- list() 41 | # for each trace find the frame offset 42 | for (i in traceList) { 43 | a <- df %>% 44 | filter(trace == i) %>% 45 | select(frame, x) 46 | frame0 <- a$frame[1] 47 | a <- a[-1,] 48 | tList[[i]] <- a$frame - frame0 49 | } 50 | # find the maximum frame offset of all traces 51 | tListmax <- max(unlist(lapply(tList, max))) 52 | if(deltaT > tListmax) { 53 | # break because requested interval would result in no data 54 | return(NULL) 55 | } 56 | # create matrices for x and y coords one row for each frame, each trace in a column 57 | displacementXMat <- matrix(data = NA, nrow = tListmax, ncol = length(traceList)) 58 | colnames(displacementXMat) <- traceList 59 | rownames(displacementXMat) <- 1:tListmax 60 | displacementYMat <- displacementXMat 61 | # now we repeat the process to extract the x y coords for each frame offset, store in matrices. 62 | for (i in traceList){ 63 | a <- df %>% 64 | filter(trace == i) %>% 65 | select(x, y, frame) 66 | frame0 <- a$frame[1] 67 | a <- a[-1,] 68 | a$frame <- a$frame - frame0 69 | displacementXMat[a$frame,i] <- a$x 70 | displacementYMat[a$frame,i] <- a$y 71 | } 72 | 73 | # calculate displacement between points for x and y 74 | deltaXCoords <- displacementXMat[(1 + deltaT) : tListmax,] - displacementXMat[1 : (tListmax-deltaT),] 75 | deltaYCoords <- displacementYMat[(1 + deltaT) : tListmax,] - displacementYMat[1 : (tListmax-deltaT),] 76 | # calculate distance 77 | jdMat <- sqrt(deltaXCoords**2 + deltaYCoords**2) 78 | # convert to vector 79 | jdVec <- c(jdMat) 80 | jdVec <- jdVec[!is.na(jdVec)] 81 | # convert to dataframe with column called jump 82 | jdDF <- data.frame(jump = jdVec) 83 | # record jumptime which is deltaT * time resoltuion, store all other params 84 | jdParam <- list(jumptime = deltaT * calibration[2,1], 85 | deltaT = deltaT, 86 | nPop = nPop, 87 | mode = mode, 88 | init = init, 89 | units = calibration$unit[1:2], 90 | timeRes = timeRes, 91 | breaks = breaks) 92 | # make list of these two and return 93 | jumpList <- list(jdDF, jdParam) 94 | 95 | return(jumpList) 96 | } 97 | 98 | -------------------------------------------------------------------------------- /R/calculateMSD.R: -------------------------------------------------------------------------------- 1 | #' Calculate Mean Squared Displacement (MSD) 2 | #' 3 | #' Calculation of the MSD of multiple tracks. 4 | #' There are two methods for everaging MSD data from multiple tracks: 5 | #' ensemble = for each time lag average all squared displacements from all tracks 6 | #' time-averaged = find MSD for each track and then generate the average MSD from these curves 7 | #' The MSD curves will be identical if all tracks are the same length, and diverge if not. 8 | #' Standard deviation will be large for ensemble and smaller for time-averaged data. 9 | #' Input is a data frame of tracks imported using readTrackMateXML() 10 | #' 11 | #' @param df data frame must include at a minimum - trace (track ID), x, y and t (in real coords) 12 | #' @param method string. Either "ensemble" or "timeaveraged" (default) 13 | #' @param N numeric variable for MSD. dt should be up to 1/N of number of data points (4 recommended) 14 | #' @param short numeric variable for the shortest number of points we will analyse. Note, this uses the number of frames from start, not number of points in track, i.e. a track with % 38 | filter(trace == i) %>% 39 | select(frame, x) 40 | frame0 <- a$frame[1] 41 | a <- a[-1,] 42 | tList[[i]] <- a$frame - frame0 43 | } 44 | # filter out short traces 45 | if(short > 0) { 46 | traceList_temp <- subset(traceList,unlist(lapply(tList, max)) >= short) 47 | tList_temp <- subset(tList,unlist(lapply(tList, max)) >= short) 48 | if(length(traceList_temp) == 0) { 49 | cat("No traces are long enough to calculate MSD. Reverting to short = 0.\n") 50 | } else { 51 | traceList <- traceList_temp 52 | tList <- tList_temp 53 | } 54 | } 55 | # find the maximum frame offset of all traces 56 | tListmax <- max(unlist(lapply(tList, max))) 57 | # create matrices for x and y coords one row for each frame, each trace in a column 58 | displacementXMat <- matrix(data = NA, nrow = tListmax, ncol = length(traceList)) 59 | colnames(displacementXMat) <- traceList 60 | rownames(displacementXMat) <- 1:tListmax 61 | displacementYMat <- displacementXMat 62 | # now we repeat the process to extract the x y coords for each frame offset, store in matrices. 63 | for (i in traceList){ 64 | a <- df %>% 65 | filter(trace == i) %>% 66 | select(x, y, frame) 67 | frame0 <- a$frame[1] 68 | a <- a[-1,] 69 | a$frame <- a$frame - frame0 70 | displacementXMat[a$frame,i] <- a$x 71 | displacementYMat[a$frame,i] <- a$y 72 | } 73 | # find the 90th centile longest trace (in terms of frame offset) 74 | tListmax2 <- quantile(unlist(lapply(tList, max)), probs=.9) 75 | # dt should be up to 1/4 of number of data points (default) 76 | numberOfdeltaT = floor(tListmax2/N) 77 | if(numberOfdeltaT == 0) { 78 | cat("The number of frames is too few to calculate MSD.\n") 79 | return(NULL) 80 | } 81 | # make matrix to store the averaged msd summary 82 | msd <- matrix(data = NA, nrow = numberOfdeltaT, ncol = 5 ) 83 | colnames(msd) <- c("mean", "sd", "n", "size", "t") 84 | tstep <- df$t[match(1,df$frame)] 85 | # make matrix to store the msd curve for each track 86 | trackmsd <- matrix(data = NA, nrow = numberOfdeltaT, ncol = length(traceList) ) 87 | colnames(trackmsd) <- traceList 88 | 89 | for(deltaT in 1 : numberOfdeltaT){ 90 | # calculate displacement between points for x and y 91 | deltaXCoords <- displacementXMat[(1 + deltaT) : tListmax,] - displacementXMat[1 : (tListmax-deltaT),] 92 | deltaYCoords <- displacementYMat[(1 + deltaT) : tListmax,] - displacementYMat[1 : (tListmax-deltaT),] 93 | # calculate squared displacement 94 | squaredDisplacement <- deltaXCoords**2 + deltaYCoords**2 95 | # we collect the MSD for each track (column) 96 | if(length(dim(squaredDisplacement)) < 2) { 97 | next 98 | } 99 | eachSquaredDisplacement <- colMeans(squaredDisplacement, na.rm = TRUE) 100 | # summary statistics for each deltaT 101 | if(method == "ensemble") { 102 | # store the ensemble data 103 | msd[deltaT,1] <- mean(squaredDisplacement, na.rm = TRUE) # average 104 | msd[deltaT,2] <- sd(squaredDisplacement, na.rm = TRUE) # standard deviation 105 | msd[deltaT,3] <- sum(!is.na(squaredDisplacement)) # n 106 | } else { 107 | # store the time-averaged data 108 | msd[deltaT,1] <- mean(eachSquaredDisplacement, na.rm = TRUE) # average 109 | msd[deltaT,2] <- sd(eachSquaredDisplacement, na.rm = TRUE) # standard deviation 110 | msd[deltaT,3] <- sum(!is.na(eachSquaredDisplacement)) # n 111 | } 112 | # place the MSD for this deltaT and for every track into the matrix 113 | trackmsd[deltaT,] <- eachSquaredDisplacement 114 | } 115 | # send msd curves for each track to compute the diffusive behaviour 116 | alphas <- calculateAlpha(trackmsd, tstep) 117 | 118 | # send x y displacements (1 frame lag) to calculate CVE 119 | deltaXCoords <- displacementXMat[2 : tListmax,] - displacementXMat[1 : (tListmax-1),] 120 | deltaYCoords <- displacementYMat[2 : tListmax,] - displacementYMat[1 : (tListmax-1),] 121 | cves <- calculateCVE(deltaXCoords, deltaYCoords, traceList, tstep) 122 | 123 | # format msd matrix into data frame 124 | msd <- as.data.frame(msd) 125 | msd$size <- c(1 : numberOfdeltaT) 126 | msd$t <- msd$size * tstep 127 | 128 | msdList <- list(msd,alphas,cves) 129 | 130 | return(msdList) 131 | } 132 | 133 | -------------------------------------------------------------------------------- /R/calculateTrackDensity.R: -------------------------------------------------------------------------------- 1 | #' Calculate density of tracks 2 | #' 3 | #' Calculate for each track, using its starting frame, what is the relative density of tracks. 4 | #' We use a search radius to find how many tracks in the starting frame are neighbours of the track. 5 | #' The number of neighbours is normalised to the search circle, so that a track in the corner of the image with 2 neighbours has a density of 8. 6 | #' Code for calculating search area (intersection between search circle and the image border) is taken from 7 | #' https://petrelharp.github.io/circle_rectangle_intersection/circle_rectangle_intersection.html 8 | #' 9 | #' @param dataList list of a data frame (must include at a minimum - trace (track ID), x, y and frame (in real coords)) and a calibration data frame 10 | #' @param radius numeric variable for search radius (in spatial units of the data) 11 | #' @return data frame 12 | #' @examples 13 | #' xmlPath <- system.file("extdata", "ExampleTrackMateData.xml", package="TrackMateR") 14 | #' tmObj <- readTrackMateXML(XMLpath = xmlPath) 15 | #' tmObj <- correctTrackMateData(dataList = tmObj, xyscalar = 0.04) 16 | #' tdDF <- calculateTrackDensity(dataList = tmObj, radius = 2) 17 | #' @export 18 | 19 | calculateTrackDensity <- function(dataList, radius = 1) { 20 | x <- y <- trace <- NULL 21 | 22 | if(inherits(dataList, "list")) { 23 | df <- dataList[[1]] 24 | calibration <- dataList[[2]] 25 | } else { 26 | cat("Function requires a list of TrackMate data and calibration data\n") 27 | return(NULL) 28 | } 29 | # make a list of all traces (this is the filtered list of traces from TrackMate XML) 30 | traceList <- unique(df$trace) 31 | 32 | # for each trace, find the first frame 33 | for (i in traceList) { 34 | a <- df %>% 35 | filter(trace == i) %>% 36 | select(frame, x, y) 37 | frame0 <- a$frame[1] 38 | x0 <- a$x[1] 39 | y0 <- a$y[1] 40 | # select first frame for this track 41 | a <- df %>% 42 | filter(frame == frame0) %>% 43 | select(x, y) 44 | # calculate the distance from x0, y0 to all other coords 45 | distances <- find_distances(x0,y0,a) 46 | # count how many are less than search radius (this will include the track itself, so subtract 1) 47 | neighbours <- sum(distances <= radius, na.rm = TRUE) - 1 48 | # calculate how much of the search circle was inside the frame 49 | search_fraction <- find_td_area(r = radius, xy = c(x0,y0), a= c(0,calibration[3,1]), b = c(0,calibration[4,1])) / (pi * radius^2) 50 | subdf <- data.frame(trace = i, 51 | neighbours = neighbours, 52 | fraction = search_fraction) 53 | if(i == traceList[1]) { 54 | dfall <- subdf 55 | } else { 56 | dfall <- rbind(dfall,subdf) 57 | } 58 | } 59 | # divide the count by the fraction of circle that was inside the frame to give "density" - do this at the end 60 | dfall$density <- dfall$neighbours / dfall$fraction 61 | 62 | return(dfall) 63 | } 64 | 65 | #' Find distance between xy coordinate and a series of other xy coordinates 66 | #' 67 | #' @param xx x coord of point for comparison 68 | #' @param yy y coord of point for comparison 69 | #' @param df data frame containing x and y columns for other points 70 | #' @return numeric vector of distances 71 | #' @keywords internal 72 | find_distances <- function(xx, yy, df) { 73 | df$x <- (df$x - xx)^2 74 | df$y <- (df$y - yy)^2 75 | out <- sqrt(df$x + df$y) 76 | 77 | return(out) 78 | } 79 | 80 | #' Track Density - Find area A1 81 | #' 82 | #' @param x value 83 | #' @param r radius 84 | #' @return numeric variable 85 | #' @keywords internal 86 | find_td_A1 <- function (x, r) { 87 | out <- 0 88 | if (x < r) { 89 | out <- r^2 * acos(x/r) - x * sqrt(r^2 - x^2) 90 | } 91 | return(out) 92 | } 93 | 94 | #' Track Density - Find area A2 95 | #' 96 | #' @param x value 97 | #' @param y value 98 | #' @param r radius 99 | #' @return numeric variable 100 | #' @keywords internal 101 | find_td_A2 <- function (x, y, r) { 102 | out <- 0 103 | if (x^2 + y^2 < r^2) { 104 | out <- ( 105 | (r^2 / 2) * (acos(y/r) + acos(x/r) - pi/2) 106 | - x * sqrt(r^2 - x^2) / 2 107 | - y * sqrt(r^2 - y^2) / 2 108 | + x * y 109 | ) 110 | } 111 | return(out) 112 | } 113 | 114 | #' Track Density - Find search area 115 | #' 116 | #' Find the area of the intersection of the circle centered at xy with radius r 117 | #' and the radius with vertical sides at a and horizontal sides at b. 118 | #' xy, a, and b must be vectors of length 2, and xy must lie within the rectangle. 119 | #' 120 | #' @param r radius of search circle 121 | #' @param xy numeric vector (length 2) 122 | #' @param a numeric vector (length 2) 123 | #' @param b numeric vector (length 2) 124 | #' @return numeric variable 125 | #' @examples 126 | #' find_td_area(r=2, xy=c(4, 4), a=c(0, 8), b=c(0, 5)) 127 | #' @export 128 | #' @keywords internal 129 | find_td_area <- function(r, xy, a, b) { 130 | stopifnot(length(xy) == 2 && length(a) == 2 && length(b) == 2) 131 | x1 <- xy[1] - a[1] 132 | x2 <- a[2] - xy[1] 133 | y1 <- xy[2] - b[1] 134 | y2 <- b[2] - xy[2] 135 | stopifnot(min(x1, x2, y1, y2) >= 0) 136 | A <- ( 137 | find_td_A1(x1, r) 138 | + find_td_A1(x2, r) 139 | + find_td_A1(y1, r) 140 | + find_td_A1(y2, r) 141 | - find_td_A2(x1, y1, r) 142 | - find_td_A2(x1, y2, r) 143 | - find_td_A2(x2, y1, r) 144 | - find_td_A2(x2, y2, r) 145 | ) 146 | stopifnot(A >= 0) 147 | stopifnot(A <= pi * r^2) 148 | 149 | return(pi * r^2 - A) 150 | } 151 | -------------------------------------------------------------------------------- /R/compareDatasets.R: -------------------------------------------------------------------------------- 1 | #' Compare datasets 2 | #' 3 | #' Requires TrackMate XML files to be organised into subfolders named according to the condition. 4 | #' If these condition folders are in `Data/` within the working directory, the code will run automatically. 5 | #' Since there is no easy cross-platform way for the user to interactively pick a directory, the organisation of files in `Data/` is a requirement. 6 | #' The `Data/` folder can be elsewhere on your computer, just change the `wd` prior to running `compareDatasets()` and the routine will run. 7 | #' The code will process all the datasets individually, compile them according to condition and compare across conditions. 8 | #' Outputs are saved to `Output/Plots/` in the working directory. 9 | #' 10 | #' If TrackMate XML files require recalibration, this is possible by placing a csv file into each subfolder. 11 | #' All xml files in that folder whose calibration does not match the calibration csv file will be altered. 12 | #' Ideally, all conditions should have the same scaling, and within a condition they should be similar. 13 | #' The code will run if this is not the case, but beware that these discrepancies are not detected. 14 | #' For example, comparing two datasets in um/s with one in mm/min. 15 | #' 16 | #' @param ... pass additional parameters to modify the defaults (N, short, deltaT, mode, nPop, init, timeRes, breaks, radius) 17 | #' 18 | #' @return multiple pdf reports 19 | #' @export 20 | compareDatasets <- function(...) { 21 | 22 | condition <- value <- dataid <- cumulative_distance <- track_duration <- mean_intensity <- NULL 23 | 24 | if(!dir.exists("Data")) { 25 | # there is no cross-platform way to safely choose directory 26 | cat("Please organise your XML files in a folder called Data in the working directory\r") 27 | return(-1) 28 | } else { 29 | datadir <- "Data" 30 | } 31 | 32 | # ellipsis processing 33 | l <- NULL 34 | l <- list(...) 35 | l <- processEllipsis(l) 36 | 37 | # loop through condition folders within data folder 38 | condFolderNames <- list.dirs(path = datadir, recursive = FALSE) 39 | # break if there were no folders in Data directory 40 | if(identical(condFolderNames, character(0)) == TRUE) { 41 | return(-1) 42 | } 43 | 44 | for(i in 1:length(condFolderNames)) { 45 | condFolderPath <- condFolderNames[i] 46 | condFolderName <- basename(condFolderPath) 47 | allTrackMateFiles <- list.files(condFolderPath, pattern = "*.xml") 48 | # skip if there were no XML files in this folder 49 | if(identical(allTrackMateFiles, character(0)) == TRUE) { 50 | next 51 | } 52 | # check to see if a calibration file is present 53 | calibrationFiles <- list.files(condFolderPath, pattern = "*.csv") 54 | calibrationFile <- paste0(condFolderPath,"/",calibrationFiles[1]) 55 | if(identical(calibrationFiles, character(0)) == TRUE) { 56 | calibrationXY <- 1 57 | calibrationT <- 1 58 | calibrate <- FALSE 59 | } else { 60 | # load calibration file and store calibrations 61 | calibDF <- read.csv(calibrationFile) 62 | calibrate <- TRUE 63 | } 64 | cat(paste0("\n","Processing ",condFolderName,"\n")) 65 | bigtm <- bigmsd <- bigalpha <- bigdee <- bigjd <- bigtd <- bigfd <- data.frame() 66 | for(j in 1:length(allTrackMateFiles)) { 67 | fileName <- allTrackMateFiles[j] 68 | thisFilePath <- paste0(condFolderPath, "/", fileName) 69 | # read dataset 70 | tmObj <- readTrackMateXML(XMLpath = thisFilePath) 71 | if(is.null(tmObj)) { 72 | cat(paste0("Skipping ",fileName," - no data found!\n")) 73 | next 74 | } 75 | # scale dataset if required 76 | if(calibrate) { 77 | calibrationDF <- tmObj[[2]] 78 | # scalar for conversion is new / old (units not relevant) 79 | calibrationXY <- calibDF[1,1] / calibrationDF[1,1] 80 | calibrationT <- calibDF[2,1] / calibrationDF[2,1] 81 | # 0 in calibDF indicates no scaling is to be done 82 | calibrationXY <- ifelse(calibrationXY == 0, 1, calibrationXY) 83 | calibrationT <- ifelse(calibrationT == 0, 1, calibrationT) 84 | # ignore an error of 2.5% 85 | calibrationXY <- ifelse(calibrationXY < 1.025 & calibrationXY > 0.975, 1, calibrationXY) 86 | calibrationT <- ifelse(calibrationT < 1.025 & calibrationT > 0.975, 1, calibrationT) 87 | if(calibrationXY != 1 & calibrationT != 1) { 88 | tmObj <- correctTrackMateData(dataList = tmObj, xyscalar = calibrationXY, tscalar = calibrationT, xyunit = calibDF[1,2], tunit = calibDF[2,2]) 89 | } else if(calibrationXY != 1 & calibrationT == 1) { 90 | tmObj <- correctTrackMateData(dataList = tmObj, xyscalar = calibrationXY, xyunit = calibDF[1,2]) 91 | } else if(calibrationXY == 1 & calibrationT != 1) { 92 | tmObj <- correctTrackMateData(dataList = tmObj, tscalar = calibrationT, tunit = calibDF[2,2]) 93 | } else { 94 | # the final possibility is nothing needs scaling but units need to change. 95 | # do not test if they are the same just flush the units with the values in the csv file 96 | tmObj <- correctTrackMateData(dataList = tmObj, xyunit = calibDF[1,2], tunit = calibDF[2,2]) 97 | } 98 | } 99 | # we can filter here if required - for example only analyse tracks of certain length 100 | tmDF <- tmObj[[1]] 101 | calibrationDF <- tmObj[[2]] 102 | # sanity check - probably not needed 103 | if(is.null(tmDF)) { 104 | cat(paste0("Skipping ",fileName," - no data found!\n")) 105 | next 106 | } 107 | # if the data is not rich enough for a summary we will skip it 108 | if(calibrationDF[5,1] < 3 & calibrationDF[6,1] < 10) { 109 | cat(paste0("Skipping ",fileName," as it has less than 3 tracks and longest track has less than 10 frames\n")) 110 | next 111 | } 112 | # take the units 113 | units <- calibrationDF$unit[1:2] 114 | 115 | ## we need to combine data frames 116 | # first add a column to id the data 117 | thisdataid <- paste0(condFolderName,"_",as.character(j)) 118 | tmDF$dataid <- thisdataid 119 | 120 | # calculate MSD 121 | msdObj <- calculateMSD(tmDF, N = 3, short = 8) 122 | msdDF <- msdObj[[1]] 123 | alphaDF <- msdObj[[2]] 124 | deeDF <- msdObj[[3]] 125 | if(!is.null(msdDF) | !is.null(alphaDF) | !is.null(deeDF)) { 126 | # we need to add the dataid to the summary 127 | msdDF$dataid <- thisdataid 128 | alphaDF$dataid <- thisdataid 129 | deeDF$dataid <- thisdataid 130 | } 131 | 132 | # jump distance calc with deltaT of 1 133 | deltaT <- 1 134 | jdObj <- calculateJD(dataList = tmObj, deltaT = l$deltaT, nPop = l$nPop, mode = l$mode, init = l$init, timeRes = l$timeRes, breaks = l$breaks) 135 | jdDF <- jdObj[[1]] 136 | if(is.null(jdDF)) { 137 | jdObj <- NULL 138 | } else { 139 | jdDF$dataid <- thisdataid 140 | timeRes <- jdObj[[2]] 141 | jdObj <- list(jdDF,timeRes) 142 | } 143 | # track density with a radius of 1.5 units 144 | tdDF <- calculateTrackDensity(dataList = tmObj, radius = l$radius) 145 | if(!is.null(tdDF)) { 146 | tdDF$dataid <- thisdataid 147 | } 148 | # fractal dimension 149 | fdDF <- calculateFD(dataList = tmObj) 150 | if(!is.null(fdDF)) { 151 | fdDF$dataid <- thisdataid 152 | } 153 | 154 | # add to the big dataframes 155 | if(!is.null(tmDF)) { 156 | bigtm <- rbind(bigtm,tmDF) 157 | } 158 | if(!is.null(msdDF)) { 159 | bigmsd <- rbind(bigmsd,msdDF) 160 | } 161 | if(!is.null(alphaDF)) { 162 | bigalpha <- rbind(bigalpha,alphaDF) 163 | } 164 | if(!is.null(deeDF)) { 165 | bigdee <- rbind(bigdee,deeDF) 166 | } 167 | if(!is.null(jdDF)) { 168 | bigjd <- rbind(bigjd,jdDF) 169 | } 170 | if(!is.null(tdDF)) { 171 | bigtd <- rbind(bigtd,tdDF) 172 | } 173 | if(!is.null(fdDF)) { 174 | bigfd <- rbind(bigfd,fdDF) 175 | } 176 | 177 | # create the report for this dataset 178 | fileName <- tools::file_path_sans_ext(basename(thisFilePath)) 179 | both <- makeSummaryReport(tmList = tmObj, msdList = msdObj, jumpList = jdObj, tddf = tdDF, fddf = fdDF, 180 | titleStr = condFolderName, subStr = fileName, auto = TRUE, summary = FALSE, 181 | msdplot = l$msdplot) 182 | p <- both[[1]] 183 | destinationDir <- paste0("Output/Plots/", condFolderName) 184 | setupOutputPath(destinationDir) 185 | filePath <- paste0(destinationDir, "/report_",as.character(j),".pdf") 186 | ggsave(filePath, plot = p, width = 25, height = 19, units = "cm") 187 | # retrieve other data 188 | df_report <- both[[2]] 189 | df_report$condition <- condFolderName 190 | df_report$dataid <- thisdataid 191 | if(i == 1 & j == 1) { 192 | megareport <- df_report 193 | } else if(!exists("megareport")) { 194 | megareport <- df_report 195 | } else { 196 | megareport <- rbind(megareport,df_report) 197 | } 198 | } 199 | bigtmObj <- list(bigtm,calibrationDF) 200 | bigmsdObj <- list(bigmsd,bigalpha,bigdee) 201 | bigjdObj <- list(bigjd,timeRes) 202 | # now we have our combined dataset we can make a summary 203 | # note we use the timeRes of the final dataset; so it is suitable for only when all files have the same calibration 204 | summaryObj <- makeSummaryReport(tmList = bigtmObj, msdList = bigmsdObj, jumpList = bigjdObj, tddf = bigtd, fddf = bigfd, 205 | titleStr = condFolderName, subStr = "Summary", auto = TRUE, summary = TRUE, 206 | msdplot = l$msdplot) 207 | p <- summaryObj[[1]] 208 | destinationDir <- paste0("Output/Plots/", condFolderName) 209 | filePath <- paste0(destinationDir, "/combined.pdf") 210 | ggsave(filePath, plot = p, width = 25, height = 19, units = "cm") 211 | # save data as csv 212 | destinationDir <- paste0("Output/Data/", condFolderName) 213 | setupOutputPath(destinationDir) 214 | # save each dataset-level data 215 | write.csv(bigtm, paste0(destinationDir, "/allTM.csv"), row.names = FALSE) 216 | write.csv(bigmsd, paste0(destinationDir, "/allMSD.csv"), row.names = FALSE) 217 | write.csv(bigjd, paste0(destinationDir, "/allJD.csv"), row.names = FALSE) 218 | write.csv(bigfd, paste0(destinationDir, "/allFD.csv"), row.names = FALSE) 219 | # mega data frame of msd averages per dataset; alpha values, track density, speed/duration/distance, intensity by trace/dataid/condition 220 | msdSummary <- summaryObj[[2]] 221 | msdSummary$condition <- condFolderName 222 | bigspeed <- bigtm %>% 223 | group_by(dataid, trace) %>% 224 | summarise(cumdist = max(cumulative_distance), cumtime = max(track_duration), intensity = max(mean_intensity)) 225 | bigspeed$speed <- bigspeed$cumdist / bigspeed$cumtime 226 | bigspeed$condition <- condFolderName 227 | if(i == 1 | !exists("megamsd")) { 228 | megamsd <- msdSummary 229 | megaalpha <- bigalpha 230 | megadee <- bigdee 231 | megatd <- bigtd 232 | megaspeed <- bigspeed 233 | megafd <- bigfd 234 | } else { 235 | megamsd <- rbind(megamsd,msdSummary) 236 | megaalpha <- rbind(megaalpha,bigalpha) 237 | megadee <- rbind(megadee,bigdee) 238 | megatd <- rbind(megatd,bigtd) 239 | megaspeed <- rbind(megaspeed,bigspeed) 240 | megafd <- rbind(megafd,bigfd) 241 | } 242 | } 243 | 244 | # save summary data as csv 245 | destinationDir <- "Output/Data" 246 | write.csv(megamsd, paste0(destinationDir, "/allMSDCurves.csv"), row.names = FALSE) 247 | write.csv(megareport, paste0(destinationDir, "/allComparison.csv"), row.names = FALSE) 248 | 249 | # for alpha values, estimator of D, track density, peed/duration/distance, intensity by trace/dataid/condition we must combine into one 250 | # set the name of dee to estdee 251 | names(megadee)[names(megadee) == "dee"] <- "estdee" 252 | megatrace <- Reduce(mergeDataFramesForExport, list(megaalpha, megadee, megatd, megaspeed, megafd)) 253 | write.csv(megatrace, paste0(destinationDir, "/allTraceData.csv"), row.names = FALSE) 254 | 255 | # generate the comparison plots and save 256 | p <- makeComparison(df = megareport, msddf = megamsd, units = units, msdplot = l$msdplot) 257 | destinationDir <- "Output/Plots/" 258 | filePath <- paste0(destinationDir, "/comparison.pdf") 259 | ggsave(filePath, plot = p, width = 25, height = 19, units = "cm") 260 | } 261 | -------------------------------------------------------------------------------- /R/correctTrackMateData.R: -------------------------------------------------------------------------------- 1 | #' Correct distance and time of imported TrackMate data. 2 | #' 3 | #' If the TrackMate data is in pixels and/or frames, the data frame can be converted with this function. 4 | #' 5 | #' @param dataList a list of a data frame (of track data) and a calibration data frame (from TrackMate XML) 6 | #' @param xyscalar numeric multiplier to correct pixel size of original movie. Assumes isotropic scaling, i.e. pixel height = pixel width 7 | #' @param tscalar numeric multiplier to correct frame interval of original movie. Frame interval of tracked data. 8 | #' @param xyunit string to describe spatial units 9 | #' @param tunit string to describe temporal unit 10 | #' @return list of two data frames 11 | #' @examples 12 | #' xmlPath <- system.file("extdata", "ExampleTrackMateData.xml", package="TrackMateR") 13 | #' tmObj <- readTrackMateXML(XMLpath = xmlPath) 14 | #' # in the case where pixel size is 0.03 um and original data is 1 pixel, xyscalar = 0.03 15 | #' tmObj <- correctTrackMateData(dataList = tmObj, xyscalar = 0.03) 16 | #' @export 17 | 18 | # correcting dataframe - assumes that scaling is currently 1 pixel and/or 1 frame 19 | correctTrackMateData <- function(dataList, xyscalar = 1, tscalar = 1, xyunit = NULL, tunit = NULL) { 20 | if(xyscalar == 1 & tscalar == 1) { 21 | cat("No correction applied.\n") 22 | return(dataList) 23 | } else { 24 | msg <- "" 25 | } 26 | if(!inherits(dataList, "list")) { 27 | cat("Requires TrackMate data frame and calibration data frame, as a list.\n") 28 | return(dataList) 29 | } 30 | df <- dataList[[1]] 31 | calib <- dataList[[2]] 32 | if(xyscalar != 1) { 33 | msg <- paste0("Correcting XY scale. ",msg) 34 | # change pixel size 35 | df$x <- df$x * xyscalar 36 | df$y <- df$y * xyscalar 37 | df$displacement <- df$displacement * xyscalar 38 | df$cumulative_distance <- df$cumulative_distance * xyscalar 39 | df$radius <- df$radius * xyscalar 40 | # there are potentially other columns present that may need adjusting 41 | if("area" %in% colnames(df)) 42 | { 43 | df$area <- df$area * xyscalar^2 44 | } 45 | if("perimeter" %in% colnames(df)) 46 | { 47 | df$perimeter <- df$perimeter * xyscalar 48 | } 49 | if("ellipse_x0" %in% colnames(df)) 50 | { 51 | df$ellipse_x0 <- df$ellipse_x0 * xyscalar 52 | df$ellipse_y0 <- df$ellipse_y0 * xyscalar 53 | df$ellipse_major <- df$ellipse_major * xyscalar 54 | df$ellipse_minor <- df$ellipse_minor * xyscalar 55 | } 56 | # correct calibration data 57 | calib[1,1] <- calib[1,1] * xyscalar 58 | calib[3:4,1] <- calib[3:4,1] * xyscalar # border of image 59 | } 60 | if(tscalar != 1) { 61 | msg <- paste0("Correcting timescale. ",msg) 62 | # change time scale 63 | df$t <- df$t * tscalar 64 | df$track_duration <- df$track_duration * tscalar 65 | # correct calibration data 66 | calib[2,1] <- calib[2,1] * tscalar 67 | } 68 | # will change units even if no scaling is done 69 | if(!is.null(xyunit)) { 70 | calib[1,2] <- xyunit 71 | } 72 | if(!is.null(tunit)) { 73 | calib[2,2] <- tunit 74 | } 75 | msg <- paste0(msg,"\n") 76 | cat(msg) 77 | tstep <- df$t[match(1,df$frame)] 78 | df$speed <- df$displacement / tstep 79 | 80 | dataList <- list(df,calib) 81 | 82 | return(dataList) 83 | } 84 | -------------------------------------------------------------------------------- /R/fittingJD.R: -------------------------------------------------------------------------------- 1 | #' Fitting jump distance (JD) data 2 | #' 3 | #' Jump Distances have been calculated for a given time lag. 4 | #' They can be described by fitting curves to the data, either using a histogram or cumulative probability density function. 5 | #' Firtting to a histogram is sensitive to binning parameters and ECDF performs better for general use. 6 | #' The idea behind this analysis is given in: 7 | #' - Weimann et al. (2013) A quantitative comparison of single-dye tracking analysis tools using Monte Carlo simulations. PloS One 8, e64287. 8 | #' - Menssen & Mani (2019) A Jump-Distance-Based Parameter Inference Scheme for Particulate Trajectories, Biophysical Journal, 117: 1, 143-156. 9 | #' This function is called from inside `makeSummaryReport()`, using the defaults. 10 | #' However, you can pass additional arguments for `nPop`, `init` and `breaks` via the ellipsis. 11 | #' Fitting is tricky and two populations (default) is written to catch errors and retry. 12 | #' In the case of failure, try passing better guesses via `init`. 13 | #' To fit 3 populations, you must pass `nPop = 3` as an additional argument, you are advised to also pass guesses via `init`. 14 | #' 15 | #' @param jumpList list of a data frame (with a column named jump of jump distances), and a list of params 16 | #' @return ggplot 17 | #' @examples 18 | #' xmlPath <- system.file("extdata", "ExampleTrackMateData.xml", package="TrackMateR") 19 | #' tmObj <- readTrackMateXML(XMLpath = xmlPath) 20 | #' tmObj <- correctTrackMateData(tmObj, xyscalar = 0.04) 21 | #' jdObj <- calculateJD(dataList = tmObj, deltaT = 2) 22 | #' fittingJD(jumpList = jdObj) 23 | #' @export 24 | 25 | fittingJD <- function(jumpList) { 26 | coef <- counts <- countsCum <- hist <- mid <- nls <- value <- variable <- NULL 27 | 28 | if(inherits(jumpList, "list")) { 29 | df <- jumpList[[1]] 30 | params <- jumpList[[2]] 31 | } else { 32 | cat("Function requires a list of jump distance data and some parameters\n") 33 | return(NULL) 34 | } 35 | 36 | nPop <- mode <- init <- units <- timeRes <- breaks <- NULL 37 | nPop <- params$nPop 38 | mode <- params$mode 39 | init <- params$init 40 | units <- params$units 41 | timeRes <- params$timeRes 42 | breaks <- params$breaks 43 | 44 | if(nPop < 1 | nPop > 3) { 45 | return(NULL) 46 | } 47 | 48 | if(is.null(init)) { 49 | if(mode == "ECDF") { 50 | guess <- ((mean(df$jump, na.rm = T) / timeRes) / 4) / breaks 51 | # if(nPop == 1) {init <- list(D1 = 0.05)} 52 | if(nPop == 1) {init <- list(D1 = guess)} 53 | # if(nPop == 2) {init <- list(D1 = 0.001, D2 = 0.4, D3 = 0.1)} 54 | if(nPop == 2) {init <- list(D1 = guess, D2 = 0.4, D3 = guess*10)} 55 | # if(nPop == 3) {init <- list(D1 = 0.1, D2 = 0.8, D3 = 0.01, D4 = 0.2, D6 = 0.2)} 56 | if(nPop == 3) {init <- list(D1 = guess, D2 = guess*10, D3 = guess*100, D4 = 0.2, D6 = 0.2)} 57 | } else { 58 | if(nPop == 1) {init <- list(D2 = 100, D1 = 0.1)} 59 | if(nPop == 2) {init <- list(D2 = 0.01, D1 = 0.1, D3 = 10, D4 = 100)} 60 | if(nPop == 3) {init <- list(D2 = 1, D1 = 0.1, D3 = 0.01, D4 = 10, D5 = 100, D6 = 30)} 61 | } 62 | } 63 | # make histogram 64 | hd <- hist(df$jump, breaks = breaks, plot = FALSE) 65 | hdata <- data.frame(counts = hd$counts, 66 | mid = hd$mids, 67 | countsCum = cumsum(hd$counts) / sum(hd$counts) ) 68 | xStr <- paste0("Displacement (",units[1],")") 69 | if(mode == "ECDF") { 70 | p <- ggplot(data = hdata, aes(x = mid, y = countsCum)) + 71 | geom_point() + 72 | lims(x = c(0, max(hdata$mid)), y = c(0, 1.4)) + 73 | labs(x = xStr, y = "Frequency") + 74 | theme_classic() + 75 | theme(legend.position = "none") 76 | } else { 77 | p <- ggplot(data = hdata, aes(x = mid, y = counts)) + 78 | geom_col(fill = "light grey", colour = "dark grey") + 79 | lims(x = c(0, 1.1 * max(hdata$mid)), y = c(0, 1.4 * max(hdata$counts))) + 80 | labs(x = xStr, y = "Counts") + 81 | theme_classic() + 82 | theme(legend.position = "none") 83 | } 84 | 85 | # fitting 86 | x <- seq(0, max(hdata$mid), length = 10 * length(hdata$mid)) 87 | 88 | if(nPop == 1) { 89 | 90 | if(mode == "ECDF") { 91 | fitc <- NULL 92 | try(fitc <- nls(hdata$countsCum ~ 93 | 1 - exp(-hdata$mid^2 / (4 * D1 * timeRes)), 94 | data = hdata, 95 | start = init), 96 | silent = TRUE); 97 | if(is.null(fitc)) { 98 | cat("Failed to fit jump distances with 1 population. Try using different parameters for `init` and/or `nPop`.\n") 99 | return(p) 100 | } 101 | y <- 1 - exp(-x^2 / (4 * coef(fitc)[1] * timeRes)) 102 | fitStr <- paste0("D = ",format(round(coef(fitc)[1],4), nsmall = 4)) 103 | } else { 104 | fit <- NULL 105 | try(fit <- nls(hdata$counts ~ 106 | D2 * hdata$mid/(2 * D1 * timeRes) * exp(-hdata$mid^2 / (4 * D1 * timeRes)), 107 | data = hdata, 108 | start = init), 109 | silent = TRUE); 110 | if(is.null(fit)) { 111 | cat("Failed to fit jump distances with 3 populations. Try using different parameters for `init` and/or `nPop`.\n") 112 | return(p) 113 | } 114 | y <-(coef(fit)[[1]]) * x / (2 * (coef(fit)[[2]]) * timeRes) * exp(-x^2 / (4 * (coef(fit)[[2]]) * timeRes)) 115 | fitStr <- paste0("D = ",format(round(coef(fit)[2],4), nsmall = 4),"\n","A = ", format(round(coef(fit)[1],4), nsmall = 4)) 116 | } 117 | fitdf <- data.frame(x = x, y = y) 118 | } 119 | 120 | if(nPop == 2) { 121 | 122 | if(mode == "ECDF") { 123 | fitc2 <- NULL 124 | try(fitc2 <- nls(hdata$countsCum ~ 125 | (1 - D2 * exp(-hdata$mid^2 / (4 * D1 * timeRes)) - 126 | (1 - D2) * exp(-hdata$mid^2 / (4 * D3 * timeRes))), 127 | data = hdata, 128 | start = init), 129 | silent = TRUE); 130 | if(is.null(fitc2)) { 131 | cat("Failed to fit jump distances with 2 populations. Try using different parameters for `init` and/or `nPop`.\n") 132 | return(p) 133 | } 134 | y1 <- coef(fitc2)[2] - (coef(fitc2)[2] * exp(-x^2 / (4 * coef(fitc2)[1] * timeRes))) 135 | y2 <- (1 - coef(fitc2)[2]) - (1 - coef(fitc2)[2]) * exp(-x^2 / (4 * coef(fitc2)[3] * timeRes)) 136 | fitStr <- paste0("D1 = ",format(round(coef(fitc2)[1],4), nsmall = 4),"\n", 137 | "A1 = ", format(round(coef(fitc2)[2],4), nsmall = 4),"\n", 138 | "D2 = ",format(round(coef(fitc2)[3],4), nsmall = 4),"\n", 139 | "A2 = ", format(round(1 - coef(fitc2)[2],4), nsmall = 4)) 140 | } else { 141 | fit2 <- NULL 142 | try(fit2 <- nls(hdata$counts ~ 143 | D3 * hdata$mid / (2 * D1 * timeRes) * exp(-hdata$mid^2 / (4 * D1 * timeRes)) + 144 | D4 * hdata$mid / (2 * D2 * timeRes) * exp(-hdata$mid^2 / (4 * D2 * timeRes)), 145 | data = hdata, 146 | start = init), 147 | silent = TRUE); 148 | if(is.null(fit2)) { 149 | cat("Failed to fit jump distances with 2 populations. Try using different parameters for `init` and/or `nPop`.\n") 150 | return(p) 151 | } 152 | y1 <- (coef(fit2)[[4]]) * x / (2 * (coef(fit2)[[1]]) * timeRes) * exp(-x^2 / (4 * (coef(fit2)[[1]]) * timeRes)) 153 | y2 <- (coef(fit2)[[3]]) * x / (2 * (coef(fit2)[[2]]) * timeRes) * exp(-x^2 / (4 * (coef(fit2)[[2]]) * timeRes)) 154 | fitStr <- paste0("D1 = ",format(round(coef(fit2)[1],4), nsmall = 4),"\n", 155 | "A1 = ", format(round(coef(fit2)[4],4), nsmall = 4),"\n", 156 | "D2 = ",format(round(coef(fit2)[2],4), nsmall = 4),"\n", 157 | "A2 = ", format(round(coef(fit2)[3],4), nsmall = 4)) 158 | } 159 | y <- y1 + y2 160 | fitdf <- data.frame(x = x, y = y, y1 = y1, y2 = y2) 161 | } 162 | 163 | if(nPop == 3) { 164 | 165 | if(mode == "ECDF") { 166 | fitc3 <- NULL 167 | try(fitc3 <- nls(hdata$countsCum ~ 168 | (1 - D4 * exp(-hdata$mid^2 / (4 * D1 * timeRes)) - 169 | (1-D4-D6) * exp(-hdata$mid^2 / (4 * D2 * timeRes)) - 170 | D6 * exp(-hdata$mid^2 / (4 * D3 * timeRes))), 171 | data = hdata, 172 | start = init), 173 | silent = TRUE); 174 | if(is.null(fitc3)) { 175 | cat("Failed to fit jump distances with 3 populations. Try using different parameters for `init` and/or `nPop`.\n") 176 | return(p) 177 | } 178 | d5 <- 1 - (coef(fitc3)[4] + coef(fitc3)[5]) 179 | y1 <- coef(fitc3)[4] - coef(fitc3)[4] * exp(-x^2 / (4 * coef(fitc3)[1] * timeRes)) 180 | y2 <- d5 - d5 * exp(-x^2 / (4 * coef(fitc3)[2] * timeRes)) 181 | y3 <- coef(fitc3)[5] - coef(fitc3)[5] * exp(-x^2 / (4 * coef(fitc3)[3] * timeRes)) 182 | fitStr <- paste0("D1 = ",format(round(coef(fitc3)[1],4), nsmall = 4),"\n", 183 | "A1 = ", format(round(coef(fitc3)[4],4), nsmall = 4),"\n", 184 | "D2 = ",format(round(coef(fitc3)[2],4), nsmall = 4),"\n", 185 | "A2 = ", format(round(d5,4), nsmall = 4),"\n", 186 | "D3 = ",format(round(coef(fitc3)[3],4), nsmall = 4),"\n", 187 | "A3 = ", format(round(coef(fitc3)[5],4), nsmall = 4),"\n") 188 | } else { 189 | fit3 <- NULL 190 | try(fit3 <- nls(hdata$counts ~ 191 | D4 * hdata$mid / (2 * D1 * timeRes) * exp(-hdata$mid^2 / (4 * D1 * timeRes)) + 192 | D5 * hdata$mid / (2 * D2 * timeRes) * exp(-hdata$mid^2 / (4 * D2 * timeRes)) + 193 | D6 * hdata$mid / (2 * D3 * timeRes) * exp(-hdata$mid^2 / (4 * D3 * timeRes)), 194 | data = hdata, 195 | start = init), 196 | silent = TRUE); 197 | if(is.null(fit3)) { 198 | cat("Failed to fit jump distances with 3 populations. Try using different parameters for `init` and/or `nPop`.\n") 199 | return(p) 200 | } 201 | y1 <- (coef(fit3)[[4]]) * x / (2 * (coef(fit3)[[1]]) * timeRes) * exp(-x^2 / (4 * (coef(fit3)[[1]]) * timeRes)) 202 | y2 <- (coef(fit3)[[5]]) * x / (2 * (coef(fit3)[[2]]) * timeRes) * exp(-x^2 / (4 * (coef(fit3)[[2]]) * timeRes)) 203 | y3 <- (coef(fit3)[[6]]) * x / (2 * (coef(fit3)[[3]]) * timeRes) * exp(-x^2 / (4 * (coef(fit3)[[3]]) * timeRes)) 204 | fitStr <- paste0("D1 = ",format(round(coef(fit3)[1],4), nsmall = 4),"\n", 205 | "A1 = ", format(round(coef(fit3)[4],4), nsmall = 4),"\n", 206 | "D2 = ",format(round(coef(fit3)[2],4), nsmall = 4),"\n", 207 | "A2 = ", format(round(coef(fit3)[5],4), nsmall = 4),"\n", 208 | "D3 = ",format(round(coef(fit3)[3],4), nsmall = 4),"\n", 209 | "A3 = ", format(round(coef(fit3)[6],4), nsmall = 4)) 210 | } 211 | y <- y1 + y2 + y3 212 | fitdf <- data.frame(x = x, y = y, y1 = y1, y2 = y2, y3 = y3) 213 | } 214 | # first melt so that we can plot all fit curves regardless of how many there are 215 | fitdf <- melt(fitdf, id.vars="x") 216 | # add fit lines to ggplot 217 | p <- p + geom_line(data = fitdf, aes(x = x, y = value, col = variable)) 218 | # add fitting coefficients 219 | if(mode == "ECDF") { 220 | p <- p + geom_text(aes(label = fitStr, x = 0, y = Inf), size = 2, hjust = 0, vjust = 1, check_overlap = TRUE) 221 | p <- p + geom_text(aes(label = paste(timeRes,units[2]), x = 0, y = Inf), size = 2, hjust = 1, vjust = 0, check_overlap = TRUE) 222 | } else { 223 | p <- p + geom_text(aes(label = fitStr, x = 0, y = Inf), size = 2, hjust = 0, vjust = 1, check_overlap = TRUE) 224 | p <- p + geom_text(aes(label = paste(timeRes,units[2]), x = Inf, y = 0), size = 2, hjust = 1, vjust = 1, check_overlap = TRUE) 225 | } 226 | 227 | return(p) 228 | } 229 | -------------------------------------------------------------------------------- /R/gt_functions.R: -------------------------------------------------------------------------------- 1 | #' Compare ground truth datasets 2 | #' 3 | #' Requires Ground Truth csv files to be organised into subfolders named according to the condition. 4 | #' Outputs are saved to `Output/Plots/GT/` in the working directory. 5 | #' 6 | #' @param path string, filepath to directory equivalent to Data in compareDatasets() 7 | #' @param xyscale numeric, pixel size (ground truth data is in pixel units) 8 | #' @param xyunit string, spatial units 9 | #' @param tscale numeric, frame interval (ground truth data is in frames) 10 | #' @param tunit string, temporal units 11 | #' @param ... pass additional parameters to modify the defaults (N, short, deltaT, mode, nPop, init, timeRes, breaks, radius) 12 | #' 13 | #' @return multiple pdf reports 14 | #' @export 15 | compareGTDatasets <- function(path = NULL, xyscale = 0.04, xyunit = "um", tscale = 0.06, tunit = "s", ...) { 16 | 17 | condition <- value <- dataid <- cumulative_distance <- track_duration <- mean_intensity <- NULL 18 | 19 | if(is.null(path)) { 20 | # there is no cross-platform way to safely choose directory 21 | cat("Please organise your ground truth csv files in a folder and use the path as an argument\r") 22 | return(-1) 23 | } else { 24 | datadir <- path 25 | } 26 | 27 | # ellipsis processing 28 | l <- NULL 29 | l <- list(...) 30 | l <- processEllipsis(l) 31 | 32 | # loop through condition folders within data folder 33 | condFolderNames <- list.dirs(path = datadir, recursive = FALSE) 34 | # break if there were no folders in Data directory 35 | if(identical(condFolderNames, character(0)) == TRUE) { 36 | return(-1) 37 | } 38 | 39 | for(i in 1:length(condFolderNames)) { 40 | condFolderPath <- condFolderNames[i] 41 | condFolderName <- basename(condFolderPath) 42 | allTrackMateFiles <- list.files(condFolderPath, pattern = "*.csv") 43 | # skip if there were no csv files in this folder 44 | if(identical(allTrackMateFiles, character(0)) == TRUE) { 45 | next 46 | } 47 | calibrate <- TRUE 48 | 49 | cat(paste0("\n","Processing ",condFolderName,"\n")) 50 | for(j in 1:length(allTrackMateFiles)) { 51 | fileName <- allTrackMateFiles[j] 52 | thisFilePath <- paste0(condFolderPath, "/", fileName) 53 | # read dataset 54 | tmObj <- readGTFile(thisFilePath) 55 | # scale dataset if required 56 | if(calibrate) { 57 | tmObj <- correctTrackMateData(dataList = tmObj, xyscalar = xyscale, tscalar = tscale, xyunit = xyunit, tunit = tunit) 58 | } 59 | tmDF <- tmObj[[1]] 60 | calibrationDF <- tmObj[[2]] 61 | # take the units 62 | units <- calibrationDF$unit[1:2] 63 | # we need to combine data frames 64 | # first add a column to id the data 65 | thisdataid <- paste0(condFolderName,"_",as.character(j)) 66 | tmDF$dataid <- thisdataid 67 | # calculate MSD 68 | msdObj <- calculateMSD(tmDF, N = 3, short = 8) 69 | msdDF <- msdObj[[1]] 70 | alphaDF <- msdObj[[2]] 71 | deeDF <- msdObj[[3]] 72 | # same here, combine msd summary and alpha summary 73 | msdDF$dataid <- thisdataid 74 | alphaDF$dataid <- thisdataid 75 | deeDF$dataid <- thisdataid 76 | # jump distance calc with deltaT of 1 77 | deltaT <- 1 78 | jdObj <- calculateJD(dataList = tmObj, deltaT = l$deltaT, nPop = l$nPop, mode = l$mode, init = l$init, timeRes = l$timeRes, breaks = l$breaks) 79 | jdDF <- jdObj[[1]] 80 | jdDF$dataid <- thisdataid 81 | timeRes <- jdObj[[2]] 82 | jdObj <- list(jdDF,timeRes) 83 | # track density with a radius of 1.5 units 84 | tdDF <- calculateTrackDensity(dataList = tmObj, radius = l$radius) 85 | tdDF$dataid <- thisdataid 86 | # fractal dimension 87 | fdDF <- calculateFD(dataList = tmObj) 88 | fdDF$dataid <- thisdataid 89 | # now if it's the first one make the big dataframe, otherwise add df to bigdf 90 | if(j == 1) { 91 | bigtm <- tmDF 92 | bigmsd <- msdDF 93 | bigdee <- deeDF 94 | bigalpha <- alphaDF 95 | bigjd <- jdDF 96 | bigtd <- tdDF 97 | bigfd <- fdDF 98 | } else { 99 | bigtm <- rbind(bigtm,tmDF) 100 | bigmsd <- rbind(bigmsd,msdDF) 101 | bigdee <- rbind(bigdee,deeDF) 102 | bigalpha <- rbind(bigalpha,alphaDF) 103 | bigjd <- rbind(bigjd,jdDF) 104 | bigtd <- rbind(bigtd,tdDF) 105 | bigfd <- rbind(bigfd,fdDF) 106 | } 107 | 108 | # create the report for this dataset 109 | fileName <- tools::file_path_sans_ext(basename(thisFilePath)) 110 | both <- makeSummaryReport(tmList = tmObj, msdList = msdObj, jumpList = jdObj, tddf = tdDF, fddf = fdDF, 111 | titleStr = condFolderName, subStr = fileName, auto = TRUE, summary = FALSE, 112 | msdplot = l$msdplot) 113 | p <- both[[1]] 114 | destinationDir <- paste0("Output/Plots/GT/", condFolderName) 115 | setupOutputPath(destinationDir) 116 | filePath <- paste0(destinationDir, "/report_",as.character(j),".pdf") 117 | ggsave(filePath, plot = p, width = 25, height = 19, units = "cm") 118 | # retrieve other data 119 | df_report <- both[[2]] 120 | df_report$condition <- condFolderName 121 | df_report$dataid <- thisdataid 122 | if(i == 1 & j == 1) { 123 | megareport <- df_report 124 | } else if(!exists("megareport")) { 125 | megareport <- df_report 126 | } else { 127 | megareport <- rbind(megareport,df_report) 128 | } 129 | } 130 | bigtmObj <- list(bigtm,calibrationDF) 131 | bigmsdObj <- list(bigmsd,bigalpha,bigdee) 132 | bigjdObj <- list(bigjd,timeRes) 133 | # now we have our combined dataset we can make a summary 134 | # note we use the timeRes of the final dataset; so it is suitable for only when all files have the same calibration 135 | summaryObj <- makeSummaryReport(tmList = bigtmObj, msdList = bigmsdObj, jumpList = bigjdObj, tddf = bigtd, fddf = bigfd, 136 | titleStr = condFolderName, subStr = "Summary", auto = TRUE, summary = TRUE, 137 | msdplot = l$msdplot) 138 | p <- summaryObj[[1]] 139 | destinationDir <- paste0("Output/Plots/GT/", condFolderName) 140 | filePath <- paste0(destinationDir, "/combined.pdf") 141 | ggsave(filePath, plot = p, width = 25, height = 19, units = "cm") 142 | # save data as csv 143 | destinationDir <- paste0("Output/Data/GT/", condFolderName) 144 | setupOutputPath(destinationDir) 145 | # save each dataset-level data 146 | write.csv(bigtm, paste0(destinationDir, "/allTM.csv"), row.names = FALSE) 147 | write.csv(bigmsd, paste0(destinationDir, "/allMSD.csv"), row.names = FALSE) 148 | write.csv(bigjd, paste0(destinationDir, "/allJD.csv"), row.names = FALSE) 149 | write.csv(bigfd, paste0(destinationDir, "/allFD.csv"), row.names = FALSE) 150 | # mega data frame of msd averages per dataset; alpha values, track density, speed/duration/distance, intensity by trace/dataid/condition 151 | msdSummary <- summaryObj[[2]] 152 | msdSummary$condition <- condFolderName 153 | bigspeed <- bigtm %>% 154 | group_by(dataid, trace) %>% 155 | summarise(cumdist = max(cumulative_distance), cumtime = max(track_duration), intensity = max(mean_intensity)) 156 | bigspeed$speed <- bigspeed$cumdist / bigspeed$cumtime 157 | bigspeed$condition <- condFolderName 158 | if(i == 1 | !exists("megamsd")) { 159 | megamsd <- msdSummary 160 | megaalpha <- bigalpha 161 | megadee <- bigdee 162 | megatd <- bigtd 163 | megaspeed <- bigspeed 164 | megafd <- bigfd 165 | } else { 166 | megamsd <- rbind(megamsd,msdSummary) 167 | megaalpha <- rbind(megaalpha,bigalpha) 168 | megadee <- rbind(megadee,bigdee) 169 | megatd <- rbind(megatd,bigtd) 170 | megaspeed <- rbind(megaspeed,bigspeed) 171 | megafd <- rbind(megafd,bigfd) 172 | } 173 | } 174 | 175 | # save summary data as csv 176 | destinationDir <- "Output/Data/GT" 177 | write.csv(megamsd, paste0(destinationDir, "/allMSDCurves.csv"), row.names = FALSE) 178 | write.csv(megareport, paste0(destinationDir, "/allComparison.csv"), row.names = FALSE) 179 | 180 | # for alpha values, estimator of D, track density, peed/duration/distance, intensity by trace/dataid/condition we must combine into one 181 | # set the name of dee to estdee 182 | names(megadee)[names(megadee) == "dee"] <- "estdee" 183 | megatrace <- Reduce(mergeDataFramesForExport, list(megaalpha, megadee, megatd, megaspeed, megafd)) 184 | write.csv(megatrace, paste0(destinationDir, "/allTraceData.csv"), row.names = FALSE) 185 | 186 | # generate the comparison plots and save 187 | p <- makeComparison(df = megareport, msddf = megamsd, units = units, msdplot = l$msdplot) 188 | destinationDir <- "Output/Plots/GT" 189 | filePath <- paste0(destinationDir, "/comparison.pdf") 190 | ggsave(filePath, plot = p, width = 19, height = 19, units = "cm") 191 | } 192 | 193 | 194 | #' Read a ground truth csv file 195 | #' 196 | #' Ground truth csv file has four columns TrackID,x,y,frame 197 | #' this function reads the data and formats in a way that is equivalent to the way that TrackMateR reads a TrackMate XML file. 198 | #' 199 | #' @param path string, filepath to ground truth csv file 200 | #' 201 | #' @return list of two data frames 202 | #' @export 203 | readGTFile <- function(path) { 204 | 205 | daten <- read.csv(path, header = TRUE) 206 | # gt data is TrackID,x,y,frame - trace is required and must be character 207 | daten$trace <- as.character(daten$TrackID) 208 | daten$t <- daten$frame 209 | 210 | # construct calibration file 211 | calibrationDF <- data.frame(value = c(1,1,199,199), 212 | unit = c("pixel","frame","widthpixels","heightpixels")) 213 | 214 | # displacement, cumulative distance and duration 215 | displacement <- sqrt(c(0,diff(daten$x))^2 + c(0,diff(daten$y))^2) 216 | cumdist <- numeric() 217 | cumdist[1] <- 0 218 | dur <- numeric() 219 | dur[1] <- 0 220 | startdur <- daten$t[1] 221 | 222 | for (i in 2:nrow(daten)){ 223 | if(daten$trace[i] == daten$trace[i-1]) { 224 | cumdist[i] <- cumdist[i-1] + displacement[i] 225 | } else { 226 | displacement[i] <- 0 227 | cumdist[i] <- 0 228 | startdur <- daten$t[i] 229 | } 230 | dur[i] <- daten$t[i] - startdur 231 | } 232 | daten$displacement <- displacement 233 | daten$cumulative_distance <- cumdist 234 | daten$track_duration <- dur 235 | # if there is no intensity, set it to 1 236 | if(!"mean_intensity" %in% daten) { 237 | daten$mean_intensity <- rep.int(1,nrow(daten)) 238 | } 239 | 240 | # it is possible that xy coords lie outside the image(!) 241 | # we can detect xy coords that are less than 0,0 and then use this information to offset *all* coords by this 242 | # this is necessary because later code relies on the origin 243 | minx <- min(daten$x) 244 | miny <- min(daten$y) 245 | if(minx < 0) { 246 | daten$x <- daten$x - minx 247 | } 248 | if(miny < 0) { 249 | daten$y <- daten$y - miny 250 | } 251 | # now we need to redefine the size of the "image" because xy coords may lie outside, or they may now lie outside after offsetting 252 | maxx <- max(daten$x) 253 | maxy <- max(daten$y) 254 | if(maxx > calibrationDF[3,1]) { 255 | calibrationDF[3,1] <- ceiling(maxx) 256 | } 257 | if(maxy > calibrationDF[4,1]) { 258 | calibrationDF[4,1] <- ceiling(maxy) 259 | } 260 | 261 | # format as tmObj 262 | dfList <- list(daten,calibrationDF) 263 | 264 | return(dfList) 265 | } 266 | -------------------------------------------------------------------------------- /R/makeComparison.R: -------------------------------------------------------------------------------- 1 | #' Make Comparison Plots 2 | #' 3 | #' A series of ggplots to compare between conditions. 4 | #' Called from `compareDatasets()` this function generates plots using summary data of datasets, per condition. 5 | #' 6 | #' @param df data frame called megareport 7 | #' @param msddf data frame of msd averages per dataset 8 | #' @param units character vector of space and time units for axes 9 | #' @param msdplot keyword used to specify msdplot axes, either "loglog", "linlin" (default), "loglin" or "linlog" 10 | #' @param titleStr string used as the title for the report 11 | #' @param subStr string used as the subtitle for the report 12 | #' @return patchwork ggplot 13 | #' @export 14 | makeComparison <- function (df, msddf, units = c("um","s"), msdplot = "linlin", titleStr = "Comparison", subStr = NULL) { 15 | condition <- dee <- neighbours <- speed <- fd <- width <- intensity <- duration <- NULL 16 | 17 | oldw <- getOption("warn") 18 | options(warn = -1) 19 | 20 | symlim <- findLog2YAxisLimits(df$alpha) 21 | 22 | # plot alpha comparison 23 | p_alpha <- ggplot(data = df, aes(x = condition, y = alpha, colour = condition)) + 24 | geom_boxplot(colour = "grey", outlier.shape = NA) + 25 | geom_sina(alpha = 0.5, stroke = 0) + 26 | geom_hline(yintercept = 1, linetype = "dashed", colour = "grey") + 27 | scale_y_continuous(limits = symlim, trans = "log2") + 28 | guides(x = guide_axis(angle = 90)) + 29 | labs(x = "", y = "Mean alpha") + 30 | theme_classic() + 31 | theme(legend.position = "none") 32 | 33 | # plot speed comparison 34 | p_speed <- ggplot(data = df, aes(x = condition, y = speed, colour = condition)) + 35 | geom_boxplot(colour = "grey", outlier.shape = NA) + 36 | geom_sina(alpha = 0.5, stroke = 0) + 37 | ylim(c(0,NA)) + 38 | guides(x = guide_axis(angle = 90)) + 39 | labs(x = "", y = paste0("Mean speed (",units[1],"/",units[2],")")) + 40 | theme_classic() + 41 | theme(legend.position = "none") 42 | 43 | # plot intensity 44 | p_intensity <- ggplot(data = df, aes(x = condition, y = intensity, colour = condition)) + 45 | geom_boxplot(colour = "grey", outlier.shape = NA) + 46 | geom_sina(alpha = 0.5, stroke = 0) + 47 | ylim(c(0,NA)) + 48 | guides(x = guide_axis(angle = 90)) + 49 | labs(x = "", y = "Median intensity (AU)") + 50 | theme_classic() + 51 | theme(legend.position = "none") 52 | 53 | # plot duration 54 | p_duration <- ggplot(data = df, aes(x = condition, y = duration, colour = condition)) + 55 | geom_boxplot(colour = "grey", outlier.shape = NA) + 56 | geom_sina(alpha = 0.5, stroke = 0) + 57 | ylim(c(0,NA)) + 58 | guides(x = guide_axis(angle = 90)) + 59 | labs(x = "", y = paste0("Median duration (",units[2],")")) + 60 | theme_classic() + 61 | theme(legend.position = "none") 62 | 63 | # plot diffusion constant 64 | p_dee <- ggplot(data = df, aes(x = condition, y = dee, colour = condition)) + 65 | geom_boxplot(colour = "grey", outlier.shape = NA) + 66 | geom_sina(alpha = 0.5, stroke = 0) + 67 | ylim(c(0,NA)) + 68 | guides(x = guide_axis(angle = 90)) + 69 | labs(x = "", y = substitute(paste("Diffusion coefficient (",mm^2,"/",nn,")"), list(mm = units[1], nn = units [2]))) + 70 | theme_classic() + 71 | theme(legend.position = "none") 72 | 73 | # plot neighbour density 74 | p_density <- ggplot(data = df, aes(x = condition, y = neighbours, colour = condition)) + 75 | geom_boxplot(colour = "grey", outlier.shape = NA) + 76 | geom_sina(alpha = 0.5, stroke = 0) + 77 | ylim(c(0,NA)) + 78 | guides(x = guide_axis(angle = 90)) + 79 | labs(x = "", y = paste0("Median track density")) + 80 | theme_classic() + 81 | theme(legend.position = "none") 82 | 83 | # plot fractal dimension 84 | p_fd <- ggplot(data = df, aes(x = condition, y = fd, colour = condition)) + 85 | geom_boxplot(colour = "grey", outlier.shape = NA) + 86 | geom_sina(alpha = 0.5, stroke = 0) + 87 | ylim(c(0,NA)) + 88 | guides(x = guide_axis(angle = 90)) + 89 | labs(x = "", y = paste0("Median fractal dimension")) + 90 | theme_classic() + 91 | theme(legend.position = "none") 92 | 93 | # plot width of track summary 94 | p_width <- ggplot(data = df, aes(x = condition, y = width, colour = condition)) + 95 | geom_boxplot(colour = "grey", outlier.shape = NA) + 96 | geom_sina(alpha = 0.5, stroke = 0) + 97 | ylim(c(0,NA)) + 98 | guides(x = guide_axis(angle = 90)) + 99 | labs(x = "", y = paste0("Median track width")) + 100 | theme_classic() + 101 | theme(legend.position = "none") 102 | 103 | # plot msd summary curves altogether 104 | p_msd <- ggplot(data = msddf, aes(x = t, y = mean, fill = condition)) + 105 | geom_ribbon(aes(ymin = mean - sd, ymax = mean + sd), alpha = 0.2) + 106 | geom_line(aes(colour = condition), size = 1) 107 | if(msdplot == "linlin") { 108 | p_msd <- p_msd + ylim(0,NA) + xlim(0,NA) 109 | } else if(msdplot == "loglin") { 110 | p_msd <- p_msd + scale_y_log10() + xlim(0,NA) 111 | } else if(msdplot == "linlog") { 112 | p_msd <- p_msd + ylim(0,NA) + scale_x_log10() 113 | } else if(msdplot == "loglog") { 114 | p_msd <- p_msd + scale_y_log10() + scale_x_log10() 115 | } else { 116 | # no scaling applied if any other string is used 117 | } 118 | p_msd <- p_msd + labs(x = "Time (s)", y = "MSD") + 119 | theme_classic() + 120 | theme(legend.position = "none") 121 | 122 | r_report <- (p_alpha + p_speed + p_intensity + p_duration + p_dee + p_fd + p_width + p_density + p_msd) + plot_layout(ncol = 3, nrow = 3) 123 | r_report <- r_report + plot_annotation(title = titleStr, subtitle = subStr) 124 | 125 | options(warn = oldw) 126 | 127 | return(r_report) 128 | } 129 | -------------------------------------------------------------------------------- /R/makeSummaryReport.R: -------------------------------------------------------------------------------- 1 | #' Make Summary Report 2 | #' 3 | #' Generate several plots to visualise TrackMate data and generate a report. 4 | #' The use of um is because ggsave does not currently save unicode to PDF reliably. 5 | #' The code is switchable to accommodate making a "report" (one dataset) or a "summary" (several related datasets combined) 6 | #' 7 | #' @param tmList list of trackmate data and calibration 8 | #' @param msdList MSD summary and alpha list = output from calculateMSD() 9 | #' @param jumpList list of a data frame of jump data and a variable to be passed to timeRes 10 | #' @param tddf data frame of track density data 11 | #' @param fddf data frame of fractal dimension data 12 | #' @param titleStr string used as the title for the report 13 | #' @param subStr string used as the subtitle for the report 14 | #' @param auto boolean which selects for returning the patchwork report (FALSE) or a list of the patchwork report and a data frame of summary (TRUE) 15 | #' @param summary boolean which selects for report (FALSE) or summary (TRUE) 16 | #' @param ... additional arguments passed to methods 17 | #' @examples 18 | #' xmlPath <- system.file("extdata", "ExampleTrackMateData.xml", package="TrackMateR") 19 | #' tmObj <- readTrackMateXML(XMLpath = xmlPath) 20 | #' tmObj <- correctTrackMateData(tmObj, xyscalar = 0.04) 21 | #' tmDF <- tmObj[[1]] 22 | #' calibrationDF <- tmObj[[2]] 23 | #' msdObj <- calculateMSD(df = tmDF, method = "ensemble", N = 3, short = 8) 24 | #' jdObj <- calculateJD(dataList = tmObj, deltaT = 1) 25 | #' tdDF <- calculateTrackDensity(dataList = tmObj, radius = 1.5) 26 | #' fdDF <- calculateFD(dataList = tmObj) 27 | #' fileName <- tools::file_path_sans_ext(basename(xmlPath)) 28 | #' reportObj <- makeSummaryReport(tmList = tmObj, msdList = msdObj, jumpList = jdObj, 29 | #' tddf = tdDF, fddf = fdDF, titleStr = "Report", subStr = fileName, auto = TRUE) 30 | #' @return patchwork ggplot or a list of patchwork ggplot and data frame of summary data 31 | #' @export 32 | 33 | makeSummaryReport <- function(tmList, msdList, jumpList, tddf, fddf, titleStr = "", subStr = "", auto = FALSE, summary = FALSE, ...) { 34 | oldw <- getOption("warn") 35 | options(warn = -1) 36 | 37 | x <- y <- displacement <- track_duration <- cumulative_distance <- speed <- dataid <- density <- NULL 38 | xstr <- ystr <- alphaValue <- NULL 39 | 40 | l <- NULL 41 | l <- list(...) 42 | 43 | # get ready for plotting 44 | df <- tmList[[1]] 45 | calibration <- tmList[[2]] 46 | units <- calibration$unit[1:2] 47 | if(summary) { 48 | ndata <- length(unique(df$dataid)) 49 | alphaLevel <- ifelse(ndata < 4, 0.5, ifelse(ndata < 8, 0.25,0.1)) 50 | } 51 | 52 | # make msd plot 53 | msddf <- msdList[[1]] 54 | xmsd <- ymsd <- FALSE 55 | msdplot <- l$msdplot 56 | if(is.null(msdplot)) { 57 | msdplot == "linlin" 58 | } else if(msdplot == "loglog") { 59 | xmsd <- ymsd <- TRUE 60 | } else if(msdplot == "linlog") { 61 | xmsd <- TRUE 62 | } else if(msdplot == "loglin") { 63 | ymsd <- TRUE 64 | } else {} 65 | if(summary){ 66 | msdreturn <- plot_tm_NMSD(msddf, xlog = xmsd, ylog = ymsd, auto = TRUE) 67 | p_msd <- msdreturn[[1]] 68 | msdSummary <- msdreturn[[2]] 69 | } else { 70 | msdreturn <- plot_tm_MSD(msddf, units, xlog = xmsd, ylog = ymsd, auto = TRUE) 71 | p_msd <- msdreturn[[1]] 72 | dee <- msdreturn[[2]] 73 | } 74 | 75 | # plot all tracks colour coded by trace number (or by dataset) 76 | p_allTracks <- plot_tm_allTracks(input = df, summary = summary, alphaLevel = alphaLevel) 77 | 78 | # plot displacement over time 79 | p_displacementOverTime <- plot_tm_displacementOverTime(input = tmList, summary = summary) 80 | 81 | # plot cumulative distance over time 82 | p_cumdistOverTime <- plot_tm_cumdistOverTime(input = tmList, summary = summary, alphaLevel = alphaLevel) 83 | 84 | # ggplot histogram of displacements 85 | if(auto == TRUE & summary == FALSE) { 86 | dispObj <- plot_tm_displacementHist(input = tmList, auto = auto) 87 | p_displacementHist <- dispObj[[1]] 88 | median_disp <- dispObj[[2]] 89 | } else { 90 | p_displacementHist <- plot_tm_displacementHist(input = tmList, auto = FALSE) 91 | } 92 | 93 | # ggplot histogram of intensities 94 | if(auto == TRUE & summary == FALSE) { 95 | intObj <- plot_tm_intensityHist(input = tmList, auto = auto) 96 | p_intensityHist <- intObj[[1]] 97 | median_int <- intObj[[2]] 98 | } else { 99 | p_intensityHist <- plot_tm_intensityHist(input = tmList, auto = FALSE) 100 | } 101 | 102 | # ggplot histogram of intensities 103 | if(auto == TRUE & summary == FALSE) { 104 | durObj <- plot_tm_durationHist(input = tmList, auto = auto) 105 | p_durationHist <- durObj[[1]] 106 | median_dur <- durObj[[2]] 107 | } else { 108 | p_durationHist <- plot_tm_durationHist(input = tmList, auto = FALSE) 109 | } 110 | 111 | # alpha distribution of traces 112 | alphas <- msdList[[2]] 113 | alphas <- na.omit(alphas) 114 | # within a sensible range (log 2) 115 | identify <- alphas$alpha <= 4 & alphas$alpha >= -4 116 | # we will take log2 117 | # alphas <- data.frame(alpha = alphas[identify,]) 118 | alphas <- subset(alphas, identify) 119 | # convert back to real numbers 120 | median_alpha <- 2^(median(alphas$alpha, na.rm = TRUE)) 121 | 122 | p_alpha <- plot_tm_alpha(df = alphas, median_alpha = median_alpha) 123 | 124 | # find the median D value 125 | median_dee <- median(alphas$dee, na.rm = TRUE) 126 | labelstr <- substitute(paste("D (",mm^2,"/",nn,")"), list(mm = units[1], nn = units [2])) 127 | 128 | # D distribution of traces 129 | p_dee <- plot_tm_dee(df = alphas, median_dee = median_dee, xstr = labelstr) 130 | 131 | # distribution of estimator of D 132 | cves <- msdList[[3]] 133 | cves <- na.omit(cves) 134 | median_estdee <- median(cves$dee, na.rm = TRUE) 135 | labelstr <- substitute(paste("Estimator D (",mm^2,"/",nn,")"), list(mm = units[1], nn = units [2])) 136 | p_estdee <- plot_tm_dee(df = cves, median_dee = median_estdee, xstr = labelstr) 137 | 138 | # make a plot of average speed per track 139 | if(auto == TRUE & summary == FALSE) { 140 | speedObj <- plot_tm_speed(input = tmList, summary = summary, auto = auto) 141 | p_speed <- speedObj[[1]] 142 | median_speed <- speedObj[[2]] 143 | } else { 144 | p_speed <- plot_tm_speed(input = tmList, summary = summary, auto = FALSE) 145 | } 146 | 147 | # make a plot of jump distance distribution 148 | p_jump <- fittingJD(jumpList) 149 | 150 | # calculate neighbours within 1.5 units 151 | if(auto == TRUE & summary == FALSE) { 152 | neighbourObj <- plot_tm_neighbours(df = tddf, auto = auto) 153 | p_neighbours <- neighbourObj[[1]] 154 | median_density <- neighbourObj[[2]] 155 | } else { 156 | p_neighbours <- plot_tm_neighbours(df = tddf, auto = FALSE) 157 | } 158 | 159 | # fractal dimension and width plots 160 | if(auto == TRUE & summary == FALSE) { 161 | fdObj <- plot_tm_fd(df = fddf, auto = auto) 162 | p_fd <- fdObj[[1]] 163 | median_fd <- fdObj[[2]] 164 | widthObj <- plot_tm_width(df = fddf, units = units, auto = auto) 165 | p_width <- widthObj[[1]] 166 | median_width <- widthObj[[2]] 167 | } else { 168 | p_fd <- plot_tm_fd(df = fddf, auto = FALSE) 169 | p_width <- plot_tm_width(df = fddf, units = units, auto = FALSE) 170 | } 171 | 172 | # substitute blank elements if any df was null 173 | if(is.null(p_msd)) { 174 | p_msd <- plot_spacer() 175 | dee <- NA 176 | } 177 | if(is.null(p_dee)) { 178 | p_dee <- plot_spacer() 179 | } 180 | if(is.null(p_alpha)) { 181 | p_alpha <- plot_spacer() 182 | median_alpha <- NA 183 | } 184 | if(is.null(p_estdee)) { 185 | p_estdee <- plot_spacer() 186 | } 187 | if(is.null(p_jump)) { 188 | p_jump <- plot_spacer() 189 | } 190 | if(is.null(p_neighbours)) { 191 | p_neighbours <- plot_spacer() 192 | median_density <- NA 193 | } 194 | if(is.null(p_fd)) { 195 | p_fd <- plot_spacer() 196 | median_fd <- NA 197 | } 198 | if(is.null(p_width)) { 199 | p_width <- plot_spacer() 200 | median_width <- NA 201 | } 202 | 203 | # make the report (patchwork of ggplots) 204 | design <- " 205 | 11236 206 | 11457 207 | " 208 | toprow <- p_allTracks + 209 | p_displacementOverTime + p_displacementHist + p_durationHist + 210 | p_cumdistOverTime + p_speed + p_intensityHist + 211 | plot_layout(design = design) 212 | bottomrow <- p_msd + p_dee + p_alpha + p_estdee + p_jump + p_neighbours + p_fd + p_width + plot_layout(ncol = 4, nrow = 2) 213 | r_report <- toprow / bottomrow 214 | r_report <- r_report + plot_annotation(title = titleStr, subtitle = subStr) 215 | 216 | if(auto == TRUE & summary == FALSE) { 217 | # make a multi-column object and pass directly back 218 | df_report <- data.frame(alpha = median_alpha, 219 | speed = median_speed, 220 | intensity = median_int, 221 | duration = median_dur, 222 | dee = dee, # D that is passed here is the value from MSD fit of all data not median D from traces 223 | displacement = median_disp, 224 | neighbours = median_density, 225 | fd = median_fd, 226 | width = median_width) 227 | returnList <- list(r_report,df_report) 228 | return(returnList) 229 | } else if(auto == TRUE & summary == TRUE) { 230 | returnList <- list(r_report,msdSummary) 231 | return(returnList) 232 | } else { 233 | return(r_report) 234 | } 235 | 236 | options(warn = oldw) 237 | 238 | } 239 | -------------------------------------------------------------------------------- /R/readTrackMateXML.r: -------------------------------------------------------------------------------- 1 | #' Read TrackMate XML output files. 2 | #' 3 | #' Produces a data frame of all spots from filtered tracks, ordered by track number. 4 | #' A warning is generated if the scaling is in pixels rather than real units. 5 | #' 6 | #' @param XMLpath path to the xml file 7 | #' @return list of two data frames 8 | #' @examples 9 | #' xmlPath <- system.file("extdata", "ExampleTrackMateData.xml", package="TrackMateR") 10 | #' tmObj <- readTrackMateXML(XMLpath = xmlPath) 11 | #' # get the track data in a data frame 12 | #' tmDF <- tmObj[[1]] 13 | #' # get the calibration data in a data frame 14 | #' calibrationDF <- tmObj[[2]] 15 | #' @export 16 | 17 | readTrackMateXML<- function(XMLpath){ 18 | 19 | # get necessary XMLNodeSet 20 | e <- xmlParse(XMLpath) 21 | track <- getNodeSet(e, "//Track") 22 | if(length(track) == 0) { 23 | cat("No tracks found in XML file\n") 24 | return(NULL) 25 | } 26 | filtered <- getNodeSet(e, "//TrackID") 27 | subdoc <- getNodeSet(e,"//AllSpots//SpotsInFrame//Spot") 28 | sublist <- getNodeSet(e,"//FeatureDeclarations//SpotFeatures//Feature/@feature") 29 | attrName <- c("name",unlist(sublist)) 30 | # what are the units? 31 | attrList <- c("spatialunits","timeunits") 32 | unitVec <- sapply(attrList, function(x) xpathSApply(e, "//Model", xmlGetAttr, x)) 33 | unitVec <- c(unitVec,"widthpixels","heightpixels","ntraces","maxframes") 34 | attrList <- c("pixelwidth","timeinterval","width","height") 35 | valueVec <- sapply(attrList, function(x) xpathSApply(e, "//ImageData", xmlGetAttr, x)) 36 | calibrationDF <- data.frame(value = c(as.numeric(valueVec),0,0), 37 | unit = unitVec) 38 | # convert the width and height of the image from pixels (0-based so minus 1 from wdth/height) to whatever units the TrackMate file uses 39 | calibrationDF[3:4,1] <- (calibrationDF[3:4,1] - 1) * calibrationDF[1,1] 40 | # use s for seconds 41 | calibrationDF[2,2] <- ifelse(calibrationDF[2,2] == "sec", "s", calibrationDF[2,2]) 42 | # readout what the units were and warn if spatial units are pixels 43 | cat("Units are: ",calibrationDF[1,1],calibrationDF[1,2],"and",calibrationDF[2,1],calibrationDF[2,2],"\n") 44 | if(unitVec[1] == "pixel") { 45 | cat("Spatial units are in pixels - consider transforming to real units\n") 46 | } 47 | 48 | # multicore processing 49 | numCores <- parallelly::availableCores() 50 | 51 | if (.Platform[["OS.type"]] == "windows") { 52 | ## PSOCK-based parallel processing 53 | cl <- parallel::makeCluster(numCores) 54 | on.exit(parallel::stopCluster(cl)) 55 | registerDoParallel(cl = cl) 56 | } else { 57 | ## Forked parallel processing 58 | registerDoParallel(cores = numCores) 59 | } 60 | 61 | # perform parallel read 62 | # test if we are running on windows 63 | if (.Platform[["OS.type"]] == "windows") { 64 | cat("Collecting spot data...\n") 65 | dtf <- as.data.frame(foreach(i = 1:length(attrName), .packages = c("foreach","XML"), .combine = cbind) %do% { 66 | sapply(subdoc, xmlGetAttr, attrName[i]) 67 | }) 68 | } else { 69 | cat(paste0("Collecting spot data. Using ",numCores," cores\n")) 70 | dtf <- as.data.frame(foreach(i = 1:length(attrName), .combine = cbind) %dopar% { 71 | sapply(subdoc, xmlGetAttr, attrName[i]) 72 | }) 73 | } 74 | 75 | for (i in 2:length(attrName)){ 76 | suppressWarnings(dtf[,i] <- as.numeric(as.character(dtf[,i]))) 77 | } 78 | # more R-like headers 79 | headerNames <- tolower(attrName) 80 | # remove _CH1 or whatever from headers 81 | headerNames <- gsub("\\wCH\\d$","",headerNames,ignore.case = T) 82 | # change x y z t 83 | headerNames <- gsub("^position\\w","",headerNames,ignore.case = T) 84 | names(dtf) <- headerNames 85 | 86 | cat("Matching track data...\n") 87 | 88 | # trace is an alternative name for track 89 | IDtrace <- data.frame(name = NA, trace = NA, displacement = NA, speed = NA) 90 | 91 | for (i in seq(along = track)){ 92 | subDoc = xmlDoc(track[[i]]) 93 | IDvec <- unique(c(unlist(xpathApply(subDoc, "//Edge", xmlGetAttr, "SPOT_SOURCE_ID")), 94 | unlist(xpathApply(subDoc, "//Edge", xmlGetAttr, "SPOT_TARGET_ID")))) 95 | traceVec <- rep(sapply(track[i], function(el){xmlGetAttr(el, "TRACK_ID")}), length(IDvec)) 96 | traceDF <- data.frame(name = paste0("ID",IDvec), trace = traceVec) 97 | # now retrieve displacement and speed for target spots in track 98 | targetVec <- unlist(xpathApply(subDoc, "//Edge", xmlGetAttr, "SPOT_TARGET_ID")) 99 | dispVec <- unlist(xpathApply(subDoc, "//Edge", xmlGetAttr, "DISPLACEMENT")) 100 | speedVec <- unlist(xpathApply(subDoc, "//Edge", xmlGetAttr, "SPEED")) 101 | dataDF <- data.frame(name = paste0("ID",targetVec), displacement = as.numeric(dispVec), speed = as.numeric(speedVec)) 102 | # left join (will give NA for the first spot) 103 | allDF <- merge(x = traceDF, y = dataDF, by = "name", all.x = TRUE) 104 | allDF$displacement <- ifelse(is.na(allDF$displacement), 0, allDF$displacement) 105 | allDF$speed <- ifelse(is.na(allDF$speed), 0, allDF$speed) 106 | # grow the dataframe 107 | IDtrace <- rbind(IDtrace, allDF) 108 | } 109 | 110 | # merge track information with spot data 111 | daten <- merge(IDtrace, dtf, by="name") 112 | # now we subset for filtered tracks 113 | FTvec <- sapply(filtered, xmlGetAttr, "TRACK_ID") 114 | daten <- subset(daten, trace %in% FTvec) 115 | # sort final data frame 116 | daten <- daten[order(daten$trace, daten$t),] 117 | 118 | cat("Calculating distances...\n") 119 | 120 | # cumulative distance and duration 121 | cumdist <- numeric() 122 | cumdist[1] <- 0 123 | dur <- numeric() 124 | dur[1] <- 0 125 | startdur <- daten$t[1] 126 | 127 | for (i in 2:nrow(daten)){ 128 | if(daten$trace[i] == daten$trace[i-1]) { 129 | cumdist[i] <- cumdist[i-1] + daten$displacement[i] 130 | }else{ 131 | cumdist[i] <- 0 132 | startdur <- daten$t[i] 133 | } 134 | dur[i] <- daten$t[i] - startdur 135 | } 136 | daten$cumulative_distance <- cumdist 137 | daten$track_duration <- dur 138 | 139 | # users have reported tracks/traces with multiple spots per frame which cannot be processed 140 | # possibly caused by track splitting or merging. Produce warning about this. 141 | b <- daten %>% group_by(trace,frame) %>% 142 | reframe(n = n()) 143 | # because it is not possible to have n = 0 in this column (as it is reframe) we can do 144 | if(sum(b$n) > nrow(b)) { 145 | cat("Warning: Detected multiple spots per frame for one or more tracks.\nTrackMateR will only process single tracks. Subsequent analysis will likely fail!\n") 146 | } 147 | 148 | # it is possible that xy coords lie outside the image(!) 149 | # we can detect xy coords that are less than 0,0 and then use this information to offset *all* coords by this 150 | # this is necessary because later code relies on the origin 151 | minx <- min(daten$x) 152 | miny <- min(daten$y) 153 | if(minx < 0) { 154 | daten$x <- daten$x - minx 155 | } 156 | if(miny < 0) { 157 | daten$y <- daten$y - miny 158 | } 159 | # now we need to redefine the size of the "image" because xy coords may lie outside, or they may now lie outside after offsetting 160 | maxx <- max(daten$x) 161 | maxy <- max(daten$y) 162 | if(maxx > calibrationDF[3,1]) { 163 | calibrationDF[3,1] <- ceiling(maxx) 164 | } 165 | if(maxy > calibrationDF[4,1]) { 166 | calibrationDF[4,1] <- ceiling(maxy) 167 | } 168 | # we need to know how many traces we have and how many frames is the longest one for later 169 | calibrationDF[5,1] <- length(unique(daten$trace)) 170 | calibrationDF[6,1] <- max(daten$track_duration) / calibrationDF[2,1] 171 | 172 | # daten is our dataframe of all data, calibrationDF is the calibration data 173 | dfList <- list(daten,calibrationDF) 174 | 175 | return(dfList) 176 | } 177 | -------------------------------------------------------------------------------- /R/reportDataset.R: -------------------------------------------------------------------------------- 1 | #' Report Dataset 2 | #' 3 | #' This function automates the process of making a report. 4 | #' If more control is needed over the default parameters: 5 | #' a user can generate MSD, jump distance and track density objects from their imported TrackMate data, and make a report manually. 6 | #' 7 | #' @param tmList list of TrackMate data and calibration 8 | #' @param ... pass additional parameters to modify the defaults (N, short, deltaT, mode, nPop, init, timeRes, breaks, radius) 9 | #' @return patchwork ggplot 10 | #' @export 11 | reportDataset <- function(tmList, ...) { 12 | 13 | if(inherits(tmList, "list")) { 14 | tmDF <- tmList[[1]] 15 | calibrationDF <- tmList[[2]] 16 | } else { 17 | cat("Function requires a list of TrackMate data and calibration data\n") 18 | return(NULL) 19 | } 20 | 21 | # ellipsis processing 22 | l <- NULL 23 | l <- list(...) 24 | l <- processEllipsis(l) 25 | 26 | # take the units 27 | units <- calibrationDF$unit[1:2] 28 | # calculate MSD 29 | msdObj <- calculateMSD(tmDF, N = l$N, short = l$short) 30 | msdDF <- msdObj[[1]] 31 | alphaDF <- msdObj[[2]] 32 | # jump distance calc 33 | jdObj <- calculateJD(dataList = tmList, deltaT = l$deltaT, nPop = l$nPop) 34 | # track density with a radius of 1.5 units 35 | tdDF <- calculateTrackDensity(dataList = tmList, radius = l$radius) 36 | # fractal dimension 37 | fdDF <- calculateFD(dataList = tmList) 38 | # create the report for this dataset 39 | p <- makeSummaryReport(tmList = tmList, msdList = msdObj, jumpList = jdObj, tddf = tdDF, fddf = fdDF, titleStr = "Report", subStr = "", auto = FALSE, summary = FALSE) 40 | 41 | return(p) 42 | } 43 | -------------------------------------------------------------------------------- /R/tm_misc.R: -------------------------------------------------------------------------------- 1 | #' Setup Output Path 2 | #' 3 | #' In the working directory, we need to check if a path to a directory exists, and if not create it. 4 | #' Ensures smooth running of the automated functions in TestTrackMateR. 5 | #' 6 | #' @param fpath string containing a path that may or may not exist 7 | #' @return NULL 8 | #' @keywords internal 9 | setupOutputPath <- function(fpath) { 10 | # example "Output/Data/foo/bar" 11 | path_components <- unlist(strsplit(fpath, split = "/")) 12 | 13 | for(i in 1:length(path_components)){ 14 | test_path <- paste(path_components[1:i], collapse = "/") 15 | ifelse(!dir.exists(test_path), dir.create(test_path),FALSE) 16 | } 17 | } 18 | 19 | #' Merge Data Frames for export 20 | #' 21 | #' Properties for individual traces (tracks) from different experiments need to be merged for export. 22 | #' Utility function, not for export. 23 | #' 24 | #' @param x data frame to be coerced 25 | #' @param y data frame to be coerced 26 | #' @return NULL 27 | #' @keywords internal 28 | mergeDataFramesForExport <- function(x, y) { 29 | merge(x, y, by = c("trace", "dataid"), all = TRUE) 30 | } 31 | 32 | #' Process additional parameters 33 | #' 34 | #' Ensure ellipsis parameters have the default values 35 | #' This function is used in two separate workflows, so a single function to edit makes sense. 36 | #' 37 | #' @param input list of ellipsis arguments 38 | #' @return list of arguments adjusted for defaults 39 | #' @keywords internal 40 | processEllipsis <- function(input) { 41 | if (is.null(input$N)) input$N <- 3 42 | if (is.null(input$short)) input$short <- 8 43 | if (is.null(input$deltaT)) input$deltaT <- 1 44 | if (is.null(input$mode)) input$mode <- "ECDF" 45 | if (is.null(input$nPop)) input$nPop <- 2 46 | #if (is.null(input$init)) input$init <- NULL 47 | if (is.null(input$timeRes)) input$timeRes <- 1 48 | if (is.null(input$breaks)) input$breaks <- 100 49 | if (is.null(input$radius)) input$radius <- 1.5 50 | if (is.null(input$msdplot)) input$msdplot <- "linlin" 51 | 52 | return(input) 53 | } 54 | 55 | #' Find log2 y limits for symmetrical axis 56 | #' 57 | #' @param x vector of log2 values 58 | #' @return list two limits for y-axis 59 | #' @keywords internal 60 | findLog2YAxisLimits <- function(x) { 61 | lo <- min(0.5, x, na.rm = TRUE) 62 | hi <- max(2, x, na.rm = TRUE) 63 | limit <- max(abs(floor(log2(lo))), ceiling(log2(hi))) 64 | lims <- c(2^(-limit), 2^limit) 65 | 66 | return(lims) 67 | } 68 | -------------------------------------------------------------------------------- /R/tm_plots.R: -------------------------------------------------------------------------------- 1 | #' Make a plot of all tracks 2 | #' 3 | #' @param input either a data frame of TrackMate data or list of TrackMate data frame and calibration data frame 4 | #' @param summary boolean to specify if plot is of one dataset or several related datasets 5 | #' @param xstr string to label x-axis 6 | #' @param ystr string to label y-axis 7 | #' @param alphaLevel numeric variable to set alpha for the plot 8 | #' @return ggplot 9 | #' @export 10 | plot_tm_allTracks <- function(input, summary = FALSE, xstr = "", ystr = "", alphaLevel = 0.5) { 11 | x <- y <- dataid <- NULL 12 | if(inherits(input, "list")) { 13 | df <- input[[1]] 14 | } else { 15 | df <- input 16 | } 17 | if(!summary) { 18 | alphaLevel <- 0.5 19 | } 20 | if(summary) { 21 | p <- ggplot(data = df, aes(x = x, y = y, group = interaction(dataid, trace))) + 22 | geom_path(aes(colour = dataid), alpha = alphaLevel) 23 | } else { 24 | p <- ggplot(data = df, aes(x = x, y = y)) + 25 | geom_path(aes(colour = trace), alpha = alphaLevel) 26 | } 27 | p <- p + lims(x = c(0,max(df$x,df$y)),y = c(max(df$x,df$y),0)) + 28 | labs(x = xstr, y = ystr) + 29 | coord_fixed() + 30 | theme_bw() + 31 | theme(legend.position = "none") 32 | 33 | return(p) 34 | } 35 | 36 | 37 | #' Make a plot of displacement over time 38 | #' 39 | #' @param input either a data frame of TrackMate data or list of TrackMate data frame and calibration data frame 40 | #' @param summary boolean to specify if plot is of one dataset or several related datasets 41 | #' @param xstr string to label x-axis 42 | #' @param ystr string to label y-axis 43 | #' @return ggplot 44 | #' @export 45 | plot_tm_displacementOverTime <- function(input, summary = FALSE, xstr = NULL, ystr = NULL) { 46 | t <- displacement <- dataid <- NULL 47 | if(inherits(input, "list")) { 48 | df <- input[[1]] 49 | calibration <- input[[2]] 50 | units <- calibration$unit[1:2] 51 | xstr <- paste0("Time (",units[2],")") 52 | ystr <- paste0("Displacement (",units[1],")") 53 | } else { 54 | df <- input 55 | } 56 | 57 | p <- ggplot(data = df, aes(x = t, y = displacement)) 58 | if(summary) { 59 | if(nrow(df) < 50) { 60 | p <- p + geom_path(aes(group = interaction(dataid, trace), alpha = 0.01)) 61 | } else { 62 | p <- p + geom_path(aes(y = rollmean(displacement, 20, na.pad = TRUE), group = interaction(dataid, trace), alpha = 0.01)) 63 | } 64 | } else { 65 | if(nrow(df) < 50) { 66 | p <- p + geom_path(aes(group = trace, alpha = 0.1)) 67 | } else { 68 | p <- p + geom_path(aes(y = rollmean(displacement, 20, na.pad = TRUE), group = trace, alpha = 0.1)) 69 | } 70 | } 71 | 72 | p <- p + geom_smooth(method = "gam", formula = (y ~ s(x, bs = 'cs'))) + 73 | ylim(0,NA) + 74 | labs(x = xstr, y = ystr) + 75 | theme_classic() + 76 | theme(legend.position = "none") 77 | 78 | return(p) 79 | } 80 | 81 | #' Make a plot of cumulative distance over time 82 | #' 83 | #' @param input either a data frame of TrackMate data or list of TrackMate data frame and calibration data frame 84 | #' @param summary boolean to specify if plot is of one dataset or several related datasets 85 | #' @param xstr string to label x-axis 86 | #' @param ystr string to label y-axis 87 | #' @param alphaLevel numeric variable to set alpha for the plot 88 | #' @return ggplot 89 | #' @export 90 | plot_tm_cumdistOverTime <- function(input, summary = FALSE, xstr = NULL, ystr = NULL, alphaLevel = 0.1) { 91 | track_duration <- cumulative_distance <- dataid <- NULL 92 | 93 | if(inherits(input, "list")) { 94 | df <- input[[1]] 95 | calibration <- input[[2]] 96 | units <- calibration$unit[1:2] 97 | xstr <- paste0("Time (",units[2],")") 98 | ystr <- paste0("Cumulative distance (",units[1],")") 99 | } else { 100 | df <- input 101 | } 102 | 103 | if(!summary) { 104 | alphaLevel <- 0.1 105 | } 106 | if(summary) { 107 | p <- ggplot(data = df, aes(x = track_duration, y = cumulative_distance, group = interaction(dataid, trace))) + 108 | geom_path(aes(colour = dataid), alpha = alphaLevel) 109 | } else { 110 | p <- ggplot(data = df, aes(x = track_duration, y = cumulative_distance, group = trace)) + 111 | geom_path(aes(alpha = alphaLevel)) 112 | } 113 | p <- p + labs(x = xstr, y = ystr) + 114 | theme_classic() + 115 | theme(legend.position = "none") 116 | 117 | return(p) 118 | } 119 | 120 | #' Make a histogram of displacements 121 | #' 122 | #' @param input either a data frame of TrackMate data or list of TrackMate data frame and calibration data frame 123 | #' @param summary boolean to specify if plot is of one dataset or several related datasets 124 | #' @param xstr string to label x-axis 125 | #' @param ystr string to label y-axis 126 | #' @param auto boolean to switch between returning a ggplot and a list of ggplot and variable 127 | #' @return ggplot or list of ggplot and variable 128 | #' @export 129 | plot_tm_displacementHist <- function(input, summary = FALSE, xstr = NULL, ystr = NULL, auto = FALSE) { 130 | displacement <- NULL 131 | 132 | if(inherits(input, "list")) { 133 | df <- input[[1]] 134 | calibration <- input[[2]] 135 | units <- calibration$unit[1:2] 136 | xstr <- paste0("Displacement (",units[1],")") 137 | ystr <- "Frequency" 138 | } else { 139 | df <- input 140 | } 141 | 142 | median_disp <- median(df$displacement) 143 | nBin <- floor(1 + log2(nrow(df))) 144 | 145 | p <- ggplot(data = df, aes(x = displacement)) + 146 | geom_histogram(bins = nBin) + 147 | geom_text(aes(label = format(round(median_disp,3), nsmall = 3), x = max(displacement, na.rm = TRUE), y = Inf), size = 3, hjust = 1, vjust = 1, check_overlap = TRUE) + 148 | labs(x = xstr, y = ystr) + 149 | theme_classic() + 150 | theme(legend.position = "none") 151 | 152 | if(auto) { 153 | returnList <- list(p, median_disp) 154 | return(returnList) 155 | } else { 156 | return(p) 157 | } 158 | } 159 | 160 | #' Make a histogram of intensities 161 | #' 162 | #' Function uses the maximum value of the mean_intensity column for all traces in supplied TrackMate data 163 | #' 164 | #' @param input either a data frame of TrackMate data or list of TrackMate data frame and calibration data frame 165 | #' @param summary boolean to specify if plot is of one dataset or several related datasets 166 | #' @param xstr string to label x-axis 167 | #' @param ystr string to label y-axis 168 | #' @param auto boolean to switch between returning a ggplot and a list of ggplot and variable 169 | #' @return ggplot or list of ggplot and variable 170 | #' @export 171 | plot_tm_intensityHist <- function(input, summary = FALSE, xstr = NULL, ystr = NULL, auto = FALSE) { 172 | dataid <- trace <- mean_intensity <- intensity <- NULL 173 | 174 | if(inherits(input, "list")) { 175 | df <- input[[1]] 176 | calibration <- input[[2]] 177 | units <- calibration$unit[1:2] 178 | xstr <- "Intensity (AU)" 179 | ystr <- "Frequency" 180 | } else { 181 | df <- input 182 | } 183 | 184 | if(summary) { 185 | intDF <- df %>% 186 | group_by(dataid, trace) %>% 187 | summarise(intensity = max(mean_intensity)) 188 | } else { 189 | intDF <- df %>% 190 | group_by(trace) %>% 191 | summarise(intensity = max(mean_intensity)) 192 | } 193 | 194 | median_int <- median(intDF$intensity, na.rm = TRUE) 195 | nBin <- max(floor(1 + log2(nrow(intDF))),30) 196 | 197 | p <- ggplot(data = intDF, aes(x = intensity)) + 198 | geom_histogram(bins = nBin) + 199 | geom_text(aes(label = format(round(median_int,3), nsmall = 3), x = max(intensity, na.rm = TRUE), y = Inf), size = 3, hjust = 1, vjust = 1, check_overlap = TRUE) + 200 | lims(x = c(0,NA)) + 201 | labs(x = xstr, y = ystr) + 202 | theme_classic() + 203 | theme(legend.position = "none") 204 | 205 | if(auto) { 206 | returnList <- list(p, median_int) 207 | return(returnList) 208 | } else { 209 | return(p) 210 | } 211 | } 212 | 213 | 214 | #' Make a histogram of track durations 215 | #' 216 | #' @param input either a data frame of TrackMate data or list of TrackMate data frame and calibration data frame 217 | #' @param summary boolean to specify if plot is of one dataset or several related datasets 218 | #' @param xstr string to label x-axis 219 | #' @param ystr string to label y-axis 220 | #' @param auto boolean to switch between returning a ggplot and a list of ggplot and variable 221 | #' @return ggplot or list of ggplot and variable 222 | #' @export 223 | plot_tm_durationHist <- function(input, summary = FALSE, xstr = NULL, ystr = NULL, auto = FALSE) { 224 | dataid <- trace <- mean_intensity <- instensity <- cumtime <- track_duration <- NULL 225 | 226 | if(inherits(input, "list")) { 227 | df <- input[[1]] 228 | calibration <- input[[2]] 229 | units <- calibration$unit[1:2] 230 | xstr <- paste0("Duration (",units[2],")") 231 | ystr <- "Frequency" 232 | } else { 233 | df <- input 234 | } 235 | 236 | if(summary) { 237 | durDF <- df %>% 238 | group_by(dataid, trace) %>% 239 | summarise(cumtime = max(track_duration)) 240 | } else { 241 | durDF <- df %>% 242 | group_by(trace) %>% 243 | summarise(cumtime = max(track_duration)) 244 | } 245 | 246 | median_duration <- median(durDF$cumtime, na.rm = TRUE) 247 | nBin <- max(floor(1 + log2(nrow(durDF))),30) 248 | 249 | p <- ggplot(data = durDF, aes(x = cumtime)) + 250 | geom_histogram(bins = nBin) + 251 | geom_text(aes(label = format(round(median_duration,3), nsmall = 3), x = max(cumtime, na.rm = TRUE), y = Inf), size = 3, hjust = 1, vjust = 1, check_overlap = TRUE) + 252 | lims(x = c(0,NA)) + 253 | labs(x = xstr, y = ystr) + 254 | theme_classic() + 255 | theme(legend.position = "none") 256 | 257 | if(auto) { 258 | returnList <- list(p, median_duration) 259 | return(returnList) 260 | } else { 261 | return(p) 262 | } 263 | } 264 | 265 | #' Make a histogram of alpha values 266 | #' 267 | #' @param df data frame of alpha values 268 | #' @param median_alpha variable for adding label to plot 269 | #' @param xstr string to label x-axis 270 | #' @param ystr string to label y-axis 271 | #' @return ggplot 272 | #' @export 273 | plot_tm_alpha <- function(df, median_alpha = NULL, xstr = "alpha (log2)", ystr = "Frequency") { 274 | if(!inherits(df, "data.frame")) { 275 | return(NULL) 276 | } 277 | p <- ggplot(data = df, aes(x = alpha)) + 278 | geom_histogram(binwidth = 0.1) 279 | 280 | if(!missing(median_alpha)) { 281 | p <- p + geom_text(aes(label = format(round(median_alpha,3), nsmall = 3), x = max(alpha, na.rm = TRUE), y = Inf), size = 3, hjust = 1, vjust = 1, check_overlap = TRUE) 282 | } 283 | p <- p + labs(x = xstr, y = ystr) + 284 | theme_classic() + 285 | theme(legend.position = "none") 286 | 287 | return(p) 288 | } 289 | 290 | #' Make a histogram of D values 291 | #' 292 | #' @param df data frame of D values 293 | #' @param median_dee variable for adding label to plot 294 | #' @param xstr string to label x-axis 295 | #' @param ystr string to label y-axis 296 | #' @return ggplot 297 | #' @export 298 | plot_tm_dee <- function(df, median_dee = NULL, xstr = "D", ystr = "Frequency") { 299 | if(!inherits(df, "data.frame")) { 300 | return(NULL) 301 | } 302 | dee <- NULL 303 | 304 | p <- ggplot(data = df, aes(x = dee)) + 305 | geom_histogram(binwidth = 0.01) 306 | 307 | if(!missing(median_dee)) { 308 | p <- p + geom_text(aes(label = format(round(median_dee,3), nsmall = 3), x = max(dee, na.rm = TRUE), y = Inf), size = 3, hjust = 1, vjust = 1, check_overlap = TRUE) 309 | } 310 | p <- p + labs(x = xstr, y = ystr) + 311 | lims(x = c(0,NA)) + 312 | theme_classic() + 313 | theme(legend.position = "none") 314 | 315 | return(p) 316 | } 317 | 318 | #' Make a histogram of average speed 319 | #' 320 | #' @param input either a data frame of TrackMate data or list of TrackMate data frame and calibration data frame 321 | #' @param summary boolean to specify if plot is of one dataset or several related datasets 322 | #' @param xstr string to label x-axis 323 | #' @param ystr string to label y-axis 324 | #' @param auto boolean to switch between returning a ggplot and a list of ggplot and variable 325 | #' @return ggplot or list of ggplot and variable 326 | #' @export 327 | plot_tm_speed <- function(input, summary = FALSE, xstr = NULL, ystr = NULL, auto = FALSE) { 328 | dataid <- cumulative_distance <- track_duration <- speed <- NULL 329 | 330 | if(inherits(input, "list")) { 331 | df <- input[[1]] 332 | calibration <- input[[2]] 333 | units <- calibration$unit[1:2] 334 | xstr <- paste0("Speed (",units[1],"/",units[2],")") 335 | ystr <- "Frequency" 336 | } else { 337 | df <- input 338 | } 339 | 340 | if(summary) { 341 | speedDF <- df %>% 342 | group_by(dataid, trace) %>% 343 | summarise(cumdist = max(cumulative_distance), cumtime = max(track_duration)) 344 | } else { 345 | speedDF <- df %>% 346 | group_by(trace) %>% 347 | summarise(cumdist = max(cumulative_distance), cumtime = max(track_duration)) 348 | } 349 | 350 | speedDF$speed <- speedDF$cumdist / speedDF$cumtime 351 | median_speed <- median(speedDF$speed, na.rm = TRUE) 352 | nBin <- max(floor(1 + log2(nrow(speedDF))),30) 353 | 354 | p <- ggplot(data = speedDF, aes(x = speed)) + 355 | geom_histogram(bins = nBin) + 356 | geom_text(aes(label = format(round(median_speed,3), nsmall = 3), x = max(speed, na.rm = TRUE), y = Inf), size = 3, hjust = 1, vjust = 1, check_overlap = TRUE) + 357 | lims(x = c(0,NA)) + 358 | labs(x = xstr, y = ystr) + 359 | theme_classic() + 360 | theme(legend.position = "none") 361 | 362 | if(auto) { 363 | returnList <- list(p, median_speed) 364 | return(returnList) 365 | } else { 366 | return(p) 367 | } 368 | } 369 | 370 | 371 | #' Make a histogram of track density (number of neighbours) 372 | #' 373 | #' @param df data frame of TrackMate data 374 | #' @param auto boolean to switch between returning a ggplot and a list of ggplot and variable 375 | #' @return ggplot or list of ggplot and variable 376 | #' @export 377 | plot_tm_neighbours <- function(df, auto = FALSE) { 378 | density <- NULL 379 | 380 | if(!inherits(df, "data.frame")) { 381 | return(NULL) 382 | } 383 | 384 | median_density <- median(df$density, na.rm = TRUE) 385 | nBin <- max(floor(1 + log2(nrow(df))),30) 386 | 387 | p <- ggplot(data = df, aes(x = density)) + 388 | geom_histogram(bins = nBin) + 389 | geom_text(aes(label = format(round(median_density,3), nsmall = 3), x = max(density, na.rm = TRUE), y = Inf), size = 3, hjust = 1, vjust = 1, check_overlap = TRUE) + 390 | labs(x = "Track density", y = "Frequency") + 391 | theme_classic() + 392 | theme(legend.position = "none") 393 | 394 | if(auto) { 395 | returnList <- list(p, median_density) 396 | return(returnList) 397 | } else { 398 | return(p) 399 | } 400 | } 401 | 402 | #' Make a histogram of fractal dimension 403 | #' 404 | #' @param df data frame of fractal dimension data 405 | #' @param auto boolean to switch between returning a ggplot and a list of ggplot and variable 406 | #' @return ggplot or list of ggplot and variable 407 | #' @export 408 | plot_tm_fd <- function(df, auto = FALSE) { 409 | fd <- NULL 410 | if(!inherits(df, "data.frame")) { 411 | return(NULL) 412 | } 413 | median_fd <- median(df$fd, na.rm = TRUE) 414 | nBin <- max(floor(1 + log2(nrow(df))),30) 415 | 416 | p <- ggplot(data = df, aes(x = fd)) 417 | if(sd(df$fd, na.rm = TRUE) < 0.001) { 418 | p <- p + geom_histogram(breaks = seq(mean(df$fd) - 1, mean(df$fd) + 1, length.out = 30)) 419 | } else { 420 | p <- p + geom_histogram(bins = nBin) 421 | } 422 | p <- p + geom_text(aes(label = format(round(median_fd,3), nsmall = 3), x = max(fd, na.rm = TRUE), y = Inf), size = 3, hjust = 1, vjust = 1, check_overlap = TRUE) + 423 | labs(x = "Fractal dimension", y = "Frequency") + 424 | theme_classic() + 425 | theme(legend.position = "none") 426 | 427 | if(auto) { 428 | returnList <- list(p, median_fd) 429 | return(returnList) 430 | } else { 431 | return(p) 432 | } 433 | } 434 | 435 | #' Make a histogram of the largest point-to-point distance in each track 436 | #' 437 | #' @param df data frame of fractal dimension data 438 | #' @param units character vector of space and time units (default is um and s) 439 | #' @param auto boolean to switch between returning a ggplot and a list of ggplot and variable 440 | #' @return ggplot or list of ggplot and variable 441 | #' @export 442 | plot_tm_width <- function(df, units = c("um","s"), auto = FALSE) { 443 | wide <- NULL 444 | if(!inherits(df, "data.frame")) { 445 | return(NULL) 446 | } 447 | median_width <- median(df$wide, na.rm = TRUE) 448 | nBin <- max(floor(1 + log2(nrow(df))),30) 449 | 450 | p <- ggplot(data = df, aes(x = wide)) + 451 | geom_histogram(bins = nBin) + 452 | geom_text(aes(label = format(round(median_width,3), nsmall = 3), x = max(wide, na.rm = TRUE), y = Inf), size = 3, hjust = 1, vjust = 1, check_overlap = TRUE) + 453 | labs(x = paste0("Maximum width (",units[1],")"), y = "Frequency") + 454 | theme_classic() + 455 | theme(legend.position = "none") 456 | 457 | if(auto) { 458 | returnList <- list(p, median_width) 459 | return(returnList) 460 | } else { 461 | return(p) 462 | } 463 | } 464 | 465 | #' Make a plot of MSD data 466 | #' 467 | #' Generate a plot of MSD over a series of increasing time lags. 468 | #' Input is the output from CalculateMSD(), so the plot will display the ensemble or time-averaged MSD (whatever was requested) 469 | #' A fit to the first four points is displayed to evaluate alpha. Diffusion coefficient from this fit is displayed top-left. 470 | #' 471 | #' @param df MSD summary = output from calculateMSD() 472 | #' @param units character vector to describe units (defaults are um, micrometres and s, seconds) 473 | #' @param bars boolean to request error bars (1 x SD) 474 | #' @param xlog boolean to request log10 x axis 475 | #' @param ylog boolean to request log10 y axis 476 | #' @param auto boolean to request plot only, TRUE gives plot and D as a list 477 | #' @examples 478 | #' xmlPath <- system.file("extdata", "ExampleTrackMateData.xml", package="TrackMateR") 479 | #' datalist <- readTrackMateXML(XMLpath = xmlPath) 480 | #' data <- datalist[[1]] 481 | #' # use the ensemble method and only look at tracks with more than 8 points 482 | #' msdobj <- calculateMSD(df = data, method = "ensemble", N = 3, short = 8) 483 | #' msddf <- msdobj[[1]] 484 | #' plot_tm_MSD(msddf, bars = FALSE) 485 | #' @return ggplot or ggplot and variable 486 | #' @export 487 | plot_tm_MSD <- function(df, units = c("um","s"), bars = FALSE, xlog = FALSE, ylog = FALSE, auto = FALSE) { 488 | xlab <- paste0("Time (",units[2],")") 489 | pred <- NULL 490 | 491 | if(!inherits(df, "data.frame")) { 492 | return(NULL) 493 | } 494 | if(all(is.na(df$mean)) | all(is.na(df$t))) { 495 | dee <- NA 496 | df$pred <- 0 497 | } else { 498 | # fit to first four data points 499 | mod <- lm(mean ~ t, weights = c(n), data = df[1:4,]) 500 | # make a column containing model y points for each t 501 | df$pred <- (mod$coefficients[2] * df$t) + mod$coefficients[1] 502 | # calculate diffusion constant (D) 503 | dee <- df$pred[1] / (4 * df$t[1]) 504 | } 505 | # make ggplot 506 | if(bars) { 507 | p <- ggplot(df, aes(x = t, y = mean)) + 508 | geom_line() + 509 | geom_errorbar(aes(ymin = mean - sd, ymax = mean + sd), width=0) + 510 | geom_point() 511 | } else { 512 | p <- ggplot(df, aes(x = t, y = mean)) + 513 | geom_line() 514 | } 515 | 516 | p <- p + geom_line(aes(x = t, y = pred), colour = "red", linetype = 2) + 517 | geom_text(aes(label = paste0("D = ",format(round(dee,3), nsmall = 3)), x = min(t), y = Inf), size = 3, hjust = 0, vjust = 1, check_overlap = TRUE) + 518 | labs(x = xlab, y = "MSD") + 519 | theme_classic() + 520 | theme(legend.position = "none") 521 | if(xlog) { 522 | p <- p + scale_x_log10() 523 | } 524 | if(ylog) { 525 | p <- p + scale_y_log10() 526 | } 527 | 528 | listReturn <- list(p, dee) 529 | 530 | if(auto) { 531 | return(listReturn) 532 | } else { 533 | return(p) 534 | } 535 | } 536 | 537 | 538 | #' Plot several (n) MSD curves 539 | #' 540 | #' Generate a plot of several MSD curves together with a summary curve. 541 | #' This function is used to compile multiple datasets from the same condition. 542 | #' 543 | #' @param df dataframe of MSD summary data from multiple datasets (labelled by dataid) 544 | #' @param xlog boolean to request log10 x axis 545 | #' @param ylog boolean to request log10 y axis 546 | #' @param auto boolean to request plot only, TRUE gives plot and summary dataframe as a list 547 | #' @return ggplot or list of ggplot and dataframe of summary data 548 | #' @export 549 | plot_tm_NMSD <- function(df, xlog = FALSE, ylog = FALSE, auto = FALSE) { 550 | dataid <- pred <- value <- size <- NULL 551 | if(!inherits(df, "data.frame")) { 552 | return(NULL) 553 | } 554 | # generate a mean of the MSD curve over time (lag) 555 | # rename mean to value so as not to upset ddplyr 556 | colnames(df)[1] <- "value" 557 | 558 | # find the parameters for interpolation 559 | t1 <- df %>% 560 | subset(size == 1) 561 | minT <- min(t1$t, na.rm = TRUE) 562 | maxT <- max(df$t, na.rm = TRUE) 563 | steps <- ceiling(maxT / minT) 564 | 565 | # need to work per dataset 566 | datasets <- unique(df$dataid) 567 | 568 | alldf <- data.frame() 569 | for (i in datasets) { 570 | temp <- df %>% 571 | subset(dataid == i) 572 | 573 | if((nrow(temp) - sum(is.na(temp$t)) < 3) | (nrow(temp) - sum(is.na(temp$value)) < 3)) { 574 | next 575 | } 576 | 577 | newdf <- data.frame(approx(x = temp$t, y = temp$value, xout = seq(from = minT, to = steps * minT, by = minT))) 578 | newdf$dataid <- i 579 | alldf <- rbind(alldf,newdf) 580 | } 581 | # rename header 582 | names(alldf) <- c("t", "value", "dataid") 583 | 584 | msdmean <- alldf %>% 585 | group_by(t) %>% 586 | summarise(mean = mean(value, na.rm = TRUE), sd = sd(value, na.rm = TRUE), n = sum(!is.na(value))) 587 | 588 | minN <- ceiling(max(msdmean$n) / 3) 589 | tmp <- msdmean[-1,] 590 | tmp <- tmp %>% 591 | filter(n < minN) 592 | if(nrow(tmp) > 0) { 593 | maxX <- min(tmp$t) 594 | } else { 595 | maxX <- NA 596 | } 597 | 598 | p <- ggplot(data = df, aes(x = t, y = value)) + 599 | geom_line(aes(group = dataid), colour = "blue", alpha = 0.5) + 600 | geom_ribbon(data = msdmean, aes(ymin = mean - sd, ymax = mean + sd), alpha = 0.2) + 601 | geom_line(data = msdmean, aes(x = t, y = mean), size = 1) + 602 | labs(x = "Time (s)", y = "MSD") + 603 | theme_classic() + 604 | theme(legend.position = "none") 605 | if(xlog) { 606 | p <- p + scale_x_log10() 607 | } else { 608 | p <- p + ylim(0,NA) 609 | } 610 | if(ylog) { 611 | p <- p + scale_y_log10() 612 | } else { 613 | p <- p + xlim(0,maxX) 614 | } 615 | 616 | # fit to first four data points 617 | if(all(is.na(msdmean$mean)) | all(is.na(msdmean$t))) { 618 | dee <- NA 619 | # add line to show MSD with alpha = 1 620 | p <- p + geom_text(aes(label = paste0("D = ",format(round(dee,3), nsmall = 3)), x = min(msdmean, na.rm = TRUE), y = Inf), size = 3, hjust = 0, vjust = 1, check_overlap = TRUE) 621 | } else { 622 | mod <- lm(mean ~ t, data = msdmean[1:4,]) 623 | # make a column containing model y points for each t 624 | msdmean$pred <- (mod$coefficients[2] * msdmean$t) + mod$coefficients[1] 625 | # calculate diffusion constant (D) 626 | dee <- msdmean$pred[1] / (4 * msdmean$t[1]) 627 | # add line to show MSD with alpha = 1 628 | p <- p + geom_line(data = msdmean, aes(x = t, y = pred), colour = "red", linetype = 2) + 629 | geom_text(aes(label = paste0("D = ",format(round(dee,3), nsmall = 3)), x = min(msdmean, na.rm = TRUE), y = Inf), size = 3, hjust = 0, vjust = 1, check_overlap = TRUE) 630 | } 631 | 632 | 633 | # for export we only want the summary data that is plotted i.e. cut off appropriately 634 | if(!is.na(maxX)) { 635 | cutrow <- which(msdmean$t == maxX) 636 | if(!is.null(cutrow) & cutrow > 3) { 637 | msdmean <- msdmean[1:cutrow - 1,] 638 | } 639 | } 640 | 641 | if(auto) { 642 | listReturn <- list(p, msdmean) 643 | return(listReturn) 644 | } else { 645 | return(p) 646 | } 647 | } 648 | 649 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, echo = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-", 12 | message = FALSE 13 | ) 14 | ``` 15 | 16 | # TrackMateR 17 | 18 | 19 | [![R-CMD-check](https://github.com/quantixed/TrackMateR/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/quantixed/TrackMateR/actions/workflows/R-CMD-check.yaml) 20 | [![DOI](https://zenodo.org/badge/453722113.svg)](https://zenodo.org/badge/latestdoi/453722113) 21 | 22 | 23 | Analysis of TrackMate XML outputs in R. 24 | 25 | [TrackMate](https://imagej.net/plugins/trackmate/) is a single-particle tracking plugin for ImageJ/Fiji. 26 | The standard output from a tracking session is in TrackMate XML format. 27 | 28 | The goal of this R package is to import all of the data associated with the final filtered tracks in TrackMate for further analysis and visualization in R. 29 | 30 | ## Installation 31 | 32 | Once you have installed [R](https://cran.rstudio.com) and [RStudio Desktop](https://www.rstudio.com/products/rstudio/download/), you can install TrackMateR using devtools 33 | 34 | ```{r gh-installation, eval = FALSE} 35 | # install.packages("devtools") 36 | devtools::install_github("quantixed/TrackMateR") 37 | ``` 38 | 39 | ## An Example 40 | 41 | A basic example is to load one TrackMate XML file, calibrate it (if needed) and analyse it. 42 | 43 | ```{r example, fig.height = 8, fig.width = 8} 44 | library(ggplot2) 45 | library(TrackMateR) 46 | # an example file is provided, otherwise use file.choose() 47 | xmlPath <- system.file("extdata", "ExampleTrackMateData.xml", package="TrackMateR") 48 | # read the TrackMate XML file into R using 49 | tmObj <- readTrackMateXML(XMLpath = xmlPath) 50 | # Pixel size is actually 0.04 um and original data was 1 pixel, xyscalar = 0.04 51 | tmObj <- correctTrackMateData(dataList = tmObj, xyscalar = 0.04, xyunit = "um") 52 | # generate a report 53 | reportDataset(tmObj) 54 | ``` 55 | 56 | TrackMateR can generate several different types of plot individually using commands or it can make them all automatically and create a report for you. 57 | 58 | - For details of how to make individual plots and/or tweak the default parameters, see `vignette("TrackMateR")` 59 | - To see how to compare different datasets, see `vignette("comparison")` 60 | - In order to rescale or recalibrate TrackMate data, see `vignette("recalibration")` 61 | 62 | ## Credits 63 | 64 | - TrackMateR builds on initial work by Julien Godet on [trackR](https://github.com/jgodet/trackR). 65 | - Méghane Sittewelle provided example TrackMate data and helped with testing TrackMateR. 66 | - The Fiji plug-in [TrackMate](https://github.com/trackmate-sc/TrackMate) is developed by Jean-Yves Tinevez. 67 | 68 | ## Limitations, future development 69 | 70 | - TrackMateR is currently written for 2D data. 3D data is read but analysis is currently on the first two dimensions. 71 | - Addition of a dry run option to quickly report what data would be analysed by `compareDatasets()`. 72 | - Addition of number of tracks per frame and more advanced calculation of the density of tracks. 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # TrackMateR 5 | 6 | 7 | 8 | [![R-CMD-check](https://github.com/quantixed/TrackMateR/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/quantixed/TrackMateR/actions/workflows/R-CMD-check.yaml) 9 | [![DOI](https://zenodo.org/badge/453722113.svg)](https://zenodo.org/badge/latestdoi/453722113) 10 | 11 | 12 | Analysis of TrackMate XML outputs in R. 13 | 14 | [TrackMate](https://imagej.net/plugins/trackmate/) is a single-particle 15 | tracking plugin for ImageJ/Fiji. The standard output from a tracking 16 | session is in TrackMate XML format. 17 | 18 | The goal of this R package is to import all of the data associated with 19 | the final filtered tracks in TrackMate for further analysis and 20 | visualization in R. 21 | 22 | ## Installation 23 | 24 | Once you have installed [R](https://cran.rstudio.com) and [RStudio 25 | Desktop](https://www.rstudio.com/products/rstudio/download/), you can 26 | install TrackMateR using devtools 27 | 28 | ``` r 29 | # install.packages("devtools") 30 | devtools::install_github("quantixed/TrackMateR") 31 | ``` 32 | 33 | ## An Example 34 | 35 | A basic example is to load one TrackMate XML file, calibrate it (if 36 | needed) and analyse it. 37 | 38 | ``` r 39 | library(ggplot2) 40 | library(TrackMateR) 41 | # an example file is provided, otherwise use file.choose() 42 | xmlPath <- system.file("extdata", "ExampleTrackMateData.xml", package="TrackMateR") 43 | # read the TrackMate XML file into R using 44 | tmObj <- readTrackMateXML(XMLpath = xmlPath) 45 | #> Units are: 1 pixel and 0.07002736 s 46 | #> Spatial units are in pixels - consider transforming to real units 47 | #> Collecting spot data. Using 20 cores 48 | #> Matching track data... 49 | #> Calculating distances... 50 | # Pixel size is actually 0.04 um and original data was 1 pixel, xyscalar = 0.04 51 | tmObj <- correctTrackMateData(dataList = tmObj, xyscalar = 0.04, xyunit = "um") 52 | #> Correcting XY scale. 53 | # generate a report 54 | reportDataset(tmObj) 55 | ``` 56 | 57 | ![](man/figures/README-example-1.png) 58 | 59 | TrackMateR can generate several different types of plot individually 60 | using commands or it can make them all automatically and create a report 61 | for you. 62 | 63 | - For details of how to make individual plots and/or tweak the default 64 | parameters, see `vignette("TrackMateR")` 65 | - To see how to compare different datasets, see `vignette("comparison")` 66 | - In order to rescale or recalibrate TrackMate data, see 67 | `vignette("recalibration")` 68 | 69 | ## Credits 70 | 71 | - TrackMateR builds on initial work by Julien Godet on 72 | [trackR](https://github.com/jgodet/trackR). 73 | - Méghane Sittewelle provided example TrackMate data and helped with 74 | testing TrackMateR. 75 | - The Fiji plug-in 76 | [TrackMate](https://github.com/trackmate-sc/TrackMate) is developed by 77 | Jean-Yves Tinevez. 78 | 79 | ## Limitations, future development 80 | 81 | - TrackMateR is currently written for 2D data. 3D data is read but 82 | analysis is currently on the first two dimensions. 83 | - Addition of a dry run option to quickly report what data would be 84 | analysed by `compareDatasets()`. 85 | - Addition of number of tracks per frame and more advanced calculation 86 | of the density of tracks. 87 | -------------------------------------------------------------------------------- /TrackMateR.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: knitr 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://quantixed.github.io/TrackMateR/ 2 | 3 | authors: 4 | Stephen J. Royle: 5 | href: https://quantixed.org 6 | 7 | template: 8 | params: 9 | bootswatch: flatly 10 | 11 | repo: 12 | url: 13 | home: https://github.com/quantixed/TrackMateR/ 14 | source: https://github.com/quantixed/TrackMateR/blob/master/ 15 | issue: https://github.com/quantixed/TrackMateR/issues/ 16 | user: https://github.com/quantixed/ 17 | 18 | navbar: 19 | left: 20 | - icon: fa-home fa-lg 21 | href: index.html 22 | - text: Getting Started 23 | href: articles/TrackMateR.html 24 | - text: Guides 25 | menu: 26 | - text: Getting Started 27 | href: articles/TrackMateR.html 28 | - text: Comparison of multiple datasets 29 | href: articles/comparison.html 30 | - text: Recalibration of datasets 31 | href: articles/recalibration.html 32 | - text: Reference 33 | href: reference/index.html 34 | - text: Change log 35 | href: news/index.html 36 | right: 37 | - icon: fa-github fa-lg 38 | href: https://github.com/quantixed/TrackMateR 39 | 40 | reference: 41 | - title: Loading and correcting TrackMate data 42 | desc: > 43 | To start, TrackMate XML data needs to be read into R. It may need recalibrating 44 | if the data were captured with incorrect parameters. 45 | contents: 46 | - readTrackMateXML 47 | - correctTrackMateData 48 | - title: Automated reports 49 | desc: > 50 | TrackMateR can be used manually to have fine control over your analysis, however 51 | if you just want to analyse your data with default parameters, these two functions will 52 | process one dataset, or many 53 | contents: 54 | - reportDataset 55 | - compareDatasets 56 | - title: Analysing track dynamics 57 | desc: > 58 | To analyse track dynamics, there are several functions to calculate them. 59 | These functions are called (with set parameters) in the automated workflows 60 | and all of them must be called to make a report. 61 | contents: 62 | - calculateAlpha 63 | - calculateCVE 64 | - calculateFD 65 | - calculateJD 66 | - calculateMSD 67 | - calculateTrackDensity 68 | - fittingJD 69 | - title: Generating reports and summaries 70 | desc: > 71 | Reports and summaries are collections of plots. They can be generated automatically 72 | using the automated workflows, or they can be made manually after fine-tuning the analysis 73 | of track dynamics. 74 | contents: 75 | - makeSummaryReport 76 | - makeComparison 77 | - title: Plotting functions 78 | desc: > 79 | The autoimated workflows and the reports that can be generated with TrackMateR 80 | use several different plotting functions. 81 | contents: 82 | - plot_tm_MSD 83 | - plot_tm_NMSD 84 | - plot_tm_allTracks 85 | - plot_tm_alpha 86 | - plot_tm_cumdistOverTime 87 | - plot_tm_dee 88 | - plot_tm_displacementHist 89 | - plot_tm_displacementOverTime 90 | - plot_tm_durationHist 91 | - plot_tm_fd 92 | - plot_tm_intensityHist 93 | - plot_tm_neighbours 94 | - plot_tm_speed 95 | - plot_tm_width 96 | - title: Analysis of non-TrackMate data 97 | desc: > 98 | TrackMateR can read and process csv files in a similar way to TrackMate XML outputs. 99 | These are typically "ground truth" data sets that have been generated synthetically, 100 | but could come from some other package. 101 | contents: 102 | - compareGTDatasets 103 | - readGTFile 104 | -------------------------------------------------------------------------------- /man/calculateAlpha.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/calculateAlpha.R 3 | \name{calculateAlpha} 4 | \alias{calculateAlpha} 5 | \title{Calculate alpha (relationship between MSD and normal diffusion)} 6 | \usage{ 7 | calculateAlpha(alphaMat, tstep) 8 | } 9 | \arguments{ 10 | \item{alphaMat}{matrix of msd curves, each col is a track, each row is time lag (will contain NAs)} 11 | 12 | \item{tstep}{variable. Time step in seconds} 13 | } 14 | \value{ 15 | data frame 16 | } 17 | \description{ 18 | Alpha is the MSD exponent. 19 | Normal diffusion is alpha = 1. Subdiffusion is alpha < 1 and superdiffusion is alpha > 1. 20 | Input is a data matrix of msd curves. 21 | Output is mean of log2(alpha), one value for each trace. 22 | D, calculated from a fit of the first four data points is also outputted. 23 | } 24 | -------------------------------------------------------------------------------- /man/calculateCVE.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/calculateCVE.R 3 | \name{calculateCVE} 4 | \alias{calculateCVE} 5 | \title{Calculate CVE (covariance-based estimator)} 6 | \usage{ 7 | calculateCVE(xMat, yMat, tlist, tstep) 8 | } 9 | \arguments{ 10 | \item{xMat}{matrix of x displacements, each col is a track, each row is time (will contain NAs)} 11 | 12 | \item{yMat}{matrix of y displacements, each col is a track, each row is time (will contain NAs)} 13 | 14 | \item{tlist}{list of trace names} 15 | 16 | \item{tstep}{variable. Time step in seconds} 17 | } 18 | \value{ 19 | data frame 20 | } 21 | \description{ 22 | From Vestergaard et al., Physical Review E (2019) 89, 022726 23 | Input is two data matrices of X and Y displacements for each trace. 24 | Output is estimator of D and sigma for each trace. 25 | } 26 | -------------------------------------------------------------------------------- /man/calculateFD.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/calculateFD.R 3 | \name{calculateFD} 4 | \alias{calculateFD} 5 | \title{Calculate fractal dimension (FD)} 6 | \usage{ 7 | calculateFD(dataList) 8 | } 9 | \arguments{ 10 | \item{dataList}{list of a data frame (must include at a minimum - trace (track ID), x, y and frame (in real coords)) and a calibration data frame} 11 | } 12 | \value{ 13 | data frame 14 | } 15 | \description{ 16 | Calculate for each track, its fractal dimension 17 | Katz & George (1985) define fractal dimension (D) as log(n) / (log(n) + log(d/L)) 18 | where n is the number of steps, d is the longest of all possible point-to-point distances and 19 | L is the cumulative length of the track. 20 | D is ~1 for directed trajectories, ~2 for confined and ~3 for subdiffusion 21 | Here we calculate this and store D (called fd) and d (called wide) for return. 22 | Note that this method does not take into account gaps in the track. 23 | For a track with many gaps, n will be lowered. 24 | } 25 | \examples{ 26 | xmlPath <- system.file("extdata", "ExampleTrackMateData.xml", package="TrackMateR") 27 | tmObj <- readTrackMateXML(XMLpath = xmlPath) 28 | tmObj <- correctTrackMateData(dataList = tmObj, xyscalar = 0.04) 29 | fdDF <- calculateFD(dataList = tmObj) 30 | } 31 | -------------------------------------------------------------------------------- /man/calculateJD.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/calculateJD.R 3 | \name{calculateJD} 4 | \alias{calculateJD} 5 | \title{Calculate Jump Distance (JD)} 6 | \usage{ 7 | calculateJD( 8 | dataList, 9 | deltaT = 1, 10 | nPop = 2, 11 | mode = "ECDF", 12 | init = NULL, 13 | timeRes = 1, 14 | breaks = 100 15 | ) 16 | } 17 | \arguments{ 18 | \item{dataList}{list of data frame (must include at a minimum - trace (track ID), x, y and t (in real coords)) and calibration} 19 | 20 | \item{deltaT}{integer to represent the multiple of frames that are to be analysed} 21 | 22 | \item{nPop}{integer (either 1,2 or 3) number of populations for the jump distance fitting} 23 | 24 | \item{mode}{string indicated ECDF (default) or hist (histogram)} 25 | 26 | \item{init}{initialisation parameters for the nls fit for example list(D2 = 200, D1 = 0.1) or list(D2 = 0.01, D1=0.1, D3=10, D4=100)} 27 | 28 | \item{timeRes}{time resolution per unit of jump. Frame interval is 0.5 s and jump interval is two steps, timeRes = 1.} 29 | 30 | \item{breaks}{number of bins for histogram. With ECDF breaks can be high e.g. 100, for mode = "hist" they should be low, perhaps 30.} 31 | } 32 | \value{ 33 | a list of data frame of jump distances, NAs removed; and a list of parameters called jumpParam (jumptime, deltaT, nPop) 34 | } 35 | \description{ 36 | Calculation of the JD of multiple tracks. 37 | Calculation is equivalent to a single time lag point on the ensemble MSD curve, typically represented as a histogram 38 | Input is a data frame of tracks imported using readTrackMateXML() 39 | The default time step is one frame - which is the equivalent to the plot generated to show displacement versus time. 40 | } 41 | \examples{ 42 | xmlPath <- system.file("extdata", "ExampleTrackMateData.xml", package="TrackMateR") 43 | tmObj <- readTrackMateXML(XMLpath = xmlPath) 44 | tmObj <- correctTrackMateData(tmObj, xyscalar = 0.04) 45 | jdObj <- calculateJD(dataList = tmObj, deltaT = 2) 46 | } 47 | -------------------------------------------------------------------------------- /man/calculateMSD.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/calculateMSD.R 3 | \name{calculateMSD} 4 | \alias{calculateMSD} 5 | \title{Calculate Mean Squared Displacement (MSD)} 6 | \usage{ 7 | calculateMSD(df, method = "timeaveraged", N = 4, short = 0) 8 | } 9 | \arguments{ 10 | \item{df}{data frame must include at a minimum - trace (track ID), x, y and t (in real coords)} 11 | 12 | \item{method}{string. Either "ensemble" or "timeaveraged" (default)} 13 | 14 | \item{N}{numeric variable for MSD. dt should be up to 1/N of number of data points (4 recommended)} 15 | 16 | \item{short}{numeric variable for the shortest number of points we will analyse. Note, this uses the number of frames from start, not number of points in track, i.e. a track with 4 | Learn how to get started with the basics of TrackMateR. 5 | output: 6 | rmarkdown::html_vignette: 7 | 8 | vignette: > 9 | %\VignetteIndexEntry{Getting Started} 10 | %\VignetteEngine{knitr::rmarkdown} 11 | %\VignetteEncoding{UTF-8} 12 | --- 13 | 14 | ```{r, echo = FALSE} 15 | knitr::opts_chunk$set( 16 | collapse = TRUE, 17 | comment = "#>" 18 | ) 19 | ``` 20 | 21 | 22 | TrackMateR is an R package to analyse TrackMate XML outputs. 23 | 24 | [TrackMate](https://imagej.net/plugins/trackmate/) is a single-particle tracking plugin for ImageJ/Fiji. 25 | The standard output from a tracking session is in TrackMate XML format. 26 | 27 | The goal of this R package is to import all of the data associated with the final filtered tracks in TrackMate for further analysis and visualization in R. 28 | 29 | ## Installation 30 | 31 | Once you have installed [R](https://cran.rstudio.com) and [RStudio Desktop](https://www.rstudio.com/products/rstudio/download/), you can install TrackMateR using devtools 32 | 33 | ```{r gh-installation, eval = FALSE} 34 | # install.packages("devtools") 35 | devtools::install_github("quantixed/TrackMateR") 36 | ``` 37 | 38 | ## An Example 39 | 40 | A basic example is to load one TrackMate XML file and analyse it. 41 | This would typically be a tracking session associated with a single movie (referred to here as a single dataset). 42 | 43 | ```{r example} 44 | library(ggplot2) 45 | library(TrackMateR) 46 | # an example file is provided, otherwise use file.choose() 47 | xmlPath <- system.file("extdata", "ExampleTrackMateData.xml", package="TrackMateR") 48 | # read the TrackMate XML file into R using 49 | tmObj <- readTrackMateXML(XMLpath = xmlPath) 50 | ``` 51 | 52 | With the example data, we got a warning telling us that the data is scaled as pixels. 53 | If your TrackMate data is correctly calibrated you can skip this step but if you need to recalibrate it: 54 | 55 | ```{r} 56 | # Pixel size is 0.04 um and original data was 1 pixel, xyscalar = 0.04 57 | tmObj <- correctTrackMateData(dataList = tmObj, xyscalar = 0.04, xyunit = "um") 58 | ``` 59 | 60 | With this done, we can have a look at the data. 61 | 62 | ```{r, fig.height = 4, fig.width = 4} 63 | plot_tm_allTracks(tmObj) 64 | ``` 65 | 66 | So, after reading in the TrackMate file with `readTrackMateXML()` we have an object which we can visualise using `plot_tm_allTracks()`. 67 | 68 | TrackMateR can generate several different types of plot individually using commands like `plot_tm_allTracks()` or it can make them all automatically and create a report for you. 69 | 70 | ```{r, message = FALSE, fig.height = 8, fig.width = 8} 71 | reportDataset(tmObj) 72 | ``` 73 | 74 | In the next section you will see how to do more advanced analysis, and pick and choose what outputs you will make. 75 | However, it is possible to change the defaults of `reportDatasets()` to tune things a bit more while still making a nice report. 76 | For example, `reportDatasets(tmObj, radius = 100)` will use the defaults for everything except the radius for searching for neighbours in the track density analysis. 77 | Parameters that can be changed are: 78 | 79 | - N, short - from MSD analysis 80 | - deltaT, mode, nPop, init, timeRes, breaks - from jump distance calculation and fitting 81 | - radius - from track density analysis 82 | 83 | ## A more advanced example 84 | 85 | Perhaps the default options do not suit your needs. 86 | With just a few more lines of code, you can tweak the report to do the analysis you want. 87 | 88 | Below you will see that the dataset we read in was not scaled correctly. 89 | After reading in, we rescale it and then we start to generate the report. 90 | 91 | The report shows analysis of mean squared displacement, jump distance and track density, among other plots. 92 | These three things must be calculated first using `calculateMSD()`, `calculateJD()` and `calculateTrackDensity`. 93 | This allows you to have some control over the analysis, for example setting the search radius for Track Density. 94 | Finally, these items can be fed into `makeSummaryReport()` as shown below, and the report is created. 95 | 96 | 97 | ```{r, message = FALSE, fig.height = 8, fig.width = 8} 98 | # we can get a data frame of the correct TrackMate data 99 | tmDF <- tmObj[[1]] 100 | # and a data frame of calibration information 101 | calibrationDF <- tmObj[[2]] 102 | # we can calculate mean squared displacement 103 | msdObj <- calculateMSD(df = tmDF, method = "ensemble", N = 3, short = 8) 104 | # and jump distance 105 | jdObj <- calculateJD(dataList = tmObj, deltaT = 1) 106 | # and look at the density of tracks 107 | tdDF <- calculateTrackDensity(dataList = tmObj, radius = 1.5) 108 | # and calculate fractal dimension 109 | fdDF <- calculateFD(dataList = tmObj) 110 | # if we extract the name of the file 111 | fileName <- tools::file_path_sans_ext(basename(xmlPath)) 112 | # we can send all these things to makeSummaryReport() to get a nice report of our dataset 113 | makeSummaryReport(tmList = tmObj, msdList = msdObj, jumpList = jdObj, tddf = tdDF, fddf = fdDF, 114 | titleStr = "Report", subStr = fileName, auto = FALSE) 115 | ``` 116 | 117 | Note that `makeSummaryReport` can take additional arguments to give the user some control over fitting to jump distance. 118 | For example, `nPop = 1` will fit one population instead of two (the dafault), and `init = list()` can be a list of guesses for the initial fit. 119 | 120 | This vignette described the analysis of a single dataset. 121 | For analysis of more than one dataset, see `vignette("comparison")` 122 | -------------------------------------------------------------------------------- /vignettes/comparison.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Comparison of multiple datasets" 3 | description: > 4 | Learn how to get compare multiple datasets and conditions using TrackMateR. 5 | output: 6 | rmarkdown::html_vignette: 7 | fig_width: 8 8 | fig_height: 8 9 | vignette: > 10 | %\VignetteIndexEntry{Comparison of multiple datasets} 11 | %\VignetteEngine{knitr::rmarkdown} 12 | %\VignetteEncoding{UTF-8} 13 | --- 14 | 15 | ```{r, echo = FALSE} 16 | knitr::opts_chunk$set( 17 | collapse = TRUE, 18 | comment = "#>" 19 | ) 20 | ``` 21 | 22 | In the first vignette `vignette("TrackMateR")` we saw how to analyse one TrackMate XML file. 23 | This is one _dataset_, the result of a TrackMate tracking session on a single movie. 24 | A more likely scenario is that you have several datasets, from different conditions. 25 | 26 | In this scenario we would like to see: 27 | 28 | - a report for each dataset 29 | - a compiled report for all datasets in that condition 30 | - a comparison between conditions of key parameters 31 | 32 | ## Organising your data 33 | 34 | **Recommended** 35 | Start a new RStudio session in a new directory using Create Project. 36 | We find it useful to follow [this convention](https://martinctc.github.io/blog/rstudio-projects-and-working-directories-a-beginner's-guide/) for RStudio projects. 37 | This [gist](https://gist.github.com/quantixed/42625f988a7b5da25b7e333c4a660b97) can be run in the console to quickly set up the folder structure for your new project. 38 | Now place your data in a folder called `Data/` in this directory. 39 | 40 | **Required** 41 | Place all your TrackMate XML files into subfolders according to condition. 42 | For example you might have: 43 | 44 | * Data/ 45 | + Control/ 46 | - cell1.xml 47 | - cell2.xml 48 | + Drug/ 49 | - cell1.xml 50 | - cell2.xml 51 | - cell3.xml 52 | 53 | Only one level of folders is allowed. 54 | 55 | ## Analysing multiple datasets 56 | 57 | Once the data is in place, a single command will generate a series of reports, summaries and a comparison 58 | 59 | ```{r, eval = FALSE} 60 | library(TrackMateR) 61 | compareDatasets() 62 | ``` 63 | 64 | The outputs are generated using standard parameters equivalent to `reportDataset()`. 65 | However, it is possible to change the defaults by passing additional parameters. 66 | For example, `compareDatasets(radius = 100)` will use the defaults for everything except the radius for searching for neighbours in the track density analysis (this example will use a value of 100 units for the radius). 67 | Parameters that can be changed are: 68 | 69 | - N, short - from MSD analysis 70 | - deltaT, mode, nPop, init, timeRes, breaks - from jump distance calculation and fitting 71 | - radius - from track density analysis 72 | - msdplot - change the MSD plots from linear-linear (default) to e.g. log-log. Options: loglog, loglin, linlog, linlin. 73 | 74 | 75 | ## Outputs 76 | 77 | In the example above, the code will produce the following in a directory called `Output/` 78 | 79 | * Plots/ 80 | + comparison.pdf 81 | + Control/ 82 | - combined.pdf 83 | - report_1.pdf 84 | - report_2.pdf 85 | + Drug/ 86 | - combined.pdf 87 | - report_1.pdf 88 | - report_2.pdf 89 | - report_3.pdf 90 | 91 | That is, the individual reports are saved with a new name (regardless of the original XML filename), all datasets are combined (per condition) in `combined.pdf` and a comparison of a summary of all conditions is saved in the top level folder (`comparison.pdf`). 92 | 93 | As well as graphical outputs, `compareDatasets()` saves several csv files of data (to `Outputs/Data/`). 94 | In each condition folder, the data frames are collated and saved. 95 | This includes: TrackMate data, MSD, jump distance and fractal dimension data. 96 | There is also a csv file of the data frame used to plot the average msd for this condition. 97 | 98 | Above the condition folders, three csv files are saved. 99 | A collation of the msd summary data `allMSCurves.csv` for all conditions; summary data per dataset `allComparison.csv` which is the data frame used for making the comparison plots; and `allTraceData.csv` which is a data frame of properties per trace per dataset for all conditions. 100 | 101 | The idea with these files is that a user can load them back into R and process the data in new ways and go beyond TrackMateR. 102 | An advanced user can make their own data frames by running TrackMateR functions. 103 | A good place to start is to peruse the code for `compareDatasets()` and modify from there. 104 | 105 | ## No data? No problem! 106 | 107 | If you don't have your own datasets, or if you'd like to download some data to process and understand how comparison between datasets work in TrackMateR, there are some test datasets available [here](https://doi.org/10.5281/zenodo.7985498). 108 | There is raw data, TrackMate XML files and example TrackMateR outputs for movies with particles undergoing 6 different kinds of motion. 109 | To try it out, download the files and place the contents of `TrackMateOutput` folder into you RStudio project folder under `Data` as described above. 110 | Now, run 111 | 112 | ```{r eval=FALSE} 113 | library(TrackMateR) 114 | compareDatasets() 115 | ``` 116 | 117 | to see how the data can be processed. 118 | Using the default parameters, the track density is quite sparse, so one option to try to alter first is `radius`, i.e. `compareDatasets(radius = 4)`. 119 | 120 | The test datasets are generated by tracking synthetic data. 121 | Since the movies are synthetic, we have ground truth data for the exact position and identities of the tracks. 122 | The ground truth datasets are useful for verifying TrackMate outputs, benchmarking TrackMate performance and so on. 123 | To load a single ground truth data set, use `readGTFile("path/to/file")` and then recalibrate as you would for a TrackMate file (the ground truth data is in pixels and frames). 124 | Or you can compare many ground truth csv files using `compareGTDatasets("path/to/Data")` and specifying the rescaling arguments. 125 | 126 | Finally, to generate your own "ground truth" data without using the code [here](https://doi.org/10.5281/zenodo.7985498), all that is required is a csv file specifying the tracks with columns TrackID,x,y,frame. 127 | 128 | ## Limitations 129 | 130 | When using `compareDatasets()` it is assumed that the files and datasets have similar scaling. 131 | If files need recalibration, see `vignette("calibration")`. 132 | Be aware that if datasets with varying calibration are used, some plots generated by `compareDatasets()` will be misleading. 133 | If the scaling is different between conditions: 134 | 135 | * if the scaling is correct and consistent - all plots are OK 136 | * if the time scaling is correct but inconsistent within a condition 137 | + jump distance plot - in `combined.pdf` will be meaningless, individual plots are OK 138 | + MSD plot - combined average may look odd since different durations may be used. This is also an issue if movies have different lengths or if tracking is poor in some movies. 139 | * if the spatial scaling is correct but inconsistent - all plots are OK 140 | 141 | Note that if either spatial or temporal scaling is incorrect, most of the plots will be garbage. 142 | 143 | -------------------------------------------------------------------------------- /vignettes/recalibration.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Recalibration of datasets" 3 | description: > 4 | Learn how to get recalibrate datasets using TrackMateR. 5 | output: 6 | rmarkdown::html_vignette: 7 | fig_width: 8 8 | fig_height: 8 9 | vignette: > 10 | %\VignetteIndexEntry{Recalibration of datasets} 11 | %\VignetteEngine{knitr::rmarkdown} 12 | %\VignetteEncoding{UTF-8} 13 | --- 14 | 15 | In the original example, we saw how to load one dataset and recalibrate. 16 | 17 | ```{r basic-example, eval=FALSE} 18 | library(ggplot2) 19 | library(TrackMateR) 20 | # an example file is provided, otherwise use file.choose() 21 | xmlPath <- system.file("extdata", "ExampleTrackMateData.xml", package="TrackMateR") 22 | # read the TrackMate XML file into R using 23 | tmObj <- readTrackMateXML(XMLpath = xmlPath) 24 | # Pixel size is 0.04 um and original data was 1 pixel, xyscalar = 0.04 25 | tmObj <- correctTrackMateData(dataList = tmObj, xyscalar = 0.04, xyunit = "um") 26 | ``` 27 | 28 | For the example file, the spatial units are 1 pixel and not the real value which is 0.04 um per pixel. 29 | So to convert we can specify `xyscalar` to change 1 to 0.04 and specify `xyunit` to change from "pixel" to "um". 30 | If the file had 2 pixels as the spatial unit (for some reason), to convert to 0.04 um we would need to use `xyscalar = 0.02, xyunit = "um"` 31 | 32 | The units (`xyunit` and `tunit`) can be any arbitrary string. 33 | The use of "um" is preferred over "µm" because the mu character prints inconsistently in R graphics across platforms. 34 | 35 | If the time scaling of the file is 0.07 s and we would prefer ms as the unit, 36 | 37 | ```{r eval=FALSE} 38 | tmObj <- correctTrackMateData(dataList = tmObj, xyscalar = 0.04, xyunit = "um", tscalar = 1000, tunit = "ms") 39 | ``` 40 | 41 | this is because the file is loaded in and scaled at 1 s, so the conversion needs to be 1000 ms. 42 | If we wanted to switch to minutes, we would use `tscale = 1/60, tunit = "min"`. 43 | 44 | If the time scaling is incorrect, e.g. 1 frame, we would need to rescale using `tscale = 0.07, tunit = "s"`, `tscale = 70, tunit = "ms"`, or `tscale = 0.07/60, tunit = "min"`. 45 | 46 | 47 | ## Recalibrating TrackMate files 48 | 49 | When using `compareDatasets()`, it is possible to recalibrate TrackMate XML files _en masse_ by using a CSV file in the condition subfolder containing the XML files, see `vignette("comparison")`. 50 | To recalibrate, a separate CSV file is needed for each condition. 51 | Any conditions that do not need recalibrating simply do not need a CSV file present. 52 | The CSV file can have any name but must follow this format. 53 | 54 | ```{text} 55 | value,unit 56 | 0.04,um 57 | 0.07,s 58 | 59 | ``` 60 | 61 | Using this file, all XML files in the folder will be recalibrated if they are not already at 0.04 micron pixel size and 0.07 s per frame. 62 | Adjustments smaller than 2.5% are ignored. 63 | 64 | **Note that the calibration file should give the correct final scaling required for all files in the folder.** 65 | 66 | When the rescaling is done using a file like this, TrackMateR will work out the scalar for you. 67 | So the file needs to specify what final scaling is required. 68 | 69 | In the situation where you have datasets with differing (but correct) timescales and spatial units need to be corrected, use 0 for the value corresponding to time (time rescaling will be ignored) and spatial rescaling will go ahead. 70 | Similarly, if you have differening (but correct) spatial scaling between datasets you can rescale time only by using 0 for the spatial value. 71 | If you have datasets that need calibrating differently, you will need to group them into condition-level subfolders with a single csv file to specify the correct parameters. 72 | Alternatively, consider going back and retracking in TrackMate and specifying the correct parameters. 73 | 74 | ## Limitations 75 | 76 | The recalibration is not smart, it doesn't recognise the units and calculate based off that. 77 | For example, it doesn't know that 1000 ms are 1 s. 78 | So if you use the recalibration file method, be aware of this limitation. 79 | --------------------------------------------------------------------------------