Skip to content

Commit

Permalink
Merge pull request tiagocoutinho#42 from otaku42/improve_controls
Browse files Browse the repository at this point in the history
Further improval of controls
  • Loading branch information
tiagocoutinho committed May 22, 2023
2 parents 95209ef + a98df12 commit 429f340
Show file tree
Hide file tree
Showing 10 changed files with 746 additions and 123 deletions.
131 changes: 112 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,35 +87,37 @@ Getting information about the device:
Format(width=640, height=480, pixelformat=<PixelFormat.MJPEG: 1196444237>}

>>> for ctrl in cam.controls.values(): print(ctrl)
<Control brightness type=integer min=0 max=255 step=1 default=128 value=64>
<Control contrast type=integer min=0 max=255 step=1 default=32 value=32>
<Control saturation type=integer min=0 max=100 step=1 default=64 value=64>
<Control hue type=integer min=-180 max=180 step=1 default=0 value=0>
<Control white_balance_automatic type=boolean default=1 value=1>
<Control gamma type=integer min=90 max=150 step=1 default=120 value=120>
<Control gain type=integer min=1 max=7 step=1 default=1 value=1>
<Control power_line_frequency type=menu min=0 max=2 step=1 default=2 value=2>
<Control white_balance_temperature type=integer min=2800 max=6500 step=1 default=4000 value=4000 flags=inactive>
<Control sharpness type=integer min=0 max=7 step=1 default=2 value=2>
<Control backlight_compensation type=integer min=0 max=1 step=1 default=0 value=0>
<Control auto_exposure type=menu min=0 max=3 step=1 default=3 value=3>
<Control exposure_time_absolute type=integer min=10 max=333 step=1 default=156 value=156 flags=inactive>
<Control exposure_dynamic_framerate type=boolean default=0 value=1>```
<IntegerControl brightness min=0 max=255 step=1 default=128 value=128>
<IntegerControl contrast min=0 max=255 step=1 default=32 value=32>
<IntegerControl saturation min=0 max=100 step=1 default=64 value=64>
<IntegerControl hue min=-180 max=180 step=1 default=0 value=0>
<BooleanControl white_balance_automatic default=True value=True>
<IntegerControl gamma min=90 max=150 step=1 default=120 value=120>
<MenuControl power_line_frequency default=1 value=1>
<IntegerControl white_balance_temperature min=2800 max=6500 step=1 default=4000 value=4000 flags=inactive>
<IntegerControl sharpness min=0 max=7 step=1 default=2 value=2>
<IntegerControl backlight_compensation min=0 max=2 step=1 default=1 value=1>
<MenuControl auto_exposure default=3 value=3>
<IntegerControl exposure_time_absolute min=4 max=1250 step=1 default=156 value=156 flags=inactive>
<BooleanControl exposure_dynamic_framerate default=False value=False>

>>> cam.controls["saturation"]
<Control saturation type=integer min=0 max=100 step=1 default=64 value=64>
<IntegerControl saturation min=0 max=100 step=1 default=64 value=64>

>>> cam.controls["saturation"].id
9963778
>>> cam.controls[9963778]
<Control saturation type=integer min=0 max=100 step=1 default=64 value=64>
<IntegerControl saturation min=0 max=100 step=1 default=64 value=64>

>>> cam.controls.brightness
<Control brightness type=integer min=0 max=255 step=1 default=128 value=64>
>>> cam.controls.brightness.value = 128
<IntegerControl brightness min=0 max=255 step=1 default=128 value=128>
>>> cam.controls.brightness.value = 64
>>> cam.controls.brightness
<Control brightness type=integer min=0 max=255 step=1 default=128 value=128>
<IntegerControl brightness min=0 max=255 step=1 default=128 value=64>
```

(see also [v4l2py-ctl](examples/v4l2py-ctl.py) example)

### asyncio

v4l2py is asyncio friendly:
Expand Down Expand Up @@ -222,6 +224,97 @@ with Device.from_id(0) as cam:
buff = io.BytesIO(frame)
```

## Improved device controls

Device controls have been improved to provide a more pythonic interface. The
new interface is the default now; however, the legacy interface can be
requested: `Device.from_id(x, legacy_controls=True)`.

Before:
```python
>>> from v4l2py import Device
>>> cam = Device.from_id(0)
>>> cam.open()
>>> for ctrl in cam.controls.values():
... print(ctrl)
... for item in ctrl.menu.values():
... print(f" - {item.index}: {item.name}")
<Control brightness type=integer min=0 max=255 step=1 default=128 value=255>
<Control contrast type=integer min=0 max=255 step=1 default=32 value=255>
<Control saturation type=integer min=0 max=100 step=1 default=64 value=100>
<Control hue type=integer min=-180 max=180 step=1 default=0 value=0>
<Control white_balance_automatic type=boolean min=0 max=1 step=1 default=1 value=1>
<Control gamma type=integer min=90 max=150 step=1 default=120 value=150>
<Control gain type=integer min=1 max=7 step=1 default=1 value=1>
<Control power_line_frequency type=menu min=0 max=2 step=1 default=2 value=2>
- 0: Disabled
- 1: 50 Hz
- 2: 60 Hz
<Control white_balance_temperature type=integer min=2800 max=6500 step=1 default=4000 value=4000 flags=inactive>
<Control sharpness type=integer min=0 max=7 step=1 default=2 value=7>
<Control backlight_compensation type=integer min=0 max=1 step=1 default=0 value=1>
<Control auto_exposure type=menu min=0 max=3 step=1 default=3 value=3>
- 1: Manual Mode
- 3: Aperture Priority Mode
<Control exposure_time_absolute type=integer min=10 max=333 step=1 default=156 value=156 flags=inactive>
<Control exposure_dynamic_framerate type=boolean min=0 max=1 step=1 default=0 value=1>

>>> type(cam.controls.exposure_dynamic_framerate.value)
<class 'int'>
```

Now:
```python
>>> from v4l2py.device import Device, MenuControl
>>> cam = Device.from_id(0)
>>> cam.open()
>>> for ctrl in cam.controls.values():
... print(ctrl)
... if isinstance(ctrl, MenuControl):
... for (index, name) in ctrl.items():
... print(f" - {index}: {name}")
<IntegerControl brightness min=0 max=255 step=1 default=128 value=255>
<IntegerControl contrast min=0 max=255 step=1 default=32 value=255>
<IntegerControl saturation min=0 max=100 step=1 default=64 value=100>
<IntegerControl hue min=-180 max=180 step=1 default=0 value=0>
<BooleanControl white_balance_automatic default=True value=True>
<IntegerControl gamma min=90 max=150 step=1 default=120 value=150>
<IntegerControl gain min=1 max=7 step=1 default=1 value=1>
<MenuControl power_line_frequency default=2 value=2>
- 0: Disabled
- 1: 50 Hz
- 2: 60 Hz
<IntegerControl white_balance_temperature min=2800 max=6500 step=1 default=4000 value=4000 flags=inactive>
<IntegerControl sharpness min=0 max=7 step=1 default=2 value=7>
<IntegerControl backlight_compensation min=0 max=1 step=1 default=0 value=1>
<MenuControl auto_exposure default=3 value=3>
- 1: Manual Mode
- 3: Aperture Priority Mode
<IntegerControl exposure_time_absolute min=10 max=333 step=1 default=156 value=156 flags=inactive>
<BooleanControl exposure_dynamic_framerate default=False value=True>

>>> type(cam.controls.white_balance_automatic.value)
<class 'bool'>
>>> cam.controls.white_balance_automatic.value
<BooleanControl white_balance_automatic default=True value=True>
>>> cam.controls.white_balance_automatic.value = False
<BooleanControl white_balance_automatic default=True value=False>

>>> wba = cam.controls.white_balance_automatic
>>> wba.value = "enable" # or "on", "1", "true", "yes"
>>> wba
<BooleanControl white_balance_automatic default=True value=True>
>>> wba.value = "off" # or "disable", "0", "false", "no"
>>> wba
<BooleanControl white_balance_automatic default=True value=False>
```

The initial upgrade path for existing code is to request the legacy interface
by passing `legacy_controls=True` when instantiating the `Device` object, use
`LegacyControl` instead of `Control` for instantiations, and `BaseControl`
for isinstance() checks. And in the unlikely case your code does isinstance()
checks for `MenuItem`, these should be changed to `LegacyMenuItem`.

## References

See the ``linux/videodev2.h`` header file for details.
Expand Down
7 changes: 5 additions & 2 deletions examples/qt/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
# Copyright (c) 2021 Tiago Coutinho
# Distributed under the GPLv3 license. See LICENSE for more info.

# install requirements: pip install cv2 qtpy pyqt6
# run with: QT_API=pyqt6 python widget.py
# install extra requirements:
# python3 -m pip install opencv-python qtpy pyqt6

# run from this directory with:
# QT_API=pyqt6 python widget.py

import cv2
from qtpy import QtCore, QtGui, QtWidgets
Expand Down
200 changes: 200 additions & 0 deletions examples/v4l2py-ctl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import argparse

from v4l2py.device import Device, MenuControl, LegacyControl


def _get_ctrl(cam, control):
if control.isdigit() or control.startswith("0x"):
_ctrl = int(control, 0)
else:
_ctrl = control

try:
ctrl = cam.controls[_ctrl]
except KeyError:
return None
else:
return ctrl


def show_control_status(device: str, legacy_controls: bool) -> None:
with Device(device, legacy_controls=legacy_controls) as cam:
print("Showing current status of all controls ...\n")
print(f"*** {cam.info.card} ***")

for cc in cam.controls.used_classes():
print(f"\n{cc.name.title()} Controls\n")

for ctrl in cam.controls.with_class(cc):
print("0x%08x:" % ctrl.id, ctrl)
if isinstance(ctrl, MenuControl):
for key, value in ctrl.items():
print(11 * " ", f" +-- {key}: {value}")
elif isinstance(ctrl, LegacyControl):
for item in ctrl.menu.values():
print(11 * " ", f" +-- {item}")
print("")


def get_controls(device: str, controls: list, legacy_controls: bool) -> None:
with Device(device, legacy_controls=legacy_controls) as cam:
print("Showing current value of given controls ...\n")

for control in controls:
ctrl = _get_ctrl(cam, control)
if not ctrl:
print(f"{control}: unknown control")
continue

if not ctrl.is_flagged_write_only:
print(f"{control} = {ctrl.value}")
else:
print(f"{control} is write-only, thus cannot be read")
print("")


def set_controls(
device: str, controls: list, legacy_controls: bool, clipping: bool
) -> None:
controls = (
(ctrl.strip(), value.strip())
for (ctrl, value) in (c.split("=") for c in controls)
)

with Device(device, legacy_controls=legacy_controls) as cam:
print("Changing value of given controls ...\n")

cam.controls.set_clipping(clipping)
for control, value_new in controls:
ctrl = _get_ctrl(cam, control)
if not ctrl:
print(f"{control}: unknown control")
continue

if not ctrl.is_flagged_write_only:
value_old = ctrl.value
else:
value_old = "(write-only)"

try:
ctrl.value = value_new
except Exception as err:
success = False
reason = f"{err}"
else:
success = True

result = "%-5s" % ("OK" if success else "ERROR")

if success:
print(f"{result} {control}: {value_old} -> {value_new}\n")
else:
print(
f"{result} {control}: {value_old} -> {value_new}\n{result} {reason}\n"
)


def reset_controls(device: str, controls: list, legacy_controls: bool) -> None:
with Device(device, legacy_controls=legacy_controls) as cam:
print("Resetting given controls to default ...\n")

for control in controls:
ctrl = _get_ctrl(cam, control)
if not ctrl:
print(f"{control}: unknown control")
continue

try:
ctrl.set_to_default()
except Exception as err:
success = False
reason = f"{err}"
else:
success = True

result = "%-5s" % ("OK" if success else "ERROR")

if success:
print(f"{result} {control} reset to {ctrl.default}\n")
else:
print(f"{result} {control}:\n{result} {reason}\n")


def reset_all_controls(device: str, legacy_controls: bool) -> None:
with Device(device, legacy_controls=legacy_controls) as cam:
print("Resetting all controls to default ...\n")
cam.controls.set_to_default()


def csv(string: str) -> list:
return [v.strip() for v in string.split(",")]


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--legacy",
default=False,
action="store_true",
help="use legacy controls (default: %(default)s)",
)
parser.add_argument(
"--clipping",
default=False,
action="store_true",
help="when changing numeric controls, enforce the written value to be within allowed range (default: %(default)s)",
)
parser.add_argument(
"--device",
type=str,
default="0",
metavar="<dev>",
help="use device <dev> instead of /dev/video0; if <dev> starts with a digit, then /dev/video<dev> is used",
)
parser.add_argument(
"--get-ctrl",
type=csv,
default=[],
metavar="<ctrl>[,<ctrl>...]",
help="get the values of the specified controls",
)
parser.add_argument(
"--set-ctrl",
type=csv,
default=[],
metavar="<ctrl>=<val>[,<ctrl>=<val>...]",
help="set the values of the specified controls",
)
parser.add_argument(
"--reset-ctrl",
type=csv,
default=[],
metavar="<ctrl>[,<ctrl>...]",
help="reset the specified controls to their default values",
)
parser.add_argument(
"--reset-all",
default=False,
action="store_true",
help="reset all controls to their default value",
)

args = parser.parse_args()

if args.device.isdigit():
dev = f"/dev/video{args.device}"
else:
dev = args.device

if args.reset_all:
reset_all_controls(dev, args.legacy)
elif args.reset_ctrl:
reset_controls(dev, args.reset_ctrl, args.legacy)
elif args.get_ctrl:
get_controls(dev, args.get_ctrl, args.legacy)
elif args.set_ctrl:
set_controls(dev, args.set_ctrl, args.legacy, args.clipping)
else:
show_control_status(dev, args.legacy)

print("Done.")
3 changes: 3 additions & 0 deletions examples/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
# Copyright (c) 2021 Tiago Coutinho
# Distributed under the GPLv3 license. See LICENSE for more info.

# Extra dependency required to run this example:
# python3 -m pip install flask

# run from this directory with: FLASK_APP=web flask run -h 0.0.0.0

import flask
Expand Down
5 changes: 3 additions & 2 deletions examples/web/async.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
# Distributed under the GPLv3 license. See LICENSE for more info.

# Extra dependencies required to run this example:
# pip install fastapi jinja2 python-multipart cv2 pillow uvicorn
# python3 -m pip install fastapi jinja2 python-multipart opencv-python \
# pillow uvicorn

# run from this directory with:
# uvicorn async:app
Expand Down Expand Up @@ -88,7 +89,7 @@ def cameras() -> list[Camera]:
global CAMERAS
if CAMERAS is None:
cameras = {}
for device in iter_video_capture_devices():
for device in iter_video_capture_devices(legacy_controls=True):
cameras[device.index] = Camera(device)
CAMERAS = cameras
return CAMERAS
Expand Down
Loading

0 comments on commit 429f340

Please sign in to comment.