AV1 video encoding for biological microscopy data.
AV1R is an R package for biologists that converts legacy microscopy video formats (H.264/H.265, AVI/MJPEG, TIFF stacks) to the modern AV1 codec with minimal quality loss.
AV1R automatically selects the best available backend:
| Priority | Backend | How |
|---|---|---|
| 1 | VAAPI |
av1_vaapi via FFmpeg — best speed/quality (AMD/Intel) |
| 2 | Vulkan |
VK_KHR_VIDEO_ENCODE_AV1 — fastest, CQP only |
| 3 | CPU (SVT-AV1) |
libsvtav1 via FFmpeg |
| GPU | Driver | Status |
|---|---|---|
| AMD Radeon RX 9070 (RDNA4) | Mesa RADV (GFX1201) | Working |
Vulkan AV1 encode headers are bundled in src/vk_video/, so no SDK upgrade is needed at build time. Builds with any Vulkan SDK >= 1.3.275. Runtime support depends on GPU driver.
| Backend | Time (s) | Size (MB) | Compression | SSIM | FPS | vs CPU |
|---|---|---|---|---|---|---|
| Vulkan | 2.9 | 3.31 | 1.9x | 0.9339 | 245.9 | 4.0x |
| VAAPI | 2.0 | 3.97 | 1.6x | 0.9609 | 363.3 | 5.9x |
| CPU (SVT-AV1) | 11.7 | 8.09 | 0.8x | 0.9645 | 61.6 | 1.0x |
Compression = input clip size / encoded size. SSIM >= 0.97 visually transparent. vs CPU = CPU time / backend time (higher = faster).
Reproduce: Rscript inst/examples/benchmark_backends.R
library(AV1R)
# Convert microscopy recording
convert_to_av1("recording.mp4", "recording_av1.mp4")
# TIFF stack with custom settings
convert_to_av1("stack.tif", "stack.mp4", av1r_options(crf = 20))
# Batch convert entire experiment folder
convert_folder("experiment/", "compressed/")
# Lossless archive (FFV1 in MKV): 16-bit TIFF stack, or a video as gray16
archive_tiff("stack.tif")
archive_video("recording.avi", audio = TRUE)
# Check what backend will be used
detect_backend()
# Check GPU availability
vulkan_available()
vulkan_devices()
# Force GPU backend
convert_to_av1("input.mp4", "output.mp4", av1r_options(backend = "vulkan"))
# Force CPU backend
convert_to_av1("input.mp4", "output.mp4", av1r_options(backend = "cpu"))
Each backend uses the quality control approach best suited to its rate control:
| Backend | Video input | TIFF/image input |
|---|---|---|
| CPU (SVT-AV1) | CRF directly | CRF directly |
| Vulkan | CRF directly (CQP) | CRF directly (CQP) |
| VAAPI | VBR at 55% of input bitrate | CQP (CRF mapped to QP 0–255) |
Frames smaller than the hardware minimum coded extent are automatically scaled up proportionally (no distortion). Audio from the original file is preserved automatically.
CRF is a unified quality parameter (0 = best, 63 = worst). Since each backend uses a different internal quantizer scale, AV1R maps CRF automatically. The table below shows equivalent settings calibrated by file size:
| CRF | CPU (KB) | VAAPI QP | VAAPI (KB) | Vulkan CRF | Vulkan (KB) |
|---|---|---|---|---|---|
| 1 | 3815.5 | 8 | 3800.8 | 2 | 3369.2 |
| 3 | 2766.5 | 11 | 2822.4 | 2 | 3369.2 |
| 5 | 1983.7 | 14 | 1982.1 | 3 | 2064.3 |
| 8 | 924.4 | 24 | 952.6 | 5 | 955.0 |
| 10 | 578.7 | 31 | 597.1 | 7 | 548.9 |
| 15 | 264.4 | 50 | 278.7 | 11 | 260.5 |
| 20 | 162.1 | 75 | 156.5 | 15 | 163.8 |
| 30 | 84.4 | 110 | 82.0 | 30 | 61.9 |
| 40 | 53.2 | 150 | 53.3 | 40 | 46.9 |
| 50 | 38.8 | 190 | 38.8 | 50 | 37.6 |
| 63 | 25.4 | 255 | 29.5 | 63 | 30.5 |
Reproduce: Rscript inst/examples/calibrate_vaapi_cqp.R
Note: Vulkan uses CQP (constant quantizer) rate control — the only mode currently working on RADV. CQP does not adapt to scene complexity, so Vulkan produces slightly lower SSIM (~0.93) compared to CPU/VAAPI (~0.96). This is a driver limitation; VBR/CRF support will improve quality when RADV implements it.
| Format | Extension |
|---|---|
| H.264 / MP4 |
.mp4, .mkv, .mov
|
| H.265 / HEVC |
.mp4, .mkv
|
| AVI / MJPEG | .avi |
| FLV | .flv |
| MPEG-1/2 |
.mpg, .mpeg
|
| WebM (VP8/VP9) | .webm |
| TIFF stack |
.tif, .tiff
|
| TIFF sequence | frame%04d.tif |
libsvtav1 (required for CPU encoding); ffmpeg
and ffprobe are also used by archive_video() and for verify = TRUE
tiff (optional, for archive_tiff())archive_tiff() and
archive_video())# Ubuntu 22.04+
sudo apt install ffmpeg libvulkan-dev
glslc (shaderc) is needed only at install time, for the GPU path of the
FFV1 archive. Install it from your distribution if packaged
(sudo apt install glslc on recent Ubuntu/Debian) or from the
Vulkan SDK; configure also looks in
$GLSLC and $VULKAN_SDK/bin. Without it the CPU encoder is used.
If the Vulkan SDK was installed from the LunarG tarball, source its environment in the same shell before installing:
source /path/to/vulkan-sdk/<version>/setup-env.sh
TIFF stacks use near-lossless CRF 5 by default (tiff_crf). Output resolution can be controlled via tiff_scale.
Note for quantitative microscopy: AV1 is a lossy codec even at CRF 1. If your workflow relies on pixel intensity measurements (fluorescence quantification, ratiometric imaging, colocalization analysis), lossy compression will alter the data. For such use cases, keep the original TIFF/PNG files for analysis and use AV1R only for visualization, sharing, and archival of preview copies. For lossless archiving use
archive_tiff()(FFV1, see below).
# 2x magnification (proportional, no distortion)
convert_to_av1("stack.tif", "stack.mp4", av1r_options(tiff_scale = 2))
# Fit into 1920x1080 bounding box (proportional)
convert_to_av1("stack.tif", "stack.mp4", av1r_options(tiff_scale = c(1920, 1080)))
See inst/examples/tiff_scale.R for more examples.
archive_tiff() stores 16-bit grayscale TIFF stacks losslessly as FFV1
version 3 (RFC 9043) in Matroska.
Decoding gives back the original pixel values bit for bit, so the archive
can replace the TIFF for quantitative analysis. archive_video() does the
same for video files (see below).
# stack.tif -> stack.mkv + stack.mkv.json
archive_tiff("stack.tif")
# Decode with FFmpeg and compare bit-exactly with the input
archive_tiff("stack.tif", "archive/stack.mkv", verify = TRUE)
# Force the encoder: "auto" (default, GPU if available), "vulkan" or "cpu"
archive_tiff("stack.tif", backend = "cpu")
# Slice raster and context model
archive_tiff("stack.tif", slices = c(4L, 4L), context_model = 0L)
archive_video() decodes a video with FFmpeg and archives its luma (Y)
channel as 16-bit grayscale FFV1. Frames are decoded and encoded on the
fly in chunks, so long recordings need no more memory than one chunk.
When to use it. FFV1 is lossless: it keeps every pixel of the decoded
frames, including the noise and artefacts of a lossy codec. Use
archive_video() for lossless or uncompressed sources (raw or
uncompressed AVI, scientific camera recordings), where it saves space
without loss. For lossy sources (MPEG-4, H.264, H.265, ...) the archive
is much larger than the original — e.g. about 25x per frame for an MPEG-4
AVI — so keep the original file as the master copy instead.
# recording.avi -> recording.mkv + recording.mkv.json (video only)
archive_video("recording.avi", verify = TRUE)
# Keep the sound track: audio streams are copied unchanged
archive_video("recording.avi", audio = TRUE)
gray16le:
colour (chroma) is not stored, and 8-bit luma is scaled to 16 bits by
FFmpeg. A colour source is reported in the messages.-vsync passthrough), at
the source's exact nominal frame rate (e.g. 24000/1001). Variable frame
rate timing is not preserved.audio = FALSE (default) writes the video only. With audio = TRUE the
audio streams are added after encoding by an FFmpeg remux (stream copy,
one extra pass over the file); the FFV1 video stays byte-identical.verify = TRUE decodes the archive and the source again and compares
the grayscale frames chunk by chunk.AV1R has its own FFV1 encoder and Matroska writer: for archive_tiff()
FFmpeg is needed only for verify = TRUE and to decode the archive later.
The FFV1 bitstream is byte-identical to FFmpeg's encoder (checked packet by
packet in the tests, FFmpeg 6.1) for
ffmpeg -f rawvideo -pix_fmt gray16le -s WxH -i stack.raw \
-c:v ffv1 -level 3 -coder 1 -context 0 -slices 4 -slicecrc 1 -g 1 stack.mkv
with -slices = slices[1] * slices[2] and -context = context_model.
The track is stored as FFmpeg stores FFV1 in Matroska (CodecID
V_MS/VFW/FOURCC), so archives play in FFmpeg-based tools, VLC,
GStreamer and mpv / Celluloid.
Every frame is a keyframe and every slice carries a CRC, so a damaged frame
does not affect the others. To read the samples back:
ffmpeg -i stack.mkv -map 0:v:0 -vsync passthrough -f rawvideo -pix_fmt gray16le stack.raw
With backend = "vulkan" (or "auto" when a GPU is found) slices are
encoded by Vulkan compute shaders. GPU and CPU produce byte-identical
archives. The GPU path is built when Vulkan headers and glslc are found
at install time; the device used is recorded in the sidecar
(encoder_backend).
Set AV1R_FFV1_PROFILE=1 to print the memory types of the GPU buffers and
the time per stage (conversion, upload, dispatch, gather, mux) to stderr
when an archive is closed. inst/examples/profile_ffv1_shader.R times the
shader alone for several slice rasters and batch sizes.
Slices are coded independently: more slices give more parallelism but
worse compression, especially on small frames, because every slice starts
with untrained contexts. context_model = 1 adapts slowly and pays off
only on large frames. Compression ratio on a small-frame stack (510 frames
171x196, context_model = 0):
| slices | ratio |
|---|---|
| 2x2 | 1.35 |
| 4x4 | 1.22 |
| 8x8 | 1.08 |
Compression is identical to FFmpeg's. Since the GPU gets its parallelism
from batches of frames, the default c(2L, 2L) is fast on the GPU too.
inst/examples/benchmark_ffv1.R, slices 2x2, context_model = 0, AMD
RX 9070, FFmpeg 6.1.1, MB/s of 16-bit input, frames in memory.
| input | AV1R vulkan | AV1R cpu | ffmpeg -threads 1
|
ffmpeg |
|---|---|---|---|---|
| mitosis.tif, 171x196, 510 frames | 67.3 | 20.0 | 28.3 | 80.1 |
FFmpeg encodes slices on all CPU cores; the AV1R CPU encoder uses one.
<output>.mkv — FFV1 video track (plus the copied audio streams with
archive_video(audio = TRUE)); OME-XML from the TIFF ImageDescription is
attached as metadata.ome.xml.<output>.mkv.json (sidecar = TRUE) — codec parameters, frame size
and count, MD5 of the source and the archive, verification result and
FFmpeg version, encoder backend, AV1R and R versions, OME-XML; for video
also the source pixel format, the gray16 conversion and whether audio was
copied.archive_tiff() requires the tiff package; archive_video() requires
ffmpeg and ffprobe. Both work in chunks, so inputs larger than RAM
work.av1r_options(
crf = 28, # quality: 0 (best) - 63 (worst)
preset = 8, # speed: 0 (slow/best) - 13 (fast/worst), CPU only
threads = 0, # 0 = auto, CPU only
tiff_crf = 5, # CRF for TIFF input (near-lossless)
tiff_scale = NULL, # NULL, multiplier (e.g. 2), or c(width, height)
verbose = TRUE, # informational messages
backend = "auto" # "auto", "cpu", "vaapi", or "vulkan"
)
MIT