Task 759: .V File Format

Task 759: .V File Format

The .V file format refers to the Vicon V-File Format, a binary format used for storing motion capture trial data, including kinematics, parameters, and time-varying dynamic information. It is designed for flexibility, extensibility, and backwards compatibility, primarily in motion capture systems.

1. List of Properties Intrinsic to the .V File Format

The following properties are fundamental to the format's structure and handling, derived from its specifications. These include encoding, units, coordinate systems, and structural characteristics:

  • File Type: Binary, structured into a header, static data area (sections), and dynamic data area (records).
  • Endianness: Little-endian byte order for all multi-byte data types.
  • Identifier: Starts with ASCII characters 'V' (0x56) followed by '#' (0x23).
  • Version: Specified as a 16-bit short integer (little-endian); current standard version is 1.
  • Units: Fixed and standardized without per-file variation:
  • Angles: Radians.
  • Distances/Lengths/Positions: Millimeters (mm).
  • Forces: Newtons (N).
  • Masses: Kilograms (kg).
  • Moments: Newton-millimeters (Nmm).
  • Frame Rates: Hertz (Hz).
  • Coordinate System: Global system is Z-up.
  • Orientation Representation: Angle-axis format (three floats: X, Y, Z axis vector; vector magnitude represents rotation angle in radians).
  • Data Types: Supports byte (8-bit unsigned), short (16-bit signed), long (32-bit signed), float (32-bit IEEE 754), double (64-bit IEEE 754), boolean (byte: 0=false, nonzero=true), text (ASCII strings), and binary.
  • Section Structure: Static area consists of variable-length sections with 32-byte headers (4-byte length, 28-byte null-padded ID name); terminated by a zero-filled header.
  • Record Structure: Many sections are record-based, with 2-byte length prefixes; terminated by a zero-length record.
  • Dynamic Records: Include 2-byte length, 2-byte group ID, 4-byte frame number, followed by degrees-of-freedom (DOF) values; frame order may vary across groups.
  • DOF Naming Convention: Tags follow "{subject}:{entity} " format, where uppercase suffixes indicate global coordinates (e.g., P-X for position X) and lowercase indicate local (e.g., p-X); suffixes include P/p (position), A/a (angle-axis), F (force), M (moment), O (occluded flag), S (scaled analog), B (binary analog).
  • Extensibility: Optional zero-filled buffers allow future additions without file rewriting; unknown sections can be skipped.

The following are direct links to sample .V files from the Carnegie Mellon University Motion Capture Database, representing motion capture trials in Vicon format:

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .V File Dump

The following is a complete, self-contained HTML page with embedded JavaScript that allows a user to drag and drop a .V file. It parses the file according to the specifications and dumps the properties (header details, static sections including parameters and datagroups, and a summary of dynamic data) to the screen in a readable format.

.V File Parser
Drag and drop a .V file here

Note: This JavaScript implementation provides a basic parser for demonstration. In production, expand the parseRecord method to handle full parameter and datagroup details, including multi-dimensional data and all types.

4. Python Class for .V File Handling

The following Python class can open, decode, read, write (basic modifications), and print the properties to the console.

import struct
import os

class VFile:
    def __init__(self, filepath):
        self.filepath = filepath
        self.properties = {}
        self.data = b''
        self.parse()

    def read_byte(self): return struct.unpack('<B', self.data[self.offset:self.offset+1])[0]; self.offset += 1
    def read_short(self): return struct.unpack('<h', self.data[self.offset:self.offset+2])[0]; self.offset += 2
    def read_long(self): return struct.unpack('<i', self.data[self.offset:self.offset+4])[0]; self.offset += 4
    def read_float(self): return struct.unpack('<f', self.data[self.offset:self.offset+4])[0]; self.offset += 4
    def read_double(self): return struct.unpack('<d', self.data[self.offset:self.offset+8])[0]; self.offset += 8
    def read_string(self, len_): return self.data[self.offset:self.offset+len_].decode('ascii').rstrip('\x00'); self.offset += len_

    def parse_header(self):
        self.offset = 0
        id1, id2 = self.read_byte(), self.read_byte()
        version = self.read_short()
        if id1 != 0x56 or id2 != 0x23: raise ValueError('Invalid .V file')
        self.properties['version'] = version

    def parse_static_sections(self):
        sections = {}
        while True:
            length = self.read_long()
            id_ = self.read_string(28).strip('\x00')
            if length == 0 and not id_: break
            section_start = self.offset
            sections[id_] = self.parse_records()
            self.offset = section_start + length
        self.properties['sections'] = sections

    def parse_records(self):
        records = []
        while True:
            rec_len = self.read_short()
            if rec_len == 0: break
            rec_start = self.offset
            record = self.parse_record(rec_len)
            records.append(record)
            self.offset = rec_start + rec_len
        return records

    def parse_record(self, rec_len):
        # Simplified parsing; extend for full details
        record = {}
        record['name_len'] = self.read_byte()
        record['name'] = self.read_string(record['name_len'])
        record['type'] = self.read_byte()
        record['dims'] = self.read_byte()
        # Additional parsing omitted for brevity
        return record

    def parse_dynamic(self):
        dynamic = []
        while self.offset < len(self.data):
            len_ = self.read_short()
            if len_ == 0: break  # Safety
            group_id = self.read_short()
            frame_num = self.read_long()
            dynamic.append({'group_id': group_id, 'frame_num': frame_num})
            self.offset += len_ - 6  # Skip values
        self.properties['dynamic_summary'] = dynamic

    def parse(self):
        with open(self.filepath, 'rb') as f:
            self.data = f.read()
        self.parse_header()
        self.parse_static_sections()
        self.parse_dynamic()

    def print_properties(self):
        print('Properties:')
        print(f"Version: {self.properties.get('version')}")
        print('Sections:')
        for id_, recs in self.properties.get('sections', {}).items():
            print(f"  {id_}: {len(recs)} records")
        print(f"Dynamic Summary: {len(self.properties.get('dynamic_summary', []))} records")

    def write(self, new_filepath=None):
        # Basic write: saves current data; extend for modifications
        filepath = new_filepath or self.filepath
        with open(filepath, 'wb') as f:
            f.write(self.data)

# Example usage:
# v = VFile('example.v')
# v.print_properties()
# v.write('modified.v')

Note: This class provides basic reading and writing. For full decoding, expand parse_record to handle all data types and dimensions.

5. Java Class for .V File Handling

The following Java class can open, decode, read, write, and print the properties to the console.

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.file.*;

public class VFile {
    private String filepath;
    private ByteBuffer buffer;
    private final ByteOrder order = ByteOrder.LITTLE_ENDIAN;
    private final java.util.Map<String, Object> properties = new java.util.HashMap<>();

    public VFile(String filepath) throws IOException {
        this.filepath = filepath;
        byte[] data = Files.readAllBytes(Paths.get(filepath));
        buffer = ByteBuffer.wrap(data).order(order);
        parse();
    }

    private int readByte() { return buffer.get() & 0xFF; }
    private short readShort() { return buffer.getShort(); }
    private int readLong() { return buffer.getInt(); }
    private float readFloat() { return buffer.getFloat(); }
    private double readDouble() { return buffer.getDouble(); }
    private String readString(int len) {
        byte[] bytes = new byte[len];
        buffer.get(bytes);
        int nullPos = 0;
        for (; nullPos < len && bytes[nullPos] != 0; nullPos++);
        return new String(bytes, 0, nullPos);
    }

    private void parseHeader() {
        int id1 = readByte(), id2 = readByte();
        short version = readShort();
        if (id1 != 0x56 || id2 != 0x23) throw new RuntimeException("Invalid .V file");
        properties.put("version", (int) version);
    }

    private void parseStaticSections() {
        java.util.Map<String, java.util.List<java.util.Map<String, Object>>> sections = new java.util.HashMap<>();
        while (true) {
            int length = readLong();
            String id = readString(28).trim();
            int pos = buffer.position();
            if (length == 0 && id.isEmpty()) break;
            java.util.List<java.util.Map<String, Object>> records = parseRecords();
            sections.put(id, records);
            buffer.position(pos + length);
        }
        properties.put("sections", sections);
    }

    private java.util.List<java.util.Map<String, Object>> parseRecords() {
        java.util.List<java.util.Map<String, Object>> records = new java.util.ArrayList<>();
        while (true) {
            short recLen = readShort();
            if (recLen == 0) break;
            int pos = buffer.position();
            java.util.Map<String, Object> record = parseRecord(recLen);
            records.add(record);
            buffer.position(pos + recLen);
        }
        return records;
    }

    private java.util.Map<String, Object> parseRecord(short recLen) {
        java.util.Map<String, Object> record = new java.util.HashMap<>();
        int nameLen = readByte();
        record.put("name", readString(nameLen));
        record.put("type", readByte());
        record.put("dims", readByte());
        // Extend for full parsing
        return record;
    }

    private void parseDynamic() {
        java.util.List<java.util.Map<String, Object>> dynamic = new java.util.ArrayList<>();
        while (buffer.hasRemaining()) {
            short len = readShort();
            if (len == 0) break; // Safety
            short groupId = readShort();
            int frameNum = readLong();
            java.util.Map<String, Object> rec = new java.util.HashMap<>();
            rec.put("groupId", (int) groupId);
            rec.put("frameNum", frameNum);
            dynamic.add(rec);
            buffer.position(buffer.position() + len - 6);
        }
        properties.put("dynamicSummary", dynamic);
    }

    private void parse() {
        parseHeader();
        parseStaticSections();
        parseDynamic();
    }

    public void printProperties() {
        System.out.println("Properties:");
        System.out.println("Version: " + properties.get("version"));
        System.out.println("Sections:");
        @SuppressWarnings("unchecked")
        java.util.Map<String, java.util.List<?>> sections = (java.util.Map<String, java.util.List<?>>) properties.get("sections");
        if (sections != null) {
            for (java.util.Map.Entry<String, java.util.List<?>> entry : sections.entrySet()) {
                System.out.println("  " + entry.getKey() + ": " + entry.getValue().size() + " records");
            }
        }
        @SuppressWarnings("unchecked")
        java.util.List<?> dynamic = (java.util.List<?>) properties.get("dynamicSummary");
        System.out.println("Dynamic Summary: " + (dynamic != null ? dynamic.size() : 0) + " records");
    }

    public void write(String newFilepath) throws IOException {
        // Basic write; extend for modifications
        Files.write(Paths.get(newFilepath != null ? newFilepath : filepath), buffer.array());
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     VFile v = new VFile("example.v");
    //     v.printProperties();
    //     v.write("modified.v");
    // }
}

Note: This class provides basic functionality. Expand parseRecord for comprehensive type and dimension handling.

6. JavaScript Class for .V File Handling

The following JavaScript class (Node.js compatible) can open, decode, read, write, and print the properties to the console.

const fs = require('fs');

class VFile {
    constructor(filepath) {
        this.filepath = filepath;
        this.properties = {};
        this.buffer = fs.readFileSync(filepath);
        this.dataView = new DataView(this.buffer.buffer);
        this.offset = 0;
        this.parse();
    }

    readByte() { return this.dataView.getUint8(this.offset++); }
    readShort() { return this.dataView.getInt16(this.offset, true); this.offset += 2; }
    readLong() { return this.dataView.getInt32(this.offset, true); this.offset += 4; }
    readFloat() { return this.dataView.getFloat32(this.offset, true); this.offset += 4; }
    readDouble() { return this.dataView.getFloat64(this.offset, true); this.offset += 8; }
    readString(len) {
        let str = '';
        for (let i = 0; i < len; i++) {
            const char = this.readByte();
            if (char === 0) break;
            str += String.fromCharCode(char);
        }
        return str;
    }

    parseHeader() {
        const id1 = this.readByte();
        const id2 = this.readByte();
        const version = this.readShort();
        if (id1 !== 0x56 || id2 !== 0x23) throw new Error('Invalid .V file');
        this.properties.version = version;
    }

    parseStaticSections() {
        const sections = {};
        while (true) {
            const length = this.readLong();
            const id = this.readString(28).trim();
            const pos = this.offset;
            if (length === 0 && id === '') break;
            sections[id] = this.parseRecords();
            this.offset = pos + length;
        }
        this.properties.sections = sections;
    }

    parseRecords() {
        const records = [];
        while (true) {
            const recLen = this.readShort();
            if (recLen === 0) break;
            const pos = this.offset;
            const record = this.parseRecord(recLen);
            records.push(record);
            this.offset = pos + recLen;
        }
        return records;
    }

    parseRecord(recLen) {
        const record = {};
        record.nameLen = this.readByte();
        record.name = this.readString(record.nameLen);
        record.type = this.readByte();
        record.dims = this.readByte();
        // Extend for full parsing
        return record;
    }

    parseDynamic() {
        const dynamic = [];
        while (this.offset < this.dataView.byteLength) {
            const len = this.readShort();
            if (len === 0) break; // Safety
            const groupId = this.readShort();
            const frameNum = this.readLong();
            dynamic.push({ groupId, frameNum });
            this.offset += len - 6;
        }
        this.properties.dynamicSummary = dynamic;
    }

    parse() {
        this.parseHeader();
        this.parseStaticSections();
        this.parseDynamic();
    }

    printProperties() {
        console.log('Properties:');
        console.log(`Version: ${this.properties.version}`);
        console.log('Sections:');
        for (const [id, recs] of Object.entries(this.properties.sections || {})) {
            console.log(`  ${id}: ${recs.length} records`);
        }
        console.log(`Dynamic Summary: ${this.properties.dynamicSummary ? this.properties.dynamicSummary.length : 0} records`);
    }

    write(newFilepath = null) {
        fs.writeFileSync(newFilepath || this.filepath, this.buffer);
    }
}

// Example usage:
// const v = new VFile('example.v');
// v.printProperties();
// v.write('modified.v');

Note: This class is for Node.js. Expand for browser if needed, and enhance parsing depth.

7. C Class (Struct-Based) for .V File Handling

The following C implementation uses a struct as a "class" equivalent, with functions to open, decode, read, write, and print properties to the console.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>

typedef struct {
    char* filepath;
    uint8_t* data;
    size_t size;
    size_t offset;
    int version;
    // Simplified: use maps or arrays for sections/dynamic; here use placeholders
    int section_count; // Example property
    int dynamic_count; // Example property
} VFile;

bool read_file(VFile* vf, const char* filepath) {
    FILE* f = fopen(filepath, "rb");
    if (!f) return false;
    fseek(f, 0, SEEK_END);
    vf->size = ftell(f);
    fseek(f, 0, SEEK_SET);
    vf->data = malloc(vf->size);
    fread(vf->data, 1, vf->size, f);
    fclose(f);
    vf->filepath = strdup(filepath);
    vf->offset = 0;
    return true;
}

uint8_t read_byte(VFile* vf) { return vf->data[vf->offset++]; }
int16_t read_short(VFile* vf) {
    int16_t val = *(int16_t*)(vf->data + vf->offset);
    vf->offset += 2;
    return val;
}
int32_t read_long(VFile* vf) {
    int32_t val = *(int32_t*)(vf->data + vf->offset);
    vf->offset += 4;
    return val;
}
float read_float(VFile* vf) {
    float val = *(float*)(vf->data + vf->offset);
    vf->offset += 4;
    return val;
}
double read_double(VFile* vf) {
    double val = *(double*)(vf->data + vf->offset);
    vf->offset += 8;
    return val;
}
char* read_string(VFile* vf, int len) {
    char* str = malloc(len + 1);
    memcpy(str, vf->data + vf->offset, len);
    str[len] = '\0';
    for (int i = 0; i < len; i++) if (str[i] == 0) str[i] = '\0';
    vf->offset += len;
    return str;
}

void parse_header(VFile* vf) {
    uint8_t id1 = read_byte(vf);
    uint8_t id2 = read_byte(vf);
    vf->version = read_short(vf);
    if (id1 != 0x56 || id2 != 0x23) {
        fprintf(stderr, "Invalid .V file\n");
        exit(1);
    }
}

void parse_static_sections(VFile* vf) {
    int sections = 0;
    while (true) {
        int32_t length = read_long(vf);
        char* id = read_string(vf, 28);
        size_t pos = vf->offset;
        if (length == 0 && strlen(id) == 0) {
            free(id);
            break;
        }
        // Parse records (simplified count)
        while (true) {
            int16_t rec_len = read_short(vf);
            if (rec_len == 0) break;
            vf->offset += rec_len;
        }
        sections++;
        free(id);
        vf->offset = pos + length;
    }
    vf->section_count = sections;
}

void parse_dynamic(VFile* vf) {
    int count = 0;
    while (vf->offset < vf->size) {
        int16_t len = read_short(vf);
        if (len == 0) break; // Safety
        read_short(vf); // groupId
        read_long(vf); // frameNum
        vf->offset += len - 6;
        count++;
    }
    vf->dynamic_count = count;
}

void parse(VFile* vf) {
    parse_header(vf);
    parse_static_sections(vf);
    parse_dynamic(vf);
}

void print_properties(VFile* vf) {
    printf("Properties:\n");
    printf("Version: %d\n", vf->version);
    printf("Section Count: %d\n", vf->section_count);
    printf("Dynamic Count: %d\n", vf->dynamic_count);
}

void write_file(VFile* vf, const char* new_filepath) {
    const char* path = new_filepath ? new_filepath : vf->filepath;
    FILE* f = fopen(path, "wb");
    fwrite(vf->data, 1, vf->size, f);
    fclose(f);
}

void destroy(VFile* vf) {
    free(vf->data);
    free(vf->filepath);
}

// Example usage:
// int main() {
//     VFile vf;
//     if (!read_file(&vf, "example.v")) return 1;
//     parse(&vf);
//     print_properties(&vf);
//     write_file(&vf, "modified.v");
//     destroy(&vf);
//     return 0;
// }

Note: This C implementation is basic and uses little-endian assumptions (no explicit byte swapping needed on little-endian systems). Expand for full record parsing and dynamic memory management for complex structures.