Watches a piece of train track, detects trains, and stitches together images of them.
See the code⚠️ Onlytrains is no longer being maintained due to time constraints. The frontend, with some archived data still available, is at https://trains.jo-m.ch. ⚠️
Other deployments:
Watches a piece of train track, detects passing trains, and stitches together images of them. Should work with any video4linux USB cam, or Raspberry Pi camera v3 modules.
The name Onlytrains is credited to @timethy.
It also contains some packages which might be useful for other purposes:
The binaries are currently built and tested on X86_64 and a Raspberry Pi 4 B.
The computer vision used in trainbot is fairly naive and simple. There is no camera calibration, image stabilization, undistortion, perspective mapping, or "real" object tracking. This allows us to stay away from complex dependencies like OpenCV, and keeps the computational requirements low. All processing happens on CPU.
The assumptions are (there might be more implicit ones):
As this is just a hobby project for me, the documentation is pretty sparse. This very README is the most important part of it. To deploy this project yourself, you should have some basic sysadmin, web servers, and ideally Go knowledge. When in doubt, the source of truth is ... the source code.
All config options can be passed as ENV vars or CLI flags.
See config struct on top of cmd/trainbot/main.go, or run trainbot --help to see all options.
The two Makefiles (root and frontend/) also contain some hints.
There are two parts to deploy: First, the Go binary which detects trains, and second the web frontend.
How to get binaries? There are multiple options:
go install jo-m.ch/go/trainbot/cmd/trainbot@latestRun the interactive tool to adjust camera and select a crop rectangle:
# On the host machine
make deploy_confighelper host=TRAINBOT_DEPLOY_TARGET_SSH_HOST
# Example:
make deploy_confighelper host=pi@10.20.0.12
# On the raspberry pi
sudo usermod -a -G video pi
# The --input arg has to be adapted to your actual camera config.
./confighelper-arm64 --log-pretty --input=picam3 --listen-addr=0.0.0.0:8080
Example "Production" deployment to a remote host (will install a systemd user unit):
First, you need to create a env file (copy env.example).
Then, from the host machine:
make deploy_trainbot host=TRAINBOT_DEPLOY_TARGET_SSH_HOST
# To see logs, on the target device:
journalctl --user -eu trainbot.service
Download latest data from Raspberry Pi:
ssh "$TRAINBOT_DEPLOY_TARGET_SSH_HOST" sqlite3 trainbot/data/db.sqlite3
.backup trainbot/data/db.sqlite3.bak
# Ctrl+D
rsync --verbose --archive --rsh=ssh "$TRAINBOT_DEPLOY_TARGET_SSH_HOST:trainbot/data/" data/
rm data/db.sqlite3-shm data/db.sqlite3-wal
mv data/db.sqlite3.bak data/db.sqlite3
The frontend is a VueJS SPA app written in Typescript. It consists of only static files (after the JS build process). There is no web backend, the frontend simply loads the entire SQLite database from the server, and then does all the queries itself. This means that the frontend can be deployed entirely independently from the trainbot binary, as long as there is some way for the date (db + pics) to get to the web server.
My Raspberry Pi is not exposed to the internet, and I also already had a web hosting account with FTP access available. Thus, in my setup, the binary and the frontend are running on two entirely different machines in two different networks.
The frontend is built and deployed via:
export FRONTEND_DEPLOY_TARGET_SSH_HOST=myuser@mywebserver:/var/www/trains/
cd frontend
make deploy
The binary on the Raspberry Pi in my home network will upload pictures and the updated db file via FTP to this webspace whenever a new train is detected.
This is configured via the ENABLE_UPLOAD=true and UPLOAD_... env vars (or the corresponding CLI flags).
Alternative uploaders (e.g. SFTP, SCP, WebDAV, ...) could be pretty easily implemented (but they are not because I do not need them).
For this, the Uploader interface from internal/pkg/upload/upload.go needs to be implemented, and corresponding configuration options added.
It is possible to deploy the fronted on the same machine where trainbot runs. There is no finished solution provided in this repo, but some hints are here:
cd frontend; make build; [s]cp dist /var/www/trains.--data-dir / DATA_DIR.
/var/www/trains, trainbot would be running with --data-dir=/var/www/trains/dataNote that this can lead to transient inconsistencies when the web server is delivering the sqlite file at the same time the binary is writing to it. The clean solution would be to add another "local FS" uploader to trainbot (see previous section).
I use a Raspberry Pi 4 Mod B with 2GiB of RAM, and a Raspberry Pi Camera v3 (narrow lens). Distance from camera to tracks is ca. 50m.
All this is installed on my balcony in a waterproof case, as seen in the MagPi Magazine.
The case is this one from AliExpress: https://www.aliexpress.com/item/1005003010275396.html
Errata (not corrected in the models linked above):
This repo is set up to compile for x86_64 and aarch64.
There is support for building on your machine directly, or inside a Docker container.
Also, there is an extensive test suite. Tests may also be executed locally, or inside Docker.
The single entrypoint for everything (incl. Docker) is the Makefile.
You can list available targets via make list.
Same is true for the frontend - check out frontend/Makefile.
Example:
git clone https://github.com/jo-m/trainbot
cd trainbot
make docker_build
# Find binaries in build/ after this has completed.
To work with the Vulkan code, you need to activate the vk build tag.
For VSCode, add the following to .vscode/settings.json:
"go.buildTags": "vk,moretests"
# list
ffmpeg -f v4l2 -list_formats all -i /dev/video2
v4l2-ctl --all --device /dev/video2
# exposure
v4l2-ctl -c exposure_auto=3 --device /dev/video2
# autofocus
v4l2-ctl -c focus_auto=1 --device /dev/video2
# fixed
v4l2-ctl -c focus_auto=0 --device /dev/video2
v4l2-ctl -c focus_absolute=0 --device /dev/video2
v4l2-ctl -c focus_absolute=1023 --device /dev/video2
ffplay -f video4linux2 -framerate 30 -video_size 3264x2448 -pixel_format mjpeg /dev/video2
ffplay -f video4linux2 -framerate 30 -video_size 1920x1080 -pixel_format mjpeg /dev/video2
ffmpeg -f v4l2 -framerate 30 -video_size 3264x2448 -pixel_format mjpeg -i /dev/video2 output.avi
# setup
sudo apt-get install libcamera0 libcamera-apps-lite
sudo apt install -y vlc
# grab frame
# https://www.raspberrypi.com/documentation/computers/camera_software.html#libcamera-and-libcamera-apps
libcamera-jpeg -o out.jpg -t 1 --width 4608 --height 2592 --rotation 180 --autofocus-mode=manual --lens-position=2
libcamera-jpeg -o out.jpg -t 1 --width 2304 --height 1296 --rotation 180 --autofocus-mode=manual --lens-position=4.5 --roi 0.25,0.5,0.5,0.5
# record video
DATE=$(date +'%F_%H-%M-%S'); libcamera-vid -o $DATE.h264 --save-pts $DATE.txt --width 1080 --height 720 --rotation 180 --autofocus-mode=manual --lens-position=0 -t 0
# stream through network
libcamera-vid -t 0 --inline --nopreview --width 4608 --height 2592 --rotation 180 --codec mjpeg --framerate 5 --listen -o tcp://0.0.0.0:8080 --autofocus-mode=manual --lens-position=0 --roi 0.25,0.5,0.5,0.5
# on localhost
ffplay http://pi4:8080/video.mjpeg
# manually record video for test cases
libcamera-vid \
--verbose=1 \
--timeout=0 \
--inline \
--nopreview \
--width 240 --height 280 \
--roi 0.429688,0.185185,0.104167,0.216049 \
--mode=2304:1296:12:P \
--framerate 30 \
--autofocus-mode=manual --lens-position=0.000000 \
--rotation=0 \
-o vid.h264 --save-pts vid-timestamps.txt
mkvmerge -o test.mkv --timecodes 0:vid-timestamps.txt vid.h264
panic(), "application" code use log.Panic()...For debugging and tweaking a Prometheus-compatible endpoint can be exposed at port 18963 using --prometheus=true. A Grafana dashboard is also available.
libcamera-vid
│
▼
┌─────────────────┐
│ │
│ source queue │
│ │
└─────────────────┘
│
▼
findOffset ──────► discard
│
record
│
▼
┌────────────┐
│ │
│ sequence │
│ │
└────────────┘
│
▼
fitDx
│
▼
stitch
│
▼
┌───────────┐
│ │
│ image │
│ │
└───────────┘
s.cam.GetOutput()Go
66.8%
C
11.1%
Vue
10.8%
TypeScript
4.3%
Makefile
2.6%
HTML
1.9%
Dockerfile
1.8%
Watches a piece of train track, detects trains, and stitches together images of them.
See the code⚠️ Onlytrains is no longer being maintained due to time constraints. The frontend, with some archived data still available, is at https://trains.jo-m.ch. ⚠️
Other deployments:
Watches a piece of train track, detects passing trains, and stitches together images of them. Should work with any video4linux USB cam, or Raspberry Pi camera v3 modules.
The name Onlytrains is credited to @timethy.
It also contains some packages which might be useful for other purposes:
The binaries are currently built and tested on X86_64 and a Raspberry Pi 4 B.
The computer vision used in trainbot is fairly naive and simple. There is no camera calibration, image stabilization, undistortion, perspective mapping, or "real" object tracking. This allows us to stay away from complex dependencies like OpenCV, and keeps the computational requirements low. All processing happens on CPU.
The assumptions are (there might be more implicit ones):
As this is just a hobby project for me, the documentation is pretty sparse. This very README is the most important part of it. To deploy this project yourself, you should have some basic sysadmin, web servers, and ideally Go knowledge. When in doubt, the source of truth is ... the source code.
All config options can be passed as ENV vars or CLI flags.
See config struct on top of cmd/trainbot/main.go, or run trainbot --help to see all options.
The two Makefiles (root and frontend/) also contain some hints.
There are two parts to deploy: First, the Go binary which detects trains, and second the web frontend.
How to get binaries? There are multiple options:
go install jo-m.ch/go/trainbot/cmd/trainbot@latestRun the interactive tool to adjust camera and select a crop rectangle:
# On the host machine
make deploy_confighelper host=TRAINBOT_DEPLOY_TARGET_SSH_HOST
# Example:
make deploy_confighelper host=pi@10.20.0.12
# On the raspberry pi
sudo usermod -a -G video pi
# The --input arg has to be adapted to your actual camera config.
./confighelper-arm64 --log-pretty --input=picam3 --listen-addr=0.0.0.0:8080
Example "Production" deployment to a remote host (will install a systemd user unit):
First, you need to create a env file (copy env.example).
Then, from the host machine:
make deploy_trainbot host=TRAINBOT_DEPLOY_TARGET_SSH_HOST
# To see logs, on the target device:
journalctl --user -eu trainbot.service
Download latest data from Raspberry Pi:
ssh "$TRAINBOT_DEPLOY_TARGET_SSH_HOST" sqlite3 trainbot/data/db.sqlite3
.backup trainbot/data/db.sqlite3.bak
# Ctrl+D
rsync --verbose --archive --rsh=ssh "$TRAINBOT_DEPLOY_TARGET_SSH_HOST:trainbot/data/" data/
rm data/db.sqlite3-shm data/db.sqlite3-wal
mv data/db.sqlite3.bak data/db.sqlite3
The frontend is a VueJS SPA app written in Typescript. It consists of only static files (after the JS build process). There is no web backend, the frontend simply loads the entire SQLite database from the server, and then does all the queries itself. This means that the frontend can be deployed entirely independently from the trainbot binary, as long as there is some way for the date (db + pics) to get to the web server.
My Raspberry Pi is not exposed to the internet, and I also already had a web hosting account with FTP access available. Thus, in my setup, the binary and the frontend are running on two entirely different machines in two different networks.
The frontend is built and deployed via:
export FRONTEND_DEPLOY_TARGET_SSH_HOST=myuser@mywebserver:/var/www/trains/
cd frontend
make deploy
The binary on the Raspberry Pi in my home network will upload pictures and the updated db file via FTP to this webspace whenever a new train is detected.
This is configured via the ENABLE_UPLOAD=true and UPLOAD_... env vars (or the corresponding CLI flags).
Alternative uploaders (e.g. SFTP, SCP, WebDAV, ...) could be pretty easily implemented (but they are not because I do not need them).
For this, the Uploader interface from internal/pkg/upload/upload.go needs to be implemented, and corresponding configuration options added.
It is possible to deploy the fronted on the same machine where trainbot runs. There is no finished solution provided in this repo, but some hints are here:
cd frontend; make build; [s]cp dist /var/www/trains.--data-dir / DATA_DIR.
/var/www/trains, trainbot would be running with --data-dir=/var/www/trains/dataNote that this can lead to transient inconsistencies when the web server is delivering the sqlite file at the same time the binary is writing to it. The clean solution would be to add another "local FS" uploader to trainbot (see previous section).
I use a Raspberry Pi 4 Mod B with 2GiB of RAM, and a Raspberry Pi Camera v3 (narrow lens). Distance from camera to tracks is ca. 50m.
All this is installed on my balcony in a waterproof case, as seen in the MagPi Magazine.
The case is this one from AliExpress: https://www.aliexpress.com/item/1005003010275396.html
Errata (not corrected in the models linked above):
This repo is set up to compile for x86_64 and aarch64.
There is support for building on your machine directly, or inside a Docker container.
Also, there is an extensive test suite. Tests may also be executed locally, or inside Docker.
The single entrypoint for everything (incl. Docker) is the Makefile.
You can list available targets via make list.
Same is true for the frontend - check out frontend/Makefile.
Example:
git clone https://github.com/jo-m/trainbot
cd trainbot
make docker_build
# Find binaries in build/ after this has completed.
To work with the Vulkan code, you need to activate the vk build tag.
For VSCode, add the following to .vscode/settings.json:
"go.buildTags": "vk,moretests"
# list
ffmpeg -f v4l2 -list_formats all -i /dev/video2
v4l2-ctl --all --device /dev/video2
# exposure
v4l2-ctl -c exposure_auto=3 --device /dev/video2
# autofocus
v4l2-ctl -c focus_auto=1 --device /dev/video2
# fixed
v4l2-ctl -c focus_auto=0 --device /dev/video2
v4l2-ctl -c focus_absolute=0 --device /dev/video2
v4l2-ctl -c focus_absolute=1023 --device /dev/video2
ffplay -f video4linux2 -framerate 30 -video_size 3264x2448 -pixel_format mjpeg /dev/video2
ffplay -f video4linux2 -framerate 30 -video_size 1920x1080 -pixel_format mjpeg /dev/video2
ffmpeg -f v4l2 -framerate 30 -video_size 3264x2448 -pixel_format mjpeg -i /dev/video2 output.avi
# setup
sudo apt-get install libcamera0 libcamera-apps-lite
sudo apt install -y vlc
# grab frame
# https://www.raspberrypi.com/documentation/computers/camera_software.html#libcamera-and-libcamera-apps
libcamera-jpeg -o out.jpg -t 1 --width 4608 --height 2592 --rotation 180 --autofocus-mode=manual --lens-position=2
libcamera-jpeg -o out.jpg -t 1 --width 2304 --height 1296 --rotation 180 --autofocus-mode=manual --lens-position=4.5 --roi 0.25,0.5,0.5,0.5
# record video
DATE=$(date +'%F_%H-%M-%S'); libcamera-vid -o $DATE.h264 --save-pts $DATE.txt --width 1080 --height 720 --rotation 180 --autofocus-mode=manual --lens-position=0 -t 0
# stream through network
libcamera-vid -t 0 --inline --nopreview --width 4608 --height 2592 --rotation 180 --codec mjpeg --framerate 5 --listen -o tcp://0.0.0.0:8080 --autofocus-mode=manual --lens-position=0 --roi 0.25,0.5,0.5,0.5
# on localhost
ffplay http://pi4:8080/video.mjpeg
# manually record video for test cases
libcamera-vid \
--verbose=1 \
--timeout=0 \
--inline \
--nopreview \
--width 240 --height 280 \
--roi 0.429688,0.185185,0.104167,0.216049 \
--mode=2304:1296:12:P \
--framerate 30 \
--autofocus-mode=manual --lens-position=0.000000 \
--rotation=0 \
-o vid.h264 --save-pts vid-timestamps.txt
mkvmerge -o test.mkv --timecodes 0:vid-timestamps.txt vid.h264
panic(), "application" code use log.Panic()...For debugging and tweaking a Prometheus-compatible endpoint can be exposed at port 18963 using --prometheus=true. A Grafana dashboard is also available.
libcamera-vid
│
▼
┌─────────────────┐
│ │
│ source queue │
│ │
└─────────────────┘
│
▼
findOffset ──────► discard
│
record
│
▼
┌────────────┐
│ │
│ sequence │
│ │
└────────────┘
│
▼
fitDx
│
▼
stitch
│
▼
┌───────────┐
│ │
│ image │
│ │
└───────────┘
s.cam.GetOutput()Go
66.8%
C
11.1%
Vue
10.8%
TypeScript
4.3%
Makefile
2.6%
HTML
1.9%
Dockerfile
1.8%