A do-it-yourself speed camera.
This project will require the following skills:
- Linux
- Minor electronics
- Python
- Craftiness
I am using a Raspberry Pi 4, but I recommend getting a 5. It should handle the object detection far better. It takes some time for the 4 to process the video.
The adapters are used to mount the camera to the door of the enclosure. The lens I chose does not have any threads on it. I modified the 27mm-30mm adapter by filing off the outer threads. This allowed it to fit inside the ring of the lens. I used super glue gel to attach it.
For the outside step adapter, I used paintable silicone to attach it to the enclosure along with the camera shroud, fans, and round vents. The camera shroud will need some modification for the step up adapter to fit inside of it.
I purchased the C version of the radar because I thought I would be using the range function. I wound up not using it and have recommended the A version. Be sure to read the documentation they provide on their website. To calibrate the radar, I used the cosine correction and made passes with my car with the corrections in the config file set to 0. Since my car has a digital speedometer, it really made it easy. Based on local roadside police radars, my speedometer read 1 mph faster. Any discrepancies between the radar report and my speedometer were entered into the config file.
Raspberry Pi 4 or 5, 8GB RAM
Omni Presense OPS243-A
Pi heatsink and fan
Pi SSD mount
Enclosure
50mm fans
Round vents
Outside step up adapter (25mm-58mm)
Clear lens filter
Stepdown adapter (30mm-25mm)
Step up adapter (27mm-30mm)
Camera shroud
HQ camera
Enclosure mount
POE Injector
POE surge protector
POE Splitter
Temperature sensor
Cable clips for temperature sensor
USB connector for 5v power
Dupont terminal block
Relay
Waterproof ethernet connector
Standoffs
Download the installation script.
wget https://raw.githubusercontent.com/ajkelsey/speedcam-st/main/speedcam-st.sh
Install
./speedcam-st.sh install
Remove
./speedcan-t.sh remove
Be sure the following directory structure exists:
└── opt
└── speedcam - Speedcam root directory
├── data - CSV file.
├── http - Used by ffmpeg_streamer.py and contains index.html.
├── imageq - Image persistent file queue.
├── images - Images of speeding cars.
├── log - Log files.
├── video - Video files to be processed.
└── videoq - Video persistent file queue.
-
GPIO 17: Fan relay
-
GPIO 22: IR filter
-
GPIO 27: IR filter
sudo apt install python3-apscheduler python3-matplotlib python3-matplotlib \
python3-pandas python3-persist-queue python3-rich
pip install ultralytics --break-system-packages
- /boot/firmware/config.txt
- Add:
- Depending on your camera choice, add:
- HQ:
dtoverlay=imx477,cma-512
- Global:
dtoverlay-imx296, cma-512``max_framebuffers=8``camera_auto_detect=0
- HQ:
- Depending on your camera choice, add:
- Add:
- raspiconfig:
- This is for the temperature sensor.
- Interface Options > enable 1-wire interface > reboot
- This is for the temperature sensor.
- Edit /opt/speedcam/speedcam-config.json accordingly.
- Edit stats.py variables speed_brackets and speed_fines to configure accurate fine estimation for your state. NJ is the default.
- Check that speedcam.service and case_fans.service are enabled and started.
To gain access to the Facebook API, try this blog post. I believe things have changed a bit since it was posted, but should be very helpful. Here are the notes I have for doing this. I know I struggled getting this right at the time and they may not be totally accurate. My script is designed to use a Facebook Page. You can create one using your existing account.
- Create app
- Choose other than business. Name to be the same as the page you want to create
- Dashboard > App Settings > App Page > select or create page with the same name as the app.
- page_id: go page, about tab, page transparency.
- -Graph API Explorer > Permissions:
- pages_manage_engagement
- pages_manage_posts
- pages_read_engagement
- pages_read_user_comment
- pages_manage_engagement
- Generate token, sign in, select done
- debug user token, extend user token. never expires
- user id: settings, business integrations
- Choose other than business. Name to be the same as the page you want to create
The speed camera and case fan scripts are run at boot as services; speedcam.service and case_fans.service.
On startup, the speed camera will initialize the logger, speedcam scheduler, camera, radar, ir_filter, alpr, and facebook facilities. Then launches the vehicle detection thread.
There is a bug somewhere in the software or hardware that causes the files written by the camera to be 0 bytes in size. I found the only way to recover from this is to restart the speedcam service. This happens daily at 3 am via the scheduler, and will do it if it detects a 0 byte file write.
After the initializations, the main loop will be launched. The radar.get_speed() method will be called where the radar serial buffer is checked for data. The output of the radar is continuous when an object is detected and results in multiple speed readings per vehicle. If the gap between radar reports is less than 750ms, the speed camera will consider that reading to be for the same vehicle.
The camera is slow, and coordinating a still picture to be taken at the correct time is not possible. For this reason, I use video. When there is data to be read from the radar buffer, the camera begins to record video. Reports from the radar are accumulated and then averaged. After the final report, the camaera.stop_video() thread is launched.
If the speed is above the minimum report threshold, the data is added to the daily CSV, and the video will be processed for vehicle detection. This happens in the aplr thread launched during initialization.
The vehicle detection uses a persistent queue to process videos. As each video takes some time to detect, this prevents multiple videos being processed at the same time. It also allows for detection to be resilient across script shutdowns.
For my setup, there is only a part of the image that is clear enough to read the license plates. For this reason, I created a detection zone at line 91 in alpr.py. The video is processed frame by frame. The frame will be checked for objects, and found objects will be checked if they are a vehicle. The vehicles are cropped from the frame using the bounding box and the labeling is applied to the image. Videos recorded from dusk to dawn are not processed because there is not enough light.
Image filenames are kept in a persistent queue for posting to Facebook. A post will occur every hour and upload the images in the queue. Daily at 1 am, the Speeder of the Day is posted, and at 2 am, the chart of daily speeders is posted.
alpr.py | Primarily detects vehicles in the captured video. The intent is to have an automatic license plate recognition feature in the future. |
camera.py | Handles the starting and stopping of video recording. |
case_fans.py | Operates the case fans on linux startup. Records temperature to a file every 5 minutes. Not integrated with the speed camera. |
case_temp.py | Utility that displays a text based graph on screen. Not integrated with the speed camera. |
data.py | Handles data that is stored in the CSV file. |
facebook.py | Posts to Facebook |
ffmpeg_streamer.py | Utility that streams the camera to a small web server. Helps in pointing the camera during setup. |
fq.py | File queue used by case_fans.py |
http/index.html | Used by ffmpeg_streamer.py. Be sure to edit the URL contained in this file. |
ir_filter.py | Operates the camera IR filter. The IR filter is removed for nighttime recording. |
plot_the_plots.py | Utility that can generate graphs based on recorded data. Outputs to jpeg. plot_the_plots --help |
radar.py | Communicates with the radar. |
speedcam.py | Main speed camera file. |
stats.py | Used to generate the speeder of the day. |
vehicle.py | Vehicle class used to handle the data points for each vehicle detected. |
street_name | Name of the street camera is on. |
inbound | Cardinal direction for traffic heading towards the camera. |
outbound | Cardinal direction for traffic heading away from the camera. |
lat | Latitude of camera in decimal. Used to determine sunrise/sunset. |
lon | Longitude of camera in decimal. Used to determine sunrise/sunset. |
timezone | Timezone using the tz database format. |
inbound correction | Speed correction for inbound traffic using chosen speed unit. |
outbound correction | Speed correction for outbound traffic using chosen speed unit. |
angle2street | The angle the camera is facing the street. 0 is parallel. |
angle2ground | The angle the camera is facing the ground. 0 is level. Currently unused. |
min_speed_report | Minimum speed recorded in the database. |
min_speed_post | Minimum speed required to post to Facebook. |
speed_units | Speed units that are displayed on the image. |
camera_facility | Enable/disable camera recording. 0=off, 1=on |
resolution_x | Horizontal image resolution. |
resolution_y | Vertical image resolution. |
cam_pre_record | Length of pre-recording of video prior to radar detection in seconds. |
cam_post_record | Length of post-recording of video after radar detection in seconds. |
post_to_facebook | Enable/disable posting to facebook. 0=off, 1=on |
vpn | Enable/disable vpn. 0=off, 1=on (currently unused) |
logging_level | DEBUG, INFO, WARNING |
Setttings related to Facebook's Graph API | |
radar | device_path: Confirm this setting for your device. settings: Settings sent to the radar. See radar documentation for details. |