This folder contains the Python code for the image capture component of this project. It runs on a Raspberry Pi with a Raspberry Pi Camera Module attached.
Every so many seconds (configurable), the code takes a new picture in JPEG format and stores it in a Redis Hash along with some basic metadata. The key name for each hash is image:<unix time stamp when image was captured>
.
I've tested this on a Raspberry Pi 3B using both the Raspberry Pi Camera Module v2.1 and Raspberry Pi Camera Module v3. Other models of Raspberry Pi that have the camera connector (3A+, 3B+, 4B etc) should work too.
I haven't tested with the High Quality Camera. Of the cheaper models, the v3 is a good choice for this project as it has auto focus and higher resolution than the v2.1 and is easy to find online at a reasonable price. The pictures from my v2.1 camera can be blurry as there's no auto focus.
Note: As Redis keeps a copy of all the data in memory, you should bear in mind that an 8Mb image file will require at least 8Mb of RAM on the Redis server. To help manage the amount of memory used, this project automatically expires the image data from Redis after a configurable amount of time (see later for details).
All of the code for this component is in a single file: capture.py
. Here's a high level run through of how it works... You should also refer to the redis-py documentation and Raspberry Pi's Picamera2 library documentation.
The first thing that the script does is initialize and configure the Picamera2
library:
picam2 = Picamera2()
picam2.start_preview(Preview.NULL)
camera_config = picam2.still_configuration
picam2.configure(camera_config)
...
picam2.start()
The v3 camera module has autofocus capabilities. These are enabled like so, and only if an environment variable is set to do so (see later for details):
if CAMERA_AUTOFOCUS == True:
picam2.set_controls({"AfMode": controls.AfModeEnum.Continuous})
Use the Picamera2
documentation to adjust the camera configuration in camera_config
e.g. to capture lower resolution pictures. This configuration assumes we are running on a headless Raspberry Pi so there's no preview window required.
Next, a connection to Redis is established, using the value of an environment variable:
redis_client = redis.from_url(os.getenv("REDIS_URL", "redis://localhost:6379"))
The code then enters an infinite loop, in which is captures an image plus some metadata from the camera, stores it in Redis and sleeps for a configurable number of seconds before doing it all again.
If the camera module has autofocus and it is enabled... we start an autofocus cycle to make sure that the camera's focus is in the right place:
if CAMERA_AUTOFOCUS == True:
picam2.autofocus_cycle()
This is synchronous, so may take a short amount of time to complete.
We want to capture the image into a file like structure in memory, rather than write it to the filesystem. We use an in memory binary stream declared like this:
image_data = io.BytesIO()
An image from the camera is grabbed into that binary stream buffer, and we work out the current UNIX timestamp (seconds since 1 January 1970):
image_metadata = picam2.capture_file(image_data, format="jpeg")
current_timestamp = int(time.time())
The return value of picam2.capture_file
is some metadata from the camera. This isn't currently stored in Redis, but is printed out so you can determine if any of it is useful to you. See later in this file for an example.
Now it's time to create a Hash in Redis and store our image plus a couple of other pieces of data there, including the Lux value from the metadata:
redis_key = f"image:{current_timestamp}"
data_to_save = dict()
data_to_save["image_data"] = image_data.getvalue()
data_to_save["timestamp"] = current_timestamp
data_to_save["mime_type"] = "image/jpeg"
data_to_save["lux"] = int(image_metadata["Lux"])
First, we create the key name we're going to use when storing the Hash. It's image:<timestamp>
.
data_to_save
is a Python dictionary containing the name/value pairs to store in the Redis Hash. This needs to be a flat map of name/value pairs - nested structure isn't allowed in a Redis Hash. If you want to model a more complex data structure, use the JSON data type in Redis Stack.
Hashes in Redis are schemaless, so if you add extra fields there's no need to change any database schema (if you're looking for one, it doesn't exist!). You'll just need to modify any application code that reads the Hashes to use new fields.
We store the bytes of the image, the timestamp and the MIME or media type of the image... so that any front end knows what encoding the data in image_data
is in.
Saving the Hash to Redis is then simply a matter of running the HSET
command, passing it the key name and dict of name/value pairs to store. When saving this data, we also want to set an expiry time for it which we do with the Redis EXPIRE
command. The time to live for each Hash is a configurable number of seconds, read from the IMAGE_EXPIRY
environment variable (see later for details).
This means that we want to send two commands to Redis. To save on network bandwidth, let's use a feature of the Redis protocol called a pipeline and send both in the same network round trip:
pipe = redis_client.pipeline(transaction=False)
pipe.hset(redis_key, mapping = data_to_save)
pipe.expire(redis_key, IMAGE_EXPIRY)
pipe.execute()
This sets up the HSET
and EXPIRE
commands in a pipeline, which is then sent to Redis using the execute
function. We don't need the results returned from Redis in this instance, but if we did then we can access them as a List returned by execute
.
To get this component working, you'll need to connect your camera to the Raspberry Pi, ensure the operating system is configured correctly for it and install some Python dependencies (to connect to Redis).
These instructions assume you are running the latest version of Raspberry Pi OS. They have been tested with the following version:
$ cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 11 (bullseye)"
NAME="Raspbian GNU/Linux"
VERSION_ID="11"
VERSION="11 (bullseye)"
VERSION_CODENAME=bullseye
ID=raspbian
ID_LIKE=debian
$ uname -s -r -v -m
Linux 6.1.21-v7+ #1642 SMP Mon Apr 3 17:20:52 BST 2023 armv7l
Setting up the camera may require some changes to the operating system configuration of the Raspberry Pi. This is what worked for me on the Raspberry Pi 3B using either the Camera Module v2.1 or v3 (recommended).
First, connect the camera to the Raspberry Pi with the ribbon cable provided. If you are unsure how to do this, follow Raspberry Pi's instructions here.
Then, as the root user (sudo vi /boot/config.txt
), ensure the following are in /boot/config.txt
and add them if not:
camera_auto_detect=0
dtoverlay=vc4-kms-v3d
max_framebuffers=10
dtoverlay=imx219
The imx219
value may differ for your camera. Use imx219
for the Raspberry Pi Camera Module v2.1, or imx708
for the v3. If you are using something different, you'll need to research appropriate values for your camera. Raspberry Pi provide this information in their camera documentation.
If you made any changes, save them and reboot the Raspberry Pi (sudo reboot
).
You need Python 3.7 or higher (I've tested this with Python 3.9.2). To check your Python version:
python3 --version
The operating system should have been supplied with an appropriate Python version pre-installed. If you find that you need to upgrade your Python version, use your operating system's package manager or refer to this guide from raspberrypitips.com.
You'll also need the Pip package manager. Install it with:
sudo apt install python3-pip
Verify that Pip was installed correctly:
pip --version
Once you have these, use Pip to install the project requirements like so:
pip install -r requirements.txt
The code assumes by default that your Redis server is running on localhost
port 6379
. If this is not the case, you'll need to set the REDIS_URL
environment variable to a valid Redis URL describing where and how to connect to your Redis server.
For example, here's how to connect to a server on myhost
at port 9999
with password secret123
:
export REDIS_URL=redis://default:secret123@myhost:9999/
If you have a username and a password for your Redis server, use something like this:
export REDIS_URL=redis://myusername:secret123@myhost:9999/
If you don't need a username or a password:
export REDIS_URL=redis://myhost:9999/
Be sure to configure both the capture script and the separate server component to talk to the same Redis instance!
You'll also need to set the following environment variables:
IMAGE_EXPIRY
- set this to the number of seconds that you want the image data to be stored in Redis for before it is expired e.g.300
for 5 minutes.CAMERA_AUTOFOCUS
- set this to1
if your camera module has autofocus (v3) or0
if it doesn't (v2).- TODO THE NEW ONES...
Alternatively (recommended), you can create a file in the server
folder called .env
and store your environment variable values there. See env.example
for an example. Don't commit .env
to source control, as your Redis credentials should be considered a secret and managed as such!
With the setup steps completed, start the capture script as follows:
python3 capture.py
You should expect to see output similar to the following on startup (example using camera module v2.1):
[0:34:17.749739445] [847] INFO Camera camera_manager.cpp:299 libcamera v0.0.4+22-923f5d70
[0:34:17.795151761] [848] WARN RPI raspberrypi.cpp:1357 Mismatch between Unicam and CamHelper for embedded data usage!
[0:34:17.796473001] [848] INFO RPI raspberrypi.cpp:1476 Registered camera /base/soc/i2c0mux/i2c@1/imx219@10 to Unicam device /dev/media3 and ISP device /dev/media0
[0:34:17.814204535] [847] INFO Camera camera.cpp:1028 configuring streams: (0) 3280x2464-BGR888
[0:34:17.814804062] [848] INFO RPI raspberrypi.cpp:851 Sensor: /base/soc/i2c0mux/i2c@1/imx219@10 - Selected sensor format: 3280x2464-SBGGR10_1X10 - Selected unicam format: 3280x2464-pBAA
Your output may differ if you are using a different camera. It appears that this warning can be ignored:
WARN RPI raspberrypi.cpp:1357 Mismatch between Unicam and CamHelper for embedded data usage!
Every so many seconds, the script will capture a new image. Expect to see output similar to the following:
Stored new image at image:1681923128
{'SensorTimestamp': 2058296354000, 'ScalerCrop': (0, 0, 3280, 2464), 'DigitalGain': 1.1096521615982056, 'ColourGains': (1.1879777908325195, 2.4338300228118896), 'SensorBlackLevels': (4096, 4096, 4096, 4096), 'AeLocked': False, 'Lux': 85.72087097167969, 'FrameDuration': 59489, 'ColourCorrectionMatrix': (1.6235777139663696, -0.38433241844177246, -0.23924528062343597, -0.5687134861946106, 2.019625425338745, -0.45091837644577026, -0.09334515780210495, -1.2399080991744995, 2.3332533836364746), 'AnalogueGain': 4.0, 'ColourTemperature': 2874, 'ExposureTime': 59413}
With the exception of the Lux
value, he camera metadata isn't stored in Redis - it's just output for informational purposes. If any of it is considered useful enough to keep, it should be easy to modify capture.py
to add it to the Redis Hash that stores the image and associated data.
To stop the script, press Ctrl-C.