R/dti_tracks_utils.R

Defines functions print.dti.track.iterator trk.track.iterator mrtrix.track.iterator dti.track.iterator dti.track.bbox dti.track.count merge.bbox coord.bbox skip.connection.bytes scan.trk.file scan.mrtrix.file scan.dti.tract.file detect.dti.tract.format groups.in.bbox subset.groups group.start.rows

Documented in coord.bbox detect.dti.tract.format dti.track.bbox dti.track.count dti.track.iterator groups.in.bbox group.start.rows merge.bbox mrtrix.track.iterator print.dti.track.iterator scan.dti.tract.file scan.mrtrix.file scan.trk.file skip.connection.bytes subset.groups trk.track.iterator

# Utilities that operate on collections of tracts (streamlines) --------------
#
# These helpers are shared by the TRK and TCK/TSF readers. They deal with the
# "array sequence" layout used throughout the package: all points of all tracts
# concatenated in one matrix, plus one length per tract.
#
# The functions in this file that start with `dti.` are exported and work on
# files rather than on in-memory data: they stream through a tract file without
# ever holding all of it in memory, which is what makes them usable on the
# multi-gigabyte whole-brain tractograms that tractography pipelines produce.


#' @title Compute the first row of every group in a concatenated matrix.
#'
#' @param lengths integer vector, the number of rows of each group.
#'
#' @return integer vector with one entry per group.
#'
#' @keywords internal
group.start.rows <- function(lengths) {
  if (length(lengths) == 0L) {
    return(integer(0L));
  }
  return(cumsum(c(1L, lengths[-length(lengths)])));
}


#' @title Extract selected groups from a concatenated matrix.
#'
#' @param points numeric matrix holding the concatenated rows of all groups.
#'
#' @param lengths integer vector with the number of rows of each group.
#'
#' @param sel integer vector, the indices of the groups to extract.
#'
#' @return named list with entries \code{points} (matrix with the rows of the
#'   selected groups) and \code{lengths} (their lengths).
#'
#' @keywords internal
#'
#' @exportS3Method NULL
subset.groups <- function(points, lengths, sel) {
  sel <- as.integer(sel);
  if (length(sel) == 0L) {
    return(list(points = matrix(numeric(0L), nrow = 0L, ncol = ncol(points)),
                lengths = integer(0L)));
  }
  sel_lengths <- lengths[sel];
  starts <- group.start.rows(lengths)[sel];
  rows <- rep.int(starts, sel_lengths) + sequence(sel_lengths) - 1L;
  return(list(points = points[rows, , drop = FALSE], lengths = sel_lengths));
}


#' @title Check which groups have at least one point inside a box.
#'
#' @param points numeric matrix with 3 columns, the concatenated points.
#'
#' @param lengths integer vector with the number of points of each group.
#'
#' @param bbox numeric vector of length 6:
#'   \code{c(xmin, xmax, ymin, ymax, zmin, zmax)}.
#'
#' @return logical vector with one entry per group.
#'
#' @keywords internal
groups.in.bbox <- function(points, lengths, bbox) {
  if (length(lengths) == 0L) {
    return(logical(0L));
  }
  # rowsum() counts the points inside the box per streamline; it is vectorized,
  # unlike tapply() on a factor with millions of levels. It omits groups that
  # have no points at all, so the result is expanded back to one entry per
  # streamline (an empty streamline has no point, so it is never inside a box).
  inside_group <- logical(length(lengths));
  with_points <- which(lengths > 0L);
  if (length(with_points) > 0L) {
    inside <- points[, 1L] >= bbox[1L] & points[, 1L] <= bbox[2L] &
      points[, 2L] >= bbox[3L] & points[, 2L] <= bbox[4L] &
      points[, 3L] >= bbox[5L] & points[, 3L] <= bbox[6L];
    counts <- as.vector(rowsum(as.integer(inside), rep.int(with_points, lengths[with_points]),
                               reorder = FALSE));
    inside_group[with_points] <- counts > 0L;
  }
  return(inside_group);
}


#' @title Detect the format of a DTI tract file.
#'
#' @param filepath character string, path to the file.
#'
#' @return character string, one of 'tck', 'tsf' or 'trk'.
#'
#' @keywords internal
detect.dti.tract.format <- function(filepath) {
  if (!file.exists(filepath)) {
    stop(sprintf("File '%s' does not exist.\n", filepath));
  }

  gzipped <- is.gzip.file(filepath);

  # The TRK magic number is the 5 byte string 'TRACK'; the header field it is
  # stored in is 6 bytes and NUL padded. Compressed TRK files are supported as
  # well, so the magic bytes are read through the gzip layer when needed.
  fh <- open.maybe.gzip(filepath, gzipped, mode = "rb");
  magic <- readBin(fh, what = "raw", n = 5L);
  close(fh);
  if (identical(magic, charToRaw("TRACK"))) {
    return("trk");
  }

  con <- open.maybe.gzip(filepath, gzipped, mode = "r");
  on.exit(
    {
      close(con);
    },
    add = TRUE
  );
  first_line <- readLines(con, n = 1L, warn = FALSE);
  if (length(first_line) == 1L) {
    first_line <- trimws(first_line);
    if (identical(first_line, "mrtrix tracks")) {
      return("tck");
    }
    if (identical(first_line, "mrtrix track scalars")) {
      return("tsf");
    }
  }

  stop(sprintf("File '%s' is not in TRK, TCK or TSF format.\n", filepath));
}


#' @title Scan a tract file without holding its data in memory.
#'
#' @description Streams through the tract file and either counts the tracts, or
#'   computes the bounding box of all their points, or both. Nothing but the
#'   current chunk is ever held in memory, so this works on files of any size.
#'
#' @inheritParams read.dti.tck
#'
#' @param want character vector, any combination of 'count' and 'bbox'.
#'
#' @param chunk_values integer, number of payload values to read per chunk.
#'
#' @return named list with the entries that were requested:
#'   \code{count} (integer, the number of tracts) and \code{bbox} (numeric
#'   vector of length 6, \code{c(xmin, xmax, ymin, ymax, zmin, zmax)}), or
#'   \code{NULL} for \code{bbox} if no point was found.
#'
#' @keywords internal
scan.dti.tract.file <- function(filepath, want = c("count", "bbox"), chunk_values = 4e6) {
  want <- match.arg(want, c("count", "bbox"), several.ok = TRUE);
  format <- detect.dti.tract.format(filepath);

  if (identical(format, "trk")) {
    return(scan.trk.file(filepath, want));
  }
  return(scan.mrtrix.file(filepath, want, chunk_values = chunk_values));
}


#' @title Scan an MRtrix TCK or TSF file.
#'
#' @param filepath character string, path to the file.
#'
#' @param want character vector, the values to compute, see
#'   \code{scan.dti.tract.file}.
#'
#' @param chunk_values integer, number of payload values to read per chunk.
#'
#' @return named list with entries \code{count} and \code{bbox}.
#'
#' @keywords internal
scan.mrtrix.file <- function(filepath, want, chunk_values = 4e6) {
  want_bbox <- "bbox" %in% want;

  # max_tracks = 1 tells the payload opener that the data is streamed and never
  # held in memory as a whole, so its up-front allocation check does not apply.
  payload <- open.mrtrix.payload(filepath, max_tracks = 1L);
  con <- payload$con;
  on.exit(
    {
      close(con);
    },
    add = TRUE
  );

  if (want_bbox && !payload$is_tck) {
    stop("TSF files store per-point scalars, not coordinates, so they have no bounding box.\n");
  }

  if (payload$offset > 0) {
    skip.connection.bytes(con, payload$offset, payload$gzipped, filepath);
  }

  values_per_point <- payload$values_per_point;
  pending <- matrix(numeric(0L), nrow = 0L, ncol = values_per_point);
  leftover <- numeric(0L);
  count <- 0L;
  bbox <- NULL;

  repeat {
    chunk <- readBin(con = con, what = numeric(), n = chunk_values, size = payload$dsize,
                     endian = payload$endian);
    if (length(chunk) == 0L) {
      break;
    }

    values <- if (length(leftover) == 0L) chunk else c(leftover, chunk);
    num_values <- length(values);
    num_rows <- num_values %/% values_per_point;
    if (num_rows == 0L) {
      leftover <- values;
      next;
    }
    mat <- matrix(values[seq_len(num_rows * values_per_point)], ncol = values_per_point, byrow = TRUE);
    leftover <- if (num_values > num_rows * values_per_point) {
      values[(num_rows * values_per_point + 1L):num_values];
    } else {
      numeric(0L);
    }

    termin <- match(TRUE, infinite.rows(mat));
    if (!is.na(termin)) {
      mat <- if (termin > 1L) mat[seq_len(termin - 1L), , drop = FALSE] else {
        matrix(numeric(0L), nrow = 0L, ncol = values_per_point);
      }
    }

    groups <- split.mrtrix.chunk(mat, pending, finish_pending = !is.na(termin));
    pending <- groups$pending;
    count <- count + length(groups$lengths);
    if (want_bbox && nrow(groups$points) > 0L) {
      bbox <- merge.bbox(bbox, coord.bbox(groups$points[, seq_len(3L), drop = FALSE]));
    }

    if (!is.na(termin)) {
      break;
    }
  }

  return(list(count = count, bbox = bbox));
}


#' @title Scan a TRK file.
#'
#' @param filepath character string, path to the file.
#'
#' @param want character vector, the values to compute, see
#'   \code{scan.dti.tract.file}.
#'
#' @return named list with entries \code{count} and \code{bbox}.
#'
#' @keywords internal
scan.trk.file <- function(filepath, want) {
  want_bbox <- "bbox" %in% want;

  endian <- get.dti.trk.endianness(filepath);
  trk_header <- read.dti.trk.header(filepath);
  values_per_point <- 3L + trk_header$n_scalars;
  num_properties <- trk_header$n_properties;

  gzipped <- is.gzip.file(filepath);
  con <- open.maybe.gzip(filepath, gzipped, mode = "rb");
  on.exit(
    {
      close(con);
    },
    add = TRUE
  );
  skip.connection.bytes(con, trk_header$hdr_size, gzipped, filepath);

  count <- 0L;
  bbox <- NULL;

  repeat {
    num_points <- readBin(con, integer(), n = 1L, size = 4L, endian = endian);
    if (length(num_points) == 0L) {
      break;
    }
    if (is.na(num_points) || num_points < 0L) {
      stop(sprintf("Invalid point count %s in TRK file '%s', the file is corrupt.\n", num_points, filepath));
    }
    # A track with zero points is legal and is counted like any other track, see
    # read.trk.records(). Its properties still have to be consumed to stay in
    # sync with the file.

    if (want_bbox) {
      record <- readBin(con, numeric(), n = num_points * values_per_point, size = 4L, endian = endian);
      if (length(record) != num_points * values_per_point) {
        stop(sprintf("Truncated TRK file: track %d is incomplete.\n", count + 1L));
      }
      record <- matrix(record, ncol = values_per_point, byrow = TRUE);
      bbox <- merge.bbox(bbox, coord.bbox(record[, seq_len(3L), drop = FALSE]));
    } else {
      # The records are self-delimiting, so the payload can be skipped over
      # without reading it at all.
      skip.connection.bytes(con, 4 * num_points * values_per_point, gzipped, filepath);
    }
    if (num_properties > 0L) {
      skip.connection.bytes(con, 4 * num_properties, gzipped, filepath);
    }
    count <- count + 1L;
  }

  return(list(count = count, bbox = bbox));
}


#' @title Skip over bytes of a connection, transparently handling gzip.
#'
#' @description Skips forward from the current position of the connection. R
#'   cannot seek on a gzfile connection (the underlying \code{gzseek()} fails
#'   with 'invalid or incomplete compressed data' and only warns instead of
#'   failing loudly), so compressed connections are skipped by reading and
#'   discarding the bytes instead.
#'
#' @param con a connection opened in binary read mode.
#'
#' @param num_bytes numeric, the number of bytes to skip, relative to the
#'   current position of the connection.
#'
#' @param gzipped logical, whether the connection is a gzfile connection, as
#'   reported by \code{is.gzip.file}.
#'
#' @param filepath character string, used in error messages only.
#'
#' @return \code{TRUE}, invisibly.
#'
#' @keywords internal
skip.connection.bytes <- function(con, num_bytes, gzipped, filepath = "") {
  if (num_bytes <= 0) {
    return(invisible(TRUE));
  }
  if (!gzipped) {
    seek(con, where = num_bytes, origin = "current");
    return(invisible(TRUE));
  }

  skip <- num_bytes;
  while (skip > 0) {
    skipped <- readBin(con, what = "raw", n = as.integer(min(skip, 1e6)));
    if (length(skipped) == 0L) {
      stop(sprintf("File '%s' is truncated: %d byte(s) could not be skipped.\n", filepath, num_bytes));
    }
    skip <- skip - length(skipped);
  }
  return(invisible(TRUE));
}


#' @title Compute the bounding box of a set of coordinates.
#'
#' @param coords numeric matrix with 3 columns.
#'
#' @return numeric vector of length 6,
#'   \code{c(xmin, xmax, ymin, ymax, zmin, zmax)}, or NULL if there are no rows.
#'
#' @keywords internal
coord.bbox <- function(coords) {
  if (nrow(coords) == 0L) {
    return(NULL);
  }
  return(as.vector(rbind(apply(coords, 2L, min), apply(coords, 2L, max))));
}


#' @title Merge two bounding boxes.
#'
#' @param bbox1 numeric vector of length 6 or NULL.
#'
#' @param bbox2 numeric vector of length 6 or NULL.
#'
#' @return numeric vector of length 6, the union of the two boxes.
#'
#' @keywords internal
#'
#' @exportS3Method NULL
merge.bbox <- function(bbox1, bbox2) {
  if (is.null(bbox1)) {
    return(bbox2);
  }
  if (is.null(bbox2)) {
    return(bbox1);
  }
  return(c(min(bbox1[1L], bbox2[1L]), max(bbox1[2L], bbox2[2L]),
           min(bbox1[3L], bbox2[3L]), max(bbox1[4L], bbox2[4L]),
           min(bbox1[5L], bbox2[5L]), max(bbox1[6L], bbox2[6L])));
}


# Exported file-level utilities ------------------------------------------------

#' @title Count the tracts in a DTI tract file.
#'
#' @description Counts the tracts in a TRK, TCK or TSF file without reading
#'   their coordinates, so this works on arbitrarily large tractograms with a
#'   small and constant amount of memory. This is the equivalent of
#'   \code{tckinfo <file> -count} in MRtrix.
#'
#' @inheritParams read.dti.tck
#'
#' @param chunk_values integer, the number of payload values to read per chunk.
#'   Advanced tuning parameter, see \code{\link{read.dti.tck}}.
#'
#' @return integer, the number of tracts in the file. Note that this can differ
#'   from the \code{count} entry in the file header, which the MRtrix
#'   documentation explicitly describes as unreliable, and which does not count
#'   empty tracts.
#'
#' @examples
#' \dontrun{
#' dti.track.count("brain.tck");
#' }
#'
#' @export
dti.track.count <- function(filepath, chunk_values = 4e6) {
  return(scan.dti.tract.file(filepath, want = "count", chunk_values = chunk_values)$count);
}


#' @title Compute the bounding box of all tract coordinates in a file.
#'
#' @description Streams through the tract file and returns the bounding box of
#'   all coordinates, without keeping the coordinates in memory. This is useful
#'   to determine the axis limits for plotting a large tractogram.
#'
#' @inheritParams read.dti.tck
#'
#' @param chunk_values integer, the number of payload values to read per chunk.
#'   Advanced tuning parameter, see \code{\link{read.dti.tck}}.
#'
#' @return numeric vector of length 6,
#'   \code{c(xmin, xmax, ymin, ymax, zmin, zmax)}, or \code{NULL} if the file
#'   contains no coordinates. The coordinates are in the coordinate system used
#'   by the file, see the note in \code{\link{read.dti.trk}}.
#'
#' @examples
#' \dontrun{
#' bbox <- dti.track.bbox("brain.tck");
#' }
#'
#' @export
dti.track.bbox <- function(filepath, chunk_values = 4e6) {
  return(scan.dti.tract.file(filepath, want = "bbox", chunk_values = chunk_values)$bbox);
}


#' @title Create an iterator over the tracts of a DTI tract file.
#'
#' @description Reads a TRK, TCK or TSF file tract by tract, so that a
#'   tractogram of any size can be processed with a constant amount of memory.
#'   This is the low-level interface behind \code{\link{dti.track.count}} and
#'   friends, use it when neither reading a subset
#'   (\code{max_tracks}, \code{skip_tracks}, \code{bbox} in
#'   \code{\link{read.dti.tck}}) nor one of the aggregate functions fits your
#'   use case.
#'
#'   The returned object is an environment with the following entries:
#'   \itemize{
#'     \item \code{next.track()}: returns the next tract (a n x 3 matrix for TCK,
#'       a numeric vector of per-point values for TSF, and a list with the
#'       entries \code{coords}, \code{num_points}, \code{scalars} and
#'       \code{properties} for TRK, just like \code{tracks[[i]]} does for the
#'       readers), or \code{NULL} when there are no more tracts.
#'     \item \code{close()}: closes the underlying file connection. It is safe to
#'       call this more than once, and it is also called automatically when the
#'       iterator is garbage collected.
#'     \item \code{tracks.read}: the number of tracts returned so far.
#'     \item \code{filepath}, \code{format}: the file and its detected format.
#'   }
#'
#' @inheritParams read.dti.tck
#'
#' @return the iterator environment, see the description.
#'
#' @examples
#' \dontrun{
#' itr <- dti.track.iterator("brain.tck");
#' total_points <- 0;
#' while (!is.null(track <- itr$next.track())) {
#'   total_points <- total_points + nrow(track);
#' }
#' itr$close();
#' }
#'
#' @export
dti.track.iterator <- function(filepath, skip_tracks = 0L, bbox = NULL, chunk_values = 4e6) {
  format <- detect.dti.tract.format(filepath);
  validate.bbox(bbox);
  if (!is.null(bbox) && identical(format, "tsf")) {
    stop("TSF files store per-point scalars, not coordinates, so 'bbox' cannot be used with them.\n");
  }

  if (identical(format, "trk")) {
    return(trk.track.iterator(filepath, skip_tracks = skip_tracks, bbox = bbox));
  }
  return(mrtrix.track.iterator(filepath, skip_tracks = skip_tracks, bbox = bbox,
                               chunk_values = chunk_values));
}


#' @title Create an iterator over the tracts of an MRtrix TCK or TSF file.
#'
#' @param filepath character string, path to the file.
#'
#' @param skip_tracks integer, the number of tracts to skip.
#'
#' @param bbox numeric vector of length 6 or NULL, a bounding box, see
#'   \code{read.dti.tck}.
#'
#' @param chunk_values integer, the number of payload values to read per chunk.
#'   This only affects the peak memory usage and the I/O granularity of the
#'   iterator, and is rarely needed. For TRK files it is ignored, since those
#'   records are read one at a time.
#'
#' @return the iterator environment.
#'
#' @keywords internal
mrtrix.track.iterator <- function(filepath, skip_tracks = 0L, bbox = NULL, chunk_values = 4e6) {
  # max_tracks = 1 tells the payload opener that the data is streamed and never
  # held in memory as a whole, so its up-front allocation check does not apply.
  payload <- open.mrtrix.payload(filepath, max_tracks = 1L);
  values_per_point <- payload$values_per_point;

  state <- new.env(parent = emptyenv());
  state$con <- payload$con;
  state$offset <- payload$offset;
  state$dsize <- payload$dsize;
  state$endian <- payload$endian;
  state$gzipped <- payload$gzipped;
  state$skip <- as.integer(skip_tracks);
  state$offset_done <- FALSE;
  state$leftover <- numeric(0L);
  state$pending <- matrix(numeric(0L), nrow = 0L, ncol = values_per_point);
  state$buf.points <- matrix(numeric(0L), nrow = 0L, ncol = values_per_point);
  state$buf.starts <- integer(0L);
  state$buf.lengths <- integer(0L);
  state$buf.index <- 0L;
  state$terminator <- FALSE;
  state$eof <- FALSE;

  itr <- new.env(parent = emptyenv());
  itr$filepath <- filepath;
  itr$format <- if (payload$is_tck) "tck" else "tsf";
  itr$tracks.read <- 0L;
  itr$closed <- FALSE;
  itr$bbox <- bbox;
  itr$values_per_point <- values_per_point;
  itr$state <- state;

  itr$next.track <- function() {
    if (itr$closed) {
      return(NULL);
    }
    if (!state$offset_done) {
      skip.connection.bytes(state$con, state$offset, state$gzipped, filepath);
      state$offset_done <- TRUE;
    }

    repeat {
      # Serve the streamlines that were parsed from the chunk that is buffered.
      if (state$buf.index < length(state$buf.lengths)) {
        state$buf.index <- state$buf.index + 1L;
        start <- state$buf.starts[state$buf.index];
        num_points <- state$buf.lengths[state$buf.index];
        points <- state$buf.points[start:(start + num_points - 1L), , drop = FALSE];
        itr$tracks.read <- itr$tracks.read + 1L;
        if (identical(itr$format, "tsf")) {
          return(as.vector(points));
        }
        return(points);
      }
      if (state$eof) {
        return(NULL);
      }

      chunk <- readBin(con = state$con, what = numeric(), n = chunk_values, size = state$dsize,
                       endian = state$endian);
      if (length(chunk) == 0L) {
        # Running out of data is a normal end of the streamlines, see
        # read.mrtrix.stream(). A trailing incomplete streamline is dropped.
        state$eof <- TRUE;
        return(NULL);
      }

      values <- if (length(state$leftover) == 0L) chunk else c(state$leftover, chunk);
      num_values <- length(values);
      num_rows <- num_values %/% values_per_point;
      if (num_rows == 0L) {
        state$leftover <- values;
        next;
      }
      mat <- matrix(values[seq_len(num_rows * values_per_point)], ncol = values_per_point, byrow = TRUE);
      state$leftover <- if (num_values > num_rows * values_per_point) {
        values[(num_rows * values_per_point + 1L):num_values];
      } else {
        numeric(0L);
      }

      termin <- match(TRUE, infinite.rows(mat));
      if (!is.na(termin)) {
        state$terminator <- TRUE;
        mat <- if (termin > 1L) mat[seq_len(termin - 1L), , drop = FALSE] else {
          matrix(numeric(0L), nrow = 0L, ncol = values_per_point);
        };
      }

      groups <- split.mrtrix.chunk(mat, state$pending, finish_pending = state$terminator);
      state$pending <- groups$pending;
      points <- groups$points;
      lengths <- groups$lengths;

      if (state$skip > 0L && length(lengths) > 0L) {
        to_drop <- min(state$skip, length(lengths));
        state$skip <- state$skip - to_drop;
        keep <- if (to_drop >= length(lengths)) integer(0L) else seq.int(to_drop + 1L, length(lengths));
        selected <- subset.groups(points, lengths, keep);
        points <- selected$points;
        lengths <- selected$lengths;
      }
      if (!is.null(itr$bbox) && length(lengths) > 0L) {
        inside <- groups.in.bbox(points, lengths, itr$bbox);
        if (!all(inside)) {
          selected <- subset.groups(points, lengths, which(inside));
          points <- selected$points;
          lengths <- selected$lengths;
        }
      }

      state$buf.points <- points;
      state$buf.lengths <- as.integer(lengths);
      state$buf.starts <- group.start.rows(state$buf.lengths);
      state$buf.index <- 0L;
      if (state$terminator) {
        state$eof <- TRUE;
      }
    }
  };

  itr$close <- function() {
    if (!itr$closed) {
      try(close(state$con), silent = TRUE);
      itr$closed <- TRUE;
    }
    return(invisible(NULL));
  };

  class(itr) <- "dti.track.iterator";
  reg.finalizer(itr, function(e) {
    if (!isTRUE(e$closed)) {
      try(close(e$state$con), silent = TRUE);
      e$closed <- TRUE;
    }
  }, onexit = TRUE);

  return(itr);
}


#' @title Create an iterator over the tracts of a TRK file.
#'
#' @param filepath character string, path to the file.
#'
#' @param skip_tracks integer, the number of tracts to skip.
#'
#' @param bbox numeric vector of length 6 or NULL, a bounding box, see
#'   \code{read.dti.tck}.
#'
#' @return the iterator environment.
#'
#' @keywords internal
trk.track.iterator <- function(filepath, skip_tracks = 0L, bbox = NULL) {
  endian <- get.dti.trk.endianness(filepath);
  trk_header <- read.dti.trk.header(filepath);
  values_per_point <- 3L + trk_header$n_scalars;
  num_properties <- trk_header$n_properties;

  gzipped <- is.gzip.file(filepath);
  con <- open.maybe.gzip(filepath, gzipped, mode = "rb");
  skip.connection.bytes(con, trk_header$hdr_size, gzipped, filepath);

  state <- new.env(parent = emptyenv());
  state$con <- con;
  state$endian <- endian;
  state$gzipped <- gzipped;
  state$skip <- as.integer(skip_tracks);

  itr <- new.env(parent = emptyenv());
  itr$filepath <- filepath;
  itr$format <- "trk";
  itr$tracks.read <- 0L;
  itr$closed <- FALSE;
  itr$bbox <- bbox;
  itr$values_per_point <- values_per_point;
  itr$n_scalars <- trk_header$n_scalars;
  itr$n_properties <- num_properties;
  itr$state <- state;

  itr$next.track <- function() {
    if (itr$closed) {
      return(NULL);
    }

    repeat {
      num_points <- readBin(state$con, integer(), n = 1L, size = 4L, endian = state$endian);
      if (length(num_points) == 0L) {
        return(NULL);
      }
      if (is.na(num_points) || num_points < 0L) {
        stop(sprintf("Invalid point count %s in TRK file '%s', the file is corrupt.\n", num_points, filepath));
      }
      # A track with zero points is legal and is returned like any other track,
      # see read.trk.records(). Its properties still have to be consumed to stay
      # in sync with the file.

      if (state$skip > 0L && is.null(itr$bbox)) {
        # The records are self-delimiting, so a skipped tract does not have to
        # be read at all.
        skip.connection.bytes(state$con, 4 * num_points * values_per_point, state$gzipped, filepath);
        if (num_properties > 0L) {
          skip.connection.bytes(state$con, 4 * num_properties, state$gzipped, filepath);
        }
        state$skip <- state$skip - 1L;
        next;
      }

      record <- readBin(state$con, numeric(), n = num_points * values_per_point, size = 4L,
                        endian = state$endian);
      if (length(record) != num_points * values_per_point) {
        stop(sprintf("Truncated TRK file: a tract is incomplete.\n"));
      }
      record <- matrix(record, ncol = values_per_point, byrow = TRUE);

      properties <- NULL;
      if (num_properties > 0L) {
        properties <- readBin(state$con, numeric(), n = num_properties, size = 4L, endian = state$endian);
        if (length(properties) != num_properties) {
          stop(sprintf("Truncated TRK file: a tract is missing its properties.\n"));
        }
      }

      if (state$skip > 0L) {
        state$skip <- state$skip - 1L;
        next;
      }

      coords <- record[, seq_len(3L), drop = FALSE];
      if (!is.null(itr$bbox)) {
        inside <- coords[, 1L] >= itr$bbox[1L] & coords[, 1L] <= itr$bbox[2L] &
          coords[, 2L] >= itr$bbox[3L] & coords[, 2L] <= itr$bbox[4L] &
          coords[, 3L] >= itr$bbox[5L] & coords[, 3L] <= itr$bbox[6L];
        if (!any(inside)) {
          next;
        }
      }

      scalars <- NULL;
      if (itr$n_scalars > 0L) {
        scalars <- record[, (3L + 1L):values_per_point, drop = FALSE];
      }
      itr$tracks.read <- itr$tracks.read + 1L;
      return(list("scalars" = scalars, "properties" = properties, "coords" = coords,
                  "num_points" = num_points));
    }
  };

  itr$close <- function() {
    if (!itr$closed) {
      try(close(state$con), silent = TRUE);
      itr$closed <- TRUE;
    }
    return(invisible(NULL));
  };

  class(itr) <- "dti.track.iterator";
  reg.finalizer(itr, function(e) {
    if (!isTRUE(e$closed)) {
      try(close(e$state$con), silent = TRUE);
      e$closed <- TRUE;
    }
  }, onexit = TRUE);

  return(itr);
}


#' @title Print a tract file iterator.
#'
#' @param x a \code{dti.track.iterator} instance.
#'
#' @param ... ignored.
#'
#' @return the iterator, invisibly.
#'
#' @export
print.dti.track.iterator <- function(x, ...) {
  cat(sprintf("dti.track.iterator for %s (%s format), %d tract(s) read so far%s\n",
              x$filepath, x$format, x$tracks.read, if (isTRUE(x$closed)) ", closed." else "."));
  cat("Use $next.track() to read the next tract and $close() when done.\n");
  return(invisible(x));
}

Try the freesurferformats package in your browser

Any scripts or data that you put into this service are public.

freesurferformats documentation built on Sept. 25, 2026, 1:07 a.m.