Skip to content

Commit 1e69296

Browse files
author
Simon Prickett
authored
Merge pull request #4 from simonprickett/update-for-pi-camera-3
Updated for Pi Camera Module 3 with autofocus.
2 parents 6c41642 + 4c10c13 commit 1e69296

File tree

11 files changed

+112
-58
lines changed

11 files changed

+112
-58
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Redis data
2+
redisdata/
3+
14
# Byte-compiled / optimized / DLL files
25
__pycache__/
36
*.py[cod]

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ Here's what the front end looks like when a few images have been captured by the
88

99
![Front end showing captured images](server_component_running.png)
1010

11-
(Images are somewhat out of focus as the camera I am using doesn't have auto focus and I didn't adjust its position for these pics, they were just test data!)
12-
1311
And here's a Raspberry Pi with a camera attached:
1412

1513
![Raspberry Pi 3 with Camera Module attached](raspberry_pi_3_with_camera_module.jpg)
@@ -43,6 +41,8 @@ When you're done with the Docker container, stop it like this:
4341
docker-compose down
4442
```
4543

44+
Your data is saved in a [Redis Append Only File](https://redis.io/docs/management/persistence/) in the `redisdata` folder. Redis Stack will reload the dataset from this file when you restart the container.
45+
4646
With the container running, you can access the [Redis CLI](https://redis.io/docs/ui/cli/) using this command:
4747

4848
```
@@ -67,6 +67,7 @@ Each key contains a hash with the following name/value pairs:
6767

6868
* `mime_type`: The [MIME type](https://en.wikipedia.org/wiki/Media_type) for the captured image data. This will always be `image/jpeg` unless you change it and the image capture format in the `capture.py` script.
6969
* `timestamp`: The [UNIX timestamp](https://en.wikipedia.org/wiki/Unix_time) that the image was captured at, as recoded from the Raspberry Pi's clock. This will be the same value as the timestamp in the key name.
70+
* `lux`: The [lux](https://en.wikipedia.org/wiki/Lux) value captured by the camera when the image was taken.
7071
* `image_data`: A binary representation of the bytes for the image captured by the camera. This will be a [JPEG image](https://en.wikipedia.org/wiki/JPEG) unless you change the capture format in `capture.py`.
7172

7273
Here's a complete example, with the image data truncated for brevity:
@@ -76,11 +77,13 @@ Here's a complete example, with the image data truncated for brevity:
7677
1) "mime_type"
7778
2) "image/jpeg"
7879
3) "timestamp"
79-
4) "1681843615"
80-
5) "image_data"
81-
6) "\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00...
80+
4) "lux"
81+
5) "268"
82+
6) "1681843615"
83+
7) "image_data"
84+
8) "\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00...
8285
```
83-
With the camera that I used ([Raspberry Pi Camera Module 2.1](https://www.raspberrypi.com/products/camera-module-v2/) capturing at 3280x2464 pixels - configurable in `capture.py`) you can expect each Hash to require around 2Mb of RAM in Redis.
86+
With the camera that I used ([Raspberry Pi Camera Module 3](https://www.raspberrypi.com/products/camera-module-3/) capturing at 4608x2592 pixels - configurable in `capture.py`) you can expect each Hash to require around 1Mb of RAM in Redis.
8487

8588
## (Optional, but Recommended): RedisInsight
8689

docker-compose.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ services:
66
ports:
77
- 6379:6379
88
- 8001:8001
9+
volumes:
10+
- ./redisdata:/data
11+
environment:
12+
- REDIS_ARGS=--appendonly yes --save ""
913
deploy:
1014
replicas: 1
1115
restart_policy:
12-
condition: on-failure
16+
condition: on-failure

pi/README.md

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66

77
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.
88

9-
Every 10 or so seconds, 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>`.
9+
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>`.
1010

11-
I've tested this on a [Raspberry Pi 3B](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) using the [Raspberry Pi Camera Module v2.1](https://www.raspberrypi.com/products/camera-module-v2/). Other models of Raspberry Pi that have the camera connector ([3A+](https://www.raspberrypi.com/products/raspberry-pi-3-model-a-plus/), [3B+](https://www.raspberrypi.com/products/raspberry-pi-3-model-b-plus/), [4B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) etc) should work too.
11+
I've tested this on a [Raspberry Pi 3B](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) using both the [Raspberry Pi Camera Module v2.1](https://www.raspberrypi.com/products/camera-module-v2/) and [Raspberry Pi Camera Module v3](https://www.raspberrypi.com/products/camera-module-3/). Other models of Raspberry Pi that have the camera connector ([3A+](https://www.raspberrypi.com/products/raspberry-pi-3-model-a-plus/), [3B+](https://www.raspberrypi.com/products/raspberry-pi-3-model-b-plus/), [4B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) etc) should work too.
1212

13-
I haven't tested with the [Raspberry Pi Camera Module v3](https://www.raspberrypi.com/products/camera-module-3/) or the [High Quality Camera](https://www.raspberrypi.com/products/raspberry-pi-high-quality-camera/). I would imagine that the v3 is a good choice for this project as it has auto focus. The pictures from my v2.1 camera can be blurry as there's no auto focus.
13+
I haven't tested with the [High Quality Camera](https://www.raspberrypi.com/products/raspberry-pi-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.
1414

15-
**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. As it stands, the code doesn't expire pictures from Redis after a period of time, but you could easily add this using a call to the [`EXPIRE`](https://redis.io/commands/expire/) command whenever you add a new image Hash.
15+
**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).
1616

1717
## How it Works
1818

@@ -29,6 +29,13 @@ picam2.configure(camera_config)
2929
picam2.start()
3030
```
3131

32+
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):
33+
34+
```python
35+
if CAMERA_AUTOFOCUS == True:
36+
picam2.set_controls({"AfMode": controls.AfModeEnum.Continuous})
37+
```
38+
3239
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.
3340

3441
Next, a connection to Redis is established, using the value of an environment variable:
@@ -37,7 +44,16 @@ Next, a connection to Redis is established, using the value of an environment va
3744
redis_client = redis.from_url(os.getenv("REDIS_URL", "redis://localhost:6379"))
3845
```
3946

40-
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 10 seconds before doing it all again.
47+
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.
48+
49+
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:
50+
51+
```python
52+
if CAMERA_AUTOFOCUS == True:
53+
picam2.autofocus_cycle()
54+
```
55+
56+
This is synchronous, so may take a short amount of time to complete.
4157

4258
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](https://docs.python.org/3/library/io.html#binary-i-o) declared like this:
4359

@@ -54,14 +70,15 @@ current_timestamp = int(time.time())
5470

5571
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.
5672

57-
Now it's time to create a Hash in Redis and store our image plus a couple of other pieces of data there:
73+
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:
5874

5975
```python
6076
redis_key = f"image:{current_timestamp}"
6177
data_to_save = dict()
6278
data_to_save["image_data"] = image_data.getvalue()
6379
data_to_save["timestamp"] = current_timestamp
6480
data_to_save["mime_type"] = "image/jpeg"
81+
data_to_save["lux"] = int(image_metadata["Lux"])
6582
```
6683

6784
First, we create the key name we're going to use when storing the Hash. It's `image:<timestamp>`.
@@ -72,20 +89,18 @@ Hashes in Redis are schemaless, so if you add extra fields there's no need to ch
7289

7390
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.
7491

75-
Saving the Hash to Redis is then simply a matter of running the [`HSET` command](https://redis.io/commands/hset/), passing it the key name and dict of name/value pairs to store:
92+
Saving the Hash to Redis is then simply a matter of running the [`HSET` command](https://redis.io/commands/hset/), passing it the key name and dict of name/value pairs to store. When saving this fata, we also want to set an expiry time for it which we do with the Redis [`EXPIRE` command](https://redis.io/commands/expire/). The time to live for each hash is a configurable number of seconds, read from the `IMAGE_EXPIRY` environment variable (see later for details).
7693

77-
```python
78-
redis_client.hset(redis_key, mapping = data_to_save)
79-
```
80-
81-
As it stands, the images will stay in Redis until manually deleted. If you want to set a time to live on the image, use the [EXPIRE command](https://redis.io/commands/expire/). Redis will consider the Hash deleted after the number of seconds you specify has passed, freeing up resources associated with it on the Redis server. To implement this with a 1hr expiry time, modify the code as follows:
94+
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](https://redis.io/docs/manual/pipelining/) and send both in the same network round trip:
8295

8396
```python
84-
redis_client.hset(redis_key, mapping = data_to_save)
85-
redis_client.expire(redis_key, 3600) # 60 secs = 1 min x 60 = 1hr
97+
pipe = redis_client.pipeline(transaction=False)
98+
pipe.hset(redis_key, mapping = data_to_save)
99+
pipe.expire(redis_key, IMAGE_EXPIRY)
100+
pipe.execute()
86101
```
87102

88-
Redis also has an [EXPIREAT command](https://redis.io/commands/expireat/) if you prefer to specify a time and date for expiry, rather than a number of seconds in the future.
103+
This sets up the `HSET` and `EXPIRE` commands in a pipeline, which is then sent to Redis using the `execute` function.
89104

90105
## Setup
91106

@@ -109,7 +124,7 @@ Linux 6.1.21-v7+ #1642 SMP Mon Apr 3 17:20:52 BST 2023 armv7l
109124

110125
### Camera Setup
111126

112-
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 the Camera Module v2.1.
127+
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).
113128

114129
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](https://projects.raspberrypi.org/en/projects/getting-started-with-picamera/2).
115130

@@ -122,13 +137,13 @@ max_framebuffers=10
122137
dtoverlay=imx219
123138
```
124139

125-
The `imx219` value may differ for your camera. I was using the Raspberry Pi Camera Module v2.1. If you are using something different, you'll need to research appropriate values for your camera.
140+
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](https://www.raspberrypi.com/documentation/accessories/camera.html#preparing-the-software).
126141

127-
If you made any changes, save them and reboot the Raspberry Pi.
142+
If you made any changes, save them and **reboot the Raspberry Pi** (`sudo reboot`).
128143

129144
### Python Setup
130145

131-
You need Python 3.7 or higher (I've tested this with Python 3.9.2. To check your Python version:
146+
You need Python 3.7 or higher (I've tested this with Python 3.9.2). To check your Python version:
132147

133148
```
134149
python3 --version
@@ -178,7 +193,13 @@ export REDIS_URL=redis://myhost:9999/
178193

179194
Be sure to configure both the capture script and the separate server component to talk to the same Redis instance!
180195

181-
Alternatively, 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!
196+
You'll also need to set the following environment variables:
197+
198+
* `IMAGE_CAPTURE_FREQUENCY` - set this to the number of seconds that you want the code to wait between capturing images, e.g. `30`.
199+
* `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.
200+
* `CAMERA_AUTOFOCUS` - set this to `1` if your camera module has autofocus (v3) or `0` if it doesn't (v2).
201+
202+
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!
182203

183204
### Running the Capture Script
184205

@@ -188,7 +209,7 @@ With the setup steps completed, start the capture script as follows:
188209
python3 capture.py
189210
```
190211

191-
You should expect to see output similar to the following on startup:
212+
You should expect to see output similar to the following on startup (example using camera module v2.1):
192213

193214
```
194215
[0:34:17.749739445] [847] INFO Camera camera_manager.cpp:299 libcamera v0.0.4+22-923f5d70
@@ -204,13 +225,13 @@ Your output may differ if you are using a different camera. It appears that thi
204225
WARN RPI raspberrypi.cpp:1357 Mismatch between Unicam and CamHelper for embedded data usage!
205226
```
206227

207-
Every 10 seconds or so, the script will capture a new image. Expect to see output similar to the following:
228+
Every so many seconds, the script will capture a new image. Expect to see output similar to the following:
208229

209230
```
210231
Stored new image at image:1681923128
211232
{'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}
212233
```
213234

214-
The 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.
235+
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.
215236

216237
To stop the script, press Ctrl-C.

pi/capture.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@
88

99
load_dotenv()
1010

11+
# Get the configurable values for how often to capture images and
12+
# how long to keep them.
13+
IMAGE_CAPTURE_FREQUENCY = int(os.getenv("IMAGE_CAPTURE_FREQUENCY"))
14+
IMAGE_EXPIRY = int(os.getenv("IMAGE_EXPIRY"))
15+
CAMERA_AUTOFOCUS = os.getenv("CAMERA_AUTOFOCUS") == "1"
16+
1117
# Picamera2 docs https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf
1218
picam2 = Picamera2()
13-
# Headless so we don't want a preview window.
1419
picam2.start_preview(Preview.NULL)
15-
16-
# This will use max resolution, adjust if you want less (see above PDF).
1720
camera_config = picam2.still_configuration
1821

22+
# For a v3 Camera Module, set continuous autofocus.
23+
if CAMERA_AUTOFOCUS == True:
24+
picam2.set_controls({"AfMode": controls.AfModeEnum.Continuous})
25+
1926
# Tweak camera_config as needed before calling configure.
2027
picam2.configure(camera_config)
2128

@@ -26,34 +33,42 @@
2633
picam2.start()
2734

2835
# Put this in a loop or whatever you want to do with capturing images. Let's take
29-
# an image every 10 seconds or so...
36+
# an image every so many seconds...
3037

3138
while True:
32-
image_data = io.BytesIO()
39+
# For the Camera Module 3, trigger an autofocus cycle.
40+
if CAMERA_AUTOFOCUS == True:
41+
picam2.autofocus_cycle()
42+
43+
image_data = io.BytesIO()
3344

34-
# Take a picture and grab the metadata at the same time.
35-
image_metadata = picam2.capture_file(image_data, format="jpeg")
36-
current_timestamp = int(time.time())
45+
# Take a picture and grab the metadata at the same time.
46+
image_metadata = picam2.capture_file(image_data, format="jpeg")
47+
current_timestamp = int(time.time())
3748

38-
# Prepare data to save in Redis...
39-
redis_key = f"image:{current_timestamp}"
40-
data_to_save = dict()
41-
data_to_save["image_data"] = image_data.getvalue()
42-
data_to_save["timestamp"] = current_timestamp
43-
data_to_save["mime_type"] = "image/jpeg"
44-
# Add any other flat name/value pairs you want to save into this dict
45-
# e.g. light meter value, noise values, whatever really...
49+
# Prepare data to save in Redis...
50+
redis_key = f"image:{current_timestamp}"
51+
data_to_save = dict()
52+
data_to_save["image_data"] = image_data.getvalue()
53+
data_to_save["timestamp"] = current_timestamp
54+
data_to_save["mime_type"] = "image/jpeg"
55+
data_to_save["lux"] = int(image_metadata["Lux"])
56+
# Add any other flat name/value pairs you want to save into this dict
57+
# e.g. light meter value, noise values, whatever really...
4658

47-
# Store data in a Redis Hash (flat map of name/value pairs at a single
48-
# Redis key)
49-
redis_client.hset(redis_key, mapping = data_to_save)
59+
# Store data in a Redis Hash (flat map of name/value pairs at a single
60+
# Redis key), also set an expiry time for the image.
61+
pipe = redis_client.pipeline(transaction=False)
62+
pipe.hset(redis_key, mapping = data_to_save)
63+
pipe.expire(redis_key, IMAGE_EXPIRY)
64+
pipe.execute()
5065

51-
print(f"Stored new image at {redis_key}")
66+
print(f"Stored new image at {redis_key}")
5267

53-
# Optional - do something with the metadata if you want to.
54-
print(image_metadata)
68+
# Optional - do something with the metadata if you want to.
69+
print(image_metadata)
5570

56-
time.sleep(10)
71+
time.sleep(IMAGE_CAPTURE_FREQUENCY)
5772

5873
# This code is unreachable but shows how to release the Redis client
5974
# connection nicely should we want to say just take some pictures then

pi/env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
REDIS_URL=redis://default:password@host:6379/
1+
REDIS_URL=redis://default:password@host:6379/
2+
IMAGE_CAPTURE_FREQUENCY=30
3+
IMAGE_EXPIRY=300
4+
CAMERA_AUTOFOCUS=1

raspberry_pi_3_with_camera_module.jpg

-803 Bytes
Loading

0 commit comments

Comments
 (0)