Skip to content

Commit

Permalink
Merge pull request #4 from simonprickett/update-for-pi-camera-3
Browse files Browse the repository at this point in the history
Updated for Pi Camera Module 3 with autofocus.
  • Loading branch information
Simon Prickett authored May 18, 2023
2 parents 6c41642 + 4c10c13 commit 1e69296
Show file tree
Hide file tree
Showing 11 changed files with 112 additions and 58 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Redis data
redisdata/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ Here's what the front end looks like when a few images have been captured by the

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

(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!)

And here's a Raspberry Pi with a camera attached:

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

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.

With the container running, you can access the [Redis CLI](https://redis.io/docs/ui/cli/) using this command:

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

* `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.
* `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.
* `lux`: The [lux](https://en.wikipedia.org/wiki/Lux) value captured by the camera when the image was taken.
* `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`.

Here's a complete example, with the image data truncated for brevity:
Expand All @@ -76,11 +77,13 @@ Here's a complete example, with the image data truncated for brevity:
1) "mime_type"
2) "image/jpeg"
3) "timestamp"
4) "1681843615"
5) "image_data"
6) "\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00...
4) "lux"
5) "268"
6) "1681843615"
7) "image_data"
8) "\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00...
```
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.
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.

## (Optional, but Recommended): RedisInsight

Expand Down
6 changes: 5 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ services:
ports:
- 6379:6379
- 8001:8001
volumes:
- ./redisdata:/data
environment:
- REDIS_ARGS=--appendonly yes --save ""
deploy:
replicas: 1
restart_policy:
condition: on-failure
condition: on-failure
67 changes: 44 additions & 23 deletions pi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

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 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>`.
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](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.
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.

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

**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.
**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).

## How it Works

Expand All @@ -29,6 +29,13 @@ 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):

```python
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:
Expand All @@ -37,7 +44,16 @@ Next, a connection to Redis is established, using the value of an environment va
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 10 seconds before doing it all again.
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:

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

Expand All @@ -54,14 +70,15 @@ 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:
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:

```python
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>`.
Expand All @@ -72,20 +89,18 @@ Hashes in Redis are schemaless, so if you add extra fields there's no need to ch

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](https://redis.io/commands/hset/), passing it the key name and dict of name/value pairs to store:
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).

```python
redis_client.hset(redis_key, mapping = data_to_save)
```

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:
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:

```python
redis_client.hset(redis_key, mapping = data_to_save)
redis_client.expire(redis_key, 3600) # 60 secs = 1 min x 60 = 1hr
pipe = redis_client.pipeline(transaction=False)
pipe.hset(redis_key, mapping = data_to_save)
pipe.expire(redis_key, IMAGE_EXPIRY)
pipe.execute()
```

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.
This sets up the `HSET` and `EXPIRE` commands in a pipeline, which is then sent to Redis using the `execute` function.

## Setup

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

### Camera Setup

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

Expand All @@ -122,13 +137,13 @@ max_framebuffers=10
dtoverlay=imx219
```

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.
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).

If you made any changes, save them and reboot the Raspberry Pi.
If you made any changes, save them and **reboot the Raspberry Pi** (`sudo reboot`).

### Python Setup

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

```
python3 --version
Expand Down Expand Up @@ -178,7 +193,13 @@ 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!

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!
You'll also need to set the following environment variables:

* `IMAGE_CAPTURE_FREQUENCY` - set this to the number of seconds that you want the code to wait between capturing images, e.g. `30`.
* `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 to `1` if your camera module has autofocus (v3) or `0` if it doesn't (v2).

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!

### Running the Capture Script

Expand All @@ -188,7 +209,7 @@ 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:
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
Expand All @@ -204,13 +225,13 @@ Your output may differ if you are using a different camera. It appears that thi
WARN RPI raspberrypi.cpp:1357 Mismatch between Unicam and CamHelper for embedded data usage!
```

Every 10 seconds or so, the script will capture a new image. Expect to see output similar to the following:
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}
```

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.
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.
61 changes: 38 additions & 23 deletions pi/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@

load_dotenv()

# Get the configurable values for how often to capture images and
# how long to keep them.
IMAGE_CAPTURE_FREQUENCY = int(os.getenv("IMAGE_CAPTURE_FREQUENCY"))
IMAGE_EXPIRY = int(os.getenv("IMAGE_EXPIRY"))
CAMERA_AUTOFOCUS = os.getenv("CAMERA_AUTOFOCUS") == "1"

# Picamera2 docs https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf
picam2 = Picamera2()
# Headless so we don't want a preview window.
picam2.start_preview(Preview.NULL)

# This will use max resolution, adjust if you want less (see above PDF).
camera_config = picam2.still_configuration

# For a v3 Camera Module, set continuous autofocus.
if CAMERA_AUTOFOCUS == True:
picam2.set_controls({"AfMode": controls.AfModeEnum.Continuous})

# Tweak camera_config as needed before calling configure.
picam2.configure(camera_config)

Expand All @@ -26,34 +33,42 @@
picam2.start()

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

while True:
image_data = io.BytesIO()
# For the Camera Module 3, trigger an autofocus cycle.
if CAMERA_AUTOFOCUS == True:
picam2.autofocus_cycle()

image_data = io.BytesIO()

# Take a picture and grab the metadata at the same time.
image_metadata = picam2.capture_file(image_data, format="jpeg")
current_timestamp = int(time.time())
# Take a picture and grab the metadata at the same time.
image_metadata = picam2.capture_file(image_data, format="jpeg")
current_timestamp = int(time.time())

# Prepare data to save in Redis...
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"
# Add any other flat name/value pairs you want to save into this dict
# e.g. light meter value, noise values, whatever really...
# Prepare data to save in Redis...
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"])
# Add any other flat name/value pairs you want to save into this dict
# e.g. light meter value, noise values, whatever really...

# Store data in a Redis Hash (flat map of name/value pairs at a single
# Redis key)
redis_client.hset(redis_key, mapping = data_to_save)
# Store data in a Redis Hash (flat map of name/value pairs at a single
# Redis key), also set an expiry time for the image.
pipe = redis_client.pipeline(transaction=False)
pipe.hset(redis_key, mapping = data_to_save)
pipe.expire(redis_key, IMAGE_EXPIRY)
pipe.execute()

print(f"Stored new image at {redis_key}")
print(f"Stored new image at {redis_key}")

# Optional - do something with the metadata if you want to.
print(image_metadata)
# Optional - do something with the metadata if you want to.
print(image_metadata)

time.sleep(10)
time.sleep(IMAGE_CAPTURE_FREQUENCY)

# This code is unreachable but shows how to release the Redis client
# connection nicely should we want to say just take some pictures then
Expand Down
5 changes: 4 additions & 1 deletion pi/env.example
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
REDIS_URL=redis://default:password@host:6379/
REDIS_URL=redis://default:password@host:6379/
IMAGE_CAPTURE_FREQUENCY=30
IMAGE_EXPIRY=300
CAMERA_AUTOFOCUS=1
Binary file modified raspberry_pi_3_with_camera_module.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 1e69296

Please sign in to comment.