Turn ordinary video into calibrated, analysis-ready motion data.
Track an object, correct for lens distortion, and export every frame as real-world coordinates.
VideoGrapher is a focused, interactive alternative to heavyweight video-analysis software. Give it a video and a known-length reference stick; it gives you a tracked video plus timestamped position data in pixels and centimeters.
It is especially handy for classroom physics, motion experiments, and quick computer-vision measurements where lens distortion makes a single pixels-per-cm ratio too crude.
| Capability | What you get | |
|---|---|---|
| 🎯 | CSRT object tracking | Robust frame-by-frame tracking from one hand-drawn bounding box |
| 📐 | Interactive calibration | A known-length ruler becomes a real-world coordinate system |
| 🪄 | Distortion correction | Draggable 10 cm ticks create a local, piecewise-linear scale |
| ⊹ | Nine reference points | Track any corner, edge midpoint, or the center of the object |
| 📦 | Useful artifacts | An annotated MP4 and a structured position_data.json |
| 📈 | Analysis pipeline | Export CSV, reject timestamp noise, and isolate linear motion |
Important
VideoGrapher uses interactive OpenCV windows and requires a graphical desktop. The object must be visible in the video's first frame.
uv is the shortest path:
git clone https://github.com/ThatXliner/videographer.git
cd videographer
uv syncOr create your own Python 3.12+ environment and install the two runtime dependencies:
python -m pip install "opencv-contrib-python>=4.8.0" "numpy>=1.24.0"Note
Use opencv-contrib-python, not opencv-python: the tracker depends on the
contrib build's CSRT implementation.
uv run main.py input.mp4 tracked.mp4VideoGrapher opens a guided interface:
- Mark both ends of a reference stick.
- Drag the generated ticks onto the stick's real markings.
- Draw a tight box around the object.
- Choose the point on that box you want to measure.
- Let the tracker process the video.
When it finishes, the working directory contains:
tracked.mp4 annotated video with bounding box and position overlay
position_data.json timestamps, coordinates, bounding boxes, and calibration
Extract the tracked horizontal position as tab-separated data:
uv run to_csv.py position_data.json --axis x --header > position.tsvOr extract the bottom of the box on the vertical axis, subtracting a 1 cm offset:
uv run to_csv.py position_data.json \
--reference bbox_bottom \
--axis y \
--offset 1 \
--header > position.tsv┌──────────────┐ ┌────────────────┐ ┌──────────────┐ ┌─────────────┐
│ Mark a ruler │ → │ Adjust 10 cm │ → │ Select and │ → │ Export MP4 │
│ in frame 1 │ │ calibration │ │ track object │ │ + JSON │
└──────────────┘ └────────────────┘ └──────────────┘ └─────────────┘
pixels local scale in cm CSRT per frame analysis
A single global scale assumes every centimeter occupies the same number of pixels. Real lenses often break that assumption.
VideoGrapher divides the reference stick into 10 cm segments. You can drag each tick to its observed position, giving every segment its own local scale. For a tracked point, the app finds the nearest segment, projects the point onto it, interpolates the along-stick coordinate, and uses that segment's scale for the perpendicular distance.
This approximation handles moderate barrel, pincushion, fisheye, and perspective distortion without requiring a full camera-calibration rig.
Tip
Put the reference stick in the same plane as the motion. Calibration cannot compensate for depth differences between the ruler and the tracked object.
1 top-left 2 top-center 3 top-right
4 center-left 5 center 6 center-right
7 bottom-left 8 bottom-center 9 bottom-right
The default is bottom-center, which works well for an object moving along a surface. Use the center for projectile motion, or an edge when measuring a leading/trailing boundary.
uv run main.py VIDEO [OUTPUT] [--stick-length CM] [--no-calibrate]| Argument | Default | Description |
|---|---|---|
VIDEO |
required | Input video path |
OUTPUT |
output.mp4 |
Annotated output video path |
--stick-length CM |
100.0 |
Length of the visible reference stick |
--no-calibrate |
off | Skip calibration and retain pixel-only tracking |
During selection, press Enter to confirm, R to reset, or Esc to cancel. During tracking, press Q to stop early.
position_data.json preserves both the experiment metadata and the per-frame
measurements:
{
"metadata": {
"video_path": "input.mp4",
"output_path": "tracked.mp4",
"total_frames": 300,
"calibrated": true,
"reference_point": "bottom-center",
"calibration": {
"reference_length_cm": 100.0,
"tick_positions": [[120, 640, 0], [198, 632, 10]],
"method": "piecewise linear interpolation with lens distortion correction"
}
},
"tracking_data": [
{
"frame": 0,
"timestamp": 0.0,
"reference_point": "bottom-center",
"position_x_pixels": 320,
"position_y_pixels": 450,
"position_x_cm": 42.5,
"position_y_cm": 58.3,
"bbox_pixels": {"x": 280, "y": 400, "w": 80, "h": 50},
"bbox_cm": {"x": 37.2, "y": 51.8, "w": 10.6, "h": 6.5}
}
]
}Pixel values are always retained. Centimeter fields and bbox_cm are populated
when calibration is enabled.
The repository includes three small command-line tools for moving from raw tracking output to analysis-ready data.
uv run to_csv.py position_data.json [options]| Option | Default | Description |
|---|---|---|
-r, --reference |
position |
position, a box edge, or a box center |
-a, --axis |
x |
Extract the x or y axis |
-o, --offset |
0.0 |
Subtract an offset from each position |
--header |
off | Include timestamp and position_cm |
--delimiter |
tab | Output delimiter |
Available box references are bbox_top, bbox_bottom, bbox_left,
bbox_right, bbox_center_x, and bbox_center_y.
clean_csv.py removes global timestamp outliers with the IQR rule, rejects
erroneous zero resets, and averages positions that share a timestamp.
uv run clean_csv.py raw.csv --header -o cleaned.csvTune detection with --iqr-factor (lower is more aggressive), or disable the
global pass with --no-global-outliers.
clean_for_linearized.py trims the beginning and end until it finds the longest
region meeting an R² threshold:
uv run clean_for_linearized.py cleaned.csv \
--min-r2 0.95 \
--min-points 5 \
--header \
-o linear.csvThe tools compose cleanly:
uv run clean_csv.py raw.csv --header |
uv run clean_for_linearized.py /dev/stdin --min-r2 0.9 --header -o linear.csv- Lock the camera on a tripod and avoid changing zoom or focus mid-shot.
- Keep the ruler parallel to—and in the same plane as—the motion.
- Use bright, even lighting and a fast shutter to reduce motion blur.
- Select a compact bounding box with distinctive visual features.
- Align calibration ticks carefully; they determine the local scale.
- Validate a short test clip before processing a long recording.
CSRT can tolerate some scale change and partial occlusion, but it is still a single-object visual tracker. Severe occlusion, abrupt appearance changes, or an object leaving the frame can cause tracking loss.
videographer/
├── main.py interactive calibration and tracking
├── to_csv.py JSON-to-delimited-data exporter
├── clean_csv.py timestamp cleanup and duplicate averaging
├── manual_clean_csv.py alternate/manual cleanup utility
├── clean_for_linearized.py linear-region extraction
├── pyproject.toml Python metadata and dependencies
└── CITATION.cff machine-readable citation metadata
If VideoGrapher helps your research, class, or project, please cite it:
@software{hu2025videographer,
author = {Hu, Bryan},
title = {VideoGrapher: Object Tracking with Lens Distortion Correction},
year = {2025},
version = {0.1.0},
url = {https://github.com/ThatXliner/videographer}
}Citation metadata is also available in CITATION.cff.
Issues and pull requests are welcome. High-impact directions include automatic object detection, multiple-object tracking, alternate trackers, and a unified desktop interface.