Skip to content

Commit

Permalink
feat: Expand camera control ability (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
mryel00 authored Jun 5, 2024
1 parent ef32028 commit 662b630
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 27 deletions.
37 changes: 20 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,26 @@ This will start the server with the following default configuration:

On startup the following arguments are supported:

| Argument | Description | Default |
|-------------------------------|-----------------------------------------------------------------------------------------------------|--------------|
| `-b`, `--bindaddress` | Address where the server will listen for new incoming connections. | `0.0.0.0` |
| `-p`, `--port` | Port where the server will listen for new incoming connections. | `8080` |
| `-r`, `--resolution` | Resolution of the captured frames. This argument expects the format <width>x<height> | `640x480` |
| `-f`, `--fps` | Framerate in frames per second (fps). | `15` |
| `-st`, `--stream_url` | Sets the URL for the mjpeg stream. | `/stream` |
| `-sn`, `--snapshot_url` | Sets the URL for snapshots (single frame of stream). | `/snapshot` |
| `-af`, `--autofocus` | Autofocus mode. Supported modes: `manual`, `continuous` | `continuous` |
| `-l`, `--lensposition` | Set focal distance. 0 for infinite focus, 0.5 for approximate 50cm. Only used with Autofocus manual | `0.0` |
| `-s`, `--autofocusspeed` | Autofocus speed. Supported values: `normal`, `fast`. Only used with Autofocus continuous | `normal` |
| `-ud` `--upsidedown` | Rotate the image by 180° (see below) | |
| `-fh` `--flip_horizontal` | Mirror the image horizontally (see below) | |
| `-fv` `--flip_vertical` | Mirror the image vertically (see below) | |
| `-or` `--orientation_exif` | Set the image orientation using an EXIF header (see below) | |
| `-tf` `--tuning_filter` | Set a tuning filter file name. | |
| `-tfd` `--tuning_filter_dir` | Set the directory to look for tuning filters. | |
| Argument | Description | Default |
|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------|--------------|
| `-b`, `--bindaddress` | Address where the server will listen for new incoming connections. | `0.0.0.0` |
| `-p`, `--port` | Port where the server will listen for new incoming connections. | `8080` |
| `-r`, `--resolution` | Resolution of the captured frames. This argument expects the format \<width\>x\<height\> | `640x480` |
| `-f`, `--fps` | Framerate in frames per second (fps). | `15` |
| `-st`, `--stream_url` | Sets the URL for the mjpeg stream. | `/stream` |
| `-sn`, `--snapshot_url` | Sets the URL for snapshots (single frame of stream). | `/snapshot` |
| `-af`, `--autofocus` | Autofocus mode. Supported modes: `manual`, `continuous`. | `continuous` |
| `-l`, `--lensposition` | Set focal distance. 0 for infinite focus, 0.5 for approximate 50cm. Only used with Autofocus manual. | `0.0` |
| `-s`, `--autofocusspeed` | Autofocus speed. Supported values: `normal`, `fast`. Only used with Autofocus continuous | `normal` |
| `-ud`, `--upsidedown` | Rotate the image by 180° (see below) | |
| `-fh`, `--flip_horizontal` | Mirror the image horizontally (see below) | |
| `-fv`, `--flip_vertical` | Mirror the image vertically (see below) | |
| `-or`, `--orientation_exif` | Set the image orientation using an EXIF header (see below) | |
| `-c`, `--controls` | Define camera controls to start spyglass with. Can be used multiple times. This argument expects the format \<control\>=\<value\>. | |
| `--list-controls` | List all available libcamera controls onto the console. Those can be used with `--controls` | |
| `-tf`, `--tuning_filter` | Set a tuning filter file name. | |
| `-tfd`, `--tuning_filter_dir` | Set the directory to look for tuning filters. | |

Starting the server without any argument is the same as

```shell
Expand Down
67 changes: 67 additions & 0 deletions docs/camera-controls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
Spyglass offers a few CLI parameters for the most commonly used camera controls.
Controls not directly available through the CLI can be used with the `--controls` (`-c`) or `--controls-string` (`-cs`) parameters or the `CONTROLS` section inside the `spyglass.conf`.


## How to list available controls?

Spyglass provides a CLI parameter to list all available controls `--list-controls`. The available controls are then printed onto your shell under `Available controls:`.

Following shows an example for a Raspberry Pi Module v3:
```sh
Available controls:
NoiseReductionMode (int) : min=0 max=4 default=0
ScalerCrop (tuple) : min=(0, 0, 0, 0) max=(65535, 65535, 65535, 65535) default=(0, 0, 0, 0)
Sharpness (float) : min=0.0 max=16.0 default=1.0
AwbEnable (bool) : min=False max=True default=None
FrameDurationLimits (int) : min=33333 max=120000 default=None
ExposureValue (float) : min=-8.0 max=8.0 default=0.0
AwbMode (int) : min=0 max=7 default=0
AeExposureMode (int) : min=0 max=3 default=0
Brightness (float) : min=-1.0 max=1.0 default=0.0
AfWindows (tuple) : min=(0, 0, 0, 0) max=(65535, 65535, 65535, 65535) default=(0, 0, 0, 0)
AfSpeed (int) : min=0 max=1 default=0
AfTrigger (int) : min=0 max=1 default=0
LensPosition (float) : min=0.0 max=32.0 default=1.0
AfRange (int) : min=0 max=2 default=0
AfPause (int) : min=0 max=2 default=0
ExposureTime (int) : min=0 max=66666 default=None
AeEnable (bool) : min=False max=True default=None
AeConstraintMode (int) : min=0 max=3 default=0
AfMode (int) : min=0 max=2 default=0
AnalogueGain (float) : min=1.0 max=16.0 default=None
ColourGains (float) : min=0.0 max=32.0 default=None
AfMetering (int) : min=0 max=1 default=0
AeMeteringMode (int) : min=0 max=3 default=0
Contrast (float) : min=0.0 max=32.0 default=1.0
Saturation (float) : min=0.0 max=32.0 default=1.0
```


## How to apply a camera control?

There are multiple ways to apply a camera control. All methods are case insensitive.

### Shell

There are two different parameters to apply the controls:

- `--controls`/`-c` can be used multiple times, to set multiple controls. E.g. using `-c brightness=0.5 -c awbenable=false` will apply `0.5` to the `Brightness` and `False` as the new `AwbEnable` value.
- `--controls-string`/`cs` can be used only once. E.g. using `--controls-string "brightness=0.5, awbenable=16"` will apply `0.5` on the `Brightness` and `False` as the new `AwbEnable` control. Note: The `"` are required and the controls need to be separated by a `,`. This is intended only for parsing the config.

### Config

The `spyglass.conf` accepts camera controls under the `CONTROLS` option. E.g. `CONTROLS="brightness=0,awbenable=false"` will apply `0.5` to the `Brightness` and `False` as the new `AwbEnable` value.

### Webinterface

Spyglass also provides an API endpoint to change the camera controls during runtime. This endpoint is available under `http://<ip.of.your.pi>:<port>/controls` and cannot be changed.

Calling it without any parameters will show you a list of all available controls, like `--list-controls`.

E.g. `http://<ip.of.your.pi>:<port>/controls?brightness=0.5&awbenable=false` will apply `0.5` to the `Brightness` and `False` as the new `AwbEnable` value.

If you apply parameters the interface will show you the parameters Spyglass found inside the url and which controls got actually processed:
- `Parsed Controls` shows you the parameters Spyglass found during the request.
- `Processed Controls` shows you the parameters of the `Parsed Controls` Spyglass could actually set for the cam.

E.g. `http://<ip.of.your.pi>:<port>/controls?brightness=0.5&foo=bar&foobar` will show you `Parsed Controls: [('brightness', '1'), ('foo', 'bar')]` and `Processed Controls: {'Brightness': 1}`.
69 changes: 69 additions & 0 deletions resources/controls_style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
body {
font-family: Arial, sans-serif;
background-color: #f6f6f6;
margin: 40px;
display: flex;
flex-direction: column;
align-items: center;
}

.card-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 1100px;
}

.card-container:nth-child(odd) .card {
background-color: white;
}

.card-container:nth-child(even) .card {
background-color: #f0f0f0;
}

.card {
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 0px 20px 20px 20px; /* top right bottom left */
width: 80%;
box-sizing: border-box;
transition: 0.3s;
}

.card:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}

.card h2 {
color: #2C3E50;
border-bottom: 1px solid #2C3E50;
padding-bottom: 10px;
margin-bottom: 10px;
}

.card-content {
display: flex;
margin-bottom: 1px;
}

.setting {
flex: 1;
display: flex;
margin: 0 5px;
}

.label,
.value {
font-size: 14px;
}

.label {
font-weight: bold;
color: #7F8C8D;
}

.value {
margin-left: 4px;
}
5 changes: 5 additions & 0 deletions resources/spyglass.conf
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ AF_SPEED="normal"
#### r270 - Rotate 270 CW
ORIENTATION_EXIF="h"

#### Camera Controls
#### NOTE: Set v4l2 controls your camera supports at startup
#### EXAMPLE: CONTROLS="brightness=0,awbenable=false"
CONTROLS=""

#### Tuning Filter Directory (STRING)[default: none]
#### NOTE: Directory where to search for tuning filters(if defined).
#### Directory only used if TUNING_FILTER is defined
Expand Down
5 changes: 3 additions & 2 deletions scripts/spyglass
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,10 @@ run_spyglass() {
--autofocus "${AUTO_FOCUS:-continuous}" \
--lensposition "${FOCAL_DIST:-0.0}" \
--autofocusspeed "${AF_SPEED:-normal}" \
--orientation_exif "${ORIENTATION_EXIF:-h}"\
--orientation_exif "${ORIENTATION_EXIF:-h}" \
--tuning_filter "${TUNING_FILTER:-}"\
--tuning_filter_dir "${TUNING_FILTER_DIR:-}"
--tuning_filter_dir "${TUNING_FILTER_DIR:-}" \
--controls-string "${CONTROLS:-0=0}" # 0=0 to prevent error on empty string
}

#### MAIN
Expand Down
6 changes: 5 additions & 1 deletion spyglass/camera.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import libcamera
from spyglass.camera_options import process_controls
from picamera2 import Picamera2


def init_camera(
width: int,
height: int,
Expand All @@ -12,6 +12,7 @@ def init_camera(
upsidedown=False,
flip_horizontal=False,
flip_vertical=False,
control_list: list[list[str]]=[],
tuning_filter=None,
tuning_filter_dir=None):

Expand All @@ -26,6 +27,9 @@ def init_camera(
picam2 = Picamera2(tuning=tuning)
controls = {'FrameRate': fps}

c = process_controls(picam2, [tuple(ctrl) for ctrl in control_list])
controls.update(c)

if 'AfMode' in picam2.camera_controls:
controls['AfMode'] = autofocus
controls['AfSpeed'] = autofocus_speed
Expand Down
104 changes: 104 additions & 0 deletions spyglass/camera_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import libcamera
import ast

def parse_dictionary_to_html_page(camera, parsed_controls='None', processed_controls='None'):
html = """
<!DOCTYPE html>
<html lang="en">
"""
html += f"""
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Camera Settings</title>
<style>{get_style()}</style>
</head>
"""
html += f"""
<body>
<h1>Available camera options</h1>
<h3>Parsed Controls: {parsed_controls}</h3>
<h3>Processed Controls: {processed_controls}</h3>
"""
for control, values in camera.camera_controls.items():
html += f"""
<div class="card-container">
<div class="card">
<h2>{control}</h2>
<div class="card-content">
<div class="setting">
<span class="label">Min:</span>
<span class="value">{values[0]}</span>
</div>
<div class="setting">
<span class="label">Max:</span>
<span class="value">{values[1]}</span>
</div>
<div class="setting">
<span class="label">Default:</span>
<span class="value">{values[2]}</span>
</div>
</div>
</div>
</div>
"""
html += """
</body>
</html>
"""
return html

def get_style():
with (open('resources/controls_style.css', 'r')) as f:
return f.read()

def process_controls(camera, controls: list[tuple[str, str]]) -> dict[str, any]:
controls_dict_lower = { k.lower(): k for k in camera.camera_controls.keys() }
if controls == None:
return {}
processed_controls = {}
for key, value in controls:
key = key.lower().strip()
if key.lower() in controls_dict_lower.keys():
value = value.lower().strip()
k = controls_dict_lower[key]
v = parse_from_string(value)
processed_controls[k] = v
return processed_controls

def parse_from_string(input_string: str) -> any:
try:
return ast.literal_eval(input_string)
except (ValueError, TypeError, SyntaxError):
pass

if input_string.lower() in ['true', 'false']:
return input_string.lower() == 'true'

return input_string

def get_type_str(obj) -> str:
return str(type(obj)).split('\'')[1]

def get_libcamera_controls_string(camera_path: str) -> str:
ctrls_str = ""
libcam_cm = libcamera.CameraManager.singleton()
cam = libcam_cm.cameras[0]
def rectangle_to_tuple(rectangle):
return (rectangle.x, rectangle.y, rectangle.width, rectangle.height)
for k, v in cam.controls.items():
if isinstance(v.min, libcamera.Rectangle):
min = rectangle_to_tuple(v.min)
max = rectangle_to_tuple(v.max)
default = rectangle_to_tuple(v.default)
else:
min = v.min
max = v.max
default = v.default

str_first = f"{k.name} ({get_type_str(min)})"
str_second = f"min={min} max={max} default={default}"
str_indent = (30 - len(str_first)) * ' ' + ': '
ctrls_str += str_first + str_indent + str_second + '\n'

return ctrls_str.strip()
Loading

0 comments on commit 662b630

Please sign in to comment.